Files
msgexchange-v2/docs/flight-state.md
T
windyboy 4449ee5dcc docs(flight-state): 按 2026-09-09 二轮评审修正设计文档,开放项锚定 ACM2-31 (ACM2-31)
依据 ACM2-31 二轮外部评审逐条修正,待协议确认项已对照 SIS_AODB_RMS V0.1 佐证:

- 标注约定:区分规范要求与现状/未决披露,杜绝混作依据
- 航班身份:FLID≠航班号,FDEL+ADFT 换名时 FLID 可能不同(规范 §3.18),代码共享独立事件经 MAID/TAID 关联
- FDAY 三日期概念区分(业务日期/快照覆盖日期/名单归属日期);跨日归属回迁列为未决(§3.3 约束 4)
- 规则 M 补成立条件标注(快照生成时点 + 投递顺序前提未逐条核实)
- 差删完整性前提引协议口径:单包不分段、RECS 记录数、完整快照注释(§3.16.1)
- 动态线集合全量/增量语义待逐类确认(CHOT CSNO 实为机位序号 §3.22.2),Replace 全集仅对快照线定案
- tombstone 澄清为自定义删除事件(非 Kafka 原生墓碑),删除版本/世代缺口开放
- 行锁表述纠正(跨实例同样生效,但不实现认领/选主/切换/FIFO);Oracle 空串=NULL 为既定行为,验证目标改为字段语义保真
- ACTT 实际时间口径(进港 ATA/出港 ATD §3.20),修正示例字段名;FLOP 29 类改为规范 §3.19–3.43 实数;99 上限标注协议出处
- 语言修订:主泵/数据一致性比对/JSON 数组/整态替换措辞等;错误矩阵每类错误单一默认路径
- 开放项(回迁防护/动态集合语义/删除世代/幂等键/FIFO 参数/一致性读/TAOP 语义核对等)归集 ACM2-31
2026-09-09 10:47:03 +08:00

