refactor(biz/project): 修实体类型不一致 projectId String→Long + startTime/endTime String→Date

- BizProject.projectId String→Long (9 文件: domain / IBizProjectService / BizProjectServiceImpl /
  BizProjectMapper.java+xml / BizProjectController / BizExecutionIntentController /
  BizPublicityIntentController / BizPublicController). 删 SnowflakeId.injectIfEmpty
  (Long setter 不匹配反射 NoSuchMethodException, biz_project 用 AUTO_INCREMENT 安全)
- BizProject.startTime/endTime String→Date, 加 @JsonFormat yyyy-MM-dd HH:mm:ss (跟 BizMeeting 一致)
- Mapper XML 8 处 <if test='x != null and x != ""'> → <if test='x != null'> (Date 无 isEmpty)
- _self/manager_projects.md: §6.3 + §9.1 标已修, 加修复记录章节
This commit is contained in:
郭庆泰
2026-08-18 22:03:45 +08:00
parent 14968d46e5
commit 66068c6f57
10 changed files with 486 additions and 40 deletions
+442
View File
@@ -0,0 +1,442 @@
# 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" 双向兼容, 零前端改动。 |
---
## 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 | ✅ 是否结算过滤 |
| ❌ **缺** | sponsor_admin_user_id | ⚠️ `selectSponsorList` WHERE `sponsor_admin_user_id = ?` |
| ❌ **缺** | lead_user_id | ⚠️ 主表没用,但 JOIN sys_user 用 |
| ❌ **缺** | start_time / end_time | ⚠️ 范围查询 `>= ? AND <= ?` |
### 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 | ✅ |
| ❌ **缺** | UNIQUE (project_id, execution_unit_id) | ⚠️ **业务上** "一个项目 + 一个执行方" 应唯一,否则可能重复分配 |
### 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 表索引够用但**不够优化**(4 个 WHERE 列无索引),assign 表缺唯一约束
---
## 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` | 全删 | **P1** |
| 6 | 评分默认值不一致 | `Projects.vue:502 vs 514` | 统一为 0 或 5 | **P2** |
| 7 | `batchScoreRows` 死代码 | `Projects.vue:501` | 删 | **P2** |
| 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` | 改 `type="datetime"` 或 value-format 用 `YYYY-MM-DD` | **P1** |
| 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` | 加 `idx_sponsor_admin_user_id` | **P1** |
| 13 | biz_project 缺索引 `start_time`, `end_time` (范围查询) | `DB` | 加 `idx_start_time`, `idx_end_time` 或组合索引 | **P2** |
| 14 | biz_project_assign 缺 UNIQUE (project_id, execution_unit_id) | `DB` | 加唯一约束防重复分配 | **P1** |
| 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 索引)