36 KiB
运营航班状态设计评审与重构建议
历史评审:以下结论针对 cd5de56,不是当前状态。最新核查见 ACM2-29 完成度与简化计划,现行设计见 航班状态设计。保留原文用于追溯,不重复维护其阶段清单。
评审日期:2026-09-08。代码基线:cd5de56。评审对象:decision-flight-state.md、相关 SQL、仓储、快照流程、消息处理和测试。
本文件是有证据的设计评审和可实施提案,不代表下述重构已经实装,也不自动取代 ACM2-28/29 的历史决策。本次只新增评审文档及原决策文档的评审入口,没有修改生产代码、执行数据库迁移或更新 Plane 状态。
1. 结论与部署边界
保留“自有关系数据库内原子提交航班状态、处理终态和 outbox”的架构;建议撤销“全宽表、零子表、超界丢弃”的硬性限制,改为航班标量主表、无损重复明细、按需展示视图。
两项决策必须分开:选择一个事务内的权威存储,解决的是一致性;如何存放重复资源,解决的是信息保真和查询模型。前者正确,不足以证明后者正确。
本轮用户已明确:现场提供 Oracle 时一定是 Oracle 11g;否则自行安装 PostgreSQL。 因此实际产品边界是两种互斥部署配置,不能继续把 Oracle 当作遥远的假设,也不应让一个部署同时把 PG 和 Oracle 都作为动态状态权威。共享 MySQL 信箱边界保持原样。
评审建议分为三个层次:
- 立即修正会丢数据或破坏事务不变量的实现与验收口径。
- 用最小的领域结构承载 SIS 已有数据,完成一个 DNLD→资源增量→查询/通知的纵向闭环。
- 分别验收 Oracle 11g 和 PostgreSQL 适配;完整 AODB、资源优化调度、票务和历史分析不纳入本次重构。
2. 当前决策的合理部分与证据不足部分
| 原设计 | 评审结论 | 理由 |
|---|---|---|
航班状态迁回自有库,与 PROC_STATE、MSG_EVENT 同事务 |
保留 | 消除原 Redis 状态与 PG 终态之间的跨存储提交窗口。 |
| Redis 退出状态权威 | 保留 | 本项目已依赖自有关系数据库,增加另一个状态权威不带来必要收益。 |
| Handler 纯函数,基础设施通过仓储隔离 | 保留 | 可以独立验证 KEEP/FIX 业务规则;物理表不应决定 Handler 的业务表达能力。 |
| 增量按字段合并,DNLD 全量替换 | 保留总体方向 | 需进一步区别字段未出现、显式清空、集合全量、操作记录增量。 |
FDAY 域化差删、ADFT 不随旧代误删 |
保留 | FDAY 是快照所有权,不应冒充航班运营日期。 |
PIPELINE_LOCK 行锁 |
有条件保留 | 是写事务互斥工具,还不是完整的双实例 FIFO 或主备协议。 |
| 全字段字符串保持 legacy 兼容 | 保留原始值,补类型化查询面 | 时间字符串不适合排序/范围查询;Oracle 空串语义也会影响保真。 |
| 固定 2/3/2/2/2 槽位,超界丢弃 | 否决作为权威表示 | 项目自带三登机门样例已能触发丢失,不只是 XSD 理论边界。 |
| 多次操作只留一次、按资源号去重 | 否决作为通用规则 | 分配记录与物理资源不是同一实体;操作序号和资源身份有业务含义。 |
| 零 CLOB、零子表、零方言成本 | 撤销硬性约束 | Oracle 11g 支持关系明细和 CLOB;串行事务与多表原子写入并不冲突。 |
| “重启即恢复,RPO=0” | 限定故障模型 | 本地事务关闭双写窗口,不等于数据库介质损坏、异步副本切换也零丢失。需单独定义备份、恢复与提交持久性要求。 |
| “单行 upsert 亚毫秒,无新增往返” | 改为待测目标 | 本轮未见支撑数据。当前增量路径实际包含父行 upsert 和字段 UPDATE,快照还涉及批量、差删、版本、事件。 |
文档也有演进失配:decision-flight-state.md §6/8/9 分别保留 JSONB、集合文本、固定槽位三个阶段,design.md §2 仍描述集合序列化文本;architecture.md 同时有行锁互斥与运行期排他尚未完成的说明。应为“当前有效规则”和“历史被替代规则”设置明确索引。
3. 互联网方案:哪些可以借鉴,哪些不能照搬
本次使用 TinyFish 搜索并读取一手资料。连接器未提供 batch_create,因此资料逐个 URL 读取,未使用异步浏览自动化。没有获得商业 AODB 的生产物理 DDL;不能把产品介绍或信息交换标准描述为厂商数据库表结构。
| 来源与性质 | 已核实内容 | 对本项目的适用性与限制 |
|---|---|---|
| IATA《AIDX Implementation Guide》v3.0,2014,基于 14.1 schema;行业信息模型 | 航段是核心结构;重复元素保留顺序;文档包括资源、共享航班、运行时刻和不正常运行身份。 | 用于检验模型是否丢失领域信息。它不是本项目 SIS 的替代协议,也不是最新版本认证。 |
| EUROCONTROL A-CDM Specification,2025-01-30;运行流程模型 | 将进港与出港关联,通过里程碑推进周转和离港估计。 | 支持把进出港记录、周转关系和操作时刻分清;不要求本项目实现完整 A-CDM。 |
| SITA Airport Management;商业产品功能边界 | 区分航班管理、固定资源、移动资源、协同决策,并涉及多机场。 | 可借鉴职责分离;公开页不披露其物理表,不能推断“它采用某种子表”。 |
| Postgres Professional Airlines 演示库,版本 10 文档;公开关系 schema | flights 有独立 flight_id、计划/实际时刻和机场引用;通过 flights_v 提供附加展示信息。 |
是能阅读字段、键和约束的数据库示例,但侧重订票,不是机场生产 AODB。 |
| Oracle Database 11g SQL Reference;平台约束 | SQL VARCHAR2 有 4000 字节上限;字符长度为零按 NULL 处理。 |
必须影响长度、空值、测试和驱动适配,不能仅替换 upsert 语法。 |
来源:IATA AIDX 指南、EUROCONTROL 2025 A-CDM、SITA Airport Management、Postgres Professional schema、Oracle 11g 数据类型、Oracle 11g NULL。
具体可借鉴之处:
- AIDX §3.1.9 要求重复数据保持可追踪的顺序,更新一个重复元素时携带其余元素;附录 H 中登机门可重复三次、值机可表达不同类型和不连续范围、延误原因可有多项。这足以反驳“两登机门、一延误是通用航空事实”,但本项目每个消息类型的最终行为仍应以 SIS 和现场协议为准。IATA 指南
- 进港和出港关联有运营价值,但不应因为是同一架飞机就合成同一条记录;周转关系应显式表达,不能从航班号或机尾号临时猜测。EUROCONTROL A-CDM
- 展示可以是视图,权威表不必复制屏幕布局。演示库的
flights_v提供本地时刻及机场附加信息,展示了这种分离方式;其票务实体和简化自然键不应直接搬入 SIS 中间件。Postgres Professional schema
以下表设计是结合这些资料和项目代码得出的本次建议,不是宣称行业标准强制规定了这些表名或范式。
4. 关键发现:契约保真
F1 — 高:当前两个登机门槽位直接违反本仓库样例
证据:
- SIS §3.34.3,源文件约 8321–8340 行,同时给出
GTNO=1/G28、GTNO=2/G33、GTNO=3/G23。 JdbcPgRepositories.kt:367定义 GTDT 两槽位;:466的flattenSlots使用.take(c.slots.size)。decision-flight-state.md§9 却称已通过该规范样例核验,并允许超界丢弃。
当前转换实际把三条变成两条,G23 消失。对于合法输入,系统仍可能提交 SUCCEEDED,不会因丢失而自动重试。这是权威存储的损失性转换,不只是展示不全。
建议:完整保存记录,界面需要两列时仅在视图投影前两项,并显示总数/更多信息。仅把上限从 2 改成 3 不能解决其他集合及将来的合法输入。
F2 — 高:资源编号、分配身份和数组位置被混为一谈
flattenSlots 并不按 GTNO/CKNO 选槽,而是按去重后的数组顺序赋槽;slotView 再用下标合成序号。因此只输入 GTNO=3,读回就是 GTNO=1。这也与文档“槽位号即报文序号属性”不符。
同一函数对五类集合都按资源号去重。两个 CHKC=01、CCLS=F/Y 的分配会丢掉后一条;机位不同时间段、相同资源不同属性也存在同类风险。没有证据证明这些是可无条件抹平的冗余。
建议:分别保存 ordinal(报文出现顺序)、source_seq(源序号字符串)和资源号;资源号本身不设“每航班唯一”的约束。是否允许重复源序号应由协议矩阵决定,不能由 SQL 唯一键替上游作决定。
F3 — 高:操作历史被不充分地压缩为单一里程碑
- SIS 约 5127–5157 行明确 ABTM 按每次靠桥/离桥重复,
ASNO标识操作序号,ABDG标识实际桥资源。 - XSD
BRIDGEDATA(约 1714 行)也包含ABDG/ABOP/AOTM/ASNO。 flattenAirbridge只取首个 A 和首个 D,读回时合成 1/2,完全不输出原条目的ABDG。flattenChocks只保留首次 ON/OFF,丢掉CSNO/CHST;chocksView给两者都生成CSNO=1。flattenDelay总取首条;XSD 的 DELY 为maxOccurs="unbounded"(约 2251 行),不能从“一次只显示一条原因”推出只存一条。
建议:原始操作列表和“当前/首次/最近一次里程碑”分开。后者可以派生,但必须命名清楚,并定义按源序号、发生时刻还是到达顺序选择;现在的 firstOrNull 不具备“当前有效”的证明。
F4 — 高:显式清空与缺失混用,异常对象处理又不一致
FlightFields 是 Map<String,String>,通过键是否存在、字符串中嵌套 JSON 表达不同含义。当前存在:
- GTDT 的
[]/null会清空槽位,读回没有 GTDT 键;如果下游以“键缺失=保持旧值”消费,就不能代表清空。 flattenExceptions对FRET="null"、空对象、非对象或非法 JSON 返回,不写清空列;旧异常可能保留。- 异常缺失属性不清空,与文档“集合键出现即整体替换”的统称不一致。
- 自由文本在
REMC/RSN/value/#text中猜一个键,读侧固定输出另一个键,无法承诺原格式往返。
FRET="null" 不产生清空写入已由转换诊断复现。是否在每一种真实 XML 清除消息中触发,还需 Codec/Handler 的实际映射;不能把诊断输入误称为已经上线的报文路径。
建议:消息解释层显式使用 Unchanged / Clear / Set 和 Unchanged / Clear / Replace / 按协议定义的操作。编解码层负责将 XML 删除标记翻译成操作;仓储不要靠字符串内容猜业务命令。
F5 — 高:出站载荷与数据库当前态可能在同一次提交后不同
SnapshotFlow.kt:99 从未压缩的 normalized 构造事件;仓储在写入时截断/去重。三门快照由此可能产生“Kafka 有三门,数据库点查只有两门”。普通路径也分别接受 flightChanges 和已序列化 schdPush,一致性依赖 Handler 自觉。
事务原子性保证两者一起提交,不保证它们表达同一状态。
建议:统一生成完整的 nextState,无损持久化和线格式序列化都使用它。增加 readAfterWrite == nextState 和 wirePayload == wire(nextState) 两道断言;不要只测试事件数量与事务提交。
5. 关键发现:迁移、并发与恢复
F6 — 高:V1.2.0 不是有数据的安全升级迁移
V1.2.0 SQL 第 97–104 行直接删除 16 个旧集合列,前面只有 ADD COLUMN,没有数据转换或回填。若 V1.1.0 数据库有集合值,升级后旧值丢失,新列仍为 NULL。
空库首次安装不会出现这个损失;本轮未访问现场,不能声称现场已发生丢失。但当前脚本不具备带数据升级能力。
建议:扩展表结构→回填→逐字段核验→切换读写→观察→最后删旧列。已应用的 Flyway 版本不要直接改写;尚未升级但会经过 V1.2.0 的实例,必须在它执行前做无损导出或另行制定受控升级路线。后续新增迁移无法凭空恢复已被删除的数据。
F7 — 高:CAS 失败后版本相同,不能证明是同一个快照重放
SnapshotFlow.kt:59 在锁外读取 generation,:69 先覆盖数据,:82–87 在 CAS 失败时只要 again.version == expected + 1 就继续成功。
可由代码推导的交错:
- 处理者 A、B 在锁外都读到版本 5,但携带不同快照。
- A 拿锁,写入集合 A,把 generation 推到 6 并提交。
- B 随后拿锁,按旧预期写入集合 B 和旧差集。
- B 的 CAS 失败,看到版本恰为 6,于是继续提交事件和 SUCCEEDED。
- generation 成员仍可能是 A,航班内容却被 B 改写;这不是 no-op。
生产严格只有一个活动处理者时,此交错不会自然出现;它揭示的是文档宣称的交叉实例保护不能成立,也不能以“已有行锁”作为开放多实例的依据。本轮未做真实双连接复现。
现有 FlightSchdInvariantTest:223 的 mock 在 CAS 失败后仍返回版本 5,没有覆盖恰好为 6 的分支。
建议:锁内读 generation 和有效处理身份;CAS 失败统一回滚。若需要重放短路,必须在任何状态写入前,使用已成功的消息身份/明确的快照身份识别,同内容摘要只能作为补充,不能仅凭版本号。为 generation 增加 last_snapshot_message_id(或同等可核验身份)与可选摘要,保留版本只作为顺序令牌。
F8 — 高(双实例场景):行锁没有覆盖“读状态→决策”
Pump.kt 在 txManager.inTransaction 之前读航班视图并调用 Handler。两实例可以基于同一旧状态分别计算,然后串行覆盖。PIPELINE_LOCK 也不自动保证较小信箱 ID 的处理者先取得锁。
建议:继续单活动实例部署。要增强防错,应在锁内复核处理终态与队头、读取当前态、应用纯决策并提交;XML 解析等不依赖当前态的工作可在锁外。切勿使用 SKIP LOCKED 跳过较小业务消息破坏严格 FIFO。数据库隔离级别也不能替代处理身份与顺序协议。
F9 — 高(提交后异常):回填失败可能落回处理失败路径
Pump.kt:235 的 backfillOnSuccess 位于状态事务提交之后;异常会被外层 processOne 归为 INFRA 并交给 procFailure。快照回填也处于提交后。若再次进入完整处理流程,有重复事件、重复快照推进或覆盖新状态的风险。
建议:成功终态不可被回填错误降回业务失败。单独持久化回填待办,失败只重试回填;业务重试入口复核数据库中已提交终态。恢复能力应围绕“事务提交前/后”和“外部投递前/后”分别测试。
6. Oracle 11g 兼容性:不是把 ON CONFLICT 换成 MERGE
| 维度 | 当前风险 | 设计要求 |
|---|---|---|
| 空串 | PG 可保存空字符串,Oracle 字符零长度按 NULL;Map 的键存在性也会变化。 |
对有业务意义的空值保留 presence 标记或无损文档;不要未经协议认可全局将空串、NULL、缺失视为同义。Oracle NULL |
| 长度 | 现有 *_TEXT VARCHAR(512/2000) 是序列化后的整列表;XSD 不限条数的列表可合法超长。 |
明确 BYTE/CHAR 与数据库字符集;11g SQL VARCHAR2 仍受 4000 字节边界,使用子表或 CLOB 承接无界集合。Oracle 数据类型 |
| 时间 | SODT/ESTT/ACTT 是源字符串;数据库审计时间使用另一套类型。 |
区分业务发生时刻、收到时刻、提交时刻;原值和标准化时间并存。 |
| DDL | PG 迁移含 BIGSERIAL、TEXT、ADD COLUMN 等,整个辅助管道也需适配。 |
分 PG/Oracle migration location;定义 NUMBER、序列、长文本、时间与约束的映射。 |
| DML | 不只有 upsert,还包括生成键、分页、锁、批量和查询日期处理。 | 方言留在适配层,业务层不按数据库类型分支。 |
| 事务 | 行锁概念可共用,连接绑定和失效恢复仍须实测。 | 明确同一事务同一数据源;JdbcOps 当前 ThreadLocal 未按数据源区分,未来多个自有连接源不得误复用。 |
| 运行组合 | 当前依赖有 PG 驱动与 PG Flyway 模块,没有可据此验收的 Oracle 组合。 | 在目标 11.2 补丁级别上验证 JDK 25、具体 JDBC 驱动、迁移工具和连接池组合。不能凭“ojdbc8”名称认定支持。 |
11g 缺少现代 SQL/JSON 能力,不等于不能存储 JSON 文本。当前 V1.2.0 自己还把 SRVT/VIPF/MAFL 保存为 JSON 数组字符串;“废除 JSON 存储”因此也不是准确描述。真正需要选择的是:哪些字段要求 SQL 查询/约束,哪些原始结构只需无损保存。
超长 fail fast 优于静默截断,但如果合法消息仅因本地任意长度限制进入反复重试,严格 FIFO 会被持续阻塞,最终 DEAD 后仍缺失有效更新。容量问题应在投产前按契约和观测量解决;永久验证错误与暂态基础设施错误也应分开分类。
7. 建议的领域边界
本项目是外部 AODB 报文的有状态接收与分发系统。这里的“权威”指本系统已处理消息所形成的当前态及下游输出,不表示取得上游 AODB 的业务编辑权。
| 概念 | 建议定义 | 不要混用 |
|---|---|---|
| 航班记录 | 当前源系统用 FLID 标识的进/离港运营记录 | 航线模板、每天重复的航班号、航空器、旅客行程 |
| 航段/周转关系 | 对现有源关系作明确映射,需要时关联两个记录 | 不能先假定本地 FLID 与 AIDX 航段完全同义,也不能仅按机尾号自动配对 |
| 资源分配记录 | 某航班某一序号的门/柜台/转盘/计划机位等分配 | 资源主数据;相同资源号不一定是同一分配 |
| 操作记录 | 一次靠桥、撤桥、上/下轮挡等源记录 | 当前资源、首次/最近里程碑 |
| 运营日期 | 按明确机场时区及业务口径定义的日期 | FDAY 快照归属日;不要直接改现有主键 |
| 消息身份 | 源消息去重与处理恢复标识 | 快照版本、FLID、发生时刻 |
| 当前态、历史、展示 | 当前态由消息合并;历史单独留痕;展示可派生 | outbox 的合并通知不是完整运行事件历史 |
现有单机场/单 AODB 数据域可继续以 FLID 作主键。双流/天府若以后汇入一个实例,先核实跨机场和跨源 FLID 唯一性;只有证实会冲突才引入 (source_system, airport_scope, external_flid) 唯一映射及内部 ID。当前没有证据证明已经发生碰撞,不把这个潜在需求扩大为立即全表换键。
8. 推荐关系模型
8.1 主表与明细的分工
erDiagram
FLIGHT_SCHD ||--o{ FLIGHT_GATE : contains
FLIGHT_SCHD ||--o{ FLIGHT_CHECKIN : contains
FLIGHT_SCHD ||--o{ FLIGHT_BELT : contains
FLIGHT_SCHD ||--o{ FLIGHT_BRIDGE_OP : records
FLIGHT_SCHD ||--o{ FLIGHT_CHOCK_OP : records
FLIGHT_SCHD ||--o{ FLIGHT_ROUTE_POINT : contains
SCHD_GEN ||--o{ SCHD_GEN_FLID : owns
图只列主要关系;计划机位、通道、延误的明细见下表。SCHD_GEN_FLID 是代成员历史元数据,不强行对当前航班加级联外键,避免清场或跨代迁移改变既有差删语义。
FLIGHT_SCHD 保留 FLID、FDAY、标量业务字段、created/updated 时间。新增字段按实际查询和恢复需求选择:
LAST_MESSAGE_ID:追踪最后影响该航班的消息,关联审计,不代替业务去重。STATE_VERSION:需要一致查询或恢复校验时使用;每次有效变更推进,不因无副作用重放推进。SCHEDULED_AT等类型化时间:从原始 SODT 等派生,保留原值。- presence/解析状态:只在需区分空/缺失或解析失败的字段上引入,不滥加全表状态。
8.2 16 类结构的建议映射
| 现有集合 | 建议存储 | 必须保留的关键含义 |
|---|---|---|
| GTDT | FLIGHT_GATE |
ordinal、GTNO、GATE、PGOT/PGCT/GOTM/GCTM、GTYP |
| CKDT | FLIGHT_CHECKIN |
ordinal、CKNO、CHKC、CCLS、计划/实际开放关闭、CTYP;不按 CHKC 去重 |
| CLDT | FLIGHT_BELT |
ordinal、CLNO、BELT、BCLS、计划时刻、首末行李时刻、BTYP |
| PSDT | FLIGHT_STAND_PLAN |
ordinal、PSNO、PSST、开始/结束时刻 |
| CHDT | FLIGHT_CHUTE |
ordinal、CHNO、CHUT、等级、计划/实际时刻、类型 |
| DELY | FLIGHT_DELAY |
ordinal、CODE、STRT、DURA、原文说明;不以“唯一当前原因”覆盖列表 |
| ABTM | FLIGHT_BRIDGE_OP |
ordinal、ASNO、ABDG、ABOP、AOTM |
| CHOT | FLIGHT_CHOCK_OP |
ordinal、CSNO、CHST、CHID、CHTM |
| ROUT、ERUT | 共用 FLIGHT_ROUTE_POINT,区分 route_kind |
ordinal、原 RTNO、APCD、SCAT、SCDT,避免自定义分隔符和重编号 |
| FDIV、FRET、FLAB | 主表前缀列可保留 | 单值符合 XSD;明确文本键、空值、清除规则即可,不必强拆子表 |
| SRVT、VIPF、MAFL | 第一阶段允许无损文本旁表;查询需求明确后再结构化 | JSON 数组原有顺序、全部属性和长度;Oracle 使用 CLOB,PG 使用 TEXT;应用验证结构 |
低频集合旁表可定义为 FLIGHT_COLLECTION(FLID, COLLECTION_KIND, PAYLOAD, SCHEMA_VERSION),主键是 (FLID, COLLECTION_KIND)。这不是用于任意核心字段的 EAV 表,只承接暂未需要 SQL 过滤的完整集合。MAFL 的共享航班关系一旦用于关联查询,应提升为明确关系,而不是在 SQL 中解析文本。
明细表数量由独立属性和读写语义决定,既不追求“零子表”,也不要求所有 16 类各建一张。1:N 表只存实际 N 行;支持 XSD 上限并不等于每航班预建 99 行。
8.3 单个明细表的可评审形状
以下是逻辑字段设计,非可直接运行的双数据库 DDL:
FLIGHT_GATE
FLID FK -> FLIGHT_SCHD
ORDINAL positive integer; input order
SOURCE_SEQ source GTNO string
GATE_CODE source GATE
PLANNED_OPEN_RAW source PGOT
PLANNED_CLOSE_RAW source PGCT
ACTUAL_OPEN_RAW source GOTM
ACTUAL_CLOSE_RAW source GCTM
RESOURCE_TYPE source GTYP
[typed timestamps / presence data as required]
PK (FLID, ORDINAL)
INDEX (GATE_CODE, FLID) when resource lookup is required
SOURCE_SEQ 不强转整数,以免丢掉契约/legacy 需要保留的形式。ORDINAL 是当前集合内部位置,不承诺跨版本稳定身份。若某报文是按序号逐项变更,必须由其业务规则使用源序号定位,并单独验证重复序号。
子表 FK 可随主表删除级联,以保证删除完整;对资源主数据的引用建议先做软校验和未知代码可观测性,避免参考数据刷新滞后使整个 FIFO 卡住。引用完整性是否升级硬约束需结合上游保证。
8.4 标量类型与时区
SIS 的时间示例是 DDMONYYHHMM 当地时间(例如 GTDT 的定义),不能因为 AIDX 使用 UTC 就直接给 SIS 字符串加 Z。应使用明确的 Asia/Shanghai 和英文月份解析,定义两位年份的世纪窗口,保留原字符串;测试跨月、跨年、午夜和非法值。其他机场接入时必须重新绑定时区。
高频查询使用标准化时间列;原始 SODT 字符串的字典序不等于时间序。PG 使用相应时区时间类型,Oracle 使用经过验证的 TIMESTAMP 类型与 JDBC 绑定;保留机场时区配置,不能认为数据库保存了瞬间就保存了源业务时区。
FTSS、取消、异常、资源操作里程碑也不要合成一个强制单向状态机。来自外部的更正消息可以修正历史值,哪些允许更正由 KEEP/FIX 矩阵决定。
9. 写入与消息语义
建议将模块边界调整为:
SIS XML -> 契约解码与校验 -> 类型明确的命令
|
当前态 + 纯 Handler -> nextState / 通知意图
|
同一自有库事务:主表 + 明细 + generation + outbox + SUCCEEDED
|
提交后回填补偿 / 至少一次投递
9.1 明确操作,不让仓储推测
| 输入情况 | 标量动作 | 集合动作 |
|---|---|---|
| 字段/集合未出现 | 不修改 | 不修改 |
| 合法值出现 | Set(value) | 对被确认是集合快照的类型,Replace(完整列表) |
| 明确删除标记 | Clear | Clear,按线协议输出明确的清除表达 |
| 操作型消息 | 按该类型规则 | 按序号更新、追加或替换由逐类型契约决定 |
| 非法对象、未知属性、超容量 | 可诊断失败 | 禁止成功后悄悄丢弃;是否可扩展接收由版本策略决定 |
GTDT/CKDT 的“0=全部未分配”在本地 SIS 文档有依据;不能直接推广为所有类型的序号 0 都有同样含义。ABTM/CHOT 尤其需要确认报文携带操作全集还是本次变化。AIDX 的重复元素约定可帮助设计,但不能替代 SIS 实测。
Clear 后数据库与 wire 是否输出 []、null 或协议专属标记,必须按下游契约固定;本轮不擅自改变 Kafka 线格式。
9.2 保持原子性与 FIFO 的提交顺序
- 锁外解析并验证完整消息,生成不依赖当前态的命令;快照整包校验重复 FLID、日域和集合边界。
- 开自有库事务,取得 writer 行锁。
- 锁内复核队头与消息终态;已成功的同一消息直接结束业务应用,不增加版本/事件。
- 读取当前航班与 generation,调用纯函数计算下一状态。
- 写主表和受影响的明细;快照进行 FDAY 域化差删、版本和成员推进。CAS 失败即回滚。
- 从同一个完整状态产生 outbox;记录必要身份和版本;置 SUCCEEDED。
- 提交后独立执行回填/投递;其失败不回退业务终态。
集合替换可在同一事务内 DELETE WHERE FLID=? 后 JDBC batch INSERT。小集合无需先实现复杂行级 diff;其他连接只能看到已提交状态,但多条 SELECT 的组装仍需一致读取策略。
DNLD 删除某航班后,下游如何得知删除也需要验收:显式删除通知、整批替换标记或重新拉取必须选定一种。当前 SnapshotFlow 只为新集合构造 schd 事件,不能只验收数据库少了一行。
10. 查询与展示:避免把屏幕列数写成业务上限
项目当前没有前端,现有输出是 /all/flights 与 Kafka。所谓运营“显示”应落实为它们的读模型,而非凭空增加 UI。
兼容 API 可以继续提供原来的 FLTR/字段结构,仓储聚合主表和明细。DBA 需要首个/第二个登机门时,提供 FLIGHT_SCHD_DISPLAY 查询视图,额外给出 GATE_COUNT,并保留完整明细查询。主表无需新增任意数量的 GATE3...99。
实现时注意:
- 不把多个 1:N 表直接 JOIN 成一个巨大的结果;三个门乘四个柜台再乘两个转盘会重复行。
- 主表分页后,按 FLID 批量读取各类明细并组装,避免每航班 N+1 查询。
- 父子表分多次 SELECT 时使用同一一致快照,或读取版本并重试;否则可能混合更新前的主表与更新后的明细。PG/Oracle 分别验收隔离行为。
MAID过滤保留现有共享航班查询契约。共享记录、实际承运信息和营销航班关系不要合并成单一文本标签。- 资源反查索引通常从
(resource_code, flid)出发;航班列表按实际日期/进离港筛选建立组合索引。用真实查询计划确定,不是把所有宽表列都加 B-tree。 /all/flights的兼容全量接口先保留;需要新增分页接口时另定 API 契约,不能悄悄把原接口改成只返回第一页。
性能验收应测报文峰值、最大 DNLD、每类集合 p95/p99/最大值、事务 p95/p99、锁等待、提交日志量、查询组装成本和消息积压。只有生产样本分布,不能证明业务硬上限;同时要有协议边界测试。
11. 分阶段迁移与回退
| 阶段 | 具体工作 | 完成证据 |
|---|---|---|
| P0:契约和阻断项 | 建立 16 类集合的缺失/清空/替换/操作矩阵;修 CAS、提交后回填边界;恢复完整集合承载能力。 | 三门、多舱柜台、多次桥/轮挡、异常清除、两快照冲突都通过。 |
| P1:无损扩展 | 增加明细/过渡旁表;保留现有展示列;建立版本和来源追踪;增量写同库同事务。 | 新旧读取与源完整状态对比,所有合法输入无静默损失。 |
| P2:数据回填与切换 | 按已部署版本选择回填来源,完整校验后切读;保留旧结构一个回退窗口。 | 数量、属性、顺序、空值、状态及 outbox 载荷核对;无未解释偏差。 |
| P3:双数据库验收 | 分别提供 PG/Oracle DDL/DML、迁移与集成环境;业务契约用同一组测试。 | 两套隔离环境均通过;11g 驱动/JDK/迁移组合已实际验证。 |
| P4:查询优化与收尾 | 按负载加索引/展示视图,移除确认不再使用的损失性列与转换函数。 | 查询计划和时延达标;回退窗口关闭、数据备份可恢复。 |
不同安装状态必须区别处理:
- 空库:可新建正确结构,但仍需通过升级链的空库安装测试。
- V1.1.0 有数据、尚未到 V1.2.0:必须在 DROP 之前保存完整集合;由原
*_TXT回填并对拍。 - V1.2.0 已运行:固定槽位里缺失的第三个门、源序号等不可从现表推算回来。需要旧库备份、完整历史输入或可信的完整快照。新的 DNLD 只能恢复它包含的当前态,不能保证恢复全部历史操作。
- 回放恢复:进入隔离库、隔离投递目标;先重建并核对水位,再受控切换。不能把旧消息直接灌回实时 FIFO 覆盖新状态。
PG→Oracle 切换不做两个权威库之间的实时应用双写。以停写水位、待投递事件状态、完整导出、目标导入对拍和单活动实例切换为基本路线;具体停机窗口与回退点待部署资料确定。
12. 验证结果与还缺什么
12.1 本轮已执行
使用 JDK 25 执行:
GRADLE_USER_HOME=/home/windy/project/msgexchange-v2/.gradle-home \
TMPDIR=/home/windy/project/msgexchange-v2/.gradletmp \
./gradlew test \
--tests '*FlightSchdInvariantTest' \
--tests '*FlightStoreDiffToolTest' \
--tests '*FlightFieldsJsonTest'
结果:13 tests,0 failures,0 errors,0 skipped。分别为 6 个状态不变量测试、5 个对拍测试、2 个序列化测试。初次沙箱运行因 Gradle 本机网络绑定失败;离线重试缺依赖;授权的在线运行成功。编译存在既有弃用/注解警告,没有据此修改依赖。
另以临时 Java 诊断直接调用本次编译产物中的 flattenForStorage 和 mapFlightRow。DataSource 代理禁止任何数据库调用,ResultSet 为内存字段映射;这是转换级诊断,不是 JDBC 集成测试。复现结果:
| 输入 | 实际观察 |
|---|---|
| 三条 GTDT:G28/G33/G23 | 读回两条,G23 丢失 |
| 单条 GTNO=3 | 读回 GTNO=1 |
| 同 CHKC 的 F/Y 两条分配 | 读回一条 |
| 两次靠桥、不同 ASNO/ABDG | 只余一次,原桥号和源序号消失 |
| GTDT=[] | 读回没有 GTDT 键 |
| FRET=null | 没有生成任何清空列 |
| DELY 两条原因 | 读回首条 |
这解释了为什么既有测试全绿不足以支持“100% 保真”:相关测试未覆盖上述反例,stub 也没有模拟 JDBC 的损失性表示。
12.2 尚未执行及原因
- 未执行全部测试及现有
FlightSchdJdbcPgTest。其setUp固定连接本机msgx,cleanup:70删除TEST_%航班及2026-09-01以后的 generation;未确认隔离性,不应在设计评审中直接运行该清场逻辑。 - 该 JDBC 测试在连接不可用时会
assumeTrue跳过,当前构建没有以 Testcontainers 提供隔离 PG 的证据;因此文档中的集成验收措辞不能只看总任务绿色。 - 未连接 Oracle 11g;没有驱动/JDK/迁移工具兼容验收,没有带真实数据迁移、跨连接并发和恢复性能结果。
- 没有现场真实流量分布或双机场 FLID 唯一性材料。文档声称的现场规律与本轮可核验事实分开记录。
12.3 实施时必需的回归矩阵
| 类别 | 至少覆盖 |
|---|---|
| 契约往返 | 三门、99 条允许集合、重复资源不同舱位、非连续源序号、顺序、每个可选属性、中文长文本 |
| 空值 | 未出现、空串、NULL、空数组、0 删除标记、异常对象清除;PG 与 Oracle 同语义 |
| 操作 | 多次靠撤桥/轮挡、更正原操作、不同桥/机位、多个延误、路线空属性 |
| 快照 | 同 FLID 重复、跨日归属、ADFT 存活、空快照、非法快照整包拒绝、响应匹配 |
| 事务 | 任意明细/outbox 写失败全回滚;generation 内容与版本一致;不同消息争同一下一版本必须冲突 |
| 恢复 | 提交成功后回填失败不重做业务;重放不加版本和事件;下游已接收可去重 |
| 多连接 | 锁等待、断连释放、读后写冲突、不能跳过 FIFO;不能只用内存 mock 证明 |
| 迁移 | 有数据 V1.1→目标、V1.2 损失检查、故障恢复、回退窗口、两数据库新建/升级 |
| 查询/投递 | 父子状态一致读取、全量接口保兼容、无 N+1/笛卡尔积、删除可见、DB 与 wire 等价 |
对拍工具继续保留严格默认:不通过忽略 GTNO、忽略第三个资源、忽略 ABDG 等方式把有损转换“洗绿”。任何合法偏离必须有业务依据、明确范围和黄金样例。
13. Plane workflow 对齐与后续落点
已按 plane-workflow 读取 ACM2-28 与 ACM2-29,确认属于 ACM2 项目;并搜索已有航班主题工作项。没有新建任务、修改状态或发表评论。
- ACM2-28 的单库权威决策可以保留;RPO、完成清单和当前平台措辞应按本评审限定。
- ACM2-29 直接覆盖本次问题,但其中“全系统零子表”“100% 吻合”“零方言包袱”等验收标准需要修订为可测的不变量。
- 建议将后续工作拆为:契约保真与安全存储、快照/回填事务修正、有数据迁移、Oracle 11g 适配、读模型与查询验收。工作项依赖应在获授权写入时建立 Plane relation,不靠标题排序替代依赖。
- 本轮用户已明确的部署条件应写入后续平台方案,不再重复询问是否可能是 Oracle 12c/19c;仍需的是实际 11.2 补丁、字符集、权限和可测试环境。
评审意见:ACM2-28 的一致性方向通过;ACM2-29 的损失性宽表方案不建议作为生产权威形态验收。优先完成无损表示和事务恢复,再扩展批量 Handler。