2026-05-15-REQ-USR-003.md 17.2 KB

req_id: REQ-USR-003 date: 2026-05-15

spec_ref: docs/superpowers/specs/2026-05-15-REQ-USR-003.md

REQ-USR-003 修改用户 Implementation Plan

Execution: Parent skill feature-tdd executes this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 实现 GET /api/v1/users/{userId} + PUT /api/v1/users/{userId} 两个端点;PUT 支持部分字段更新 + permissionCategoryIds 增量增删差集 + 自我停用守卫;GET 返回 UserDetailVo(含 employeeName + permissionCategoryIds)。

Architecture:

  • 鉴权 / 角色守卫 / 事务 / Result / 异常处理全部复用 REQ-USR-002 基础设施。
  • 新增 UserUpdateService(PUT)+ UserDetailService(GET),单一职责。
  • permissionCategoryIds 用差集:先 select 现有 → 计算 toAdd / toRemove → DELETE + INSERT 在同一事务。
  • PATCH 语义简化:缺省 / 显式 null 都视为不变;employeeId=0 作为约定的"解除关联"信号。

Tech Stack: 复用 REQ-USR-002(Spring Boot 3 + MyBatis-Plus + Jakarta Validation)。


Schema 改动

无。


文件变更清单

新增

  • backend/src/main/java/com/xly/erp/module/usr/dto/UpdateUserReq.java
  • backend/src/main/java/com/xly/erp/module/usr/vo/UserDetailVo.java
  • backend/src/main/java/com/xly/erp/module/usr/service/UserDetailService.java
  • backend/src/main/java/com/xly/erp/module/usr/service/impl/UserDetailServiceImpl.java
  • backend/src/main/java/com/xly/erp/module/usr/service/UserUpdateService.java
  • backend/src/main/java/com/xly/erp/module/usr/service/impl/UserUpdateServiceImpl.java

