diff --git a/docs/contracts/interface-contract.md b/docs/contracts/interface-contract.md index 45cdda8..0cc3b95 100644 --- a/docs/contracts/interface-contract.md +++ b/docs/contracts/interface-contract.md @@ -11,17 +11,17 @@ | 接口 | 请求契约 | 成功响应契约 | 失败契约 | 尚需确定 | |---|---|---|---|---| | `POST /cminmsgs/send` | 请求体是 XML 原文;接受 `text/xml`、`application/xml`、`text/plain`,默认 UTF-8;仅限内网,网络层限制来源。 | 报文写入 `CMINMSGS` 后返回信箱编号;写入的报文与上游投递走同一条处理路径、效果一致;该响应只证明已落信,不证明业务处理或下游投递(`US-02`)。 | 空报文、超大小上限、非法 XML 不落信并返回错误;写信失败不返回编号;XML 解析禁用外部实体和外部资源访问。 | 大小上限、请求编码与 `Content-Type` 的精确处理规则、HTTP 状态码、成功/失败响应体字段及样例(`Q15`)。 | -| `POST /schd/sync` | 触发一次 `RQFD` 日计划请求;请求字段尚未定义,登记与落信规则见「在途与作废」。 | 响应内容尚未定义;无论表示登记还是落信,都不代表 AODB 已收到。 | 错误响应尚未定义。 | 请求字段与时间格式、成功响应表示已登记还是已落信(`Q16`)、状态码、响应字段及样例。 | -| `GET /all/flights` | 无已定义的请求字段;从 Redis 投影读取当前全部动态航班,不含共享航班,与网页客户端同源(`US-12`)。 | HTTP 200;响应体是裸 JSON 数组(不套旧 `ResponseDto`),元素为与 `KAFKA:schd` 同形的航班 JSON;共享航班(标量 `MAID` 非空)不出现在数组里;不分页(`Q21` 暂定)。 | HTTP 503;JSON 对象 `{"error":"FLIGHT_PROJECTION_UNAVAILABLE","reason":"<细节>"}`;Redis 或投影读失败时不得返回 200 空数组。 | 数组元素字段是否与旧 `SCHD.FLTR` 逐字一致的对拍样例。 | +| `POST /schd/sync` | 触发一次 `RQFD` 日计划请求;请求体为空、请求全量(`C-4`);登记与落信规则见「在途与作废」。 | HTTP 200,返回请求编号;只表示已登记,不代表已落信、更不代表 AODB 已收到。 | 已有未结案请求时 HTTP 409,响应体 `open-request-exists`。 | — | +| `GET /all/flights` | 无已定义的请求字段;从 Redis 投影读取当前全部动态航班,不含共享航班,与网页客户端同源(`US-12`)。 | HTTP 200;响应体是裸 JSON 数组(不套旧 `ResponseDto`),元素为日计划 `SCHD.FLTR` 转成的 JSON,与 `KAFKA:schd` 数组元素同形(`C-9`、`C-11`);共享航班(`MAID` 非空)不出现在数组里;不分页。 | HTTP 503;JSON 对象 `{"error":"FLIGHT_PROJECTION_UNAVAILABLE","reason":"<细节>"}`;Redis 或投影读失败时不得返回 200 空数组(`INV-11`)。 | — | 旧系统线索(来源:旧项目用户故事「HTTP 接口清单」「日计划请求」): - 三个接口共用 `ResponseDto`,字段为 `is_success`、`err_code`、`err_msg`、`body`。 - `POST /cminmsgs/send` 的成功 `body` 是 `CMINMSGS_ID`,样例为 `{is_success: true, body: }`。 -- 旧 `GET /all/flights` 用 `ResponseDto`,`body` 是非共享航班的 `SCHD.FLTR` 列表;新版暂定直接返回该列表对应的 JSON 数组(`Q21`)。 -- `POST /schd/sync` 的请求体是 `{startDate, endDate}`,时间格式 `yyyy-MM-dd hh:mm` 用无 AM/PM 的 12 小时制。 +- 旧 `GET /all/flights` 用 `ResponseDto`,`body` 是非共享航班的 `SCHD.FLTR` 列表(对象即日计划 XML 解码后的 `FLTR`,再序列化为 JSON);新版成功体直接返回该列表对应的裸 JSON 数组(`C-11`),元素形状不变。 +- `POST /schd/sync` 的请求体是 `{startDate, endDate}`,时间格式 `yyyy-MM-dd hh:mm` 用无 AM/PM 的 12 小时制,解析结果写进 `RQFD` 的 `STDB`/`STDE`(`ddMMMyyHHmm`,大写)。 -这些字段是否沿用、时间改用何种无歧义格式,都要核对后定稿。新版日计划是 AODB 当前时刻的完整航班列表(`US-07`),出站编码已定(`C-4`),不带日期筛选。 +旧系统的 `{startDate, endDate}` 与 `RQFD` 的 `STDB`/`STDE` 均不沿用:新版日计划是 AODB 当前时刻的完整航班列表(`US-07`),出站编码已定(`C-4`),不带日期筛选;`POST /schd/sync` 请求与响应口径见 `Q16`。旧系统响应体是否能作为 `POST /cminmsgs/send` 的定稿样例,见 `Q15`。 ## 入站报文与请求应答 @@ -40,8 +40,8 @@ AODB 经 CIIMS adapter 把 XML 报文写入 `CMINMSGS`,格式以架构指定 | 主题 | 已确定的消息语义 | 尚需确定 | |---|---|---| -| `msg` | 单条航班变更通知;航班动态与删除处理完成后投递;发送失败自动重试,一直失败的记录保留可查并告警(`US-08`);同一 `FLID` 的变更保序,对外按至少一次投递(`CLM-3`,单分区)。删除通知的来源有三处:`FDEL` 删除(`US-06`)、日计划快照覆盖范围内缺席删除(`US-07`)、历史清理在物理删除前必要时登记(架构 `D1`)。 | Kafka value 的字段与类型、变更和删除的区分方式(`Q5`)、编码方式。 | -| `schd` | 定时批量发送最新航班状态;两次 tick 之间积累的航班组成 `SCHD.FLTR` 数组 JSON,整批作为单条 record 发出(沿用旧系统,`Q1`);空字段不输出,字段与类型见 [XSD](../legacy/unisysaodbsis.xsd) 的 `FLTR`;没有变化不发;删除航班不进入本主题,由 `msg` 发一条删除通知;发送失败自动重试,对外按至少一次投递(`US-08`)。 | 消费方按数组格式解析的确认(`Q1`)。 | +| `msg` | 单条航班变更通知;Kafka value 是整条 `MSG` 的 JSON(`META` 加对应业务体,空字段不输出),变更与删除由 `META` 的类型与子类型区分(`Q5`);航班动态与删除处理完成后投递;发送失败自动重试,一直失败的记录保留可查并告警(`US-08`);同一 `FLID` 的变更保序,对外按至少一次投递(`CLM-3`,单分区)。删除通知的来源有三处:`FDEL` 删除(`US-06`)、日计划快照覆盖范围内缺席删除(`US-07`)、历史清理在物理删除前必要时登记(架构 `D1`)。 | 编码方式(JSON 之外的压缩或封装是否引入)。 | +| `schd` | 定时批量发送最新航班状态;两次 tick 之间积累的航班组成 `SCHD.FLTR` 数组 JSON,整批作为单条 record 发出,条数不设上限(沿用旧系统,`Q1`);空字段不输出,字段与类型见 [XSD](../legacy/unisysaodbsis.xsd) 的 `FLTR`;没有变化不发;删除航班不进入本主题,由 `msg` 发一条删除通知;发送失败自动重试,对外按至少一次投递(`US-08`)。 | — | 旧系统线索(来源:旧项目用户故事「前端通知」「动态类(FLOP-*)处理」): @@ -75,13 +75,13 @@ AODB 经 CIIMS adapter 把 XML 报文写入 `CMINMSGS`,格式以架构指定 | 存储 | 承载内容 | 边界 | 尚需确定 | |---|---|---|---| -| Redis | 航班查询投影 | 只作查询,不是权威,也不存处理状态;只由本系统写入和移除,网页客户端与 `GET /all/flights` 读同一份,内容来自自有 PostgreSQL 的航班当前态;写投影成功、删除时移除成功,才算对应消息处理完成(架构「数据归属与一致性」;`US-05`、`US-06`)。 | 网页客户端读取约定(`Q6`)。 | +| Redis | 航班查询投影 | 只作查询,不是权威,也不存处理状态;只由本系统写入和移除,网页客户端经 `GET /all/flights` 读同一份、不直连 Redis,内容来自自有 PostgreSQL 的航班当前态;投影 value 与 `KAFKA:schd` 数组元素、查询成功体元素同形,均为日计划 `FLTR` 转成的 JSON(`C-9`、`C-11`);写投影成功、删除时移除成功,才算对应消息处理完成(架构「数据归属与一致性」;`US-05`、`US-06`)。 | — | 旧系统线索(来源:旧项目用户故事「Redis key 汇总」「术语与数据语义」「动态航班转历史」): -- 投影是 hash `flightInfo`:field 为 `FLID`,value 为完整 `SCHD.FLTR` 对象的带类型 JSON,不设过期;Redis 里没有名为 `schd` 的 key,`schd` 只是 Kafka 主题。 +- 投影是 hash `flightInfo`:field 为 `FLID`,value 为完整 `SCHD.FLTR` 对象的带类型 JSON,不设过期;Redis 里没有名为 `schd` 的 key,`schd` 只是 Kafka 主题。新版 value 仍是 `FLTR` 转 JSON,但不使用旧系统的 Jackson 默认类型标注。 - 写入路径:日计划下载(`DNLD`/`RESP`)整体写入当天航班,单条变更(`ADFT`、`FLOP`)只写对应的一条,转历史时按 `FLID` 逐条移除。整体写入不删除本次映射中缺席的航班,与 `US-07` AC5 相反,新版按 `US-07` AC5 在覆盖范围内刷新。 -- 写入前生成主航班的共享航班列表 `MAFL`(`G-MAFL`),是否为网页客户端所需仍未定(`Q6`);登机桥字段 `abdg` 本版不提供——旧系统拼它的数据源是机位与登机桥映射缓存,已列入需求「范围与非目标」不交付。 +- 写入前生成主航班的共享航班列表 `MAFL`,共享航班不单列、随主航班下发(`Q6`);旧系统在日计划下载时按 `MAID` 把共享航班挂进主航班 `MAFL`,登机桥字段 `abdg` 本版不提供——旧系统拼它的数据源是机位与登机桥映射缓存,已列入需求「范围与非目标」不交付。 ### 自有 PostgreSQL:内部存储与 admin-api 只读 @@ -91,7 +91,7 @@ AODB 经 CIIMS adapter 把 XML 报文写入 `CMINMSGS`,格式以架构指定 | 航班当前态表 | 航班当前态的唯一权威;`FLID` 唯一(`INV-6`);Redis 和 Kafka 从处理结果派生,不反向覆盖这些表。 | 主键、字段与类型、外键/索引。 | | 内部处理表 | 管道处理、请求跟踪、留痕与互斥由本系统维护;不对外提供直接读写接口。 | 字段与约束由内部实现设计确定。 | -admin-api 还从本系统数据库只读季度计划;供数方与报文形态待 `Q25` 确定。 +季度计划不属本系统边界:admin-api 直接读 Oracle `FIMS_FLIGHTSCHD_SEASON`(`Q25`、[legacy/flight-apis.md](../legacy/flight-apis.md)),本系统只提供日计划请求。 自有 PostgreSQL 的物理表结构由本系统的迁移定稿。生产环境若改用 Oracle 11g,字段类型需先完成适配验证。 @@ -130,4 +130,4 @@ admin-api 还从本系统数据库只读季度计划;供数方与报文形态 ## 待决事项 -由本系统与需求方确定的事项见 [specification.md](../specification.md)「本系统与需求方待决」。 +由本系统与需求方确定的事项见 [specification.md](../specification.md)「待确认事项台账」。 diff --git a/docs/implementation.md b/docs/implementation.md index ccc5f55..e6b13dc 100644 --- a/docs/implementation.md +++ b/docs/implementation.md @@ -236,7 +236,7 @@ PENDING → SENT → DONE - **只进不退**:`STATE_VERSION` 只升不降。 - **条件标记**:`SENT` 时 `WHERE STATE_VERSION = 本批版本`;已被新版本覆盖则下轮重发。 -发送:取未发送行(`PARAM:msgx.schd.flush-limit`)→ 整批 `SCHD.FLTR` JSON(`C-9`)→ 条件标 `SENT`。删航班只走 `msg`。 +发送:取未发送行(`PARAM:msgx.schd.flush-limit`)→ 把各行载荷聚成 `SCHD.FLTR` JSON 数组发出(`C-9`)→ 条件标 `SENT`。`MSG_EVENT` 里每条 `schd` 载荷、以及 Redis 投影 value,都是单条 `FLTR` JSON(由 PG 当前态重建,`C-11`);删航班只走 `msg`。 周期与批上限见 [reference.md](reference.md)。`msg` 与 `schd` 不承诺顺序。 diff --git a/docs/legacy/msgexchange-api-legacy-user-stories.md b/docs/legacy/msgexchange-api-legacy-user-stories.md index ac1aec5..d304a92 100644 --- a/docs/legacy/msgexchange-api-legacy-user-stories.md +++ b/docs/legacy/msgexchange-api-legacy-user-stories.md @@ -410,7 +410,7 @@ msgexchange-api 为成都机场 OMMS 消息交换服务(`com.gzzn.omms:msgexch | `GET /flights/migrate` | — | 成功 | 迁移 `flight_hts` 至 `flight_hts2` | `FlightsController` | | `POST /kafka/topics/{name}/msgs` | JSON 或 XML | 成功 | 转发至指定 Kafka 主题 | `KafkaController` | -统一响应封装:`dto/ResponseDto.java`(字段 `is_success`、`err_code`、`err_msg`、`body`)与 `enums/ResultCode.java`。 +统一响应封装:`dto/ResponseDto.java`(字段 `is_success`、`err_code`、`err_msg`、`body`)与 `enums/ResultCode.java`。成功取 `ResultCode.SUCCESS`,其 `code=1`、`message="成功"`;`POST /cminmsgs/send` 成功样例为 `{"is_success":true,"err_code":1,"err_msg":"成功","body":}`,`POST /schd/sync` 成功时 `body` 为 `null`。工程内没有 `@ControllerAdvice` 等全局异常处理,未捕获异常按 Spring Boot 默认错误响应返回 HTTP 500,不是 `ResponseDto`;各环境配置均未设置请求体大小上限。 ### 5.2 定时任务与后台线程 diff --git a/docs/requirements.md b/docs/requirements.md index 0c9f347..b1c7488 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -19,7 +19,7 @@ - 不调用 admin-api,不从 admin-api 拉取、补全或合并任何数据。 - 不在 Redis 缓存机位基础数据与登机桥映射,本版不交付(`C-10`)。 -**查询侧(admin-api)**:admin-api 从本系统数据库只读基础数据与季度计划(供数方待 `Q25`);历史航班检索读 Elasticsearch(`Q23`)。其余能力(航班动态导出、字典)不由本系统提供。 +**查询侧(admin-api)**:admin-api 从本系统数据库只读基础数据;历史航班检索读 Elasticsearch(`Q23`);季度计划由 admin-api 直接读 Oracle `FIMS_FLIGHTSCHD_SEASON`,不经本系统(`Q25`)。其余能力(航班动态导出、字典)不由本系统提供。 ## 2. 用户故事 diff --git a/docs/specification.md b/docs/specification.md index b3ea6dd..85325f4 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -47,7 +47,7 @@ ### 2.2 上游(AODB / SIS) - **C-3** 一条报文的身份 = `SNDR` + `TYPE` + `STYP` + `SEQN`。`SEQN` 自增,极少在消息服务器重启时重置;重置后不与历史冲突。 -- **C-4** 出站请求写入 `COUTMSGS`;本系统只保证写入信箱,不保证 AODB 收到。编码:`SNDR=OMMS`;`SEQN` 本系统生成;`DTTM` 北京时间 `YYYYMMDDHHMMSS`;`RQRD` 共 14 种子类型(以 SIS 为准);`RQFD` 的 `STYP=NONE`,全量不带 `STDB`/`STDE` 筛选。 +- **C-4** 出站请求写入 `COUTMSGS`;本系统只保证写入信箱,不保证 AODB 收到。编码:`SNDR=OMMS`;`SEQN` 本系统生成;`DTTM` 北京时间 `YYYYMMDDHHMMSS`;`RQRD` 共 14 种子类型(以 SIS 为准);`RQFD` 的 `STYP=NONE`,全量不带 `STDB`/`STDE` 筛选。季度计划不属本系统出站范围(`Q25`)。 - **C-5** 删航班只打删除标记;主航班与共享航班各自独立标记。 - **C-6** 日计划快照里没带的字段,视为 AODB 已删掉该值,本地也清掉(`US-07` AC3)。 @@ -55,15 +55,13 @@ - **C-7** `POST /cminmsgs/send`:XML 写进入站信箱,与 adapter 走同一套处理。成功返回消息编号(只表示已写入、尚未处理);失败不返回编号。接受 `text/xml`、`application/xml`、`text/plain`(UTF-8);拒收空报文、超长、非法 XML;解析禁止访问外部资源。内网访问由网络层控制;仅供内部联调。 - 待确认:大小上限、HTTP 状态码与响应体 → `Q15`。 -- **C-8** `POST /schd/sync`:登记一次 `RQFD` 日计划请求。同类型若还有未处理完的请求,新请求先不写信箱,等旧的处理完再写 `COUTMSGS`(`US-09` AC1)。 - - 待确认:请求参数与 HTTP 响应 → `Q16`。 +- **C-8** `POST /schd/sync`:登记一次 `RQFD` 日计划请求。请求体为空、请求全量(`C-4`);成功返回请求编号,已有未结案请求时返回 `409`(`Q16`)。同类型若还有未处理完的请求,新请求先不写信箱,等旧的处理完再写 `COUTMSGS`(`US-09` AC1)。 ### 2.4 下游(admin-api、网页客户端) -- **C-9** 航班变更发 Kafka:`msg` 一条变更一条消息;`schd` 把两轮采集之间积累的变化合成一条消息,内容是 `SCHD.FLTR` JSON 数组。空字段不输出,没变化不发;删航班只走 `msg`;不设 message key;可能重复投递(待确认 `Q1`)。 +- **C-9** 航班变更发 Kafka:`msg` 一条变更一条消息,消息体是整条 `MSG` 的 JSON(`META` 加对应业务体),变更与删除由 `META` 的类型与子类型区分(`Q5`);`schd` 把两轮采集之间积累的变化合成一条消息,内容是 `SCHD.FLTR` JSON 数组,条数不设上限(`Q1`)。空字段不输出,没变化不发;删航班只走 `msg`;不设 message key;可能重复投递。 - **C-10** 静态参考数据写本系统库,admin-api 只读;本系统不调用 admin-api。 -- **C-11** 航班快照写 Redis;`GET /all/flights` 与网页客户端读同一份;JSON 由报文 XML 转换。 - - 待确认:`MAFL` 是否一并提供(`Q6`);状态码与响应包装(`Q21`)。 +- **C-11** 航班快照写 Redis;网页客户端经 `GET /all/flights` 读这一份,不直连 Redis;共享航班(`MAID` 非空)不单列返回(是否以主航班 `MAFL` 提供见 `Q6`)。成功 HTTP 200 返回裸 JSON 数组(不套旧 `ResponseDto`),不分页;失败 HTTP 503,JSON 对象 `{"error":"FLIGHT_PROJECTION_UNAVAILABLE","reason":"<细节>"}`;投影读失败不得返回 200 空数组(`INV-11`)。投影 value、`KAFKA:schd` 数组元素与查询成功体元素同形:都是日计划一条 `FLTR` 转成的 JSON(字段名与 [XSD](legacy/unisysaodbsis.xsd) `FLTR` 一致,空字段不输出,与 `C-9` 相同);由自有 PG 当前态重建,不是另起包装。 ## 3. 前提 @@ -113,12 +111,12 @@ | 编号 | 状态 | 事项 | 说明 | |---|---|---|---| -| Q1 | 待对方 | `schd` 一条 Kafka 消息里装多少变更 | 见 `C-9` | -| Q2 | 待对方 | FLOP 里 `SRVT`/`VIPF` 等集合段缺席是否等于删除 | 见 `G-SRVT-VIPF` | -| Q3 | 待对方 | SIS 没写明的 FLOP 子类型怎么解析、处理完算啥 | 按现有逻辑处理(`US-05` AC1);见 `G-FLOP-SEMANTICS`、`CLM-1` | -| Q4 | 待对方 | 上游重发时会不会改正文 | 假定内容不变,只认身份;改正文会被当重复跳过 | -| Q5 | 待对方 | `msg` 消息体格式、变更与删除怎么区分 | 沿用旧系统 JSON(接口契约「Kafka」) | -| Q6 | 待对方 | 网页客户端怎么读 Redis 快照 | 见 `C-11`;`MAFL` 是否提供未定 | +| Q1 | 已定 | `schd` 一条 Kafka 消息里装多少变更 | 两轮发送之间积累的全部变更合成一条,条数不设上限(`C-9`) | +| Q2 | 待对方 | FLOP 里 `SRVT`/`VIPF` 等集合段缺席是否等于删除 | 见 `G-SRVT-VIPF`;旧系统不解析这两段,无法对拍 | +| Q3 | 待对方 | SIS 没写明的 FLOP 子类型怎么解析、处理完算啥 | 按现有逻辑处理(`US-05` AC1);见 `G-FLOP-SEMANTICS`、`CLM-1`。旧系统找不到对应处理器时按失败兜底,仍写回处理时间且无业务改动 | +| Q4 | 待对方 | 上游重发时会不会改正文 | 假定内容不变,只认身份;改正文会被当重复跳过。旧系统不比较正文 | +| Q5 | 已定 | `msg` 消息体格式、变更与删除怎么区分 | 整条 `MSG` 的 JSON,空字段不输出;删除由 `META` 的类型与子类型标识(`C-9`) | +| Q6 | 本系统 | 网页客户端怎么读 Redis 快照 | 经 `GET /all/flights` 读、不直连 Redis(`C-11`);`MAFL` 是否随主航班返回仍待定,旧系统提供 | | Q7 | 已定 | 信箱编号只增不减、不重用 | `INV-1`、`CLM-2`、`US-01` AC3 | | Q8 | 已定 | 入站处理时间列名 `CMINMSGS_DATE_PROCESSED` | 术语「处理标记」、`US-10` | | Q9 | 已定 | 入站行删除条件与保留期 | 本系统只写回处理标记、不删除;清除与保留期由库方负责(`C-1`) | @@ -127,17 +125,17 @@ | Q12 | 已定 | 主航班与共享航班删除联动 | `US-06` AC2 | | Q13 | 已定 | `RQRD` / `RQFD` 编码字段 | `C-4` | | Q14 | 本系统 | 生产用 PostgreSQL 还是 Oracle 11g | Oracle 未验证前不作支持承诺 | -| Q15 | 本系统 | `POST /cminmsgs/send` HTTP 约定 | 大小上限、状态码、响应体;见 `C-7` | -| Q16 | 本系统 | `POST /schd/sync` 请求与响应 | 见 `C-8` | -| Q17 | 本系统 | 人工发 `RQRD` 的入口 | `US-09` 要求能人工发,HTTP 清单里没有 | -| Q18 | 本系统 | 回退时正在处理的消息怎么办 | 见 `OPS-4` | +| Q15 | 本系统 | `POST /cminmsgs/send` HTTP 约定 | 大小上限、状态码、最终响应体;见 `C-7`。旧系统成功响应是 `ResponseDto`(`err_code=1`,`body` 为信箱编号),且未配置大小上限 | +| Q16 | 已定 | `POST /schd/sync` 请求与响应 | 请求体为空、请求全量(`C-4`);成功返回请求编号,开放请求未结案返回 `409`(`C-8`) | +| Q17 | 本系统 | 人工发 `RQRD` 的入口 | `US-09` 要求能人工发,HTTP 清单里没有;旧系统也没有 | +| Q18 | 已定 | 回退时正在处理的消息怎么办 | 未写回处理标记的消息仍算未处理,由旧系统继续;旧系统停机只等在途任务跑完(`OPS-4`) | | Q19 | — | (未分配) | — | | Q20 | — | (未分配) | — | -| Q21 | 本系统 | `GET /all/flights` HTTP 约定 | 见 `C-11` | +| Q21 | 已定 | `GET /all/flights` HTTP 约定 | 成功裸数组、失败 503 与错误对象;见 `C-11` | | Q22 | 已定 | 静态参考数据对应哪些表 | `C-10`;物理为 schema `basicdata` 多表(非 `REF_MASTER` 单表),映射见 `implementation.md`「静态参考数据」 | -| Q23 | 本系统 | Elasticsearch 历史怎么写、保留多久 | 见 `US-14`、`G-FLIGHT-HIST-RETENTION` | -| Q24 | 本系统 | `REQ_TRACK` 已结案记录保留多久 | 见 `G-REQ-TRACK-RETENTION` | -| Q25 | 本系统 | 季度计划从哪来、什么格式 | SIS 只有日计划;旧系统读 Oracle 季度表 | +| Q23 | 本系统 | Elasticsearch 历史怎么写、保留多久 | 见 `US-14`、`G-FLIGHT-HIST-RETENTION`。旧系统按 `SODT`+`FLID` 写入 `flight_hts`,保留期未实现 | +| Q24 | 本系统 | `REQ_TRACK` 已结案记录保留多久 | 见 `G-REQ-TRACK-RETENTION`;旧系统没有对等记录,只有 `COUTMSGS` 的 ACK 列 | +| Q25 | 已定 | 季度计划从哪来、什么格式 | 不属本系统范围:admin-api 从 Oracle `FIMS_FLIGHTSCHD_SEASON` 读(`C-4`) | 状态说明:**待对方** = 需对接方确认;**已定** = 结论已写入 `C-x`/`INV-x`;**本系统** = 由本系统与需求方决定;**—** = 编号保留未用。 diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/ingress/FlightQueryController.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/ingress/FlightQueryController.kt index 1183736..2942c08 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/ingress/FlightQueryController.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/ingress/FlightQueryController.kt @@ -15,8 +15,8 @@ import io.micronaut.http.annotation.Produces * 数据只从投影来,不回落 PG:查询与网页客户端必须读同一份,不然两边会看到不同的航班 * (`INV-11`)。投影读不出来就返回错误——空列表会被前端当成"现在没有航班"(`US-12` AC2)。 * - * 响应口径暂定(`Q21`):成功是裸 JSON 数组、不套旧系统的 `ResponseDto`;失败是 503 加一个 - * 错误对象。投影载荷的形状与 `KAFKA:schd` 同一份,尚未按旧系统 `SCHD.FLTR` 逐字对拍。 + * 成功:裸 JSON 数组、不套旧系统的 `ResponseDto`;失败:503 加错误对象(`C-11`)。 + * 数组元素与 `KAFKA:schd` 同形,都是日计划 `FLTR` 转成的 JSON(`C-9`、`C-11`)。 */ @Controller("/all/flights") class FlightQueryController( @@ -45,7 +45,7 @@ class FlightQueryController( private fun nonSharedFlights(): List = projection.readAll() .map { mapper.readTree(it) } - .filter { it.path("scalars").path("MAID").asText("").isBlank() } + .filter { it.path("MAID").asText("").isBlank() } private companion object { private const val PROJECTION_UNAVAILABLE = "FLIGHT_PROJECTION_UNAVAILABLE" diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/processing/DynamicProcessors.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/processing/DynamicProcessors.kt index d4d32e4..cf4d7a3 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/processing/DynamicProcessors.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/processing/DynamicProcessors.kt @@ -189,13 +189,22 @@ class AdftProcessor( // 共享小工具(处理器层私有约定) // ===================================================================== -/** 航班整态载荷:`KAFKA:schd` 事件与 Redis 投影共用这一份形状。 */ -internal fun flightPayload(next: FlightSnapshot): Map = linkedMapOf( - "flid" to next.flid, - "stateVersion" to next.stateVersion, - "scalars" to next.scalars, - "collections" to next.collections, -) +/** + * 航班整态载荷:`KAFKA:schd` 事件与 Redis 投影共用。 + * + * 形状就是日计划一条 `FLTR` 转成 JSON(字段名与 SIS/XSD 一致,`C-9`、`C-11`):先 `FLID`,再标量,再集合段。 + * 空串与空集合不输出;内部的 `stateVersion` 不进载荷,只用于投影/事件代次守卫。 + */ +internal fun flightPayload(next: FlightSnapshot): Map { + val out = linkedMapOf("FLID" to next.flid) + next.scalars.forEach { (key, value) -> + if (value.isNotEmpty()) out[key] = value + } + next.collections.forEach { (key, rows) -> + if (rows.isNotEmpty()) out[key] = rows + } + return out +} /** * 删除通知:只走 `KAFKA:msg`,`schd` 不发 tombstone(`C-9`)。 diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/processing/FlightProjection.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/processing/FlightProjection.kt index faabe99..2888d8c 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/processing/FlightProjection.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/processing/FlightProjection.kt @@ -46,7 +46,7 @@ interface FlightProjectionPort { class FlightProjectionFailure(msgId: Long, cause: Throwable) : RuntimeException("flight projection write failed msgId=$msgId: ${cause.message ?: cause.javaClass.simpleName}", cause) -/** 投影载荷与 `KAFKA:schd` 整态同一份 JSON:投影和通知同源,对账时不用比两种形状(`C-11`)。 */ +/** 投影载荷与 `KAFKA:schd` 整态同一份:都是 `FLTR` 转 JSON(`C-9`、`C-11`)。 */ internal fun projectionOf(snapshot: FlightSnapshot, mapper: ObjectMapper): FlightProjectionWrite.Upsert = FlightProjectionWrite.Upsert( flid = snapshot.flid, diff --git a/src/test/kotlin/com/gzzn/omms/msgexchange/ingress/FlightQueryControllerTest.kt b/src/test/kotlin/com/gzzn/omms/msgexchange/ingress/FlightQueryControllerTest.kt index 5a137c5..391b340 100644 --- a/src/test/kotlin/com/gzzn/omms/msgexchange/ingress/FlightQueryControllerTest.kt +++ b/src/test/kotlin/com/gzzn/omms/msgexchange/ingress/FlightQueryControllerTest.kt @@ -8,7 +8,7 @@ import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test -/** `GET /all/flights`:投影读、过滤共享航班、失败时 503(`US-12`、`C-11`、`Q21` 暂定)。 */ +/** `GET /all/flights`:投影读、过滤共享航班、失败时 503(`US-12`、`C-11`)。 */ class FlightQueryControllerTest { private val mapper = ObjectMapper() @@ -16,8 +16,8 @@ class FlightQueryControllerTest { @Test fun `returns bare JSON array of non-shared flights`() { val projection = StubFlightProjectionPort() - projection.snapshots["1"] = """{"flid":"1","scalars":{"MAID":""}}""" - projection.snapshots["2"] = """{"flid":"2","scalars":{"MAID":"1"}}""" + projection.snapshots["1"] = """{"FLID":"1","FLNO":"CA001"}""" + projection.snapshots["2"] = """{"FLID":"2","FLNO":"CA002","MAID":"1"}""" val controller = FlightQueryController(projection, mapper) val response = controller.all() @@ -26,7 +26,8 @@ class FlightQueryControllerTest { val root = mapper.readTree(response.body()!!) assertTrue(root.isArray) assertEquals(1, root.size()) - assertEquals("1", root[0].path("flid").asText()) + assertEquals("1", root[0].path("FLID").asText()) + assertEquals("CA001", root[0].path("FLNO").asText()) } @Test diff --git a/src/test/kotlin/com/gzzn/omms/msgexchange/processing/FlightPayloadTest.kt b/src/test/kotlin/com/gzzn/omms/msgexchange/processing/FlightPayloadTest.kt new file mode 100644 index 0000000..4961a85 --- /dev/null +++ b/src/test/kotlin/com/gzzn/omms/msgexchange/processing/FlightPayloadTest.kt @@ -0,0 +1,45 @@ +package com.gzzn.omms.msgexchange.processing + +import com.gzzn.omms.msgexchange.domain.flight.FlightSnapshot +import com.gzzn.omms.msgexchange.domain.flight.FlightState +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +/** 投影/`schd` 载荷是日计划 `FLTR` 转 JSON(`C-9`、`C-11`)。 */ +class FlightPayloadTest { + + @Test + fun `emits FLTR-shaped JSON and omits empty fields and internal version`() { + val snapshot = FlightSnapshot( + flid = "121112312", + operationDay = null, + state = FlightState.ACTIVE, + stateVersion = 7, + scalars = mapOf( + "FLNO" to "UA724", + "MAID" to "", + "REMC" to "keep", + ), + collections = mapOf( + "GTDT" to listOf(mapOf("GATE" to "G28")), + "DELY" to emptyList(), + ), + ) + + val payload = flightPayload(snapshot) + + assertEquals("121112312", payload["FLID"]) + assertEquals("UA724", payload["FLNO"]) + assertEquals("keep", payload["REMC"]) + assertEquals(listOf(mapOf("GATE" to "G28")), payload["GTDT"]) + assertFalse(payload.containsKey("MAID")) + assertFalse(payload.containsKey("DELY")) + assertFalse(payload.containsKey("stateVersion")) + assertFalse(payload.containsKey("scalars")) + assertFalse(payload.containsKey("collections")) + assertFalse(payload.containsKey("flid")) + assertTrue(payload.keys.first() == "FLID") + } +}