Files
guoju0808/_self/manager_projects_detail.md
郭庆泰andClaude 99b9807270 fix(detail): 评分 4 维度改走 /business/project/rate + manager_score 同步回写
P0 #1 修复: 评分之前写到 biz_project (错表), 4 维度 (scoreQuality/Speed/Coordination/Compliance) 实体无字段, mapper 无 <if>, Spring Boot Jackson 默认丢弃未知字段 — 全部失效。

改动:
- 后端 BizProjectController.rate: 同步聚合该项目所有 compliance 评分的 4 维度总分之平均, 写回 biz_project.manager_score (1 位小数, HALF_UP)。复用现有 selectList, 不动 mapper xml。
- 前端 ManagerProjectDetail.submitScore: 改调 POST /business/project/rate, body 用后端字段名 (qualityScore/responseScore/cooperationScore/complianceScore), raterRole='compliance' 跟原型"角色:合规"对齐。加 totalScore<4 校验。
- 附: _self/manager_projects_detail.md 11 章审查报告 (含本修复)

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

540 lines
39 KiB
Markdown
Raw Permalink 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.
## 0. 4 跳定位 (铁律: 不按名字猜)
| 跳 | 文件:行 | 结果 |
|---|---|---|
| ① 角色菜单 | `ry-vue3/src/layout/AdminLayout.vue:96` | `path: '/manager/projects', title: '项目管理'` (父菜单, 无独立菜单) |
| ② 路由 | `ry-vue3/src/router/index.js:63` | `name: 'manager-projects-detail'`, component = `ManagerProjectDetail.vue` ✓ |
| ③ 组件 | `ry-vue3/src/views/manager/ManagerProjectDetail.vue` | **459 行** (本审查对象, 已 style-aligned) |
| ④ 原型 | `proto/html/components/project-detail-compliance.html` | **合规管理员视角的项目详情独立页原型** (236 行, 4 个 .card 卡片 + 9 字段项目信息) — *修正: 之前误判为"无原型", 实际是 components/ 根目录, 不在 manager/ 子目录* |
> **调用路径**: `manager/Projects.vue:424` `viewDetail(row)` → `router.push('/manager/projects/detail/${row.projectId}')` (列表行 dropdown `view` 命令或 toolbar "详情" 按钮触发)。
> **同级路由** (`router/index.js:61-63`):
> - `projects/new` → `ProjectsNew.vue` (新建)
> - `projects/edit/:projectId` → `ProjectsNew.vue` (编辑, 复用组件)
> - `projects/detail/:projectId` → `ManagerProjectDetail.vue` (详情) ✓
---
## 1. 主要功能 + 可见性
### 1.1 主要功能
| 功能 | 前端入口 | 后端接口 | 涉及表 |
|---|---|---|---|
| 加载项目基本信息 | `loadProject` (`ManagerProjectDetail.vue:234-263`) | `GET /business/project/{id}``BizProjectController.getInfo``bizProjectService.getById``BizProjectMapper.selectByPrimaryKey` | biz_project (+ JOIN biz_org/sponsor / sys_user/lead / biz_person/createUserName / sub-select exec_org_names) |
| 加载角色劳务 (JSON 字段) | 同上 L249-251 | 同上 (期望 biz_project.role_labor 列) | **biz_project.role_labor — 字段不存在** ⚠️ |
| 加载监督员 (JSON 字段) | 同上 L253-256 | 同上 (期望 biz_project.monitors 列) | **biz_project.monitors — 字段不存在** ⚠️ |
| 加载评分 4 维度 | `scoreForm` 初始化 L243-246 | 同上 (期望 biz_project.score_quality/score_speed/score_coordination/score_compliance 列) | **biz_project — 4 列全不存在** ⚠️ |
| 加载公告列表 | `loadAnnouncements` (L265-274) | `GET /business/publicity/list?projectId=X``BizPublicityController.list` (L30: `entity.setStatus(null)` 全查) → `bizPublicityMapper.selectByProjectId` | biz_publicity |
| 发布公告 | `submitAnnouncement` (L318-338) + `bizAdd('publicity', {...})` | `POST /business/publicity``BizPublicityController.add` (L42-50, status 默认 '1') | biz_publicity |
| 删除公告 | `deleteAnnouncement` (L339-348) + `bizDelete('publicity', id)` | `DELETE /business/publicity/{id}` | biz_publicity |
| 预览公告附件 | `openPreview` (L381-386) + `<Preview>` 组件 | URL 直接走 OSS / 内网文件服务 | (前端, 不入库) |
| 增加监督员 (本地) | `confirmAddMonitor` (L296-302) | **无保存接口** ⚠️ | (本地数组) |
| 删除监督员 (本地) | `removeMonitorAt` (L303-306) | **无保存接口** ⚠️ | (本地数组) |
| 增加角色行 (本地) | `addRoleRow` (L277-280) | **无保存接口** ⚠️ | (本地数组) |
| 删除角色行 (本地) | `removeRoleRowAt` (L281-284) | **无保存接口** ⚠️ | (本地数组) |
| 加载监督员候选人 | `openAddMonitor` (L287-295) + `bizList('person', {role:'supervisor', unitType:'sponsor'})` | `GET /business/person/list?role=supervisor&unitType=sponsor``BizPersonController.list` | sys_user (role=supervisor) + biz_person |
| 项目评分 (4 维度 + 总分) | `submitScore` (L354-374) + `bizUpdate('project', {scoreQuality, scoreSpeed, scoreCoordination, scoreCompliance, managerScore})` | `PUT /business/project``BizProjectController.edit``bizProjectMapper.updateByPrimaryKey` | **biz_project — 4 维度字段不存在, 仅 managerScore 入库 (详见 §10 #1)** ⚠️ |
### 1.2 哪些人能看到本页面
| 层 | 来源 | 校验字段 |
|---|---|---|
| 前端菜单 | `AdminLayout.vue:96` (`/manager/projects`) | 无 `requireMain`, 主/子账号均可见 (manager 角色没有子账号业务) |
| 路由守卫 | `router/index.js:63` | `meta.role` 无显式, 走全局 `permission.js` 校验登录态 |
| 后端 SQL | `BizProjectController.getInfo` `@Autowired` service | **无 role 过滤**, 任何登录角色都可 hit 接口 — 跟前端 "manager 全局可见" 语义一致 |
**其他角色对应功能**:
- sponsor 走 `sponsorList` (`BizProjectController.sponsorList`) 按 sponsorAdminUserId 过滤, 自己的项目可看
- executor 走 `executorList` 按 execUserId/executionUnitId 过滤, 自己被分配的项目可看
- doctor 走 `myProjects` 按 executionIntent 过滤, 自己意向的项目可看
- admin 走通用 `list`, 全部可见
**数据隔离**: manager 是无界角色, 可看任何项目; 但**评分接口 `/rate` (BizProjectController:197-210)** 走 `biz_project_rating` (中间表, 有 `rater_id` 隔离), 设计 OK。⚠️ **本页面评分实际写到 `biz_project` 而非 `biz_project_rating`** (详见 §10 #1)。
---
## 2. 筛选项 (filter-form)
> 本页是详情页, **无 filter-form**。下面对应区段内的 el-form-item:
| UI label | 控件 | 绑定 | 后端 SQL | 命中表.字段 | 备注 |
|---|---|---|---|---|---|
| 项目编号 | `<span>` (el-form-item 静态显示) | `row.projectNo` | `selectByPrimaryKey` (BizProjectMapper.xml:82-85) `selectFields` | biz_project.project_no | ✅ |
| 项目名称 | `<span>` | `row.projectName` | 同上 | biz_project.project_name | ✅ |
| 总场次/总期数 | `<span>` | `row.totalSessions` | 同上 | biz_project.total_sessions | ✅ |
| 总金额 | `<span class="money">` | `row.totalAmount` | 同上 | biz_project.total_amount | ✅ |
| 管理费及税金 | `<span class="money">` | `row.manageFee` | 同上 | biz_project.manage_fee | ✅ |
| 项目形式 | `<span>` | `row.projectForm` | 同上 | biz_project.project_form | ✅ |
| 项目开始时间 | `<span>` | `row.startTime` | 同上 | biz_project.start_time | ✅ |
| 项目结束时间 | `<span>` | `row.endTime` | 同上 | biz_project.end_time | ✅ |
| 是否招标项目 | `<span>` | `row.isBidProject` (Y/N) | 同上 | biz_project.is_bid_project | ✅ |
| 项目负责人 | `<span>` | `row.leadUserName || row.sponsorAdminUserName` | selectFields L57 LEFT JOIN sys_user lu ON lu.user_id = p.lead_user_id | sys_user.user_name (派生) || biz_project.sponsor_admin_user_name (兜底) | ⚠️ **业务语义偏差**: 项目负责人应是 leadUserName, sponsorAdminUserName 是支持方负责人; 兜底顺序可能显示错 |
| 支持公司 | `<span>` | `row.sponsorAdminUserName` | selectFields L55 LEFT JOIN biz_org o ON o.user_id = p.sponsor_admin_user_id AND o.org_type='sponsor' | sys_user.user_name (JOIN) — 字段实际存 biz_project.sponsor_admin_user_name (冗余) | ✅ |
| 项目评价 | `<a class="op-link">` | `row.managerScore` | 同上 | biz_project.manager_score | ✅ 链接到评分 dialog |
**注意**: 详情页 11 字段全部 `<span>` 只读, label-width 120px, 字段已 align style (max-width 1200px, 居左)。
---
## 3. 工具栏按钮 (toolbar)
> 本页**无传统工具栏**, 工具栏按钮分散在各章节尾部, 走 el-button link 风格:
| 按钮 | 功能 | 接口 | 涉及表 | 备注 |
|---|---|---|---|---|
| + 增加角色 (L64) | `addRoleRow` (L277-280) | **无保存** | — | P1 隐患: 修改不落库 |
| 删除 (角色行, L58) | `removeRoleRowAt` (L281-284) | **无保存** | — | P1 隐患 |
| + 增加监督员 (L79) | `openAddMonitor` (L287-295) → `confirmAddMonitor` (L296-302) | **无保存** | — | P1 隐患 |
| 删除 (监督员, L74) | `removeMonitorAt` (L303-306) | **无保存** | — | P1 隐患 |
| 发布公告 (L85) | `openUpload``submitAnnouncement` (L318-338) | `POST /business/publicity` | biz_publicity | ✅ 走完整 CRUD |
| 刷新 (L86) | `loadAnnouncements` (L265-274) | `GET /business/publicity/list?projectId=X` | biz_publicity | ✅ |
| 查看 (公告行, L94) | `openPreview` (L381-386) | URL 直连 | — | ✅ Preview 组件 |
| 删除 (公告行, L95) | `deleteAnnouncement` (L339-348) | `DELETE /business/publicity/{id}` | biz_publicity | ✅ ElMessageBox 二次确认 |
| 保存评分 (L134) | `submitScore` (L354-374) | `PUT /business/project` ⚠️ | **biz_project (错表)** | P0 隐患: 4 维度被静默丢弃 |
---
## 4. 表格 (el-table)
> 本页只有 1 个 el-table (公告列表), 数据源见 §4.1; 角色劳务走原生 `<table class="info-table">`, 不算 el-table。
### 4.1 已发布公告 el-table 数据来源
`GET /business/publicity/list?projectId=X``BizPublicityController.list` (L26-33) → `bizPublicityService.selectList(entity)``BizPublicityMapper.selectList` (BizPublicityMapper.xml:32-40) **或** `selectByProjectId` (L42-46) (前端走 selectList 透传 projectId)。
```sql
SELECT id, project_id, project_no, project_name, announce_type, title, file_url, remark, status, rating_time, create_by, create_time, update_by, update_time
FROM biz_publicity
WHERE project_id = #{projectId} [AND announce_type = ?] [AND title LIKE ?]
ORDER BY create_time DESC
```
> ⚠️ **`publish_time` 列在 resultMap / selectFields 中均无映射** (实测 mapper L5-20), 但表里有此列 (DDL COMMENT '发布时间') — 实际业务里所有数据 `publish_time = NULL` (实测 4 行)。
> ✅ 已映射的 `rating_time` 列 (DDL COMMENT '发布时间' 也指这个) 通过 `BizPublicity.ratingTime` Java 字段暴露, 但**字段名误导**: rating 让人以为是评分时间, 实际是发布时间。
EXPLAIN 实测: `key=idx_pub_project (project_id)`, `ref=const`, `rows=1` ✅ 走索引。
### 4.2 列映射
| 列名 | `row.*` | 表.字段 | 备注 |
|---|---|---|---|
| # | (type=index) | — | ✅ |
| 公告标题 | `row.title` | biz_publicity.title | ✅ |
| 发布时间 | `row.createTime` (formatter=fmtDate) | biz_publicity.create_time | ⚠️ **字段错位**: DDL 有 `publish_time` 列但未映射, 实际显示 `create_time` (创建时间, 通常早于发布时间) |
| 发布人 | `row.creatorName` | ⚠️ **字段名错** | 前端模板 (L91) 用 `creatorName`, 但 mapper / entity 暴露的是 `createBy` (varchar 64, 存 username 而非中文名) — 显示会是 admin/manager 等英文账号名, 不是中文名 |
| 操作: 查看/删除 | — | — | ✅ |
### 4.3 角色劳务 table (`<table class="info-table role-labor-table">`, L40-62)
| 列名 | 数据源 | 表.字段 | 备注 |
|---|---|---|---|
| 角色 | `r.role` | (本地数组 form.roleRows) | ⚠️ **不落库**`form.roleRows` 仅内存, 期望 `biz_project.role_labor` JSON 列, 但该列不存在 |
| 劳务金额 | `r.amount` | 同上 | 同上 |
| 操作 | — | — | 删除按钮 (L57-59) |
---
## 5. 行内操作按钮 (op-cell)
| 按钮 | 触发函数 | 接口 | 后端动作 | 涉及表.字段 |
|---|---|---|---|---|
| 角色行 删除 (L58) | `removeRoleRowAt(idx)` | — | (本地 splice) | (期望 biz_project.role_labor JSON) |
| 监督员 删除 (L74) | `removeMonitorAt(idx)` | — | (本地 splice) | (期望 biz_project.monitors JSON) |
| 公告 查看 (L94) | `openPreview(row.fileUrl, row.title)` | URL 直连 | — | — |
| 公告 删除 (L95) | `deleteAnnouncement(row)` | `DELETE /business/publicity/{id}` | BizPublicityController.remove → BizPublicityMapper.deleteByPrimaryKey | biz_publicity.id |
**真实落点**: 角色劳务 / 监督员所有改动**完全不入库** — 后端 `BizProject` 实体无 `roleLabor` / `monitors` 字段 (§6.4), mapper `updateByPrimaryKey` 也没有对应 `<if>`, 即使前端发到后端也会被 Jackson 静默丢弃 (Spring Boot 默认 `spring.jackson.deserialization.fail-on-unknown-properties = false`)。
---
## 6. Java Entity ↔ 数据库表 一致性
### 6.1 biz_project DDL (实测 2026-08-20)
```sql
CREATE TABLE biz_project (
project_id bigint NOT NULL AUTO_INCREMENT COMMENT '项目ID',
project_no varchar(50) NOT NULL COMMENT '项目编号 ZH-2026-658',
project_name varchar(200) NOT NULL COMMENT '项目名称',
project_form varchar(20) DEFAULT NULL COMMENT '项目形式 线上/线下/线上+线下/其他',
total_sessions int DEFAULT '0' COMMENT '总场次/总期数',
done_sessions int DEFAULT '0' COMMENT '已执行',
todo_sessions int DEFAULT '0' COMMENT '未执行',
total_amount decimal(15,2) DEFAULT '0.00' COMMENT '总金额',
available_amount decimal(15,2) DEFAULT '0.00' COMMENT '可用金额',
paid_labor_amount decimal(15,2) DEFAULT '0.00' COMMENT '已支付劳务',
paid_meeting_amount decimal(15,2) DEFAULT '0.00' COMMENT '已支付会务',
manage_fee decimal(15,2) DEFAULT '0.00' COMMENT '管理费及税金',
is_finished char(1) DEFAULT '0' COMMENT '是否完成 0否 1是',
is_settled char(1) DEFAULT 'N' COMMENT '是否结算 N否 Y是',
manager_score decimal(3,1) DEFAULT NULL COMMENT '合规管理员评均分',
sponsor_score decimal(3,1) DEFAULT NULL COMMENT '支持方评均分',
sponsor_admin_user_id bigint DEFAULT NULL COMMENT '赞助方负责人用户ID',
sponsor_admin_user_name varchar(200) DEFAULT NULL COMMENT '赞助方负责人用户名(冗余)',
lead_user_id bigint DEFAULT NULL COMMENT '项目负责人 user_id',
is_bid_project char(1) DEFAULT 'N' COMMENT '是否招标项目 Y/N',
start_time datetime DEFAULT NULL COMMENT '项目开始时间',
end_time datetime DEFAULT NULL COMMENT '项目结束时间',
submit_deadline_days int DEFAULT '0' COMMENT '提交材料截止天数',
support_contract_url varchar(500) DEFAULT NULL,
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_user_id bigint DEFAULT NULL,
create_time datetime DEFAULT NULL,
update_by varchar(64) DEFAULT '',
update_time datetime 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),
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='项目';
```
数据规模: **12 行** (实测)。
### 6.2 biz_publicity DDL (实测)
```sql
CREATE TABLE biz_publicity (
id bigint NOT NULL AUTO_INCREMENT COMMENT 'PK',
project_id bigint NOT NULL COMMENT '项目ID',
project_no varchar(50) DEFAULT NULL COMMENT '项目编号 (冗余)',
project_name varchar(200) DEFAULT NULL COMMENT '项目名称 (冗余)',
announce_type varchar(20) DEFAULT NULL COMMENT '公告类型 (invitation/support/notice/agenda)',
title varchar(200) DEFAULT NULL COMMENT '公告标题',
file_url varchar(500) DEFAULT NULL COMMENT '公告文件URL',
publish_time datetime DEFAULT NULL COMMENT '发布时间',
remark varchar(500) DEFAULT NULL,
status char(1) DEFAULT '1' COMMENT '状态 (0保存 1发布)',
rating_time datetime DEFAULT NULL COMMENT '发布时间', -- ⚠️ 命名误导: COMMENT 说"发布时间", 但列名 rating_time
create_by varchar(64) DEFAULT NULL,
create_time datetime DEFAULT NULL,
update_by varchar(64) DEFAULT NULL,
update_time datetime DEFAULT NULL,
PRIMARY KEY (id),
KEY idx_pub_project (project_id),
KEY idx_pub_create_time (create_time),
KEY idx_pub_project_no (project_no),
KEY idx_pub_announce_type (announce_type),
KEY idx_pub_title (title)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='项目公告';
```
数据规模: **4 行** (实测)。
### 6.3 biz_project_rating DDL (实测, 与评分相关)
```sql
CREATE TABLE biz_project_rating (
rating_id bigint NOT NULL AUTO_INCREMENT COMMENT '雪花ID',
project_id bigint NOT NULL COMMENT '项目ID',
project_no varchar(50) DEFAULT NULL,
project_name varchar(200) DEFAULT NULL,
rater_id bigint DEFAULT NULL COMMENT '评分人ID',
rater_name varchar(50) DEFAULT NULL,
rater_role varchar(20) DEFAULT NULL COMMENT '评分人角色 compliance/executor/sponsor',
quality_score int DEFAULT '0' COMMENT '履约质量 1-5',
response_score int DEFAULT '0' COMMENT '时效响应 1-5',
cooperation_score int DEFAULT '0' COMMENT '配合度 1-5',
compliance_score int DEFAULT '0' COMMENT '合规安全 1-5',
remark varchar(500) DEFAULT NULL,
rating_time datetime DEFAULT NULL,
create_by varchar(64) DEFAULT NULL,
create_time datetime DEFAULT NULL,
update_by varchar(64) DEFAULT NULL,
update_time datetime DEFAULT NULL,
PRIMARY KEY (rating_id),
UNIQUE KEY uk_rating (project_id, rater_id, rater_role),
KEY idx_pr_project (project_id),
KEY idx_pr_rater (rater_id),
KEY idx_pr_rating_time (rating_time)
) ENGINE=InnoDB AUTO_INCREMENT=31 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='项目评分表';
```
数据规模: **30 行** (实测)。
### 6.4 Entity ↔ DB 字段对照
#### BizProject (ManagerProjectDetail 涉及的 12 字段)
| 实体字段 | 中文列名 (DDL COMMENT) | 类型 | 表字段 | 类型 | 一致? |
|---|---|---|---|---|---|
| projectId | 项目ID | Long | project_id | bigint | ✅ |
| projectNo | 项目编号 | String | project_no | varchar(50) | ✅ |
| projectName | 项目名称 | String | project_name | varchar(200) | ✅ |
| totalSessions | 总场次/总期数 | Long | total_sessions | int | ✅ (Long 兼容 int) |
| totalAmount | 总金额 | BigDecimal | total_amount | decimal(15,2) | ✅ |
| manageFee | 管理费及税金 | BigDecimal | manage_fee | decimal(15,2) | ✅ |
| managerScore | 合规管理员评均分 | BigDecimal | manager_score | decimal(3,1) | ✅ |
| sponsorScore | 支持方评均分 | BigDecimal | sponsor_score | decimal(3,1) | ✅ |
| sponsorAdminUserName | 赞助方负责人用户名(冗余) | String | sponsor_admin_user_name | varchar(200) | ✅ |
| sponsorAdminUserId | 赞助方负责人用户ID | Long | sponsor_admin_user_id | bigint | ✅ |
| leadUserId | 项目负责人 user_id | Long | lead_user_id | bigint | ✅ |
| isBidProject | 是否招标项目 | String | is_bid_project | char(1) | ✅ |
| startTime | 项目开始时间 | Date | start_time | datetime | ✅ |
| endTime | 项目结束时间 | Date | end_time | datetime | ✅ |
| submitDeadlineDays | 提交材料截止天数 | Integer | submit_deadline_days | int | ✅ |
| supportContractUrl | 支持合同文件URL | String | support_contract_url | varchar(500) | ✅ |
| executeContractUrl | 执行合同文件URL | String | execute_contract_url | varchar(500) | ✅ |
| invitationUrl | 邀请函文件URL | String | invitation_url | varchar(500) | ✅ |
| supportLetterUrl | 支持函文件URL | String | support_letter_url | varchar(500) | ✅ |
| publishUrl | 已发布公告URL | String | publish_url | varchar(500) | ✅ |
| createTime | 创建时间 | Date | create_time | datetime | ✅ |
| updateTime | 更新时间 | Date | update_time | datetime | ✅ |
#### ⚠️ 前端使用但实体/DB 完全不存在的字段
| 前端字段 | 用途 | 实体 | Mapper `<if>` | DB 列 | 状态 |
|---|---|---|---|---|---|
| `row.scoreQuality` (L243) | 评分 4 维度 - 履约质量 | ❌ | ❌ | ❌ | **完全不存在** |
| `row.scoreSpeed` (L244) | 评分 4 维度 - 时效响应 | ❌ | ❌ | ❌ | **完全不存在** |
| `row.scoreCoordination` (L245) | 评分 4 维度 - 配合度 | ❌ | ❌ | ❌ | **完全不存在** |
| `row.scoreCompliance` (L246) | 评分 4 维度 - 合规安全 | ❌ | ❌ | ❌ | **完全不存在** |
| `row.roleLabor` (L249) | 角色劳务 JSON | ❌ | ❌ | ❌ | **完全不存在** |
| `row.monitors` (L254) | 监督员 JSON | ❌ | ❌ | ❌ | **完全不存在** |
> **结论**: 上面 6 个字段前端读不到 (始终 `undefined` → JSON.parse 抛错 → catch 兜底用默认值), 后端写不入 (Jackson 静默丢弃)。
#### BizPublicity (ManagerProjectDetail 涉及的 7 字段)
| 实体字段 | 中文列名 (DDL COMMENT) | 类型 | 表字段 | 类型 | 一致? |
|---|---|---|---|---|---|
| id | PK | Long | id | bigint | ✅ |
| projectId | 项目ID | Long | project_id | bigint | ✅ |
| projectNo | 项目编号 | String | project_no | varchar(50) | ✅ |
| projectName | 项目名称 | String | project_name | varchar(200) | ✅ |
| announceType | 公告类型 | String | announce_type | varchar(20) | ✅ |
| title | 公告标题 | String | title | varchar(200) | ✅ |
| fileUrl | 公告文件URL | String | file_url | varchar(500) | ✅ |
| remark | 备注 | String | remark | varchar(500) | ✅ |
| status | 状态 | String | status | char(1) | ✅ |
| ratingTime | 发布时间 (COMMENT 误导) | Date | rating_time | datetime | ✅ |
| createBy | 创建者 | String | create_by | varchar(64) | ✅ |
| createTime | 创建时间 | Date | create_time | datetime | ✅ |
| updateBy | 更新者 | String | update_by | varchar(64) | ✅ |
| updateTime | 更新时间 | Date | update_time | datetime | ✅ |
#### ⚠️ BizPublicity 表里有但 mapper 不映射的字段
| DB 列 | DDL COMMENT | 实体字段 | 备注 |
|---|---|---|---|
| `publish_time` | 发布时间 | ❌ | mapper `selectFields` / resultMap / insert / update 全部未引用, 实测 4 行数据此列全为 NULL |
### 6.5 修订
> v1 误判: `biz_publicity.rating_time` 看似是"评分时间" — 实际 DDL COMMENT 明确写 "发布时间", 与 `biz_project_rating.rating_time` (评分时间) 同列名但不同语义。**应统一改名为 `publish_time`** (跟同表 `publish_time` 列对齐, 删掉冗余 `rating_time`)。
---
## 7. 索引检查
### 7.1 biz_project
| Key | 列 | 用途覆盖 |
|---|---|---|
| PRIMARY | `project_id` | ✅ |
| uk_project_no | `project_no` (UNIQUE) | ✅ selectByPrimaryKey / sponsorList / selectList / manager 端精确查 |
| idx_project_form | `project_form` | ✅ selectList WHERE project_form = ? |
| idx_is_finished | `is_finished` | ✅ selectList WHERE is_finished = ? |
| idx_is_settled | `is_settled` | ✅ selectList WHERE is_settled = ? |
| idx_sponsor_admin_user_id | `sponsor_admin_user_id` | ✅ sponsorList WHERE sponsor_admin_user_id = ? |
| idx_start_end_time | `(start_time, end_time)` | ✅ selectList WHERE start_time >= ? AND end_time <= ? |
**冗余检查**: 无明显冗余索引。
### 7.2 biz_publicity
| Key | 列 | 用途覆盖 |
|---|---|---|
| PRIMARY | `id` | ✅ |
| idx_pub_project | `project_id` | ✅ selectList WHERE project_id = ? (本页面主查询) — EXPLAIN 实测命中 |
| idx_pub_create_time | `create_time` | ✅ ORDER BY create_time desc |
| idx_pub_project_no | `project_no` | ⚠️ 当前页面未用, 待清理或保留 |
| idx_pub_announce_type | `announce_type` | ✅ selectList WHERE announce_type = ? |
| idx_pub_title | `title` | ✅ selectList WHERE title LIKE ? |
**冗余检查**: 5 个单列索引, 数据规模 4 行, 优化器实测选 `idx_pub_project` (本页面主查询), 其它索引暂保留无害。
### 7.3 biz_project_rating (评分备用表)
| Key | 列 | 用途覆盖 |
|---|---|---|
| PRIMARY | `rating_id` | ✅ |
| uk_rating | `(project_id, rater_id, rater_role)` UNIQUE | ✅ upsert 一人评一项目一次 |
| idx_pr_project | `project_id` | ⚠️ 前缀被 uk_rating 覆盖, EXPLAIN 应优先 uk |
| idx_pr_rater | `rater_id` | ✅ 按评分人查 |
| idx_pr_rating_time | `rating_time` | ✅ 按时间排序 |
### 7.4 EXPLAIN 实测 (2026-08-20)
| # | 查询 | 命中索引 | 备注 |
|---|---|---|---|
| Q1 | `SELECT_BY_PRIMARY_KEY biz_publicity WHERE project_id=2` | `idx_pub_project` (ref const) | ✅ |
| Q2 | `SELECT_BY_PRIMARY_KEY biz_project WHERE project_id=2` | `PRIMARY` (const) | ✅ |
---
## 8. 与原型差异 (实现偏离)
> **原型**: `proto/html/components/project-detail-compliance.html` (236 行, "角色:合规 父级:manager.html 原型:原型/项目详情.html" 注释确认)
> **结构**: 4 个独立 `.card` + `h1.page-title` 标题 + 单 `<main class="content">` 包裹 + 底部 `返回` 按钮 + 全页红字 `*项目详情可修改,只有创建人可修改`
### 8.1 实现新增 (原型没有)
| 项 | 说明 |
|---|---|
| 跳转子页面 (而非 dialog) | 详情页是独立路由 `ManagerProjectDetail.vue`, 走 `router.push` (列表行 dropdown `view` 命令触发) |
| 项目信息扩展 5 字段 | 原型 9 字段 → Vue 11 字段, 新增 `项目开始时间 / 项目结束时间 / 是否招标项目 / 项目负责人 / 支持公司` (映射 `biz_project.start_time / end_time / is_bid_project / lead_user_name (JOIN) / sponsor_admin_user_name`) |
| 项目评价 dialog | 原型只显示 4.5 一个数字 + alert 弹窗, Vue 改成 el-dialog 4 维度 el-rate × 5 + 总分20 实时计算 |
| 增加监督员 dialog | el-select 远程搜索候选人 (按 `role='supervisor'` + `unitType='sponsor'`), 原型用 prompt() 弹输入框 |
| 角色劳务增/删按钮 | 原型只显示静态 2 行 (主席5000 / 主持3000), Vue 加 `+ 增加角色` 按钮 + 行内 `删除` 按钮 — **但无保存接口**, 改完刷新即丢 (§10 #2) |
| 公告预览组件 | `<Preview>` 组件走 URL 直连 + OSS, 原型是 alert('预览文件') |
### 8.2 原型有但实现缺失
| 项 | 原型位置 | 实现 | 严重度 |
|---|---|---|---|
| h1 page-title "项目详情" | `project-detail-compliance.html:89` `<h1 class="page-title" id="pageTitle">项目详情</h1>` | ⚠️ Vue 仅面包屑 `<span class="current">项目详情</span>`, **无大标题** | **P3** |
| 4 个独立 .card | 原型 L92/109/132/144 (4 个 `<div class="card">`) | ⚠️ Vue 用 1 个 page-card + 内部 .new-card-title 章节标题 — 视觉简化, 业务一致 | **P3** |
| 项目信息 — 服务机构字段 | 原型 L101 `<span class="info-label">服务机构:</span><span class="info-value" id="vService">北京XXXX公司</span>` | ⚠️ **Vue 完全没显示** — 但 DB 有 `exec_org_names` 派生字段 (mapper L50-53) 可用 | **P2** |
| 邀请函 / 支持函 文件区 | 原型 L147-169 3 子标题 (邀请函 / 已发布公告 / 支持函), 每段 查看+下载 | ⚠️ Vue 仅做"已发布公告" el-table, **邀请函和支持函完全未实现** — DB 已有 `biz_project.invitation_url` / `support_letter_url` 列 (selectFields L46-69 已读), 但页面没用 | **P2** |
| 角色劳务 — 无增/删按钮 | 原型 L131-141 仅展示 2 行静态数据 | ✅ Vue 加了增/删按钮 (但无保存, 配合 §10 #2 形成新 bug) | — |
| 红字提示 *项目详情可修改,只有创建人可修改 | 原型 L172 `<p class="hint-red">*项目详情可修改,只有创建人可修改</p>` | ⚠️ Vue 没加, **业务规则没人管** (谁可以编辑项目?) | **P3** |
| 底部"返回"按钮 | 原型 L174-176 `<div class="form-actions"><button onclick="goBack()">返回</button></div>` | ⚠️ Vue 无底部"返回"按钮 (只能靠浏览器后退或面包屑) | **P3** |
| 评分 4 维度持久化 | 原型未细说, 但应有评分持久化逻辑 | ⚠️ Vue 评分 dialog 提交写到 **错表** (biz_project 而非 biz_project_rating), 4 维度全部丢失 | **P0** |
| 角色劳务持久化 | 原型未细说, 但应有持久化 | ⚠️ Vue `form.roleRows` 仅内存, 改完刷新即丢 | **P1** |
| 监督员持久化 | 原型未细说, 但应有持久化 | ⚠️ Vue `monitors` 数组仅内存, 改完刷新即丢 | **P1** |
### 8.3 文字 / 标签差异
| 原型 | 实现 | 差异 |
|---|---|---|
| 项目管理 > 项目详情 (面包屑) | ✅ Vue 1:1 (L3-6) | — |
| 项目详情 (h1 page-title) | 仅面包屑, 无 h1 | ⚠️ 缺 |
| 项目信息 (9 字段) | 项目信息 (11 字段, 缺"服务机构", 多 5 字段) | ⚠️ 字段不齐 |
| 支持单位监督员 | ✅ 同 | — |
| 角色劳务设置 | ✅ 同 (但 Vue 加了增/删按钮) | ⚠️ 增/删按钮但无保存, 形成新 bug |
| 公告文件 | 已发布公告 (Vue 改 el-table) | ⚠️ 邀请函/支持函 缺, 实现偏离 |
| 红字 *项目详情可修改,只有创建人可修改 | (未加) | ⚠️ 缺业务规则 |
| *增加只显示已点支持意向的 | ✅ Vue 加 (hint-text 灰字) | 颜色从红字 → 灰字 (跟 ProjectsNew 对齐) |
| *点击后可查看具体评价维度,评价标准与供应商系统保持一致 | ⚠️ Vue 缺 | 缺 |
### 8.4 总结
整体方向: **业务核心对齐 + 多处扩展偏离**:
**严重问题** (按优先级):
- **P0 #1** 评分写到错表 (biz_project 而非 biz_project_rating), 4 维度全部丢失 — 即使原型没说, 但已有 `biz_project_rating` 表 + `/business/project/rate` 接口
- **P1 #2** 角色劳务 / 监督员 UI 有但无保存 (原型未要求增/删按钮, Vue 自加反而暴露了无保存 bug)
- **P2 #4** 邀请函 / 支持函 完全未实现 (DB 有列, mapper 已 select, 页面不用)
- **P2 #5** "服务机构" 字段缺 (mapper 有派生 `exec_org_names`, 页面没用)
**风格问题** (P3):
- 缺 h1 大标题 / 底部返回按钮 / "项目详情可修改" 红字业务规则
- 监督员提示从红字改灰字 (已 style-align, 跟 ProjectsNew 一致)
**建议优先级**: P0 #1 → P1 #2 → P2 #4+#5 → P3 其它。
---
## 9. 表字段冗余 / 设计问题
### 9.1 派生 vs 持久化字段
- `biz_project.sponsor_admin_user_name`: 持久化 (冗余), 用于列表展示避免 JOIN biz_org, 设计 OK
- `biz_project.sponsor_admin_user_id`: 持久化, 用于 sponsorList 过滤, 必需
- `BizProject.sponsorOrgName / leadUserName / createUserName / execOrgNames`: 派生字段 (Mapper sub-select / LEFT JOIN), 不入库, 仅展示, 设计 OK
### 9.2 唯一约束语义检查
- ✅ biz_project: `uk_project_no (project_no)` — 业务上项目编号唯一, 合理
- ✅ biz_publicity: 无 uk, 同一项目可发多条公告 (合理)
- ✅ biz_project_rating: `uk_rating (project_id, rater_id, rater_role)` — 一人评一项目一次, upsert 防重, 合理
### 9.3 业务闭环检查
- ✅ 项目评分已有专门表 `biz_project_rating` (有 rater_id 隔离, uk 防重, 走 upsert)
- ⚠️ **本页面评分实际写到 `biz_project` 而非 `biz_project_rating`** (§10 #1) — 既没走专用接口, 4 维度也丢
- ⚠️ **角色劳务 / 监督员无持久化** (§10 #2) — 期望落 `biz_project.role_labor / monitors` JSON 列, 但这俩列从未建过
### 9.4 路由安全性
- `manager-projects-detail` 路由 `meta.role` 无显式, 走全局登录态校验, 任何已登录角色都可访问 — 与 admin-projects 同款 (`router/index.js:28` admin-projects 也指向 manager/Projects.vue), 业务上 OK 但**没显式 `meta: { role: 'manager' }`** 是隐性约定
### 9.5 命名误导
- `biz_publicity.rating_time` 列 (COMMENT "发布时间") — 跟 `biz_project_rating.rating_time` (评分时间) 同列名但不同语义。**应统一改名为 `publish_time`** (跟同表 `publish_time` 列对齐)
### 9.6 mapper 不映射的列
- `biz_publicity.publish_time` 列存在但 mapper 全链路 (resultMap / selectFields / insert / update) 不引用 — 业务上需要这个字段时无法存储
---
## 10. 待修复列表 (按优先级)
| # | 问题 | 文件:行 | 修复建议 | 严重度 |
|---|---|---|---|---|
| 1 | **评分 4 维度写到 biz_project (错表)**, 4 维度 (scoreQuality/Speed/Coordination/Compliance) 实体无字段, mapper 无 `<if>`, **Spring Boot Jackson 默认丢弃未知字段** — 全部失效 | `ManagerProjectDetail.vue:354-374` `submitScore` | 改走 `POST /business/project/rate` (BizProjectController L197-210, 已有专用接口写 biz_project_rating 表), body 含 `{projectId, raterRole:'compliance', qualityScore/scoreSpeed/cooperationScore/complianceScore, remark}`, **不再写 managerScore** (managerScore 应在 SELECT 时从 biz_project_rating 派生 AVG) | **P0** |
| 2 | **角色劳务无保存按钮**, UI 可改可加可删但 `form.roleRows` 仅内存, 刷新后丢失 | `ManagerProjectDetail.vue:37-65, 277-284` | DB 加 `biz_project.role_labor JSON` 列 + 实体加 `roleLabor` 字段 + mapper `updateByPrimaryKey``<if test="roleLabor != null">role_labor = #{roleLabor},</if>`; 前端加 "保存角色劳务" 按钮 + 提交时 JSON.stringify(form.roleRows) 走 `bizUpdate('project', {projectId, roleLabor: JSON.stringify(form.roleRows)})` | **P1** |
| 3 | **监督员无保存按钮**, 同 #2, `monitors` 数组仅内存 | `ManagerProjectDetail.vue:67-80, 287-306` | 同 #2: DB 加 `biz_project.monitors JSON` 列 + 实体 + mapper; 前端加 "保存监督员" 按钮 | **P1** |
| 4 | **biz_publicity.publish_time 列存在但 mapper 全链路不映射**, 实测 4 行数据此列全 NULL | `BizPublicityMapper.xml` 全文 | `selectFields``publish_time`, resultMap 加 `<result property="publishTime" column="publish_time" />`, BizPublicity 实体加 `private Date publishTime`, BizPublicityController.add 在 setStatus('1') 时同步 `entity.setPublishTime(new Date())` | **P2** |
| 5 | **biz_publicity.announceTypes 写死 ('invitation/support/notice/agenda')** 跟前端 enum 一致, 但 DDL COMMENT 与 entity `@Excel` 注解未提供枚举说明, 后端无校验 | `BizPublicityController:42-50` | 加 enum 校验或 enum 字典表 | **P3** |
| 6 | **公告 `creatorName` 字段名错** (前端 L91), 实际 mapper 映射 `createBy` (英文 username), 显示会是 "admin" 而非 "张三" | `ManagerProjectDetail.vue:91` | mapper 改造: BizPublicityMapper.xml `selectFields``LEFT JOIN sys_user u ON u.user_name = p.create_by`, 派生 `creator_name`; 或前端接受 username 显示 | **P3** |
| 7 | **biz_publicity.rating_time 命名误导** (COMMENT 是"发布时间", 但跟 biz_project_rating.rating_time 同名冲突) | `biz_publicity` (DDL) | 改列名 `rating_time``publish_time` (跟同表 `publish_time` 列对齐, 删一个冗余) + 同步 entity 字段 + mapper | **P3** |
| 8 | **`项目负责人` 字段兜底显示 sponsorAdminUserName**, 业务语义偏差 | `ManagerProjectDetail.vue:29` | 仅显示 `row.leadUserName`, 无值时显示 "-" 不兜底 sponsorAdminUserName | **P3** |
| 9 | **页面无 loading 态** (el-form 上没用 `v-loading`), 加载时只读区闪烁空白 | `ManagerProjectDetail.vue:10` | `<el-form ... v-loading="loading">` (loading ref 已定义 L173 但未使用) | **P3** |
| 10 | **`previewOpen / previewUrl / previewTitle` 跟业务耦合** — 4 处共用 Preview 组件, 但当前只有公告预览用 | `ManagerProjectDetail.vue:209-211, 381-386` | OK, 但若增加"邀请函预览"等需复用同一组件, 注意 title 不要硬编码 | **P3** |
| 11 | **biz_publicity.create_by 字段宽度仅 64** (DDL), 后续 username 含特殊字符可能截断 | DDL | 改 varchar(64) → varchar(128) | **P3** |
| 12 | **`manager-projects-detail` 路由 `meta.role` 无显式**, 走全局登录态 | `router/index.js:63` | 加 `meta: { role: 'manager' }` 跟其它 manager 路由一致 | **P3** |
| 13 | **biz_publicity 5 个单列索引**, 数据规模 4 行, EXPLAIN 优化器选 idx_pub_project, 其它 4 个暂无命中场景 | `biz_publicity` (DB) | 数据量增长后再 EVAL DROP | **P3** |
| 14 | **评分 dialog 提交后未校验 totalScore 是否在 4~20 范围** (允许 0 分但语义无意义) | `submitScore` L354 | 加 `if (totalScore < 4) return ElMessage.warning('请至少给每个维度 1 分')` | **P3** |
---
## 11. 引用清单
### 前端
- `ry-vue3/src/layout/AdminLayout.vue:96` — manager 菜单 (项目管理入口)
- `ry-vue3/src/router/index.js:61-63` — 路由 `manager-projects-new / edit / detail`
- `ry-vue3/src/views/manager/ManagerProjectDetail.vue` — 本审查对象 (459 行)
- `ry-vue3/src/views/manager/Projects.vue:424``viewDetail` (列表行入口)
- `ry-vue3/src/components/Preview.vue` — 公告预览组件
- `ry-vue3/src/api/public.js:90-98``bizGet/bizAdd/bizUpdate/bizDelete` (CRUD 包装)
### 后端
- `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizProjectController.java:139-210` — 项目 getInfo / rate (评分专用接口, **本页面未走此接口, 见 §10 #1**)
- `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPublicityController.java:26-65` — 公告 list/getInfo/add/edit/remove
- `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPersonController.java:36-44, 65-79` — 业务角色白名单 (`executor/sponsor/doctor/manager`), 监督员候选人按 `role='supervisor'` + `unitType='sponsor'` 过滤
- `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/service/IBizProjectRatingService.java` + Impl — `upsertRating` (评分专用, **本页面未调用**)
- `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizPublicityService.java` + Impl
- `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProject.java` — 项目实体 (**无 roleLabor/monitors/scoreQuality 等字段, 见 §6.4**)
- `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizPublicity.java` — 公告实体 (**无 publishTime 字段, 见 §6.4**)
- `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProjectRating.java` — 评分实体 (走 `quality_score/response_score/cooperation_score/compliance_score`)
- `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/mapper/BizPublicityMapper.java` + xml — selectByProjectId / selectList / insert / updateByPrimaryKey (**publish_time 不映射, 见 §6.4**)
### DB (实测 2026-08-20)
- `biz_project` (12 行, 34 列): 含 sponsor_admin_user_id / sponsor_admin_user_name / total_sessions / total_amount / manage_fee / submit_deadline_days / manager_score / sponsor_score / lead_user_id / is_bid_project / start_time / end_time / 5 URL 列 + 索引 6 个
- `biz_publicity` (4 行, 15 列): id / project_id / project_no / project_name / announce_type / title / file_url / publish_time (未映射) / remark / status / rating_time (命名误导) / 4 audit 列 + 索引 5 个
- `biz_project_rating` (30 行, 16 列): rating_id / project_id / rater_id / rater_name / rater_role / 4 维度分 (quality/response/cooperation/compliance) / remark / rating_time + 4 audit 列 + 索引 4 个 (含 uk_rating)
### 配置
- `ry-api/ruoyi-admin/src/main/resources/application-druid.yml` — DB `127.0.0.1:3306/guoju0808/root/cu2oh2co3`
### 原型
- `proto/html/manager.html` — manager 菜单入口 (iframe 路由)
- `proto/html/components/project-manage.html` — 项目管理列表原型 (对应 `manager/Projects.vue`, 详情页无独立原型)
### 相关 (非本审查, 引用以辅助)
- `_self/manager_projects.md` — 上游列表页审查报告 (本审查的调用方)
- `_self/manager_projects_assign.md` — 兄弟页 `/manager/projects/assign` 审查报告 (分配执行方)
- `biz_project_sponsor_assign` (15 列) — **与本页面无关**: 存"赞助方分配监察员"中间表, 跟本页面"项目详情监督员"是不同业务 (本页面监督员期望落 biz_project.monitors JSON 列, **未实现**)