25 KiB
R01-D4「GET 评分接口写快照迁移方案设计与证据核查」
日期:2026-09-16
分支:refactor/r01-structure-performance
HEAD:b56f792 perf: batch conversation unread counts
本轮性质:独立现状核查 + 权威迁移设计(未实施生产改造)
1. Git 基线
- 分支
refactor/r01-structure-performance;HEADb56f792;暂存区为空。 - 迁移目录无任何改动(V1–V43 原样);
AGENTS.md未发现。 - 已跟踪工作区存在 1 个仅被 Git 工作树元数据标记为修改的文件(
GlobalExceptionHandler.java);普通git diff为空且 blob 与 HEAD 相同。本轮不触碰、不暂存该文件,也不把它计入 R01-D4 交付。 - 历史未跟踪文件全部保留;未 commit、push。
2. 当前三个 GET 写库调用链
| 端点 | 服务方法 | 调用链 | 事务 | 写入 | 修改字段 |
|---|---|---|---|---|---|
GET /profile/me/candidate-scoring |
CandidateScoringEngineService#getOverview |
computeAll → upsertSnapshots → buildOverview |
@Transactional(默认 rw) |
candidate_dimension_scores |
score、status、sub_scores_json、weight、rules_version、calculated_at、version+1、updated_at=NOW() |
GET /profile/me/candidate-scoring/dimensions/{dimension} |
#getDimensionDetail |
computeAll → filter → upsertSnapshots(单维度) |
@Transactional(rw) |
同上 | 同上(仅该维度行) |
GET /company/candidates/{applicationId}/scoring |
#getCompanyScoring |
授权(company/job 归属) → computeAll → upsertSnapshots |
@Transactional(rw) |
同上 | 同上(目标学生 8 行) |
GET /profile/me/candidate-scoring/trend |
#getTrend |
snapshotRepository.findTrendFrom 只读 |
@Transactional(readOnly = true) |
无 | — |
写库 SQL(CandidateScoringCalculator#upsertSnapshots)为原生 INSERT ... VALUES (...) ON DUPLICATE KEY UPDATE score=VALUES(score), status=..., sub_scores_json=..., weight=..., rules_version=..., calculated_at=..., version=version+1, updated_at=NOW()。唯一键 uk_candidate_dim_scores(student_user_id, dimension, period)(V24 建表、实体 @UniqueConstraint 双重声明)。触发条件为"存在唯一键冲突"即无条件版本自增——即使计算值与上次相同,重复 GET 仍使 version 单调递增、updated_at 改写。
3. 全部快照写入方
| 写入方 | 入口 | 触发 | 是否用户可重复触发 |
|---|---|---|---|
CandidateScoringCalculator#upsertSnapshots(被下三处调用) |
— | — | — |
—— CandidateScoringEngineService#getOverview |
GET /profile/me/candidate-scoring |
GET 读取请求 | 是(任何学生重复刷新) |
—— CandidateScoringEngineService#getDimensionDetail |
GET .../dimensions/{dimension} |
GET 读取请求 | 是 |
—— CandidateScoringEngineService#getCompanyScoring |
GET /company/candidates/{id}/scoring |
GET 读取请求 | 是(企业每次查看) |
CandidateScoringCalculator#recomputeAndSnapshot |
见下 | — | — |
—— CandidateScoringGovernanceService#decideAppeal(APPROVE 分支) |
POST /admin/candidate-scoring/appeals/{id}/decision |
申诉决定 | 否(终态幂等) |
—— CandidateScoringGovernanceService#rollback |
POST .../appeals/{id}/rollback |
申诉回滚 | 否(回滚限额+终态) |
| ——(趋势/排名/看板不写) | — | — | — |
4. 全部快照消费者
| 消费者 | 读取方式 | 依赖字段 | 影响 |
|---|---|---|---|
getTrend(趋势) |
findTrendFrom(student, startPeriod) |
period/score/status/rulesVersion | 只读物化快照;完全依赖快照 |
CandidateScoringGovernanceService#createAppeal |
findSnapshot |
score(scoreAtAppeal)、version(snapshotVersionAtAppeal) | 申诉证据基于快照版本 |
decideAppeal/rollback |
recomputeAndSnapshot + snapshotHistoryRepository.save |
before/after 分数与版本 | 申诉决定/回滚重算写新版本 |
CandidateScoringSnapshotHistory(V25 表) |
仅由申诉决定/回滚写入,getSnapshotHistory 读取 |
version/rulesVersion | 变更审计 |
CompetitionRankingCalculator.loadLatestSnapshots |
findUpToPeriod(students, asOf) |
最新 period 的 8 维分数 | 竞岗排名用物化快照(非实时计算) |
CompanyScoringDashboardService |
读取快照聚合 | score/status | 企业看板/百分位(r01 优化为 CandidatePercentileCalculator) |
CareerProgressService |
读取快照 | 各期分数 | 求职进度 |
FairnessMonitoringRepository |
原生 SQL 按 period 聚合 candidate_dimension_scores + snapshot_history+appeal | score/status/样本数 | 公平性监控(当前快照 + 历史申诉双源) |
PersonalDataCollector |
findByStudentUserId |
全字段 | GDPR 数据导出 |
5. 真实/集成副作用证据
5.1 集成证据(H2,本轮新增独立现状证据测试,见 §17)
R01D4GetWritesSnapshotEvidenceTest 结果 5/5 全绿(未依赖 Docker):
- 首调
getOverview:candidate_dimension_scores新增 8 行;重复调用行数保持 8、version 全部 +1; - 单维度 GET:行数不变、version 继续递增;
- trend GET:前后行数/version 完全一致(只读确认);
- 非法 dimension:校验先于 upsert,零写入;
- GET 评分不写
audit_logs、notifications。
证据边界:该类直接调用 Service,并非真实 HTTP/SecurityFilterChain 测试;它证明 overview、dimension、trend 的持久化行为,但没有覆盖企业评分 GET、无权限、资源不存在、HTTP 响应或 RabbitMQ。这些项目不得依据该 5/5 结果标为已验证,必须在实施验收中补齐。
5.2 真实 MySQL 证据
BLOCKED:Docker Engine 本轮不可用(docker version 无法连接 Engine),按任务书纪律不伪造通过。已在 5.1 提供同级 H2 集成证据;真实 MySQL 行级快照(首/二次 GET 的行数、version、updated_at、返回 JSON 是否变化、错误路径零写、audit/notification/RabbitMQ 无副作用)由 D4-2 阶段实施前用真实 MySQL 补充复验(§12 验收清单已含)。
静态代码可直接确认的行为(无需运行):upsertSnapshots 的 ON DUPLICATE KEY UPDATE ... version=version+1, updated_at=NOW() 保证重复 GET 必使 version 单调递增、updated_at 刷新;趋势/申诉/监控/看板/排名全部消费该物化快照。
6. 首次与重复 GET 的行数/version/updated_at
| 场景 | 行数 | version | updated_at | 返回 JSON |
|---|---|---|---|---|
| 首次 GET overview | 0 → 8 | 0(插入初值) | 写入 NOW() | 完整评分卡 |
| 第二次相同 GET | 8(不变) | +1(8 行全部) | 改写为新 NOW() | 与首次等值(公式确定) |
| 单维度 GET(已有 8 行) | 8(不变) | 该维度行 +1 | 该行改写 | 明细不变 |
| trend GET | 不变 | 不变 | 不变 | 由物化快照导出 |
| 非法 dimension | 不变 | 不变 | 不变 | Service 抛业务异常(H2 已验证) |
| 无权限 / 资源不存在 | 静态代码预期不变 | 静态代码预期不变 | 静态代码预期不变 | 本轮未做真实 HTTP 证据 |
结论:"读取放大写入"确凿——GET 对每次读取强制物化并推进版本,自增 version 不代表业务状态迁移,period 快照语义是"每次读取覆盖值"而非"首次值/最新值"。
7. trend 的现有依赖
getTrend 只读 candidate_dimension_scores(findTrendFrom,period>=start 升序)。它不调用 computeAll/upsert,因此完全依赖由其他路径(当前主要是三个 GET + 申诉重算)持续写入的快照。若直接删除 GET upsert 而未建立写侧物化,趋势将冻结在最后一次写入时刻。
8. 申诉、回滚与公平性监控依赖
- 申诉创建:
findSnapshot(student, dimension, period)读取当前行,固化scoreAtAppeal与snapshotVersionAtAppeal(写入candidate_scoring_appeals)。若快照过期,申诉取证基于旧版本。 - 申诉决定(APPROVE):
recomputeAndSnapshot重算写新版本 + 写入candidate_scoring_snapshot_history(before/after score、version、rules_version)。 - 回滚:同决定路径,另扣
candidate_scoring_rollback_limits。 - 公平性监控:
FairnessMonitoringRepository两条原生聚合:candidate_dimension_scores WHERE period=?按学生分组(overview/dimensions 用),以及candidate_scoring_snapshot_history JOIN appeal(appeals 用)——当前快照 + 历史申诉双源。 - 申诉期间快照若被外部 GET 再次覆盖(version 再 +1),
snapshotVersionAtAppeal不再等于当前版本——审计可辨,但"申诉取证与当前快照差 version"是现状语义的一部分,迁移时不得破坏。
9. 写侧事件矩阵(按计算器真实数据源)
| 评分输入变化 | 计算器读取证据 | 推荐刷新入口 | 处理方式 | 备注 | |
|---|---|---|---|---|---|
| 学生资料更新 | StudentProfileRepository:学历、学校、技能及资料完整度 |
ProfileService#updateProfile 成功提交后 |
标记学生快照为待刷新 | 仅实际影响字段变化时触发 | |
| 多版本简历变化 | resume_versions ACTIVE 数量与 MAX(updated_at) |
ResumeVersionService#createVersion/updateVersion/deleteVersion 成功提交后 |
标记待刷新 | setDefault 是否影响评分需按 SQL 判断;当前 SQL 不读取默认标记,不应无条件触发 |
|
| 求职偏好更新 | StudentPreferenceRepository |
PreferenceService#savePreferences 成功提交后 |
标记待刷新 | 影响 COMPLIANCE | |
| 能力测评结果完成 | AbilityAssessmentScoreSource#findLatestValidCompletedResult |
AbilityAttemptService#submit 成功提交后 |
标记待刷新 | saveAnswers 尚未形成完成结果,不应刷新 |
|
| 投递创建/撤回 | applications 数量、WITHDRAWN 数、近 90 天数量 | ApplicationService#apply/withdraw 成功提交后 |
标记待刷新 | 企业侧状态更新只有在实际改变计算器读取字段时才触发;当前聚合只区分 WITHDRAWN | |
| 登录及审计活动 | audit_logs 近 30 天数量与 LOGIN 数 | 统一审计写入成功后或补偿扫描 | 优先脏标记/补偿 | 入口很多,不能逐 Controller 机械接入 | |
| Offer 状态变化 | offers 中 ACCEPTED/EXPIRED 数量 | Offer 状态机成功提交后 | 标记待刷新 | 原报告遗漏 | |
| 候选人反馈变化 | candidate_feedback 数量与 AVG(rating) | 候选人反馈创建/修改的真实写入口 | 标记待刷新 | 不是 JobReviewService;原报告混淆了两类评价 |
|
| 学生发送消息 | messages.sender_user_id 数量 | 消息发送成功提交后 | 标记待刷新 | 撤回不改变总消息数,按现公式无需刷新 | |
| 账户创建时间 | users.created_at | 无日常写入口 | 初次物化/补偿扫描 | 时间本身不变 | |
| 申诉决定/回滚 | 已有 GovernanceService#decideAppeal/rollback |
保持现有合法写侧 | 同事务 | 不与普通刷新事件混用历史审计语义 | |
| rulesVersion 或时间窗口跨界 | rulesVersion、90/30 日窗口、能力结果有效期 | 发布批次及定时补偿扫描 | 批处理 | 即使没有业务写事件,分数也会随时间变化,因此仅靠事件触发不完备 |
明确排除:当前 computeAll 不读取 RIASEC 结果、面试评分或岗位评价,不得为这三类入口增加刷新;传统 ProfileService#uploadResume 的文件字段也不是 queryResume 的数据源,除非同时改变资料完整度实际依赖字段,否则不应触发。
现状结论:除申诉决定/回滚外,真实输入源缺少统一物化机制;同时存在时间窗口自然变化,因此“把若干 Service 同步调用 refresh”仍不能证明覆盖 100%。
10. 三个迁移选项比较
| 维度 | 方案 A:同步写侧触发 | 方案 B:事务提交后异步刷新 | 方案 C:脏标记+定时物化 |
|---|---|---|---|
| 一致性 | 强(同一事务) | 最终一致(短窗口) | 最终一致(更长窗口) |
| 事务耦合 | 高(评分写并进业务事务) | 中(分离;核心事务不受失败连累) | 低 |
| 失败恢复 | 回滚即重来,无独立补偿 | 需重试/死信 + 补偿扫描 | 靠定时器重扫,天然补偿 |
| 幂等性 | 唯一键天然幂等(version 语义需调整) | 需幂等键 + 去重 | 需幂等键 + 去重 |
| 并发 | 数据库条件更新兜底 | 需"旧事件不得覆盖新事件"防护 | 需锁/版本比对 |
| 实施范围 | 小(线程内调用)但侵入全部写方法 | 中(需要可靠发布/订阅或 MQ) | 中(脏标记 + 调度器) |
| 对现有 RabbitMQ 依赖 | 无 | 强依赖(需事务后发布、重试、去重、死信) | 无(可走内置调度) |
| 对趋势/申诉/监控影响 | 不变 | 短窗口内趋势滞后,申诉取证可能取旧版本 | 更明显滞后 |
| 发布/回滚难度 | 低 | 中(MQ 基建变化) | 中(调度器+补偿) |
| 是否需要 Flyway | 否 | 否(建议无) | 否(可选脏标记列则需迁移) |
| 是否新增公网接口 | 否 | 否 | 否 |
11. 唯一推荐方案
分两阶段,先收敛写入口并建立可靠的“待刷新”机制,后让 GET 停写。唯一推荐为方案 C(持久化脏标记/刷新任务 + 定时物化),写侧提交后只登记待刷新任务;物化 Worker 复用唯一计算器和快照 Writer。
理由:
- 不能把评分刷新放入资料、投递、消息等核心业务事务;评分失败不应回滚这些业务,原报告在“同事务”和“失败不影响业务”之间自相矛盾。
- 当前 RabbitMQ 没有已验证的事务消息、持久去重和死信恢复链路,不能直接依赖纯异步 MQ。
- 评分包含 90/30 日窗口与能力结果有效期,仅靠业务事件不完备,必须有定时补偿。
- 幂等键若不持久化就无法跨进程、重启和重试去重;因此原报告“无表结构变更但用 sourceEventKey 去重”不可实现。
- 持久任务允许业务事务只做一次轻量 upsert 标记,Worker 失败可重试,不阻塞核心业务;GET 兜底在迁移期保留。
12. 分阶段实施顺序
D4-1(可靠物化基础设施,GET 兜底保留)
- 新增唯一
CandidateScoringSnapshotWriter,集中快照差异比较、条件更新和版本规则;申诉专用 history 语义仍由 Governance 管理。 - 新增持久化刷新任务/脏标记(需要新的 Flyway,不能再声称“无需迁移”)。唯一键至少覆盖 student + target period;任务记录 requested_at、attempts、next_attempt_at、processed_at/version,不保存敏感评分输入。
- §9 的真实输入写侧仅登记任务,不在线程内计算八维评分;时间窗口和 rulesVersion 由定时扫描登记任务。
- Worker 在独立事务中领取任务、计算并写快照;失败可重试,成功后原子完成;不得依赖 JVM 锁。
- 保持 GET upsert 作为迁移兜底,完成真实 MySQL 并发、重试和覆盖证据。
D4-2(GET 停写)
- 三个 GET 改为
@Transactional(readOnly = true),删除 upsert 调用(computeAll 保留用于实时计算响应字段,与物化无关)。 - 开启补偿扫描,对时间窗口、rulesVersion 变化以及遗漏任务进行修复;与持久任务幂等协作。
- 真实 MySQL 验收:GET 重复请求前后
candidate_dimension_scores完整不变、无 audit/notification/RabbitMQ 副作用、趋势/申诉/监控/排名表现与 D4-1 一致。 - 达标后方可切换/废弃 GET 兜底。
阶段切换网关(未满足不得进入 D4-2):真实数据源覆盖完整、相同任务重放不造成无效 version 增长、并发领取恰好一次有效物化、失败可重试且不影响原业务提交、时间窗口可由补偿扫描推进、GET 停写后的趋势/申诉/排名/监控真实 MySQL 回归通过。
13. 并发与幂等设计
- 任务幂等键:持久任务按
(studentUserId, targetPeriod)合并;多次输入变化只需保证至少一次最新状态重算。rulesVersion 是计算结果属性,不用虚构的内存sourceEventKey充当持久去重。 - 快照唯一性:继续使用
uk_candidate_dim_scores(student_user_id, dimension, period)。 - 无效重放:Writer 比较 score、status、sub_scores_json、weight、rules_version 等业务值;完全相同则不更新 version/updated_at。
calculated_at是计算时间,不能单独作为“业务变化”依据,否则每次重算仍会制造版本。 - 并发/旧任务:任务不是携带完整旧数据的事件,而是“请按数据库当前状态重算”的合并信号,因此晚到任务只会重新读取最新状态,不会用旧载荷覆盖新快照。
- rulesVersion 变化强制刷新:
rules_version参与唯一比较,升级批次以新版本全量重算。 - 两个输入变化并发:数据库任务唯一键合并;Worker 使用条件领取/乐观锁或
SELECT ... FOR UPDATE SKIP LOCKED,禁止 JVM 本地锁;必要时完成后检测 requested_at 是否在处理期间推进,若推进则保留待处理状态再算一次。 - 失败重试:普通输入变化只登记持久任务,实际评分由 Worker 独立事务完成;失败更新重试状态并由补偿扫描兜底。重试不得重复写
candidate_scoring_snapshot_history/audit(该表只在申诉决定/回滚写,普通物化不写它)。 - 申诉期间快照变化:
scoreAtAppeal/snapshotVersionAtAppeal保留申诉创建时刻值;写侧刷新不得改写申诉取证字段;决定/回滚的重算仍走recomputeAndSnapshot(其版本语义与写侧事件在calculated_at/rules_version条件上一致,回滚目标版本 = beforeVersion+1 的既有确定性逻辑需在 D4-2 复核不冲突)。 - 回滚后的新输入事件:允许再次计算——回滚后新事件刷新按正常路径写入新版本,
snapshot_history只记录申诉动作,不记录普通事件。
14. 失败恢复策略
- 写侧业务事务只 upsert 持久刷新任务;任务登记失败应随该业务事务回滚,避免提交了源数据却永远没有刷新信号。评分计算不在核心业务事务中执行。
- Worker 独立事务执行,失败记录 attempts/next_attempt_at 并指数退避;超过阈值保留 FAILED/PENDING 状态供告警和补偿,不伪装成功。
- 定时补偿根据活跃用户、时间窗口与 rulesVersion 登记任务,不直接绕过 Writer 写快照。
- 最终一致性:允许短暂滞后(分钟级),趋势/监控为展示性质;申诉取证在写侧事件刷新后基于新版本,行为与现状一致(现状也是"最新一次写入者胜")。
15. 数据迁移与兼容策略
candidate_dimension_scores本身优先保持结构不变;D4-1 需要新增持久刷新任务表及唯一/领取索引,因此必须新增下一号 Flyway(不得修改 V1–V43)。具体表设计在实施前独立评审。- 现有存量数据:保留;D4-2 上线前对活跃学生做一次全量
upsert对齐(事件覆盖前不删 GET 兜底,因此存量自然收敛)。 - version 语义从"每次读取递增"收敛为"每次有效变更递增 + rulesVersion 升级递增";历史 version 不重写。
- 趋势/申诉/监控/排名/导出读取代码零改动。
16. 下一轮(D4)精确修改文件清单(预估,实施前以代码现状复核)
- 新增:
candidatescore/CandidateScoringSnapshotWriter.java、刷新任务实体/Repository/Worker/调度器,以及下一号 Flyway(具体命名先核对迁移目录最大版本)。 - 修改(仅写侧事件化,不改变业务语义):
profile/ProfileService.java、resume/ResumeVersionService.java、preference/PreferenceService.javaability/AbilityAttemptService.java、application/ApplicationService.java- Offer、候选人反馈、消息与审计的真实写入 Service(实施前精确定位)
candidatescore/CandidateScoringCalculator.java(upsert 委托 + 条件 version 递增)
- D4-2 才改:
candidatescore/CandidateScoringEngineService.java(三 GET 改为 readOnly 并移除 upsert 调用)。 - 文档:
docs/backend-development-roadmap.md(本报告结论)、docs/candidate-scoring-api.md(若契约文字涉及写语义)。 - 允许且预计需要新增一个 Flyway、刷新任务表及必要索引;仍不新增 ErrorCode 和公网接口。
17. D4 必须先写的失败测试(D4 实施轮的第一批测试,用于驱动实现)
GET 重复请求前后 candidate_dimension_scores 完整内容不变(行数/score/status/version/created_at/updated_at 全等)。- 三个 GET 不写 audit、notification、RabbitMQ(消息计数前置后置相等)。
- GET 返回 JSON 与基线一致(黄金 JSON 对比)。
- §9 每个真实输入源变化都会登记或合并一条待刷新任务;明确排除的非输入事件不得登记任务。
- 同一学生/period 的重复登记合并;完全相同的计算结果不增加快照 version。
- 并发登记后最终至少执行一次基于数据库最新状态的重算,任务不丢失且快照行不损坏。
- 刷新失败可重试、不写重复历史/审计、不影响原业务事务。
- rulesVersion 升级触发全量重算(version 递增)。
- 申诉创建取证字段(scoreAtAppeal/snapshotVersionAtAppeal)在写侧刷新后仍正确。
- 越权/非法 dimension/资源不存在零写入。
- 真实 MySQL 行级副作用快照 + RabbitMQ ready/unacked 归零。
- OpenAPI 176 paths / 214 operations 保持;不新增公网刷新接口。
现有
CandidateScoringPersistenceIntegrationTest#upsertSnapshots_shouldBeIdempotentAndIncrementVersion的断言(重复 upsert version+1)在 D4-2 后语义变为"仅有效变更 +1",届时按新契约更新该既有断言(属于既定契约变更的测试演进,非草率改断言)。
18. 风险与回滚方案
| 风险 | 影响 | 缓解/回滚 |
|---|---|---|
| 写侧事件覆盖不全 → 某学生快照停滞 | 趋势/排名/监控滞后 | D4-1 保留 GET 兜底期间逐步补覆盖;D4-2 依赖补偿扫描兜底;若需开关必须在实施中明确新增并测试,不能假定现有开关存在 |
| 申诉取证版本语义漂移 | 申诉对比失真 | 事件刷新不触碰取证字段;D4-2 前在真实 MySQL 对申诉全链路回归 |
| 异步刷新丢失 | 快照缺失 | 事务后发布 + 死信 + 补偿扫描(幂等键避免重复写) |
| version 语义变化被下游误读 | 审计/导出版本含义差异 | 文档化;规则升级仍强制递增 |
| 回滚难度 | — | D4-1 可停止任务登记和 Worker,但新增表由 Flyway 保留不用;D4-2 可恢复 GET upsert。不得回滚或删除已执行迁移 |
19. 自动化实际结果(本轮)
| 命令 | 结果 |
|---|---|
mvn -Dtest="R01D4GetWritesSnapshotEvidenceTest" test(新增现状证据) |
5/5,BUILD SUCCESS |
mvn -Dtest="CandidateScoringEngineServiceTest,CandidateScoringPersistenceIntegrationTest,*CandidateScoring*Test,*ScoringMonitoring*Test" test |
134/134,F=0 E=0 S=0,BUILD SUCCESS |
mvn clean verify |
2109/2109(≥2104 预期),Failures=0、Errors=0、Skipped=20,BUILD SUCCESS |
python docs\verify_openapi.py |
176 paths / 214 operations,All checks passed |
Skipped=20 逐项说明:全部来自 Q01 既有的 JobSearchQueryExplainMySqlIntegrationTest 与 JobSearchQuerySideEffectMySqlIntegrationTest(@Testcontainers(disabledWithoutDocker = true),分别 15/5 例)。Docker Engine 本轮不可用 → Testcontainers 显式约定跳过,为既有机制而非本轮核心核查,且非"把环境缺失伪装为通过";R01-D4 核心副作用证据由 H2 集成测试 5/5 提供,真实 MySQL 证据单独标注 BLOCKED(§5.2)。
20. Git 状态与边界声明
- 未修改任何生产代码;未修改既有测试断言;未修改 V1–V43;未新增 Flyway/表/索引/ErrorCode/公网接口。
- 新增(工作区未提交):
docs/r01-d4-get-scoring-snapshot-migration-design.md、R01D4GetWritesSnapshotEvidenceTest.java(现状证据测试,非契约断言)。 - 未 commit、push;未进入 R01-D5。历史未跟踪文件未删除未移动;暂存区为空。
- 真实 MySQL 副作用证据标注 BLOCKED(Docker Engine 不可用),未伪装通过;H2 集成证据 5/5 已提供。
21. 最终结论
R01-D4 GET评分快照写入迁移方案设计完成,待负责人确认后分阶段实施。
阻塞/依赖声明:当前 GET 兜底写快照服务多个真实输入域与 6 类消费者(趋势/申诉/监控/排名/看板/导出),且评分会因时间窗口自然变化;因此严禁直接删除 GET upsert。最小可行迁移路径 = D4-1(唯一 Writer + 持久刷新任务 + Worker/补偿扫描 + GET 兜底)→ 真实 MySQL 验证并发、重试与数据源覆盖 → D4-2(GET 转 readOnly)。