t1~t3落地+10-22精修: 消费授权v3(owner/grant/订阅/目录)+答卷投递v2(可信提交上下文/快照/幂等关联)+答卷对账v1(清单/派发/租约/结果)
build / build-push (push) Successful in 1m26s

含10-22精修六类修复: 续租renewRunning/结果INCOMPLETE_RESULT门/终态TASK_TERMINAL/liveGrant复用ACL/hosted幂等重放绑定/错误明细整条重序列化。
合规审计与证据: 工作区docs/tasks/REFINEMENT_AUDIT.md + evidence/refinement-20261022/(patch+指纹)。SQL未执行、测试未运行(仅-skipTests编译)。
This commit is contained in:
chenxing
2026-10-22 10:02:57 +08:00
parent 4b76d616dd
commit d7473112b0
55 changed files with 10092 additions and 41 deletions
+128
View File
@@ -0,0 +1,128 @@
-- ============================================================
-- 表单中心 · 差异对账任务 DDL(MySQL 5.7 兼容复核版,TASK_3)
-- 范围:answer-reconcile-v1 §2/§3/§5 = 三张新表
-- answer_reconcile_task(任务)/ answer_reconcile_manifest_row(不可变清单)/
-- answer_reconcile_result(结果回报)
-- 依据:任务书 TASK_3 改动点 1/2/4/8;不复用 answer_op_log 当任务表、不动 answer/answer_delivery。
-- 用途:生产 ddl-auto=none 时的升级脚本;开发期(本机 bsch_formcenter,ddl-auto=update)由
-- Hibernate 自动追加,本脚本同时是"实表应当长什么样"的评审权威(沿 v2/v3 先例)。
-- 5.7 复核要点:①无 CTE/WITH、无函数索引、无生成列、无 CHECK 约束;②utf8mb4 显式写 CHARSET/COLLATE;
-- ③<b>不建物理外键</b>——task_uuid 是逻辑引用,清单与结果要能在任务行之后继续存在以支撑复盘;
-- ④清单游标与本地扫描游标同为 answer_uuid(32 位小写十六进制),general_ci 下与 binary 序一致。
-- 身份口径:consumer_app_key 存平台 appKey(验签三件套里的 X-App-Key),不是 form.app_code 那种人读 slug。
-- 审计纪律:三张表都<b>没有删除路径</b>(服务层与仓库层都不提供 delete),终态留表供复盘。
-- 非幂等:MySQL 5.7 无评审友好的 CREATE TABLE IF NOT EXISTS 写法(写了反而掩盖"表已存在但列不对")。
-- 执行前先 `SHOW TABLES LIKE 'answer_reconcile%'` 确认三张表都不存在。
-- ============================================================
CREATE TABLE `answer_reconcile_task` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`task_uuid` VARCHAR(36) NOT NULL COMMENT '任务对外身份(服务端 UUID,≤36 字符;URL 与指令都用它)',
`consumer_app_key` VARCHAR(64) NOT NULL COMMENT '对账目标的消费应用平台 appKey(<b>不是</b> app_code slug)',
`consumer_school_id` BIGINT NOT NULL COMMENT '消费租户域:与投递行同口径,也就是答卷归属校',
`form_uuid` VARCHAR(32) NOT NULL COMMENT '本轮范围=一张表单;全量=按生效订阅 fission 成多行任务',
`form_version` INT NOT NULL COMMENT '物化时核验过的发布版本(缺席答复与修复信封的 formVersion 权威值)',
`trigger_type` VARCHAR(16) NOT NULL COMMENT 'SCHEDULE / MANUAL(列名避开 MySQL 保留字 TRIGGER)',
`operator` VARCHAR(64) DEFAULT NULL COMMENT '人工触发时的操作人(调度产生的为 NULL)',
`idempotency_key` VARCHAR(128) NOT NULL COMMENT '日期+订阅+范围唯一键:调度重复跑不出第二行任务',
`state` VARCHAR(16) NOT NULL DEFAULT 'QUEUED' COMMENT 'QUEUED/DISPATCHED/RUNNING/COMPLETED/PARTIAL/FAILED/CANCELLED',
`manifest_state` VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING/MATERIALIZING/COMPLETE/FAILED:派发只在 COMPLETE 后',
`manifest_complete` TINYINT NOT NULL DEFAULT 0 COMMENT '清单结束标记:<b>只有它为 1</b>,"清单里没有"才可能是权威缺席',
`total_rows` INT NOT NULL DEFAULT 0 COMMENT '物化完成后=清单行数(上限见 fc.answer.reconcile.max-rows)',
`total_pages` INT NOT NULL DEFAULT 0 COMMENT '确定总页(ceil(total_rows/page_size),不是"某页空了")',
`materialized_rows` INT NOT NULL DEFAULT 0 COMMENT '已写入清单行数(进度与断点续作的计数)',
`materialized_pages` INT NOT NULL DEFAULT 0 COMMENT '已物化页数',
`checkpoint_cursor` VARCHAR(32) DEFAULT NULL COMMENT '物化检查点:已写到的最后一个 answer_uuid(重启从这里续)',
`window_start` DATETIME NOT NULL COMMENT '任务起点=一致读事务开始时刻;晚于此刻的本地行不得判为多余行',
`window_end` DATETIME DEFAULT NULL COMMENT '清单物化完成时刻(窗口长度给运维复盘用)',
`grant_version` INT NOT NULL DEFAULT 1 COMMENT '物化时刻的授权纪元;每轮读取再核一次,进位即按撤权处置',
`attempt` INT NOT NULL DEFAULT 0 COMMENT '派发尝试次数(首派 0;租约超期或派发失败后 +1,有上限)',
`lease_until` DATETIME DEFAULT NULL COMMENT '派发/物化租约到期时刻:超期由扫描按 attempt+1 有限退避接手',
`next_dispatch_at` DATETIME DEFAULT NULL COMMENT '退避到点才再派(base×2^n 封顶),耗尽即 FAILED',
`dispatched_at` DATETIME DEFAULT NULL COMMENT '对端 2xx 收下指令的时刻(<b>不代表执行完成</b>,终态只看 result 回报)',
`finished_at` DATETIME DEFAULT NULL COMMENT '终态落定时刻',
`cancel_requested` TINYINT NOT NULL DEFAULT 0 COMMENT '取消请求:置 1 后清单/快照读取一律以取消口径答复,消费端据此停止剩余修复',
`error_code` VARCHAR(32) DEFAULT NULL COMMENT '稳定错误码(GRANT_REVOKED/RESOURCE_LIMIT/MANIFEST_INCOMPLETE/…)',
`error_detail` VARCHAR(500) DEFAULT NULL COMMENT '末次错误摘要(截断存;答卷值文档绝不进这里)',
`create_by` VARCHAR(255) DEFAULT NULL,
`create_time` DATETIME DEFAULT NULL,
`update_by` VARCHAR(255) DEFAULT NULL,
`update_time` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_reconcile_task_uuid` (`task_uuid`),
UNIQUE KEY `uk_reconcile_task_idem` (`idempotency_key`),
KEY `idx_reconcile_dispatch` (`state`, `next_dispatch_at`),
KEY `idx_reconcile_consumer` (`consumer_app_key`, `consumer_school_id`, `id`),
KEY `idx_reconcile_coord` (`consumer_app_key`, `consumer_school_id`, `form_uuid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
COMMENT='差异对账任务(TASK_3 §2:一行=一个 (消费应用, 消费租户域, 表单) 坐标上的一次对账企图;不是投递行、不是答卷、不是 op_log)';
CREATE TABLE `answer_reconcile_manifest_row` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`task_id` BIGINT NOT NULL COMMENT '逻辑引用 answer_reconcile_task.id(不建物理外键)',
`task_uuid` VARCHAR(36) NOT NULL COMMENT '任务身份冗余一份:清单行脱离任务上下文也要能自证是哪一轮',
`form_uuid` VARCHAR(32) NOT NULL COMMENT '物化时核验过的表单坐标',
`form_version` INT NOT NULL COMMENT '该答卷钉住的 schema 版本(= answer.version,不从旧清单猜版本)',
`answer_uuid` VARCHAR(32) NOT NULL COMMENT '答卷 uuid:清单游标与消费端本地游标同为它',
`revision` BIGINT NOT NULL COMMENT '物化时刻的对外修订号(含 0:存量未变更行原样入清单,修复端点会拒绝并只报告)',
`content_hash` CHAR(64) NOT NULL COMMENT 'fc.canon.v1 摘要(与投递行同一条规范化实现,本表不复制算法)',
`is_deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '墓碑也进清单:"缺席"与"存在但已删"是两个不同结论',
`seq_no` INT NOT NULL COMMENT '物化写入序号(审计与断点续作的序号锚;分页游标不是它)',
`create_by` VARCHAR(255) DEFAULT NULL,
`create_time` DATETIME DEFAULT NULL,
`update_by` VARCHAR(255) DEFAULT NULL,
`update_time` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_manifest_row` (`task_id`, `answer_uuid`),
KEY `idx_manifest_page` (`task_id`, `answer_uuid`),
KEY `idx_manifest_seq` (`task_id`, `seq_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
COMMENT='任务不可变清单(TASK_3 §3:一致读事务内物化四元摘要含墓碑;此后分页只读它,全列 updatable=false,无 update/delete 方法)';
CREATE TABLE `answer_reconcile_result` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`task_id` BIGINT NOT NULL COMMENT '逻辑引用 answer_reconcile_task.id',
`task_uuid` VARCHAR(36) NOT NULL COMMENT '任务身份(回报定位用)',
`consumer_app_key` VARCHAR(64) NOT NULL COMMENT '回报来源:<b>必须等于已验签身份</b>,自报不符即拒',
`consumer_school_id` BIGINT NOT NULL COMMENT '消费租户域(跨校回报的判据之一)',
`form_uuid` VARCHAR(32) NOT NULL COMMENT '范围坐标(每条错误与计数都能定位到这里)',
`attempt` INT NOT NULL COMMENT '对应的派发尝试;比最新 attempt 更旧的回报不推进状态(不倒退新 attempt)',
`state` VARCHAR(16) NOT NULL COMMENT 'COMPLETED / PARTIAL / FAILED / CANCELLED / RUNNING(进度回报)',
`local_scan_complete` TINYINT NOT NULL DEFAULT 0 COMMENT '消费端本地扫描是否整轮完成(多余行判定的前提之一)',
`manifest_complete` TINYINT NOT NULL DEFAULT 0 COMMENT '消费端实际读到的中心结束标记(与中心不一致则本轮结论不成立)',
`scanned_rows` INT NOT NULL DEFAULT 0 COMMENT '比较过的答卷数',
`pages_processed` INT NOT NULL DEFAULT 0 COMMENT '处理页数',
`missing_repaired` INT NOT NULL DEFAULT 0 COMMENT '缺失修复数(repairAuthoritative=APPLIED,期望三元组全 null)',
`stale_repaired` INT NOT NULL DEFAULT 0 COMMENT '陈旧修复数(本地 revision 更低)',
`hash_conflicts` INT NOT NULL DEFAULT 0 COMMENT '同 revision 异 hash:记录后按中心当前值修复',
`tombstones_repaired` INT NOT NULL DEFAULT 0 COMMENT '墓碑修复数(同一权威修复原语,保留本地业务扩展)',
`local_ahead` INT NOT NULL DEFAULT 0 COMMENT '本地 revision 更高:只向中心复核,禁止降级覆盖',
`deferred` INT NOT NULL DEFAULT 0 COMMENT '授权核验不可达/撤权/纪元不等而暂停的条目',
`extra_reported` INT NOT NULL DEFAULT 0 COMMENT '只报告的多余行(含期间新增、createTime 未知、范围不明)',
`extra_retired` INT NOT NULL DEFAULT 0 COMMENT '经 FC 签名缺席证明实际退役的行数(无证明即不增)',
`error_count` INT NOT NULL DEFAULT 0 COMMENT '错误条目总数(超上限会截断,明细以 errors_json 为准)',
`errors_json` MEDIUMTEXT NOT NULL COMMENT '错误明细(有上限、截断存;每条含 consumer/task/range/answer 四坐标,<b>不含 values</b>)',
`resumed` TINYINT NOT NULL DEFAULT 0 COMMENT '1=进程重启后从检查点续跑的同一 attempt 再报',
`reported_at` DATETIME NOT NULL COMMENT '中心受理该回报的时刻',
`create_by` VARCHAR(255) DEFAULT NULL,
`create_time` DATETIME DEFAULT NULL,
`update_by` VARCHAR(255) DEFAULT NULL,
`update_time` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_result_attempt` (`task_id`, `attempt`),
KEY `idx_result_task` (`task_id`, `attempt`),
KEY `idx_result_consumer` (`consumer_app_key`, `consumer_school_id`, `id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
COMMENT='对账结果回报(TASK_3 §5:(task_id, attempt) 唯一=幂等锚点;一 attempt 一行不覆盖,APPLIED 类计数不代表建件或流程成功)';
-- 自验(执行后逐条核对):
-- SHOW CREATE TABLE answer_reconcile_task\G -- 应含 uk_reconcile_task_idem(同日同订阅同范围只一行)
-- SHOW CREATE TABLE answer_reconcile_manifest_row\G -- 应含 uk_manifest_row(同任务同答卷不重复入清单)
-- SHOW CREATE TABLE answer_reconcile_result\G -- 应含 uk_result_attempt(重复回报幂等吸收的库级形态)
-- SELECT COUNT(*) FROM information_schema.referential_constraints
-- WHERE table_name IN ('answer_reconcile_task','answer_reconcile_manifest_row','answer_reconcile_result');
-- -- 必须 0 行(无物理外键)
-- SELECT COUNT(*) FROM information_schema.columns
-- WHERE table_schema=DATABASE() AND table_name='answer_reconcile_result'
-- AND column_name IN ('values_json','snapshot_json','secret','token');
-- -- 必须 0 行(值文档与密钥不进对账留痕)