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>
25 KiB
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 行<li><a href="#" data-page="components/sponsor-people.html" class="menu-link">人员管理</a></li>- 原型图:
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) |
看不到此菜单 |
| 匿名未登录用户 | 看不到 (侧栏菜单不渲染) |
数据隔离机制 (后端强制, 前端绕不开):
BizPersonController.sponsorList()第 73-81 行:bizPerson.getParams().put("sponsorOwnerUid", mainUid), 把当前登录主账号 user_id 注入 mapper 参数BizPersonMapper.xmlselectSponsorList第 152-153 行:p.unit_type='sponsor' AND u.parent_user_id = #{params.sponsorOwnerUid}— 三层过滤 (del_flag='0' + unit_type='sponsor' + parent_user_id 匹配)- 即使前端篡改请求参数, 这两条硬编码 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) 的 <where> 块各加了一行:
<if test="orgName != null and orgName != ''"> and o.org_name like concat('%', #{orgName}, '%')</if>
原理:
biz_person.org_id是 FK, JOINbiz_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 文件)
-- 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):
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 中 <update> 不含 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的<trim prefix="SET">块不含 status 子句 - 真正生效的是 service 里同步
sys_user.status(BizPersonServiceImpl.java:115-117)
6. Java Entity ↔ 数据库表 一致性 (实地校对 2026-08-18)
6.1 biz_org 真实 DDL (MySQL SHOW CREATE TABLE 实测)
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 实测)
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. 表字段冗余 / 设计问题 (修订)
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.accountType / parentUserId / orgName / orgType在 BizPerson 上LEFT JOIN: 是只读展示字段, 但 mapper XML 把它们塞到 resultMap, 前端依赖. 若未来要分库 / 视图, 会很别扭. 可考虑抽出BizPersonListVO与实体分离.biz_person.user_id唯一约束 (uk_person_user_id): 一个 sys_user 子账号最多对应一个 biz_person, 符合"一人一员"语义, ✅ 设计正确.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) |
||
| 1.5 | biz_person.org_id=NULL 导致"工作单位"列空 |
(inline mysql, 不落 .sql 文件) | ✅ 已修复 — 数据回填 (11 → 0 NULL), 见 §2.2 | |
| 2 | 缺"职务"筛选输入框 | SponsorPeople.vue filter-form |
加 <el-form-item label="职务"><el-input v-model="q.position"></el-input></el-form-item> |
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← 本次对比基准