Files
guoju0808/_self/manager_projects.md
T
郭庆泰 c3b6212478 docs(manager_projects): 更新修复记录 + 索引表 + §10 标已修 (P1/P2 5 项)
修复记录章节加 6 条 (2026-08-18):
- #7 batchScoreRows 死代码删除
- #5 4 处 console 调试删除
- #9 日期选择器 value-format 对齐 type
- #12 biz_project 加 idx_sponsor_admin_user_id
- #13 biz_project 加 idx_start_end_time 复合索引
- #14 biz_project_assign 加 uk_project_unit UNIQUE (executor 分配防重复)

§7 索引表更新:
- biz_project 7 个 (新增 idx_sponsor_admin_user_id + idx_start_end_time)
- biz_project_assign 5 个 (新增 uk_project_unit)

§10 待修复列表 5 项标 ~~删除线~~ (5/7/9/12/13/14)
2026-08-18 22:12:28 +08:00

448 lines
30 KiB
Markdown
Raw Blame History

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