轻松聘AI开发
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

9.5 KiB

R01-D4-1「候选人评分快照可靠物化基础设施」实施报告

日期:2026-09-16 分支:refactor/r01-structure-performance;HEAD 检查点:b10c4a7 docs: design scoring snapshot materialization(后于 b56f792) 性质:D4-1 实施(GET 兜底未删除,三 GET 仍经 upsertSnapshots 写库)

1. Git 基线

  • 分支正确;设计检查点已提交 b10c4a7(3 个设计文件)。
  • 暂存区空;未 commit/push 本轮实现;V1–V43 未修改;V44 为唯一新增迁移。
  • GlobalExceptionHandler.java 保持行尾符标记现象(内容 diff 为空)未动。

2. V44 表结构(V44__candidate_scoring_refresh_tasks.sql)

candidate_scoring_refresh_tasks: id PK、student_user_id、target_period(7)、status、requested_at、processing_started_at、processed_at、next_attempt_at、attempts、last_error_code(40)、version、created_at、updated_at;uk_refresh_task_student_period(student_user_id,target_period)、ck_refresh_task_status(PENDING/PROCESSING/RETRY/COMPLETED/FAILED)、idx_refresh_task_claim(status,next_attempt_at)、FK→users(id) ON DELETE CASCADE。不保存 Token/手机号/简历/评分源数据。

3. 任务唯一键与状态机

唯一键 (student_user_id, target_period)。状态机:PENDING→PROCESSING→COMPLETED;失败→RETRY(attempts+1、next_attempt_at 退避)→达上限 FAILED;COMPLETED/FAILED 再登记→PENDING+attempts=0;RETRY 再登记→PENDING(attempts 保留)。

