diff --git a/_self/executor_people.md b/_self/executor_people.md new file mode 100644 index 0000000..2e4838f --- /dev/null +++ b/_self/executor_people.md @@ -0,0 +1,378 @@ +# executor / People — 端到端页面技术审查 + +> 审查日期: 2026-08-18 +> 审查范围: 前端 → 后端 → 数据库 → 原型 +> 审查目标: `ry-vue3/src/views/executor/People.vue` + +--- + +## 0. 四跳定位 + +| 跳 | 文件 / 位置 | 命中 | +|---|---|---| +| ① 角色菜单 | `ry-vue3/src/layout/AdminLayout.vue:119` | `{ path: '/executor/people', title: '人员管理', icon: User, requireMain: true }` | +| ② 路由 | `ry-vue3/src/router/index.js:101-110` | 父路由 `meta: { role: 'executor' }` → 子路由 `{ path: 'people', name: 'executor-people', component: () => import('@/views/executor/People.vue'), meta: { title: '人员管理' } }` | +| ③ 组件 | `ry-vue3/src/views/executor/People.vue` | ✅ 审查目标 (1-171 行) | +| ④ 原型 | `proto/html/components/people.html` | `proto/html/executor.html:206` → `data-page="components/people.html"` | + +**角色定义**: `AdminLayout.vue:65` — `executor: '执行人'` + +--- + +## 1. 主要功能与可见性 + +### 1.1 主要功能 + +| 功能 | 前端入口 | 后端接口 | 数据表 | +|---|---|---|---| +| 列表 + 筛选 + 分页 | `loadList()` (onMounted) | `GET /business/person/executorList` | `biz_person` + LEFT JOIN `biz_org`, `sys_user` | +| 新建人员 | `goNew()` → 跳 `/executor/people/new` | (另页 `NewPerson.vue`) | `biz_person` + `sys_user` (新建 SUB 子账号) | +| 编辑人员 | `goEdit(row)` → 跳 `/executor/people/edit/:id` | `PUT /business/person` | `biz_person` + 同步 `sys_user` | +| 查看详情 | `onView(row)` (本地 dialog) | `GET /business/person/{id}` | 同列表 | +| 启用 / 禁用 | `onToggleStatus(row, action)` | `PUT /business/person` (传 `status` + `userId`) | `biz_person` (无字段,status transient) + 同步 `sys_user.status` | +| 批量导入 | `submitImport()` (dialog) | `POST /business/person/executorImport` (本页面**未调用**,UI 占位) | `biz_person` + `sys_user` | + +### 1.2 可见性三层过滤 + +| 层 | 校验 | 行为 | +|---|---|---| +| 前端菜单 | `AdminLayout.vue:119` `requireMain: true` | **仅主账号可见** (子账号在菜单里看不到该入口) | +| 路由守卫 | `router/index.js:101` `meta.role = 'executor'` | 仅 executor 角色可进入 | +| 后端 SQL | `BizPersonMapper.xml:170-187` `selectExecutorList` | ✅ 硬编码 `p.unit_type = 'executor'` + `(u.parent_user_id = #{executorOwnerUid} OR u.user_id = #{executorOwnerUid})`,前端绕不开 | + +**不能看到本页面的人群**: +- 其他角色映射到自己的同名页: `admin` / `manager` 在 `router/index.js:41-43, 79-81` 有 `executor-people` (实际是"全公司视角",看所有执行方的人员);`sponsor` 有自己的 `sponsor-people` (router:119) +- executor 子账号 — 菜单项 `requireMain: true`,前端就不渲染 + +**数据隔离机制** (核心): +``` +-- BizPersonMapper.xml:170-187 (selectExecutorList) +where u.del_flag = '0' + and p.unit_type = 'executor' -- SQL 硬编码,前端改 unit_type 也无效 + and (u.parent_user_id = #{params.executorOwnerUid} -- 主账号视角: 看到我的子账号人员 + or u.user_id = #{params.executorOwnerUid}) -- 主账号自己也是 person,避免漏自己 +``` +⚠️ 注意 `executorOwnerUid` 由 `BizPersonController.java:81-89` 的 `getUserId()` 注入 — 后端取的是**当前登录用户**,前端无法注入他人 UID。 + +--- + +## 2. 筛选项 (filter-form) + +| UI label | 控件 | 绑定字段 | 后端 SQL | 命中表.字段 | 选项来源 | 原型对照 | +|---|---|---|---|---|---|---| +| 姓名 | `el-input` | `q.name` | mapper.xml:177 ` and p.name like concat('%', #{name}, '%')` | `biz_person.name` (模糊) | 用户输入 | ✅ 原型有 | +| 手机号 | `el-input` | `q.phone` | mapper.xml:178 `and p.phone = #{phone}` | `biz_person.phone` (精确) | 用户输入 | ✅ | +| 所属公司 | `el-input` | `q.orgName` | mapper.xml:180 `and o.org_name like concat('%', #{orgName}, '%')` | `biz_org.org_name` (模糊) | 用户输入 | ⚠️ 原型 label "工作单位",实现改成 "所属公司" | +| 部门 | `el-input` | `q.department` | mapper.xml:181 `and p.department = #{department}` | `biz_person.department` | 用户输入 | ✅ | +| 职务 | `el-input` | `q.position` | mapper.xml:182 `and p.position = #{position}` | `biz_person.position` | 用户输入 | ✅ | + +**附注 — 角色 / 状态列**: +- 前端不展示"角色" / "状态"作为筛选项 (但表格里作为列展示),后端 SQL mapper.xml:183-184 提供 `` 支持,但前端没传参 + +--- + +## 3. 工具栏按钮 (toolbar) + +| 按钮 | 功能 | 接口 | 涉及表 | 原型对照 | +|---|---|---|---|---| +| 查找 | 触发列表刷新 | 无 (本地) | — | ✅ | +| 重置 | 清空筛选 + 回 pageNum=1 | 无 (本地) | — | ⚠️ 原型无"重置"按钮 | +| 新建人员 | 跳独立页 `/executor/people/new` | (另页) | `biz_person` + `sys_user` | ✅ | +| 批量导入 | 弹 dialog (本页面 UI 占位,未实际上传) | (UI 占位) | — | ✅ (原型 alert,实现也 alert) | + +⚠️ **P2**: `downloadTemplate` / `submitImport` 都只 `ElMessage.info` 占位,没有真实 Excel 下载 / 上传逻辑。BizPersonController.java:152-164 已实现 `/executorImportTemplate` 和 `/executorImport` 接口,但前端未对接。 + +--- + +## 4. 表格 (el-table) + +### 4.1 数据来源 + +`BizPersonMapper.xml:170-187` (`selectExecutorList`): + +```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 as user_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 = 'executor' + and (u.parent_user_id = #{params.executorOwnerUid} + or u.user_id = #{params.executorOwnerUid}) + [ and p.name like concat('%', #{name}, '%') ] + [ and p.phone = #{phone} ] + [ and p.org_id = #{orgId} ] + [ and o.org_name like concat('%', #{orgName}, '%') ] + [ and p.department = #{department} ] + [ and p.position = #{position} ] + [ and p.role = #{role} ] + [ and u.status = #{status} ] +order by p.person_id desc +``` + +### 4.2 列映射 + +| 列名 | `row.*` | DB 表.字段 | 原型对照 | +|---|---|---|---| +| 姓名 | `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` (`translatePersonRole` 转中文) | ✅ | +| 状态 | `row.status` | `sys_user.status` (LEFT JOIN) | ✅ | +| 操作 | — | — | ✅ | + +**附注 — status 字段不在 biz_person**: +`BizPerson.java:51-53` 标注 `status` 字段为 `transient`: +```java +/** 启停状态 '0'/'1' (前端 toggle 用, BizPersonServiceImpl 同步到 sys_user.status) - 非持久化字段 */ +@com.fasterxml.jackson.annotation.JsonProperty("status") +private transient String status; +``` +所以表格里的 `row.status` 是 `sys_user.status` 通过 LEFT JOIN 进来,**不要尝试往 biz_person.status UPDATE**(不存在这列)。 + +--- + +## 5. 行内操作按钮 + +| 按钮 | 触发函数 | 接口 | 后端动作 | 涉及表.字段 | 原型对照 | +|---|---|---|---|---|---| +| 查看 | `onView(row)` | 无 (本地弹 dialog) | 无 | — | ✅ | +| 编辑 | `goEdit(row)` | 跳 `/executor/people/edit/${row.personId}` | `PUT /business/person` (另页) | `biz_person.*` + 同步 `sys_user.nick_name/phonenumber/status` | ✅ | +| 禁用 / 恢复 | `onToggleStatus(row, action)` | `PUT /business/person` (本页面直调) | `BizPersonServiceImpl.updateByPrimaryKey` (impl:106-118): 先 updateByPrimaryKey 改 biz_person 业务字段,**`if (entity.getUserId() != null)` 同步** sys_user.status | `sys_user.status` (NOT `biz_person.status`,后者不存在) | ✅ | + +**保护逻辑** (People.vue:46-50): +```js + +``` +✅ 主账号的"本人"行不显示禁用按钮 — 防主账号把自己禁用造成锁定。但**没禁掉子账号主账号的(主账号不是 executor 自己,这里只针对本主账号自己)**。 + +**真实落点 — 同步到 sys_user**: +`BizPersonServiceImpl.java:106-118` (`updateByPrimaryKey`): +```java +int n = bizPersonMapper.updateByPrimaryKey(entity); +if (n > 0 && entity.getUserId() != null) { + SysUser u = new SysUser(); + u.setUserId(entity.getUserId()); + u.setNickName(entity.getName()); + u.setPhonenumber(entity.getPhone()); + u.setUpdateBy(SecurityUtils.getUsername()); + if (entity.getStatus() != null) { + u.setStatus(entity.getStatus()); + } + sysUserMapper.updateUser(u); +} +``` +⚠️ **关键**: `entity.getUserId() == null` 时,**不会同步 sys_user.status**。前端 People.vue:144 已传 userId ✅。 + +--- + +## 6. Java Entity ↔ 数据库表 一致性 + +### 6.1 `biz_person` DDL (DB 实测 2026-08-18) + +```sql +CREATE TABLE `biz_person` ( + `person_id` bigint NOT NULL DEFAULT '0', + `user_id` bigint DEFAULT NULL, + `name` varchar(50) NOT NULL, + `phone` varchar(20) NOT NULL, + `org_id` bigint DEFAULT NULL, + `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`) +) +``` + +### 6.2 `biz_org` DDL (DB 实测) + +```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, + `address` varchar(500) DEFAULT NULL, + `tax_no` varchar(50) DEFAULT NULL, + `contact_name` varchar(50) DEFAULT NULL, + `contact_phone` varchar(20) DEFAULT NULL, + `intent_count` int DEFAULT '0', + `status` varchar(20) DEFAULT '0', + `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`) +) +``` + +### 6.3 `BizPerson` Entity ↔ `biz_person` 表 + +| 实体字段 | Java 类型 | 表字段 | 表类型 | 一致? | 备注 | +|---|---|---|---|---|---| +| personId | String | person_id | bigint | ✅ | **类型不一致**: Java String vs DB bigint — 但 MyBatis resultMap 兼容 (driver 自动转),⚠️ 性能上不建议 | +| name | String | name | varchar(50) NOT NULL | ✅ | | +| phone | String | phone | varchar(20) NOT NULL | ✅ | 有 uk_person_phone 唯一约束 | +| orgId | Long | org_id | bigint | ✅ | LEFT JOIN biz_org 时填 orgName/orgType | +| orgName | String | — (JOIN) | — | ✅ | 仅展示字段,resultMap 映射到 org_name | +| orgType | String | — (JOIN) | — | ✅ | 仅展示 | +| department | String | department | varchar(100) | ✅ | | +| position | String | position | varchar(50) | ✅ | | +| role | String | role | varchar(20) | ✅ | | +| unitType | String | unit_type | varchar(20) | ✅ | | +| userId | Long | user_id | bigint | ✅ | 有 uk_person_user_id 唯一约束 | +| status | String (transient) | — (不存在) | — | ✅ | 实际写 sys_user.status | +| accountType | String (JOIN) | — | — | ✅ | LEFT JOIN sys_user.account_type | +| parentUserId | Long (JOIN) | — | — | ✅ | LEFT JOIN sys_user.parent_user_id | +| loginUsername | String (transient) | — | — | ✅ | 创建 sys_user 子账号用 | +| loginPassword | String (transient) | — | — | ✅ | 创建后丢弃 | +| delFlag | String (return null) | — | — | ⚠️ | getter 永远返回 null — 软删走 sys_user.del_flag='1' | + +### 6.4 修订:之前在 `_self/sponsor_people.md` 写过的"`work_unit` 已删除"假设 + +**实测 DB 后**: `biz_person` 表里**没有 `work_unit` 字段**,但**也没有 `biz_person.work_unit`** 这张废表;目前存"所属公司"的方式是 `biz_person.org_id` LEFT JOIN `biz_org`。所以现在的设计是**外键指向公司表**,比之前假设的"work_unit 字段"更规范 ✅。 + +--- + +## 7. 索引检查 + +### 7.1 `biz_person` (DB 实测) + +| Key | 列 | 用途覆盖 | 评估 | +|---|---|---|---| +| PRIMARY | `person_id` | ✅ `updateByPrimaryKey WHERE person_id = ?`, `selectByPrimaryKey` | OK | +| uk_person_phone | `phone` (UNIQUE) | ✅ `insert` 唯一性,`SELECT WHERE phone = ?` 精确 | OK | +| uk_person_user_id | `user_id` (UNIQUE) | ✅ `insert` 唯一性,JOIN `sys_user` 关键 | OK | +| idx_person_role | `role` | ✅ `mapper:183 and p.role = #{role}`,`idx_person_org_role` 已覆盖 | OK (但有冗余) | +| idx_person_unit_type | `unit_type` | ✅ `selectList:49 and p.unit_type = #{unitType}`,`selectSponsorList`/`selectExecutorList` 硬编码 unit_type='sponsor'/'executor' | OK | +| idx_person_org_id | `org_id` | ✅ `selectByPrimaryKey` JOIN,`selectList:52 and p.org_id = #{orgId}` | OK | +| idx_person_org_role | `(org_id, role)` | ✅ `countAdminByOrgId` (impl 中可能) | OK | + +### 7.2 `biz_org` + +| Key | 列 | 用途覆盖 | 评估 | +|---|---|---|---| +| PRIMARY | `org_id` | ✅ JOIN | OK | +| uk_org_user_type | `(user_id, org_type)` | ✅ 主账号唯一公司 | OK | +| idx_org_type_status | `(org_type, status)` | OK | | +| idx_org_type_name | `(org_type, org_name)` | ✅ `selectList:53 LIKE '%org_name%'` — **但 LIKE 前缀模糊会走全索引扫描**(若 % 开头,索引失效) | ⚠️ 暂可接受(org 数量小) | +| idx_org_user_type_name | `(user_id, org_type, org_name)` | OK | | + +### 7.3 `sys_user` (已实测,见 `_self/sponsor_account.md` §7) + +| Key | 列 | 评估 | +|---|---|---| +| PRIMARY | user_id | ✅ | +| idx_parent_user_id | parent_user_id | ✅ SELECT 子账号 | +| — | phonenumber | ❌ 缺(暂可接受,26 行) | + +⚠️ `selectExecutorList` 的关键过滤 `(u.parent_user_id = X OR u.user_id = X)` 走 PRIMARY 主键索引 (`u.user_id = X`) + idx_parent_user_id,虽然 OR 走全索引扫描但小表 OK;破万后考虑重写为 UNION ALL。 + +--- + +## 8. 与原型差异 (`proto/html/components/people.html`) + +### 8.1 实现新增 (原型没有) + +- **分页** (`el-pagination`) — 原型 8 行静态,实现加分页支持 +- **数据隔离** — 原型无账号概念,实现按主账号/子账号隔离 +- **行内按钮"恢复"(启用)** — 原型叫"启用",实现叫"恢复";语义一致 ✅ +- **本人禁用按钮隐藏** — 业务逻辑(防主账号自禁用),原型无 +- **status 状态列颜色 tag** — 扩展 +- **角色列 tag** — 原型是普通 span,实现是 el-tag + type + +### 8.2 原型有但实现缺失 + +- **顶部 h1 "人员管理" + breadcrumb** — 实现只用 `class="page-sub"` 一行小字"管理执行方人员,分配会务执行角色",无 h1,无面包屑;**与项目其它页风格不一致**(其它页用 breadcrumb) +- **复选框列** — 原型表头第一列是 checkbox(全选/单选),实现完全删了。原型注释 (people.html:30) 自承"整合为整列",但实际实现把整列删了 +- **红字提示** `*角色:执行方管理员新建人员时,默认只有会议执行的角色` — 原型 people.html:282-284 有提示卡片,实现无 ⚠️ + +### 8.3 文字 / 标签差异 + +| 项 | 原型 | 实现 | 评估 | +|---|---|---|---| +| 筛选项 label | "工作单位:" | "所属公司" | ⚠️ 改了语义,后端 SQL `LIKE %org_name%` 含义不变,但用户口径不一致 | +| 列表列名 | "工作单位" | "所属公司" | ⚠️ 同上 | +| 操作按钮文字 | "启用" | "恢复" | 轻微差异,语义等价 ✅ | +| 表头 | 无角色/状态标题 color | "角色"/"状态"独立列 | ✅ 改进 | + +### 8.4 总结 + +- **方向**: 实现**做了减法 + 加法** — 删了复选框 / 标题 / 提示,加了分页 / 隔离 / 状态管理 +- **缺失最严重的**: 顶部 h1 + breadcrumb(项目其它页都是这个风格);"工作单位"→"所属公司" 是有意的口径调整(后端表名 `biz_org.org_name`) + +--- + +## 9. 表字段冗余 / 设计问题 + +| # | 问题 | 文件 | 说明 | +|---|---|---|---| +| 1 | `BizPerson.status` 字段是 `transient`,前端 toggle 后通过 service 同步到 `sys_user.status` | `BizPerson.java:51-53` + `BizPersonServiceImpl.java:106-118` | 字段不持久化,语义跨表,易出 bug — 但前端已规范传 `userId`,目前安全 ⚠️ | +| 2 | `BizPerson.delFlag` getter 永远返回 `null` | `BizPerson.java:84-86` | 设计意图是"软删走 sys_user",getter 留作兼容 — 但保留这个 getter 容易误导 | +| 3 | `BizPerson.personId` Java 类型是 `String`,DB 是 `bigint` | `BizPerson.java:13` vs DDL | MyBatis 自动转换 OK,但后续若有 `personId > 100` 这类比较会 ClassCastException ⚠️ | +| 4 | `biz_person.unit_type` 默认值 `'execution'`,但 executor 端 SQL 硬编码 `unit_type='executor'` | DDL vs mapper.xml:174 | DEFAULT 是历史值,新代码已用 'executor' 字符串;DEFAULT 未统一 | +| 5 | People.vue:144 调用 `bizUpdate('person', ...)` 传 `userId` 是**强依赖**,没传则 status 不更新 | `BizPersonServiceImpl.java:108` | 这是隐式契约,前端注释 (People.vue:142-143) 已标,但**没有任何后端校验**(传 null 会 silently noop) | +| 6 | `selectExecutorList` 包含 `u.status = ?` 过滤条件,前端未用,SQL 永远不过滤 | `BizPersonMapper.xml:184` | 是预留扩展 OK,但注意 status 列展示**不是用这个条件** — 展示来自 `select *`(全字段) | +| 7 | 删除走 `sys_user.del_flag='1'`,业务侧 biz_person 记录永远存在;若要"恢复"还得改 `del_flag='0'`,当前无"恢复已删人员" UI | `BizPersonMapper.xml:122-145` | 是设计选择,但产品流程上若有"误删恢复"需求会找不到入口 | + +--- + +## 10. 待修复列表 (按优先级) + +| # | 问题 | 文件 / 位置 | 修复建议 | 严重度 | +|---|---|---|---|---| +| 1 | 顶部缺 h1 + breadcrumb,与项目其它页风格不一致 | `People.vue:3` | 加 breadcrumb `首页 / 人员管理` + (可选) h1,跟 manager/executor/Overview 风格统一 | **P2** | +| 2 | 批量导入 dialog UI 占位,未对接 `/executorImport` 接口 | `People.vue:64-80, 154-158` | 调用 `POST /business/person/executorImport` + 模板下载 `GET /executorImportTemplate`;返回的 `errorRows` 渲染成明细 | **P2** | +| 3 | 缺原型红字提示 "*角色:执行方管理员新建人员时,默认只有会议执行的角色" | `People.vue:18-23` `batch-bar` | 在表格上方或表单区补一行提示 | **P2** | +| 4 | 原型有复选框列(全选/批量操作),实现完全删了 | `People.vue:26-53` | 如不需批量操作,可保留一行 checkbox 列+ disabled;若产品决定不做批量,需与原型对齐(在原型文件加 "调整说明" 备注) | **P2** | +| 5 | `biz_person.unit_type` 默认值 'execution' 与 executor SQL 'executor' 不一致 | DDL DEFAULT vs mapper.xml | 把 DDL DEFAULT 改 'executor' (或同步 sponsor SQL) | **P3** | +| 6 | `BizPerson.delFlag` getter 返回 null 是误导 | `BizPerson.java:84-86` | 移除 getter/setter 或改名 `getSysUserDelFlag()` | **P3** | +| 7 | `BizPerson.personId` 用 String 存 bigint | `BizPerson.java:13` | 改为 Long,前端 `personId` 转 Number | **P3** | +| 8 | serviceImpl updateByPrimaryKey 在 userId=null 时 silently noop status 同步 | `BizPersonServiceImpl.java:108` | 加日志 warn 或抛异常提示前端 | **P3** | +| 9 | "工作单位" → "所属公司" 改名是 SQL 含义,产品口径需统一 | 前端文案 | 与产品确认:对外叫"所属公司"还是"工作单位" | **P3** | + +--- + +## 11. 引用清单 + +| 用途 | 文件 | 行号 | +|---|---|---| +| 角色菜单 | `ry-vue3/src/layout/AdminLayout.vue` | 65, 115-122 | +| 路由 | `ry-vue3/src/router/index.js` | 101-110 | +| 路由守卫 | `ry-vue3/src/router/index.js` | 134+ | +| 审查目标 | `ry-vue3/src/views/executor/People.vue` | 1-171 | +| 前端 API 封装 | `ry-vue3/src/api/business/person.js` | 24-31 (`listExecutorPerson`) | +| 后端 Controller | `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPersonController.java` | 39-62 (BUSINESS_MAIN_ROLES), 81-89 (executorList), 152-164 (executorImport) | +| 后端 Mapper XML | `ry-api/ruoyi-business/src/main/resources/mapper/business/BizPersonMapper.xml` | 4-22 (resultMap), 24-38 (selectFields), 105-120 (updateByPrimaryKey), 122-145 (deleteByPrimaryKey/Keys), 168-187 (selectExecutorList), 189-194 (countAdminByOrgId) | +| 后端 Service 实现 | `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizPersonServiceImpl.java` | 96-98 (SysUser 构造), 106-118 (updateByPrimaryKey + sys_user 同步) | +| Entity | `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizPerson.java` | 9-108 (字段定义 + setter/getter,行 51-53 status 是 transient) | +| DDL (DB 实测) | `mysql -h 127.0.0.1 -e 'USE guoju0808; SHOW CREATE TABLE biz_person\G'` | 2026-08-18 | +| DDL (DB 实测) | `mysql -h 127.0.0.1 -e 'USE guoju0808; SHOW CREATE TABLE biz_org\G'` | 2026-08-18 | +| 原型 | `proto/html/components/people.html` | 全文 434 行 (含详细注释) | +| 原型菜单链 | `proto/html/executor.html` | 206 (data-page="components/people.html") | +| 记忆 (角色真相) | memory/sys-user-role-type-truth.md | role_type 是单一可信源 | +| 记忆 (sponsor 同模式) | commit 50c2678 | executorList 加 executorList 兜底 (sponsor 同模式) | \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizPersonServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizPersonServiceImpl.java index fb4b490..be07895 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizPersonServiceImpl.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizPersonServiceImpl.java @@ -73,6 +73,9 @@ public class BizPersonServiceImpl implements IBizPersonService } // 1. 创建 sys_user 子账号 + // 继承主账号 role_type, 避免子账号被 sys_user.role_type DEFAULT 'executor' 覆盖 + // (之前不写 roleType 时, sponsor 主账号的子账号会被 DEFAULT 错位成 executor) + SysUser mainUser = mainUserId == null ? null : sysUserMapper.selectUserById(mainUserId); SysUser newUser = new SysUser(); newUser.setUserName(entity.getLoginUsername()); newUser.setNickName(entity.getName()); @@ -82,6 +85,9 @@ public class BizPersonServiceImpl implements IBizPersonService newUser.setParentUserId(mainUserId); newUser.setStatus("0"); newUser.setDelFlag("0"); + if (mainUser != null && mainUser.getRoleType() != null) { + newUser.setRoleType(mainUser.getRoleType()); + } newUser.setCreateBy(SecurityUtils.getUsername()); // mybatis useGeneratedKeys 自动回填 userId sysUserMapper.insertUser(newUser); diff --git a/ry-vue3/src/views/auth/Login.vue b/ry-vue3/src/views/auth/Login.vue index b0c681a..fea0899 100644 --- a/ry-vue3/src/views/auth/Login.vue +++ b/ry-vue3/src/views/auth/Login.vue @@ -283,7 +283,9 @@ async function afterLogin(token, displayName, fallbackRole) { }) } ElMessage.success(`欢迎,${displayName}(${userTypes.find(u => u.value === role)?.name || role})`) - router.replace(roleHome[role] || '/leader/home') + // 没拿到角色就跳回登录页 (之前 fallback 到 /leader/home 会让子账号误入 leader) + if (!role || !roleHome[role]) return router.replace({ name: 'login' }) + router.replace(roleHome[role]) } function switchMode(mode) { @@ -335,8 +337,8 @@ async function onSmsLoginSubmit() { await runSmsLoginOnce(async () => { try { const res = await smsLogin({ phone: smsForm.phone, smsCode: smsForm.smsCode, uuid: smsForm.uuid }) - // 短信登录后无 username, 用 phone 作为显示名; role 兜底默认 leader - await afterLogin(res.token, smsForm.phone, 'leader') + // 短信登录后无 username, 用 phone 作为显示名; fallbackRole 留空, 让 /getInfo 决定 + await afterLogin(res.token, smsForm.phone, '') } catch (e) { // request.js 已弹错误 } @@ -351,7 +353,8 @@ function autoRole(username) { if (u.startsWith('doctor')) return 'doctor' if (u.startsWith('executor')) return 'executor' if (u.startsWith('sponsor')) return 'sponsor' - return 'leader' + // 不匹配返回空串, 让 /getInfo 决定 (避免名字不规范时误进 leader) + return '' } const roleHome = {