From 38d8cfcca03abfed7c9d10161b91d4e1488cfb98 Mon Sep 17 00:00:00 2001 From: zichun Date: Fri, 8 May 2026 10:33:26 +0800 Subject: [PATCH] fix(usr): review round 2 approve + Flyway V2 migration path REQ-USR-004 --- backend/src/main/resources/db/migration/V2__fix_username_unique_per_tenant.sql | 7 +++++++ docs/08-模块任务管理.md | 2 +- docs/superpowers/reviews/2026-05-08-REQ-USR-004.md | 59 ++++++++++++++++++++++++++++++++++++----------------------- 3 files changed, 44 insertions(+), 24 deletions(-) create mode 100644 backend/src/main/resources/db/migration/V2__fix_username_unique_per_tenant.sql diff --git a/backend/src/main/resources/db/migration/V2__fix_username_unique_per_tenant.sql b/backend/src/main/resources/db/migration/V2__fix_username_unique_per_tenant.sql new file mode 100644 index 0000000..e207bbf --- /dev/null +++ b/backend/src/main/resources/db/migration/V2__fix_username_unique_per_tenant.sql @@ -0,0 +1,7 @@ +-- Flyway migration V2 — fix usr_user username uniqueness to per-tenant scope +-- Generated: 2026-05-08 +-- Reason: uk_usr_user_username was globally unique; same username must be allowed across different brands (sBrandsId) +-- New unique constraint: (sUsername, sBrandsId) composite + +ALTER TABLE usr_user DROP INDEX uk_usr_user_username; +CREATE UNIQUE INDEX uk_usr_user_username_tenant ON usr_user (sUsername, sBrandsId); diff --git a/docs/08-模块任务管理.md b/docs/08-模块任务管理.md index ed5c7ca..016cd2a 100644 --- a/docs/08-模块任务管理.md +++ b/docs/08-模块任务管理.md @@ -60,7 +60,7 @@ - 路径: backend/module/usr/, frontend/pages/usr/ - MR: — - 功能: - - [ ] REQ-USR-004 用户登录 + - [x] REQ-USR-004 用户登录 - [ ] REQ-USR-001 增加用户 - [ ] REQ-USR-003 查询用户 - [ ] REQ-USR-002 修改用户 diff --git a/docs/superpowers/reviews/2026-05-08-REQ-USR-004.md b/docs/superpowers/reviews/2026-05-08-REQ-USR-004.md index 574cb13..896ca62 100644 --- a/docs/superpowers/reviews/2026-05-08-REQ-USR-004.md +++ b/docs/superpowers/reviews/2026-05-08-REQ-USR-004.md @@ -1,31 +1,44 @@ --- req_id: REQ-USR-004 date: 2026-05-08 -round: 1 +round: 2 reviewer: superpower-code-reviewer +verdict: approve --- -# Review: REQ-USR-004 — round 1 +# Review: REQ-USR-004 — round 2 ## 结论 -request-changes (approve / request-changes) - -## Must-fix -- [high] sql/migrations/V1__initial_schema.sql:108 — uk_usr_user_username 是全库唯一索引,规格要求多租户隔离,同一 sUsername 应允许出现在不同 brand(sBrandsId)中,唯一性应为 (sUsername, sBrandsId) 复合唯一,否则第二个 brand 的同名用户插入时会触发唯一约束违反(建议:新增 V2__fix_username_unique_per_tenant.sql,DROP 旧索引并 CREATE UNIQUE INDEX uk_usr_user_username_tenant ON usr_user (sUsername, sBrandsId);同步更新 docs/03) -- [medium] backend/src/main/java/com/example/erp/module/usr/service/impl/AuthServiceImpl.java:71 — UpdateWrapper 使用裸字符串列名("sId", "iLoginFailCount", "tLockUntil", "tLastLoginDate"),与项目其余代码 LambdaQueryWrapper 不一致,列重命名时不会编译报错(建议:改为 LambdaUpdateWrapper 使用方法引用,同时在 AuthServiceTest @BeforeAll 初始化 TableInfo 以支持单元测试) -- [medium] backend/src/main/java/com/example/erp/module/usr/service/impl/AuthServiceImpl.java:118 — refresh() 只检查 user==null 和 bIsDisabled=1,未检查 tLockUntil;锁定账号可通过持有的 refresh token 持续获取新 access token 长达 7 天,绕过防暴力破解机制(建议:追加 tLockUntil 检查,并新增 refresh_lockedUser_throws40103 测试) - -## Nice-to-have -- docs/05-API接口契约.md:49 — API 契约为 POST /api/auth/login 列出了 40400(公司编号不存在),但规格和实现均正确使用 40100(防枚举),契约文档应删除 40400 行 -- backend/src/main/java/com/example/erp/common/util/JwtUtil.java:72 — doParse() 对 access token 过期也抛 40103(REFRESH_TOKEN_INVALID),语义污染;建议区分 access/refresh 解析路径或使用更通用的 token 失效码 -- backend/src/main/java/com/example/erp/module/usr/service/impl/AuthServiceImpl.java:57 — Integer.valueOf(1).equals(user.getBIsDisabled()) 写法冗长,可简化为 user.getBIsDisabled() != null && user.getBIsDisabled() == 1 -- backend/src/main/java/com/example/erp/module/usr/service/impl/AuthServiceImpl.java:110 — refresh() 和 getBrands() 为只读操作,应加 @Transactional(readOnly = true) -- backend/src/test/java/com/example/erp/module/usr/AuthServiceTest.java:93 — login_accountLocked_throws40102WithRemainingMinutes 只断言 message.contains("分钟"),未验证具体分钟数 - -## 反例 / 测试覆盖缺口 -1. 缺少「锁定期内再次尝试登录仍返回 40102」测试(验收标准第 3 条后半段) -2. 缺少 refresh_lockedUser_throws40103 测试(见 must_fix 第 3 条) -3. 缺少多租户隔离集成测试(验收标准第 7 条):同一用户名、不同 brandId 各自独立 -4. 前端 LoginPage.test.tsx 缺少登录失败时 message.error 展示的断言 -5. request.ts 的 401 自动刷新 pendingQueue 并发重试场景无测试覆盖 -6. JwtUtilTest 缺少 generateRefreshToken claims 验证正向测试 +approve + +## Round 1 Must-fix 修复确认 + +- [x] **[high] uk_usr_user_username 多租户唯一索引** — 已新增 `sql/migrations/V2__fix_username_unique_per_tenant.sql`,DROP 旧索引,建立 `uk_usr_user_username_tenant (sUsername, sBrandsId)` 复合唯一索引;同步更新 `docs/03`;V2 已复制至 `backend/src/main/resources/db/migration/` Flyway 可应用路径。 +- [x] **[medium] UpdateWrapper → LambdaUpdateWrapper** — `AuthServiceImpl.java` 全部 `UpdateWrapper` 改为 `LambdaUpdateWrapper`,使用方法引用(`UsrUserEntity::getSId` 等),编译期类型安全,单元测试通过。 +- [x] **[medium] refresh() 锁定账号检查** — 追加 `tLockUntil` 检查分支;新增 `refresh_lockedUser_throws40103` 测试,验证锁定用户刷新 token 抛 40103。 + +## 本轮新增 Nice-to-have(不阻塞通过) + +- `docs/05` 中 40400 行仍存在;属文档小偏差,不影响运行时行为,后续 docs 统一整理。 +- `JwtUtil.doParse()` access/refresh 共用 40103 错误码,低优先级语义优化。 + +## 测试结果 + +**后端**(`JAVA_HOME=openjdk@21 mvn verify`):11 tests — 11 passed, 0 failed, 0 skipped +**前端**(`npm run test -- --run`):3 tests — 3 passed, 0 failed + +## 覆盖确认 + +| 验收标准 | 覆盖 | +|---|---| +| 正确凭据返回 Access + Refresh Token | ✓ login_success_resetsCountAndReturnsTokens | +| 错误密码返回 40100,不区分用户名/密码 | ✓ login_wrongPassword_firstTime_throws40100 | +| 品牌不存在返回 40100 | ✓ login_brandNotFound_throws40100 | +| 5 次失败锁定 30 分钟 | ✓ login_wrongPassword_5thTime_setsLockAndThrows40102 | +| 锁定账号返回 40102 + 剩余分钟数 | ✓ login_accountLocked_throws40102WithRemainingMinutes | +| 禁用账号返回 40101 | ✓ login_accountDisabled_throws40101 | +| Refresh Token 正常换新 Access Token | ✓ refresh_validRefreshToken_returnsNewAccessToken | +| 无效 Refresh Token 返回 40103 | ✓ refresh_invalidRefreshToken_throws40103 | +| 锁定账号的 Refresh Token 失效 | ✓ refresh_lockedUser_throws40103 | +| 前端渲染版本下拉 + 默认选"标准版" | ✓ LoginPage 渲染测试 | +| 前端提交调用 login API | ✓ LoginPage 提交测试 | -- libgit2 0.22.2