---
req_id: REQ-USR-001
date: 2026-04-30
round: 1
reviewer: superpower-code-reviewer
---

# Review: REQ-USR-001 — round 1

## 结论
approve

## Must-fix
（无）

## Nice-to-have

- docs/superpowers/specs/2026-04-30-REQ-USR-001.md § 实现范围与边界抉择 #3 + § 验收标准 工程验收 #2 写「BCryptPasswordEncoder bean 在 SecurityConfig 注册」，但实现已移到 `common/config/PasswordEncoderConfig.java`（理由：SecurityConfig 上 `@ConditionalOnWebApplication(SERVLET)` 在 `webEnvironment=NONE` 的 mapperIT/serviceTest 上下文不加载会让 encoder 缺失）。**正向设计修正**——密码编码器是领域基础设施而非 Web 安全基础设施。建议把 spec 这两处改为「BCryptPasswordEncoder 在 common/config/PasswordEncoderConfig 注册」，并在 PasswordEncoderConfig.java 类注释里写一行可追溯说明。
- backend/src/main/java/com/xly/erp/module/usr/service/impl/UserServiceImpl.java:104 — 循环里每次 `LocalDateTime.now()` 让 N 行 tUserPermission 的 `tCreateDate` 出现微秒级偏差。建议在 create 入口取一次 `LocalDateTime now`，user + 全部 permission 共享，更贴合「同一事务一次创建」的语义。
- backend/src/main/java/com/xly/erp/module/usr/service/impl/UserServiceImpl.java:69-75 — `permissionCategoryIds` 含重复 id（如 `[1,1,2]` 全部有效）时 SQL IN 隐式去重 → `countActiveByIds=2 ≠ ids.size=3` → 误抛 40023 假阴性。spec 没要求去重；建议 service 层先 `ids = ids.stream().distinct().toList()`，4 行修复。
- backend/src/test/java/com/xly/erp/module/usr/controller/UserControllerIT.java:73-75 — `postValidBody_with_jwt_returns200_andPersists` 没断言响应 `data` 中不含 `sPasswordHash`。spec § 验收 工程 #5 明言「不返回 sPasswordHash」。建议加 `assertThat(jb.get("data").has("sPasswordHash")).isFalse()` 锁住该不变量，未来若有人改 Map.of 加字段不会无声泄漏。
- backend/src/test/java/com/xly/erp/module/usr/controller/UserControllerIT.java:170-198 — `postWithoutJwt_permitAllStub_returns200_andCreatedBySTUBADMIN` / `postTamperedJwt_returns20001` 沿袭 MOD 模块 stub 路径仍缺 `// REQ-MOD-001 stub: see USR-004 follow-up` 锚点。建议在 module-report 阶段统一一次性补齐（与 MOD-004 review 同处理）。
- backend/src/main/java/com/xly/erp/common/security/SecurityConfig.java:26 — stub 锚点放在 `/api/mod/**` 与 `/api/usr/**` 共同上方。USR-004 实际收紧时需要分两条 requestMatchers 各自指向 hasAuthority/anyRequest，建议在 USR-004 plan 阶段显式记一笔回填动作。
- backend/src/main/java/com/xly/erp/module/usr/service/impl/UserServiceImpl.java:59 — `create` 方法体缺 `// REQ-USR-001: 用户新增` 行内锚点（与 MOD 模块同源遗漏）。建议在 module-report 阶段一次性把 USR / MOD 两个模块的 REQ 行内锚点统一补齐。

## 反例 / 测试覆盖缺口

Spec 验收清单（service 8 + mapperIT 4 + controllerIT 9 + 工程 5）100% 落地，sourcing `.env.local` 后 `mvn -B test` 全量 89/89 全绿。

- BCryptPasswordEncoder 重定位是必要正向修正，被 mapperIT × 5（NONE 环境）+ serviceTest × 8 + controllerIT × 9（RANDOM_PORT 环境）双向覆盖。
- 校验顺序（type/lang 枚举 → staff 存在 → permission 存在 → INSERT user catch DuplicateKey → INSERT permission × N）严格匹配 spec § 业务规则 1–6，`@Transactional(rollbackFor=Exception.class)` 包整个流程。
- `permissionCategoryMapper.countActiveByIds` 由 service 短路空 list 保护，避免 SQL `IN ()` 错误。
- N+1 INSERT tUserPermission 是 spec § 业务规则 #7 显式 YAGNI 取舍；docs/04 § 3.4「循环中禁止执行 DB 查询」语义针对 SELECT N+1 不是 INSERT。
- `sPasswordHash` 由 service 返回 Map 仅 put 两个 key 不会泄漏（构造保证），但缺直接断言锁不变量。
- BCrypt 每次 salt 不同——单测用真实 `BCryptPasswordEncoder` + `startsWith("$2a$")` 断言（非 mock），正确。
- IT cleanup 顺序 tUserPermission → tUser → tStaff → tPermissionCategory 满足 FK（iUserId CASCADE / iStaffId SET NULL / iCategoryId RESTRICT），无外键孤儿。
- SecurityContextHelper 匿名处理与 MOD 同款，stub 锚点缺失沿袭 MOD-004 已点出的范畴。

非阻塞缺口：spec 文字与实现 BCryptPasswordEncoder 位置不一致（见 nice-to-have #1）；`sPasswordHash` regression 锁断言（#4）；permissionCategoryIds 重复值假阴性（#3）；行内 REQ 锚点 + stub 锚点统一补齐（留 module-report）。
