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>
260 lines
15 KiB
Markdown
260 lines
15 KiB
Markdown
# 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` | `<h1 class="page-title">账号信息</h1>` |
|
|
|
|
**角色定义**:
|
|
- `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) — `<if test="nickName != null and nickName != ''">nick_name = #{nickName}</if>` | `sys_user.nick_name` | 接口回填 | ⚠️ **原型 readonly,实现可编辑** |
|
|
| 手机号 | `el-input` | `form.phonenumber` | mapper.xml:235 — `<if test="phonenumber != null">phonenumber = #{phonenumber}</if>` | `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`,实现改成 `<el-input v-model>`
|
|
- **前端密码长度上限 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 用 `<if>` 拼接,每次 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 重写,与本页面同模式) | |