419 lines
48 KiB
Markdown
419 lines
48 KiB
Markdown
# 航班运行数据接入与当前状态管理设计
|
||||
|
|
|
|||
|
|
本文是航班状态接入与当前态存储的唯一设计与架构规范文档:统一界定系统职责、存储模型、字段语义、状态流转与跨模块不变量。已定决策与演进红线以正文为准;未完成项与开放风险在各节显式标注。
|
|||
|
|
|
|||
|
|
**标注约定**:正文未加标注的段落是规范要求;凡标注「开放项 / 未决 / 已知偏差 / 待补 / 验收目标 / 现状」的段落,是对当前实现或未定设计的如实披露,不是已交付事实,两类内容不得混作依据。
|
|||
|
|
|
|||
|
|
## 阅读速览:专用数据与设计逻辑
|
|||
|
|
|
|||
|
|
### 专用数据与术语
|
|||
|
|
|
|||
|
|
| 数据 / 术语 | 含义与用途 |
|
|||
|
|
|---|---|
|
|||
|
|
| SIS / AODB | SIS 是 AODB(机场运行数据库,上游业务权威)向各子系统分发数据的接口规范(SIS_AODB_RMS V0.1);本系统从共享 MySQL 信箱接收 AODB 报文。共享信箱是接入边界,本地数据库是本模块对外提供当前状态的唯一读取源,不是高于 AODB 的业务权威。 |
|
|||
|
|
| `FLID` | AODB 分配的航班唯一 ID(Number(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 起),后者保存报文中的源序号。源序号可空、不唯一,资源号也不是去重依据,不能据此把多条分配记录合并为一条。 |
|
|||
|
|
| SCHD:DNLD / RESP / ADFT | 日计划相关报文。DNLD、RESP 按整包快照换代并差删;ADFT 是单航班临时增量,不换代、不差删、不完成请求。当前仅 DNLD 接入,RESP/ADFT 及请求闭环仍有未决项。 |
|
|||
|
|
| FLOP / GTDT | FLOP 是字段级航班运行事件;规范 §3.19–3.43 定义了 25 类航班级事件(AODB→RMS 事件总计 44 类,§3.1–3.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 | M1:9/9 快照 `{A, B}` | 9/9:版本 1,消息 M1 | 9/9:`{A, B}` | A、B 写入,`FDAY=9/9`。 |
|
|||
|
|
| 2 | M2:9/9 快照 `{A, C}` | 9/9:版本 2,消息 M2 | 9/9:`{A, C}` | 候选差删 `{B}`;B 仍属 9/9,删除 B 并登记 tombstone;A 更新,C 新增。 |
|
|||
|
|
| 3 | M3:9/10 快照 `{C}` | 9/10:版本 1,消息 M3;9/9 不变 | 9/10:`{C}`;9/9 仍为 `{A, C}` | C 被 9/10 快照收录,主行 `FDAY` 改为 9/10。 |
|
|||
|
|
| 4 | M4:9/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.19–3.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,2;SOURCE_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_*` 列全部置 NULL;SRVT 为空串/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..N,SOURCE_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 | 路由不到 Handler(4.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 协议无损。
|