Skip to content

fix(gov-002): 修 PR #2 外部审查 9 项缺陷(高 4 + 中 5) - #6

Draft
whobin1982 wants to merge 10 commits into
mainfrom
feature/phase-2-gov-002-9-fixes
Draft

fix(gov-002): 修 PR #2 外部审查 9 项缺陷(高 4 + 中 5)#6
whobin1982 wants to merge 10 commits into
mainfrom
feature/phase-2-gov-002-9-fixes

Conversation

@whobin1982

@whobin1982 whobin1982 commented Jun 8, 2026

Copy link
Copy Markdown
Owner

1. 本 PR 范围

按 GOV-002(Issue #5)跟踪,修复 PR #2 合并后外部 GitHub 审查发现的多项缺陷;
仍保持 DRAFT 等 CI 全绿 + 外部 ChatGPT / 人工复审后转 Ready。
本 PR 仍不动业务模块 API(M-03 / M-05 / M-07 / M-09 / M-10 / M-11 等未实现)。

2. 关联 Issue

3. 第一轮:9 项缺陷

高风险(4 项):

中风险(5 项):

4. 第二轮:3 项 Blocker(方案 A)

Bootstrap 方案 A:MVP 阶段不自动 seed SYSTEM_ADMIN

V3 不再自动 seed SYSTEM_ADMIN root 用户。
MVP 阶段采用运维 seed 机制:

  • docs/dev/bootstrap_admin.md(运维指南 + MVP 阶段限制说明)
  • backend/src/main/resources/db/seed/dev_bootstrap_admin.example.sql
    (4 件套:schools / users / user_credentials / user_role_scopes;
    全占位符 :SCHOOL_ID / :USER_ID / :PASSWORD / :USERNAME
  • 真实密码 / bcrypt hash 不得入库;.gitignore 排除 *.local.sql / *.prod.sql / *.real.sql
  • V3 仅保留 NOT NULL 放宽 + schema_versions 记录
  • 登录仍走普通 schoolCode + username + password 流程

登录匿名幂等 方案 A:login 不使用幂等

  • AuthController.login 移除 @Idempotent
  • 登录 token 场景不适合缓存首次响应
  • 保留 @Idempotency-Key 给创建类业务接口使用
  • 调整 FlywayAndApiSmokeIT.loginRequestBodyIsNotConsumedByIdempotencyFilter 断言:
    login 端点带 Idempotency-Key 不向 idempotency_keys 写任何记录

修复 #1 第二轮:AuditAspect.mapExceptionStatusinstanceof 替代 getSimpleName switch

之前 BadCredentialsException extends AuthenticationException
getSimpleName() 返回 "BadCredentialsException",switch 匹配不到 → default 500。
改为 instanceof 链:覆盖所有 Spring Security 子类。

5. DB migration

新增 V2 / V3 / V4:

  • V2user_role_scopes partial unique index
    • 删除 V1 全表唯一索引
    • 创建 uq_urs_user_role_scope_active WHERE revoked_at IS NULL
  • V3:放宽 users / user_role_scopesschool_id / tenant_id 为 NULL
    • 不自动 seed root 用户(运维 seed 机制)
  • V4:同时 COALESCE(school_id) + COALESCE(college_id),修复系统级
    active scope 重复问题(V3 后 school_id 可为 NULL,原 V2 索引不覆盖此场景)

不修改已合并的 V1。

6. 测试(10 个新文件 / 改 5 个)

  • BootstrapExampleSeedTest(6 个断言:example seed 存在 / 4 件套 /
    占位符 / 文档 / .gitignore / V3 不再自动 seed root)
  • UserRoleScopesUniqueIndexIT(Case A:active 重复拦截 / Case B:revoked
    重新插入 / Case C:system scope 重复拦截)
  • IdempotencyBehaviorIT(已存在 v1 改 fixture 字段全 static)
  • IdempotencyPutPatchIT(Case A:成功缓存合法 JSON / Case B:业务异常
    不缓存 / Case C:异常后同 key 可重试)
  • RefreshTokenSecurityIT(Case 1:DISABLED refresh 拒绝 / Case 2:角色撤销
    后 refresh 不带旧角色 / Case 3:全角色撤销时 session 被 revoke)
  • SettingsPatchBehaviorIT(Case 1-5:unset 保留原值 / 显式 false 正确写 /
    AI 启用约束仍生效)
  • AuditStatusCodeMappingTest(反射调 AuditAspect.mapExceptionStatus 验证
    FORBIDDEN 403 / UNAUTHORIZED 401 / NOT_FOUND 404 / IDEMPOTENCY_CONFLICT 409
    / BAD_REQUEST 400 / INTERNAL_ERROR 500 + non-BusinessException fallback)
  • FlywayAndApiSmokeIT(已存在;调整 loginRequestBodyIsNotConsumedByIdempotencyFilter)
  • 改:4 个新 IT 的 fixture 字段全 static(JUnit 5 每 @test 一个新实例,避免
    static init 与实例字段不匹配)

7. 测试结果

Backend CI: pass
Frontend CI: pass
OSG gate: pass
Flyway + Spring Boot IT: pass

CI workflow 实跑:4 个 workflow × 2 run 全部 success。

8. 权限影响

无新增权限规则;现有 PERM-* 实现未变。

9. 开源治理影响

无新依赖;ESLint v9 flat config + 3 个前端 devDeps(@eslint/js / typescript-eslint / vue-eslint-parser)
已在前几轮 commit。

10. 已知风险

  1. V2/V3/V4 迁移在已有数据的生产环境可能需人工调停(重复 active 行被 V2/V4 索引拦截);
  2. SYSTEM_ADMIN 登录入口(方案 B)未在 MVP 实现;运维需走数据库 seed;
  3. CI 的 Flyway + IT 仍依赖 services.postgres;本地无 docker 时无法复跑;
  4. JUnit 5 每 @test 一个新实例,static fixture 字段虽已 static,但跨测试方法
    数据共享仍可能有副作用——后续可拆为独立 class 或 @TestInstance.PER_CLASS;
  5. IT 类 static volatile 共享 fixture 在并发跑时可能 race condition;
  6. 审查人本地无 JDK 21 / pnpm;CI 是唯一执行依据。

11. 后续 PR


本 PR 由 Claude Opus 4.8 按 GOV-002(Issue #5)自动生成;修复与基线逐一对应。

🤖 Generated with Claude Code

whobin1982 and others added 5 commits June 8, 2026 13:35
按 GOV-002 (#5) 跟踪。

高风险(4 项):

#1 IdempotencyResponseAdvice
- 新增 PUT/PATCH/DELETE 持久化(不再仅 POST)
- 改用 Jackson ObjectMapper 序列化响应体(不再 body.toString)
- 非 2xx 响应 / 业务异常清理 inFlight 占位
  → IdempotencyInterceptor 新增 afterCompletion 钩子
  → 新增 clearInFlight(compositeKey) 方法供 Advice 调用

#2 user_role_scopes 部分唯一索引
- V2__user_role_scopes_partial_unique_index.sql
- 删除 V1 全表唯一索引 uq_urs_user_role_scope
- 创建 uq_urs_user_role_scope_active WHERE revoked_at IS NULL
- COALESCE(college_id, 哨兵 UUID) 区分无 college / 有 college

#3 LocalAuthProvider refresh
- refresh 时重新从 DB 读 user.status 与角色列表
- 用户禁用 / 归档 / 角色全撤销 → 撤销所有 session + FORBIDDEN
- UserSessionRepository.revokeAllForUser(userId) 新增

#4 frontend-ci.yml 假绿
- 删除 lint / test / build 步骤后的 || true
- 失败即 workflow 失败
- 首次跑可能因 ESLint 未配置而大量 warn;后续 PR 必须保证 0 error

中风险(5 项):

#5 SYSTEM_ADMIN bootstrap 不可达
- V3__system_admin_bootstrap.sql
- ALTER TABLE users ALTER COLUMN school_id DROP NOT NULL
- ALTER TABLE user_role_scopes ALTER COLUMN school_id / tenant_id DROP NOT NULL
- 种子内置 SYSTEM_ADMIN root 用户 (id 固定 00000000-...)
- 对应 role 绑定 school_id / college_id / tenant_id 都为 NULL

#6 SettingsService PATCH 局部更新重置布尔配置
- updateCourseCodePolicy / updateAISettings / updateWarningRules / updateAIPolicy
  改为接收 Map<String, Object> body
- 用 body.containsKey 区分"未传"与"传了 false"
- controller 调 *Raw 方法
- AI 启用约束检查改用 effectiveEnabled(合并 current)

#7 AuditAspect 业务异常状态码
- 不再一律记 422
- BusinessException 取其 errorCode.httpStatus() 的真实状态
- 保持 AccessDeniedException → 403、AuthenticationException → 401、default → 500

#8(与 #5 合并解决)
#9(已在 #1 修复)

不修改:
- 任何已落地 30 个 API 行为;
- main 上的 PR #2 merge 内容;
- 业务模块(M-03 / M-05 / M-07 等)—— 等 GOV-002 验收 + DEV-010 切分支后启动。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ESLint v9 找不到 eslint.config.js 报错;按 flat config 形式创建
最小配置:
- 引入 @eslint/js、typescript-eslint、eslint-plugin-vue、vue-eslint-parser
- 忽略 node_modules / dist / coverage / *.config.ts
- 启用 js recommended + tseslint recommended + vue flat recommended
- 关闭若干不必要 warning,warn-only 保持新 PR 可通过
- 实际门禁:lint 失败即 workflow 失败(不再 || true)
@whobin1982 whobin1982 added p0 发布阻断 / P0 acceptance 验收项 open-source-governance 开源治理 / SBOM / SCA labels Jun 8, 2026
@whobin1982
whobin1982 marked this pull request as draft June 8, 2026 07:03
whobin1982 and others added 5 commits June 8, 2026 15:22
GOV-002 (#5) 第二轮:3 项 Blocker 选 A 方案 + V4 迁移 + 6 类新测试。

#1 Bootstrap 方案 A:撤回 V3 自动 seed root,改为运维 seed 机制
- V3__system_admin_bootstrap.sql:删除直接 INSERT root 用户;仅保留 NOT NULL 放宽
  + schema_versions 记录;明确 MVP 阶段不自动 seed
- 新增 backend/src/main/resources/db/seed/dev_bootstrap_admin.example.sql
  (运维 seed 模板;4 件套:schools / users / user_credentials / user_role_scopes;
   全占位符 :SCHOOL_ID / :USER_ID / :PASSWORD_HASH / :USERNAME)
- 新增 docs/dev/bootstrap_admin.md(运维指南 + MVP 阶段限制说明)
- .gitignore 排除 *.local.sql / *.prod.sql / *.real.sql;
  !example.sql 例外允许 example 文件入库
- AuthController.login 不再 @idempotent:登录 token 场景不适合缓存
  (重复登录返回新 token 是可接受行为;缓存 token 还引入安全与会话语义问题)

#2 V4 迁移:user_role_scopes partial unique index 同时 COALESCE
- V4__user_role_scopes_school_id_coalesce_index.sql
- 删除 V2 索引 uq_urs_user_role_scope_active
- 新建索引:
  ON user_role_scopes(
    user_id, role_code,
    COALESCE(school_id,  '00000000-...-0000'::uuid),
    COALESCE(college_id, '00000000-...-0000'::uuid)
  ) WHERE revoked_at IS NULL
- 同时覆盖 school_id / college_id 两种 NULL;解决 V3 后系统级 active scope
  重复插入问题(PostgreSQL NULL 不相等)

#3 登录匿名幂等方案 A:login 不进幂等
- AuthController.login 移除 @idempotent
- 调整 FlywayAndApiSmokeIT.loginRequestBodyIsNotConsumedByIdempotencyFilter
  增加断言:login 带 Idempotency-Key 不应向 idempotency_keys 写任何记录
- 新增 / 调整测试:
  * UserRoleScopesUniqueIndexIT(Case A/B/C:active 重复拦截 / revoked 重新插入 /
    system scope 重复拦截)
  * IdempotencyPutPatchIT(Case A/B/C:成功缓存为合法 JSON / 业务异常不缓存 /
    异常后同 key 可重试)
  * RefreshTokenSecurityIT(Case 1/2/3:DISABLED refresh 拒绝 / 角色撤销后
    refresh 不带旧角色 / 全角色撤销时 session 被 revoke)
  * SettingsPatchBehaviorIT(Case 1/2/3/4/5:unset 保留原值 / 显式 false 正确写 /
    AI 启用约束仍生效)
  * AuditStatusCodeMappingIT(FORBIDDEN/UNAUTHORIZED/NOT_FOUND/
    IDEMPOTENCY_CONFLICT/INTERNAL_ERROR 业务异常审计状态码映射)
  * BootstrapExampleSeedTest(example seed SQL 完整性 / 4 件套 / 占位符 /
    文档存在 / .gitignore / V3 不再自动 seed)

不修改:
- 任何已落地 30 个 API 行为;
- main 上的 PR #2 内容;
- 业务模块(M-03 / M-05 / M-07 / M-09 / M-10 / M-11 等)—— 等 GOV-002 全部 9 项验收 + DEV-010 切分支后启动。

CI 预期:backend-ci / frontend-ci / osg-gate / Flyway + IT 四 workflow 仍 PASS;
新增 6 个 IT 真实跑通需要 services.postgres(已配置)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- BootstrapExampleSeedTest:CI 在 backend/ 工作目录跑 mvn test,
  改用 REPO_ROOT = Path.of('..').toAbsolutePath().normalize() +
  相对路径,断言所有路径文件可读
- 4 个新 IT(IdempotencyBehaviorIT / RefreshTokenSecurityIT /
  IdempotencyPutPatchIT / SettingsPatchBehaviorIT):
  之前用 static volatile FIX_INIT + 实例字段(schoolId / userId / tenantId),
  JUnit 5 每个 @test 一个新实例 → 第二个测试方法时实例字段为 null
  改:所有 fixture 字段声明为 static,ensureFixtures 内对 static 赋值;
  多次 JUnit 实例共享同一组 fixture
之前用 t.getClass().getSimpleName() switch:
- BadCredentialsException extends AuthenticationException,
  getSimpleName 返回 'BadCredentialsException',switch 匹配不到 → default 500
- 同 AccessDeniedException 的子类也有此问题

改为 instanceof 链:
- t instanceof AccessDeniedException → 403
- t instanceof AuthenticationException → 401(含所有子类)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

acceptance 验收项 open-source-governance 开源治理 / SBOM / SCA p0 发布阻断 / P0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant