Files
guoju0808/_self/manager_projects.md
T
郭庆泰andClaude d1c755971d docs: 加 _self/manager_projects_assign.md + manager_projects 修复记录
- manager_projects_assign.md: 新增 11 章端到端审查报告 (4 跳定位 + Entity 一致性 + 索引 + 原型对比 + 待修复 16 条)
- manager_projects.md: 修复记录表加 2026-08-20 P1/P2/P3 修复条目 (与上面 commit 一致)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-20 23:31:41 +08:00

495 lines
36 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.
## 修复记录
| 日期 | 章节 | 修复内容 |
|---|---|---|
| 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`。 |
| 2026-08-20 | §2 / §10 #3 | **P0 修复: selectList 补 managerScore 过滤** (88df386)。前端评价得分筛选透传到后端但 mapper 没读, 形同虚设。加 `<if test="managerScore != null">and manager_score = #{managerScore}</if>`, 跟 INSERT/UPDATE 同款 BigDecimal null check。EXPLAIN 验证 12 行数据走 PRIMARY Backward scan, 命中 1 行 manager_score=5。 |
| 2026-08-20 | §2 / §8.1 | **支持单位筛选改多选 remote el-select** (1b35161 + c85370d + 9d2ed60)。前后端 3 commit: 后端 `selectSponsorOrgOptions` 加 LIMIT 5 + mapper `sponsor_admin_user_id IN <foreach>` + 前端 multi-select + 200ms debounce 远程搜索 + 已选 userId 重拉回保证 label 存在。 |
| 2026-08-20 | §2 / §8.1 | **服务机构筛选改多选 remote el-select** (8d50450 + e462eea + b4acd72)。镜像支持单位, 区别: 服务机构走 EXISTS + biz_project_assign.exec_user_id IN (1 项目 = N 服务方, 关联表), 支持单位走单列 IN。`selectExecutorOrgOptions` 也加 LIMIT 5。 |
| 2026-08-20 | §6.3 | **彻底删除 BizProject.execOrgName** (2b69ef1)。`execOrgName` 旧 LIKE 查询条件字段已废弃, 新查询走 `execAdminUserIds`, 字段保留无意义, 直接删。`execOrgNames` (复数, GROUP_CONCAT 派生) 保留。 |
| 2026-08-20 | §9.5 | **DB: biz_project_assign 加复合索引 `idx_assign_exec_user_project (exec_user_id, project_id)`** (be1c973)。新加的 EXISTS+IN 查询从 `idx_assign_exec_user` 单列 + 回表 → 复合 covering, EXPLAIN 验证 `Using index` + `LooseScan`/`FirstMatch`, 无需回表。 |
| 2026-08-20 | doc | **§10 #1 / #2 删除 + 7 处相关误标清理** (25cec1e)。原 §10 P0 #1 (单条开通误改 is_finished='0') 和 #2 (confirmDeleteAnn 引用不存在字段) 经核实属业务正确, 删除 doc 误标, 不补 "业务正确" 注释。 |
---
## 0. 4 跳定位 (铁律: 不按名字猜)
| 跳 | 文件:行 | 结果 |
|---|---|---|
| ① 角色菜单 | `ry-vue3/src/layout/AdminLayout.vue:96` | `path: '/manager/projects', title: '项目管理'` ✓ |
| ② 路由 | `ry-vue3/src/router/index.js:60` | `component: () => import('@/views/manager/Projects.vue')` ✓ |
| ③ 组件 | `ry-vue3/src/views/manager/Projects.vue` | 920 行,本审查对象 |
| ④ 原型 | `proto/html/components/project-manage.html` + `project-assign.html` | 项目管理 + 项目分配 dialog ✓ |
> **⚠️ 重要**: AdminLayout L96 (`manager.projects`) 与 AdminLayout L86 (`admin.projects` 一级菜单的子项, 待确认) **不冲突**, 但路由表 L28 `admin-projects` 也指向同一 `manager/Projects.vue` 组件 (`router/index.js:28`)。说明 admin 角色的"项目"也复用本页, 走相同的 `selectList` 接口。
---
## 1. 主要功能 + 可见性
### 1.1 主要功能
| 功能 | 前端入口 | 后端接口 | 涉及表 |
|---|---|---|---|
| 项目列表(分页+筛选) | `manager/Projects.vue` 表头筛选 + `load()` | `GET /business/project/list``BizProjectController.list``BizProjectService.selectList` | biz_project |
| 单条/批量结题 | `openClose` / `openBatch('close')` | `PUT /business/project``bizProjectService.updateByPrimaryKey` (只改 `is_finished='1'`) | biz_project.is_finished |
| 单条/批量开通 | `openActivate` / `openBatch('activate')` | `PUT /business/project` `{projectId, isFinished: '0'}` | biz_project.is_finished='0' |
| 删除公告(单条) | `confirmDeleteAnn` | `PUT /business/project` (清空 publishUrl/invitationUrl/supportLetterUrl) | biz_project |
| 单条/批量执行单位评分 | `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 |
| 导出报名专家 | `exportExperts` | `POST /business/executionIntent/export?projectNo=...` | biz_execution_intent |
| 新建/编辑/查看 | 跳独立页 `/manager/projects/new``/edit/:id``/detail/:id` | 同 | biz_project |
> **⚠️ 原型顶部 "导出" / "项目评价导出" 按钮在 Vue 是 stub** (`Projects.vue:361-362`, `exportProjects` / `exportEval` 仅 `ElMessage.info('开发中')`)。仅 "已报名专家信息导出" 实装, 但挪到了行操作 dropdown。
### 1.2 哪些人能看到本页面
三层过滤:
| 层 | 来源 | 校验字段 |
|---|---|---|
| 前端菜单 | `AdminLayout.vue:96` `MENU.manager` | 无 `requireMain`, 主账号/子账号均可见 (manager 角色没子账号概念) |
| 路由守卫 | `router/index.js:60` `name: 'manager-projects'` | `meta.role` 无显式声明, 走全局 `permission.js` 校验登录态 |
| 后端 SQL | `BizProjectController.list` 注入 `@Autowired BizProjectService``selectList` | **无 role 过滤**, 返回全部项目, 跟前端的"manager 全局可见" 语义一致 |
**其他角色映射**:
- sponsor → 走 `/business/project/sponsorList` (按 `sponsor_admin_user_id = uid` 过滤), 独立端 `sponsor/Projects.vue`, 不进本页
- executor → 走 `/business/project/executorList` (按 `biz_project_assign` 关联过滤), 独立端 `executor/Projects.vue`, 不进本页
- doctor → 走 `/business/executionIntent` (跟项目关系是报名, 不是项目主), 独立端 `doctor/Projects.vue`
**数据隔离**: manager 是无界角色, 看到全量项目, 不存在"自己的项目"概念。
---
## 2. 筛选项 (filter-form)
| UI label | 控件 | v-model | 后端 SQL | 命中表.字段 | 选项来源 | 原型对照 |
|---|---|---|---|---|---|---|
| 支持单位 | `el-select` (multiple+remote) | `q.sponsorAdminUserIds` (List<Long>) | `selectList` line 167-172 `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 164 `project_no LIKE concat('%',?, '%')` | biz_project.project_no | — | ✅ 原型有 |
| 项目名称 | `el-input` | `q.projectName` | line 165 `project_name LIKE concat('%',?, '%')` | biz_project.project_name | — | ✅ 原型有 |
| 项目形式 | `el-select` | `q.projectForm` | line 166 `project_form = ?` | biz_project.project_form | **写死**: 线上/线下/线上+线下/其他 | ✅ 原型有 |
| 项目时间(起/止) | `el-date-picker` type="date" + `value-format="YYYY-MM-DD"` | `q.startTime/endTime` | line 154-155 `start_time >= ? AND end_time <= ?` (sparentList); line 96-97 同 (sponsorList); line 142-143 同 (executorList) | biz_project.start_time/end_time | — | ✅ 原型有 |
| 是否结题 | `el-select` | `q.isFinished` | line 185 `is_finished = ?` | biz_project.is_finished | **写死**: 已结题→'1' / 未结题→'0' | ✅ 原型有 |
| 是否结算 | `el-select` | `q.isSettled` | line 186 `is_settled = ?` | biz_project.is_settled | **写死**: 已结算→'Y' / 未结算→'N' | ⚠️ 原型**无** (实现新增) |
| 评价得分 | `el-input` | `q.managerScore` | line 188 `manager_score = ?` (BigDecimal, 跟 INSERT/UPDATE 同款 `!= null` 检查) | biz_project.manager_score | — | ✅ 原型有 |
| 按钮 | 查找/重置 | — | — | — | — | ✅ 原型有 |
> **其他角色访问**:
> - sponsor → `GET /business/project/sponsorList` (按 `sponsor_admin_user_id = uid` 过滤)
> - executor → `GET /business/project/executorList` (按 `biz_project_assign` 关联过滤)
> - 这两个**不会进** manager/Projects.vue,但共用 controller
---
## 3. 工具栏按钮 (toolbar)
| 按钮 | 功能 | 接口 | 涉及表 | 原型对照 |
|---|---|---|---|---|
| 新建项目 | 跳转独立页 | (无, 仅 router.push) | — | ✅ 原型有 |
| 项目分配 | 打开分配 dialog (单选/批量) | `PUT /business/project` + `POST /business/project/{id}/assigns` | biz_project + biz_project_assign | ✅ 原型有 |
| 批量评分 | 打开批量评分 dialog (共享 4 维度) | `POST /business/project/rate` × N + `PUT /business/project` × N | biz_project_rating + biz_project.manager_score | ✅ 原型有 |
| 批量结题 | 打开批量结题 dialog (纯确认) | `PUT /business/project` {isFinished: '1'} × N | biz_project.is_finished | ✅ 原型有 |
| 批量开通 | 打开批量开通 dialog (含截止时间 picker) | `PUT /business/project` {endTime} × N | biz_project.end_time | ✅ 原型有 |
| ~~导出~~ | stub `exportProjects` | (无) | — | ⚠️ 原型有, **实现未做** |
| ~~项目评价导出~~ | stub `exportEval` | (无) | — | ⚠️ 原型有, **实现未做** |
---
## 4. 表格 (el-table)
### 4.1 数据来源
`selectList` SQL (`BizProjectMapper.xml:149-189`):
```xml
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_user_id, 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, -- LEFT JOIN biz_org (sponsor)
lu.user_name as lead_user_name, -- LEFT JOIN sys_user (project manager)
bp.name as create_user_name, -- LEFT JOIN biz_person (creator)
(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
LEFT JOIN biz_person bp ON bp.user_id = p.create_user_id
WHERE
[projectIds IN (...) if params.projectIds] -- 预留, manager 端没传
AND [projectNos IN (...) if params.projectNos] -- 预留, manager 端没传
AND [project_no LIKE ? if q.projectNo]
AND [project_name LIKE ? if q.projectName]
AND [project_form = ? if q.projectForm]
AND [sponsor_admin_user_id IN (...) if q.sponsorAdminUserIds] -- 多选 IN
AND [EXISTS (bpa WHERE exec_user_id IN (...)) if q.execAdminUserIds] -- 多选 EXISTS+IN
AND [is_finished = ? if q.isFinished]
AND [is_settled = ? if q.isSettled]
AND [manager_score = ? if q.managerScore] -- 88df386 新加
ORDER BY project_id DESC
```
### 4.2 列映射
| 列名 | `row.*` | 表.字段 | 原型对照 |
|---|---|---|---|
| 选择 | (el-table-column type="selection") | — | ✅ |
| 项目编号 | `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` | biz_project.total_amount | ✅ |
| 可用金额 | `row.availableAmount` | biz_project.available_amount | ✅ |
| 已支付劳务费 | `row.paidLaborAmount` | biz_project.paid_labor_amount | ✅ |
| 已支付会务费 | `row.paidMeetingAmount` | 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` | biz_project.start_time | ✅ |
| 项目结束时间 | `row.endTime` | biz_project.end_time | ✅ |
| 操作 | (dropdown 菜单) | — | ✅ |
---
## 5. 行内操作按钮 (op-cell)
| 按钮 | 触发函数 | 接口 | 后端动作 | 涉及表.字段 | 原型对照 |
|---|---|---|---|---|---|
| 查看 | `viewDetail` | (router.push) | — | — | ✅ |
| 编辑 | `doEdit` | (router.push) | — | — | ✅ |
| **更多** dropdown | `onAction` | — | — | — | (原型无, 折叠菜单扩展) |
| └ 建会 | `doCreateMeeting` | (router.push) | — | — | ✅ |
| └ 结题 | `openClose` + `confirmClose` | `PUT /business/project` `{projectId, isFinished: '1'}` | `bizProjectService.updateByPrimaryKey` | biz_project.is_finished='1' | ✅ |
| └ 项目分配 | `doAssign` | 同 toolbar `openAssign` | — | — | ✅ (原型叫"分配", Vue 改名) |
| └ 执行单位评分 | `openSingleScore` + `confirmSingleScore` | `PUT /business/project` `{managerScore}` + `POST /business/project/rate` | `bizProjectRatingService.upsertRating` | biz_project.manager_score + biz_project_rating (新行或更新) | ✅ |
| └ 导出报名专家 (divided) | `exportExperts` | `POST /business/executionIntent/export?projectNo=...` | (单独执行方意图表) | biz_execution_intent | ✅ (原型是顶部按钮, Vue 挪到行内) |
| └ 开通 (divided, 条件) | `openActivate` + `confirmActivate` | `PUT /business/project` `{projectId, isFinished: '0'}` | 同上 | biz_project.is_finished='0' | ✅ |
| └ 删除公告 (divided, 条件) | `openDeleteAnnouncement` + `confirmDeleteAnn` | `PUT /business/project` 清空 3 个 URL | 同上 | biz_project.{publish_url,invitation_url,support_letter_url}='\0' | ✅ |
**条件可见**:
- `开通` / `删除公告`: `v-if="row.isPublished === '1' || row.publishUrl"` (L132-133)
- `结题`: `v-if="row.isFinished !== '1' && row.isFinished !== 1"` (L128)
> **下拉菜单 vs 全部展开**: 原型 8 个按钮一字排开 (`.col-op width: 420px`), Vue 折叠成 "查看 / 编辑" + "更多" dropdown, 列宽 180px。屏幕利用率更好, 但每行多一次点击展开。
---
## 6. Java Entity ↔ 数据库表 一致性
### 6.1 biz_project DDL (实测 2026-08-20)
```sql
CREATE TABLE biz_project (
project_id bigint NOT NULL AUTO_INCREMENT COMMENT '项目ID',
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' COMMENT '总场次/总期数',
done_sessions int DEFAULT '0' COMMENT '已执行',
todo_sessions int DEFAULT '0' COMMENT '未执行',
total_amount decimal(15,2) DEFAULT '0.00' COMMENT '总金额',
available_amount decimal(15,2) DEFAULT '0.00' COMMENT '可用金额',
paid_labor_amount decimal(15,2) DEFAULT '0.00' COMMENT '已支付劳务费',
paid_meeting_amount decimal(15,2) DEFAULT '0.00' COMMENT '已支付会务费',
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 COMMENT '合规管理员评均分',
sponsor_admin_user_id bigint DEFAULT NULL COMMENT '赞助方负责人用户ID (sys_user.user_id, role_type=sponsor)',
sponsor_admin_user_name varchar(200) DEFAULT NULL COMMENT '赞助方负责人用户名(冗余)',
lead_user_id bigint DEFAULT NULL COMMENT '项目负责人 user_id (FK sys_user.user_id, role_type=manager)',
is_bid_project char(1) DEFAULT 'N' COMMENT '是否招标项目 Y/N (默认 N)',
start_time datetime DEFAULT NULL COMMENT '项目开始时间',
end_time datetime DEFAULT NULL COMMENT '项目结束时间',
submit_deadline_days int DEFAULT '0' COMMENT '提交材料截止天数',
support_contract_url varchar(500) DEFAULT NULL COMMENT '支持合同文件URL',
execute_contract_url varchar(500) DEFAULT NULL COMMENT '执行合同文件URL',
invitation_url varchar(500) DEFAULT NULL COMMENT '邀请函文件URL',
support_letter_url varchar(500) DEFAULT NULL COMMENT '支持函文件URL',
publish_url varchar(500) DEFAULT NULL COMMENT '已发布公告URL',
create_by varchar(64) DEFAULT '' COMMENT '创建者',
create_user_id bigint DEFAULT NULL COMMENT '创建人user_id (JOIN biz_person 取 name)',
create_time datetime DEFAULT NULL COMMENT '创建时间',
update_by varchar(64) DEFAULT '' COMMENT '更新者',
update_time datetime DEFAULT NULL COMMENT '更新时间',
sponsor_score decimal(3,1) DEFAULT NULL COMMENT '支持方评均分',
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),
KEY idx_sponsor_admin_user_id (sponsor_admin_user_id),
KEY idx_start_end_time (start_time,end_time)
) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='项目表';
```
### 6.2 关联表 DDL
- `biz_org` (16 列, 主表): `PRIMARY(org_id) + uk_org_user_type(user_id,org_type) + idx_org_type_status + idx_org_type_name + idx_org_user_type_name`
- `biz_project_assign` (12 列, 关联表): `PRIMARY(assign_id) + uk_project_unit(project_id,execution_unit_id) + idx_assign_project + idx_assign_unit + idx_assign_exec_user + idx_assign_exec_user_project(be1c973)`
- `sys_user`: `PRIMARY(user_id) + idx_parent_user_id`
- `biz_person` (15 列, 创建人姓名来源): `PRIMARY(person_id) + uk_person_user_id(user_id,unit_type)`
- `biz_project_rating` (15 列, 4 维度评分明细表): `PRIMARY(rating_id) + uk_rating(project_id,rater_id,rater_role)`
### 6.3 Entity ↔ DB 字段对照
| 实体字段 | 中文列名 (DDL COMMENT) | 类型 | 表字段 | 类型 | 一致? |
|---|---|---|---|---|---|
| projectId | 项目ID | Long | project_id | bigint | ✅ |
| projectNo | 项目编号 | String | project_no | varchar(50) | ✅ |
| projectName | 项目名称 | String | project_name | varchar(200) | ✅ |
| projectForm | 项目形式 | String | project_form | varchar(20) | ✅ |
| totalSessions | 总场次/总期数 | Long | total_sessions | int | ✅ (Java Long 兼容 SQL int) |
| assignedSessions | (派生) | Long | — | — | ⚠️ **派生字段**,只在 executor 端 SQL 用 |
| assignedAmount | (派生) | BigDecimal | — | — | ⚠️ 同上 |
| 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) | ✅ |
| managerScore | 合规管理员评均分 | BigDecimal | manager_score | decimal(3,1) | ✅ |
| sponsorScore | 支持方评均分 | BigDecimal | sponsor_score | decimal(3,1) | ✅ |
| sponsorAdminUserName | 赞助方负责人用户名(冗余) | String | sponsor_admin_user_name | varchar(200) | ✅ |
| sponsorOrgName | (派生) | String | — | — | ⚠️ JOIN biz_org 派生 |
| sponsorAdminUserIds | (查询参数) | List\<Long\> | — | — | ✅ 多选 select 透传, 走 IN 精确查询 |
| execOrgNames | (派生) | String | — | — | ⚠️ GROUP_CONCAT 派生, 仅展示 |
| execAdminUserIds | (查询参数) | List\<Long\> | — | — | ✅ 多选 select 透传, 走 EXISTS + IN 精确查询 |
| leadUserName | (派生) | String | — | — | ⚠️ JOIN sys_user 派生 |
| leadUserId | 项目负责人 | Long | lead_user_id | bigint | ✅ |
| sponsorAdminUserId | 赞助方负责人 | Long | sponsor_admin_user_id | bigint | ✅ |
| isFinished | 是否结题 | String | is_finished | char(1) | ✅ |
| isSettled | 是否结算 | String | is_settled | char(1) | ✅ |
| isBidProject | 是否招标项目 | String | is_bid_project | char(1) | ✅ |
| manageFee | 管理费及税金 | BigDecimal | manage_fee | decimal(15,2) | ✅ |
| startTime | 项目开始时间 | Date | start_time | datetime | ✅ |
| endTime | 项目结束时间 | Date | end_time | datetime | ✅ |
| 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 / updateBy | 创建者/更新者 | String | create_by / update_by | varchar(64) | ✅ |
| createUserId | 创建人 user_id | Long | create_user_id | bigint | ✅ |
| createUserName | (派生) | String | — | — | ⚠️ JOIN biz_person.name 派生 |
| createTime / updateTime | 创建时间/更新时间 | Date | create_time / update_time | datetime | ✅ |
| sponsorRating, sponsorQ1-4, sponsorRemark | (DDL 中不存在) | | ❌ **DDL 中不存在** | — | 🐛 残留支持方评分字段,未使用 |
### 修订标记
> 较前一版已删除: `execOrgName` (已废弃 LIKE 查询, 2b69ef1 清理)、`noticeUrl/scheduleUrl/isPublished/publishTime/announcementType` (经核实属业务正确, 25cec1e 删除相关误标 doc)
> 仍保留: `sponsorRating/sponsorQ1-4/sponsorRemark` 6 个字段 — 实体有但 DDL 无, mapper 不读, 纯死字段 (独立 issue, 未修)
---
## 7. 索引检查
### 7.1 biz_project (主表)
| Key | 列 | 用途覆盖 |
|---|---|---|
| PRIMARY | `project_id` | ✅ SELECT/WHERE/ORDER BY/JOIN |
| uk_project_no | `project_no` | ✅ 唯一约束 + LIKE 模糊 + uk 索引本身可走 |
| idx_project_form | `project_form` | ✅ line 166 `project_form = ?` |
| idx_is_finished | `is_finished` | ✅ line 185 `is_finished = ?` |
| idx_is_settled | `is_settled` | ✅ line 186 `is_settled = ?` |
| idx_sponsor_admin_user_id | `sponsor_admin_user_id` | ✅ line 167-172 `IN <foreach>` (range + Using index) |
| idx_start_end_time | `(start_time, end_time)` | ✅ line 154-155 `start_time >= ? AND end_time <= ?` (range) |
### 7.2 biz_org (机构表)
| Key | 列 | 用途覆盖 |
|---|---|---|
| PRIMARY | `org_id` | ✅ JOIN + ORDER BY |
| uk_org_user_type | `(user_id, org_type)` | ✅ LEFT JOIN `o.user_id = p.sponsor_admin_user_id AND o.org_type = 'sponsor'` (eq_ref) |
| idx_org_type_status | `(org_type, status)` | ✅ options 查询命中 (小数据量下优化器优先选小索引) |
| idx_org_type_name | `(org_type, org_name)` | ✅ options `org_name LIKE` 兜底 |
| idx_org_user_type_name | `(user_id, org_type, org_name)` | ✅ 注释说走这个, EXPLAIN 实际走 idx_org_type_status (见 §9.5) |
### 7.3 biz_project_assign (项目-执行方关联表)
| Key | 列 | 用途覆盖 |
|---|---|---|
| PRIMARY | `assign_id` | ✅ |
| uk_project_unit | `(project_id, execution_unit_id)` | ✅ 唯一约束 + GROUP_CONCAT 子查询 `bpa.project_id = p.project_id` (ref) |
| idx_assign_project | `project_id` | ✅ |
| idx_assign_unit | `execution_unit_id` | ✅ |
| idx_assign_exec_user | `exec_user_id` | ✅ 单列回表 |
| **idx_assign_exec_user_project** (2026-08-20) | `(exec_user_id, project_id)` | ✅ EXISTS+IN 复合 covering (be1c973,见 §9.5) |
### 7.4 sys_user / biz_person / biz_project_rating
- `sys_user`: PRIMARY(user_id) + idx_parent_user_id — 用于 LEFT JOIN eq_ref
- `biz_person`: PRIMARY(person_id) + uk_person_user_id(user_id,unit_type) — 用于 LEFT JOIN eq_ref
- `biz_project_rating`: PRIMARY(rating_id) + uk_rating(project_id,rater_id,rater_role) — 评分明细去重
---
## 8. 与原型差异 (实现偏离)
### 8.1 实现新增 (原型没有)
| 项 | 说明 |
|---|---|
| 服务机构列 | 原型列叫"服务机构", 但 Vue 实际展示的是 `biz_project_assign` 多服务方 GROUP_CONCAT 字符串, 比原型 1 个单元格更复杂 |
| 项目分配 dialog | 原型 `.col-op` 一字排开"分配"按钮, Vue 提到 toolbar 按钮 + 弹 dialog (支持单选/批量 + 总场次实时校验) |
| 批量开通截止时间 picker | 原型批量开通是直接按钮, Vue 加截止时间 datetime picker (一致于批量评分设计) |
| 评价得分过滤后端生效 | 原型前端有筛选框, Vue 加 selectList 后端过滤 (88df386) |
### 8.2 原型有但实现缺失
| 项 | 原型位置 | 实现 | 严重度 |
|---|---|---|---|
| 顶部"导出"按钮 | `project-manage.html:135` | `Projects.vue:361``ElMessage.info('开发中')` | **P2** stub |
| 顶部"项目评价导出"按钮 | `project-manage.html:136` | `Projects.vue:362``ElMessage.info('开发中')` | **P2** stub |
| 顶部"已报名专家信息导出"按钮 | `project-manage.html:137` | 已挪到行操作 dropdown (`exportExperts`), ✅ 实装 | — |
### 8.3 文字 / 标签差异
| 原型 | 实现 | 差异 |
|---|---|---|
| 分配 | 项目分配 | Vue 改名加 "项目" 前缀, 更明确 (操作对象是项目, 不是 sponsor) |
| 评分单元格 | 仅数字显示 | Vue 数字显示 + 链接点击打开单条评分 (更省操作) |
| 状态 tag | `.status-pending` / `.status-done` / `.status-activated` | Vue 用 el-tag success/warning, 等价 |
### 8.4 总结
整体方向:**扩展** > **缺失**。多选筛选、项目分配 dialog、批量开通 picker 都是增强, 仅 2 个导出按钮 stub 待实装。
---
## 9. 表字段冗余 / 设计问题
### 9.1 派生 vs 持久化字段
- `sponsorOrgName` / `execOrgNames` / `leadUserName` / `createUserName` / `assignedSessions` / `assignedAmount` 全部是**派生字段**,不入库,只在 SQL JOIN 时填充,前端可读但不应让前端 PUT 回去
- mapper 的 `updateByPrimaryKey` 已经用 `<if test="...">` 防御,不会回写 — OK
### 9.2 唯一约束语义检查
- ✅ 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.3 业务闭环检查
- 项目分配 dialog: `assignForm.execRows` 行内 el-select 的 `value=MAIN user_id`,后端 `BizProjectAssignServiceImpl` 应该从 exec_user_id 反查 biz_org 写 execution_unit_id (mapper 注释有写,需确认 service 实现)
- 项目分配 → 评分 → 公示 → 通知: 整条业务链已铺,但 manager_score 聚合分写的是 manager 评分的均值,逻辑合理
### 9.5 索引分析与 EXPLAIN (2026-08-20)
数据规模: biz_project 12 行 / biz_org 33 行 / biz_project_assign 12 行 / biz_project_rating 22 行 / sys_user 47 行 / biz_person 29 行 (测试数据)
| # | 查询 | 命中索引 | 备注 |
|---|---|---|---|
| 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。
**过时注释修正** (2026-08-20):
- `BizOrgMapper.xml` 旧注释写 "走 `idx_org_user_type_name` 索引", EXPLAIN 实际命中 `idx_org_type_status`, 已修正为实测索引 + Using where 描述
---
## 10. 待修复列表 (按优先级)
| # | 问题 | 文件:行 | 修复建议 | 严重度 |
|---|---|---|---|---|
| 1 | ~~`managerScore` 筛选前端有,后端 selectList 不读~~ | ~~`BizProjectMapper.xml:141-183` (selectList)~~ | ✅ 已修 (88df386,见 §修复记录) | — |
| 2 | ~~biz_project 缺索引 `sponsor_admin_user_id`~~ | ~~`DB`~~ | ✅ 已修 (见 §修复记录) | — |
| 3 | ~~biz_project 缺索引 `start_time`, `end_time` (范围查询)~~ | ~~`DB`~~ | ✅ 已修 (见 §修复记录) | — |
| 4 | ~~biz_project_assign 缺 UNIQUE (project_id, execution_unit_id)~~ | ~~`DB`~~ | ✅ 已修 (见 §修复记录) | — |
| 5 | ~~生产代码残留 console.info/error (4 处)~~ | ~~`Projects.vue:299, 302, 306, 875`~~ | ✅ 已修 (见 §修复记录) | — |
| 6 | ~~日期选择器 `value-format="YYYY-MM-DD HH:mm:ss"` 跟 `type="date"` 不匹配~~ | ~~`Projects.vue:17, 19`~~ | ✅ 已修 (见 §修复记录) | — |
| 7 | ~~`batchScoreRows` 死代码~~ | ~~`Projects.vue:501`~~ | ✅ 已修 (见 §修复记录) | — |
| 8 | 下拉菜单 `divided` 冗余 (activate/delAnn 双分隔线) | `Projects.vue:132-133` | 删 `activate``divided` | **P1** |
| 9 | 评分默认值不一致 | `Projects.vue:512 vs 524` | 统一为 0 或 5 (批量=5, 单行=0) | **P2** |
| 10 | `isSettled` 筛选但表格无列显示 | `Projects.vue:58-62` vs §4.2 表 | 加列 或 删筛选 | **P2** |
| 11 | 重复 score table HTML (批量评分 + 单条评分) | `Projects.vue:217-229, 274-285` | 抽公共组件 | **P2** |
| 12 | 2 个导出按钮是 stub (`exportProjects` / `exportEval`) | `Projects.vue:361-362` | 实装 或 删按钮 (原型有) | **P2** |
| 13 | `is_finished``is_settled` 状态值约定不一致 (0/1 vs Y/N) | `BizProject.java` + mapper + 前端 select | 统一约定 (全 0/1 或全 Y/N) | **P3** |
| 14 | selectList / selectExecutorList 有 4 个相关子查询 | `BizProjectMapper.xml:50-53, 118-121` | 数据小可接受, 记录为 P3 待优化 | **P3** |
| 15 | `.score-row` CSS 死样式 | `Projects.vue:965-967` | 删 | **P3** |
| 16 | `sponsorRating/sponsorQ1-4/sponsorRemark` 6 个字段实体有 DDL 无 | `BizProject.java:131-136` + 字段映射 | 删实体字段 或 补 DDL (业务走 sponsor_rating 表) | **P3** |
---
## 11. 引用清单
### 前端
- `ry-vue3/src/layout/AdminLayout.vue:96` — manager 菜单定义
- `ry-vue3/src/router/index.js:60` — 路由 `manager-projects`
- `ry-vue3/src/views/manager/Projects.vue` — 本审查对象 (920 行)
- `ry-vue3/src/views/manager/ProjectsNew.vue` — 新建/编辑项目独立页
- `ry-vue3/src/api/business/project.js``bizList/bizAdd/bizUpdate/bizDelete`
- `ry-vue3/src/api/system.js``listSponsorOrgs/listExecutorOrgs`
- `ry-vue3/src/utils/request.js` — axios 拦截器 (TableDataInfo 归一化)
### 后端
- `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizProjectController.java:33``@RequestMapping("/business/project")` (16 endpoints)
- `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/BizProjectService.java` — 接口
- `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectServiceImpl.java` — 实现
- `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProject.java` — 实体 (含 8 个 `@Excel` 导出字段)
- `ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectMapper.xml` — 4 个查询: selectByPrimaryKey, selectSponsorList, selectExecutorList, selectList (主)
- `ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectAssignMapper.xml` — 分配子查询
- `ry-api/ruoyi-business/src/main/resources/mapper/business/BizOrgMapper.xml` — sponsor/executor options
- `ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectRatingMapper.xml` — 4 维度评分
### DB
- `biz_project` (主表, 36 列)
- `biz_org` (机构表, 16 列, sponsor/executor 共用)
- `biz_project_assign` (项目-执行方关联表, 12 列)
- `sys_user` (用户表, 关联 sponsor_admin_user_id/lead_user_id/create_user_id)
- `biz_person` (创建人姓名来源, LEFT JOIN 取 `name`)
- `biz_project_rating` (评分明细表, 4 维度 × project_id × rater)
### 配置
- `ry-api/ruoyi-admin/src/main/resources/application.yml` — Spring 主配置
- `ry-api/ruoyi-admin/src/main/resources/application-druid.yml` — DB 配置 (127.0.0.1:3306/guoju0808/root)
### 原型
- `proto/html/manager.html:206` — 项目管理菜单
- `proto/html/components/project-manage.html` — 项目管理主页 (15 行 × 19 列 + 8 行内按钮)
- `proto/html/components/project-assign.html` — 项目分配 dialog 原型