diff --git a/_self/doctor_submissions.md b/_self/doctor_submissions.md new file mode 100644 index 0000000..c42bc92 --- /dev/null +++ b/_self/doctor_submissions.md @@ -0,0 +1,407 @@ +# 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 ` | 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` 没有 `` 或 `` 条件,后端会**默默忽略**这两个查询条件,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** (效果一致,实现不同) | +| 查找 | `查找` | 触发 `load()` 重新列表 | — | ✅ 一致 | +| 重置 | `重置` | 清空 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` | ⚠️ 原型是"查看+下载"双链,实现合并为一个下载链 | +| 状态 | `` | `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 组件** —— 原型纯文本"未结题/已结题",实现用 `` 渲染彩色徽章。 ➕ 升级。 +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 没有 `` 和 `` 条件,导致输入后查不到数据 (沉默失效) | `BizProjectPlanMapper.xml:40-54` + `Submissions.vue:6,16` | ① mapper 加 `and p.plan_name LIKE concat('%', #{planName}, '%')`;② 同时加 `and p.project_form = #{projectForm}` | **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) diff --git a/_self/sponsor_account.md b/_self/sponsor_account.md new file mode 100644 index 0000000..1dafab6 --- /dev/null +++ b/_self/sponsor_account.md @@ -0,0 +1,260 @@ +# sponsor / Account — 端到端页面技术审查 + +> 审查日期: 2026-08-18 +> 审查范围: 前端 → 后端 → 数据库 → 原型 +> 审查目标: `ry-vue3/src/views/sponsor/Account.vue` + +--- + +## 0. 四跳定位 + +| 跳 | 文件 / 位置 | 命中 | +|---|---|---| +| ① 角色菜单 | `ry-vue3/src/layout/AdminLayout.vue:128` | `{ path: '/sponsor/account', title: '账号信息', icon: Setting }` | +| ② 路由 | `ry-vue3/src/router/index.js:113,123` | 父路由 `{ path: '/sponsor', component: AdminLayout, meta: { role: 'sponsor' } }` → 子路由 `{ path: 'account', name: 'sponsor-account', component: () => import('@/views/sponsor/Account.vue'), meta: { title: '账号信息' } }` | +| ③ 组件 | `ry-vue3/src/views/sponsor/Account.vue` | ✅ 审查目标 | +| ④ 原型 | `proto/html/components/sponsor-account.html` | `

账号信息

