diff --git a/README.md b/README.md index 9a2287b..02a0028 100644 --- a/README.md +++ b/README.md @@ -167,8 +167,9 @@ MICRONAUT_ENVIRONMENTS=dev ./gradlew run # dev stub 冒烟:内存 stub,无 核心流程语义(流程 1 主路径/compat 分述)、失败/重试/重放、不变量落点、参数表、已知缺口。 - [docs/user-stories.md](docs/user-stories.md):阶段 A US-01~US-14、延后清场 US-15、上线 Epic、 legacy HTTP 去留及逐项文档 TODO;包含验收标准、依赖、实现差距与待确认问题。 -- [docs/user-stories-todo.md](docs/user-stories-todo.md):ACM2-15~22 对应的文档整改清单、 - 已完成落点、待 Plane/产品确认事项与验证门禁。 + (原 ACM2-15~22 文档整改清单 user-stories-todo.md 已收敛完成并移除,跟踪记录见 Plane。) +- [docs/decision-flight-state.md](docs/decision-flight-state.md):决策提案——运营航班表 + FLIGHT_STATE 是否提前落自有 PG(阶段 A 权威化);讨论 issue ACM2-28,定案前不实施。 - [docs/legacy/](docs/legacy/):外部参考/基线材料(自 legacy 仓库拷贝,非本系统文档)—— `msgexchange-api-legacy-user-stories.md`(legacy 行为对拍基线,ACMA-4)、`unisysaodbsis.xsd`、 `SIS_AODB_RMS-V0.1.md`(消息结构唯一事实源;CIIMS 中间件交换模型)。 diff --git a/docs/architecture.md b/docs/architecture.md index 9032744..a446b9a 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,251 +1,151 @@ # msgexchange-v2 架构文档 -> **系统定位**:机场 OMMS **上游报文处理中间件**,消费 CIIMS/AODB 等上游写入共享 MySQL 信箱的 XML 报文,经严格 FIFO 管道解析决策后维护 Redis 航班动态权威,并向下游(Kafka / 出站信箱 / 查询接口)投递;非报文源系统,不替代 CIIMS 或 AODB。 -> **架构基准**:总体基线遵循 Plane ACM2-3(综合架构 v4),存储边界与事务模型以 ACM2-12(自有 PostgreSQL + 共享 MySQL 信箱 + Redis 动态/快照 gen;阶段 B 缓做)为准。模块级实现与交互细节参见配套 [design.md](design.md)。 -## 1. 系统定位 +## 1. 系统定位与范围 -**机场 OMMS 上游报文处理中间件**:消费 CIIMS/AODB 等上游写入共享信箱的 XML 报文, -经严格 FIFO 管道解析、决策、维护航班动态权威态,并向下游(Kafka / 出站信箱 / 查询接口) -投递;**非**报文源系统,**不**替代 CIIMS 或 AODB。替换 legacy `msgexchange-api` -(Java 8 / Spring Boot 1.5 / Maven)。过渡策略为**双跑三步**: +msgexchange-v2 是机场 OMMS 的上游报文处理中间件,用于替换旧版 `msgexchange-api`。 +它读取 CIIMS、AODB 等系统写入共享 MySQL 信箱的 XML 报文,按顺序更新航班动态,再将结果提供给下游。 -``` -影子对拍(共享库水位/回放 + 自有 PG 独立 schema 比对)→ 切流(nextgen 权威)→ 旧仓库冻结 +本系统负责**收报、解析、状态更新和结果投递**,不生成上游业务报文,不替代 CIIMS/AODB,也不提供 AODB 主数据编辑能力。 + +- **主要入口**:轮询共享 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 + ▼ +共享 MySQL:CMINMSGS + │ 轮询未处理记录 + ▼ +┌──────────────── msgexchange-v2(单实例)────────────────┐ +│ ingress:发现报文 → PostgreSQL 持久化入队 │ +│ │ │ +│ processing:取 FIFO 队头 → 解析 / 去重 → Handler 决策 │ +│ ├─ 更新 Redis 航班动态 / 快照 │ +│ └─ PG 事务:处理结果 + 待发事件 │ +│ │ +│ jobs:在主泵空闲或消息退避窗口内执行维护作业 │ +│ delivery:读取 PG 待发事件 → 投递 / 重试 │ +└─────────────────────────┬──────────────────────────────┘ + ├─ Kafka:msg / schd + └─ 共享 MySQL:COUTMSGS + +处理结果提交后,再回填 CMINMSGS 的处理标记;失败需补偿。 +查询接口读取航班动态,不参与状态写入。 ``` -- legacy 维护不受本仓库影响;本仓库不声明 legacy 旧表 schema,**且不在共享 MySQL 建任何表** - (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 ⑤)。 +收报、处理和投递各使用一条专用线程,不占用 HTTP 事件循环。**只有主泵可以写航班动态及快照版本**,维护作业也必须遵守这一规则。 -### 1.1 上下游边界(中间件职责) +采用 Kotlin + JDK 25、Micronaut 编译期依赖注入和 JDBC 持久化。数据库变更由 Flyway 管理,但只作用于自有 PostgreSQL。具体依赖版本以 `build.gradle.kts` 为准,不在架构文档重复维护。 -| 方向 | 角色 | 本系统做什么 | 本系统**不**做什么 | -|---|---|---|---| -| 入站(主路径) | CIIMS / AODB 等上游 | JDBC 轮询共享 MySQL `CMINMSGS` 发现新信 → 自有 PG 入队 → 解析处理 | 不生成原始业务报文;不替代 CIIMS 落信 | -| 入站(compat) | 手工工具 / 对拍 | HTTP `POST /cminmsgs/send` 写信箱 + PG 入队 | 非生产主拓扑 | -| 处理 | 本系统 | 维护 Redis 航班动态权威态;Handler 纯函数决策 | 不持有 AODB 主数据编辑权 | -| 出站 | 下游消费者 | Kafka(msg/schd)、共享 MySQL `COUTMSGS`、查询 HTTP | 不直接推送至前端(经 Kafka 等中转) | +## 3. 模块职责 -与 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 25(Micronaut 5.1 依赖基线要求) | -| 框架 | Micronaut platform BOM **5.1.3**(core 系实际解析 **5.1.13**,版本重钉属 U02/U05) | 编译期 DI:KSP(`kotlin-ksp` + `micronaut-inject-kotlin` **5.1.3**)生成 `*$Definition` | -| 持久化 | **自有 PostgreSQL**(全部内部状态)+ 共享 MySQL 信箱 | 自有库:管道状态 PROC_STATE/MSG_EVENT + 任务调度 PUMP_JOB/REQ_TRACK + 21 类静态 REF_MASTER(Flyway 迁移 `db/migration`);共享库严格保持 CMINMSGS/COUTMSGS 最小 DML 契约。JDBC 仓储与 InboxPoller 已有初版,事务/补偿/出站适配属 U05(ACM2-12) | -| 权威存储 | Redis(航班动态 flightInfo + 快照 gen) | 仅主泵单线程写入(I5);Lua 脚本执行原子状态覆盖与代际版本推进(gen 协议重设计属 U09) | -| 投递 | Kafka(acks=all + 幂等生产;ACM2-23) | transactional outbox 模式,经自有库 MSG_EVENT 表中转 | -| 投影(阶段 B) | Elasticsearch(历史) | **阶段 B 缓做(ACM2-12)**:FLIGHT_STATE 不落关系表,Redis 保持动态权威;历史投影链路待阶段 B 重启评估 | -| 注册中心 | Eureka(Micronaut 原生注册) | 服务名契约 `msgexchangeapi`(影子实例 `msgexchangeapi-shadow`)——当前运行时仍取 `micronaut.application.name`,配置映射待 U17 完善 | +| 自有 PostgreSQL | `PROC_STATE` 处理状态、`MSG_EVENT` 待发事件、`PUMP_JOB` 作业、`REQ_TRACK` 请求跟踪、`REF_MASTER` 静态数据,以及 `PROC_STATE_HST` 归档 | 本系统的内部持久化状态;唯一的本地事务边界。 | +| 共享 MySQL | `CMINMSGS` 入站信箱、`COUTMSGS` 出站信箱 | 外部系统所有。仅执行约定的信箱读写和处理标记回填,不建表、不迁移 schema、不写历史表。兼容 HTTP 入口可按既有契约写入入站信箱。 | +| Redis | 航班动态 `flightInfo` 和快照版本 `gen` | 阶段 A 的动态权威存储,不是可随意清空的缓存。 | -## 3. 总体拓扑 +**不使用跨库事务。** PG 事务只能保证“处理结果与待发事件一起提交”,不能覆盖 Redis 更新、MySQL 回填或 Kafka 发送。跨存储依靠幂等、重试和持久化补偿恢复: -``` - CIIMS/上游 ──外部写(他人系统)──▶ 共享 MySQL CMINMSGS(信箱) - │ - │ JDBC 轮询/重扫(① 发现 DATE_PROCESSED IS NULL 新信) - ▼ - ┌──────────────────────────────────────────────────┐ - │ msgexchange-nextgen │ - │ (单实例 · 单写者) │ - │ ingress(InboxPoller + 补偿重扫,U05) │ - │ └──② 自有 PG PROC_STATE(PENDING) │ - │ (跨库非同事务;② 失败→① 重扫补建) │ - │ (compat)POST /cminmsgs/send ──▶ 信箱 insert │ - │ + PG 入队(现役 HTTP 写路径,U16 对拍) │ - │ │ - │ processing(msgx-pump 线程,严格 FIFO 队头) │ - │ Pump ──tick──▶ MessageProcessor │ - │ │ │ decode(XmlCodec) │ - │ │ │ identity 绑定(I3) │ - │ │ │ Handler.decide(纯函数) │ - │ │─Schd RESP/DNLD▶ SnapshotFlow(流程4) │ - │ │─PUMP_JOB───▶ JobExecutor(作业窗口,决策1) │ - │ │ │ - │ ├────Redis Lua──▶ Redis flightInfo(A权威) │ - │ ├──自有 PG 事务2──▶ MSG_EVENT + SUCCEEDED │ - │ └──共享 MySQL 回填 DATE_PROCESSED(外部副作用)│ - │ │ - │ delivery(msgx-dispatcher 线程,每 target FIFO) │ - │ Dispatcher ──逐条──▶ Kafka(msg) │ - │ └─flushSchd 聚合─▶ Kafka(schd) │ - │ (阶段 B 追加:ES flight_hts → Redis 投影删除) │ - └──────────────────────────────────────────────────┘ - 共享 MySQL(信箱)◀──① 轮询发现 / 回填──▶ 自有 PostgreSQL ◀──② 管道状态 - Redis(动态+gen)◀── Lua 写 ── processing - │ │ - ▼ ▼ - 下游 Kafka topic Eureka / logstash -``` +| 中断位置 | 恢复要求 | +|---|---| +| 信箱已有报文,PG 入队失败 | 重扫补建,并按信箱 ID 去重。 | +| Redis 更新成功,PG 提交失败 | 消息重试可能再次更新 Redis;更新与快照协议必须支持幂等重放和版本校验。 | +| PG 已提交,信箱回填失败 | 持久化记录补偿任务并重试回填,不能重新执行已完成的业务处理。 | +| 下游已接收,本地尚未标记发送成功 | 允许重发;下游或出站适配协议必须具备去重能力。 | -要点: +对外投递按**至少一次**设计,不承诺端到端恰好一次。Kafka 生产者幂等不能消除应用重启或 outbox 重发带来的所有重复。Redis 恢复也不能仅依赖 PG 处理状态:备份、报文保留及回放范围需要在上线前验证。 -- **收报主路径(与现役/SIS 一致)**:上游经 CIIMS 等**外部系统**写入共享 MySQL `CMINMSGS` - (本系统不建表);`ingress` 经 **JDBC 轮询**(`DATE_PROCESSED IS NULL`,1s 节律,与 - legacy `MsgExchangeRunner` 同口径)发现新信 → 自有 PG 建 `PROC_STATE(PENDING)`。 - PG 入队失败时以共享库水位**重扫补建**(U05)。`POST /cminmsgs/send` 为现役 HTTP - **写**路径(手工/对拍),非上游报文到达的主拓扑。 +## 7. 关键决策索引 -- **三条专用 daemon 单线程**(`msgx-inbox-poller` / `msgx-pump` / `msgx-dispatcher`)由 `PipelineLifecycle` - 在 `ServerStartupEvent` 后拉起,不占用 Netty event loop;停机 `requestStop` + - interrupt + join(U07)。仅当 `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,信箱交互为外部读/写 + 最终一致。 +保留 D1–D12 编号,便于设计文档和工程历史引用;以下是决策摘要,而非完成清单。 -## 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 | 主要类 | -|---|---|---|---| -| `ingress/` | 收报:JDBC 轮询共享信箱发现新信 → 自有 PG 建 PENDING(+ 补偿重扫;HTTP 写路径 compat);不解析报文 | 流程 1,I3 | `InboxPoller`(U05)`InboxController` `InboxService` | -| `processing/` | 主泵:FIFO 领取、ignoreMsg、identity 绑定、纯函数决策、RESP/DNLD 快照、自有 PG 事务2 | 流程 2/4,I1/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 开关 | I1–I5 | `ProcState` `MsgEvent` `Decision` `MsgKind` | -| `infra/` | 仓储接口、重试策略、Redis Lua、stub、健康、日志 | 数据模型节 | 见 design.md | -| `config/` | `PipelineProps` 参数表(ACMA-8 参数初值) | — | `PipelineProps` | +## 8. 部署、切换与运维 -## 5. 关键架构决策 +**部署与安全** -> 决策编号保持与 [design.md](design.md) 及工程历史引用一致;按【定案】与【设计依据与权衡】两段式规范表述。 +- 生产维持单活动实例,停机时停止接收新任务并等待工作线程退出。运行期排他保护尚未完成,当前不能依靠程序自动阻止双实例写入。 +- 配置、口令和环境端点通过环境变量提供。兼容写接口沿用内网信任模式,缺少鉴权,必须限制网络访问;管理端点不得直接暴露到生产外网。 +- Eureka 用于服务发现,Logstash 接收结构化日志;日志出口故障不应阻塞业务处理。 -### D1 — 业务报文严格 FIFO 与维护作业窗口化调度 +**替换旧系统** -- **定案**:业务报文按最小未完成 `CMINMSGS_ID` 严格保序、逐条执行;清场/归档/投影重建等维护作业(`PUMP_JOB`)单独排入持久化作业队列表,严禁与业务报文混排。作业仅在消息队列为空、或队头报文处于重试退避等待窗口且作业可在窗口期内完成时触发。 -- **设计依据与权衡**:报文到达与处理时序直接决定航班生命周期状态机的权威正确性(如“计划变更”与“航班取消”时序颠倒将导致严重脏数据),业务 FIFO 为最高优先级不变式(I1)。维护作业属于低频异步运维任务,若与业务报文抢占同一调度队列将引入队头阻塞与事务锁竞争;采用窗口化插针调度,既确保业务报文零干扰,又实现后台任务免停机自适应推进。 -- **落地与边界**:`Pump.tick`、`JobExecutor`;作业饥饿防护与精确退避窗口由 U15 固化。 +采用“影子对拍 → 切流 → 旧系统冻结”。共享信箱不能让新旧系统同时认领和回填;影子输入使用只读水位或回放。影子环境须隔离 PG schema/实例、Redis key 空间、Kafka topic 和服务注册身份,并禁止误写生产信箱。切流时保证只有一个权威写者。 -### D2 — 阶段 B 历史投递与投影清理同步编排(缓做) +**可观测性要求** -- **定案**:历史航班写入 ES 成功后,由同一消费线程同步向自有库 `MSG_EVENT` 写入“删除 Redis 投影”事件,不采用跨系统异步轮询或分布式两阶段提交。本项归属阶段 B(ACM2-12 缓做),待后续阶段评估重启。 -- **设计依据与权衡**:利用同线程同步顺序调用保障“ES 写入成功”与“删除事件就绪”的因果强一致性,消除外部索引已更新但缓存清理事件悬挂丢失的竞态窗口;避免引入额外的分布式协调器与待确认补偿表,控制架构复杂度。 -- **落地与边界**:`Dispatcher.tick`(定案 2;阶段 B 重启后实装)。 +使用消息 ID、事件 ID 关联处理与投递日志;健康检查反映依赖实际可用性,而不只是进程存活。运行中重点关注队列积压、队头滞留时间、投递延迟、重试/DEAD 数量和回填补偿积压。死信和一致性异常需要可执行的告警与重放流程,不能只留一条错误日志。 -### D3 — 调度快照(schd)批量聚合与最新态压缩投递 +## 9. 当前实现与上线门槛 -- **定案**:调度快照类报文(`KAFKA_SCHD`)严禁进入逐条投递链路,在逐条轮询中显式排除;出站唯一路径为 `flushSchd` 定时与批阈值触发的批量聚合流程:按航班唯一标识(`FLID`)分组去重,仅提取组内最新一条(`max(EVENT_ID)`)快照聚合为批,一次性投递至 Kafka。 -- **设计依据与权衡**:调度类报文存在高频状态刷新特征,逐条下发会导致 Kafka 主题与下游消费者遭遇瞬态数据风暴,且会无谓广播已被新快照覆盖的历史过期状态(现役系统缺陷);聚合去重确保下游获取确定性的全量最新切面,同时大幅削减网络 I/O 与 Kafka 吞吐压力。 -- **落地与边界**:`Dispatcher.tick`、`SchdAggregation`(U06/N03)。 +当前已有管道骨架、重试机制、部分 JDBC 适配和开发环境 stub 冒烟能力,**不能据此认定生产链路已闭环**。默认配置关闭管道自动启动及真实数据库/信箱适配。 -### D4 — 未实装报文可重放机制(FAILED-UNSUPPORTED) +上线前至少需要完成并验证: -- **定案**:处理管道遇到尚未实装 Handler 的报文类型或未就绪的快照 Staging 时,统一标记为 `FAILED(UNSUPPORTED)` 并进入指数退避,严禁写入不可逆终态(如 DEAD 或伪成功)。 -- **设计依据与权衡**:支持业务协议分阶段平滑演进与上线。报文协议 Handler 翻译分批交付,过渡期提前接入的未支持报文必须保持可重放状态,待新版本 Handler 发布后通过 `ReplayService` 批量重放激活,杜绝因协议尚未覆盖而造成数据永久丢弃。 -- **落地与边界**:`MessageProcessor`、`SnapshotFlow`、`ReplayService`(U10/N21)。 +- 真实 PG 事务、信箱水位与补扫、回填补偿、出站信箱,以及所需业务 Handler。 +- Redis 更新幂等性、快照版本校验、故障中断恢复与权威数据恢复方案。 +- FIFO、身份去重、作业窗口和投递故障下的回归测试。 +- 生产启动校验、单实例排他保护、影子隔离和 Kafka 配置约束;当前配置仍允许 Kafka 参数覆盖,且默认 in-flight 值与 D11 要求不同。 +- 死信告警、人工重放、端到端追踪、积压指标及安全边界。 -### D5 — 异常分级收敛与 JVM 致命故障快速失败 - -- **定案**:报文级业务与编解码异常(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 审计)。 -- **共享 MySQL(cdairport,他人系统库)——本系统不建任何表/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)。 -- **阶段 B(FLIGHT_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 启动校验)、U09(Redis 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+sensitive;dev/影子经 - 顶层 `endpoints.*`(**非** `micronaut.endpoints.*`——实测前缀错误时不生效)放开 `/env`、`/beans` - 与 health 明细。工程未引入 micronaut-security,sensitive 的实际拦截行为待 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 日志为落点。 +具体进度由配套设计(design.md §9 已知缺口表)与 Plane ACM2-10 实施计划维护,本文不记录测试数量、临时补丁版本或逐项工单进展。架构基线沿用 ACM2-3,存储与事务边界以 ACM2-12 的修订为准。 diff --git a/docs/decision-flight-state.md b/docs/decision-flight-state.md new file mode 100644 index 0000000..6a6a614 --- /dev/null +++ b/docs/decision-flight-state.md @@ -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 | U09(gen Redis 内 CAS 协议重设计)存在的根因就是「权威在 Redis、终态在 PG」的跨存储窗口——ACMA-8 v4 原设计 gen 本在 DB,ACM2-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 快照 + replace;SnapshotFlow 的 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. 影子对拍跨存储 diff(nextgen 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 口径修订说明。 diff --git a/docs/design.md b/docs/design.md index 80f5600..a106f65 100644 --- a/docs/design.md +++ b/docs/design.md @@ -1,288 +1,228 @@ # msgexchange-v2 设计文档 -> **系统角色**:机场 OMMS **上游报文处理中间件**——消费 CIIMS/AODB 等上游经共享 MySQL 信箱 -> (`CMINMSGS`)投递的 XML 报文,解析处理后维护 Redis 动态并向 Kafka / 出站信箱投递; -> **非**报文源系统。生产主路径 = **JDBC 轮询**发现新信;HTTP `POST /cminmsgs/send` = compat 写路径。 -> 本文对应仓库当前实现,给出模块级设计语义与依据;架构总览见 -> [architecture.md](architecture.md),架构基线为 Plane **ACM2-3**,其存储/事务边界由后续 -> **ACM2-12** 覆盖;实施计划与逐项验收为 -> **ACM2-10(U01–U30)**。文中标注「TODO/未实装」的条目均为已知开放项,不属文档遗漏。 +## 1. 阅读说明 -## 0. 系统边界速览 +本文说明模块如何协作、状态如何流转,以及失败后如何恢复。系统范围、存储归属和部署约束见 [architecture.md](architecture.md),不在这里重复。 -``` -上游(CIIMS/AODB…) ──外部写──▶ 共享 MySQL CMINMSGS(DATE_PROCESSED IS NULL) - │ JDBC 轮询/重扫(InboxPoller,U05) - ▼ - 自有 PG PROC_STATE(PENDING) ──▶ 主泵 FIFO 处理 - │ - ┌───────────────┼───────────────┐ - ▼ ▼ ▼ - Redis 动态 Kafka msg/schd COUTMSGS 出站 - (阶段 A 权威) (下游订阅) (他人读取发送) +以下流程是阶段 A 的目标设计,不是实现完成清单。当前代码仍有占位和过渡实现,与设计的主要差异集中在第 10 节。阶段 B 的历史投影和清场暂不启用,也不改变 Redis 作为航班动态权威存储的定位。 -(compat)POST /cminmsgs/send ──▶ insertRaw + PG 入队(手工/对拍,非主拓扑) -``` +## 2. 数据与领域模型 -- **中间件定位**:本系统位于 CIIMS 与下游消费者之间,负责**采集 → 解析 → 决策 → 投递**; - 报文原文由上游写入共享信箱,本系统只读(主路径)或 compat 写(辅助)。 -- **与 legacy 对齐**:legacy `MsgExchangeRunner` 同样以 1s 轮询 `CMINMSGS` 为处理入口; - legacy HTTP 收报接口在 nextgen 中保留为 compat,不改变生产主拓扑。 +### 2.1 持久化记录 -## 1. 领域模型 +所有内部表都属于自有 PostgreSQL;共享 MySQL 只保留约定的信箱读写边界。 -### 1.1 状态机与错误分类 - -``` -ProcStatus(PROC_STATE.STATE,消息处理侧): - PENDING ──处理成功──▶ SUCCEEDED(终态;回填共享库 CMINMSGS 为外部副作用,最终一致) - │ ──同 identity 已绑定──▶ SKIPPED(终态,lastError=duplicate-of:) - └──失败──▶ FAILED(非终态,attempts+1 + nextAttemptAt 退避) - │ attempts ≥ maxAttempts 或 队头滞留超 head-deadline - ▼ - DEAD(终态/DLQ,ERROR_CLASS=EXHAUSTED 规范化) - -EventStatus(MSG_EVENT.STATE,投递侧): - PENDING ──▶ SENT;失败退避回 PENDING;attempts 耗尽整批/单条 → 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=NULL,errorClass/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)`=队头 | -| MSG_EVENT | 发消息侧:统一投递 outbox | `EVENT_ID` 自增=全序;`idx_evt_head(TARGET, STATE, EVENT_ID)`=每 target 队头 | -| PUMP_JOB | 泵作业调度(作业不插队,队头空闲/退避窗口执行) | kind:ARCHIVE/HISTORY_SWEEP/PROJECTION_REBUILD | -| REQ_TRACK | 15 类请求状态机 | REGISTERED/SENT/WAITING/DONE/EXPIRED;`COUTMSGS_ID BIGINT`(U18 修正) | -| REF_MASTER | 21 类静态主数据 | `(RTYPE,RKEY)` PK;SOURCE=ADMINAPI/AODB/PIPELINE;REFRESHED_AT | +| `PROC_STATE` | 入站消息的处理状态、身份、重试次数和错误原因 | `CMINMSGS_ID` 主键防止重复入队;`IDENTITY_KEY` 唯一约束防止业务重复;按最小未完成消息 ID 取队头。 | +| `MSG_EVENT` | 等待投递的事件(outbox) | `EVENT_ID` 决定投递顺序;`TARGET` 区分目标;`PARTITION_KEY` 在 `schd` 中为 `FLID`。 | +| `PUMP_JOB` | 持久化维护作业 | 状态为 `QUEUED / RUNNING / DONE / FAILED`;不与业务消息共用排序序号。 | +| `REQ_TRACK` | 上游请求及应答关联 | 保存请求类型、参数、出站信箱 ID、发送和完成时间;同类只允许一个开放请求。 | +| `REF_MASTER` | 静态参考数据 | `(RTYPE, RKEY)` 唯一,`SOURCE` 记录数据来源。 | +| `PROC_STATE_HST` | 终态处理记录的归档目标 | 属于目标设计,当前迁移尚未建表;不得改写为共享库历史表。 | -已知 DDL 缺口(U18,随本库 PG 化修正/收窄):REQ_TRACK.COUTMSGS_ID 已按 BIGINT; -时间列须 DATETIME(6)/显式 UTC 口径在 U05 数据层实现时定;FLIGHT_STATE 因缓做不在本库。 +字段与索引定义以 `src/main/resources/db/migration/` 为准。报文原文仍从共享信箱读取,因此必须协调原文保留期,不能在消息尚需处理或重放时提前清理。 -**存储边界(ACM2-12 定案)**: -- **自有 PostgreSQL** = 上表全部(消息管道 + 调度 + 请求 + 21 类)。本地事务只在此库: - 处理侧「MSG_EVENT 插入 + PROC_STATE→SUCCEEDED」同事务;其余跨存储一律外部副作用。 -- **共享 MySQL(cdairport,他人系统)仅信箱 DML、不建表**:上游外部写 CMINMSGS; - 本系统 JDBC 轮询读 + 处理回填;出站写 COUTMSGS(他人读取发送)。见 §3.1/§3.2 的事务模型。 -- **Redis**:航班动态 flightInfo + 快照 **SCHD_GEN(gen)**——Lua 内原子「覆盖+按代差删+ - 版本推进」;重放幂等由 Lua 承接(协议重设计属 U09),`RefDataRepository` 为目标实现的 - 过渡占位接口。 -- **FLIGHT_STATE(阶段 B 权威):缓做不落表**(Redis 永续动态权威)。 -- 实现状态:迁移 SQL 已按 PG 落地(V1.0.0);Repository 接口归属注释已对正(自有 PG / - 信箱封装 / gen→Redis 占位 / FlightState 缓做);Micronaut Data 实装与信箱适配层 - (CminmsgMailbox/OutboxMailbox)属 U05 批次。 +Redis 保存 `flightInfo` 与快照代际元数据 `gen`。`gen` 记录快照所属日期、版本及该代航班集合,不属于静态参考数据。 -## 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` -(`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 必有 now−updatedAt≤60s<10m,**滞留超时分支实际 - 不可达**,仅 attempts 维度生效;锚点语义随 U05/U13 定案并补测试(Pump 未注入 Clock,主泵级 - 门禁无单测锁定,见 §7);否则 sleep 至 nextAttemptAt。 -4. 正常队头 → `MessageProcessor.processOne`: - 入口守卫(FAILED 且已 exhausted → DEAD)→ `rawOf` 缺失 → DEAD(MALFORMED) → - decode(MALFORMED→DEAD / CODEC_ERROR→FAILED)→ ignoreMsg 匹配(LDM/REGN/RSTA/EROR, - 命中→SKIPPED,回填遵循 US-09;当前未实装)→ identity 首绑 - (`tryBindIdentity` 失败 → SKIPPED,I3)→ Schd RESP/DNLD → `SnapshotFlow`(ACM2-16 定案: - DNLD 与 RESP 均走 SnapshotFlow;RESP 成功后在同事务完成匹配开放 RQFD 的 `REQ_TRACK→DONE`; - 迟到或无匹配 RESP 严禁更新快照,直接转 SKIPPED 并审计)→ - 其余 → `Handler.decide(redis.hgetAllFlightInfo(), msg)` → - 阶段 A:Redis 先写(I2 happens-before,TODO 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 严格 FIFO(I1 双层同策略):`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/D2,ACM2-12 缓做):ES 投递成功 → 同线程同步 `insertSync` 删除事件 - (`deleteOf`:Jackson 结构化序列化,refs 可空恒合法 JSON——U14)。当前不启用。 - -### 3.4 流程 4:日计划快照(`SnapshotFlow`,RESP/DNLD) +业务身份统一由 `Identity.of` 生成: +```text +SNDR | TYPE | STYP | SEQN ``` -SCHD-RESP / SCHD-DNLD - → staging(流式解析+整包校验,TODO 阶段2;未实装→FAILED(UNSUPPORTED)) - → 守卫判定:若为 SCHD-RESP,检查开放 RQFD(dttm < sentAt 或无匹配/已过期 → 严禁更新快照,转 SKIPPED 并审计) - → Redis Lua SNAPSHOT_REPLACE(同一 hash 原子「覆盖新代+按代差删」,删除集=旧代flids−新代) - → gen 版本推进(ACM2-12:gen 随 flightInfo 同在 Redis,Lua 内原子版本 CAS) - → 自有 PG 本地事务(原子性):PROC_STATE→SUCCEEDED + (RESP 匹配时)REQ_TRACK→DONE + MSG_EVENT 插入 - → PG 提交后异步触发信箱回填(持久化补偿,ACM2-19) -数据流说明:SCHD-RESP 处理依赖 US-08 已登记的开放 REQ_TRACK;US-06 与 US-08 为单向数据流耦合(US-06 依赖 US-08 登记能力),不构成双向故事依赖(ACM2-24)。 +接收时只按信箱 ID 去重;解码后才首次绑定业务身份。重试保留原有绑定,不能把自己判为重复消息。身份被另一条记录占用时,当前消息转为 `SKIPPED`,记录 `duplicate-of:`。是否加入日期边界取决于上游序号重置规则,默认关闭;上线后不能随意更换身份算法。 -**已知缺口(U09,未定案,ACM2-12 后重设计为 Redis 内协议)**:gen 与 Lua/SUCCEEDED 不再 -分属两存储即可同原子(全部在 Redis Lua);真正跨存储的窗口收窄为「Lua 已完成、PG SUCCEEDED -未写」——重放判据(版本不二次自增)与按代差删在 Lua 内以版本 CAS 承接,恢复协议待定案并补 -测试。现有代码的「CAS 重放二次自增」缺陷(版本 1→2)与实现注随协议重设计一并消除。 +Handler 是纯函数: -### 3.5 泵作业(`JobExecutor`;PUMP_JOB 自有 PG,作业窗口执行) +```text +Handler.decide(flightView, message) → Decision +Decision = 航班变更 + msg 通知 + schd 状态 + 出站意图 + 静态数据变更 +``` -- HISTORY_SWEEP(3:30 清场,I4 同步链):判史 → 同步写 ES → 仅删成功集。 - **占位门禁(U10/T07 修订)**:ES saveSync 接线前 `pickHistory` 恒空集、删除量恒 0, - 禁止「全量可删」fail-open 默认;现役五条判史规则 golden 通过后才允许接线。 - **阶段归属**:按 ACM2-12 阶段 B 缓做口径标为 DEFERRED,不作为阶段 A 切流门禁;启用前 - 重新确认“历史链路不变”与阶段 B 投影范围、ES/OpenSearch 产品边界。 -- ARCHIVE(3: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;重新评估后再启用)。 +Handler 不写 Redis、Kafka 或数据库。主泵负责应用决策;各类变更的持久化与重试边界必须明确,不能把“返回了 Decision”当成副作用已执行。 -## 4. 失败与重试统一设计(U08) +### 2.3 状态与错误分类 -| 侧 | 组件 | 迁移语义 | -|---|---|---| -| ProcState(处理/快照) | `ProcFailure` + `FailureScheduler` | attempts+1 → exhausted ? DEAD(EXHAUSTED) : FAILED+nextAttemptAt | -| MsgEvent 逐条(投递) | `Dispatcher.retryOrDead` | 同上(DLQ 保留行,attempts 审计) | -| MsgEvent 批量(schd) | `flushSchd` 整批 | 队首未到期不 claim;整批退避;达上限整批 DEAD | +```text +处理:PENDING / FAILED → SUCCEEDED(成功) + → SKIPPED(忽略或重复) + → FAILED(等待重试) + → DEAD(非法报文或重试耗尽) -- 退避表 `[1s,2s,4s,8s,16s]`,单档封顶 `backoff-cap-ms=60s`;`attempt≤0` 兜底首档(N28)。 -- 时间一律经可注入 `java.time.Clock`(`TimeFactory`;测试用 MutableClock,无真实睡眠)。 -- loop 兜底 catch 不做状态迁移(迁移已在边界完成),仅防线程静默死亡。 +投递:PENDING → SENT + → PENDING(退避后重试) + → DEAD(重试耗尽) +``` -## 5. 不变量与实现落点 +`SUCCEEDED / SKIPPED / DEAD` 是处理终态,不再阻塞后续消息;`FAILED` 不是终态,仍占据队头。`DEAD` 表示需要处置,不等于业务成功。 -| 不变量 | 语义 | 落点 | 状态 | -|---|---|---|---| -| I1 | 单写者严格 FIFO + HOL 阻塞 + 毒丸升级 | `headUnfinished`/`headUnsent` 队头语义、`poisoned()` | 实装(attempts 毒丸生效;head-deadline 判据不可达待修,见 §3.2 注;job 为 head-state 近似 / 作业窗口属 U15) | -| I2 | Redis 先写、后于事件创建(happens-before) | `processOne` 阶段 A 分支 | TODO redisApply(流程占位已留) | -| I3 | identity 首绑幂等;接收层无唯一约束;SUCCEEDED 回填 | `Identity`/`tryBindIdentity`/`backfillOnSuccess` | 实装 | -| I4 | 清场仅删 ES 成功集;按代差删 | `HistorySweepJob`/SNAPSHOT_REPLACE delFields | 门禁实装,ES 接线 TODO | -| I5 | 阶段 A Redis 写仅主泵线程;Delivery 不写 Redis | 单线程拓扑 + Targets.phaseA | 实装(拓扑约束,U26 运行期保护未做) | +| 错误类别 | 处理方式 | +|---|---| +| `MALFORMED` | 报文非法,直接 `DEAD`,不在原记录重放白名单内。 | +| `CODEC_ERROR` | 解码能力问题,退避重试;修复后允许重放。 | +| `UNSUPPORTED` | Handler 或快照能力未实现,按可恢复失败处理,不直接当作非法报文;仍受重试上限约束。 | +| `INFRA` | 基础设施或执行异常,退避重试。 | +| `EXHAUSTED` | 重试耗尽或滞留超时,转 `DEAD`,人工复核后允许重放。 | -## 6. 配置参数(`msgx.*`,ACMA-8 参数表初值) +## 3. 收报与主泵 -| 键 | 默认 | 说明 | -|---|---|---| -| `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) | +### 3.1 收报 -基础设施键位口径(Micronaut 5.1,U03):`datasources.default.*`(**自有 PostgreSQL**, -ACM2-12)、`flyway.datasources.default.*`、`mailbox.shared-mysql.*`(共享信箱,仅 DML)、 -`kafka.producers.default.*`、`eureka.client.*`;logback 独立于本文件, -环境变量前缀 `MSGX_LOGSTASH_*`。 +`InboxPoller` 默认每秒读取未处理信箱记录,在 PG 建立 `PENDING`,不解析业务载荷。PG 插入必须按信箱 ID 幂等,失败由后续扫描补建。 -## 7. 测试策略 +水位优化分为两条路径:快路径读取水位之后的新记录,补偿路径重扫遗漏的未处理记录。只有本批 PG 入队全部确认后才能推进水位。**水位不是已处理标记,也不能单独证明较小 ID 已收齐**;迟提交和补扫场景的顺序保证需要在启用前验证。 -- **接口驱动 + 假仓储**:管道语义全部离线单测(无 DB/Redis/Kafka),时间用 `MutableClock`。 -- **不变量测试**:FIFO/HOL、schd 批退避与 DLQ、重试上限、重放白名单、 - 聚合最新态、配置绑定、DI 装配冒烟(PipelineSmokeTest:收报→FAILED(CODEC_ERROR,stub 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 红即阻塞;「新增逻辑必伴生不变量测试」入贡献约定。 +兼容 HTTP 入口执行“写入共享信箱 → PG 入队”。两步不在同一事务中:信箱成功而 PG 失败时,原文不能丢失,由轮询补建;客户端失败重试可能再次写信箱,业务身份去重仍然必需。 -## 8. 可观测性设计(U12 已落地部分) +### 3.2 主泵调度 -- logstash TCP 经 AsyncAppender(queueSize 4096 / neverBlock / discardingThreshold 0): - 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)。 +每次 `Pump.tick`: -## 9. 已知缺口与定案待办(对照 ACM2-10) +1. 读取最小未完成消息,必须包含 `FAILED`,不能只查当前可执行的记录。 +2. 无消息,或队头仍在退避窗口内时,允许执行一个维护作业;消息已可执行时优先处理消息。 +3. 队头达到重试或滞留上限时转 `DEAD(EXHAUSTED)`;未到重试时间则等待,不领取后续消息。 +4. 其余情况调用 `MessageProcessor.processOne`。 -| 项 | 缺口 | 计划 | -|---|---|---| -| 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) | WP2(legacy 逐字对拍固化) | -| 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 | -| U22–U24 | 载荷类型收敛、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` 第二库规划 | 仅作决策脉络参考 | +作业执行时长和饥饿边界需要限制,不能用长期作业阻塞已到期消息。滞留超时应基于稳定的起始时刻,不能用每次失败都会刷新的 `updatedAt` 代替。 -## 10. 用户故事 +### 3.3 单条处理 -面向需求优化的用户故事已集中到 [user-stories.md](user-stories.md)。该文档将目标能力、验收标准、 -依赖与待确认问题分开,覆盖阶段 A US-01~US-14、延后 US-15、上线 EPIC 及 legacy HTTP 去留, -避免把当前实现、目标设计和遗留兼容行为混成同一项承诺。 +```text +读取原文 → 解码 → 忽略规则 → 首次绑定身份 + ├─ 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)。本文件不维护工单流水账、测试数量或历史方案全文。 diff --git a/docs/user-stories-todo.md b/docs/user-stories-todo.md deleted file mode 100644 index f59db09..0000000 --- a/docs/user-stories-todo.md +++ /dev/null @@ -1,50 +0,0 @@ -# 用户故事文档整改 TODO - -> 本清单只跟踪文档收敛;实现任务仍由对应 Plane 工作项管理。新建 Plane 汇总项会与 -> ACM2-15~22 重复,因此在确认合并策略前不重复创建。 - -## P0:先消除范围冲突 - -- [x] **ACM2-16 — RESP/DNLD 路由**:在 `user-stories.md` 增加报文路由矩阵;RESP/DNLD - 共用 SnapshotFlow,RESP 成功后再完成匹配的 RQFD 请求;ADFT 保持增量 Handler。 -- [x] **ACM2-17 — 阶段边界**:ARCHIVE 独立为阶段 A US-11;HISTORY_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 删生产降级 env;US-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/23~27 重叠收束)。 -- [x] **ACM2-22**:写入二轮审查闭环评论(已验证属实 + 新发现 1~5 + ACM2-23~27)。 - -## 验证清单 - -- [x] `architecture.md`、`design.md`、`user-stories.md` 的 RESP/DNLD 和阶段表述一致。 -- [x] README 指向新的故事范围。 -- [x] Markdown 变更通过 `git diff --check`。 -- [x] Plane ACM2-23~27 已 commit 后转 Done(346929e)。 -- [ ] 实现尚未随本文修改;后续代码 PR 必须补 FIFO、快照、回填和双库集成测试。 diff --git a/docs/user-stories.md b/docs/user-stories.md index 1ca9843..9ee3c4a 100644 --- a/docs/user-stories.md +++ b/docs/user-stories.md @@ -1,272 +1,326 @@ -# msgexchange-v2 用户故事草案 +# msgexchange-v2 用户故事与实施清单 -> 依据 `architecture.md`、`design.md`、Plane ACM2-3/5/6/7/12/15~22 与 legacy 基线整理。 -> 本文描述目标能力和明确保留的兼容行为,不代表当前代码已完成。 +## 1. 如何使用本文 -## 1. 约定 +本文是把现有脚手架补成可用系统的实施入口:**故事定义要交付什么,验收标准定义怎样证明完成,代码落点说明从哪里改起**。保留 US-01~US-15、OPS-1~OPS-4 编号,便于关联已有任务和测试。 -- “信箱已落信”“PG 已入队”“业务处理成功”“共享信箱已回填”“下游已投递”是不同事实。 -- `KEEP` 表示兼容现役;`FIX` 表示修复 legacy 缺陷;`DEFERRED` 表示不属于阶段 A。 -- 依赖只表示前置能力,不形成循环;待决策内容不得伪装成验收标准。 +- 系统边界见 [architecture.md](architecture.md),模块流程见 [design.md](design.md)。本文不重复设计全文,也不以工单状态代替代码验收。 +- “当前基础”来自本轮代码核对,只表示有接口或部分实现,不表示故事完成。`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 与一个不依赖参考数据的 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 是基础管道,不依赖具体业务 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`;这里只采集入队,不代表业务已处理。 -2. 快路径使用持久化高水位增量扫描;另以受控周期补偿重扫“未处理且 PG 无状态”的记录。水位只在本批入队确认后推进。 -3. 每个 ID 在 PG 至多一个 `PROC_STATE(PENDING)`;重复发现不重复入队。 -4. PG 不可用时不改共享信箱标记;恢复后补偿重扫能补建遗漏状态。 -5. 单条异常和数据库故障可观察,轮询线程不得静默退出。 +1. 按配置周期、ID 升序、有限批次采集未处理记录;接收层只入队,不解析业务、不回填已处理标记。 +2. 按信箱 ID 幂等建立 PG `PENDING`;重复扫描、并发兼容入队和进程重启都不能重置已有终态。 +3. 快路径用持久水位,补偿路径受控重扫遗漏;本批 PG 入队全部确认后才推进水位。补偿可分页推进,不能被已入队但尚未回填的前一批永久挡住。 +4. PG 不可用或批次中途失败时不改信箱标记;恢复后补建遗漏,记录失败次数与扫描进度。 +5. 较小 ID 迟提交、ID 有空洞、兼容入口先入队较大 ID 时,必须遵守经 Q2 确认的发现与顺序协议;不能用“最终会重扫”冒充严格 FIFO。 -**依赖**:共享库读权限、字段与索引契约。 -**实现差距**:当前 `InboxPoller` 固定 `afterId=0` 全量扫描,无持久化水位和独立补偿频控。 +**当前基础与落点**:`ingress/InboxPoller.kt`、`InboxEnqueue.kt`、`infra/persistence/jdbc/JdbcCminmsgInboxRepository.kt` 已有轮询和判重;固定 `afterId=0`,水位与补偿未实现。扩展 `InboxPollerTest`,补真实 PG/MySQL 中断恢复测试。 -### US-02 通过兼容接口注入报文 +**前置**:共享库读契约;Q2 决定严格顺序的端到端验收。 -**作为** 联调人员,**我希望** 通过 `POST /cminmsgs/send` 注入 XML,**以便** 执行回放和对拍。 +### US-02 兼容 HTTP 注入报文(KEEP) + +**目标**:联调工具通过 `POST /cminmsgs/send` 提交 XML,得到真实的信箱接收结果。 **验收标准** -1. 信箱落信后返回 `CMINMSGS_ID`,响应不得暗示 PG 已入队或业务已处理。 -2. 落信成功但 PG 入队失败时,由 US-01 最终补建。 -3. Content-Type 支持 `text/xml`、`application/xml` 与 `text/plain`(默认按 UTF-8 解码);空报文、超大报文(> 10MB)及畸形 XML 返回规范错误。 -4. 响应结构逐字兼容 legacy `ResponseDto`:成功返回 `{"is_success": true, "body": }`,失败返回 `{"is_success": false, "err_code": "", "err_msg": ""}`。 -5. 生产默认沿用现役内网互信免密姿态(网关限定内部 IP 网段与审计);外露或跨网络时启用 Header 认证。 +1. 支持 `text/xml`、`application/xml`、`text/plain`,默认 UTF-8;空报文、超过 10MB 的请求和畸形 XML 返回规范错误,不落信。XML 校验禁用 DTD、外部实体与外部资源访问。 +2. 信箱确认落信后返回 ID;PG 入队失败不把已落信伪装成未接收,由 US-01 补建。信箱写入未确认时不返回成功。 +3. 目标为兼容 `ResponseDto`;固定成功/失败样例、HTTP 状态码、响应媒体类型和错误码表后加入契约测试,见 Q3。成功只承诺信箱落信,不承诺业务处理或下游完成。 +4. 生产保持内网信任边界,由网关限制来源并审计;外露或跨网络必须先落实认证,不能把免密入口直接暴露。 -**依赖**:US-01。 +**目标响应体示例**(数字和错误码仅作示例,错误码表见 Q3): -### US-03 严格按序且幂等地执行处理管道 +```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 提交。先用假 Handler 验证管道,不等 US-05 全部实现。 **验收标准** -1. 选择最小未完成 ID;`PENDING`、`FAILED` 都占队头,退避期间后续消息不得越过。 -2. identity 首绑为 `SNDR|TYPE|STYP|SEQN`;冲突转 `SKIPPED` 并记录原 ID。 -3. `MALFORMED` 直接 DEAD;`CODEC_ERROR/UNSUPPORTED/INFRA` 退避;attempts 或 HOL deadline 耗尽后转 `DEAD(EXHAUSTED)`。 -4. PUMP_JOB 不与消息形成统一全序,只在无队头或队头尚在退避窗口时执行;不得让已到期消息饥饿。 -5. Redis 先应用;`MSG_EVENT` 与 `PROC_STATE→SUCCEEDED` 在同一 PG 事务提交。 -6. Redis 已写、PG 未提交的窗口可幂等重放。 +1. 只取最小未完成 ID,`PENDING / FAILED` 均占队头;退避未到期不得越过。作业仅在无队头或退避窗口执行,已到期消息优先,作业有界且不会造成无限饥饿。 +2. 安全解码 XML,至少覆盖 META、SCHD、FLOP、参考应答与忽略类路由;合法但能力未支持是 `UNSUPPORTED`,不能一律归为非法报文。保留原文以支持诊断和回放。 +3. 解码后首次绑定 `SNDR|TYPE|STYP|SEQN`;冲突转 `SKIPPED` 并记录原 ID;自身重试保留绑定。生产 `include-day-boundary=false`,更改算法须另行评审上游序号规则。 +4. `MALFORMED` 直接 `DEAD`;`CODEC_ERROR / UNSUPPORTED / INFRA` 按次数和退避处理,耗尽转 `DEAD(EXHAUSTED)`。不能无限重试未实现类型,也不能立即当非法报文丢弃。 +5. HOL deadline 使用稳定的 `PROC_STATE.CREATED_AT`,不使用每次重试刷新的 `updatedAt`;所有调度判断注入 `Clock`。默认 5 次重试、10 分钟滞留限制;积压与人工重放的 deadline 边界按 Q6 验证。 +6. 当前 Redis 基线下,由主泵先幂等应用状态,再在同一 PG 事务提交事件与处理结果;终态回填意图通过 US-09 同事务保存。Redis 已成功而 PG 失败可安全重试,不能重复生成业务事件。 +7. Handler 只返回 `Decision`,副作用由管道执行;失败只在持有消息上下文的边界落账,中断向上传递,不作为普通失败吞掉。 +8. 权威存储不可用或未完成恢复时停止业务处理;不能把“整个状态丢失”误判为“单航班不存在”而批量成功结束增量报文。 -**依赖**:US-01。 -**既有基线定案**:生产幂等键默认采用 `SNDR|TYPE|STYP|SEQN`,`include-day-boundary=false` 禁开,防止跨日重放漏判;HOL deadline 起算时间统一固化为 `PROC_STATE.CREATED_AT`(稳定入队时间戳),消除重试刷新 `updatedAt` 导致的超时不可达缺陷。 +**当前基础与落点**:`processing/Pump.kt`(含 `MessageProcessor`)、`Identity.kt`、`codec/XmlCodec.kt`、`infra/retry/`、`JdbcPgRepositories.kt`。已有身份/重试边界测试,但 codec 为接口,主泵直取系统时间,普通 Redis 更新占位,PG 操作未共享事务;补 `CREATED_AT` 迁移及主泵级回归测试。 -### US-04 忽略非业务报文(KEEP) +**前置**:US-01;Q1 决定状态提交实现,Q6 决定 deadline 边界。现有 `FlightStateRepository` 空实现不是建表授权。 -**作为** 运维人员,**我希望** 已确认无需处理的报文被明确忽略,**以便** 不产生 DLQ 噪声。 +### US-04 明确忽略非业务报文(KEEP) + +**目标**:无需处理的报文有可追踪的终结结果,不制造无效重试与死信。 **验收标准** -1. 解码 META 后、查 Handler 前,大小写不敏感匹配 `TYPE-STYP` 与 `TYPE-*`。 -2. 基线为 `LDM-*`、`REGN-*`、`RSTA-*`、`EROR-*`;统一采用 `EROR`,消除 test 的 `ERROR` 漂移。 -3. 命中后进入 `SKIPPED`,记录 `ignored:`,不创建业务事件。 -4. 按 US-09 的规则回填共享信箱,并保留审计计数。 +1. 解码 META 后、身份绑定及业务 Handler 查找前,大小写不敏感匹配 `TYPE-STYP` 或 `TYPE-*`;基线为 `LDM-* / REGN-* / RSTA-* / EROR-*`,不混用 `ERROR`。 +2. 命中后转 `SKIPPED`,记录 `ignored:` 和计数;不更新航班、不创建业务通知。 +3. 通过 US-09 保存回填意图;命中、未命中、大小写和重扫均有测试。合法忽略报文不应因 `MsgKind` 尚不能表达它而先解码失败。 -**依赖**: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 类 FLOP(KEEP + FIX) + +**目标**:增量报文正确更新航班及主/共享关系,并生成符合现役语义的通知。 **验收标准** -1. `SCHD-ADFT` 和 29 个 FLOP 子类型均有纯函数 Handler;未知类型进入 `FAILED(UNSUPPORTED)`。 -2. 每类覆盖输入、Redis 变化、msg、schd、处理终态五面断言。 -3. 航班不存在按 legacy KEEP 语义结束且不重试:以 `SUCCEEDED` 终态结束,按 US-09 回填共享信箱 `DATE_PROCESSED = now()`, `STATUS = 'SUCCESS'`,防止死循环。 -4. 共享航班默认不直接发通知,而是更新并通知主航班;FDEL 例外定案:删除共享航班时更新主航班 MAFL 列表并发出主航班通知;若删除主航班则删除其及所有子共享关联并发出删除通知;目标航班不存在时幂等成功退出。 -5. ADFT/FDEL 使用值相等比较,主/共享关系作为一次原子 Redis 变更持久化(FIX)。 -6. PSDT 依赖 US-14,Handler 不直接调用 admin-api。 +1. `SCHD-ADFT` 与 29 个 FLOP 子类型逐项列入覆盖矩阵,每项有纯函数 Handler;未知类型可恢复失败。RESP/DNLD 不计入这批 Handler,走 US-06。 +2. 每类固定“输入与前态 → 后态 → msg → schd → 终态”五面样例;区分字段缺失、显式清空、重复报文和主/共享航班。清单和 golden 样例按 Q8 补齐,不以“已写 29 个类”替代验收。 +3. 对按 KEEP 规则需忽略的不存在航班,以 `SUCCEEDED` 无副作用结束,并由 US-09 回填;ADFT 建航班等行为按各类型矩阵执行。此规则只适用于权威状态健康时。 +4. 共享航班通常更新并通知主航班,不直接发共享通知。FDEL 删除共享航班时更新主航班 MAFL 并通知;删除主航班时删除主航班及其子共享关联并发删除通知;目标不存在幂等成功。 +5. ADFT/FDEL 使用值相等比较;主/共享关系一次原子变更,不出现主已删、子残留等半状态。 +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-03;PSDT 另依赖 US-14;Q1、Q8。 ### US-06 导入 RESP/DNLD 日计划快照 -**作为** 航班计划使用方,**我希望** RESP 与 DNLD 共用快照流程,**以便** 主动下载和请求应答获得相同终态。 +**目标**:主动下发和请求应答使用同一套全量计划处理,迟到应答不覆盖新状态。 -| 报文 | SnapshotFlow | REQ_TRACK | Kafka msg | 迟到/无匹配处理 | -|---|---|---|---|---| -| `SCHD-DNLD` | 是 | 不更新 | 成功后通知 | 不适用(广播/全量) | -| `SCHD-RESP` | 是 | 匹配开放 RQFD 后 DONE | 成功后通知 | 严禁更快照,转 SKIPPED 并审计 | -| `SCHD-ADFT` | 否,走 US-05 | 不更新 | 按增量规则 | 增量应用 | +| 报文 | 路由 | 请求状态 | 无匹配时 | +|---|---|---|---| +| `SCHD-DNLD` | SnapshotFlow | 不更新请求 | 不要求开放请求 | +| `SCHD-RESP` | 匹配守卫后进入 SnapshotFlow | 成功提交时匹配 RQFD → DONE | SKIPPED、审计,禁止更新快照 | +| `SCHD-ADFT` | US-05 增量 Handler | 不更新请求 | 不适用 | **验收标准** -1. RESP/DNLD 共用 staging 流式整包校验和 SnapshotFlow,不作为普通 FLOP 增量 Handler 重复实现。 -2. 主泵执行先后严格服从 I2(happens-before):主泵线程先执行 Redis Lua 完成写新代、旧代差集删除及 generation 版本 CAS(ADFT 航班存活,FIX);Redis 执行成功后进入自有 PG 本地事务。 -3. 自有 PG 本地事务原子性:同一 PG 事务内原子提交 `PROC_STATE → SUCCEEDED`、匹配开放请求的 `REQ_TRACK → DONE`(记录 completed_at 与 resp_cminmsgs_id)及 `MSG_EVENT` 出站通知。 -4. 整包失败保留旧快照;相同报文重放(CAS 版本一致)幂等,不重复增代或差删。 -5. 迟到(`dttm < req.sentAt`)或无匹配/过期(`EXPIRED`)RESP **严禁更新快照**,转入 `PROC_STATE → SKIPPED` 并记录审计与告警,防止历史快照时光倒流覆盖新状态(对齐 G12)。 -6. 覆盖首次/连续发布、DNLD→ADFT→DNLD′、RESP 匹配和崩溃窗口。 +1. RESP/DNLD 共用流式解析、整包校验和规范化;校验失败不发布半包,旧快照保持可用。 +2. RESP 仅匹配未过期、已发送的开放 RQFD;`DTTM < SENT_AT`、已过期、已被替代或无匹配时,不写业务状态,记录跳过原因。 +3. 当前 Redis 基线下,Lua 原子执行新代写入、旧代差集删除和版本校验/推进;删除集只来自上一代成员,不误删集合外的 ADFT 航班。 +4. 随后在一个 PG 事务提交 `SUCCEEDED`、待发通知、回填意图;匹配 RESP 同事务完成请求,并保存完成时间与应答信箱 ID。 +5. 相同报文重放不二次增代或删数据;覆盖首次/连续快照、DNLD→ADFT→DNLD′、旧应答与 Redis 成功/PG 失败窗口。不能只靠“读取当前版本再加一”实现重放识别。 -**依赖**:US-03、US-08、Redis gen 协议。 +**当前基础与落点**:`processing/SnapshotFlow.kt` 的 staging 为占位,主处理只分流 DNLD,gen 走进程内过渡仓储。需补 RESP 路由、版本协议及 `REQ_TRACK` 应答关联字段和事务测试。 + +**前置**:US-03、US-08 请求登记/匹配基础;Q1、Q5。 ### US-07 可靠、有序地投递 Kafka -**作为** Kafka 消费者,**我希望** 状态可见后有序投递,**以便** 通知不指向旧状态。 +**目标**:状态应用完成后投递通知;重试可识别、不乱序、不静默丢失。 **验收标准** -1. `KAFKA:msg` 按 EVENT_ID FIFO,确认后才标 SENT。 -2. `KAFKA:schd` 按周期和上限领取;同一 FLID 仅发批内最新事件。 -3. 失败保持队头并退避;耗尽后 DEAD,不静默丢弃。 -4. 事件包含稳定去重标识,分区键和重复投递有契约测试:`KAFKA:msg` 以 `SNDR` 作为分区键;`KAFKA:schd` 严格以 `FLID` 为分区键,确保单航班有序。 -5. 生产环境对接 Kafka 2.8+ / 3.x+,生产者强制 `acks=all`、`enable.idempotence=true` 与 `max.in.flight.requests.per.connection=1`,严禁非幂等降级;投递失败退避重试,达上限转 `DEAD(DLQ)` 告警,绝不静默丢弃。 +1. `KAFKA:msg` 按目标内 `EVENT_ID` 顺序发送,确认后才标 `SENT`;队头退避时不跳过,发送有超时上限。 +2. `KAFKA:schd` 只通过 `flushSchd` 聚合,默认 3 秒/500 条;同一 FLID 取批内最新状态,成功确认覆盖对应原事件,失败保持批次可恢复并退避,耗尽可见为 `DEAD`。 +3. 外部接收成功、本地确认失败或进程重启后允许重发;事件标识跨重发稳定,消费者有去重约定,不宣称端到端恰好一次。 +4. msg 的目标分区键为 `SNDR`。schd 的聚合粒度与分区键先按 Q4 定案,明确哪些顺序只在单分区成立;不能把多航班数组声称为“每个 FLID 都是该 Kafka record 的 key”。 +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 请求 -**作为** 业务或运维人员,**我希望** 发起请求并跟踪生命周期,**以便** 区分登记、落信、等待、完成和超时。 +**目标**:区分请求登记、出站落信、等待、完成与超时,不把过期应答应用到新请求。 + +**实施拆分**:请求登记/出站补偿 → 匹配/超时 → 14 类参考应答;RQFD 快照效果由 US-06 集成验收。 **验收标准** -1. 支持 14 类 RQRD 参考请求和 1 类 RQFD-NONE 日计划请求。 -2. REQ_TRACK 先登记;COUTMSGS 落信后才关联 ID 并标 SENT,落信不等于对方已发送。 -3. 逐类超时与并发规则定案:同类请求并发严格为 1(新请求注册时强制同类未决请求转 `EXPIRED`);`RQFD-NONE` 超时为 60s,14 类 `RQRD` 超时默认为 30s。 -4. 响应经 US-01/US-03 入站:优先以报文回显 `SEQN`(`echoSeqn`)精确匹配开放请求;无回显时降级为时序判定(仅接受 `DTTM >= SENT_AT` 的开放请求),并在完成时同 PG 事务标 `DONE`。 -5. 超时或被替代的请求 EXPIRED;迟到响应(`DTTM < SENT_AT`)或无匹配响应严禁更新业务状态/快照,转 SKIPPED 并留审计。 -6. **集成验收**:SCHD-RESP 遵循 US-06;14 类响应刷新 REF_MASTER,不与 admin-api 21 类混同。 +1. 覆盖 14 类 RQRD 参考请求和 1 类 RQFD-NONE;逐类名称、编码和映射见 Q8,不与 admin-api 的 21 类混算。 +2. 先持久化 `REGISTERED` 与出站意图;COUTMSGS 确认落信后关联其 ID 并标 `SENT`,不宣称对方已发送。落信成功而 PG 未确认时可恢复,不能盲目重发。 +3. 同类开放请求最多一个,新请求使旧请求 `EXPIRED`,并发登记不产生两个开放请求。默认 RQFD 60 秒、RQRD 30 秒,从确认落信的发送时间起算;`SENT/WAITING` 均不得成为永不超时的死分支。 +4. 优先按已确认的 SEQN 回显匹配;无回显的降级匹配按 Q5 明确风险,只接受已发送开放请求且 `DTTM ≥ SENT_AT`。统一转换为可比较的时间,不能把报文日期数字直接与 epoch 毫秒比较。 +5. 迟到、无匹配或已关闭请求的应答不得更新数据,转 `SKIPPED` 并审计。参考应答成功写入 REF_MASTER 后,与请求完成、处理终态和事件在 PG 边界内保持所需原子性。 +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 终态。 -2. **持久化补偿**:未完成或失败的回填由后台补偿任务按指数退避重试;暴露待回填积压量与最老年龄指标,持续失败触发告警。回填 SQL 具备幂等性(`UPDATE CMINMSGS SET DATE_PROCESSED = :now, STATUS = :status ... WHERE CMINMSGS_ID = :id`)。 -3. **各终态回填规则矩阵**(单值锁定;库方/legacy 实测前为占位,见 §7-9): - - `SUCCEEDED`:**必须回填**(`DATE_PROCESSED = now()`, `STATUS = 'SUCCESS'`,补齐 META 子系统列)。 - - `ignore SKIPPED`(规则忽略):**必须回填**(`DATE_PROCESSED = now()`, `STATUS = 'SKIPPED'`),防止上游视作未处理积压持续重扫。 - - `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 终态与信箱回填状态。 +1. PG 终态与回填意图同事务保存;所有终态路径都经过统一提交边界,不只覆盖成功路径。事务回滚时不得留下可执行回填意图。 +2. 提交后由后台执行回填,主泵不等待共享库;失败按持久记录退避,重启继续执行,不重新执行已完成业务。 +3. SUCCEEDED、规则忽略、身份重复、DEAD 均需回填处理时间;PENDING/FAILED 禁止回填。具体 STATUS 编码按 Q7 确认,内部终态不能直接当作外部字段值。 +4. 重复补偿效果幂等,保留稳定的完成时间与审计;重放后的新处理结果不能被旧回填任务覆盖。非法报文缺 META 时也有明确回填方式。 +5. 影子模式禁写,双跑仅一个系统持有标记写权;暴露 PG 终态、回填状态、积压、最老年龄与持续失败告警。 -**依赖**:US-03、共享库更新权限、共享库 STATUS 值域确认(库方/legacy 对拍)。 +**当前基础与落点**:`backfillOnSuccess` 当前仅同步写 `PROCESSED`,无持久意图。补自有 PG 意图模型/迁移、事务提交入口和独立补偿执行器;不在共享库新增补偿表。 -### US-10 运维重放与故障处置 +**前置**:US-03 终态接口;Q7、共享库更新权限。测试覆盖四类终态、事务回滚、重复补偿和重放竞争。 -**作为** 运维人员,**我希望** 查询并安全重放失败项,**以便** 修复故障而不破坏顺序。 +### US-10 安全重放与故障处置 + +**目标**:运维能定位失败、限定恢复范围,并了解重放对当前航班状态的影响。 **验收标准** -1. 可按记录、错误类和时间查询 attempts、错误及 traceId。 -2. 仅 `CODEC_ERROR/UNSUPPORTED/INFRA/EXHAUSTED` 可重放;请求含 MALFORMED 时静默跳过该记录,并返回逐项结果。 -3. 重放清零 attempts/nextAttemptAt,保留错误审计,仍服从 FIFO。 -4. DEAD、持续回填失败、队列年龄越界产生告警;操作记录操作者、原因、范围和结果。 +1. 按 ID、错误类、时间查询次数、错误、关联事件与回填状态;重放前预览范围,记录操作者、原因和逐项结果。 +2. 仅 `CODEC_ERROR / UNSUPPORTED / INFRA / EXHAUSTED` 的 FAILED/DEAD 允许申请重放;MALFORMED 与其他不允许项不改状态,返回跳过原因。 +3. 重置 attempts/nextAttemptAt,保留身份、原始入队时间和错误审计;采用 Q6 确认的重放 deadline 策略。重新入队仍按 ID 处理,但不承诺已执行过的后续消息自动撤销。 +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`;保留期可配置为 1~7 天;不迁 PENDING/FAILED。 -2. **共享库零建表与 DML 最小化定案**:严禁向共享 MySQL 写入 `CMINMSGS_HST`,共享库严格限定为信箱两表(CMINMSGS 读/回填,COUTMSGS 写入);共享 MySQL 自身历史清理交由库方自身 DBA 策略。 -3. **归档迁入自有 PG**:在自有 PostgreSQL 设计 `PROC_STATE_HST`(及 `MSG_EVENT_HST`)承载历史归档数据。 -4. 重复执行幂等并记录计数;迁移失败时 fail-closed,不删除源记录。 +1. 默认归档接收时间早于 1 天的 SUCCEEDED/SKIPPED/DEAD,保留期可配置 1~7 天;PENDING/FAILED 禁止归档。明确接收时间字段来源,不混用 UPDATED_AT 或本地入队时间。 +2. 归档到自有 PG `PROC_STATE_HST`;关联 `MSG_EVENT` 的历史目标和保留规则一并设计。仍有未完成投递、回填或恢复依赖时,不移除所需记录。 +3. 迁移与删除在自有库事务内完成,重复执行幂等;失败保留源记录并报告计数。归档后同信箱 ID/业务身份再次到达,仍能按约定去重。 +4. 不写共享 MySQL `CMINMSGS_HST`,不清理外部信箱;原文可用性与重放保留期由 Q7/Q8 关联确认。 -**阶段**:A。 +**当前基础与落点**:`jobs/JobExecutor.kt`、`PumpJobRepository` 为入口;当前迁移无归档表。先确定去重记录保留与关联策略,再补迁移及归档中断测试。 -### US-12 查询实时航班 +**前置**:US-03、US-09;US-07 提供事件终态规则,US-10 提供恢复保留要求。不依赖 US-15。 -**作为** 授权调用方,**我希望** 查询实时航班,**以便** 获得与 Redis 权威态一致的数据。 +### US-12 查询实时航班(KEEP) + +**目标**:调用方读取与当前权威状态一致的实时航班视图。 **验收标准** -1. KEEP `GET /all/flights`:返回 Redis 当前航班并过滤 `MAID != NULL` 的共享航班。 -2. 固定响应、空结果、排序、分页/大小上限和一致性时点。 -3. 影子只查询影子 key;接口具备认证、限流和审计。 +1. 保留 `GET /all/flights`,过滤 `MAID != NULL` 的共享航班;不改写业务状态。 +2. 固定响应样例、空结果、排序、大小限制及一致性时点。现役未分页时不能无声改为只返回第一页;分页或响应结构变更按 Q3 决定。 +3. 依赖异常不能伪装为空数组成功;影子只读影子状态,入口有约定的访问控制、限流与审计。 + +**当前基础与落点**:`InboxController.kt` 中仅有该接口 TODO;新增查询控制器与只读服务,复用权威读取端口,不能从空占位仓储返回成功。新增接口/状态不可用测试。 + +**前置**:Q1、Q3;所查询的 US-05/US-06 状态发布能力。 ### US-13 刷新 21 类参考主数据 -**作为** 业务组件,**我希望** 从 admin-api 刷新 21 类数据到 REF_MASTER,**以便** 使用可审计的本地主数据。 +**目标**:业务使用来自 admin-api 的本地参考数据,刷新失败仍有上次可用版本。 **验收标准** -1. 采用 ACM2-5 清单;admin-api 拉取与 US-08 的 AODB 请求是两个入口。 -2. 以 `(RTYPE,RKEY)` 幂等 upsert,记录 SOURCE、刷新时间和批次审计。 -3. 单类失败不发布半批,不破坏上个可用版本;影子默认不主动刷新生产数据。 +1. 按 Q8 的 21 类清单配置端点、RTYPE/RKEY、字段映射;这是独立于 US-08 的数据入口,不另建“参考专用第二 PG”。 +2. 按 `(RTYPE,RKEY)` 幂等写 REF_MASTER,记录 SOURCE、刷新时间和批次;单类完整校验后发布,失败不暴露半批。 +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 类分开统计。 -2. 通过适配器拉取并缓存;Handler 不直接 HTTP。 -3. 近机位生成登机桥,远机位或清空时 `abdg` 为空;多桥规则由 golden 固定。 -4. admin-api 不可用时使用最后可用版本或明确失败,不写不完整缓存。 +1. 保留 `ORMS_STAND / ORMS_STAND_AIRBRIDGE` 两类,与 US-13 的 21 类分开统计;适配器拉取、完整校验后原子发布只读缓存。 +2. 近机位产生登机桥值,远机位或清空机位时 `abdg` 为空;一机位多桥、缺失映射与共享航班规则用 golden 固定。 +3. 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)。 -2. 阶段归属与 ES 边界定案:`HISTORY_SWEEP` 延后为阶段 B 能力(DEFERRED),阶段 A 永续以 Redis 作为航班动态权威,完全不接入 ES;不作为阶段 A 切流门禁。 -3. 五条判史规则与 ES 写入留在阶段 B 启用前完成 100% golden 对拍;逐条隔离坏数据;仅历史写成功的 FLID 可由主泵删除。 -4. 与快照保持单写者串行并记录计数。 +### US-15 历史航班清场(DEFERRED,阶段 B) -## 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 积压为 0;MSG_EVENT 最老滞留 < 5s;切流后设立 48 小时观察期,24 小时内支持按 Runbook 平滑一键回滚。 - -依赖按实际进入上线范围的阶段 A 故事计算,不包含 US-15。 - -## 5. Legacy HTTP 工具面 - -| 端点 | 决定 | 目标口径 | +| 编号 | 必须交付的能力 | 验证证据 | |---|---|---| -| `POST /cminmsgs/send` | KEEP | US-02 | -| `POST /schd/sync` | KEEP,修正 | 24 小时制、非空/区间校验;只承诺 RQFD 落 COUTMSGS | -| `GET /all/flights` | KEEP | US-12 | -| `POST /kafka/topics/{name}/msgs` | 不进生产 | 若开发仍需,另建工具并限制 topic allowlist | -| `GET /flights/migrate` | 不做 | legacy 一次性 ES 迁移工具 | +| OPS-1 单写者与启动安全 | 生产缺真实适配器、误用 stub、未启用必需管道时拒启;第二活动写者不能启动,失去写权后不得继续写;中断与停机能正确退出。 | 配置拒启、双实例/失去写权及停机测试。单靠副本数配置不算运行期保护。 | +| OPS-2 可观测与安全 | 真实依赖健康、队列/队头年龄、投递/回填滞后、DEAD 和一致性异常有指标、告警与处理入口;敏感管理操作有访问控制,日志不泄漏口令或完整敏感报文。 | 故障注入触发真实告警,消息到事件可关联;日志出口断开不阻塞业务。 | +| OPS-3 影子隔离 | PG、Redis key、topic、服务注册身份隔离;输入只读水位或回放,禁生产回填、真实出站和误注册。Q1 若改存储,隔离方案同步修改。 | 配置与集成测试证明生产信箱、状态、topic 未被影子修改。 | +| OPS-4 切流与恢复 | 对拍不少于 7 天,未解释业务字段差异为 0,DLQ 积压为 0,MSG_EVENT 最老滞留 < 5 秒;切流后 48 小时观察,24 小时内具备经演练的回滚能力。 | 明确负载与统计口径的对拍报告;Runbook 含停写、排空/水位、状态恢复、写权交接和失败回退,不能只回滚程序版本。 | -## 6. 详细文档 TODO +上述阈值沿用既有需求基线,需在真实环境提供证据,不代表当前已满足。Redis 权威方案还必须验证空态/全损恢复期间停止增量处理,以及备份和报文保留能支持的恢复范围;不承诺未经演练的“一键无损回滚”。 -| 顺序 | Plane | 文档动作 | 完成条件 | -|---|---|---|---| -| 1 | ACM2-16 | 固定 RESP/DNLD/ADFT 路由 | US-06、design、ACM2-6 一致,迟到/无匹配禁更快照定案 | -| 2 | ACM2-17 | 拆归档与清场并标阶段 | US-11 属 A,US-15 DEFERRED;HST 禁写,自有 PG 归档定案 | -| 3 | ACM2-19 | 增补信箱回填 | 提交后执行、持久化补偿、四终态回填、影子禁写定案 | -| 4 | ACM2-21 | 消除循环依赖并补管道边界 | US-03 仅依赖 US-01,业务例外归 US-05 | -| 5 | ACM2-15 | 增补 ignoreMsg | 规则、终态、回填和拼写确定 | -| 6 | ACM2-18 | 补查询、机位、21 类数据 | US-12~14 与 US-08 分界明确 | -| 7 | ACM2-20 | 声明 HTTP 工具去留 | 五端点均有决定 | -| 8 | ACM2-22 | 吸收评审剩余项 | 水位、依赖、Broker、deadline 一致 | -| 9 | — | 同步 architecture/design/README | 权威口径、索引与阶段表一致 | +### HTTP 工具边界 -## 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`。 -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 超时 60s,RQRD 超时 30s;优先 SEQN 回显,无回显退化 DTTM 时序判定;迟到/无匹配禁更快照。 -6. **【已定案·ACM2-22/23】Kafka 生产契约**:强制 `acks=all` 与 `idempotence=true`,分区键按 FLID(schd)/SNDR(msg)固化;严禁非幂等降级;现网 Broker API 版本未确认前 US-07 不得标实施完成;README 不提供生产降级 env。 -7. **【已定案·ACM2-17】时区与判史边界**:统一 `Asia/Shanghai` 时区;HISTORY_SWEEP 延后至阶段 B,阶段 A 不依赖 ES,Redis 永续动态权威。 -8. **【已定案·ACM2-22】对拍与回滚阈值**:影子对拍至少 7 天;业务差异 0 容忍;DLQ 积压为 0;切流后 48 小时保驾、24 小时可平滑回滚。 -9. **【已定案·ACM2-26】信箱回填 STATUS 值域**:`SUCCEEDED`→`SUCCESS`;ignore `SKIPPED`→`SKIPPED`;duplicate `SKIPPED`→`DUPLICATE`;`DEAD`→`DEAD`(库方/legacy 实测前为占位;若库方禁新值则仅用 legacy 已用集合)。 +## 6. 编码前必须处理的决策与契约 + +这些是**阻塞相应实现的具体问题**,不是已完成的验收项。保留原有目标值,但不把矛盾或外部未确认内容写成事实。 + +| 编号 | 问题与当前口径 | 解除阻塞的产物 | +|---|---|---| +| Q1 权威存储 | 当前基线是 Redis;FLIGHT_STATE 提前到 PG 只是提案。影响 US-03/05/06/12、迁移和健康检查。 | 明确选择及批准记录,同步三份主文档后再固定事务、快照协议;未定案可先做无关接口和调度测试。 | +| Q2 入队顺序 | 水位+补扫无法自动保证较小 ID 迟提交不越序;当前有限批扫描也可能被未回填记录挡住。 | 库方 ID/提交顺序约束,或明确的发现完整性与暂停/恢复协议;晚提交、空洞、兼容入口与重扫联合测试。不能凭空假定 ID 连续。 | +| 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 验证和发布证据。 + +历史文档整改中的勾选不代表业务已实现,也不替代本清单。后续进度放在实施任务与测试证据中,本文保持需求和验收口径稳定。