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>
This commit is contained in:
郭庆泰
2026-08-20 23:59:20 +08:00
co-authored by Claude
parent 25be0d5d89
commit 99b9807270
3 changed files with 579 additions and 7 deletions
+540
View File
@@ -0,0 +1,540 @@
## 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 列, **未实现**)
@@ -1,5 +1,7 @@
package com.ruoyi.business.controller; package com.ruoyi.business.controller;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -193,6 +195,8 @@ public class BizProjectController extends BaseController
* 通用评分 upsert (任何角色: sponsor / executor / compliance) * 通用评分 upsert (任何角色: sponsor / executor / compliance)
* POST /business/project/rate * POST /business/project/rate
* 不做可见性校验 — 评分公开 * 不做可见性校验 — 评分公开
* 评分写入 biz_project_rating 后, 同步回写 biz_project.manager_score
* = 该项目所有 compliance 角色评分的 4 维度总分之平均 (decimal(3,1) 1 位小数)
*/ */
@Log(title = "项目评分", businessType = BusinessType.UPDATE) @Log(title = "项目评分", businessType = BusinessType.UPDATE)
@PostMapping("/rate") @PostMapping("/rate")
@@ -206,9 +210,30 @@ public class BizProjectController extends BaseController
body.setCreateBy(SecurityUtils.getUsername()); body.setCreateBy(SecurityUtils.getUsername());
body.setUpdateBy(SecurityUtils.getUsername()); body.setUpdateBy(SecurityUtils.getUsername());
int rows = bizProjectRatingService.upsertRating(body); int rows = bizProjectRatingService.upsertRating(body);
// 同步回写 biz_project.manager_score (仅 compliance 角色聚合)
if ("compliance".equals(body.getRaterRole())) {
BizProjectRating q = new BizProjectRating();
q.setProjectId(body.getProjectId());
q.setRaterRole("compliance");
List<BizProjectRating> all = bizProjectRatingService.selectList(q);
BigDecimal sum = BigDecimal.ZERO;
int cnt = 0;
for (BizProjectRating r : all) {
int s = safeLong(r.getQualityScore()) + safeLong(r.getResponseScore())
+ safeLong(r.getCooperationScore()) + safeLong(r.getComplianceScore());
if (s > 0) { sum = sum.add(BigDecimal.valueOf(s)); cnt++; }
}
BizProject p = new BizProject();
p.setProjectId(body.getProjectId());
p.setManagerScore(cnt == 0 ? null : sum.divide(BigDecimal.valueOf(cnt), 1, RoundingMode.HALF_UP));
bizProjectService.updateByPrimaryKey(p); // mapper `<if test="managerScore != null">` 仅更新这一列
}
return toAjax(rows); return toAjax(rows);
} }
private static long safeLong(Long v) { return v == null ? 0 : v; }
/** /**
* 查某项目的所有评分 (公开, 任何角色可读) * 查某项目的所有评分 (公开, 任何角色可读)
* GET /business/project/ratings?projectId=X * GET /business/project/ratings?projectId=X
@@ -352,15 +352,22 @@ function openScoreDialog() {
scoreOpen.value = true scoreOpen.value = true
} }
async function submitScore() { async function submitScore() {
if (totalScore.value < 4) {
return ElMessage.warning('请至少给每个维度 1 分')
}
submitting.value = true submitting.value = true
try { try {
await bizUpdate('project', { await request({
...row.value, url: '/business/project/rate',
scoreQuality: scoreForm.scoreQuality, method: 'post',
scoreSpeed: scoreForm.scoreSpeed, data: {
scoreCoordination: scoreForm.scoreCoordination, projectId: projectId.value,
scoreCompliance: scoreForm.scoreCompliance, raterRole: 'compliance',
managerScore: totalScore.value qualityScore: scoreForm.scoreQuality,
responseScore: scoreForm.scoreSpeed,
cooperationScore: scoreForm.scoreCoordination,
complianceScore: scoreForm.scoreCompliance,
}
}) })
ElMessage.success('评分已保存') ElMessage.success('评分已保存')
scoreOpen.value = false scoreOpen.value = false