Commit 976f2cade430943e95ec9431ec904984c33b62c4

Authored by zichun
1 parent 6e0c0e78

docs(module_usr): add module completion report

docs/superpowers/module-reports/2026-05-07-module_usr.md 0 → 100644
  1 +---
  2 +module_id: module_usr
  3 +date: 2026-05-07
  4 +git_range: 237a97e..6e0c0e7 (27 commits)
  5 +---
  6 +
  7 +# 模块完成报告 — module_usr 用户管理
  8 +
  9 +## ① 模块信息
  10 +- 模块 ID: module_usr
  11 +- 模块名: 用户管理(账户主数据 / 权限关联 / 列表查询 / 登录认证)
  12 +- 开发区间: 237a97e(master,含 module_mod merge)→ 6e0c0e7(test-gate evidence),共 27 个 commits
  13 +
  14 +## ② REQ 完成清单
  15 +
  16 +- [x] REQ-USR-001 — 用户新增(`POST /api/users`)
  17 + - spec: docs/superpowers/specs/2026-05-06-REQ-USR-001.md
  18 + - plan: docs/superpowers/plans/2026-05-06-REQ-USR-001.md
  19 + - review: docs/superpowers/reviews/2026-05-06-REQ-USR-001.md(round 2 approve)
  20 +- [x] REQ-USR-002 — 用户修改(`PUT /api/users/{id}`)
  21 + - spec: docs/superpowers/specs/2026-05-06-REQ-USR-002.md
  22 + - plan: docs/superpowers/plans/2026-05-06-REQ-USR-002.md
  23 + - review: docs/superpowers/reviews/2026-05-06-REQ-USR-002.md(round 1 approve)
  24 +- [x] REQ-USR-003 — 用户查询(`GET /api/users`)
  25 + - spec: docs/superpowers/specs/2026-05-06-REQ-USR-003.md
  26 + - plan: docs/superpowers/plans/2026-05-06-REQ-USR-003.md
  27 + - review: docs/superpowers/reviews/2026-05-06-REQ-USR-003.md(round 2 approve)
  28 +- [x] REQ-USR-004 — 用户登录(`POST /api/auth/login`)
  29 + - spec: docs/superpowers/specs/2026-05-06-REQ-USR-004.md
  30 + - plan: docs/superpowers/plans/2026-05-06-REQ-USR-004.md
  31 + - review: docs/superpowers/reviews/2026-05-06-REQ-USR-004.md(round 2 approve)
  32 +
  33 +## ③ 文件变更表
  34 +
  35 +| 文件 | 操作 | 说明 |
  36 +|---|---|---|
  37 +| backend/pom.xml | M | 追加 jjwt-api / jjwt-impl / jjwt-jackson 0.12.6(REQ-USR-004 JWT 实现) |
  38 +| 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 |
  39 +| backend/src/main/java/com/xly/erp/common/response/PageResult.java | A | 通用分页 VO(REQ-USR-003 引入) |
  40 +| backend/src/main/java/com/xly/erp/common/exception/AccountLockedException.java | A | 携带 cooldownSeconds 的账号锁定异常(REQ-USR-004) |
  41 +| backend/src/main/java/com/xly/erp/common/exception/GlobalExceptionHandler.java | M | 追加 AccountLockedException 专用 handler 把 cooldownSeconds 写入 ApiResponse.data |
  42 +| backend/src/main/java/com/xly/erp/config/PasswordConfig.java | A | BCryptPasswordEncoder bean(REQ-USR-001 引入) |
  43 +| backend/src/main/java/com/xly/erp/config/MybatisPlusConfig.java | A | PaginationInnerInterceptor 注册(REQ-USR-003 引入) |
  44 +| 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)|
  45 +| backend/src/main/java/com/xly/erp/module/usr/mapper/{User,Staff,PermissionCategory,UserPermission}Mapper.java | A | BaseMapper 子接口;UserMapper 追加 searchUsers 自定义 SQL 方法 |
  46 +| backend/src/main/resources/mapper/usr/UserMapper.xml | A | 跨表 JOIN tStaff + 动态 WHERE + CAST 处理 bit(1) deleted(REQ-USR-003)|
  47 +| backend/src/main/java/com/xly/erp/module/usr/dto/{UserCreate,UserUpdate,UserQuery,Login}DTO.java | A | 4 个 DTO(POST/PUT/GET/Login) |
  48 +| backend/src/main/java/com/xly/erp/module/usr/vo/{User,UserListItem,LoginResult}VO.java | A | 3 个 VO(创建/修改返回 / 列表行 / 登录结果含嵌套 LoginUserInfo) |
  49 +| 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 |
  50 +| 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 |
  51 +| backend/src/main/java/com/xly/erp/module/usr/security/{LoginAttemptStore,InMemoryLoginAttemptStore,JwtTokenProvider}.java | A | JWT 签发 + 5 次失败 15 min 内存锁定(REQ-USR-004) |
  52 +| backend/src/test/java/com/xly/erp/common/response/ApiResponseTest.java | M | 追加 6 个错误码断言 |
  53 +| backend/src/test/java/com/xly/erp/module/usr/dto/{UserCreate,UserUpdate,UserQuery,Login}DTOValidationTest.java | A | 4 套 Bean Validation 单测(共 18 个用例)|
  54 +| backend/src/test/java/com/xly/erp/module/usr/mapper/{UsrMappersIT,UserMapperSearchIT}.java | A | 4 表 insert/select smoke + searchUsers SQL IT |
  55 +| 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)|
  56 +| 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) |
  57 +| backend/src/test/java/com/xly/erp/module/usr/security/{InMemoryLoginAttemptStore,JwtTokenProvider}Test.java | A | 7 单测(5 store + 2 jwt)|
  58 +| docs/08-模块任务管理.md | M | § 二 module_usr 4 个 REQ 全部勾选 |
  59 +| docs/superpowers/specs/2026-05-06-REQ-USR-00{1..4}.md | A | 4 份功能规格 |
  60 +| docs/superpowers/plans/2026-05-06-REQ-USR-00{1..4}.md | A | 4 份任务级实现计划 |
  61 +| docs/superpowers/reviews/2026-05-06-REQ-USR-00{1..4}.md | A | 4 份 AI 审阅报告(含 round 2 修复闭环) |
  62 +| docs/superpowers/module-reports/module_usr-test-gate.md | A | 本模块 test-gate 闸门证据 |
  63 +
  64 +## ④ 数据库使用表
  65 +
  66 +- 读: `tUser`(4 个 REQ 都读)/ `tStaff`(USR-001 staff 校验 + USR-003 LEFT JOIN 列表 + USR-002 父校验)/ `tPermissionCategory`(USR-001 / USR-002 关联校验)/ `tUserPermission`(USR-002 重建关联前的 select)
  67 +- 写: `tUser`(USR-001 insert / USR-002 update / USR-004 update tLastLoginDate)/ `tUserPermission`(USR-001 insert 关联 / USR-002 重建关联 delete + insert)
  68 +
  69 +本模块**不写** `tStaff` 和 `tPermissionCategory`——这两张表当前是只读字典,spec 已注明若需增删改请新建独立模块。
  70 +
  71 +## ⑤ 测试结果
  72 +
  73 +- `scripts/test.sh` 最终:green
  74 +- 通过: 172 / 失败: 0 / 跳过: 0
  75 +- 覆盖率: 未启用 JaCoCo(与 module_mod 一致)
  76 +
  77 +测试分布:
  78 +- 单元测试约 81 个:DTO Validation 18 + ApiResponseTest 7 + GlobalExceptionHandlerTest 4 + ModuleServiceImplTest 26 + ModuleCreateDTOValidationTest 5 + UserServiceImplTest 21 + LoginServiceImplTest 7 + 其他(store / jwt 等)8
  79 +- 集成测试约 91 个:ApplicationTest 1 + SecurityConfigTest 1 + ModuleMapperIT 2 + ModuleControllerIT 28 + UsrMappersIT 4 + UserMapperSearchIT 2 + UserControllerIT 25 + LoginControllerIT 10 + 其他
  80 +
  81 +`./scripts/test.sh` 流程:setup-test-db.sh DROP+CREATE → mvn build → mvn lint(compile) → mvn test → frontend skip → e2e 略 → reset DB。耗时 ~25s。
  82 +
  83 +## ⑥ 本模块新增 Migration
  84 +
  85 +—(本模块未引入 schema 改动;tUser / tStaff / tPermissionCategory / tUserPermission 由 V1__initial_schema.sql 在 A4 阶段创建)
  86 +
  87 +## ⑦ 跨模块改动清单(软规则 S2)
  88 +
  89 +本模块改动了 `common/` 包下的横切组件,按 CLAUDE.md § 🟡 S2 登记:
  90 +
  91 +| 文件 | 改动 | 原因 | 影响评估 |
  92 +|---|---|---|---|
  93 +| 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 同步登记 |
  94 +| common/response/PageResult.java | 新建 | REQ-USR-003 引入通用分页 VO;下一模块需要分页时直接复用 | 横向组件;本模块单点引入,未来其他模块共享 |
  95 +| common/exception/AccountLockedException.java | 新建 | REQ-USR-004 携带 cooldownSeconds 的专用异常 | 仅 LoginService 抛;GlobalExceptionHandler 专用 handler 不影响其他 BizException 路径 |
  96 +| common/exception/GlobalExceptionHandler.java | 追加 `@ExceptionHandler(AccountLockedException.class)` | 把 cooldownSeconds 写入 ApiResponse.data | 既有 BizException / Validation / Exception handler 路径 0 改动;前端契约 100% 兼容 |
  97 +| config/PasswordConfig.java | 新建 | REQ-USR-001 引入 BCryptPasswordEncoder bean | 横向组件,REQ-USR-004 复用;无副作用 |
  98 +| config/MybatisPlusConfig.java | 新建 | REQ-USR-003 引入 PaginationInnerInterceptor | 影响所有 MP `Page<T>` 调用路径,但 module_mod 现有 mapper 未用 Page,无回归 |
  99 +
  100 +> **未自动生成 cross-module-log 存根**:log-cross-module.sh hook 未触发(可能因为 common/ 不在 hook 监控的"其他业务模块"路径中);本节内容由 module-report 直接登记。
  101 +
  102 +## ⑧ 偏离 spec 清单
  103 +
  104 +- **REQ-USR-001**:spec/plan 早期草稿要求 `tUserPermission.bSelected=1` 字段,与 docs/03 修订版(已删该列)不一致。fix commit 520c01f 把 spec/plan 中 bSelected 提及改为"无该列"注解;UserPermissionEntity 不含此字段。
  105 +- **REQ-USR-002**:iStaffId 加 `FieldStrategy.IGNORED` 让 NULL 写入生效——同 module_mod ModuleEntity.iParentId 的全局副作用;本期所有 update 路径走 load-then-modify 安全,但**未来 partial update 路径必须 selectById 后再 updateById**,否则 iStaffId 会被静默清空。
  106 +- **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)。
  107 +- **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。
  108 +- **REQ-USR-004**:InMemoryLoginAttemptStore round 1 锁定到期不重置 count(business rule #4 不达成),fix commit d439c0d 修复 cooldownSeconds + recordFailure 双路径处理过期 reset。
  109 +- **REQ-USR-004 (范围说明已声明)**:本期**仅签发 JWT**,不切换其他端点为 authenticated;module_mod 4 端点 + module_usr 3 端点(POST/PUT/GET)仍 permitAll。这是已知技术债(spec § 范围说明 + § ⑩ 已知问题登记)。
  110 +- **REQ-USR-004**:spec § 业务规则 7 客户端 IP 审计未实施(log.info 没拿 HttpServletRequest.getRemoteAddr());登记为已知 gap。
  111 +- **跨 REQ — 多租户字段 / sCreatedBy 留 NULL**:所有写入路径中 `sBrandsId / sSubsidiaryId / sCreatedBy` 都落 NULL;同 module_mod 一致,等 REQ-USR-XXX 引入登录上下文 / 多租户拦截器后回填。
  112 +
  113 +## ⑨ AI reviewer 报告汇总
  114 +
  115 +- 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)
  116 +- REQ-USR-002: round 1 — approve(link: docs/superpowers/reviews/2026-05-06-REQ-USR-002.md)
  117 +- 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)
  118 +- 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)
  119 +
  120 +## ⑩ 已知问题
  121 +
  122 +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。
  123 +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),独立工时排期。
  124 +3. **多租户字段 / sCreatedBy NULL**:与 module_mod 一致;REQ-USR-XXX 引入登录上下文后回填 + 必要时 V_n migration 给历史数据补默认值 + 视业务决策收紧 schema 为 NOT NULL。
  125 +4. **iStaffId IGNORED 全局副作用**:UserEntity.iStaffId 与 ModuleEntity.iParentId 同类风险——任何 partial updateById 路径都会静默清空该列。本期所有路径走 load-then-modify 安全;未来贡献者新增 partial path 必须复用 LambdaUpdateWrapper.set(...) 模式。
  126 +5. **InMemoryLoginAttemptStore 单机限制**:5 次失败锁定在多实例部署下不工作;spec 已声明 Redis 替换留作后续 REQ。`expireLockForTest` 包私有调试入口暴露在生产 jar 中——下一 sweep 移到 src/test 的 testutil。
  127 +6. **客户端 IP 审计未实施**:spec § 业务规则 7 要求 log.info 含客户端 IP;当前 LoginService 只记 sUserName。建议后续在 LoginController 注入 HttpServletRequest.getRemoteAddr() 传给 service。
  128 +7. **Redis 凭据 / 部署**:docs/04 § 零 列了 Redis;本仓库 .env.local 未配 Redis;REQ-USR-004 内存锁定 + 后续可能的会话 / 缓存特性都待 Redis 接入后才能演进。
  129 +8. **REQ-USR-002 spec 验收 #11 IT 回滚证据**:受 IT @Transactional+@Rollback 包裹,service 层回滚无法在 IT 中观测;service 单测已覆盖语义,IT 留 nice-to-have。
  130 +9. **REQ-USR-003 nice-to-have 6 条 IT 缺失**:department equals / deleted=false 显式 / notContains / 排序 / matchType 非枚举 / 空结果 IT;service 单测已覆盖核心。
  131 +10. **docs/05 错误码段位与实际实现偏差**:docs/05 § REQ-USR-001 写 40020 段位;实现统一用 40010 PARAM_INVALID。docs sweep 时对齐。
  132 +11. **JacksonConfig 字段访问可见性配置全局生效**:影响所有 DTO/VO 的 JSON 序列化;当前所有字段都用 @Data 暴露,没有 @JsonIgnore 跳过敏感字段的需求;sPasswordHash 不暴露是因为 UserVO / LoginResultVO.LoginUserInfo / UserListItemVO 都不含该字段而非 @JsonIgnore。后续若有需要隐藏字段的 VO,需重新评估全局策略。
  133 +
  134 +## ⑪ 下一模块预览
  135 +
  136 +按 docs/02 § 二 顺序,**module_usr 是最后一个模块**——module_mod 已 merge,本 module_usr 是项目计划中的第二个也是最终模块。MR merge 后即视为「全部 REQ 完成」。
  137 +
  138 +后续工作(不属于 docs/02 计划清单的 REQ):
  139 +- **鉴权清算 sweep**:切 SecurityFilterChain 到 authenticated + JwtAuthenticationFilter + 所有 controller @PreAuthorize + 既有 IT 携带 token 改造(见 § ⑩ #2)。
  140 +- **JWT_SECRET history 清理**:评估是否需要 BFG / git-filter-repo 重写 history(见 § ⑩ #1)。
  141 +- **多租户上下文 + Redis 引入**:补完 sBrandsId / sSubsidiaryId / sCreatedBy 写入;用 Redis 替换 InMemoryLoginAttemptStore(见 § ⑩ #3, #5, #7)。
  142 +- **前端**:本期完全无 frontend 实现;docs/06 § 五 已规划用户管理页面 + 登录页,待前端工程接入后实现。
  143 +
  144 +## ⑫ MR 链接
  145 +
  146 +待 `mr-create` 推送后回填。
... ...