419 lines
48 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 航班运行数据接入与当前状态管理设计
本文是航班状态接入与当前态存储的唯一设计与架构规范文档:统一界定系统职责、存储模型、字段语义、状态流转与跨模块不变量。已定决策与演进红线以正文为准;未完成项与开放风险在各节显式标注。
**标注约定**:正文未加标注的段落是规范要求;凡标注「开放项 / 未决 / 已知偏差 / 待补 / 验收目标 / 现状」的段落,是对当前实现或未定设计的如实披露,不是已交付事实,两类内容不得混作依据。
## 阅读速览:专用数据与设计逻辑
### 专用数据与术语
| 数据 / 术语 | 含义与用途 |
|---|---|
| SIS / AODB | SIS 是 AODB(机场运行数据库,上游业务权威)向各子系统分发数据的接口规范(SIS_AODB_RMS V0.1);本系统从共享 MySQL 信箱接收 AODB 报文。共享信箱是接入边界,本地数据库是本模块对外提供当前状态的唯一读取源,不是高于 AODB 的业务权威。 |
| `FLID` | AODB 分配的航班唯一 IDNumber(1-12),规范 §3.16.2);主表一行对应一个 `FLID`,明细、状态事件及删除通知均按它归属航班。`FLID` ≠ 航班号(`FLNO` 是标量列):关键键字段(航司代码、航班号、航班日期、航班时间、进离港标识)变更时,AODB 以「删除事件 FDEL + 新航班事件 ADFT」重新指名,两个事件的 `FLID` 可能不同(规范 §3.18);主航班与代码共享航班各有 `FLID`、事件分别发送,经 `MAID`/`TAID` 关联。 |
| `FLIGHT_SCHD` + 9 张明细表 | 一个航班的完整权威态:主表保存标量、异常对象前缀列及文本字段;明细表保存 10 类变长集合,其中 ROUT/ERUT 共用路线表。展示视图只是只读投影。 |
| `FDAY`(名单归属运营日) | 当前负责该航班成员管理与差删的日计划快照所覆盖的运营日(三个日期概念中的「名单归属日期」,区分见 §3.2),不是报文接收日或落库日,也不承诺等同于业务运营日。`NULL` 表示尚未被任何日计划代收录,不表示航班没有运行日;跨日归属由整包快照更新,动态事件不修改。 |
| `SCHD_GEN` / `SCHD_GEN_FLID` | 每个运营日的快照账本与当前成员名单。成员资格以名单为准;差删先比较新旧名单,再用当前 `FDAY` 防止误删已迁往其他运营日的航班。 |
| `VERSION` / `STATE_VERSION` | 前者是同一运营日的快照版本,每次新快照推进;后者是单个航班的状态版本。运营日、快照版本、航班状态版本是不同维度。 |
| `identity_key` / `LAST_MESSAGE_ID` | 前者按“发送方\|类型\|子类型\|序号”识别重复业务消息;后者追踪最近一次写入消息,其中快照账本的该字段用于最近一条快照的重放短路,不能拦住任意历史快照。 |
| `ORDINAL` / `SOURCE_SEQ` | 前者是明细输入顺序(从 1 起),后者保存报文中的源序号。源序号可空、不唯一,资源号也不是去重依据,不能据此把多条分配记录合并为一条。 |
| SCHDDNLD / RESP / ADFT | 日计划相关报文。DNLD、RESP 按整包快照换代并差删;ADFT 是单航班临时增量,不换代、不差删、不完成请求。当前仅 DNLD 接入,RESP/ADFT 及请求闭环仍有未决项。 |
| FLOP / GTDT | FLOP 是字段级航班运行事件;规范 §3.19–3.43 定义了 25 类航班级事件(AODB→RMS 事件总计 44 类,§3.13.44),GTDT(§3.34)是其中的登机门更新。当前仅接入 GTDT;动态事件只表达所携带字段的变化。 |
| `REQ_TRACK` / `COUTMSGS` | 前者登记请求及其应答、超时状态,后者承担请求出站 outbox;出站发送与超时重发尚未实现。 |
| `PROC_STATE` / `BACKFILL_TODO` | 前者记录消息处理状态,后者记录共享信箱回填补偿。回填意图与业务终态同事务持久化仍是待补齐目标。 |
| `KAFKA_SCHD` / `KAFKA_MSG` / tombstone | 分别表示航班整态投影、变更通知、删除通知。整态投递可合并同航班的中间版本,消费端按整态替换处理、不得按补丁合并。本文 tombstone 指自定义删除事件:载荷仅含 `FLID``DELETED` 标记、value 非 null**不是** Kafka 日志压缩语义的原生墓碑消息,Kafka 不会对其执行压缩删除。 |
集合与异常、文本字段的逐项映射见第 6 节;表结构职责与代际记账见第 3 节;一个运营日的数据全景、历史版本不留存决策与跨运营日清理(开放项)见 §3.5/§3.6。
### 设计逻辑
- **提交**:单活动主泵(即消息处理主循环,下同)按 FIFO 推进;航班状态、快照账本、outbox 与处理终态同事务提交,回填和投递在提交后独立重试。
- **合并**:快照与动态事件均保留未出现的字段,显式清除才清空;同日快照覆盖其携带字段的已有值。集合按整组替换,保留全部条目及顺序。
- **记账**`SCHD_GEN` 每运营日一行,记录最近成功提交的快照版本和消息 ID;`SCHD_GEN_FLID` 每运营日保存该版快照的完整航班名单,新版提交时替换旧名单。
- **差删**:从旧名单中找出新快照不再包含的航班,仅删除当前 `FLIGHT_SCHD.FDAY` 仍等于该运营日的记录。未入代航班和已迁往其他日的航班保留,见 §3.3 示例。
- **投递**`KAFKA_SCHD` 按航班聚合为最新整态,删除发 tombstone;`KAFKA_MSG` 仅通知变化,两个主题不保证先后顺序。
## 1. 系统定位与核心不变量
本系统作为消息交换与航班运行状态汇聚核心:从共享 MySQL 信箱接入 SIS(AODB)报文,按报文语义把报文合并进本地权威的"航班当前状态",再把状态变化以标准化领域事件发往 Kafka。
系统坚持三条架构底线:
- **单一权威状态(Single Source of Truth)。** 本地数据库是本模块对外提供当前状态的唯一读取源;共享 MySQL 信箱与 Kafka 均为本地提交之后的外围边界,各自独立重试,任何外围故障都不会让已提交的状态回滚或重复计算。它不是高于 AODB 的业务权威——上游业务事实以 AODB 为源,本模块是其在本系统范围内的当前态投影。
- **单活动主泵(Single Active Pump)。** 生产部署约束为同一时刻仅一个实例执行信箱消费与状态推进。事务行锁可以串行化所有遵守该锁协议的数据库事务(跨进程、跨实例同样生效),但行锁本身不能实现消息认领、主实例选举、故障切换或全链路 FIFO——跨实例安全由「单活动实例」部署约束保证,而非行锁。
- **单提交事务(Single Commit Transaction)。** 状态、outbox 事件、处理终态(`PROC_STATE`)在同一个本地事务内落库,全部成功或整体回滚。
**职责边界。** 本模块只同步上游报文并维护当前态,不推导业务状态:取消、延误、备降、返航等运行状态由上游报文给定(对应 FDEL/CNCL/FDIV/FRET 等事件,规范 §3.27–3.33),本模块不做状态机推导;登机、登机结束等旅客服务状态不在接入范围。实际时间取 SIS §3.20 口径——进港为 ATA、出港为 ATD 的单值时间(本地列 `ACTT`),不区分 A-CDM 建模的落地/入位/离位/起飞等多节点;进出港过站关联按到港记录携带的 TAOP/TAFL/TAID 原样保存(见 §3.2)。
处理按信箱 FIFO 严格有序推进,按业务身份幂等去重,失败按指数退避重试。
数据库口径:现场目标库为 Oracle 11g;当前实际接通并作为验证基准的只有 PostgreSQL,本文描述 PostgreSQL 实现细节。
## 2. 关键架构决策
| 架构决策 | 理由与边界 |
|---|---|
| 状态、快照代、处理终态与 outbox 同库提交 | 消除 Redis 状态与数据库终态跨库双写窗口;Redis 等缓存不参与动态状态权威。 |
| 当前验证基准为 PostgreSQL,现场目标库为 Oracle 11g | 单次部署只绑定一个内部权威库;Oracle 尚未完成全链路适配,不可作为运行时可用配置开关。 |
| 航班标量主表 + 9 张关系明细表 | 完整保留 10 类变长集合的输入顺序、源序号及多条资源/操作记录,不按展示槽位截断。 |
| 异常对象保留主表前缀列;SRVT/VIPF/MAFL 保留文本列 | 既有载体可承载时不新增抽象或关系表;其读写语义需以回归证明。 |
| 单活动主泵,事务内 `PIPELINE_LOCK` 行锁 | 行锁串行化遵守锁协议的数据库事务(跨实例同样生效),但不实现消息认领、FIFO 或跨实例协调;跨实例安全依赖单活动实例部署约束。 |
| 外部回填与事件投递按幂等重试处理 | 不使用跨库分布式事务;回填意图与业务终态同事务持久化是待补齐目标(见第 8 节)。 |
| 展示视图仅为只读投影 | 仅展示前序资源(前两个登机门/柜台等),不作为全量权威状态或数据一致性比对输入。 |
**废弃方案与架构约束。** 系统明确放弃:固定 GATE1/GATE2 式槽位作为权威、"超界数据静默丢弃"、"零子表为架构约束"、"存储模型决定零方言迁移成本"、以及脱离全量备份前提的 RPO=0 承诺。历史决策论证与撤销记录见 [决策历史](legacy/decision-flight-state-history.md)。
## 3. 存储:标量主表 + 9 张明细表
### 3.1 为何不采用固定槽位
航班资源本质是变长集合,数量因航班、机型与保障安排而异。SIS 规范 §2 资源限制表给出协议上限:登机门、值机柜台、行李转盘每航班各 99,计划机位 9、当前机位 1,实际机位移动数量不限;规范未给出数量下限,本设计不做数量假设。ACM2-29 曾采用"全标量宽表、零子表"GATE1/GATE2 式固定槽位),当日即被推翻——槽位会截断数据,而设计约束要求保留重复资源的全部属性、输入顺序与源序号。结论:标量进主表,集合进明细表。
### 3.2 表清单
| 表 | 职责 |
|---|---|
| `FLIGHT_SCHD` | 主表,一行一 `FLID`:标量字段、异常前缀列、`SRVT/VIPF/MAFL_TEXT``FDAY``STATE_VERSION``LAST_MESSAGE_ID` 与时间戳。 |
| 8 张集合明细表 | `flight_gate`/`flight_checkin`/`flight_belt`/`flight_stand_plan`/`flight_chute`/`flight_delay`/`flight_bridge_op`/`flight_chock_op`;当前主键 `(FLID, ORDINAL)`。 |
| `FLIGHT_ROUTE_POINT` | 第 9 张明细表;ROUT/ERUT 两类路线共用,以 `ROUTE_KIND` 区分。 |
| `SCHD_GEN` / `SCHD_GEN_FLID` | 快照代际记账,见 3.3。 |
| `PIPELINE_LOCK` | 单写者行锁:每个处理事务先锁 `FLIGHT_SCHD_WRITER` 行;缺失时两事务并发读写同一航班会互相覆盖。 |
| `BACKFILL_TODO` | 回填补偿待办,见第 8 节。 |
| `FLIGHT_SCHD_DISPLAY` | PG 展示视图(前两个登机门/柜台、首个转盘/延误、资源数);仅为投影,非状态来源。 |
9 张明细表承载 10 类集合——路线表一张覆盖两类。
**`FDAY` 的语义。** 涉及「日期」的三个概念必须区分:**航班业务日期**(该航班实例业务上归属哪一天、以什么口径确定——模型不保存,ADFT/动态新增行 `FDAY=NULL` 正是因为模型不为未上计划的行保存业务日期)、**快照覆盖日期**(本份快照查询/发布的运营日 D)、**名单归属日期**(当前由哪一天的快照负责其成员管理与差删)。`FLIGHT_SCHD.FDAY` 保存的是第三者,由整包快照赋值,与接收日、落库日无关。三者在本协议下通常同值,但那是快照接管规则的结果,不是定义使然。动态事件保留 `FDAY`;动态新增或 ADFT 新增航班尚未被收录时为 `NULL`,后续被快照收录再赋值。`NULL` 不表示航班没有运行日,其清理规则见第 7 节。
**过站关联与报文排序(协议事实)。** 到港记录携带 TAOP/TAFL/TAID——离开当前机场的过站连接航班的承运人代码/航班号/AODB ID,仅到港有效(§3.16.2);SCHD 报文内离港航班先于到港航班、代码共享航班跟在主航班之后(§3.16.1 排序注释)。注意:V1.1 迁移注释把 TAOP/TAFL/TAID 标注为「实际承运」,与规范的「过站连接」语义不符,待核对修正(ACM2-31)。
### 3.3 快照记账:每运营日一份版本记录、一份成员名单
**记账单位是运营日,同一天可以多次更新。** 例如 9 月 9 日的日计划先后收到三份快照,对应同一运营日的版本 1、2、3。“换代”指替换该日的当前版本及名单,不是进入下一个自然日。两张账本表记录当前版,不保存各历史版本的完整航班状态。
#### 三份数据各记什么
| 数据 | 记录粒度 | 内容 | 用途 |
|---|---|---|---|
| `SCHD_GEN` | 每运营日一行 | `FDAY``VERSION``LAST_MESSAGE_ID` | 记录该日最近成功提交的快照版本及消息;版本用于 CAS,消息 ID 用于最近一条快照的重放判断。 |
| `SCHD_GEN_FLID` | 每运营日、每成员航班一条记录 | 运营日与 `FLID` 的对应关系 | 保存该日当前版快照包含的完整名单,作为下一次比较的旧名单;新版提交时整体替换。 |
| `FLIGHT_SCHD` | 每 `FLID` 一行 | 航班当前状态及 `FDAY` | 保存快照与动态事件合并后的状态;`FDAY` 表示当前归属,差删时据此排除已跨日迁移的航班。 |
`SCHD_GEN.VERSION` 是日计划快照版本;`FLIGHT_SCHD.STATE_VERSION` 是单航班状态版本。动态事件可以推进后者,但不更新快照版本或成员名单。
#### 一份新快照如何更新账本
设快照覆盖运营日为 D,消息 ID 为 M,航班名单为 N。以下动作与航班状态、outbox、处理终态在同一事务内完成:
1. 锁内读取 D 的账本和旧名单 O。首次快照没有旧名单,按空集比较,成功提交后版本为 1。
2. 若 M 等于账本的 `LAST_MESSAGE_ID`,按最近快照重放短路:不改航班状态、不替换名单、不推进版本、不新增事件;处理成功并执行提交后回填。
3. 计算候选差删名单 `O N`。逐项检查当前航班主行,只保留 `FLIGHT_SCHD.FDAY = D` 的航班作为实际删除对象。
4. 合并写入 N 中各航班的状态,将它们的 `FDAY` 设为 D;删除实际差删对象的主行、全部明细,并登记 tombstone。
5. 将 D 的 `SCHD_GEN_FLID` 名单整体替换为 N;以旧版本为条件执行 CAS,将 `VERSION` 加 1,并把 `LAST_MESSAGE_ID` 更新为 M。版本不符则整个事务回滚。
6. 登记状态事件和处理成功状态,提交。任一步失败,旧版本、旧名单和旧航班状态一并保留,不产生半份快照。
因此,版本计数对应**成功提交的新快照**,不是接收次数;失败重试和最近快照重放不额外加版本。
#### 示例:同日替换与跨日保护
下表以 A、B、C 表示三个 `FLID`,日期均为 2026 年。
| 顺序 | 输入 | `SCHD_GEN` 提交后 | `SCHD_GEN_FLID` 提交后 | 航班状态变化 |
|---|---|---|---|---|
| 1 | M19/9 快照 `{A, B}` | 9/9:版本 1,消息 M1 | 9/9`{A, B}` | A、B 写入,`FDAY=9/9`。 |
| 2 | M29/9 快照 `{A, C}` | 9/9:版本 2,消息 M2 | 9/9`{A, C}` | 候选差删 `{B}`;B 仍属 9/9,删除 B 并登记 tombstoneA 更新,C 新增。 |
| 3 | M39/10 快照 `{C}` | 9/10:版本 1,消息 M39/9 不变 | 9/10`{C}`9/9 仍为 `{A, C}` | C 被 9/10 快照收录,主行 `FDAY` 改为 9/10。 |
| 4 | M49/9 快照 `{A}` | 9/9:版本 3,消息 M4 | 9/9`{A}`9/10 仍为 `{C}` | 候选差删 `{C}`;C 当前属 9/10,保留主行和明细,不发删除事件。 |
| 5 | M4 再次进入快照重放检查 | 不变 | 不变 | 命中 9/9 的最近消息 ID,状态和事件不重复写入。 |
第 3 步说明两者为何都需要:**旧日名单记着上次快照包含 C,主行 `FDAY` 记着 C 已归属新日。** 名单负责找出消失者,主行归属负责判断能否删除。仅凭旧名单会误删跨日航班;仅扫描主行 `FDAY` 则丢失了“上次快照包含谁”的依据。
**重放的三种去向。** 同一条报文重新到达有三种情形,去重与重放守卫各管一层:
- **入口去重**:同一消息首次处理时已绑定 `identity_key`(见 4.1),再次从信箱到达按重复 `SKIPPED`
- **同一消息重试**:事务内失败退回重试时,消息未提交过,正常走全流程;提交成功后因入口去重不会重算。
- **人工重放**:历史快照报文(其消息 ID ≠ 当前 `LAST_MESSAGE_ID`)不会命中重放短路;若已清空入口身份记录而被人工重放,本设计不承诺旧状态不被覆盖——人工重放须携带明确的版本/业务日期策略方可放行(见第 7 节)。
需明确的实现约束:
1. **明细表不声明外键。** 清理动作由仓储显式执行,不依赖数据库级联。`SOURCE_SEQ` 可空且不唯一,仅为记录性质——逐条定位更新的 Apply 协议本期不实现(无生产调用方,且定位键不可靠)。
2. **差删按代圈定范围,不扫 `FDAY`。** 目标运营日 = 本份快照覆盖的运营日。最终删除集合 =(旧名单 − 新名单)∩ 当前仍 `FDAY`=目标运营日的航班;仅对该集合显式删除明细行与主行。`FDAY=NULL`、已归属其他运营日、未在旧名单中的航班均不受本次差删影响;删除与名单替换同事务提交。
3. **演进纪律。** 不为尚无消费者的查询新增类型化时间列、摘要、额外载荷表或索引;时间类字段先保留原始字符串,字段与长度以已应用迁移为准。
4. **跨日归属回迁(未决)。** 上述流程把新名单 N 中各航班的 `FDAY` 一律置为 D:若某航班已被更晚运营日的快照接管(`FDAY=D'`),一份迟到的旧日快照再次收录它时会把 `FDAY` 改回 D,并以旧值覆盖新日的合并态(示例:C 已属 9/10,迟到的 9/9 快照仍含 C → C 被改回 9/9)。现行「跨日保护」(约束 2)只拦截差删删除,不拦截归属回写与字段回退。归属迁移的时序判断是开放项(ACM2-31);定案前本设计不承诺 `FDAY` 单向迁移。
### 3.4 已知待修
路线表当前主键为 `(FLID, ORDINAL)`:同一航班 ROUT 与 ERUT 各自从序号 1 起写,二者共存即冲突。计划通过新迁移改为 `(FLID, ROUTE_KIND, ORDINAL)`(见 ACM2-30)。
### 3.5 一个运营日的数据全景
运营日 D 的快照提交后,库内构成如下整体视图:
| 数据 | 当天形态 |
|---|---|
| `SCHD_GEN` | 一行:D 的最新快照版本与 `LAST_MESSAGE_ID`,同日内被各次快照顺序覆盖。 |
| `SCHD_GEN_FLID` | 当前版快照的完整名单 N,每次新快照整体替换。 |
| `FLIGHT_SCHD` | 被 D 收录的航班各一行(`FDAY=D`),入库后可继续被动态事件推进;跨日被新快照收录时 `FDAY` 改写。 |
| 9 张明细表 | 各 `FDAY=D` 航班的集合行,随每次写入整组替换。 |
| tombstone / outbox | 差删航班与整态投递事件,与上述状态同事务落账。 |
**历史版本不留存(已定决策;下列为四项独立决策,不可互相推论)。****不保存状态历史**:账本只记当前版,同日快照版本 1→2→3 的中间完整状态不保存;② **不逐条投递中间版本**`KAFKA_SCHD` 只按当前整态输出;③ **不提供历史查询**:无此接口;④ **不保存原始报文**:由共享信箱侧保留策略决定,不是本模块承诺。①② 的理由:权威态唯一,差删与 CAS 只依赖当前名单与版本,消费端按 `(FLID, STATE_VERSION, UPDATED_AT)` 去重也不依赖中间版本。后果:当日被覆盖的中间态不可从库中恢复;「可凭重放原始报文重建」依赖信箱保留期限、处理顺序与解析器版本,只是理论可能性,不作为恢复承诺。需要中间态历史线或审计留存时须另行设计,本设计不承担。
### 3.6 跨运营日生命周期
- 差删是历史航班出场的唯一常规路径:旧名单 − 新名单 ∩ 仍属本日的航班,同事务删除主行、明细并登记 tombstone。
- 被新运营日快照接管的航班,其集合经整组替换,仅保留最新值、不保存被替换记录(同 3.5 决策)。
- `FDAY=NULL` 未归属行的清理见第 7 节;已归属历史日的数据没有对应的清理规则。
- **开放项(未决)**:旧运营日的 `SCHD_GEN` / `SCHD_GEN_FLID` 行随日期无限累积,当前无定期清理或归档设计;快照长期停发时历史日留存航班也无兜底清理。清理策略须待真实保留期限需求定案后补齐,清理动作须经 `FLIGHT_SCHD` 与账本的一致性校验,不得破坏差删依赖的"旧名单 + 主行 `FDAY`"双依据。跨日归属回迁的时序防护同样未决(§3.3 约束 4)。
## 4. 状态的两条来路
状态仅有两条来源,各成一条处理线:
- **请求/下载线(基准)。** 建立**指定运营日**航班的基准全量。本系统向 AODB 发送日计划请求,AODB 返回 SCHD 报文——该运营日的全部航班——快照整体落库;该运营日未再出现的航班被差删。
- **动态线(运行事件)。** 跟踪运行中的持续变化——实际时间、登机门开关、值机柜台、行李转盘、延误、靠撤桥、轮挡等。FLOP 字段级事件在规范 §3.193.43 共 25 类(AODB→RMS 事件总计 44 类),当前仅接入 GTDT 一类;单条报文只变更一个航班的一类资源,其余字段不受影响。
两条线共用同一入口与同一套事务/失败规则:进入事务后统一为"锁内读态 → 合并 → 写库 → 事件入 outbox → 终态 → 提交 → 回填"。
```text
信箱队头 → 解码 → 绑定身份(去重)→ 按 Handler 路由
├─ SCHD-DNLD(已接通)────────→ 请求/下载线
├─ SCHD-RESP / ADFT(未接通)─→ 请求/下载线
└─ FLOP 事件(已接 GTDT)────→ 动态线
```
图中「已接通/未接通」为适用范围标注,不是完成度承诺:「已接通」仅表示该类型已进入本设计的路由与事务链,解析、出站与应答闭环等环节是否齐备以 §4.2 各处未决标注为准。
### 4.1 公共入口(所有消息必经)
1. **排队。** 信箱消息与定时任务共用一个 FIFO 队列,主泵只处理队头;队头失败按退避等待,重试超限或队头滞留超过时限 → `DEAD`。一条异常报文会阻塞其后所有航班与定时任务:最大阻塞时限、隔离/旁路规则与告警阈值是运维未决参数(ACM2-31)。
2. **解码。** 报文非法 → `DEAD`(不重试);解码器缺陷类错误 → `FAILED`(退避重试)。
3. **绑定身份。** 幂等键 = `发送方|类型|子类型|序号`,仅首次处理时绑定;该键已被其他消息占用 → `SKIPPED`(重复),自身照常回填共享信箱。协议中 SEQN 为发送方设置的 6 位序号(1–999999,规范 §2 元数据),其唯一范围与重置/回绕策略规范未定义;上游序号重置、重启后重复,以及同身份不同载荷时的处理均为未决项,当前实现仅按键判重(ACM2-31)。
4. **路由。** 按报文类型定位 Handler;无注册 Handler → `FAILED`(可重放,不写终态)。
### 4.2 请求/下载线:基准全量的建立
SIS 循环:本系统向 AODB 发送日计划请求 → AODB 返回 SCHD。三个子类型进入本流程时的规则:
| 子类型 | `FDAY` 来源 | 是否换代 | 是否参与差删 | 可完成请求 |
|---|---|---|---|---|
| **DNLD** | 快照覆盖的运营日 | 是 | 是(整包成员差) | 是(运营日匹配时) |
| **RESP** | 快照覆盖的运营日 | 是 | 是(整包成员差) | 是(且要求对应 `REQ_TRACK` 存在) |
| **ADFT** | 无(保持/置为 NULL | 否 | 否 | 否 |
ADFT 为单条临时航班增量:只增改本条 `FLID`(整行整集合写入),不触发换代与整包差删,也不算完成任何请求。ADFT 报文不携带运营日,本行也不单独承载它:`FDAY=NULL` 仅表示该航班未被任何日计划代收录(见 §3.2)——临时航班在现实中有运行日,但模型不为未上计划的行保存该日期。同 `FLID` 后续被整包快照收录时由快照接管(见 §3.2 `FDAY` 语义)。
请求登记于 `REQ_TRACK`(同类并发=1,新请求把旧请求强制置 `EXPIRED`),出站经 `COUTMSGS` outbox——出站发送与超时重发尚未实现。
收到下载后的落库步骤:
1. 解析整包并校验,单包上限 10000 个航班(超限 → `DEAD`;协议口径:SCHD 单包不分段、不超过 CIIMS 10MB 上限,`RECS` 为 1–4 位记录数,§3.16.1)。**差删的完整性前提**:「本包 = 该运营日完整名单」由协议保证——规范注释明确日计划是「AODB 最新数据的完整快照」(§3.16.1 注释 2)、单包不分段、`RECS` 给出包内记录数(`RECS=0` 即空名单);`RECS` 与实收航班数的核对、同包重复 `FLID` 的处理为待补校验(ACM2-31)。
2. 进入事务(先锁 `PIPELINE_LOCK` 行):
- **重放短路**:该消息已提交过(与 `SCHD_GEN.LAST_MESSAGE_ID` 相同,记账见 3.3)→ 直接 `SUCCEEDED`,不加版本、不发事件。
- **算成员差**:新快照相对上代消失的航班进入差删名单。
- **合并写入**:锁内逐航班读当前态 → 合并 → 整集合替换,`FDAY` 置为该快照覆盖的运营日(不是报文接收或落库日期)。
- **差删**:删除差删名单内航班的明细与主行(规则见 3.3);每个被删航班同事务登记一条 tombstone 事件入 outbox(载荷见第 8 节)。
- **CAS 推进日代版本**:版本不符则整体回滚 → `FAILED(INFRA)` 退避。
- **应答匹配**:与 `REQ_TRACK` 同事务联动——关联键 =(运营日, 发送方, 请求类型),完成该组合下最新一条 PENDING 请求(协议无请求关联号:RQFD 请求仅含时间范围筛选字段,RESP 也不回指请求报文,此关联键是规范约束下的替代方案,与「同类并发=1、新请求强制 EXPIRED 旧请求」共同收敛错配风险);迟到(已 `EXPIRED`)或重复应答照常执行快照落库(数据仍权威),但不重复置完成、不报错;DNLD 仅在运营日匹配时完成请求,不按类型隐式完成。
- 每个航班一条 `KAFKA_SCHD` 事件入 outbox`PROC_STATE``SUCCEEDED`;提交。
3. 提交后回填共享信箱(失败落 `BACKFILL_TODO`,见第 8 节);重放短路同样执行回填。
**同日重快照的权威规则(M)。** 整包快照是同日**最高权威**:两次快照之间已应用的动态运行更新(实际时间、登机门、柜台等),在重叠字段上以快照值覆盖(含快照显式清空);动态报文更新当前状态,其与快照的冲突按本规则处理。快照省略的字段按「整行替换混合规则」(第 5、7 节)保留合并后当前值,不属于覆盖例外的组成部分。
**规则 M 的成立条件(未决)。** 本规则隐含两个上游前提:① 快照内容是 AODB 生成/发送时点的完整最新数据(§3.16.1 注释 2 支持生成时点口径);② 两条线的投递顺序与上游发送顺序一致(共享信箱 FIFO + 同一发送方 SEQN 单调)。若前提 ② 不成立(快照生成后延迟投递、SEQN 按类型分段编号等),重叠字段会被旧值回退——例如 10:00 生成的快照 10:03 才入队,会把 10:02 已处理的登机门更新改回旧值。前提 ② 尚未从协议条款逐条核实,把规则 M 作为对外承诺前须经设计评审确认(ACM2-31)。
**示例:一次 DNLD 快照的落库。** 示例中的 CA100、MU201、CZ300 均为 AODB 分配的 `FLID` 值(写成形似航班号仅为可读;`FLNO` 航班号是主表标量列,与 `FLID` 无恒定关系)。报文为 AODB 下发的**运营日 2026-09-09** 的整包快照,含 CA100、MU201 两个航班(`FDAY` 取快照覆盖的运营日 2026-09-09,与该报文何时接收、何时落库无关);库内该运营日的代(`SCHD_GEN`)当前成员为 CA100、CZ300。进入事务后依次发生:
| 步骤 | 动作 | 结果 |
|---|---|---|
| 重放检查 | 该消息 ID 等于日代 `LAST_MESSAGE_ID` | 否,继续 |
| 算成员差 | 上代成员 {CA100, CZ300} 新快照 {CA100, MU201},∩ 当前仍 `FDAY`=2026-09-09 | 差删名单 = {CZ300} |
| 合并写入 | CA100 已存在 → 锁内读态、整集合替换,`FDAY=2026-09-09`;MU201 新航班 → 先插主行再写明细 | 两航班状态就位 |
| 差删 | 删 CZ300 主行 + 全部明细行 | CZ300 消失 |
| CAS 推版本 | `SCHD_GEN` 版本 7 → 8;不符则整体回滚 `FAILED(INFRA)` | 版本推进 |
| 事件 | CA100、MU201 各一条 `KAFKA_SCHD` 入 outbox | |
| 终态 | `PROC_STATE``SUCCEEDED`,提交 | 提交后回填共享信箱 |
此后 Dispatcher 按 FLID 聚合这批 `KAFKA_SCHD` 发出(见第 8 节)。
**覆盖范围与未决。** 本流程的设计范围当前仅覆盖 DNLD 子类型;RESP 与 ADFT 的 Handler、「请求 → 应答 → 标记完成」闭环为未决设计项,未覆盖类型的处理按 `FAILED(UNSUPPORTED)` 对齐。参考数据请求(RQRD;参考数据事件见规范 §3.1–3.15)同样登记于 `REQ_TRACK`,但应答落参考库、由 RequestCoordinator 处理,与航班状态无关。
### 4.3 动态线:运行事件的落库(以 GTDT 为例)
1. **Handler 纯函数决策。** 输入 = 受影响航班当前态 + 报文,输出 = 字段变更与待发事件;此步不算账、不落库。每类资源的语义在 Handler 内固定,如 GTDT 为登机门集合整体替换(GTNO=0 表示清除)。
2. 进入事务(先锁 `PIPELINE_LOCK` 行):
- 每个受影响航班:锁内重读最新态 → 合并字段变更 → 整集合替换写入,保留既有 `FDAY`(保留该行所属的运营日/代;动态事件不携带也不推断运营日,跨运营日的归属更正只由请求/下载线的快照接管完成,见 §3.2)。
- 事件(`KAFKA_MSG` 变更通知 + `KAFKA_SCHD` 状态推送)入 outbox`PROC_STATE``SUCCEEDED`;提交。
3. 提交后回填共享信箱(同上)。
**示例:一条 GTDT 报文如何变更登机门。** 报文(FLOP 增量,仅携带登机门,其余字段未出现):
```text
META: SNDR=AODB, TYPE=FLOP, STYP=GTDT, SEQN=42
FLOP: FLID=CA100
GTDT GTNO=1 GATE=A1 GTYP=D
GTDT GTNO=3 GATE=B2 GTYP=I
```
库内 CA100 当前登机门为 G28(序号 1)、G33(序号 2)。处理过程:
1. **解析字段集**GTDT = [(GTNO=1, GATE=A1, GTYP=D), (GTNO=3, GATE=B2, GTYP=I)]。其余键未出现 = 不动——延误、机位、时间一概不碰。
2. **翻译命令**GTDT 出现 → Replace,整组替换为这 2 条。
3. **锁内合并**:读当前态 → 整组替换 → 版本 +1、记 `LAST_MESSAGE_ID`
4. **写库**`flight_gate` 中 CA100 旧 2 行删除,按输入顺序插入 2 行(ORDINAL=1,2SOURCE_SEQ=1,3);GTDT 无标量列,主表仅推进版本/消息/更新时间。
5. **事件**`KAFKA_MSG` 变更通知;`KAFKA_SCHD` 载荷 `"GTDT":[{"GATE":"A1","GTNO":"1","GTYP":"D"},{"GATE":"B2","GTNO":"3","GTYP":"I"}]`——JSON 数组、不做二次字符串编码,键按字典序。
6. **提交后**:回填共享信箱。
读回时按 ORDINAL 组装主行与 `flight_gate` 2 行为权威态;展示视图取前两个登机门 A1、B2。
**清除与拒绝语义示例:**
- `<GTDT GTNO="0"/>` 单条且全 0 → Clear`flight_gate` 行删空,`KAFKA_SCHD` 中 GTDT 键消失。
- `[{"GTNO":"1","GATE":"A1"},{"GTNO":"0"}]`:0 与正常条目混排 → 非法结构,整条消息失败,不猜测、不截断。
- 字段未出现 ≠ 清除:仅收到一条 DELY 报文时登机门不受影响。
- 异常键的空值才构成 Clear:FDIV 出现为 `{}` → Clear → `FDIV_*` 列全部置 NULLSRVT 为空串/null 目前不算 Clear(见第 6 节),只有显式 Clear 命令能清列。
## 5. 状态变更实现(两条线共用)
所有状态变更统一经加工链:**报文 → 命令 → 合并出新状态 → 写库**。
### 5.1 数据统一形态
内存中一个航班的状态即一个模型(`FlightNextState`):一组标量键值 + 10 类集合(每类为一组条目,每条目一组键值)+ 版本/消息追踪字段。主表存标量、明细表存集合,列映射固定(如 GTDT 的 GATE/GOTM/GCTM → `flight_gate` 对应列);读回时按 ORDINAL 组装集合,数据库权威态 = 主行 + 明细行。
### 5.2 第一步:报文到命令
当前 Handler 返回字段集,`commandsFromFields` 将其翻译为命令;仓储仍会解析异常/文本 JSON,显式命令尚未贯通全部边界。
| 命令 | 标量 | 集合 |
|---|---|---|
| Unchanged(未出现) | 不修改 | 不修改 |
| Set(value)(出现) | 覆盖 | — |
| Clear | 删键,物理列置 NULL | 删除全部明细行 |
| Replace(list) | — | 同事务 DELETE + 批量 INSERT 整集合替换 |
| Apply(item, sourceSeq) | — | 引擎内预留:按首个 SOURCE_SEQ 匹配更新,否则追加;无生产调用方,尚非已确认协议 |
规则:
- **字段未出现 = 不动。** 增量报文只携带变化部分,其余不受影响。
- **标量出现 = Set(覆盖)。** 异常键(FDIV/FRET/FLAB)载荷为空串、null、空对象时 = Clear。
- **集合出现 = Replace。** 整组替换为报文给定条目。条目序号属性(如 GTNO)为 "0" 是协议显式清除标记:仅当集合内**全部**条目均为 0 时翻译为 Clear;与正常条目混排属非法结构,fail fast 拒绝。
- **空数组 `[]` = Replace(空集)**,明细行清空——区别于字段缺失的 Unchanged。
- DELY 无协议序号属性,不支持逐条 Apply;清除走空数组替换。
**验收目标。** 非法 JSON、未知形状、超容输入不得在截断后成功。当前 `parseCollection` 未校验数组元素必须是对象,仓储会忽略未知属性;该目标尚未完成。
### 5.3 第二步:命令合并进当前态(纯函数)
`FlightStateEngine.apply` 从当前态拷贝一份并逐条应用命令:Set 覆盖键值;Clear 删键并记入 clearedKeys(被清除的标量键清单,供第三步写库时据此将物理列置 NULL);Replace 整组替换。随后版本 +1、记 `LAST_MESSAGE_ID`,产出 `FlightNextState`。纯函数:不碰库、不发事件,同样输入恒得同样输出。
### 5.4 第三步:新状态写库(唯一写入口 `persistNextStates`
- **写库写的是合并后完整态,不是报文字段。** 主表与集合写入的输入是锁内合并后的完整 `FlightNextState`;「未提字段一律不动」发生在引擎合并层(5.3),不在写库层。快照线整行替换(`FDAY`=该快照覆盖的运营日)写出的同样是合并后状态——快照省略的字段以合并流程结束时的当前值持久化。例:航班当前态含 `ESTT=0900`(预计时间)与登机门 {G28},快照给出 `STD``ACTT=1012`(实际时间,进港语义 ATA/出港 ATD)与登机门 {A1},省略 `ESTT` → 输出 = `STD`/`ACTT`/登机门改为快照值,`ESTT` 保留 0900。若快照显式给空(Clear/空数组)才构成清空。`FDAY` 由快照 Set 显式推进,是保留规则唯一的结构性例外。
- **动态线**仅 UPDATE 本次出现的列,clearedKeys 对应物理列显式置 NULL;新航班先插一行 `FDAY=NULL`——动态消息不携带运营日、不触发换代,新行尚未被任何日计划代收录(并非"无运营日"),待后续被某运营日整包快照收录时由快照赋值(清理见第 7 节「未归属行」)。
- **明细集合**:每次写航班,9 张明细表对该 `FLID` 先 DELETE 再按合并后集合整组 INSERT——即使某集合本次未变也重写,以保证库内与内存状态严格一致。此即"整集合替换"的落地:ORDINAL 按输入顺序 1..NSOURCE_SEQ 存协议序号,RECORD_VERSION 记写入时版本。
- `last_message_id` / `state_version` / `updated_at` 一并更新。
### 5.5 事件由状态序列化而来
`KAFKA_SCHD` 载荷 = `FlightNextState` 序列化(`FlightFieldsJson`):键按字典序输出保证同状态字节级稳定;集合键输出为 JSON 数组/对象,不做二次字符串编码。快照线的事件与落库共用同一份事务内状态;动态线的事件目前来自 Handler 的锁外预览(见 5.6),偏差收敛前动态事件载荷不承诺与提交状态逐字节一致——对外状态的最终一致性由投递聚合按当前整态投影兜底(第 8 节)。
### 5.6 已知偏差(待收敛)
GTDT Handler 在事务外先算一遍预览并构造事件,主泵在事务内再算一遍;该偏差收敛前,不能宣称动态事件载荷与提交状态一致(见 5.5,投递侧以整态投影兜底)。目标状态为 Handler 只表达业务变化,事务内产出唯一一份 nextState,落库与 `KAFKA_SCHD` 共用——尚未实现,列为可靠性验收项。
## 6. 字段语义矩阵
### 6.1 16 类结构映射
| 集合键 | 存储载体 | 序号属性 | 出现语义 | 显式清除 | Apply |
|---|---|---|---|---|---|
| GTDT 登机门 | `flight_gate` | GTNO | Replace | GTNO=0 全清 | 引擎预留(未启用;GTNO 定位) |
| CKDT 值机柜台 | `flight_checkin` | CKNO | Replace | CKNO=0 全清 | 引擎预留(未启用);同 CHKC 多舱位分配按 CKNO 区分,不按资源号去重 |
| CLDT 行李转盘 | `flight_belt` | CLNO | Replace | CLNO=0 全清 | 引擎预留(未启用) |
| PSDT 计划机位 | `flight_stand_plan` | PSNO | Replace | PSNO=0 全清 | 引擎预留(未启用) |
| CHDT 行李滑槽 | `flight_chute` | CHNO | Replace | CHNO=0 全清 | 引擎预留(未启用) |
| DELY 延误 | `flight_delay` | 无 | Replace(仅保存本次集合,不追加历史) | `[]` 清空 | 不支持(协议无定位键) |
| ABTM 靠撤桥 | `flight_bridge_op` | ASNO | Replace(操作全集) | ASNO=0 全清 | 引擎预留(未启用;ASNO 定位,多次靠/撤桥各占一行) |
| CHOT 轮挡 | `flight_chock_op` | CSNO | Replace | CSNO=0 全清 | 引擎预留(未启用;CHID=ON/OFF 为业务属性,非序号) |
| ROUT 计划航路 | `flight_route_point(route_kind=ROUT)` | RTNO | Replace | RTNO=0 全清 | 引擎预留(未启用) |
| ERUT 扩展航路 | `flight_route_point(route_kind=ERUT)` | RTNO | Replace | RTNO=0 全清 | 引擎预留(未启用) |
| FDIV 备降 | 主表前缀列 `FDIV_*` | — | Set(对象) | `"null"`/`{}`/空串 → Clear | — |
| FRET 返航 | 主表前缀列 `FRET_*` | — | Set | 同上 | — |
| FLAB 中止 | 主表前缀列 `FLAB_*` | — | Set | 同上 | — |
| SRVT 服务 | 主表 `SRVT_TEXT` | — | Set | 当前字段转换器不将 null/{}/空串转 Clear;显式 ScalarCommand.Clear 可清列 | — |
| VIPF 贵宾 | 主表 `VIPF_TEXT` | — | Set | 同 SRVT | — |
| MAFL 共享航班 | 主表 `MAFL_TEXT` | — | Set | 同 SRVT | 无查询需求时维持现有载体 |
公共明细列:`FLID`(逻辑归属,当前无数据库外键)、`ORDINAL`(输入顺序,1 起)、`SOURCE_SEQ`(报文序号属性,如 GTNO=3;仅记录,不参与定位、不唯一)、`RECORD_VERSION``CREATED_AT/UPDATED_AT`(整组替换为物理重写:`CREATED_AT` 反映最近一次重建时刻,不是业务记录时间);当前主键 `(FLID, ORDINAL)`。路线表尚需把 ROUTE_KIND 纳入主键以避免 ROUT/ERUT 冲突。相同资源号不代表同一条分配记录,禁止按资源号去重。**表中"引擎预留(未启用)"的 Apply 仅指引擎分支存在,生产只生成 Replace/Clear。**
**集合全集语义:协议依据与待确认项。** 快照线(SCHD)可按全集理解:规范注释明确日计划是「AODB 最新数据的完整快照」,可选字段/段未出现即 AODB 无此信息、子系统应删除本地已有值(§3.16.1 注释 4),DELY 亦明确「该标签对所有已记录延误重复出现」。动态线各事件每次携带全量集合还是仅本次变更,规范未逐条写明——CHOT 的 CSNO 实为机位序号(非轮挡序号),每段记录一次上/下轮挡移动(§3.22.2),若事件仅携带本次移动,Replace 会丢失既有操作记录;各集合「序号=0 全清」的协议出处也需逐类核实。动态线集合语义确认前,Replace 全集语义仅对快照线可视为定案(ACM2-31 未决)。
### 6.2 清除的库内与线上表达
- 库内:标量/异常/文本键 Clear → 对应列置 `NULL`;集合键 Clear/Replace(空集) → 明细行删除。
- 线上(`KAFKA_SCHD`):清空的表达**定为键缺失**——Clear 与 Replace(空集) 事件同语义,`KAFKA_SCHD` 载荷不输出该集合键;下游不得把「键缺失」与「`[]`」作两种解释。库内读回(无键)与 wire 表达按此收敛,黄金样例按此口径固定(投递聚合与 tombstone 见第 8 节)。
- **两个方向的「缺失」语义相反**,消费契约必须写明:上游输入报文中字段缺失 = Unchanged(保留原值);下游 `KAFKA_SCHD` 整态投影中字段/键缺失 = 当前态无该数据,消费端应移除本地旧值。下游接收的是整态替换,不得按补丁合并。
- 异常对象 Set 在增量 SQL 中仅写出现的属性;旧对象被省略的属性可能残留,与对象整体替换目标不一致,待回归修正。
### 6.3 时区与空值
- `FDAY` 只记录日期(该行所属代覆盖的运营日,见 §3.2),其来源与日界都按「快照覆盖的运营日」口径:由日计划快照决定,不由报文接收/落库时刻推算——跨午夜接收不改变归属;动态事件不更新 `FDAY`
- SIS 时间串(`DDMONYYHHMM`,机场当地时)原值保存;类型化提升按实际查询需要另行定案。
- PG 当前保留字符串空值,集合 JSON null 属性会被解析为空串。Oracle 11g 对零长度字符值按 NULL 处理是官方文档明确的既定行为,不再是待确认的数据库事实;待验证的是本模块在该行为下能否维持字段语义——空串往返、SRVT/VIPF/MAFL「空串不算 Clear」的判断、显式命令的持久化保真,均列为 Oracle 适配的交付门槛验证项。
## 7. 失败规则与不变量(两条线共用)
事务内任一步失败 → 整体回滚,消息记 `FAILED` 退避重试。已提交的成功不受后续失败影响:回填与投递失败仅落补偿待办,不降级 `SUCCEEDED`,不重放业务。
**不变量:**
- **状态、事件、终态同事务**:库内可见的必是完整提交结果。
- **计算是合并不是覆盖;写库是一致不是增量。** 报文未提字段一律不动(加工链见第 5 节);写入库的永远是锁内合并后的完整 `FlightNextState`,其中包括快照线整行替换写出的合并后状态(混合规则与例见 5.4)。快照线更新 `FDAY`(置为该快照覆盖的运营日),动态线保留 `FDAY`(不改变归属、不推断运营日),动态/ADFT 新行 `FDAY=NULL`(尚未被任何运营日代收录,不等于无运营日)。
- **快照是同日最高权威**:重叠字段上快照覆盖动态更新(见 4.2 规则 M 及其成立条件标注);动态报文更新当前状态,与快照的冲突按规则 M 处理。
- **快照重放仅一道防线**:凭 `schd_gen.last_message_id`(见 3.3)识别最近一次快照的重放;消息去重凭 `identity_key`;版本号只是顺序令牌。更早历史快照的重入是否安全,依赖人工重放的版本/业务日期策略(见 3.3 重放三种去向)。
- **未归属行(`FDAY=NULL`)的生命周期**:动态线先于快照到达或从未上日计划的 `FLID``FDAY=NULL` 建行,不隶属任何代、差删不触达,不引用 `SCHD_GEN` 名单。其清理由定期历史化作业执行:条件 = 最后更新时间超过 N 天(默认 7,可配置)且未被任何待答请求/名单引用;N 的默认值须有真实消费方复核后启用。已归属历史日的账本行与航班清理为另一独立未决项(见 §3.6),不适用本规则。
**错误分类与队列行为(目标矩阵):**
| 错误类别 | 示例 | 终态 | 重试 | 队头行为 |
|---|---|---|---|---|
| 协议结构非法(解码乱码/0 与正常条目混排/未知形状/缺身份键) | §5.2 非法结构 | `DEAD` | 不重试 | 立即释放 |
| 容量超限(单包 >10000 航班等) | §4.2 超限 | `DEAD` | 不重试 | 立即释放 |
| 无注册 Handler | 路由不到 Handler4.1 | `FAILED`,不写终态 | 不自动重试,可人工重放 | 队头滞留至超限 |
| 未定义 SCHD 子类型 | 未在子类型规则表中的子类型 | 现状 `FAILED(UNSUPPORTED)`;目标终态待定案(其余行为以本行现状标注为准) | 不自动重试 | 队头滞留至超限 |
| 解码器缺陷/内部错误 | 事务 DB 故障 `FAILED(INFRA)` | `FAILED` → 退避 | 超限转 `DEAD` | 队头滞留直至超限 |
| 并发版本 CAS 冲突 | 日代推进版本不符 | 事务回滚(无终态) | 同事务或下一轮重读自动重试 | 队头滞留直至成功 |
| 人工重放 | — | 仅 `DEAD` 消息可人工重放,须携带版本/业务日期策略(见 4.2/3.3) | — | — |
矩阵中「既有」:`DEAD`/`FAILED` 两类终态、CAS 自动回滚、队头失败退避;「待补校验」:超容与结构非法的显式拒绝(§5.2 验收目标)、未定义子类型的终态归属、人工重放的幂等身份策略(失败前已绑定的 `identity_key` 与重放身份的关系未定义)。每类错误有且只有一个默认路径,以本矩阵行为准。
字段缺失、清除、空数组、异常对象的精确行为见第 6 节。
## 8. 提交之后:回填与事件
提交成功后有两件外部副作用:回填共享 MySQL 信箱、异步投递 Kafka,二者各自独立重试。
回填恢复依赖一条持久待办:
- **现状**:仅在回填失败后写 `BACKFILL_TODO`;提交后立即崩溃的窗口内没有待办,无法恢复。
- **目标**:在业务终态同一事务内登记回填意图,提交后回填成功即删待办。崩溃后自然有账可查,无需额外"重启对账框架"。
- **红线**:登记待办后再失败,不得把已提交的 `SUCCEEDED` 降级为 `FAILED`
**`KAFKA_SCHD` 投递语义(按 FLID 聚合,第 8 节的主体)。** Dispatcher 聚合未发出的同 `FLID` 事件,按该 `FLID` **当前整态投影**输出——不是逐事件补丁流:
-`FLID` 多条未发事件合并为一次按最新 `STATE_VERSION` 生成的整态载荷;中间版本的载荷不逐条补发。
- 旧批次的投递重试可晚于新批次发出;消费端以 `(FLID, STATE_VERSION, UPDATED_AT)` 去重,晚到的旧投影不应覆盖新状态。
- `KAFKA_MSG` 仅作变更通知,不承载状态;两个主题之间不承诺顺序。
- 未归属行约定(第 7 节)与 tombstone(下)也是事件身份的一部分:`FLID` 不重复重发。
- **tombstone(差删的对外删除语义;自定义删除事件,非 Kafka 原生墓碑)**:被差删航班的最后一类 `KAFKA_SCHD` 事件以 tombstone 表达——载荷仅含 `FLID``DELETED` 标记(无状态字段;value 非 null,Kafka 不会按日志压缩语义处理它);与差删同事务登记,投递失败照常重试、不过期,下游接到后删除该 `FLID` 本地数据。删除通知不携带版本号:迟到删除、删除后恢复与旧更新事件的冲突消解由消费端负责;主行物理删除后 `STATE_VERSION` 的连续性(删除前与重建后的事件可能同号)未定义——删除事件的版本/世代标识为开放项(ACM2-31)。
## 9. 读取路径
当前读一个航班 = 主行 1 次 + 10 类集合各 1 次,共 11 次查询;全量读 N 个航班约 1+10N 次(N=航班数,主行批量 1 次 + 每航班 10 类集合各 1 次)。多次读不一定落在同一数据库快照,主子状态不保证一致。
后续在仓储边界按 FLID 批次加载明细,并明确一致性读事务;投递侧 Dispatcher 的整态投影读取受同一缺口影响,一并纳入验收。不引入缓存权威或通用查询框架。`GET /all/flights` 仍是 Controller TODO——仓储方法与展示视图存在不等于接口已交付。
`KAFKA_SCHD` 输出结构化 JSON,集合禁止双重编码为字符串。清空的 wire 表达已按第 6 节定案(键缺失);未知属性、异常局部替换与空值往返仍存在验收缺口(见第 6 节);单个全字段样例通过并不等于整个 SIS 协议无损。