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-tddexecutes 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.java—iStaffId加updateStrategy = 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_yieldsViolationUserUpdateDTOValidationTest#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 锁定):
-
target = userMapper.selectById(id);null或bDeleted=true→BizException(USR_NOT_FOUND) - iStaffId 校验(仅当 dto.iStaffId 非空):
staffMapper.selectById(...)null / bDeleted →BizException(STAFF_NOT_FOUND) - 权限分类校验(仅当 dto.permissionCategoryIds 非空):
selectBatchIds长度 / bDeleted 检查 →BizException(PERM_CATEGORY_NOT_FOUND) - 字段合并到 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(...)(部分更新)
-
userMapper.updateById(target)- 重建权限关联:
-
userPermissionMapper.delete(LambdaQueryWrapper.eq(iUserId, id))清空所有 - 若 dto.permissionCategoryIds 非空 → 循环 insert(每条 iUserId / iCategoryId / tCreateDate=now)
-
- 返回
UserVO.from(target, dto.permissionCategoryIds 或 [])
-
标
@Transactional(rollbackFor = Exception.class)-
Step 3.1 写失败测试(9 个)
update_targetNotFound_throws40431update_targetSoftDeleted_throws40431update_staffNotFound_throws40421update_staffSoftDeleted_throws40421update_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_returns40431put_staffNotFound_returns40421put_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)