From cedd859bdf58cbf19aa67c70ff8f75de97a657c6 Mon Sep 17 00:00:00 2001 From: windyboy Date: Sun, 13 Sep 2026 20:31:13 +0800 Subject: [PATCH] docs(acm2-74): consolidate design documentation --- AGENTS.md | 6 +- README.md | 31 +- docs/README.md | 96 ++--- docs/architecture.md | 60 ++-- docs/contracts.md | 92 ----- docs/flight-state.md | 128 ------- docs/{design.md => implementation.md} | 223 ++++++++---- docs/invariants.md | 127 ------- docs/legacy/decision-flight-state-history.md | 2 +- .../msgexchange-api-legacy-user-stories.md | 2 +- docs/reference.md | 60 ++-- docs/requirements.md | 298 ++++++++++++++++ docs/spec-boundary-closure.md | 51 --- docs/specification.md | 232 +++++++++++++ docs/user-stories.md | 325 ----------------- scripts/check-docs.py | 327 ++++++++++++++++++ scripts/check-docs.sh | 6 + .../omms/msgexchange/codec/JacksonXmlCodec.kt | 2 +- .../omms/msgexchange/codec/SisWireMapper.kt | 2 +- .../omms/msgexchange/config/PipelineProps.kt | 4 +- .../omms/msgexchange/delivery/Dispatcher.kt | 2 +- .../gzzn/omms/msgexchange/domain/MsgEvent.kt | 2 +- .../omms/msgexchange/domain/OperationDay.kt | 2 +- .../omms/msgexchange/domain/SnapshotLog.kt | 2 +- .../msgexchange/domain/flight/FlightModel.kt | 8 +- .../domain/flight/FlightStateEngine.kt | 2 +- .../infra/persistence/Repositories.kt | 2 +- .../persistence/jdbc/JdbcPgRepositories.kt | 2 +- .../msgexchange/infra/stub/StubAdapters.kt | 2 +- .../omms/msgexchange/ingress/InboxService.kt | 2 +- .../omms/msgexchange/jobs/HistorySweepJob.kt | 2 +- .../gzzn/omms/msgexchange/processing/Pump.kt | 2 +- src/main/resources/application.yml | 16 +- .../migration/V1__flight_state_baseline.sql | 4 +- .../db/migration/oracle11g/README.md | 2 +- .../processing/BackfillServiceTest.kt | 2 +- 36 files changed, 1173 insertions(+), 955 deletions(-) delete mode 100644 docs/contracts.md delete mode 100644 docs/flight-state.md rename docs/{design.md => implementation.md} (64%) delete mode 100644 docs/invariants.md create mode 100644 docs/requirements.md delete mode 100644 docs/spec-boundary-closure.md create mode 100644 docs/specification.md delete mode 100644 docs/user-stories.md create mode 100755 scripts/check-docs.py create mode 100755 scripts/check-docs.sh diff --git a/AGENTS.md b/AGENTS.md index 0c98543..7b7905b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,9 +10,9 @@ ### 2. Grill with Docs Skill(文档严审与盘问) - **权威设计**:`docs/` 是唯一设计依据(`docs/legacy/` 仅作参考)。 - - 核心约束锚点:`invariants.md` (PRE/INV/CLM), `contracts.md` (C-x/Q), `architecture.md` (D1–D4), `reference.md` (PARAM)。 + - 核心约束锚点:`specification.md` (C/PRE/INV/CLM/Q/G), `architecture.md` (D1–D4), `implementation.md` (机制与航班域), `reference.md` (PARAM)。 - **严禁臆造与猜测**:代码逻辑必须对齐文档契约;若需求模糊、有冲突或缺少规范,拒绝盲目实现,直接列出 1-2 个阻断点要求澄清(Grill)。 -- **文档引用纪律**:交叉引用仅使用稳定 ID(如 `PRE-x`, `C-x`, `PARAM:`),禁止使用章节号;参数值和默认值不重复书写,统一指向原处;文档绝不记录进度(进度走 Plane)。 +- **文档引用纪律**:交叉引用仅使用稳定 ID(如 `PRE-x`, `C-x`, `PARAM:`)或「文件名 + 小节名」指针,禁止使用章节号;参数值和默认值不重复书写,统一指向原处;文档绝不记录进度(进度走 Plane)。 # 项目核心约束 (com.gzzn.omms.msgexchange) @@ -24,7 +24,7 @@ ### 环境与构建陷阱 - **工具链**:JDK 25 + Micronaut 5.1.3(已锁版本,禁止随意升级)。 - **编译/DI**:严禁改动 KSP (`kotlin-ksp` + `micronaut-inject-kotlin`),否则 DI 静默失效;Jackson XML 强依赖 `-Xannotation-default-target=param-property`。 -- **测试规范**:涉及排序/重试/幂等/投递必须补齐针对 `invariants.md` 的回归测试;必须使用 `TestClocks.kt` 与内存适配器,**严禁使用 Thread.sleep 或真实外部基础设施**。 +- **测试规范**:涉及排序/重试/幂等/投递必须补齐针对 `specification.md` 的回归测试;必须使用 `TestClocks.kt` 与内存适配器,**严禁使用 Thread.sleep 或真实外部基础设施**。 - **Flyway**:仅对自身 PG 生效;Dev 运行需声明 `MSGX_FLYWAY_ENABLED=true`。 ### 交付规范 diff --git a/README.md b/README.md index 2ca75cb..c4fdfbf 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ - **事务模型**:与共享库交互均为外部副作用(ACM2-12)——主路径=上游外部写信箱 → JDBC 轮询发现新信 → 自有 PG 建 PENDING 入队(失败重扫补建);HTTP `/cminmsgs/send` 为 compat 写路径;处理成功回填 DATE_PROCESSED/STATUS 为最终一致。 -- **航班状态**:写入自有 PostgreSQL;完整规则见 `docs/flight-state.md`。 +- **航班状态**:写入自有 PostgreSQL;完整规则见 [implementation.md](docs/implementation.md)「航班域」。 - 影子对拍:自有 PG 开独立 schema;共享信箱为单信箱无法双写,影子输入=只读水位/回放口径。 @@ -118,7 +118,7 @@ docker compose ps ### 切流前 Kafka Broker 版本确认 -生产契约以 `user-stories.md` US-07 / §7-6 为准:对接 Kafka 2.8+ / 3.x+,生产者强制 `acks=all`、`enable.idempotence=true` 与 `max.in.flight.requests.per.connection=1`,**严禁非幂等降级**。README 不提供生产降级环境变量组合。 +生产契约以 [requirements.md](docs/requirements.md) `US-07` 为准:对接 Kafka 2.8+ / 3.x+,生产者强制 `acks=all`、`enable.idempotence=true` 与 `max.in.flight.requests.per.connection=1`,**严禁非幂等降级**。README 不提供生产降级环境变量组合。 旧系统 `msgexchange-api` 底层依赖 `kafka-clients:0.10.1.1`;现网 Broker 确切版本须在切流前实测确认: @@ -156,24 +156,13 @@ MICRONAUT_ENVIRONMENTS=dev ./gradlew run # dev stub 冒烟:内存 stub,无 ## 文档 -从 [设计文档入口](docs/README.md) 开始阅读;航班状态设计见 -[docs/flight-state.md](docs/flight-state.md)。 +`docs/` 是唯一设计依据;从 [设计文档入口](docs/README.md) 开始阅读(职责、事实归属、ID 语法与引用纪律都在那里)。顶层 6 个文件: -- [docs/architecture.md](docs/architecture.md):架构速览——**中间件定位**、总体拓扑(JDBC 轮询主路径)、 - 上下游边界、模块职责、关键决策、数据边界、部署与安全姿态、可观测性、就绪度。 -- [docs/design.md](docs/design.md):设计细节——系统边界、状态机与错误分类、数据模型、 - 核心流程语义(流程 1 主路径/compat 分述)、失败/重试/重放、不变量落点、参数表、已知缺口。 -- [docs/user-stories.md](docs/user-stories.md):阶段 A US-01~US-14、延后清场 US-15、上线 Epic、 - legacy HTTP 去留及逐项文档 TODO;包含验收标准、依赖、实现差距与待确认问题。 - (原 ACM2-15~22 文档整改清单 user-stories-todo.md 已收敛完成并移除,跟踪记录见 Plane。) -- [docs/flight-state.md](docs/flight-state.md):航班状态的唯一现行设计规范;历史方案不作为实现依据。 -- [docs/legacy/](docs/legacy/):外部参考/基线材料(自 legacy 仓库拷贝,非本系统文档)—— - `msgexchange-api-legacy-user-stories.md`(legacy 行为对拍基线,ACMA-4)、`unisysaodbsis.xsd`、 - `SIS_AODB_RMS-V0.1.md`(消息结构唯一事实源;CIIMS 中间件交换模型)。 +- [architecture.md](docs/architecture.md):系统边界、模块职责、存储归属、总体流程与 `D1`–`D4` 决策。 +- [requirements.md](docs/requirements.md):阶段范围与非目标、`US-xx` / `OPS-x` 验收目标、需求覆盖与依赖。 +- [specification.md](docs/specification.md):术语、契约 `C-x`、前提 `PRE-x`、不变量 `INV-x`、声明边界 `CLM-x`、待确认 `Q`、已知偏差 `G`、验证映射。 +- [implementation.md](docs/implementation.md):管道机制与航班域——数据模型、状态机、事务、投递、作业与恢复,航班权威模型与合并语义。 +- [reference.md](docs/reference.md):参数 `PARAM:`、指标与健康、模块与代码入口、错误分类。 +- [legacy/](docs/legacy/):外部协议与旧系统基线(`SIS_AODB_RMS-V0.1.md` 为消息结构唯一事实源、`unisysaodbsis.xsd`、legacy 行为对拍基线、历史决策记录)。 -## 关联 - -Plane **`airport_chengdu_msgexchange_v2`(ACM2,现行入口)**:ACM2-3(综合架构 v4, -架构权威,存储边界按 ACM2-12 修订;ACM2-11 为决策史)、ACM2-4(脚手架跟踪)、ACM2-10(评审与实施计划 -U01–U30)、ACM2-12(存储边界与共享信箱决策);ACMA 系列仅作归档历史/迁移来源 -(ACMA-8 v4 / ACMA-6 选型 / ACMA-9 JDK 口径在归档中可溯)。 +实现进度与缺口处置在 Plane(ACM2)。运行规程在上线/切流前另立 `docs/runbooks/`,不进入顶层。 diff --git a/docs/README.md b/docs/README.md index f05c2f6..465b01f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,61 +1,77 @@ # 设计文档入口 -实现核对日期:2026-09-11(代码基线以 `git log -- docs/` 为准)。文档描述**目标设计**,不等于已实现;测试通过不等于现场已发布。进度与缺口在 Plane(ACM2)跟踪;文档只记录「对外主张当前是否可声明」,见 [invariants.md](invariants.md) 声明边界。 +`docs/` 是本系统目标设计的唯一依据:只记录「设计是什么」与「对外主张当前是否可声明」。实现进度、缺口处置与排期在 Plane(ACM2),不在本文档体系内。 -## 1. 按读者选入口 - -| 你是谁 | 从哪读起 | -|---|---| -| 新开发者 | [invariants.md](invariants.md)(前提 / 不变量 / 声明边界)→ [design.md](design.md)(机制)→ [reference.md](reference.md)(参数 / 指标 / 模块) | -| 库方 / 上游接口人 | [contracts.md](contracts.md)(条款 `C-x` 与待确认 `Q`) | -| 值班主任 / 运维 | [reference.md](reference.md)(参数与指标)→ [invariants.md](invariants.md)(哪些主张当前不可声明)。执行规程在上线/切流前另立,设计阶段只保留前置条件与红线 | -| 评审 / PR 作者 | [invariants.md](invariants.md) + 本文 §5 评审清单 | - -## 2. 文档职责(一个事实只有一处定义) +## 1. 文档职责(一个事实只有一处定义) | 文档 | 唯一职责 | |---|---| -| [architecture.md](architecture.md) | 系统边界、模块职责、存储归属、架构决策(D1–D4)。 | -| [invariants.md](invariants.md) | 前提 `PRE-x`、不变量 `INV-x`、声明边界 `CLM-x`、验证映射。 | -| [design.md](design.md) | 管道机制:收报·水位、主泵·处理·事务边界、回填、投递、作业与归档。 | -| [contracts.md](contracts.md) | 对外承诺与要求(`C-x`)、待确认事项(`Q`)。 | -| [reference.md](reference.md) | 参数注册表、指标与健康、模块入口、错误分类。 | -| [flight-state.md](flight-state.md) | 航班域模型与合并语义。 | -| [user-stories.md](user-stories.md) | US / OPS 验收目标。 | -| [legacy/](legacy/) | 现役行为基线与外部协议事实([SIS 规范](legacy/SIS_AODB_RMS-V0.1.md)、[XSD](legacy/unisysaodbsis.xsd))。外部协议事实(报文结构、字段语义、上游行为)以 SIS/XSD 为准;legacy 现役行为只是基线,已知缺陷不作依据;与本系统目标设计的已知差异见 `C-25`/`C-26`,确认状态见 `Q13`/`Q14`。 | +| [README.md](README.md) | 入口、阅读顺序、文件职责、事实归属、ID 语法与引用纪律。 | +| [requirements.md](requirements.md) | 阶段范围与非目标、`US-xx` / `OPS-x` 验收目标、需求覆盖与依赖。 | +| [architecture.md](architecture.md) | 系统边界、模块职责、存储归属、总体流程与 `D1`–`D4` 决策。 | +| [specification.md](specification.md) | 术语、外部契约 `C-x`、前提 `PRE-x`、不变量 `INV-x`、声明边界 `CLM-x`、待确认 `Qn`、当前已知偏差 `G-NAME`、验证映射。 | +| [implementation.md](implementation.md) | 数据模型、状态机、管道机制、事务、投递、作业与恢复;航班域权威模型与合并语义。 | +| [reference.md](reference.md) | 参数 `PARAM:`、指标与健康、模块与代码入口、错误分类。 | +| [legacy/](legacy/) | 外部协议与旧系统基线:现役行为对拍、[SIS 规范](legacy/SIS_AODB_RMS-V0.1.md)、[XSD](legacy/unisysaodbsis.xsd)、[历史决策记录](legacy/decision-flight-state-history.md)。外部协议事实(报文结构、字段语义、上游行为)以 SIS/XSD 为准;legacy 现役行为只是基线,已知缺陷不作依据。 | + +## 2. 按读者选入口 + +| 你是谁 | 从哪读起 | +|---|---| +| 新开发者 | [specification.md](specification.md)(前提 / 不变量 / 声明边界)→ [implementation.md](implementation.md)(机制与航班域)→ [reference.md](reference.md)(参数 / 指标 / 代码入口) | +| 库方 / 上游接口人 | [specification.md](specification.md)(`C-x` 与 `Q`) | +| 值班主任 / 运维 | [reference.md](reference.md)(参数与指标)→ [specification.md](specification.md)(哪些主张当前不可声明)。执行规程在上线/切流前另立(`docs/runbooks/`),设计阶段只保留前置条件与红线 | +| 评审 / PR 作者 | [specification.md](specification.md) + 本文「维护清单」 | ## 3. 事实归属表 | 事实 | 唯一归属 | 其他文档怎么写 | |---|---|---| -| 水位 `W`、空洞判定与推进条件 | design「收报与水位」 | contracts 写对库方的承诺 `C-1`–`C-3`;reference 写参数 | -| 保留期下界 `R_keep`、清除前置条件 | contracts「保留与清除」 | design 只引 `C-x`;执行步骤在上线前另立 | -| 处理标记值集与写权限 | contracts `C-5` | design 只写行为约束「只写空标记、不回撤、不覆盖」(`INV-7`) | -| 回填四结果、放弃语义、`R` 的作用 | design「回填」 | invariants 记结论与可声明性 | -| 退避 / `claim-batch` / 回填期限等取值 | reference「参数」 | design 只引 `PARAM:x` | -| 消费权排他、ID 不复位、报文不可变、时钟、单实例 | invariants「前提」 | 其他文档只引 `PRE-x` | -| 航班身份、合并语义、`STATE_VERSION`、`OPERATION_DAY` | flight-state.md | design 只引域规则 | -| 对外术语(落信 / 入站 / 库方 / 处理标记) | contracts「术语」 | — | +| 水位 `W`、空洞判定与推进条件 | implementation.md「收报与水位」 | specification.md 写对库方的承诺 `C-1`–`C-3`;reference.md 写参数 | +| 保留期下界 `R_keep`、清除前置条件 | specification.md「契约」 | implementation.md 只写行为约束;执行步骤在上线前另立 | +| 处理标记值集与写权限 | specification.md `C-5` | implementation.md 只写行为约束「只写空标记、不回撤、不覆盖」(`INV-7`) | +| 回填四结果、放弃语义、`R` 的作用 | implementation.md「回填」 | specification.md 记结论与可声明性 | +| 退避 / `claim-batch` / 回填期限等取值 | reference.md「参数」 | 其余文档只引 `PARAM:` | +| 消费权排他、ID 不复位、报文不可变、时钟、单实例 | specification.md「前提」 | 其他文档只引 `PRE-x` | +| 航班身份、合并语义、`STATE_VERSION`、`OPERATION_DAY` | implementation.md「航班域」 | 其余文档只引域规则与 `INV-x` | +| 对外术语(落信 / 入站 / 库方 / 处理标记) | specification.md「术语」 | — | +| 管道内部术语(`W` / 队头 / 终态 / 回填意图) | implementation.md「术语与持久化记录」 | — | -## 4. 引用与写作纪律 +## 4. ID 定义语法与引用纪律 -1. **引用只用稳定 ID**:`PRE-x`、`INV-x`、`CLM-x`、`C-x`、`D-x`、`OPS-x`、`PARAM:x`、`[G-x]`、`[Q-x]`、`US-xx`、`ACM2-nn`。**不再用章节号做跨文档引用。** - 本条取代旧版 README 的「其他文档引用章节号」规则:章节号随增删章节腐烂,指针失效后必然被改写为复述——重构前实测有 30 处「唯一定义处」与 33 处跨文档章节引用。 -2. 指针之后**不再复述**被指内容。若两处需要同一段话,说明它放错了位置。 -3. 文档不记进度,只记「主张是否可对外声明」。进度在 Plane。 -4. 机制段只写「是什么 / 为什么」。验收 → invariants 验证映射;前置条件、红线与库方方案 → contracts / invariants;参数默认值 → reference;操作步骤在上线/切流前另立规程,设计阶段不写。一句话一个主张;只强调关键结论,避免整段加粗。 -5. 数值只有两个家:参数默认值与指标名在 reference.md;契约数值(保留期、值集、下界)在 contracts.md。 +**持久规范 ID 族**:`US-nn`、`OPS-n`、`Dn`、`C-n`、`PRE-n`、`INV-n`、`CLM-n`、`Qn`、`G-NAME`、`PARAM:`。 + +- 定义只能出现在所属文件,并采用固定语法: + - `US-xx` 用三级标题(`### US-01 …`); + - `C-x`、`INV-x` 用加粗定义行(`- **C-5** …`); + - `CLM-n`、`OPS-n`、`Dn`、`PRE-n`、`Qn`、`G-NAME` 与 `PARAM:` 用注册表首列;首列必须是**单个裸 ID** + (`` `ID` `` 或 `ID`)。成组登记(`` `a` / `b` ``)、带括注的首列与写成 `` `ID` `` 的引用行都不算定义。 + - 同一行登记多个 ID(如验证映射的 `INV-20 / CLM-3`)是引用行,不构成定义。 + 其他位置一律是引用。 +- 编号稳定:条款被取代时标 `[作废 by C-y]` 并保留原文;不静默改写,不重编号。 +- **引用只用稳定 ID**,不用章节号:写 `INV-3`、`C-8`、`PARAM:msgx.pipeline.claim-batch`,或「见 implementation.md『收报与水位』」这类文件名 + 小节名指针。章节号随增删章节腐烂,指针失效后必然被改写为复述。 +- 指针之后**不再复述**被指内容。若两处需要同一段话,说明它放错了位置。 +- 外部 SIS 证据统一写 `SIS:
`(如 `SIS:3.16-note-4`),解析到 [legacy/SIS_AODB_RMS-V0.1.md](legacy/SIS_AODB_RMS-V0.1.md) 的章节;它不属于本项目规范 ID,不参与唯一定义检查。 +- `G-NAME` 是活跃偏差 ID:定义只在 specification.md「当前已知偏差」注册表,其他位置只写标记;偏差闭合时在同一变更中删除定义与全仓引用,历史与关闭证据只留 Plane。 + +**数值只有两个家**:参数默认值与指标名在 reference.md;契约数值(保留期、值集、下界)在 specification.md。 + +**文档不记进度**:不写日期式状态、完成记录与 changelog;只记「主张是否可对外声明」(`CLM-x`)。 ## 5. 维护清单 -**Q 答复落地(4 步)**:① 在 contracts.md 就地补结论与日期 → ② 改 reference.md 的默认值与「依据」列 → ③ 机制有变才改 design.md → ④ 在 invariants.md 的 CLM 上划掉挂起。 +**Q 答复落地(4 步)**:① 在 specification.md 就地补结论与日期 → ② 改 reference.md 的默认值与「依据」列 → ③ 机制有变才改 implementation.md → ④ 在 specification.md 的 CLM 上划掉挂起。 + +**偏差闭合**:在 specification.md 删除该 `G` 定义,并删除全仓 `G-NAME` 引用;关闭证据留在 Plane。 **PR 评审 4 问**:新事实是否已有归属?是否复述了别处?是否用了稳定 ID?是否把运维 / 验收写进了机制段? -**尺寸触发器**:design.md 超过约 800 行才按子系统再拆;invariants.md 超过两页先怀疑混进了机制。 +**日常维护**:运行 `scripts/check-docs.sh`,校验顶层六文件、ID 定义唯一性与全仓引用、旧文件名与章节号零命中、活跃 `G`、仓库内链接。合并或重命名文档时传入迁移前 Git 基线(`scripts/check-docs.sh <基线>`),额外比较持久 ID 与活跃 `G` 集合。 -## 6. 当前状态 +**尺寸触发器**:implementation.md 的管道与航班域两章各自超过约 500 行才考虑拆入 `docs/` 专题目录。 -- 2026-09-11 文档体系重构:新增 invariants.md / contracts.md / reference.md;design.md 由 design.md + message-lifecycle.md 合并,后者于 2026-09-12 删除(内容已全部并入 design.md);跨文档引用由章节号改为稳定 ID。 -- 不建 runbooks.md:设计阶段没有可执行的运行环境,操作步骤在上线/切流前另立(见 Plane ACM2-48);设计阶段需要的只有前置条件与红线,它们分别在 contracts(`C-7`–`C-12`)与 invariants(CLM-x)。 -- 目标设计与交付状态分离:未交付、未确认、不可声明的主张见 invariants.md 的声明边界与 Plane ACM2,不在正文里逐段标注。 +## 6. 不建的文件 + +- 顶层不再增加 Markdown:`docs/` 顶层固定为上述 6 个文件加允许的专题目录。 +- 不建运行规程文件:设计阶段没有可执行的运行环境,操作步骤在上线/切流前另立(`docs/runbooks/*.md`);设计阶段需要的只有前置条件与红线,它们分别在 specification.md(`C-7`–`C-12`)与 specification.md 的 `CLM-x`。 +- 声明边界与 Plane 分离:未交付、未确认、不可声明的主张记在 specification.md,逐项处置在 Plane(ACM2),不在正文逐段标注。 diff --git a/docs/architecture.md b/docs/architecture.md index 19e8a7f..641516d 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,4 +1,4 @@ -# msgexchange-v2 架构文档 +# 架构 ## 1. 系统定位与范围 @@ -10,12 +10,11 @@ msgexchange-v2 是机场 OMMS 的上游报文处理中间件,用于替换旧 - **主要入口**:轮询共享 MySQL 的 `CMINMSGS`。 - **兼容入口**:`POST /cminmsgs/send`,供现役兼容、手工工具和对拍使用;写入信箱后返回记录 ID,不是生产收报主路径。 - **输出**:Kafka 的 `msg` / `schd` 消息、共享 MySQL 的 `COUTMSGS` 出站信箱,以及查询 HTTP 接口;不直接推送前端。 -- **当前范围(阶段 A)**:航班当前态落自有 PostgreSQL(`FLIGHT_SCHD` + 资源明细表 + `FLIGHT_ROUTE_POINT`,权威口径见 [flight-state.md](flight-state.md))。无 Redis 依赖;ES 历史投影属暂缓的阶段 B。 +- **当前范围(阶段 A)**:航班当前态落自有 PostgreSQL(`FLIGHT_SCHD` + 资源明细表 + `FLIGHT_ROUTE_POINT`,权威口径见 [implementation.md](implementation.md)「航班域」)。无 Redis 依赖;ES 历史投影属阶段 B。 -本文描述架构约束,不代表所有能力已实现;实现缺口见 [invariants.md](invariants.md) 的声明边界与 Plane(ACM2)。模块交互、状态机与参数详见 [design.md](design.md),前提与不变量见 [invariants.md](invariants.md),对外契约见 [contracts.md](contracts.md),需求见 [user-stories.md](user-stories.md),参数与指标见 [reference.md](reference.md);操作步骤在上线/切流前另立规程(设计阶段只保留前置条件与红线)。历史报文契约仍以 [SIS 接口规范](legacy/SIS_AODB_RMS-V0.1.md) 和 [XSD](legacy/unisysaodbsis.xsd) 为兼容依据,其他 legacy 资料仅作参考,旧系统行为基线的职责见 [README](README.md) 职责表。 +现场供库时目标为 Oracle 11g,否则自建 PostgreSQL;Oracle 适配必须通过方言与集成验证后才能作为运行时选项。 -现场供库时目标为 Oracle 11g,否则自建 PostgreSQL;当前只有 PG 实现可运行,Oracle 不是已支持的平台。 -航班表结构和处理逻辑见 [运营航班状态设计](flight-state.md)。 +本文只描述架构约束与归属,不代表能力已实现:机制见 [implementation.md](implementation.md),前提、不变量与可声明性见 [specification.md](specification.md),对外契约见同文件「契约」,需求见 [requirements.md](requirements.md),参数与代码入口见 [reference.md](reference.md);操作步骤在上线/切流前另立规程(设计阶段只保留前置条件与红线)。历史报文契约以 [SIS 接口规范](legacy/SIS_AODB_RMS-V0.1.md) 和 [XSD](legacy/unisysaodbsis.xsd) 为兼容依据。 ## 2. 总体架构 @@ -44,7 +43,7 @@ CIIMS / AODB 等上游 收报、处理、投递与维护作业各使用独立线程,不占用 HTTP 事件循环。**航班当前态的写入只发生在持有 `PIPELINE_LOCK` 的事务内**,由主泵串行驱动。 -采用 Kotlin + JDK 25、Micronaut 编译期依赖注入和 JDBC 持久化。数据库变更由 Flyway 管理,但只作用于自有 PostgreSQL。具体依赖版本以 `build.gradle.kts` 为准,不在架构文档重复维护。 +技术栈:Kotlin + JDK 25、Micronaut 编译期依赖注入、JDBC 持久化;数据库变更由 Flyway 管理,只作用于自有 PostgreSQL。依赖版本以 `build.gradle.kts` 为准,不在本文重复维护。 ## 3. 模块职责 @@ -54,25 +53,27 @@ CIIMS / AODB 等上游 | `codec` | XML 解码,区分非法报文与可修复的解码失败。 | | `processing` | FIFO 调度、业务身份绑定与去重、领域决策与落库(SCHD/FLOP/FDEL/ADFT):纯领域逻辑只返回决策;Processor 作为事务协调器,在锁事务内完成状态写入、事件与回填意图登记,不直接触碰 Kafka。 | | `delivery` | 消费待发事件,负责按目标保序、`schd` 聚合、投递和失败重试。 | -| `jobs` | 回填补偿扫描、航班历史清理与留痕保留期清理;独立 job 线程执行(调度见 design.md「生命周期与清除」,红线见 flight-state.md「生命周期与开放项」,与主泵的互斥见 `INV-18`),不参与 FIFO。 | +| `jobs` | 回填补偿扫描、航班历史清理与留痕保留期清理;独立 job 线程执行,不参与 FIFO(与主泵的互斥见 `INV-18`)。 | | `domain` / `config` | 领域状态、事件和决策模型,以及运行参数。 | | `infra` | 仓储(JDBC/stub)、外部适配器、重试、健康检查与日志;通过接口隔离基础设施。 | +代码入口清单见 [reference.md](reference.md)「模块与代码入口」。 + ## 4. 主流程 `InboxPoller` 发现 → 自有 PG 入队(`INV-2`)→ 主泵按最小未完成 `MSG_ID` 取队头、解码并按业务身份去重 → 处理器在持 `PIPELINE_LOCK` 的同一事务内写航班变更、待发事件、处理终态与回填意图(`INV-17`)→ 提交后回填共享信箱处理标记 → `Dispatcher` 投递 `KAFKA:msg` 与 `KAFKA:schd`(至少一次,`INV-10`)。 -机制细节各有归属:扫描谓词与水位见 design.md「收报与水位」,分派与事务边界见「主泵调度与单条处理」,回填见「回填」,保序与 `schd` 聚合见「事件投递」。 +机制细节各有归属:扫描谓词与水位、分派与事务边界、回填、保序与 `schd` 聚合见 [implementation.md](implementation.md)。 ## 5. 必须保持的约束 -本节只列约束的**归属**;完整定义与验证映射见 [invariants.md](invariants.md),实现与演进不得违反: +本节只列约束的**归属**;完整定义与验证映射见 [specification.md](specification.md),实现与演进不得违反: -- 消息严格 FIFO:`INV-3`、`INV-4`、`INV-5`(发现完整性依赖 `PRE-2`/`PRE-3`,当前不可对外声明)。 +- 消息严格 FIFO:`INV-3`、`INV-4`、`INV-5`(发现完整性依赖 `PRE-2`/`PRE-3`)。 - 动态状态单写者与写者集合互斥:`D2`、`INV-18`。 -- 身份去重:`INV-9`(身份组成见 design.md「消息、身份与决策」)。 +- 身份去重:`INV-9`。 - 快照可恢复与运营日不可变:`INV-12`、`INV-13`。 -- 航班当前态的物理清除只发生在历史归档之后:`D1`(红线见 flight-state.md「生命周期与开放项」)。 +- 航班当前态的物理清除只发生在历史归档之后:`D1`。 这些约束优先于吞吐量优化。单写者降低了并发复杂度,代价是队头阻塞和吞吐上限;如需并行化,必须先重新定义顺序与状态归属,不能只调整线程数。 @@ -80,37 +81,20 @@ CIIMS / AODB 等上游 | 存储 | 承载内容 | 职责说明 | |---|---|---| -| 自有 PostgreSQL | 单行锁 `PIPELINE_LOCK`、处理状态与回填事实 `PROC_STATE`、消费水位 `INBOX_CURSOR`、待发事件 `MSG_EVENT`、请求跟踪 `REQ_TRACK`、航班当前态 `FLIGHT_SCHD` + 现有 8 张资源明细表 + `FLIGHT_ROUTE_POINT`(`SRVT`/`VIPF` 专用明细尚未实现 `[G-SRVT-VIPF]`)、留痕 `SCHD_SNAP_LOG` | 本系统唯一业务数据库。消息处理、状态推进、处理终态、回填意图与待发事件在单事务内原子提交;本地事务只在此库。 | -| 共享 MySQL | `CMINMSGS` 入站信箱、`COUTMSGS` 出站信箱 | 外部系统所有。本系统仅执行约定的信箱读写与处理标记回填,不建表、不迁移 schema、不写历史表;由库方按 `Q9` 执行的清除与历史归档见 contracts.md「保留与清除」。兼容 HTTP 入口可按既有契约写入入站信箱。 | +| 自有 PostgreSQL | 单行锁 `PIPELINE_LOCK`、处理状态与回填事实 `PROC_STATE`、消费水位 `INBOX_CURSOR`、待发事件 `MSG_EVENT`、请求跟踪 `REQ_TRACK`、航班当前态 `FLIGHT_SCHD` + 资源明细表 + `FLIGHT_ROUTE_POINT`、留痕 `SCHD_SNAP_LOG` | 本系统唯一业务数据库。消息处理、状态推进、处理终态、回填意图与待发事件在单事务内原子提交;本地事务只在此库。记录级定义见 [implementation.md](implementation.md)「持久化记录」。 | +| 共享 MySQL | `CMINMSGS` 入站信箱、`COUTMSGS` 出站信箱 | 外部系统所有。本系统仅执行约定的信箱读写与处理标记回填,不建表、不迁移 schema、不写历史表;由库方按 `Q9` 执行的清除与历史归档见 [specification.md](specification.md)「契约」。 | -**不使用跨库事务。** PG 事务只能保证“处理结果与待发事件一起提交”(`INV-17`),不能覆盖 MySQL 回填或 Kafka 发送等外部副作用。跨存储依靠幂等、重试和持久化补偿恢复;各中断位置的判定与恢复动作见 design.md「中断恢复」。 +**不使用跨库事务。** PG 事务只能保证「处理结果与待发事件一起提交」(`INV-17`),不能覆盖 MySQL 回填或 Kafka 发送等外部副作用。跨存储依靠幂等、重试和持久化补偿恢复;各中断位置的判定与恢复动作见 [implementation.md](implementation.md)「中断恢复」。 对外投递按**至少一次**设计,不承诺端到端恰好一次。Kafka 生产者幂等不能消除应用重启或 outbox 重发带来的所有重复。 ## 7. 关键决策 -仅保留仍具约束价值、且无法从正文(§4–§6、design.md)直接推出的决策,按 D1–D4 连续编号供正文与 design.md 引用;其余曾编号条目(严格 FIFO、stub 门控、本地事务、UNSUPPORTED 处理等)已在正文以约束形式表达,不再重复列表。第三列只给证据与缺口指针;交付状态与可声明性见 invariants.md 声明边界与 Plane,决策不因状态变化而撤销。 +仅保留仍具约束价值、且无法从正文与 [implementation.md](implementation.md) 直接推出的决策,按 `D1`–`D4` 连续编号;其余曾编号条目(严格 FIFO、stub 门控、本地事务、UNSUPPORTED 处理等)已在正文以约束形式表达,不再重复列表。第三列只给证据与偏差指针;可声明性见 [specification.md](specification.md),决策不因状态变化而撤销。 -| 编号 | 决策及理由 | 证据 / 缺口 | +| 编号 | 决策及理由 | 证据 / 偏差 | |---|---|---| -| D1 | 航班清场只在历史写入成功后进行,未接通时删 0 条;未经 FDEL 的清场须先补发删除事件。ES 历史投影(阶段 B)暂缓。 | 红线见 flight-state.md「生命周期与开放项」;历史写入与删除事件之间仍无恢复方案(见 design.md「生命周期与清除」) | -| D2 | 动态状态单写者,生产只允许一个活动实例;多实例必须先具备可靠的排他保护。 | 事务行锁见 design.md「事务边界」;实例级排他属部署前提 `PRE-5`,可声明性见 `CLM-6` | -| D3 | Kafka 生产要求 `acks=all`、`enable.idempotence=true`、`max.in.flight=1`;不允许通过关闭幂等来满足生产接入。 | 参数默认值与 D3 不一致 `[G-KAFKA-D3]`(取值见 reference 参数表) | -| D4 | 自有库终态记录只归档到 `PROC_STATE_HST`,不侵入共享库的表结构或保留策略。 | 目标表未建 `[G-PROC-HST]` | - -## 8. 部署、切换与运维 - -**部署与安全** - -- 生产维持单活动实例,停机时停止接收新任务并等待工作线程退出。已有事务级行锁,但消息认领和整个实例的排他保护尚未完成,不能依靠行锁宣称支持双实例 FIFO。 -- 配置、口令和环境端点通过环境变量提供。兼容写接口沿用内网信任模式,缺少鉴权,必须限制网络访问;管理端点不得直接暴露到生产外网。 -- Eureka 用于服务发现,Logstash 接收结构化日志;日志出口故障不应阻塞业务处理。 -- 默认不自动启动管道:真实 PG、共享信箱与出站适配必须显式开启(键见 [reference.md](reference.md) 参数表)。 - -**替换旧系统** - -采用“影子对拍 → 切流 → 旧系统冻结”。共享信箱不能让新旧系统同时认领和回填;影子输入使用只读水位或回放。影子环境须隔离 PG schema/实例、Kafka topic 和服务注册身份,并禁止误写生产信箱。切流时保证只有一个权威写者。 - -**可观测性要求** - -用消息 ID、事件 ID 关联处理与投递日志;健康检查反映依赖实际可用性,而不只是进程存活;死信与一致性异常需要可执行的告警与重放流程,不能只留一条错误日志。指标名、取数规则与作业健康见 [reference.md](reference.md)「指标与健康」。 +| D1 | 航班清场只在历史写入成功后进行,未接通时删 0 条;未经 FDEL 的清场须先补发删除事件。ES 历史投影(阶段 B)暂缓。 | 红线见 [implementation.md](implementation.md)「生命周期」 | +| D2 | 动态状态单写者,生产只允许一个活动实例;多实例必须先具备可靠的排他保护。 | 事务行锁见 [implementation.md](implementation.md)「事务边界」;实例级排他属 `PRE-5`,可声明性见 `CLM-6` | +| D3 | Kafka 生产要求 `acks=all`、`enable.idempotence=true`、`max.in.flight=1`;不允许通过关闭幂等来满足生产接入。 | 取值见 [reference.md](reference.md) 参数表 | +| D4 | 自有库终态记录只归档到 `PROC_STATE_HST`,不侵入共享库的表结构或保留策略。 | `G-PROC-HST` | diff --git a/docs/contracts.md b/docs/contracts.md deleted file mode 100644 index 20e814d..0000000 --- a/docs/contracts.md +++ /dev/null @@ -1,92 +0,0 @@ -# 对外契约与待确认事项 - -本文件是本系统与外部对手方之间**承诺与要求**的唯一出处,也是待确认事项(`Q`)的唯一台账。条款编号 `C-x`、问题编号 `Q-x` 稳定不变;条款被取代时标 `[作废 by C-y]` 并保留原文,不静默改写。 - -读者:库方(共享 MySQL 管理方)接口人、上游(CIIMS / AODB / SIS)接口人、本系统开发与运维。 - -条款状态词只有三种:`[待确认 Q-x]`(未取得对方书面确认)、`[已确认 YYYY-MM-DD]`(对方书面确认且已回写)、`[我们单方承诺]`(不依赖对方,已生效)。 - -## 术语 - -| 术语 | 含义 | -|---|---| -| 上游 | 向信箱写入报文的源头系统(CIIMS、AODB 等)。 | -| 信箱 | 共享 MySQL 的入站表 `CMINMSGS`;出站方向为 `COUTMSGS`。 | -| 库方 | 共享 MySQL 的管理方;表结构变更与数据清除只能由库方执行或书面授权。 | -| 处理标记 | 信箱行上表示「本系统已处理」的约定字段;逻辑名 `DATE_PROCESSED` / `STATUS`,实际列名以库方契约为准。本系统只把空标记写成已处理值,不回撤、不覆盖。 | -| 落信 | 报文进入信箱(`CMINMSGS` 存在该行),执行方是上游。 | -| 入队 | 本系统在自有 PG 建立 `PROC_STATE` 记录,开始处理。 | -| 已回填 | 本系统已把处理标记写回该信箱行。 | -| 投递确认 | 投递目标已接受且本地 `MSG_EVENT` 已置 `SENT`;不表示业务消费者已消费。 | -| 自有 PG | 本系统唯一的业务数据库 PostgreSQL;与信箱之间不存在跨库事务。 | - -## A. 共享信箱(库方) - -### A.1 ID 与可见性 - -- **C-1** ID 单调:信箱 ID 按提交顺序分配,已发布水位之下不再出现更小的新 ID。`[待确认 Q2]` -- **C-2** ID 分配 → 事务可见时延上界由库方**直接给出**。该值决定空洞老化阈值;**不可由 SIS 报文 `Expiry` 推导**(`Expiry` 是报文保留与传输恢复口径,与「ID 分配后多久对读事务可见」不是同一个量)。`[待确认 Q2]` -- **C-3** ID 空间不复位、不复用、不回退:含表轮换、备份恢复、`AUTO_INCREMENT` 归零。采用整表轮换方案时,新表种子必须 ≥ `max(ID)+1`,保证 ID 不断链;本系统的水位 `W` 是不可逆单游标,ID 回退会导致其后所有行永久不可见。`[待确认 Q2]` -- **C-4** 报文行不可变:同一业务身份(`SNDR|TYPE|STYP|SEQN`)的重发必为同一内容。若上游会以同一身份改发正文,需要另定识别规则(`Q15`)。`[待确认 Q15]` - -### A.2 保留与清除(标记、保留期、清除前提) - -- **C-5** 处理标记值集与写权限:本系统只写入库方认可的 legacy 值集内的「已处理」值(默认 `PROCESSED`),只写空标记、不回撤、不覆盖;内部原因(死信、重复、放弃)记录在自有 PG,**不在信箱新增枚举**。`[待确认 Q7]` -- **C-6** 清除语义必须是「标记 + 保留期」:打标本身不触发清除,触发条件是「到达保留期 `R_keep`」且「边界内全部行已打标」。若库方语义是「打标即可清除」,则清除前置条件不成立,且**增大 `R` 无法补救**,必须另行约定保留期或引入独立原文保留通道。`[待确认 Q7][待确认 Q9]` -- **C-7** 保留期下界(本文件是唯一定义处): - `R_keep ≥ max(人工重放期限 + 人工处置期限, 审计期限, 回填重试上限)`。 - 这是「重放窗口内原文仍在」的**唯一保证来源**。报文在 CIIMS 的 `Expiry`(480 分钟量级,SIS §3.16)可作为原文保留期的参照,但它是报文有效期,不等于本处所需的保留期。`[待确认 Q6][待确认 Q9]` -- **C-8** 清除前置条件(本文件是唯一定义处):执行清除时,边界内**每行必须已有终局**——即「已持有处理标记」**或**「已登记在本系统的回填放弃清单中且经人工对账确认」。放弃行不写标记,未达终态的行顺延至处理完成后清除;本系统不执行 DDL,也不写共享历史表。`[待确认 Q7][待确认 Q9]` -- **C-9** 清除执行方与方案:清除由库方执行或书面授权执行。方案 A(按 `DATE_RECEIVED` 日分区 + `TRUNCATE/DROP PARTITION`)为首选;方案 B(`CREATE TABLE ... LIKE` + 保留窗复制 + `RENAME TABLE` + 对账 + `CMINMSGS_HST` 归档 + `DROP`)为备选。现场 MySQL 版本与分区 DDL 能力待确认。`[待确认 Q9]` -- **C-10** 时间语义:时间比较与换算统一采用机场时区 `Asia/Shanghai` 及明确类型转换;`DATE_RECEIVED` 由上游/库方写入,其时钟基准需可解释(见 `PRE-4`)。`[待确认 Q7]` - -### A.3 原文保留与重放 - -- **C-11** 重放窗口内的原文必须可读:legacy 现役按接收超 1 天归档并删除 `CMINMSGS`;若沿用该窗口,则与 `C-7` 冲突,须以 `C-7` 为准。`[待确认 Q9]` -- **C-12** 若原文被提前清除(违反保留契约),本系统的死信处置不变,按契约违例走运维追责;该情形不改变 `C-8` 的清除前提。`[我们单方承诺]` - -### A.4 我们向库方的承诺 - -- **C-13** 只读约定区间的信箱行(`ID > W`),单活动实例运行,不引入并行消费者。`[我们单方承诺]` -- **C-14** 不建表、不改表结构、不迁移 schema、不写共享历史表;兼容 HTTP 入口按既有契约写入入站信箱。`[我们单方承诺]` -- **C-15** 处理标记只写 `C-5` 认可的值,不回撤、不覆盖已有非空标记。`[我们单方承诺]` -- **C-16** 回填放弃清单在对应信箱边界被清除前必须保持可查:`C-8` 以本清单作为清除授权证据之一,该证据不得随处理记录的归档或清除而消失。`[我们单方承诺][待确认 Q7][待确认 Q9]` - -## B. 上游(SIS / AODB) - -- **C-20** 业务身份四元组 `SNDR|TYPE|STYP|SEQN` 的语义由上游定义;`SEQN` 的取值范围与回绕见 SIS §2.8.1。**重置周期未知**,它决定业务身份是否加入日期边界(默认不加)。`SNDR` 取值域也需对拍(SIS 为 AODB/RMS,legacy 实发 OSH5 等)。`[待确认 Q11]` -- **C-21** `FLID` 在保留期内不复用。若复用,事件版本(`STATE_VERSION`)必须按 incarnation 作用域,否则「保留最新版本」的合并规则会把新航班的事件压掉,旧 tombstone 也可能删掉在用航班。`[待确认 Q16]` -- **C-22** 报文不可变(同 `C-4`)。`[待确认 Q15]` -- **C-23** 请求/应答回显契约:目标优先按已确认的回显字段精确匹配;回显未确认时的降级匹配(同类开放请求且报文 `DTTM ≥ sentAt`)存在跨代误配风险,必须明确接受并审计,不得宣称精确关联。比较前统一时区与时间单位。`[待确认 Q5]` -- **C-24** 出站信箱 `COUTMSGS`:消费方与消费顺序、`COUTMSGS_ACK_DATE_RECV` / `COUTMSGS_ACK_RESEND_TIMES` / `COUTMSGS_DATE_SENT` / `COUTMSGS_ERROR` 各列语义与写入责任、出站行清除责任与保留期、落信成功但本地未置 `SENT` 时的重复写入风险及下游去重契约,均未确认。本系统对出站的交付承诺只到**落信**为止。`[待确认 Q10]` -- **C-25** 主 / 共享删除顺序与 EROR 回报:SIS 要求删主航班前先删子共享航班,顺序不符时 RMS 应向 AODB 回发 EROR(SIS §1.6.1-1.d,事件定义 SIS §4.8);现行设计为幂等原子级联、不回发 EROR。二选一。`[待确认 Q14]` -- **C-26** 日计划缺失可选字段的语义:SIS 要求最新日计划中未发送的可选字段表示 AODB 已无该数据、子系统应删除本地值(SIS §3.16 注释 4,RESP 同格式见 §3.17),与现行「未携带字段保留」相反。`[待确认 Q13]` -- **C-27** 历史积压批次中「不再处理」的确认主体、审批留痕与跳过值集。`[待确认 Q12]` - -### B.1 我们向上游的承诺 - -- **C-28** 兼容 HTTP 入口的响应只表示**接收结果**,不表示业务处理成功:目标为现役 `ResponseDto`(`is_success` / `body`),请求体上限暂定 10MB;请求媒体类型、字符集与失败响应仍需与现役逐项对拍。`[待确认 Q3]` -- **C-29** 对外投递按**至少一次**设计,不承诺端到端恰好一次;Kafka 消息的 key 为 `FLID`,同一 `FLID` 内保序,跨 `FLID` 不承诺顺序。`[待确认 Q4]` - -## C. 待确认事项台账(Q) - -| 编号 | 事项 | 当前假定 | 阻塞 | 状态 | -|---|---|---|---|---| -| Q1 | 权威存储(内部方向) | 自有 PG 单库权威 + 无损明细;现场供库目标 Oracle 11g | — | 已定案(内部),Oracle 适配与部署验收另计 | -| Q2 | 信箱 ID 单调、ID 分配→事务可见时延上界、ID 空间不复位;空洞与迟到处置 | 时延按 5 分钟 `max-commit-delay`(**缺少依据的占位值**,不可由 SIS `Expiry` 推导) | 发现完整性声明、空洞老化阈值、水位不可逆性 | 未确认 | -| Q3 | HTTP 契约:媒体类型、字符集、错误码、查询接口对拍 | 目标与上限见 `C-28` | 兼容入口验收 | 未确认 | -| Q4 | Kafka wire:发送粒度、key、去重标识、分区与批次确认 | 逐 `FLID` 发送,key=`FLID` | 投递契约 | 未确认 | -| Q5 | 请求匹配:回显字段可靠性与降级匹配 | `RQFD` 60 秒 / `RQRD` 30 秒超时 | 请求跟踪闭环 | 未确认 | -| Q6 | 重放期限与人工处置期限的取值(唯一作用是决定 `R_keep` 下界) | `R` = 30 天;重放/处置期限未定 | `R_keep` 取值 | 未确认 | -| Q7 | 处理标记值集与写权限、原文保留期、处理时间语义 | 写入 `PROCESSED` | 回填值集、保留期下界 | 未确认 | -| Q8 | 逐类覆盖清单(积压摸底的类型分布依据) | — | 积压处置与逐类矩阵 | 暂缓(现阶段不处理) | -| Q9 | 清除执行方与 DDL 授权、方案 A/B 选型、分区能力 | 首选方案 A | `R_keep` 与清除边界 | 未确认 | -| Q10 | 出站消费方、ACK 列语义、出站清理与去重契约 | — | 出站信箱 | 未确认 | -| Q11 | 上游 `SEQN` 重置周期与业务身份的日期边界 | 不含日期边界 | 身份算法 | 未确认 | -| Q12 | 积压批次「不再处理」的确认主体、审批留痕与跳过值集 | — | 积压跳过处置 | 未确认 | -| Q13 | 日计划缺失可选字段的删除语义 | 保留未携带字段;真实消息与 SIS 冲突时以真实消息为准 | 快照合并 | 暂缓(测试前不处理) | -| Q14 | 主/共享删除顺序与 EROR 回报义务 | 幂等原子级联 | 出站事件类型 | 未确认 | -| Q15 | 上游是否会以同一业务身份改发正文(决定是否需要区分「重复」与「改发」) | 假定期望不可变(`C-4`) | 身份去重语义 | 未确认 | -| Q16 | `FLID` 重用语义(决定版本是否按 incarnation 作用域) | 假定不复用(`C-21`) | 事件合并与 tombstone | 未确认 | - -Q 的答复只在本表就地更新(补「结论」与日期),并触发 [README.md](README.md)「维护清单」的落地 4 步;不另开文件。 diff --git a/docs/flight-state.md b/docs/flight-state.md deleted file mode 100644 index 48f09ac..0000000 --- a/docs/flight-state.md +++ /dev/null @@ -1,128 +0,0 @@ -# 运营航班状态设计 - -本文是航班状态的唯一现行设计规范。它定义权威数据、处理语义、生命周期和对外投递;其他文档只描述管道或产品验收,不重复定义航班状态规则。 - -## 1. 目标与边界 - -系统从共享 MySQL 信箱接收 SIS/AODB 报文,把结果合并到自有 PostgreSQL 中的航班当前态,再通过 outbox 投递 Kafka。共享信箱和 Kafka 都不是状态权威,也不在本地事务的提交范围内。 - -- `FLID` 是航班实例的唯一标识;不得由航班号、日期或资源号推断身份。 -- `FLIGHT_SCHD` 及其明细表是唯一权威当前态;展示视图只读,不能作为写入或对账来源。 -- 单活动主泵按信箱 FIFO 推进。事务内 `PIPELINE_LOCK` 只串行化本地状态提交,不替代选主或消息认领。 -- 状态写入、outbox 事件、处理终态和回填意图在同一 PostgreSQL 事务中提交;回填与 Kafka 投递在提交后独立重试。 - -现场目标库为 Oracle 11g;当前已验证的实现基准是 PostgreSQL。Oracle 适配未完成前,不能把它视为可切换的运行时选项。 - -## 2. 权威模型 - -| 对象 | 职责 | -|---|---| -| `FLIGHT_SCHD` | 一行一个 `FLID`,保存标量字段、`STATE`、`STATE_VERSION`、`OPERATION_DAY`、最近消息 ID 和审计时间。 | -| 资源明细表 | 当前 8 张表保存登机门、值机柜台、转盘、计划机位、滑槽、延误、靠撤桥、轮挡等变长集合;`SRVT`/`VIPF` 专用明细尚未实现 `[G-SRVT-VIPF]`。主键为 `(FLID, ORDINAL)`。 | -| `FLIGHT_ROUTE_POINT` | ROUT 与 ERUT 两类路线点,使用 `ROUTE_KIND` 区分;主键应包含该列,避免两类路线的序号冲突。 | -| `PROC_STATE` | 信箱消息的处理终态、业务身份幂等记录,以及回填事实(`RECEIVED_AT` / `BACKFILL_*`)。 | -| `MSG_EVENT` | 事务 outbox,承载整态投影、变更通知和删除 tombstone。 | -| `INBOX_CURSOR` | 共享信箱消费水位(读取进度,与处理标记互不替代)。 | -| `SCHD_SNAP_LOG` | 日计划处理留痕,只追加、可重建,不参与状态决策。 | - -### 2.1 航班身份与运营日 - -`FLID` 是主键。`OPERATION_DAY` 从 SCHD 记录的 `SODT` 按配置的机场时区和切日规则推导;它不是消息接收日或落库日。 - -一旦已写入非空 `OPERATION_DAY`,同一 `FLID` 不得改到另一个运营日。遇到冲突,整包日计划按协议错误拒绝,既有状态保持不变。尚未由日计划收录的航班可以为 `NULL`;这不表示该航班没有运营日,只表示当前模型无法为它确定归属日。 - -### 2.2 字段与集合 - -标量与异常对象前缀字段存于主表。协议中的 `SRVT`、`VIPF` 是无界集合,目标形态必须按集合完整保存到专用明细表示;当前只在 wire/domain 保留其出现事实与原始内容,专用明细、合并与投递尚未实现 `[G-SRVT-VIPF]`。`MAFL` 不是 SIS/XML 入站字段,而是由共享航班的 `MAID`、`FLID`、`FLNO` 生成的主航班派生投影;当前尚未实现 `[G-MAFL]`。 - -- `ORDINAL` 是持久化顺序,从 1 开始;`SOURCE_SEQ` 是上游序号,允许为空或重复。 -- 相同资源号不代表同一条分配,禁止按资源号去重。 -- 每次持久化完整航班状态时,明细表按该 `FLID` 先删后插,以完整合并结果为准。 -- ROUT 与 ERUT 是两类独立集合,不能因相同序号覆盖彼此。 -- 主/共享关系以主表的 `MAID` 为事实来源:`MAID` 是共享航班指向主航班 `FLID` 的引用(非共享航班为 `NULL`);`MAFL` 只在读取和事件投影时从子航班事实派生,不按入站标量解析或保存。 - -### 2.3 主/共享投影(`MAFL`) - -`MAFL` 是主航班的派生集合,元素为子航班的 `FLID` 与 `FLNO`;内容与变更传播分别由 `INV-21`、`INV-22` 保证。 - -- 子航班集合 = `STATE = ACTIVE` 且 `MAID = 主航班 FLID` 的 `FLIGHT_SCHD` 行;已 FDEL 的子航班(`STATE = DELETED`)自然退出投影,不需要改写主航班行。 -- 只有 `MAID` 为空的主航班携带 `MAFL`;共享航班只携带自身 `MAID`、`CSOP`、`CSFT`,不携带 `MAFL`,避免下游双向合并。 -- 投影按 `FLID` 升序,与到达顺序及 `FLNO` 变更无关:同一 `STATE_VERSION` 的投影逐字节稳定,重发与消费端比对才有意义。 -- `MAID = FLID` 的自引用行不进入任何 `MAFL`;`MAID` 指向不存在主航班的悬挂引用不阻断该子航班自身处理,只是不产生投影。 -- 子航班集合变化(新增、删除、`MAID` 迁移)必须让涉及的主航班在同一事务内推进 `STATE_VERSION` 并登记主航班事件(`KAFKA:msg` + `KAFKA:schd`);否则整态投影的只进不退写入会丢弃它(`design.md`「`schd` 聚合」)。共享航班自身不单独发通知。 -- 派生主航班投影与产生它的状态写入必须同一事务或一致读快照;按 `MAID` 取子航班要求该列有索引(`INV-17`)。 - -## 3. 合并与写入语义 - -领域决策逻辑(如 `FlightStateEngine` 及各类 Handler 规则)保持纯粹:它根据当前完整态和已解码报文,返回下一完整态与待发事件,不执行数据库或 Kafka I/O。`ScheduleProcessor` / `FlopProcessor` / `FdelProcessor` / `AdftProcessor` 是事务协调器,负责在统一事务边界内调用决策逻辑并持久化结果。 - -### 3.1 SCHD 日计划 - -SCHD DNLD/RESP 在整包校验通过后,逐条将报文携带的航班写入当前态。日计划只更新或创建其携带的 `FLID`,**不会因其他航班未出现在本次报文中而删除任何记录**;SIS 同向(§3.16 注释 1 要求子系统自行保留前一日延误航班)。 - -日计划在重叠字段上可以覆盖当前动态值;未携带的字段按合并规则保留,显式清空才清除。每个成功写入的航班推进 `STATE_VERSION`,并在同一事务登记 `KAFKA:schd` 与 `KAFKA:msg` 事件。 - -**字段缺失语义与外部规范冲突**:SIS 规定最新日计划中未发送的可选字段表示 AODB 已无该数据、子系统应删除本地已有值(`SIS_AODB_RMS-V0.1.md` §3.16 注释 4;RESP 与 DNLD 同格式,见 §3.17),并要求以 AODB 最新数据覆盖本地(SIS §1.6.2)。这与上面的"未携带字段保留"相反。确认前两条并存,按 Q13 跟踪,不得据本节推定已与上游对齐。 - -消息重放由 `PROC_STATE` 的消息 ID 与 `identity_key` 控制;已成功提交的消息不得再次写入或重复登记事件。整包校验失败或运营日冲突时,整包不落地。 - -DNLD、RESP 和 ADFT 均已有路由入口。RESP 的请求匹配闭环、出站请求与超时重发仍是待交付项;未注册处理器不能被视为已实现。 - -### 3.2 动态运行事件 - -FLOP 事件只修改它表达的字段或资源集合,其余航班状态保持不变。每个动态子类型的语义都必须有明确 Handler 规则和回归测试,不能只因已被路由就推定其业务语义完整。 - -动态事件保留既有 `OPERATION_DAY`,也不基于接收时间重新推导它。未知或已删除航班的具体处理遵从对应 Handler 的幂等规则。 - -### 3.3 删除与重建 - -FDEL 是业务删除入口:仅在 `ACTIVE → DELETED` 时推进版本、保留明细并与 tombstone 同事务登记;重复 FDEL 或不存在的航班按幂等成功处理。 - -物理删除仅由独立历史清理在归档成功后执行。日计划报文不是删除依据。若未经 FDEL 而由生命周期清理,清理前需要登记一次 tombstone;已经 FDEL 的记录不重复发出。 - -ADFT 的字段缺失语义尚待上游确认。在确认前采用保守的 Set-only 规则:出现字段可更新,缺失字段不清空;不得把它当成日计划或动态全量替换。新建 ADFT 若带可解析的 `SODT`,按同一运营日规则计算 `OPERATION_DAY`;否则保留为 `NULL`。 - -主/共享航班级联:删除共享航班时重算主航班 `MAFL`(见「主/共享投影」)并向主航班通知;删除主航班时级联删除其子共享关联并发出删除通知;主/共享关系必须一次原子变更,不出现主已删、子残留的半状态。共享航班增量通常只更新并通知主航班,不直接发共享通知。这些语义同样约束 FDEL 之外的生命周期清理。主/共享关联的增删按 `FLID` 做值比较,不使用引用比较。 - -SIS 规定删除主航班时必须先删子共享航班、再删主航班,顺序不符时 RMS 应向 AODB 回发 EROR(`SIS_AODB_RMS-V0.1.md` §1.6.1-1.d,事件定义见 SIS §4.8)。本文的原子级联不发该回报,两者取舍见 Q14。 - -## 4. 处理事务与失败规则 - -所有状态处理遵循下列顺序: - -1. 主泵只处理 FIFO 队头,解码并绑定业务身份。 -2. 校验消息结构、声明数量、航班标识和运营日;协议错误标记 `DEAD`,不提交半包。 -3. 在一个事务中取得 `PIPELINE_LOCK`,读取当前完整态,计算下一状态,写主表和明细表,登记 outbox 和回填意图。 -4. 同一事务提交处理终态;任一步失败则整体回滚并按错误类别重试或终止。 -5. 提交后回填共享信箱并异步投递 outbox;后续失败不得把已提交的 `SUCCEEDED` 改回失败。 - -`identity_key` 由 `发送方|类型|子类型|序号` 构成,用于业务重复检测。`STATE_VERSION` 是单航班的单调版本,供下游判定新旧;它不是日计划版本,也不表示运营日版本。 - -## 5. Kafka 与读取 - -`KAFKA:schd` 是按 `FLID` 的完整状态投影。Dispatcher 可以合并同一 `FLID` 尚未投递的中间版本,只发最新状态;消费端用 `(FLID, STATE_VERSION, UPDATED_AT)` 防止旧投影覆盖新状态。 - -`KAFKA:msg` 只通知变化,不承载权威状态;两个 topic 不承诺顺序。FDEL 和必要的生命周期清理使用 tombstone:键为 `FLID`,删除记录以 null 值投递,通知下游移除旧状态。 - -读取完整航班需要读取主表和全部明细。当前逐航班读取有多次查询,尚未保证跨表一致性快照;批量加载与明确的一致性读边界是后续优化项。 - -## 6. 生命周期与开放项 - -运营日过去不等于航班结束。历史清理须同时满足配置保留期与终态证据或足够静默期,先成功写入历史存储,后物理删除当前态;历史存储失败时必须删除零行。 - -以下事项仍需确认或交付: - -- Oracle 11g 的完整方言与集成验证; -- RESP 请求—应答匹配和出站请求重发; -- ADFT 缺失字段和 `FLID` 重用的上游语义(`FLID` 复用见 `Q16`); -- 日计划缺失可选字段的删除语义(与 SIS 的冲突见本文件「合并与写入语义」,`Q13`); -- 主/共享删除顺序与 EROR 回报义务(Q14); -- `MAFL` 投影是否出现在查询视图,随 `Q3`;字段名与空集合表示随 `Q4`; -- 未归属 `OPERATION_DAY` 航班的终止与保留策略; -- 航班历史存储自身的保留期与容量上限:`FLIGHT_SCHD` 物理清除后它是唯一副本 `[G-FLIGHT-HIST-RETENTION]`; -- ROUT/ERUT 联合主键迁移; -- 批量一致性读取,以及动态事件仅在事务内计算一次的收敛。 - -## 7. 不变量 - -航班域不变量的**定义处是 [invariants.md](invariants.md)**(`INV-11`–`INV-20`),本节不再重复。 diff --git a/docs/design.md b/docs/implementation.md similarity index 64% rename from docs/design.md rename to docs/implementation.md index b598df5..3407017 100644 --- a/docs/design.md +++ b/docs/implementation.md @@ -1,17 +1,16 @@ -# msgexchange-v2 设计文档(管道机制) +# 实现设计 -本文件定义**管道机制**:记录模型、状态机、收报与水位、主泵与事务边界、回填、投递、失败恢复与维护作业。 +本文件是实现设计的唯一出处,分两章: -- 系统边界、模块职责、存储归属、架构决策:architecture.md -- 前提 `PRE-x`、不变量 `INV-x`、声明边界 `CLM-x`:invariants.md -- 对外承诺与要求 `C-x`、待确认事项 `Q`:contracts.md -- 参数、指标、模块入口、错误分类:reference.md -- 航班域模型与合并语义:flight-state.md +- **处理管道**章:记录模型、状态机、收报与水位、主泵与事务边界、回填、快照与请求、投递、失败恢复与维护作业; +- **航班域**章:航班当前态的权威模型、合并与写入语义、删除与重建。 -正文描述**目标设计**。交付状态不在正文标注:主张能否对外声明记在 invariants.md 的声明边界,实现进度在 Plane(ACM2)。 +正文描述**目标设计**,不标注交付状态:可声明性见 [specification.md](specification.md)「声明边界」与「当前已知偏差」,进度在 Plane(ACM2)。契约与不变量只引稳定 ID;参数取值只引 `PARAM:<完整键>`(见 [reference.md](reference.md))。 ## 1. 术语与持久化记录 +### 1.1 管道术语 + | 术语 | 语义 | |---|---| | `W`(水位) | 信箱 ID 的连续上界:`(min, W]` 已全部读入自有 PG;只随新 ID 成功入队推进(永久空洞放行是唯一例外)。 | @@ -19,20 +18,22 @@ | 队头 | 最小的未完成消息(`PENDING` 与 `FAILED` 都占位)。 | | 终态 | `SUCCEEDED` / `SKIPPED` / `DEAD`;到达后队列方可推进。 | | 回填意图 | 「还欠一次信箱标记」的持久化事实,与终态同一条语句落库。 | -| `R`、`R_keep`、处理标记 | 定义见 contracts.md(契约数值只在那里)。 | +| `R`、`R_keep`、处理标记 | 定义见 [specification.md](specification.md)。 | + +### 1.2 持久化记录 | 记录 | 用途 | 关键约束 | |---|---|---| -| `PROC_STATE` | 入站消息的处理状态、身份、尝试次数、错误原因与回填事实 | `MSG_ID = CMINMSGS_ID` 主键防重复入队;`IDENTITY_KEY` 唯一约束防业务重复;按最小未完成 `MSG_ID` 取队头;`BACKFILL_NEXT_AT` 非空 = 还欠一次回填,`BACKFILL_AT` 非空 = 标记已确认,`BACKFILL_ABANDONED_AT/REASON` 非空 = 已停止自动重试(**不等于**标记已确认);`RECEIVED_AT` 复制自信箱接收时间、**可能为 NULL**、仅用于对账与展示;`ENQUEUED_AT` 是本地入队时间、非空、是超期判据的唯一依据;归档后的去重影子行置 `STATE='ARCHIVED'`、只保留 `IDENTITY_KEY` 与 `MSG_ID`,不占队头、不触发回填、不参与积压聚合(`[G-PROC-HST]`)。 | -| `MSG_EVENT` | 等待投递的事件(outbox) | `EVENT_ID` 对 `KAFKA:msg` 是稳定事件身份并决定投递顺序;对 `KAFKA:schd` 是每次接受 upsert 时替换的写代次。`TARGET` 区分 `KAFKA:msg` / `KAFKA:schd`;`PARTITION_KEY` 当前取 `FLID`(`Q4` 定案前为假定,见 `C-29`);`EVENT_TYPE` 区分 UPSERT 与 TOMBSTONE。`KAFKA:schd` 按 `FLID` 单行 upsert,只保留最新 `STATE_VERSION`;`SENT_AT` 在投递确认的同一条 UPDATE 内写入,是保留期判定的唯一基准(`[G-EVENT-RETENTION]`)。 | -| `REQ_TRACK` | 上游请求及应答关联 | 状态 `PENDING / SENT / DONE / EXPIRED`;保存请求类型、覆盖运营日、发送方、出站信箱 ID 与发送/完成时间;**「同类只允许一个开放请求」的唯一键 = `(请求类型, 覆盖运营日, 发送方)`,且仅对开放状态生效**。登记、超时与应答匹配尚未实现 `[G-REQ-TRACK]`。 | -| `REF_MASTER` | 静态参考数据(目标表) | `(RTYPE, RKEY)` 唯一;尚未建表,客户端与刷新流程见 user-stories US-13/US-14,US-14 两类映射的存储落点未定。 | -| `FLIGHT_SCHD` | 航班标量及单值异常字段 | `FLID` 主键;`OPERATION_DAY` 一经确定不可变;版本与最近消息 ID 用于追踪。现有变长集合存于 8 张资源明细表与 `FLIGHT_ROUTE_POINT`;`SRVT`/`VIPF` 专用明细尚未实现 `[G-SRVT-VIPF]`,规则见 flight-state.md。 | +| `PROC_STATE` | 入站消息的处理状态、身份、尝试次数、错误原因与回填事实 | `MSG_ID = CMINMSGS_ID` 主键防重复入队;`IDENTITY_KEY` 唯一约束防业务重复;按最小未完成 `MSG_ID` 取队头;`BACKFILL_NEXT_AT` 非空 = 还欠一次回填,`BACKFILL_AT` 非空 = 标记已确认,`BACKFILL_ABANDONED_AT/REASON` 非空 = 已停止自动重试(**不等于**标记已确认);`RECEIVED_AT` 复制自信箱接收时间、**可能为 NULL**、仅用于对账与展示;`ENQUEUED_AT` 是本地入队时间、非空、是超期判据的唯一依据;归档后的去重影子行置 `STATE='ARCHIVED'`、只保留 `IDENTITY_KEY` 与 `MSG_ID`,不占队头、不触发回填、不参与积压聚合(`G-PROC-HST`)。 | +| `MSG_EVENT` | 等待投递的事件(outbox) | `EVENT_ID` 对 `KAFKA:msg` 是稳定事件身份并决定投递顺序;对 `KAFKA:schd` 是每次接受 upsert 时替换的写代次。`TARGET` 区分 `KAFKA:msg` / `KAFKA:schd`;`PARTITION_KEY` 当前取 `FLID`(`Q4` 定案前为假定,见 `C-29`);`EVENT_TYPE` 区分 UPSERT 与 TOMBSTONE。`KAFKA:schd` 按 `FLID` 单行 upsert,只保留最新 `STATE_VERSION`;`SENT_AT` 在投递确认的同一条 UPDATE 内写入,是保留期判定的唯一基准。 | +| `REQ_TRACK` | 上游请求及应答关联 | 状态 `PENDING / SENT / DONE / EXPIRED`;保存请求类型、覆盖运营日、发送方、出站信箱 ID 与发送/完成时间;**「同类只允许一个开放请求」的唯一键 = `(请求类型, 覆盖运营日, 发送方)`,且仅对开放状态生效**。 | +| `REF_MASTER` | 静态参考数据(目标表) | `(RTYPE, RKEY)` 唯一;客户端与刷新流程见 [requirements.md](requirements.md) `US-13`/`US-14`。 | +| `FLIGHT_SCHD` | 航班标量及单值异常字段 | `FLID` 主键;`OPERATION_DAY` 一经确定不可变;版本与最近消息 ID 用于追踪。变长集合存于资源明细表与 `FLIGHT_ROUTE_POINT`,规则见「航班域」。 | | `INBOX_CURSOR` | 消费水位 `W`、空洞计时 `holeSince`、播种事实 `SEEDED_AT` | 单行游标;`W` 只随新 ID 成功入队推进,遇空洞即停;`HOLE_SINCE` 持久化空洞观测时刻,进程重启不丢计时。`SEEDED_AT IS NULL` **不等于**从未消费(已有库新增列后同样为 NULL)。 | -| `SCHD_SNAP_LOG` | 日计划处理留痕 | 只追加、可重建,不参与状态决策;保留期见 reference。 | -| `PROC_STATE_HST` | 终态处理记录的归档目标 | 尚未建表 `[G-PROC-HST]`;只归档到自有 PG 的目标表,不落共享库历史表;自身保留期与清除作业未定义 `[G-HST-RETENTION]`。 | +| `SCHD_SNAP_LOG` | 日计划处理留痕 | 只追加、可重建,不参与状态决策;保留期见 [reference.md](reference.md)。 | +| `PROC_STATE_HST` | 终态处理记录的归档目标 | 只归档到自有 PG,不落共享库历史表(`G-PROC-HST`、`G-HST-RETENTION`)。 | -字段与索引以 `src/main/resources/db/migration/` 的迁移链为准(Oracle 11g 目录为占位,未接入 Flyway)。报文原文仍从共享信箱读取,原文保留期必须覆盖处理与重放窗口(`C-7`)。 +字段与索引以 `src/main/resources/db/migration/` 的迁移链为准(Oracle 11g 目录为占位,未接入 Flyway)。报文原文仍从共享信箱读取,原文保留期必须覆盖处理与重放窗口(`C-7`);清除前提、保留期下界与处理标记值集见 `C-5`~`C-12`。 ## 2. 消息、身份与决策 @@ -44,11 +45,13 @@ 分派与落库由 `MessageProcessor` 协调:按 `MsgKind` 把已绑定身份的队头消息交给对应事务协调器(DNLD/RESP → `ScheduleProcessor`,ADFT → `AdftProcessor`,FLOP → `FlopProcessor`,FDEL → `FdelProcessor`,其余 → `FAILED(UNSUPPORTED)`)。这些处理器在 `PIPELINE_LOCK` 事务内读取当前完整态,调用纯领域决策逻辑得到下一完整态与待发事件,再统一落库并登记回填意图;它们不直接触碰 Kafka。领域决策逻辑不执行 I/O。 +忽略规则:解码后、分派前按大小写不敏感的 `TYPE-STYP` / `TYPE-*` 匹配忽略清单(基线 `LDM` / `REGN` / `RSTA` / `EROR`,不混用 `ERROR`),命中转 `SKIPPED` 并记录 `ignored:`;忽略报文照常绑定身份,但不更新航班、不创建业务通知(`US-04`)。 + ## 3. 状态与错误分类 ```text 处理:PENDING / FAILED → SUCCEEDED(成功) - → SKIPPED(业务重复已实现;忽略 / 无匹配分支见 US-04/US-06,尚未实现) + → SKIPPED(业务重复、忽略、无匹配) → FAILED(等待退避重试) → DEAD(MALFORMED / PROTOCOL / EXHAUSTED,均需人工处置) @@ -59,16 +62,7 @@ `SUCCEEDED / SKIPPED / DEAD` 是处理终态,不再阻塞后续消息;`FAILED` 不是终态,仍占据队头。`DEAD` 表示需要处置,不等于业务成功。`ARCHIVED` 不是处理态:它是终态行归档后留在主表的去重影子行,不占队头、不触发回填、不参与积压聚合(见「生命周期与清除」)。 -| 错误类别 | 处理方式 | -|---|---| -| `MALFORMED` | 报文非法、原文缺失或解码结果缺少该类型必需的业务载荷,直接 `DEAD`,不在重放白名单内。 | -| `PROTOCOL` | 载荷存在但整包违反业务协议(运营日冲突、声明数量不符等),立即 `DEAD`,整包不落地、不重试。 | -| `CODEC_ERROR` | 解码能力问题,退避重试;修复后允许重放。 | -| `UNSUPPORTED` | 处理器或快照能力未实现,按可恢复失败处理,不直接当作非法报文;仍受重试上限约束。 | -| `INFRA` | 基础设施或执行异常,退避重试。 | -| `EXHAUSTED` | 重试耗尽,转 `DEAD`,人工复核后允许重放。 | - -重试次数用尽时统一转 `DEAD(EXHAUSTED)`:`ERROR_CLASS` 被覆写为 `EXHAUSTED`,原始错误类别不再保留(`LAST_ERROR` 保留原因文本)。重放白名单包含 `EXHAUSTED`,这类记录仍可人工重放。 +重试次数用尽时统一转 `DEAD(EXHAUSTED)`:`ERROR_CLASS` 被覆写为 `EXHAUSTED`,原始错误类别不再保留(`LAST_ERROR` 保留原因文本)。重放白名单与错误分类表见 [reference.md](reference.md)「错误分类与重放白名单」。 ## 4. 收报与水位 @@ -88,15 +82,13 @@ `holeSince` 落在 `INBOX_CURSOR.HOLE_SINCE`,进程重启不丢计时。旧空洞补齐后出现的新空洞从新观测时刻重新计时,不继承旧等待时间。 -**代价(必须接受并观测)**:水位遇空洞即停意味着空洞之后的所有消息最多要等一个老化窗口才能入队;自增回滚等会在 ID 序列留下永久空位,每出现一个永久空位就是一次等长的入队停摆,空位频繁时有效吞吐按比例下降。运行期必须观测永久空洞计数与水位滞后(指标见 reference)。 +**代价(必须接受并观测)**:水位遇空洞即停意味着空洞之后的所有消息最多要等一个老化窗口才能入队;自增回滚等会在 ID 序列留下永久空位,每出现一个永久空位就是一次等长的入队停摆,空位频繁时有效吞吐按比例下降。运行期必须观测永久空洞计数与水位滞后(指标见 [reference.md](reference.md))。 -### 4.2 发现完整性依赖与扫描路径 +### 4.2 发现完整性依赖 水位的有效性依赖 `PRE-2`、`PRE-3`(`C-1`/`C-2`/`C-3`)。承诺缺失时 `W` 只是快路径提示,不足以证明该区间收齐。ID 分配 → 事务可见时延上界必须由库方直接给出,**不可由 SIS 报文 `Expiry` 推导**。 -| 路径 | 目的 | 谓词 | 状态 | -|---|---|---|---| -| 快路径(日常) | 发现水位之后的新消息 | `ID > W ORDER BY ID ASC LIMIT claim-batch` | 已实现 | +日常扫描只有一条快路径:`ID > W ORDER BY ID ASC LIMIT claim-batch`。 ### 4.3 切流播种 @@ -138,19 +130,20 @@ processOne(head): 已被本消息占用 → 继续 已被别的消息占用 → SKIPPED(duplicate-of:),结束 空闲 → 写入 IDENTITY_KEY(独立单语句,不参与业务事务) - 4. 按 MsgKind 分派: + 4. 忽略规则命中 → SKIPPED(ignored:)(非业务型终态) + 5. 按 MsgKind 分派: SCHD-DNLD / SCHD-RESP → ScheduleProcessor(快照事务) SCHD-ADFT → AdftProcessor(单航班事务) FLOP / FDEL → Flop / FdelProcessor(单航班事务) Unsupported → FAILED(UNSUPPORTED) 退避 载荷缺失 → DEAD(MALFORMED) 整包协议拒绝 → DEAD(PROTOCOL),不落半包 - 5. 业务型成功:处理器在自己的事务内写航班变更 + 待发事件 + SUCCEEDED + 回填意图 - 6. 结束:主泵不做回填;回填意图已随终态落库,由扫描补写信箱标记 + 6. 业务型成功:处理器在自己的事务内写航班变更 + 待发事件 + SUCCEEDED + 回填意图 + 7. 结束:主泵不做回填;回填意图已随终态落库,由扫描补写信箱标记 ``` - 原文缺失归为 `MALFORMED`;读取异常按基础设施失败进入重试,与「原文缺失」区分。 -- 忽略规则(`LDM / REGN / RSTA / EROR` → `SKIPPED`)尚未实现(`[G-IGNORE]`,US-04);类型未覆盖不等于报文非法:忽略类报文在规则实现前不按 `MALFORMED` 处理。 +- 类型未覆盖不等于报文非法:忽略类报文在规则实现前不按 `MALFORMED` 处理。 - **主泵不回填**:终态与回填意图由同一条 UPDATE 落库,回填一律由扫描驱动,不占用 FIFO 关键路径。回填只需消息 ID,缺 META 或解码失败的死信同样可补写。影子环境禁写。 ### 5.3 事务边界 @@ -175,7 +168,7 @@ processOne(head): - 不加速、不分流、不走旁路:不允许并行队头,也不允许实时消息跳过积压。 - 尝试上限与退避对积压同样生效,不因积压而放宽。 - 经确认不再处理的行置 `SKIPPED` 并记录原因,到达终态后走回填通道;不存在「整段 DELETE」的快速通道(授权与留痕见 `C-27`/`Q12`)。 -- 消化期间的可观测项与完成时限口径见 reference 与 CLM-9:**扫描周期不是完成时限**。 +- 消化期间的可观测项与完成时限口径见 [reference.md](reference.md) 与 `CLM-9`:**扫描周期不是完成时限**。 ## 6. 回填 @@ -209,7 +202,7 @@ LIMIT PARAM:msgx.pipeline.backfill-batch ### 6.3 `R` 的作用 -`R`(契约值见 contracts)有两个作用: +`R`(取值见 [reference.md](reference.md))有两个作用: 1. **取消退避**:已终态但超期未打标的行,每轮扫描都被尝试,不再等退避到期; 2. **暂时性故障的放弃期限**:超时、连接失败这类暂时性故障**在 `R` 之前只退避重试、不放弃**;到 `R` 仍未打标才停止自动重试、记入放弃清单并告警。 @@ -218,13 +211,13 @@ LIMIT PARAM:msgx.pipeline.backfill-batch 关于「最终一定打标」,准确表述是三段,缺一不可: -1. 退避重试(`R` 之前不放弃,参数见 reference); +1. 退避重试(`R` 之前不放弃); 2. 到 `R` 仍失败则停止自动重试、告警,进入放弃清单,保留人工恢复(`reopen`); 3. `C-8` 允许以「放弃清单 + 人工确认」作为清除判定,避免一行永久卡住整个分区。 两个边界要说清:`MISSING_ROW`(信箱行不存在)是**确定性结论**,立即放弃,不受 `R` 保护;`R` 仍然**不保护重放窗口**——`R` 与 `R_keep` 只要求 `R ≤ R_keep`,重放窗口的唯一保证来源是 `C-7`。 -重放窗口的保护只有两条路:约定保留期(`C-6` + `C-7`,目标前提),或另设原文保留通道(`[G-REPLAY-CHANNEL]`,尚未设计)。若库方清除语义是「打标即可清除」,则当天打标的原文当天即可被清除,增大 `R` 无效。 +重放窗口的保护只有两条路:约定保留期(`C-6` + `C-7`,目标前提),或另设原文保留通道(`G-REPLAY-CHANNEL`)。若库方清除语义是「打标即可清除」,则当天打标的原文当天即可被清除,增大 `R` 无效。 ## 7. 日计划快照与请求匹配 @@ -237,13 +230,13 @@ LIMIT PARAM:msgx.pipeline.backfill-batch 3. **事务写入**:锁内按 `FLID` 点查归属日,发现同一航班跨运营日即整包回滚并 `DEAD(PROTOCOL)`;通过后合并写主表与资源明细。报文未携带的航班不因本次日计划报文被删除。 4. **提交结果**:同一事务保存 `KAFKA:schd` / `KAFKA:msg` 事件、置消息 `SUCCEEDED` 并预登记回填意图;提交后信箱回填由扫描承接,留痕在事务外追加。 -消息重放由 `PROC_STATE` 的消息 ID 与业务身份控制;版本号不能单独证明消息身份。 +字段缺失与清空语义、运营日规则见「航班域」。 -**应答守卫 `[G-RESP-GUARD]`**:`RESP` 应匹配开放请求(无匹配、过期或报文早于发送时间则不更新快照);当前 `RESP` 与 `DNLD` 无差别进入快照写入,因此当前不构成匹配闭环。 +`RESP` 应匹配开放请求:无匹配、过期或报文早于发送时间时不更新快照(`G-RESP-GUARD`)。 ### 7.2 上游请求与静态数据 -`REQ_TRACK` 表与仓储已存在,但没有运行时协调器:出站 `COUTMSGS` 适配、请求编码、超时与应答匹配均未实现(`[G-REQ-TRACK]`)。目标机制: +请求状态机(`G-REQ-TRACK`): ```text PENDING → SENT → DONE @@ -253,8 +246,8 @@ PENDING → SENT → DONE - 注册同类新请求前使旧开放请求过期;只有 `COUTMSGS` 写入确认后才标记 `SENT` 并关联出站记录;写信箱成功但本地未确认的情况需要补偿与去重,不能无条件重新发送。 - 应答优先按已确认的回显字段精确匹配;降级匹配的跨代误配风险必须明确接受并审计(`C-23`)。 - 时间比较统一时区与单位,并需定义时钟偏斜容忍;容忍判据未定(`Q5`),在定义前不得把降级匹配描述为精确关联。 -- 参考应答写入自有 `REF_MASTER`(尚未建表),日计划应答走快照流程;请求完成必须在相应数据处理成功之后,超时和迟到应答不能修改已关闭请求对应的状态。 -- 出站承诺只到落信(`C-24`、CLM-8);主为 EROR 回报义务见 `C-25`。 +- 参考应答写入自有 `REF_MASTER`,日计划应答走快照流程;请求完成必须在相应数据处理成功之后,超时和迟到应答不能修改已关闭请求对应的状态。 +- 出站承诺只到落信(`C-24`、`CLM-8`);主 / 共享删除的 EROR 回报义务见 `C-25`。 ## 8. 事件投递 @@ -268,7 +261,7 @@ PENDING → SENT → DONE 发送确认后才标记 `SENT`,失败记录次数并按退避推后,达到上限转 `DEAD`(记录保留作 DLQ)。所有外部调用需要有界超时,避免阻塞投递线程。 -投递是至少一次:Broker 或其他目标已接受但本地未标记成功时可能重发;目标端接受不等于业务消费者已消费。Kafka 生产约束沿用 architecture 的 D3,生产者幂等不替代应用层事件去重。 +投递是至少一次:Broker 或其他目标已接受但本地未标记成功时可能重发;目标端接受不等于业务消费者已消费。Kafka 生产约束沿用 `D3`,生产者幂等不替代应用层事件去重。 ### 8.2 `schd` 聚合 @@ -285,9 +278,9 @@ PENDING → SENT → DONE 2. 逐条发送:UPSERT 发送该 `FLID` 的最新整态(key = `FLID`);TOMBSTONE 发送 null 值删除通知; 3. 成功后按上条规则标记完成并推进 `lastFlush`;失败按退避推后,达到上限转 `DEAD`。 -默认聚合周期与批上限见 reference。`KAFKA:msg` 与 `KAFKA:schd` 之间不承诺顺序。`schd` 行与 `msg` 行共用 `MSG_EVENT`,靠 `TARGET` 区分。 +聚合周期与批上限见 [reference.md](reference.md)。`KAFKA:msg` 与 `KAFKA:schd` 之间不承诺顺序。`schd` 行与 `msg` 行共用 `MSG_EVENT`,靠 `TARGET` 区分。 -### 8.3 清理 +### 8.3 事件清理 已 `SENT` 的事件行按 `PARAM:msgx.pipeline.event-retention` 由维护作业清理。`KAFKA:msg` 的 `DEAD` 行保留作 DLQ,人工处置后再清理;`KAFKA:schd` 的 `DEAD` 行只保留到同一 `FLID` 出现新的、可接受的状态代次,新代次会把单行投影重置为 `PENDING` 并清空旧错误。该取舍服从 schd 只保存最新状态的契约,因此被替换的 schd DEAD 代次不再由 `MSG_EVENT` 提供持久审计句柄。 @@ -297,7 +290,7 @@ PENDING → SENT → DONE `ProcFailure` 与 `FailureScheduler` 统一处理侧失败落账,投递侧按同一套次数与退避规则迁移事件。启动自检强制退避档位数与尝试上限匹配,让「表里有档但永不触发」的配置无法通过。失败必须在持有具体消息、事件或批次的位置记录,外层循环只做兜底日志和等待,不重复增加次数。线程中断应恢复中断标记并向上传递;不把 JVM `Error` 当普通业务失败捕获。 -`ReplayService` 只允许 `CODEC_ERROR / UNSUPPORTED / INFRA / EXHAUSTED` 从 `FAILED / DEAD` 回到 `PENDING`,并重置尝试次数、下次重试时间与错误原因,**不重置 `IDENTITY_KEY`**(保留身份,避免重放时把自己判成重复消息)。它按错误类全局批量重放,尚无按记录预检、操作审计与管理入口(US-10)。重放与回填通过 `MessageLifecycleGate` 在同一实例内互斥;**旧消息进入终态后后续消息可能已执行,重新入队不等于恢复历史顺序**,重放前必须评估状态覆盖和版本保护(CLM-3)。 +`ReplayService` 只允许重放白名单内的错误类(见 [reference.md](reference.md))从 `FAILED / DEAD` 回到 `PENDING`,并重置尝试次数、下次重试时间与错误原因,**不重置 `IDENTITY_KEY`**(保留身份,避免重放时把自己判成重复消息)。重放与回填通过 `MessageLifecycleGate` 在同一实例内互斥;**旧消息进入终态后后续消息可能已执行,重新入队不等于恢复历史顺序**,重放前必须评估状态覆盖和版本保护(`CLM-3`)。 ### 9.2 中断恢复 @@ -318,29 +311,29 @@ PENDING → SENT → DONE ### 9.3 生命周期与清除 -`JobRunner` 用独立 daemon 线程按周期触发回填扫描、航班历史清理与留痕清理(终态归档为待交付项 `[G-PROC-HST]`);作业不参与消息 FIFO,也不使到期消息饥饿。`INV-18` 要求历史清理的删除与主泵处理互斥。 +`JobRunner` 用独立 daemon 线程按周期触发回填扫描、航班历史清理与留痕清理;作业不参与消息 FIFO,也不使到期消息饥饿。`INV-18` 要求历史清理的删除与主泵处理互斥。 **通则**(对本系统所有持久对象适用) - **时间不构成清除依据**:到期只是必要条件,**终局证据才是充分条件**(共享库见 `C-8`,航班见 `D1`)。 -- **归档不是终点**:归档目标是新的无界集合,必须有独立保留期与清除作业 `[G-HST-RETENTION]`,否则只是把容量问题从热表移到冷表。 +- **归档不是终点**:归档目标是新的无界集合,必须有独立保留期与清除作业 `G-HST-RETENTION`,否则只是把容量问题从热表移到冷表。 - **证据不随清除消失**:清除所依赖的证据(如 `C-8` 引用的回填放弃清单,本期承诺见 `C-16`)在其覆盖的信箱边界被清除前必须保持可查。 - **证据缺失或结果不明时按最保守处置**:航班清理为删 0 条(`D1`)。 -**逐对象生命周期**(保留期取值一律见 reference) +**逐对象生命周期**(保留期取值一律见 [reference.md](reference.md)) -| 对象 | 终局判据 | 归档目标 | 清除证据 | 执行方 | 缺口 | +| 对象 | 终局判据 | 归档目标 | 清除证据 | 执行方 | 偏差 | |---|---|---|---|---|---| | 共享信箱 `CMINMSGS` 原文 | 处理标记 / 回填放弃清单 | 库方历史表(`C-9` 方案 B) | `C-8` | 库方 | 契约未确认(`Q6`/`Q7`/`Q9`) | | `FLIGHT_SCHD` + 资源明细 | 判史规则 | 历史存储 | 归档确认 + 版本复查 | 我们 | — | -| 航班历史存储 | 保留期 | — | — | 我们 | `[G-FLIGHT-HIST-RETENTION]` | +| 航班历史存储 | 保留期 | — | — | 我们 | `G-FLIGHT-HIST-RETENTION` | | `SCHD_SNAP_LOG` | 保留期 | 无(本地可重建) | 无 | 我们 | — | -| `MSG_EVENT` 已发送行 | `SENT` | 无 | 无 | 我们 | `[G-EVENT-RETENTION]` | -| `PROC_STATE` 终态行 | 见下 | `PROC_STATE_HST` | 回填了结 | 我们 | `[G-PROC-HST]` | -| `PROC_STATE_HST` | 保留期 | — | — | 我们 | `[G-HST-RETENTION]` | -| `REQ_TRACK` 关闭态行 | 保留期 | 无 | 无 | 我们 | `[G-REQ-TRACK-RETENTION]` | +| `MSG_EVENT` 已发送行 | `SENT` | 无 | 无 | 我们 | — | +| `PROC_STATE` 终态行 | 见下 | `PROC_STATE_HST` | 回填了结 | 我们 | `G-PROC-HST` | +| `PROC_STATE_HST` | 保留期 | — | — | 我们 | `G-HST-RETENTION` | +| `REQ_TRACK` 关闭态行 | 保留期 | 无 | 无 | 我们 | `G-REQ-TRACK-RETENTION` | -原文副本是**条件对象**:仅当库方清除语义不满足 `C-6` 时才成立(`[G-REPLAY-CHANNEL]`、`CLM-5`),窗口与 `R_keep` 相同,落在自有 PG。 +原文副本是**条件对象**:仅当库方清除语义不满足 `C-6` 时才成立(`G-REPLAY-CHANNEL`、`CLM-5`),窗口与 `R_keep` 相同,落在自有 PG。 **处理终态归档**(`PROC_STATE` → `PROC_STATE_HST`) @@ -351,7 +344,7 @@ PENDING → SENT → DONE 归档范围只含终态;归档后仍须保留业务去重能力(`INV-9`)——去重记忆期长于工作状态在线期,实现取「主表保留去重影子行」:主行置 `STATE='ARCHIVED'`、只留 `IDENTITY_KEY` 与 `MSG_ID`,`IDENTITY_KEY` 唯一约束留在主表不动。队头推进、`backlog()` 与回填扫描的谓词显式排除 `ARCHIVED`,不靠状态包含列表隐式过滤。 -**时间常数排序**(取值见 contracts.md) +**时间常数排序**(取值见 [specification.md](specification.md)「契约数值」) 1. `R ≤ R_keep`(`C-7`)。 2. 去重记忆期 ≥ `R_keep`;否则「归档后重复」不成立(`INV-9`)。 @@ -360,18 +353,114 @@ PENDING → SENT → DONE **其余清理** -- **航班历史清理**:按 reference 的历史判据选候选(含 `DELETED`),先成功归档再删除;未经 FDEL 的生命周期清除需先补发删除事件。语义与红线见 flight-state.md。 +- **航班历史清理**:按 [reference.md](reference.md) 的历史判据选候选(含 `DELETED`),先成功归档再删除;未经 FDEL 的生命周期清除需先补发删除事件。语义与红线见「航班域」。 - **留痕清理**:`SCHD_SNAP_LOG` 按保留期与 `(SCOPE_END, RECV_AT)` 删除,不依赖历史存储开关。 -- **出站事件清理**:见投递清理规则。 +- **出站事件清理**:见「事件清理」。 -共享信箱保留策略由库方管理(contracts「保留与清除」)。历史写入与删除事件入队之间仍需恢复方案;顺序调用不构成原子提交。 +共享信箱保留策略由库方管理(`C-5`~`C-12`)。历史写入与删除事件入队之间仍需恢复方案;顺序调用不构成原子提交。 ## 10. 容量假设与设计取舍 -本设计按以下量级选型(`[待确认]`,未实测;参数默认值的依据列见 reference,可声明性见 `CLM-10`): +本设计按以下量级选型(参数默认值的依据列见 [reference.md](reference.md),可声明性见 `CLM-10`): - 单机场、单活动实例、单维护者;入站日消息量千级到万级;单条报文量级 ≤ 10⁴ 字节。 -- 处理延迟秒级可接受;航班可见性延迟不劣于现役(轮询间隔 ≤ 1 秒 + 聚合周期秒级)。 +- 处理延迟秒级可接受;航班可见性延迟不劣于现役(轮询间隔 + 聚合周期秒级)。 - 因此:不引入多实例并行、分布式锁、分区表;用单行锁与单线程换确定性。 -容量假设变化时,需要重新评估的项:批次大小与轮询间隔、聚合周期与批上限、指标取数口径(`backlog()` 是 `PROC_STATE` 聚合,`PROC_STATE_HST` 未交付前成本随历史增长)、以及 `MSG_EVENT` 保留期。 +容量假设变化时,需要重新评估的项:批次大小与轮询间隔、聚合周期与批上限、指标取数口径(`backlog()` 是 `PROC_STATE` 聚合;`G-PROC-HST`)、以及 `MSG_EVENT` 保留期。 + +## 11. 航班域:权威模型与合并写入语义 + +本章是航班状态的唯一现行设计规范。其他各章只描述管道机制,不重复定义航班域规则。 + +系统从共享 MySQL 信箱接收 SIS/AODB 报文,把结果合并到自有 PostgreSQL 中的航班当前态,再通过 outbox 投递 Kafka。共享信箱和 Kafka 都不是状态权威,也不在本地事务的提交范围内。 + +- `FLID` 是航班实例的唯一标识;不得由航班号、日期或资源号推断身份。 +- `FLIGHT_SCHD` 及其明细表是唯一权威当前态;展示视图只读,不能作为写入或对账来源(`INV-11`)。 +- 单活动主泵按信箱 FIFO 推进。事务内 `PIPELINE_LOCK` 只串行化本地状态提交,不替代选主或消息认领。 +- 状态写入、outbox 事件、处理终态和回填意图在同一 PostgreSQL 事务中提交(`INV-17`);回填与 Kafka 投递在提交后独立重试。 + +现场目标库为 Oracle 11g;Oracle 适配必须通过方言与集成验证后才能作为可切换的运行时选项。 + +### 11.1 权威模型 + +| 对象 | 职责 | +|---|---| +| `FLIGHT_SCHD` | 一行一个 `FLID`,保存标量字段、`STATE`、`STATE_VERSION`、`OPERATION_DAY`、最近消息 ID 和审计时间。 | +| 资源明细表 | 保存登机门、值机柜台、转盘、计划机位、滑槽、延误、靠撤桥、轮挡等变长集合;`SRVT`/`VIPF` 专用明细见 `G-SRVT-VIPF`。主键为 `(FLID, ORDINAL)`。 | +| `FLIGHT_ROUTE_POINT` | ROUT 与 ERUT 两类路线点,使用 `ROUTE_KIND` 区分;主键应包含该列,避免两类路线的序号冲突。 | +| `PROC_STATE` | 信箱消息的处理终态、业务身份幂等记录,以及回填事实(`RECEIVED_AT` / `BACKFILL_*`)。 | +| `MSG_EVENT` | 事务 outbox,承载整态投影、变更通知和删除 tombstone。 | +| `INBOX_CURSOR` | 共享信箱消费水位(读取进度,与处理标记互不替代)。 | +| `SCHD_SNAP_LOG` | 日计划处理留痕,只追加、可重建,不参与状态决策。 | + +### 11.2 航班身份与运营日 + +`FLID` 是主键。`OPERATION_DAY` 从 SCHD 记录的 `SODT` 按配置的机场时区和切日规则推导;它不是消息接收日或落库日。 + +一旦已写入非空 `OPERATION_DAY`,同一 `FLID` 不得改到另一个运营日(`INV-12`)。遇到冲突,整包日计划按协议错误拒绝,既有状态保持不变(`INV-19`)。尚未由日计划收录的航班可以为 `NULL`;这不表示该航班没有运营日,只表示当前模型无法为它确定归属日。 + +### 11.3 字段与集合 + +标量与异常对象前缀字段存于主表。协议中的 `SRVT`、`VIPF` 是无界集合,目标形态必须按集合完整保存到专用明细表示;专用明细、合并与投递见 `G-SRVT-VIPF`。`MAFL` 不是 SIS/XML 入站字段,而是由共享航班的 `MAID`、`FLID`、`FLNO` 生成的主航班派生投影(`G-MAFL`)。 + +- `ORDINAL` 是持久化顺序,从 1 开始;`SOURCE_SEQ` 是上游序号,允许为空或重复。 +- 相同资源号不代表同一条分配,禁止按资源号去重。 +- 每次持久化完整航班状态时,明细表按该 `FLID` 先删后插,以完整合并结果为准(`INV-14`)。 +- ROUT 与 ERUT 是两类独立集合,不能因相同序号覆盖彼此。 +- 主/共享关系以主表的 `MAID` 为事实来源:`MAID` 是共享航班指向主航班 `FLID` 的引用(非共享航班为 `NULL`);`MAFL` 只在读取和事件投影时从子航班事实派生,不按入站标量解析或保存。 + +### 11.4 主/共享投影(`MAFL`) + +`MAFL` 是主航班的派生集合,元素为子航班的 `FLID` 与 `FLNO`;内容与变更传播分别由 `INV-21`、`INV-22` 保证。 + +- 子航班集合 = `STATE = ACTIVE` 且 `MAID = 主航班 FLID` 的 `FLIGHT_SCHD` 行;已 FDEL 的子航班(`STATE = DELETED`)自然退出投影,不需要改写主航班行。 +- 只有 `MAID` 为空的主航班携带 `MAFL`;共享航班只携带自身 `MAID`、`CSOP`、`CSFT`,不携带 `MAFL`,避免下游双向合并。 +- 投影按 `FLID` 升序,与到达顺序及 `FLNO` 变更无关:同一 `STATE_VERSION` 的投影逐字节稳定,重发与消费端比对才有意义。 +- `MAID = FLID` 的自引用行不进入任何 `MAFL`;`MAID` 指向不存在主航班的悬挂引用不阻断该子航班自身处理,只是不产生投影。 +- 子航班集合变化(新增、删除、`MAID` 迁移)必须让涉及的主航班在同一事务内推进 `STATE_VERSION` 并登记主航班事件(`KAFKA:msg` + `KAFKA:schd`);否则整态投影的只进不退写入会丢弃它(见「`schd` 聚合」)。共享航班自身不单独发通知。 +- 派生主航班投影与产生它的状态写入必须同一事务或一致读快照;按 `MAID` 取子航班要求该列有索引(`INV-17`)。 + +## 12. 航班域:合并、删除与生命周期 + +领域决策逻辑(如 `FlightStateEngine` 及各类 Handler 规则)保持纯粹:它根据当前完整态和已解码报文,返回下一完整态与待发事件,不执行数据库或 Kafka I/O。处理器是事务协调器,负责在统一事务边界内调用决策逻辑并持久化结果(`US-03`、`INV-17`)。 + +### 12.1 SCHD 日计划 + +SCHD DNLD/RESP 在整包校验通过后,逐条将报文携带的航班写入当前态。日计划只更新或创建其携带的 `FLID`,**不会因其他航班未出现在本次报文中而删除任何记录**(`INV-15`);SIS 同向(`SIS:3.16-note-1` 要求子系统自行保留前一日延误航班)。 + +日计划在重叠字段上可以覆盖当前动态值;未携带的字段按合并规则保留,显式清空才清除。每个成功写入的航班推进 `STATE_VERSION`(`INV-13`),并在同一事务登记 `KAFKA:schd` 与 `KAFKA:msg` 事件。 + +**字段缺失语义与外部规范冲突**:SIS 规定最新日计划中未发送的可选字段表示 AODB 已无该数据、子系统应删除本地已有值(`SIS:3.16-note-4`;RESP 与 DNLD 同格式,见 `SIS:3.17`),并要求以 AODB 最新数据覆盖本地(`SIS:1.6.2`)。这与上面的「未携带字段保留」相反。确认前两条并存,按 `Q13` 跟踪,不得据本节推定已与上游对齐。 + +消息重放由 `PROC_STATE` 的消息 ID 与 `IDENTITY_KEY` 控制;已成功提交的消息不得再次写入或重复登记事件。整包校验失败或运营日冲突时,整包不落地(`INV-19`)。 + +### 12.2 动态运行事件 + +FLOP 事件只修改它表达的字段或资源集合,其余航班状态保持不变。每个动态子类型的语义都必须有明确 Handler 规则和回归测试,不能只因已被路由就推定其业务语义完整(`INV-20`)。 + +动态事件保留既有 `OPERATION_DAY`,也不基于接收时间重新推导它。未知或已删除航班的具体处理遵从对应 Handler 的幂等规则。 + +### 12.3 删除与重建 + +FDEL 是业务删除入口:仅在 `ACTIVE → DELETED` 时推进版本、保留明细并与 tombstone 同事务登记;重复 FDEL 或不存在的航班按幂等成功处理。 + +物理删除仅由独立历史清理在归档成功后执行(`D1`)。日计划报文不是删除依据(`INV-15`)。若未经 FDEL 而由生命周期清理,清理前需要登记一次 tombstone;已经 FDEL 的记录不重复发出。 + +ADFT 的字段缺失语义尚待上游确认。在确认前采用保守的 Set-only 规则:出现字段可更新,缺失字段不清空;不得把它当成日计划或动态全量替换。新建 ADFT 若带可解析的 `SODT`,按同一运营日规则计算 `OPERATION_DAY`;否则保留为 `NULL`。 + +主/共享航班级联:删除共享航班时重算主航班 `MAFL`(见「主/共享投影」)并向主航班通知;删除主航班时级联删除其子共享关联并发出删除通知;主/共享关系必须一次原子变更,不出现主已删、子残留的半状态。共享航班增量通常只更新并通知主航班,不直接发共享通知。这些语义同样约束 FDEL 之外的生命周期清理。主/共享关联的增删按 `FLID` 做值比较,不使用引用比较。 + +SIS 规定删除主航班时必须先删子共享航班、再删主航班,顺序不符时 RMS 应向 AODB 回发 EROR(`SIS:1.6.1-1.d`,事件定义见 `SIS:4.8`)。本章的原子级联不发该回报,两者取舍见 `C-25`。 + +### 12.4 Kafka 与读取 + +`KAFKA:schd` 是按 `FLID` 的完整状态投影。Dispatcher 可以合并同一 `FLID` 尚未投递的中间版本,只发最新状态;消费端用 `(FLID, STATE_VERSION, UPDATED_AT)` 防止旧投影覆盖新状态。 + +`KAFKA:msg` 只通知变化,不承载权威状态;两个 topic 不承诺顺序。FDEL 和必要的生命周期清理使用 tombstone:键为 `FLID`,删除记录以 null 值投递,通知下游移除旧状态。 + +读取完整航班必须在明确的一致性读边界内批量加载主表和全部明细。 + +### 12.5 生命周期 + +运营日过去不等于航班结束。历史清理须同时满足配置保留期与终态证据或足够静默期,先成功写入历史存储,后物理删除当前态;历史存储失败时必须删除零行(`D1`、`G-FLIGHT-HIST-RETENTION`)。 diff --git a/docs/invariants.md b/docs/invariants.md deleted file mode 100644 index 916495c..0000000 --- a/docs/invariants.md +++ /dev/null @@ -1,127 +0,0 @@ -# 前提、不变量与声明边界 - -本文件是三件东西的唯一出处: - -- **前提 PRE-x**:由外部提供、我们无法单方保证的事实。前提失效时不变量必须整体重估。 -- **不变量 INV-x**:本系统自己保证的性质。变更用「追加 + 作废」(`INV-7 → [作废 by INV-7b]`),不静默改写。 -- **声明边界 CLM-x**:每条对外主张依赖哪些 PRE/INV、当前**可否声明**、挂起原因(`[G-x]` 缺口 / `[Q-x]` 待确认)。 - -验证映射在本文件 §4,是验收口径的唯一清单;代码与测试只做证据,不在此重复叙述。 - -## 1. 前提(外部提供) - -| 编号 | 前提 | 若不成立的影响 | 状态 | -|---|---|---|---| -| PRE-1 | 信箱消费权排他:同一时刻只有一个系统有权处理、打标、判定可清除(迁移期由切流规程保证单一权威写者) | 水位、身份去重、清除前提全部失效 | `[待确认]`(切流由运维规程保证,上线前另立) | -| PRE-2 | ID 单调 + 可见时延上界:见 `C-1`/`C-2` | 水位只能当快路径提示;空洞老化阈值无依据;不能声明发现完整性 | `[待确认 Q2]` | -| PRE-3 | ID 空间不复位、不复用、不回退:见 `C-3` | 水位(不可逆单游标)之后的行永久不可见 | `[待确认 Q2]` | -| PRE-4 | 报文的 `DATE_RECEIVED` 时钟基准可解释(偏斜在有界范围内) | 跨系统时间比较(`RECEIVED_AT` 与本地 `NOW`)会提前或推迟判定 | `[待确认 Q7]` | -| PRE-5 | 单活动实例运行(信箱读取不加锁、水位是单行覆盖写) | 水位互相覆盖、空洞计时失真 | `[我们自证]`(部署约束,见 architecture) | -| PRE-6 | 信箱与自有 PG 之间没有跨库事务 | 回填、水位推进、清除都不能声称原子 | `[我们自证]`(架构事实) | -| PRE-7 | 报文不可变:同一业务身份的重发必为同一内容:见 `C-4` | 上游改发会被判为重复并静默跳过 | `[待确认 Q15]` | -| PRE-8 | `FLID` 在保留期内不复用:见 `C-21` | 「保留最新版本」的合并规则可能压掉新航班事件,旧 tombstone 可能删掉在用航班 | `[待确认 Q16]` | - -## 2. 不变量 - -### A. 管道 - -- **INV-1** 五个独立事实互不替代:落信 / 入队 / 处理完成 / 已回填 / 投递确认各有独立证据,前一个不蕴含后一个。 -- **INV-2** 水位与入队同事务:不允许出现「水位已推进、消息未入队」的持久化状态;水位只增不减,遇空洞即停,只有判定为永久空洞才放行,且放行只跳过空洞本身、不越过任何已存在的行。 -- **INV-3** 队头唯一:任一时刻只有一个可执行队头(最小未完成 `MSG_ID`,`PENDING` 与 `FAILED` 都占位);`FAILED` 未退避到期时后续消息不得越过。 -- **INV-4** 只领取已发现的行:主泵只领 `MSG_ID ≤ W`;水位之外的行只可能来自兼容入口,必须等水位追平后按序处理。 -- **INV-5** 发现与处理互不阻塞:收报只看 `ID > W`,不以处理标记为谓词;终态而未回填的行不阻断后续消息的发现。 -- **INV-6** 处理终态不可逆:已提交的 `SUCCEEDED` 不因回填或投递失败回改。 -- **INV-7** 处理标记单调:任何路径只把空标记写成已处理值,不回撤、不覆盖。 -- **INV-8** 回填只针对终态(`PENDING` / `FAILED` 永不写标记);「还欠一次回填」的事实与终态由**同一条语句**落库,不存在第二处落账。 -- **INV-9** 一信一行、一身份一记录:`PROC_STATE` 按 `MSG_ID` 唯一;同一业务身份至多绑定一条有效处理记录。 -- **INV-10** 对外投递至少一次;端到端恰好一次不在交付范围。 - -### B. 航班域(定义处;flight-state.md 只引编号) - -- **INV-11** 自有 PG 的航班当前态是唯一权威;信箱、Kafka、展示视图都不是权威。 -- **INV-12** `FLID` 唯一;已写入非空的 `OPERATION_DAY` 不可改变。 -- **INV-13** 每个航班每次成功状态写入单调推进 `STATE_VERSION`;重复消息不重复推进。 -- **INV-14** 报文未携带的字段不被隐式清空;集合按完整合并结果写入,保留输入顺序与源序号。 -- **INV-15** 缺席于某个日计划不构成删除理由;删除只由 FDEL 或受控历史清理触发。 -- **INV-16** 外部副作用(回填、Kafka 投递、出站信箱)失败可重试,但不回滚已提交的本地业务结果。 -- **INV-17** 状态变更、待发事件、处理终态与回填意图在同一 PG 事务内原子提交。 -- **INV-18** 航班表的写者集合是「主泵处理器」与「历史清理」;两者必须互斥(同一 `PIPELINE_LOCK`,或清理在同一事务内复查判据后再删除),不得出现清理删除与处理器更新同一 `FLID` 的竞态。 -- **INV-19** 整包校验失败或运营日冲突时整包不落地,既有状态与版本保持不变。 -- **INV-20** 处理器幂等:同一消息重复执行只产生一次业务效果。身份唯一只防「重复记录」,不防「重新执行」;29 类 FLOP 幂等矩阵补全前,本条**不可声明**。`[G-FLOP-IDEMPOTENT]` -- **INV-21** `MAFL` 是派生投影:内容恒等于「`STATE = ACTIVE` 且 `MAID = 主航班 FLID`」的子航班集合(元素 `FLID` + `FLNO`,按 `FLID` 升序),不落库、不从入站解析;自引用与悬挂引用不入投影。 -- **INV-22** 子航班集合变化必须使涉及的主航班在同一事务内推进 `STATE_VERSION` 并登记主航班事件;投影只进不退,版本不推进即被下游丢弃。 - -## 3. 声明边界 - -| 编号 | 主张 | 依赖 | 当前可否声明 | 挂起原因 | -|---|---|---|---|---| -| CLM-3 | 重放不产生重复业务副作用 | INV-20、`G-FLOP-IDEMPOTENT` | **不可** | 29 类 FLOP 幂等矩阵未补全;重放不恢复历史顺序 | -| CLM-4 | 回填不会被短暂故障放弃:最终打标,或进入可对账的放弃清单 | INV-8、`C-5`、`C-8` | **可声明(有条件)** | 条件:`R` 之前不放弃;`MISSING_ROW` 立即放弃并告警;放弃行须经人工对账才可用于清除判定(`C-8`)。原文保留另见 CLM-5 | -| CLM-5 | 重放窗口内原文仍可读 | `C-6`、`C-7`、`Q7`、`Q9` | **不可** | 清除语义与保留期未确认;「打标即清除」下无补救 | -| CLM-6 | 单实例内严格 FIFO | PRE-5、INV-3 | **可**(限于单活动实例) | — | -| CLM-7 | 事件投递在同一 `FLID` 内保序 | INV-10、投递设计 | **可**(跨 `FLID` 不承诺) | 实现当前按目标级全序投递,收敛到按 `FLID` 属投递改造,关联 ACM2-34 | -| CLM-8 | 出站交付承诺只到「落信」 | `C-24`、`Q10` | **可**(仅落信语义) | 消费方与 ACK 列语义未确认 | -| CLM-9 | 处理标记延迟由调度周期决定(≤30 秒) | — | **不可** | 30 秒只是扫描调度周期;批次积压、单行超时与历史作业都会延长实际延迟 | -| CLM-10 | 容量量级假设(单实例、入站日消息量千级到万级、单报文 ≤ 10⁴ 字节) | — | **不可** | 未实测,无生产负载数据;解除条件:取得现役信箱日量、峰值与单报文上限后重估 | - -## 4. 验证映射 - -每条不变量至少一条证据;「缺口」表示尚无回归。测试名以仓库现状为准,新增测试按此表补位。 - -| 不变量 | 场景 | 证据 / 缺口 | -|---|---|---| -| INV-1 | 五事实互不替代:入队不引用标记、回填不引用投递、投递不引用回填 | 缺口(需接口级断言) | -| INV-2 | 重复扫描、入队中断 | 不重复入队、不丢记录;`InboxPollerTest` | -| INV-2 | 空洞老化与重置 | 阈值内不推进、不越过入队;超期只放行空洞本身;旧空洞补齐后新空洞获得完整窗口 | -| INV-2 | 水位写入与入队同事务 | 缺口(需真实 PG 事务用例,关联 ACM2-39) | -| INV-3 | 较小 ID 迟提交 | **缺口基线已固定**:`InboxPollerTest` 钉住「水位越过后到达的较小 ID 不被发现」;水位遇空洞即停、空洞老化放行只跳过空洞本身 | -| INV-4 | 兼容入口与空洞并发 | 兼容入口登记的行超出水位、主泵不领取;`PipelineSmokeTest`「compat injected high id is not claimed until the watermark catches up」 | -| INV-3 | 队头失败、退避及作业竞争 | 消息不越队;到期后恢复;作业不使消息无限饥饿 | -| INV-5 | 终态未回填不阻断发现 | 缺口(补齐后应断言发现谓词不引用处理状态) | -| INV-6 | 投递失败后终态不变 | 缺口 | -| INV-7 | 回填四种结果 | 写入成功 / 早已标记(不覆盖、记成功)/ 信箱行不存在(立即放弃并告警,不得视为已标记)/ 暂时故障持续到 `R` 仍未打标(停止自动重试,可人工恢复) | -| INV-7 | `RECEIVED_AT` 为 NULL | 超期分支仍成立且不导致标记提前写入——判据是本地 `ENQUEUED_AT`,与库方时钟及 NULL 无关 | -| INV-8 | PG 提交失败、信箱回填失败 | 事件、终态与回填意图一起回滚;已提交结果只补写标记,不重放业务;中间态永不补写 | -| INV-8 | 非业务型终态 | 不触碰航班表 / `MSG_EVENT`,只写 `PROC_STATE`,且终态与回填意图同语句生效 | -| INV-9 | 同身份多条记录、失败后重试、归档后重复 | 只产生一次有效业务处理,不把自身重试判为重复 | -| INV-10 | 投递确认丢失、批次失败、次数耗尽 | 允许可识别的重发、保持目标顺序、整批退避并保留死信 | -| INV-11 | 权威唯一 | 缺口(展示视图与缓存不得成为写入或对账来源) | -| INV-12 / INV-13 | PG 事务失败、快照重复或迟到 | 整体回滚重试、不重复推进版本、不回退状态、不误删增量航班 | -| INV-12 | 运营日冲突 | 整包 `DEAD(PROTOCOL)`,既有状态与版本不变 | -| INV-19 | 整包协议拒绝(声明数不符、运营日冲突) | `DEAD(PROTOCOL)`,整包不落地、整体回滚、既有状态不变 | -| INV-15 | 缺席不删除 | 缺口(F-del 与清理路径分别断言) | -| INV-16 | 外部副作用失败后本地结果不变 | 待核对 | -| INV-17 | 业务型终态四件套同事务 | 待核对;真实 PG 用例待补(ACM2-39) | -| INV-18 | 清理与处理并发 | `HistorySweepJobTest`(归档后被主泵更新的航班不删除、不发 tombstone)+ `HistorySweepPurgePgTest`(删除阶段失败时 tombstone 与删除整体回滚) | -| INV-20 / CLM-3 | 重放同一条消息 | 缺口:29 类 FLOP 幂等矩阵未补全 | -| INV-21 | `MAFL` 投影与 `ACTIVE` 子航班集合一致(子航班删除后退出、自引用与悬挂引用不入、顺序确定) | 缺口:投影未实现(`[G-MAFL]`) | -| INV-22 | 子航班新增、删除、`MAID` 迁移时主航班版本与事件 | 缺口:主/共享级联未实现(`[G-MAFL]`) | -| CLM-4 | 放弃行与清除前提 | 断言放弃行不写标记、不被当作已打标(关联 ACM2-36) | -| CLM-9 | 回填/积压完成时限 | 指标已就位:`msgx.pipeline.job.heartbeat_age_seconds` / `ticks.total` / `failures.total` / `last_sweep_selected` 与 `msgx.pipeline.backfill.oldest_unmarked_seconds`(关联 ACM2-38);实际延迟仍需现场数据,CLM-9 不可声明 | -| — | 请求超时、无匹配 RESP、时间单位不一致 | 不误用迟到应答、不提前完成请求 | -| — | stub 误配置、重复实例、停机中断 | 生产拒绝不安全启动,工作线程能正确退出 | - -## 5. 缺口索引与 Plane 的关系 - -本表是**缺口标记的唯一清单**:其他文档只在相应位置写 `[G-x]`,不解释、不记进度;工作进度在 Plane(ACM2)。 - -| 缺口 | 含义 | 影响 | -|---|---|---| -| ~~`G-IGNORE`~~ | ~~忽略规则(`LDM`/`REGN`/`RSTA`/`EROR`)未实现~~ | 已关闭:`IgnoreRules` 在身份绑定后精确匹配 `TYPE` 字段(codec 已大写),命中写 `SKIPPED` + 回填意图(`US-04`) | -| `G-RESP-GUARD` | `RESP` 应答守卫未实现,当前与 `DNLD` 无差别进入快照写入 | 请求匹配闭环;`C-23` | -| `G-REQ-TRACK` | `REQ_TRACK` 无运行时协调器:出站适配、请求编码、超时与应答匹配未实现 | US-08;`C-24` | -| `G-PROC-HST` | `PROC_STATE_HST` 未建表,终态归档未落地 | US-11;归档能力 | -| `G-FLOP-IDEMPOTENT` | 29 类 FLOP 幂等矩阵未补全 | `INV-20`、CLM-3 | -| ~~`G-EVENT-RETENTION`~~ | ~~`MSG_EVENT` 已发送行的保留期与清理作业未实现~~ | 已关闭:`SENT_AT` 列 + 投递原子写 + `EventCleanupJob` 按 `eventRetention` 有界删除 | -| ~~`G-BACKFILL-BACKOFF`~~ | ~~回填独立退避键未实现,代码内硬编码~~ | 已关闭:`backfill-backoff-ms` / `backfill-backoff-cap-ms` 配置绑定落地,`BackfillService` 从 `PipelineProps` 读取 | -| `G-KAFKA-D3` | ~~已闭合~~:`max-in-flight` 默认收敛到 1,启动自检钉住三项联合满足 D3 | ~~投递幂等前提~~ | -| `G-REPLAY-CHANNEL` | 「打标即清除」语义下的独立原文保留通道未设计 | CLM-5 | -| `G-MAFL` | 主航班 `MAFL` 派生投影及主/共享原子级联未实现(规则见 `INV-21`/`INV-22`);`MAFL` 不是 SIS/XML 入站字段 | 航班完整态;删除与重建 | -| `G-SRVT-VIPF` | SIS/XML 的 `SRVT`、`VIPF` 无界集合尚未映射到持久化明细;wire/domain 只保留出现事实与原始内容,不参与合并与投递(清空语义见 `Q13`) | 航班完整态;无损字段保存 | -| `G-COMPAT-HTTP` | compat 入口仍未实现 Q3 定案后的 ResponseDto、媒体类型、字符集、失败响应与请求体上限 | `C-28`;US-02 | -| `G-REQ-OPEN-UNIQUE` | `REQ_TRACK` 尚无约束开放态 `(REQ_TYPE, OPERATION_DAY, SENDER)` 唯一性的部分索引 | US-08;`G-REQ-TRACK` | -| `G-HST-RETENTION` | 归档目标(`PROC_STATE_HST` 及后续归档表)的保留期与清除作业未定义 | 归档只转移不减少容量占用;`G-PROC-HST` | -| `G-FLIGHT-HIST-RETENTION` | 航班历史存储(外部)的保留期与容量上限未定义 | `D1`;`FLIGHT_SCHD` 物理清除后历史存储是唯一副本 | -| `G-REQ-TRACK-RETENTION` | `REQ_TRACK` 关闭态行(`DONE`/`EXPIRED`)的保留期与清除作业未定义 | 自有 PG 无界增长;US-08 | - -缺口标记与 Plane 工作项的对应关系在 Plane 侧维护。 diff --git a/docs/legacy/decision-flight-state-history.md b/docs/legacy/decision-flight-state-history.md index 830f38b..964e505 100644 --- a/docs/legacy/decision-flight-state-history.md +++ b/docs/legacy/decision-flight-state-history.md @@ -2,4 +2,4 @@ 旧版航班状态方案已撤销,不再作为设计或实现依据。 -现行航班设计统一见 [运营航班状态设计](../flight-state.md)。 +现行航班设计统一见 [航班域](../implementation.md)。 diff --git a/docs/legacy/msgexchange-api-legacy-user-stories.md b/docs/legacy/msgexchange-api-legacy-user-stories.md index 72a45ac..ac1aec5 100644 --- a/docs/legacy/msgexchange-api-legacy-user-stories.md +++ b/docs/legacy/msgexchange-api-legacy-user-stories.md @@ -505,7 +505,7 @@ msgexchange-api 为成都机场 OMMS 消息交换服务(`com.gzzn.omms:msgexch ### 6.2 迁移验收边界 -1. **日计划语义**:新系统的日计划处理规则以 [运营航班状态设计](../flight-state.md) 为准,不从 legacy Redis 行为推导。 +1. **日计划语义**:新系统的日计划处理规则以 [航班域](../implementation.md) 为准,不从 legacy Redis 行为推导。 2. **共享航班增删链路**:决定保留或消除 FDEL 对共享航班的通知(US-B1 例外);主/共享航班关系(ADFT 添加、FDEL 删除)须在同一原子状态变更中持久化(修复 ADFT/FDEL 未写回与 FDEL `==` 引用比较缺陷,见 5.7-14)。 3. **可执行验收矩阵**:覆盖 3 个 SCHD 与 29 个 FLOP 子类型,维度为「输入样本 × Redis 状态变更 × Kafka 通知结果」;对当前无测试的关键路径(如 ACTT、CNCL、FDEL、RESP、DNLD、ADFT)补自动化用例。 4. **入站可靠性**:每类消息的幂等键、重复投递与多实例并发行为。 diff --git a/docs/reference.md b/docs/reference.md index ec9d831..2400f1a 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -1,12 +1,12 @@ -# 参考注册表:参数、指标、模块、错误分类 +# 参考注册表:参数、指标、代码入口、错误分类 -本文件是**可派生事实**的唯一出处:参数默认值、指标名、模块入口、错误分类。正文(design.md)只引 `PARAM:<完整键>`,不写数值。 +本文件是**可派生事实**的唯一出处:参数默认值、指标名、模块与代码入口、错误分类。正文只引 `PARAM:<完整键>`,不写数值。 -维护规则:新增或改名一个 `msgx.*` 键、增删一个指标名,必须同步本文件(人工核对,本项目不做 CI 校验)。`mailbox.*`、`datasources.*`、`kafka.*` 属基础设施键,按 §1.3 成组登记。 +维护规则:新增或改名一个 `msgx.*` 键、增删一个指标名,必须同步本文件(人工核对,本项目不做 CI 校验)。`mailbox.*`、`datasources.*`、`kafka.*` 属基础设施键,成组登记。 ## 1. 参数注册表 -「依据」列含义:**契约** = 由 contracts.md 的 `C-x` 决定;**现役** = 沿用 legacy 行为基线;**假定** = 无依据的占位值,必须在对应 `Q` 关闭后重评;**安全默认** = 关闭态,需显式开启。 +「依据」列含义:**契约** = 由 [specification.md](specification.md) 的 `C-x` 决定;**现役** = 沿用 legacy 行为基线;**假定** = 无依据的占位值,必须在对应 `Q` 关闭后重评;**安全默认** = 关闭态,需显式开启。 ### 1.1 管道节奏与重试(`msgx.pipeline.*`) @@ -18,17 +18,17 @@ | `msgx.pipeline.backoff-ms` | `[1000,2000,4000,8000]` | ms / 档 | 假定 | **档位数必须 = `max-attempts − 1`**,启动自检拦截错位 | | `msgx.pipeline.backoff-cap-ms` | `60000` | ms | 假定 | 单档封顶;默认表内无档触及 | | `msgx.pipeline.max-commit-delay` | `5m` | Duration | **假定(无依据)** | 空洞老化阈值;由 `C-2` 决定,**不可由 SIS `Expiry` 推导**(`Q2`) | -| `msgx.pipeline.overdue-backfill`(`R`) | `30d` | Duration | 契约(`R ≤ R_keep`) | 进入强补写窗口、**取消退避**的阈值;**不是兜底保证**,不保护重放窗口(`Q6`) | +| `msgx.pipeline.overdue-backfill` | `30d` | Duration | 契约(`R ≤ R_keep`) | 即 `R`:进入强补写窗口、**取消退避**的阈值;**不是兜底保证**,不保护重放窗口(`Q6`) | | `msgx.pipeline.backfill-batch` | `100` | 条 | 假定 | 回填扫描单批条数 | | `msgx.pipeline.backfill-scan-period` | `30s`(代码常量,无配置键) | Duration | 现役 | 回填扫描作业周期;批次积压与单行超时会延长实际标记延迟(`CLM-9`) | -| `msgx.pipeline.backfill-max-attempts` | `100` | 次 | 假定 | 单行重试的**告警阈值**;放弃判据是 `R` 超期,不是次数(见 design「回填」) | -| `msgx.pipeline.backfill-backoff-ms` | `30000` | ms | 假定 | 回填独立退避起步间隔;`BackfillService` 指数退避的首档 `[G-BACKFILL-BACKOFF ✓]` | -| `msgx.pipeline.backfill-backoff-cap-ms` | `900000` | ms | 假定 | 回填退避封顶(15 分钟)`[G-BACKFILL-BACKOFF ✓]` | +| `msgx.pipeline.backfill-max-attempts` | `100` | 次 | 假定 | 单行重试的**告警阈值**;放弃判据是 `R` 超期,不是次数 | +| `msgx.pipeline.backfill-backoff-ms` | `30000` | ms | 假定 | 回填独立退避起步间隔;`BackfillService` 指数退避的首档 | +| `msgx.pipeline.backfill-backoff-cap-ms` | `900000` | ms | 假定 | 回填退避封顶(15 分钟) | | `msgx.pipeline.cutover-watermark` | 不设置 | `min\|zero\|max\|` | 一次性运维决策 | 显式播种水位;非法值由启动自检挡下;升级实例拒绝重新播种 | | `msgx.pipeline.delivery-batch` | `200` | 条 | 假定 | `KAFKA:msg` 每轮每目标领取上限 | | `msgx.pipeline.delivery-drain-rounds` | `10` | 轮 | 假定 | 连取批数上限,让出循环跑 `schd` flush,防状态通知被积压饿死 | | `msgx.pipeline.autostart` | `false` | 布尔 | 安全默认 | 启动即拉起收报 / 主泵 / 投递循环;需真实仓储或 `msgx.stubs=true` | -| `msgx.pipeline.event-retention` | `7d` | Duration | 假定 | 已 `SENT` 事件行保留期,由清理作业删除 `sent_at < now - retention` 的行;`[G-EVENT-RETENTION]` | +| `msgx.pipeline.event-retention` | `7d` | Duration | 假定 | 已 `SENT` 事件行保留期,由清理作业删除 `sent_at < now - retention` 的行 | ### 1.2 其它 `msgx.*` @@ -49,7 +49,7 @@ | `msgx.history.deleted-hours` | `48` | 假定 | 历史清理的已删除判据窗口 | | `msgx.history.idle-hours` | `168` | 假定 | 历史清理的静默期判据 | | `msgx.history.snap-log-retention-days` | `90` | 假定 | `SCHD_SNAP_LOG` 保留天数 | -| `msgx.proc-state.archive-after` | `1d` | 假定 | **未实现**(`[G-PROC-HST]`);终态记录归档阈值,建议范围 1~7 天;见 `US-11` | +| `msgx.proc-state.archive-after` | `1d` | 假定 | 终态记录归档阈值,建议范围 1~7 天;目标表见 `G-PROC-HST`,见 `US-11` | ### 1.3 信箱与外部依赖(成组登记) @@ -57,14 +57,14 @@ |---|---|---|---| | `mailbox.processed-value` | `PROCESSED` | 契约(`C-5`/`Q7`) | 处理标记写入值;仅限库方认可值集 | | `mailbox.shared-mysql.enabled` | `false` | 安全默认 | 真实信箱接线门控 | -| `mailbox.shared-mysql.connect-timeout-ms` / `socket-timeout-ms` | `3000` / `30000` | 假定 | 有界外部调用;Connector/J 默认无限等待,必须显式 | -| `mailbox.shared-mysql.pool-connection-timeout-ms` / `pool-validation-timeout-ms` | `5000` / `3000` | 假定 | 池级超时 | -| `mailbox.shared-mysql.url` / `username` / `password` | 环境变量 | 安全 | 零入库,见 `.env.example` | -| `datasources.default.connection-timeout` / `validation-timeout` / `idle-timeout` / `max-lifetime` | `5000` / `3000` / `300000` / `1800000` | 假定 | 自有 PG 池(毫秒数,非 Duration 字面量) | -| `datasources.default.data-source-properties.connectTimeout` / `socketTimeout` | `3` / `30` | 假定 | 驱动级超时(秒),防网络黑洞 | -| `kafka.producers.default.acks` | `all` | 契约(architecture D3) | 允许环境变量覆盖 | -| `kafka.producers.default.enable-idempotence` | `true` | 契约(D3) | 允许环境变量覆盖 | -| `kafka.producers.default.max-in-flight-requests-per-connection` | `1` | 契约(D3) | 启动自检钉住三项联合满足 D3;允许环境变量覆盖 | +| `mailbox.shared-mysql.connect-timeout-ms` | `3000` | 假定 | 有界外部调用;Connector/J 默认无限等待,必须显式;同组 `socket-timeout-ms=30000` | +| `mailbox.shared-mysql.pool-connection-timeout-ms` | `5000` | 假定 | 池级连接超时;同组 `pool-validation-timeout-ms=3000` | +| `mailbox.shared-mysql.url` | 环境变量 | 安全 | 零入库,见 `.env.example`;同组 `username` / `password` | +| `datasources.default.connection-timeout` | `5000` | 假定 | 自有 PG 池(毫秒数,非 Duration 字面量);同组 `validation-timeout=3000`、`idle-timeout=300000`、`max-lifetime=1800000` | +| `datasources.default.data-source-properties.connectTimeout` | `3` | 假定 | 驱动级连接超时(秒);同组 `socketTimeout=30` | +| `kafka.producers.default.acks` | `all` | 契约(`D3`) | 允许环境变量覆盖 | +| `kafka.producers.default.enable-idempotence` | `true` | 契约(`D3`) | 允许环境变量覆盖 | +| `kafka.producers.default.max-in-flight-requests-per-connection` | `1` | 契约(`D3`) | 启动自检钉住三项联合满足 `D3`;允许环境变量覆盖 | 环境变量清单以 `.env.example` 为准。 @@ -83,27 +83,27 @@ | `msgx.pipeline.job.ticks.total` | 作业 tick 完成次数 | 不增长 → 作业停摆 | | `msgx.pipeline.job.failures.total` | 作业 tick 抛错次数 | 增长 → 扫描/历史作业异常 | | `msgx.pipeline.job.last_sweep_selected` | 上一轮回填扫描选中的待办条数(扫描积压) | 持续顶到批次上限 → 扫描吃不消 | -| `msgx.pipeline.codec.srvt_seen.total` | 入站记录中出现 `SRVT` 段的条数(尚未落明细表,`[G-SRVT-VIPF]`) | > 0 → 真实流量确有该段,按真实报文定案 `Q13` | -| `msgx.pipeline.codec.vipf_seen.total` | 入站记录中出现 `VIPF` 段的条数(尚未落明细表,`[G-SRVT-VIPF]`) | 同上 | -| `msgx.pipeline.processing.ignored.total` | 命中 US-04 忽略清单的报文条数 | 增长是正常流量;归零反而需确认配置是否丢失 | +| `msgx.pipeline.codec.srvt_seen.total` | 入站记录中出现 `SRVT` 段的条数(`G-SRVT-VIPF`) | > 0 → 真实流量确有该段,按真实报文定案 `Q13` | +| `msgx.pipeline.codec.vipf_seen.total` | 入站记录中出现 `VIPF` 段的条数(`G-SRVT-VIPF`) | 同上 | +| `msgx.pipeline.processing.ignored.total` | 命中 `US-04` 忽略清单的报文条数 | 增长是正常流量;归零反而需确认配置是否丢失 | 取数规则:统一走 `BacklogSnapshotProvider`(`PARAM:msgx.health.backlog-cache-ttl-ms`),`/health` 与 `/metrics` 共用同一快照——`backlog()` 是 `PROC_STATE` 的全表聚合,不能被高频抓取打穿;**无法取数上报 `NaN`,无可比记录的年龄/滞后类仪表上报 `-1`,都不伪造 0**。日志出口故障不得阻塞业务线程。进程内计数类指标不经快照,重启归零。 作业健康:回填的唯一驱动是扫描作业,因此作业存活必须独立可观测——`msgx.pipeline.job.heartbeat_age_seconds` / `ticks.total` / `failures.total` 是作业心跳,`msgx.pipeline.job.last_sweep_selected` 是扫描积压,`msgx.pipeline.backfill.oldest_unmarked_seconds` 是实际回填延迟;作业线程停摆由 `/health` 的作业指示器判 `DOWN`(心跳超过 `3 × 扫描周期`,周期是代码常量)。 -## 3. 模块入口 +## 3. 模块与代码入口 | 关注点 | 主要入口 | |---|---| -| 收报与兼容接口 | `ingress/InboxPoller.kt`、`InboxService.kt`、`InboxController.kt` | -| 解码 | `codec/JacksonXmlCodec.kt`、`SisWireMapper.kt`、`SisMessageBody.kt` | -| 调度与处理 | `processing/Pump.kt`(含 `MessageProcessor`)、`DynamicProcessors.kt`(FLOP/FDEL/ADFT)、`Identity.kt` | -| 日计划 | `processing/ScheduleProcessor.kt`;请求协调尚无实现(`REQ_TRACK` 仓储见 `infra/persistence/`) | -| 回填与投递作业 | `processing/BackfillService.kt`、`jobs/JobRunner.kt`、`HistorySweepJob.kt`、`delivery/Dispatcher.kt` | -| 持久化与恢复 | `infra/persistence/`、`infra/retry/`(`ProcFailure` / `ReplayService` / `FailureScheduler`) | -| 启停与配置 | `PipelineLifecycle.kt`、`config/PipelineProps.kt`、`config/HistoryProps.kt`、`config/OperationDayProps.kt`(含运营日时区启动自检) | +| 收报与兼容接口 | `ingress/InboxPoller.kt`、`ingress/InboxService.kt`、`ingress/InboxController.kt` | +| 解码与忽略规则 | `codec/JacksonXmlCodec.kt`、`codec/XmlCodec.kt`、`codec/SisWireMapper.kt`、`codec/SisMessageBody.kt`、`processing/IgnoreRules.kt` | +| 调度与处理 | `processing/Pump.kt`(含 `MessageProcessor`)、`processing/DynamicProcessors.kt`(FLOP/FDEL/ADFT)、`processing/ScheduleProcessor.kt`、`processing/Identity.kt`、`processing/MessageLifecycleGate.kt` | +| 回填与投递 | `processing/BackfillService.kt`、`delivery/Dispatcher.kt`、`infra/kafka/KafkaDeliveryPort.kt` | +| 维护作业 | `jobs/JobRunner.kt`、`jobs/HistorySweepJob.kt`、`jobs/EventCleanupJob.kt`、`infra/persistence/SnapshotLogPurge.kt` | +| 持久化与恢复 | `infra/persistence/`(含 `jdbc/JdbcPgRepositories.kt`、`jdbc/JdbcCminmsgInboxRepository.kt`)、`infra/retry/`(`ProcFailure` / `ReplayService` / `FailureScheduler`) | +| 启停与配置 | `PipelineLifecycle.kt`、`config/PipelineProps.kt`、`config/HistoryProps.kt`、`config/OperationDayProps.kt`(含运营日时区启动自检)、`config/MailboxProps.kt`、`config/KafkaD3Check.kt`(`D3` 三联合启动自检) | | 指标与健康 | `infra/metrics/PipelineMetrics.kt`、`infra/metrics/JobActivity.kt`、`infra/health/BacklogSnapshotProvider.kt`、`infra/health/JobRunnerHealthIndicator.kt` | -| 迁移 | `src/main/resources/db/migration/`(单基线 `V1__flight_state_baseline.sql`:原 V1–V10 的净结构已合并,迁移链收敛为一条;`oracle11g/` 为占位) | +| 迁移 | `src/main/resources/db/migration/`(单基线 `V1__flight_state_baseline.sql`;`oracle11g/` 为占位) | ## 4. 错误分类与重放白名单 diff --git a/docs/requirements.md b/docs/requirements.md new file mode 100644 index 0000000..73b367e --- /dev/null +++ b/docs/requirements.md @@ -0,0 +1,298 @@ +# 需求与验收目标 + +本文件定义阶段 A 的范围、非目标与验收口径:**需求定义要交付什么,验收标准定义怎样证明完成**。以下事实以本文件为唯一出处: + +- `US-01`~`US-15`(三级标题定义)、`OPS-1`~`OPS-4`(注册表定义); +- 需求覆盖与依赖关系。 + +代码入口与参数取值不在本文件:前者见 [reference.md](reference.md)「模块与代码入口」,后者见其参数表。前提、不变量、契约与偏差见 [specification.md](specification.md);机制与航班域见 [implementation.md](implementation.md)。工程纪律(工具链、测试设施、提交规范)以根 `AGENTS.md` 为唯一出处。 + +## 1. 范围与非目标 + +**阶段 A 闭合**:`US-01`~`US-14`、`OPS-1`~`OPS-4`。 + +**非目标**: + +- 航班当前态只存自有 PG;不引入 Redis、阶段 A 的 ES 投影、新业务库、并行主泵或分布式锁。 +- 共享 MySQL 只做契约内读写与回填;不建表、不增列、不迁移、不写共享历史表(`C-14`)。 +- 生产保持单活动实例;不承诺端到端恰好一次、跨 `FLID` 顺序或未经验证的 Oracle 11g 支持。 +- `US-15` 未启用前遵守 `D1`:历史写入未确认成功时航班清场删除 0 条。 +- 不生成上游业务报文,不替代 CIIMS/AODB,不提供 AODB 主数据编辑能力。 + +## 2. 阶段 A 用户故事 + +### US-01 可靠采集共享信箱 + +**目标**:上游继续向 `CMINMSGS` 落信,本系统持续、可恢复地采集,不要求上游改投递方式。 + +**验收标准** + +1. 按配置周期、ID 升序、有限批次采集信箱行;扫描谓词以 [implementation.md](implementation.md)「收报与水位」为准(按 ID 区间,不以处理标记为谓词)。接收层只入队,不解析业务、不回填已处理标记。 +2. 按信箱 ID 幂等建立 PG `PENDING`;重复扫描、并发兼容入队和进程重启都不能重置已有终态。 +3. 快路径用持久水位,本批 PG 入队全部确认后才推进水位。 +4. PG 不可用或批次中途失败时不改信箱标记;恢复后补建遗漏,记录失败次数与扫描进度。 +5. 较小 ID 迟提交、ID 有空洞、兼容入口先入队较大 ID 时,必须遵守经 `Q2` 确认的发现与顺序协议;不能用「最终会重扫」冒充严格 FIFO。 + +**前置**:共享库读契约;`Q2` 决定严格顺序的端到端验收;水位与扫描谓词口径以 [implementation.md](implementation.md)「收报与水位」为准。 + +### US-02 兼容 HTTP 注入报文(KEEP) + +**目标**:联调工具通过 `POST /cminmsgs/send` 提交 XML,得到真实的信箱接收结果。 + +**验收标准** + +1. 支持 `text/xml`、`application/xml`、`text/plain`,默认 UTF-8;空报文、超过请求体上限(见 `C-28`)的请求和畸形 XML 返回规范错误,不落信。XML 校验禁用 DTD、外部实体与外部资源访问。 +2. 信箱确认落信后返回 ID;PG 入队失败不把已落信伪装成未接收,由 `US-01` 补建。信箱写入未确认时不返回成功。 +3. 目标为兼容 `ResponseDto`;固定成功/失败样例、HTTP 状态码、响应媒体类型和错误码表后加入契约测试,见 `Q3`。成功只承诺信箱落信,不承诺业务处理或下游完成。 +4. 生产保持内网信任边界,由网关限制来源并审计;外露或跨网络必须先落实认证,不能把免密入口直接暴露。 + +**目标响应体示例**(数字和错误码仅作示例,错误码表见 `Q3`): + +```json +{"is_success": true, "body": 12345} +{"is_success": false, "err_code": "", "err_msg": ""} +``` + +**前置**:`US-01` 补建能力;`Q3`。HTTP 基础格式校验不替代 `US-03` 的业务解码。 + +### US-03 严格按序、幂等地执行管道 + +**目标**:报文失败和重试不造成航班状态倒序,也不重复产生副作用。 + +**实施拆分**:调度与时钟 → 安全解码及路由 → 身份绑定 → 状态应用与 PG 提交。先用假处理器验证管道,不等 `US-05` 全部实现。 + +**验收标准** + +1. 只取最小未完成 ID,`PENDING / FAILED` 均占队头;退避未到期不得越过。维护作业由独立线程执行,不参与消息 FIFO;作业必须有界,且不得因争用资源使已到期消息无限饥饿。 +2. 安全解码 XML,至少覆盖 META、SCHD、FLOP、参考应答与忽略类路由;合法但能力未支持是 `UNSUPPORTED`,不能一律归为非法报文。保留原文以支持诊断和回放。 +3. 解码后首次绑定 `SNDR|TYPE|STYP|SEQN`;冲突转 `SKIPPED` 并记录原 ID;自身重试保留绑定。生产按 `PARAM:msgx.identity.include-day-boundary` 配置(当前口径不含日期边界);更改算法须先确认 `Q11`。 +4. `MALFORMED` 直接 `DEAD`;`CODEC_ERROR / UNSUPPORTED / INFRA` 按次数和退避处理,耗尽转 `DEAD(EXHAUSTED)`。不能无限重试未实现类型,也不能立即当非法报文丢弃。 +5. 终态判据只有尝试上限(`PARAM:msgx.pipeline.max-attempts`),**没有按时间的毒丸**;调度判断注入 `Clock`。人工重放的可重放范围以 `Q6` 决定的 `R_keep` 下界为准。 +6. 主泵在同一 PG 事务提交航班主表/明细、事件与处理结果;终态回填意图通过 `US-09` 同事务保存。任一步失败整体回滚;提交后只重试外部回填,不重复生成业务事件。 +7. 领域决策逻辑只读取当前完整态与已解码报文,返回下一完整态和待发事件,不执行 I/O;Processor 作为事务协调器,将状态写入、事件、处理终态与回填意图收敛在同一事务边界内,不直接触碰 Kafka。失败只在持有消息上下文的边界落账,中断向上传递,不作为普通失败吞掉。 +8. 权威存储不可用或未完成恢复时停止业务处理;不能把「整个状态丢失」误判为「单航班不存在」而批量成功结束增量报文。 + +**前置**:`US-01`;`Q1` 已定单库方向,`Q6` 决定 `R_keep` 下界(重放窗口)。数据库迁移只落自有库。 + +### US-04 明确忽略非业务报文(KEEP) + +**目标**:无需处理的报文有可追踪的终结结果,不制造无效重试与死信。 + +**验收标准** + +1. 解码 META 后、处理器分派前,大小写不敏感匹配 `TYPE-STYP` 或 `TYPE-*`;基线为 `LDM-* / REGN-* / RSTA-* / EROR-*`,不混用 `ERROR`。转 `SKIPPED` 前必须已完成身份绑定(`US-03`、`INV-9`),忽略报文照常绑定身份。 +2. 命中后转 `SKIPPED`,记录 `ignored:` 和计数;不更新航班、不创建业务通知。 +3. 通过 `US-09` 保存回填意图;命中、未命中、大小写和重扫均有测试。合法忽略报文不应因 `MsgKind` 尚不能表达它而先解码失败。 + +**前置**:`US-03` 解码/终态接口、`US-09`。 + +### US-05 应用 ADFT 与 29 类 FLOP(KEEP + FIX) + +**目标**:增量报文正确更新航班及主/共享关系,并生成符合现役语义的通知。 + +**验收标准** + +1. `SCHD-ADFT` 与 29 个 FLOP 子类型逐项列入覆盖矩阵,每项有对应的处理器规则与回归测试;未知类型可恢复失败。RESP/DNLD 不计入这批处理器,走 `US-06`。 +2. 每类固定「输入与前态 → 后态 → msg → schd → 终态」五面样例;区分字段缺失、显式清空、重复报文和主/共享航班。清单和 golden 样例按 `Q8` 补齐,不以「已写 29 个类」替代验收。 +3. 对按 KEEP 规则需忽略的不存在航班,以 `SUCCEEDED` 无副作用结束,并由 `US-09` 回填;ADFT 建航班等行为按各类型矩阵执行。航班当前态以自有 PG 为唯一权威,重启即恢复,不存在 Redis 全损后白名单无法找回的损坏路径。 +4. 共享航班更新与删除级联语义以 [implementation.md](implementation.md)「删除与重建」为规范(共享航班通知、主航班 `MAFL` 更新、级联删除、原子变更;不出现主已删、子残留);本条目验收实现不偏离该规范,目标不存在时幂等成功。 +5. ADFT/FDEL 的值相等比较与半状态禁止规则见 [implementation.md](implementation.md)「删除与重建」。 +6. PSDT 通过 `US-14` 的只读映射计算 `abdg`,处理器不直接调用 admin-api。 + +**前置**:`US-03`;PSDT 另依赖 `US-14`;`Q1`、`Q8`、`Q14`。 + +### US-06 导入 RESP/DNLD 日计划快照 + +**目标**:主动下发和请求应答使用同一套全量计划处理,迟到应答不覆盖新状态。 + +| 报文 | 路由 | 请求状态 | 无匹配时 | +|---|---|---|---| +| `SCHD-DNLD` | ScheduleProcessor | 不更新请求 | 不要求开放请求 | +| `SCHD-RESP` | 匹配守卫后进入 ScheduleProcessor | 成功提交时匹配 RQFD → DONE | SKIPPED、审计,禁止更新快照 | +| `SCHD-ADFT` | `US-05` 增量处理器 | 不更新请求 | 不适用 | + +**验收标准** + +1. RESP/DNLD 共用流式解析、整包校验和规范化;校验失败不发布半包,旧快照保持可用。 +2. RESP 仅匹配未过期、已发送的开放 RQFD;`DTTM < SENT_AT`、已过期、已被替代或无匹配时,不写业务状态,记录跳过原因。 +3. 在自有 PG 单事务内,批处理写入已校验的 `FLIGHT_SCHD` 航班状态与资源明细;本次日计划中未出现的航班不因此被删除。 +4. 在同一 PG 事务中提交 `FLIGHT_SCHD` 变更、`MSG_EVENT` 待发通知与 `PROC_STATE(SUCCEEDED)`;匹配 RESP 同事务完成请求并置 `DONE`;提交后信箱回填由扫描承接。 +5. 相同报文重放不二次写入或重复发事件;单事务崩溃整体回滚,重放幂等。 + +**前置**:`US-03`、`US-08` 请求登记/匹配基础;`Q1`、`Q5`、`Q13`。 + +### US-07 可靠、有序地投递 Kafka + +**目标**:状态应用完成后投递通知;重试可识别、不乱序、不静默丢失。 + +**验收标准** + +1. `KAFKA:msg` 按目标内 `EVENT_ID` 顺序发送,确认后才标 `SENT`;队头退避时不跳过,发送有超时上限。 +2. `KAFKA:schd` 只通过 `flushSchd` 聚合,聚合周期与批上限见 reference;同一 FLID 取批内最新状态,成功确认覆盖对应原事件,失败保持批次可恢复并退避,耗尽可见为 `DEAD`。 +3. 外部接收成功、本地确认失败或进程重启后允许重发;事件标识跨重发稳定,消费者有去重约定,不宣称端到端恰好一次。 +4. 当前 `KAFKA:msg` 与 `KAFKA:schd` 的分区键均为 `FLID`,schd 逐 `FLID` 发送最新状态,不再是 legacy 的多航班数组。`msg` 是否需按 `SNDR` 分区、发送粒度与去重标识的放置以 `Q4` 定案为准;定案前不宣称单分区之外的顺序保证。 +5. 生产强制 `acks=all`、`enable.idempotence=true`、`max.in.flight.requests.per.connection=1`;Broker 支持幂等生产协议并完成实际验证,不允许非幂等降级通过验收。 +6. 普通/聚合发送失败、确认丢失、批次标记中断和目标阻塞均有测试;DEAD 保留记录并告警。 + +**前置**:`US-03` 事件提交;`Q4`、现网 Broker 验证。wire 不兼容的标识字段不能直接加到现役载荷。 + +### US-08 发起并跟踪 15 类 AODB 请求 + +**目标**:区分请求登记、出站落信、等待、完成与超时,不把过期应答应用到新请求。 + +**实施拆分**:请求登记/出站补偿 → 匹配/超时 → 14 类参考应答;RQFD 快照效果由 `US-06` 集成验收。 + +**验收标准** + +1. 覆盖 14 类 RQRD 参考请求和 1 类 RQFD-NONE;逐类名称、编码和映射见 `Q8`,不与 admin-api 的 21 类混算。 +2. 先持久化 `PENDING` 与出站意图;COUTMSGS 确认落信后关联其 ID 并标 `SENT`,不宣称对方已发送。落信成功而 PG 未确认时可恢复,不能盲目重发。 +3. 同类开放请求最多一个,新请求使旧请求 `EXPIRED`,并发登记不产生两个开放请求。从确认落信的发送时间起算,超时值按 `Q5`;`PENDING`/`SENT` 均不得成为永不超时的死分支。 +4. 优先按已确认的 SEQN 回显匹配;无回显的降级匹配按 `Q5` 明确风险,只接受已发送开放请求且 `DTTM ≥ SENT_AT`。统一转换为可比较的时间,不能把报文日期数字直接与 epoch 毫秒比较。 +5. 迟到、无匹配或已关闭请求的应答不得更新数据,转 `SKIPPED` 并审计。参考应答成功写入 `REF_MASTER` 后,与请求完成、处理终态和事件在 PG 边界内保持所需原子性。 +6. `POST /schd/sync` 复用请求入口,采用 24 小时制和非空/区间校验;响应明确已登记还是已落信,不承诺计划已更新。 + +**前置**:`US-01`、`US-03`;`Q5`、`Q8`、`Q14`、出站信箱去重契约。请求基础不依赖 `US-06`。 + +### US-09 持久化补偿回填信箱 + +**目标**:本地处理终态最终反映到共享信箱,不因共享库故障回滚已完成业务。 + +**验收标准** + +1. PG 终态与回填意图同事务保存;所有终态路径都经过统一提交边界,不只覆盖成功路径。事务回滚时不得留下可执行回填意图。 +2. 提交后由后台执行回填,主泵不等待共享库;失败按持久记录退避,重启继续执行,不重新执行已完成业务。 +3. SUCCEEDED、规则忽略、身份重复、DEAD 均需回填处理时间;PENDING/FAILED 禁止回填。具体 STATUS 编码按 `Q7` 确认,内部终态不能直接当作外部字段值。 +4. 重复补偿效果幂等,保留稳定的完成时间与审计;重放后的新处理结果不能被旧回填任务覆盖。非法报文缺 META 时也有明确回填方式。 +5. 影子模式禁写,双跑仅一个系统持有标记写权;暴露 PG 终态、回填状态、积压、最老年龄与持续失败告警。 + +**前置**:`US-03` 终态接口;`Q7`、共享库更新权限。覆盖四类终态、事务回滚、重复补偿和重放竞争;生命周期与超期补写以 [implementation.md](implementation.md)「中断恢复」「回填」为准,清除口径以 [specification.md](specification.md)「契约」为准。 + +### US-10 安全重放与故障处置 + +**目标**:运维能定位失败、限定恢复范围,并了解重放对当前航班状态的影响。 + +**验收标准** + +1. 按 ID、错误类、时间查询次数、错误、关联事件与回填状态;重放前预览范围,记录操作者、原因和逐项结果。 +2. 仅 `CODEC_ERROR / UNSUPPORTED / INFRA / EXHAUSTED` 的 FAILED/DEAD 允许申请重放;MALFORMED 与其他不允许项不改状态,返回跳过原因。 +3. 重置 attempts/nextAttemptAt,保留身份、原始入队时间和错误审计;可重放范围受 `Q6` 决定的 `R_keep` 下界(原文保留窗口)约束。重新入队仍按 ID 处理,但不承诺已执行过的后续消息自动撤销。 +4. DEAD 之后可能已有新状态,必须预检版本与覆盖风险;不安全时拒绝直接重放,改用经批准的隔离重建或恢复流程,禁止无保护的全量 `replayAll` 生产入口。 +5. 操作有认证、授权、范围限制与审计;死信、持续补偿失败、队列年龄越界有告警和处理 Runbook。 + +**前置**:`US-03`、`US-09` 的恢复状态;`Q6`、`OPS-1`/`OPS-2` 的安全与可观测基础。 + +### US-11 归档自有库终态记录 + +**目标**:控制自有 PG 在线表规模,不丢未完成工作、不破坏去重与恢复;不是清理共享信箱。 + +**验收标准** + +1. 终态记录在**了结后**经过的时间(`UPDATED_AT`)达到 `PARAM:msgx.proc-state.archive-after` 时列为归档候选;`PENDING`/`FAILED` 禁止归档,回填未了结的终态行不进入候选。 +2. 归档到自有 PG `PROC_STATE_HST`,主表保留 `STATE='ARCHIVED'` 的去重影子行(仅 `IDENTITY_KEY` 与 `MSG_ID`),使归档后同业务身份再次到达仍可去重;`MSG_EVENT` 的历史目标与保留规则由投递清理独立处理,不构成归档判据。 +3. 归档写入与主行置 `ARCHIVED` 在同一自有库事务内完成,按候选时的状态条件复查,影响 0 行即整体回滚;重复执行幂等,失败保留源记录并报告计数。 +4. 本系统不写共享 MySQL `CMINMSGS_HST`、不清理外部信箱;由库方按 `Q9` 执行的清除与历史归档见 [specification.md](specification.md)「契约」。原文可用性与重放保留期由 `Q6`/`Q7` 关联确认。 + +**前置**:`US-03`、`US-09`;`US-07` 提供事件终态规则,`US-10` 提供恢复保留要求。不依赖 `US-15`。自有记录归档见 [implementation.md](implementation.md)「生命周期与清除」。 + +### US-12 查询实时航班(KEEP) + +**目标**:调用方读取与当前权威状态一致的实时航班视图。 + +**验收标准** + +1. 保留 `GET /all/flights`,直接从自有 PostgreSQL `FLIGHT_SCHD` 查询,排除共享航班(`MAID != NULL`,定义见 [implementation.md](implementation.md)「字段与集合」);不改写业务状态。 +2. 固定响应样例、空结果、排序、大小限制及一致性时点。现役未分页时不能无声改为只返回第一页;分页或响应结构变更按 `Q3` 决定。 +3. 依赖异常不能伪装为空数组成功;影子只读影子状态,入口有约定的访问控制、限流与审计。 + +**前置**:`Q1`、`Q3`;所查询的 `US-05`/`US-06` 状态发布能力。 + +### US-13 刷新 21 类参考主数据 + +**目标**:业务使用来自 admin-api 的本地参考数据,刷新失败仍有上次可用版本。 + +**验收标准** + +1. 按 `Q8` 的 21 类清单配置端点、RTYPE/RKEY、字段映射;这是独立于 `US-08` 的数据入口,不另建「参考专用第二 PG」。 +2. 按 `(RTYPE,RKEY)` 幂等写 `REF_MASTER`,记录 SOURCE、刷新时间和批次;单类完整校验后发布,失败不暴露半批。 +3. 一类失败不破坏其他类或该类旧版本;同类由 AODB/admin-api 都提供时明确覆盖优先级,全量刷新时明确已删除项的处理,不能仅靠 SOURCE 日志解决冲突。 +4. 影子默认不主动刷新生产数据;需要参考样本时显式导入隔离副本。 + +**前置**:admin-api 访问契约、`Q8`。可独立于消息处理器开发。 + +### US-14 提供机位与登机桥映射 + +**目标**:PSDT 在不调用外部 HTTP 的情况下得到完整映射,正确计算 `abdg`。 + +**验收标准** + +1. 保留 `ORMS_STAND / ORMS_STAND_AIRBRIDGE` 两类,与 `US-13` 的 21 类分开统计;适配器拉取、完整校验后原子发布只读缓存。 +2. 近机位产生登机桥值,远机位或清空机位时 `abdg` 为空;一机位多桥、缺失映射与共享航班规则用 golden 固定。 +3. admin-api 不可用时使用最后可用版本;无可用版本或映射不完整时明确失败,不用空映射冒充正常清空,也不发布半批。 +4. 处理器输入包含所需只读参考视图,不允许其直接 HTTP 或写缓存。 + +**前置**:机位/桥数据契约及 `Q8`;不要求 `US-13` 全部完成。 + +### US-15 历史航班清场(DEFERRED,阶段 B) + +历史存储确认成功后,才允许删除对应实时航班;逐条隔离坏数据,不能删除写历史失败的集合。判史规则与保留期、业务时区、历史写入与删除事件之间的恢复协议需在启用前完成 golden 对拍。 + +阶段 A 不依赖 ES,不启用 `PROJECTION_REBUILD`。历史清理作业在历史存储未接通(或 `PARAM:msgx.history.history-store-enabled=false`)时删除 0 条;红线见 [implementation.md](implementation.md)「生命周期」。 + +## 3. 运行与切流验收 + +| 编号 | 必须交付的能力 | 验证证据 | +|---|---|---| +| OPS-1 单写者与启动安全 | 生产缺真实适配器、误用 stub、未启用必需管道时拒启;第二活动写者不能启动,失去写权后不得继续写;中断与停机能正确退出。 | 配置拒启、双实例/失去写权及停机测试。单靠副本数配置不算运行期保护。 | +| OPS-2 可观测与安全 | 真实依赖健康、队列/队头年龄、投递/回填滞后、积压与最老未处理信龄、DEAD 和一致性异常有指标、告警与处理入口;敏感管理操作有访问控制,日志不泄漏口令或完整敏感报文。 | 故障注入触发真实告警,消息到事件可关联;日志出口断开不阻塞业务。 | +| OPS-3 影子隔离 | 自有数据库/schema、topic、服务注册身份隔离;输入只读水位或回放,禁生产回填、真实出站和误注册。 | 配置与集成测试证明生产信箱、状态、topic 未被影子修改。 | +| OPS-4 切流与恢复 | 对拍不少于 7 天,未解释业务字段差异为 0,DLQ 积压为 0,`MSG_EVENT` 最老滞留 < 5 秒;切流后 48 小时观察,24 小时内具备经演练的回滚能力。 | 明确负载与统计口径的对拍报告;Runbook 含停写、排空/水位、状态恢复、写权交接和失败回退,不能只回滚程序版本。 | + +上述阈值沿用既有需求基线,需在真实环境提供证据。自有库备份、报文保留和完整状态重建需要恢复演练;本地事务不能承诺任意数据库灾难下 RPO=0,也不承诺未经演练的「一键无损回滚」。 + +## 4. 需求覆盖与依赖 + +### 4.1 覆盖矩阵 + +| 需求 | 必须闭合的能力 | 约束 / 偏差锚点 | +|---|---|---| +| `US-01` | 按 ID 有限采集、持久水位、幂等入队、空洞与中断恢复 | `INV-2`~`INV-5`、`Q2` | +| `US-02` | 安全兼容注入,落信确认与业务完成分离 | `C-28`、`Q3`、`G-COMPAT-HTTP` | +| `US-03` | 严格 FIFO、安全解码、身份去重、事务提交与持久重试 | `INV-3`、`INV-6`~`INV-10`、`INV-17`、`Q6`、`Q11`、`Q15` | +| `US-04` | 忽略报文在身份绑定后无业务副作用终结并回填 | `INV-8`、`INV-9` | +| `US-05` | ADFT、FDEL、29 类 FLOP、完整航班态及主/共享关系 | `INV-11`~`INV-22`、`Q8`、`Q13`、`Q14`、`Q16`、`G-FLOP-IDEMPOTENT`、`G-MAFL`、`G-SRVT-VIPF` | +| `US-06` | DNLD/RESP 整包快照、请求守卫、迟到应答隔离 | `INV-12`、`INV-15`、`INV-19`、`Q5`、`Q13`、`G-RESP-GUARD` | +| `US-07` | Kafka 至少一次投递、同 `FLID` 保序、schd 聚合、失败与清理 | `D3`、`INV-10`、`C-29`、`Q4` | +| `US-08` | 14 类 RQRD、1 类 RQFD、出站落信、开放请求唯一、匹配与超时 | `C-23`、`C-24`、`Q3`~`Q5`、`Q8`、`Q10`、`Q14`、`G-REQ-TRACK`、`G-REQ-OPEN-UNIQUE`、`G-REQ-TRACK-RETENTION` | +| `US-09` | 终态回填意图、后台补偿、四结果、放弃与人工恢复 | `INV-7`、`INV-8`、`C-5`~`C-8`、`Q7`、`Q9`;`C-6` 不成立时闭合 `G-REPLAY-CHANNEL` | +| `US-10` | 可查询、可预览、白名单重放、风险预检、授权与审计 | `CLM-3`、`Q6` | +| `US-11` | 已了结终态归档、去重影子、竞态复查及独立保留期 | `D4`、`C-14`、`C-16`、`G-PROC-HST`、`G-HST-RETENTION` | +| `US-12` | 从 PG 权威态查询实时主航班,固定契约且依赖失败不伪装为空 | `INV-11`、`Q3` | +| `US-13` | 21 类参考主数据完整校验、原子发布、失败保旧 | `Q8` | +| `US-14` | 机位/登机桥映射原子发布,PSDT 只读计算 | `Q8` | +| `US-15` | 阶段 B 历史归档成功后清场及删除事件恢复 | `D1`、`INV-18`、`Q9`、`G-FLIGHT-HIST-RETENTION` | +| `OPS-1` | 真实适配器、配置与单写者拒启,失权停写,安全停机 | `D2`、`PRE-5` | +| `OPS-2` | 真实健康、积压/失败指标、告警、受控处置与敏感信息保护 | reference.md | +| `OPS-3` | 影子数据库、topic、服务身份隔离并禁生产写 | `PRE-1` | +| `OPS-4` | 对拍、切流观察、恢复演练及完整回退规程 | `C-1`~`C-29` | + +### 4.2 契约依赖索引 + +`Q` 的定义与完整表述只在本文件之外一处:[specification.md](specification.md)「待确认事项台账」。此处只列与验收直接相关的依赖: + +- `US-01` / `US-09` 的验收依赖 `Q2`(发现完整性)与 `C-8`(清除前提); +- `US-06` / `US-13` / `US-14` 依赖 `Q8`(逐类清单)与 `Q13`(字段缺失语义); +- `US-08` 依赖 `Q3`(HTTP 契约)、`Q4`(Kafka wire)、`Q5`(请求匹配)、`Q10`(出站契约); +- `US-10` 依赖重放白名单与 `CLM-3`(重放安全); +- `US-15` 依赖 `Q9`(清除授权与 DDL)。 + +业务日期/日计划采用 `Asia/Shanghai`;持久化与比较使用明确的时间类型和转换规则,不靠服务器默认时区,也不直接比较不同单位的数字。 + +## 5. HTTP 工具边界 + +| 端点 | 范围 | +|---|---| +| `POST /cminmsgs/send` | `US-02`,保留接收兼容性。 | +| `POST /schd/sync` | `US-08`,保留并修正参数校验;不等同于同步完成快照。 | +| `GET /all/flights` | `US-12`,保留查询语义。 | +| `POST /kafka/topics/{name}/msgs` | 不进生产;开发工具若保留,另行限制 topic allowlist。 | +| `GET /flights/migrate` | 不做,属于 legacy 一次性迁移工具。 | diff --git a/docs/spec-boundary-closure.md b/docs/spec-boundary-closure.md deleted file mode 100644 index 033249e..0000000 --- a/docs/spec-boundary-closure.md +++ /dev/null @@ -1,51 +0,0 @@ -# 需求边界闭合方案 - -## 1. 目标 - -阶段 A 闭合 `US-01`~`US-14`、`OPS-1`~`OPS-4`;`US-15` 保留为阶段 B 边界。 - -- 需求与验收以 `US-xx`、`OPS-x` 为唯一依据,本文件只做实施覆盖映射。 -- 实现必须遵守 `D1`~`D4`、`PRE-1`~`PRE-8`、`INV-1`~`INV-22`、`CLM-3`~`CLM-10`、`C-1`~`C-29`。 -- 参数、指标和错误分类只引用 `PARAM:` 与 `reference.md`,不重复取值。 -- `Q2`~`Q16` 未定案时只阻断对应验收,不补写猜测。 - -## 2. 边界 - -- 航班当前态只存自有 PG;不引入 Redis、阶段 A 的 ES、新业务库、并行主泵或分布式锁。 -- 共享 MySQL 只做契约内读写与回填;不建表、不增列、不迁移、不写共享历史表(`C-14`)。 -- 生产保持单活动实例;不承诺端到端恰好一次、跨 `FLID` 顺序或未经验证的 Oracle 11g 支持。 -- `US-15` 未启用前遵守 `D1`:历史写入未确认成功时航班清场删除 0 条。 -- 进度与排期只记 Plane,本文件不记录完成状态。 - -## 3. 需求覆盖 - -| 需求 | 必须闭合的能力 | 约束 / 缺口锚点 | -|---|---|---| -| `US-01` | 按 ID 有限采集、持久水位、幂等入队、空洞与中断恢复 | `INV-2`~`INV-5`、`Q2` | -| `US-02` | 安全兼容注入,落信确认与业务完成分离 | `C-28`、`Q3`、`[G-COMPAT-HTTP]` | -| `US-03` | 严格 FIFO、安全解码、身份去重、事务提交与持久重试 | `INV-3`、`INV-6`~`INV-10`、`INV-17`、`Q6`、`Q11`、`Q15` | -| `US-04` | 忽略报文在身份绑定后无业务副作用终结并回填 | `INV-8`、`INV-9`、`[G-IGNORE]` | -| `US-05` | ADFT、FDEL、29 类 FLOP、完整航班态及主/共享关系 | `INV-11`~`INV-22`、`Q8`、`Q13`、`Q14`、`Q16`、`[G-FLOP-IDEMPOTENT]`、`[G-MAFL]`、`[G-SRVT-VIPF]` | -| `US-06` | DNLD/RESP 整包快照、请求守卫、迟到应答隔离 | `INV-12`、`INV-15`、`INV-19`、`Q5`、`Q13`、`[G-RESP-GUARD]` | -| `US-07` | Kafka 至少一次投递、同 `FLID` 保序、schd 聚合、失败与清理 | `D3`、`INV-10`、`C-29`、`Q4`、`[G-KAFKA-D3]`、`[G-EVENT-RETENTION]` | -| `US-08` | 14 类 RQRD、1 类 RQFD、出站落信、开放请求唯一、匹配与超时 | `C-23`、`C-24`、`Q3`~`Q5`、`Q8`、`Q10`、`Q14`、`[G-REQ-TRACK]`、`[G-REQ-OPEN-UNIQUE]`、`[G-REQ-TRACK-RETENTION]` | -| `US-09` | 终态回填意图、后台补偿、四结果、放弃与人工恢复 | `INV-7`、`INV-8`、`C-5`~`C-8`、`Q7`、`Q9`、`[G-BACKFILL-BACKOFF]`;`C-6` 不成立时闭合 `[G-REPLAY-CHANNEL]` | -| `US-10` | 可查询、可预览、白名单重放、风险预检、授权与审计 | `CLM-3`、`Q6` | -| `US-11` | 已了结终态归档、去重影子、竞态复查及独立保留期 | `D4`、`C-14`、`C-16`、`[G-PROC-HST]`、`[G-HST-RETENTION]` | -| `US-12` | 从 PG 权威态查询实时主航班,固定契约且依赖失败不伪装为空 | `INV-11`、`Q3` | -| `US-13` | 21 类参考主数据完整校验、原子发布、失败保旧 | `Q8` | -| `US-14` | 机位/登机桥映射原子发布,PSDT 只读计算 | `Q8` | -| `US-15` | 阶段 B 历史归档成功后清场及删除事件恢复 | `D1`、`INV-18`、`Q9`、`[G-FLIGHT-HIST-RETENTION]` | -| `OPS-1` | 真实适配器、配置与单写者拒启,失权停写,安全停机 | `D2`、`PRE-5` | -| `OPS-2` | 真实健康、积压/失败指标、告警、受控处置与敏感信息保护 | `reference.md` | -| `OPS-3` | 影子数据库、topic、服务身份隔离并禁生产写 | `PRE-1` | -| `OPS-4` | 对拍、切流观察、恢复演练及完整回退规程 | `C-1`~`C-29` | - -## 4. 统一验收 - -- 每个 PR 标明 `US-xx/验收条目`、相关 `Q`、迁移、配置、恢复影响和未包含范围。 -- 排序、重试、幂等、投递必须覆盖 `invariants.md` 的验证映射;业务类型必须有 golden 样例。 -- 时间测试使用 `TestClocks.kt`;领域与适配器测试使用内存实现,事务与迁移使用既有 PG 测试设施。 -- 自动化测试禁止 `Thread.sleep` 和真实外部基础设施;Kafka/MySQL 现场验收单独留证。 -- 使用锁定的 JDK 25、Micronaut 5.1.3 及现有 KSP/Jackson 编译参数,不升级或重构无关模块。 -- 真实适配器、契约、测试及对应 `OPS-x` 未闭合时,不得宣称需求或生产切流完成。 diff --git a/docs/specification.md b/docs/specification.md new file mode 100644 index 0000000..38d807f --- /dev/null +++ b/docs/specification.md @@ -0,0 +1,232 @@ +# 规范:术语、契约、前提、不变量与声明边界 + +本文件是以下事实的唯一出处: + +- 对外**术语**(本文件「术语」节); +- 本系统与外部对手方的**承诺与要求** `C-x`,以及我们向库方/上游的单方承诺; +- **待确认事项** `Qn` 注册表; +- 外部提供的**前提** `PRE-x`; +- 本系统保证的**不变量** `INV-x`; +- **声明边界** `CLM-x`:每条对外主张依赖哪些 `PRE`/`INV`、可否声明、挂起原因; +- **当前已知偏差** `G` 注册表; +- **验证映射**:验收口径的唯一清单。 + +编号稳定不变;条款被取代时标 `[作废 by C-y]` 并保留原文,不静默改写。不变量变更用「追加 + 作废」(`INV-7 → [作废 by INV-7b]`)。机制与领域规则见 [implementation.md](implementation.md),参数取值见 [reference.md](reference.md)。 + +## 1. 术语 + +| 术语 | 含义 | +|---|---| +| 上游 | 向信箱写入报文的源头系统(CIIMS、AODB 等)。 | +| 信箱 | 共享 MySQL 的入站表 `CMINMSGS`;出站方向为 `COUTMSGS`。 | +| 库方 | 共享 MySQL 的管理方;表结构变更与数据清除只能由库方执行或书面授权。 | +| 处理标记 | 信箱行上表示「本系统已处理」的约定字段;逻辑名 `DATE_PROCESSED` / `STATUS`,实际列名以库方契约为准。本系统只把空标记写成已处理值,不回撤、不覆盖。 | +| 落信 | 报文进入信箱(`CMINMSGS` 存在该行),执行方是上游。 | +| 入队 | 本系统在自有 PG 建立 `PROC_STATE` 记录,开始处理。 | +| 已回填 | 本系统已把处理标记写回该信箱行。 | +| 投递确认 | 投递目标已接受且本地 `MSG_EVENT` 已置 `SENT`;不表示业务消费者已消费。 | +| 自有 PG | 本系统唯一的业务数据库 PostgreSQL;与信箱之间不存在跨库事务。 | + +管道内部术语(`W`、队头、终态、回填意图)定义在 [implementation.md](implementation.md)「术语与持久化记录」。 + +条款状态词只有三种:`[待确认 Qn]`(未取得对方书面确认)、`[已确认 YYYY-MM-DD]`(对方书面确认且已回写)、`[我们单方承诺]`(不依赖对方,已生效)。 + +## 2. 契约 + +读者:库方(共享 MySQL 管理方)接口人、上游(CIIMS / AODB / SIS)接口人、本系统开发与运维。 + +### 2.1 共享信箱(库方) + +**ID 与可见性** + +- **C-1** ID 单调:信箱 ID 按提交顺序分配,已发布水位之下不再出现更小的新 ID。`[待确认 Q2]` +- **C-2** ID 分配 → 事务可见时延上界由库方**直接给出**。该值决定空洞老化阈值;**不可由 SIS 报文 `Expiry` 推导**(`Expiry` 是报文保留与传输恢复口径,与「ID 分配后多久对读事务可见」不是同一个量)。`[待确认 Q2]` +- **C-3** ID 空间不复位、不复用、不回退:含表轮换、备份恢复、`AUTO_INCREMENT` 归零。采用整表轮换方案时,新表种子必须 ≥ `max(ID)+1`,保证 ID 不断链;本系统的水位 `W` 是不可逆单游标,ID 回退会导致其后所有行永久不可见。`[待确认 Q2]` +- **C-4** 报文行不可变:同一业务身份(`SNDR|TYPE|STYP|SEQN`)的重发必为同一内容。若上游会以同一身份改发正文,需要另定识别规则(`Q15`)。`[待确认 Q15]` + +**保留与清除(标记、保留期、清除前提)** + +- **C-5** 处理标记值集与写权限:本系统只写入库方认可的 legacy 值集内的「已处理」值(默认值见 [reference.md](reference.md) `PARAM:mailbox.processed-value`),只写空标记、不回撤、不覆盖;内部原因(死信、重复、放弃)记录在自有 PG,**不在信箱新增枚举**。`[待确认 Q7]` +- **C-6** 清除语义必须是「标记 + 保留期」:打标本身不触发清除,触发条件是「到达保留期 `R_keep`」且「边界内全部行已打标」。若库方语义是「打标即可清除」,则清除前置条件不成立,且**增大 `R` 无法补救**,必须另行约定保留期或引入独立原文保留通道。`[待确认 Q7][待确认 Q9]` +- **C-7** 保留期下界(本文件是唯一定义处): + `R_keep ≥ max(人工重放期限 + 人工处置期限, 审计期限, 回填重试上限)`。 + 这是「重放窗口内原文仍在」的**唯一保证来源**。报文在 CIIMS 的 `Expiry`(480 分钟量级,`SIS:3.16`)可作为原文保留期的参照,但它是报文有效期,不等于本处所需的保留期。`[待确认 Q6][待确认 Q9]` +- **C-8** 清除前置条件(本文件是唯一定义处):执行清除时,边界内**每行必须已有终局**——即「已持有处理标记」**或**「已登记在本系统的回填放弃清单中且经人工对账确认」。放弃行不写标记,未达终态的行顺延至处理完成后清除;本系统不执行 DDL,也不写共享历史表。`[待确认 Q7][待确认 Q9]` +- **C-9** 清除执行方与方案:清除由库方执行或书面授权执行。方案 A(按 `DATE_RECEIVED` 日分区 + `TRUNCATE/DROP PARTITION`)为首选;方案 B(`CREATE TABLE ... LIKE` + 保留窗复制 + `RENAME TABLE` + 对账 + `CMINMSGS_HST` 归档 + `DROP`)为备选。现场 MySQL 版本与分区 DDL 能力待确认。`[待确认 Q9]` +- **C-10** 时间语义:时间比较与换算统一采用机场时区 `Asia/Shanghai` 及明确类型转换;`DATE_RECEIVED` 由上游/库方写入,其时钟基准需可解释(见 `PRE-4`)。`[待确认 Q7]` + +**原文保留与重放** + +- **C-11** 重放窗口内的原文必须可读:legacy 现役按接收超 1 天归档并删除 `CMINMSGS`;若沿用该窗口,则与 `C-7` 冲突,须以 `C-7` 为准。`[待确认 Q9]` +- **C-12** 若原文被提前清除(违反保留契约),本系统的死信处置不变,按契约违例走运维追责;该情形不改变 `C-8` 的清除前提。`[我们单方承诺]` + +**我们向库方的承诺** + +- **C-13** 只读约定区间的信箱行(`ID > W`),单活动实例运行,不引入并行消费者。`[我们单方承诺]` +- **C-14** 不建表、不改表结构、不迁移 schema、不写共享历史表;兼容 HTTP 入口按既有契约写入入站信箱。`[我们单方承诺]` +- **C-15** 处理标记只写 `C-5` 认可的值,不回撤、不覆盖已有非空标记。`[我们单方承诺]` +- **C-16** 回填放弃清单在对应信箱边界被清除前必须保持可查:`C-8` 以本清单作为清除授权证据之一,该证据不得随处理记录的归档或清除而消失。`[我们单方承诺][待确认 Q7][待确认 Q9]` + +### 2.2 上游(SIS / AODB) + +- **C-20** 业务身份四元组 `SNDR|TYPE|STYP|SEQN` 的语义由上游定义;`SEQN` 的取值范围与回绕见 `SIS:2.8.1`。**重置周期未知**,它决定业务身份是否加入日期边界(默认不加)。`SNDR` 取值域也需对拍(SIS 为 AODB/RMS,legacy 实发 OSH5 等)。`[待确认 Q11]` +- **C-21** `FLID` 在保留期内不复用。若复用,事件版本(`STATE_VERSION`)必须按 incarnation 作用域,否则「保留最新版本」的合并规则会把新航班的事件压掉,旧 tombstone 也可能删掉在用航班。`[待确认 Q16]` +- **C-22** 报文不可变(同 `C-4`)。`[待确认 Q15]` +- **C-23** 请求/应答回显契约:目标优先按已确认的回显字段精确匹配;回显未确认时的降级匹配(同类开放请求且报文 `DTTM ≥ sentAt`)存在跨代误配风险,必须明确接受并审计,不得宣称精确关联。比较前统一时区与时间单位。`[待确认 Q5]` +- **C-24** 出站信箱 `COUTMSGS`:消费方与消费顺序、`COUTMSGS_ACK_DATE_RECV` / `COUTMSGS_ACK_RESEND_TIMES` / `COUTMSGS_DATE_SENT` / `COUTMSGS_ERROR` 各列语义与写入责任、出站行清除责任与保留期、落信成功但本地未置 `SENT` 时的重复写入风险及下游去重契约,均未确认。本系统对出站的交付承诺只到**落信**为止。`[待确认 Q10]` +- **C-25** 主 / 共享删除顺序与 EROR 回报:SIS 要求删主航班前先删子共享航班,顺序不符时 RMS 应向 AODB 回发 EROR(`SIS:1.6.1-1.d`,事件定义 `SIS:4.8`);现行设计为幂等原子级联、不回发 EROR。二选一。`[待确认 Q14]` +- **C-26** 日计划缺失可选字段的语义:SIS 要求最新日计划中未发送的可选字段表示 AODB 已无该数据、子系统应删除本地值(`SIS:3.16-note-4`,RESP 同格式见 `SIS:3.17`),与现行「未携带字段保留」相反。`[待确认 Q13]` +- **C-27** 历史积压批次中「不再处理」的确认主体、审批留痕与跳过值集。`[待确认 Q12]` + +**我们向上游的承诺** + +- **C-28** 兼容 HTTP 入口的响应只表示**接收结果**,不表示业务处理成功:目标为现役 `ResponseDto`(`is_success` / `body`),请求体上限暂定 10MB;请求媒体类型、字符集与失败响应仍需与现役逐项对拍。`[待确认 Q3]` +- **C-29** 对外投递按**至少一次**设计,不承诺端到端恰好一次;Kafka 消息的 key 为 `FLID`,同一 `FLID` 内保序,跨 `FLID` 不承诺顺序。`[待确认 Q4]` + +## 3. 前提(外部提供) + +前提失效时不变量必须整体重估。 + +| 编号 | 前提 | 若不成立的影响 | 状态 | +|---|---|---|---| +| PRE-1 | 信箱消费权排他:同一时刻只有一个系统有权处理、打标、判定可清除(迁移期由切流规程保证单一权威写者) | 水位、身份去重、清除前提全部失效 | `[待确认]`(切流由运维规程保证,上线前另立) | +| PRE-2 | ID 单调 + 可见时延上界:见 `C-1`/`C-2` | 水位只能当快路径提示;空洞老化阈值无依据;不能声明发现完整性 | `[待确认 Q2]` | +| PRE-3 | ID 空间不复位、不复用、不回退:见 `C-3` | 水位(不可逆单游标)之后的行永久不可见 | `[待确认 Q2]` | +| PRE-4 | 报文的 `DATE_RECEIVED` 时钟基准可解释(偏斜在有界范围内) | 跨系统时间比较(`RECEIVED_AT` 与本地 `NOW`)会提前或推迟判定 | `[待确认 Q7]` | +| PRE-5 | 单活动实例运行(信箱读取不加锁、水位是单行覆盖写) | 水位互相覆盖、空洞计时失真 | `[我们自证]`(部署约束,见 architecture.md) | +| PRE-6 | 信箱与自有 PG 之间没有跨库事务 | 回填、水位推进、清除都不能声称原子 | `[我们自证]`(架构事实) | +| PRE-7 | 报文不可变:同一业务身份的重发必为同一内容:见 `C-4` | 上游改发会被判为重复并静默跳过 | `[待确认 Q15]` | +| PRE-8 | `FLID` 在保留期内不复用:见 `C-21` | 「保留最新版本」的合并规则可能压掉新航班事件,旧 tombstone 可能删掉在用航班 | `[待确认 Q16]` | + +## 4. 不变量 + +### 4.1 管道 + +- **INV-1** 五个独立事实互不替代:落信 / 入队 / 处理完成 / 已回填 / 投递确认各有独立证据,前一个不蕴含后一个。 +- **INV-2** 水位与入队同事务:不允许出现「水位已推进、消息未入队」的持久化状态;水位只增不减,遇空洞即停,只有判定为永久空洞才放行,且放行只跳过空洞本身、不越过任何已存在的行。 +- **INV-3** 队头唯一:任一时刻只有一个可执行队头(最小未完成 `MSG_ID`,`PENDING` 与 `FAILED` 都占位);`FAILED` 未退避到期时后续消息不得越过。 +- **INV-4** 只领取已发现的行:主泵只领 `MSG_ID ≤ W`;水位之外的行只可能来自兼容入口,必须等水位追平后按序处理。 +- **INV-5** 发现与处理互不阻塞:收报只看 `ID > W`,不以处理标记为谓词;终态而未回填的行不阻断后续消息的发现。 +- **INV-6** 处理终态不可逆:已提交的 `SUCCEEDED` 不因回填或投递失败回改。 +- **INV-7** 处理标记单调:任何路径只把空标记写成已处理值,不回撤、不覆盖。 +- **INV-8** 回填只针对终态(`PENDING` / `FAILED` 永不写标记);「还欠一次回填」的事实与终态由**同一条语句**落库,不存在第二处落账。 +- **INV-9** 一信一行、一身份一记录:`PROC_STATE` 按 `MSG_ID` 唯一;同一业务身份至多绑定一条有效处理记录。 +- **INV-10** 对外投递至少一次;端到端恰好一次不在交付范围。 + +### 4.2 航班域 + +- **INV-11** 自有 PG 的航班当前态是唯一权威;信箱、Kafka、展示视图都不是权威。 +- **INV-12** `FLID` 唯一;已写入非空的 `OPERATION_DAY` 不可改变。 +- **INV-13** 每个航班每次成功状态写入单调推进 `STATE_VERSION`;重复消息不重复推进。 +- **INV-14** 报文未携带的字段不被隐式清空;集合按完整合并结果写入,保留输入顺序与源序号。 +- **INV-15** 缺席于某个日计划不构成删除理由;删除只由 FDEL 或受控历史清理触发。 +- **INV-16** 外部副作用(回填、Kafka 投递、出站信箱)失败可重试,但不回滚已提交的本地业务结果。 +- **INV-17** 状态变更、待发事件、处理终态与回填意图在同一 PG 事务内原子提交。 +- **INV-18** 航班表的写者集合是「主泵处理器」与「历史清理」;两者必须互斥(同一 `PIPELINE_LOCK`,或清理在同一事务内复查判据后再删除),不得出现清理删除与处理器更新同一 `FLID` 的竞态。 +- **INV-19** 整包校验失败或运营日冲突时整包不落地,既有状态与版本保持不变。 +- **INV-20** 处理器幂等:同一消息重复执行只产生一次业务效果。身份唯一只防「重复记录」,不防「重新执行」;29 类 FLOP 幂等矩阵补全前,本条**不可声明**(`G-FLOP-IDEMPOTENT`)。 +- **INV-21** `MAFL` 是派生投影:内容恒等于「`STATE = ACTIVE` 且 `MAID = 主航班 FLID`」的子航班集合(元素 `FLID` + `FLNO`,按 `FLID` 升序),不落库、不从入站解析;自引用与悬挂引用不入投影。 +- **INV-22** 子航班集合变化必须使涉及的主航班在同一事务内推进 `STATE_VERSION` 并登记主航班事件;投影只进不退,版本不推进即被下游丢弃。 + +## 5. 声明边界 + +| 编号 | 主张 | 依赖 | 当前可否声明 | 挂起原因 | +|---|---|---|---|---| +| CLM-3 | 重放不产生重复业务副作用 | INV-20、`G-FLOP-IDEMPOTENT` | **不可** | 29 类 FLOP 幂等矩阵未补全;重放不恢复历史顺序 | +| CLM-4 | 回填不会被短暂故障放弃:最终打标,或进入可对账的放弃清单 | INV-8、`C-5`、`C-8` | **可声明(有条件)** | 条件:`R` 之前不放弃;`MISSING_ROW` 立即放弃并告警;放弃行须经人工对账才可用于清除判定(`C-8`)。原文保留另见 CLM-5 | +| CLM-5 | 重放窗口内原文仍可读 | `C-6`、`C-7`、`Q7`、`Q9` | **不可** | 清除语义与保留期未确认;「打标即清除」下无补救 | +| CLM-6 | 单实例内严格 FIFO | PRE-5、INV-3 | **可**(限于单活动实例) | — | +| CLM-7 | 事件投递在同一 `FLID` 内保序 | INV-10、投递设计 | **可**(跨 `FLID` 不承诺) | 实现当前按目标级全序投递,收敛到按 `FLID` 属投递改造 | +| CLM-8 | 出站交付承诺只到「落信」 | `C-24`、`Q10` | **可**(仅落信语义) | 消费方与 ACK 列语义未确认 | +| CLM-9 | 处理标记延迟由调度周期决定 | — | **不可** | 扫描周期不等于完成时限;批次积压、单行超时与历史作业都会延长实际延迟 | +| CLM-10 | 容量量级假设(单实例、入站日消息量千级到万级、单报文 ≤ 10⁴ 字节) | — | **不可** | 未实测,无生产负载数据;解除条件:取得现役信箱日量、峰值与单报文上限后重估 | + +## 6. 验证映射 + +每条不变量至少一条证据。测试名以仓库现状为准;新增测试按本表补位。本表只记录**验收口径与证据位置**,覆盖进展只在 Plane(ACM2)。 + +| 不变量 / 声明边界 | 场景 | 证据 / 测试 | +|---|---|---| +| INV-1 | 五事实互不替代:入队不引用标记、回填不引用投递、投递不引用回填 | 需接口级断言 | +| INV-2 | 重复扫描、入队中断 | 不重复入队、不丢记录;`InboxPollerTest` | +| INV-2 | 空洞老化与重置 | 阈值内不推进、不越过入队;超期只放行空洞本身;旧空洞补齐后新空洞获得完整窗口 | +| INV-2 | 水位写入与入队同事务 | 需真实 PG 事务用例 | +| INV-3 | 较小 ID 迟提交 | `InboxPollerTest` 钉住「水位越过后到达的较小 ID 不被发现」;水位遇空洞即停、空洞老化放行只跳过空洞本身 | +| INV-3 | 队头失败、退避及作业竞争 | 消息不越队;到期后恢复;作业不使消息无限饥饿 | +| INV-4 | 兼容入口与空洞并发 | `PipelineSmokeTest`「compat injected high id is not claimed until the watermark catches up」 | +| INV-5 | 终态未回填不阻断发现 | 需断言发现谓词不引用处理状态 | +| INV-6 | 投递失败后终态不变 | 需用例 | +| INV-7 | 回填四种结果 | 写入成功 / 早已标记(不覆盖、记成功)/ 信箱行不存在(立即放弃并告警,不得视为已标记)/ 暂时故障持续到 `R` 仍未打标(停止自动重试,可人工恢复) | +| INV-7 | `RECEIVED_AT` 为 NULL | 超期分支仍成立且不导致标记提前写入——判据是本地 `ENQUEUED_AT`,与库方时钟及 NULL 无关 | +| INV-8 | PG 提交失败、信箱回填失败 | 事件、终态与回填意图一起回滚;已提交结果只补写标记,不重放业务;中间态永不补写 | +| INV-8 | 非业务型终态 | 不触碰航班表 / `MSG_EVENT`,只写 `PROC_STATE`,且终态与回填意图同语句生效 | +| INV-9 | 同身份多条记录、失败后重试、归档后重复 | 只产生一次有效业务处理,不把自身重试判为重复 | +| INV-10 | 投递确认丢失、批次失败、次数耗尽 | 允许可识别的重发、保持目标顺序、整批退避并保留死信 | +| INV-11 | 权威唯一 | 需断言展示视图与缓存不得成为写入或对账来源 | +| INV-12 / INV-13 | PG 事务失败、快照重复或迟到 | 整体回滚重试、不重复推进版本、不回退状态、不误删增量航班 | +| INV-12 | 运营日冲突 | 整包 `DEAD(PROTOCOL)`,既有状态与版本不变 | +| INV-15 | 缺席不删除 | 需分别断言 FDEL 与清理路径 | +| INV-16 | 外部副作用失败后本地结果不变 | 需用例 | +| INV-17 | 业务型终态四件套同事务 | 需真实 PG 用例 | +| INV-18 | 清理与处理并发 | `HistorySweepJobTest`(归档后被主泵更新的航班不删除、不发 tombstone)+ `HistorySweepPurgePgTest`(删除阶段失败时 tombstone 与删除整体回滚) | +| INV-19 | 整包协议拒绝(声明数不符、运营日冲突) | `DEAD(PROTOCOL)`,整包不落地、整体回滚、既有状态不变 | +| INV-21 | `MAFL` 投影与 `ACTIVE` 子航班集合一致(子航班删除后退出、自引用与悬挂引用不入、顺序确定) | `G-MAFL`:投影未实现 | +| INV-22 | 子航班新增、删除、`MAID` 迁移时主航班版本与事件 | `G-MAFL`:主/共享级联未实现 | +| INV-20 / CLM-3 | 重放同一条消息 | 阻塞于 29 类 FLOP 幂等矩阵 | +| — | 请求超时、无匹配 RESP、时间单位不一致 | 不误用迟到应答、不提前完成请求 | +| — | stub 误配置、重复实例、停机中断 | 生产拒绝不安全启动,工作线程能正确退出 | + +上表首列是**引用**(`INV-x` 的定义见本文件「不变量」);同一行可覆盖多个 `INV`,例如 `INV-20 / CLM-3`。 + +声明边界的证据指针:`CLM-4` 断言放弃行不写标记、不被当作已打标;`CLM-9` 的指标已就位(`msgx.pipeline.job.heartbeat_age_seconds` / `ticks.total` / `failures.total` / `last_sweep_selected` 与 `msgx.pipeline.backfill.oldest_unmarked_seconds`),实际延迟仍需现场数据。 + +## 7. 当前已知偏差 + +本表是**偏差标记的唯一出处**:其他文档只在相应位置写 `G-NAME`,不解释、不记进度。偏差只写事实与受影响稳定 ID,不写负责人、日期、进度或 Plane 状态;闭合时在同一变更中删除本行与全仓 `G-NAME` 引用,关闭证据留在 Plane。 + +| 偏差 | 含义 | 影响 | +|---|---|---| +| `G-RESP-GUARD` | `RESP` 应答守卫未实现,当前与 `DNLD` 无差别进入快照写入 | 请求匹配闭环;`C-23` | +| `G-REQ-TRACK` | `REQ_TRACK` 无运行时协调器:出站适配、请求编码、超时与应答匹配未实现 | `US-08`;`C-24` | +| `G-PROC-HST` | `PROC_STATE_HST` 未建表,终态归档未落地 | `US-11`;归档能力 | +| `G-FLOP-IDEMPOTENT` | 29 类 FLOP 幂等矩阵未补全 | `INV-20`、`CLM-3` | +| `G-REPLAY-CHANNEL` | 「打标即清除」语义下的独立原文保留通道未设计 | `CLM-5` | +| `G-MAFL` | 主航班 `MAFL` 派生投影及主/共享原子级联未实现(规则见 `INV-21`/`INV-22`);`MAFL` 不是 SIS/XML 入站字段 | 航班完整态;删除与重建 | +| `G-SRVT-VIPF` | SIS/XML 的 `SRVT`、`VIPF` 无界集合尚未映射到持久化明细;wire/domain 只保留出现事实与原始内容,不参与合并与投递(清空语义见 `Q13`) | 航班完整态;无损字段保存 | +| `G-COMPAT-HTTP` | compat 入口仍未实现 `Q3` 定案后的 ResponseDto、媒体类型、字符集、失败响应与请求体上限 | `C-28`;`US-02` | +| `G-REQ-OPEN-UNIQUE` | `REQ_TRACK` 尚无约束开放态 `(REQ_TYPE, OPERATION_DAY, SENDER)` 唯一性的部分索引 | `US-08`;`G-REQ-TRACK` | +| `G-HST-RETENTION` | 归档目标(`PROC_STATE_HST` 及后续归档表)的保留期与清除作业未定义 | 归档只转移不减少容量占用;`G-PROC-HST` | +| `G-FLIGHT-HIST-RETENTION` | 航班历史存储(外部)的保留期与容量上限未定义 | `D1`;`FLIGHT_SCHD` 物理清除后历史存储是唯一副本 | +| `G-REQ-TRACK-RETENTION` | `REQ_TRACK` 关闭态行(`DONE`/`EXPIRED`)的保留期与清除作业未定义 | 自有 PG 无界增长;`US-08` | + +## 8. 待确认事项台账 + +| 编号 | 事项 | 当前假定 | 阻塞 | 状态 | +|---|---|---|---|---| +| Q1 | 权威存储(内部方向) | 自有 PG 单库权威 + 无损明细;现场供库目标 Oracle 11g | — | 已定案(内部),Oracle 适配与部署验收另计 | +| Q2 | 信箱 ID 单调、ID 分配→事务可见时延上界、ID 空间不复位;空洞与迟到处置 | 时延按 5 分钟 `max-commit-delay`(**缺少依据的占位值**,不可由 SIS `Expiry` 推导) | 发现完整性声明、空洞老化阈值、水位不可逆性 | 未确认 | +| Q3 | HTTP 契约:媒体类型、字符集、错误码、查询接口对拍 | 目标与上限见 `C-28` | 兼容入口验收 | 未确认 | +| Q4 | Kafka wire:发送粒度、key、去重标识、分区与批次确认 | 逐 `FLID` 发送,key=`FLID` | 投递契约 | 未确认 | +| Q5 | 请求匹配:回显字段可靠性与降级匹配 | `RQFD` 60 秒 / `RQRD` 30 秒超时 | 请求跟踪闭环 | 未确认 | +| Q6 | 重放期限与人工处置期限的取值(唯一作用是决定 `R_keep` 下界) | `R` = 30 天;重放/处置期限未定 | `R_keep` 取值 | 未确认 | +| Q7 | 处理标记值集与写权限、原文保留期、处理时间语义 | 写入 `PROCESSED` | 回填值集、保留期下界 | 未确认 | +| Q8 | 逐类覆盖清单(积压摸底的类型分布依据) | — | 积压处置与逐类矩阵 | 暂缓(现阶段不处理) | +| Q9 | 清除执行方与 DDL 授权、方案 A/B 选型、分区能力 | 首选方案 A | `R_keep` 与清除边界 | 未确认 | +| Q10 | 出站消费方、ACK 列语义、出站清理与去重契约 | — | 出站信箱 | 未确认 | +| Q11 | 上游 `SEQN` 重置周期与业务身份的日期边界 | 不含日期边界 | 身份算法 | 未确认 | +| Q12 | 积压批次「不再处理」的确认主体、审批留痕与跳过值集 | — | 积压跳过处置 | 未确认 | +| Q13 | 日计划缺失可选字段的删除语义 | 保留未携带字段;真实消息与 SIS 冲突时以真实消息为准 | 快照合并 | 暂缓(测试前不处理) | +| Q14 | 主/共享删除顺序与 EROR 回报义务 | 幂等原子级联 | 出站事件类型 | 未确认 | +| Q15 | 上游是否会以同一业务身份改发正文(决定是否需要区分「重复」与「改发」) | 假定期望不可变(`C-4`) | 身份去重语义 | 未确认 | +| Q16 | `FLID` 重用语义(决定版本是否按 incarnation 作用域) | 假定不复用(`C-21`) | 事件合并与 tombstone | 未确认 | + +Q 的答复只在本表就地更新(补「结论」与日期),并触发 [README.md](README.md)「维护清单」的落地 4 步;不另开文件。 + +## 9. 契约数值 + +| 量 | 定义处 | 约束 | +|---|---|---| +| `R_keep` | `C-7` | `R_keep ≥ max(人工重放期限 + 人工处置期限, 审计期限, 回填重试上限)`;「重放窗口内原文仍在」的唯一保证来源 | +| `R` | [reference.md](reference.md) `PARAM:msgx.pipeline.overdue-backfill` | `R ≤ R_keep`;只决定强补写与放弃期限,**不保护重放窗口** | +| 去重记忆期 | `INV-9` | ≥ `R_keep`;否则「归档后重复」不成立 | +| 回填放弃清单可见期 | `C-16` | ≥ `R_keep`;否则库方清除缺 `C-8` 依据 | diff --git a/docs/user-stories.md b/docs/user-stories.md deleted file mode 100644 index f09ec39..0000000 --- a/docs/user-stories.md +++ /dev/null @@ -1,325 +0,0 @@ -# msgexchange-v2 用户故事与实施清单 - -## 1. 如何使用本文 - -本文定义阶段 A 的实施范围与验收口径:**故事定义要交付什么,验收标准定义怎样证明完成,代码落点说明从哪里改起**。保留 US-01~US-15、OPS-1~OPS-4 编号,便于关联已有任务和测试。 - -- 系统边界见 [architecture.md](architecture.md),模块流程见 [design.md](design.md),前提与不变量见 [invariants.md](invariants.md),对外契约见 [contracts.md](contracts.md),航班规则见 [flight-state.md](flight-state.md)。不以工单状态代替代码验收。 -- “当前基础”来自本轮代码核对,只表示有接口或部分实现,不表示故事完成。`KEEP` 是保留业务兼容,`FIX` 是明确修正旧缺陷,`DEFERRED` 不进入阶段 A。 -- 五个独立事实(落信、入队、处理完成、回填、投递确认)的定义与判定见 [design.md](design.md)「术语与持久化记录」;接口、日志和测试必须分开表达,投递确认不等于业务消费者已消费。 -- 所有内部迁移只落自有 PG;共享 MySQL 不建表、不增列、不写历史表。本文用 `DATE_PROCESSED / STATUS` 表示逻辑字段,实际列名以库方契约为准。 -- 验收条目可按 `US-xx/条目号` 引用。故事较大时按下文子范围拆成小 PR,不把一个故事等同于一个提交。 - -**存储基线**:当前 PG 主表与明细表是状态权威,Redis 不参与动态写路径。Oracle 11g 是尚待完整适配的部署目标。航班状态设计和当前缺口见 [运营航班状态设计](flight-state.md)。 - -## 2. 建议实施顺序 - -先补可靠性边界,再打通一条真实业务链,最后扩充报文类型。每批均可先用假适配器测试,但真实链路验收不能省略。 - -| 批次 | 实施范围 | 本批交付证明 | -|---|---|---| -| S0:契约与基础 | 确认 Q1;整理报文/参考数据清单;补 Clock、PG 事务入口、迁移及 OPS-1 基础门禁 | 数据源归属明确;迁移可用;关键 Bean 装配测试;未就绪生产配置拒启。其他 Q 项只阻塞对应功能,不阻塞无关单测。 | -| S1:可靠收报与处理 | US-01、US-03 调度/解码/提交子范围、US-09、US-04 | 未处理信箱 → PG 入队 → 忽略终态 → 持久补偿回填;重试不越队,注入中断可恢复。 | -| S2:最小业务闭环 | US-05 的 ADFT 与一个不依赖参考数据的 FLOP,US-07 | 在 Q1/Q4 定案后,用真实存储和 Kafka 验证“建航班 → 更新 → 事件投递”,包括提交和确认丢失窗口。不是全部业务完成。 | -| S3:请求与全量计划 | US-08 的登记/出站/匹配基础 → US-06;补齐 US-08 的参考应答 | DNLD、匹配 RESP、迟到 RESP 和快照崩溃恢复通过;15 类请求逐项验收。 | -| S4:业务覆盖与接口 | US-13、US-14 → US-05 其余类型;US-02、US-12 | 增量类型、参考数据、机位映射和兼容 HTTP 均有契约样例与回归测试。接口可在前期单独开发。 | -| S5:运维恢复 | US-10、US-11,完成 OPS-1~OPS-3 | 安全重放、归档后去重、故障告警、单写者保护、影子禁写均有验证证据。 | -| S6:上线验证 | OPS-4,复核所有进入切流范围的故事 | 对拍、故障演练、配置和恢复 Runbook 验收后切流;US-15 不作为门槛。 | - -**依赖口径**:US-03 是基础管道,不依赖具体业务处理器;US-09 依赖其终态提交接口,US-04 复用 US-09。US-08 的请求登记与匹配基础不依赖 US-06;US-06 消费该基础,二者共同完成 RESP 集成验收,不形成开发依赖环。US-05 只有 PSDT 子范围依赖 US-14,不应阻塞其余处理器。 - -## 3. 阶段 A 用户故事 - -### US-01 可靠采集共享信箱 - -**目标**:上游继续向 `CMINMSGS` 落信,本系统持续、可恢复地采集,不要求上游改投递方式。 - -**验收标准** - -1. 按配置周期、ID 升序、有限批次采集信箱行;扫描谓词以 [design.md](design.md)「收报与水位」为准(按 ID 区间,不以处理标记为谓词)。接收层只入队,不解析业务、不回填已处理标记。 -2. 按信箱 ID 幂等建立 PG `PENDING`;重复扫描、并发兼容入队和进程重启都不能重置已有终态。 -3. 快路径用持久水位,本批 PG 入队全部确认后才推进水位。 -4. PG 不可用或批次中途失败时不改信箱标记;恢复后补建遗漏,记录失败次数与扫描进度。 -5. 较小 ID 迟提交、ID 有空洞、兼容入口先入队较大 ID 时,必须遵守经 Q2 确认的发现与顺序协议;不能用“最终会重扫”冒充严格 FIFO。 - -**当前基础与落点**:`ingress/InboxPoller.kt` 按 ID 区间扫描(`ID > W`,不以处理标记为谓词),水位落 `INBOX_CURSOR` 并与入队同事务推进;`JdbcCminmsgInboxRepository.readRange/maxId` 与 `ProcStateRepository.insertIfAbsent` 承担发现与幂等入队。空洞老化阈值取 `msgx.pipeline.max-commit-delay`。剩余:Q2 未书面确认前,老化阈值与严格顺序仍是假定口径;真实 MySQL 的中断恢复与迟提交联合测试待现场环境。 - -**前置**:共享库读契约;Q2 决定严格顺序的端到端验收。水位与扫描谓词口径以 [design.md](design.md)「收报与水位」为准。 - -### US-02 兼容 HTTP 注入报文(KEEP) - -**目标**:联调工具通过 `POST /cminmsgs/send` 提交 XML,得到真实的信箱接收结果。 - -**验收标准** - -1. 支持 `text/xml`、`application/xml`、`text/plain`,默认 UTF-8;空报文、超过请求体上限(见 `C-28`)的请求和畸形 XML 返回规范错误,不落信。XML 校验禁用 DTD、外部实体与外部资源访问。 -2. 信箱确认落信后返回 ID;PG 入队失败不把已落信伪装成未接收,由 US-01 补建。信箱写入未确认时不返回成功。 -3. 目标为兼容 `ResponseDto`;固定成功/失败样例、HTTP 状态码、响应媒体类型和错误码表后加入契约测试,见 Q3。成功只承诺信箱落信,不承诺业务处理或下游完成。 -4. 生产保持内网信任边界,由网关限制来源并审计;外露或跨网络必须先落实认证,不能把免密入口直接暴露。 - -**目标响应体示例**(数字和错误码仅作示例,错误码表见 Q3): - -```json -{"is_success": true, "body": 12345} -{"is_success": false, "err_code": "", "err_msg": ""} -``` - -**当前基础与落点**:`ingress/InboxController.kt` 当前返回 200 + 纯文本 ID,尚非目标 DTO;`InboxService.kt` 在落信后直接调用入队,需处理 PG 失败。新增 HTTP 媒体类型、大小限制及跨库失败测试。 - -**前置**:US-01 补建能力;Q3。HTTP 基础格式校验不替代 US-03 的业务解码。 - -### US-03 严格按序、幂等地执行管道 - -**目标**:报文失败和重试不造成航班状态倒序,也不重复产生副作用。 - -**实施拆分**:调度与时钟 → 安全解码及路由 → 身份绑定 → 状态应用与 PG 提交。先用假处理器验证管道,不等 US-05 全部实现。 - -**验收标准** - -1. 只取最小未完成 ID,`PENDING / FAILED` 均占队头;退避未到期不得越过。维护作业由独立线程执行,不参与消息 FIFO;作业必须有界,且不得因争用资源使已到期消息无限饥饿。 -2. 安全解码 XML,至少覆盖 META、SCHD、FLOP、参考应答与忽略类路由;合法但能力未支持是 `UNSUPPORTED`,不能一律归为非法报文。保留原文以支持诊断和回放。 -3. 解码后首次绑定 `SNDR|TYPE|STYP|SEQN`;冲突转 `SKIPPED` 并记录原 ID;自身重试保留绑定。生产按 `PARAM:msgx.identity.include-day-boundary` 配置(当前口径不含日期边界);更改算法须先确认 `Q11`。 -4. `MALFORMED` 直接 `DEAD`;`CODEC_ERROR / UNSUPPORTED / INFRA` 按次数和退避处理,耗尽转 `DEAD(EXHAUSTED)`。不能无限重试未实现类型,也不能立即当非法报文丢弃。 -5. 终态判据只有尝试上限(`PARAM:msgx.pipeline.max-attempts`),**没有按时间的毒丸**;调度判断注入 `Clock`。人工重放的可重放范围以 Q6 决定的 `R_keep` 下界为准。 -6. 主泵在同一 PG 事务提交航班主表/明细、事件与处理结果;终态回填意图通过 US-09 同事务保存。任一步失败整体回滚;提交后只重试外部回填,不重复生成业务事件。 -7. 领域决策逻辑只读取当前完整态与已解码报文,返回下一完整态和待发事件,不执行 I/O;Processor 作为事务协调器,将状态写入、事件、处理终态与回填意图收敛在同一事务边界内,不直接触碰 Kafka。失败只在持有消息上下文的边界落账,中断向上传递,不作为普通失败吞掉。 -8. 权威存储不可用或未完成恢复时停止业务处理;不能把“整个状态丢失”误判为“单航班不存在”而批量成功结束增量报文。 - -**当前基础与落点**:`processing/Pump.kt`(含 `MessageProcessor`)、`DynamicProcessors.kt`、`Identity.kt`、`codec/JacksonXmlCodec.kt`、`infra/retry/`。严格 FIFO 主泵、SCHD(DNLD/RESP/ADFT)/FLOP/FDEL 处理器、PG 单事务(含回填意图预登记)、身份绑定与重试已实现;调度取时由可注入 `Clock` 提供。尚未完成:忽略规则分支(US-04),以及逐类矩阵与 golden 样例(US-05)。长期积压不设时间判据;人工重放能覆盖多久仍由 Q6 决定的 `R_keep` 下界定案。 - -**前置**:US-01;Q1 已定单库方向,Q6 决定 `R_keep` 下界(重放窗口)。数据库迁移只落自有库。 - -### US-04 明确忽略非业务报文(KEEP) - -**目标**:无需处理的报文有可追踪的终结结果,不制造无效重试与死信。 - -**验收标准** - -1. 解码 META 后、处理器分派前,大小写不敏感匹配 `TYPE-STYP` 或 `TYPE-*`;基线为 `LDM-* / REGN-* / RSTA-* / EROR-*`,不混用 `ERROR`。转 `SKIPPED` 前必须已完成身份绑定(`US-03`、`INV-9`),忽略报文照常绑定身份。 -2. 命中后转 `SKIPPED`,记录 `ignored:` 和计数;不更新航班、不创建业务通知。 -3. 通过 US-09 保存回填意图;命中、未命中、大小写和重扫均有测试。合法忽略报文不应因 `MsgKind` 尚不能表达它而先解码失败。 - -**当前基础与落点**:`MessageProcessor` 尚无忽略分支;`MsgKind` 已有 SCHD/FLOP/FDEL/Unsupported 分派。在解码后的路由边界补忽略匹配,不把规则散落到各处理器。 - -**前置**:US-03 解码/终态接口、US-09。 - -### US-05 应用 ADFT 与 29 类 FLOP(KEEP + FIX) - -**目标**:增量报文正确更新航班及主/共享关系,并生成符合现役语义的通知。 - -**验收标准** - -1. `SCHD-ADFT` 与 29 个 FLOP 子类型逐项列入覆盖矩阵,每项有对应的处理器规则与回归测试;未知类型可恢复失败。RESP/DNLD 不计入这批处理器,走 US-06。 -2. 每类固定“输入与前态 → 后态 → msg → schd → 终态”五面样例;区分字段缺失、显式清空、重复报文和主/共享航班。清单和 golden 样例按 Q8 补齐,不以“已写 29 个类”替代验收。 -3. 对按 KEEP 规则需忽略的不存在航班,以 `SUCCEEDED` 无副作用结束,并由 US-09 回填;ADFT 建航班等行为按各类型矩阵执行。航班当前态以自有 PG 为唯一权威,重启即恢复,不存在 Redis 全损后白名单无法找回的损坏路径。 -4. 共享航班更新与删除级联语义以 [flight-state.md](flight-state.md)「删除与重建」为规范(共享航班通知、主航班 `MAFL` 更新、级联删除、原子变更;不出现主已删、子残留);本条目验收实现不偏离该规范,目标不存在时幂等成功。 -5. ADFT/FDEL 的值相等比较与半状态禁止规则见 [flight-state.md](flight-state.md)「删除与重建」。 -6. PSDT 通过 US-14 的只读映射计算 `abdg`,处理器不直接调用 admin-api。 - -**当前基础与落点**:落点已变为 `processing/DynamicProcessors.kt`(`FlopProcessor`/`FdelProcessor`/`AdftProcessor`)与 `domain/flight/FlightStateEngine`、`FlightStateRepository`。FDEL/ADFT 与通用 FLOP 处理器已接入(含 DELETED 幂等与重激活、tombstone 同事务登记);29 类逐类语义矩阵与 golden 样例仍未补全,不能因处理器存在就视为覆盖完成。 - -**前置**:US-03;PSDT 另依赖 US-14;Q1、Q8、Q14。 - -### US-06 导入 RESP/DNLD 日计划快照 - -**目标**:主动下发和请求应答使用同一套全量计划处理,迟到应答不覆盖新状态。 - -| 报文 | 路由 | 请求状态 | 无匹配时 | -|---|---|---|---| -| `SCHD-DNLD` | ScheduleProcessor | 不更新请求 | 不要求开放请求 | -| `SCHD-RESP` | 匹配守卫后进入 ScheduleProcessor | 成功提交时匹配 RQFD → DONE | SKIPPED、审计,禁止更新快照 | -| `SCHD-ADFT` | US-05 增量处理器 | 不更新请求 | 不适用 | - -**验收标准** - -1. RESP/DNLD 共用流式解析、整包校验和规范化;校验失败不发布半包,旧快照保持可用。 -2. RESP 仅匹配未过期、已发送的开放 RQFD;`DTTM < SENT_AT`、已过期、已被替代或无匹配时,不写业务状态,记录跳过原因。 -3. 在自有 PG 单事务内,批处理写入已校验的 `FLIGHT_SCHD` 航班状态与资源明细;本次日计划中未出现的航班不因此被删除。 -4. 在同一 PG 事务中提交 `FLIGHT_SCHD` 变更、`MSG_EVENT` 待发通知与 `PROC_STATE(SUCCEEDED)`;匹配 RESP 同事务完成请求并置 `DONE`;提交后信箱回填由扫描承接。 -5. 相同报文重放不二次写入或重复发事件;单事务崩溃整体回滚,重放幂等。 - -**当前基础与落点**:DNLD 与 RESP 已共同路由到 `processing/ScheduleProcessor.applyScheduleRecords`,整包校验、归属日冲突整包拒绝与单事务写入已实现;`REQ_TRACK` 表与 `ReqTrackRepository` 已建。仍需补 RESP 应答守卫(开放 RQFD 匹配、时间比对)与请求完成关联逻辑。 - -**前置**:US-03、US-08 请求登记/匹配基础;Q1、Q5、Q13。 - -### US-07 可靠、有序地投递 Kafka - -**目标**:状态应用完成后投递通知;重试可识别、不乱序、不静默丢失。 - -**验收标准** - -1. `KAFKA:msg` 按目标内 `EVENT_ID` 顺序发送,确认后才标 `SENT`;队头退避时不跳过,发送有超时上限。 -2. `KAFKA:schd` 只通过 `flushSchd` 聚合,聚合周期与批上限见 reference;同一 FLID 取批内最新状态,成功确认覆盖对应原事件,失败保持批次可恢复并退避,耗尽可见为 `DEAD`。 -3. 外部接收成功、本地确认失败或进程重启后允许重发;事件标识跨重发稳定,消费者有去重约定,不宣称端到端恰好一次。 -4. 当前 `KAFKA:msg` 与 `KAFKA:schd` 的分区键均为 `FLID`,schd 逐 `FLID` 发送最新状态,不再是 legacy 的多航班数组。`msg` 是否需按 `SNDR` 分区、发送粒度与去重标识的放置以 Q4 定案为准;定案前不宣称单分区之外的顺序保证。 -5. 生产强制 `acks=all`、`enable.idempotence=true`、`max.in.flight.requests.per.connection=1`;Broker 支持幂等生产协议并完成实际验证,不允许非幂等降级通过验收。 -6. 普通/聚合发送失败、确认丢失、批次标记中断和目标阻塞均有测试;DEAD 保留记录并告警。 - -**当前基础与落点**:`delivery/Dispatcher.kt` 已实现逐条 `KAFKA:msg` 与 `flushSchd` 聚合(按 `FLID` 合并最新 `STATE_VERSION`、TOMBSTONE 发 null)以及退避/DEAD 迁移;`DeliveryPort` 接口含 topic/key/事件类型参数。生产 Kafka 适配器未交付(仅 stub),强制配置校验与真实 Broker 验证需补齐。 - -**前置**:US-03 事件提交;Q4、现网 Broker 验证。wire 不兼容的标识字段不能直接加到现役载荷。 - -### US-08 发起并跟踪 15 类 AODB 请求 - -**目标**:区分请求登记、出站落信、等待、完成与超时,不把过期应答应用到新请求。 - -**实施拆分**:请求登记/出站补偿 → 匹配/超时 → 14 类参考应答;RQFD 快照效果由 US-06 集成验收。 - -**验收标准** - -1. 覆盖 14 类 RQRD 参考请求和 1 类 RQFD-NONE;逐类名称、编码和映射见 Q8,不与 admin-api 的 21 类混算。 -2. 先持久化 `PENDING` 与出站意图;COUTMSGS 确认落信后关联其 ID 并标 `SENT`,不宣称对方已发送。落信成功而 PG 未确认时可恢复,不能盲目重发。 -3. 同类开放请求最多一个,新请求使旧请求 `EXPIRED`,并发登记不产生两个开放请求。从确认落信的发送时间起算,超时值按 Q5;`PENDING`/`SENT` 均不得成为永不超时的死分支。 -4. 优先按已确认的 SEQN 回显匹配;无回显的降级匹配按 Q5 明确风险,只接受已发送开放请求且 `DTTM ≥ SENT_AT`。统一转换为可比较的时间,不能把报文日期数字直接与 epoch 毫秒比较。 -5. 迟到、无匹配或已关闭请求的应答不得更新数据,转 `SKIPPED` 并审计。参考应答成功写入 REF_MASTER 后,与请求完成、处理终态和事件在 PG 边界内保持所需原子性。 -6. `POST /schd/sync` 复用请求入口,采用 24 小时制和非空/区间校验;响应明确已登记还是已落信,不承诺计划已更新。 - -**当前基础与落点**:`ReqTrackRepository` 与 JDBC 实现、`REQ_TRACK` 表已存在;运行时协调器、`COUTMSGS` 出站适配与请求编码未实现(`XmlCodec.encodeRqrd` 仅占位),超时与应答匹配未闭环。需补协调器、出站适配、并发约束、应答路由与故障测试。 - -**前置**:US-01、US-03;Q5、Q8、Q14、出站信箱去重契约。请求基础不依赖 US-06。 - -### US-09 持久化补偿回填信箱 - -**目标**:本地处理终态最终反映到共享信箱,不因共享库故障回滚已完成业务。 - -**验收标准** - -1. PG 终态与回填意图同事务保存;所有终态路径都经过统一提交边界,不只覆盖成功路径。事务回滚时不得留下可执行回填意图。 -2. 提交后由后台执行回填,主泵不等待共享库;失败按持久记录退避,重启继续执行,不重新执行已完成业务。 -3. SUCCEEDED、规则忽略、身份重复、DEAD 均需回填处理时间;PENDING/FAILED 禁止回填。具体 STATUS 编码按 Q7 确认,内部终态不能直接当作外部字段值。 -4. 重复补偿效果幂等,保留稳定的完成时间与审计;重放后的新处理结果不能被旧回填任务覆盖。非法报文缺 META 时也有明确回填方式。 -5. 影子模式禁写,双跑仅一个系统持有标记写权;暴露 PG 终态、回填状态、积压、最老年龄与持续失败告警。 - -**当前基础与落点**:回填意图与处理终态同体同行(`PROC_STATE.BACKFILL_*`),随业务事务提交,`BACKFILL_TODO` 已在单基线中下线(原 V2 迁移的净结果已并入 `V1__flight_state_baseline.sql`);终态落库后回填一律由定时扫描驱动(`BackfillService.sweep`,扫描周期与退避取值见 [reference.md](reference.md);独立退避键 `backfill-backoff-ms` / `backfill-backoff-cap-ms` 已落地),处理关键路径不做跨库写;本地入队时间(`ENQUEUED_AT`)超过超期期限 `R` 时强制补写(见 design「回填」)。死信同样可补写——回填只需消息 ID,不依赖 META。剩余:Q7 的标记值集与写权限书面确认;影子环境禁写尚未实装。 - -**前置**:US-03 终态接口;Q7、共享库更新权限。覆盖四类终态、事务回滚、重复补偿和重放竞争;生命周期与超期补写以 [design.md](design.md)「中断恢复」「回填」为准,清除口径以 [contracts.md](contracts.md)「保留与清除」为准。 - -### US-10 安全重放与故障处置 - -**目标**:运维能定位失败、限定恢复范围,并了解重放对当前航班状态的影响。 - -**验收标准** - -1. 按 ID、错误类、时间查询次数、错误、关联事件与回填状态;重放前预览范围,记录操作者、原因和逐项结果。 -2. 仅 `CODEC_ERROR / UNSUPPORTED / INFRA / EXHAUSTED` 的 FAILED/DEAD 允许申请重放;MALFORMED 与其他不允许项不改状态,返回跳过原因。 -3. 重置 attempts/nextAttemptAt,保留身份、原始入队时间和错误审计;可重放范围受 Q6 决定的 `R_keep` 下界(原文保留窗口)约束。重新入队仍按 ID 处理,但不承诺已执行过的后续消息自动撤销。 -4. DEAD 之后可能已有新状态,必须预检版本与覆盖风险;不安全时拒绝直接重放,改用经批准的隔离重建或恢复流程,禁止无保护的全量 `replayAll` 生产入口。 -5. 操作有认证、授权、范围限制与审计;死信、持续补偿失败、队列年龄越界有告警和处理 Runbook。 - -**当前基础与落点**:`infra/retry/ReplayService.kt` 已按错误类批量把 FAILED/DEAD 置回 `PENDING`(重置次数与下次执行时间,保留身份与错误审计)并返回数量;需补按记录选择、版本/覆盖预检、操作审计和管理入口,扩展 `ReplayServiceTest`。 - -**前置**:US-03、US-09 的恢复状态;Q6、OPS-1/OPS-2 的安全与可观测基础。 - -### US-11 归档自有库终态记录 - -**目标**:控制自有 PG 在线表规模,不丢未完成工作、不破坏去重与恢复;不是清理共享信箱。 - -**验收标准** - -1. 终态记录在**了结后**经过的时间(`UPDATED_AT`)达到 `PARAM:msgx.proc-state.archive-after` 时列为归档候选;`PENDING`/`FAILED` 禁止归档,回填未了结的终态行不进入候选。 -2. 归档到自有 PG `PROC_STATE_HST`,主表保留 `STATE='ARCHIVED'` 的去重影子行(仅 `IDENTITY_KEY` 与 `MSG_ID`),使归档后同业务身份再次到达仍可去重;`MSG_EVENT` 的历史目标与保留规则由 `G-EVENT-RETENTION` 独立处理,不构成归档判据。 -3. 归档写入与主行置 `ARCHIVED` 在同一自有库事务内完成,按候选时的状态条件复查,影响 0 行即整体回滚;重复执行幂等,失败保留源记录并报告计数。 -4. 本系统不写共享 MySQL `CMINMSGS_HST`、不清理外部信箱;由库方按 `Q9` 执行的清除与历史归档见 [contracts.md](contracts.md)「保留与清除」。原文可用性与重放保留期由 Q6/Q7 关联确认。 - -**当前基础与落点**:`PROC_STATE_HST` 未建表,也没有归档处理记录的作业;先确定去重记录保留与关联策略,再补迁移与归档中断测试。航班历史清理(`HistorySweepJob`,属 US-15 红线范围)与本文档处理记录归档不是同一件事,不能混为一谈。 - -**前置**:US-03、US-09;US-07 提供事件终态规则,US-10 提供恢复保留要求。不依赖 US-15。自有记录归档见 [design.md](design.md)「生命周期与清除」,重放与原文可用性见 [design.md](design.md)「失败、重试与重放」。 - -### US-12 查询实时航班(KEEP) - -**目标**:调用方读取与当前权威状态一致的实时航班视图。 - -**验收标准** - -1. 保留 `GET /all/flights`,直接从自有 PostgreSQL `FLIGHT_SCHD` 查询,排除共享航班(`MAID != NULL`,定义见 [flight-state.md](flight-state.md)「字段与集合」);不改写业务状态。 -2. 固定响应样例、空结果、排序、大小限制及一致性时点。现役未分页时不能无声改为只返回第一页;分页或响应结构变更按 Q3 决定。 -3. 依赖异常不能伪装为空数组成功;影子只读影子状态,入口有约定的访问控制、限流与审计。 - -**当前基础与落点**:`InboxController.kt` 仅有该接口的 TODO(阶段 2);权威读取端口为 `FlightStateRepository` 的全量快照读取。新增查询控制器与只读服务,不能从空占位仓储返回成功,并补接口/状态不可用测试。 - -**前置**:Q1、Q3;所查询的 US-05/US-06 状态发布能力。 - -### US-13 刷新 21 类参考主数据 - -**目标**:业务使用来自 admin-api 的本地参考数据,刷新失败仍有上次可用版本。 - -**验收标准** - -1. 按 Q8 的 21 类清单配置端点、RTYPE/RKEY、字段映射;这是独立于 US-08 的数据入口,不另建“参考专用第二 PG”。 -2. 按 `(RTYPE,RKEY)` 幂等写 REF_MASTER,记录 SOURCE、刷新时间和批次;单类完整校验后发布,失败不暴露半批。 -3. 一类失败不破坏其他类或该类旧版本;同类由 AODB/admin-api 都提供时明确覆盖优先级,全量刷新时明确已删除项的处理,不能仅靠 SOURCE 日志解决冲突。 -4. 影子默认不主动刷新生产数据;需要参考样本时显式导入隔离副本。 - -**当前基础与落点**:`reference/` 包与 `REF_MASTER` 表均未落地(无迁移、无客户端、无刷新服务)。按 Q8 清单从零补建:先固定 21 类端点与字段契约,再补客户端、批次发布/事务与失败保旧测试。 - -**前置**:admin-api 访问契约、Q8。可独立于消息处理器开发。 - -### US-14 提供机位与登机桥映射 - -**目标**:PSDT 在不调用外部 HTTP 的情况下得到完整映射,正确计算 `abdg`。 - -**验收标准** - -1. 保留 `ORMS_STAND / ORMS_STAND_AIRBRIDGE` 两类,与 US-13 的 21 类分开统计;适配器拉取、完整校验后原子发布只读缓存。 -2. 近机位产生登机桥值,远机位或清空机位时 `abdg` 为空;一机位多桥、缺失映射与共享航班规则用 golden 固定。 -3. admin-api 不可用时使用最后可用版本;无可用版本或映射不完整时明确失败,不用空映射冒充正常清空,也不发布半批。 -4. 处理器输入包含所需只读参考视图,不允许其直接 HTTP 或写缓存。 - -**当前基础与落点**:在 `reference/` 与 `infra/` 增加映射服务/适配器,必要时扩展处理器输入上下文;PSDT 测试使用固定映射,无需在线 admin-api。 - -**前置**:机位/桥数据契约及 Q8;不要求 US-13 全部完成。 - -## 4. 暂缓范围 - -### US-15 历史航班清场(DEFERRED,阶段 B) - -历史存储确认成功后,才允许删除对应实时航班;逐条隔离坏数据,不能删除写历史失败的集合。判史规则与保留期(`HistoryProps`)、业务时区 `Asia/Shanghai`、历史写入与删除事件之间的恢复协议需在启用前完成 golden 对拍。 - -阶段 A 不依赖 ES,不启用 `PROJECTION_REBUILD`。`HistorySweepJob` 已作为每日清理脚手架接入,但历史存储未接通(或 `msgx.history.history-store-enabled=false`)时删除 0 条;脚手架存在不等于清场已交付,红线见 [flight-state.md](flight-state.md)「生命周期与开放项」。 - -## 5. 运行与切流验收 - -| 编号 | 必须交付的能力 | 验证证据 | -|---|---|---| -| OPS-1 单写者与启动安全 | 生产缺真实适配器、误用 stub、未启用必需管道时拒启;第二活动写者不能启动,失去写权后不得继续写;中断与停机能正确退出。 | 配置拒启、双实例/失去写权及停机测试。单靠副本数配置不算运行期保护。 | -| OPS-2 可观测与安全 | 真实依赖健康、队列/队头年龄、投递/回填滞后、积压与最老未处理信龄、DEAD 和一致性异常有指标、告警与处理入口;敏感管理操作有访问控制,日志不泄漏口令或完整敏感报文。 | 故障注入触发真实告警,消息到事件可关联;日志出口断开不阻塞业务。 | -| OPS-3 影子隔离 | 自有数据库/schema、topic、服务注册身份隔离;输入只读水位或回放,禁生产回填、真实出站和误注册。 | 配置与集成测试证明生产信箱、状态、topic 未被影子修改。 | -| OPS-4 切流与恢复 | 对拍不少于 7 天,未解释业务字段差异为 0,DLQ 积压为 0,MSG_EVENT 最老滞留 < 5 秒;切流后 48 小时观察,24 小时内具备经演练的回滚能力。 | 明确负载与统计口径的对拍报告;Runbook 含停写、排空/水位、状态恢复、写权交接和失败回退,不能只回滚程序版本。 | - -上述阈值沿用既有需求基线,需在真实环境提供证据,不代表当前已满足。自有库备份、报文保留和完整状态重建需要恢复演练;本地事务不能承诺任意数据库灾难下 RPO=0,也不承诺未经演练的“一键无损回滚”。 - -### HTTP 工具边界 - -| 端点 | 范围 | -|---|---| -| `POST /cminmsgs/send` | US-02,保留接收兼容性。 | -| `POST /schd/sync` | US-08,保留并修正参数校验;不等同于同步完成快照。 | -| `GET /all/flights` | US-12,保留查询语义。 | -| `POST /kafka/topics/{name}/msgs` | 不进生产;开发工具若保留,另行限制 topic allowlist。 | -| `GET /flights/migrate` | 不做,属于 legacy 一次性迁移工具。 | - -## 6. 编码前必须处理的决策与契约 - -这些是**阻塞相应实现的具体问题**,不是已完成的验收项。保留原有目标值,但不把矛盾或外部未确认内容写成事实。 - -**Q 台账已迁至 [contracts.md](contracts.md)「待确认事项台账」**(唯一出处,覆盖 Q1–Q16)。本文件只保留与验收直接相关的依赖关系,不重复问题正文: - -- US-01 / US-09 的验收依赖 `Q2`(发现完整性)与 `C-8`(清除前提); -- US-06 / US-13 / US-14 依赖 `Q8`(逐类清单)与 `Q13`(字段缺失语义); -- US-08 依赖 `Q3`(HTTP 契约)、`Q4`(Kafka wire)、`Q5`(请求匹配)、`Q10`(出站契约); -- US-10 依赖重放白名单与 CLM-3(重放安全,见 [invariants.md](invariants.md)); -- US-15 依赖 `Q9`(清除授权与 DDL)。 - -编号、当前口径与解除阻塞产物的完整表述见 contracts.md。 - -业务日期/日计划采用 `Asia/Shanghai`;持久化与比较使用明确的时间类型和转换规则,不靠服务器默认时区,也不直接比较不同单位的数字。 - -## 7. 每个实施 PR 的完成条件 - -1. 写明所覆盖的 `US-xx/验收条目`、未包含的子范围及相关 Q 项结论,不用“管道已接通”代替全部验收。 -2. 列出涉及模块、配置、PG 迁移、外部契约和恢复影响;源码现存的旧注释或空适配器不是正确性依据。 -3. 提交对应正常/失败/重复/中断测试;顺序、身份、快照和投递变更必须有不变量回归,PG 事务必须有真实数据库集成测试。 -4. 用 JDK 25 执行 `./gradlew test`;受限环境将 `GRADLE_USER_HOME`、`TMPDIR` 指向可写目录。测试受阻时记录原因,不写“全绿”。 -5. 真实适配器未完成、golden 未覆盖或契约仍有阻塞时,不把整项故事标完成;上线另需 OPS 验证和发布证据。 - -历史文档整改中的勾选不代表业务已实现,也不替代本清单。后续进度放在实施任务与测试证据中,本文保持需求和验收口径稳定。 diff --git a/scripts/check-docs.py b/scripts/check-docs.py new file mode 100755 index 0000000..e4e2ce0 --- /dev/null +++ b/scripts/check-docs.py @@ -0,0 +1,327 @@ +#!/usr/bin/env python3 +"""文档体系机械校验(docs/README.md「维护清单」)。 + +校验五件事: + ① docs 顶层 Markdown 固定为六个文件; + ② 稳定 ID 定义唯一性与语法:每个 ID 恰好定义一次,且全仓引用均有定义; + ③ 旧文件名、章节号引用与已闭合 G 标记零命中(docs/legacy/ 外); + ④ 仓库内 Markdown 链接有效。 + ⑤ 给出 Git 基线时,持久 ID 集合与活跃 G 集合保持不变。 + +用法:scripts/check-docs.py [仓库根目录] [Git 基线] +""" +from __future__ import annotations + +import re +import subprocess +import sys +from pathlib import Path + +ROOT = Path(sys.argv[1] if len(sys.argv) > 1 else ".").resolve() +BASELINE = sys.argv[2] if len(sys.argv) > 2 else None + +DOCS = ROOT / "docs" +TOP_FILES = ["README.md", "requirements.md", "architecture.md", "specification.md", + "implementation.md", "reference.md"] +TEXT_SUFFIXES = {".md", ".kt", ".kts", ".yml", ".yaml", ".sql", ".py", ".sh"} +SKIP_DIRS = { + ".git", ".gradle", ".gradle-home", ".gradletmp", ".idea", ".kotlin", + ".opencode", ".qoder", ".zcode", "build", "node_modules", "out", +} +OWNER = { + "US": "requirements.md", "OPS": "requirements.md", "D": "architecture.md", + "C": "specification.md", "PRE": "specification.md", "INV": "specification.md", + "CLM": "specification.md", "Q": "specification.md", "G": "specification.md", + "PARAM": "reference.md", +} +# 文档中的 ID 族示例不是真实定义。 +PLACEHOLDER_WORDS = {"G-NAME"} + +PATTERNS = { + "US": re.compile(r"\bUS-\d+\b"), + "OPS": re.compile(r"\bOPS-\d+\b"), + "D": re.compile(r"(? bool: + if ident in PLACEHOLDER_WORDS: + return True + # 通配/残缺键(如 `msgx.pipeline.*` 里被截出的 `msgx.pipeline.`)不算 ID + return ident.endswith(".") or "*" in ident + + +HEADING = re.compile(r"^(#{2,3})\s+(.+)$") + + +def section_of(lines: list[str]) -> list[str]: + """为每一行给出其所属小节的标题文本(二级取全名,三级取 `二级 / 三级`)。""" + out, h2, h3 = [], "", "" + for line in lines: + m = HEADING.match(line) + if m: + if len(m.group(1)) == 2: + h2, h3 = m.group(2).strip(), "" + else: + h3 = m.group(2).strip() + out.append(f"{h2} / {h3}" if h3 else h2) + return out + + +def first_col(line: str) -> str | None: + """取表格首列原始内容(不含两侧管道与空白);非表格行返回 None。""" + stripped = line.strip() + if not stripped.startswith("|"): + return None + cell = stripped[1:].split("|", 1)[0] + return cell.strip() + + +def repo_text_files(*, include_legacy: bool = False) -> list[Path]: + """返回仓库内需受文档引用纪律约束的文本文件,排除生成物。""" + files: list[Path] = [] + for path in ROOT.rglob("*"): + if not path.is_file() or any(part in SKIP_DIRS for part in path.parts): + continue + if (not include_legacy and "legacy" in path.parts) or path.suffix not in TEXT_SUFFIXES: + continue + files.append(path) + return sorted(files) + + +def git_top_docs(ref: str) -> dict[str, str]: + """读取某 Git 基线的 docs 顶层 Markdown,不读取工作树或 legacy。""" + listed = subprocess.run( + ["git", "ls-tree", "-r", "--name-only", ref, "--", "docs"], + cwd=ROOT, + check=True, + capture_output=True, + text=True, + ).stdout.splitlines() + docs: dict[str, str] = {} + for name in listed: + path = Path(name) + if path.parent != Path("docs") or path.suffix != ".md": + continue + docs[name] = subprocess.run( + ["git", "show", f"{ref}:{name}"], + cwd=ROOT, + check=True, + capture_output=True, + text=True, + ).stdout + return docs + + +def ids_in(texts: list[str], kind: str) -> set[str]: + found: set[str] = set() + for text in texts: + for ident in PATTERNS[kind].findall(text): + ident = ident.rstrip("`") + if not is_placeholder(kind, ident): + found.add(ident) + return found + + +def active_g_in(texts: list[str]) -> set[str]: + """从基线注册表首列提取未划销、未标已闭合的 G。""" + found: set[str] = set() + for text in texts: + for line in text.splitlines(): + if "~~" in line or re.search(r"已(?:闭合|关闭)", line): + continue + for ident in PATTERNS["G"].findall(line): + if is_definition("G", ident, line): + found.add(ident) + return found + + +def is_definition(kind: str, ident: str, line: str) -> bool: + """定义语法见 docs/README.md「ID 定义语法与引用纪律」。 + + 注册表的表格首列在「单个 ID」时构成定义;成组登记与同行多 ID 均视为引用。 + """ + if kind == "US": + return re.match(r"^###\s+" + re.escape(ident) + r"(\D|$)", line) is not None + if kind in ("C", "INV"): + return re.match(r"^-\s+\*\*" + re.escape(ident) + r"\*\*", line) is not None + cell = first_col(line) + if cell is None: + return False + # 严格匹配:`ID` 引用行(如 `INV-20` / `CLM-3`)不算定义 + return cell in (ident, f"`{ident}`") + + +def main() -> int: + failures: list[str] = [] + + # ① docs 顶层固定为六个 Markdown 文件 + actual_top = {path.name for path in DOCS.glob("*.md")} + expected_top = set(TOP_FILES) + if actual_top != expected_top: + failures.append( + "docs 顶层 Markdown 不等于固定六文件:" + f"缺少={sorted(expected_top - actual_top)},多出={sorted(actual_top - expected_top)}" + ) + else: + print("OK docs 顶层固定为六个 Markdown 文件") + + repo_files = repo_text_files() + all_repo_files = repo_text_files(include_legacy=True) + + # ② ID 注册表:每个被引用的 ID 必须恰好有一处定义,且位于自己的注册表 + # 触发检查的范围是「定义行」(加粗定义行 / US 标题 / 注册表首列), + # 与 docs/README.md「ID 定义语法与引用纪律」一致。 + defs: dict[tuple[str, str], list[str]] = {} + for doc in sorted(DOCS.glob("*.md")): + lines = doc.read_text(encoding="utf-8").splitlines() + sections = section_of(lines) + for i, line in enumerate(lines, 1): + for kind, pattern in PATTERNS.items(): + for ident in pattern.findall(line): + ident = ident.rstrip("`") + if is_placeholder(kind, ident) or not is_definition(kind, ident, line): + continue + defs.setdefault((kind, ident), []).append(f"{doc.name}:{i}:{sections[i - 1]}") + + # 参数与指标同表登记、语义不同,二者都算已登记 + REGISTRY = { # ID → (所属文件, 允许的定义小节) + "US": ("requirements.md", ["用户故事"]), + "OPS": ("requirements.md", ["运行与切流验收", "需求覆盖与依赖"]), + "D": ("architecture.md", ["关键决策"]), + "C": ("specification.md", ["契约"]), + "PRE": ("specification.md", ["前提"]), + "INV": ("specification.md", ["不变量"]), + "CLM": ("specification.md", ["声明边界"]), + "Q": ("specification.md", ["待确认事项台账"]), + "G": ("specification.md", ["当前已知偏差"]), + "PARAM": ("reference.md", ["参数注册表", "指标与健康"]), + } + totals = 0 + for (kind, ident), where in sorted(defs.items()): + totals += 1 + owner, sections = REGISTRY[kind] + ok = any(w.startswith(f"{owner}:") and any(sec in w for sec in sections) for w in where) + if len(where) > 1 and len(set(where)) > 1: + failures.append(f"{kind} {ident} 定义 {len(where)} 次({', '.join(where)})") + elif not ok: + failures.append(f"{kind} {ident} 定义不在 docs/{owner}{sections}:{where[0]}") + # 被引用但无定义的 ID(`defs` 已由上面的注册表检查确保位置与语法正确) + for kind, pattern in USE_PATTERNS.items(): + used: set[str] = set() + for doc in repo_files: + for line in doc.read_text(encoding="utf-8").splitlines(): + for ident in pattern.findall(line): + ident = ident.rstrip("`") + if not is_placeholder(kind, ident): + used.add(ident) + for ident in sorted(used): + if (kind, ident) not in defs: + failures.append(f"{kind} {ident} 已引用但无定义行") + if not failures: + print(f"OK ID 注册表:{totals} 个定义各一处且位于所属注册表") + + # ③ 旧文件名、章节号引用与已闭合 G 标记零命中(legacy 外) + stale_name = re.compile( + r"(design|invariants|contracts|flight-state|user-stories|spec-boundary-closure" + r"|message-lifecycle|runbooks)\.md") + section_ref = re.compile(r"(?:§\s*\d|(?:第\s*)?\d+(?:\.\d+)*\s*节)") + closed_g = re.compile(r"G-[A-Z][A-Z0-9-]*.*(?:✓|已闭合|已关闭)") + stale_hits, section_hits, closed_g_hits = [], [], [] + for f in all_repo_files: + try: + text = f.read_text(encoding="utf-8") + except (UnicodeDecodeError, OSError): + continue + for i, line in enumerate(text.splitlines(), 1): + if stale_name.search(line): + stale_hits.append(f"{f.relative_to(ROOT)}:{i} -> {stale_name.search(line).group(0)}") + if "legacy" not in f.parts and section_ref.search(line): + section_hits.append(f"{f.relative_to(ROOT)}:{i}") + if "legacy" not in f.parts and closed_g.search(line): + closed_g_hits.append(f"{f.relative_to(ROOT)}:{i}") + if stale_hits: + failures.append("存在指向已删除文档的文件名引用") + failures += stale_hits + else: + print("OK 旧文件名零命中") + if section_hits: + failures.append("存在章节号引用(应改为稳定 ID 或「文件名 + 小节名」)") + failures += section_hits + else: + print("OK 章节号引用零命中") + if closed_g_hits: + failures.append("存在已闭合 G 标记(应删除定义与全仓引用)") + failures += closed_g_hits + else: + print("OK 已闭合 G 标记零命中") + + # ④ 仓库内 Markdown 链接 + link_files = list(DOCS.rglob("*.md")) + [ROOT / "README.md", ROOT / "AGENTS.md"] + count, broken = 0, [] + for f in link_files: + if not f.exists(): + continue + for i, line in enumerate(f.read_text(encoding="utf-8").splitlines(), 1): + for m in re.finditer(r"\[[^\]]*\]\(([^)]+)\)", line): + target = m.group(1).split("#")[0].strip() + if not target or target.startswith(("http://", "https://", "mailto:")): + continue + count += 1 + if not (f.parent / target).resolve().exists(): + broken.append(f"{f.relative_to(ROOT)}:{i} -> {target}") + if broken: + failures.append("存在失效链接") + failures += broken + else: + print(f"OK 仓库内 Markdown 链接({count} 个)") + + # ⑤ 文档合并/重命名时,与给定基线比较持久 ID 与活跃 G 集合 + if BASELINE: + try: + baseline_texts = list(git_top_docs(BASELINE).values()) + except subprocess.CalledProcessError as exc: + failures.append(f"无法读取 Git 基线 {BASELINE}: {exc.stderr.strip()}") + else: + for kind in ("US", "OPS", "D", "C", "PRE", "INV", "CLM", "Q", "PARAM"): + before = ids_in(baseline_texts, kind) + after = {ident for defined_kind, ident in defs if defined_kind == kind} + if before != after: + failures.append( + f"{kind} 集合相对 {BASELINE} 变化:" + f"删除={sorted(before - after)},新增={sorted(after - before)}" + ) + before_g = active_g_in(baseline_texts) + after_g = {ident for defined_kind, ident in defs if defined_kind == "G"} + if before_g != after_g: + failures.append( + f"活跃 G 集合相对 {BASELINE} 变化:" + f"删除={sorted(before_g - after_g)},新增={sorted(after_g - before_g)}" + ) + if not any("相对" in failure for failure in failures): + print(f"OK 持久 ID 与活跃 G 集合相对 {BASELINE} 无变化") + + if failures: + for item in failures: + print(f"FAIL {item}") + print("存在失败项") + return 1 + print("全部通过") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/check-docs.sh b/scripts/check-docs.sh new file mode 100755 index 0000000..3308cb4 --- /dev/null +++ b/scripts/check-docs.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# 文档体系机械校验入口:顶层结构、ID 注册表与全仓引用、陈旧引用、活跃 G、Markdown 链接。 +# 用法:scripts/check-docs.sh [Git 基线] (在仓库根目录执行) +set -uo pipefail +cd "$(dirname "$0")/.." +exec python3 scripts/check-docs.py . "$@" diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/codec/JacksonXmlCodec.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/codec/JacksonXmlCodec.kt index a310bf2..edee6bc 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/codec/JacksonXmlCodec.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/codec/JacksonXmlCodec.kt @@ -37,7 +37,7 @@ class JacksonXmlCodec : XmlCodec { mapper.readValue(trimmed, SisMessageXml::class.java) } catch (e: JsonMappingException) { // META 层绑定失败=发送方违反必填契约(MALFORMED);体层(SCHD/FLOP)结构不匹配 - // =wire 结构超出编解码器当前绑定能力(CODEC_ERROR:退避重试,修复后可重放,design §2.3)。 + // =wire 结构超出编解码器当前绑定能力(CODEC_ERROR:退避重试,修复后可重放,docs/implementation.md「状态与错误分类」)。 val first = e.path.firstOrNull()?.fieldName ?: "" if (first.equals("meta", ignoreCase = true)) { return DecodeResult.Err(DecodeFailure(ErrorClass.MALFORMED, "xml-meta:${e.originalMessage.take(200)}")) diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/codec/SisWireMapper.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/codec/SisWireMapper.kt index ac189b1..8b9650c 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/codec/SisWireMapper.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/codec/SisWireMapper.kt @@ -35,7 +35,7 @@ internal object SisWireMapper { /** * 已落库的 10 类集合:缺席(字段默认空列表)不产生键,出现但为空的元素得到一行空行 `[{}]`; - * `filterValues` 去掉的正是"缺席",避免整包凭空清空本地明细(合并语义见 flight-state.md §3.1)。 + * `filterValues` 去掉的正是"缺席",避免整包凭空清空本地明细(合并语义见 docs/implementation.md「SCHD 日计划」)。 * * `SRVT`/`VIPF` 尚未有明细表(`[G-SRVT-VIPF]`):只用"键是否存在"表达段是否出现,保留原始 * 内容与顺序,不参与合并、不判断清空语义(`Q13`)——出现(哪怕为空)与缺席不再被抹平。 diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/config/PipelineProps.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/config/PipelineProps.kt index e969b79..3c1a848 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/config/PipelineProps.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/config/PipelineProps.kt @@ -54,10 +54,10 @@ class PipelineProps { */ var backfillMaxAttempts: Int = 100 - /** 回填独立退避的起步间隔 `[G-BACKFILL-BACKOFF]`。 */ + /** 回填独立退避的起步间隔。 */ var backfillBackoffMs: Long = 30_000 - /** 回填独立退避的封顶间隔 `[G-BACKFILL-BACKOFF]`。 */ + /** 回填独立退避的封顶间隔。 */ var backfillBackoffCapMs: Long = 900_000 /** diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/delivery/Dispatcher.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/delivery/Dispatcher.kt index 35e1001..a6820f6 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/delivery/Dispatcher.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/delivery/Dispatcher.kt @@ -36,7 +36,7 @@ interface DeliveryPort { * 两个主题之间不保证先后顺序。 * * 失败处理:队首的重试时间没到就不取;一批里有发送失败,整批重试次数加一并推后退避, - * 次数用尽整批转 DEAD 当死信。见 docs/flight-state.md §5。 + * 次数用尽整批转 DEAD 当死信。见 docs/implementation.md「Kafka 与读取」。 */ @Singleton class Dispatcher( diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/domain/MsgEvent.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/domain/MsgEvent.kt index 871a0b8..e602eb8 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/domain/MsgEvent.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/domain/MsgEvent.kt @@ -12,7 +12,7 @@ enum class EventStatus { PENDING, SENT, DEAD } * 写业务数据时在同一个事务里往这里插一行,投递线程随后按行发出,这样业务提交和"该发的事件" * 不会脱节。KAFKA_SCHD 发完整状态,KAFKA_MSG 只发"这个航班变了"的通知;两个主题之间不保证 * 先后顺序。TOMBSTONE 只在两种情况下登记:航班从在用变成删除,或者被生命周期清理前补发 - * 一次删除通知。见 docs/flight-state.md §5。 + * 一次删除通知。见 docs/implementation.md「Kafka 与读取」。 * * stateVersion 是发布时的航班版本号;同一 FLID 攒了多条待发事件时,只发版本号最新的那条。 */ diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/domain/OperationDay.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/domain/OperationDay.kt index b1e5d3c..27f8440 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/domain/OperationDay.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/domain/OperationDay.kt @@ -9,7 +9,7 @@ import java.time.ZoneId * 接收日、也不是落库日,算出来之后就固定不变。 * * 民航的一天不一定从零点开始,切日边界由 msgx.operation-day.cutoff-hour 配。默认 0 点只是 - * 占位,真实口径还要业务确认。见 docs/flight-state.md §2.1。 + * 占位,真实口径还要业务确认。见 docs/implementation.md「航班身份与运营日」。 */ class OperationDayCalculator( zone: ZoneId, diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/domain/SnapshotLog.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/domain/SnapshotLog.kt index f22efe9..bc5a65f 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/domain/SnapshotLog.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/domain/SnapshotLog.kt @@ -8,7 +8,7 @@ import java.time.LocalDate * * 只追加不修改;写失败不影响主流程,只记一条指标。数据丢了可以靠重放重建,它也不参与任何 * 状态决策。一行对应一次处理尝试,被重放的包会再记一行。默认保留 90 天,按(快照覆盖截止日, - * 接收时间)清理。见 docs/flight-state.md §2、docs/design.md §6.2。 + * 接收时间)清理。见 docs/implementation.md「生命周期与清除」。 */ enum class SnapshotResult { COMMITTED, REPLAY_SKIPPED, ROLLED_BACK } diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/domain/flight/FlightModel.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/domain/flight/FlightModel.kt index 1e27066..48e403d 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/domain/flight/FlightModel.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/domain/flight/FlightModel.kt @@ -4,7 +4,7 @@ import java.time.Instant import java.time.LocalDate /** - * 航班实例当前态模型,规则见 docs/flight-state.md §2/§3。 + * 航班实例当前态模型,规则见 docs/implementation.md「航班域」。 * * FLID(航班实例 ID)是航班唯一的关联键,同一航班的所有报文都靠它对上号。OPERATION_DAY * (运营保障日)在航班首次入库时确定,之后不允许再改;日计划还没收录它之前可以是 NULL。 @@ -28,7 +28,7 @@ data class FlightMainRow( * * scalars 和 collections 只装报文里真正出现过的字段:出现就覆盖本地值(标量给空串表示 * 显式清空),没出现就保留库里已有的值;集合一旦出现就按合并后的完整结果整体覆盖写入。 - * 合并规则见 docs/flight-state.md §3.1。 + * 合并规则见 docs/implementation.md「SCHD 日计划」。 */ data class ScheduleRecord( val flid: String, @@ -40,7 +40,7 @@ data class ScheduleRecord( * FLOP/ADFT 报文的增量载荷:只带这次要改的字段和集合,没出现的字段保留库里已有的值。 * * ADFT 里缺失字段到底算清空还是算保留,上游还没给准话,所以暂时保守处理成"只设不改", - * 不按整包替换来理解。见 docs/flight-state.md §3.2/§3.3。 + * 不按整包替换来理解。见 docs/implementation.md「动态运行事件」「删除与重建」。 */ data class MergeChange( val flid: String, @@ -79,7 +79,7 @@ data class HistoryRules( * wasNeverFdel 表示这个航班从没收到过 FDEL(航班终止报文),是被生命周期直接清掉的, * 所以清除前要补发一次删除通知,否则下游不知道它已经没了。目前只能靠推断:没有地方记录 * "曾经收过 FDEL",于是把 state = ACTIVE 当成没收到过。副作用是 FDEL 之后又被 ADFT - * 重新激活的航班会被误判、重复补发,解决办法待定(见 docs/flight-state.md §6 开放项)。 + * 重新激活的航班会被误判、重复补发,解决办法待定(见 docs/implementation.md「生命周期」)。 */ data class HistoryCandidate( val flid: String, diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/domain/flight/FlightStateEngine.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/domain/flight/FlightStateEngine.kt index 803e603..b7c8d71 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/domain/flight/FlightStateEngine.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/domain/flight/FlightStateEngine.kt @@ -12,7 +12,7 @@ import java.time.LocalDate * 没出现的保留,标量给空串表示清空;已删除的航班保持删除,日计划救不回来; * - [mergedState]:把 FLOP/ADFT 的增量合并进当前态,只改报文明确表达的字段和集合。 * - * 合并规则见 docs/flight-state.md §3.1/§3.2。 + * 合并规则见 docs/implementation.md「SCHD 日计划」「动态运行事件」。 */ object FlightStateEngine { diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/infra/persistence/Repositories.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/infra/persistence/Repositories.kt index fcc1531..5d3bf05 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/infra/persistence/Repositories.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/infra/persistence/Repositories.kt @@ -20,7 +20,7 @@ import java.time.ZoneId // 同一个事务里,并且进事务后第一件事就是拿 PIPELINE_LOCK 这把单行锁。锁只负责让 // 写事务排队,不负责选主或故障切换。 // -// 航班模型与合并规则的完整说明见 docs/flight-state.md。 +// 航班模型与合并规则的完整说明见 docs/implementation.md「航班域」。 // ===================================================================== /** 把一段代码包进一个数据库事务:航班状态、待发事件、处理终态要么一起提交,要么一起回滚。 */ diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/infra/persistence/jdbc/JdbcPgRepositories.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/infra/persistence/jdbc/JdbcPgRepositories.kt index 1d45a2e..4305853 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/infra/persistence/jdbc/JdbcPgRepositories.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/infra/persistence/jdbc/JdbcPgRepositories.kt @@ -46,7 +46,7 @@ import javax.sql.DataSource // 自有 PostgreSQL 的仓储实现,表结构见 db/migration/V1__flight_state_baseline.sql。 // // 航班相关的写操作都要求调用方先开事务、再拿 PIPELINE_LOCK 单行锁(见 Repositories.kt -// 顶部说明)。航班模型与合并规则见 docs/flight-state.md。 +// 顶部说明)。航班模型与合并规则见 docs/implementation.md「航班域」。 // ===================================================================== @Singleton diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/infra/stub/StubAdapters.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/infra/stub/StubAdapters.kt index 460ab97..ec5ed5a 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/infra/stub/StubAdapters.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/infra/stub/StubAdapters.kt @@ -8,7 +8,7 @@ import jakarta.inject.Singleton * 投递端口的内存假实现,只有配置 msgx.stubs=true 时才装配,本地开发和测试用。 * * 每次发送都往 sent 里记一条(topic、key、payload);payload 为 null 的那条就是删除通知 - * (tombstone,下游按"这个键没了"理解成删除)。见 docs/flight-state.md §5。 + * (tombstone,下游按"这个键没了"理解成删除)。见 docs/implementation.md「Kafka 与读取」。 */ @Requires(property = "msgx.stubs", value = "true") @Singleton diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/ingress/InboxService.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/ingress/InboxService.kt index 54a5a98..b9567f1 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/ingress/InboxService.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/ingress/InboxService.kt @@ -15,7 +15,7 @@ import java.util.concurrent.atomic.AtomicLong * 收报轮询会按 ID 把它补进来,所以不会丢消息。 * * 该入口**不参与水位**:它直接写 PROC_STATE,登记的行可能超出水位;主泵只领 `msgId ≤ W`, - * 因此不破坏 FIFO——这类行等水位追平后按序自然领取(`invariants.md` INV-4)。 + * 因此不破坏 FIFO——这类行等水位追平后按序自然领取(`specification.md` `INV-4`)。 */ @Singleton class InboxService( diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/jobs/HistorySweepJob.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/jobs/HistorySweepJob.kt index 729cb70..e8ab3e7 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/jobs/HistorySweepJob.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/jobs/HistorySweepJob.kt @@ -26,7 +26,7 @@ import java.time.Instant * 5. 归档失败或者结果说不清的,原样留着下次再来。 * * 红线:历史存储没接通时必须一条都不删。先删当前态、事后再补历史,是不允许的。 - * 见 docs/flight-state.md §6。 + * 见 docs/implementation.md「生命周期」。 */ @Singleton class HistorySweepJob( diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/processing/Pump.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/processing/Pump.kt index f4ff6fb..718fd14 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/processing/Pump.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/processing/Pump.kt @@ -84,7 +84,7 @@ class Pump( // // 水位以内的行都是收报按 ID 顺序发现并登记的;水位之外的行只可能来自兼容入口 // 直接写 PROC_STATE(它不参与水位)。若允许领取,它就会越过那些尚未入队的较小 ID, - // 破坏 FIFO(不变量"只领取已发现的行",`invariants.md` INV-4)。这种行在空洞补齐、`W` 追平之后自然可领取。 + // 破坏 FIFO(不变量"只领取已发现的行",`specification.md` `INV-4`)。这种行在空洞补齐、`W` 追平之后自然可领取。 val watermark = cursor.load().committedUpTo if (head.msgId > watermark) { warnBeyondWatermark(head.msgId, watermark) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 19c4e46..9a2facf 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -5,7 +5,7 @@ # 口令/端点全部环境变量外置——零入库。 # 存储边界:自有 PostgreSQL(datasources.default,全部内部状态与运营航班权威 FLIGHT_SCHD/SCHD_GEN)+ # 共享 MySQL 信箱(mailbox.shared-mysql,仅 DML,不建表)+ -# Kafka + Eureka/logstash。权威设计:docs/flight-state.md。 +# Kafka + Eureka/logstash。权威设计:docs/implementation.md。 # 键位口径:Micronaut 5.1(U03):datasources.* / flyway.datasources.* / # kafka.producers..* / eureka.client.*(Micronaut 原生键)。 # ===================================================================== @@ -18,26 +18,26 @@ msgx: max-attempts: 5 # 处理/投递同值 backoff-ms: [1000, 2000, 4000, 8000] # 指数退避;档位数必须 = max-attempts - 1(启动自检) backoff-cap-ms: 60000 - max-commit-delay: 5m # §5.1 空洞老化:W+1 空洞超过该时延判定为永久(Q2 最大提交时延) - overdue-backfill: 30d # 超期补写期限 R(Q6):仅须 R ≤ R_keep,不保护重放窗口;判据比较本地 ENQUEUED_AT(design「回填」) + max-commit-delay: 5m # 空洞老化:W+1 空洞超过该时延判定为永久(Q2 最大提交时延) + overdue-backfill: 30d # 超期补写期限 R(Q6):仅须 R ≤ R_keep,不保护重放窗口;判据比较本地 ENQUEUED_AT(implementation.md「回填」) backfill-batch: 100 # 回填扫描单批条数 - backfill-max-attempts: 100 # 单行重试的告警阈值;放弃判据是 R 超期,不是次数(design「回填」) + backfill-max-attempts: 100 # 单行重试的告警阈值;放弃判据是 R 超期,不是次数(implementation.md「回填」) # 一次性切流播种:默认(注释掉)不播种。min=读现存全部 | zero=从 0 按空洞规则 | max=跳过可见存量 | # cutover-watermark: min autostart: false # U07:启动即拉起 Pump/Dispatcher 循环;需真实仓储或 msgx.stubs=true 才开启(dev 见 application-dev.yml) schd: flush-period: 3s # KEEP 现役推送节律 flush-limit: 500 # 批上限,防积压尖峰 - operation-day: # §3.5:SODT + 机场时区 + 切日边界(默认占位 0 点,待业务确认 §10) + operation-day: # SODT + 机场时区 + 切日边界(默认占位 0 点,待业务确认) zone: Asia/Shanghai cutoff-hour: 0 - history: # §8:历史判定窗口与保留期 + history: # 历史判定窗口与保留期 cancelled-hours: 48 terminal-hours: 48 deleted-hours: 48 idle-hours: 168 snap-log-retention-days: 90 - history-store-enabled: false # 未接通历史存储时 HISTORY_SWEEP 删 0 条(§8.2) + history-store-enabled: false # 未接通历史存储时 HISTORY_SWEEP 删 0 条 micronaut: application: @@ -78,7 +78,7 @@ flyway: # 处理回填;出站写 COUTMSGS;不建表/schema,ACM2-12)。驱动/依赖与信箱适配层 # (CminmsgMailbox/OutboxMailbox)随 U05 批次引入。 mailbox: - processed-value: PROCESSED # §5.2 处理标记写入值(Q7:值集与写权限以库方契约为准) + processed-value: PROCESSED # 处理标记写入值(C-5/Q7:值集与写权限以库方契约为准) shared-mysql: enabled: false url: ${MSGX_MAILBOX_URL} diff --git a/src/main/resources/db/migration/V1__flight_state_baseline.sql b/src/main/resources/db/migration/V1__flight_state_baseline.sql index a06c32a..88db37a 100644 --- a/src/main/resources/db/migration/V1__flight_state_baseline.sql +++ b/src/main/resources/db/migration/V1__flight_state_baseline.sql @@ -18,7 +18,7 @@ -- 升级路径:已应用过 V1–V10 的库无法直接升级(版本链与校验和都对不上)。重建 schema -- 或删除数据卷后重跑迁移;禁止对生产库手工 repair 或改写 flyway_schema_history。 -- --- 分层(职责与写者见 docs/flight-state.md、docs/design.md): +-- 分层(职责与写者见 docs/implementation.md「航班域」): -- · 决策层:FLIGHT_SCHD + 8 张资源明细表 + FLIGHT_ROUTE_POINT —— 权威当前态(INV-11); -- · 管道层:PIPELINE_LOCK / INBOX_CURSOR / PROC_STATE / MSG_EVENT / REQ_TRACK; -- · 留痕层:SCHD_SNAP_LOG —— 只追加、可重建、不参与决策; @@ -294,7 +294,7 @@ CREATE TABLE MSG_EVENT ( ERROR_CLASS VARCHAR(20), LAST_ERROR VARCHAR(1000), CREATED_AT TIMESTAMP(6) WITH TIME ZONE NOT NULL, - SENT_AT TIMESTAMP(6) WITH TIME ZONE -- 投递确认的同一条 UPDATE 内写入;是保留期判定的唯一基准(G-EVENT-RETENTION) + SENT_AT TIMESTAMP(6) WITH TIME ZONE -- 投递确认的同一条 UPDATE 内写入;是保留期判定的唯一基准 ); CREATE INDEX idx_evt_head ON MSG_EVENT (TARGET, STATE, EVENT_ID); -- 每 target 队头 -- KAFKA:schd 按 FLID 单行(只保留最新 STATE_VERSION);KAFKA:msg 仍是多行 append-log,不受此约束。 diff --git a/src/main/resources/db/migration/oracle11g/README.md b/src/main/resources/db/migration/oracle11g/README.md index ecdbddb..3c85d93 100644 --- a/src/main/resources/db/migration/oracle11g/README.md +++ b/src/main/resources/db/migration/oracle11g/README.md @@ -7,7 +7,7 @@ Flyway 配置**;PG 路径使用 `classpath:db/migration`,两者互不混用 1. 现场 11.2 补丁级别、数据库字符集、DBA 权限清单拿到,且可提供可测试的目标库。 2. JDK 25 × ojdbc 驱动(具体版本)× Flyway Oracle 支持 × 连接池组合在目标库实测通过 - ——不能以"PG 通过"代替 Oracle 验收(Oracle 11g 适配为开放项,见 `docs/flight-state.md`)。 + ——不能以"PG 通过"代替 Oracle 验收(Oracle 11g 适配为开放项,见 `docs/implementation.md`)。 3. 11g 的 upsert(MERGE INTO)与绑定顺序适配完成并通过与 PG 同粒度的集成测试后, 才允许把仓储 SQL 切到 11g 方言。先前编译级交付的 `SqlDialect` 方言接缝已随 基线列名更替(fday/last_message_id → operation_day/last_msg_id)退役删除; diff --git a/src/test/kotlin/com/gzzn/omms/msgexchange/processing/BackfillServiceTest.kt b/src/test/kotlin/com/gzzn/omms/msgexchange/processing/BackfillServiceTest.kt index a7f9144..c6d3cfc 100644 --- a/src/test/kotlin/com/gzzn/omms/msgexchange/processing/BackfillServiceTest.kt +++ b/src/test/kotlin/com/gzzn/omms/msgexchange/processing/BackfillServiceTest.kt @@ -364,7 +364,7 @@ class BackfillServiceTest { } /** - * 语义固定(门禁裁决 D6):超期判据是**本地入队时间** `ENQUEUED_AT`,与 `RECEIVED_AT` 无关。 + * `INV-7`:超期判据是**本地入队时间** `ENQUEUED_AT`,与 `RECEIVED_AT` 无关。 * 上游没给接收时间(NULL)不再让 `R` 兜底失效——这正是引入 `ENQUEUED_AT` 要消除的窗口。 */ @Test