4. 登记事务语义(CandidateScoringRefreshRequestService#requestRefresh)

原生参数化 upsert 合并;REQUIRED 传播——随调用方业务事务,源业务提交失败则登记一并回滚;登记不触发线程内计算、不发 RabbitMQ、不写审计、无敏感值;period 取 capturedNow 单次读取。

5. Worker 领取与并发(CandidateScoringRefreshWorker)

数据库条件更新领取(status IN (PENDING,RETRY) AND next_attempt_at 到期 置 PROCESSING,0 行即被并发方领取);无 JVM 锁/Thread.sleep。领取使用独立短事务;计算、8 维 Writer 与完成状态使用另一个 REQUIRES_NEW 原子事务;失败状态再用独立事务持久化。因此单维写失败会回滚该任务全部快照,但不会污染同批其他任务。并发双 Worker 仅一次有效物化。

6. PROCESSING 期间新请求防丢失

登记对 PROCESSING 保留状态并推进任务 version;Worker 领取时记录 claimedVersion,完成时仅在 version 未变化时置 COMPLETED;处理期间有新请求导致 version 变化时回到 PENDING。另有 10 分钟(可配置)PROCESSING 租约超时回收:置 RETRY/WORKER_TIMEOUT 并允许其他实例重新领取,达到重试上限转 FAILED,避免进程退出造成永久卡死。

7. 重试与 FAILED

失败捕获→固定分类(COMPUTE_FAILED/WRITE_FAILED/UNKNOWN_REFRESH_FAILURE;租约超时为 WORKER_TIMEOUT)入 last_error_code(不含异常原文/SQL/手机号/堆栈)→独立事务 attempts+1→RETRY+next_attempt_at;达上限 FAILED;失败不回抛、不回滚同批其他任务;补偿扫描可重新登记。

8. Writer 比较字段(CandidateScoringSnapshotWriter)

score、status、sub_scores_json、weight、rules_version 全部相同→version/updated_at 不变;任一变化→version+1;calculated_at 不触发;JSON 写入前 canonical(字段字典序)防伪变化;null-safe 相等用 (= ? OR (IS NULL AND ? IS NULL))(MySQL/H2 双兼容)。

9. version 精确语义

普通物化:值变才 +1(D4-1 已授权语义收敛——重复相同 GET/重放不再制造版本噪声);rulesVersion 变化强制 +1。

10. Governance 强制路径

recomputeAndSnapshot(申诉决定/回滚)委托 upsertForcingVersion 无条件 +1,保持 afterVersion=beforeVersion+1、snapshot_history 与 scoreAtAppeal/snapshotVersionAtAppeal/回滚限额语义不变;普通物化不写 snapshot_history。

11. 实际接入的真实输入源(写侧 Service,同事务登记)

ProfileService#updateProfile;ResumeVersionService#createVersion/updateVersion/deleteVersion;PreferenceService#savePreferences;AbilityAttemptService#submit;ApplicationService#apply/withdraw/updateStatus;OfferService#transitionStatus;CandidateFeedbackService#createFeedback;MessagingService#sendMessage(仅 STUDENT 发送者登记)。补偿扫描兜底 audit/时间窗口。

12. 明确未接入

RIASEC(AssessmentAttemptService);InterviewScoreService;JobReviewService;AbilityAttemptService#saveAnswers;ResumeVersionService#setDefault(公式未读默认标记);消息撤回;只读查询;AuditService 实时登记(防 刷新→审计→刷新 循环,审计变化由补偿扫描覆盖)。

13. 时间窗口与补偿扫描(CandidateScoringRefreshCompensationScanner)

基础版:按 user id 做 keyset 分页(页大小默认 100)遍历近 90 天投递/近 30 天审计活跃学生并登记刷新任务;一次扫描会跨越全部页面,不再反复只处理首批 ID;不直接写快照。rulesVersion 切换由事件/重扫覆盖。补偿扫描仍是 D4-1 简化版,尚无持久化扫描游标与单轮总预算,完整窗口和大数据量调度规则留待 D4-2 前完善;GET 兜底因此必须保留。

14. 修复前失败测试(TDD 证据)

实现前基线即检查点后现状(b10c4a7 上证据测试 5/5 全绿,其中"重复 GET version+1"断言证明旧行为);实施中 Writer 两次 H2 语法不兼容(<=>、VALUES() 表达式)→ Errors=7→4→0 收敛;@InjectMocks 缺新依赖 → Errors=55→30→0;Persistence 旧断言与新契约冲突 → 1 Failure 被改写为幂等断言。最终全绿。

15. H2 与真实 MySQL 结果

H2 定向集成已覆盖:登记合并、并发领取、处理中重登记、租约超时恢复、跨三页以上补偿扫描、单任务中途写失败全回滚、失败状态独立提交、同批任务事务隔离及外层业务回滚。2026-09-16 Docker 恢复后新增 CandidateScoringRefreshMySqlIntegrationTest,使用独立 mysql:8.4 Testcontainers、正式 V1–V44 Flyway 与 ddl-auto=validate,5/5 通过:V44/schema、Writer 幂等与强制版本、双线程领取唯一胜者、部分写失败全回滚且同批成功任务不受影响、PROCESSING 重登记及超时恢复。未复用或修改开发库。

16. 查询预算

登记:1 条 upsert;Worker:到期 ID 查询 + 每任务独立 claim/load + computeAll + 8 维锁定比较/条件写入 + complete;失败另有 1 次独立状态写。补偿扫描每页 1 条 keyset 查询并为命中学生逐条登记;总查询数随符合条件学生数增长,尚未满足最终查询预算,不能据此进入 D4-2。

17. RabbitMQ 与数据库副作用

Worker/登记不发布 RabbitMQ、不写 notification、不写 snapshot_history、不写 audit;测试 20/22 验证。快照仅 candidate_dimension_scores 与任务表。

18. 全量测试

Docker 恢复后的最终实测 mvn clean verify:2130/2130,Failures=0、Errors=0、Skipped=0,BUILD SUCCESS。其中既有 Q01 MySQL 20 项与新增 D4-1 MySQL 5 项均真实执行。OpenAPI 176 paths / 214 operations,校验通过。

19. OpenAPI

176 paths / 214 operations,Actuator 不进业务契约;无新增公网刷新接口。

20. 精确修改文件

新增:V44 迁移;candidatescore/refresh/:CandidateScoringRefreshTask、CandidateScoringRefreshRequestService、CandidateScoringSnapshotWriter、CandidateScoringRefreshWorker、CandidateScoringRefreshTaskExecutor、CandidateScoringRefreshCompensationScanner、CandidateScoringRefreshScheduler;测试 CandidateScoringRefreshInfrastructureTest、CandidateScoringRefreshFailureTransactionTest。 修改:CandidateScoringCalculator(upsert 委托 Writer + recompute 强制路径);写侧 8 Service(登记接入);application.yml(app.scoring.refresh.* 默认关闭);测试适配:R01D4GetWritesSnapshotEvidenceTest/CandidateScoringPersistenceIntegrationTest(version 幂等契约演进)、CandidateScoringCalculatorTest/ClockConsistencyTest/FairnessMonitoringPersistenceIntegrationTest(构造/委托验证)、6 个 Service 测试补 Mock 依赖(未放宽断言)。

21. Git 状态

未 commit/push;暂存区空;V44 唯一新增迁移;历史未跟踪文件未删未移;GlobalExceptionHandler 行尾现象保持。

22. 进入 D4-2 的网关(当前:未达到)

  • 唯一 Writer + 幂等 version 语义(H2 验证)
  • 任务登记/防丢失/并发领取/重试/failure 分类(H2 验证)
  • 写侧 8 类事件登记 + 不接入项确认
  • 真实 MySQL 8.4 核心基础设施验收(V44/validate、Writer、并发领取、事务隔离、重登记与超时恢复)
  • D4-2 前消费者黑盒(GET 零写切换、趋势/申诉/排名/看板/监控、副作用与 RabbitMQ)
  • 补偿扫描完整窗口规则 + 查询预算真实库验证
  • 写侧覆盖证明(滚动运行期对账) → 未满足 → 不得移除 GET 写入

23. 剩余风险

补偿扫描仍缺持久化游标与单轮总预算;POST/PUT 申诉决定/回滚在真实库对 version 语义的回归未验(H2 已验);普通 Writer 的 MySQL 幂等、差异更新、强制版本、SELECT FOR UPDATE 与 no-op upsert 已有专用真实库证据。

最终状态

R01-D4-1 核心基础设施及真实 MySQL 8.4 验证已完成;GET 兜底仍保留。 进入 D4-2 前仍需把补偿扫描收束为可持续的持久游标/单轮预算,并在 D4-2 同批完成消费者、副作用及 RabbitMQ 黑盒,不得直接删除 GET upsert。