diff --git a/docs/superpowers/module-reports/2026-05-07-module_usr.md b/docs/superpowers/module-reports/2026-05-07-module_usr.md new file mode 100644 index 0000000..776bc8d --- /dev/null +++ b/docs/superpowers/module-reports/2026-05-07-module_usr.md @@ -0,0 +1,146 @@ +--- +module_id: module_usr +date: 2026-05-07 +git_range: 237a97e..6e0c0e7 (27 commits) +--- + +# 模块完成报告 — module_usr 用户管理 + +## ① 模块信息 +- 模块 ID: module_usr +- 模块名: 用户管理(账户主数据 / 权限关联 / 列表查询 / 登录认证) +- 开发区间: 237a97e(master,含 module_mod merge)→ 6e0c0e7(test-gate evidence),共 27 个 commits + +## ② REQ 完成清单 + +- [x] REQ-USR-001 — 用户新增(`POST /api/users`) + - spec: docs/superpowers/specs/2026-05-06-REQ-USR-001.md + - plan: docs/superpowers/plans/2026-05-06-REQ-USR-001.md + - review: docs/superpowers/reviews/2026-05-06-REQ-USR-001.md(round 2 approve) +- [x] REQ-USR-002 — 用户修改(`PUT /api/users/{id}`) + - spec: docs/superpowers/specs/2026-05-06-REQ-USR-002.md + - plan: docs/superpowers/plans/2026-05-06-REQ-USR-002.md + - review: docs/superpowers/reviews/2026-05-06-REQ-USR-002.md(round 1 approve) +- [x] REQ-USR-003 — 用户查询(`GET /api/users`) + - spec: docs/superpowers/specs/2026-05-06-REQ-USR-003.md + - plan: docs/superpowers/plans/2026-05-06-REQ-USR-003.md + - review: docs/superpowers/reviews/2026-05-06-REQ-USR-003.md(round 2 approve) +- [x] REQ-USR-004 — 用户登录(`POST /api/auth/login`) + - spec: docs/superpowers/specs/2026-05-06-REQ-USR-004.md + - plan: docs/superpowers/plans/2026-05-06-REQ-USR-004.md + - review: docs/superpowers/reviews/2026-05-06-REQ-USR-004.md(round 2 approve) + +## ③ 文件变更表 + +| 文件 | 操作 | 说明 | +|---|---|---| +| backend/pom.xml | M | 追加 jjwt-api / jjwt-impl / jjwt-jackson 0.12.6(REQ-USR-004 JWT 实现) | +| backend/src/main/java/com/xly/erp/common/response/ErrorCode.java | M | 追加 6 个常量:STAFF_NOT_FOUND / PERM_CATEGORY_NOT_FOUND / USR_NOT_FOUND / USR_USER_NAME_OR_NO_DUP / LOGIN_INVALID_CREDENTIALS / LOGIN_ACCOUNT_LOCKED | +| backend/src/main/java/com/xly/erp/common/response/PageResult.java | A | 通用分页 VO(REQ-USR-003 引入) | +| backend/src/main/java/com/xly/erp/common/exception/AccountLockedException.java | A | 携带 cooldownSeconds 的账号锁定异常(REQ-USR-004) | +| backend/src/main/java/com/xly/erp/common/exception/GlobalExceptionHandler.java | M | 追加 AccountLockedException 专用 handler 把 cooldownSeconds 写入 ApiResponse.data | +| backend/src/main/java/com/xly/erp/config/PasswordConfig.java | A | BCryptPasswordEncoder bean(REQ-USR-001 引入) | +| backend/src/main/java/com/xly/erp/config/MybatisPlusConfig.java | A | PaginationInnerInterceptor 注册(REQ-USR-003 引入) | +| backend/src/main/java/com/xly/erp/module/usr/entity/{User,Staff,PermissionCategory,UserPermission}Entity.java | A | 4 张表实体;UserEntity.iStaffId 加 FieldStrategy.IGNORED(REQ-USR-002)| +| backend/src/main/java/com/xly/erp/module/usr/mapper/{User,Staff,PermissionCategory,UserPermission}Mapper.java | A | BaseMapper 子接口;UserMapper 追加 searchUsers 自定义 SQL 方法 | +| backend/src/main/resources/mapper/usr/UserMapper.xml | A | 跨表 JOIN tStaff + 动态 WHERE + CAST 处理 bit(1) deleted(REQ-USR-003)| +| backend/src/main/java/com/xly/erp/module/usr/dto/{UserCreate,UserUpdate,UserQuery,Login}DTO.java | A | 4 个 DTO(POST/PUT/GET/Login) | +| backend/src/main/java/com/xly/erp/module/usr/vo/{User,UserListItem,LoginResult}VO.java | A | 3 个 VO(创建/修改返回 / 列表行 / 登录结果含嵌套 LoginUserInfo) | +| backend/src/main/java/com/xly/erp/module/usr/service/{User,Login}Service.java + impl/{User,Login}ServiceImpl.java | A | 5 业务方法:UserService.create/update/search + LoginService.login | +| backend/src/main/java/com/xly/erp/module/usr/controller/{User,Login}Controller.java | A | 4 端点:POST /api/users / PUT /api/users/{id} / GET /api/users / POST /api/auth/login | +| backend/src/main/java/com/xly/erp/module/usr/security/{LoginAttemptStore,InMemoryLoginAttemptStore,JwtTokenProvider}.java | A | JWT 签发 + 5 次失败 15 min 内存锁定(REQ-USR-004) | +| backend/src/test/java/com/xly/erp/common/response/ApiResponseTest.java | M | 追加 6 个错误码断言 | +| backend/src/test/java/com/xly/erp/module/usr/dto/{UserCreate,UserUpdate,UserQuery,Login}DTOValidationTest.java | A | 4 套 Bean Validation 单测(共 18 个用例)| +| backend/src/test/java/com/xly/erp/module/usr/mapper/{UsrMappersIT,UserMapperSearchIT}.java | A | 4 表 insert/select smoke + searchUsers SQL IT | +| backend/src/test/java/com/xly/erp/module/usr/service/{User,Login}ServiceImplTest.java | A | Mockito 单测:30 个用例(9 create + 9 update + 5 search + 7 login)| +| backend/src/test/java/com/xly/erp/module/usr/controller/{User,Login}ControllerIT.java | A | MockMvc 集成:32 用例(7 POST + 8 PUT + 8 GET + 9 login) | +| backend/src/test/java/com/xly/erp/module/usr/security/{InMemoryLoginAttemptStore,JwtTokenProvider}Test.java | A | 7 单测(5 store + 2 jwt)| +| docs/08-模块任务管理.md | M | § 二 module_usr 4 个 REQ 全部勾选 | +| docs/superpowers/specs/2026-05-06-REQ-USR-00{1..4}.md | A | 4 份功能规格 | +| docs/superpowers/plans/2026-05-06-REQ-USR-00{1..4}.md | A | 4 份任务级实现计划 | +| docs/superpowers/reviews/2026-05-06-REQ-USR-00{1..4}.md | A | 4 份 AI 审阅报告(含 round 2 修复闭环) | +| docs/superpowers/module-reports/module_usr-test-gate.md | A | 本模块 test-gate 闸门证据 | + +## ④ 数据库使用表 + +- 读: `tUser`(4 个 REQ 都读)/ `tStaff`(USR-001 staff 校验 + USR-003 LEFT JOIN 列表 + USR-002 父校验)/ `tPermissionCategory`(USR-001 / USR-002 关联校验)/ `tUserPermission`(USR-002 重建关联前的 select) +- 写: `tUser`(USR-001 insert / USR-002 update / USR-004 update tLastLoginDate)/ `tUserPermission`(USR-001 insert 关联 / USR-002 重建关联 delete + insert) + +本模块**不写** `tStaff` 和 `tPermissionCategory`——这两张表当前是只读字典,spec 已注明若需增删改请新建独立模块。 + +## ⑤ 测试结果 + +- `scripts/test.sh` 最终:green +- 通过: 172 / 失败: 0 / 跳过: 0 +- 覆盖率: 未启用 JaCoCo(与 module_mod 一致) + +测试分布: +- 单元测试约 81 个:DTO Validation 18 + ApiResponseTest 7 + GlobalExceptionHandlerTest 4 + ModuleServiceImplTest 26 + ModuleCreateDTOValidationTest 5 + UserServiceImplTest 21 + LoginServiceImplTest 7 + 其他(store / jwt 等)8 +- 集成测试约 91 个:ApplicationTest 1 + SecurityConfigTest 1 + ModuleMapperIT 2 + ModuleControllerIT 28 + UsrMappersIT 4 + UserMapperSearchIT 2 + UserControllerIT 25 + LoginControllerIT 10 + 其他 + +`./scripts/test.sh` 流程:setup-test-db.sh DROP+CREATE → mvn build → mvn lint(compile) → mvn test → frontend skip → e2e 略 → reset DB。耗时 ~25s。 + +## ⑥ 本模块新增 Migration + +—(本模块未引入 schema 改动;tUser / tStaff / tPermissionCategory / tUserPermission 由 V1__initial_schema.sql 在 A4 阶段创建) + +## ⑦ 跨模块改动清单(软规则 S2) + +本模块改动了 `common/` 包下的横切组件,按 CLAUDE.md § 🟡 S2 登记: + +| 文件 | 改动 | 原因 | 影响评估 | +|---|---|---|---| +| common/response/ErrorCode.java | 追加 6 个常量(STAFF_NOT_FOUND / PERM_CATEGORY_NOT_FOUND / USR_NOT_FOUND / USR_USER_NAME_OR_NO_DUP / LOGIN_INVALID_CREDENTIALS / LOGIN_ACCOUNT_LOCKED) | USR 模块业务异常分类 | 仅追加,不修改既有常量;与 module_mod 共享 ErrorCode 段位(如 STAFF_NOT_FOUND(40421) 与 MOD_NOT_FOUND(40421) 数值相同语义不同),由枚举名 + message 区分。docs/05 全局错误码表后续 sweep 同步登记 | +| common/response/PageResult.java | 新建 | REQ-USR-003 引入通用分页 VO;下一模块需要分页时直接复用 | 横向组件;本模块单点引入,未来其他模块共享 | +| common/exception/AccountLockedException.java | 新建 | REQ-USR-004 携带 cooldownSeconds 的专用异常 | 仅 LoginService 抛;GlobalExceptionHandler 专用 handler 不影响其他 BizException 路径 | +| common/exception/GlobalExceptionHandler.java | 追加 `@ExceptionHandler(AccountLockedException.class)` | 把 cooldownSeconds 写入 ApiResponse.data | 既有 BizException / Validation / Exception handler 路径 0 改动;前端契约 100% 兼容 | +| config/PasswordConfig.java | 新建 | REQ-USR-001 引入 BCryptPasswordEncoder bean | 横向组件,REQ-USR-004 复用;无副作用 | +| config/MybatisPlusConfig.java | 新建 | REQ-USR-003 引入 PaginationInnerInterceptor | 影响所有 MP `Page` 调用路径,但 module_mod 现有 mapper 未用 Page,无回归 | + +> **未自动生成 cross-module-log 存根**:log-cross-module.sh hook 未触发(可能因为 common/ 不在 hook 监控的"其他业务模块"路径中);本节内容由 module-report 直接登记。 + +## ⑧ 偏离 spec 清单 + +- **REQ-USR-001**:spec/plan 早期草稿要求 `tUserPermission.bSelected=1` 字段,与 docs/03 修订版(已删该列)不一致。fix commit 520c01f 把 spec/plan 中 bSelected 提及改为"无该列"注解;UserPermissionEntity 不含此字段。 +- **REQ-USR-002**:iStaffId 加 `FieldStrategy.IGNORED` 让 NULL 写入生效——同 module_mod ModuleEntity.iParentId 的全局副作用;本期所有 update 路径走 load-then-modify 安全,但**未来 partial update 路径必须 selectById 后再 updateById**,否则 iStaffId 会被静默清空。 +- **REQ-USR-003**:spec § 业务规则 6 deleted 字段过滤 round 1 review 发现 SQL 注入 + 实现缺陷;fix commit f53689c 改成 `LambdaUpdateWrapper` 的 `column` 通过 `@Param` 单独传入(防 GET query-string 绕过白名单)+ XML deleted 分支用 `CAST(#{queryValue} AS UNSIGNED)` 兼容 bit(1)。 +- **REQ-USR-004 (CRITICAL)**:JwtTokenProviderTest round 1 提交时硬编码了与 .env.local 完全相同的生产 JWT_SECRET(commit b7ed804 已入 git history)。fix commit d439c0d 把测试 SECRET 改成与生产无关的 fake 值,**.env.local JWT_SECRET 已本地旋转为新随机值;旧值仍在 git history**——所有部署环境必须运维侧同步轮换 JWT_SECRET。 +- **REQ-USR-004**:InMemoryLoginAttemptStore round 1 锁定到期不重置 count(business rule #4 不达成),fix commit d439c0d 修复 cooldownSeconds + recordFailure 双路径处理过期 reset。 +- **REQ-USR-004 (范围说明已声明)**:本期**仅签发 JWT**,不切换其他端点为 authenticated;module_mod 4 端点 + module_usr 3 端点(POST/PUT/GET)仍 permitAll。这是已知技术债(spec § 范围说明 + § ⑩ 已知问题登记)。 +- **REQ-USR-004**:spec § 业务规则 7 客户端 IP 审计未实施(log.info 没拿 HttpServletRequest.getRemoteAddr());登记为已知 gap。 +- **跨 REQ — 多租户字段 / sCreatedBy 留 NULL**:所有写入路径中 `sBrandsId / sSubsidiaryId / sCreatedBy` 都落 NULL;同 module_mod 一致,等 REQ-USR-XXX 引入登录上下文 / 多租户拦截器后回填。 + +## ⑨ AI reviewer 报告汇总 + +- REQ-USR-001: round 1 — request-changes(spec/plan 残留 bSelected)→ round 2 — approve(link: docs/superpowers/reviews/2026-05-06-REQ-USR-001.md,修复 commit 520c01f) +- REQ-USR-002: round 1 — approve(link: docs/superpowers/reviews/2026-05-06-REQ-USR-002.md) +- REQ-USR-003: round 1 — request-changes(HIGH SQL 注入 column 字段绕过 + spec § 6 deleted 未实现 + IT 静默移除 + XML 边界)→ round 2 — approve(link: docs/superpowers/reviews/2026-05-06-REQ-USR-003.md,修复 commit f53689c) +- REQ-USR-004: round 1 — request-changes(CRITICAL 测试 SECRET 与生产一致 + HIGH 锁定到期不重置 + MEDIUM 验收 #9 无测试)→ round 2 — approve(link: docs/superpowers/reviews/2026-05-06-REQ-USR-004.md,修复 commit d439c0d) + +## ⑩ 已知问题 + +1. **JWT_SECRET 已永久污染 git history**:commit b7ed804(JwtTokenProviderTest 早期版本)含与 .env.local 相同的 32 字节 JWT_SECRET。.env.local 已旋转为新随机值。**所有部署环境必须由运维同步轮换 JWT_SECRET**——任何生产 / 测试 / 演示环境若仍用旧 secret 视同已泄露,必须签发新 secret 并强制所有用户重新登录。建议下一 sweep 评估 BFG / git-filter-repo 重写 history 移除该 secret。 +2. **鉴权延期未清算**:本 REQ 落地登录 + JWT 签发,但 module_mod 4 端点 + module_usr 3 端点(POST /api/users / PUT /api/users/{id} / GET /api/users)仍 permitAll,没有 @PreAuthorize 也没 JwtAuthenticationFilter 拦截 token。技术债:(a) 切 SecurityFilterChain 为 authenticated();(b) 写 JwtAuthenticationFilter 校验 token 并装 SecurityContext;(c) 给所有 controller 方法加 @PreAuthorize;(d) 改造既有 IT 携带 token。这是中等量工作(约 1-2 个 sweep day),独立工时排期。 +3. **多租户字段 / sCreatedBy NULL**:与 module_mod 一致;REQ-USR-XXX 引入登录上下文后回填 + 必要时 V_n migration 给历史数据补默认值 + 视业务决策收紧 schema 为 NOT NULL。 +4. **iStaffId IGNORED 全局副作用**:UserEntity.iStaffId 与 ModuleEntity.iParentId 同类风险——任何 partial updateById 路径都会静默清空该列。本期所有路径走 load-then-modify 安全;未来贡献者新增 partial path 必须复用 LambdaUpdateWrapper.set(...) 模式。 +5. **InMemoryLoginAttemptStore 单机限制**:5 次失败锁定在多实例部署下不工作;spec 已声明 Redis 替换留作后续 REQ。`expireLockForTest` 包私有调试入口暴露在生产 jar 中——下一 sweep 移到 src/test 的 testutil。 +6. **客户端 IP 审计未实施**:spec § 业务规则 7 要求 log.info 含客户端 IP;当前 LoginService 只记 sUserName。建议后续在 LoginController 注入 HttpServletRequest.getRemoteAddr() 传给 service。 +7. **Redis 凭据 / 部署**:docs/04 § 零 列了 Redis;本仓库 .env.local 未配 Redis;REQ-USR-004 内存锁定 + 后续可能的会话 / 缓存特性都待 Redis 接入后才能演进。 +8. **REQ-USR-002 spec 验收 #11 IT 回滚证据**:受 IT @Transactional+@Rollback 包裹,service 层回滚无法在 IT 中观测;service 单测已覆盖语义,IT 留 nice-to-have。 +9. **REQ-USR-003 nice-to-have 6 条 IT 缺失**:department equals / deleted=false 显式 / notContains / 排序 / matchType 非枚举 / 空结果 IT;service 单测已覆盖核心。 +10. **docs/05 错误码段位与实际实现偏差**:docs/05 § REQ-USR-001 写 40020 段位;实现统一用 40010 PARAM_INVALID。docs sweep 时对齐。 +11. **JacksonConfig 字段访问可见性配置全局生效**:影响所有 DTO/VO 的 JSON 序列化;当前所有字段都用 @Data 暴露,没有 @JsonIgnore 跳过敏感字段的需求;sPasswordHash 不暴露是因为 UserVO / LoginResultVO.LoginUserInfo / UserListItemVO 都不含该字段而非 @JsonIgnore。后续若有需要隐藏字段的 VO,需重新评估全局策略。 + +## ⑪ 下一模块预览 + +按 docs/02 § 二 顺序,**module_usr 是最后一个模块**——module_mod 已 merge,本 module_usr 是项目计划中的第二个也是最终模块。MR merge 后即视为「全部 REQ 完成」。 + +后续工作(不属于 docs/02 计划清单的 REQ): +- **鉴权清算 sweep**:切 SecurityFilterChain 到 authenticated + JwtAuthenticationFilter + 所有 controller @PreAuthorize + 既有 IT 携带 token 改造(见 § ⑩ #2)。 +- **JWT_SECRET history 清理**:评估是否需要 BFG / git-filter-repo 重写 history(见 § ⑩ #1)。 +- **多租户上下文 + Redis 引入**:补完 sBrandsId / sSubsidiaryId / sCreatedBy 写入;用 Redis 替换 InMemoryLoginAttemptStore(见 § ⑩ #3, #5, #7)。 +- **前端**:本期完全无 frontend 实现;docs/06 § 五 已规划用户管理页面 + 登录页,待前端工程接入后实现。 + +## ⑫ MR 链接 + +待 `mr-create` 推送后回填。