t1~t3落地+10-22精修: 消费授权v3(owner/grant/订阅/目录)+答卷投递v2(可信提交上下文/快照/幂等关联)+答卷对账v1(清单/派发/租约/结果)
build / build-push (push) Successful in 1m26s
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:
@@ -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 行(值文档与密钥不进对账留痕)
|
||||
Reference in New Issue
Block a user