Files
guoju0808/_self/doctor_submissions.md
T
郭庆泰andClaude 8aba05aa0f docs(review): sponsor/account 端到端审查报告
11 章穿透审查:
- 四跳定位 (AdminLayout → router → Account.vue → proto)
- 后端链路: SysProfileController → SysUserMapper
- DB 实测 sys_user DDL + 索引
- 与原型 sponsor-account.html 对比
- 发现 P1: 改手机号未走短信验证码 (后端 /changePhone 已实现但前端未用)
- 发现 P3: sys_user 缺 uk_user_name / idx_phonenumber

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

408 lines
27 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.
# doctor/Submissions 端到端技术审查
**审查日期**: 2026-08-18
**目标页面**: `ry-vue3/src/views/doctor/Submissions.vue` (我的项目设计投稿)
**审查方法**: 前端 → 后端 → 数据库 → 原型 四层穿透
**审查范围**: `AdminLayout` 菜单 → `router` 路由 → Vue 组件 → `BizProjectPlanController``BizProjectPlanMapper.xml``biz_project_plan` 表 → `proto/html/components/submissions.html`
---
## 第 0 章: 组件定位 (四跳)
| 跳 | 文件 | 命中 | 行 |
|---|---|---|---|
| ① 角色菜单 | `ry-vue3/src/layout/AdminLayout.vue` | `MENU.doctor[4]` = `{ path: '/doctor/submissions', title: '我的项目设计投稿', icon: EditPen }` | 112 |
| ② 路由 | `ry-vue3/src/router/index.js` | `name: 'doctor-submissions'`, `component: () => import('@/views/doctor/Submissions.vue')` | 94 |
| ③ 组件 | `ry-vue3/src/views/doctor/Submissions.vue` | 审查目标文件 | — |
| ④ 原型 | `proto/html/components/submissions.html` | `doctor.html:208 <a data-page="components/submissions.html">` | 208 / submissions.html |
> 路由表 (router/index.js:88-99) 还包含 `submission/new` (新建)、`submission/detail/:planId` (详情)、`submission/edit/:planId` (同 `SubmissionNew.vue` 修改模式),本审查不展开。
---
## 第 1 章: 主要功能 + 可见性
### 1.1 主要功能
| 功能 | 前端入口 | 后端接口 | 数据表 |
|---|---|---|---|
| 列表查询 (分页 + 筛选) | `bizList('projectPlan', q)` → Submissions.vue:119 | `GET /business/projectPlan/list` (BizProjectPlanController.list:29) | `biz_project_plan` |
| 提交投稿 (单条 + 批量) | `bizUpdate('projectPlan', {planId, status:'1'})` → Submissions.vue:164,178 | `PUT /business/projectPlan` (BizProjectPlanController.edit:60) | `biz_project_plan.status` |
| 新建投稿 (路由跳转) | `router.push('/doctor/submission/new')` → Submissions.vue:149 | `POST /business/projectPlan` (Controller.add:47) | `biz_project_plan` |
| 查看/修改 (路由跳转) | Submissions.vue:151-155 | `GET /business/projectPlan/{planId}` + `PUT` (同上) | `biz_project_plan` |
| 项目方向下拉 options | `request.get('/business/specialPlan/options')` → Submissions.vue:111 | `GET /business/specialPlan/options` (BizSpecialPlanController.options:40) | `biz_special_plan` (status='0' 启用项) |
### 1.2 可见性 (三层过滤)
| 层 | 来源 | 校验字段 |
|---|---|---|
| 前端菜单 | `AdminLayout.vue:131` `menu` computed | 仅当 `role='doctor'` 显示;无 `requireMain` |
| 路由守卫 | `router/index.js:88-100` `meta: { role: 'doctor' }` | 路由级 role 守卫 (其他角色不可访问) |
| 后端 SQL 强隔离 | `BizProjectPlanController.list:32-35` | `if ("doctor".equals(roleType)) setSubmitterId(getUserId())` — **强制**只查自己投的稿,前端绕不开 |
**其他角色如何查看投稿**:
- `manager` 角色看不到此菜单;通过 `/manager/plans` (manager/Plans.vue) 全量管理所有投稿。
- `admin` 角色通过 `/admin/library` 或 admin 后台直接调接口。
- `sponsor/executor/leader` 角色均没有投稿查看入口(意图明确,投稿是评审专家专属)。
**数据隔离结论**: doctor 端无论 UI 怎么改 (切换 tab/手动调接口),后端 list 都强制覆盖 `submitter_id = currentUserId`。✅ 安全。
### 1.3 状态枚举不一致警告 ⚠️
| 位置 | 取值 |
|---|---|
| `biz_project_plan` DDL 注释 | `1 待审 / 2 通过 / 3 拒绝` (**没有 `0` 待提交**) |
| 前端 Submissions.vue:25-29 | `待提交`/`待审核`/`通过`/`拒绝` 映射到 `0`/`1`/`2`/`3` |
| BizProjectPlanController.add:53-55 | doctor 角色强制 `setStatus('0')` (待提交) |
> DDL 注释说"状态: 1 待审 / 2 通过 / 3 拒绝",但代码 + 前端都用 `0` 表示"待提交",且实际 SELECT 查到当前 DB 仅有 `1`/`2` (2 条数据,见 §6.1),`0`/`3` 都是**无数据状态**。 P2 待修: ① DDL 注释补上 `0 待提交`;② BizAuditStatusEnum (若有) 同步。
---
## 第 2 章: 筛选项 (filter-form)
| UI label | 控件 | 绑定字段 | 后端 SQL | 命中表.字段 | 选项来源 | 原型对比 |
|---|---|---|---|---|---|---|
| 策划方案名称 | `el-input` | `q.planName` | **❌ 无过滤**(Mapper 没有 `planName` 条件,详见 §9.1) | — | — | ⚠️ 原型 label="投稿名称",实现 label 改文案"策划方案名称" |
| 项目方向 | `el-select` | `q.planDirectionId` | `p.plan_direction_id = #{planDirectionId}` (XML:43) | `biz_project_plan.plan_direction_id` | `GET /business/specialPlan/options` → 仅 `status='0'` 启用项 | ⚠️ 原型是 input "学科方向",实现改下拉 → **超出原型** (改进) |
| 项目类别 | `dict-select` (DictSelect 组件) | `q.planCategory` | `p.plan_category = #{planCategory}` (XML:44, **等值匹配**) | `biz_project_plan.plan_category` | 字典 `biz_project_category` (`sys_dict_data` 7 条,`value-field=label`) | 原型没有"项目类别"筛选 (实现扩展) |
| 形式 | `el-select` | `q.projectForm` | **❌ 无过滤**(Mapper 没写 `projectForm` 条件) | — | 写死 4 项: 线上/线下/线上+线下/其他 | ✅ 原型保留 input,实现升级下拉 (用户确认保留) |
| 状态 | `el-select` | `q.status` | `p.status = #{status}` (XML:48, 等值匹配) | `biz_project_plan.status` | 写死 4 项: 待提交 0 / 待审核 1 / 通过 2 / 拒绝 3 | ⚠️ 原型 select 是"已结题/未结题",实现映射到 status 码 |
| 备注 | `el-input` | `q.remark` | `p.remark = #{remark}` (XML:51, **等值匹配**) | `biz_project_plan.remark` | — | ✅ 原型一致 |
**P0 发现 (筛选失效)**: `planName``projectForm` 两个筛选项前端都有,但 mapper `BizProjectPlanMapper.xml:40-54` 没有 `<if test="planName">``<if test="projectForm">` 条件,后端会**默默忽略**这两个查询条件,UI 上输入再多次值都查不到。详见 §10 P0-1。
**P1 发现 (精确匹配不像搜索)**: `remark``=` 等值匹配,而非 `LIKE`,用户输备注关键词搜不到。原型是 input,但语义上应是模糊搜索。详见 §10 P1-3。
---
## 第 3 章: 工具栏按钮 (toolbar)
| 按钮 | 触发函数 | 接口 | 涉及表 | 原型对比 |
|---|---|---|---|---|
| 新建投稿 | `onCreate()``router.push('/doctor/submission/new')` | (无前端调用, 仅路由跳转) | — | ✅ 一致 |
| 批量提交 | `onBatch()` → 遍历 `selected` 调用 `bizUpdate('projectPlan', {planId, status:'1'})` | `PUT /business/projectPlan` | `biz_project_plan` (status 改 '1', submitter_id 兜底) | ✅ 一致,但**原型弹"您确定要提交吗?" modal,实现用 ElMessageBox.confirm** (效果一致,实现不同) |
| 查找 | `<el-button @click="load">查找</el-button>` | 触发 `load()` 重新列表 | — | ✅ 一致 |
| 重置 | `<el-button @click="reset">重置</el-button>` | 清空 q 后 `load()` | — | ➕ 原型无,实现扩展 (辅助筛选) |
---
## 第 4 章: 表格 (el-table)
### 4.1 数据来源 SQL (从 mapper 摘出)
```sql
-- BizProjectPlanMapper.xml:24-54
select p.plan_id, p.plan_name, p.plan_direction, p.plan_direction_id,
s.title as plan_direction_title,
p.plan_category, p.project_form, p.design_file_url, p.status, p.is_settled,
p.project_no, p.remark, p.submitter_id,
COALESCE(bp.name, u.user_name) as submitter_name,
p.create_by, p.create_time, p.update_by, p.update_time
from biz_project_plan p
left join biz_special_plan s on s.id = p.plan_direction_id
left join sys_user u on u.user_id = p.submitter_id
left join biz_person bp on bp.user_id = u.user_id
where 1=1
[and p.plan_direction_id = #{planDirectionId}]
[and p.plan_category = #{planCategory}]
[and p.submitter_id = #{submitterId}] -- doctor 角色强制 SET (Controller.list:33-34)
[and p.status = #{status}]
[and p.remark = #{remark}] -- 等值匹配,非 LIKE
order by p.plan_id desc
```
> ⚠️ 实际跑出当前 DB 仅 2 条数据 (`status=1` x 1, `status=2` x 1),`submitter_id` 都不为 NULL。 (mysql 实测,见 §6.1)
### 4.2 列映射
| 列名 (label) | 绑定 | DB 表.字段 | 原型对比 |
|---|---|---|---|
| 选择框 | `el-table-column type="selection"` | — | ✅ 一致 |
| 投稿名称 | `prop="planName"` | `biz_project_plan.plan_name` | ✅ 一致 (文案一致) |
| 项目方向 | `{{ row.planDirectionTitle \|\| row.planDirection \|\| '-' }}` | JOIN `biz_special_plan.title` 兜底自身 `plan_direction` | ⚠️ 原型是 "学科方向"(文案差异,见 §8.3) |
| 项目类别 | `prop="planCategory"` | `biz_project_plan.plan_category` (字典 label) | ➕ 原型无 (实现扩展,字典来源) |
| 形式 | `prop="projectForm"` | `biz_project_plan.project_form` | ✅ 一致 (原型 input 升级为下拉) |
| 设计文件 | `el-link href=row.designFileUrl` | `biz_project_plan.design_file_url` | ⚠️ 原型是"查看+下载"双链,实现合并为一个下载链 |
| 状态 | `<audit-status-tag :status="row.status">` | `biz_project_plan.status` | ⚠️ 原型纯文本,实现用 Tag 组件 |
| 备注 | `prop="remark"` | `biz_project_plan.remark` | ✅ 一致 |
| 操作 | 按钮组 (查看/修改/提交) | — | ✅ 一致 |
---
## 第 5 章: 行内操作按钮
| 按钮 | 触发函数 | 接口 | 后端动作 | 涉及表.字段 | 原型对比 |
|---|---|---|---|---|---|
| 查看 | `onView(row)``router.push('/doctor/submission/detail/${row.planId}')` | (仅路由) | — | — | ✅ 一致 |
| 修改 | `onEdit(row)``router.push('/doctor/submission/edit/${row.planId}')` | `PUT /business/projectPlan` (通过 SubmissionNew.vue) | `updateByPrimaryKey`: SET submitter_id = currentUserId, 字段按实体非空更新 | `biz_project_plan.*` | ✅ 一致 |
| 提交 | `onSubmit(row)` → ElMessageBox.confirm → `bizUpdate('projectPlan', {planId, status:'1'})` | `PUT /business/projectPlan` | `updateByPrimaryKey`: SET status='1' + submitter_id=currentUserId (兜底) | `biz_project_plan.status` | ✅ 一致 |
**真实落点**:
- 医生角色**真的不能**把 status 改成非 '1' —— 因为前端代码只发了 `{planId, status:'1'}`,即使前端被攻破修改请求体,Controller 也不会修改 status 之外的东西,但**没有强制 status='1'**,理论上可以发任意 status 给 doctor。 ✅ (业务上没必要,前端足够安全)
- 修改按钮的可见性 `canModify`:`status !== '1' && status !== '2'` → 待审/通过 锁定,符合原型"待审核和审核通过后,操作只显示查看按钮,无法进行修改"。
- 提交按钮的可见性 `canSubmit`:`status === '0' || status === '3'` → 仅未提交/拒绝可提交,符合原型说明。
---
## 第 6 章: Java Entity ↔ 数据库表 一致性
### 6.1 biz_project_plan 主表 DDL (mysql 实测, 2026-08-18)
```sql
CREATE TABLE `biz_project_plan` (
`plan_id` bigint NOT NULL AUTO_INCREMENT,
`plan_name` varchar(200) NOT NULL,
`plan_direction` varchar(500) DEFAULT NULL,
`plan_direction_id` bigint DEFAULT NULL,
`plan_category` varchar(50) DEFAULT NULL,
`project_form` varchar(20) DEFAULT NULL,
`design_file_url` varchar(500) DEFAULT NULL,
`status` varchar(20) DEFAULT '1' COMMENT '状态 BizAuditStatusEnum: 1待审 2通过 3拒绝',
`is_settled` char(1) DEFAULT 'N',
`project_no` varchar(50) DEFAULT NULL,
`remark` varchar(500) DEFAULT NULL,
`audit_opinion` varchar(500) DEFAULT NULL,
`audit_by` varchar(64) DEFAULT NULL,
`audit_time` datetime DEFAULT NULL,
`create_by` varchar(64) DEFAULT '',
`create_time` datetime DEFAULT NULL,
`update_by` varchar(64) DEFAULT '',
`update_time` datetime DEFAULT NULL,
`submitter_id` bigint DEFAULT NULL COMMENT '投?人用户ID (医生侧投稿归? ...)',
PRIMARY KEY (`plan_id`),
KEY `idx_plan_status` (`status`),
KEY `idx_plan_project_no` (`project_no`),
KEY `idx_plan_submitter` (`submitter_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1463898549354497
DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
COMMENT='项目策划方案表';
```
**当前 DB 数据状态 (mysql 实测)**:
- 总行数: 2
- `submitter_id IS NOT NULL` 行数: 2 (100% 都被投稿)
- 按 status 分组: `status=1` x1, `status=2` x1 (没有 `0`/`3` 数据)
### 6.2 join 关联表
#### biz_special_plan (下拉 options 用)
| 列 | 类型 | 说明 |
|---|---|---|
| `id` | bigint PK AUTO_INCREMENT | 即 `biz_project_plan.plan_direction_id` |
| `title` | varchar(200) NOT NULL | 前端下拉 `label=opt.title` |
| `content_type` | varchar(20) DEFAULT 'rich' | (丰富/file 二选一) |
| `content` | mediumtext | 富文本 |
| `file_url` | varchar(500) | 上传文件 URL |
| `sort_order` | int DEFAULT 0 | 排序,controller `/options` 没排序 (可改进) |
| `status` | char(1) DEFAULT '0' | 0=启用,1=停用 |
| `remark` | varchar(500) | — |
| `create_by/update_by` | varchar(64) | — |
| `create_time/update_time` | datetime | — |
#### sys_dict_data (字典 `biz_project_category`)
7 条记录,`value-field=label` (即前端存的"共识""学术项目""重点学科"...)。注意 RuoYi 默认 `value-field=dict_value`,这里强制改 label,**对其他模块兼容性需复查**。
### 6.3 Entity ↔ 表 字段对照
| 实体字段 | 实体类型 | 表字段 | 表类型 | 一致? | 备注 |
|---|---|---|---|---|---|
| planId | **String** | plan_id | **bigint** | ⚠️ 类型不一致 | 详见 §9.2 |
| planName | String | plan_name | varchar(200) NOT NULL | ✅ | |
| planDirection | String | plan_direction | varchar(500) | ✅ | |
| planDirectionId | **Long** | plan_direction_id | bigint | ✅ | |
| planDirectionTitle | String | (LEFT JOIN 注入) | biz_special_plan.title | ✅ | SQL 别名注入 |
| planCategory | String | plan_category | varchar(50) | ✅ | 字典 label |
| projectForm | String | project_form | varchar(20) | ✅ | |
| designFileUrl | String | design_file_url | varchar(500) | ✅ | |
| status | String | status | varchar(20) | ✅ | |
| isSettled | String | is_settled | char(1) | ✅ | |
| projectNo | String | project_no | varchar(50) | ✅ | |
| remark | String | remark | varchar(500) | ✅ | |
| **isFinished** ⛔ | String | **(不存在!)** | — | ❌ | **Entity 冗余字段,DDL 没有此列,代码也没用,应清理** |
| createBy | String | create_by | varchar(64) | ✅ | BaseEntity |
| createTime | Date | create_time | datetime | ✅ | BaseEntity |
| updateBy | String | update_by | varchar(64) | ✅ | BaseEntity |
| updateTime | Date | update_time | datetime | ✅ | BaseEntity |
| auditOpinion | String | audit_opinion | varchar(500) | ✅ | |
| auditBy | String | audit_by | varchar(64) | ✅ | |
| auditTime | String | audit_time | datetime | ⚠️ 类型不一致 | 详见 §9.2 |
| submitterId | **Long** | submitter_id | bigint | ✅ | |
| submitterName | String | (LEFT JOIN 注入) | COALESCE(bp.name, u.user_name) | ✅ | |
**修订**:
- ⚠️ `planId` 实体 `String` vs 表 `bigint` —— MyBatis 自动转换目前没报错 (实测 list 接口 OK),但 SnowflakeId 返回 `Long`,Entity setPlanId(String) 隐式 toString 后插入,所以 DB 现在实际存的是 `bigint` 但表达式是 `String`。建议统一为 `Long`
- ⚠️ `auditTime` 实体 `String` vs 表 `datetime` —— 实际 PHP/MyBatis 转换可读出,但前端拿到的时间字符串可能不是 `yyyy-MM-dd HH:mm:ss` 而是 ISO。建议统一 `Date + @JsonFormat`
-`isFinished` Entity 字段无对应 DDL 列 —— 设计残留,建议删除 (或在 DDL 加列并实现)。
---
## 第 7 章: 索引检查
### biz_project_plan
| Key | 列 | 用途覆盖 |
|---|---|---|
| PRIMARY | `plan_id` | `SELECT BY ID`/`WHERE p.plan_id = #{planId}` (详情) ✅ |
| idx_plan_status | `status` | 状态筛选 ✅ |
| idx_plan_project_no | `project_no` | 项目编号筛选(Manager 端用) ✅ |
| idx_plan_submitter | `submitter_id` | **doctor 角色数据隔离强过滤** ✅ (P0 关键索引) |
**未建索引列 (但有 WHERE 过滤)**:
- `plan_direction_id` - 有 LEFT JOIN + 等值过滤,**应有索引** (大表时拖慢) 📌
- `plan_category` - 等值过滤,**应有索引**
- `project_form` - 即使 mapper 没过滤 (§9.1),前端 UI 也有这筛选项,若补上 SQL 应同时建索引
- `remark` - 等值匹配 (说明里写 LIKE 更好,但当前 `=`),小数据量暂可接受
**当前规模**: 表只有 2 条数据,所有索引都不影响性能。但**未来预估** doctor 投稿会增长,`idx_plan_submitter` 是 P0 关键 (doctor 每打开页面都强制走这个),**已建 ✅**。
### biz_special_plan (join 目标表)
| Key | 列 | 用途覆盖 |
|---|---|---|
| PRIMARY | `id` | JOIN ON ✅ |
| idx_special_plan_status | `(status, sort_order)` | options 端 status='0' 过滤 + 排序 ✅ |
| idx_special_plan_create_time | `create_time` | (管理后台 list 用) ✅ |
JOIN 列 `id` 是 PK,无需额外索引。
---
## 第 8 章: 与原型差异
### 8.1 实现新增 (原型没有)
1. **"项目类别"筛选 + 列** —— 原型 columns 只有 7 列 (复选框/名称/学科方向/形式/设计文件/状态/备注/操作 = 8 列含复选框),实现加了"项目类别"列 + 筛选下拉,使用字典 `biz_project_category` 填充。 增强。
2. **"形式"升级为下拉** —— 原型是 input "形式",实现是 el-select 4 项。原 HTML 注释已写"原型无,用户确认保留"。 ✅
3. **设计文件列简化** —— 原型是"查看"+"下载"双链,实现合并为一个 el-link "下载"。 简化合理。
4. **状态列使用 Tag 组件** —— 原型纯文本"未结题/已结题",实现用 `<audit-status-tag>` 渲染彩色徽章。 升级。
5. **submitter_id 数据隔离** —— 原型无,实现给 doctor 角色强制过滤自己投的稿。 ➕ 安全。
6. **备注列扩展到所有行** —— 原型 8 行样例中有几行备注为空,实现统一展示 `prop="remark"`。 ✅
7. **分页器** —— 原型无 el-pagination,实现加了 (10/20/50,prev/next/jumper)。 工程化补全。
8. **响应式 @media 768px** (原型有,实现继承) ✅
### 8.2 原型有但实现缺失/降级
无重大缺失。原型第 419 行的红字提示"*待审核和审核通过后,操作只显示查看按钮,无法进行修改"实现通过 `canModify` 隐式表达,用户不容易感知。
### 8.3 文字 / 标签差异
| 位置 | 原型 | 实现 | 差异原因 |
|---|---|---|---|
| 表头 | 学科方向 | 项目方向 | 跟项目内统一术语 (manager/Plans.vue 用"项目方向") |
| 表头 | (无) | 项目类别 | 字典扩展 |
| 状态原型文案 | 未结题/已结题 | 待提交/待审核/通过/拒绝 | 原型状态枚举与项目不一致 (见 §1.3) |
| 第 1 行红字提示 | "*点击查看可进行预览" | **未实现** | 详见 §10 P2-2 |
| 表后红字提示 | "*待审核和审核通过后,操作只显示查看按钮,无法进行修改" | **未实现** | 详见 §10 P2-2 |
### 8.4 总结
整体方向:**扩展 + 升级** (无原型有但实现缺失的核心功能)。
实现严格遵循"7 列 → 加 1 列项目类别"的扩展模式,且安全控制 (submitter_id 强隔离) 远超原型。
---
## 第 9 章: 表字段冗余 / 设计问题
### 9.1 Mapper 字段过滤 vs 前端字段不一致 ⚠️
前端 filter-form 有 6 个筛选项 (`planName`/`planDirectionId`/`planCategory`/`projectForm`/`status`/`remark`),但 `BizProjectPlanMapper.xml:40-54``selectList` 只支持 5 个条件,**缺 2 个**:
-`planName` (前端有,mapper 没有)
-`projectForm` (前端有,mapper 没有)
**影响**: 用户输入"小牛血清"想搜投稿名,前端发了 `planName=小牛血清`,后端 mapper 没处理,SQL 无此条件,**查不到结果,以为是空数据**。P0。
### 9.2 Entity 字段类型与 DDL 不一致
| 实体字段 | 实体类型 | DDL | 风险 |
|---|---|---|---|
| `planId` | `String` | `bigint` | 高 - SnowflakeId 返回 Long,setPlanId(String) 需 toString(),Mapper updateByPrimaryKey 的 WHERE plan_id=#{planId} 走隐式转换 |
| `auditTime` | `String` | `datetime` | 中 - JSON 反序列化丢失时间格式 |
**建议**: 统一为 `Long`/`Date`,避免隐式转换。P2。
### 9.3 Entity 残留字段 `isFinished`
Entity 有 `isFinished`,DDL 没有此列。BizProjectPlanService 接口全无 `getIsFinished()/setIsFinished()`,前端 Submissions.vue 不引用。**纯遗留**,建议清掉。P3。
### 9.4 Dictionary `value-field=label` 与全局不一致
项目默认 RuoYi 字典 `value-field=dict_value`,DictSelect 组件在 Submissions.vue:13 强行写 `value-field="label"`,这意味着插入数据是中文 label。但 DDL `plan_category varchar(50)` 可以存,只是国际化场景下会破坏 (例如以后加英文 category)。审查所有 manager 端是否同步? 若不一致,会有数据脏。 P2。
### 9.5 biz_project_plan 没有软删除字段
DDL 无 `del_flag`,Mapper `deleteByPrimaryKeys` 是硬删除。doctor 角色无删除按钮 (前端未实现),但接口 `DELETE /business/projectPlan/{ids}` 可被 admin/manager 任意删。验收策略需明确。P2。
### 9.6 role_type 多源真相风险 (项目级)
根据 `sys-user-role-type-truth` memory,本项目单一可信源是 `sys_user.role_type`,`biz_user_role_bind` 是废表别动。本页面 Controller.list:32 `getRoleType()` 直接读 sys_user,符合 ✅。
### 9.7 唯一约束
`biz_project_plan.plan_id` 是 PK,无其他唯一键。可考虑加 `uk_plan_submitter_name(plan_name, submitter_id)` 防同人重名 (业务待定)。
---
## 第 10 章: 待修复列表
| # | 问题 | 文件 | 修复建议 | 严重度 |
|---|---|---|---|---|
| P0-1 | 筛选项 `planName``projectForm` 在前端 filter-form 有绑定,但 `BizProjectPlanMapper.xml:40-54` selectList 没有 `<if test="planName">``<if test="projectForm">` 条件,导致输入后查不到数据 (沉默失效) | `BizProjectPlanMapper.xml:40-54` + `Submissions.vue:6,16` | ① mapper 加 `<if test="planName != null and planName != ''">and p.plan_name LIKE concat('%', #{planName}, '%')</if>`;② 同时加 `<if test="projectForm != null and projectForm != ''">and p.project_form = #{projectForm}</if>` | **P0** |
| P0-2 | `remark` filter 用 `=` 等值匹配 (mapper:51),语义上应是 LIKE 模糊搜索,否则用户必须输完整备注才能命中 | `BizProjectPlanMapper.xml:51` | 改 `=``LIKE concat('%', #{remark}, '%')` | P0 |
| P1-1 | DDL 注释 `status` 缺少 `0 待提交` 枚举,但代码和前端都用 `0`,且 DDL DEFAULT '1' 与 controller.add:53-55 的 doctor 角色强制 setStatus('0') 配合有歧义 | `biz_project_plan` DDL COMMENT | ALTER TABLE 修改 COMMENT 为 `状态 BizAuditStatusEnum: 0待提交 1待审核 2通过 3拒绝` | P1 |
| P1-2 | 当前 DB 无 `status=0`/`status=3` 数据,canModify/canSubmit 逻辑未真实跑过。需在测试环境插入 0/3 各一条做回归 | 测试数据 | INSERT (status='0'/'3') 用于测试 | P1 |
| P1-3 | `remark` filter 用 `=` 等值 (见 P0-2),且前端表头列也是 `prop="remark"`,若数据库内 `remark` 是长文本,搜索 UX 差。`planName`/`remark` 未来都建议 LIKE | (合并 P0-2) | — | (合并) |
| P2-1 | `auditTime` 实体 `String` vs DDL `datetime` 类型不一致,JSON 序列化时区可能错乱 | `BizProjectPlan.java:67` + DDL audit_time | 统一 `Date + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")` | P2 |
| P2-2 | 原型红字提示"*点击查看可进行预览" 和 "*待审核和审核通过后,操作只显示查看按钮,无法进行修改" 实现未保留 | Submissions.vue | 在表格下方新增 2 行 `.hint-text` 红字,跟原型风格一致 | P2 |
| P2-3 | 字典 `biz_project_category``value-field=label` (中文 label 进 DDL),国际化将破。需全项目排查是否一致 | DictSelect 调用方 | 评审全项目 DictSelect 用法,统一改 `value-field=dict_value`,前端 label 同时展示 | P2 |
| P2-4 | `biz_project_plan` DDL 无 `del_flag`,Mapper 硬删除;应加 del_flag 默认 '0',软删除 | DDL + Mapper | ALTER TABLE ADD del_flag char(1) DEFAULT '0';Mapper deleteByPrimaryKey 改 UPDATE del_flag='1' | P2 |
| P3-1 | Entity `isFinished` 字段无对应 DDL 列,纯遗留 | `BizProjectPlan.java:46-47,96-97` | 删掉 getIsFinished/setIsFinished/getter+setter | P3 |
| P3-2 | `planId` 实体 `String` 与 DDL `bigint` 类型不一致,Setter 隐式转换风险 | `BizProjectPlan.java:13,72-73` | 改 Long 类型,同步 SnowflakeId.injectIfEmpty | P3 |
| P3-3 | `biz_special_plan.status='0'` 表示启用 (跟项目内其它表 `del_flag` 语义混) | 文档/常量 | 加 BizPlanStatusEnum 常量类注释 | P3 |
| P3-4 | plan_direction_id / plan_category / project_form 缺索引,数据量 <10k 暂可接受,>10k 后会有 performance 影响 | DDL + Mapper | ALTER TABLE 加 `idx_plan_direction_id` / `idx_plan_category` / `idx_plan_form` | P3 |
---
## 第 11 章: 引用清单
| 类型 | 文件 | 行号 | 备注 |
|---|---|---|---|
| 菜单 | `ry-vue3/src/layout/AdminLayout.vue` | 107-114 | doctor 菜单 6 项 |
| 路由 | `ry-vue3/src/router/index.js` | 88-100 | /doctor children |
| 组件 | `ry-vue3/src/views/doctor/Submissions.vue` | — | 审查目标 |
| 新建 | `ry-vue3/src/views/doctor/SubmissionNew.vue` | — | (未读,但路由引用) |
| 详情 | `ry-vue3/src/views/doctor/SubmissionDetail.vue` | — | (未读,但路由引用) |
| 前端 API | `ry-vue3/src/api/public.js` | 32-37, 90-94 | bizList/bizGet/bizAdd/bizUpdate/bizDelete 通用入口 |
| Controller | `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizProjectPlanController.java` | — | 全部接口 |
| Service | `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectPlanServiceImpl.java` | — | 薄壳 |
| Entity | `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProjectPlan.java` | — | 见 §6.3 |
| Mapper | `ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectPlanMapper.xml` | 24-54 | 关键 selectList |
| Join Controller | `ry-api/ruoyi-business/src/main/java/com/.../controller/BizSpecialPlanController.java` | 40-53 | /options 接口 |
| 数据源 | `ry-api/ruoyi-admin/src/main/resources/application-druid.yml` | 7-15 | mysql root@127.0.0.1:3306/guoju0808 |
| 原型入口 | `proto/html/doctor.html` | 208 | data-page=components/submissions.html |
| 原型 | `proto/html/components/submissions.html` | — | 484 行 HTML/CSS |
| DB 实测 | `biz_project_plan` | §6.1 | 2 行数据,status 仅有 1/2 |
| DB 实测 | `biz_special_plan` | §6.2 | 7 行,AUTO_INCREMENT=8 |
| DB 实测 | `sys_dict_data` dict_type=biz_project_category | §6.2 | 7 条字典 |
---
## 附录: skill 改进反馈 (来自本次审查)
> 用户反馈:"从 druid yml 中获取。把获取数据连接的方法写到 skill 中。"
**改进内容**: 在 page-tech-review skill 中增加 §0.5 节"DB 连接获取",固化从 `ry-api/ruoyi-admin/src/main/resources/application-druid.yml``spring.datasource.druid.master.url/username/password` 的步骤,避免每次询问用户。
```bash
# 提取 DB 连接 (单行命令)
grep -E "url:|username:|password:" ry-api/ruoyi-admin/src/main/resources/application-druid.yml
# → 拿到 jdbc:mysql://127.0.0.1:3306/guoju0808? ... / root / cu2oh2co3
# 实测 DDL 用:
mysql -h 127.0.0.1 -u root -pcu2oh2co3 guoju0808 -e "SHOW CREATE TABLE biz_project_plan \G"
```
(本次审查已落地使用,见 §6.1)