Merged
Merge Request #2 · created by 朱子纯


feat(module_usr): 用户管理

模块完成报告

docs/superpowers/module-reports/2026-05-07-module_usr.md(本 MR 仓库内完整贴入下方)。



module_id: module_usr date: 2026-05-07

git_range: 237a97e..6e0c0e7 (27 commits)

模块完成报告 — module_usr 用户管理

① 模块信息

  • 模块 ID: module_usr
  • 模块名: 用户管理(账户主数据 / 权限关联 / 列表查询 / 登录认证)
  • 开发区间: 237a97e4(master,含 module_mod merge)→ 6e0c0e78(test-gate evidence),共 27 个 commits

② REQ 完成清单

  • 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)
  • 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)
  • 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)
  • 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)

本模块不写 tStafftPermissionCategory——这两张表当前是只读字典,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<T> 调用路径,但 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 520c01f2 把 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 f53689c3 改成 LambdaUpdateWrappercolumn 通过 @Param 单独传入(防 GET query-string 绕过白名单)+ XML deleted 分支用 CAST(#{queryValue} AS UNSIGNED) 兼容 bit(1)。
  • REQ-USR-004 (CRITICAL):JwtTokenProviderTest round 1 提交时硬编码了与 .env.local 完全相同的生产 JWT_SECRET(commit b7ed804a 已入 git history)。fix commit d439c0d9 把测试 SECRET 改成与生产无关的 fake 值,.env.local JWT_SECRET 已本地旋转为新随机值;旧值仍在 git history——所有部署环境必须运维侧同步轮换 JWT_SECRET。
  • REQ-USR-004:InMemoryLoginAttemptStore round 1 锁定到期不重置 count(business rule #4 不达成),fix commit d439c0d9 修复 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 520c01f2
  • 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 f53689c3
  • 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 d439c0d9

⑩ 已知问题

  1. JWT_SECRET 已永久污染 git history:commit b7ed804a(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 推送后回填。


本地闸门证据

  • test.sh: green(subagent: a04f12f4a6f932a7d)

审核入口

  • 本 MR = 模块 module_usr 的唯一人工介入点
  • Approve + Merge 后,下次用户运行 /erp-workflow:coding-start 时入口会自动扫描到 GitLab API state=merged,探测默认分支后 git pull --ff-only 同步并推进下一模块

From module-module_usr into master

Merged by 朱子纯

1 participants