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>
This commit is contained in:
郭庆泰
2026-08-20 23:31:41 +08:00
co-authored by Claude
parent 79d7849a27
commit d1c755971d
2 changed files with 699 additions and 260 deletions
+277 -260
View File
@@ -1,11 +1,3 @@
# manager/projects — 端到端技术审查
> **审查时间**: 2026-08-18
> **审查范围**: `ry-vue3/src/views/manager/Projects.vue` (主列表) + 后端 BizProject 全链路 + DB 实测
> **对比原型**: `proto/html/components/project-manage.html`
---
## 修复记录 ## 修复记录
| 日期 | 章节 | 修复内容 | | 日期 | 章节 | 修复内容 |
@@ -18,7 +10,12 @@
| 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 #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.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-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 / §8.1 / §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 / §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 误标, 不补 "业务正确" 注释。 |
--- ---
@@ -26,10 +23,12 @@
| 跳 | 文件:行 | 结果 | | 跳 | 文件:行 | 结果 |
|---|---|---| |---|---|---|
| ① 角色菜单 | `ry-vue3/src/layout/AdminLayout.vue:98` | `path: '/manager/projects', title: '项目管理'` ✓ | | ① 角色菜单 | `ry-vue3/src/layout/AdminLayout.vue:96` | `path: '/manager/projects', title: '项目管理'` ✓ |
| ② 路由 | `ry-vue3/src/router/index.js:65` | `component: () => import('@/views/manager/Projects.vue')` ✓ | | ② 路由 | `ry-vue3/src/router/index.js:60` | `component: () => import('@/views/manager/Projects.vue')` ✓ |
| ③ 组件 | `ry-vue3/src/views/manager/Projects.vue` | 881 行,本审查对象 | | ③ 组件 | `ry-vue3/src/views/manager/Projects.vue` | 920 行,本审查对象 |
| ④ 原型 | `proto/html/manager.html:206` | `<a data-page="components/project-manage.html">` ✓ | | ④ 原型 | `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` 接口。
--- ---
@@ -40,28 +39,32 @@
| 功能 | 前端入口 | 后端接口 | 涉及表 | | 功能 | 前端入口 | 后端接口 | 涉及表 |
|---|---|---|---| |---|---|---|---|
| 项目列表(分页+筛选) | `manager/Projects.vue` 表头筛选 + `load()` | `GET /business/project/list``BizProjectController.list``BizProjectService.selectList` | biz_project | | 项目列表(分页+筛选) | `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 | | 单条/批量结题 | `openClose` / `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 | | 单条/批量开通 | `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 | | 单条/批量执行单位评分 | `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 | | 项目分配(单条+批量) | `doAssign` / `openAssign` | `PUT /business/project` (改 sponsor) + `POST /business/project/{id}/assigns` (先删后插) | biz_project + biz_project_assign |
| 删除公告(单条) | `confirmDeleteAnn` | `PUT /business/project` (清空 publishUrl/invitationUrl/supportLetterUrl) | biz_project |
| 导出报名专家 | `exportExperts` | `POST /business/executionIntent/export?projectNo=...` | biz_execution_intent | | 导出报名专家 | `exportExperts` | `POST /business/executionIntent/export?projectNo=...` | biz_execution_intent |
| 新建/编辑/查看 | 跳独立页 `/manager/projects/new``/edit/:id``/detail/:id` | 同 | biz_project | | 新建/编辑/查看 | 跳独立页 `/manager/projects/new``/edit/:id``/detail/:id` | 同 | biz_project |
### 1.2 可见性(三层隔离) > **⚠️ 原型顶部 "导出" / "项目评价导出" 按钮在 Vue 是 stub** (`Projects.vue:361-362`, `exportProjects` / `exportEval` 仅 `ElMessage.info('开发中')`)。仅 "已报名专家信息导出" 实装, 但挪到了行操作 dropdown。
### 1.2 哪些人能看到本页面
三层过滤:
| 层 | 来源 | 校验字段 | | 层 | 来源 | 校验字段 |
|---|---|---| |---|---|---|
| 前端菜单 | `AdminLayout.vue:98` | 只在 `manager` 角色菜单出现 | | 前端菜单 | `AdminLayout.vue:96` `MENU.manager` | `requireMain`, 主账号/子账号均可见 (manager 角色没子账号概念) |
| 路由守卫 | `router/index.js:61` `meta: { role: 'manager' }` | 路由 meta.role | | 路由守卫 | `router/index.js:60` `name: 'manager-projects'` | `meta.role` 无显式声明, 走全局 `permission.js` 校验登录态 |
| 后端 | `BizProjectController.list``selectList` **无 user_id 过滤**(manager 可看所有项目) | 无 | | 后端 SQL | `BizProjectController.list` 注入 `@Autowired BizProjectService``selectList` | **无 role 过滤**, 返回全部项目, 跟前端的"manager 全局可见" 语义一致 |
> ⚠️ **manager 是全局视图角色**(合规管理员), 看所有项目不过滤 — 设计正确 **其他角色映射**:
> - sponsor → 走 `/business/project/sponsorList` (按 `sponsor_admin_user_id = uid` 过滤), 独立端 `sponsor/Projects.vue`, 不进本页
> **其他角色访问**: - executor → 走 `/business/project/executorList` (按 `biz_project_assign` 关联过滤), 独立端 `executor/Projects.vue`, 不进本页
> - sponsor → `GET /business/project/sponsorList` (按 `sponsor_admin_user_id = uid` 过滤) - doctor → 走 `/business/executionIntent` (跟项目关系是报名, 不是项目主), 独立端 `doctor/Projects.vue`
> - executor → `GET /business/project/executorList` (按 `biz_project_assign` 关联过滤)
> - 这两个**不会进** manager/Projects.vue,但共用 controller **数据隔离**: manager 是无界角色, 看到全量项目, 不存在"自己的项目"概念。
--- ---
@@ -69,17 +72,21 @@
| UI label | 控件 | v-model | 后端 SQL | 命中表.字段 | 选项来源 | 原型对照 | | 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.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-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.projectNo` | line 164 `project_no LIKE concat('%',?, '%')` | biz_project.project_no | — | ✅ 原型有 |
| 项目名称 | `el-input` | `q.projectName` | line 157 `project_name LIKE concat('%',?, '%')` | biz_project.project_name | — | ✅ 原型有 | | 项目名称 | `el-input` | `q.projectName` | line 165 `project_name LIKE concat('%',?, '%')` | biz_project.project_name | — | ✅ 原型有 |
| 项目形式 | `el-select` | `q.projectForm` | line 158 `project_form = ?` | biz_project.project_form | **写死**: 线上/线下/线上+线下/其他 | ✅ 原型有 | | 项目形式 | `el-select` | `q.projectForm` | line 166 `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-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 179 `is_finished = ?` | biz_project.is_finished | **写死**: 已结题→'1' / 未结题→'0' | ✅ 原型有 | | 是否结题 | `el-select` | `q.isFinished` | line 185 `is_finished = ?` | biz_project.is_finished | **写死**: 已结题→'1' / 未结题→'0' | ✅ 原型有 |
| 是否结算 | `el-select` | `q.isSettled` | line 180 `is_settled = ?` | biz_project.is_settled | **写死**: 已结算→'Y' / 未结算→'N' | ⚠️ 原型**无** (实现新增) | | 是否结算 | `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 | — | ✅ 原型有 | | 评价得分 | `el-input` | `q.managerScore` | line 188 `manager_score = ?` (BigDecimal, 跟 INSERT/UPDATE 同款 `!= null` 检查) | biz_project.manager_score | — | ✅ 原型有 |
| 按钮 | 查找/重置 | — | — | — | — | ✅ 原型有 |
> ✅ **已修 (88df386)**:`managerScore` 过滤已加 selectList; `selectSponsorList` / `selectExecutorList` 故意不加 (评分字段与该角色无关,manager 端独有) > **其他角色访问**:
> - sponsor → `GET /business/project/sponsorList` (按 `sponsor_admin_user_id = uid` 过滤)
> - executor → `GET /business/project/executorList` (按 `biz_project_assign` 关联过滤)
> - 这两个**不会进** manager/Projects.vue,但共用 controller
--- ---
@@ -87,292 +94,311 @@
| 按钮 | 功能 | 接口 | 涉及表 | 原型对照 | | 按钮 | 功能 | 接口 | 涉及表 | 原型对照 |
|---|---|---|---|---| |---|---|---|---|---|
| 新建项目 | 跳转独立页 | `router.push('/manager/projects/new')``ProjectsNew.vue` | biz_project | ✅ 原型有 | | 新建项目 | 跳转独立页 | (无, 仅 router.push) | — | ✅ 原型有 |
| 项目分配 | dialog (单条或批量,按勾选数) | `PUT /business/project` (改 sponsor_admin_user_id) + `POST /business/project/{id}/assigns` | biz_project + biz_project_assign | ✅ 原型有 | | 项目分配 | 打开分配 dialog (单选/批量) | `PUT /business/project` + `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 (共享 4 维度) | `POST /business/project/rate` × N + `PUT /business/project` × N | biz_project_rating + biz_project.manager_score | ✅ 原型有 |
| 批量结题 | dialog,确认后批量改 is_finished='1' | `PUT /business/project` × N | biz_project.is_finished | ✅ 原型有 | | 批量结题 | 打开批量结题 dialog (纯确认) | `PUT /business/project` {isFinished: '1'} × N | biz_project.is_finished | ✅ 原型有 |
| 批量开通 | dialog,选截止时间,批量改 end_time | `PUT /business/project` × N | biz_project.end_time | ✅ 原型有 | | 批量开通 | 打开批量开通 dialog (含截止时间 picker) | `PUT /business/project` {endTime} × N | biz_project.end_time | ✅ 原型有 |
| 导出 (3 个按钮) | 项目/评价/已报名专家导出 | `exportProjects` / `exportEval` / `exportExperts` | — | ⚠️ 前两个是 stub,仅 `exportExperts` 实装 | | ~~导出~~ | stub `exportProjects` | (无) | — | ⚠️ 原型有, **实现未做** |
| ~~项目评价导出~~ | stub `exportEval` | (无) | — | ⚠️ 原型有, **实现未做** |
> ⚠️ **stub 函数**:`exportProjects` (line 347) / `exportEval` (line 348) 仅弹 `ElMessage.info('开发中')`
> ⚠️ **遗漏**:原型有 3 个导出按钮 (导出/项目评价导出/已报名专家信息导出), 实装只有 1 个
> ⚠️ **额外按钮 (实现新增)**: 删除公告(原型的 #7), 不是"扩展"而是"实现"
--- ---
## 4. 表格 (el-table) ## 4. 表格 (el-table)
### 4.1 数据来源 SQL(manager 通用列表 `selectList`) ### 4.1 数据来源
```sql `selectList` SQL (`BizProjectMapper.xml:149-189`):
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, ```xml
p.paid_meeting_amount, p.manager_score, p.sponsor_score, SELECT p.project_id, p.project_no, p.project_name, p.total_sessions, p.done_sessions, p.todo_sessions,
p.sponsor_admin_user_name, p.project_form, p.is_finished, p.is_settled, 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.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.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.start_time, p.end_time, p.submit_deadline_days,
p.support_contract_url, p.execute_contract_url, p.invitation_url, p.support_contract_url, p.execute_contract_url, p.invitation_url, p.support_letter_url, p.publish_url,
p.support_letter_url, p.publish_url, o.org_name as sponsor_org_name, -- LEFT JOIN biz_org (sponsor)
o.org_name as sponsor_org_name, lu.user_name as lead_user_name, -- LEFT JOIN sys_user (project manager)
lu.user_name as lead_user_name, bp.name as create_user_name, -- LEFT JOIN biz_person (creator)
(SELECT group_concat(distinct o2.org_name separator ',') (SELECT GROUP_CONCAT(DISTINCT o2.org_name SEPARATOR ',')
FROM biz_project_assign bpa FROM biz_project_assign bpa
JOIN biz_org o2 ON o2.org_id = bpa.execution_unit_id AND o2.org_type = 'executor' 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 WHERE bpa.project_id = p.project_id) AS exec_org_names
FROM biz_project p 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 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 sys_user lu ON lu.user_id = p.lead_user_id
[WHERE ...] 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 列映射 ### 4.2 列映射
| 列 | row.* | DB 表.字段 | 原型对照 | | 列 | `row.*` | 表.字段 | 原型对照 |
|---|---|---|---| |---|---|---|---|
| 复选框 | — | — | ✅ | | 选择 | (el-table-column type="selection") | — | ✅ |
| 项目编号 | `row.projectNo` | biz_project.project_no | ✅ | | 项目编号 | `row.projectNo` | biz_project.project_no | ✅ |
| 项目名称 | `row.projectName` | biz_project.project_name | ✅ | | 项目名称 | `row.projectName` | biz_project.project_name | ✅ |
| 总场次/总期数 | `row.totalSessions` | biz_project.total_sessions | ✅ | | 总场次/总期数 | `row.totalSessions` | biz_project.total_sessions | ✅ |
| 已执行 | `row.doneSessions` | biz_project.done_sessions | ✅ | | 已执行 | `row.doneSessions` | biz_project.done_sessions | ✅ |
| 未执行 | `row.todoSessions` | biz_project.todo_sessions | ✅ | | 未执行 | `row.todoSessions` | biz_project.todo_sessions | ✅ |
| 总金额 | `row.totalAmount` (fmtMoney) | biz_project.total_amount | ✅ | | 总金额 | `row.totalAmount` | biz_project.total_amount | ✅ |
| 可用金额 | `row.availableAmount` (fmtMoney) | biz_project.available_amount | ✅ | | 可用金额 | `row.availableAmount` | biz_project.available_amount | ✅ |
| 已支付劳务费 | `row.paidLaborAmount` (fmtMoney) | biz_project.paid_labor_amount | ✅ | | 已支付劳务费 | `row.paidLaborAmount` | biz_project.paid_labor_amount | ✅ |
| 已支付会务费 | `row.paidMeetingAmount` (fmtMoney) | biz_project.paid_meeting_amount | ✅ | | 已支付会务费 | `row.paidMeetingAmount` | biz_project.paid_meeting_amount | ✅ |
| 执行单位分(合规) | `row.managerScore` | biz_project.manager_score | ✅ | | 执行单位分(合规) | `row.managerScore` | biz_project.manager_score | ✅ |
| 执行单位分(支持) | `row.sponsorScore` | biz_project.sponsor_score | ✅ | | 执行单位分(支持) | `row.sponsorScore` | biz_project.sponsor_score | ✅ |
| 支持单位 | `row.sponsorOrgName` | biz_org.org_name (JOIN) | ✅ | | 支持单位 | `row.sponsorOrgName` | biz_org.org_name (JOIN) | ✅ |
| 服务机构 | `row.execOrgNames` | GROUP_CONCAT of biz_org.org_name | ✅ | | 服务机构 | `row.execOrgNames` | GROUP_CONCAT of biz_org.org_name | ✅ |
| 项目形式 | `row.projectForm` | biz_project.project_form | ✅ | | 项目形式 | `row.projectForm` | biz_project.project_form | ✅ |
| 是否结题 | `row.isFinished` (tag 化) | biz_project.is_finished | ✅ | | 是否结题 | `row.isFinished` (tag 化) | biz_project.is_finished | ✅ |
| 项目开始时间 | `row.startTime` (fmtDate) | biz_project.start_time | ✅ | | 项目开始时间 | `row.startTime` | biz_project.start_time | ✅ |
| 项目结束时间 | `row.endTime` (fmtDate) | biz_project.end_time | ✅ | | 项目结束时间 | `row.endTime` | biz_project.end_time | ✅ |
| 操作 | 6 个 link + dropdown | — | ✅ 8 项操作齐全(建会/分配/结题/查看/编辑/开通/删除公告/执行单位评分) | | 操作 | (dropdown 菜单) | — | ✅ |
--- ---
## 5. 行内操作按钮 ## 5. 行内操作按钮 (op-cell)
| 按钮 | 函数 | 接口 | 后端动作 | 涉及表.字段 | 原型对照 | | 按钮 | 触发函数 | 接口 | 后端动作 | 涉及表.字段 | 原型对照 |
|---|---|---|---|---|---| |---|---|---|---|---|---|
| 查看 | `viewDetail` | `router.push('/manager/projects/detail/${row.projectId}')` | (独立页) | — | ✅ | | 查看 | `viewDetail` | (router.push) | — | — | ✅ |
| 编辑 | `doEdit` | `router.push('/manager/projects/edit/${row.projectId}')` | (独立页 ProjectsNew.vue) | biz_project (全字段) | ✅ | | 编辑 | `doEdit` | (router.push) | — | — | ✅ |
| 建会 | `doCreateMeeting` | `router.push('/manager/meetings/new?projectId=${row.projectId}')` | (独立页 MeetingNew.vue) | — | ✅ | | **更多** dropdown | `onAction` | — | — | — | (原型无, 折叠菜单扩展) |
| 项目分配 | `doAssign` | 同 `openAssign` | — | — | ✅ | | └ 建会 | `doCreateMeeting` | (router.push) | — | — | ✅ |
| 结题 (单条) | `openClose` + `confirmClose` | `PUT /business/project` `{projectId, isFinished: '1'}` | `bizProjectService.updateByPrimaryKey` | biz_project.is_finished='1' | ✅ | | 结题 | `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' | ✅ | | └ 项目分配 | `doAssign` | 同 toolbar `openAssign` | | — | ✅ (原型叫"分配", Vue 改名) |
| 删除公告 | `openDeleteAnnouncement` + `confirmDeleteAnn` | `PUT /business/project` 清空 3 个 URL | 同上 | biz_project.{publish_url,invitation_url,support_letter_url}='\0' | ✅ | | └ 执行单位评分 | `openSingleScore` + `confirmSingleScore` | `PUT /business/project` `{managerScore}` + `POST /business/project/rate` | `bizProjectRatingService.upsertRating` | biz_project.manager_score + biz_project_rating (新行或更新) | ✅ |
| 执行单位评分 (单条) | `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 挪到行内) |
| 导出报名专家 | `exportExperts` | `POST /business/executionIntent/export?projectNo=...` | (单独执行方意图表) | biz_execution_intent | ✅ | | └ 开通 (divided, 条件) | `openActivate` + `confirmActivate` | `PUT /business/project` `{projectId, isFinished: '0'}` | 同上 | biz_project.is_finished='0' | ✅ |
| 批量评分 | `openBatch('score')` + `confirmBatch` | `PUT + POST /rate` × N | 同单条,按勾选遍历 | biz_project + biz_project_rating | ✅ | | └ 删除公告 (divided, 条件) | `openDeleteAnnouncement` + `confirmDeleteAnn` | `PUT /business/project` 清空 3 个 URL | 同上 | biz_project.{publish_url,invitation_url,support_letter_url}='\0' | ✅ |
| 批量结题 | `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' 等同于"取消结题",跟"开通"语义不符 **条件可见**:
> - `开通` / `删除公告`: `v-if="row.isPublished === '1' || row.publishUrl"` (L132-133)
> 🟢 **正确性**: 评分走 `biz_project_rating` 中间表 + `upsertRating` SQL 是 `(project_id, rater_id, rater_role)` 复合唯一,符合"一人评一次"的语义 - `结题`: `v-if="row.isFinished !== '1' && row.isFinished !== 1"` (L128)
> **下拉菜单 vs 全部展开**: 原型 8 个按钮一字排开 (`.col-op width: 420px`), Vue 折叠成 "查看 / 编辑" + "更多" dropdown, 列宽 180px。屏幕利用率更好, 但每行多一次点击展开。
--- ---
## 6. Java Entity ↔ 数据库表 一致性 ## 6. Java Entity ↔ 数据库表 一致性
### 6.1 biz_project 主表 DDL (实测) ### 6.1 biz_project DDL (实测 2026-08-20)
```sql ```sql
CREATE TABLE `biz_project` ( CREATE TABLE biz_project (
`project_id` bigint NOT NULL AUTO_INCREMENT, project_id bigint NOT NULL AUTO_INCREMENT COMMENT '项目ID',
`project_no` varchar(50) NOT NULL COMMENT '项目编号 ZH-2026-658', project_no varchar(50) NOT NULL COMMENT '项目编号 ZH-2026-658',
`project_name` varchar(200) NOT NULL COMMENT '项目名称', project_name varchar(200) NOT NULL COMMENT '项目名称',
`project_form` varchar(20) DEFAULT NULL COMMENT '项目形式 线上/线下/线上+线下/其他', project_form varchar(20) DEFAULT NULL COMMENT '项目形式 线上/线下/线上+线下/其他',
`total_sessions` int DEFAULT '0', total_sessions int DEFAULT '0' COMMENT '总场次/总期数',
`done_sessions` int DEFAULT '0', done_sessions int DEFAULT '0' COMMENT '已执行',
`todo_sessions` int DEFAULT '0', todo_sessions int DEFAULT '0' COMMENT '未执行',
`total_amount` decimal(15,2) DEFAULT '0.00', total_amount decimal(15,2) DEFAULT '0.00' COMMENT '总金额',
`available_amount` decimal(15,2) DEFAULT '0.00', available_amount decimal(15,2) DEFAULT '0.00' COMMENT '可用金额',
`paid_labor_amount` decimal(15,2) DEFAULT '0.00', paid_labor_amount decimal(15,2) DEFAULT '0.00' COMMENT '已支付劳务费',
`paid_meeting_amount` decimal(15,2) DEFAULT '0.00', paid_meeting_amount decimal(15,2) DEFAULT '0.00' COMMENT '已支付会务费',
`manage_fee` 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_finished char(1) DEFAULT '0' COMMENT '是否结题 0否 1是',
`is_settled` char(1) DEFAULT 'N' COMMENT '是否结算 N否 Y是', is_settled char(1) DEFAULT 'N' COMMENT '是否结算 N否 Y是',
`manager_score` decimal(3,1) DEFAULT NULL, manager_score decimal(3,1) DEFAULT NULL COMMENT '合规管理员评均分',
`sponsor_admin_user_id` bigint DEFAULT NULL, sponsor_admin_user_id bigint DEFAULT NULL COMMENT '赞助方负责人用户ID (sys_user.user_id, role_type=sponsor)',
`sponsor_admin_user_name` varchar(200) DEFAULT NULL, sponsor_admin_user_name varchar(200) DEFAULT NULL COMMENT '赞助方负责人用户名(冗余)',
`lead_user_id` bigint DEFAULT NULL, 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', is_bid_project char(1) DEFAULT 'N' COMMENT '是否招标项目 Y/N (默认 N)',
`start_time` datetime DEFAULT NULL, start_time datetime DEFAULT NULL COMMENT '项目开始时间',
`end_time` datetime DEFAULT NULL, end_time datetime DEFAULT NULL COMMENT '项目结束时间',
`submit_deadline_days` int DEFAULT '0', submit_deadline_days int DEFAULT '0' COMMENT '提交材料截止天数',
`support_contract_url` varchar(500) DEFAULT NULL, support_contract_url varchar(500) DEFAULT NULL COMMENT '支持合同文件URL',
`execute_contract_url` varchar(500) DEFAULT NULL, execute_contract_url varchar(500) DEFAULT NULL COMMENT '执行合同文件URL',
`invitation_url` varchar(500) DEFAULT NULL, invitation_url varchar(500) DEFAULT NULL COMMENT '邀请函文件URL',
`support_letter_url` varchar(500) DEFAULT NULL, support_letter_url varchar(500) DEFAULT NULL COMMENT '支持函文件URL',
`publish_url` varchar(500) DEFAULT NULL, publish_url varchar(500) DEFAULT NULL COMMENT '已发布公告URL',
`create_by` varchar(64) DEFAULT '', create_by varchar(64) DEFAULT '' COMMENT '创建者',
`create_time` datetime DEFAULT NULL, create_user_id bigint DEFAULT NULL COMMENT '创建人user_id (JOIN biz_person 取 name)',
`update_by` varchar(64) DEFAULT '', create_time datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL, update_by varchar(64) DEFAULT '' COMMENT '更新者',
`sponsor_score` decimal(3,1) DEFAULT NULL, update_time datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`project_id`), sponsor_score decimal(3,1) DEFAULT NULL COMMENT '支持方评均分',
UNIQUE KEY `uk_project_no` (`project_no`), PRIMARY KEY (project_id),
KEY `idx_project_form` (`project_form`), UNIQUE KEY uk_project_no (project_no),
KEY `idx_is_finished` (`is_finished`), KEY idx_project_form (project_form),
KEY `idx_is_settled` (`is_settled`) KEY idx_is_finished (is_finished),
) ENGINE=InnoDB 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 biz_project_assign / biz_project_rating DDL (实测) ### 6.2 关联表 DDL
**biz_project_assign** 索引:PRIMARY(assign_id) + idx_assign_project / idx_assign_unit / idx_assign_exec_user - `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)`
**biz_project_rating** 索引: ### 6.3 Entity ↔ DB 字段对照
- 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) | 实体字段 | 中文列名 (DDL COMMENT) | 类型 | 表字段 | 类型 | 一致? |
| 实体字段 | 中文列名 (DB COMMENT) | 类型 | 表字段 | 类型 | 一致? |
|---|---|---|---|---|---| |---|---|---|---|---|---|
| projectId | 项目ID | Long | project_id | bigint | ✅ 已修 (2026-08-18, 见 §修复记录) | | projectId | 项目ID | Long | project_id | bigint | ✅ |
| projectNo | 项目编号 ZH-2026-658 | String | project_no | varchar(50) | ✅ | | projectNo | 项目编号 | String | project_no | varchar(50) | ✅ |
| projectName | 项目名称 | String | project_name | varchar(200) | ✅ | | projectName | 项目名称 | String | project_name | varchar(200) | ✅ |
| totalSessions | 总场次/总期数 | Long | total_sessions | int | ✅ | | 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 | ✅ | | doneSessions | 已执行 | Long | done_sessions | int | ✅ |
| todoSessions | 未执行 | Long | todo_sessions | int | ✅ | | todoSessions | 未执行 | Long | todo_sessions | int | ✅ |
| totalAmount | 总金额 | BigDecimal | total_amount | decimal(15,2) | ✅ | | totalAmount | 总金额 | BigDecimal | total_amount | decimal(15,2) | ✅ |
| availableAmount | 可用金额 | BigDecimal | available_amount | decimal(15,2) | ✅ | | availableAmount | 可用金额 | BigDecimal | available_amount | decimal(15,2) | ✅ |
| paidLaborAmount | 已支付劳务费 | BigDecimal | paid_labor_amount | decimal(15,2) | ✅ | | paidLaborAmount | 已支付劳务费 | BigDecimal | paid_labor_amount | decimal(15,2) | ✅ |
| paidMeetingAmount | 已支付会务费 | BigDecimal | paid_meeting_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) | ✅ | | managerScore | 合规管理员评均分 | BigDecimal | manager_score | decimal(3,1) | ✅ |
| sponsorScore | 支持方评均分 | BigDecimal | sponsor_score | decimal(3,1) | ✅ | | sponsorScore | 支持方评均分 | BigDecimal | sponsor_score | decimal(3,1) | ✅ |
| sponsorAdminUserName | 赞助方负责人用户名(冗余) | String | sponsor_admin_user_name | varchar(200) | ✅ | | sponsorAdminUserName | 赞助方负责人用户名(冗余) | String | sponsor_admin_user_name | varchar(200) | ✅ |
| sponsorAdminUserId | 赞助方负责人用户ID | Long | sponsor_admin_user_id | bigint | ✅ | | sponsorOrgName | (派生) | String | — | — | ⚠️ JOIN biz_org 派生 |
| leadUserId | 项目负责人 user_id | Long | lead_user_id | bigint | ✅ | | sponsorAdminUserIds | (查询参数) | List\<Long\> | — | — | ✅ 多选 select 透传, 走 IN 精确查询 |
| projectForm | 项目形式 线上/线下/线上+线下/其他 | String | project_form | varchar(20) | ✅ | | execOrgNames | (派生) | String | — | — | ⚠️ GROUP_CONCAT 派生, 仅展示 |
| isFinished | 是否结题 0否 1是 | String | is_finished | char(1) | ✅ | | execAdminUserIds | (查询参数) | List\<Long\> | — | — | ✅ 多选 select 透传, 走 EXISTS + IN 精确查询 |
| isSettled | 是否结算 N否 Y是 | String | is_settled | char(1) | ✅ | | leadUserName | (派生) | String | — | — | ⚠️ JOIN sys_user 派生 |
| isBidProject | 是否招标项目 Y/N (默认 N) | String | is_bid_project | char(1) | ✅ | | leadUserId | 项目负责人 | Long | lead_user_id | bigint | ✅ |
| startTime | 项目开始时间 | Date | start_time | datetime | ✅ 已修 (2026-08-18, 加 @JsonFormat, 见 §修复记录) | | sponsorAdminUserId | 赞助方负责人 | Long | sponsor_admin_user_id | bigint | ✅ |
| endTime | 项目结束时间 | Date | end_time | datetime | ✅ 已修 (2026-08-18, 同上) | | 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 | ✅ | | submitDeadlineDays | 提交材料截止天数 | Integer | submit_deadline_days | int | ✅ |
| supportContractUrl | 支持合同文件URL | String | support_contract_url | varchar(500) | ✅ | | supportContractUrl | 支持合同文件URL | String | support_contract_url | varchar(500) | ✅ |
| executeContractUrl | 执行合同文件URL | String | execute_contract_url | varchar(500) | ✅ | | executeContractUrl | 执行合同文件URL | String | execute_contract_url | varchar(500) | ✅ |
| invitationUrl | 邀请函文件URL | String | invitation_url | varchar(500) | ✅ | | invitationUrl | 邀请函文件URL | String | invitation_url | varchar(500) | ✅ |
| supportLetterUrl | 支持函文件URL | String | support_letter_url | varchar(500) | ✅ | | supportLetterUrl | 支持函文件URL | String | support_letter_url | varchar(500) | ✅ |
| publishUrl | 已发布公告URL | String | publish_url | varchar(500) | ✅ | | publishUrl | 已发布公告URL | String | publish_url | varchar(500) | ✅ |
| createBy/createTime | 创建者 / 创建时间 | | (BaseEntity) | | ✅ | | createBy / updateBy | 创建者/更新者 | String | create_by / update_by | varchar(64) | ✅ |
| updateBy/updateTime | 更新者 / 更新时间 | | (BaseEntity) | | ✅ | | createUserId | 创建人 user_id | Long | create_user_id | bigint | ✅ |
| assignedSessions | (派生) | Long | **无对应字段** | — | ⚠️ **派生字段**,只在 executor 端 SQL 用 | | createUserName | (派生) | String | — | — | ⚠️ JOIN biz_person.name 派生 |
| assignedAmount | (派生) | BigDecimal | ❌ **无对应字段** | — | ⚠️ 同上 | | createTime / updateTime | 创建时间/更新时间 | Date | create_time / update_time | datetime | ✅ |
| 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 派生 |
| sponsorRating, sponsorQ1-4, sponsorRemark | (DDL 中不存在) | | ❌ **DDL 中不存在** | — | 🐛 残留支持方评分字段,未使用 | | 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. 索引检查
### 7.1 biz_project 索引 ### 7.1 biz_project (主表)
| Key | 列 | 用途覆盖 | | Key | 列 | 用途覆盖 |
|---|---|---| |---|---|---|
| PRIMARY | project_id | ✅ ORDER BY, 主键 | | PRIMARY | `project_id` | ✅ SELECT/WHERE/ORDER BY/JOIN |
| uk_project_no | project_no (UNIQUE) | ✅ 编号精确/模糊查询 | | uk_project_no | `project_no` | ✅ 唯一约束 + LIKE 模糊 + uk 索引本身可走 |
| idx_project_form | project_form | ✅ 项目形式过滤 | | idx_project_form | `project_form` | ✅ line 166 `project_form = ?` |
| idx_is_finished | is_finished | ✅ 是否结题过滤 | | idx_is_finished | `is_finished` | ✅ line 185 `is_finished = ?` |
| idx_is_settled | is_settled | ✅ 是否结算过滤 | | idx_is_settled | `is_settled` | ✅ line 186 `is_settled = ?` |
| idx_sponsor_admin_user_id | sponsor_admin_user_id | ✅ `selectSponsorList` WHERE `sponsor_admin_user_id = ?` | | 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) | ✅ 范围查询 `start_time >= ? AND end_time <= ?`,EXPLAIN 验证 type=range | | idx_start_end_time | `(start_time, end_time)` | ✅ line 154-155 `start_time >= ? AND end_time <= ?` (range) |
| ❌ **缺** | lead_user_id | ⚠️ 主表没用,但 JOIN sys_user 用 — 可接受,小数据量 |
### 7.2 biz_project_assign 索引 ### 7.2 biz_org (机构表)
| Key | 列 | 用途覆盖 | | Key | 列 | 用途覆盖 |
|---|---|---| |---|---|---|
| PRIMARY | assign_id | ✅ | | PRIMARY | `org_id` | ✅ JOIN + ORDER BY |
| idx_assign_project | project_id | ✅ `SELECT WHERE project_id = ?` | | 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_assign_unit | execution_unit_id | ✅ | | idx_org_type_status | `(org_type, status)` | ✅ options 查询命中 (小数据量下优化器优先选小索引) |
| idx_assign_exec_user | exec_user_id | ✅ | | idx_org_type_name | `(org_type, org_name)` | ✅ options `org_name LIKE` 兜底 |
| **uk_project_unit** | (project_id, execution_unit_id) UNIQUE | ✅ 防重复分配 (实测 11 行 0 重复, 安全) | | idx_org_user_type_name | `(user_id, org_type, org_name)` | ✅ 注释说走这个, EXPLAIN 实际走 idx_org_type_status (见 §9.5) |
### 7.3 biz_project_rating 索引 ### 7.3 biz_project_assign (项目-执行方关联表)
| Key | 列 | 用途覆盖 | | Key | 列 | 用途覆盖 |
|---|---|---| |---|---|---|
| PRIMARY | rating_id | ✅ | | PRIMARY | `assign_id` | ✅ |
| **uk_rating** | (project_id, rater_id, rater_role) UNIQUE | ✅ `upsertRating` 用 ON DUPLICATE KEY,完美 | | uk_project_unit | `(project_id, execution_unit_id)` | ✅ 唯一约束 + GROUP_CONCAT 子查询 `bpa.project_id = p.project_id` (ref) |
| idx_pr_project | project_id | ✅ `SELECT WHERE project_id = ?` | | idx_assign_project | `project_id` | ✅ |
| idx_pr_rater | rater_id | ✅ | | idx_assign_unit | `execution_unit_id` | ✅ |
| idx_pr_rating_time | rating_time | ✅ 按时间排序 | | 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) |
> **结论**:rating 表索引设计优秀(uk_rating 三元组唯一约束)。project 表索引已补齐 (7 个, 涵盖所有 WHERE 列)。assign 表唯一约束到位 (uk_project_unit 防重复分配)。 ### 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. 与原型差异 (实现偏离)
### 8.1 实现新增 (原型没有) ### 8.1 实现新增 (原型没有)
| 项 | 位置 | 说明 | | 项 | 说明 |
|---|---|---| |---|---|
| 项目分配 dialog | `assignOpen` (line 119) | 原型 `project-assign.html` 是独立页,实现合并到本 dialog,功能更全 (支持方下拉 + 总场次/金额校验) | | 服务机构列 | 原型列叫"服务机构", 但 Vue 实际展示的是 `biz_project_assign` 多服务方 GROUP_CONCAT 字符串, 比原型 1 个单元格更复杂 |
| 评分明细反显 | `openSingleScore` (line 424-449) | 原型无,实装调用 `/business/project/ratings` 反显当前管理员历史评分 | | 项目分配 dialog | 原型 `.col-op` 一字排开"分配"按钮, Vue 提到 toolbar 按钮 + 弹 dialog (支持单选/批量 + 总场次实时校验) |
| 评分默认值 | `batchScoreForm default=5`, `singleScoreForm default=0` | 原型无 | | 批量开通截止时间 picker | 原型批量开通是直接按钮, Vue 加截止时间 datetime picker (一致于批量评分设计) |
| 批量开通截止时间字段 | `activateDeadline` | 原型批量开通仅"确认"无时间字段,实装加了截止时间 | | 评价得分过滤后端生效 | 原型前端有筛选框, Vue 加 selectList 后端过滤 (88df386) |
| isSettled 筛选 | line 26-30 | 原型无"是否结算"筛选项 |
### 8.2 原型有但实现缺失 ### 8.2 原型有但实现缺失
| 原型 | 实现状态 | | 项 | 原型位置 | 实现 | 严重度 |
|---|---| |---|---|---|---|
| 导出 (项目) | 🐛 **stub**`exportProjects``ElMessage.info('开发中')` | | 顶部"导出"按钮 | `project-manage.html:135` | `Projects.vue:361``ElMessage.info('开发中')` | **P2** stub |
| 项目评价导出 | 🐛 **stub**`exportEval``ElMessage.info('开发中')` | | 顶部"项目评价导出"按钮 | `project-manage.html:136` | `Projects.vue:362``ElMessage.info('开发中')` | **P2** stub |
| 已报名专家信息导出 | ✅ **实装**`exportExperts` POST `executionIntent/export` | | 顶部"已报名专家信息导出"按钮 | `project-manage.html:137` | 已挪到行操作 dropdown (`exportExperts`), ✅ 实装 | — |
| `managerScore` 评价得分筛选 | ✅ **后端已生效** — selectList 加 `<if test="managerScore != null">and manager_score = #{managerScore}</if>` (88df386) |
### 8.3 文案/标签差异 ### 8.3 文字 / 标签差异
| 原型 | 实现 | 差异 | | 原型 | 实现 | 差异 |
|---|---|---| |---|---|---|
| 评分标准输入框 placeholder "1~5" | `el-input-number :min=0 :max=5 :step=1` | 实现用数字输入控件(更好) | | 分配 | 项目分配 | Vue 改名加 "项目" 前缀, 更明确 (操作对象是项目, 不是 sponsor) |
| "您确定要开通吗?" 弹框 | 仅"确定/取消",**无截止时间字段** (单条版) | 原型单条版也无,批量版实现加了 | | 评分单元格 | 仅数字显示 | Vue 数字显示 + 链接点击打开单条评分 (更省操作) |
| 表格 `min-width=2900px` 横滚 | `el-table` 自动布局 | 实现用 `fixed="right"` 操作列,列宽合理 | | 状态 tag | `.status-pending` / `.status-done` / `.status-activated` | Vue 用 el-tag success/warning, 等价 |
| 状态颜色: 蓝色 status-activated (已开通) | ❌ 实现**无 "已开通" 状态列**,只有"已结题/未结题"列 | 实现走 `end_time` 而非 `is_finished`,前端没标"已开通" tag |
### 8.4 总结 ### 8.4 总结
- **整体方向**: 实现比原型更完整 (评分明细反显、批量开通截止时间、支持方/执行方下拉),但丢了 3 个导出按钮 (2 个 stub,1 个已报名专家实装) 整体方向:**扩展** > **缺失**。多选筛选、项目分配 dialog、批量开通 picker 都是增强, 仅 2 个导出按钮 stub 待实装
- **设计偏离**: 把 `is_finished` 复用为"开通/结题"两种语义 (改值而非新增字段),导致前端状态显示混乱 (已结题是 tag,已开通没 tag)
--- ---
## 9. 表字段冗余 / 设计问题 ## 9. 表字段冗余 / 设计问题
### 9.1 实体 vs 表不一致 (详见 §6.3) ### 9.1 派生 vs 持久化字段
- **已修**: projectId String → Long (见 §修复记录) - `sponsorOrgName` / `execOrgNames` / `leadUserName` / `createUserName` / `assignedSessions` / `assignedAmount` 全部是**派生字段**,不入库,只在 SQL JOIN 时填充,前端可读但不应让前端 PUT 回去
- **已修**: startTime/endTime String → Date (见 §修复记录) - mapper 的 `updateByPrimaryKey` 已经用 `<if test="...">` 防御,不会回写 — OK
### 9.2 JOIN 字段 vs 持久化字段混淆 ### 9.2 唯一约束语义检查
- `sponsorOrgName` / `execOrgNames` / `leadUserName` / `assignedSessions` / `assignedAmount` 全部是**派生字段**,不入库,只在 SQL JOIN 时填充,前端可读但不应让前端 PUT 回去
- 当前 mapper 的 `updateByPrimaryKey` 已经用 `<if test="...">` 防御,不会回写 — OK
### 9.3 唯一约束语义检查
- ✅ biz_project: uk_project_no — 项目编号唯一,合理 - ✅ biz_project: uk_project_no — 项目编号唯一,合理
- ✅ biz_project_rating: uk_rating (project_id, rater_id, rater_role) — 一人评一次,合理 - ✅ biz_project_rating: uk_rating (project_id, rater_id, rater_role) — 一人评一次,合理
- ✅ biz_project_assign: uk_project_unit (project_id, execution_unit_id) — 同一项目同一执行方只能分配一次 - ✅ 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) ### 9.5 索引分析与 EXPLAIN (2026-08-20)
数据规模: biz_project 12 行 / biz_org 33 行 / biz_project_assign 12 行 / sys_user 47 行 (测试数据) 数据规模: biz_project 12 行 / biz_org 33 行 / biz_project_assign 12 行 / biz_project_rating 22 行 / sys_user 47 行 / biz_person 29 行 (测试数据)
| # | 查询 | 命中索引 | 备注 | | # | 查询 | 命中索引 | 备注 |
|---|---|---|---| |---|---|---|---|
@@ -400,17 +426,10 @@ ALTER TABLE biz_project_assign ADD INDEX idx_assign_exec_user_project (exec_user
评估: 强制 hint 后确实变 `Backward index scan` 无 filesort, 评估: 强制 hint 后确实变 `Backward index scan` 无 filesort,
但优化器在 33 行数据下仍选 `idx_org_type_status` (2 列更小, 估算成本更低)。 但优化器在 33 行数据下仍选 `idx_org_type_status` (2 列更小, 估算成本更低)。
决策: 数据量小不需要, **DROP**, 等生产数据增长到 1k+ 行再 EVAL。 决策: 数据量小不需要, **DROP**, 等生产数据增长到 1k+ 行再 EVAL。
- `biz_org (org_type, status, is_published)` 等 — 无明显 QPS 压力场景, 不加。
**过时注释修正** (2026-08-20): **过时注释修正** (2026-08-20):
- `BizOrgMapper.xml` 旧注释写 "走 idx_org_user_type_name 索引", EXPLAIN 实际命中 `idx_org_type_status`, 已修正为实测命中索引 + Using where 描述 - `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` 字段 — 用现有字段表达多语义,容易跟"结题"冲突,设计可商榷
--- ---
@@ -418,61 +437,59 @@ ALTER TABLE biz_project_assign ADD INDEX idx_assign_exec_user_project (exec_user
| # | 问题 | 文件:行 | 修复建议 | 严重度 | | # | 问题 | 文件:行 | 修复建议 | 严重度 |
|---|---|---|---|---| |---|---|---|---|---|
| 3 | ~~`managerScore` 筛选前端有,后端 selectList 不读~~ | ~~`BizProjectMapper.xml:141-183` (selectList)~~ | ✅ 已修 (88df386,见 §修复记录) | — | | 1 | ~~`managerScore` 筛选前端有,后端 selectList 不读~~ | ~~`BizProjectMapper.xml:141-183` (selectList)~~ | ✅ 已修 (88df386,见 §修复记录) | — |
| 4 | 下拉菜单 `divided` 冗余 (activate/delAnn 双分隔线) | `Projects.vue:100-101` | 删 `activate``divided` | **P1** | | 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`~~ | ✅ 已修 (见 §修复记录) | — | | 5 | ~~生产代码残留 console.info/error (4 处)~~ | ~~`Projects.vue:299, 302, 306, 875`~~ | ✅ 已修 (见 §修复记录) | — |
| 6 | 评分默认值不一致 | `Projects.vue:501 vs 513` | 统一为 0 或 5 | **P2** | | 6 | ~~日期选择器 `value-format="YYYY-MM-DD HH:mm:ss"` 跟 `type="date"` 不匹配~~ | ~~`Projects.vue:17, 19`~~ | ✅ 已修 (见 §修复记录) | — |
| 7 | ~~`batchScoreRows` 死代码~~ | ~~`Projects.vue:501`~~ | ✅ 已修 (见 §修复记录) | — | | 7 | ~~`batchScoreRows` 死代码~~ | ~~`Projects.vue:501`~~ | ✅ 已修 (见 §修复记录) | — |
| 8 | `isSettled` 筛选但表格无列显示 | `Projects.vue:26-30` vs §4.2 表 | 加列 或 删筛选 | **P2** | | 8 | 下拉菜单 `divided` 冗余 (activate/delAnn 双分隔线) | `Projects.vue:132-133` | 删 `activate``divided` | **P1** |
| 9 | ~~日期选择器 `value-format="YYYY-MM-DD HH:mm:ss"` 跟 `type="date"` 不匹配~~ | ~~`Projects.vue:17, 19`~~ | ✅ 已修 (见 §修复记录) | — | | 9 | 评分默认值不一致 | `Projects.vue:512 vs 524` | 统一为 0 或 5 (批量=5, 单行=0) | **P2** |
| 10 | 重复 score table HTML (批量评分 + 单条评分) | `Projects.vue:201-213, 257-269` | 抽公共组件 | **P2** | | 10 | `isSettled` 筛选但表格无列显示 | `Projects.vue:58-62` vs §4.2 表 | 加列 或 删筛选 | **P2** |
| 11 | 2 个导出按钮是 stub (`exportProjects` / `exportEval`) | `Projects.vue:347-348` | 实装 或 删按钮 | **P2** | | 11 | 重复 score table HTML (批量评分 + 单条评分) | `Projects.vue:217-229, 274-285` | 抽公共组件 | **P2** |
| 12 | ~~biz_project 缺索引 `sponsor_admin_user_id`~~ | ~~`DB`~~ | ✅ 已修 (见 §修复记录) | — | | 12 | 2 个导出按钮是 stub (`exportProjects` / `exportEval`) | `Projects.vue:361-362` | 实装 或 删按钮 (原型有) | **P2** |
| 13 | ~~biz_project 缺索引 `start_time`, `end_time` (范围查询)~~ | ~~`DB`~~ | ✅ 已修 (见 §修复记录) | — | | 13 | `is_finished``is_settled` 状态值约定不一致 (0/1 vs Y/N) | `BizProject.java` + mapper + 前端 select | 统一约定 (全 0/1 或全 Y/N) | **P3** |
| 14 | ~~biz_project_assign 缺 UNIQUE (project_id, execution_unit_id)~~ | ~~`DB`~~ | ✅ 已修 (见 §修复记录) | — | | 14 | selectList / selectExecutorList 有 4 个相关子查询 | `BizProjectMapper.xml:50-53, 118-121` | 数据小可接受, 记录为 P3 待优化 | **P3** |
| 15 | `is_finished``is_settled` 状态值约定不一致 (0/1 vs Y/N) | `BizProject.java:65-68` + mapper | 统一约定 (要么全 0/1 要么全 Y/N) | **P3** | | 15 | `.score-row` CSS 死样式 | `Projects.vue:965-967` | 删 | **P3** |
| 16 | selectList / selectExecutorList 有 4 个相关子查询 | `BizProjectMapper.xml:46-50, 110-124` | 数据小可接受,但应记录为 P3 待优化 | **P3** | | 16 | `sponsorRating/sponsorQ1-4/sponsorRemark` 6 个字段实体有 DDL 无 | `BizProject.java:131-136` + 字段映射 | 删实体字段 或 补 DDL (业务走 sponsor_rating 表) | **P3** |
| 17 | `.score-row` CSS 死样式 | `Projects.vue:926-928` | 删 | **P3** |
--- ---
## 11. 引用清单 ## 11. 引用清单
### 前端 ### 前端
- `ry-vue3/src/views/manager/Projects.vue`主列表(881 行) - `ry-vue3/src/layout/AdminLayout.vue:96`manager 菜单定义
- `ry-vue3/src/views/manager/ProjectsNew.vue` — 新建/编辑独立页 - `ry-vue3/src/router/index.js:60` — 路由 `manager-projects`
- `ry-vue3/src/views/manager/ManagerProjectDetail.vue`详情独立页 - `ry-vue3/src/views/manager/Projects.vue`本审查对象 (920 行)
- `ry-vue3/src/layout/AdminLayout.vue:98`菜单项定义 - `ry-vue3/src/views/manager/ProjectsNew.vue`新建/编辑项目独立页
- `ry-vue3/src/router/index.js:65-68` — 路由 (含 /new, /edit/:id, /detail/:id) - `ry-vue3/src/api/business/project.js``bizList/bizAdd/bizUpdate/bizDelete`
- `ry-vue3/src/api/public.js``bizList/bizAdd/bizUpdate/bizDelete` 通用 CRUD - `ry-vue3/src/api/system.js``listSponsorOrgs/listExecutorOrgs`
- `ry-vue3/src/api/system.js``listExecutorOrgs/listSponsorOrgs` 项目分配用 - `ry-vue3/src/utils/request.js` — axios 拦截器 (TableDataInfo 归一化)
- `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/controller/BizProjectController.java:33``@RequestMapping("/business/project")` (16 endpoints)
- `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/service/BizProjectService.java`接口
- `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProjectAssign.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/BizProjectRating.java`评分实体 - `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProject.java` — 实体 (含 8 个 `@Excel` 导出字段)
- `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizProjectRatingController.java` — 评分 CRUD (单独 controller) - `ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectMapper.xml` — 4 个查询: selectByPrimaryKey, selectSponsorList, selectExecutorList, selectList (主)
- `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectAssignServiceImpl.java` — 分配 service (分配时反查 execution_unit_id) - `ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectAssignMapper.xml` — 分配子查询
- `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectRatingServiceImpl.java` — 评分 upsert service - `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 维度评分
### Mapper XML ### DB
- `ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectMapper.xml` — 项目主 mapper (301 行, 4 个 select + insert + update + 2 个 delete) - `biz_project` (主表, 36 列)
- `ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectAssignMapper.xml` — 分配 mapper (106 行) - `biz_org` (机构表, 16 列, sponsor/executor 共用)
- `ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectRatingMapper.xml` — 评分 mapper (含 `upsertRating` 三元组 ON DUPLICATE KEY) - `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-druid.yml:9-10` — DB 连接 (root / cu2oh2co3) - `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/manager.html:206`项目管理菜单
- `proto/html/components/project-manage.html` — 项目管理原型 (676 行,含 5 个 modal) - `proto/html/components/project-manage.html` — 项目管理主页 (15 行 × 19 列 + 8 行内按钮)
- `proto/html/components/project-assign.html` — 项目分配原型 (未深读,但 dialog 实装与之有偏差) - `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 索引)
+422
View File
@@ -0,0 +1,422 @@
## 0. 4 跳定位 (铁律: 不按名字猜)
| 跳 | 文件:行 | 结果 |
|---|---|---|
| ① 角色菜单 | `ry-vue3/src/layout/AdminLayout.vue:96` | `path: '/manager/projects', title: '项目管理'` (子入口, 无独立菜单) |
| ② 路由 | `ry-vue3/src/router/index.js:64` | `name: 'manager-projects-assign'`, component = `ManagerProjectsAssign.vue` ✓ |
| ③ 组件 | `ry-vue3/src/views/manager/ManagerProjectsAssign.vue` | 441 行, **本审查对象** |
| ④ 原型 | `proto/html/components/project-assign.html` (1.html 同源备份) | 项目分配独立页原型 |
> **调用路径**: `manager/Projects.vue` 行 dropdown `assign` 命令或 toolbar "项目分配" 按钮 → `router.push({path:'/manager/projects/assign', query:{projectId|projectIds}})` (`Projects.vue:274-280, 465-480`)。
> 单一路由 + query 区分单条 (`?projectId=X`) / 批量 (`?projectIds=1,2,3`), 用 `batchMode = !!route.query.projectIds` 切换 UI (`ManagerProjectsAssign.vue:113-115`)。
---
## 1. 主要功能 + 可见性
### 1.1 主要功能
| 功能 | 前端入口 | 后端接口 | 涉及表 |
|---|---|---|---|
| 加载项目基本信息 (单条) | `loadSingleProject` (`ManagerProjectsAssign.vue:257-275`) | `GET /business/project/{id}``BizProjectController.getInfo``bizProjectService.getById` | biz_project |
| 加载项目基本信息 (批量) | `loadBatchProjects` (L277-309) | 同上, **N 次**串行调用 (无批量接口) | biz_project × N |
| 加载已分配执行方 | `loadAssigns` (L200-222) | `GET /business/project/{id}/assigns``BizProjectController.getAssigns``bizProjectAssignService.selectByProjectId` | biz_project_assign |
| 支持方下拉选项 | `searchSponsorOrgs` (L172-191) | `GET /business/org/sponsorOptions?orgName=&userId=``BizOrgController.sponsorOptions``bizOrgService.selectSponsorOrgOptions` | biz_org (org_type='sponsor') + sys_user |
| 执行方下拉选项 (行内) | `searchExecutors` (L227-248) | `GET /business/org/executorOptions?orgName=&userId=``BizOrgController.executorOptions``bizOrgService.selectExecutorOrgOptions` | biz_org (org_type='executor') + sys_user (MAIN only) |
| 保存支持方 + 截止天数 | `submitAssign` 中 PUT (L365-374, 388-393) | `PUT /business/project``bizProjectService.updateByPrimaryKey` | biz_project.sponsor_admin_user_id / sponsor_admin_user_name / submit_deadline_days |
| 保存执行方分配 (单条) | `submitAssign` 中 POST (L394-399) | `POST /business/project/{id}/assigns``BizProjectController.saveAssigns` (deleteByProjectId 后逐条 insert) | biz_project_assign |
| 保存执行方分配 (批量) | `submitAssign` 中循环 (L366-386) | 同上 × N, 错单跳过收集到 `errs` | biz_project_assign × N |
### 1.2 哪些人能看到本页面
| 层 | 来源 | 校验字段 |
|---|---|---|
| 前端菜单 | `AdminLayout.vue:96` (`/manager/projects`) | 无 `requireMain`, 主/子账号均可见 (manager 角色没有子账号业务) |
| 路由守卫 | `router/index.js:64` | `meta.role` 无显式, 走全局 `permission.js` 校验登录态 |
| 后端 SQL | `BizProjectController` `@Autowired` service | **无 role 过滤**, 任何登录角色都可 hit 接口 — 跟前端 "manager 全局可见" 语义一致 |
**其他角色对应功能**:
- sponsor / executor / doctor 都不进本页面 (他们有自己的项目页, 走 `sponsorList` / `executorList` / `myProjects` 接口)
- admin 角色: 路由表 L28 `admin-projects` 也指向 `manager/Projects.vue`, 但 admin 端 Projects.vue 走同样的 `selectList`, 走本子页面前无 role 校验 (admin 也可触发分配) — 业务上 OK
**数据隔离**: manager 是无界角色, 可分配任何项目; 分配动作**无 owner 校验**, 但写入路径 (`PUT /business/project` + `POST /business/project/{id}/assigns`) 不限制调用者, 任何已登录用户均可触发 — 风险点见 §10 #3
---
## 2. 筛选项 (filter-form)
> 本页**无传统筛选项**(单条模式无 filter, 批量模式也无筛选), 只有表单字段。下面对应"el-form-item":
| UI label | 控件 | v-model | 后端 SQL | 命中表.字段 | 选项来源 | 原型对照 |
|---|---|---|---|---|---|---|
| 支持方 | `el-select` (single+remote+filterable) | `assignForm.sponsorAdminUserId` | `selectSponsorOrgOptions` (`BizOrgMapper.xml:111-123`) `WHERE o.org_type='sponsor' AND u.del_flag='0' AND [org_name LIKE] [user_id = ?] LIMIT 5` | biz_org.user_id (MAIN) + biz_org.org_name | `/business/org/sponsorOptions` (LIMIT 5) | ✅ 原型第 2 区块 |
| 执行方 (行内) | `el-select` (row 内 remote+filterable) | `row.execUserId` | `selectExecutorOrgOptions` (`BizOrgMapper.xml:135-148`) `WHERE o.org_type='executor' AND u.del_flag='0' AND u.parent_user_id IS NULL [org_name LIKE] [user_id = ?] LIMIT 5` | biz_org.user_id (MAIN only) + biz_org.org_name | `/business/org/executorOptions` (LIMIT 5, 限定 MAIN) | ✅ 原型第 3 区块 |
| 场数 (行内) | `el-input-number` | `row.sessions` | 直接 `biz_project_assign.sessions` | biz_project_assign.sessions | — | ✅ 原型 `场数:` |
| 金额 (行内) | `el-input-number` `:precision="2"` | `row.amount` | 直接 `biz_project_assign.amount` | biz_project_assign.amount (decimal 15,2) | — | ✅ 原型 `总金额:` |
| 提交截止(天) | `el-input-number` | `assignForm.deadlineDays` | `updateByPrimaryKey` L268 `<if>submit_deadline_days = #{...}` | biz_project.submit_deadline_days | — | ⚠️ 原型 max 100, **Vue 无校验** (见 §10 #5) |
> **类型注意**: biz_project_assign.amount 类型 `decimal(15,2)`, Vue 用 el-input-number precision=2 ✅ 一致。
> **类型注意**: biz_project.submit_deadline_days 类型 `int`, Vue 用 el-input-number 无 min/max ✅ 类型对, **但未做 1~100 边界校验** (原型说"超过 100 报错", Vue 没做)。
---
## 3. 工具栏按钮 (toolbar)
| 按钮 | 功能 | 接口 | 涉及表 | 原型对照 |
|---|---|---|---|---|
| + 增加执行方 | `addExecRow` (L165-169) | (本地, 推 row 到 execRows) | — | ✅ 原型 `+ 增加执行方` |
| 删除 (行内) | `assignForm.execRows.splice($index, 1)` | (本地) | — | ✅ 原型 `删除` 链接 |
| 取消 | `confirmCancel` (L321-325) | ElMessageBox.confirm → `goBack` (router.push 回列表) | — | ✅ 原型 `取消` |
| 保存 | `submitAssign` (L328-406) | 见 §1.1 表 (PUT + POST) | biz_project + biz_project_assign | ✅ 原型 `保存` |
---
## 4. 表格 (el-table)
### 4.1 数据来源
本页面**不展示项目列表表格**, 只有"执行方分配行表"(el-table bound to `assignForm.execRows`) 和单条模式顶部 6 字段描述。
#### 4.1.1 顶部描述区 (单条模式 el-descriptions) 数据源
`GET /business/project/{id}``BizProjectController.getInfo``BizProjectServiceImpl.getById``BizProjectMapper.selectByPrimaryKey` (`BizProjectMapper.xml:46-49`)
```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_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
FROM biz_project p
WHERE project_id = #{projectId}
```
Vue 字段映射 (`ManagerProjectsAssign.vue:18-23`):
- `projectNo` ← biz_project.project_no ✅
- `projectName` ← biz_project.project_name ✅
- `projectForm` ← biz_project.project_form ✅
- `totalSessions` ← biz_project.total_sessions (int) ✅
- `totalAmount` ← biz_project.total_amount (decimal 15,2) ✅
- `manageFee` ← biz_project.manage_fee (decimal 15,2) ✅
#### 4.1.2 执行方行表数据源
`GET /business/project/{id}/assigns``BizProjectController.getAssigns``BizProjectAssignMapper.selectByProjectId` (`BizProjectAssignMapper.xml:31-35`)
```sql
SELECT assign_id, project_id, execution_unit_id, exec_user_id,
sessions, amount, remark, status,
create_by, create_time, update_by, update_time
FROM biz_project_assign
WHERE project_id = #{projectId} AND status = '0'
ORDER BY assign_id ASC
```
EXPLAIN 实测: `key=uk_project_unit (project_id, execution_unit_id)`, `ref=const`, `rows=1`, `Extra=Using where; Using filesort` ✅ 走唯一索引。
### 4.2 列映射
| 列名 | `row.*` | 表.字段 | 原型对照 |
|---|---|---|---|
| # | (type="index") | — | ✅ 原型 `1. 2. 3.` 序号 |
| 执行方名称 | `row.execUserId` (el-select label=orgName) | biz_org.org_name (JOIN, label); biz_project_assign.exec_user_id (value) | ✅ 原型 `select` |
| 场次 | `row.sessions` | biz_project_assign.sessions (int) | ✅ 原型 `场数:` |
| 金额 | `row.amount` | biz_project_assign.amount (decimal 15,2) | ✅ 原型 `总金额:` |
| 操作 | 删除按钮 | — | ✅ 原型 `删除` 链接 |
> **注**: 原型行内显示单位名称 (`org_name`), 但 Vue 把 el-select 的 `:value=u.userId :label=u.orgName` — label 确实显示公司名 ✅。但**不显示执行方账户名** (u.userName), 原型也没要求, 业务上可接受。
---
## 5. 行内操作按钮 (op-cell)
无传统行内操作按钮 (本页是分配表单, 不是列表)。删除按钮在 §3 工具栏行内列出。
**特殊按钮**:
- `+ 增加执行方` (L85) → `addExecRow()` — 不调接口, 只在 `assignForm.execRows` 推入新空行, 然后 `searchExecutors('', row)` 异步拉取执行方选项 (避免新行 el-select 没数据)
- 行内 `删除` (L80) → `splice` — 不调接口, 跟 `submitAssign` 时整体 deleteByProjectId + 全部 insert 一并提交
---
## 6. Java Entity ↔ 数据库表 一致性
### 6.1 biz_project_assign DDL (实测 2026-08-20)
```sql
CREATE TABLE biz_project_assign (
assign_id bigint NOT NULL AUTO_INCREMENT COMMENT '分配ID',
project_id bigint NOT NULL COMMENT '项目ID',
execution_unit_id bigint NOT NULL COMMENT '执行单位ID',
sessions int DEFAULT '0' COMMENT '分配场数',
amount decimal(15,2) DEFAULT '0.00' COMMENT '总金额',
exec_user_id bigint DEFAULT NULL COMMENT '执行方用户ID (sys_user.user_id)',
remark varchar(500) DEFAULT NULL COMMENT '备注',
status char(1) DEFAULT '0' COMMENT '状态 0正常 1已撤销',
create_by varchar(64) DEFAULT '' COMMENT '创建者',
create_time datetime DEFAULT NULL COMMENT '创建时间',
update_by varchar(64) DEFAULT '' COMMENT '更新者',
update_time datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (assign_id),
UNIQUE KEY uk_project_unit (project_id, execution_unit_id),
KEY idx_assign_project (project_id),
KEY idx_assign_unit (execution_unit_id),
KEY idx_assign_exec_user (exec_user_id),
KEY idx_assign_exec_user_project (exec_user_id, project_id) -- 2026-08-20 加, 见 manager_projects.md §9.5
) ENGINE=InnoDB AUTO_INCREMENT=1463963329224705 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='项目-执行单位分配';
```
数据规模: **12 行** (实测 `SELECT COUNT(*)`)。
### 6.2 biz_org DDL (实测 2026-08-20)
```sql
CREATE TABLE biz_org (
org_id bigint NOT NULL AUTO_INCREMENT,
org_name varchar(200) NOT NULL,
org_type varchar(20) NOT NULL DEFAULT 'sponsor' COMMENT 'sponsor赞助方/execution执行方',
business_nature varchar(20) DEFAULT NULL COMMENT '企业性质 私营/国营/中外合资/外资/其他',
address varchar(500) DEFAULT NULL COMMENT '单位地址',
tax_no varchar(50) DEFAULT NULL COMMENT '税号/统一社会信用代码',
contact_name varchar(50) DEFAULT NULL COMMENT '联系人',
contact_phone varchar(20) DEFAULT NULL COMMENT '联系电话',
intent_count int DEFAULT '0' COMMENT '意向项目数',
status varchar(20) DEFAULT '0' COMMENT '合作状态 合作中/待签约/已停用',
create_by varchar(64) DEFAULT '' COMMENT '创建者',
create_time datetime DEFAULT NULL,
update_by varchar(64) DEFAULT '' COMMENT '更新者',
update_time datetime DEFAULT NULL,
user_id bigint DEFAULT NULL COMMENT '主账号 sys_user.user_id (注册时自动关联, 一用户一类型一公司)',
PRIMARY KEY (org_id),
UNIQUE KEY uk_org_user_type (user_id, org_type),
KEY idx_org_type_status (org_type, status),
KEY idx_org_type_name (org_type, org_name),
KEY idx_org_user_type_name (user_id, org_type, org_name)
) ENGINE=InnoDB AUTO_INCREMENT=41 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='支持单位';
```
数据规模: **33 行** (实测)。COMMENT"支持单位" 实为 sponsor+executor 共用表, 见 §9.1。
### 6.3 biz_project (分配写入字段) DDL (实测)
```sql
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 '赞助方负责人用户名(冗余)'
total_sessions int DEFAULT NULL COMMENT '总场次/总期数'
total_amount decimal(15,2) DEFAULT '0.00' COMMENT '总金额'
manage_fee decimal(15,2) DEFAULT '0.00' COMMENT '管理费及税金'
submit_deadline_days int DEFAULT '0' COMMENT '提交材料截止天数'
```
### 6.4 Entity ↔ DB 字段对照
#### BizProjectAssign
| 实体字段 | 中文列名 (DDL COMMENT) | 类型 | 表字段 | 类型 | 一致? |
|---|---|---|---|---|---|
| assignId | 分配ID | String | assign_id | bigint | ⚠️ Java String vs DB bigint (走 SnowflakeId → Service 层 `injectIfEmpty` 转 String, 经全局 BaseEntity 反序列化兼容) |
| projectId | 项目ID | Long | project_id | bigint | ✅ |
| executionUnitId | 执行单位ID | Long | execution_unit_id | bigint | ✅ |
| execUserId | 执行方用户ID | Long | exec_user_id | bigint | ✅ |
| sessions | 分配场数 | Integer | sessions | int | ✅ |
| amount | 总金额 | BigDecimal | amount | decimal(15,2) | ✅ |
| remark | 备注 | String | remark | varchar(500) | ✅ |
| status | 状态 | String | status | char(1) | ✅ |
| createBy / updateBy | 创建者/更新者 | String | create_by / update_by | varchar(64) | ✅ |
| createTime / updateTime | 创建时间/更新时间 | Date | create_time / update_time | datetime | ✅ |
#### BizProject (assign 涉及字段)
| 实体字段 | 中文列名 | 类型 | 表字段 | 类型 | 一致? |
|---|---|---|---|---|---|
| sponsorAdminUserId | 赞助方负责人 | Long | sponsor_admin_user_id | bigint | ✅ |
| sponsorAdminUserName | 赞助方负责人用户名(冗余) | String | sponsor_admin_user_name | varchar(200) | ✅ |
| totalSessions | 总场次/总期数 | Long | total_sessions | int | ✅ (Java Long 兼容 SQL int) |
| totalAmount | 总金额 | BigDecimal | total_amount | decimal(15,2) | ✅ |
| manageFee | 管理费及税金 | BigDecimal | manage_fee | decimal(15,2) | ✅ |
| submitDeadlineDays | 提交材料截止天数 | Integer | submit_deadline_days | int | ✅ |
#### BizOrg (options 字段)
| 实体字段 | DB 字段 | 类型 | 一致? |
|---|---|---|---|
| orgId / userId / orgName / orgType | 同上 | Long / Long / String / String | ✅ |
| status | status | String (实际存 '0'/'1') vs DDL `varchar(20)` 注释说"合作中/待签约/已停用" | ⚠️ **值约定混乱**: 注释允许多字符串, 但 `BizOrgController.toggleStatus` (L99-107) 严格限定 '0'/'1', BizProject 控制台代码无字符串判定, BizOrgController `/toggleStatus` 用 Map 接 body 防御误传 |
### 6.5 修订
> v1 误判: biz_project_assign.assignId 类型 String vs bigint — 实际 `SnowflakeId.injectIfEmpty` 在 Service 层处理, mapper.xml L55 `<if test="assignId != null and assignId != ''">assign_id,</if>` 用 String 也能写 bigint 列 (MyBatis 隐式转换), DB 是 AUTO_INCREMENT 实际由 SQL 自增, Java 端通常拿不到 insert id。**实际写入靠 AUTO_INCREMENT**, String 字段只是反序列化壳, 无 bug。
---
## 7. 索引检查
### 7.1 biz_project_assign
| Key | 列 | 用途覆盖 |
|---|---|---|
| PRIMARY | `assign_id` | ✅ |
| uk_project_unit | `(project_id, execution_unit_id)` | ✅ `selectByProjectId` EXPLAIN 实际命中 (实测) |
| idx_assign_project | `project_id` | ⚠️ **冗余**: `uk_project_unit` 前缀是 project_id, 单列 `project_id = ?` 优化器优先选 uk (实测 EXPLAIN 命中 uk_project_unit) |
| idx_assign_unit | `execution_unit_id` | ⚠️ 当前页面未用, 待清理 (或保留给未来按执行方查分配) |
| idx_assign_exec_user | `exec_user_id` | ⚠️ **冗余**: `idx_assign_exec_user_project` 前缀覆盖, 优化器在新索引后偏好复合 |
| **idx_assign_exec_user_project** | `(exec_user_id, project_id)` | ✅ projects `selectList` EXISTS+IN 复合 covering (be1c973, 见 `manager_projects.md` §9.5) |
**索引冗余**: `idx_assign_project``idx_assign_exec_user``uk_project_unit` / `idx_assign_exec_user_project` 前缀重叠, 数据量小(<1k) 优化器可能优先小索引, 但**索引维护成本**是真实的。建议保留 (代码注释明确, 不影响功能)。
### 7.2 biz_org
| Key | 列 | 用途覆盖 |
|---|---|---|
| PRIMARY | `org_id` | ✅ |
| uk_org_user_type | `(user_id, org_type)` | ✅ options JOIN `u.user_id = o.user_id` (eq_ref) + "一用户一类型一公司" 唯一约束 |
| idx_org_type_status | `(org_type, status)` | ✅ options `WHERE org_type = ?` EXPLAIN 实测命中 |
| idx_org_type_name | `(org_type, org_name)` | ✅ `org_name LIKE` 兜底 |
| idx_org_user_type_name | `(user_id, org_type, org_name)` | ⚠️ **冗余**: 3 列复合, EXPLAIN 实测优化器在小数据量选更小的 `idx_org_type_status` (2 列), 此索引未被命中 |
### 7.3 EXPLAIN 实测 (2026-08-20)
数据规模: biz_project_assign 12 / biz_org 33 / biz_project 12
| # | 查询 | 命中索引 | 备注 |
|---|---|---|---|
| Q1 | `selectSponsorOrgOptions(orgName='test')` | `idx_org_type_status` (ref const) + `PRIMARY` sys_user (eq_ref) | ✅ filesort (ORDER BY org_id DESC), 33 行内可接受 |
| Q2 | `selectExecutorOrgOptions(orgName='test')` | 同上 + `u.parent_user_id IS NULL` Using where | ✅ 同 Q1 |
| Q3 | `selectByProjectId(projectId=1)` | `uk_project_unit` (ref const) + filesort | ✅ 完美命中唯一索引 |
---
## 8. 与原型差异 (实现偏离)
### 8.1 实现新增 (原型没有)
| 项 | 说明 |
|---|---|
| 跳转子页面 (而非 dialog) | 原型是单页 (`project-assign.html`), Vue 独立路由 `ManagerProjectsAssign.vue`, 走 `router.push` 而非 dialog (`Projects.vue:150` 注释确认) |
| 实时校验栏 | 单条模式: 已分配场次/金额 vs 项目总场次/总金额实时对比, 超出红色高亮 + 警告 (`assign-sessions-bar` L38-46) |
| 批量分配模式 | 原型只有单项目, Vue 加 `?projectIds=1,2,3` 批量入口 (`Projects.vue:465-480`), 校验按各项目自身 (`ManagerProjectsAssign.vue:336-345`) |
| 已选项 label 保留 | options 加载后, 已选 userId 不在结果里时单独按 userId 拉回加首位 (`ManagerProjectsAssign.vue:181-185, 237-241`), 防止切换搜索条件后 el-select label 变 ID |
| row 私有 _loading / _options / _timer | 每行独立 loading 状态, 防异步竞态 (L150, 228-247) |
### 8.2 原型有但实现缺失
| 项 | 原型位置 | 实现 | 严重度 |
|---|---|---|---|
| 顶部面包屑 "项目管理 > 项目分配" | `project-assign.html:354-356` | Vue: `首页 / 项目管理 / <span class="current">项目分配</span>` (L3-6), ✅ 一致 (但首页是 Vue 默认面包屑) |
| 项目信息 6 字段 (编号/名称/形式/总场次/总金额/管理费及税金) | `project-assign.html:367-392` | el-descriptions 6 字段 ✅ 实装 (单条模式) | — |
| 支持方提示文字 "*支持单位只有一个, 支持模糊查找, 选择项" | `project-assign.html:401` | ⚠️ **Vue 未加红色提示** (L27-33 el-form-item 没 hint) | **P3** |
| 执行方提示文字 (4 段) | `project-assign.html:472-483` | ⚠️ **Vue 完全省略** 4 段红色提示 (含"可用金额=总金额×(1-管理费税费)-累计劳务-累计会务" 公式) | **P2** |
| 截止时间提示 (3 段) | `project-assign.html:493-495` | ⚠️ **Vue 完全省略** 3 段红色提示 (含"天数最大不能超过 100") | **P2** |
| 截止天数 1~100 校验 | `project-assign.html:646-650` `if (deadline && (deadline < 1 \|\| deadline > 100))` | ⚠️ **Vue 无校验** (L88-90 el-input-number `:min="0"` 缺 max) | **P1** |
| 合同文件展示 (支持合同/执行合同, OA 同步) | `project-assign.html:501-515` | ⚠️ **Vue 完全省略** 合同区 (`support_contract_url` / `execute_contract_url` 已 SELECT 到, 但页面没用) | **P1** |
| 总金额超额服务端校验 | `project-assign.html:642-645` `totalExecAmount > project.amount` | ⚠️ **Vue 只在客户端 sum 校验** (L139-148, 335-355), 服务端没二次校验 | **P2** (客户端已拦, 风险低) |
| 执行方单位 = 1 时"删除"禁用 | `project-assign.html:582-588` `disabled = rows.length === 1 && i === 0` | ⚠️ **Vue 永远可点** (L80 链接 `el-button link type="danger"`), 可一键删到空数组, 提交时 `execRows.length === 0` 会被 `if (!valid.length)` 拦住, 但 UI 不友好 | **P3** |
### 8.3 文字 / 标签差异
| 原型 | 实现 | 差异 |
|---|---|---|
| 项目分配 | 项目分配 | ✅ 完全一致 (标题、面包屑、表单 label) |
| 支持单位 | 支持方 | ⚠️ Vue 把"支持单位"改成"支持方" (`ManagerProjectsAssign.vue:27`), 跟其它页(支持单位管理/支持单位列表)不一致, 业务方混用 |
| 执行单位 | 执行方 | ⚠️ Vue 把"执行单位"改成"执行方" (L35 divider 文字 "执行方分配") |
| 提交材料截止时间 | 提交截止(天) | ⚠️ Vue 简化为"提交截止(天)", 失去"材料"和"时间"语义 |
| 总金额(元) | 总金额(元) (L22) | ✅ |
| 场数 | 场次 | ⚠️ Vue "场次", 原型"场数" — 同义, 但跟 `biz_project.total_sessions` (总场次/总期数) 中文对齐更顺 |
### 8.4 总结
整体方向: **业务核心对齐 + 扩展**, 但**视觉/提示/校验**比原型薄弱。P1 缺失 2 项 (合同区 + 截止天数校验), P2 缺失 4 项 (提示文案 + 服务端校验), P3 缺失 3 项 (文案微调 + 删除按钮禁用)。**建议优先级**: P1 #1 截止天数校验 → P1 #2 合同区 → P2 提示文案 → P3 文案统一。
---
## 9. 表字段冗余 / 设计问题
### 9.1 派生 vs 持久化字段
- `biz_project_assign.exec_user_id`: 持久化字段, 但**实际只用来反查 `biz_org``execution_unit_id`** (BizProjectAssignServiceImpl.insert L47-56), 然后 exec_user_id 也保留用于列表 JOIN — 冗余但有查询价值
- `biz_project.sponsor_admin_user_name`: 持久化 (冗余), 用于列表展示避免 JOIN, 设计 OK
### 9.2 唯一约束语义检查
- ✅ biz_project_assign: `uk_project_unit (project_id, execution_unit_id)` — 同一项目同一执行方只能分配一次, 合理 (跟"执行方总金额显示在执行方的项目列表"业务一致)
- ⚠️ 但 saveAssigns 是 `deleteByProjectId` + 逐条 `insert`, uk 约束会**在重新分配时**触发 (如果先 insert 再 delete 老数据就违反 uk)。当前实现**先 delete 再 insert**, 安全 ✅。
### 9.3 业务闭环检查
- ✅ 项目分配 → biz_project.sponsor_admin_user_id 更新 → sponsor 端列表可见 (`selectSponsorList` 按此过滤)
- ✅ 项目分配 → biz_project_assign 写入 → executor 端列表可见 (`selectExecutorList` 按 exec_user_id 过滤)
- ⚠️ **管理费及税金 (manage_fee) 在本页面只展示, 不参与任何计算**: 原型"可用金额=总金额×(1-管理费税费)-累计劳务-累计会务"公式**未实现** (§10 #4 P2)
- ⚠️ **biz_project.available_amount (可用金额) 字段**在原型公式中出现, 但本页面没读也没改, 是否由其他流程 (会议执行?) 维护需查 — 暂列 P3
### 9.4 路由安全性
- `manager-projects-assign` 路由 `meta.role` 无显式, 走全局登录态校验, 任何已登录角色都可访问 — 与 admin-projects 同款 (`router/index.js:28` admin-projects 也指向 manager/Projects.vue), 业务上 OK 但**没显式 `meta: { role: 'manager' }`** 是隐性约定
### 9.5 索引冗余总结
- biz_project_assign: `idx_assign_project` + `idx_assign_exec_user` 跟 uk_project_unit + idx_assign_exec_user_project 前缀重叠, 优化器在小数据量偏好小索引, 但 EXPLAIN 实测**都未被命中** (Q3 走 uk, Q1/Q2 是 SELECT biz_org), 跟 manager_projects.md §9.5 评估一致: **数据量小时保留无害, 生产数据增长后再 EVAL 是否 DROP**
---
## 10. 待修复列表 (按优先级)
| # | 问题 | 文件:行 | 修复建议 | 严重度 |
|---|---|---|---|---|
| 1 | 截止天数无上限校验 (原型 max=100) | `ManagerProjectsAssign.vue:88-90` | `<el-input-number :min="0" :max="100">` + on-change 校验 | **P1** |
| 2 | 合同文件区完全缺失 (支持合同/执行合同 + OA 同步) | `ManagerProjectsAssign.vue` 全文 | 加第 5 区块, 读 `currentProject.supportContractUrl / executeContractUrl`, 走 `Preview` 组件预览 (Projects.vue:146 已有 Preview 组件) | **P1** |
| 3 | 分配接口无 owner 校验, 任何登录角色可触发 PUT /business/project 改 sponsor | `BizProjectController.saveAssigns` (L170-182) + `updateByPrimaryKey` (L151-155) | 加 `@PreAuthorize("hasAnyRole('admin','manager')")` 或 service 层校验 `SecurityUtils.getUserId()` 在 manager role | **P1** |
| 4 | 管理费及税金 (manage_fee) 不参与可用金额计算 (原型公式未实现) | `ManagerProjectsAssign.vue` 实时校验栏 | `availableAmount = totalAmount * (1 - manageFee/totalAmount) - paidLabor - paidMeeting` 实时显示 (原型公式) | **P2** |
| 5 | 截止天数无下限校验 (原型要求 ≥1 自然日) | 同 #1 | `<el-input-number :min="1" :max="100">` | **P2** |
| 6 | 执行方超额校验只在客户端, 服务端无二次校验 | `BizProjectController.saveAssigns` | service 层 `if (sumAmount > project.totalAmount) throw` | **P2** |
| 7 | 原型 4 段提示文案 (可用金额公式 / 选择后业务闭环 / 截止天数规则) 全部省略 | `ManagerProjectsAssign.vue:27-91` | 加 `el-alert type="warning" :closable="false"``p.hint-text` 红字提示 | **P2** |
| 8 | "支持方" / "执行方" 跟其它页"支持单位" / "执行单位" 命名不一致 | `ManagerProjectsAssign.vue:27, 35` | 统一为"支持单位" / "执行单位" (跟 biz_org.org_type 注释一致) | **P3** |
| 9 | 执行方行删除按钮永远可点, 可删到空数组 | `ManagerProjectsAssign.vue:80` | 当 `execRows.length === 1` 时禁用第一个删除按钮 (跟原型一致) | **P3** |
| 10 | 批量模式 (单条 sponsor 多项目共用) 不允许每项目独立 sponsor | `ManagerProjectsAssign.vue:297-302, 365-374` | 加 "按各项目原 sponsor 填充" toggle, 或支持逐行 sponsor 覆盖 (低优先级, 业务需求未明) | **P3** |
| 11 | 加载项目时无 loading 态 (顶部 6 字段空白闪烁) | `ManagerProjectsAssign.vue:17-24` (el-descriptions 直接绑 currentProject) | 加 `v-loading="loading"` 或 skeleton | **P3** |
| 12 | 加载批量项目时, 任一失败静默跳过 (L294) | `ManagerProjectsAssign.vue:282-295` | `console.warn` 或弹窗提示"项目 X 加载失败已跳过" | **P3** |
| 13 | biz_org 索引 `idx_org_user_type_name``idx_org_type_name` 前缀重叠, EXPLAIN 实测未命中 | `biz_org` (DB) | 数据量小时保留无害, 生产数据增长后再 EVAL | **P3** |
| 14 | biz_project_assign 索引 `idx_assign_project``uk_project_unit` 前缀重叠 | `biz_project_assign` (DB) | 同 #13 | **P3** |
| 15 | biz_org.status 注释允许多字符串, 但 controller.toggleStatus 严格 '0'/'1' | `BizOrgController.toggleStatus` (L99-107) | 统一 status 字段约定 (删 DDL 注释, 或扩 enum) | **P3** |
| 16 | `manager-projects-assign` 路由 `meta.role` 无显式, 走全局登录态 | `router/index.js:64` | 加 `meta: { role: 'manager' }` 跟其它 manager 路由一致 | **P3** |
---
## 11. 引用清单
### 前端
- `ry-vue3/src/layout/AdminLayout.vue:96` — manager 菜单 (项目管理入口)
- `ry-vue3/src/router/index.js:64` — 路由 `manager-projects-assign`
- `ry-vue3/src/views/manager/ManagerProjectsAssign.vue` — 本审查对象 (441 行)
- `ry-vue3/src/views/manager/Projects.vue:274-280``doAssign` (单条入口)
- `ry-vue3/src/views/manager/Projects.vue:465-480``openAssign` (批量入口)
- `ry-vue3/src/api/public.js``bizGet/bizUpdate` (CRUD 包装)
- `ry-vue3/src/api/system.js:28-38``listSponsorOrgs/listExecutorOrgs` (下拉选项)
### 后端
- `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizProjectController.java:139-189` — 项目 getInfo / assigns GET/POST/DELETE
- `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizProjectService.java` + `BizProjectServiceImpl``getById/updateByPrimaryKey`
- `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProject.java` — 项目实体 (含 `sponsorAdminUserId / sponsorAdminUserName / submitDeadlineDays / manageFee / totalSessions / totalAmount`)
- `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizProjectMapper.java` + xml — 4 个 select + updateByPrimaryKey
- `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizOrgController.java:38-59``sponsorOptions/executorOptions`
- `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizOrgMapper.java:14-20` + xml L111-148 — `selectSponsorOrgOptions/selectExecutorOrgOptions`
- `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProjectAssign.java` — 分配实体 (8 字段)
- `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizProjectAssignMapper.java` + xml — 分配 CRUD
- `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectAssignServiceImpl.java:34-73` — insert 时反查 biz_org 写 execution_unit_id
### DB (实测 2026-08-20)
- `biz_project_assign` (12 行): 12 列, 5 索引 (PK + uk + 3 复合/单列)
- `biz_org` (33 行): 16 列, 4 索引 (PK + uk + 2 复合/单列)
- `biz_project` (12 行, 36 列): 含 sponsor_admin_user_id / sponsor_admin_user_name / total_sessions / total_amount / manage_fee / submit_deadline_days
### 配置
- `ry-api/ruoyi-admin/src/main/resources/application-druid.yml` — DB `127.0.0.1:3306/guoju0808/root/cu2oh2co3`
### 原型
- `proto/html/manager.html:206` — 项目管理菜单入口 (iframe 路由)
- `proto/html/components/project-assign.html` — 项目分配独立页原型 (主)
- `proto/html/components/project-assign-1.html` — 项目分配独立页原型 (备份, 同源)
### 相关 (非本审查, 引用以辅助)
- `_self/manager_projects.md` — 上游列表页审查报告 (本审查的调用方)
- `biz_project_sponsor_assign` (15 列) — **与本页面无关**: 该表存"赞助方分配监察员" (sponsor → monitor), 跟"项目分配支持方" (project → sponsor) 是不同业务, 本页面不写该表