# manager/projects — 端到端技术审查 > **审查时间**: 2026-08-18 > **审查范围**: `ry-vue3/src/views/manager/Projects.vue` (主列表) + 后端 BizProject 全链路 + DB 实测 > **对比原型**: `proto/html/components/project-manage.html` --- ## 修复记录 | 日期 | 章节 | 修复内容 | |---|---|---| | 2026-08-18 | §6.3 / §9.1 | **BizProject.projectId** String → Long (9 文件: domain / IBizProjectService / BizProjectServiceImpl / BizProjectMapper.java+xml / BizProjectController / BizExecutionIntentController / BizPublicityIntentController / BizPublicController)。SnowflakeId.injectIfEmpty 反射依赖 setter(String), Long 后会抛 NoSuchMethodException 被吞掉 (SnowflakeId.java:30-31), 安全 — 但显式删掉更干净 (biz_project 用 AUTO_INCREMENT)。 | | 2026-08-18 | §6.3 / §9.1 | **BizProject.startTime / endTime** String → Date, 加 `@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")` (跟 BizMeeting 一致)。Mapper XML 8 处 `` 改为 `` (Date 无 isEmpty, OGNL 会报错)。前端 el-date-picker value-format "YYYY-MM-DD HH:mm:ss" 双向兼容, 零前端改动。 | | 2026-08-18 | §10 #7 | **`batchScoreRows` 死代码删除** (Projects.vue:501)。注释说"保留兼容旧引用", 但 grep 全文 0 个引用, 纯死变量。 | | 2026-08-18 | §10 #5 | **生产代码残留 console.info/error 4 处删除** (Projects.vue:299, 302, 306, 874)。`load()` 进/出/失败各一处 + 路由 watch reload 一处, 共 4 行, 全部删除。 | | 2026-08-18 | §10 #9 | **日期选择器 value-format 对齐 type** (Projects.vue:17, 19)。原 `type="date"` + `value-format="YYYY-MM-DD HH:mm:ss"` 矛盾 (date picker 给不出 HH:mm:ss), 改 value-format 为 `YYYY-MM-DD`, MySQL 对 datetime 列隐式转换。 | | 2026-08-18 | §7.1 / §10 #12 | **DB: biz_project 加索引 `idx_sponsor_admin_user_id`**。ALTER TABLE biz_project ADD INDEX idx_sponsor_admin_user_id (sponsor_admin_user_id)。EXPLAIN 验证: type=ref, Extra=Using index。 | | 2026-08-18 | §7.1 / §10 #13 | **DB: biz_project 加复合索引 `idx_start_end_time (start_time, end_time)`**。比 2 个单列索引更适合 `WHERE start_time >= ? AND end_time <= ?` 的范围查询。EXPLAIN 验证: type=range, Extra=Using where; Using index。 | | 2026-08-18 | §7.2 / §10 #14 | **DB: biz_project_assign 加 UNIQUE `uk_project_unit (project_id, execution_unit_id)`**。先查 11 行 0 重复, 安全。防重复分配。注意: `biz_project_assign` 是 **项目→executor (执行方)** 分配表, 不是 sponsor; sponsor 走独立的 `biz_project_sponsor_assign`。 | --- ## 0. 4 跳定位 (铁律: 不按名字猜) | 跳 | 文件:行 | 结果 | |---|---|---| | ① 角色菜单 | `ry-vue3/src/layout/AdminLayout.vue:98` | `path: '/manager/projects', title: '项目管理'` ✓ | | ② 路由 | `ry-vue3/src/router/index.js:65` | `component: () => import('@/views/manager/Projects.vue')` ✓ | | ③ 组件 | `ry-vue3/src/views/manager/Projects.vue` | 881 行,本审查对象 | | ④ 原型 | `proto/html/manager.html:206` | `` ✓ | --- ## 1. 主要功能 + 可见性 ### 1.1 主要功能 | 功能 | 前端入口 | 后端接口 | 涉及表 | |---|---|---|---| | 项目列表(分页+筛选) | `manager/Projects.vue` 表头筛选 + `load()` | `GET /business/project/list` → `BizProjectController.list` → `BizProjectService.selectList` | biz_project | | 单条/批量结题 | `onToggleStatus` (批量走 `openBatch('close')`) | `PUT /business/project` → `bizProjectService.updateByPrimaryKey` (只改 `is_finished='1'`) | biz_project.is_finished | | 单条/批量开通 | `openActivate` / `openBatch('activate')` | `PUT /business/project` (改 `end_time`) | biz_project.end_time | | 单条/批量执行单位评分 | `openSingleScore` / `openBatch('score')` | `POST /business/project/rate` (4 维度) + `PUT /business/project` (写聚合分) | biz_project_rating + biz_project.manager_score | | 项目分配(单条+批量) | `doAssign` / `openAssign` | `PUT /business/project` (改 sponsor) + `POST /business/project/{id}/assigns` (先删后插) | biz_project + biz_project_assign | | 删除公告(单条) | `confirmDeleteAnn` | `PUT /business/project` (清空 publishUrl/invitationUrl/supportLetterUrl/noticeUrl/scheduleUrl + is_published='0') | biz_project | | 导出报名专家 | `exportExperts` | `POST /business/executionIntent/export?projectNo=...` | biz_execution_intent | | 新建/编辑/查看 | 跳独立页 `/manager/projects/new`、`/edit/:id`、`/detail/:id` | 同 | biz_project | ### 1.2 可见性(三层隔离) | 层 | 来源 | 校验字段 | |---|---|---| | 前端菜单 | `AdminLayout.vue:98` | 只在 `manager` 角色菜单出现 | | 路由守卫 | `router/index.js:61` `meta: { role: 'manager' }` | 路由 meta.role | | 后端 | `BizProjectController.list` → `selectList` **无 user_id 过滤**(manager 可看所有项目) | 无 | > ⚠️ **manager 是全局视图角色**(合规管理员), 看所有项目不过滤 — 设计正确 > > **其他角色访问**: > - sponsor → `GET /business/project/sponsorList` (按 `sponsor_admin_user_id = uid` 过滤) > - executor → `GET /business/project/executorList` (按 `biz_project_assign` 关联过滤) > - 这两个**不会进** manager/Projects.vue,但共用 controller --- ## 2. 筛选项 (filter-form) | UI label | 控件 | v-model | 后端 SQL | 命中表.字段 | 选项来源 | 原型对照 | |---|---|---|---|---|---|---| | 支持单位 | `el-select` (multiple+remote) | `q.sponsorAdminUserIds` (List) | `selectList` line 167-173 `sponsor_admin_user_id IN ` | biz_project.sponsor_admin_user_id | `/business/org/sponsorOptions` (LIMIT 5) | ✅ 原型有 | | 服务机构 | `el-input` | `q.execOrgName` | line 167-178 `EXISTS (biz_project_assign JOIN biz_org + sys_user WHERE org_name/user_name/nick_name LIKE ?)` | biz_org.org_name + sys_user.user_name/nick_name | — | ✅ 原型有 | | 项目编号 | `el-input` | `q.projectNo` | line 156 `project_no LIKE concat('%',?, '%')` | biz_project.project_no | — | ✅ 原型有 | | 项目名称 | `el-input` | `q.projectName` | line 157 `project_name LIKE concat('%',?, '%')` | biz_project.project_name | — | ✅ 原型有 | | 项目形式 | `el-select` | `q.projectForm` | line 158 `project_form = ?` | biz_project.project_form | **写死**: 线上/线下/线上+线下/其他 | ✅ 原型有 | | 项目时间(起/止) | `el-date-picker` type="date" + `value-format="YYYY-MM-DD HH:mm:ss"` | `q.startTime/endTime` | line 93-94 `start_time >= ? AND end_time <= ?` (sponsorList); line 134-135 同 | biz_project.start_time/end_time | — | ✅ 原型有 | | 是否结题 | `el-select` | `q.isFinished` | line 179 `is_finished = ?` | biz_project.is_finished | **写死**: 已结题→'1' / 未结题→'0' | ✅ 原型有 | | 是否结算 | `el-select` | `q.isSettled` | line 180 `is_settled = ?` | biz_project.is_settled | **写死**: 已结算→'Y' / 未结算→'N' | ⚠️ 原型**无** (实现新增) | | 评价得分 | `el-input` | `q.managerScore` | ❌ **未生效** — 后端 `selectList` 无 `managerScore` 过滤 | — | — | ✅ 原型有 | > ⚠️ **重要 bug**:`managerScore` 筛选项前端展示 + 前端 reset 也保留字段,但**后端 selectList 完全没读这个参数**,筛选无效 > 另见 `selectSponsorList` 和 `selectExecutorList` 也都没有 `managerScore` 过滤 --- ## 3. 工具栏按钮 (toolbar) | 按钮 | 功能 | 接口 | 涉及表 | 原型对照 | |---|---|---|---|---| | 新建项目 | 跳转独立页 | `router.push('/manager/projects/new')` → `ProjectsNew.vue` | biz_project | ✅ 原型有 | | 项目分配 | 弹 dialog (单条或批量,按勾选数) | `PUT /business/project` (改 sponsor_admin_user_id) + `POST /business/project/{id}/assigns` | biz_project + biz_project_assign | ✅ 原型有 | | 批量评分 | 弹 dialog,共用 4 维度评分 | `PUT /business/project` (managerScore) + `POST /business/project/rate` × N | biz_project + biz_project_rating | ✅ 原型有 | | 批量结题 | 弹 dialog,确认后批量改 is_finished='1' | `PUT /business/project` × N | biz_project.is_finished | ✅ 原型有 | | 批量开通 | 弹 dialog,选截止时间,批量改 end_time | `PUT /business/project` × N | biz_project.end_time | ✅ 原型有 | | 导出 (3 个按钮) | 项目/评价/已报名专家导出 | `exportProjects` / `exportEval` / `exportExperts` | — | ⚠️ 前两个是 stub,仅 `exportExperts` 实装 | > ⚠️ **stub 函数**:`exportProjects` (line 347) / `exportEval` (line 348) 仅弹 `ElMessage.info('开发中')` > ⚠️ **遗漏**:原型有 3 个导出按钮 (导出/项目评价导出/已报名专家信息导出), 实装只有 1 个 > ⚠️ **额外按钮 (实现新增)**: 删除公告(原型的 #7), 不是"扩展"而是"实现" --- ## 4. 表格 (el-table) ### 4.1 数据来源 SQL(manager 通用列表 `selectList`) ```sql SELECT p.project_id, p.project_no, p.project_name, p.total_sessions, p.done_sessions, p.todo_sessions, p.total_amount, p.available_amount, p.paid_labor_amount, p.paid_meeting_amount, p.manager_score, p.sponsor_score, p.sponsor_admin_user_name, p.project_form, p.is_finished, p.is_settled, p.sponsor_admin_user_id, p.lead_user_id, p.is_bid_project, p.create_by, p.create_time, p.update_by, p.update_time, p.manage_fee, p.start_time, p.end_time, p.submit_deadline_days, p.support_contract_url, p.execute_contract_url, p.invitation_url, p.support_letter_url, p.publish_url, o.org_name as sponsor_org_name, lu.user_name as lead_user_name, (SELECT group_concat(distinct o2.org_name separator ',') FROM biz_project_assign bpa JOIN biz_org o2 ON o2.org_id = bpa.execution_unit_id AND o2.org_type = 'executor' WHERE bpa.project_id = p.project_id) AS exec_org_names FROM biz_project p LEFT JOIN biz_org o ON o.user_id = p.sponsor_admin_user_id AND o.org_type = 'sponsor' LEFT JOIN sys_user lu ON lu.user_id = p.lead_user_id [WHERE 动态过滤...] ``` ### 4.2 列映射 | 列 | row.* | DB 表.字段 | 原型对照 | |---|---|---|---| | 复选框 | — | — | ✅ | | 项目编号 | `row.projectNo` | biz_project.project_no | ✅ | | 项目名称 | `row.projectName` | biz_project.project_name | ✅ | | 总场次/总期数 | `row.totalSessions` | biz_project.total_sessions | ✅ | | 已执行 | `row.doneSessions` | biz_project.done_sessions | ✅ | | 未执行 | `row.todoSessions` | biz_project.todo_sessions | ✅ | | 总金额 | `row.totalAmount` (fmtMoney) | biz_project.total_amount | ✅ | | 可用金额 | `row.availableAmount` (fmtMoney) | biz_project.available_amount | ✅ | | 已支付劳务费 | `row.paidLaborAmount` (fmtMoney) | biz_project.paid_labor_amount | ✅ | | 已支付会务费 | `row.paidMeetingAmount` (fmtMoney) | biz_project.paid_meeting_amount | ✅ | | 执行单位得分(合规) | `row.managerScore` | biz_project.manager_score | ✅ | | 执行单位得分(支持) | `row.sponsorScore` | biz_project.sponsor_score | ✅ | | 支持单位 | `row.sponsorOrgName` | biz_org.org_name (JOIN) | ✅ | | 服务机构 | `row.execOrgNames` | GROUP_CONCAT of biz_org.org_name | ✅ | | 项目形式 | `row.projectForm` | biz_project.project_form | ✅ | | 是否结题 | `row.isFinished` (tag 化) | biz_project.is_finished | ✅ | | 项目开始时间 | `row.startTime` (fmtDate) | biz_project.start_time | ✅ | | 项目结束时间 | `row.endTime` (fmtDate) | biz_project.end_time | ✅ | | 操作 | 6 个 link + dropdown | — | ✅ 8 项操作齐全(建会/分配/结题/查看/编辑/开通/删除公告/执行单位评分) | --- ## 5. 行内操作按钮 | 按钮 | 函数 | 接口 | 后端动作 | 涉及表.字段 | 原型对照 | |---|---|---|---|---|---| | 查看 | `viewDetail` | `router.push('/manager/projects/detail/${row.projectId}')` | (独立页) | — | ✅ | | 编辑 | `doEdit` | `router.push('/manager/projects/edit/${row.projectId}')` | (独立页 ProjectsNew.vue) | biz_project (全字段) | ✅ | | 建会 | `doCreateMeeting` | `router.push('/manager/meetings/new?projectId=${row.projectId}')` | (独立页 MeetingNew.vue) | — | ✅ | | 项目分配 | `doAssign` | 同 `openAssign` | — | — | ✅ | | 结题 (单条) | `openClose` + `confirmClose` | `PUT /business/project` `{projectId, isFinished: '1'}` | `bizProjectService.updateByPrimaryKey` | biz_project.is_finished='1' | ✅ | | 开通 (单条) | `openActivate` + `confirmActivate` | `PUT /business/project` `{projectId, isFinished: '0'}` ⚠️ **字段写错了** | 同上 | biz_project.is_finished='0' | ✅ 但**实现不对** | | 删除公告 | `openDeleteAnnouncement` + `confirmDeleteAnn` | `PUT /business/project` 清空 5 个 URL + `isPublished: '0'` | 同上 | biz_project.{publish_url,invitation_url,support_letter_url,notice_url,schedule_url}='\0' + is_published='0' | ✅ | | 执行单位评分 (单条) | `openSingleScore` + `confirmSingleScore` | `PUT /business/project` `{managerScore}` + `POST /business/project/rate` | `bizProjectRatingService.upsertRating` | biz_project.manager_score + biz_project_rating (新行或更新) | ✅ | | 导出报名专家 | `exportExperts` | `POST /business/executionIntent/export?projectNo=...` | (单独执行方意图表) | biz_execution_intent | ✅ | | 批量评分 | `openBatch('score')` + `confirmBatch` | `PUT + POST /rate` × N | 同单条,按勾选遍历 | biz_project + biz_project_rating | ✅ | | 批量结题 | `openBatch('close')` + `confirmBatch` | `PUT /business/project` `{isFinished: '1'}` × N | 同单条 | biz_project.is_finished | ✅ | | 批量开通 | `openBatch('activate')` + `confirmBatch` | `PUT /business/project` `{endTime: activateDeadline}` × N | 同单条 | biz_project.end_time | ✅ | > ⚠️ **🐛 Bug**: 单条"开通"(confirmActivate line 397-401) **改了 `is_finished='0'` 而不是 `end_time`** — 应该跟批量开通一致改 `end_time`。改 is_finished='0' 等同于"取消结题",跟"开通"语义不符 > > 🟢 **正确性**: 评分走 `biz_project_rating` 中间表 + `upsertRating` SQL 是 `(project_id, rater_id, rater_role)` 复合唯一,符合"一人评一次"的语义 --- ## 6. Java Entity ↔ 数据库表 一致性 ### 6.1 biz_project 主表 DDL (实测) ```sql CREATE TABLE `biz_project` ( `project_id` bigint NOT NULL AUTO_INCREMENT, `project_no` varchar(50) NOT NULL COMMENT '项目编号 ZH-2026-658', `project_name` varchar(200) NOT NULL COMMENT '项目名称', `project_form` varchar(20) DEFAULT NULL COMMENT '项目形式 线上/线下/线上+线下/其他', `total_sessions` int DEFAULT '0', `done_sessions` int DEFAULT '0', `todo_sessions` int DEFAULT '0', `total_amount` decimal(15,2) DEFAULT '0.00', `available_amount` decimal(15,2) DEFAULT '0.00', `paid_labor_amount` decimal(15,2) DEFAULT '0.00', `paid_meeting_amount` decimal(15,2) DEFAULT '0.00', `manage_fee` decimal(15,2) DEFAULT '0.00' COMMENT '管理费及税金', `is_finished` char(1) DEFAULT '0' COMMENT '是否结题 0否 1是', `is_settled` char(1) DEFAULT 'N' COMMENT '是否结算 N否 Y是', `manager_score` decimal(3,1) DEFAULT NULL, `sponsor_admin_user_id` bigint DEFAULT NULL, `sponsor_admin_user_name` varchar(200) DEFAULT NULL, `lead_user_id` bigint DEFAULT NULL, `is_bid_project` char(1) DEFAULT 'N' COMMENT '是否招标项目 Y/N', `start_time` datetime DEFAULT NULL, `end_time` datetime DEFAULT NULL, `submit_deadline_days` int DEFAULT '0', `support_contract_url` varchar(500) DEFAULT NULL, `execute_contract_url` varchar(500) DEFAULT NULL, `invitation_url` varchar(500) DEFAULT NULL, `support_letter_url` varchar(500) DEFAULT NULL, `publish_url` varchar(500) DEFAULT NULL, `create_by` varchar(64) DEFAULT '', `create_time` datetime DEFAULT NULL, `update_by` varchar(64) DEFAULT '', `update_time` datetime DEFAULT NULL, `sponsor_score` decimal(3,1) DEFAULT NULL, PRIMARY KEY (`project_id`), UNIQUE KEY `uk_project_no` (`project_no`), KEY `idx_project_form` (`project_form`), KEY `idx_is_finished` (`is_finished`), KEY `idx_is_settled` (`is_settled`) ) ENGINE=InnoDB ``` ### 6.2 biz_project_assign / biz_project_rating DDL (实测) **biz_project_assign** 索引:PRIMARY(assign_id) + idx_assign_project / idx_assign_unit / idx_assign_exec_user **biz_project_rating** 索引: - PRIMARY (rating_id) - UNIQUE `uk_rating` (project_id, rater_id, rater_role) ✅ 一人评一次 - idx_pr_project, idx_pr_rater, idx_pr_rating_time ### 6.3 Entity ↔ 表字段对照 (BizProject.java) | 实体字段 | 中文列名 (DB COMMENT) | 类型 | 表字段 | 类型 | 一致? | |---|---|---|---|---|---| | projectId | 项目ID | Long | project_id | bigint | ✅ 已修 (2026-08-18, 见 §修复记录) | | projectNo | 项目编号 ZH-2026-658 | String | project_no | varchar(50) | ✅ | | projectName | 项目名称 | String | project_name | varchar(200) | ✅ | | totalSessions | 总场次/总期数 | Long | total_sessions | int | ✅ | | doneSessions | 已执行 | Long | done_sessions | int | ✅ | | todoSessions | 未执行 | Long | todo_sessions | int | ✅ | | totalAmount | 总金额 | BigDecimal | total_amount | decimal(15,2) | ✅ | | availableAmount | 可用金额 | BigDecimal | available_amount | decimal(15,2) | ✅ | | paidLaborAmount | 已支付劳务费 | BigDecimal | paid_labor_amount | decimal(15,2) | ✅ | | paidMeetingAmount | 已支付会务费 | BigDecimal | paid_meeting_amount | decimal(15,2) | ✅ | | manageFee | 管理费及税金 | BigDecimal | manage_fee | decimal(15,2) | ✅ | | managerScore | 合规管理员评均分 | BigDecimal | manager_score | decimal(3,1) | ✅ | | sponsorScore | 支持方评均分 | BigDecimal | sponsor_score | decimal(3,1) | ✅ | | sponsorAdminUserName | 赞助方负责人用户名(冗余) | String | sponsor_admin_user_name | varchar(200) | ✅ | | sponsorAdminUserId | 赞助方负责人用户ID | Long | sponsor_admin_user_id | bigint | ✅ | | leadUserId | 项目负责人 user_id | Long | lead_user_id | bigint | ✅ | | projectForm | 项目形式 线上/线下/线上+线下/其他 | String | project_form | varchar(20) | ✅ | | isFinished | 是否结题 0否 1是 | String | is_finished | char(1) | ✅ | | isSettled | 是否结算 N否 Y是 | String | is_settled | char(1) | ✅ | | isBidProject | 是否招标项目 Y/N (默认 N) | String | is_bid_project | char(1) | ✅ | | startTime | 项目开始时间 | Date | start_time | datetime | ✅ 已修 (2026-08-18, 加 @JsonFormat, 见 §修复记录) | | endTime | 项目结束时间 | Date | end_time | datetime | ✅ 已修 (2026-08-18, 同上) | | submitDeadlineDays | 提交材料截止天数 | Integer | submit_deadline_days | int | ✅ | | supportContractUrl | 支持合同文件URL | String | support_contract_url | varchar(500) | ✅ | | executeContractUrl | 执行合同文件URL | String | execute_contract_url | varchar(500) | ✅ | | invitationUrl | 邀请函文件URL | String | invitation_url | varchar(500) | ✅ | | supportLetterUrl | 支持函文件URL | String | support_letter_url | varchar(500) | ✅ | | publishUrl | 已发布公告URL | String | publish_url | varchar(500) | ✅ | | createBy/createTime | 创建者 / 创建时间 | | (BaseEntity) | | ✅ | | updateBy/updateTime | 更新者 / 更新时间 | | (BaseEntity) | | ✅ | | assignedSessions | (派生) | Long | ❌ **无对应字段** | — | ⚠️ **派生字段**,只在 executor 端 SQL 用 | | assignedAmount | (派生) | BigDecimal | ❌ **无对应字段** | — | ⚠️ 同上 | | sponsorOrgName | (派生) | String | ❌ **无对应字段** | — | ⚠️ JOIN biz_org 派生 | | execOrgName | (派生) | String | ❌ **无对应字段** | — | ⚠️ 查询参数,不入库 | | execOrgNames | (派生) | String | ❌ **无对应字段** | — | ⚠️ GROUP_CONCAT 派生 | | leadUserName | (派生) | String | ❌ **无对应字段** | — | ⚠️ JOIN sys_user 派生 | | noticeUrl | (DDL 中不存在) | String | ❌ **DDL 中不存在** | — | 🐛 **实体有但表无** | | scheduleUrl | (DDL 中不存在) | String | ❌ **DDL 中不存在** | — | 🐛 同上 | | isPublished | (DDL 中不存在) | String | ❌ **DDL 中不存在** | — | 🐛 同上,被 `confirmDeleteAnn` 当字段更新 | | publishTime | (DDL 中不存在) | Date | ❌ **DDL 中不存在** | — | 🐛 同上 | | announcementType | (DDL 中不存在) | String | ❌ **DDL 中不存在** | — | � 同上 | | sponsorRating, sponsorQ1-4, sponsorRemark | (DDL 中不存在) | | ❌ **DDL 中不存在** | — | 🐛 残留支持方评分字段,未使用 | ### 修订标记 > **修订**:`is_published` / `notice_url` / `schedule_url` / `publish_time` / `announcement_type` 这些字段**只在 BizProject.java 实体里有,DDL 实测不存在**。 > 后果:`confirmDeleteAnn` 调 `bizUpdate('project', { publishUrl, invitationUrl, supportLetterUrl, noticeUrl, scheduleUrl, isPublished: '0' })`,后端 mapper 的 `updateByPrimaryKey` 的 `` 会因为没传值而**不更新** notice_url/schedule_url/is_published,只清空前 3 个。**功能部分失效**(第 5 章结论需修正)。 --- ## 7. 索引检查 ### 7.1 biz_project 索引 | Key | 列 | 用途覆盖 | |---|---|---| | PRIMARY | project_id | ✅ ORDER BY, 主键 | | uk_project_no | project_no (UNIQUE) | ✅ 编号精确/模糊查询 | | idx_project_form | project_form | ✅ 项目形式过滤 | | idx_is_finished | is_finished | ✅ 是否结题过滤 | | idx_is_settled | is_settled | ✅ 是否结算过滤 | | idx_sponsor_admin_user_id | sponsor_admin_user_id | ✅ `selectSponsorList` WHERE `sponsor_admin_user_id = ?` | | idx_start_end_time | (start_time, end_time) | ✅ 范围查询 `start_time >= ? AND end_time <= ?`,EXPLAIN 验证 type=range | | ❌ **缺** | lead_user_id | ⚠️ 主表没用,但 JOIN sys_user 用 — 可接受,小数据量 | ### 7.2 biz_project_assign 索引 | Key | 列 | 用途覆盖 | |---|---|---| | PRIMARY | assign_id | ✅ | | idx_assign_project | project_id | ✅ `SELECT WHERE project_id = ?` | | idx_assign_unit | execution_unit_id | ✅ | | idx_assign_exec_user | exec_user_id | ✅ | | **uk_project_unit** | (project_id, execution_unit_id) UNIQUE | ✅ 防重复分配 (实测 11 行 0 重复, 安全) | ### 7.3 biz_project_rating 索引 | Key | 列 | 用途覆盖 | |---|---|---| | PRIMARY | rating_id | ✅ | | **uk_rating** | (project_id, rater_id, rater_role) UNIQUE | ✅ `upsertRating` 用 ON DUPLICATE KEY,完美 | | idx_pr_project | project_id | ✅ `SELECT WHERE project_id = ?` | | idx_pr_rater | rater_id | ✅ | | idx_pr_rating_time | rating_time | ✅ 按时间排序 | > **结论**:rating 表索引设计优秀(uk_rating 三元组唯一约束)。project 表索引已补齐 (7 个, 涵盖所有 WHERE 列)。assign 表唯一约束到位 (uk_project_unit 防重复分配)。 --- ## 8. 与原型差异 ### 8.1 实现新增 (原型没有) | 项 | 位置 | 说明 | |---|---|---| | 项目分配 dialog | `assignOpen` (line 119) | 原型 `project-assign.html` 是独立页,实现合并到本 dialog,功能更全 (支持方下拉 + 总场次/金额校验) | | 评分明细反显 | `openSingleScore` (line 424-449) | 原型无,实装调用 `/business/project/ratings` 反显当前管理员历史评分 | | 评分默认值 | `batchScoreForm default=5`, `singleScoreForm default=0` | 原型无 | | 批量开通截止时间字段 | `activateDeadline` | 原型批量开通仅"确认"无时间字段,实装加了截止时间 | | isSettled 筛选 | line 26-30 | 原型无"是否结算"筛选项 | ### 8.2 原型有但实现缺失 | 原型项 | 实现状态 | |---|---| | 导出 (项目) | 🐛 **stub** — `exportProjects` 仅 `ElMessage.info('开发中')` | | 项目评价导出 | 🐛 **stub** — `exportEval` 仅 `ElMessage.info('开发中')` | | 已报名专家信息导出 | ✅ **实装** — `exportExperts` POST `executionIntent/export` | | `managerScore` 评价得分筛选 | 🐛 **后端未生效** — 前端有字段,后端 selectList 无此过滤 | ### 8.3 文案/标签差异 | 原型 | 实现 | 差异 | |---|---|---| | 评分标准输入框 placeholder "1~5" | `el-input-number :min=0 :max=5 :step=1` | 实现用数字输入控件(更好) | | "您确定要开通吗?" 弹框 | 仅"确定/取消",**无截止时间字段** (单条版) | 原型单条版也无,批量版实现加了 | | 表格 `min-width=2900px` 横滚 | `el-table` 自动布局 | 实现用 `fixed="right"` 操作列,列宽合理 | | 状态颜色: 蓝色 status-activated (已开通) | ❌ 实现**无 "已开通" 状态列**,只有"已结题/未结题"列 | 实现走 `end_time` 而非 `is_finished`,前端没标"已开通" tag | ### 8.4 总结 - **整体方向**: 实现比原型更完整 (评分明细反显、批量开通截止时间、支持方/执行方下拉),但丢了 3 个导出按钮 (2 个 stub,1 个已报名专家实装) - **设计偏离**: 把 `is_finished` 复用为"开通/结题"两种语义 (改值而非新增字段),导致前端状态显示混乱 (已结题是 tag,已开通没 tag) --- ## 9. 表字段冗余 / 设计问题 ### 9.1 实体 vs 表不一致 (详见 §6.3) - ✅ **已修**: projectId String → Long (见 §修复记录) - ✅ **已修**: startTime/endTime String → Date (见 §修复记录) - **🔴 严重冗余** (未修): noticeUrl / scheduleUrl / isPublished / publishTime / announcementType 这 5 个字段**只在实体里,DDL 实测不存在**,但 `confirmDeleteAnn` 仍按它们构造 payload,后端 mapper 的 `` 静默跳过,**公告删除功能只删了 publishUrl/invitation/supportLetter 3 个** ### 9.2 JOIN 字段 vs 持久化字段混淆 - `sponsorOrgName` / `execOrgNames` / `leadUserName` / `assignedSessions` / `assignedAmount` 全部是**派生字段**,不入库,只在 SQL JOIN 时填充,前端可读但不应让前端 PUT 回去 - 当前 mapper 的 `updateByPrimaryKey` 已经用 `` 防御,不会回写 — OK ### 9.3 唯一约束语义检查 - ✅ biz_project: uk_project_no — 项目编号唯一,合理 - ✅ biz_project_rating: uk_rating (project_id, rater_id, rater_role) — 一人评一次,合理 - ❌ biz_project_assign: **缺唯一约束 (project_id, execution_unit_id)** — 可能重复分配同一执行方 ### 9.4 业务闭环检查 - 项目分配 dialog: `assignForm.execRows` 行内 el-select 的 `value=MAIN user_id`,后端 `BizProjectAssignServiceImpl` 应该从 exec_user_id 反查 biz_org 写 execution_unit_id (mapper 注释有写,需确认 service 实现) - 项目分配 → 评分 → 公示 → 通知: 整条业务链已铺,但 manager_score 聚合分写的是 manager 评分的均值,逻辑合理 - 批量开通: 改 `end_time` 而不是新建 `is_activated` 字段 — 用现有字段表达多语义,容易跟"结题"冲突,设计可商榷 --- ## 10. 待修复列表 (按优先级) | # | 问题 | 文件:行 | 修复建议 | 严重度 | |---|---|---|---|---| | 1 | 单条"开通"误改 `is_finished='0'` 而非 `end_time` | `Projects.vue:398` | 改 `endTime` 而非 `isFinished` | **P0** | | 2 | `confirmDeleteAnn` 引用了不存在的字段 `noticeUrl/scheduleUrl/isPublished` | `Projects.vue:411-419` + `BizProject.java:113-122` | 要么补 DDL,要么删实体字段;mapper 不更新这些字段导致功能部分失效 | **P0** | | 3 | `managerScore` 筛选前端有,后端 selectList 不读 | `BizProjectMapper.xml:141-183` (selectList) | 加 `` 过滤 | **P0** | | 4 | 下拉菜单 `divided` 冗余 (activate/delAnn 双分隔线) | `Projects.vue:100-101` | 删 `activate` 的 `divided` | **P1** | | 5 | ~~生产代码残留 console.info/error (4 处)~~ | ~~`Projects.vue:299, 302, 306, 875`~~ | ✅ 已修 (见 §修复记录) | — | | 6 | 评分默认值不一致 | `Projects.vue:501 vs 513` | 统一为 0 或 5 | **P2** | | 7 | ~~`batchScoreRows` 死代码~~ | ~~`Projects.vue:501`~~ | ✅ 已修 (见 §修复记录) | — | | 8 | `isSettled` 筛选但表格无列显示 | `Projects.vue:26-30` vs §4.2 表 | 加列 或 删筛选 | **P2** | | 9 | ~~日期选择器 `value-format="YYYY-MM-DD HH:mm:ss"` 跟 `type="date"` 不匹配~~ | ~~`Projects.vue:17, 19`~~ | ✅ 已修 (见 §修复记录) | — | | 10 | 重复 score table HTML (批量评分 + 单条评分) | `Projects.vue:201-213, 257-269` | 抽公共组件 | **P2** | | 11 | 2 个导出按钮是 stub (`exportProjects` / `exportEval`) | `Projects.vue:347-348` | 实装 或 删按钮 | **P2** | | 12 | ~~biz_project 缺索引 `sponsor_admin_user_id`~~ | ~~`DB`~~ | ✅ 已修 (见 §修复记录) | — | | 13 | ~~biz_project 缺索引 `start_time`, `end_time` (范围查询)~~ | ~~`DB`~~ | ✅ 已修 (见 §修复记录) | — | | 14 | ~~biz_project_assign 缺 UNIQUE (project_id, execution_unit_id)~~ | ~~`DB`~~ | ✅ 已修 (见 §修复记录) | — | | 15 | `is_finished` 和 `is_settled` 状态值约定不一致 (0/1 vs Y/N) | `BizProject.java:65-68` + mapper | 统一约定 (要么全 0/1 要么全 Y/N) | **P3** | | 16 | selectList / selectExecutorList 有 4 个相关子查询 | `BizProjectMapper.xml:46-50, 110-124` | 数据小可接受,但应记录为 P3 待优化 | **P3** | | 17 | `.score-row` CSS 死样式 | `Projects.vue:926-928` | 删 | **P3** | --- ## 11. 引用清单 ### 前端 - `ry-vue3/src/views/manager/Projects.vue` — 主列表(881 行) - `ry-vue3/src/views/manager/ProjectsNew.vue` — 新建/编辑独立页 - `ry-vue3/src/views/manager/ManagerProjectDetail.vue` — 详情独立页 - `ry-vue3/src/layout/AdminLayout.vue:98` — 菜单项定义 - `ry-vue3/src/router/index.js:65-68` — 路由 (含 /new, /edit/:id, /detail/:id) - `ry-vue3/src/api/public.js` — `bizList/bizAdd/bizUpdate/bizDelete` 通用 CRUD - `ry-vue3/src/api/system.js` — `listExecutorOrgs/listSponsorOrgs` 项目分配用 - `ry-vue3/src/components/Preview.vue` — 附件预览 (import 但未使用?) ### 后端 (Java) - `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizProjectController.java` — 项目主 controller (291 行, 14 个 endpoint) - `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProject.java` — 实体 (230 行, 字段定义含 5 个 DDL 不存在的字段) - `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProjectAssign.java` — 分配实体 - `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProjectRating.java` — 评分实体 - `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizProjectRatingController.java` — 评分 CRUD (单独 controller) - `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectAssignServiceImpl.java` — 分配 service (分配时反查 execution_unit_id) - `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectRatingServiceImpl.java` — 评分 upsert service ### Mapper XML - `ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectMapper.xml` — 项目主 mapper (301 行, 4 个 select + insert + update + 2 个 delete) - `ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectAssignMapper.xml` — 分配 mapper (106 行) - `ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectRatingMapper.xml` — 评分 mapper (含 `upsertRating` 三元组 ON DUPLICATE KEY) ### 配置 / 工具 - `ry-api/ruoyi-admin/src/main/resources/application-druid.yml:9-10` — DB 连接 (root / cu2oh2co3) ### 原型 - `proto/html/manager.html:206` — 菜单入口 - `proto/html/components/project-manage.html` — 项目管理原型 (676 行,含 5 个 modal) - `proto/html/components/project-assign.html` — 项目分配原型 (未深读,但 dialog 实装与之有偏差) ### 数据库 (实测) - `biz_project` — 11 行,5 索引 (含 1 唯一),缺 3 个索引 - `biz_project_assign` — 11 行,4 索引,缺唯一约束 - `biz_project_rating` — 21 行,5 索引 (含 1 唯一),设计优秀 - `biz_org` — 33 行 (待查索引,本审查未深入) - `sys_user` — 40 行 (待查 role_type 索引)