# 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` ← **本次对比基准**