2026-05-06-REQ-USR-002.md 9.11 KB

req_id: REQ-USR-002 date: 2026-05-06

spec_ref: docs/superpowers/specs/2026-05-06-REQ-USR-002.md

REQ-USR-002 用户修改 Implementation Plan

Execution: Parent skill feature-tdd executes this plan task-by-task.

Goal: 实现 PUT /api/users/{id}:除 sUserNo / sUserName / sPasswordHash 外的字段全量替换 + 权限组重建(先删后插),返回 UserVO。

Architecture: 复用 REQ-USR-001 的 entity/mapper/service/exception/Jackson 体系。Service load-then-modify:selectById → 校验 + 字段合并 → updateById(user)→ delete(关联) + 循环 insert(关联)。iStaffId 字段加 FieldStrategy.IGNORED 让 NULL 写入生效。

Tech Stack: 沿用前序 REQ。


Schema 改动

无。

文件变更清单

  • 修改: backend/src/main/java/com/xly/erp/common/response/ErrorCode.java — 追加 USR_NOT_FOUND(40431, "用户不存在或已删除")
  • 修改: backend/src/main/java/com/xly/erp/module/usr/entity/UserEntity.javaiStaffIdupdateStrategy = FieldStrategy.IGNORED
  • 创建: backend/src/main/java/com/xly/erp/module/usr/dto/UserUpdateDTO.java
  • 修改: backend/src/main/java/com/xly/erp/module/usr/service/UserService.java — 追加 update(Integer id, UserUpdateDTO dto): UserVO
  • 修改: backend/src/main/java/com/xly/erp/module/usr/service/impl/UserServiceImpl.java — 实现 update
  • 修改: backend/src/main/java/com/xly/erp/module/usr/controller/UserController.java — 追加 @PutMapping("/{id}")
  • 修改: backend/src/test/java/com/xly/erp/common/response/ApiResponseTest.java — 追加 1 个错误码断言
  • 创建: backend/src/test/java/com/xly/erp/module/usr/dto/UserUpdateDTOValidationTest.java
  • 修改: backend/src/test/java/com/xly/erp/module/usr/service/UserServiceImplTest.java — 追加 9 个 update 单测
  • 修改: backend/src/test/java/com/xly/erp/module/usr/controller/UserControllerIT.java — 追加 8 个 PUT 集成测试

任务步骤

Task 1: 错误码追加 + UserEntity.iStaffId IGNORED

Files:

  • Modify: backend/src/main/java/com/xly/erp/common/response/ErrorCode.java
  • Modify: backend/src/main/java/com/xly/erp/module/usr/entity/UserEntity.java
  • Modify: backend/src/test/java/com/xly/erp/common/response/ApiResponseTest.java

API shape:

  • USR_NOT_FOUND(40431, "用户不存在或已删除") 追加到 ErrorCode 枚举
  • UserEntity#iStaffId 字段注解改为 @TableField(value = "iStaffId", updateStrategy = FieldStrategy.IGNORED),并加注释说明(与 REQ-MOD-002 / module_mod 同样的副作用警告)

  • Step 1.1 写失败断言

    • ApiResponseTest#errorCode_constantsMatchDocs05Spec 追加 assertThat(ErrorCode.USR_NOT_FOUND.getCode()).isEqualTo(40431);
    • 子会话: FAIL
  • Step 1.2 实现错误码 + Entity 注解

    • 子会话验证:mvn -B test(全量;让 SpringBootTest 预热 lambda cache)应仍 PASS(USR-001 现有用例 + 新断言)
  • Step 1.3 提交

    • git commit -m "feat(common): USR_NOT_FOUND + iStaffId IGNORED REQ-USR-002"

Task 2: UserUpdateDTO + 校验单测

Files:

  • Create: backend/src/main/java/com/xly/erp/module/usr/dto/UserUpdateDTO.java
  • Test: backend/src/test/java/com/xly/erp/module/usr/dto/UserUpdateDTOValidationTest.java

API shape:

  • 字段(与 UserCreateDTO 相比剥除 sUserNo / sUserName):

    • Integer iStaffId(可空)
    • @NotBlank @Pattern(regexp="^(普通用户|超级管理员)$") String sUserType
    • @NotBlank @Pattern(regexp="^(zh|en|zh-TW)$") String sLanguage
    • Boolean bCanModifyDocs(可空,service 层语义:null 保留原值)
    • List<Integer> permissionCategoryIds(可空,空数组 / null 都视为清空)
  • Step 2.1 写失败测试(4 个)

    • UserUpdateDTOValidationTest#allValidFields_yieldsNoViolations
    • UserUpdateDTOValidationTest#blankRequiredFields_yieldsViolations(2 个 @NotBlank)
    • UserUpdateDTOValidationTest#invalidUserTypeEnum_yieldsViolation
    • UserUpdateDTOValidationTest#invalidLanguageEnum_yieldsViolation
    • 子会话: FAIL
  • Step 2.2 实现 DTO

    • 子会话: PASS
  • Step 2.3 提交

    • git commit -m "feat(usr): user update DTO REQ-USR-002"

Task 3: UserService.update + Mockito 单测

Files:

  • Modify: backend/src/main/java/com/xly/erp/module/usr/service/UserService.java(追加方法签名)
  • Modify: backend/src/main/java/com/xly/erp/module/usr/service/impl/UserServiceImpl.java
  • Modify: backend/src/test/java/com/xly/erp/module/usr/service/UserServiceImplTest.java(追加 9 个测试)

