含10-22精修六类修复: 续租renewRunning/结果INCOMPLETE_RESULT门/终态TASK_TERMINAL/liveGrant复用ACL/hosted幂等重放绑定/错误明细整条重序列化。 合规审计与证据: 工作区docs/tasks/REFINEMENT_AUDIT.md + evidence/refinement-20261022/(patch+指纹)。SQL未执行、测试未运行(仅-skipTests编译)。
26 KiB
答卷差异对账 v1(TASK_3 · FC 统一发起,SH 执行修复并回传可追踪结果)
2026-10-22代码精修修订(优先于本文初版描述)
- 指令签名方是FC,目标消费方是SH,两种appKey分开核验。SH必须设置
svchall.answer-reconcile.source-app-key为FC实际签名身份;空值拒收。信封sourceAppKey也与该配置核对;不能要求FC使用SH身份签名。 - 派发不可用同样先领取尝试次数,再进入有界退避/FAILED;不在QUEUED状态调用只匹配DISPATCHED的更新。
- RUNNING心跳可多次刷新,绑定当前attempt;同attempt的RUNNING记录可以推进为终态结果。COMPLETED必须同时声明两侧扫描完成且中心manifest已物化完整。终态结果重复提交只作身份核验与幂等确认,不重新执行;终态任务不再提供清单/快照/缺席证明,返回TASK_TERMINAL。
- SH同/旧attempt重复指令不重开终态行;更高attempt才可重新排队,work_version防止旧Worker回写新指令。执行/结果上报失败使用独立retry_count,默认6次、指数退避至上限;结果上报失败只重报,不重扫。新增本地字段升级脚本add_reconcile_work_recovery.sql未执行;新库使用answer_reconcile_work.sql,二者择一。
- 修复冲突后的中心重读穿透缓存,缓存按页清理;DEFERRED保留计数/错误/候选但不推进水位。权威答复及信封逐项核对task/consumer/school/form/answer/version/grant,不把合法签名等同任意坐标合法。
- 授权决策复用TASK_1的requireRead/requireSubscribe;资源校域校验已补齐,但应用可服务学校集合事实源仍未实现,不能宣称解决Task2 §9#1的全部问题。
- 本轮仅编译/跳过测试打包;历史36+36测试结果不构成精修后状态机、数据库并发或签名互调验证。Task4已交付独立文档草案,不实施候选schema功能。
日期:2026-10-21。适用范围:form-center(中心/发起方)与 svc-hall(消费方/执行方)之间的答卷集合对账协议。
前置:TASK_1 消费授权 v3(docs/consumer-access-v3.md)、TASK_2 快照投递 v2 及其前置修正
(docs/answer-delivery-v2.md §8/§8.1、docs/tasks/TASK_2_PREFIX_CHECK.md)。
本文是协议与边界的事实源;TASK_1/2 的文件一律只调用、不修改。
1. 这套东西解决什么、不解决什么
游标增量拉取(GET /oapi/form/answers)回答的是"最近变了哪些",它不能回答三个问题:
两侧集合到底一不一致、不一致时以谁为准、修复之后有没有可追踪的回报。本任务补的就是这三件事。
| 它是什么 | 它不是什么 |
|---|---|
| 一轮双边集合验证 + 有界修复 + 可追踪回报 | 不是第二份增量拉取(一致的行不会去中心取值) |
| 中心签发的任务级权威结论(清单/当前快照/缺席证明) | 不是消费方本地判断的背书(本地永远不能自证"这行该删") |
| 投递链(TASK_2)之外的独立修复通道 | 不是重投、不是建件、不是流程补偿 |
answer_reconcile_* 三张中心表 + 一张消费表 |
不是 answer_op_log(不得拿它当任务/答卷/投递主表) |
明确不在范围内:svc-hall 的 FlowReconcileJob 仍是流程恢复域,本任务不碰它;
对账修复不生成虚假的 MQ 消费成功回执,不重放历史 intent,不自动发起办件或流程命令。
2. 任务坐标:一行任务 = 一个「消费方 appKey + 消费租户域 + 表单」
consumerSchoolId 与投递行同口径,就是答卷的归属校(中心侧 findByUuidAndSchId(uuid, consumerSchoolId)
与 listEnabledSubscriptions(formUuid, answer.getSchId()) 都是这个语义)。
任务范围只到一张表单,不做"一行任务覆盖全部表单"的设计,理由不是审美:
- 清单游标与消费端本地扫描游标都是
answerUuid(32 位小写十六进制,单字段升序)。 跨表单就必须复合游标,两侧无法锁步比较,多余行判定会失去依据; - 全量 = 按生效订阅裂变成多行任务,每行仍然单表单。裂变性受
max-tasks-per-run约束, 因此不存在"一次人工触发把整库变成一个无界大任务"的形态; - 单并发、取消、租约、结果幂等这些语义都落在"一行任务"上才有清晰定义。
scope=ANSWER(单答卷)是同一坐标下的一行清单特例:清单里就这一行(或零行=缺席候选)。
2.1 简单表单不强建消费者
建任务的第一道闸是 TASK_1 的生效订阅(enabled=1 AND subscribe_allowed=1 AND read_allowed=1)。
没有订阅就没有任务——本任务不创建 grant 行,也不因为"这张表看起来有人要"而代填授权。
读权与订阅权必须同时开着:清单答复与权威快照都走 requireRead,而消费端修复前的新鲜核验走
requireSubscribe,缺一条就会造出一轮"能建、必失败"的任务。
scope=ANSWER(单答卷)同样过这道闸:答卷归属校就是它的消费租户域坐标,该域上没有生效订阅时
直接以 GRANT_REVOKED 拒建——否则"指定一份答卷"会变成撤权后的后门。
3. 不可变清单(manifest)
3.1 物化
AnswerReconcileReadRepository(本任务新增的只读仓库)按 (form_uuid, sch_id) 的 uuid 升序 keyset
分页读 answer,在一个一致读事务内把摘要写进 answer_reconcile_manifest_row:
(answer_uuid, revision, content_hash, is_deleted, form_uuid, form_version, seq_no)
- 含墓碑:
is_deleted=1的行照样入清单。"缺席"与"存在但已删"是两个不同结论, 后者必须走权威修复保留本地业务扩展,不能被当成前者; content_hash由AnswerSnapshotService#contentHash现算(fc.canon.v1), 与投递行同一条规范化实现。本任务不复制任何规范化逻辑;form_version取该行answer.version(答卷钉住的 schema 版本),不是表单当前版本;- 清单行的 JPA 列全部
updatable=false,仓库层不提供 update/delete——它是这一轮的冻结事实源; - 事务回滚时清单行与检查点一起消失,因此崩溃后是从头重物化,不存在"半截清单 + 半截检查点" 这种会把差异判没的形态;
- 修订号为 0 的存量行原样入清单,但不可被权威修复(消费端
validate要求 revision ≥ 1), Worker 按DIFF_REVISION_ZERO只报告。
3.2 分页与完整性
GET /oapi/form-sync/tasks/{taskId}/manifest?cursor={answerUuid}&size={n}
→ { taskId, rows[], nextCursor, completed, totalRows, totalPages, taskState }
size默认page-size(200),上限 500;游标非 32 位十六进制即拒(页码与库内 id 都不是合法游标);completed=true是唯一的结束标记。某一页空了不构成结束证据——中心清单物化完就有total_rows/total_pages,读侧只需把completed当判据;- 清单未完整(
manifestComplete=0)时该端点直接拒答分页,避免"读到空=集合空"的误判; - 消费端把每页的
taskState也当判据:中心已把任务落成CANCELLED时立刻停止剩余修复。
3.3 资源上限(超限=拒绝,不是硬扛)
| 键 | 默认 | 越界处置 |
|---|---|---|
max-rows |
20000 | 建任务前按 count(answer) 预检;越界→不落清单,任务 FAILED + RESOURCE_LIMIT + 原因 |
max-pages |
200 | 双保险(页大小被调大时仍兜底);物化途中越界→整笔回滚 + FAILED |
max-tasks-per-run |
20 | 一次调度/一次 ALL 人工触发最多裂变这么多行 |
max-error-items / max-error-chars |
50 / 8000 | 结果明细截断存,计数不丢 |
SH max-pending-extras |
500 | 多余行候选越界→本轮一行都不退役,只报告 |
拒绝建任务会留下一行 FAILED 任务并带稳定码与原因:拒绝必须是可查的事实,
而不是操作人屏幕上闪过的一行字。这一行永远不会被派发(manifest_state=FAILED)。
4. 任务状态机
物化完成 派发 2xx 结果回报
QUEUED ──────────────────────────► QUEUED ────────────► DISPATCHED ────────────┬─► COMPLETED
│ (manifest PENDING→COMPLETE) (lease 起算) ├─► PARTIAL
│ ├─► FAILED
└── 超限/撤权/跨校 → FAILED(带原因,永不派发) └─► CANCELLED
RUNNING ← 消费端进度回报(只续租,不落终态)
七个状态是本任务的验收闭集,语义钉死:
| 状态 | 判据 | 明确不代表 |
|---|---|---|
QUEUED |
清单已完整(或尚未物化),等派发 | — |
DISPATCHED |
对端 2xx 收下指令并落库 | 不代表任何修复;不代表消费方本地数据已对 |
RUNNING |
消费端进度回报刷新过租约 | 不是终态,不推进计数结论 |
COMPLETED |
清单与本地扫描都完整,且无未决差异 | 不代表"修好了很多行"(可能是本来就一致) |
PARTIAL |
跑完了,但仍有只报告项/暂停项/重试耗尽项 | 不等于失败,也不等于成功 |
FAILED |
资源拒绝、物化中断、派发或租约预算耗尽 | 不猜对端状态 |
CANCELLED |
人工取消,或核验时发现撤权/授权纪元进位 | — |
推进规则:只有 result 回报能推终态;attempt 是幂等键的一部分。
- 派发领取与租约续期都是条件更新(
claimForDispatch带state='QUEUED' AND manifest_complete=1 AND (next_dispatch_at IS NULL OR <= now)),两个扫描线程同时看到同一行时只有一个能刷新租约; - 同一消费方存在
DISPATCHED/RUNNING在途任务时不派下一条(单并发); - 租约超期:
attempt < max-dispatch-attempts→ 回QUEUED并按base×2^n(封顶backoff-cap-seconds) 重派,同taskId复用检查点;到上限 →FAILED,留可查记录,不无限重投。 - 消费端收到更大 attempt 的指令时:本地行若已是终态或已停摆,就按新 attempt 重新排队并从水位续读
(只回
duplicate吸掉会让中心一路重派到预算耗尽,最后落成FAILED假象); 若本地仍在跑且租约有效,则只刷新身份与坐标,不打断正在执行的这一轮。 更旧 attempt 一律不动attempt、不动检查点、不动已提交结论。
5. 协议端点与身份纪律
| 方向 | 端点 | 用途 |
|---|---|---|
| FC | GET /oapi/form-sync/tasks/{taskId}/manifest |
读这一轮的不可变清单分页 |
| FC | GET /oapi/form-sync/tasks/{taskId}/answers/{answerUuid}[?expectedRevision=] |
权威当前快照信封,或权威缺席证明 |
| FC | POST /oapi/form-sync/tasks/{taskId}/result |
结果回报((taskId, attempt) 幂等) |
| FC | POST 派发 → SH POST /oapi/form-sync/reconcile |
指令下发(服务端登记地址 + 平台签名) |
| FC 管理面 | /api/form/admin/sync/** |
建任务/列表/详情/结果/取消/单答卷重放/投递失败查看 |
五条不可绕的纪律:
- 身份只来自平台验签。中心读
request.getAttribute(AppAuthFilter.APP_AUTH_ATTR)经ConsumerPrincipalResolver换ConsumerPrincipal;请求体里自报的consumerAppKey只用于 比对是否冒充,与验签身份不符即 403。消费端同理:指令的consumerAppKey与验签来的 appKey 必须都等于本应用消费身份,否则整条指令拒收。 - 每一次清单/快照/结果调用都重新核验:任务存在 → 身份等于任务目标 → 未取消 →
最新 grant 仍生效(
enabled/subscribe/read三条)且纪元未进位。 撤权或纪元进位时中心把任务落成CANCELLED并返回稳定码,消费端据此停止剩余投递与修复—— 绝不因"建任务时有权"永久放行。 - 派发地址只能来自服务端登记表
fc.answer.reconcile.targets.{consumerAppKey}。 指令载荷里没有 callbackUrl 字段,浏览器传入的任何 URL 无从采信;未登记 →TARGET_NOT_REGISTERED,任务退回QUEUED等人工处置,不改数据。 - 缺席证明与权威快照都由中心构造,
formUuid取任务行、formVersion取中心当前答卷行 (不从旧清单猜版本),eventId是服务端 UUID(≤36),proofId≤128。proofId只是可追踪引用,不是身份凭证;消费端严禁把repairAuthoritative直接绑到浏览器可提交的入口上。 - 敏感数据:答卷
values只出现在签名任务端点按授权下发的快照里;错误明细与日志里 只有consumer / task / range / answer / revision,逐字段搬运并截断,绝不整包序列化请求体。
6. 差异分类与修复规则(消费侧)
Worker 每轮从同一水位读两侧一页(中心清单 + AnswerProjectionStore#manifestPage),
取两侧页末键的较小者作本轮边界,比较 (answerUuid, revision, contentHash, isDeleted):
| 方向 | 稳定码 | 处置 |
|---|---|---|
| 中心有、本地无 | MISSING |
取中心当前快照 → repairAuthoritative(env, null, null, null, proofId)(期望三元组全 null=观察到缺失) |
| 本地 revision 更低 | STALE |
取中心当前快照 → 按本地扫描三元组条件更新 |
| 同 revision 异 hash | HASH_CONFLICT |
先记冲突,再取中心当前值走同一权威入口;不走普通 applySnapshot 强盖 |
isDeleted 不符 |
TOMBSTONE |
同一权威修复原语;本地业务扩展列(biz_ext_json/apply_id/flow_guid/link_state)由条件更新排除在写集之外 |
| 本地 revision 更高 | LOCAL_AHEAD |
向中心复核当前值:中心更大→按 STALE 修到新值;否则只报告,禁止降级覆盖 |
| 本地有、清单里没有 | EXTRA_ROW |
只登记候选,见 §7 |
| 修订号为 0 | DIFF_REVISION_ZERO |
只报告(契约要求 revision ≥ 1 才受理修复) |
一致的行不向中心取快照:完整扫描不是第二份全量拉取,只有检出差异才发起一次权威读取。
6.1 RepairResult 四分支
| 返回值 | 含义 | Worker 处置 |
|---|---|---|
APPLIED |
数据面已按中心当前值落定 | 计数 +1;不代表建件或流程成功 |
LOCAL_CHANGED |
本地期望三元组已变化(含条件更新未命中) | 重读本地行与中心当前值后有界重试 |
LOCAL_AHEAD |
本地 revision 更高 | 停止该条,向中心复核,不降级 |
DEFERRED |
新鲜订阅核验不可达/撤权/授权纪元不等 | 暂停修复,本轮按任务授权状态收尾(PARTIAL) |
6.2 并发与回滚(本轮最关键的一条)
projection_version乐观锁冲突、uk_projection_answer并发插入的唯一键异常,都必须使 当笔事务整笔回滚,然后在事务外重读后再有界重试(max-repair-retries,默认 3)。 Worker 的执行循环本身不开事务:领取、检查点落库、单次修复各自是独立短事务, 这样才可能在"已标 rollback-only 的事务"之外续跑。- 缺失插入撞唯一键后重读若已存在,则本轮该条从
MISSING退化为按本地三元组比较,不硬插第二条。 - 运行联调前必须确认
svc-hall/server/sql/add_fc_projection_version.sql已应用(已有库), 或新库直接用含该列的建表脚本。TASK_3 不得绕开这一列。
6.3 禁止事项
对账修复禁止调用 applySnapshot。它仍是普通 MQ 消费入口,保持"同版异 hash 不覆盖"的既有规则,
并可能关联意图/建件/发流程。修复入口只更新中心快照列与修复留证(last_event_id/conflict_detail),
不碰 inbox/intent/apply/flow,不生成 MQ 消费成功回执。
7. 多余行与权威缺席证明
retireAbsent(AuthoritativeAbsenceProof) 的前置条件全部成立才允许调用:
- 中心清单整轮完整结束(
manifestComplete=1且本轮读到completed=true); - 本地图像也扫完(
localCompleted=1); - 该行属于本任务的
(sch_id, form_uuid); - 该行
createTime不晚于任务起点windowStart且非空; 晚于起点=期间新增,为空或归属不确定=不可判,一律只报告; - 中心对同一
answerUuid答复presence=ABSENT且证明的taskId / formUuid / schId / answerUuid / expectedRevision与本地候选逐字一致; - 多余行候选总数未越
max-pending-extras。
expectedRevision 由消费端扫描观察值提供,作用是与 retireAbsent 内部的"版本不符即不退役 + 标冲突"
形成并发护栏;它不是中心对本地版本的断言。
任何跨页失败、撤权、范围改变、候选超限的情况:未见到的行绝不当作"应删除", 已经登记为候选的行也整体不退役(宁缺勿滥),只把差异报回去。
8. 与 TASK_2 服务能力的接触面(只调用)
| 能力 | 中心侧 | 消费侧 |
|---|---|---|
| 权威当前快照 | AnswerSnapshotService#currentAuthorized(caller, consumerSchoolId, answerUuid) |
— |
| 修复入口 | 签发信封 | AnswerProjectionStore#repairAuthoritative(env, expRev, expHash, expDel, proofId) |
| 本地集合扫描 | — | AnswerProjectionStore#manifestPage(schId, formUuid, cursor, size) |
| 缺席处置 | 签发证明 | AnswerProjectionStore#retireAbsent(proof) |
| 投递重放 | AnswerDeliveryService#replayAuthorized(...)(管理面单答卷重放) |
— |
| 投递失败查看 | AnswerDeliveryService#failedPage(...) |
— |
| 授权判据 | FormConsumerAccessService#requireRead/requireSubscribe/listEnabledSubscriptions、FormConsumerGrantRepository |
修复前 verifyGrants 新鲜核验(TASK_2 内部) |
任务书 §59 的停止线本次未触发:没有在 OapiFormService / AnswerRepository /
FcEventPublisher / AnswerProjectionStore 里补快捷查询。摘要取数走本任务新增的
AnswerReconcileReadRepository;哈希复用中心既有实现,未复制规范化代码。
9. 配置键(默认值即交付形态,本任务不改两个应用的 application.yml)
中心 fc.answer.reconcile.*(AnswerReconcileConfiguration.AnswerReconcileProperties):
| 键 | 默认 | 说明 |
|---|---|---|
schedule-enabled |
false | 每日建任务总闸;关时一行任务都不建 |
schedule-cron |
0 0 2 * * ? |
拟配置时点 |
schedule-zone |
Asia/Shanghai |
营业日按此时区取 |
dispatch-enabled |
false | 物化 + 派发 + 租约回收总闸;关时仍可建任务、查结果、手动重放 |
page-size / max-rows / max-pages |
200 / 20000 / 200 | 见 §3.3 |
max-tasks-per-run |
20 | 裂变性上限 |
lease-seconds |
900 | 租约 |
max-dispatch-attempts |
4 | 含首派的派发预算 |
backoff-base-seconds / backoff-cap-seconds |
60 / 3600 | 退避 |
targets.{appKey} |
空 | 必填才能派发:已登记的消费方后台服务地址(含 context-path) |
dispatch-path |
/oapi/form-sync/reconcile |
派发路径 |
source-app-key |
空→回落 yh.app-auth.app-key |
信封 sourceAppKey |
scan-interval-ms / scan-initial-delay-ms |
15000 / 45000 | 中心扫描闹钟 |
消费侧 svchall.answer-reconcile.*:
| 键 | 默认 | 说明 |
|---|---|---|
enabled |
false | 执行总闸(接收落库不受它管——落库只是事实,执行才动数据) |
lease-seconds |
900 | 本地执行租约(超租约的 RUNNING 可被重新领取并从水位续读) |
max-repair-retries |
3 | 单条差异的有界重试预算 |
max-pending-extras |
500 | 多余行候选上限(越界=整轮不退役) |
max-error-items |
50 | 明细上限 |
max-work-per-scan |
1 | 每 tick 执行任务数(单并发) |
progress-report-rounds |
10 | 每 N 轮向中心回报一次 RUNNING 以续租 |
backoff-base-seconds / backoff-cap-seconds |
60 / 1800 | 中断/回报不可达的退避 |
scan-interval-ms / scan-initial-delay-ms |
30000 / 20000 | 消费侧扫描闹钟 |
9.1 部署顺序(硬约束)
- 中心:
sql/answer_reconcile_v1.sql(三张表;已有库先确认表不存在,脚本非幂等); - 消费侧:确认
fc_answer_projection.projection_version已存在 (新库用fc_projection_and_apply_intent.sql,已有库用add_fc_projection_version.sql,二选一), 再执行sql/answer_reconcile_work.sql; - 密钥与白名单:消费方必须已登记进中心
yh.app-auth.trusted-apps(本机为100018), 中心100021必须已登记进消费方trusted-apps—— 否则签名互验 401; - 服务目录/派发地址:中心侧配
fc.answer.reconcile.targets.100018, 消费侧配YH_OPEN_SERVICES_FORMCENTER(yh.open.services.form-center不在本轮可改文件内,需运行时注入); - 先有生效 grant,再开
schedule-enabled/dispatch-enabled。 本轮没有开启任何调度,也没有对真实数据做过派发或修复。
10. 验收证据与未验收项(诚实边界)
已取证的(本机桩级,规则单测):
FcReconcileTaskTest:36 项 —— 调度唯一键幂等、未授权订阅不建任务、资源超限拒绝并留原因、 单并发不叠任务、跨校范围拒绝、裂变上限、清单物化含墓碑且哈希同源自 TASK_2、 物化越界整笔不落、UUID 游标跨页/整页到底/非法游标、清单未完整拒答分页、 信封坐标取中心已核验值(版本来自当前答卷行而非旧清单)、墓碑形态、 缺席证明三前置、期间新增不签证明、撤权/纪元进位即取消、跨消费方读取 403、 已取消任务不再答复、重复回报幂等吸收、旧 attempt 不倒退新 attempt、 假 result(超派发 attempt / 身份不符)拒、COMPLETED与PARTIAL与RUNNING三种推进区别、 回报状态闭集外即拒、明细逐字段规整且有上限、派发 2xx 只标DISPATCHED、 同消费方在途不派下一条、未登记地址不猜 URL、派发预算耗尽即FAILED、 租约超期重派与耗尽落败、重放先重过 ACL 再执行、跨域寻址按不存在、取消幂等。ShReconcileTaskTest:36 项 —— 接收与执行解耦(2xx 只落QUEUED)、三类身份不符拒收、 坐标/起点/attempt 校验、重复指令复用 taskId 与检查点、开关未开或出站未就绪不动数据、 缺失/陈旧/同版错 hash/墓碑四类走权威入口且applySnapshot永不被调、 本地更高不降级(含中心已推进到新值时修到新值)、一致行完全不向中心取快照、 修订号 0 只报告、DEFERRED即暂停、LOCAL_AHEAD分支、LOCAL_CHANGED事务外重读后有界重试、 乐观锁与唯一键竞争整笔回滚后重试且预算耗尽只报告、多余行需完整扫描 + 匹配证明才退役、 证明坐标不符/只报告口径不退役、期间新增与createTime为空永不退役、候选越界整轮不退役、 UUID 游标锁步跨页、空页未到底即停滞且不回报假结果、跨页失败保检查点且不退役、 断线重启从水位续读并计resumed、中心取消即停、中心不可达不写终态、 结果送不到时留QUEUED等重报、旧 attempt 回报被中心判陈旧时不覆盖结论、进度续租不落终态、 错误明细带全四坐标且不含 values、租约有效时新 attempt 只刷新身份不打断在跑的一轮。
门禁命令与退出码原文、首跑 7 红的根因表、以及门禁全绿后自查补的三处缺陷
(ANSWER 范围漏过订阅核验、一致行也向中心取快照、终态行被重派时不重新排队)见
docs/tasks/evidence/task3/gate-commands.md;Surefire 原文同目录。
未取得、留给联调的证据(不得被上述绿灯冒充):
- 真实 MySQL:一致读快照的实际隔离效果、
uk_manifest_row/uk_result_attempt/uk_reconcile_work_task的真冲突、projection_version乐观锁与uk_projection_answer并发插入的整笔回滚、 条件更新在并发下的实际原子性;两个 DDL 脚本本轮未执行; - 真实调度:本轮没有触发过
@Scheduled,02:00 时点与租约超期都是配置形态 + 桩级验证; - 真实签名互调:中心↔消费方的
/oapi/**往返、密钥互登、服务目录解析都未在本机跑通 (本机yh.open.services.form-center需运行时注入); - 前端视觉证据:
form-center/front只到构建级验证;/admin/sync的菜单模块行尚未在 UserBase 登记 (本任务禁改UserBase/**),当前只能以 hash 地址直达; - TASK_2 §9 的五条待修(尤其跨校授权缺"调用方属于该租户域"的事实源)与 TASK_1 的四条审计问题
各自保留。本任务的应对是拒绝建跨校任务(
SCOPE_MISMATCH),不是绕过它。
11. 修订记录
| 日期 | 变更 | 依据 |
|---|---|---|
| 2026-10-21 | v1 落地:任务坐标到表单为止(全量=按订阅裂变)、清单不可变物化 + completed 唯一结束判据、双流锁步水位、缺席证明六前置、RepairResult 四分支与整笔回滚后事务外重读、派发 2xx 与终态严格分离、调度与派发双闸默认关 |
任务书 TASK_3 改动点 1–8 与 TASK_2_PREFIX_CHECK.md 接入清单 |
| 2026-10-21 | 明确不接触 application.yml:全部键以 @ConfigurationProperties/@Value 默认值交付,env 注入生效 |
约束「配置在本任务独立 @Configuration 中,不争用 TASK_2 配置」 |