Files
msgexchange-v2/docs/review-flight-state-2026-09-08.md
T

410 lines
36 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.
# 运营航班状态设计评审与重构建议
> 历史评审:以下结论针对 cd5de56,不是当前状态。最新核查见
> [ACM2-29 完成度与简化计划](acm2-29-audit-and-simplification.md),现行设计见
> [航班状态设计](flight-state-design-v2.md)。保留原文用于追溯,不重复维护其阶段清单。
评审日期:2026-09-08。代码基线:`cd5de56`。评审对象:[decision-flight-state.md](decision-flight-state.md)、相关 SQL、仓储、快照流程、消息处理和测试。
本文件是有证据的设计评审和可实施提案,**不代表下述重构已经实装,也不自动取代 ACM2-28/29 的历史决策**。本次只新增评审文档及原决策文档的评审入口,没有修改生产代码、执行数据库迁移或更新 Plane 状态。
## 1. 结论与部署边界
**保留“自有关系数据库内原子提交航班状态、处理终态和 outbox”的架构;建议撤销“全宽表、零子表、超界丢弃”的硬性限制,改为航班标量主表、无损重复明细、按需展示视图。**
两项决策必须分开:选择一个事务内的权威存储,解决的是一致性;如何存放重复资源,解决的是信息保真和查询模型。前者正确,不足以证明后者正确。
本轮用户已明确:**现场提供 Oracle 时一定是 Oracle 11g;否则自行安装 PostgreSQL。** 因此实际产品边界是两种互斥部署配置,不能继续把 Oracle 当作遥远的假设,也不应让一个部署同时把 PG 和 Oracle 都作为动态状态权威。共享 MySQL 信箱边界保持原样。
评审建议分为三个层次:
1. 立即修正会丢数据或破坏事务不变量的实现与验收口径。
2. 用最小的领域结构承载 SIS 已有数据,完成一个 DNLD→资源增量→查询/通知的纵向闭环。
3. 分别验收 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.02014,基于 14.1 schema;行业信息模型 | 航段是核心结构;重复元素保留顺序;文档包括资源、共享航班、运行时刻和不正常运行身份。 | 用于检验模型是否丢失领域信息。它不是本项目 SIS 的替代协议,也不是最新版本认证。 |
| EUROCONTROL A-CDM Specification2025-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 指南](https://www.iata.org/contentassets/2db667ac37c045dbbb537eeb446dd9b2/aidx-xml-implementation-guide.pdf)、[EUROCONTROL 2025 A-CDM](https://www.eurocontrol.int/publication/eurocontrol-specification-airport-collaborative-decision-making-cdm)、[SITA Airport Management](https://www.sita.aero/solutions/sita-at-airports/sita-operations-at-airports/sita-airport-management/)、[Postgres Professional schema](https://postgrespro.com/docs/enterprise/10/apjs04.html)、[Oracle 11g 数据类型](https://docs.oracle.com/cd/E11882_01/server.112/e41084/sql_elements001.htm)、[Oracle 11g NULL](https://docs.oracle.com/cd/E11882_01/server.112/e41084/sql_elements005.htm)。
具体可借鉴之处:
- AIDX §3.1.9 要求重复数据保持可追踪的顺序,更新一个重复元素时携带其余元素;附录 H 中登机门可重复三次、值机可表达不同类型和不连续范围、延误原因可有多项。这足以反驳“两登机门、一延误是通用航空事实”,但本项目每个消息类型的最终行为仍应以 SIS 和现场协议为准。[IATA 指南](https://www.iata.org/contentassets/2db667ac37c045dbbb537eeb446dd9b2/aidx-xml-implementation-guide.pdf)
- 进港和出港关联有运营价值,但不应因为是同一架飞机就合成同一条记录;周转关系应显式表达,不能从航班号或机尾号临时猜测。[EUROCONTROL A-CDM](https://www.eurocontrol.int/publication/eurocontrol-specification-airport-collaborative-decision-making-cdm)
- 展示可以是视图,权威表不必复制屏幕布局。演示库的 `flights_v` 提供本地时刻及机场附加信息,展示了这种分离方式;其票务实体和简化自然键不应直接搬入 SIS 中间件。[Postgres Professional schema](https://postgrespro.com/docs/enterprise/10/apjs04.html)
以下表设计是结合这些资料和项目代码得出的**本次建议**,不是宣称行业标准强制规定了这些表名或范式。
## 4. 关键发现:契约保真
### F1 — 高:当前两个登机门槽位直接违反本仓库样例
证据:
- [SIS §3.34.3](legacy/SIS_AODB_RMS-V0.1.md#sec-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 约 51275157 行明确 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](../src/main/resources/db/migration/V1.2.0__flight_resources.sql) 第 97–104 行直接删除 16 个旧集合列,前面只有 ADD COLUMN,没有数据转换或回填。若 V1.1.0 数据库有集合值,升级后旧值丢失,新列仍为 NULL。
空库首次安装不会出现这个损失;本轮未访问现场,不能声称现场已发生丢失。但当前脚本不具备带数据升级能力。
**建议**:扩展表结构→回填→逐字段核验→切换读写→观察→最后删旧列。已应用的 Flyway 版本不要直接改写;尚未升级但会经过 V1.2.0 的实例,必须在它执行前做无损导出或另行制定受控升级路线。后续新增迁移无法凭空恢复已被删除的数据。
### F7 — 高:CAS 失败后版本相同,不能证明是同一个快照重放
`SnapshotFlow.kt:59` 在锁外读取 generation`:69` 先覆盖数据,`:8287` 在 CAS 失败时只要 `again.version == expected + 1` 就继续成功。
可由代码推导的交错:
1. 处理者 A、B 在锁外都读到版本 5,但携带不同快照。
2. A 拿锁,写入集合 A,把 generation 推到 6 并提交。
3. B 随后拿锁,按旧预期写入集合 B 和旧差集。
4. B 的 CAS 失败,看到版本恰为 6,于是继续提交事件和 SUCCEEDED。
5. 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](https://docs.oracle.com/cd/E11882_01/server.112/e41084/sql_elements005.htm) |
| 长度 | 现有 `*_TEXT VARCHAR(512/2000)` 是序列化后的整列表;XSD 不限条数的列表可合法超长。 | 明确 BYTE/CHAR 与数据库字符集;11g SQL VARCHAR2 仍受 4000 字节边界,使用子表或 CLOB 承接无界集合。[Oracle 数据类型](https://docs.oracle.com/cd/E11882_01/server.112/e41084/sql_elements001.htm) |
| 时间 | `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 主表与明细的分工
```mermaid
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:
```text
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. 写入与消息语义
建议将模块边界调整为:
```text
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 的提交顺序
1. 锁外解析并验证完整消息,生成不依赖当前态的命令;快照整包校验重复 FLID、日域和集合边界。
2. 开自有库事务,取得 writer 行锁。
3. 锁内复核队头与消息终态;已成功的同一消息直接结束业务应用,不增加版本/事件。
4. 读取当前航班与 generation,调用纯函数计算下一状态。
5. 写主表和受影响的明细;快照进行 FDAY 域化差删、版本和成员推进。CAS 失败即回滚。
6. 从同一个完整状态产生 outbox;记录必要身份和版本;置 SUCCEEDED。
7. 提交后独立执行回填/投递;其失败不回退业务终态。
集合替换可在同一事务内 `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 执行:
```sh
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 tests0 failures0 errors0 skipped**。分别为 6 个状态不变量测试、5 个对拍测试、2 个序列化测试。初次沙箱运行因 Gradle 本机网络绑定失败;离线重试缺依赖;授权的在线运行成功。编译存在既有弃用/注解警告,没有据此修改依赖。
另以临时 Java 诊断直接调用本次编译产物中的 `flattenForStorage``mapFlightRow`。DataSource 代理禁止任何数据库调用,ResultSet 为内存字段映射;这是转换级诊断,不是 JDBC 集成测试。复现结果:
| 输入 | 实际观察 |
|---|---|
| 三条 GTDTG28/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。**