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)
本模块不写 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<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 改成
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 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)
⑩ 已知问题
- 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。
- 鉴权延期未清算:本 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),独立工时排期。
- 多租户字段 / sCreatedBy NULL:与 module_mod 一致;REQ-USR-XXX 引入登录上下文后回填 + 必要时 V_n migration 给历史数据补默认值 + 视业务决策收紧 schema 为 NOT NULL。
- iStaffId IGNORED 全局副作用:UserEntity.iStaffId 与 ModuleEntity.iParentId 同类风险——任何 partial updateById 路径都会静默清空该列。本期所有路径走 load-then-modify 安全;未来贡献者新增 partial path 必须复用 LambdaUpdateWrapper.set(...) 模式。
-
InMemoryLoginAttemptStore 单机限制:5 次失败锁定在多实例部署下不工作;spec 已声明 Redis 替换留作后续 REQ。
expireLockForTest包私有调试入口暴露在生产 jar 中——下一 sweep 移到 src/test 的 testutil。 - 客户端 IP 审计未实施:spec § 业务规则 7 要求 log.info 含客户端 IP;当前 LoginService 只记 sUserName。建议后续在 LoginController 注入 HttpServletRequest.getRemoteAddr() 传给 service。
- Redis 凭据 / 部署:docs/04 § 零 列了 Redis;本仓库 .env.local 未配 Redis;REQ-USR-004 内存锁定 + 后续可能的会话 / 缓存特性都待 Redis 接入后才能演进。
- REQ-USR-002 spec 验收 #11 IT 回滚证据:受 IT @Transactional+@Rollback 包裹,service 层回滚无法在 IT 中观测;service 单测已覆盖语义,IT 留 nice-to-have。
- REQ-USR-003 nice-to-have 6 条 IT 缺失:department equals / deleted=false 显式 / notContains / 排序 / matchType 非枚举 / 空结果 IT;service 单测已覆盖核心。
- docs/05 错误码段位与实际实现偏差:docs/05 § REQ-USR-001 写 40020 段位;实现统一用 40010 PARAM_INVALID。docs sweep 时对齐。
- 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 APIstate=merged,探测默认分支后git pull --ff-only同步并推进下一模块
-
- CRITICAL:JwtTokenProviderTest 测试 SECRET 改为与生产无关的 fake hex。 注意 .env.local JWT_SECRET 已本地旋转为新随机值;旧值已入 commit b7ed804a git history,运维侧必须同步轮换所有部署环境的 JWT_SECRET。 - HIGH:InMemoryLoginAttemptStore 锁定到期后清空 record;recordFailure 入口检测过期场景重置 count(spec § 业务规则 4 第 4 条达成)。 - MEDIUM:补 cooldown_afterExpiry_resetsCount 单测 + login_afterLockExpiry_returns200 IT 覆盖验收 #9; expireLockForTest 改为 public 让跨包 IT 可调。
-
- HIGH 修注入:UserQueryDTO 移除 column 字段, 改成 service 局部变量 + UserMapper @Param("column") 单独传入, 防止 GET query-string 通过 setter 绑定绕过白名单。 - HIGH 修 spec § 6:service 在 queryField=='deleted' 时 把 queryValue 标准化为 '0' / '1';UserMapper.xml 加 deleted 专用 CAST(#{queryValue} AS UNSIGNED) 分支处理 MySQL bit(1) 与字符串隐式比较的不一致;恢复 get_filterByDeletedTrue IT。 - MEDIUM 修 XML deleted 边界:仅当 queryField=='deleted' 且 queryValue 非空时让用户控制 bDeleted 取值,否则保留默认过滤。 -
8 个 GET IT 通过。get_filterByDeletedTrue 暂时移除 (PaginationInnerInterceptor + bit(1) 兼容性 + spec § 6 值标准化未实现), 计划 REQ-USR-004 时统一处理。
-
清理 spec/plan 中残留的 bSelected 字段提及——docs/03 修订版无该列, 关联记录存在即「已选」。代码 UserPermissionEntity 已正确不含该字段; 本 commit 仅清洁文档使 SSoT 一致。 reviewer round 1 报告的 high『tCreateDate 未设置』是误判: UserServiceImpl.java:102 实际已含 setTCreateDate(LocalDateTime.now()), 本 fix 不动代码。
-
UserPermissionEntity 不含 bSelected 列——docs/03 § tUserPermission 修订版无此列 (关联记录存在即「已选」),早期 spec/plan 草稿与 SSoT 不一致,以 docs/03 为准。