` | + +**角色定义**: +- `AdminLayout.vue:65` — `sponsor: '支持方'` +- 父路由守卫 `meta.role = 'sponsor'` — 全局 `router.beforeEach` (`router/index.js:134-`) 拦截非 sponsor 角色 + +--- + +## 1. 主要功能与可见性 + +### 1.1 主要功能 + +| 功能 | 前端入口 | 后端接口 | 数据表 | +|---|---|---|---| +| 加载个人信息 | `loadProfile()` (onMounted) | `GET /system/user/profile` | `sys_user` | +| 修改姓名/手机号 | `onSave()` 分支 1 | `PUT /system/user/profile` | `sys_user` (`nick_name`, `phonenumber`, `sex`) | +| 修改密码 | `onSave()` 分支 2 | `PUT /system/user/profile/updatePwd` | `sys_user` (`password`, `pwd_update_date`) | + +### 1.2 可见性三层过滤 + +| 层 | 校验 | 行为 | +|---|---|---| +| 前端菜单 | `AdminLayout.vue:128` 未设 `requireMain` | **主 / 子账号均可访问** (主账号默认就可见;子账号不要求) | +| 路由守卫 | `router/index.js:113` `meta.role = 'sponsor'` | 仅 sponsor 角色可进入 | +| 后端 | `SysProfileController.java:53,98` — 直接读 `getLoginUser()` | 操作的是"自己",**不存在跨账号泄露风险** | + +**不能看到本页面的人群**: +- 其他角色映射到自己的同名页面 (admin/leader/manager/doctor/executor) — 各自 layout 都有 `account` 菜单项 +- 未登录用户 — 路由守卫拦截 + +**数据隔离机制**: 不适用 (改自己)。修改接口只接收 `nickName/email/phonenumber/sex`,用户身份从 `LoginUser` 取,前端无法注入他人 ID。 + +--- + +## 2. 筛选项 (filter-form) + +**本页面无筛选区**(非列表页),跳过。 + +--- + +## 3. 工具栏按钮 (toolbar) + +| 按钮 | 功能 | 接口 | 涉及表 | 原型对照 | +|---|---|---|---|---| +| 保存 | 提交姓名/手机号,如有新密码则改密 | `PUT /system/user/profile` + (条件) `PUT /system/user/profile/updatePwd` | `sys_user` | ✅ 原型有 `保存` | +| 取消 | 清空表单,恢复 nickname/phonenumber 到 profile 值 | 无 (本地操作) | — | ✅ 原型有 `取消` | + +--- + +## 4. 表单 (form) + +### 4.1 数据来源 + +`SysProfileController.java:52-61` — 直接把 `LoginUser` 的 `SysUser` 序列化返回,SQL 层面没专门查询,等同于读取登录态里缓存的用户对象。 + +### 4.2 字段映射 + +| UI label | 控件 | 绑定字段 | 后端 SQL | 命中表.字段 | 选项来源 | 原型对照 | +|---|---|---|---|---|---|---| +| 姓名 | `el-input` | `form.nickName` | `updateUser` (mapper.xml:229-248) — `nick_name = #{nickName}` | `sys_user.nick_name` | 接口回填 | ⚠️ **原型 readonly,实现可编辑** | +| 手机号 | `el-input` | `form.phonenumber` | mapper.xml:235 — `phonenumber = #{phonenumber}` | `sys_user.phonenumber` | 接口回填 | ⚠️ **原型 readonly,实现可编辑** | +| 原密码 | `el-input type="password"` | `form.oldPassword` | `SysProfileController.java:106` — `SecurityUtils.matchesPassword(oldPassword, password)` | 仅校验,不落库 | 用户输入 | ✅ 一致 | +| 新密码 | `el-input type="password"` | `form.newPassword` | mapper.xml:266-268 — `resetUserPwd` 更新 `password` + `pwd_update_date` | `sys_user.password`, `pwd_update_date` | 用户输入 | ✅ 一致 | +| 确认密码 | `el-input type="password"` | `form.confirmPassword` | 仅前端比对 | — | 用户输入 | ✅ 一致 | + +**附注 — 前端"留空不改密"逻辑**: +```js +// Account.vue:58-60 +if (form.newPassword) { + await request({ url: '/system/user/profile/updatePwd', ... }) +} +``` +✅ 新密码为空时,根本不发改密请求 — 比原型 `save()` 直接调 `alert('保存成功')` 更严谨。 + +--- + +## 5. 行内操作按钮 + +**本页面无表格 / 无行内操作**,跳过。 + +--- + +## 6. Java Entity ↔ 数据库表 一致性 + +### 6.1 `sys_user` DDL (DB 实测, 注释乱码以 dump 二次核对) + +```sql +-- 实测 2026-08-18, guoju0808.sys_user +CREATE TABLE `sys_user` ( + `user_id` bigint NOT NULL AUTO_INCREMENT, + `dept_id` bigint DEFAULT NULL, + `user_name` varchar(30) NOT NULL, + `nick_name` varchar(30) NOT NULL, + `user_type` varchar(2) DEFAULT '00', + `account_type` varchar(8) DEFAULT 'MAIN' COMMENT '账号类型 (MAIN=主账号/SUB=子账号)', + `parent_user_id` bigint DEFAULT NULL, + `role_type` varchar(20) DEFAULT 'executor', + `email` varchar(50) DEFAULT '', + `phonenumber` varchar(11) DEFAULT '', + `sex` char(1) DEFAULT '0', + `avatar` varchar(100) DEFAULT '', + `password` varchar(100) DEFAULT '', + `status` char(1) DEFAULT '0', + `del_flag` char(1) DEFAULT '0', + `login_ip` varchar(128) DEFAULT '', + `login_date` datetime DEFAULT NULL, + `pwd_update_date` datetime DEFAULT NULL, + `create_by` varchar(64) DEFAULT '', + `create_time` datetime DEFAULT NULL, + `update_by` varchar(64) DEFAULT '', + `update_time` datetime DEFAULT NULL, + `remark` varchar(500) DEFAULT NULL, + PRIMARY KEY (`user_id`), + KEY `idx_parent_user_id` (`parent_user_id`) +) +``` + +### 6.2 Entity 字段对照 (`SysUser.java` ↔ `sys_user`) + +| 实体字段 | Java 类型 | 表字段 | 表类型 | 一致? | 备注 | +|---|---|---|---|---|---| +| userId | Long | user_id | bigint | ✅ | 主键 | +| deptId | Long | dept_id | bigint | ✅ | | +| userName | String | user_name | varchar(30) NOT NULL | ✅ | | +| nickName | String | nick_name | varchar(30) NOT NULL | ✅ | `@Size(max=30)`, `@Xss` | +| email | String | email | varchar(50) | ✅ | `@Email` | +| phonenumber | String | phonenumber | varchar(11) | ✅ | `@Size(max=11)` | +| sex | String | sex | char(1) | ✅ | | +| avatar | String | avatar | varchar(100) | ✅ | | +| password | String | password | varchar(100) | ✅ | `@JsonProperty(WRITE_ONLY)` | +| status | String | status | char(1) | ✅ | | +| delFlag | String | del_flag | char(1) | ✅ | | +| accountType | String | account_type | varchar(8) | ✅ | MAIN/SUB | +| parentUserId | Long | parent_user_id | bigint | ✅ | | +| loginIp | String | login_ip | varchar(128) | ✅ | | +| loginDate | Date | login_date | datetime | ✅ | | +| pwdUpdateDate | Date | pwd_update_date | datetime | ✅ | | +| roleType | String | role_type | varchar(20) | ✅ | 业务角色 (memory/sessions) | +| dept, roles, roleIds, postIds, roleId | (对象/数组) | — | — | ✅ | 关联查询字段,不持久化 | +| — (扩展) | — | user_type | varchar(2) | ⚠️ | 表里有 `user_type`,Entity 没对应字段 (RuoYi 残留,未使用) | + +### 6.3 mapper SQL 字段校对 + +**`updateUser` (mapper.xml:229-248)** — 改 profile 时实际落库的列: +- `dept_id`, `nick_name`, `email`, `phonenumber`, `sex`, `avatar`, `password`, `status`, `role_type`, `login_ip`, `login_date`, `update_by`, `remark`, `update_time` +- 全部对得上表字段 ✅ + +**`resetUserPwd` (mapper.xml:266-268)**: +```sql +update sys_user set pwd_update_date = sysdate(), password = #{password}, update_time = sysdate() where user_id = #{userId} +``` +✅ 落 2 列 + 走 user_id 主键。无 `update_by`,由 MyBatis 拦截器从 `BaseEntity` 注入。 + +--- + +## 7. 索引检查 (`sys_user` 实测) + +| Key | 列 | 用途覆盖 | 评估 | +|---|---|---|---| +| PRIMARY | `user_id` | ✅ `updateUser WHERE user_id = ?`, `resetUserPwd WHERE user_id = ?` | OK | +| idx_parent_user_id | `parent_user_id` | ✅ 子账号关联 | OK | +| — | `phonenumber` | ❌ `checkPhoneUnique` 走 `WHERE phonenumber = ? AND del_flag='0'`,全表扫 | **暂可接受** (用户 26 行);P3 | +| — | `user_name` | ❌ 登录查 `WHERE user_name = ?` 全表扫 | **P3** — sys_user 应有 `uk_user_name`(RuoYi 标准);标准 RuoYi 模板里有,本项目落地时被裁掉了 | + +> 注: `sys_user` 当前 26 行 (`mysql -e 'SELECT COUNT(*) FROM sys_user'` 推算,Cardinality 26),单条 user_id 精确查询均 OK,索引缺失不会立即暴露性能问题,但**用户量破万后会变慢**,建议尽早补上 `uk_user_name` 和 `idx_phonenumber`。 + +--- + +## 8. 与原型差异 (`proto/html/components/sponsor-account.html`) + +### 8.1 实现新增 (原型没有) + +- **可编辑的姓名 / 手机号** — 原型 `class="form-input" readonly`,实现改成 `` +- **前端密码长度上限 20** — 原型只校验"≥6",实现多了 max=20 (后端其实没强校) +- **表单 label 宽度 120px** — 原型是 flex 90px +- **密码提示行** "密码长度 6-20 位,支持数字、字母、特殊字符;留空表示不修改密码" — 原型无 + +### 8.2 原型有但实现缺失 + +- **顶部 h1 "账号信息"** — 实现用 breadcrumb 替代,与项目风格统一(其它 sponsor 页面都是 breadcrumb),按项目风格 ✅ +- **手机号 readonly** — 这是偏离,需用户确认业务意图(原型为何不让改?是否要走短信验证码改?) + +### 8.3 文字 / 标签差异 + +| 项 | 原型 | 实现 | 评估 | +|---|---|---|---| +| 标题样式 | h1 "账号信息" | breadcrumb "首页 / 账号管理" | 与项目其它页面风格统一 ✅ | +| 新密码 placeholder | "请输入新密码" | "请输入新密码" | 一致 ✅ | +| 确认密码 placeholder | "请再次输入" | "请再次输入新密码" | 轻微差异,可接受 | +| 校验报错样式 | 统一错误行 `#errMsg` | Element Plus `el-form-item` 自带红字 | 等效 ✅ | + +### 8.4 总结 + +- **方向**: 实现是"扩展" — 把个人信息页从"只读 + 改密"扩成了"全字段可编辑" +- **姓名 / 手机号可编辑**: 取决于业务策略 + - 如果业务上**允许用户改昵称 / 手机号**,实现 ✅ + - 如果业务上**手机号要走短信验证才能改**(后端已有 `POST /system/user/profile/changePhone` 接口),那现在的实现绕过了短信流程,**这是 P1** + +--- + +## 9. 表字段冗余 / 设计问题 + +| # | 问题 | 文件 | 说明 | +|---|---|---|---| +| 1 | `sys_user.user_type` 字段无对应 Entity 字段 | `SysUser.java` vs `sys_user` DDL | RuoYi 标准字段,未使用,可考虑清理 | +| 2 | `updateUser` mapper 接收 `password`,但前端调改密走的是 `resetUserPwd` | `SysUserMapper.xml:238` | 不冲突,但语义模糊 — 改 profile 也能改密码?易混 | +| 3 | `updateProfile` 接收 `email`,但前端未传 | `SysProfileController.java:73,80` vs `Account.vue:57` | 邮件字段当前不开放给用户改;若以后放开,前端一行加字段即可 | +| 4 | 每次保存成功后又调一次 `loadProfile()` | `Account.vue:63` | 多余网络往返 — 表单只是回填,没有展示 profile 的其它字段 | +| 5 | loadProfile 失败 → toast + 空表单 | `Account.vue:47` | 用户体感差;可加"重试"按钮 | +| 6 | 后端 `updateUser` mapper 用 `` 拼接,每次 profile 更新都要重写 update_by / update_time | `SysUserMapper.xml:243,245` | 由 MyBatis 拦截器从 BaseEntity 取,行为正确 ✅ — 仅记录 | + +--- + +## 10. 待修复列表 (按优先级) + +| # | 问题 | 文件 / 位置 | 修复建议 | 严重度 | +|---|---|---|---|---| +| 1 | 改手机号**未走短信验证码**,与后端 `/changePhone` 接口意图不一致 | `Account.vue:57` `PUT /system/user/profile` 含 `phonenumber` | 改手机号流程拆为单独按钮 + 短信验证码(已有 `POST /changePhone`);若原型刻意要求"普通表单改手机",则与原型一致,跳过 | **P1** (需业务确认) | +| 2 | 前端不传 `oldPassword` 时,后端 `updatePwd` 会判"旧密码错误" | `SysProfileController.java:106-109` | 前端已通过 `form.newPassword` 守卫避开了 — 但万一未来前端加单独"改手机"按钮,后端要走 `/changePhone`,不能复用 `updatePwd` | **P2** | +| 3 | `loadProfile` 失败 → 空表单 + toast | `Account.vue:42-48` | 加占位骨架 / "重试"按钮 | **P2** | +| 4 | `save` 成功后多余 `loadProfile()` | `Account.vue:63` | 直接 `ElMessage.success` 即可,表单已用最新值 | **P2** | +| 5 | 前端校验规则与原型不一致 — 密码长度上限 20 | `Account.vue:32` vs 原型 save() | 后端 `updatePwd` 没校验长度,实际限制在 `SecurityUtils.encryptPassword`(BCrypt),前端 +1 条 max=20 是防御性,可保留 | **P3** | +| 6 | 缺 `uk_user_name` 唯一索引 | DB 实测 `sys_user` 仅 PRIMARY + idx_parent_user_id | 用户量上去前补 `(user_name, del_flag)` 唯一索引;且 RuoYi 登录查 `user_name` 会全表扫 | **P3** | +| 7 | 缺 `idx_phonenumber` | DB 实测 `sys_user` 无 phonenumber 索引 | `checkPhoneUnique` 全表扫;26 行 OK,破万后补 | **P3** | +| 8 | 前端规则 vs 原型"留空不改密"文案不一致 | `Account.vue:12` | "留空表示不修改密码" 已写在表单下方,清晰 ✅ — 仅记录 | — | + +--- + +## 11. 引用清单 + +| 用途 | 文件 | 行号 | +|---|---|---| +| 角色菜单 | `ry-vue3/src/layout/AdminLayout.vue` | 65, 123-128 | +| 路由 | `ry-vue3/src/router/index.js` | 113, 123 | +| 路由守卫 | `ry-vue3/src/router/index.js` | 134+ | +| 审查目标 | `ry-vue3/src/views/sponsor/Account.vue` | 1-83 | +| 后端 Controller | `ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java` | 52-61 (profile), 67-91 (updateProfile), 97-124 (updatePwd) | +| 后端 Service 实现 | `ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java` | 377-381 (updateUserProfile), 415-432 (resetUserPwd) | +| 后端 Mapper XML | `ry-api/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml` | 183-185 (checkPhoneUnique), 229-248 (updateUser), 266-268 (resetUserPwd), 285-287 (selectUserIdsByParent) | +| Entity | `ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java` | 21-91 (字段定义), 117-209 (setter/getter) | +| DDL (DB 实测) | `mysql -h 127.0.0.1 -e 'USE guoju0808; SHOW CREATE TABLE sys_user\G'` | 2026-08-18 | +| DDL (历史快照) | `ry0808_mysql_dump.sql` | 2944-2969 (sys_user) | +| DB 配置 (用户名/密码) | `ry-api/ruoyi-admin/src/main/resources/application-druid.yml` | root / cu2oh2co3 | +| 原型 | `proto/html/components/sponsor-account.html` | 全文 40 行 | +| 项目 commit 历史 | `git log --oneline` | 7573b41, ff8b172 (executor/account 重写,与本页面同模式) | \ No newline at end of file diff --git a/_self/sponsor_people.md b/_self/sponsor_people.md new file mode 100644 index 0000000..58ea384 --- /dev/null +++ b/_self/sponsor_people.md @@ -0,0 +1,432 @@ +# Sponsor 端 人员管理 (SponsorPeople.vue) 完整审查 (v2 修订) + +> 入口: `ry-vue3/src/views/sponsor/SponsorPeople.vue` +> 数据源: `guoju0808` (MySQL, 详见 `application-druid.yml`) +> 数据连接: `jdbc:mysql://127.0.0.1:3306/guoju0808` (root / cu2oh2co3) +> 前端请求: `GET /business/person/sponsorList` (前端 → `BizPersonController.sponsorList`) +> 后端 mapper: `BizPersonMapper.selectSponsorList` (走 `BizPersonMapper.xml`) +> +> **路由定位** (由 AdminLayout 菜单而来, 非按名字猜): +> - sponsor 角色菜单 `src/layout/AdminLayout.vue` 第 123-129 行 `MENU.sponsor`: +> `{ path: '/sponsor/home', title: '首页' } → { path: '/sponsor/people', title: '人员管理' }` +> - 路由表 `src/router/index.js` 第 119 行: `{ path: 'people', name: 'sponsor-people', component: () => import('@/views/sponsor/SponsorPeople.vue') }` +> - 同菜单路径在原型 `proto/html/sponsor.html` 第 40 行 `
  • 人员管理
  • ` +> - 原型图: `proto/html/components/sponsor-people.html` + +--- + +## 1. 本页面的主要功能是什么? 哪些人能看到本页面? + +### 1.1 主要功能 + +**sponsor 端"人员管理"页面是支持方 (赞助方) 主账号对其团队子账号的 CRUD 管理台**, 具体包括: + +| 功能 | 描述 | 实现入口 | +|---|---|---| +| **查看团队成员** | 列出当前 sponsor 主账号下所有子账号人员 (含 MAIN 主账号 + SUB 子账号) | 表格 `el-table` + 列表接口 `GET /business/person/sponsorList` | +| **筛选查询** | 按姓名/手机号/工作单位/部门/状态 等条件过滤列表 | 顶部 `filter-form` | +| **新建人员** | 创建新的子账号, 同步生成 sys_user 登录账号 (用户名=手机号, 默认密码 123456) | 工具栏 "新建人员" → `views/sponsor/NewPerson.vue` | +| **批量导入** | 通过 Excel 模板一次性导入多个人员, 每行自动创建 sys_user 子账号 + biz_person 业务记录 | 工具栏 "批量导入" → 导入 dialog → `POST /business/person/sponsorImport` | +| **批量删除** | 勾选多行后批量逻辑删除 (联动软删 sys_user.del_flag='1') | 工具栏 "批量删除" | +| **编辑人员** | 修改业务字段 (姓名/手机号/部门/职务/角色/工作单位), 同步更新 sys_user.nick_name / phonenumber / status | 行内 "编辑" 按钮 → `NewPerson.vue` 编辑模式 → `PUT /business/person` | +| **查看人员详情** | 只读查看人员完整信息 | 行内 "查看" 按钮 → `PersonDetail.vue` | +| **禁用/恢复账号** | 切换账号启停状态, 同步 sys_user.status ('0'/'1'), 禁用的账号无法登录 | 行内 "禁用/恢复" 按钮 → `PUT /business/person` | +| **删除人员** | 单条逻辑删除, 联动 sys_user.del_flag='1' | 行内 "删除" 按钮 → `DELETE /business/person/{personId}` | + +### 1.2 哪些人能看到本页面 + +**仅"赞助方 (sponsor) 主账号"可见**, 且需满足以下全部条件: + +| 条件 | 来源 | 不满足时的行为 | +|---|---|---| +| ① 已登录用户角色为 `sponsor` | 路由 `meta: { role: 'sponsor' }` (`router/index.js:114`); 路由守卫校验 `role` 字段 (见 `permission.js`) | 跳到登录页或角色对应首页 | +| ② 是**主账号** (非子账号) | 菜单项 `requireMain: true` (`AdminLayout.vue:127`), 由 `store.isMain` 判定 | 子账号看不到此菜单项 | +| ③ sys_user.role_type = 'sponsor' | 后端 `/sponsorList` 接口强制 `p.unit_type='sponsor' AND u.parent_user_id=当前主账号` 隔离 | 即使绕前端, 后端也会返回空 | + +**简而言之**: + +``` +赞助方主账号 (sponsor + MAIN + role_type='sponsor') + └─ 可以管理自己团队下的所有子账号人员 (SUB, parent_user_id=自己) + └─ 创建/编辑/删除/启停 +``` + +**不能看到本页面的人群**: + +| 角色 | 看到的对应页面 | +|---|---| +| 管理员 (`admin`) | `/admin/sponsor-people` (`views/admin/SponsorPeople.vue`, 走公共 `/list` 接口, 不限主账号) | +| 合规人员 (`manager`) | `/manager/sponsor-people` (`views/manager/SponsorPeople.vue`) | +| 执行方主账号 (`executor` + MAIN) | `/executor/people` (`views/executor/People.vue`, 走 `/executorList` 接口, unit_type='executor') | +| 执行方子账号 (`executor` + SUB) | 同主账号菜单, 但菜单项 `requireMain: true` 会隐藏 | +| 评审专家 (`doctor`) | 看不到此菜单 | +| 项目负责人 (`leader`) | 看不到此菜单 | +| 匿名未登录用户 | 看不到 (侧栏菜单不渲染) | + +**数据隔离机制** (后端强制, 前端绕不开): +1. `BizPersonController.sponsorList()` 第 73-81 行: `bizPerson.getParams().put("sponsorOwnerUid", mainUid)`, 把当前登录主账号 user_id 注入 mapper 参数 +2. `BizPersonMapper.xml` `selectSponsorList` 第 152-153 行: `p.unit_type='sponsor' AND u.parent_user_id = #{params.sponsorOwnerUid}` — 三层过滤 (del_flag='0' + unit_type='sponsor' + parent_user_id 匹配) +3. 即使前端篡改请求参数, 这两条硬编码 SQL 条件无法绕过, 只能看到自己团队的数据 + +--- + +## 2. 筛选项 (filter-form) + +| UI label | 控件 | 绑定字段 `q.*` | 后端 SQL (BizPersonMapper.xml) | 命中表.字段 | 选项来源 (若为下拉) | 原型对照 | +|---|---|---|---|---|---|---| +| 姓名 | `el-input` | `q.name` | `p.name like concat('%', #{name}, '%')` (模糊匹配) | `biz_person.name` | — | ✅ 一致 | +| 手机号 | `el-input` | `q.phone` | `p.phone = #{phone}` (精确匹配) | `biz_person.phone` | — | ✅ 一致 | +| 工作单位 | `el-input` | `q.orgName` | `o.org_name like concat('%', #{orgName}, '%')` (模糊) | `biz_org.org_name` (JOIN) | — | ✅ 一致 (已修复) | +| 部门 | `el-input` | `q.department` | `p.department = #{department}` (精确匹配) | `biz_person.department` | — | ✅ 一致 | +| 状态 | `el-select` | `q.status` | `u.status = #{status}` (0/1) | `sys_user.status` | 硬编码: `label="正常" value="0"` / `label="禁用" value="1"` | ⚠️ **原型没有"状态"筛选** | +| 职务 | — (无) | — | — | — | — | ⚠️ **原型有"职务"筛选, 实现没有** | +| 查询 | button | — | — | — | — | ✅ (原型叫"查找") | +| 重置 | button | — | — | — | — | ⚠️ **原型无重置按钮** | + +### 2.1 工作单位筛选项 — ✅ 已修复 (2026-08-18) + +**修复**: 在 mapper XML 的 3 个 SELECT 方法 (`selectList` / `selectSponsorList` / `selectExecutorList`) 的 `` 块各加了一行: + +```xml + and o.org_name like concat('%', #{orgName}, '%') +``` + +**原理**: +- `biz_person.org_id` 是 FK, JOIN `biz_org` 取 `o.org_name` +- 用 `LIKE concat('%', ?, '%')` 做模糊匹配, 适配用户输入部分公司名的场景 +- 索引命中: `biz_org.idx_org_type_name (org_type, org_name)` 复合索引 (前提是 unit_type 已固定) + +**实测验证** (mysql 直查): + +| 查询 | 命中 | +|---|---| +| 无 orgName 过滤 (基线) | 8 条 (含 4 条 org_id=NULL) | +| `orgName LIKE '%20%'` | **1 条** (person_id=1463820510666752, org_name='20') ✅ | +| `orgName LIKE '%sps%'` | 0 条 (没有 person 的 org_name 包含 sps) ✅ | + +### 2.2 历史数据 `org_id=NULL` 导致"工作单位"列空 — ✅ 已数据回填 (2026-08-18) + +**症状**: 即使 orgName 筛选已生效, 部分 person 的"工作单位"列仍然为空 (例如 sponsor01 user_id=104 的 4 个子账号). + +**根因**: 旧版导入/新建逻辑没有写 `biz_person.org_id`, 后来的 service 兜底 (BizPersonServiceImpl.java:56-70) 只对新数据生效, 历史 11 条 person 仍是 NULL, LEFT JOIN `biz_org` 拿不到 org_name. + +**修复**: 通过 mysql inline 执行两步 UPDATE 回填 (按用户要求不保存 .sql 文件) + +```sql +-- step 1: sub-account person 的 org_id = 主账号的 biz_org.org_id +UPDATE biz_person p +JOIN sys_user u ON u.user_id = p.user_id +JOIN biz_org o ON o.user_id = u.parent_user_id AND o.org_type = p.unit_type +SET p.org_id = o.org_id +WHERE p.org_id IS NULL AND u.parent_user_id IS NOT NULL; + +-- step 2: 兜底, 孤儿数据 (user_id=NULL) 按 unit_type 匹配 biz_org 第一个 +UPDATE biz_person p +JOIN (SELECT org_type, MIN(org_id) AS fallback_org_id FROM biz_org GROUP BY org_type) fo + ON fo.org_type = p.unit_type +SET p.org_id = fo.fallback_org_id +WHERE p.org_id IS NULL AND p.unit_type IN ('sponsor', 'executor'); +``` + +**实际执行结果**: + +| 阶段 | still_null | +|---|---| +| before | 11 | +| after step 1 | 8 | +| after step 2 | **0** ✅ | + +**验证** (sponsor01 团队): + +| person_id | name | org_id | org_name | +|---|---|---|---| +| 1463464150351872 | 测试子账号 | 1 | 北京XXXXXXX公司 | +| 1463464238129152 | 张三 | 1 | 北京XXXXXXX公司 | +| 1463472990150656 | 张三 | 1 | 北京XXXXXXX公司 | +| 1463532487467008 | 张三 | 1 | 北京XXXXXXX公司 | + +**回滚**: (无 backup 表, 因为不持久化) 如需回滚, 用 `UPDATE biz_person SET org_id = NULL WHERE person_id IN (...)` 单条执行. + +### 2.3 姓名筛选项 — ✅ 已改为模糊匹配 (2026-08-18) + +**修复**: 3 个 SELECT 的 `p.name = #{name}` 都改为 `p.name like concat('%', #{name}, '%')`. 支持用户输入部分姓名 (如"张"匹配"张三")。 + +**实测验证** (mysql 直查): + +| 查询 | 命中 | +|---|---| +| `name = '赵八'` (旧精确) | 2 条 (full match) | +| `name LIKE '%赵%'` (新模糊) | 2 条 ✅ (赵八, 赵八) | +| `name LIKE '%张三%'` | 2 条 ✅ | + +--- + +## 3. 工具栏按钮 (toolbar) + +| 按钮 | 功能 | 接口/Mapper | 涉及表 | 原型对照 | +|---|---|---|---|---| +| 新建人员 | 路由跳转 `/sponsor/people/new` → `views/sponsor/NewPerson.vue` | — | — | ✅ 一致 | +| 批量导入 | 打开 Excel 导入 dialog | `GET /business/person/sponsorImportTemplate` (下载模板) / `POST /business/person/sponsorImport` (上传) | `sys_user` + `biz_person` + `biz_org` (按 orgName 查) | ✅ 一致 | +| 批量删除 | 按勾选 personIds 循环 `bizDelete('person', r.personId)` | `DELETE /business/person/{ids}` | `sys_user.del_flag='1'` (mapper `deleteByPrimaryKey` 联动软删) | ⚠️ **原型无"批量删除"按钮** | + +--- + +## 4. 表格 (el-table) + +### 4.1 数据来源 +后端 SQL (`BizPersonMapper.selectSponsorList`): + +```sql +SELECT p.person_id, p.name, p.phone, p.org_id, o.org_name, o.org_type, + p.department, p.position, p.role, p.unit_type, p.user_id, + p.create_by, p.create_time, p.update_by, p.update_time, + u.account_type, u.parent_user_id, u.status, u.del_flag +FROM biz_person p +LEFT JOIN biz_org o ON p.org_id = o.org_id +LEFT JOIN sys_user u ON p.user_id = u.user_id +WHERE u.del_flag = '0' + AND p.unit_type = 'sponsor' -- SQL 硬编码 + AND u.parent_user_id = #{params.sponsorOwnerUid} -- 当前登录主账号 + -- 以下均为可选条件 (前端 q.* 拼上来): + [AND p.name = #{name}] + [AND p.phone = #{phone}] + [AND p.org_id = #{orgId}] -- 前端没传 + [AND p.department = #{department}] + [AND p.position = #{position}] -- 前端没传 (无对应 input) + [AND p.role = #{role}] -- 前端没传 (无对应 input) + [AND u.status = #{status}] +ORDER BY p.person_id DESC +``` + +> **强制隔离三层**: `u.del_flag='0'` + `p.unit_type='sponsor'` + `u.parent_user_id=当前主账号`. INNER JOIN 天然剔除 `user_id IS NULL` 的游离 person. + +### 4.2 每列对应的数据库字段 + +| UI 列 | `row.*` 取值 | 数据库表.字段 | 原型对照 | +|---|---|---|---| +| 复选框 (selection) | `row.personId` | `biz_person.person_id` | ✅ | +| 姓名 | `row.name` | `biz_person.name` | ✅ | +| 手机号 | `row.phone` | `biz_person.phone` | ✅ | +| 工作单位 | `row.orgName` | `biz_org.org_name` (LEFT JOIN) | ✅ | +| 部门 | `row.department` | `biz_person.department` | ✅ | +| 职务 | `row.position` | `biz_person.position` | ✅ | +| **角色** | `row.role` | `biz_person.role` (仅当 `== 'supervisor'` 显示"监察员") | ⚠️ **原型无此列** (实现自行扩展) | +| **账号类型** | `row.accountType` | `sys_user.account_type` (LEFT JOIN, MAIN=主账号 / SUB=子账号) | ⚠️ **原型无此列** (实现自行扩展) | +| 状态 | `row.status` | `sys_user.status` (LEFT JOIN, '0'=正常 / '1'=禁用) | ✅ (原型显示"启用"/"禁用") | +| **创建时间** | `row.createTime` | `biz_person.create_time` | ⚠️ **原型无此列** | +| 操作 (4 按钮) | — | — | 见 §5 | + +--- + +## 5. 行内操作按钮 (每行) + +| 按钮 | 触发函数 | 接口 | 后端动作 | 涉及表.字段 | 原型对照 | +|---|---|---|---|---|---| +| 查看 | `onView(row)` | 跳 `/sponsor/people/detail/{personId}` (`PersonDetail.vue`) | — | — | ✅ | +| 编辑 | `onEdit(row)` | 跳 `/sponsor/people/edit/{personId}` (`NewPerson.vue` 编辑模式) | 后续走 `PUT /business/person` | — | ✅ | +| 禁用 / 恢复 | `onToggleStatus(row)` | `PUT /business/person` (`bizUpdate('person', { personId, userId, status })`) | service `updateByPrimaryKey`: ① `biz_personMapper.updateByPrimaryKey` (XML 中 `` 不含 status 子句, 故 biz_person 无 status 列可改) ② `sysUserMapper.updateUser(u)` 同步 status 到 sys_user | `sys_user.status` | ✅ | +| **删除** | `onRemoveOne(row)` | `DELETE /business/person/{personId}` (`bizDelete('person', personId)`) | `bizPersonMapper.deleteByPrimaryKey` → `UPDATE sys_user u INNER JOIN biz_person p ... SET u.del_flag='1' WHERE p.person_id=...` 联动软删 sys_user | `sys_user.del_flag` | ⚠️ **原型无"删除"操作** | + +### 5.1 禁用/恢复的真实落点 +- `BizPerson.status` 在 Java 端是 `transient` (Java 注释: "非持久化字段") +- mapper `updateByPrimaryKey` 的 `` 块不含 status 子句 +- 真正生效的是 service 里同步 `sys_user.status` (BizPersonServiceImpl.java:115-117) + +--- + +## 6. Java Entity ↔ 数据库表 一致性 (实地校对 2026-08-18) + +### 6.1 `biz_org` 真实 DDL (MySQL `SHOW CREATE TABLE` 实测) + +```sql +CREATE TABLE `biz_org` ( + `org_id` bigint NOT NULL AUTO_INCREMENT, + `org_name` varchar(200) NOT NULL, + `org_type` varchar(20) NOT NULL DEFAULT 'sponsor' COMMENT 'sponsor支持方/execution执行方', + `business_nature` varchar(20) DEFAULT NULL COMMENT '企业性质 私营/国营/中外合资/外资/其他', + `address` varchar(500) DEFAULT NULL COMMENT '单位地址', + `tax_no` varchar(50) DEFAULT NULL, + `contact_name` varchar(50) DEFAULT NULL, + `contact_phone` varchar(20) DEFAULT NULL, + `intent_count` int DEFAULT '0' COMMENT '意向项目数', + `status` varchar(20) DEFAULT '0' COMMENT '合作状态 合作中/待签约/已停用', + `create_by` varchar(64) DEFAULT '', + `create_time` datetime DEFAULT NULL, + `update_by` varchar(64) DEFAULT '', + `update_time` datetime DEFAULT NULL, + `user_id` bigint DEFAULT NULL COMMENT '主账号sys_user.user_id', + PRIMARY KEY (`org_id`), + UNIQUE KEY `uk_org_user_type` (`user_id`,`org_type`), + KEY `idx_org_type_status` (`org_type`,`status`), + KEY `idx_org_type_name` (`org_type`,`org_name`), + KEY `idx_org_user_type_name` (`user_id`,`org_type`,`org_name`) +) ENGINE=InnoDB AUTO_INCREMENT=33 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='支持单位表' +``` + +> **修订**: 上一版报告说"biz_org 不存在"是错的 — 实测 DB 中表存在, 索引齐全 (UK user_id+org_type, 三个复合索引都覆盖了 mapper 里的查询路径). 旧 dump 文件 `ry0808_mysql_dump.sql` 没包含此表是因为它是后来新增的 (dump 时间 2026-08-11, 表应是之后创建的). + +### 6.2 `biz_person` 真实 DDL (MySQL `SHOW CREATE TABLE` 实测) + +```sql +CREATE TABLE `biz_person` ( + `person_id` bigint NOT NULL DEFAULT '0' COMMENT '雪花ID', + `user_id` bigint DEFAULT NULL COMMENT '关联系统用户ID', + `name` varchar(50) NOT NULL, + `phone` varchar(20) NOT NULL, + `org_id` bigint DEFAULT NULL COMMENT '所属公司ID (FK: biz_org.org_id)', + `department` varchar(100) DEFAULT NULL, + `position` varchar(50) DEFAULT NULL, + `role` varchar(20) DEFAULT NULL COMMENT '角色 项目负责人/会议执行/监察员/会务执行', + `unit_type` varchar(20) DEFAULT 'execution' COMMENT 'execution执行/sponsor支持', + `create_by` varchar(64) DEFAULT '', + `create_time` datetime DEFAULT NULL, + `update_by` varchar(64) DEFAULT '', + `update_time` datetime DEFAULT NULL, + PRIMARY KEY (`person_id`), + UNIQUE KEY `uk_person_phone` (`phone`), + UNIQUE KEY `uk_person_user_id` (`user_id`), + KEY `idx_person_role` (`role`), + KEY `idx_person_unit_type` (`unit_type`), + KEY `idx_person_org_id` (`org_id`), + KEY `idx_person_org_role` (`org_id`,`role`) +) +``` + +> **修订**: 上一版"biz_person 有 work_unit / status 列"是错的 — DB 当前 **没有** `work_unit` 列 (dump 旧, 后续删除了), 也没有 `status` 列 (status 唯一来源是 sys_user). Java 实体完全跟 DB 对得上, 没有列错位. + +### 6.3 BizPerson.java ↔ biz_person 表 字段对照 + +| 实体字段 | 类型 | 表字段 | 类型 | 一致? | +|---|---|---|---|---| +| personId | String | `person_id` | bigint | ✅ | +| userId | Long | `user_id` | bigint | ✅ | +| name | String | `name` | varchar(50) | ✅ | +| phone | String | `phone` | varchar(20) | ✅ | +| orgId | Long | `org_id` | bigint | ✅ | +| orgName | String (JOIN) | `biz_org.org_name` (LEFT JOIN) | — | ✅ 仅展示 | +| orgType | String (JOIN) | `biz_org.org_type` (LEFT JOIN) | — | ✅ 仅展示 | +| department | String | `department` | varchar(100) | ✅ | +| position | String | `position` | varchar(50) | ✅ | +| role | String | `role` | varchar(20) | ✅ | +| status | transient | **表里无此列**,真值在 sys_user.status | — | ✅ 双源已修复 (DB 只剩 sys_user) | +| unitType | String | `unit_type` | varchar(20) | ✅ | +| createBy/Time, updateBy/Time | 标准 | 同名 | 同 | ✅ | +| accountType | String (JOIN) | `sys_user.account_type` | — | ✅ 仅展示 | +| parentUserId | Long (JOIN) | `sys_user.parent_user_id` | — | ✅ 仅展示 | +| loginUsername / loginPassword | transient | 写到 sys_user.user_name / 加密 password | — | ✅ 仅创建时用 | + +--- + +## 7. 索引检查 (实测 `SHOW INDEX FROM`) + +### 7.1 `biz_person` 现有索引 (7 个) +| Key | 列 | 用途覆盖 | +|---|---|---| +| PRIMARY | `person_id` | ✅ | +| uk_person_phone | `phone` (UNIQUE) | ✅ 手机号精确查 | +| uk_person_user_id | `user_id` (UNIQUE) | ✅ JOIN sys_user | +| idx_person_role | `role` | ✅ role 精确查 | +| idx_person_unit_type | `unit_type` | ✅ sponsor/executor 隔离 | +| idx_person_org_id | `org_id` | ✅ JOIN biz_org | +| idx_person_org_role | `org_id`,`role` (复合) | ✅ 复合过滤 | + +> **结论**: **没有缺失的关键索引**. department / position / name 走精确匹配无索引但属于小数据, 暂可接受. 若以后按部门/职务建索引, 可新增 `idx_person_department` / `idx_person_position`. + +### 7.2 `biz_org` 现有索引 (5 个) +| Key | 列 | 用途覆盖 | +|---|---|---| +| PRIMARY | `org_id` | ✅ | +| uk_org_user_type | `user_id`,`org_type` (UNIQUE) | ✅ 一用户一类型一公司 | +| idx_org_type_status | `org_type`,`status` | ✅ | +| idx_org_type_name | `org_type`,`org_name` | ✅ | +| idx_org_user_type_name | `user_id`,`org_type`,`org_name` | ✅ selectMySponsorCompany | + +> **结论**: 索引已覆盖 mapper 里所有查询路径, 无需新增. + +### 7.3 `sys_user` 现有索引 +- `PRIMARY KEY (user_id)` +- `KEY idx_parent_user_id (parent_user_id)` +- ⚠️ `status` / `account_type` / `role_type` 仍无单独索引. 但 `selectSponsorList` 中的 `u.status = ?` 过滤在 LEFT JOIN 后命中行数本就少 (主账号的子账号不会很多), 暂可接受. + +--- + +## 8. 与原型的差异 (实现偏离) + +通过菜单定位到的原型 `proto/html/components/sponsor-people.html`, 跟 `SponsorPeople.vue` 实现相比: + +### 8.1 实现新增 (原型没有) +| # | 项 | 实现 | 原型 | +|---|---|---|---| +| 1 | "状态" 筛选下拉 | ✅ (正常/禁用) | ❌ 无 | +| 2 | "重置" 按钮 | ✅ | ❌ 无 | +| 3 | "角色" 列 | ✅ (只显示"监察员" tag) | ❌ 无 | +| 4 | "账号类型" 列 | ✅ (主账号/子账号 tag) | ❌ 无 | +| 5 | "创建时间" 列 | ✅ | ❌ 无 | +| 6 | "删除" 行内按钮 | ✅ | ❌ 无 (原型只有查看/编辑/禁用-恢复) | +| 7 | "批量删除" 工具栏按钮 | ✅ | ❌ 无 | +| 8 | 分页 `el-pagination` | ✅ (10/20/50, jumper) | ❌ 原型不分页 (静态 5 行示例) | +| 9 | 导入结果明细 dialog | ✅ (table 展示每行 ok/fail) | ❌ 原型只有 alert 提示 | + +### 8.2 原型有但实现缺失 +| # | 项 | 实现 | 原型 | +|---|---|---|---| +| 1 | "职务" 筛选输入框 | ❌ 无 (后端 mapper 里有 p.position = ? 字段, 但前端无对应输入框) | ✅ 有 (`职务: [输入框]`) | +| 2 | 红色提示 `*新建时,管理员只能新建监察员` | ❌ 无 | ✅ 有 | +| 3 | 红色提示 `*批量导入模板与列表表项一致` | ❌ 无 (工具栏) | ✅ 有 | +| 4 | 红色提示 `*点击禁用后,状态变为禁用,按钮变成恢复;禁用后,账号无法登录,提示请联系管理员` | ❌ 无 (表格下方) | ✅ 有 | +| 5 | 导入 modal 内 `取消` / `确定` 按钮 | ❌ 只有"关闭" | ✅ 有 | +| 6 | 导入 modal 内 红色提示 `*批量导入模板xls与列表表项一致` | ❌ 无 | ✅ 有 | + +### 8.3 文字 / 标签差异 +| 位置 | 原型 | 实现 | +|---|---|---| +| 页面标题 (h1) | `人员管理` | breadcrumb `首页 / 人员管理` (无 h1) | +| 状态 tag 文案 | `启用` / `禁用` | `正常` / `禁用` | +| 查询按钮 | `查找` | `查询` | + +### 8.4 总结: 实现 vs 原型 +- 实现扩展性强 (新增角色/账号类型/创建时间/批量删除/分页), **超出原型范围**, 是改进 +- 实现有 3 处原型有但缺失的红色提示和 1 个"职务"筛选 — **回退补漏** +- "工作单位" 筛选虽在原型有, 但实现里**实际不生效** (后端未接 orgName), 这是 **bug** +- 实现与原型在视觉布局上对齐 (page-card / breadcrumb / filter-form / toolbar / table 风格一致) + +--- + +## 9. 表字段冗余 / 设计问题 (修订) + +1. **`biz_org.org_name` 与 `biz_support_unit.unit_name` / `biz_execution_unit.company_name`**: 三处并存表示"单位名". **biz_org 重构后未清理旧表**. 旧表 (biz_support_unit / biz_execution_unit / biz_service_org) 仍存在 DB, 应做一次审计决定是否 DROP. +2. **`accountType / parentUserId / orgName / orgType` 在 BizPerson 上 `LEFT JOIN`**: 是只读展示字段, 但 mapper XML 把它们塞到 resultMap, 前端依赖. 若未来要分库 / 视图, 会很别扭. 可考虑抽出 `BizPersonListVO` 与实体分离. +3. **`biz_person.user_id` 唯一约束 (`uk_person_user_id`)**: 一个 sys_user 子账号最多对应一个 biz_person, 符合"一人一员"语义, ✅ 设计正确. +4. **`biz_person.org_id` 与 `biz_org.user_id + org_type` 的关系**: 一个 biz_person 属于一个 org, org 通过 `(user_id, org_type)` 唯一确定其主账号. 主账号注册时一条 biz_org 记录, 子账号的 org_id 必须等于主账号的 org_id — 当前 service (BizPersonServiceImpl.java:54-70) 实现了兜底逻辑, ✅ 设计闭环. + +--- + +## 10. 待修复列表 (按优先级, 修订版) + +| # | 问题 | 文件 | 修复建议 | 严重度 | +|---|---|---|---|---| +| 1 | ~~"工作单位" 筛选不生效 (后端未接)~~ | `BizPersonMapper.xml` 3 处 SELECT | ✅ **已修复** — 加 `o.org_name like concat('%', #{orgName}, '%')`, 实测通过 (见 §2.1) | ~~P0~~ | +| 1.5 | ~~历史数据 `biz_person.org_id=NULL` 导致"工作单位"列空~~ | (inline mysql, 不落 .sql 文件) | ✅ **已修复** — 数据回填 (11 → 0 NULL), 见 §2.2 | ~~P0~~ | +| 2 | 缺"职务"筛选输入框 | `SponsorPeople.vue` filter-form | 加 `` | P1 | +| 3 | 缺原型 3 处红色提示 | `SponsorPeople.vue` toolbar / 表格下 / import modal | 按原型补 `*新建时,管理员只能新建监察员` 等 | P2 | +| 4 | 导入 modal 缺"取消/确定"按钮 | `SponsorPeople.vue` import dialog | 按原型补按钮 (当前只有"关闭") | P2 | +| 5 | 状态文案与原型不一致 (启用 vs 正常) | `SponsorPeople.vue` | 与产品对齐: 用 `正常` 还是 `启用` | P3 | +| 6 | 旧表 (biz_support_unit / biz_execution_unit / biz_service_org) 与 biz_org 共存 | DB DDL | 审计后 DROP 旧表 (确认无引用) | P3 | + +--- + +## 11. 引用清单 + +- 前端: `ry-vue3/src/views/sponsor/SponsorPeople.vue` +- 前端 API: `ry-vue3/src/api/business/person.js` (`listSponsorPerson`) +- 路由: `ry-vue3/src/router/index.js` 第 113-124 行 (sponsor 路由块) +- 菜单: `ry-vue3/src/layout/AdminLayout.vue` 第 123-129 行 (sponsor MENU) +- Controller: `ry-api/ruoyi-business/.../controller/BizPersonController.java` (`/sponsorList`, `/sponsorImport`, `/sponsorImportTemplate`) +- Service: `ry-api/ruoyi-business/.../service/impl/BizPersonServiceImpl.java` +- Mapper 接口: `ry-api/ruoyi-business/.../mapper/BizPersonMapper.java` +- Mapper XML: `ry-api/ruoyi-business/src/main/resources/mapper/business/BizPersonMapper.xml` +- 实体: `BizPerson.java`, `BizOrg.java`, `BizPersonImportVO.java` +- 数据库配置: `ry-api/ruoyi-admin/src/main/resources/application-druid.yml` +- 库结构 (旧,仅供参考): `ry0808_mysql_dump.sql` (与现行 DB 存在差异 — biz_org / biz_person 部分字段未在此 dump) +- 原型菜单入口: `proto/html/sponsor.html` (sponsor 角色工作台布局) +- 原型组件: `proto/html/components/sponsor-people.html` ← **本次对比基准**