# R01-D4-2 claim token 最终独立复验报告(冻结前最后一次) 日期:2026-09-16 分支:`refactor/r01-structure-performance` HEAD:`b10c4a732740e42353ca826f63141c3dd2a49915`(复验全程未变) 复验对象:`docs/r01-d4-2-claim-fence-remediation-20260916.md` 的 claim token 整改 性质:**仅复验** —— 未修改生产代码、未修改既有测试、未暂存、未提交、未推送 --- ## 0. 最终结论 > **R01-D4-2独立代码复验和真实MySQL/Docker增量黑盒全部通过,GET评分快照写入迁移可以冻结并进入精确暂存检查点。** **FAIL = 0,SKIP = 0。** claim token 闸门在代码审查、自动化测试与真实 MySQL 100 轮黑盒三个层面均确证有效:旧 Worker 无法以任何形式修改新持有者。 --- ## 1. 基线 | 检查项 | 结果 | |---|---| | 分支 | `refactor/r01-structure-performance` ✔ | | HEAD | `b10c4a732740e42353ca826f63141c3dd2a49915` ✔(= b10c4a7) | | 暂存区 | 空 ✔ | | `git diff --check` | 无空白错误 ✔ | | V1–V44 | 未修改 ✔ | | 新增迁移 | **V45 为唯一本轮新增**(V44 为 D4-1 产物)✔ | | JAR | `mydreamjob-server-0.0.1-SNAPSHOT.jar`,2026-09-16 **17:26:51**(最新 `clean verify` 产出)✔ | --- ## 2. 独立代码审查 `CandidateScoringRefreshTaskExecutor.java`(153 行,逐行核对): | 核查项 | 结论 | 证据 | |---|---|---| | token 在领取 SQL 中原子生成并写入 | ✔ | `:53` `UUID.randomUUID().toString()`;`:56` `SET status='PROCESSING', processing_started_at=?, claim_token=?, ...` 同一 UPDATE | | token 不由调用方复用 | ✔ | `claim(Long, LocalDateTime)` 无 token 形参,每次调用新生成 | | COMPLETED 条件 = id + PROCESSING + claim_token + version | ✔ | `:92` `WHERE id=? AND status='PROCESSING' AND claim_token=? AND version=?` | | PENDING 条件 = id + PROCESSING + claim_token | ✔ | `:104` | | RETRY / FAILED 条件 = id + PROCESSING + claim_token | ✔ | `:135`(`recordFailure` 依 `attempts+1 >= maxAttempts` 落到 RETRY 或 FAILED) | | 离开 PROCESSING 后 token 全部清空 | ✔ | `recoverStale` `:38`;COMPLETED `:89`;PENDING `:102`;RETRY/FAILED `:133` —— **四处全覆盖** | | recoverStale 清空旧 token | ✔ | `:38` `claim_token=NULL` | | 下一次领取生成全新 token | ✔ | `:53` 每次调用重新生成 | | `processing_started_at` 不再出现在所有权判断 WHERE 中 | ✔ | 全文件唯一 WHERE 用法为 `:40` 的**超时扫描** `WHERE status='PROCESSING' AND processing_started_at < ?`;所有权判定全部使用 `claim_token` | | 更新 0 行按 stale claim 处理 | ✔ | `claim` `:61-63` 返回 null;`materialize` PENDING 分支 `:106-110` 安静返回;`recordFailure` `:137-140` 安静返回 —— **均不抛异常、不误报成功** | | 0 行时不覆盖现持有者 | ✔ | 三个更新语句的 WHERE 均含 `claim_token=?` | | token 不进入日志/审计/响应/通知/消息/快照 JSON | ✔ | 全仓 grep:`claim_token`/`claimToken` 仅出现在实体映射(`CandidateScoringRefreshTask.java:73-74`)、Executor 的 SQL/注释/record、以及 `CandidateScoringRefreshWorker.java:91` 的传参;refresh 包内**无任何含 token 的日志语句** | --- ## 3. 自动化复验 | 命令 | 实际 | 预期 | 结论 | |---|---|---|---| | `mvn -Dtest="*CandidateScoringRefresh*Test" test` | **49 / 49**,F=0 E=0 **S=0** | ≥ 49 | ✔ | | 关联回归(9 组通配符) | **571 / 571**,F=0 E=0 **S=0**,BUILD SUCCESS | ≥ 571 | ✔ | | `mvn clean verify` | **2176 / 2176**,F=0 E=0 **S=0**,BUILD SUCCESS | ≥ 2176 | ✔ | | `python docs\verify_openapi.py` | **176 paths / 214 operations**,All checks passed | 176/214 | ✔ | 日志:`fv-fence.log`、`fv-regression.log`、`fv-clean-verify.log` --- ## 4. 真实 MySQL 最小黑盒 脚本 `fv-fence.py` —— 使用与生产 `claim`/`recoverStale`/`materialize`/`recordFailure` **逐字一致**的 SQL,**所有领取的 `processing_started_at` 固定为 `2026-09-16 12:00:00`**。 **结果:PASS** ### A. 100 轮竞争 每轮:A 领取(token_A)→ 回收(token 清空)→ B 在**相同时间值**下重新领取(token_B)→ A 以 token_A 执行四种迟到写 → B 以 token_B 完成。 | 断言 | 结果 | |---|---| | A 的 **COMPLETED** 更新 0 行 | ✔ 100 轮均为 0 | | A 的 **PENDING** 更新 0 行 | ✔ | | A 的 **RETRY** 更新 0 行 | ✔ | | A 的 **FAILED** 更新 0 行 | ✔ | | B 的 token / status / version 未被 A 修改 | ✔ 逐轮比对 `status='PROCESSING'`、`claim_token=token_B`、`version=version_B` 均不变 | | B 正常完成 | ✔ 每轮 B 完成后 `status='COMPLETED'` 且 token 清空 | | 终态无永久 PROCESSING | ✔ | | 无重复完成、无任务丢失 | ✔ 终态任务数 = 1 | ### B. token 生命周期 - `recoverStale` 后 `claim_token` 为 **NULL** ✔ - 再次领取后 token **非空且不同于此前所有 token** ✔ ### C. 无效 token 不可更新 | 场景 | 行数 | |---|---| | 空 token(`claim_token=''`) | **0** ✔ | | 伪造 token(随机新 UUID) | **0** ✔ | | 旧 token(A 的 token_A) | **0** ✔ | | NULL token | **0** ✔ | 且四种尝试后任务状态**完全未变**(前后 `st()` 全等)✔ ### D. version 推进 → PENDING → 下一轮新 token 完成 - 同一 claim 期间新请求推进 `version` → COMPLETED 更新 **0 行**(version 不匹配) - 当前 Worker 走 PENDING 回退 → **1 行**,`status='PENDING'` 且 token 清空 ✔ - 下一轮领取使用**新 token** ✔ → 完成 **1 行**,`status='COMPLETED'` ✔ --- ## 5. 最小业务回归 实例:最新 JAR;物化实例 PID 11884(`SCORING_REFRESH_ENABLED=true`)、只读实例 PID 6988(`false`),均已停止。 | 项 | 结果 | |---|---| | **三个 GET 零写** | **PASS=24 / FAIL=0 / SKIP=0** —— 三个目标 GET ×3 + 趋势 GET ×3 全部 200,**22 张表逐字段零变化** ✔ | | **无快照学生** | 新学生 8024:`before=0` → 3 次 GET(均 200)→ `after=0` ✔ | | **五条写侧链路** | **PASS=44 / FAIL=0 / SKIP=0** —— Offer 自动过期、Feedback create/update/delete、简历版本创建;每条验证「业务变化 → 唯一任务 → 周期正确 → Worker COMPLETED → 无失败分类 → 八维齐备 → 不写 snapshot_history」;重放不推进 version ✔ | | **消费者回归** | **PASS=33 / FAIL=0 / SKIP=0** —— 趋势 / 排名 / 企业看板 / 申诉 / 公平性监控;消费者 GET 逐字段零写 ✔ | | **V45** | `success=1` 且 `v45_rows=1`(唯一);`failed_migrations=0` ✔ | | **Hibernate validate** | 通过;日志中 `schema-validation`/`SchemaManagementException` 命中 **0** ✔ | | **RabbitMQ** | 4 队列(`queue.sms.verification` / `queue.notification` / `queue.notification.dlq` / `queue.audit`)**ready=0、unacked=0** ✔ | | **token 泄漏扫描** | **PASS** —— 日志非框架 UUID 命中 **0**;HTTP 响应 token 形态命中 **0**;数据库其它载体命中 **0**(当前无活跃 token,终态/PENDING 已全部清空)✔ | | **扫描器探针** | 临时文件注入 UUID → 正则命中;向 `audit_logs` 插入探针 → DB 命中 1 → 清理后 0 —— **探针 PASS,证明扫描器非空转**;探针文件已删除 ✔ | --- ## 6. 收尾 | 项 | 结果 | |---|---| | 本轮 Java 实例 | 仅停止本轮记录的两个 PID(11884、6988);`tasklist` java.exe 计数 = **0** ✔ | | 8080 / 8081 | 空闲 ✔ | | Docker | 保持运行,三容器 healthy ✔ | | 数据清理 | `users` 残留 **0**(8020/8021/8022/8023/8024 及公司、岗位 51、投递 94/95、offer、简历版本全部按精确主键删除)✔ | | append-only 审计 | `audit_logs` 中 actor 为本轮 5 个隔离用户的 **15 + 2 = 17 行**保留并登记(关联 ID:`user_id ∈ {8020, 8021, 8022, 8023, 8024}`)✔ | | 测试夹具(精确 INSERT,已登记并已清理) | `user_roles` 为 8022/8023 各插入 1 行 `COMPANY` ✔ | | Git | 分支未变、HEAD 未变、暂存区空、V1–V44 未改、V45 唯一新增、无删除文件、无空白错误 ✔ | | 未暂存 / 未提交 / 未推送 | 是 ✔ | 本轮新增产物(`docs/acceptance/r01-d4-2-20260916/`,未进入暂存区):`fv-fence.py` + `fv-fence-out.txt`、`fv-tokenscan.py`、`fv-fence.log`、`fv-regression.log`、`fv-clean-verify.log`、`v3-fv-out.txt`、`s1-fv-out.txt`、`s4-fv-out.txt`。 --- ## 7. PASS / FAIL / SKIP 清单 **FAIL**:无。 **SKIP**:无。 **PASS**: 1. 基线(分支/HEAD/暂存区/V1–V44/V45 唯一/最新 JAR) 2. 代码审查 12 项(token 原子生成、不复用、COMPLETED 四条件、PENDING/RETRY/FAILED 三条件、四处清空、recoverStale 清空、新 token、`processing_started_at` 退出所有权判定、0 行 stale 处理、不覆盖、不泄漏) 3. fence 定向 49/49 4. 关联回归 571/571 5. 全量 clean verify 2176/2176(S=0) 6. OpenAPI 176/214 7. 真实 MySQL 100 轮竞争(四种迟到写全 0 行、B 未被修改、B 均完成、无永久 PROCESSING、无丢失) 8. token 生命周期(回收后 NULL、重领后新值) 9. 空/伪造/旧/NULL token 均无法更新 10. version 推进 → PENDING → 新 token 完成 11. 三个 GET 零写 24/24(22 表逐字段) 12. 无快照学生 0 行 13. 五条写侧 44/44 14. 消费者 33/33 15. V45 成功且唯一 16. Hibernate validate 17. RabbitMQ 四队列为 0 18. token 泄漏扫描 0 + 探针有效 --- ## 8. 复验过程中的自暴露问题(非产品缺陷) | # | 问题 | 处置 | |---|---|---| | 1 | `fv-fence.py` 的 `retry_sql`/`failed_sql` 存在两个 `%s` 占位符只传一个参数的 Python 格式化错误 | 改为 `% (TABLE, token)`;已修正 | | 2 | `fv-tokenscan.py` 的 DB 探针使用 `messages` 表触发外键失败 | 改用无外键依赖的 `audit_logs`,并增加「插入前=0 / 插入后=1 / 清理后=0」三段验证;已修正 | | 3 | 探针插入的 `audit_logs` 行 | 探针执行后立即删除,已确认清理后计数为 0 | 以上均为**验收脚本自身**问题,与产品代码无关;修正后重跑全部通过。 --- > **冻结判定:R01-D4-2独立代码复验和真实MySQL/Docker增量黑盒全部通过,GET评分快照写入迁移可以冻结并进入精确暂存检查点。**