API shape:

  • UserService.update(Integer id, UserUpdateDTO dto): UserVO
  • 实现步骤(plan 锁定):
    1. target = userMapper.selectById(id)nullbDeleted=trueBizException(USR_NOT_FOUND)
    2. iStaffId 校验(仅当 dto.iStaffId 非空):staffMapper.selectById(...) null / bDeleted → BizException(STAFF_NOT_FOUND)
    3. 权限分类校验(仅当 dto.permissionCategoryIds 非空):selectBatchIds 长度 / bDeleted 检查 → BizException(PERM_CATEGORY_NOT_FOUND)
    4. 字段合并到 target(保留 sUserNo / sUserName / sPasswordHash / iIncrement / tCreateDate / sCreatedBy / tLastLoginDate / 多租户 / sId / bDeleted 三件套):
      • target.setIStaffId(dto.getIStaffId())(含 null 清空)
      • target.setSUserType(dto.getSUserType())
      • target.setSLanguage(dto.getSLanguage())
      • if (dto.getBCanModifyDocs() != null) target.setBCanModifyDocs(...)(部分更新)
    5. userMapper.updateById(target)
    6. 重建权限关联:
      • userPermissionMapper.delete(LambdaQueryWrapper.eq(iUserId, id)) 清空所有
      • 若 dto.permissionCategoryIds 非空 → 循环 insert(每条 iUserId / iCategoryId / tCreateDate=now)
    7. 返回 UserVO.from(target, dto.permissionCategoryIds 或 [])
  • @Transactional(rollbackFor = Exception.class)

  • Step 3.1 写失败测试(9 个)

    • update_targetNotFound_throws40431
    • update_targetSoftDeleted_throws40431
    • update_staffNotFound_throws40421
    • update_staffSoftDeleted_throws40421
    • update_permissionCategoryNotFound_throws40422
    • update_full_returnsVOWithUpdatedFields_andRebuildsPermissions:mock target;ArgumentCaptor 验
    • user 已修改字段:iStaffId / sUserType / sLanguage / bCanModifyDocs
    • user 保留字段:sUserNo / sUserName / sPasswordHash / iIncrement / tCreateDate / sCreatedBy
    • userPermissionMapper.delete 调一次(wrapper 含 eq iUserId);insert 调 N 次(按 dto 的 ids)
    • update_partialNullBCanModifyDocs_keepsOriginal
    • update_clearStaffId_setsToNull:dto.iStaffId=null,断言 captor entity.iStaffId == null
    • update_emptyPermissionCategoryIds_clearsAllAssociations:dto.permissionCategoryIds=[],verify userPermissionMapper.delete 被调一次 + insert never
    • 子会话: FAIL
  • Step 3.2 实现 service.update

    • 子会话: PASS(含原 9 个 USR-001 单测 + 9 个 USR-002 单测共 18 个)
  • Step 3.3 提交

    • git commit -m "feat(usr): update user service REQ-USR-002"

Task 4: UserController PUT + 端到端 IT

Files:

  • Modify: backend/src/main/java/com/xly/erp/module/usr/controller/UserController.java
  • Modify: backend/src/test/java/com/xly/erp/module/usr/controller/UserControllerIT.java(追加 8 个 PUT IT)

API shape:

  • @PutMapping("/{id}") ApiResponse<UserVO> update(@PathVariable Integer id, @Valid @RequestBody UserUpdateDTO dto)
  • Javadoc:REQ-USR-002 用户修改 — REQ-USR-004 完成后追加 @PreAuthorize("hasAuthority('USR:UPDATE')")

  • Step 4.1 写失败测试(8 个)

    • put_validUpdate_returns200_andDbReflects:先 mapper.insert 一个 user + staff + 3 个 cat + 3 个 userPermission;PUT 改 staff(另一个) + sUserType + sLanguage + bCanModifyDocs + permissionCategoryIds(2 个新 cat);断言 200 + reload 验证字段 + 关联表替换为 2 条
    • put_clearStaffId_setsNull:原 user.iStaffId=staffId,PUT 时 iStaffId=null,断言 DB user.iStaffId IS NULL
    • put_emptyPermissionCategoryIds_clearsAssociations:原有 3 条关联,PUT 传 [],断言 DB tUserPermission count = 0
    • put_targetNotFound_returns40431
    • put_staffNotFound_returns40421
    • put_permissionCategoryNotFound_returns40422
    • put_missingRequired_returns40010:缺 sUserType
    • put_ignoresProtectedFields_doesNotChangeUserNoOrName:手工拼 body 含 sUserNo / sUserName / sPasswordHash 字段,PUT 后 reload;断言 sUserNo / sUserName / sPasswordHash 与原值相同
    • 子会话: FAIL
  • Step 4.2 实现 PUT 端点

    • 子会话: PASS
  • Step 4.3 跑全量 backend 测试

    • cd backend && mvn -B test
    • 期望 101 + 1(新错误码)+ 4(DTO valid)+ 9(service unit)+ 8(controller IT)= 123 测试,全绿
  • Step 4.4 提交

    • git commit -m "feat(usr): PUT /api/users/{id} controller REQ-USR-002"

提交计划

  • feat(common): USR_NOT_FOUND + iStaffId IGNORED REQ-USR-002(Task 1)
  • feat(usr): user update DTO REQ-USR-002(Task 2)
  • feat(usr): update user service REQ-USR-002(Task 3)
  • feat(usr): PUT /api/users/{id} controller REQ-USR-002(Task 4)