Files
guoju0808/_self/manager_projects.md
T
郭庆泰 be1c973366 docs+chore(projects): 索引 EXPLAIN 分析 + 修正过时注释
- BizOrgMapper: selectSponsorOrgOptions/selectExecutorOrgOptions 注释
  写 "走 idx_org_user_type_name", EXPLAIN 实际命中 idx_org_type_status
  (优化器小数据下选小索引), 改为实测索引 + Using where 描述
- manager_projects.md §9.3: biz_project_assign 唯一约束状态标 
  (SHOW INDEX 实测有 uk_project_unit, doc 老版本误标 )
- manager_projects.md §9.5: 新增索引分析章节, 记录 8 个关键查询
  EXPLAIN 命中 + 已加/评估后未加的索引决策

DB 实际改动 (已落库, 无 .sql 文件):
- ADD INDEX idx_assign_exec_user_project (exec_user_id, project_id)
  on biz_project_assign — 服务机机构 EXISTS+IN covering 优化
- DROP 临时测试的 idx_org_type_id — 优化器未采纳, 33 行数据量不值
2026-08-20 22:39:34 +08:00

489 lines
34 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 处 `<if test="... != null and ... != ''">` 改为 `<if test="... != null">` (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` | `<a data-page="components/project-manage.html">` ✓ |
---
## 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<Long>) | `selectList` line 167-173 `sponsor_admin_user_id IN <foreach>` | biz_project.sponsor_admin_user_id | `/business/org/sponsorOptions` (LIMIT 5) | ✅ 原型有 |
| 服务机构 | `el-select` (multiple+remote) | `q.execAdminUserIds` (List<Long>) | `selectList` line 173-181 `EXISTS (biz_project_assign WHERE exec_user_id IN <foreach>)` | biz_project_assign.exec_user_id (MAIN user_id) | `/business/org/executorOptions` (LIMIT 5) | ✅ 原型有 |
| 项目编号 | `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 派生 |
| execOrgNames | (派生) | String | ❌ **无对应字段** | — | ⚠️ GROUP_CONCAT 派生, 仅展示 |
| execAdminUserIds | (查询参数) | List\<Long\> | ❌ **无对应字段** | biz_project_assign.exec_user_id | ✅ 多选 select 透传, 走 EXISTS + IN 精确查询 |
| 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` 的 `<if test="...">` 会因为没传值而**不更新** 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 的 `<if test="...">` 静默跳过,**公告删除功能只删了 publishUrl/invitation/supportLetter 3 个**
### 9.2 JOIN 字段 vs 持久化字段混淆
- `sponsorOrgName` / `execOrgNames` / `leadUserName` / `assignedSessions` / `assignedAmount` 全部是**派生字段**,不入库,只在 SQL JOIN 时填充,前端可读但不应让前端 PUT 回去
- 当前 mapper 的 `updateByPrimaryKey` 已经用 `<if test="...">` 防御,不会回写 — OK
### 9.3 唯一约束语义检查
- ✅ biz_project: uk_project_no — 项目编号唯一,合理
- ✅ biz_project_rating: uk_rating (project_id, rater_id, rater_role) — 一人评一次,合理
- ✅ biz_project_assign: uk_project_unit (project_id, execution_unit_id) — 同一项目同一执行方只能分配一次
### 9.5 索引分析与 EXPLAIN (2026-08-20)
数据规模: biz_project 12 行 / biz_org 33 行 / biz_project_assign 12 行 / sys_user 47 行 (测试数据)
| # | 查询 | 命中索引 | 备注 |
|---|---|---|---|
| Q1 | `selectSponsorOrgOptions` | `idx_org_type_status` (2 列) | 优化器在小数据量倾向小索引, `org_name LIKE` 走 Using where 二次过滤, `ORDER BY org_id DESC` 走 filesort |
| Q2 | `selectExecutorOrgOptions` | `idx_org_type_status` + `PRIMARY` sys_user | 同上, `parent_user_id IS NULL` 走 Using where |
| Q3 | `selectList` 主项目 + `sponsor_admin_user_id IN` | `idx_sponsor_admin_user_id` range + Using index | ✅ 完美 covering, 无需回表 |
| Q4 | `selectList` 主项目 + `EXISTS(exec_user_id IN)` | `idx_assign_exec_user_project` + LooseScan | ✅ 完美 covering (新加索引, 见下) |
| Q5 | `selectList` 主项目 + sponsor IN + 多 LEFT JOIN + GROUP_CONCAT 子查询 | `idx_project_form` Backward scan + 3 个 LEFT JOIN 全 eq_ref/Using index + `uk_project_unit` GROUP_CONCAT | ✅ 全部走索引 |
| Q6 | `selectList` 主项目 + 单独 exec EXISTS (无其他过滤) | `idx_assign_exec_user_project` range + Start temporary | ⚠️ 优化器会 material 子查询, 数据量小可接受 |
| Q7 | selectList 同时 sponsor IN + exec EXISTS | `idx_project_form` 主表 + `idx_assign_exec_user_project` FirstMatch 子查询 | ✅ 组合最优, semi-join |
| Q8 | selectList 仅 projectForm + isFinished | `index_merge(intersect(idx_project_form, idx_is_finished))` | ✅ MySQL 8 自动交集 |
**已加索引** (2026-08-20):
```sql
ALTER TABLE biz_project_assign ADD INDEX idx_assign_exec_user_project (exec_user_id, project_id);
```
理由: Q4/Q6/Q7 (服务机机构 EXISTS+IN) 原命中 `idx_assign_exec_user(exec_user_id)` 单列索引,
需回表读 project_id; 新加复合索引直接 covering, EXPLAIN 显示 `Using index` + `LooseScan`/`range`, 无需回表。
**评估后未加的索引** (记录备查):
- `biz_org (org_type, org_id, org_name)` — 用于消除 options 查询的 filesort。
评估: 强制 hint 后确实变 `Backward index scan` 无 filesort,
但优化器在 33 行数据下仍选 `idx_org_type_status` (2 列更小, 估算成本更低)。
决策: 数据量小不需要, **DROP**, 等生产数据增长到 1k+ 行再 EVAL。
- `biz_org (org_type, status, is_published)` 等 — 无明显 QPS 压力场景, 不加。
**过时注释修正** (2026-08-20):
- `BizOrgMapper.xml` 旧注释写 "走 idx_org_user_type_name 索引", EXPLAIN 实际命中 `idx_org_type_status`, 已修正为实测命中索引 + Using where 描述
### 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) | 加 `<if test="managerScore != null">` 过滤 | **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 索引)