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 参数:userIdusr_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。

业务规则

  1. 权限校验principal.userType() != "超级管理员" → 抛 BizException(40300),与 createUser 逻辑一致(来源:UserServiceImpl.createUser
  2. 用户存在性:按 sId = userId AND sBrandsId = brandIdusr_user;找不到 → 抛 BizException(40400)
  3. 自身角色保护principal.userId().equals(userId) 时,禁止将 userType超级管理员 改为其他值 → 抛 BizException(40301)(防止唯一管理员自我降权导致系统锁死;即使目标 userType 已是普通用户也拦截,保持规则简单)
  4. employeeId 校验:若 employeeId != null,需确认 tStaff.sId = employeeId AND sBrandsId = brandId 存在;不存在 → 抛 BizException(40001)
  5. 更新字段:仅更新 sUserType / sLanguage / bCanEditDoc / bIsDisabled / sEmployeeIdsUsername / sUserCode / sPasswordHash / sCreatorUsername / tCreateDate 不修改
  6. 权限组 replace 策略:先 DELETE FROM usr_user_permission WHERE sUserId = userId AND sBrandsId = brandId,再批量 INSERT 新的关联记录(与 createUser 的 permGroupIds 处理方式一致);permGroupIds 为空数组时仅删除,不插入
  7. 多租户隔离:所有查询和写入均带 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,前端抽屉加载权限组列表)

验收标准

  1. PUT /api/usr/users/{validId} Body 合法 → HTTP 200,返回 userId/username/updatedAt,数据库对应字段已更新
  2. userId 不存在(或属于其他 brand)→ 返回 40400
  3. 非超级管理员 Token 调用 → 返回 40300
  4. 超级管理员修改自己的 userType → 返回 40301
  5. isDisabled=true 修改后,目标用户再登录 → 返回 40101(账号已禁用)
  6. permGroupIds 为空数组 → 该用户权限组被清空,下次查询 permission-groups 对应关联为空
  7. permGroupIds 非空 → 权限组先删后插,关联记录与提交值一致
  8. 无 Token → 返回 401
  9. 前端:UserListPage 表格行有"修改"按钮,点击弹出预填修改抽屉;提交后关闭抽屉并刷新列表

前端交互设计

UserFormDrawer 改造(复用已有组件)

  • 新增 props:userId?: stringinitialData?: { 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)
}