2026-05-08-REQ-USR-002.md
5.83 KB
req_id: REQ-USR-002 date: 2026-05-08
module: usr
Spec: REQ-USR-002 — 修改用户
目标
超级管理员可通过 userId 定位已有用户,更新其用户类型、语言、单据修改权限、启/停用状态、关联员工和权限组,修改立即生效;用户名不可修改,密码由单独接口重置。
输入 / 触发
HTTP 请求:PUT /api/usr/users/{userId}(需 Bearer Token 鉴权)
Path 参数:userId(usr_user.sId,目标用户的业务 ID)
Request Body(JSON):
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userType | String | 是 |
普通用户 或 超级管理员
|
| language | String | 是 |
中文 / 英文 / 繁体
|
| canEditDoc | boolean | 是 | 是否有单据修改权限 |
| isDisabled | boolean | 是 | true = 禁用账号 |
| employeeId | String\ | null | 否 |
| permGroupIds | String[] | 是 | 权限组 sId 列表(空数组 = 清空所有权限) |
输出 / 结果
HTTP 200,Result<UserUpdateRespVO>
{
"code": 200,
"data": {
"userId": "string",
"username": "string",
"updatedAt": "2026-05-08T10:00:00"
}
}
updatedAt 为服务端处理时的 LocalDateTime.now(),序列化为 ISO-8601。
业务规则
-
权限校验:
principal.userType() != "超级管理员"→ 抛 BizException(40300),与 createUser 逻辑一致(来源:UserServiceImpl.createUser) -
用户存在性:按
sId = userId AND sBrandsId = brandId查usr_user;找不到 → 抛 BizException(40400) -
自身角色保护:
principal.userId().equals(userId)时,禁止将userType从超级管理员改为其他值 → 抛 BizException(40301)(防止唯一管理员自我降权导致系统锁死;即使目标 userType 已是普通用户也拦截,保持规则简单) -
employeeId 校验:若
employeeId != null,需确认tStaff.sId = employeeId AND sBrandsId = brandId存在;不存在 → 抛 BizException(40001) -
更新字段:仅更新
sUserType / sLanguage / bCanEditDoc / bIsDisabled / sEmployeeId;sUsername / sUserCode / sPasswordHash / sCreatorUsername / tCreateDate不修改 -
权限组 replace 策略:先
DELETE FROM usr_user_permission WHERE sUserId = userId AND sBrandsId = brandId,再批量 INSERT 新的关联记录(与 createUser 的 permGroupIds 处理方式一致);permGroupIds 为空数组时仅删除,不插入 -
多租户隔离:所有查询和写入均带
sBrandsId = principal.brandId()
边界与约束
- 接口为写操作,需加
@Transactional - 鉴权失败返回 401(SecurityConfig 已配置)
-
userType/language仅接受指定枚举值;违反 → 由@Valid+@NotNull在 DTO 层拦截返回 40001 - 密码不在此接口处理,
sPasswordHash保持原值不变 - 响应 JSON 不返回 sPasswordHash / iLoginFailCount / tLockUntil
依赖的 schema 表 / 字段
-
usr_user (别名 u):sId, sUserType, sLanguage, bCanEditDoc, bIsDisabled, sEmployeeId, sBrandsId(查找 + 更新) -
tStaff:sId, sBrandsId(校验 employeeId 存在性) -
usr_permission_group:仅前端下拉,不涉及后端写操作 -
usr_user_permission:sUserId, sPermGroupId, sBrandsId, sId, tCreateDate(先删后插)
依赖的接口
-
POST /api/auth/login(REQ-USR-004,获取 Bearer Token) -
GET /api/usr/users(REQ-USR-003,获取列表后点击行触发修改抽屉) -
GET /api/usr/users/staffs(REQ-USR-001,前端抽屉加载员工下拉) -
GET /api/usr/users/permission-groups(REQ-USR-001,前端抽屉加载权限组列表)
验收标准
-
PUT /api/usr/users/{validId}Body 合法 → HTTP 200,返回 userId/username/updatedAt,数据库对应字段已更新 -
userId不存在(或属于其他 brand)→ 返回 40400 - 非超级管理员 Token 调用 → 返回 40300
- 超级管理员修改自己的 userType → 返回 40301
-
isDisabled=true修改后,目标用户再登录 → 返回 40101(账号已禁用) - permGroupIds 为空数组 → 该用户权限组被清空,下次查询 permission-groups 对应关联为空
- permGroupIds 非空 → 权限组先删后插,关联记录与提交值一致
- 无 Token → 返回 401
- 前端:UserListPage 表格行有"修改"按钮,点击弹出预填修改抽屉;提交后关闭抽屉并刷新列表
前端交互设计
UserFormDrawer 改造(复用已有组件):
- 新增 props:
userId?: string,initialData?: { userType, language, canEditDoc, isDisabled, employeeId, permGroupIds: string[] } - 当
userId存在时(edit 模式):- 抽屉标题改为 "修改用户"
- 隐藏用户号和用户名 Form.Item(或显示为只读文本)
-
open时用initialData初始化 form 和 selectedPermIds - 提交调
updateUser(userId, req)而非createUser
- 当
userId不存在时(create 模式):行为与现有完全一致
UserListPage 改造:
- columns 末尾加"操作"列,包含"修改"按钮(PermButton,权限
usr:edit) - 点击"修改"按钮:将该行
UserListItemVO转换为initialData(permGroupIds 需额外从后端获取或在列表中推导——因列表不返回 permGroupIds,修改抽屉打开时无法预填权限组复选框,初始化为空数组即可,用户可重新勾选) - 新增状态:
editingUser: UserListItemVO | null,控制修改抽屉开关
新增 API 函数(frontend/src/api/usr.ts):
export interface UserUpdateReq {
userType: string
language: string
canEditDoc: boolean
isDisabled: boolean
employeeId: string | null
permGroupIds: string[]
}
export interface UserUpdateResp {
userId: string
username: string
updatedAt: string
}
export function updateUser(userId: string, req: UserUpdateReq): Promise<UserUpdateResp> {
return request.put(`/usr/users/${userId}`, req)
}