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>
15 KiB
15 KiB
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 |
仅前端比对 | — | 用户输入 | ✅ 一致 |
附注 — 前端"留空不改密"逻辑:
// 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 二次核对)
-- 实测 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 |
| String | 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):
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 重写,与本页面同模式) |