-- ============================================================ -- 表单中心 · 差异对账任务 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; -- ③不建物理外键——task_uuid 是逻辑引用,清单与结果要能在任务行之后继续存在以支撑复盘; -- ④清单游标与本地扫描游标同为 answer_uuid(32 位小写十六进制),general_ci 下与 binary 序一致。 -- 身份口径:consumer_app_key 存平台 appKey(验签三件套里的 X-App-Key),不是 form.app_code 那种人读 slug。 -- 审计纪律:三张表都没有删除路径(服务层与仓库层都不提供 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(不是 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 '清单结束标记:只有它为 1,"清单里没有"才可能是权威缺席', `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 收下指令的时刻(不代表执行完成,终态只看 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 '回报来源:必须等于已验签身份,自报不符即拒', `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 四坐标,不含 values)', `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 行(值文档与密钥不进对账留痕)