--- req_id: REQ-USR-002 date: 2026-04-30 round: 1 reviewer: superpower-code-reviewer --- # Review: REQ-USR-002 — round 1 ## 结论 approve ## Must-fix (无) ## Nice-to-have - backend/src/main/java/com/xly/erp/module/usr/service/impl/UserServiceImpl.java:134-140 — 与 USR-001 review #3 同源:permissionCategoryIds=[1,1,2] 全有效时 SQL IN 隐式去重 → countActiveByIds=2 ≠ ids.size=3 → 误抛 40023 假阴性。建议 ids = ids.stream().distinct().toList() 4 行修复,或 module-report 阶段统一处理两处。 - backend/src/test/java/com/xly/erp/module/usr/controller/UserControllerIT.java:200-229 — `putValidBody_with_jwt_returns200_andUpdates` 已断言 sPasswordHash / sCreatedBy 保留,漏 tCreateDate 与 sBrandsId 锁。spec § 验收 工程 #2 显列 'sPasswordHash / sCreatedBy / tCreateDate 在 PUT 前后字面相同'。建议 SELECT 加这两列断言保留原值。 - backend/src/test/java/com/xly/erp/module/usr/controller/UserControllerIT.java:200-229 — 类似 USR-001 review #4:响应 data 缺 sPasswordHash 不变量没断言。建议加 `assertThat(jb.get("data").has("sPasswordHash")).isFalse()`。 - backend/src/test/java/com/xly/erp/module/usr/service/UserServiceImplTest.java:305-317 — `updateWithEmptyPermissionIds_clearsExisting` 只覆盖 null 路径,未覆盖 `[]` 空 list 路径。spec § 输入末行明言 'null / 空 list → 清空' 是同等语义。建议补等价 case 或 parameterize。 - backend/src/main/java/com/xly/erp/module/usr/dto/UpdateUserDTO.java — 类头缺 JavaDoc 说明 sPasswordHash 故意剔除(spec 显列 'API 契约声明该字段不可改')。两行注释帮助未来读者理解 DTO 与 CreateUserDTO 字段差异。 - backend/src/main/java/com/xly/erp/module/usr/service/impl/UserServiceImpl.java:120 — `update` 方法体缺 `// REQ-USR-002: 用户修改` 行内锚点(与 USR-001 review #7 同源)。建议 module-report 阶段统一补齐 USR/MOD 所有 REQ 行内锚点。 - backend/src/test/java/com/xly/erp/module/usr/controller/UserControllerIT.java:342-371 — `putWithoutJwt_permitAllStub_*` / `putTamperedJwt_*` 沿袭 stub 路径仍缺 `// REQ-MOD-001 stub: see USR-004 follow-up` 锚点;与上条统一 module-report 处理。 ## 反例 / 测试覆盖缺口 Spec 验收清单(service 10 + mapperIT 1 + controllerIT 10)100% 落地,mvn 110/110 全绿。 - 重建权限组策略(先 deleteByUserId 后 batch INSERT)严格匹配 spec § 业务规则 #6,`@Transactional(rollbackFor = Exception.class)` 包整流程。 - 流程顺序(selectById 存在性 → 枚举 → staff → permission → updateById catch DupKey → deleteByUserId → INSERT × N)严格匹配 spec § 业务规则 1–7。 - updateById 抛 DupKey 后 deleteByUserId 不被调用由 `verify(userPermissionMapper, never()).deleteByUserId(any())` 单测显式锁定,事务回滚由 @Transactional 保证。 - 不可改字段(sPasswordHash / sCreatedBy / tCreateDate / sBrandsId / sSubsidiaryId)依赖 MyBatis-Plus 默认 NOT_NULL 跳过 null 字段策略;service 单测用 ArgumentCaptor 断言这些字段在 entity 上为 null。 - permissionCategoryIds null / 空 list 通过 `ids != null && !ids.isEmpty()` 短路同时跳过校验与 INSERT 但仍执行 deleteByUserId,正确实现「清空」语义。 - LocalDateTime.now() 已 hoist 出 INSERT 循环(修正 USR-001 review nice-to-have #2)。 非阻塞缺口:permissionCategoryIds 重复 id 假阴性、sPasswordHash 响应不变量锁、tCreateDate/sBrandsId 保留断言、`[]` 空 list 等价路径覆盖、行内 REQ 锚点 + stub 锚点。前 4 条可在本 REQ 后续修补,后 2 条按既定路径留 module-report 统一处理。