修改

  • backend/src/main/java/com/xly/erp/common/response/ErrorCode.java(新增 40302 / 40401 + 404 映射)
  • backend/src/main/java/com/xly/erp/module/usr/mapper/SysUserMapper.java(新增 existsByUserCodeExcludingId
  • backend/src/main/java/com/xly/erp/module/usr/mapper/SysUserPermissionCategoryMapper.java(新增 selectPermissionCategoryIdsByUserId + deleteByUserAndCategoryIds
  • backend/src/main/java/com/xly/erp/module/usr/controller/UserController.java(新增 GET + PUT 方法)

文档:本 REQ 不改 docs/05(已含 REQ-USR-003 段,但 docs/05 当前漏写 GET 详情接口;本 REQ 补充)。

测试

  • backend/src/test/java/com/xly/erp/module/usr/service/UserDetailServiceImplTest.java
  • backend/src/test/java/com/xly/erp/module/usr/service/UserUpdateServiceImplTest.java
  • backend/src/test/java/com/xly/erp/module/usr/controller/UserControllerUpdateTest.java(独立文件避免冲击 REQ-USR-002 既有 UserControllerTest)

约束常量

ErrorCode 新增

常量 HTTP
USER_FORBIDDEN_SELF_DEACTIVATE 40302 403
USER_NOT_FOUND 40401 404

ErrorCode.toHttpStatus 已含 401/403/404 段位映射,不需新增。

API 形状

GET /api/v1/users/{userId}      @RequireSuperAdmin
  → Result<UserDetailVo>
  errors: 40101, 40301, 40401

PUT /api/v1/users/{userId}      @RequireSuperAdmin
  body: UpdateUserReq (PATCH)
  → Result<UserDetailVo>
  errors: 40001, 40004, 40101, 40301, 40302, 40401, 40902

UserDetailVo {
  Integer userId, String username, String userCode,
  String userType, String language, Boolean canEditDocument, Boolean isDeleted,
  Integer employeeId, String employeeName,
  List<Integer> permissionCategoryIds,
  String updatedBy, LocalDateTime updatedDate
}

UpdateUserReq {
  String userCode,                   // 缺省/null = 不变
  String userType,                   // 同上
  String language,                   // 同上
  Boolean canEditDocument,           // 同上
  Integer employeeId,                // 缺省/null = 不变;0 = 解除关联;正整数 = 更新
  Boolean isDeleted,                 // 同上
  List<Integer> permissionCategoryIds // 缺省/null = 不变;空数组 = 清空;非空 = 增删差集
}

PATCH 语义约定(写死,跨任务一致):所有字段缺省 / null 视为 "保持原值";employeeId == 0 视为 "解除关联",DB 写 NULL。其他字段无清除语义。


任务步骤

Task 1: ErrorCode 新增 40302 / 40401 + 404 映射 + docs/05 补 GET 详情段

Files:

  • Modify: backend/src/main/java/com/xly/erp/common/response/ErrorCode.java
  • Modify: backend/src/test/java/com/xly/erp/common/response/ErrorCodeTest.java
  • Modify: docs/05-API接口契约.md § REQ-USR-003 之前加 GET /api/v1/users/{userId}

API shape:

  • ErrorCode.USER_FORBIDDEN_SELF_DEACTIVATE = 40302
  • ErrorCode.USER_NOT_FOUND = 40401
  • ErrorCode.toHttpStatus(40302) == 403
  • ErrorCode.toHttpStatus(40401) == 404

docs/05 补充内容(追加到现 REQ-USR-003 段之前):

### REQ-USR-003 GET 用户详情

- Method: GET
- Path: /api/v1/users/{userId}
- Auth: Bearer Token;仅 userType=SUPER_ADMIN 可调用
- 请求: Path userId: int
- 响应: JSON UserDetailVo { userId, username, userCode, userType, language, canEditDocument, isDeleted, employeeId, employeeName, permissionCategoryIds[], updatedBy, updatedDate }

#### 错误码
- 40101 — 未携带或无效 Token
- 40301 — 当前用户非超级管理员,无权调用
- 40401 — 用户不存在
  • Step 1: 写失败测试 ErrorCodeTest#httpMappings_coverNewCodes_v003
  • Step 2: 实现最小代码 + docs/05 改动
  • Step 3: 子会话验证 PASS
  • Step 4: Commit chore(usr): docs/05 补 GET 详情 + ErrorCode 新增 40302/40401 REQ-USR-003

Task 2: UpdateUserReq + UserDetailVo

Files:

  • Create: backend/src/main/java/com/xly/erp/module/usr/dto/UpdateUserReq.java
  • Create: backend/src/main/java/com/xly/erp/module/usr/vo/UserDetailVo.java
  • Create: backend/src/test/java/com/xly/erp/module/usr/dto/UpdateUserReqValidationTest.java

API shape: 见 "约束常量"。

校验注解(仅在字段非 null 时生效,jakarta @Pattern 等天然 null-safe):

  • userCode: @Size(max=50)
  • userType: @Pattern(regexp="NORMAL|SUPER_ADMIN")
  • language: @Pattern(regexp="zh-CN|en-US|zh-TW")
  • employeeId: @Min(0) (0 = 解除关联约定)
  • 其余字段无 @ 注解

  • Step 1: 写失败测试 UpdateUserReqValidationTest 覆盖:

    • 全空请求体合法(PATCH,所有字段可选)
    • userType 非枚举 → 失败
    • language 非枚举 → 失败
    • userCode 越长 → 失败
    • userCode 空字符串 → 失败(@Size 不限定下界,但建议加 @Pattern("\S+") 防空白;本任务不强制)
    • employeeId=-1 → 失败
    • employeeId=0 合法(约定的解除关联)
  • Step 2: 实现最小代码

  • Step 3: 子会话验证 PASS

  • Step 4: Commit feat(usr): UpdateUserReq + UserDetailVo REQ-USR-003

Task 3: SysUserMapper.existsByUserCodeExcludingId

Files:

  • Modify: backend/src/main/java/com/xly/erp/module/usr/mapper/SysUserMapper.java
  • Modify: backend/src/test/java/com/xly/erp/module/usr/mapper/SysUserMapperTest.java

API shape:

  • SysUserMapper#existsByUserCodeExcludingId(String userCode, Integer excludedUserId) : boolean
  • SQL: SELECT EXISTS(SELECT 1 FROM sys_user WHERE sUserCode = #{userCode} AND iIncrement <> #{excludedUserId})

  • Step 1: 写失败测试

    • existsByUserCodeExcludingId_otherUserHasCode_returnsTrue (alice U001, query U001 excluding admin → true)
    • existsByUserCodeExcludingId_selfHasCode_returnsFalse (alice U001, query U001 excluding alice → false)
    • existsByUserCodeExcludingId_unknownCode_returnsFalse
  • Step 2: 实现最小代码

  • Step 3: 子会话验证 PASS

  • Step 4: Commit feat(usr): SysUserMapper 排除自身的 userCode 唯一查询 REQ-USR-003

Task 4: SysUserPermissionCategoryMapper 增删辅助方法

Files:

  • Modify: backend/src/main/java/com/xly/erp/module/usr/mapper/SysUserPermissionCategoryMapper.java
  • Create: backend/src/test/java/com/xly/erp/module/usr/mapper/SysUserPermissionCategoryMapperTest.java

API shape:

  • SysUserPermissionCategoryMapper#selectPermissionCategoryIdsByUserId(Integer userId) : List<Integer>
    • SQL: SELECT iPermissionCategoryId FROM sys_user_permission_category WHERE iUserId = #{userId}
  • SysUserPermissionCategoryMapper#deleteByUserAndCategoryIds(@Param("userId") Integer userId, @Param("ids") List<Integer> categoryIds) : int

    • SQL: DELETE FROM sys_user_permission_category WHERE iUserId = #{userId} AND iPermissionCategoryId IN (...) (@Select script)
  • Step 1: 写失败测试

    • 准备:admin user + 授权 {PUR, SAL}
    • selectPermissionCategoryIdsByUserId_returnsAllCurrent
    • deleteByUserAndCategoryIds_onlyDeletesGivenSubset(删 {PUR} 后剩 {SAL})
    • deleteByUserAndCategoryIds_nonMatchingIds_noop_returns0
  • Step 2: 实现最小代码

  • Step 3: 子会话验证 PASS

  • Step 4: Commit feat(usr): SysUserPermissionCategoryMapper 查/删辅助方法 REQ-USR-003

Task 5: UserDetailService 接口 + 实现

Files:

  • Create: backend/src/main/java/com/xly/erp/module/usr/service/UserDetailService.java
  • Create: backend/src/main/java/com/xly/erp/module/usr/service/impl/UserDetailServiceImpl.java
  • Create: backend/src/test/java/com/xly/erp/module/usr/service/UserDetailServiceImplTest.java

API shape:

  • UserDetailService#getById(Integer userId) : UserDetailVo
  • 找不到用户 → BizException(ErrorCode.USER_NOT_FOUND, "用户不存在")
  • 包含作废用户(用于恢复启用场景的回显)
  • 装配逻辑:select sys_user → 若 iEmployeeId 非空 select sys_employee.sEmployeeName → selectPermissionCategoryIdsByUserId

  • Step 1: 写失败测试

    • getById_existingActiveUser_returnsFullVo(含 employeeName + permissionCategoryIds)
    • getById_userWithoutEmployee_employeeNameIsNull
    • getById_userWithoutPermissions_emptyList
    • getById_deletedUser_stillReturned(iIsDeleted=1 的用户也能查到,VO.isDeleted=true)
    • getById_unknownId_throws40401
  • Step 2: 实现最小代码

  • Step 3: 子会话验证 PASS

  • Step 4: Commit feat(usr): UserDetailService 用户详情查询 REQ-USR-003

Task 6: UserUpdateService 接口 + 校验骨架

Files:

  • Create: backend/src/main/java/com/xly/erp/module/usr/service/UserUpdateService.java
  • Create: backend/src/main/java/com/xly/erp/module/usr/service/impl/UserUpdateServiceImpl.java
  • Create: backend/src/test/java/com/xly/erp/module/usr/service/UserUpdateServiceImplTest.java

API shape:

  • UserUpdateService#update(Integer userId, UpdateUserReq req, Integer operatorUserId, String operatorUsername) : UserDetailVo
  • @Transactional
  • 注入:SysUserMapper / SysEmployeeMapper / SysPermissionCategoryMapper / SysUserPermissionCategoryMapper / UserDetailService(复用详情装配返回最终 VO)

本 Task 范围:校验路径(不写入),全部抛 BizException 路径覆盖:

  1. userId 不存在 → 40401
  2. req.isDeleted == true && userId == operatorUserId → 40302
  3. req.userCode 非 null 且 existsByUserCodeExcludingId(userCode, userId) → 40902
  4. req.employeeId 是正整数(非 0)且不存在或软删 → 40004
  5. req.permissionCategoryIds 非 null 且非空且含不存在 ID → 40004
  • Step 1: 写失败测试 5 个测试覆盖以上路径
  • Step 2: 实现最小代码(更新方法体仅 throw,不写 DB)
  • Step 3: 子会话验证 PASS
  • Step 4: Commit feat(usr): UserUpdateService 校验路径骨架 REQ-USR-003

Task 7: UserUpdateService 部分字段写入 + employee 三态

Files:

  • Modify: backend/src/main/java/com/xly/erp/module/usr/service/impl/UserUpdateServiceImpl.java
  • Modify: backend/src/test/java/com/xly/erp/module/usr/service/UserUpdateServiceImplTest.java

API behavior:

  • 校验全过后用 UpdateWrapper<SysUser>().eq("iIncrement", userId) 链式 set:
    • if (req.userCode != null) → set sUserCode
    • if (req.userType != null) → set sUserType
    • if (req.language != null) → set sLanguage
    • if (req.canEditDocument != null) → set iCanEditDocument (true → 1, false → 0)
    • if (req.employeeId != null):req.employeeId == 0 → set iEmployeeId NULL;正整数 → set iEmployeeId 值
    • if (req.isDeleted != null) → set iIsDeleted (true → 1, false → 0)
    • 总是 set sUpdatedBy=operatorUsername、tUpdatedDate=NOW()
  • 不处理 permissionCategoryIds(推到 Task 8)

  • Step 1: 写失败测试

    • update_userCode_only_persisted_otherFieldsUnchanged
    • update_userType_language_canEditDocument
    • update_employeeId_positiveInteger_setsToValue
    • update_employeeId_zero_setsToNull
    • update_employeeId_unchanged_preservesOriginalValue(缺省字段不变)
    • update_isDeleted_true_marksUserDeleted
    • update_isDeleted_false_revivesUser
    • update_emptyRequest_onlyUpdatesAuditFields(sUpdatedBy + tUpdatedDate)
  • Step 2: 实现最小代码

  • Step 3: 子会话验证 PASS

  • Step 4: Commit feat(usr): UserUpdateService 部分字段写入 + employeeId 三态 REQ-USR-003

Task 8: UserUpdateService permissionCategoryIds 增量差集

Files:

  • Modify: backend/src/main/java/com/xly/erp/module/usr/service/impl/UserUpdateServiceImpl.java
  • Modify: backend/src/test/java/com/xly/erp/module/usr/service/UserUpdateServiceImplTest.java

API behavior:

  • 仅当 req.permissionCategoryIds != null 时执行:
    • current = selectPermissionCategoryIdsByUserId(userId)(Set 化以便比较)
    • target = new HashSet<>(req.permissionCategoryIds)(dedup)
    • toRemove = current \ targetdeleteByUserAndCategoryIds(userId, toRemove)(若 toRemove 非空)
    • toAdd = target \ current → 循环 insert SysUserPermissionCategory(sGrantedBy=operator)
  • 返回 userDetailService.getById(userId) 作为响应(聚合最新状态)

  • Step 1: 写失败测试

    • update_permissionCategories_emptyList_clearsAll(初始 {PUR,SAL} → 请求 [] → 最终 ∅)
    • update_permissionCategories_subsetDelta_addsAndRemoves(初始 {PUR,SAL} → 请求 [SAL, 新分类 X] → 最终 {SAL, X});断言 SAL 行 iIncrement 不变(差集而非全量替换)
    • update_permissionCategories_omitted_preservesExisting(请求中无 permissionCategoryIds → 不动)
    • update_permissionCategories_duplicateInRequest_deduped(请求 [PUR, PUR, SAL] → 最终 {PUR, SAL})
    • update_returnsUserDetailVoReflectingFinalState
  • Step 2: 实现最小代码

  • Step 3: 子会话验证 PASS

  • Step 4: Commit feat(usr): UserUpdateService 权限分类增量增删差集 REQ-USR-003

Task 9: UserController GET + PUT + 端到端测试

Files:

  • Modify: backend/src/main/java/com/xly/erp/module/usr/controller/UserController.java(追加 GET + PUT 方法)
  • Create: backend/src/test/java/com/xly/erp/module/usr/controller/UserControllerUpdateTest.java

API shape:

  • @GetMapping("/{userId}") @RequireSuperAdmin getById(@PathVariable Integer userId) : Result<UserDetailVo>
  • @PutMapping("/{userId}") @RequireSuperAdmin update(@PathVariable Integer userId, @RequestBody @Valid UpdateUserReq req) : Result<UserDetailVo>
  • PUT 调用 userUpdateService.update(userId, req, LoginContext.current().userId(), LoginContext.current().username())

端到端测试(覆盖 spec § 验收 1-23):

GET(5 个)

  • get_existingUser_returns200_andFullVo
  • get_unknownUser_returns404_40401
  • get_normalUser_returns403_40301
  • get_noAuthHeader_returns401_40101
  • get_deletedUser_stillReturns200

PUT(16 个)

  • put_updateUserCodeAndType_returns200
  • put_updateEmployeeId_toAnotherEmployee
  • put_updateEmployeeId_zero_clearsRelation
  • put_updateEmployeeId_unknown_returns400_40004
  • put_isDeletedTrue_marksAndOriginalTokenRejectedNextCall(修改后用旧 token 调 GET 应得 40101)
  • put_permissionCategories_subsetDelta(断言差集行为)
  • put_permissionCategories_emptyArray_clearsAll
  • put_permissionCategories_unknownId_returns400_40004_andRollsBack(断言事务回滚——sys_user 也未被改)
  • put_duplicateUserCode_returns409_40902
  • put_userCodeUnchangedSameAsSelf_returns200
  • put_selfDeactivate_returns403_40302
  • put_unknownProperty_username_returns400_40001
  • put_unknownProperty_password_returns400_40001
  • put_unknownUserId_returns404_40401
  • put_normalUser_returns403_40301
  • put_emptyBody_only_updates_audit_fields

  • Step 1: 写失败测试 (21 个测试)

  • Step 2: 实现最小代码(在现有 UserController 上追加方法)

  • Step 3: 子会话验证 PASS

  • Step 4: Commit feat(usr): GET + PUT /api/v1/users/{userId} controller + 端到端测试 REQ-USR-003


提交计划

Task Commit message
1 chore(usr): docs/05 补 GET 详情 + ErrorCode 新增 40302/40401 REQ-USR-003
2 feat(usr): UpdateUserReq + UserDetailVo REQ-USR-003
3 feat(usr): SysUserMapper 排除自身的 userCode 唯一查询 REQ-USR-003
4 feat(usr): SysUserPermissionCategoryMapper 查/删辅助方法 REQ-USR-003
5 feat(usr): UserDetailService 用户详情查询 REQ-USR-003
6 feat(usr): UserUpdateService 校验路径骨架 REQ-USR-003
7 feat(usr): UserUpdateService 部分字段写入 + employeeId 三态 REQ-USR-003
8 feat(usr): UserUpdateService 权限分类增量增删差集 REQ-USR-003
9 feat(usr): GET + PUT /api/v1/users/{userId} controller + 端到端测试 REQ-USR-003