docs: 幂等键检修报告;勘误 API_INVENTORY 与 HTTP 报告;同步 STATUS/JIRA

新增 docs/幂等键检修报告-2026-09-18.md:记录双向断裂(后端零处读取该头、
前端 Date.now 每次生成新键)、五处审计发现、机制设计与五个取舍
(先占位后执行 / 重放 / 失败释放占位 / 孤儿占位接管 / 作用域含 actor_id)、
列类型不变量扫描、E2E 与守门证据、三处同形状问题的连带修复。

同轮勘误:

- docs/HTTP语义检修报告-2026-09-18.md:其中「Idempotency-Key 已支持」一句
  在写下时不成立(后端当时零处读取),已显式标注勘误并保留原文,
  以免「结论先于核实」被当作既成事实继续引用。
- docs/api/API_INVENTORY.md:原记载 POST /activities/{id}/interests
  「提交活动互选」,后端从未实现(Phase 16 已并入 /feedback 的
  interestTargetUserIds),按真实端点重列并附勘误说明。

docs/STATUS.md:迁移范围更新为 V001~V046;回归行更新为 669 / 0 / 0 / 3;
新增「幂等键检修」章节;补登 HTTP 语义检修章节在两份文档中的缺失条目。
docs/JIRA_BACKLOG.md:补登 HTTP 语义检修与幂等键检修两行
(此前均未登记)。
This commit is contained in:
WorkBuddy
2026-09-18 16:37:47 +08:00
parent af5412646b
commit 2766e53fe2
5 changed files with 453 additions and 5 deletions
+2
View File
@@ -182,6 +182,8 @@
| Phase 22 部署运维配置扩展(Feature Flag)复验 | 已完成(2026-09-18) | 见 `docs/Phase-22-V11-Recovery-Completion-Report-2026-09-18.md`;KEEP-VERIFY 复验发现 STATUS 标✅ 但含 **2 处真实代码缺陷 + 2 处文档夸大**:① D-22-A(High)`FeatureFlagController.updateFlag` 仅支持布尔 flag(反射 `getMethod("set"+cap, boolean.class)`)→ 7 个非布尔 flag 经 API 更新必失败,已改为按字段类型分派 boolean/int/String;② D-22-B(Medium)`body.getOrDefault("value", false)` 缺 value 静默置 false + 非布尔转型崩,已改为缺 value/类型错→400 且不改原值;T01–T05 模块开关(`isCounselingEnabled` 等)全仓无业务调用点 → 死配置(暴露未 gate,遗留交各模块 Phase);T06 上传策略实测已在 `LocalEligibilityService`+`PhotoService` 真强制 ✅;T07 私有桶零引用 → 死配置(遗留);yml 无 `features:` 块、默认值实为 Java 字段默认(文档纠正);新增 `FeatureFlagControllerTest`(7 用例)+ 负向验证(仅布尔缺陷版恰 2 用例变红);后端 **657 tests 全绿**(起点 650,净 +7);**Phase 23 已复验(2026-09-18,部分关闭)** |
| Phase 23 可信系统测试重建(REFACTOR)复验 | ⚠️ 部分关闭(2026-09-18) | 见 `docs/Phase-23-V11-Recovery-Completion-Report-2026-09-18.md`;KEEP-VERIFY 复验发现 STATUS 标✅ 但含 **5 处真实缺陷**:① **D-23-A(Critical)** `V11InvariantsTest` 原 14 个用例全为常量字符串断言 —— INV-016「Banner 目标类型」实际断言**文章状态**常量(跑错域)、INV-017「首页 8 入口」是**对自己刚声明的字面量数组做 `length==4` 的重言式**(从未调用首页服务)、INV-018/019/020 用预约/文章/账号状态常量冒充权限与数据范围验证;已重写为 12 个用例,含 **4 个真实服务行为守卫**(INV-013 走真实 `CounselingService.getDetail` 断言 Matchmaker 读到 `userRequestSummary==null` 并以 assigned Counselor 能读正文作反证;INV-016 反射读生产 `ALLOWED_TARGET_TYPES` 断言不含外链;INV-017 真实 `getHome` 断言 8 入口编码与顺序固定;INV-019 断言 `content_editor` 读到 null)+ `assertBehaviorTestExists` 用 `Class.forName` 把「行为验证见 XxxTest」变成可执行契约;② **D-23-B(High)** `tests/e2e/` **44 个 `pass` 空壳**(4 个文件 100% 为空壳,另 2 个混合),违反 Gate「不允许 skip/pass 空壳」→ 已移除并改为诚实占位(保留完整场景清单);③ **D-23-C(High)** pytest E2E 套件**整体不可执行**:全仓无 `conftest.py`,约 15 个 fixture(`api_client`/`verified_user`/`banner_factory`…)不存在,唯一可执行的 `run_v11_tests.py`(16 用例)需活栈 + `E2E_PASSWORD`;④ **D-23-D(High,遗留未修)** CI 未串联 E2E(计划明确要求)→ 需先决定 CI 活栈供给方式,未实施;⑤ **D-23-E(Medium)** CI `migration` job 注释称「验证 SQL 语法」实为只查文件非空(真实语法校验在 `MigrationValidationTest`,随 backend job 跑)→ 改为真实序列校验(命名/非空/版本号唯一/无缺号,43 个文件 1~43 连续唯一)。**新增闸门**:`scripts/check-no-empty-tests.py`(AST 扫描阻断 `pass` 空壳与无条件 `skip`,已双向验证)+ CI `test-shell-guard` job。**证据**:负向注入 3 处生产缺陷 → 恰好 `inv013`/`inv016`/`inv017` 3 个用例精准变红、其余 9 绿、零无关失败,`md5sum -c` 还原一致;`655 tests / 通过 652 / 失败 0 / 错误 0 / 跳过 3`(起点 657,净 −2 因 14 空转用例合并为 12;3 个跳过均有显式原因,非空壳)。**遗留**:pytest fixture 体系缺失、INV-011 缺显式守卫(E2E 入 CI 与 fixture 体系已由 Phase 23B 补齐)。**结论:不宣布「失败为零可进入 UAT」**;**Phase 23B(E2E 测试基建)已于 2026-09-18 完成并关闭** |
| Phase 23B E2E 测试基建重建 | 已完成(2026-09-18) | 补齐 Phase 23 遗留 D-23-C(E2E 整体不可执行)与 D-23-D(CI 未串联 E2E)。**交付**:`tests/e2e/conftest.py`(ApiClient / `operator_token` / `new_user` / 栈探活)、`pytest.ini`、`tests/e2e/requirements.txt`、`tests/e2e/sql/seed_e2e_operators.sql`(迁移里**没有任何 operator 种子**,全新库无账号可登录 → 6 个 e2e 账号 + 角色绑定,幂等,口令由 `psql -v pwd` 注入)、CI job `e2e`(起真实栈→等就绪→seed→pytest→失败阻断→清理)、8 个 E2E 文件改造(消除 import 期 `KeyError`,替换恒真/名不副实断言)。**核心设计**:栈不可达时 `pytest.exit(4)` **而非 skip**(skip 会把未执行伪装成通过)。**验证**:本机真实活栈 **33 passed**;栈不可达/缺口令均 exit 4;后端 **656 tests / 0 failures / 0 errors / 3 skipped**(基线 655,+1 为新增构建产物守卫,已负向验证:注入重复 `V042` → 恰 1 用例变红)。**本轮暴露并修复 6 处真实生产缺陷**(H2 零覆盖):D-23B-A 构建产物残留重复迁移版本号 042 → 应用起不来;D-23B-B 非 UUID path → 500 + 数据库异常外泄;D-23B-C 6 个 E2E 文件 import 期崩溃 → 从未执行;D-23B-E `super_admin` 仅 18/48 权限、`matchmaker` 0 权限(matchmaker 部分列遗留,需产品定义);D-23B-I 非法枚举参数 → 500(应 BAD_REQUEST);D-23B-J `uuid = character varying` → JDBC 加 `stringtype=unspecified`。**另有一处判断被证伪后撤回**:初查以为 `service_appointment.register` 权限丢失,核实后端点实际用 `service.assisted_registration`(V029 已 seed),未据此改动任何代码。详见 `docs/Phase-23B-V11-E2E-Infrastructure-Completion-Report-2026-09-18.md` |
| HTTP 语义检修(失败响应携带真实 HTTP 状态码) | 已完成(2026-09-18) | 见 `docs/HTTP语义检修报告-2026-09-18.md`;起因:Phase 23B 收尾时质疑「HTTP 200 + body.code 这套约定,开放第三方还守得住吗」→ 审计确认**当时就已经是断的**。**5 处发现**:105 处 / 30 个 Controller 直返 `ApiResponse.error()` 不抛异常、不带 `@ResponseStatus` → **HTTP 恒为 200**;小程序 `api.js` 的 401/403/429 分支**永远走不到**(token 过期回 403 → 不触发 `onSessionExpired` → **用户卡死无法重登录**);`SecurityConfig` 未配 `authenticationEntryPoint` → 未认证回 403 且 body 连 `code` 都没有;`CorsConfig` 默认 `*` + `allowCredentials(true)` 而注释称"默认只允许 localhost";全量测试仅 4 文件 / 15 处断言触及状态码。**修法**:`ApiResponseCode` 新增 `httpStatus` 字段(**唯一真相源**,新增码不填就编译不过)+ 新增 `ApiResponseStatusAdvice`(`ResponseBodyAdvice`,只补 2xx、只改状态行、body 一字不动)→ **105 处 Controller 零改动获得正确状态**;补 401/403 入口点并统一 body;CORS 默认改为拒绝跨域。**同日续做(身份类型与路径规则收敛)**:兜底规则由 `authenticated()` 收紧为 `hasRole("USER")`(`@AuthenticationPrincipal UserPrincipal` 遇 `OperatorPrincipal` 类型不匹配返回 **null** → 全仓 **244 处**使用、**仅 48 处**判空 → 196 处潜在 NPE→500 在认证层一次性消解);显式枚举 9 个运营前缀(`/**/operator/**` 是**非法 PathPattern**,写出后整个过滤器链 500,实测 E2E 由 37 passed 直落 33 failed);新增 `SecurityPathCoverageTest` 机械守卫(负向验证:摘掉 `/content/operator/**` → 恰 1 用例变红并列出受影响的 6 个端点)。**验证**:E2E **40 passed**;后端 **664 / 0 failures / 0 errors / 3 skipped**(首轮 662/3 failures 经判定**均为"测试断言了旧的错误行为"**,改断言而非改回实现,且保留各自的实质性安全断言)。遗留(属范围扩张,需产品决策):第三方凭据机制、限流是否真实生效、OpenAPI 对外发布、调用审计与配额、脱敏边界 |
| 幂等键检修(`Idempotency-Key` 从「声明」到「兑现」) | 已完成(2026-09-18) | 见 `docs/幂等键检修报告-2026-09-18.md`;起因:HTTP 报告里写下「`Idempotency-Key` 已支持」,随后核对**推翻了自己的结论**。**5 处发现**:规范第八章要求 7 类端点支持幂等键,前端 **25 处调用点**(8 service + 2 TS 页面)发送该头而**后端 0 处读取**;**前端那一侧也是坏的**——`idempotencyKey: \`like:${Date.now()}\`` 每次调用都变,**即使后端实现了也永不命中**(互补的两半,谁也不起作用);唯一真幂等的 counseling 走的是 **body 字段**而非规范要求的 header;`DuplicateKeyException`(并发重复提交的最后防线)与 **30 处** `IllegalStateException`(业务状态冲突)均**无映射 → 500**,而 500 的语义是"服务端故障可重试"→ **调用方重试,制造更多重复**;**推翻上轮的安全假设**:`stringtype=unspecified` 覆盖不到经类型处理器显式指定类型的参数(V045 的 `actor_id VARCHAR` 在该参数开启的 docker profile 下仍抛 `character varying = uuid`)→ **真正的不变量是"列类型必须是 uuid"**。**修法**:V045 建 `idempotency_records` + 唯一索引 `uk_idempotency_scope`、V046 修正列类型、`IdempotencyInterceptor`(**先占位后执行**/重放首次响应/处理中回 409/**孤儿占位 300s 可接管**/**失败释放占位**)、`IdempotencyResponseCacheFilter`、`WebMvcConfig`、`@Idempotent` **30 处声明(业务代码零改动)**、异常处理器两处新映射(**刻意不映射**父类 `DataIntegrityViolationException`——CHECK 违反应继续由 500 暴露);前端键改为"**一次未成功的逻辑操作内复用同一把键、成功才释放**",25 处 `Date.now()` 全部替换(残留经 grep 为 **0**)。**列类型不变量全库机械扫描**:`actor_id` 是**唯一**违规(已修);`client_message_id`/`provider_request_id` 为外部 id,`resource_id`/`source_id` 在 Java 侧**零处作查询条件** → 无第二处同类隐患。**新增守卫** `IdempotencyDeclarationGuardTest`(2,注入变异负向验证、**各自精确报出文件名与行号**),并**修正一处假绿**:`GlobalExceptionHandlerTest` 原 3 条断言只测 DTO、**一次都没进过处理器**(现 6 条)。**验证**:E2E **49 passed**(含 `tests/e2e/idempotency_test.py` 7 用例:重放**逐字节相同**、同键只落一条、作用域隔离、**5 线程并发同键业务恰好落库一次**、无键重复 Like 不得 500);后端 **669 / 0 failures / 0 errors / 3 skipped**(首轮 664/1 failure 为**有意设计的迁移基线守卫**,按流程由 44 抬至 46);小程序 `tsc --noEmit` EXIT=0;UI 契约门控 **14/14**。**同轮清掉三处同形状问题**:① 补 `CheckinController` 手工签到的 `@Idempotent`(第 146 章"扫码两次"同类漏标);② **CI 上的 E2E 从未真正通过过** —— 迁移无任何 operator 种子、`DemoDataSeeder` 也不建运营账号,而 `conftest.admin_token` 写死 `username="admin"`(某台开发机手工创建的遗留账号,本机有所以本地一直绿,CI 全新库无 → fixture 必然 401),已改用 seed 账号 `e2e_admin` 并新增 `tests/e2e/fixture_account_guard_test.py` 机械拦截(含"解析失败即失败"断言);③ `POST /activities/{id}/interests` 契约漂移 —— `API_INVENTORY` 记载、前端已实现,但后端**从未实现**(Phase 16 已把表达意向合并进 `/activities/{id}/feedback`),调用必然 404,已更正文档并按真实端点重列、删除前端死代码链(`activityService.submitInterests` + `activityStore.submitInterests`,全仓无页面调用方),各留注释说明恢复条件 |
| UI-CLEANUP-20260913 | 已完成 | 废弃 UI 代码、冲突规范、旧计划和无效验收证据已清理;`ui-document-baseline-contracts` 锁定唯一视觉入口 |
| UI-ADMIN-20260913 | 已完成(待桌面端目视复验) | 管理端全局框架与工作台已按规格交付;18 modules + 22 routes,vue-tsc 与 vite build 全绿 |
| UI-MINIAPP-20260913 | 已完成 | 小程序 11 个核心页面已重构;11/11 页 TS-only,2026-09-16 通过用户验收 |