docs: 收敛架构设计规范与用户故事实施清单 (ACM2-28)

This commit is contained in:
windyboy
2026-09-07 15:10:57 +08:00
parent bf233f7f69
commit abacd6a3f4
6 changed files with 635 additions and 694 deletions
+3 -2
View File
@@ -167,8 +167,9 @@ MICRONAUT_ENVIRONMENTS=dev ./gradlew run # dev stub 冒烟:内存 stub,无
核心流程语义(流程 1 主路径/compat 分述)、失败/重试/重放、不变量落点、参数表、已知缺口。 核心流程语义(流程 1 主路径/compat 分述)、失败/重试/重放、不变量落点、参数表、已知缺口。
- [docs/user-stories.md](docs/user-stories.md):阶段 A US-01US-14、延后清场 US-15、上线 Epic、 - [docs/user-stories.md](docs/user-stories.md):阶段 A US-01US-14、延后清场 US-15、上线 Epic、
legacy HTTP 去留及逐项文档 TODO;包含验收标准、依赖、实现差距与待确认问题。 legacy HTTP 去留及逐项文档 TODO;包含验收标准、依赖、实现差距与待确认问题。
- [docs/user-stories-todo.md](docs/user-stories-todo.md)ACM2-1522 对应的文档整改清单、 (原 ACM2-1522 文档整改清单 user-stories-todo.md 已收敛完成并移除,跟踪记录见 Plane。)
已完成落点、待 Plane/产品确认事项与验证门禁。 - [docs/decision-flight-state.md](docs/decision-flight-state.md):决策提案——运营航班表
FLIGHT_STATE 是否提前落自有 PG(阶段 A 权威化);讨论 issue ACM2-28,定案前不实施。
- [docs/legacy/](docs/legacy/):外部参考/基线材料(自 legacy 仓库拷贝,非本系统文档)—— - [docs/legacy/](docs/legacy/):外部参考/基线材料(自 legacy 仓库拷贝,非本系统文档)——
`msgexchange-api-legacy-user-stories.md`legacy 行为对拍基线,ACMA-4)、`unisysaodbsis.xsd`、 `msgexchange-api-legacy-user-stories.md`legacy 行为对拍基线,ACMA-4)、`unisysaodbsis.xsd`、
`SIS_AODB_RMS-V0.1.md`(消息结构唯一事实源;CIIMS 中间件交换模型)。 `SIS_AODB_RMS-V0.1.md`(消息结构唯一事实源;CIIMS 中间件交换模型)。
+121 -221
View File
@@ -1,251 +1,151 @@
# msgexchange-v2 架构文档 # msgexchange-v2 架构文档
> **系统定位**:机场 OMMS **上游报文处理中间件**,消费 CIIMS/AODB 等上游写入共享 MySQL 信箱的 XML 报文,经严格 FIFO 管道解析决策后维护 Redis 航班动态权威,并向下游(Kafka / 出站信箱 / 查询接口)投递;非报文源系统,不替代 CIIMS 或 AODB。 ## 1. 系统定位与范围
> **架构基准**:总体基线遵循 Plane ACM2-3(综合架构 v4),存储边界与事务模型以 ACM2-12(自有 PostgreSQL + 共享 MySQL 信箱 + Redis 动态/快照 gen;阶段 B 缓做)为准。模块级实现与交互细节参见配套 [design.md](design.md)。
## 1. 系统定位
**机场 OMMS 上游报文处理中间件**:消费 CIIMS/AODB 等上游写入共享信箱的 XML 报文, msgexchange-v2 是机场 OMMS 上游报文处理中间件,用于替换旧版 `msgexchange-api`
经严格 FIFO 管道解析、决策、维护航班动态权威态,并向下游(Kafka / 出站信箱 / 查询接口) 它读取 CIIMS、AODB 等系统写入共享 MySQL 信箱的 XML 报文,按顺序更新航班动态,再将结果提供给下游。
投递;**非**报文源系统,**不**替代 CIIMS 或 AODB。替换 legacy `msgexchange-api`
Java 8 / Spring Boot 1.5 / Maven)。过渡策略为**双跑三步**:
``` 本系统负责**收报、解析、状态更新和结果投递**,不生成上游业务报文,不替代 CIIMS/AODB,也不提供 AODB 主数据编辑能力。
影子对拍(共享库水位/回放 + 自有 PG 独立 schema 比对)→ 切流(nextgen 权威)→ 旧仓库冻结
- **主要入口**:轮询共享 MySQL 的 `CMINMSGS`
- **兼容入口**`POST /cminmsgs/send`,供现役兼容、手工工具和对拍使用;写入信箱后返回记录 ID,不是生产收报主路径。
- **输出**Kafka 的 `msg` / `schd` 消息、共享 MySQL 的 `COUTMSGS` 出站信箱,以及查询 HTTP 接口;不直接推送前端。
- **当前范围(阶段 A)**:Redis 保存航班动态的权威状态。ES 历史投影及相关清场流程属于暂缓的阶段 B,不在当前交付范围内,也不新增关系型 `FLIGHT_STATE` 表。
本文描述架构约束,不代表所有能力已实现;实现缺口见第 9 节。模块交互、状态机和参数详见 [design.md](design.md),需求见 [user-stories.md](user-stories.md)。历史报文契约仍以 [SIS 接口规范](legacy/SIS_AODB_RMS-V0.1.md) 和 [XSD](legacy/unisysaodbsis.xsd) 为兼容依据,其他 legacy 资料仅作参考。
## 2. 总体架构
```text
CIIMS / AODB 等上游
│ 写入 XML
共享 MySQLCMINMSGS
│ 轮询未处理记录
┌──────────────── msgexchange-v2(单实例)────────────────┐
│ ingress:发现报文 → PostgreSQL 持久化入队 │
│ │ │
│ processing:取 FIFO 队头 → 解析 / 去重 → Handler 决策 │
│ ├─ 更新 Redis 航班动态 / 快照 │
│ └─ PG 事务:处理结果 + 待发事件 │
│ │
│ jobs:在主泵空闲或消息退避窗口内执行维护作业 │
│ delivery:读取 PG 待发事件 → 投递 / 重试 │
└─────────────────────────┬──────────────────────────────┘
├─ Kafkamsg / schd
└─ 共享 MySQLCOUTMSGS
处理结果提交后,再回填 CMINMSGS 的处理标记;失败需补偿。
查询接口读取航班动态,不参与状态写入。
``` ```
- legacy 维护不受本仓库影响;本仓库不声明 legacy 旧表 schema**且不在共享 MySQL 建任何表** 收报、处理和投递各使用一条专用线程,不占用 HTTP 事件循环。**只有主泵可以写航班动态及快照版本**,维护作业也必须遵守这一规则。
CMINMSGS/COUTMSGS 所在库属他人系统,本系统仅信箱 DML——ACM2-12,见 §6)。
- wire 契约冻结:消息结构唯一事实源为 `docs/legacy/SIS_AODB_RMS-V0.1.md` +
`docs/legacy/unisysaodbsis.xsd`
HTTP 端点路径与响应语义沿用现役(如 compat 写路径 `POST /cminmsgs/send` 返回记录 ID)。
- **影子对拍口径(ACM2-12)**:共享库是单信箱无法"同入口双收",影子输入改以共享库只读
水位/回放 + 自有 PG 独立 schema 比对(详见 §8 与 ACM2-12 Checks ⑤)。
### 1.1 上下游边界(中间件职责) 采用 Kotlin + JDK 25、Micronaut 编译期依赖注入和 JDBC 持久化。数据库变更由 Flyway 管理,但只作用于自有 PostgreSQL。具体依赖版本以 `build.gradle.kts` 为准,不在架构文档重复维护。
| 方向 | 角色 | 本系统做什么 | 本系统**不**做什么 | ## 3. 模块职责
|---|---|---|---|
| 入站(主路径) | CIIMS / AODB 等上游 | JDBC 轮询共享 MySQL `CMINMSGS` 发现新信 → 自有 PG 入队 → 解析处理 | 不生成原始业务报文;不替代 CIIMS 落信 |
| 入站(compat | 手工工具 / 对拍 | HTTP `POST /cminmsgs/send` 写信箱 + PG 入队 | 非生产主拓扑 |
| 处理 | 本系统 | 维护 Redis 航班动态权威态;Handler 纯函数决策 | 不持有 AODB 主数据编辑权 |
| 出站 | 下游消费者 | Kafkamsg/schd)、共享 MySQL `COUTMSGS`、查询 HTTP | 不直接推送至前端(经 Kafka 等中转) |
与 SIS / legacy 一致:上游经 CIIMS 等**外部系统**写入 `CMINMSGS` 后,本系统以 **1s JDBC 轮询** | 模块 | 职责与边界 |
`DATE_PROCESSED IS NULL`)采集并处理——与 legacy `MsgExchangeRunner` 同口径。 |---|---|
| `ingress` | 轮询信箱、持久化入队、补偿重扫及兼容 HTTP 写入;不解析业务报文。 |
| `codec` | XML 解码,区分非法报文与可修复的解码失败。 |
| `processing` | FIFO 调度、业务身份绑定与去重、Handler 决策、快照处理及状态提交。Handler 只返回决策,不直接访问数据库、Redis 或 Kafka。 |
| `delivery` | 消费待发事件,负责按目标保序、`schd` 聚合、投递和失败重试。 |
| `jobs` | 持久化维护作业,由主泵在允许的窗口执行;阶段 B 作业暂不启用。 |
| `reference` | 静态参考数据和上游请求跟踪。 |
| `domain` / `config` | 领域状态、事件和决策模型,以及运行参数。 |
| `infra` | 仓储、Redis 脚本、外部适配器、重试、健康检查与日志;通过接口隔离基础设施。 |
## 2. 技术栈 ## 4. 主流程
| 层 | 选型 | 说明 | ### 收报与处理
1. `InboxPoller` 默认每秒扫描 `DATE_PROCESSED IS NULL` 的信箱记录,在自有 PG 中建立 `PROC_STATE(PENDING)`。重复扫描不能重复入队;入队失败留待重扫。
2. 主泵只处理最小未完成 `CMINMSGS_ID`。解析报文、绑定业务身份并去重后,调用对应 Handler 生成决策。
3. 主泵执行 Redis 更新。全量快照通过代际版本 `gen` 管理覆盖和旧数据清理,避免旧快照覆盖新状态。
4. 在 PG 本地事务中同时保存处理结果和 `MSG_EVENT` 待发事件,再回填共享信箱的处理标记。
### 投递
`Dispatcher``MSG_EVENT` 取出待发事件。普通事件按投递目标和 `EVENT_ID` 保序;某个目标失败时,不能跳过其队头投递后续事件。
`schd` 是最新状态通知,不逐条发送中间变化:统一由 `flushSchd``FLID` 聚合,取批次内最新事件后发送。它不提供逐条变更历史,不能与普通事件的 FIFO 语义混为一谈。
## 5. 必须保持的约束
- **消息严格 FIFO**:队头失败并退避时,后续消息仍不能越过它。只有队头完成或按失败策略进入终态后,队列才继续推进。收报重扫和水位设计必须防止较小 ID 漏入队而被后续消息越过。
- **动态状态单写者**:Redis 航班动态和快照 `gen` 只由主泵写入。不能通过增加实例或处理线程来直接扩容。
- **身份去重**:同一业务身份只能绑定一条有效处理记录,重复报文不应再次产生业务副作用。具体身份组成和重放规则见设计文档。
- **快照可恢复**:快照覆盖、旧数据清理和版本推进需要原子性与重放保护;不能在恢复时把旧代数据重新写回。
- **作业不与消息混排**`PUMP_JOB` 是独立队列,只在没有消息队头或队头处于退避窗口时执行。执行窗口与饥饿边界需要明确验证。
这些约束优先于吞吐量优化。单写者降低了并发复杂度,代价是队头阻塞和吞吐上限;如需并行化,必须先重新定义顺序与状态归属,不能只调整线程数。
## 6. 数据归属与一致性
| 存储 | 保存内容 | 边界 |
|---|---|---| |---|---|---|
| 语言/运行时 | Kotlin 2.3 + JDK 25 | 目标运行时为 JVM 25Micronaut 5.1 依赖基线要求) | | 自有 PostgreSQL | `PROC_STATE` 处理状态、`MSG_EVENT` 待发事件、`PUMP_JOB` 作业、`REQ_TRACK` 请求跟踪、`REF_MASTER` 静态数据,以及 `PROC_STATE_HST` 归档 | 本系统的内部持久化状态;唯一的本地事务边界。 |
| 框架 | Micronaut platform BOM **5.1.3**core 系实际解析 **5.1.13**,版本重钉属 U02/U05 | 编译期 DIKSP`kotlin-ksp` + `micronaut-inject-kotlin` **5.1.3**)生成 `*$Definition` | | 共享 MySQL | `CMINMSGS` 入站信箱、`COUTMSGS` 出站信箱 | 外部系统所有。仅执行约定的信箱读写和处理标记回填,不建表、不迁移 schema、不写历史表。兼容 HTTP 入口可按既有契约写入入站信箱。 |
| 持久化 | **自有 PostgreSQL**(全部内部状态)+ 共享 MySQL 信箱 | 自有库:管道状态 PROC_STATE/MSG_EVENT + 任务调度 PUMP_JOB/REQ_TRACK + 21 类静态 REF_MASTERFlyway 迁移 `db/migration`);共享库严格保持 CMINMSGS/COUTMSGS 最小 DML 契约。JDBC 仓储与 InboxPoller 已有初版,事务/补偿/出站适配属 U05(ACM2-12 | | Redis | 航班动态 `flightInfo` 和快照版本 `gen` | 阶段 A 的动态权威存储,不是可随意清空的缓存。 |
| 权威存储 | Redis(航班动态 flightInfo + 快照 gen | 仅主泵单线程写入(I5);Lua 脚本执行原子状态覆盖与代际版本推进(gen 协议重设计属 U09) |
| 投递 | Kafkaacks=all + 幂等生产;ACM2-23 | transactional outbox 模式,经自有库 MSG_EVENT 表中转 |
| 投影(阶段 B | Elasticsearch(历史) | **阶段 B 缓做(ACM2-12**FLIGHT_STATE 不落关系表,Redis 保持动态权威;历史投影链路待阶段 B 重启评估 |
| 注册中心 | EurekaMicronaut 原生注册) | 服务名契约 `msgexchangeapi`(影子实例 `msgexchangeapi-shadow`)——当前运行时仍取 `micronaut.application.name`,配置映射待 U17 完善 |
## 3. 总体拓扑 **不使用跨库事务。** PG 事务只能保证“处理结果与待发事件一起提交”,不能覆盖 Redis 更新、MySQL 回填或 Kafka 发送。跨存储依靠幂等、重试和持久化补偿恢复:
``` | 中断位置 | 恢复要求 |
CIIMS/上游 ──外部写(他人系统)──▶ 共享 MySQL CMINMSGS(信箱) |---|---|
| 信箱已有报文,PG 入队失败 | 重扫补建,并按信箱 ID 去重。 |
│ JDBC 轮询/重扫(① 发现 DATE_PROCESSED IS NULL 新信) | Redis 更新成功,PG 提交失败 | 消息重试可能再次更新 Redis;更新与快照协议必须支持幂等重放和版本校验。 |
| PG 已提交,信箱回填失败 | 持久化记录补偿任务并重试回填,不能重新执行已完成的业务处理。 |
┌──────────────────────────────────────────────────┐ | 下游已接收,本地尚未标记发送成功 | 允许重发;下游或出站适配协议必须具备去重能力。 |
│ msgexchange-nextgen │
│ (单实例 · 单写者) │
│ ingressInboxPoller + 补偿重扫,U05
│ └──② 自有 PG PROC_STATE(PENDING) │
│ (跨库非同事务;② 失败→① 重扫补建) │
compatPOST /cminmsgs/send ──▶ 信箱 insert │
│ + PG 入队(现役 HTTP 写路径,U16 对拍) │
│ │
│ processingmsgx-pump 线程,严格 FIFO 队头) │
│ Pump ──tick──▶ MessageProcessor │
│ │ │ decodeXmlCodec
│ │ │ identity 绑定(I3
│ │ │ Handler.decide(纯函数) │
│ │─Schd RESP/DNLD▶ SnapshotFlow(流程4
│ │─PUMP_JOB───▶ JobExecutor(作业窗口,决策1
│ │ │
│ ├────Redis Lua──▶ Redis flightInfoA权威) │
│ ├──自有 PG 事务2──▶ MSG_EVENT + SUCCEEDED │
│ └──共享 MySQL 回填 DATE_PROCESSED(外部副作用)│
│ │
│ deliverymsgx-dispatcher 线程,每 target FIFO
│ Dispatcher ──逐条──▶ Kafka(msg) │
│ └─flushSchd 聚合─▶ Kafka(schd) │
│ (阶段 B 追加:ES flight_hts → Redis 投影删除) │
└──────────────────────────────────────────────────┘
共享 MySQL(信箱)◀──① 轮询发现 / 回填──▶ 自有 PostgreSQL ◀──② 管道状态
Redis(动态+gen)◀── Lua 写 ── processing
│ │
▼ ▼
下游 Kafka topic Eureka / logstash
```
要点: 对外投递按**至少一次**设计,不承诺端到端恰好一次。Kafka 生产者幂等不能消除应用重启或 outbox 重发带来的所有重复。Redis 恢复也不能仅依赖 PG 处理状态:备份、报文保留及回放范围需要在上线前验证。
- **收报主路径(与现役/SIS 一致)**:上游经 CIIMS 等**外部系统**写入共享 MySQL `CMINMSGS` ## 7. 关键决策索引
(本系统不建表);`ingress`**JDBC 轮询**`DATE_PROCESSED IS NULL`1s 节律,与
legacy `MsgExchangeRunner` 同口径)发现新信 → 自有 PG 建 `PROC_STATE(PENDING)`
PG 入队失败时以共享库水位**重扫补建**(U05)。`POST /cminmsgs/send` 为现役 HTTP
**写**路径(手工/对拍),非上游报文到达的主拓扑。
- **三条专用 daemon 单线程**`msgx-inbox-poller` / `msgx-pump` / `msgx-dispatcher`)由 `PipelineLifecycle` 保留 D1–D12 编号,便于设计文档和工程历史引用;以下是决策摘要,而非完成清单。
`ServerStartupEvent` 后拉起,不占用 Netty event loop;停机 `requestStop` +
interrupt + joinU07)。仅当 `msgx.pipeline.autostart=true` 时装配——生产默认关,
当前属**有意脚手架门禁**(生产可运行需先完成 U05 数据层实装,见 §7)。
- **单写者约束(I5)**:阶段 A 全部 Redis 写集中在主泵线程;实例数必须为 1
(运行期租约/选主保护属 U26,尚未实装,当前靠部署拓扑约束)。
- **消息严格 FIFO,作业采用窗口语义(决策 1 经 ACM2-12 修订)**:消息按最小未完成
`CMINMSGS_ID` 保序;PUMP_JOB 不与消息构成统一全序,仅在无消息队头或队头处于退避窗口时执行。
当前 `jobBefore` 的 head-state 判定已近似该语义,但窗口、饥饿边界和测试仍待 U15 固化。
- **存储边界(ACM2-12)**:本框图内事务库为**自有 PostgreSQL**PROC_STATE/MSG_EVENT/
PUMP_JOB/REQ_TRACK/21 类);CMINMSGS/COUTMSGS 在**共享 MySQL 信箱**(上游外部写、
本系统 JDBC 轮询读 + 处理回填写);Redis 除 flightInfo 还承载快照 gen。图中跨库步骤
(① JDBC 发现 / ② PG 入队 / 外部回填)与 ACM2-3 原「同库事务锚」语义不同,见 §6:
本地事务只在自有 PG,信箱交互为外部读/写 + 最终一致。
## 4. 模块职责 | 编号 | 决策及理由 |
|---|---|
| D1 | 业务报文严格 FIFO,维护作业窗口执行,优先保护航班状态的时序正确性。 |
| D2 | 阶段 B 暂缓。历史写入成功后才可生成删除事件;顺序调用本身不保证原子性,恢复与去重方案需在启用前补齐。 |
| D3 | `schd` 只从 `flushSchd` 聚合发送,减少已被覆盖的中间状态通知。 |
| D4 | 未实现的报文类型按 `UNSUPPORTED` 可恢复失败处理,不当作非法报文直接丢弃;补齐能力后按重放规则恢复。 |
| D5 | 在持有具体消息或批次上下文的位置记录失败和退避;不吞掉线程中断或 JVM 严重错误。 |
| D6 | 基础设施通过接口注入,时间通过 `Clock` 注入,便于确定性测试顺序、重试与超时。 |
| D7 | 内存 stub 仅显式开启时装配,生产禁止使用,避免把未持久化的数据误当作已落库。 |
| D8 | 使用编译期依赖注入,并以启动冒烟测试验证关键 Bean 装配。 |
| D9 | 自有 PG 内完成本地事务,共享 MySQL 仅作信箱;跨存储采用补偿,不使用 XA。 |
| D10 | 动态状态单写者,生产只允许一个活动实例;多实例必须先具备可靠的排他保护。 |
| D11 | Kafka 生产要求 `acks=all``enable.idempotence=true``max.in.flight=1`,切流前验证 Broker 兼容性;不允许通过关闭幂等来满足生产接入。 |
| D12 | 仅将自有库终态记录归档到 `PROC_STATE_HST`,不侵入共享库的表结构或保留策略。 |
| 包 | 职责 | 对应 ACMA-8 | 主要类 | ## 8. 部署、切换与运维
|---|---|---|---|
| `ingress/` | 收报:JDBC 轮询共享信箱发现新信 → 自有 PG 建 PENDING+ 补偿重扫;HTTP 写路径 compat);不解析报文 | 流程 1I3 | `InboxPoller`U05`InboxController` `InboxService` |
| `processing/` | 主泵:FIFO 领取、ignoreMsg、identity 绑定、纯函数决策、RESP/DNLD 快照、自有 PG 事务2 | 流程 2/4I1/I2/I5 | `Pump` `MessageProcessor` `SnapshotFlow` `Identity` `Handler(Registry)` |
| `delivery/` | 投递:每 target 严格 FIFO、schd 聚合 | 流程 3 | `Dispatcher` `SchdAggregation` |
| `jobs/` | 泵作业:清场/归档/投影重建(PUMP_JOB 自有 PG,作业窗口执行) | 流程 4/5/7,I4 | `JobExecutor` `HistorySweepJob` `ArchiveJob` `ProjectionRebuildJob` |
| `codec/` | XML 解码 + 失败分类(MALFORMED vs CODEC_ERROR | 决策 4 前置 | `XmlCodec` `DecodeResult` |
| `domain/` | 状态机枚举、事件/决策模型、Phase 开关 | I1I5 | `ProcState` `MsgEvent` `Decision` `MsgKind` |
| `infra/` | 仓储接口、重试策略、Redis Lua、stub、健康、日志 | 数据模型节 | 见 design.md |
| `config/` | `PipelineProps` 参数表(ACMA-8 参数初值) | — | `PipelineProps` |
## 5. 关键架构决策 **部署与安全**
> 决策编号保持与 [design.md](design.md) 及工程历史引用一致;按【定案】与【设计依据与权衡】两段式规范表述 - 生产维持单活动实例,停机时停止接收新任务并等待工作线程退出。运行期排他保护尚未完成,当前不能依靠程序自动阻止双实例写入
- 配置、口令和环境端点通过环境变量提供。兼容写接口沿用内网信任模式,缺少鉴权,必须限制网络访问;管理端点不得直接暴露到生产外网。
- Eureka 用于服务发现,Logstash 接收结构化日志;日志出口故障不应阻塞业务处理。
### D1 — 业务报文严格 FIFO 与维护作业窗口化调度 **替换旧系统**
- **定案**:业务报文按最小未完成 `CMINMSGS_ID` 严格保序、逐条执行;清场/归档/投影重建等维护作业(`PUMP_JOB`)单独排入持久化作业队列表,严禁与业务报文混排。作业仅在消息队列为空、或队头报文处于重试退避等待窗口且作业可在窗口期内完成时触发 采用“影子对拍 → 切流 → 旧系统冻结”。共享信箱不能让新旧系统同时认领和回填;影子输入使用只读水位或回放。影子环境须隔离 PG schema/实例、Redis key 空间、Kafka topic 和服务注册身份,并禁止误写生产信箱。切流时保证只有一个权威写者
- **设计依据与权衡**:报文到达与处理时序直接决定航班生命周期状态机的权威正确性(如“计划变更”与“航班取消”时序颠倒将导致严重脏数据),业务 FIFO 为最高优先级不变式(I1)。维护作业属于低频异步运维任务,若与业务报文抢占同一调度队列将引入队头阻塞与事务锁竞争;采用窗口化插针调度,既确保业务报文零干扰,又实现后台任务免停机自适应推进。
- **落地与边界**`Pump.tick``JobExecutor`;作业饥饿防护与精确退避窗口由 U15 固化。
### D2 — 阶段 B 历史投递与投影清理同步编排(缓做) **可观测性要求**
- **定案**:历史航班写入 ES 成功后,由同一消费线程同步向自有库 `MSG_EVENT` 写入“删除 Redis 投影”事件,不采用跨系统异步轮询或分布式两阶段提交。本项归属阶段 B(ACM2-12 缓做),待后续阶段评估重启 使用消息 ID、事件 ID 关联处理与投递日志;健康检查反映依赖实际可用性,而不只是进程存活。运行中重点关注队列积压、队头滞留时间、投递延迟、重试/DEAD 数量和回填补偿积压。死信和一致性异常需要可执行的告警与重放流程,不能只留一条错误日志
- **设计依据与权衡**:利用同线程同步顺序调用保障“ES 写入成功”与“删除事件就绪”的因果强一致性,消除外部索引已更新但缓存清理事件悬挂丢失的竞态窗口;避免引入额外的分布式协调器与待确认补偿表,控制架构复杂度。
- **落地与边界**`Dispatcher.tick`(定案 2;阶段 B 重启后实装)。
### D3 — 调度快照(schd)批量聚合与最新态压缩投递 ## 9. 当前实现与上线门槛
- **定案**:调度快照类报文(`KAFKA_SCHD`)严禁进入逐条投递链路,在逐条轮询中显式排除;出站唯一路径为 `flushSchd` 定时与批阈值触发的批量聚合流程:按航班唯一标识(`FLID`)分组去重,仅提取组内最新一条(`max(EVENT_ID)`)快照聚合为批,一次性投递至 Kafka 当前已有管道骨架、重试机制、部分 JDBC 适配和开发环境 stub 冒烟能力,**不能据此认定生产链路已闭环**。默认配置关闭管道自动启动及真实数据库/信箱适配
- **设计依据与权衡**:调度类报文存在高频状态刷新特征,逐条下发会导致 Kafka 主题与下游消费者遭遇瞬态数据风暴,且会无谓广播已被新快照覆盖的历史过期状态(现役系统缺陷);聚合去重确保下游获取确定性的全量最新切面,同时大幅削减网络 I/O 与 Kafka 吞吐压力。
- **落地与边界**`Dispatcher.tick``SchdAggregation`U06/N03)。
### D4 — 未实装报文可重放机制(FAILED-UNSUPPORTED 上线前至少需要完成并验证:
- **定案**:处理管道遇到尚未实装 Handler 的报文类型或未就绪的快照 Staging 时,统一标记为 `FAILED(UNSUPPORTED)` 并进入指数退避,严禁写入不可逆终态(如 DEAD 或伪成功) - 真实 PG 事务、信箱水位与补扫、回填补偿、出站信箱,以及所需业务 Handler
- **设计依据与权衡**:支持业务协议分阶段平滑演进与上线。报文协议 Handler 翻译分批交付,过渡期提前接入的未支持报文必须保持可重放状态,待新版本 Handler 发布后通过 `ReplayService` 批量重放激活,杜绝因协议尚未覆盖而造成数据永久丢弃 - Redis 更新幂等性、快照版本校验、故障中断恢复与权威数据恢复方案
- **落地与边界**`MessageProcessor``SnapshotFlow``ReplayService`U10/N21 - FIFO、身份去重、作业窗口和投递故障下的回归测试
- 生产启动校验、单实例排他保护、影子隔离和 Kafka 配置约束;当前配置仍允许 Kafka 参数覆盖,且默认 in-flight 值与 D11 要求不同。
- 死信告警、人工重放、端到端追踪、积压指标及安全边界。
### D5 — 异常分级收敛与 JVM 致命故障快速失败 具体进度由配套设计(design.md §9 已知缺口表)与 Plane ACM2-10 实施计划维护,本文不记录测试数量、临时补丁版本或逐项工单进展。架构基线沿用 ACM2-3,存储与事务边界以 ACM2-12 的修订为准。
- **定案**:报文级业务与编解码异常(FAILED、重试计数递增、退避调度)在持有具体消息上下文(head/batch)的执行边界就地捕获与状态机扭转;主泵与分发器外层循环仅兜底捕获并记录通用 `Exception`,严禁捕获或吞掉 `InterruptedException``VirtualMachineError` / `Error`
- **设计依据与权衡**:异常必须精准归因到具体报文以维护审计轨迹与隔离单条故障;主循环必须对线程中断信号做出响应以保障容器平滑优雅停机(Graceful Shutdown),同时允许 OOM 等致命底层错误即时穿透导致进程崩溃退出(Fail-Fast),防范系统在亚健康或内存损坏状态下带病运行。
- **落地与边界**`MessageProcessor``Dispatcher`U08)。
### D6 — 基础设施抽象解耦与确定性虚拟时钟驱动
- **定案**:核心领域逻辑与管道状态机与底层中间件物理实现完全解耦,存储依赖均定义为抽象接口,单测采用内存假仓储;所有时间依赖必须通过可注入的 `Clock` 获取,禁止直接调用 `System.currentTimeMillis()` 或系统默认时钟。
- **设计依据与权衡**:保障单元与架构测试环境无需启动重量级外部容器(PostgreSQL/Redis/Kafka),确保测试毫秒级完成;通过手动操纵虚拟时钟对退避窗口、超时截断等时序临界路径实现确定性断言,彻底消除线程休眠(Thread.sleep)引入的测试脆弱性与执行抖动。
- **落地与边界**`infra/persistence/Repositories.kt``FailureScheduler`
### D7 — 内存 Stub 条件装配门禁与生产硬隔离
- **定案**:所有内存 Stub 实现类统一受 `@Requires(property = "msgx.stubs", value = "true")` 条件装配注解约束,生产配置文件中该项强制缺省或设为 `false`Stub 与 `autostart` 组合仅用于本地开发与快速冒烟。
- **设计依据与权衡**:Stub 仅保留在进程内瞬态集合中,不具备持久化保证;通过框架级配置门禁强制阻断内存实现渗透至生产环境的可能,从根本上杜绝数据静默丢失风险。
- **落地与边界**`infra/stub/StubRepositories.kt`U07/U01)。
### D8 — 依赖注入编译期静态化与上下文冒烟锁定
- **定案**:采用 Micronaut KSP 编译期 AOT 处理生成无反射 `BeanDefinition`;在持续集成流水线中配置轻量级 `ApplicationContext` 启动冒烟测试,刚性校验关键 Bean 的生成与装配链。
- **设计依据与权衡**:消除运行时反射与类扫描开销,降低系统内存占用并加速容器启动;依赖升级或注解变更可能隐蔽破坏 KSP 代码生成,CI 冒烟测试将装配回归缺陷锁死在提交阶段,避免延迟至生产部署暴露。
- **落地与边界**`build.gradle.kts``PipelineSmokeTest.kt`U01)。
### D9 — 跨库存储边界与自有库本地事务闭环
- **定案**:外部共享 MySQL 严格限定为只读轮询(`CMINMSGS`)、状态回填(`DATE_PROCESSED`)及出站写入(`COUTMSGS`)的外部信箱通道,严禁在共享库执行 DDL 或创建中间表;系统全部内部状态(消息处理轨迹、事件 Outbox、任务调度、静态参考数据)统一由自有 PostgreSQL 承载。本地 ACID 事务仅在 PostgreSQL 内部闭环(`MSG_EVENT` 插入与 `PROC_STATE` 终态更新原子提交);信箱回填作为外部副作用在事务提交后异步执行,并通过失败补偿保障最终一致性。
- **设计依据与权衡**:共享 MySQL 为外部多方共用系统,无法支持两阶段提交(XA),且外部长事务会导致严重的信箱锁竞争与级联故障传导;将一致性边界收敛于自有 PostgreSQL,使核心处理管道具备完全可控的事务与审计能力,跨系统交互通过“本地消息表 + 异步补偿”解耦。
- **落地与边界**`MessageProcessor``InboxPoller``CminmsgMailbox``OutboxMailbox`ACM2-12 / ACM2-19)。
### D10 — 动态权威主泵单写者(Single-Writer)无锁模型
- **定案**:Redis 航班动态权威态(`flightInfo`)及快照代际状态(`gen`)的全部写操作,严格收敛至单一主泵线程(`msgx-pump`);系统生产拓扑强制以单实例部署运行,多实例部署需配置运行期分布式租约/排他锁拒启保护。
- **设计依据与权衡**:彻底消除多线程并发写入带来的锁竞争、死锁与 ABA 状态覆盖风险;单写者模型极大简化了复杂航班生命周期状态机的推演与证明,在无需引入重型分布式协调机制的前提下,实现极高吞吐的确定性状态更新。
- **落地与边界**`Pump``PipelineLifecycle``FlightRedisClient`(不变式 I5、U26)。
### D11 — 生产 Kafka 幂等投递与防降级硬契约
- **定案**:消息与调度事件向 Kafka 投递时,生产者客户端强制配置 `acks=all``enable.idempotence=true` 以及 `max.in.flight.requests.per.connection=1`;生产切流前必须确认 Broker 节点支持 `InitProducerId(22)` 协议,严禁在生产环境配置降级为非幂等生产。
- **设计依据与权衡**:由于自有库 Outbox 分发器在网络重试时可能重复投递,必须依赖 Kafka Broker 端的序列号机制实现精确一次写入(EOS);严禁生产降级消除了网络抖动或 Broker Leader 切换时导致下游接收乱序报文或重复业务事件的隐患。
- **落地与边界**`Dispatcher``KafkaEventPublisher`ACM2-23)。
### D12 — 报文归档自有库收敛与外部信箱零侵入
- **定案**:入站历史报文的定期归档与清理,处理对象仅限自有库中的终态记录(SUCCEEDED / SKIPPED / DEAD),归档目标确定为自有 PostgreSQL 的 `PROC_STATE_HST`;严禁向共享 MySQL 写入 `CMINMSGS_HST` 表。
- **设计依据与权衡**:外部共享库的物理表空间与历史保留策略归外部系统管辖,第三方中间件向外部数据库写入历史表不仅违反权限与隔离契约,还会因外部表结构变更引入系统性风险;将归档生命周期完全自包含在自有库中,确保系统自洽与合规。
- **落地与边界**`ArchiveJob``db/migration/V1.0.0__own_pg_pipeline.sql`ACM2-17)。
## 6. 数据边界(ACM2-12 最终口径)
- **自有 PostgreSQL(本系统唯一自有数据库)**:全部内部状态,本地事务只在此库成立。
`db/migration/V1.0.0__own_pg_pipeline.sql`PG 方言)建:`PROC_STATE`(取消息侧:处理
状态/重试/毒丸)+ `MSG_EVENT`(发消息侧:outbox+ `PUMP_JOB`(作业调度)+ `REQ_TRACK`
15 类请求)+ `REF_MASTER`(21 类静态,SOURCE 审计)。
- **共享 MySQLcdairport,他人系统库)——本系统不建任何表/schema,仅信箱 DML**
上游外部写 `CMINMSGS`;本系统 JDBC 轮询读 + 处理完成回填 `DATE_PROCESSED/STATUS`
出站写 `COUTMSGS`(他人系统读取发送)。与信箱的交互是**外部副作用**,非本系统事务的一部分:
收报主路径=JDBC 轮询发现新信 → 自有 PG 建 PENDING 入队;compat HTTP 写=
信箱 insertRaw 成功(返回 CMINMSGS_ID)→ PG 入队;PG 建行失败以共享库
`DATE_PROCESSED IS NULL` 重扫补建;回填 = 处理成功后 PG 本地事务外异步/持久化补偿,最终一致(ACM2-12 / ACM2-19)。
- **Redis**:航班动态 flightInfo(阶段 A 权威,I5+ 快照 **SCHD_GEN**Lua 内原子
「覆盖+按代差删+版本推进」,协议重设计属 U09)。
- **阶段 BFLIGHT_STATE):缓做,不落表**;ES 仍承载历史航班(判史/查询),
相关投影/删除事件链待阶段 B 重评估后启用。
- legacy 旧表 schema 归 legacy 仓库维护;CMINMSGS/COUTMSGS 的结构与保留策略由共享库方管理。
**ARCHIVE 归档定案(ACM2-17**:严禁向共享 MySQL 写入 `CMINMSGS_HST`(共享库严格保持 CMINMSGS 读/回填、
COUTMSGS 写入两表契约);终态入站消息归档目标确定为自有 PG `PROC_STATE_HST`
## 7. 权威(阶段 A Redis;阶段 B 缓做)与当前就绪度
| 阶段 | 权威 | 投递目标 | 状态 |
|---|---|---|---|
| A`msgx.phase=A` | Redis flightInfo+ gen | KAFKA:msg、KAFKA:schd | 管道骨架+重试闭环已实装;PG/JDBC 信箱轮询已有初版,但水位、事务、回填补偿与出站信箱尚未闭环;gen→Redis 协议属 U09 |
| B(缓做,重新评估后命名/启用) | Redis 仍为动态权威 | ES 历史写入与成功集清场 | FLIGHT_STATE 不落表;HISTORY_SWEEP 标为 DEFERRED,不作为阶段 A 切流门禁 |
**当前就绪度(基线状态)**:自动化测试集覆盖 39 项单元与架构测试;开发环境支持 `MICRONAUT_ENVIRONMENTS=dev` 端到端 Stub 冒烟(无外部中间件启动 → compat HTTP 写路径响应 200 → `/health` UP)。生产管道默认保持关闭(`msgx.pipeline.autostart=false`,真实数据源与信箱适配层默认 disabled)。生产就绪前置依赖包括:U05(自有 PG 本地事务、双库补偿与出站信箱闭环)、U07(生产 fail-fast 启动校验)、U09Redis Lua 快照代际 CAS 与恢复协议)、U13(投递超时升级与 DLQ 告警)、U15(统一序号与作业窗口边界固化)。逐项跟踪详见 ACM2-10 实施计划。
## 8. 部署与安全姿态
- **实例数 = 1**(主泵单写者前提);双实例误配当前无运行期防护(U26:租约/DB 锁 + 拒启,未实装)。
- **影子隔离(目标架构规范;当前为基础实现态)**:服务名(`msgexchangeapi-shadow`+ 独立
schema + Redis key 前缀 + 独立 topic 三层隔离;当前代码仅 `msgx.register-eureka=false` 生效——
Kafka topic 写死字面量 `"msg"`/`"schd"`Dispatcher)、`FlightRedisClient.eval` 无 key 前缀参数、
服务名未接 `msgx.service-name`(§2)。影子数据库:自有 PG 开独立 schema/实例;共享信箱无法
双写,影子输入=只读水位/回放口径(ACM2-12 Checks ⑤)。
- **网络信任模型**`/cminmsgs/send` 无鉴权(沿用现役内网信任姿态);eureka default-zone
回退 `127.0.0.1:8761`;口令/端点全部环境变量外置(零入库)。安全节细化属 U28。
- **管理端点**Micronaut 5.1 下 `/env` 默认**禁用**、`/beans` 默认 enabled+sensitivedev/影子经
顶层 `endpoints.*`**非** `micronaut.endpoints.*`——实测前缀错误时不生效)放开 `/env``/beans`
与 health 明细。工程未引入 micronaut-securitysensitive 的实际拦截行为待 U28 定案。
- **同名单风险**:影子与生产同名同路径时,compat HTTP 写可能误写生产信箱——切流前必须核对服务名三隔离。
## 9. 可观测性
- **日志**logstash TCP JSON 通道(Async + neverBlock 降级,logstash 不可达不阻塞业务线程);
结构化生命周期日志(入队/PENDING/SUCCEEDED/SKIPPED/FAILED/DEAD/毒丸/flush 批次);MDC `traceId`
(当前 = cminmsgsId/eventId,处理片段;贯穿入队→投递属 U12 遗留)。
- **健康**`/health` 聚合 `redis-flight-store` / `kafka-delivery` 自定义指示器——真实 ping
判定(false/异常→DOWN,缺 bean→DOWN),非仅 bean 存在。
- **指标缺口**micrometer 队列深度/投递延迟 gauge 未引入(版本对齐待 U05 批次);
DEAD/DLQ 告警出口与一致性哨兵实装(U25)未落地——告警当前以 ERROR 日志为落点。
+96
View File
@@ -0,0 +1,96 @@
# 决策提案:运营航班表 FLIGHT_STATE 是否落自有 PostgreSQL
> **状态**:提案(讨论 issue**ACM2-28**,关联 ACM2-12)。
> 本文重估 ACM2-12「阶段 B 缓做、不落表」的口径;定案前不改代码与迁移。
> 交叉引用:[architecture.md](architecture.md) §6/§7、[design.md](design.md) §2/§3.4、
> ACM2-12(存储边界)、ACM2-10 U05/U09/U15。
## 1. 问题
阶段 A 航班动态权威 = Redis(`flightInfo` hash + `SCHD_GEN`),永续驻留、不落任何关系表
ACM2-12)。FLIGHT_STATE 表原属阶段 B`Decision.kt` 注释即「阶段 B 落 FLIGHT_STATE
同事务」)。要回答的问题:**是否把 FLIGHT_STATE 提前到阶段 A 落自有 PG,作为运营航班的
权威存储。**
## 2. 触发重估的事实
| # | 事实 | 出处 |
|---|---|---|
| F1 | Redis 全损后,FLOP 报文按 KEEP 语义「航班不存在 → SUCCEEDED 不重试」持续终结——空态被当正常态,**状态损坏持续到下一次完整 DNLD**;且这些报文已回填 `DATE_PROCESSED``ReplayService` 白名单(FAILED/DEAD)无法找回 | US-05 AC3、design.md §1.1 |
| F2 | 当前唯一恢复手段 = 等下一次 DNLD 或人工触发 RQFD(依赖 AODB 外部响应,超时 60s);无本地 durable 副本 | US-08、design.md §3.4 |
| F3 | dev Valkey 虽配 AOF`compose.yaml --appendonly yes`),**生产 Redis 拓扑(持久化策略/副本)未定**;Redis 持久化本就是尽力而为,不构成状态安全边界 | compose.yaml、§8 部署姿态 |
| F4 | PG 已是处理管道硬依赖(PROC_STATE/MSG_EVENT),主泵本就停摆于 PG 不可用——航班状态放 PG **不新增** 系统级 SPOF | design.md §2 |
| F5 | U05(PG 本地事务边界)正在实装;此刻调整事务模型成本最低,切流后迁移要重开 I2/I5 与影子对拍口径 | ACM2-10 U05 |
| F6 | U09gen Redis 内 CAS 协议重设计)存在的根因就是「权威在 Redis、终态在 PG」的跨存储窗口——ACMA-8 v4 原设计 gen 本在 DBACM2-12 迁 Redis 仅因 FLIGHT_STATE 缓做 | design.md §3.4 已知缺口 |
| F7 | Handler 输入视图 `hgetAllFlightInfo()` 每报文全量读 hash,已是 U22–U24 已知缺口;PG 按 FLID 索引读可顺带收敛 | design.md §9 |
## 3. 选项
### A — 维持缓做 + 运维缓解(ACM2-12 现状)
冷启动空态自动触发 RQFD、生产 Redis 强制 AOF+副本部署要求、Runbook 记录重建流程。
**局限**:F1 的永久损坏窗口依旧存在,缓解只是缩短;恢复依赖外部系统可用性。
### B — PG 镜像(write-behind 灾备副本,非权威)
主泵在 Redis 写成功后异步把变更镜像到 PG,仅供灾难重建。
**局限**:镜像滞后窗口内的 FLOP 效果同样丢失,恢复后仍需 DNLD 修正(与 A 等价的损伤面);
却要付出接近 C 的复杂度(表 + 双表示一致性哨兵 + 重建任务)。性价比最差,仅列出备选。
### C — FLIGHT_STATE 落自有 PG,作为阶段 A 权威(推荐)
航班状态变更并入既有 **PG 事务 2**(与 `MSG_EVENT` 插入、`PROC_STATE→SUCCEEDED` 同事务
原子提交);Redis 退出动态权威写路径。
- 跨存储双写窗口(I2 的 Redis 先写)整体消失;U09 从「Redis Lua 协议重设计」变为
「SQL 版本 CAS + 集成测试」,gen/SCHD_GEN 随 FLIGHT_STATE 回 PG(回到 ACMA-8 v4 形态)。
- Redis 全损场景不再存在:PG 即状态,重启即恢复;F1 的损坏路径被根除。
- Handler 输入改为按 FLID 索引读(F7);US-12 `GET /all/flights` 读 PG。
- 影子对拍沿用 ACM2-12 既有口径(自有 PG 独立 schema 比对),不新增负担。
- 阶段 B 不受影响:清场/判史改为 SQL 驱动,ES 仍是历史投影。
- 性能:机场报文量级(秒级峰值)下单行 JSONB upsert 亚毫秒,且在既有事务内,无新增往返。
**C 的代价**(须如实计入):
- 推翻 ACM2-12 阶段 A 存储口径,I2/I5 不变量重述,U29 不变量测试清单同步。
- `FlightStateRepository` 从 day 粒度(`replaceDay`)重设计为 FLID 粒度 upsert + day 快照
replaceSnapshotFlow 的 Lua 差删改 SQL`redis-flight-store` 健康指示器调整。
- Redis 在阶段 A 角色大幅缩小(仅剩 orms_stand 热点缓存可选),部署面与文档需收口。
- 与 U05/U09/U15 排序重排:先定此决策,再收 U05 事务边界。
## 4. 对比
| 维度 | A 缓做+缓解 | B 镜像 | C PG 权威 |
|---|---|---|---|
| Redis 全损后果 | 状态永久损坏至下次 DNLD | 损坏窗口缩短,仍需 DNLD 修正 | 不存在该场景 |
| 事务模型 | 跨存储双写(I2/U09 复杂度保留) | 双写 + 镜像一致性 | 单库原子(U09 消解为 SQL) |
| 新增范围 | 无 | 表+哨兵+重建 | 表+接口+事务改造(并入 U05/U09 批次) |
| 影子对拍 | 不变 | 需比对三方 | 不变(口径已是 PG schema 比对) |
| 恢复 RTO/RPO | 依赖 AODB 响应 | 本地副本+DNLD 修正 | 重启即恢复,RPO=0 |
## 5. 建议
**推荐 C**。核心理由:F1 的损坏是**永久且不可自动找回**的(报文已回填、不可重放),
选项 A/B 只能缩短窗口不能根除;而 C 的增量成本大部分落在 U05/U09 本就要动的事务边界上,
时机(F5)与代价重合。若评审认为生产 Redis 必然配强持久化+副本且接受 DNLD 重建窗口,
A 是最小代价回退位;B 不建议。
## 6. 定案前置问题(→ Plane issue 讨论)
1. 生产 Redis/Valkey 拓扑定案:AOF 策略、副本、RPO 承诺——决定选项 A 是否可接受。
2. FLIGHT_STATE 形态:FLID 主键 + FLTR JSONB 全量(建议起步),还是归一化列
`abdg`/`PSDT` 等派生字段是否有 SQL 查询面需求)。
3. gen/SCHD_GEN 回 PG 后,U09「Redis Lua 协议重设计」是否直接取消,改为 SQL 版本 CAS
+ Testcontainers 集成测试。
4. 阶段 A 是否彻底移除 Redis 依赖:orms_stand 缓存去留、US-12 读取面、
`redis-flight-store` 健康指示器调整。
5. I2/I5 不变量重述文案与 U29 清单、architecture/design/user-stories 三文档的回改范围。
6. 影子对拍跨存储 diffnextgen PG vs legacy Redis)的验收口径与工具。
## 7. 定案后的落地清单(C 方案)
- 迁移 `V1.1.0__flight_state.sql`FLIGHT_STATE + gen 列回归(含索引 FLID/日期)。
- `FlightStateRepository` 重设计(FLID upsert / day replace / findByFlid+ JDBC 实装。
- `MessageProcessor`/`SnapshotFlow`redisApply → PG 事务内;Lua 差删 → SQL。
- Handler 视图与 US-12 读 PG;健康指示器、影子隔离(Redis key 前缀条目作废)同步。
- 文档回改:本文转「已定案」,ACM2-12 阶段 A 口径修订说明。
+192 -252
View File
@@ -1,288 +1,228 @@
# msgexchange-v2 设计文档 # msgexchange-v2 设计文档
> **系统角色**:机场 OMMS **上游报文处理中间件**——消费 CIIMS/AODB 等上游经共享 MySQL 信箱 ## 1. 阅读说明
> `CMINMSGS`)投递的 XML 报文,解析处理后维护 Redis 动态并向 Kafka / 出站信箱投递;
> **非**报文源系统。生产主路径 = **JDBC 轮询**发现新信;HTTP `POST /cminmsgs/send` = compat 写路径。
> 本文对应仓库当前实现,给出模块级设计语义与依据;架构总览见
> [architecture.md](architecture.md),架构基线为 Plane **ACM2-3**,其存储/事务边界由后续
> **ACM2-12** 覆盖;实施计划与逐项验收为
> **ACM2-10U01U30**。文中标注「TODO/未实装」的条目均为已知开放项,不属文档遗漏。
## 0. 系统边界速览 本文说明模块如何协作、状态如何流转,以及失败后如何恢复。系统范围、存储归属和部署约束见 [architecture.md](architecture.md),不在这里重复。
``` 以下流程是阶段 A 的目标设计,不是实现完成清单。当前代码仍有占位和过渡实现,与设计的主要差异集中在第 10 节。阶段 B 的历史投影和清场暂不启用,也不改变 Redis 作为航班动态权威存储的定位。
上游(CIIMS/AODB…) ──外部写──▶ 共享 MySQL CMINMSGSDATE_PROCESSED IS NULL
│ JDBC 轮询/重扫(InboxPollerU05
自有 PG PROC_STATE(PENDING) ──▶ 主泵 FIFO 处理
┌───────────────┼───────────────┐
▼ ▼ ▼
Redis 动态 Kafka msg/schd COUTMSGS 出站
(阶段 A 权威) (下游订阅) (他人读取发送)
compatPOST /cminmsgs/send ──▶ insertRaw + PG 入队(手工/对拍,非主拓扑) ## 2. 数据与领域模型
```
- **中间件定位**:本系统位于 CIIMS 与下游消费者之间,负责**采集 → 解析 → 决策 → 投递**; ### 2.1 持久化记录
报文原文由上游写入共享信箱,本系统只读(主路径)或 compat 写(辅助)。
- **与 legacy 对齐**legacy `MsgExchangeRunner` 同样以 1s 轮询 `CMINMSGS` 为处理入口;
legacy HTTP 收报接口在 nextgen 中保留为 compat,不改变生产主拓扑。
## 1. 领域模型 所有内部表都属于自有 PostgreSQL;共享 MySQL 只保留约定的信箱读写边界。
### 1.1 状态机与错误分类 | 记录 | 用途 | 关键约束 |
```
ProcStatusPROC_STATE.STATE,消息处理侧):
PENDING ──处理成功──▶ SUCCEEDED(终态;回填共享库 CMINMSGS 为外部副作用,最终一致)
│ ──同 identity 已绑定──▶ SKIPPED(终态,lastError=duplicate-of:<id>
└──失败──▶ FAILED(非终态,attempts+1 + nextAttemptAt 退避)
│ attempts ≥ maxAttempts 或 队头滞留超 head-deadline
DEAD(终态/DLQERROR_CLASS=EXHAUSTED 规范化)
EventStatusMSG_EVENT.STATE,投递侧):
PENDING ──▶ SENT;失败退避回 PENDINGattempts 耗尽整批/单条 → DEAD(DLQ)
ErrorClass(两侧共用):
MALFORMED 报文非法 → 直接 DEAD,永不重放
CODEC_ERROR 可随 codec 修复 → FAILED 可重放(白名单内)
UNSUPPORTED 能力未实装 → FAILED 可重放(白名单内)
INFRA 基础设施抖动 → FAILED 可重放(白名单内)
EXHAUSTED 重试耗尽(终态规范化类)→ 人工复核后可重放(白名单内)
```
- 「未实装 ≠ 非法」是通用规则(D4):无 handler、快照 staging 未实装都写
`FAILED(UNSUPPORTED)`,绝不写终态——阶段 2 前接入流量不会把报文变砖。
- 显式重放入口 `ReplayService`:仅白名单
`{CODEC_ERROR, UNSUPPORTED, INFRA, EXHAUSTED}` 可从 FAILED/DEAD 回 PENDING
ATTEMPTS=0、NEXT_ATTEMPT_AT=NULLerrorClass/lastError 保留审计);
含 MALFORMED 的请求对该类静默忽略。运维接口(controller/runbook)属 U11 遗留。
### 1.2 报文模型(sealed 分派)
- `MetaFields(sndr, type, styp, seqn, dttm)`——实名沿用 legacy META.java。
- `MsgKind` sealed`Schd(RESP|DNLD|ADFT)` + `Flop(29 类 STYP)``typeTag` 产出
`SCHD-XXX` / `FLOP-xxx`,与 `HandlerRegistry.keyOf` 同源(查表键=日志类型,禁止分叉)。
- 解码失败二分:`DecodeResult.Err(MALFORMED)` → DEAD`Err(CODEC_ERROR)` → FAILED 退避
T06/U11)。
- Handler 为**纯函数**`decide(flightView, msg) → Decision`flightChanges / msgNotifies /
schdPush / outboundIntents / refUpserts),不触碰 Redis/Kafka——副作用全部由泵边界执行。
- Handler 实装:0/32(骨架),翻译属阶段 2/3,逐条对照 ACM2-4 行为基线与 KEEP/FIX 矩阵。
## 2. 数据模型(自有 PostgreSQL · ACM2-12
`db/migration/V1.0.0__own_pg_pipeline.sql`(PG 方言,自有库;legacy 旧表与共享库表不在
本仓库声明,见 architecture.md §6):
| 表 | 角色 | 关键列/约束 |
|---|---|---| |---|---|---|
| PROC_STATE | 取消息侧:处理伴生状态/重试/毒丸(与共享库 CMINMSGS_ID 对应) | `uk_proc_identity(IDENTITY_KEY)` 唯一约束=I3 依据;`idx_proc_head(STATE, CMINMSGS_ID)`=队头 | | `PROC_STATE` | 入站消息的处理状态、身份、重试次数和错误原因 | `CMINMSGS_ID` 主键防止重复入队;`IDENTITY_KEY` 唯一约束防止业务重复;按最小未完成消息 ID 取队头 |
| MSG_EVENT | 发消息侧:统一投递 outbox | `EVENT_ID` 自增=全序;`idx_evt_head(TARGET, STATE, EVENT_ID)`=每 target 队头 | | `MSG_EVENT` | 等待投递的事件(outbox | `EVENT_ID` 决定投递顺序;`TARGET` 区分目标;`PARTITION_KEY``schd` 中为 `FLID` |
| PUMP_JOB | 泵作业调度(作业不插队,队头空闲/退避窗口执行) | kindARCHIVE/HISTORY_SWEEP/PROJECTION_REBUILD | | `PUMP_JOB` | 持久化维护作业 | 状态为 `QUEUED / RUNNING / DONE / FAILED`;不与业务消息共用排序序号。 |
| REQ_TRACK | 15 类请求状态机 | REGISTERED/SENT/WAITING/DONE/EXPIRED`COUTMSGS_ID BIGINT`U18 修正) | | `REQ_TRACK` | 上游请求及应答关联 | 保存请求类型、参数、出站信箱 ID、发送和完成时间;同类只允许一个开放请求。 |
| REF_MASTER | 21 类静态主数据 | `(RTYPE,RKEY)` PKSOURCE=ADMINAPI/AODB/PIPELINEREFRESHED_AT | | `REF_MASTER` | 静态参考数据 | `(RTYPE, RKEY)` 唯一,`SOURCE` 记录数据来源。 |
| `PROC_STATE_HST` | 终态处理记录的归档目标 | 属于目标设计,当前迁移尚未建表;不得改写为共享库历史表。 |
已知 DDL 缺口(U18,随本库 PG 化修正/收窄):REQ_TRACK.COUTMSGS_ID 已按 BIGINT 字段与索引定义以 `src/main/resources/db/migration/` 为准。报文原文仍从共享信箱读取,因此必须协调原文保留期,不能在消息尚需处理或重放时提前清理。
时间列须 DATETIME(6)/显式 UTC 口径在 U05 数据层实现时定;FLIGHT_STATE 因缓做不在本库。
**存储边界(ACM2-12 定案)** Redis 保存 `flightInfo` 与快照代际元数据 `gen``gen` 记录快照所属日期、版本及该代航班集合,不属于静态参考数据。
- **自有 PostgreSQL** = 上表全部(消息管道 + 调度 + 请求 + 21 类)。本地事务只在此库:
处理侧「MSG_EVENT 插入 + PROC_STATE→SUCCEEDED」同事务;其余跨存储一律外部副作用。
- **共享 MySQLcdairport,他人系统)仅信箱 DML、不建表**:上游外部写 CMINMSGS
本系统 JDBC 轮询读 + 处理回填;出站写 COUTMSGS(他人读取发送)。见 §3.1/§3.2 的事务模型。
- **Redis**:航班动态 flightInfo + 快照 **SCHD_GENgen**——Lua 内原子「覆盖+按代差删+
版本推进」;重放幂等由 Lua 承接(协议重设计属 U09),`RefDataRepository` 为目标实现的
过渡占位接口。
- **FLIGHT_STATE(阶段 B 权威):缓做不落表**(Redis 永续动态权威)。
- 实现状态:迁移 SQL 已按 PG 落地(V1.0.0);Repository 接口归属注释已对正(自有 PG /
信箱封装 / gen→Redis 占位 / FlightState 缓做);Micronaut Data 实装与信箱适配层
CminmsgMailbox/OutboxMailbox)属 U05 批次。
## 3. 核心流程设计 ### 2.2 消息、身份与决策
### 3.1 流程 1:收报(JDBC 轮询 + HTTP compat `XmlCodec` 将 XML 解码为 `DecodedMessage`,包含 `SNDR / TYPE / STYP / SEQN / DTTM` 元数据和业务载荷。`MsgKind` 区分 `SCHD``FLOP` 子类型;Handler 查找与日志类型标识使用同一套映射。
**主路径(生产/SIS 口径)**:上游经 CIIMS 等外部系统写入共享 MySQL `CMINMSGS` 业务身份统一由 `Identity.of` 生成:
`DATE_PROCESSED IS NULL`);本系统 `ingress` 经 **JDBC 轮询**发现新信(与 legacy
`MsgExchangeRunner.getNewMsgsAfterId` 同语义,1s 节律),自有 PG 入队:
1. 目标态分两路:快路径按持久化 watermark 查询 `CMINMSGS_ID > watermark AND
DATE_PROCESSED IS NULL`;补偿路径按受控周期重扫“未处理且 PG 无对应 PROC_STATE”的记录。
watermark 仅在本批 PG 入队均已确认后推进。**当前初版** `InboxPoller` 固定 `afterId=0`
每轮全量扫描未处理记录并以 PG 判重,尚未实现上述水位与补偿频控;
2. `procState.insert(id)`:自有 PG 建 PENDING 行入队;本步失败 → 下轮重扫补建;
3. 不解析报文、接收层无业务 identity 唯一约束(I3);`PROC_STATE.CMINMSGS_ID` 主键负责
轮询重扫幂等。当前无显式 `wakePump()`,主泵 1s 轮询兜底。
**compat 路径(现役 HTTP 写)**`InboxService.accept``POST /cminmsgs/send`=
共享信箱 `insertRaw` + 自有 PG 入队(跨库,非同一事务);「已持久化」响应语义与现役
对拍(U16)。用于手工注入/影子对拍,**非**上游报文到达的主拓扑。
### 3.2 流程 2:主泵 tick`Pump.tick`
每 tick 按序判定:
1. `headQueued()` 取作业、`headUnfinished()` 取最小未完成 CMINMSGS_ID**含 FAILED**
消息间严格保序:队头退避未到期即 sleep 至到期点,后方消息永不越队)。
2. `jobBefore(job, head)`head 为空或队头 FAILED 时作业先行——**当前为 head-state 近似**
(非入队时间排序),与「统一 FIFO」注释存在已知偏离(U15 未实装);ACM2-12 口径为
作业窗口执行(队头空闲/退避窗口),不追求与消息统一全序(Checks ④)。
3. 队头 FAILED 且退避未到期:`poisoned()` 判定(attempts≥maxAttempts 或滞留超
head-deadline 10m)→ DEAD(EXHAUSTED) 毒丸升级(Pump 侧;投递侧同语义属 U13,未实装);
**实现注**headDeadline 判据以 `updatedAt` 为锚,而 updatedAt 与 nextAttemptAt 同一次 FAILED
写入、退避 ≤60s(封顶)→ 有 nextAttemptAt>now 必有 nowupdatedAt≤60s<10m**滞留超时分支实际
不可达**,仅 attempts 维度生效;锚点语义随 U05/U13 定案并补测试(Pump 未注入 Clock,主泵级
门禁无单测锁定,见 §7);否则 sleep 至 nextAttemptAt。
4. 正常队头 → `MessageProcessor.processOne`
入口守卫(FAILED 且已 exhausted → DEAD)→ `rawOf` 缺失 → DEAD(MALFORMED) →
decodeMALFORMED→DEAD / CODEC_ERROR→FAILED)→ ignoreMsg 匹配(LDM/REGN/RSTA/EROR
命中→SKIPPED,回填遵循 US-09;当前未实装)→ identity 首绑
`tryBindIdentity` 失败 → SKIPPEDI3)→ Schd RESP/DNLD → `SnapshotFlow`ACM2-16 定案:
DNLD 与 RESP 均走 SnapshotFlowRESP 成功后在同事务完成匹配开放 RQFD 的 `REQ_TRACK→DONE`
迟到或无匹配 RESP 严禁更新快照,直接转 SKIPPED 并审计)→
其余 → `Handler.decide(redis.hgetAllFlightInfo(), msg)` →
阶段 ARedis 先写(I2 happens-beforeTODO redisApply)→ 自有 PG 事务 2
MSG_EVENT 插入 + PROC_STATE→SUCCEEDED(同库原子,@Transactional);
CMINMSGS 回填(DATE_PROCESSED/STATUS)为共享信箱**外部回填**ACM2-19 定案):
PG 事务提交后异步触发执行,持久化补偿、失败退避重试+告警、影子禁写;全部终态
SUCCEEDED / ignore SKIPPED / duplicate SKIPPED / DEAD)均必须回填,非终态禁止回填。
5. 异常边界(U08):`processOne` 内 try/catch → `ProcFailure.fail(INFRA)`attempts+1、
退避、达上限 DEAD);`InterruptedException` 恢复中断位后**上抛**loop 仅 catch
`Exception` 作最后防线,`Error` 任其终止进程(异常必可见)。
幂等键(I3):`SNDR|TYPE|STYP|SEQN``Identity.of` 唯一入口);「含日边界」可配置且
**默认关闭**SEQN 重置作用域 CONFIRM 前,上线后不改幂等键)。
### 3.3 流程 3:投递(`Dispatcher`
- 每 target 严格 FIFOI1 双层同策略):`headUnsent` 取队头;队头退避未到期 → 等待不跳过。
- 逐条循环显式排除 `KAFKA_SCHD`D3/U06):schd 唯一出口 `flushSchd`——
`claimBatch`ORDER BY EVENT_ID)→ 按 FLID 分组取 max(EVENT_ID)FIX:现役 buffer
无去重会重发旧值)→ FLTR JSON 数组一次发出;失败整批 attempts+1 退避、队首未到期不
claim、`lastFlush` 仅成功后推进;达上限整批 DEAD(DLQ)。周期/批上限取参数表
3s / 500)。
- 轮询间隔取参数表(下限 50ms,N18);无 200ms 硬编码。
- **Kafka 生产契约(ACM2-23**:对接 Kafka 2.8+ / 3.x+,生产者强制 `acks=all`、`enable.idempotence=true` 与 `max.in.flight.requests.per.connection=1`;切流前须确认 Broker 支持 `InitProducerId(22)`,严禁非幂等降级;README 不提供生产降级 env。
- 阶段 B(定案 2/D2ACM2-12 缓做):ES 投递成功 → 同线程同步 `insertSync` 删除事件
`deleteOf`Jackson 结构化序列化,refs 可空恒合法 JSON——U14)。当前不启用。
### 3.4 流程 4:日计划快照(`SnapshotFlow`RESP/DNLD
```text
SNDR | TYPE | STYP | SEQN
``` ```
SCHD-RESP / SCHD-DNLD
→ staging(流式解析+整包校验,TODO 阶段2;未实装→FAILED(UNSUPPORTED)
→ 守卫判定:若为 SCHD-RESP,检查开放 RQFDdttm < sentAt 或无匹配/已过期 → 严禁更新快照,转 SKIPPED 并审计)
→ Redis Lua SNAPSHOT_REPLACE(同一 hash 原子「覆盖新代+按代差删」,删除集=旧代flids−新代)
→ gen 版本推进(ACM2-12gen 随 flightInfo 同在 RedisLua 内原子版本 CAS
→ 自有 PG 本地事务(原子性):PROC_STATE→SUCCEEDED + RESP 匹配时)REQ_TRACK→DONE + MSG_EVENT 插入
→ PG 提交后异步触发信箱回填(持久化补偿,ACM2-19)
数据流说明:SCHD-RESP 处理依赖 US-08 已登记的开放 REQ_TRACKUS-06 与 US-08 为单向数据流耦合(US-06 依赖 US-08 登记能力),不构成双向故事依赖(ACM2-24) 接收时只按信箱 ID 去重;解码后才首次绑定业务身份。重试保留原有绑定,不能把自己判为重复消息。身份被另一条记录占用时,当前消息转为 `SKIPPED`,记录 `duplicate-of:<id>`。是否加入日期边界取决于上游序号重置规则,默认关闭;上线后不能随意更换身份算法
**已知缺口(U09,未定案,ACM2-12 后重设计为 Redis 内协议)**gen 与 Lua/SUCCEEDED 不再 Handler 是纯函数:
分属两存储即可同原子(全部在 Redis Lua);真正跨存储的窗口收窄为「Lua 已完成、PG SUCCEEDED
未写」——重放判据(版本不二次自增)与按代差删在 Lua 内以版本 CAS 承接,恢复协议待定案并补
测试。现有代码的「CAS 重放二次自增」缺陷(版本 1→2)与实现注随协议重设计一并消除。
### 3.5 泵作业(`JobExecutor`PUMP_JOB 自有 PG,作业窗口执行) ```text
Handler.decide(flightView, message) → Decision
Decision = 航班变更 + msg 通知 + schd 状态 + 出站意图 + 静态数据变更
```
- HISTORY_SWEEP3:30 清场,I4 同步链):判史 → 同步写 ES → 仅删成功集 Handler 不写 Redis、Kafka 或数据库。主泵负责应用决策;各类变更的持久化与重试边界必须明确,不能把“返回了 Decision”当成副作用已执行
**占位门禁(U10/T07 修订)**ES saveSync 接线前 `pickHistory` 恒空集、删除量恒 0
禁止「全量可删」fail-open 默认;现役五条判史规则 golden 通过后才允许接线。
**阶段归属**:按 ACM2-12 阶段 B 缓做口径标为 DEFERRED,不作为阶段 A 切流门禁;启用前
重新确认“历史链路不变”与阶段 B 投影范围、ES/OpenSearch 产品边界。
- ARCHIVE3:00):默认保留 **1 天**(接收时间早于 1 天)且仅终态(SUCCEEDED/SKIPPED/DEAD)可归档;保留期可配置为 1~7 天;
**定案口径(ACM2-17**:严禁向共享 MySQL 写入 `CMINMSGS_HST`(共享库严格保持两表 DML 契约);
归档目标为自有 PG `PROC_STATE_HST`(及 `MSG_EVENT_HST`)。非终态(PENDING/FAILED)禁止归档。
- PROJECTION_REBUILD(阶段 B 缓做,ACM2-12;重新评估后再启用)。
## 4. 失败与重试统一设计(U08 ### 2.3 状态与错误分类
| 侧 | 组件 | 迁移语义 | ```text
|---|---|---| 处理:PENDING / FAILED → SUCCEEDED(成功)
| ProcState(处理/快照) | `ProcFailure` + `FailureScheduler` | attempts+1 → exhausted ? DEAD(EXHAUSTED) : FAILED+nextAttemptAt | → SKIPPED(忽略或重复)
| MsgEvent 逐条(投递) | `Dispatcher.retryOrDead` | 同上(DLQ 保留行,attempts 审计) | → FAILED(等待重试)
| MsgEvent 批量(schd | `flushSchd` 整批 | 队首未到期不 claim;整批退避;达上限整批 DEAD | → DEAD(非法报文或重试耗尽)
- 退避表 `[1s,2s,4s,8s,16s]`,单档封顶 `backoff-cap-ms=60s``attempt≤0` 兜底首档(N28)。 投递:PENDING → SENT
- 时间一律经可注入 `java.time.Clock``TimeFactory`;测试用 MutableClock,无真实睡眠)。 → PENDING(退避后重试)
- loop 兜底 catch 不做状态迁移(迁移已在边界完成),仅防线程静默死亡。 → DEAD(重试耗尽)
```
## 5. 不变量与实现落点 `SUCCEEDED / SKIPPED / DEAD` 是处理终态,不再阻塞后续消息;`FAILED` 不是终态,仍占据队头。`DEAD` 表示需要处置,不等于业务成功。
| 不变量 | 语义 | 落点 | 状态 | | 错误类别 | 处理方式 |
|---|---|---|---| |---|---|
| I1 | 单写者严格 FIFO + HOL 阻塞 + 毒丸升级 | `headUnfinished`/`headUnsent` 队头语义、`poisoned()` | 实装(attempts 毒丸生效;head-deadline 判据不可达待修,见 §3.2 注;job 为 head-state 近似 / 作业窗口属 U15 | | `MALFORMED` | 报文非法,直接 `DEAD`,不在原记录重放白名单内。 |
| I2 | Redis 先写、后于事件创建(happens-before | `processOne` 阶段 A 分支 | TODO redisApply(流程占位已留) | | `CODEC_ERROR` | 解码能力问题,退避重试;修复后允许重放。 |
| I3 | identity 首绑幂等;接收层无唯一约束;SUCCEEDED 回填 | `Identity`/`tryBindIdentity`/`backfillOnSuccess` | 实装 | | `UNSUPPORTED` | Handler 或快照能力未实现,按可恢复失败处理,不直接当作非法报文;仍受重试上限约束。 |
| I4 | 清场仅删 ES 成功集;按代差删 | `HistorySweepJob`/SNAPSHOT_REPLACE delFields | 门禁实装,ES 接线 TODO | | `INFRA` | 基础设施或执行异常,退避重试。 |
| I5 | 阶段 A Redis 写仅主泵线程;Delivery 不写 Redis | 单线程拓扑 + Targets.phaseA | 实装(拓扑约束,U26 运行期保护未做) | | `EXHAUSTED` | 重试耗尽或滞留超时,转 `DEAD`,人工复核后允许重放。 |
## 6. 配置参数(`msgx.*`ACMA-8 参数表初值) ## 3. 收报与主泵
| 键 | 默认 | 说明 | ### 3.1 收报
|---|---|---|
| `phase` | A | 阶段总开关(A/B 权威切换) |
| `service-name` | msgexchangeapi | 契约冻结;影子= msgexchangeapi-shadow |
| `pipeline.poll-interval` | 1s | 泵轮询节律(KEEP 现役) |
| `pipeline.max-attempts` | 5 | 处理/投递同值 |
| `pipeline.backoff-ms` / `backoff-cap-ms` | 1s..16s / 60s | 指数退避表与封顶 |
| `pipeline.head-deadline` | 10m | 队头滞留上界(毒丸升级) |
| `pipeline.autostart` | **false** | 生命周期门禁:true 才装配 Pump/Dispatcher 线程(dev+stubs 开) |
| `schd.flush-period` / `flush-limit` | 3s / 500 | schd 聚合节律与批上限 |
| `identity.include-day-boundary` | false | 幂等键日边界(CONFIRM 前禁开) |
| `consistency-check.on-startup` / `daily-sample-ratio` | true / 0.01 | 一致性哨兵(实装属 U25) |
基础设施键位口径(Micronaut 5.1U03):`datasources.default.*`**自有 PostgreSQL** `InboxPoller` 默认每秒读取未处理信箱记录,在 PG 建立 `PENDING`,不解析业务载荷。PG 插入必须按信箱 ID 幂等,失败由后续扫描补建。
ACM2-12)、`flyway.datasources.default.*`、`mailbox.shared-mysql.*`(共享信箱,仅 DML)、
`kafka.producers.default.*`、`eureka.client.*`logback 独立于本文件,
环境变量前缀 `MSGX_LOGSTASH_*`。
## 7. 测试策略 水位优化分为两条路径:快路径读取水位之后的新记录,补偿路径重扫遗漏的未处理记录。只有本批 PG 入队全部确认后才能推进水位。**水位不是已处理标记,也不能单独证明较小 ID 已收齐**;迟提交和补扫场景的顺序保证需要在启用前验证。
- **接口驱动 + 假仓储**:管道语义全部离线单测(无 DB/Redis/Kafka),时间用 `MutableClock` 兼容 HTTP 入口执行“写入共享信箱 → PG 入队”。两步不在同一事务中:信箱成功而 PG 失败时,原文不能丢失,由轮询补建;客户端失败重试可能再次写信箱,业务身份去重仍然必需
- **不变量测试**FIFO/HOL、schd 批退避与 DLQ、重试上限、重放白名单、
聚合最新态、配置绑定、DI 装配冒烟(PipelineSmokeTest:收报→FAILED(CODEC_ERRORstub codec
恒 CODEC_ERROR)→重放→schd 聚合发出)。
**范围注**:以上覆盖的是边界级(processOne/flushSchd/仓储)语义;主泵 tick 级 HOL/毒丸/退避
门禁因 Pump 未注入 Clock 而无单测锁定(§3.2 注),DispatcherTickTest 仅锁批退避与「队首未到期
不推进」。
- 最近生成的 JUnit XML 报告为 39 项测试全绿;本轮文档审查因沙箱不能写用户级 Gradle 缓存,
未重新执行(wrapper 钉 9.6.1 + JDK 25;受限环境需将 `GRADLE_USER_HOME`/`TMPDIR` 指向可写目录)。
- **U29 门禁(规划)**:不变量清单化入 CI 红即阻塞;「新增逻辑必伴生不变量测试」入贡献约定。
## 8. 可观测性设计(U12 已落地部分) ### 3.2 主泵调度
- logstash TCP 经 AsyncAppenderqueueSize 4096 / neverBlock / discardingThreshold 0 每次 `Pump.tick`
logstash 不可达丢弃日志而非阻塞业务线程(N31b 顺序约束:先降级再补日志)。
- MDC `traceId` = cminmsgsId/eventId`TraceLog.withTrace`),覆盖处理/投递日志片段;
收报→投递全链贯穿与 micrometer gauge(队列深度/投递延迟)未实装。
- 生命周期结构化日志:收报 INFO、SUCCEEDED/SKIPPED INFO、FAILED WARN、DEAD/毒丸 ERROR、
flush 批次 INFO、整批 DLQ ERROR——告警暂以 ERROR 日志为落点(DEAD 告警出口属 U13/U25)。
## 9. 已知缺口与定案待办(对照 ACM2-10) 1. 读取最小未完成消息,必须包含 `FAILED`,不能只查当前可执行的记录。
2. 无消息,或队头仍在退避窗口内时,允许执行一个维护作业;消息已可执行时优先处理消息。
3. 队头达到重试或滞留上限时转 `DEAD(EXHAUSTED)`;未到重试时间则等待,不领取后续消息。
4. 其余情况调用 `MessageProcessor.processOne`
| 项 | 缺口 | 计划 | 作业执行时长和饥饿边界需要限制,不能用长期作业阻塞已到期消息。滞留超时应基于稳定的起始时刻,不能用每次失败都会刷新的 `updatedAt` 代替。
|---|---|---|
| U05 | JDBC PG 仓储、CMINMSGS 适配器与 **InboxPoller** 已有初版;但实现仍是逐操作独立连接,`MSG_EVENT + SUCCEEDED` 无本地事务,回填仍同步夹在两者之间;OutboxMailbox、可靠补偿、ARCHIVE 目标与真实双库集成测试未完成。`JdbcRefDataRepository` 仍为进程内过渡态,`JdbcFlightStateRepository` 为阶段 B 空实现 | 完成本地事务边界、回填/入队补偿、COUTMSGS 适配器与 Testcontainers 双库验收;生产启用前 fail-fast |
| U07/U26 | `autostart` 默认关=有意门禁,但生产无 fail-fast;双实例无运行期防护 | fail-fast 定案 + 租约/DB 锁拒启 |
| U09 | gen→Redis 协议未重设计(Lua 内原子版本推进;崩溃窗口=「Lua 完成/PG SUCCEEDED 未写」) | Redis 内版本 CAS + 恢复协议 + 测试(ACM2-12 |
| U13 | 投递侧无 createdAt/headDeadline 超时升级、无 DEAD 告警出口;处理侧 head-deadline 判据亦不可达(见 §3.2 注) | WP2 |
| U15 | job 与队头消息无统一全序(**head-state 近似**,非入队时间)——ACM2-12 口径:作业窗口执行,不追求与消息全序 | 作业窗口语义定稿(ACM2-12 Checks ④) |
| U16 | `/cminmsgs/send` 无 @Consumes/字符集(实测 text/plain 415)、无错误路径契约(@ControllerAdvice | WP2legacy 逐字对拍固化) |
| U17 | Eureka 注册名仍取 `micronaut.application.name`=msgexchange-nextgen);`msgx.service-name` 无运行时消费方 → 影子/切流前注册名与文档契约脱节 | `micronaut.application.name=${msgx.service-name}`application.yml |
| U18 | DDL 缺口(随 PG 化收窄:REQ_TRACK.COUTMSGS_ID 已 BIGINT;时间列口径 U05 定) | U05 批次 |
| U19–U21 | 请求状态机量纲/死分支、identity 绑定静默跳过、21 类静态接口/表对齐(REF_MASTER/SOURCE | WP2 |
| U22U24 | 载荷类型收敛、eventSeq 未接线、每报文全量读语义定案 | WP3 |
| U25/U28/U30 | 一致性哨兵实装、README 安全节/入口、索引与杂项 | WP3/4 |
| U27 | 专有材料(SIS md 703KB / XSD 版权头)治理决策 | WP4(ACL 核验先行) |
| ACM2-12 | 存储边界(自有 PG + 共享信箱 + Redis 动态/gen + 阶段 B 缓做):迁移 SQL/配置/接口注释已按定案调整(V1.0.0 PG);信箱适配层、gen Lua、作业窗口语义、影子重设计未实装 | ACM2-12 Checks ①–⑥ |
| ACM2-11 | **决策史(已被 ACM2-12 吸收)**:曾讨论 21 类静态独立 PG 参考库;定案为并入自有 PG `REF_MASTER`(见 ACM2-12),勿再按 `datasources.reference` 第二库规划 | 仅作决策脉络参考 |
## 10. 用户故事 ### 3.3 单条处理
面向需求优化的用户故事已集中到 [user-stories.md](user-stories.md)。该文档将目标能力、验收标准、 ```text
依赖与待确认问题分开,覆盖阶段 A US-01~US-14、延后 US-15、上线 EPIC 及 legacy HTTP 去留, 读取原文 → 解码 → 忽略规则 → 首次绑定身份
避免把当前实现、目标设计和遗留兼容行为混成同一项承诺。 ├─ RESP / DNLD:快照流程
└─ 其他:Handler 决策
Redis 应用变更
PG 本地事务:待发事件 + 处理终态
提交后补偿回填信箱
```
- 原文缺失当前归为 `MALFORMED`;读取异常不能伪装成“缺失”,应进入基础设施重试。
- 忽略规则覆盖约定的 `LDM / REGN / RSTA / EROR`,转 `SKIPPED` 并审计;不能产生业务副作用。
- Redis 更新必须先于对应事件提交。PG 事务只覆盖本库,不能靠事务注解把 Redis 或 MySQL 操作变成原子操作。
- 所有终态都需要回填信箱,包括成功、忽略、重复和死信;非终态禁止回填。回填必须在 PG 提交后执行,并有持久化补偿、退避与告警;影子环境禁写。
## 4. 日计划快照与请求匹配
### 4.1 快照发布
`SCHD-RESP``SCHD-DNLD` 都进入 `SnapshotFlow`
1. **暂存校验**:流式解析后完成整包校验和航班规范化;失败前不修改权威状态。暂存数据可在崩溃后从原文重建。
2. **应答守卫**`RESP` 必须匹配开放的 `RQFD` 请求;无匹配、已过期或报文时间早于发送时间时,转 `SKIPPED` 并审计,不更新快照。
3. **原子替换**:Redis Lua 在同一次操作中校验版本、覆盖新代、删除旧代差集并推进 `gen`。删除集为“旧代航班集合 − 新代航班集合”,不是全部现存航班,不能误删快照集合外的增量航班。
4. **提交结果**:在 PG 同一事务中保存 `MSG_EVENT`、将消息置为 `SUCCEEDED`,并将匹配 `RESP` 的请求置为 `DONE`;随后执行信箱回填。
关键恢复窗口是“Redis 已替换,PG 尚未提交”。重试同一快照必须识别已应用结果,不能再次增加版本,也不能用旧快照覆盖新状态。**版本校验与重放识别协议仍待实现验证**,单纯“读当前版本再加一”不满足要求。
### 4.2 上游请求与静态数据
`RequestCoordinator` 管理请求生命周期:
```text
REGISTERED → SENT → WAITING → DONE
└──→ EXPIRED
```
注册同类新请求前使旧开放请求过期。只有 `COUTMSGS` 写入确认后才标记 `SENT` 并关联出站记录;写信箱成功但本地未确认的情况需要补偿与去重,不能无条件重新发送。
应答优先按已确认的回显字段精确匹配。回显契约未确认时,按同类开放请求和 `DTTM ≥ sentAt` 判断的降级方式存在跨代误配风险,必须明确接受并审计,不能宣称精确关联。比较前统一时区和时间单位。
静态应答写入自有 PG 的 `REF_MASTER`,日计划应答走快照流程。请求完成必须在相应数据处理成功之后;超时和迟到应答不能修改已关闭请求对应的状态。
## 5. 事件投递
### 5.1 普通事件
`Dispatcher``TARGET` 读取最小未发送 `EVENT_ID`。队头退避未到期时,该目标停止推进;发送确认后才标记 `SENT`,失败记录次数和下次执行时间。所有外部调用需要有界超时,避免阻塞整个投递线程。
投递是至少一次:下游已接收但本地未标记成功时可能重发。Kafka 生产约束沿用架构决策 D11,但生产者幂等不替代应用层事件去重;跨重启的事件身份和下游去重契约仍需落实。共享出站信箱也必须单独解决重复写入,不能假设 Kafka 的保证适用于 MySQL。
### 5.2 `schd` 聚合
`KAFKA_SCHD` 不进入逐条投递循环,只由 `flushSchd` 发送:
1. 队头可执行后,按 `EVENT_ID` 顺序领取有界批次。
2.`FLID` 分组,保留批次内最大 `EVENT_ID` 对应的状态,组成 FLTR JSON 数组发送。
3. 成功后将本批被代表的事件一起标记完成,推进 `lastFlush`;失败则整批增加次数并退避,达到上限整批转 `DEAD`
默认聚合周期 3 秒、批上限 500。它提供最新状态通知,不保留每次中间变化;批次不能绕过尚在退避的队头。
## 6. 失败恢复与维护作业
### 6.1 失败、重试与重放
`ProcFailure``FailureScheduler` 统一处理侧的失败落账;投递侧按单条或聚合批次执行同样的次数与退避规则。默认最多 5 次,退避档位为 1、2、4、8、16 秒,单档封顶 60 秒。
失败必须在持有具体消息或批次的位置记录,外层循环只做兜底日志和等待,不重复增加次数。线程中断应恢复中断标记并向上传递;不捕获 JVM `Error` 作为普通业务失败。所有时间判断通过注入的 `Clock` 完成。
`ReplayService` 只允许 `CODEC_ERROR / UNSUPPORTED / INFRA / EXHAUSTED``FAILED / DEAD` 回到 `PENDING`,重置次数和下次执行时间,保留身份与错误审计。旧消息进入终态后,后续消息可能已经执行;因此**重新入队不等于恢复历史顺序**,人工重放前必须评估状态覆盖和版本保护,不能直接批量重放到生产。
### 6.2 归档与阶段 B 作业
`ARCHIVE` 仅归档自有库的终态记录,默认按接收时间保留 1 天,保留期可配置为 1~7 天。归档表、接收时间依据和关联事件处理尚需落地;不得归档未完成记录,也不能因移走身份记录而意外失去业务去重能力。共享信箱保留策略由库所有方管理。
`HISTORY_SWEEP``PROJECTION_REBUILD` 暂缓。未来清场必须只删除已确认成功写入历史存储的集合;历史写入未接通时默认删除零条。历史写入与删除事件入队之间仍需恢复方案,顺序调用不构成原子提交。
## 7. 接口与运行配置
兼容入口为 `POST /cminmsgs/send`,请求体为原始 XML,当前成功响应为 HTTP 200、`text/plain` 格式的信箱记录 ID。这只表示接收结果,不表示业务处理成功。请求媒体类型、字符集和失败响应仍需与现役逐项对拍;查询与其他兼容端点不能因列入需求就视为已提供。
运行配置以 `application.yml``application-dev.yml``.env.example` 为准,设计上重点区分:
- `pipeline.autostart``msgx.stubs`:分别控制管道启动和内存适配器;生产禁止 stub,默认不自动启动。
- `pipeline.poll-interval / max-attempts / head-deadline`:控制轮询、重试上限和队头滞留;不能改变 FIFO。
- `schd.flush-period / flush-limit`:控制状态通知的聚合延迟与批量大小。
- `identity.include-day-boundary`:影响去重语义,不能作为普通调优项切换。
- `phase`:阶段 A 是当前范围,不应把切为 B 当成已具备历史投影能力。
日志关联消息 ID、事件 ID 和批次;失败记录错误分类、次数、下次执行时间。健康检查反映依赖实际可用性;队头滞留、积压、死信和补偿失败需要指标及告警。日志出口故障不得阻塞业务线程。
## 8. 验证要求
单元测试使用内存仓储和可推进的 `Clock`,不依赖睡眠或在线中间件。以下不变量必须有回归测试,接口级单测不能替代主泵调度测试:
| 场景 | 必须验证的结果 |
|---|---|
| 重复扫描、入队中断、较小 ID 迟到 | 不重复入队、不丢记录、不让后续消息越序。 |
| 队头失败、退避及作业竞争 | 消息不越队;到期后恢复;作业不使消息无限饥饿。 |
| 同身份多条记录、失败后重试、归档后重复 | 只产生一次有效业务处理,不把自身重试判为重复。 |
| Redis 成功后 PG 失败、快照重复或迟到 | 不重复推进版本、不回退状态、不误删增量航班。 |
| PG 提交失败、信箱回填失败 | 事件与处理结果一起回滚;已提交结果只补偿回填。 |
| 投递确认丢失、批次失败、次数耗尽 | 允许可识别的重发、保持目标顺序、整批退避并保留死信。 |
| 请求超时、无匹配 RESP、时间单位不一致 | 不误用迟到应答,不提前完成请求。 |
| stub 误配置、重复实例、停机中断 | 生产拒绝不安全启动,工作线程能正确退出。 |
真实适配器还需 PG/MySQL 事务与补偿集成测试、Redis Lua 中断恢复测试、Kafka 故障投递验证;启动冒烟只证明装配可用,不证明这些一致性要求已满足。常规验证命令为 JDK 25 下执行 `./gradlew test`
## 9. 实现入口
生产代码根目录为 `src/main/kotlin/com/gzzn/omms/msgexchange/`
| 关注点 | 主要入口 |
|---|---|
| 收报与兼容接口 | `ingress/InboxPoller.kt``InboxService.kt``InboxController.kt` |
| 调度与处理 | `processing/Pump.kt`(含 `MessageProcessor`)、`Handler.kt``Identity.kt` |
| 快照与请求 | `processing/SnapshotFlow.kt``reference/RequestCoordinator.kt` |
| 投递与作业 | `delivery/Dispatcher.kt``SchdAggregation.kt``jobs/JobExecutor.kt` |
| 持久化与恢复 | `infra/persistence/``infra/redis/``infra/retry/` |
| 启停与配置 | `PipelineLifecycle.kt``config/PipelineProps.kt` |
## 10. 当前实现差异
以下缺口直接影响上述设计是否成立,不能以类或接口已存在作为完成依据:
- **事务与外部副作用**:当前 JDBC 操作仍分散执行;普通处理按“插事件 → 同步回填信箱 → 更新状态”调用,尚未实现要求的 PG 原子提交与提交后补偿。Redis 普通变更、出站意图及真实出站信箱也未形成完整链路。
- **收报与调度**:轮询仍从 `afterId=0` 扫描,持久水位与补扫策略未完成;作业以“队头为 FAILED”近似窗口,未区分是否已到期。主泵直取系统时间,滞留判据使用更新时刻,不能保证设计要求的超时升级。
- **快照与业务能力**:快照暂存仍为占位,当前分流仅覆盖 DNLD,RESP 守卫与请求完成事务未接通;`gen` 仍走过渡仓储,未实现 Redis 内原子版本与重放协议。忽略规则及所需 Handler 还需补齐。
- **请求、静态数据与归档**:请求在真实出站前就标记发送,时间匹配与审计仍需修正;静态数据存在进程内过渡实现,归档表与关联保留策略尚未落地。
- **生产与运维**:缺少完整启动校验、运行期单写者保护、影子隔离和 Kafka 强制配置校验;投递滞留升级、死信告警、端到端追踪与指标未闭环。
进度与验收项见 Plane ACM2-10 实施计划(U01–U30),业务契约与待确认事项见 [user-stories.md](user-stories.md)。本文件不维护工单流水账、测试数量或历史方案全文。
-50
View File
@@ -1,50 +0,0 @@
# 用户故事文档整改 TODO
> 本清单只跟踪文档收敛;实现任务仍由对应 Plane 工作项管理。新建 Plane 汇总项会与
> ACM2-15~22 重复,因此在确认合并策略前不重复创建。
## P0:先消除范围冲突
- [x] **ACM2-16 — RESP/DNLD 路由**:在 `user-stories.md` 增加报文路由矩阵;RESP/DNLD
共用 SnapshotFlowRESP 成功后再完成匹配的 RQFD 请求;ADFT 保持增量 Handler。
- [x] **ACM2-17 — 阶段边界**ARCHIVE 独立为阶段 A US-11HISTORY_SWEEP 独立为
DEFERRED US-15,不作为阶段 A 切流门禁。
- [x] **ACM2-19 — 信箱回填**:新增 US-09,区分 PG 终态与共享信箱回填,覆盖提交后执行、
持久化补偿、告警、影子禁写和双跑写权。
- [x] 将以上目标态同步回 Plane 架构权威(ACM2-3 / ACM2-16 / ACM2-17 / ACM2-19 已裁定并回写);仓库文档与 Plane 口径一致。
## P1:补齐遗漏能力
- [x] **ACM2-15**:新增 ignoreMsg US-04,固定匹配位置、规则、SKIPPED 终态及回填依赖。
- [x] **ACM2-18**:新增实时查询 US-12、21 类 REF US-13、机位/登机桥 US-14;明确
admin-api 21 类、现役 2 类与 AODB 15 类请求是不同集合。
- [x] **ACM2-20**:增加五个 legacy HTTP 端点的 KEEP/修正/不做矩阵。
- [x] **ACM2-21**US-03 仅依赖 US-01;补 FAILED 占队头、毒丸与作业窗口,业务例外移至 US-05。
## P2:统一证据与可验收性
- [x] US-01 定义 watermark 快路径和补偿重扫,并注明当前 `afterId=0` 实现差距。
- [x] US-07(原请求故事)依赖补入采集和处理;重编号后为 US-08。
- [x] Kafka 旧 Broker 降级移至待确认,不再作为无权威出处的验收条件。
- [x] MALFORMED 重放统一为“跳过并返回逐项结果”,与 `ReplayService` 语义一致。
- [x] 将原 US-10 拆成 OPS-1~OPS-4,不再用“满足约定/评审”作单项验收。
- [x] 产品确认 `user-stories.md §7` 的八项开放问题,并把答案写回对应故事。
- [x] 文档定案后更新 Plane 关联项状态(ACM2-15~22 全部推进至 Todo,定案评论已写回);没有发布证据时不得标记 Done。
## P3:二轮审查文档修复(ACM2-23~27)
- [x] **ACM2-23**Kafka 生产契约 vs README 降级指南对齐(README 删生产降级 envUS-07/§7-6 增补 Broker 版本确认依赖)。
- [x] **ACM2-24**:消除 US-06 ↔ US-08 循环依赖(US-08 移除 US-06 依赖,AC6 改为集成验收)。
- [x] **ACM2-25**US-02 ResponseDto 失败字段对齐 legacy`err_code`/`err_msg`,不用 `msg`)。
- [x] **ACM2-26**US-09 STATUS 值域定案(单值矩阵 + 库方确认依赖 + §7-9)。
- [x] **ACM2-27**:分区键 SNDR 固化、US-11/design §3.5 默认 1 天可配 1~7 天、小节末空行。
- [x] **ACM2-14**:取消并 relates_to ACM2-22(与 ACM2-22/2327 重叠收束)。
- [x] **ACM2-22**:写入二轮审查闭环评论(已验证属实 + 新发现 1~5 + ACM2-2327)。
## 验证清单
- [x] `architecture.md``design.md``user-stories.md` 的 RESP/DNLD 和阶段表述一致。
- [x] README 指向新的故事范围。
- [x] Markdown 变更通过 `git diff --check`
- [x] Plane ACM2-2327 已 commit 后转 Done346929e)。
- [ ] 实现尚未随本文修改;后续代码 PR 必须补 FIFO、快照、回填和双库集成测试。
+223 -169
View File
@@ -1,272 +1,326 @@
# msgexchange-v2 用户故事草案 # msgexchange-v2 用户故事与实施清单
> 依据 `architecture.md`、`design.md`、Plane ACM2-3/5/6/7/12/1522 与 legacy 基线整理。 ## 1. 如何使用本文
> 本文描述目标能力和明确保留的兼容行为,不代表当前代码已完成。
## 1. 约定 本文是把现有脚手架补成可用系统的实施入口:**故事定义要交付什么,验收标准定义怎样证明完成,代码落点说明从哪里改起**。保留 US-01US-15、OPS-1OPS-4 编号,便于关联已有任务和测试。
- “信箱已落信”“PG 已入队”“业务处理成功”“共享信箱已回填”“下游已投递”是不同事实 - 系统边界见 [architecture.md](architecture.md),模块流程见 [design.md](design.md)。本文不重复设计全文,也不以工单状态代替代码验收
- `KEEP` 表示兼容现役;`FIX` 表示修复 legacy 缺陷`DEFERRED` 表示不属于阶段 A。 - “当前基础”来自本轮代码核对,只表示有接口或部分实现,不表示故事完成。`KEEP` 是保留业务兼容,`FIX` 是明确修正旧缺陷`DEFERRED` 不进入阶段 A。
- 依赖只表示前置能力,不形成循环;待决策内容不得伪装成验收标准 - “信箱落信、PG 入队、业务处理完成、信箱回填、下游确认”是五个独立事实,接口、日志和测试必须分开表达
- 所有内部迁移只落自有 PG;共享 MySQL 不建表、不增列、不写历史表。本文用 `DATE_PROCESSED / STATUS` 表示逻辑字段,实际列名以库方契约为准。
- 验收条目可按 `US-xx/条目号` 引用。故事较大时按下文子范围拆成小 PR,不把一个故事等同于一个提交。
## 2. 阶段 A 用户故事 **存储基线提醒**[FLIGHT_STATE 决策提案](decision-flight-state.md) 尚未定案。本文仍按阶段 A Redis 权威描述;涉及权威状态、快照和事务顺序的最终实现,先处理 Q1,不能一边实现 Redis 协议一边擅自迁为 PG 权威。
### US-01 可靠采集共享信箱报文 ## 2. 建议实施顺序
**作为** 平台运维人员,**我希望** 持续采集 `CMINMSGS` 未处理报文,**以便** 上游无需改变投递方式 先补可靠性边界,再打通一条真实业务链,最后扩充报文类型。每批均可先用假适配器测试,但真实链路验收不能省略
| 批次 | 实施范围 | 本批交付证明 |
|---|---|---|
| S0:契约与基础 | 确认 Q1;整理报文/参考数据清单;补 Clock、PG 事务入口、迁移及 OPS-1 基础门禁 | 数据源归属明确;迁移可用;关键 Bean 装配测试;未就绪生产配置拒启。其他 Q 项只阻塞对应功能,不阻塞无关单测。 |
| S1:可靠收报与处理 | US-01、US-03 调度/解码/提交子范围、US-09、US-04 | 未处理信箱 → PG 入队 → 忽略终态 → 持久补偿回填;重试不越队,注入中断可恢复。 |
| S2:最小业务闭环 | US-05 的 ADFT 与一个不依赖参考数据的 FLOPUS-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-1OPS-3 | 安全重放、归档后去重、故障告警、单写者保护、影子禁写均有验证证据。 |
| S6:上线验证 | OPS-4,复核所有进入切流范围的故事 | 对拍、故障演练、配置和恢复 Runbook 验收后切流;US-15 不作为门槛。 |
**依赖口径**:US-03 是基础管道,不依赖具体业务 Handler;US-09 依赖其终态提交接口,US-04 复用 US-09。US-08 的请求登记与匹配基础不依赖 US-06;US-06 消费该基础,二者共同完成 RESP 集成验收,不形成开发依赖环。US-05 只有 PSDT 子范围依赖 US-14,不应阻塞其余 Handler。
## 3. 阶段 A 用户故事
### US-01 可靠采集共享信箱
**目标**:上游继续向 `CMINMSGS` 落信,本系统持续、可恢复地采集,不要求上游改投递方式。
**验收标准** **验收标准**
1. 按配置周期、ID 升序有限批次采集 `DATE_PROCESSED IS NULL`;这里只采集入队,不代表业务已处理。 1. 按配置周期、ID 升序有限批次采集未处理记录;接收层只入队,不解析业务、不回填已处理标记
2. 快路径使用持久化高水位增量扫描;另以受控周期补偿重扫“未处理且 PG 无状态”的记录。水位只在本批入队确认后推进 2. 按信箱 ID 幂等建立 PG `PENDING`;重复扫描、并发兼容入队和进程重启都不能重置已有终态
3. 每个 ID 在 PG 至多一个 `PROC_STATE(PENDING)`;重复发现不重复入队 3. 快路径用持久水位,补偿路径受控重扫遗漏;本批 PG 入队全部确认后才推进水位。补偿可分页推进,不能被已入队但尚未回填的前一批永久挡住
4. PG 不可用时不改共享信箱标记;恢复后补偿重扫能补建遗漏状态 4. PG 不可用或批次中途失败时不改信箱标记;恢复后补建遗漏,记录失败次数与扫描进度
5. 单条异常和数据库故障可观察,轮询线程不得静默退出 5. 较小 ID 迟提交、ID 有空洞、兼容入口先入队较大 ID 时,必须遵守经 Q2 确认的发现与顺序协议;不能用“最终会重扫”冒充严格 FIFO
**依赖**:共享库读权限、字段与索引契约。 **当前基础与落点**`ingress/InboxPoller.kt``InboxEnqueue.kt``infra/persistence/jdbc/JdbcCminmsgInboxRepository.kt` 已有轮询和判重;固定 `afterId=0`,水位与补偿未实现。扩展 `InboxPollerTest`,补真实 PG/MySQL 中断恢复测试。
**实现差距**:当前 `InboxPoller` 固定 `afterId=0` 全量扫描,无持久化水位和独立补偿频控。
### US-02 通过兼容接口注入报文 **前置**:共享库读契约;Q2 决定严格顺序的端到端验收。
**作为** 联调人员,**我希望** 通过 `POST /cminmsgs/send` 注入 XML,**以便** 执行回放和对拍。 ### US-02 兼容 HTTP 注入报文(KEEP
**目标**:联调工具通过 `POST /cminmsgs/send` 提交 XML,得到真实的信箱接收结果。
**验收标准** **验收标准**
1. 信箱落信后返回 `CMINMSGS_ID`,响应不得暗示 PG 已入队或业务已处理 1. 支持 `text/xml``application/xml``text/plain`,默认 UTF-8;空报文、超过 10MB 的请求和畸形 XML 返回规范错误,不落信。XML 校验禁用 DTD、外部实体与外部资源访问
2. 落信成功但 PG 入队失败时,由 US-01 最终补建 2. 信箱确认落信后返回 ID;PG 入队失败不把已落信伪装成未接收,由 US-01 补建。信箱写入未确认时不返回成功
3. Content-Type 支持 `text/xml``application/xml``text/plain`(默认按 UTF-8 解码);空报文、超大报文(> 10MB)及畸形 XML 返回规范错误 3. 目标为兼容 `ResponseDto`;固定成功/失败样例、HTTP 状态码、响应媒体类型和错误码表后加入契约测试,见 Q3。成功只承诺信箱落信,不承诺业务处理或下游完成
4. 响应结构逐字兼容 legacy `ResponseDto`:成功返回 `{"is_success": true, "body": <CMINMSGS_ID>}`,失败返回 `{"is_success": false, "err_code": "<code>", "err_msg": "<detail>"}` 4. 生产保持内网信任边界,由网关限制来源并审计;外露或跨网络必须先落实认证,不能把免密入口直接暴露
5. 生产默认沿用现役内网互信免密姿态(网关限定内部 IP 网段与审计);外露或跨网络时启用 Header 认证。
**依赖**US-01。 **目标响应体示例**(数字和错误码仅作示例,错误码表见 Q3):
### US-03 严格按序且幂等地执行处理管道 ```json
{"is_success": true, "body": 12345}
{"is_success": false, "err_code": "<code>", "err_msg": "<detail>"}
```
**作为** 航班数据消费者,**我希望** 报文严格按信箱顺序处理,**以便** 重试不会造成倒序状态 **当前基础与落点**`ingress/InboxController.kt` 当前返回 200 + 纯文本 ID,尚非目标 DTO;`InboxService.kt` 在落信后直接调用入队,需处理 PG 失败。新增 HTTP 媒体类型、大小限制及跨库失败测试
**前置**US-01 补建能力;Q3。HTTP 基础格式校验不替代 US-03 的业务解码。
### US-03 严格按序、幂等地执行管道
**目标**:报文失败和重试不造成航班状态倒序,也不重复产生副作用。
**实施拆分**:调度与时钟 → 安全解码及路由 → 身份绑定 → 状态应用与 PG 提交。先用假 Handler 验证管道,不等 US-05 全部实现。
**验收标准** **验收标准**
1. 选择最小未完成 ID`PENDING``FAILED` 占队头退避期间后续消息不得越过 1. 只取最小未完成 ID`PENDING / FAILED` 占队头退避未到期不得越过。作业仅在无队头或退避窗口执行,已到期消息优先,作业有界且不会造成无限饥饿
2. identity 首绑为 `SNDR|TYPE|STYP|SEQN`;冲突转 `SKIPPED` 并记录原 ID 2. 安全解码 XML,至少覆盖 META、SCHD、FLOP、参考应答与忽略类路由;合法但能力未支持是 `UNSUPPORTED`,不能一律归为非法报文。保留原文以支持诊断和回放
3. `MALFORMED` 直接 DEAD`CODEC_ERROR/UNSUPPORTED/INFRA` 退避;attempts 或 HOL deadline 耗尽后转 `DEAD(EXHAUSTED)` 3. 解码后首次绑定 `SNDR|TYPE|STYP|SEQN`;冲突转 `SKIPPED` 并记录原 ID;自身重试保留绑定。生产 `include-day-boundary=false`,更改算法须另行评审上游序号规则
4. PUMP_JOB 不与消息形成统一全序,只在无队头或队头尚在退避窗口时执行;不得让已到期消息饥饿 4. `MALFORMED` 直接 `DEAD``CODEC_ERROR / UNSUPPORTED / INFRA` 按次数和退避处理,耗尽转 `DEAD(EXHAUSTED)`。不能无限重试未实现类型,也不能立即当非法报文丢弃
5. Redis 先应用;`MSG_EVENT``PROC_STATE→SUCCEEDED` 在同一 PG 事务提交 5. HOL deadline 使用稳定的 `PROC_STATE.CREATED_AT`,不使用每次重试刷新的 `updatedAt`;所有调度判断注入 `Clock`。默认 5 次重试、10 分钟滞留限制;积压与人工重放的 deadline 边界按 Q6 验证
6. Redis 已写、PG 未提交的窗口可幂等重放 6. 当前 Redis 基线下,由主泵先幂等应用状态,再在同一 PG 事务提交事件与处理结果;终态回填意图通过 US-09 同事务保存。Redis 已成功而 PG 失败可安全重试,不能重复生成业务事件
7. Handler 只返回 `Decision`,副作用由管道执行;失败只在持有消息上下文的边界落账,中断向上传递,不作为普通失败吞掉。
8. 权威存储不可用或未完成恢复时停止业务处理;不能把“整个状态丢失”误判为“单航班不存在”而批量成功结束增量报文。
**依赖**US-01。 **当前基础与落点**`processing/Pump.kt`(含 `MessageProcessor`)、`Identity.kt``codec/XmlCodec.kt``infra/retry/``JdbcPgRepositories.kt`。已有身份/重试边界测试,但 codec 为接口,主泵直取系统时间,普通 Redis 更新占位,PG 操作未共享事务;补 `CREATED_AT` 迁移及主泵级回归测试。
**既有基线定案**:生产幂等键默认采用 `SNDR|TYPE|STYP|SEQN``include-day-boundary=false` 禁开,防止跨日重放漏判;HOL deadline 起算时间统一固化为 `PROC_STATE.CREATED_AT`(稳定入队时间戳),消除重试刷新 `updatedAt` 导致的超时不可达缺陷。
### US-04 忽略非业务报文(KEEP **前置**:US-01;Q1 决定状态提交实现,Q6 决定 deadline 边界。现有 `FlightStateRepository` 空实现不是建表授权。
**作为** 运维人员,**我希望** 已确认无需处理的报文被明确忽略,**以便** 不产生 DLQ 噪声。 ### US-04 明确忽略非业务报文(KEEP)
**目标**:无需处理的报文有可追踪的终结结果,不制造无效重试与死信。
**验收标准** **验收标准**
1. 解码 META 后、 Handler 前,大小写不敏感匹配 `TYPE-STYP` `TYPE-*` 1. 解码 META 后、身份绑定及业务 Handler 查找前,大小写不敏感匹配 `TYPE-STYP` `TYPE-*`;基线为 `LDM-* / REGN-* / RSTA-* / EROR-*`,不混用 `ERROR`
2. 基线为 `LDM-*``REGN-*``RSTA-*``EROR-*`;统一采用 `EROR`,消除 test 的 `ERROR` 漂移 2. 命中后转 `SKIPPED`,记录 `ignored:<rule>` 和计数;不更新航班、不创建业务通知
3. 命中后进入 `SKIPPED`,记录 `ignored:<rule>`,不创建业务事件 3. 通过 US-09 保存回填意图;命中、未命中、大小写和重扫均有测试。合法忽略报文不应因 `MsgKind` 尚不能表达它而先解码失败
4. 按 US-09 的规则回填共享信箱,并保留审计计数。
**依赖**US-03、US-09 **当前基础与落点**`MessageProcessor` 尚无忽略分支,`DecodedMessage/MsgKind` 主要覆盖 SCHD/FLOP;扩展解码与路由,不把忽略逻辑散落到 Handler
### US-05 应用 ADFT 与 29 类 FLOP 报文 **前置**US-03 解码/终态接口、US-09。
**作为** OMMS 业务,**我希望** 正确应用增量报文,**以便** Redis 动态符合 wire 契约。 ### US-05 应用 ADFT 与 29 类 FLOPKEEP + FIX
**目标**:增量报文正确更新航班及主/共享关系,并生成符合现役语义的通知。
**验收标准** **验收标准**
1. `SCHD-ADFT` 29 个 FLOP 子类型有纯函数 Handler;未知类型进入 `FAILED(UNSUPPORTED)` 1. `SCHD-ADFT` 29 个 FLOP 子类型逐项列入覆盖矩阵,每项有纯函数 Handler;未知类型可恢复失败。RESP/DNLD 不计入这批 Handler,走 US-06
2. 每类覆盖输入、Redis 变化、msg、schd、处理终态五面断言 2. 每类固定“输入与前态 → 后态 → msg → schd终态五面样例;区分字段缺失、显式清空、重复报文和主/共享航班。清单和 golden 样例按 Q8 补齐,不以“已写 29 个类”替代验收
3. 航班不存在按 legacy KEEP 语义结束且不重试:`SUCCEEDED` 终态结束, US-09 回填共享信箱 `DATE_PROCESSED = now()`, `STATUS = 'SUCCESS'`,防止死循环 3. 对按 KEEP 规则需忽略的不存在航班,`SUCCEEDED` 无副作用结束,并由 US-09 回填;ADFT 建航班等行为按各类型矩阵执行。此规则只适用于权威状态健康时
4. 共享航班默认不直接发通知,而是更新并通知主航班;FDEL 例外定案:删除共享航班时更新主航班 MAFL 列表并发出主航班通知;删除主航班删除其及所有子共享关联并发删除通知;目标航班不存在幂等成功退出 4. 共享航班通常更新并通知主航班,不直接发共享通知。FDEL 删除共享航班时更新主航班 MAFL 通知;删除主航班删除主航班及其子共享关联并发删除通知;目标不存在幂等成功。
5. ADFT/FDEL 使用值相等比较主/共享关系作为一次原子 Redis 变更持久化(FIX 5. ADFT/FDEL 使用值相等比较主/共享关系一次原子变更,不出现主已删、子残留等半状态
6. PSDT 依赖 US-14Handler 不直接调用 admin-api。 6. PSDT 通过 US-14 的只读映射计算 `abdg`Handler 不直接调用 admin-api。
**依赖**US-03、US-14、SIS/XSD 与 KEEP/FIX 矩阵。 **当前基础与落点**`processing/Handler.kt``domain/Decision.kt``infra/redis/` 只有主要扩展点;按类型新增 Handler 与同包测试。先做 ADFT + 一个普通 FLOP,再做主/共享及 PSDT,最后补齐矩阵。
**前置**US-03PSDT 另依赖 US-14Q1、Q8。
### US-06 导入 RESP/DNLD 日计划快照 ### US-06 导入 RESP/DNLD 日计划快照
**作为** 航班计划使用方,**我希望** RESP 与 DNLD 共用快照流程,**以便** 主动下和请求应答获得相同终态。 **目标**主动下和请求应答使用同一套全量计划处理,迟到应答不覆盖新状态。
| 报文 | SnapshotFlow | REQ_TRACK | Kafka msg | 迟到/无匹配处理 | | 报文 | 路由 | 请求状态 | 无匹配 |
|---|---|---|---|---| |---|---|---|---|
| `SCHD-DNLD` | 是 | 不更新 | 成功后通知 | 不适用(广播/全量) | | `SCHD-DNLD` | SnapshotFlow | 不更新请求 | 不要求开放请求 |
| `SCHD-RESP` | 是 | 匹配开放 RQFD DONE | 成功后通知 | 严禁更快照,转 SKIPPED 并审计 | | `SCHD-RESP` | 匹配守卫后进入 SnapshotFlow | 成功提交时匹配 RQFD DONE | SKIPPED、审计,禁止更新快照 |
| `SCHD-ADFT` | 否,走 US-05 | 不更新 | 按增量规则 | 增量应用 | | `SCHD-ADFT` | US-05 增量 Handler | 不更新请求 | 不适用 |
**验收标准** **验收标准**
1. RESP/DNLD 共用 staging 流式整包校验和 SnapshotFlow,不作为普通 FLOP 增量 Handler 重复实现 1. RESP/DNLD 共用流式解析、整包校验和规范化;校验失败不发布半包,旧快照保持可用
2. 主泵执行先后严格服从 I2(happens-before):主泵线程先执行 Redis Lua 完成写新代、旧代差集删除及 generation 版本 CASADFT 航班存活,FIX);Redis 执行成功后进入自有 PG 本地事务 2. RESP 仅匹配未过期、已发送的开放 RQFD;`DTTM < SENT_AT`、已过期、已被替代或无匹配时,不写业务状态,记录跳过原因
3. 自有 PG 本地事务原子性:同一 PG 事务内原子提交 `PROC_STATE → SUCCEEDED`、匹配开放请求的 `REQ_TRACK → DONE`(记录 completed_at 与 resp_cminmsgs_id)及 `MSG_EVENT` 出站通知 3. 当前 Redis 基线下,Lua 原子执行新代写入、旧代差集删除和版本校验/推进;删除集只来自上一代成员,不误删集合外的 ADFT 航班
4. 整包失败保留旧快照;相同报文重放(CAS 版本一致)幂等,不重复增代或差删 4. 随后在一个 PG 事务提交 `SUCCEEDED`、待发通知、回填意图;匹配 RESP 同事务完成请求,并保存完成时间与应答信箱 ID
5. 迟到(`dttm < req.sentAt`)或无匹配/过期(`EXPIRED`RESP **严禁更新快照**,转入 `PROC_STATE → SKIPPED` 并记录审计与告警,防止历史快照时光倒流覆盖新状态(对齐 G12) 5. 相同报文重放不二次增代或删数据;覆盖首次/连续快照、DNLD→ADFT→DNLD′、旧应答与 Redis 成功/PG 失败窗口。不能只靠“读取当前版本再加一”实现重放识别
6. 覆盖首次/连续发布、DNLD→ADFT→DNLD′、RESP 匹配和崩溃窗口。
**依赖**US-03、US-08、Redis gen 协议 **当前基础与落点**`processing/SnapshotFlow.kt` 的 staging 为占位,主处理只分流 DNLD,gen 走进程内过渡仓储。需补 RESP 路由、版本协议及 `REQ_TRACK` 应答关联字段和事务测试
**前置**US-03、US-08 请求登记/匹配基础;Q1、Q5。
### US-07 可靠、有序地投递 Kafka ### US-07 可靠、有序地投递 Kafka
**作为** Kafka 消费者,**我希望** 状态可见后有序投递,**以便** 通知不指向旧状态 **目标**:状态应用完成后投递通知;重试可识别、不乱序、不静默丢失
**验收标准** **验收标准**
1. `KAFKA:msg` EVENT_ID FIFO,确认后才标 SENT。 1. `KAFKA:msg`目标内 `EVENT_ID` 顺序发送,确认后才标 `SENT`;队头退避时不跳过,发送有超时上限
2. `KAFKA:schd` 按周期和上限领取;同一 FLID 仅发批内最新事件 2. `KAFKA:schd` 只通过 `flushSchd` 聚合,默认 3 秒/500 条;同一 FLID 批内最新状态,成功确认覆盖对应原事件,失败保持批次可恢复并退避,耗尽可见为 `DEAD`
3. 失败保持队头并退避;耗尽后 DEAD,不静默丢弃 3. 外部接收成功、本地确认失败或进程重启后允许重发;事件标识跨重发稳定,消费者有去重约定,不宣称端到端恰好一次
4. 事件包含稳定去重标识,分区键和重复投递有契约测试:`KAFKA:msg``SNDR` 作为分区键;`KAFKA:schd` 严格以 `FLID` 为分区键,确保单航班有序 4. msg 的目标分区键为 `SNDR`。schd 的聚合粒度与分区键先按 Q4 定案,明确哪些顺序只在单分区成立;不能把多航班数组声称为“每个 FLID 都是该 Kafka record 的 key”
5. 生产环境对接 Kafka 2.8+ / 3.x+,生产者强制 `acks=all``enable.idempotence=true``max.in.flight.requests.per.connection=1`,严禁非幂等降级;投递失败退避重试,达上限转 `DEAD(DLQ)` 告警,绝不静默丢弃 5. 生产强制 `acks=all``enable.idempotence=true``max.in.flight.requests.per.connection=1`;Broker 支持幂等生产协议并完成实际验证,不允许非幂等降级通过验收
6. 普通/聚合发送失败、确认丢失、批次标记中断和目标阻塞均有测试;DEAD 保留记录并告警。
**依赖**US-03、现网 Kafka Broker API 版本确认(切流前 `kafka-broker-api-versions.sh` 探测 `InitProducerId(22)`;未确认前不得标 US-07 实施完成) **当前基础与落点**`delivery/Dispatcher.kt``SchdAggregation.kt` 已有聚合和重试测试;`DeliveryPort.sendKafka` 仅接 topic/payload,无 key 或事件标识参数,真实生产适配与强制配置校验需补齐
**前置**:US-03 事件提交;Q4、现网 Broker 验证。wire 不兼容的标识字段不能直接加到现役载荷。
### US-08 发起并跟踪 15 类 AODB 请求 ### US-08 发起并跟踪 15 类 AODB 请求
**作为** 业务或运维人员,**我希望** 发起请求并跟踪生命周期,**以便** 区分登记、落信、等待、完成超时。 **目标**:区分请求登记、出站落信、等待、完成超时,不把过期应答应用到新请求
**实施拆分**:请求登记/出站补偿 → 匹配/超时 → 14 类参考应答;RQFD 快照效果由 US-06 集成验收。
**验收标准** **验收标准**
1. 支持 14 类 RQRD 参考请求和 1 类 RQFD-NONE 日计划请求 1. 覆盖 14 类 RQRD 参考请求和 1 类 RQFD-NONE;逐类名称、编码和映射见 Q8,不与 admin-api 的 21 类混算
2. REQ_TRACK 先登记COUTMSGS 落信后关联 ID 并标 SENT,落信不等于对方已发送 2. 先持久化 `REGISTERED` 与出站意图COUTMSGS 确认落信后关联 ID 并标 `SENT`,不宣称对方已发送。落信成功而 PG 未确认时可恢复,不能盲目重发
3. 逐类超时与并发规则定案:同类请求并发严格为 1(新请求注册时强制同类未决请求 `EXPIRED`);`RQFD-NONE` 超时为 60s14 类 `RQRD` 超时默认为 30s 3. 同类开放请求最多一个,新请求使旧请求 `EXPIRED`,并发登记不产生两个开放请求。默认 RQFD 60 秒、RQRD 30 秒,从确认落信的发送时间起算;`SENT/WAITING` 均不得成为永不超时的死分支
4. 响应经 US-01/US-03 入站:优先以报文回显 `SEQN``echoSeqn`)精确匹配开放请求;无回显降级为时序判定(仅接受 `DTTM >= SENT_AT` 的开放请求),并在完成时同 PG 事务标 `DONE` 4. 优先按已确认的 SEQN 回显匹配;无回显降级匹配按 Q5 明确风险,只接受已发送开放请求且 `DTTM SENT_AT`。统一转换为可比较的时间,不能把报文日期数字直接与 epoch 毫秒比较
5. 超时或被替代的请求 EXPIRED;迟到响应(`DTTM < SENT_AT`)或无匹配响应严禁更新业务状态/快照,转 SKIPPED 并审计。 5. 迟到、无匹配或已关闭请求的应答不得更新数据,转 `SKIPPED` 并审计。参考应答成功写入 REF_MASTER 后,与请求完成、处理终态和事件在 PG 边界内保持所需原子性。
6. **集成验收**SCHD-RESP 遵循 US-0614 类响应刷新 REF_MASTER,不与 admin-api 21 类混同 6. `POST /schd/sync` 复用请求入口,采用 24 小时制和非空/区间校验;响应明确已登记还是已落信,不承诺计划已更新
**依赖**US-01、US-03、COUTMSGS 适配器及逐类超时参数 **当前基础与落点**`reference/RequestCoordinator.kt``ReqTrackRepository` 和 JDBC 表已存在;当前真实出站未实现就标发送,回显匹配和超时未闭环。需补 COUTMSGS 适配器、编码、并发约束、应答路由与故障测试
### US-09 补偿回填共享信箱 **前置**US-01、US-03;Q5、Q8、出站信箱去重契约。请求基础不依赖 US-06。
**作为** 上游和运维人员,**我希望** PG 终态最终反映到共享信箱,**以便** 未处理积压语义准确。 ### US-09 持久化补偿回填信箱
**目标**:本地处理终态最终反映到共享信箱,不因共享库故障回滚已完成业务。
**验收标准** **验收标准**
1. **解耦与异步执行**:主泵 PG 事务(更新 `PROC_STATE` 终态 + 插入 `MSG_EVENT`)内写入持久化回填意图;PG 事务提交后异步触发共享信箱回填,严禁内联同步阻塞等待共享库;回填失败绝不回滚 PG 终态 1. PG 终态与回填意图同事务保存;所有终态路径都经过统一提交边界,不只覆盖成功路径。事务回滚时不得留下可执行回填意图
2. **持久化补偿**:未完成或失败的回填由后台补偿任务按指数退避重试;暴露待回填积压量与最老年龄指标,持续失败触发告警。回填 SQL 具备幂等性(`UPDATE CMINMSGS SET DATE_PROCESSED = :now, STATUS = :status ... WHERE CMINMSGS_ID = :id` 2. 提交后由后台执行回填,主泵不等待共享库;失败按持久记录退避,重启继续执行,不重新执行已完成业务
3. **各终态回填规则矩阵**(单值锁定;库方/legacy 实测前为占位,见 §7-9): 3. SUCCEEDED、规则忽略、身份重复、DEAD 均需回填处理时间;PENDING/FAILED 禁止回填。具体 STATUS 编码按 Q7 确认,内部终态不能直接当作外部字段值。
- `SUCCEEDED`**必须回填**`DATE_PROCESSED = now()`, `STATUS = 'SUCCESS'`,补齐 META 子系统列) 4. 重复补偿效果幂等,保留稳定的完成时间与审计;重放后的新处理结果不能被旧回填任务覆盖。非法报文缺 META 时也有明确回填方式
- `ignore SKIPPED`(规则忽略):**必须回填**`DATE_PROCESSED = now()`, `STATUS = 'SKIPPED'`),防止上游视作未处理积压持续重扫 5. 影子模式禁写,双跑仅一个系统持有标记写权;暴露 PG 终态、回填状态、积压、最老年龄与持续失败告警
- `duplicate SKIPPED`(身份键重复):**必须回填**`DATE_PROCESSED = now()`, `STATUS = 'DUPLICATE'`),确认去重终结。
- `DEAD`(毒丸/耗尽/MALFORMED):**必须回填**`DATE_PROCESSED = now()`, `STATUS = 'DEAD'`),避免共享信箱长期未处理告警或双跑旧系统死循环。运维人工重放基于自有 PG 驱动,不依赖信箱重置。
- 非终态(`PENDING``FAILED`):**绝对禁止回填**,保持 `DATE_PROCESSED IS NULL`
4. **影子与双跑隔离**:影子实例绝对禁止回填共享信箱;与 legacy 双跑时严格保持单系统持有标记写权。
5. 查询和日志分别展示 PG 终态与信箱回填状态。
**依赖**:US-03、共享库更新权限、共享库 STATUS 值域确认(库方/legacy 对拍) **当前基础与落点**`backfillOnSuccess` 当前仅同步写 `PROCESSED`,无持久意图。补自有 PG 意图模型/迁移、事务提交入口和独立补偿执行器;不在共享库新增补偿表
### US-10 运维重放与故障处置 **前置**:US-03 终态接口;Q7、共享库更新权限。测试覆盖四类终态、事务回滚、重复补偿和重放竞争。
**作为** 运维人员,**我希望** 查询并安全重放失败项,**以便** 修复故障而不破坏顺序。 ### US-10 安全重放与故障处置
**目标**:运维能定位失败、限定恢复范围,并了解重放对当前航班状态的影响。
**验收标准** **验收标准**
1. 可按记录、错误类时间查询 attempts、错误及 traceId 1. 按 ID、错误类时间查询次数、错误、关联事件与回填状态;重放前预览范围,记录操作者、原因和逐项结果
2.`CODEC_ERROR/UNSUPPORTED/INFRA/EXHAUSTED` 可重放;请求含 MALFORMED 时静默跳过该记录,并返回逐项结果 2.`CODEC_ERROR / UNSUPPORTED / INFRA / EXHAUSTED` 的 FAILED/DEAD 允许申请重放;MALFORMED 与其他不允许项不改状态,返回跳过原因
3.放清零 attempts/nextAttemptAt,保留错误审计,仍服从 FIFO 3. attempts/nextAttemptAt,保留身份、原始入队时间和错误审计;采用 Q6 确认的重放 deadline 策略。重新入队仍按 ID 处理,但不承诺已执行过的后续消息自动撤销
4. DEAD、持续回填失败、队列年龄越界产生告警;操作记录操作者、原因、范围和结果 4. DEAD 之后可能已有新状态,必须预检版本与覆盖风险;不安全时拒绝直接重放,改用经批准的隔离重建或恢复流程,禁止无保护的全量 `replayAll` 生产入口
5. 操作有认证、授权、范围限制与审计;死信、持续补偿失败、队列年龄越界有告警和处理 Runbook。
**依赖**US-03、鉴权与审计 **当前基础与落点**`infra/retry/ReplayService.kt` 仅按错误类批量重排并返回数量;需补按记录选择、预检、操作审计和管理入口,扩展 `ReplayServiceTest`
### US-11 归档终态入站报文 **前置**US-03、US-09 的恢复状态;Q6、OPS-1/OPS-2 的安全与可观测基础。
**作为** 平台运维人员,**我希望** 定期归档终态报文,**以便** 控制信箱规模且不丢未完成工作。 ### US-11 归档自有库终态记录
**目标**:控制自有 PG 在线表规模,不丢未完成工作、不破坏去重与恢复;不是清理共享信箱。
**验收标准** **验收标准**
1. 默认处理接收时间早于 **1 天**的 `SUCCEEDED/SKIPPED/DEAD`保留期可配置 17 天;不迁 PENDING/FAILED。 1. 默认归档接收时间早于 1 天SUCCEEDED/SKIPPED/DEAD保留期可配置 17 天;PENDING/FAILED 禁止归档。明确接收时间字段来源,不混用 UPDATED_AT 或本地入队时间
2. **共享库零建表与 DML 最小化定案**:严禁向共享 MySQL 写入 `CMINMSGS_HST`,共享库严格限定为信箱两表(CMINMSGS 读/回填,COUTMSGS 写入);共享 MySQL 自身历史清理交由库方自身 DBA 策略 2. 归档到自有 PG `PROC_STATE_HST`;关联 `MSG_EVENT` 的历史目标和保留规则一并设计。仍有未完成投递、回填或恢复依赖时,不移除所需记录
3. **归档迁入自有 PG**:在自有 PostgreSQL 设计 `PROC_STATE_HST`(及 `MSG_EVENT_HST`)承载历史归档数据 3. 迁移与删除在自有库事务内完成,重复执行幂等;失败保留源记录并报告计数。归档后同信箱 ID/业务身份再次到达,仍能按约定去重
4. 重复执行幂等并记录计数;迁移失败时 fail-closed,不删除源记录 4. 不写共享 MySQL `CMINMSGS_HST`,不清理外部信箱;原文可用性与重放保留期由 Q7/Q8 关联确认
**阶段**A **当前基础与落点**`jobs/JobExecutor.kt``PumpJobRepository` 为入口;当前迁移无归档表。先确定去重记录保留与关联策略,再补迁移及归档中断测试
### US-12 查询实时航班 **前置**US-03、US-09US-07 提供事件终态规则,US-10 提供恢复保留要求。不依赖 US-15。
**作为** 授权调用方,**我希望** 查询实时航班,**以便** 获得与 Redis 权威态一致的数据。 ### US-12 查询实时航班(KEEP
**目标**:调用方读取与当前权威状态一致的实时航班视图。
**验收标准** **验收标准**
1. KEEP `GET /all/flights`:返回 Redis 当前航班并过滤 `MAID != NULL` 的共享航班。 1. 保留 `GET /all/flights`过滤 `MAID != NULL` 的共享航班;不改写业务状态
2. 固定响应、空结果、排序、分页/大小上限和一致性时点 2. 固定响应样例、空结果、排序、大小限制及一致性时点。现役未分页时不能无声改为只返回第一页;分页或响应结构变更按 Q3 决定
3. 影子只查询影子 key;接口具备认证、限流审计。 3. 依赖异常不能伪装为空数组成功;影子只读影子状态,入口有约定的访问控制、限流审计。
**当前基础与落点**`InboxController.kt` 中仅有该接口 TODO;新增查询控制器与只读服务,复用权威读取端口,不能从空占位仓储返回成功。新增接口/状态不可用测试。
**前置**Q1、Q3;所查询的 US-05/US-06 状态发布能力。
### US-13 刷新 21 类参考主数据 ### US-13 刷新 21 类参考主数据
**作为** 业务组件,**我希望** 从 admin-api 刷新 21 类数据到 REF_MASTER,**以便** 使用可审计的本地主数据 **目标**业务使用来自 admin-api 的本地参考数据,刷新失败仍有上次可用版本
**验收标准** **验收标准**
1. 采用 ACM2-5 清单;admin-api 拉取与 US-08 的 AODB 请求是两个入口 1. 按 Q8 的 21 类清单配置端点、RTYPE/RKEY、字段映射;这是独立于 US-08 的数据入口,不另建“参考专用第二 PG”
2. `(RTYPE,RKEY)` 幂等 upsert,记录 SOURCE、刷新时间和批次审计 2. `(RTYPE,RKEY)` 幂等写 REF_MASTER,记录 SOURCE、刷新时间和批次;单类完整校验后发布,失败不暴露半批
3. 类失败不发布半批,不破坏上个可用版本;影子默认不主动刷新生产数据 3. 类失败不破坏其他类或该类旧版本;同类由 AODB/admin-api 都提供时明确覆盖优先级,全量刷新时明确已删除项的处理,不能仅靠 SOURCE 日志解决冲突
4. 影子默认不主动刷新生产数据;需要参考样本时显式导入隔离副本。
### US-14 提供机位与登机桥数据 **当前基础与落点**`reference/ReferenceService.kt` 是空刷新入口,`JdbcStaticRefRepository` 有逐条 upsert;补客户端、类型清单、批次发布/事务和失败保旧测试。
**作为** PSDT 处理逻辑,**我希望** 获得机位和登机桥映射,**以便** 正确计算 `abdg` **前置**admin-api 访问契约、Q8。可独立于消息 Handler 开发
### US-14 提供机位与登机桥映射
**目标**:PSDT 在不调用外部 HTTP 的情况下得到完整映射,正确计算 `abdg`
**验收标准** **验收标准**
1. 保留 ORMS_STAND ORMS_STAND_AIRBRIDGE,和新增 21 类分开统计 1. 保留 `ORMS_STAND / ORMS_STAND_AIRBRIDGE` 两类,与 US-13 的 21 类分开统计;适配器拉取、完整校验后原子发布只读缓存
2. 通过适配器拉取并缓存;Handler 不直接 HTTP 2. 近机位产生登机桥值,远机位或清空机位时 `abdg` 为空;一机位多桥、缺失映射与共享航班规则用 golden 固定
3. 近机位生成登机桥,远机位或清空时 `abdg` 为空;多桥规则由 golden 固定 3. admin-api 不可用时使用最后可用版本;无可用版本或映射不完整时明确失败,不用空映射冒充正常清空,也不发布半批
4. admin-api 不可用时使用最后可用版本或明确失败,不写不完整缓存。 4. Handler 输入包含所需只读参考视图,不允许其直接 HTTP 或写缓存。
## 3. 延后故事 **当前基础与落点**:在 `reference/``infra/` 增加映射服务/适配器,必要时扩展 Handler 输入上下文;PSDT 测试使用固定映射,无需在线 admin-api。
### US-15 历史航班清场(DEFERRED **前置**:机位/桥数据契约及 Q8;不要求 US-13 全部完成。
**作为** 平台运维人员,**我希望** 历史写成功后移出实时态,**以便** 控制 Redis 规模且不丢历史。 ## 4. 暂缓范围
1. 全系统统一固定基准时区为 `Asia/Shanghai`CST, UTC+8)。 ### US-15 历史航班清场(DEFERRED,阶段 B)
2. 阶段归属与 ES 边界定案:`HISTORY_SWEEP` 延后为阶段 B 能力(DEFERRED),阶段 A 永续以 Redis 作为航班动态权威,完全不接入 ES;不作为阶段 A 切流门禁。
3. 五条判史规则与 ES 写入留在阶段 B 启用前完成 100% golden 对拍;逐条隔离坏数据;仅历史写成功的 FLID 可由主泵删除。
4. 与快照保持单写者串行并记录计数。
## 4. 上线 Epic 历史存储确认成功后,才允许主泵删除对应实时航班;逐条隔离坏数据,不能删除写历史失败的集合。五条判史规则、业务时区 `Asia/Shanghai`、历史写入与删除事件之间的恢复协议需在启用前完成 golden 对拍。
阶段 A 不依赖 ES,不接通 `HISTORY_SWEEP / PROJECTION_REBUILD` 占位能力;没有历史写入能力时默认删除零条。不要因代码已有 B 枚举就启用它。
### EPIC-OPS 安全运行、影子验证与切流 ## 5. 运行与切流验收
该范围不能作为一个故事验收,拆为: | 编号 | 必须交付的能力 | 验证证据 |
1. **OPS-1 单写者与 fail-fast**:第二写实例拒启;生产缺依赖或管道未启用时失败并说明原因。
2. **OPS-2 可观测性**:健康、队列、最老年龄、投递延迟、回填滞后、DEAD 和一致性均有指标/告警。
3. **OPS-3 影子隔离**:独立 PG、Redis 前缀、topic、服务名;禁用真实出站和回填。
4. **OPS-4 切流回滚**:影子连续稳定对拍不少于 7 天;未解释业务字段差异严格为 0;DLQ 积压为 0MSG_EVENT 最老滞留 < 5s;切流后设立 48 小时观察期,24 小时内支持按 Runbook 平滑一键回滚。
依赖按实际进入上线范围的阶段 A 故事计算,不包含 US-15。
## 5. Legacy HTTP 工具面
| 端点 | 决定 | 目标口径 |
|---|---|---| |---|---|---|
| `POST /cminmsgs/send` | KEEP | US-02 | | OPS-1 单写者与启动安全 | 生产缺真实适配器、误用 stub、未启用必需管道时拒启;第二活动写者不能启动,失去写权后不得继续写;中断与停机能正确退出。 | 配置拒启、双实例/失去写权及停机测试。单靠副本数配置不算运行期保护。 |
| `POST /schd/sync` | KEEP,修正 | 24 小时制、非空/区间校验;只承诺 RQFD 落 COUTMSGS | | OPS-2 可观测与安全 | 真实依赖健康、队列/队头年龄、投递/回填滞后、DEAD 和一致性异常有指标、告警与处理入口;敏感管理操作有访问控制,日志不泄漏口令或完整敏感报文。 | 故障注入触发真实告警,消息到事件可关联;日志出口断开不阻塞业务。 |
| `GET /all/flights` | KEEP | US-12 | | OPS-3 影子隔离 | PG、Redis key、topic、服务注册身份隔离;输入只读水位或回放,禁生产回填、真实出站和误注册。Q1 若改存储,隔离方案同步修改。 | 配置与集成测试证明生产信箱、状态、topic 未被影子修改。 |
| `POST /kafka/topics/{name}/msgs` | 不进生产 | 若开发仍需,另建工具并限制 topic allowlist | | OPS-4 切流与恢复 | 对拍不少于 7 天,未解释业务字段差异为 0,DLQ 积压为 0MSG_EVENT 最老滞留 < 5 秒;切流后 48 小时观察,24 小时内具备经演练的回滚能力。 | 明确负载与统计口径的对拍报告;Runbook 含停写、排空/水位、状态恢复、写权交接和失败回退,不能只回滚程序版本。 |
| `GET /flights/migrate` | 不做 | legacy 一次性 ES 迁移工具 |
## 6. 详细文档 TODO 上述阈值沿用既有需求基线,需在真实环境提供证据,不代表当前已满足。Redis 权威方案还必须验证空态/全损恢复期间停止增量处理,以及备份和报文保留能支持的恢复范围;不承诺未经演练的“一键无损回滚”。
| 顺序 | Plane | 文档动作 | 完成条件 | ### HTTP 工具边界
|---|---|---|---|
| 1 | ACM2-16 | 固定 RESP/DNLD/ADFT 路由 | US-06、design、ACM2-6 一致,迟到/无匹配禁更快照定案 |
| 2 | ACM2-17 | 拆归档与清场并标阶段 | US-11 属 AUS-15 DEFERREDHST 禁写,自有 PG 归档定案 |
| 3 | ACM2-19 | 增补信箱回填 | 提交后执行、持久化补偿、四终态回填、影子禁写定案 |
| 4 | ACM2-21 | 消除循环依赖并补管道边界 | US-03 仅依赖 US-01,业务例外归 US-05 |
| 5 | ACM2-15 | 增补 ignoreMsg | 规则、终态、回填和拼写确定 |
| 6 | ACM2-18 | 补查询、机位、21 类数据 | US-1214 与 US-08 分界明确 |
| 7 | ACM2-20 | 声明 HTTP 工具去留 | 五端点均有决定 |
| 8 | ACM2-22 | 吸收评审剩余项 | 水位、依赖、Broker、deadline 一致 |
| 9 | — | 同步 architecture/design/README | 权威口径、索引与阶段表一致 |
## 7. 开放问题定案结论汇总 | 端点 | 范围 |
|---|---|
| `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 一次性迁移工具。 |
1. **【已定案·ACM2-17】归档存储目标**:共享库 CMINMSGS_HST 严禁写入,共享库严格限定信箱两表;归档目标确认为自有 PG `PROC_STATE_HST` ## 6. 编码前必须处理的决策与契约
2. **【已定案·ACM2-21】SEQN 重置与时钟锚点**:生产默认保持 `include-day-boundary=false`HOL deadline 锚点固化为 `PROC_STATE.CREATED_AT`(稳定入队时间戳)。
3. **【已定案·ACM2-20/25】compat 接口契约**:支持 text/xml、application/xml 与 text/plain;逐字兼容 legacy `ResponseDto`(成功 `is_success`+`body`;失败 `is_success`+`err_code`+`err_msg`,不用 `msg`);生产保持内网信任姿态 这些是**阻塞相应实现的具体问题**,不是已完成的验收项。保留原有目标值,但不把矛盾或外部未确认内容写成事实
4. **【已定案·ACM2-21】航班不存在与 FDEL**:航班不存在按 KEEP 正常结束且回填信箱;FDEL 共享航班更新主航班 MAFL 并通知,主航班删除清空关联。
5. **【已定案·ACM2-16/18】15 类请求生命周期**:同类并发严格为 1;RQFD 超时 60sRQRD 超时 30s;优先 SEQN 回显,无回显退化 DTTM 时序判定;迟到/无匹配禁更快照。 | 编号 | 问题与当前口径 | 解除阻塞的产物 |
6. **【已定案·ACM2-22/23】Kafka 生产契约**:强制 `acks=all``idempotence=true`,分区键按 FLIDschd)/SNDR(msg)固化;严禁非幂等降级;现网 Broker API 版本未确认前 US-07 不得标实施完成;README 不提供生产降级 env。 |---|---|---|
7. **【已定案·ACM2-17】时区与判史边界**:统一 `Asia/Shanghai` 时区;HISTORY_SWEEP 延后至阶段 B,阶段 A 不依赖 ES,Redis 永续动态权威。 | Q1 权威存储 | 当前基线是 RedisFLIGHT_STATE 提前到 PG 只是提案。影响 US-03/05/06/12、迁移和健康检查。 | 明确选择及批准记录,同步三份主文档后再固定事务、快照协议;未定案可先做无关接口和调度测试。 |
8. **【已定案·ACM2-22】对拍与回滚阈值**:影子对拍至少 7 天;业务差异 0 容忍;DLQ 积压为 0;切流后 48 小时保驾、24 小时可平滑回滚。 | Q2 入队顺序 | 水位+补扫无法自动保证较小 ID 迟提交不越序;当前有限批扫描也可能被未回填记录挡住。 | 库方 ID/提交顺序约束,或明确的发现完整性与暂停/恢复协议;晚提交、空洞、兼容入口与重扫联合测试。不能凭空假定 ID 连续。 |
9. **【已定案·ACM2-26】信箱回填 STATUS 值域**`SUCCEEDED``SUCCESS`ignore `SKIPPED``SKIPPED`duplicate `SKIPPED``DUPLICATE``DEAD``DEAD`(库方/legacy 实测前为占位;若库方禁新值则仅用 legacy 已用集合)。 | Q3 HTTP 契约 | 目标 ResponseDto 与现有 text/plain ID 不同;请求媒体类型目标已列出,错误码、状态码、查询格式等仍需对拍。 | 每个保留接口的真实请求/响应样例、错误表和契约测试;10MB 的字节口径、字符集及兼容变更说明一起固定。 |
| Q4 Kafka wire | schd 当前多 FLID 聚为一个数组/一条 record,与“record 按 FLID 分区”冲突;msg 的 SNDR 尚未接线。 | 下游确认发送粒度、key、去重标识放置、分区内顺序及批次确认策略;同步 Dispatcher/设计,不擅自把现役数组改成逐航班消息。 |
| Q5 请求匹配 | 目标优先 SEQN 回显,但回显是否可靠需确认;DTTM 降级存在跨代误匹配,尤其旧应答到达新请求期间。 | 15 类请求/响应样例、回显字段与时间格式;降级风险是否接受及拒绝条件。默认超时仍为 RQFD 60 秒、RQRD 30 秒。 |
| Q6 deadline 与重放 | 入队时间作为稳定锚点会把长期排队消息计入滞留;历史重放保留 CREATED_AT 后可能立即过期。 | 明确首次处理/排队过期策略与“本次恢复尝试”计时方式,保留原始时间审计;测试积压恢复和旧 DEAD 重放,不用刷新 UPDATED_AT 绕过超时。 |
| Q7 信箱外部契约 | 原目标为成功→SUCCESS、忽略→SKIPPED、重复→DUPLICATE、死信→DEAD;当前 JDBC 写 PROCESSED。新 STATUS 值尚不能假定库方支持。 | 库方认可的状态值、META/缺失字段、权限、原文保留期、出站去重与处理时间语义;若只允许 legacy 集合,显式映射内部原因,不新增外部枚举。 |
| Q8 业务覆盖清单 | “29 FLOP、14 RQRD、21 参考类、2 机位类”只是数量,不能直接当字段规范。 | 将 SIS/XSD、现役 KEEP/FIX 基线整理为逐类矩阵与脱敏样例,列明路由、字段、缺失/清空、通知、数据来源优先级、多桥规则及测试文件。资料不全的类型不标完成。 |
业务日期/日计划采用 `Asia/Shanghai`;持久化与比较使用明确的时间类型和转换规则,不靠服务器默认时区,也不直接比较不同单位的数字。
## 7. 每个实施 PR 的完成条件
1. 写明所覆盖的 `US-xx/验收条目`、未包含的子范围及相关 Q 项结论,不用“管道已接通”代替全部验收。
2. 列出涉及模块、配置、PG 迁移、外部契约和恢复影响;源码现存的旧注释或空适配器不是正确性依据。
3. 提交对应正常/失败/重复/中断测试;顺序、身份、快照和投递变更必须有不变量回归,PG 事务必须有真实数据库集成测试。
4. 用 JDK 25 执行 `./gradlew test`;受限环境将 `GRADLE_USER_HOME``TMPDIR` 指向可写目录。测试受阻时记录原因,不写“全绿”。
5. 真实适配器未完成、golden 未覆盖或契约仍有阻塞时,不把整项故事标完成;上线另需 OPS 验证和发布证据。
历史文档整改中的勾选不代表业务已实现,也不替代本清单。后续进度放在实施任务与测试证据中,本文保持需求和验收口径稳定。