feat(persistence)!: V1.4.0 退场 legacy 槽位存储,persistNextStates 成为唯一写路径 (ACM2-29 P4)
- V1.4.0 迁移:DROP FLIGHT_SCHD 全部 77 个槽位/里程碑/紧凑航路列(V1.3.0 起 零写入、P2-2 起零读取)+ 三个与 PK 前缀冗余的 (flid) 单列索引;回退窗口关闭 (对拍留档:P2-1 FS7 v2 双读差异记录) - 接口收敛:FlightSchdRepository 删除 upsertSnapshotBatch/upsertIncremental, 删除废弃 FlightStateRepository 及 Jdbc/Stub 适配器;仓储与装配器删除全部 legacy flatten/slot/milestone/route 视图代码,ALL_COLUMNS 收敛为无损承载列 - FS7 对拍工具移除 diffDualRead(legacy 槽位基准退场);diff() 对 legacy FLTR JSON 比对保留;FlightStoreDualReadTest 随工具移除 - 测试种子统一走 v2 写路径(support/SeedHelpers),GuardedFlightSchd 简化 - docs/decision-flight-state.md:头部复核入口更新 + 新增 §10 v2 落地终态索引, §1–§9 保留为历史记录 验证:MSGX_PG_PORT=5433 真实 PG 全量 100 用例 0 失败 0 跳过;无环境变量 Testcontainers 全新容器 V1.0.0→V1.4.0 迁移链 18/18 全绿
This commit is contained in:
@@ -7,8 +7,10 @@
|
||||
> **2026-09-08 复核入口**:详细证据与重构提案见 [运营航班状态设计评审](review-flight-state-2026-09-08.md)。
|
||||
> 本轮确认的部署边界:现场提供 Oracle 时使用 Oracle 11g,否则自行部署 PostgreSQL。
|
||||
> 评审保留单库原子提交方向,但发现 §9 固定资源槽位与本仓库三登机门样例冲突,
|
||||
> 并指出集合往返、迁移与快照 CAS 的风险。下文保留历史定案记录;新提案尚未实装,
|
||||
> 不应将历史“全部交付/保真”措辞视为双数据库生产验收证据。
|
||||
> 并指出集合往返、迁移与快照 CAS 的风险。**§9 全宽表·零子表方案已按 v2 设计撤销**,
|
||||
> 现行权威口径见 [§10 v2 落地](#10-acm2-29-v2-无损明细表方案落地2026-09-08)、
|
||||
> [flight-state-design-v2.md](flight-state-design-v2.md) 与
|
||||
> [flight-state-semantics.md](flight-state-semantics.md);§1–§9 保留为历史记录。
|
||||
|
||||
## 1. 问题
|
||||
|
||||
@@ -158,3 +160,23 @@ B-tree 逐列追加,11g/PG 零方言成本。
|
||||
|
||||
Oracle 11g 的 `MERGE INTO` 方言、Flyway 11.2 支持版本和 ojdbc 认证组合仍必须在目标数据库环境
|
||||
中完成平台决策与集成验收;不能用 PostgreSQL 测试结果替代该外部环境证据。
|
||||
|
||||
## 10. ACM2-29 v2 无损明细表方案落地(2026-09-08)
|
||||
|
||||
按 [评审报告](review-flight-state-2026-09-08.md)(F1–F9)与
|
||||
[flight-state-design-v2.md](flight-state-design-v2.md) 评审定案,撤销 §9「全宽表·零子表」
|
||||
作为权威存储形态,落地无损模型;本节为当前有效规则索引:
|
||||
|
||||
- **存储**:`FLIGHT_SCHD` 保留标量/异常前缀/无界文本列(V1.4.0 已 DROP 槽位、里程碑与紧凑
|
||||
航路列及冗余索引);10 类重复集合由 9 张明细表 + `flight_route_point(ROUTE_KIND)` 承载,
|
||||
PK `(FLID, ORDINAL)`,`SOURCE_SEQ` 不去重不设唯一(v2 §3.2)。
|
||||
- **写路径**:`persistNextStates` 是唯一写路径;引擎产生显式命令
|
||||
(Unchanged/Set/Clear/Replace/Apply),序号 0 条目仅在整集合清除时翻译为 Clear,
|
||||
混排 fail fast;异常对象 `"null"`/`{}`/空串 = 显式清除置 NULL。
|
||||
- **事务**:PIPELINE_LOCK 行锁内复核快照身份(`schd_gen.last_message_id`)与当前态,
|
||||
CAS 失败一律回滚 `FAILED(INFRA)`;提交后共享信箱回填失败落 `backfill_todo` 补偿重试,
|
||||
成功终态不降级(v2 §5)。
|
||||
- **读模型**:明细表为权威读;`flight_schd_display` 视图提供首/次资源与总数投影。
|
||||
- **双库**:`SqlDialect` 接缝(PG 实装;`Oracle11gDialect` 为编译级交付),
|
||||
11g 激活门控见 `db/migration/oracle11g/README.md`——现场 11.2 实测通过前不切换。
|
||||
- **字段语义唯一索引**:[flight-state-semantics.md](flight-state-semantics.md)。
|
||||
|
||||
@@ -101,11 +101,7 @@ interface FlightSchdRepository {
|
||||
val updatedAt: Instant = Instant.now(),
|
||||
)
|
||||
|
||||
/** 快照全量写入(DNLD):强行声明/更新 FDAY 归属,批处理写入,航班字段集整体替换。 */
|
||||
fun upsertSnapshotBatch(day: String, flights: List<Pair<String, FlightFields>>, now: Instant = Instant.now())
|
||||
|
||||
/** 增量更新(FLOP/ADFT):新插 FDAY=NULL,已有行保留原 FDAY;字段级合并。 */
|
||||
fun upsertIncremental(changes: List<com.gzzn.omms.msgexchange.domain.FlightChange>, now: Instant = Instant.now())
|
||||
/** v2(P4 退场):legacy upsertSnapshotBatch/upsertIncremental 已删除——唯一写路径是 persistNextStates。 */
|
||||
|
||||
/** 按代差删域化:仅删除 FDAY = day 且在 delFlids 中的记录(ADFT 与跨代已迁移行受保护)。 */
|
||||
fun deleteDiffByDay(day: String, delFlids: Collection<String>): Int
|
||||
@@ -256,17 +252,6 @@ interface PumpJobRepository {
|
||||
fun markFailed(jobId: Long, lastError: String)
|
||||
}
|
||||
|
||||
/**
|
||||
* 历史占位接口(已被 ACM2-28 之 FlightSchdRepository 取代,保留供兼容与过渡)。
|
||||
*/
|
||||
@Deprecated("Replaced by FlightSchdRepository in ACM2-28", ReplaceWith("FlightSchdRepository"))
|
||||
interface FlightStateRepository {
|
||||
/** 阶段 B 权威;replaceDay = 单事务删差集+写新代+版本提升。 */
|
||||
fun replaceDay(day: String, flights: List<Pair<String, FlightFields>>)
|
||||
|
||||
fun findByDay(day: String): List<Pair<String, FlightFields>>
|
||||
}
|
||||
|
||||
/**
|
||||
* 共享信箱 CMINMSGS 访问(ACM2-12:库属他人系统,本系统不建表)。
|
||||
*
|
||||
|
||||
+10
-140
@@ -1,15 +1,17 @@
|
||||
package com.gzzn.omms.msgexchange.infra.persistence.jdbc
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.FlightFields
|
||||
|
||||
/**
|
||||
* FLIGHT_SCHD 读侧视图组装(v2 双读对拍 FS7)。
|
||||
* FLIGHT_SCHD 读侧视图组装(v2 权威读,P4 退场后唯一读路径)。
|
||||
*
|
||||
* - [assembleMerged]:生产读路径,明细表优先、槽位列回退(与 legacy flightInfo hash 同构)。
|
||||
* - [assembleDetailOnly]:仅标量 + 明细表集合(v2 权威读)。
|
||||
* - [assembleLegacySlotsOnly]:仅标量 + V1.2.0 槽位/里程碑/航路列重建(legacy 回退读)。
|
||||
* - 标量列直读 + 无界集合文本列(SRVT/VIPF/MAFL)
|
||||
* - FDIV/FRET/FLAB 由主表前缀标量列重建对象
|
||||
* - 10 类重复集合由明细表重建(保序、保源序号),legacy 槽位/里程碑列已随 V1.4.0 退场
|
||||
*
|
||||
* 过渡期三条读路径(assembleMerged/assembleLegacySlotsOnly)与 FS7 双读对拍
|
||||
* 已随回退窗口关闭一并移除(V1.4.0,ACM2-29 P4)。
|
||||
*/
|
||||
object FlightSchdReadAssembler {
|
||||
|
||||
@@ -28,82 +30,14 @@ object FlightSchdReadAssembler {
|
||||
"FLAB_ARES", "FLAB_RSN",
|
||||
)
|
||||
|
||||
data class SlotCollection(
|
||||
val key: String,
|
||||
val seqAttr: String,
|
||||
val dedupeAttr: String,
|
||||
val slots: List<Map<String, String>>,
|
||||
)
|
||||
|
||||
private fun slotCollection(
|
||||
key: String,
|
||||
seqAttr: String,
|
||||
dedupeAttr: String,
|
||||
attrs: List<String>,
|
||||
count: Int,
|
||||
renames: Map<String, String> = emptyMap(),
|
||||
): SlotCollection = SlotCollection(
|
||||
key = key,
|
||||
seqAttr = seqAttr,
|
||||
dedupeAttr = dedupeAttr,
|
||||
slots = (1..count).map { i -> attrs.associateWith { attr -> (renames[attr] ?: attr) + i } },
|
||||
)
|
||||
|
||||
val OCCURRENCE_COLLECTIONS: List<SlotCollection> = listOf(
|
||||
slotCollection("GTDT", "GTNO", "GATE", listOf("GATE", "PGOT", "PGCT", "GOTM", "GCTM", "GTYP"), 2),
|
||||
slotCollection(
|
||||
"CKDT", "CKNO", "CHKC",
|
||||
listOf("CHKC", "CCLS", "PCOT", "PCCT", "COTM", "CCTM", "CTYP"), 3,
|
||||
),
|
||||
slotCollection(
|
||||
"CLDT", "CLNO", "BELT",
|
||||
listOf("BELT", "BCLS", "PCOT", "PCCT", "FBAG", "LBAG", "BTYP"), 2,
|
||||
renames = mapOf("PCOT" to "BPCOT", "PCCT" to "BPCCT"),
|
||||
),
|
||||
slotCollection("PSDT", "PSNO", "PSST", listOf("PSST", "STST", "STET"), 2),
|
||||
slotCollection(
|
||||
"CHDT", "CHNO", "CHUT",
|
||||
listOf("CHUT", "CCLS", "PCBT", "PCET", "CBTM", "CETM", "CTYP"), 2,
|
||||
renames = mapOf("CCLS" to "CHCLS", "CTYP" to "CHTYP"),
|
||||
),
|
||||
)
|
||||
|
||||
val MILESTONE_COLUMNS: List<String> = listOf(
|
||||
"DELY_CODE", "DELY_STRT", "DELY_DURA", "DELY_REMC",
|
||||
"ABTM_A", "ABTM_D", "CHOT_ON", "CHOT_OFF",
|
||||
)
|
||||
|
||||
val PATH_COLUMNS: List<String> = listOf("ROUT_PATH", "ERUT_PATH")
|
||||
|
||||
val TEXT_COLUMNS: List<String> = listOf("SRVT_TEXT", "VIPF_TEXT", "MAFL_TEXT")
|
||||
|
||||
val ALL_COLUMNS: List<String> = SCALAR_COLUMNS + EXCEPTION_COLUMNS +
|
||||
OCCURRENCE_COLLECTIONS.flatMap { c -> c.slots.flatMap(Map<String, String>::values) } +
|
||||
MILESTONE_COLUMNS + PATH_COLUMNS + TEXT_COLUMNS
|
||||
/** 库列名全集(读侧 SELECT 稳定顺序;与 V1.4.0 后表结构一致)。 */
|
||||
val ALL_COLUMNS: List<String> = SCALAR_COLUMNS + EXCEPTION_COLUMNS + TEXT_COLUMNS
|
||||
|
||||
private val mapper = ObjectMapper()
|
||||
|
||||
/** 生产读路径:明细表优先,缺失时由槽位/里程碑列回退重建。 */
|
||||
fun assembleMerged(
|
||||
flid: String,
|
||||
row: Map<String, String?>,
|
||||
detailCollections: Map<String, String>,
|
||||
): FlightFields {
|
||||
val fields = assembleScalarsAndExceptions(flid, row)
|
||||
val detailKeys = detailCollections.keys
|
||||
if ("DELY" !in detailKeys) delayView(row)?.let { fields["DELY"] = it }
|
||||
if ("ABTM" !in detailKeys) airbridgeView(row)?.let { fields["ABTM"] = it }
|
||||
if ("CHOT" !in detailKeys) chocksView(row)?.let { fields["CHOT"] = it }
|
||||
if ("ROUT" !in detailKeys) row["ROUT_PATH"]?.let { fields["ROUT"] = routeView(it) }
|
||||
if ("ERUT" !in detailKeys) row["ERUT_PATH"]?.let { fields["ERUT"] = routeView(it) }
|
||||
OCCURRENCE_COLLECTIONS.forEach { c ->
|
||||
if (c.key !in detailKeys) slotView(c, row)?.let { fields[c.key] = it }
|
||||
}
|
||||
detailCollections.forEach { (key, value) -> fields[key] = value }
|
||||
return fields
|
||||
}
|
||||
|
||||
/** v2 权威读:标量 + 明细表集合,不读槽位/里程碑列。 */
|
||||
/** v2 权威读:标量 + 异常 + 文本列 + 明细表集合。 */
|
||||
fun assembleDetailOnly(
|
||||
flid: String,
|
||||
row: Map<String, String?>,
|
||||
@@ -114,18 +48,6 @@ object FlightSchdReadAssembler {
|
||||
return fields
|
||||
}
|
||||
|
||||
/** legacy 回退读:标量 + 槽位/里程碑/航路列重建,不叠加明细表。 */
|
||||
fun assembleLegacySlotsOnly(flid: String, row: Map<String, String?>): FlightFields {
|
||||
val fields = assembleScalarsAndExceptions(flid, row)
|
||||
delayView(row)?.let { fields["DELY"] = it }
|
||||
airbridgeView(row)?.let { fields["ABTM"] = it }
|
||||
chocksView(row)?.let { fields["CHOT"] = it }
|
||||
row["ROUT_PATH"]?.let { fields["ROUT"] = routeView(it) }
|
||||
row["ERUT_PATH"]?.let { fields["ERUT"] = routeView(it) }
|
||||
OCCURRENCE_COLLECTIONS.forEach { c -> slotView(c, row)?.let { fields[c.key] = it } }
|
||||
return fields
|
||||
}
|
||||
|
||||
private fun assembleScalarsAndExceptions(flid: String, row: Map<String, String?>): LinkedHashMap<String, String> {
|
||||
val fields = linkedMapOf<String, String>()
|
||||
fields["FLID"] = flid
|
||||
@@ -145,56 +67,4 @@ object FlightSchdReadAssembler {
|
||||
attrs.forEach { (attr, column) -> row[column]?.let { node.put(attr, it) } }
|
||||
return if (node.size() == 0) null else mapper.writeValueAsString(node)
|
||||
}
|
||||
|
||||
private fun delayView(row: Map<String, String?>): String? {
|
||||
val node = mapper.createObjectNode()
|
||||
listOf("CODE" to "DELY_CODE", "STRT" to "DELY_STRT", "DURA" to "DELY_DURA", "REMC" to "DELY_REMC")
|
||||
.forEach { (attr, column) -> row[column]?.let { node.put(attr, it) } }
|
||||
return if (node.size() == 0) null else mapper.writeValueAsString(arrayOf(node))
|
||||
}
|
||||
|
||||
private fun airbridgeView(row: Map<String, String?>): String? {
|
||||
val items = mutableListOf<ObjectNode>()
|
||||
row["ABTM_A"]?.let {
|
||||
items += mapper.createObjectNode().apply { put("ASNO", "1"); put("ABOP", "A"); put("AOTM", it) }
|
||||
}
|
||||
row["ABTM_D"]?.let {
|
||||
items += mapper.createObjectNode().apply { put("ASNO", "2"); put("ABOP", "D"); put("AOTM", it) }
|
||||
}
|
||||
return if (items.isEmpty()) null else mapper.writeValueAsString(items)
|
||||
}
|
||||
|
||||
private fun chocksView(row: Map<String, String?>): String? {
|
||||
val items = mutableListOf<ObjectNode>()
|
||||
row["CHOT_ON"]?.let {
|
||||
items += mapper.createObjectNode().apply { put("CSNO", "1"); put("CHID", "ON"); put("CHTM", it) }
|
||||
}
|
||||
row["CHOT_OFF"]?.let {
|
||||
items += mapper.createObjectNode().apply { put("CSNO", "1"); put("CHID", "OFF"); put("CHTM", it) }
|
||||
}
|
||||
return if (items.isEmpty()) null else mapper.writeValueAsString(items)
|
||||
}
|
||||
|
||||
private fun routeView(path: String): String {
|
||||
val items = path.split(",").mapIndexed { index, segment ->
|
||||
val parts = segment.split("/", limit = 3)
|
||||
mapper.createObjectNode().apply {
|
||||
put("RTNO", (index + 1).toString())
|
||||
listOf("APCD", "SCAT", "SCDT").forEachIndexed { i, attr ->
|
||||
if (i < parts.size && parts[i].isNotEmpty()) put(attr, parts[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
return mapper.writeValueAsString(items)
|
||||
}
|
||||
|
||||
private fun slotView(c: SlotCollection, row: Map<String, String?>): String? {
|
||||
val items = c.slots.mapIndexed { index, slot ->
|
||||
mapper.createObjectNode().apply {
|
||||
put(c.seqAttr, (index + 1).toString())
|
||||
slot.forEach { (attr, column) -> row[column]?.let { put(attr, it) } }
|
||||
}
|
||||
}.filterNot { it.size() <= 1 }
|
||||
return if (items.isEmpty()) null else mapper.writeValueAsString(items)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-270
@@ -12,7 +12,6 @@ import com.gzzn.omms.msgexchange.domain.RefUpsert
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.BackfillTodoRepository
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.FlightFields
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.FlightSchdRepository
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.FlightStateRepository
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.MsgEventRepository
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.PipelineTransactionManager
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.ProcStateRepository
|
||||
@@ -412,76 +411,16 @@ class JdbcFlightSchdRepository(
|
||||
"FLAB_ARES", "FLAB_RSN",
|
||||
)
|
||||
|
||||
/**
|
||||
* 平铺出现槽位集合(ACM2-29 零子表定案):槽位序号即报文序号属性(GTNO/CKNO/...),
|
||||
* 列名 = legacy 属性名 + 槽位号;跨集合同名属性按 V1.2.0 注释加专属前缀
|
||||
* (转盘时刻 BPCOT/BPCCT、通道等级/类型 CHCLS/CHTYP)。槽位数按现场地服
|
||||
* 规律定界(登机门 2、值机柜台 3、转盘 2、计划机位 2、离港通道 2),
|
||||
* 超界条目按定案丢弃,不再为 XSD maxOccurs=99 的理论上限设计。
|
||||
*/
|
||||
private data class SlotCollection(
|
||||
val key: String,
|
||||
val seqAttr: String,
|
||||
val dedupeAttr: String,
|
||||
val slots: List<Map<String, String>>, // 报文属性 → 库列名
|
||||
)
|
||||
|
||||
private fun slotCollection(
|
||||
key: String,
|
||||
seqAttr: String,
|
||||
dedupeAttr: String,
|
||||
attrs: List<String>,
|
||||
count: Int,
|
||||
renames: Map<String, String> = emptyMap(),
|
||||
): SlotCollection = SlotCollection(
|
||||
key = key,
|
||||
seqAttr = seqAttr,
|
||||
dedupeAttr = dedupeAttr,
|
||||
slots = (1..count).map { i -> attrs.associateWith { attr -> (renames[attr] ?: attr) + i } },
|
||||
)
|
||||
|
||||
private val OCCURRENCE_COLLECTIONS: List<SlotCollection> = listOf(
|
||||
slotCollection("GTDT", "GTNO", "GATE", listOf("GATE", "PGOT", "PGCT", "GOTM", "GCTM", "GTYP"), 2),
|
||||
slotCollection(
|
||||
"CKDT", "CKNO", "CHKC",
|
||||
listOf("CHKC", "CCLS", "PCOT", "PCCT", "COTM", "CCTM", "CTYP"), 3,
|
||||
),
|
||||
slotCollection(
|
||||
"CLDT", "CLNO", "BELT",
|
||||
listOf("BELT", "BCLS", "PCOT", "PCCT", "FBAG", "LBAG", "BTYP"), 2,
|
||||
renames = mapOf("PCOT" to "BPCOT", "PCCT" to "BPCCT"),
|
||||
),
|
||||
slotCollection("PSDT", "PSNO", "PSST", listOf("PSST", "STST", "STET"), 2),
|
||||
slotCollection(
|
||||
"CHDT", "CHNO", "CHUT",
|
||||
listOf("CHUT", "CCLS", "PCBT", "PCET", "CBTM", "CETM", "CTYP"), 2,
|
||||
renames = mapOf("CCLS" to "CHCLS", "CTYP" to "CHTYP"),
|
||||
),
|
||||
)
|
||||
|
||||
/** 里程碑/单值集合列(延误单有效覆盖语义、靠撤桥/轮挡时刻)。 */
|
||||
private val MILESTONE_COLUMNS: List<String> = listOf(
|
||||
"DELY_CODE", "DELY_STRT", "DELY_DURA", "DELY_REMC",
|
||||
"ABTM_A", "ABTM_D", "CHOT_ON", "CHOT_OFF",
|
||||
)
|
||||
|
||||
/** 紧凑航路字串列("APCD/SCAT/SCDT,...",≤7 站)。 */
|
||||
private val PATH_COLUMNS: List<String> = listOf("ROUT_PATH", "ERUT_PATH")
|
||||
|
||||
/** 无界集合紧凑 JSON 数组字串列(键名去 _TEXT 后缀即 legacy 视图键,直存直取)。 */
|
||||
private val TEXT_COLUMNS: List<String> = listOf("SRVT_TEXT", "VIPF_TEXT", "MAFL_TEXT")
|
||||
|
||||
/** v2 主表写入列:标量 + 异常 + 无界文本;集合由明细表承载,不经槽位/里程碑列。 */
|
||||
/** v2 主表写入列:标量 + 异常 + 无界文本;集合由明细表承载(P4 起槽位/里程碑/航路列已退场)。 */
|
||||
private val SCALAR_WRITE_COLUMNS: List<String> = SCALAR_COLUMNS + EXCEPTION_COLUMNS + TEXT_COLUMNS
|
||||
|
||||
/** 库列名全集(读侧 SELECT 与视图重建的稳定顺序,与 V1.2.0 迁移一致)。 */
|
||||
/** 库列名全集(读侧 SELECT 稳定顺序;P4 退场后 = 无损承载列,与 V1.4.0 后表结构一致)。 */
|
||||
private val ALL_COLUMNS: List<String> = FlightSchdReadAssembler.ALL_COLUMNS
|
||||
|
||||
private val SCALAR_KEY_SET: Set<String> = SCALAR_COLUMNS.toSet()
|
||||
|
||||
/** 16 个集合视图键:写侧消费平铺、读侧重建,视图与 legacy flightInfo hash 同构。 */
|
||||
private val COLLECTION_KEYS: Set<String> = OCCURRENCE_COLLECTIONS.map { it.key }.toSet() +
|
||||
setOf("DELY", "ABTM", "CHOT", "ROUT", "ERUT", "SRVT", "VIPF", "MAFL", "FDIV", "FRET", "FLAB")
|
||||
}
|
||||
|
||||
private val mapper = ObjectMapper()
|
||||
@@ -489,37 +428,6 @@ class JdbcFlightSchdRepository(
|
||||
private fun toSqlDate(day: String): java.sql.Date =
|
||||
java.sql.Date.valueOf(day.trim().take(10))
|
||||
|
||||
/** 集合键平铺 + 标量键校验 → (列名, 值);未知键拒绝写入(fail fast,经处理边界落 FAILED(INFRA))。
|
||||
* FLID 即主键列:写侧忽略该键(主键已承载),读侧合成返回。 */
|
||||
private fun storagePairs(fields: FlightFields): List<Pair<String, String?>> =
|
||||
flattenForStorage(fields).mapNotNull { (key, value) ->
|
||||
if (key == "FLID") null else key to value
|
||||
}
|
||||
|
||||
/** 字段集 → 列名键值(标量键同名直取;16 个集合键消费为平铺列/紧凑字串列;
|
||||
* 未知键 fail fast)。值为 null 表示显式清空(单资源集合级全量快照替换语义)。 */
|
||||
private fun flattenForStorage(fields: FlightFields): Map<String, String?> {
|
||||
val out = mutableMapOf<String, String?>()
|
||||
fields.forEach { (key, value) ->
|
||||
when {
|
||||
key in SCALAR_KEY_SET -> out[key] = value
|
||||
key == "FLID" || key in COLLECTION_KEYS -> Unit // 主键跳过;集合键由下方平铺消费
|
||||
else -> throw IllegalArgumentException("unknown flight field: $key")
|
||||
}
|
||||
}
|
||||
flattenExceptions(fields, out)
|
||||
OCCURRENCE_COLLECTIONS.forEach { c -> flattenSlots(c, fields[c.key], out) }
|
||||
flattenDelay(fields["DELY"], out)
|
||||
flattenAirbridge(fields["ABTM"], out)
|
||||
flattenChocks(fields["CHOT"], out)
|
||||
fields["ROUT"]?.let { out["ROUT_PATH"] = routeToPath(it) }
|
||||
fields["ERUT"]?.let { out["ERUT_PATH"] = routeToPath(it) }
|
||||
fields["SRVT"]?.let { out["SRVT_TEXT"] = it }
|
||||
fields["VIPF"]?.let { out["VIPF_TEXT"] = it }
|
||||
fields["MAFL"]?.let { out["MAFL_TEXT"] = it }
|
||||
return out
|
||||
}
|
||||
|
||||
/** v2:仅标量/异常/文本列 flatten;集合键由明细表读写,不经过槽位列。 */
|
||||
private fun flattenScalarsOnly(scalars: Map<String, String>): Map<String, String?> {
|
||||
val out = mutableMapOf<String, String?>()
|
||||
@@ -552,170 +460,6 @@ class JdbcFlightSchdRepository(
|
||||
flatten("FLAB", mapOf("ARES" to "FLAB_ARES"), "FLAB_RSN")
|
||||
}
|
||||
|
||||
/** 出现槽位集合 → 槽位序号列。序号属性 "0" = 显式删除标记;按资源号抹平复用冗余;
|
||||
* 超出现场定界槽位数的条目按定案丢弃;集合键出现但条目为空 = 全槽位清空。 */
|
||||
private fun flattenSlots(c: SlotCollection, raw: String?, out: MutableMap<String, String?>) {
|
||||
if (raw == null) return
|
||||
val seen = mutableSetOf<String>()
|
||||
val items = collectionItems(raw)
|
||||
.filterNot { it[c.seqAttr]?.takeUnless(JsonNode::isNull)?.asText() == "0" }
|
||||
.filter { item ->
|
||||
val dedupeKey = item[c.dedupeAttr]?.takeUnless(JsonNode::isNull)?.asText()
|
||||
dedupeKey == null || seen.add(dedupeKey)
|
||||
}
|
||||
.take(c.slots.size)
|
||||
c.slots.forEachIndexed { index, slot ->
|
||||
val item = items.getOrNull(index)
|
||||
slot.forEach { (attr, column) ->
|
||||
out[column] = item?.get(attr)?.takeUnless(JsonNode::isNull)?.asText()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 延误:业务上任意时刻仅 1 个有效延误(覆盖语义)→ 首条延误平铺。 */
|
||||
private fun flattenDelay(raw: String?, out: MutableMap<String, String?>) {
|
||||
if (raw == null) return
|
||||
val first = collectionItems(raw).firstOrNull()
|
||||
out["DELY_CODE"] = first?.get("CODE")?.takeUnless(JsonNode::isNull)?.asText()
|
||||
out["DELY_STRT"] = first?.get("STRT")?.takeUnless(JsonNode::isNull)?.asText()
|
||||
out["DELY_DURA"] = first?.get("DURA")?.takeUnless(JsonNode::isNull)?.asText()
|
||||
out["DELY_REMC"] = first?.let { f ->
|
||||
listOf("REMC", "value", "#text").asSequence()
|
||||
.mapNotNull { name -> f[name]?.takeUnless(JsonNode::isNull)?.asText() }
|
||||
.firstOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
/** 靠撤桥:按 ABOP 取靠桥(A)/撤桥(D) 时刻;桥号沿用派生列 ABDG。 */
|
||||
private fun flattenAirbridge(raw: String?, out: MutableMap<String, String?>) {
|
||||
if (raw == null) return
|
||||
val items = collectionItems(raw)
|
||||
fun timeOf(op: String): String? = items.firstOrNull { it["ABOP"]?.asText() == op }
|
||||
?.get("AOTM")?.takeUnless(JsonNode::isNull)?.asText()
|
||||
out["ABTM_A"] = timeOf("A")
|
||||
out["ABTM_D"] = timeOf("D")
|
||||
}
|
||||
|
||||
/** 轮挡:按 CHID 取上轮挡(ON)/下轮挡(OFF) 时刻;机位沿用派生列 STND。 */
|
||||
private fun flattenChocks(raw: String?, out: MutableMap<String, String?>) {
|
||||
if (raw == null) return
|
||||
val items = collectionItems(raw)
|
||||
fun timeOf(id: String): String? = items.firstOrNull { it["CHID"]?.asText() == id }
|
||||
?.get("CHTM")?.takeUnless(JsonNode::isNull)?.asText()
|
||||
out["CHOT_ON"] = timeOf("ON")
|
||||
out["CHOT_OFF"] = timeOf("OFF")
|
||||
}
|
||||
|
||||
/** 航路集合 → 紧凑字串 "APCD/SCAT/SCDT,..."(空属性保留空槽位;空集合 → NULL = 显式清空)。 */
|
||||
private fun routeToPath(raw: String): String? {
|
||||
val items = collectionItems(raw)
|
||||
if (items.isEmpty()) return null
|
||||
return items.joinToString(",") { item ->
|
||||
listOf("APCD", "SCAT", "SCDT").joinToString("/") { attr ->
|
||||
item[attr]?.takeUnless(JsonNode::isNull)?.asText() ?: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 集合值统一形状:JSON 数组(多值)/对象(单值)/null(清空);其余形状 fail fast。 */
|
||||
private fun collectionItems(raw: String): List<JsonNode> {
|
||||
val node = mapper.readTree(raw)
|
||||
return when {
|
||||
node.isNull -> emptyList()
|
||||
node.isArray -> node.toList()
|
||||
node.isObject -> listOf(node)
|
||||
else -> throw IllegalArgumentException("collection value must be a JSON array or object")
|
||||
}
|
||||
}
|
||||
|
||||
override fun upsertSnapshotBatch(day: String, flights: List<Pair<String, FlightFields>>, now: Instant) {
|
||||
if (flights.isEmpty()) return
|
||||
// 快照 = 航班字段整体替换:缺省字段置 NULL,批量 SQL 形状对全部行一致
|
||||
val sql = buildString {
|
||||
append("INSERT INTO flight_schd (flid, fday, ")
|
||||
append(ALL_COLUMNS.joinToString(", "))
|
||||
append(", created_at, updated_at) VALUES (?, ?, ")
|
||||
append(ALL_COLUMNS.joinToString(", ") { "?" })
|
||||
append(", ?, ?) ON CONFLICT (flid) DO UPDATE SET ")
|
||||
append((listOf("fday") + ALL_COLUMNS + listOf("updated_at")).joinToString(", ") { "$it = EXCLUDED.$it" })
|
||||
}
|
||||
val sqlDate = toSqlDate(day)
|
||||
val sqlTimestamp = now.toSqlTimestamp()
|
||||
val conn = ds.obtainConnection()
|
||||
try {
|
||||
conn.prepareStatement(sql).use { ps ->
|
||||
var count = 0
|
||||
for ((flid, fields) in flights) {
|
||||
val stored = flattenForStorage(fields)
|
||||
var i = 1
|
||||
ps.setString(i++, flid)
|
||||
ps.setDate(i++, sqlDate)
|
||||
for (column in ALL_COLUMNS) {
|
||||
ps.setString(i++, stored[column])
|
||||
}
|
||||
ps.setTimestamp(i++, sqlTimestamp)
|
||||
ps.setTimestamp(i++, sqlTimestamp)
|
||||
ps.addBatch()
|
||||
count++
|
||||
if (count % 200 == 0) {
|
||||
ps.executeBatch()
|
||||
}
|
||||
}
|
||||
if (count % 200 != 0) {
|
||||
ps.executeBatch()
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
conn.releaseIfNotInTransaction()
|
||||
}
|
||||
}
|
||||
|
||||
override fun upsertIncremental(changes: List<com.gzzn.omms.msgexchange.domain.FlightChange>, now: Instant) {
|
||||
if (changes.isEmpty()) return
|
||||
val sqlTimestamp = now.toSqlTimestamp()
|
||||
val conn = ds.obtainConnection()
|
||||
try {
|
||||
// 父行:新插 FDAY=NULL;已有行保留原 FDAY,仅推进 updated_at
|
||||
conn.prepareStatement(
|
||||
"INSERT INTO flight_schd (flid, created_at, updated_at) VALUES (?, ?, ?) " +
|
||||
"ON CONFLICT (flid) DO UPDATE SET updated_at = EXCLUDED.updated_at",
|
||||
).use { ps ->
|
||||
var count = 0
|
||||
for (c in changes) {
|
||||
ps.setString(1, c.flid)
|
||||
ps.setTimestamp(2, sqlTimestamp)
|
||||
ps.setTimestamp(3, sqlTimestamp)
|
||||
ps.addBatch()
|
||||
count++
|
||||
if (count % 200 == 0) {
|
||||
ps.executeBatch()
|
||||
}
|
||||
}
|
||||
if (count % 200 != 0) {
|
||||
ps.executeBatch()
|
||||
}
|
||||
}
|
||||
// 字段列更新(legacy flightInfo hash 同语义:仅新增/覆盖,不删除缺失字段;
|
||||
// 集合键出现 = 单资源集合级全量快照替换,缺槽位置 NULL)
|
||||
for (c in changes) {
|
||||
val columns = storagePairs(c.fields)
|
||||
if (columns.isNotEmpty()) {
|
||||
val sql = "UPDATE flight_schd SET " +
|
||||
columns.joinToString(", ") { "${it.first} = ?" } +
|
||||
", updated_at = ? WHERE flid = ?"
|
||||
ds.update(sql) { ps ->
|
||||
var i = 1
|
||||
columns.forEach { (_, value) -> ps.setString(i++, value) }
|
||||
ps.setTimestamp(i++, sqlTimestamp)
|
||||
ps.setString(i, c.flid)
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
conn.releaseIfNotInTransaction()
|
||||
}
|
||||
}
|
||||
|
||||
override fun deleteDiffByDay(day: String, delFlids: Collection<String>): Int {
|
||||
if (delFlids.isEmpty()) return 0
|
||||
FlightDetailTables.deleteForFlids(ds, delFlids)
|
||||
@@ -1131,15 +875,3 @@ class JdbcReqTrackRepository(
|
||||
}
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Requires(property = "msgx.stubs", notEquals = "true")
|
||||
@Requires(property = "datasources.default.enabled", value = "true")
|
||||
class JdbcFlightStateRepository(
|
||||
private val flightSchd: FlightSchdRepository,
|
||||
) : FlightStateRepository {
|
||||
override fun replaceDay(day: String, flights: List<Pair<String, FlightFields>>) =
|
||||
flightSchd.upsertSnapshotBatch(day, flights)
|
||||
|
||||
override fun findByDay(day: String): List<Pair<String, FlightFields>> =
|
||||
flightSchd.findByDay(day)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import com.gzzn.omms.msgexchange.infra.persistence.BackfillTodoRepository
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.CminmsgInboxRepository
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.FlightFields
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.FlightSchdRepository
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.FlightStateRepository
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.MsgEventRepository
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.PipelineTransactionManager
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.ProcStateRepository
|
||||
@@ -220,32 +219,6 @@ class StubFlightSchd : FlightSchdRepository {
|
||||
gens.clear()
|
||||
}
|
||||
|
||||
override fun upsertSnapshotBatch(day: String, flights: List<Pair<String, FlightFields>>, now: Instant) {
|
||||
for ((flid, fields) in flights) {
|
||||
val existing = records[flid]
|
||||
records[flid] = Record(
|
||||
flid = flid,
|
||||
fday = day,
|
||||
fields = fields,
|
||||
createdAt = existing?.createdAt ?: now,
|
||||
updatedAt = now,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun upsertIncremental(changes: List<com.gzzn.omms.msgexchange.domain.FlightChange>, now: Instant) {
|
||||
for (c in changes) {
|
||||
val existing = records[c.flid]
|
||||
records[c.flid] = Record(
|
||||
flid = c.flid,
|
||||
fday = existing?.fday, // preserve existing fday; null if new
|
||||
fields = (existing?.fields ?: emptyMap()) + c.fields, // 字段级合并
|
||||
createdAt = existing?.createdAt ?: now,
|
||||
updatedAt = now,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun deleteDiffByDay(day: String, delFlids: Collection<String>): Int {
|
||||
if (delFlids.isEmpty()) return 0
|
||||
val delSet = delFlids.toSet()
|
||||
@@ -421,18 +394,6 @@ class StubReqTrack : ReqTrackRepository {
|
||||
override fun markDone(reqId: Long) { rows[reqId]?.let { rows[reqId] = it.copy(state = "DONE") } }
|
||||
}
|
||||
|
||||
@Requires(property = "msgx.stubs", value = "true")
|
||||
@Singleton
|
||||
class StubFlightState(
|
||||
private val flightSchd: FlightSchdRepository,
|
||||
) : FlightStateRepository {
|
||||
override fun replaceDay(day: String, flights: List<Pair<String, FlightFields>>) =
|
||||
flightSchd.upsertSnapshotBatch(day, flights)
|
||||
|
||||
override fun findByDay(day: String): List<Pair<String, FlightFields>> =
|
||||
flightSchd.findByDay(day)
|
||||
}
|
||||
|
||||
@Requires(property = "msgx.stubs", value = "true")
|
||||
@Singleton
|
||||
class StubBackfillTodo : BackfillTodoRepository {
|
||||
|
||||
@@ -38,10 +38,6 @@ class FlightStoreDiffTool(
|
||||
MISSING_IN_PG,
|
||||
/** 非预期偏离:Redis 缺失且非已知迁移保护行 */
|
||||
UNEXPECTED_EXTRA_IN_PG,
|
||||
/** v2 双读:明细表读路径缺失该航班 */
|
||||
MISSING_IN_DETAIL,
|
||||
/** v2 双读:legacy 槽位读路径缺失该航班 */
|
||||
MISSING_IN_LEGACY,
|
||||
}
|
||||
|
||||
data class Deviation(
|
||||
@@ -92,60 +88,9 @@ class FlightStoreDiffTool(
|
||||
|
||||
|
||||
/**
|
||||
* FS7 v2 双读对拍:明细表组装视图 vs legacy 槽位/里程碑列组装视图。
|
||||
* 严格逐字段比对,禁止通过忽略 GTNO/第三资源/ABDG 等规则洗绿。
|
||||
* FS7 v2 双读对拍已随回退窗口关闭移除(V1.4.0,ACM2-29 P4):
|
||||
* legacy 槽位/里程碑列退场后无对比基准,损失性差异已由 P2-1 对拍记录留档。
|
||||
*/
|
||||
fun diffDualRead(
|
||||
detailViews: Map<String, FlightFields>,
|
||||
legacySlotViews: Map<String, FlightFields>,
|
||||
): DiffReport = diffFlightFieldMaps(detailViews, legacySlotViews, missingInPrimary = DeviationKind.MISSING_IN_DETAIL, missingInReference = DeviationKind.MISSING_IN_LEGACY)
|
||||
|
||||
/** 两侧均为 FlightFields 的严格逐字段比对(供双读对拍与单测复用)。 */
|
||||
fun diffFlightFieldMaps(
|
||||
primaryFlights: Map<String, FlightFields>,
|
||||
referenceFlights: Map<String, FlightFields>,
|
||||
missingInPrimary: DeviationKind = DeviationKind.MISSING_IN_PG,
|
||||
missingInReference: DeviationKind = DeviationKind.UNEXPECTED_EXTRA_IN_PG,
|
||||
): DiffReport {
|
||||
val allFlids = (primaryFlights.keys + referenceFlights.keys).toSortedSet()
|
||||
var matched = 0
|
||||
val unexpected = mutableListOf<Deviation>()
|
||||
|
||||
for (flid in allFlids) {
|
||||
val primary = primaryFlights[flid]
|
||||
val reference = referenceFlights[flid]
|
||||
|
||||
if (primary == null) {
|
||||
unexpected += Deviation(
|
||||
flid = flid,
|
||||
kind = missingInPrimary,
|
||||
legacyValue = reference,
|
||||
detail = "Flight present in reference view but missing in primary view",
|
||||
)
|
||||
continue
|
||||
}
|
||||
if (reference == null) {
|
||||
unexpected += Deviation(
|
||||
flid = flid,
|
||||
kind = missingInReference,
|
||||
pgValue = primary,
|
||||
detail = "Flight present in primary view but missing in reference view",
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
val mismatches = compareFlightFields(flid, primary, reference)
|
||||
if (mismatches.isEmpty()) matched++ else unexpected.addAll(mismatches)
|
||||
}
|
||||
|
||||
return DiffReport(
|
||||
totalPg = primaryFlights.size,
|
||||
totalLegacy = referenceFlights.size,
|
||||
matchedCount = matched,
|
||||
knownDeviations = emptyList(),
|
||||
unexpectedDeviations = unexpected,
|
||||
)
|
||||
}
|
||||
|
||||
/** 单航班 FlightFields 对称比对(集合值为 JSON 文本时递归解析)。 */
|
||||
internal fun compareFlightFields(flid: String, left: FlightFields, right: FlightFields): List<Deviation> {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
-- flight-state-design-v2 §7 步骤 5 (ACM2-29 P4): 旧损失性结构退场。
|
||||
-- v2 无损明细表已是唯一权威存储(V1.3.0 起),槽位/里程碑/紧凑航路列自 V1.3.0 起零写入、
|
||||
-- P2-2 起零读取;对拍(FS7 v2 双读 + 全结构回环矩阵)与升级链验证通过后关闭回退窗口,
|
||||
-- 本迁移物理删除这些列与其冗余索引。SRVT/VIPF/MAFL_TEXT 与异常前缀列为无损承载,保留。
|
||||
|
||||
ALTER TABLE flight_schd
|
||||
DROP COLUMN IF EXISTS gate1, DROP COLUMN IF EXISTS pgot1, DROP COLUMN IF EXISTS pgct1,
|
||||
DROP COLUMN IF EXISTS gotm1, DROP COLUMN IF EXISTS gctm1, DROP COLUMN IF EXISTS gtyp1,
|
||||
DROP COLUMN IF EXISTS gate2, DROP COLUMN IF EXISTS pgot2, DROP COLUMN IF EXISTS pgct2,
|
||||
DROP COLUMN IF EXISTS gotm2, DROP COLUMN IF EXISTS gctm2, DROP COLUMN IF EXISTS gtyp2,
|
||||
|
||||
DROP COLUMN IF EXISTS chkc1, DROP COLUMN IF EXISTS ccls1, DROP COLUMN IF EXISTS pcot1,
|
||||
DROP COLUMN IF EXISTS pcct1, DROP COLUMN IF EXISTS cotm1, DROP COLUMN IF EXISTS cctm1,
|
||||
DROP COLUMN IF EXISTS ctyp1, DROP COLUMN IF EXISTS chkc2, DROP COLUMN IF EXISTS ccls2,
|
||||
DROP COLUMN IF EXISTS pcot2, DROP COLUMN IF EXISTS pcct2, DROP COLUMN IF EXISTS cotm2,
|
||||
DROP COLUMN IF EXISTS cctm2, DROP COLUMN IF EXISTS ctyp2, DROP COLUMN IF EXISTS chkc3,
|
||||
DROP COLUMN IF EXISTS ccls3, DROP COLUMN IF EXISTS pcot3, DROP COLUMN IF EXISTS pcct3,
|
||||
DROP COLUMN IF EXISTS cotm3, DROP COLUMN IF EXISTS cctm3, DROP COLUMN IF EXISTS ctyp3,
|
||||
|
||||
DROP COLUMN IF EXISTS belt1, DROP COLUMN IF EXISTS bcls1, DROP COLUMN IF EXISTS bpcot1,
|
||||
DROP COLUMN IF EXISTS bpcct1, DROP COLUMN IF EXISTS fbag1, DROP COLUMN IF EXISTS lbag1,
|
||||
DROP COLUMN IF EXISTS btyp1, DROP COLUMN IF EXISTS belt2, DROP COLUMN IF EXISTS bcls2,
|
||||
DROP COLUMN IF EXISTS bpcot2, DROP COLUMN IF EXISTS bpcct2, DROP COLUMN IF EXISTS fbag2,
|
||||
DROP COLUMN IF EXISTS lbag2, DROP COLUMN IF EXISTS btyp2,
|
||||
|
||||
DROP COLUMN IF EXISTS psst1, DROP COLUMN IF EXISTS stst1, DROP COLUMN IF EXISTS stet1,
|
||||
DROP COLUMN IF EXISTS psst2, DROP COLUMN IF EXISTS stst2, DROP COLUMN IF EXISTS stet2,
|
||||
|
||||
DROP COLUMN IF EXISTS chut1, DROP COLUMN IF EXISTS chcls1, DROP COLUMN IF EXISTS pcbt1,
|
||||
DROP COLUMN IF EXISTS pcet1, DROP COLUMN IF EXISTS cbtm1, DROP COLUMN IF EXISTS cetm1,
|
||||
DROP COLUMN IF EXISTS chtyp1, DROP COLUMN IF EXISTS chut2, DROP COLUMN IF EXISTS chcls2,
|
||||
DROP COLUMN IF EXISTS pcbt2, DROP COLUMN IF EXISTS pcet2, DROP COLUMN IF EXISTS cbtm2,
|
||||
DROP COLUMN IF EXISTS cetm2, DROP COLUMN IF EXISTS chtyp2,
|
||||
|
||||
DROP COLUMN IF EXISTS dely_code, DROP COLUMN IF EXISTS dely_strt,
|
||||
DROP COLUMN IF EXISTS dely_dura, DROP COLUMN IF EXISTS dely_remc,
|
||||
DROP COLUMN IF EXISTS abtm_a, DROP COLUMN IF EXISTS abtm_d,
|
||||
DROP COLUMN IF EXISTS chot_on, DROP COLUMN IF EXISTS chot_off,
|
||||
|
||||
DROP COLUMN IF EXISTS rout_path, DROP COLUMN IF EXISTS erut_path;
|
||||
|
||||
-- V1.3.0 的三个 (flid) 单列索引与 PK (flid, ordinal) 前缀冗余(L2),一并清理
|
||||
DROP INDEX IF EXISTS idx_flight_gate_flid;
|
||||
DROP INDEX IF EXISTS idx_flight_checkin_flid;
|
||||
DROP INDEX IF EXISTS idx_flight_belt_flid;
|
||||
-15
@@ -231,7 +231,6 @@ class FlightSchdJdbcPgTest {
|
||||
)
|
||||
assertNull(repo.findByFlid(flid)!!["GTDT"])
|
||||
assertEquals(0, ds.query("SELECT COUNT(*) FROM flight_gate WHERE flid = ?", { ps -> ps.setString(1, flid) }) { rs -> rs.getInt(1) }.first())
|
||||
assertLegacyCollectionColumnsAllNull(flid)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -533,7 +532,6 @@ class FlightSchdJdbcPgTest {
|
||||
)
|
||||
|
||||
repo.persistNextStates(day, listOf(state), snapshotReplace = true)
|
||||
assertLegacyCollectionColumnsAllNull(flid)
|
||||
|
||||
val roundtrip = repo.findNextStateByFlid(flid)!!
|
||||
assertEquals(1L, roundtrip.stateVersion)
|
||||
@@ -585,7 +583,6 @@ class FlightSchdJdbcPgTest {
|
||||
)
|
||||
|
||||
repo.persistNextStates(day, listOf(state), snapshotReplace = true)
|
||||
assertLegacyCollectionColumnsAllNull(flid)
|
||||
assertEquals("MU123", repo.findByFlid(flid)!!["FLNO"])
|
||||
assertEquals(1, ds.query("SELECT COUNT(*) FROM flight_gate WHERE flid = ?", { ps -> ps.setString(1, flid) }) { rs -> rs.getInt(1) }.first())
|
||||
assertEquals(1, ds.query("SELECT COUNT(*) FROM flight_delay WHERE flid = ?", { ps -> ps.setString(1, flid) }) { rs -> rs.getInt(1) }.first())
|
||||
@@ -794,16 +791,4 @@ class FlightSchdJdbcPgTest {
|
||||
if (e != null && a != null) assertEquals(e, a, path) else assertEquals(expected, actual, path)
|
||||
}
|
||||
|
||||
private fun assertLegacyCollectionColumnsAllNull(flid: String) {
|
||||
val columnList = com.gzzn.omms.msgexchange.support.LEGACY_COLLECTION_STORAGE_COLUMNS.joinToString(", ")
|
||||
ds.queryOne(
|
||||
"SELECT $columnList FROM flight_schd WHERE flid = ?",
|
||||
{ ps -> ps.setString(1, flid) },
|
||||
) { rs ->
|
||||
com.gzzn.omms.msgexchange.support.LEGACY_COLLECTION_STORAGE_COLUMNS.forEach { column ->
|
||||
assertNull(rs.getString(column), "legacy column $column must be null for v2 persistNextStates (flid=$flid)")
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.gzzn.omms.msgexchange.jobs
|
||||
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.FlightSchdRepository
|
||||
import com.gzzn.omms.msgexchange.infra.stub.StubFlightSchd
|
||||
import com.gzzn.omms.msgexchange.support.seedSnapshot
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNotNull
|
||||
@@ -44,7 +45,7 @@ class HistorySweepJobTest {
|
||||
"F2" to mapOf("FLID" to "F2"),
|
||||
"F3" to mapOf("FLID" to "F3"),
|
||||
)
|
||||
flightSchd.upsertSnapshotBatch("2026-09-01", flights)
|
||||
seedSnapshot(flightSchd, "2026-09-01", flights)
|
||||
flightSchd.putGenIfVersion("2026-09-01", 0L, FlightSchdRepository.GenMeta("2026-09-01", 1L, setOf("F1", "F2", "F3")))
|
||||
|
||||
// 模拟判史:F1, F2, F3 全部为历史候选
|
||||
@@ -72,7 +73,7 @@ class HistorySweepJobTest {
|
||||
"F2" to mapOf("FLID" to "F2"),
|
||||
"F3" to mapOf("FLID" to "F3"),
|
||||
)
|
||||
flightSchd.upsertSnapshotBatch("2026-09-01", flights)
|
||||
seedSnapshot(flightSchd, "2026-09-01", flights)
|
||||
|
||||
HistorySweepJob.historyPicker = { it }
|
||||
// 模拟 ES 写入:F1 和 F2 成功,F3 失败
|
||||
@@ -95,7 +96,7 @@ class HistorySweepJobTest {
|
||||
"F1" to mapOf("FLID" to "F1"),
|
||||
"F2" to mapOf("FLID" to "F2"),
|
||||
)
|
||||
flightSchd.upsertSnapshotBatch("2026-09-01", flights)
|
||||
seedSnapshot(flightSchd, "2026-09-01", flights)
|
||||
|
||||
HistorySweepJob.historyPicker = { it }
|
||||
// 模拟 ES 全部写入失败
|
||||
@@ -114,7 +115,7 @@ class HistorySweepJobTest {
|
||||
val flights = listOf(
|
||||
"F1" to mapOf("FLID" to "F1"),
|
||||
)
|
||||
flightSchd.upsertSnapshotBatch("2026-09-01", flights)
|
||||
seedSnapshot(flightSchd, "2026-09-01", flights)
|
||||
|
||||
// 第一次运行模拟:ES 写入成功,但在调用 deleteByFlids 前崩溃(PG 依然保留 F1)
|
||||
val esUpserted = mutableSetOf<String>()
|
||||
@@ -151,7 +152,7 @@ class HistorySweepJobTest {
|
||||
@Test
|
||||
fun `Scenario 5 - Replaying deleteByFlids is completely idempotent with no side effects`() {
|
||||
val flights = listOf("F1" to mapOf("FLID" to "F1"))
|
||||
flightSchd.upsertSnapshotBatch("2026-09-01", flights)
|
||||
seedSnapshot(flightSchd, "2026-09-01", flights)
|
||||
|
||||
// 首次删除
|
||||
val deletedCount1 = flightSchd.deleteByFlids(setOf("F1"))
|
||||
|
||||
@@ -29,6 +29,8 @@ import com.gzzn.omms.msgexchange.infra.stub.StubMsgEvents
|
||||
import com.gzzn.omms.msgexchange.infra.stub.StubPipelineTransactionManager
|
||||
import com.gzzn.omms.msgexchange.infra.stub.StubProcState
|
||||
import com.gzzn.omms.msgexchange.infra.stub.StubReqTrack
|
||||
import com.gzzn.omms.msgexchange.support.seedIncremental
|
||||
import com.gzzn.omms.msgexchange.support.seedSnapshot
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
@@ -269,10 +271,10 @@ class FlightSchdInvariantTest {
|
||||
|
||||
// 1. 增量更新写入一条临时加飞航班 ADFT(FDAY 为 NULL)
|
||||
val adftChange = FlightChange(flid = "ADFT_888", fields = mapOf("FLID" to "ADFT_888", "FLTY" to "ADFT"))
|
||||
flightSchd.upsertIncremental(listOf(adftChange))
|
||||
seedIncremental(flightSchd, listOf(adftChange))
|
||||
|
||||
// 2. 写入旧代的一条定期计划航班 REG_OLD(FDAY 为 2026-09-07)
|
||||
flightSchd.upsertSnapshotBatch(day, listOf("REG_OLD" to mapOf("FLID" to "REG_OLD", "FLTY" to "REG")))
|
||||
seedSnapshot(flightSchd, day, listOf("REG_OLD" to mapOf("FLID" to "REG_OLD", "FLTY" to "REG")))
|
||||
flightSchd.putGenIfVersion(day, 0L, FlightSchdRepository.GenMeta(day, 1L, setOf("REG_OLD")))
|
||||
|
||||
// 3. 执行下一轮快照 DNLD,新代仅包含 REG_NEW(REG_OLD 不在新代中,属于待删差集;ADFT 也不在新代中)
|
||||
@@ -308,11 +310,11 @@ class FlightSchdInvariantTest {
|
||||
val dayNew = "2026-09-08"
|
||||
|
||||
// 初始属旧代
|
||||
flightSchd.upsertSnapshotBatch(dayOld, listOf("FL_MIG" to mapOf("FLID" to "FL_MIG", "REMC" to "07")))
|
||||
seedSnapshot(flightSchd, dayOld, listOf("FL_MIG" to mapOf("FLID" to "FL_MIG", "REMC" to "07")))
|
||||
flightSchd.putGenIfVersion(dayOld, 0L, FlightSchdRepository.GenMeta(dayOld, 1L, setOf("FL_MIG")))
|
||||
|
||||
// 随后 09-08 快照写入将 FDAY 更新为 2026-09-08
|
||||
flightSchd.upsertSnapshotBatch(dayNew, listOf("FL_MIG" to mapOf("FLID" to "FL_MIG", "REMC" to "08")))
|
||||
seedSnapshot(flightSchd, dayNew, listOf("FL_MIG" to mapOf("FLID" to "FL_MIG", "REMC" to "08")))
|
||||
|
||||
// 此时 09-07 再次执行差删(差集中包含 FL_MIG)
|
||||
val deleted = flightSchd.deleteDiffByDay(dayOld, listOf("FL_MIG"))
|
||||
@@ -334,7 +336,7 @@ class FlightSchdInvariantTest {
|
||||
TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of("Asia/Tokyo")))
|
||||
|
||||
val now = Instant.parse("2026-09-07T08:00:00.123456Z")
|
||||
flightSchd.upsertSnapshotBatch("2026-09-07", listOf("TZ_01" to mapOf("FLID" to "TZ_01")), now)
|
||||
seedSnapshot(flightSchd, "2026-09-07", listOf("TZ_01" to mapOf("FLID" to "TZ_01")), now)
|
||||
flightSchd.putGenIfVersion("2026-09-07", 0L, FlightSchdRepository.GenMeta("2026-09-07", 1L, setOf("TZ_01")), now)
|
||||
|
||||
val gen = flightSchd.getGen("2026-09-07")
|
||||
|
||||
@@ -156,13 +156,6 @@ class MessageProcessorTest {
|
||||
val gens = mutableMapOf<String, FlightSchdRepository.GenMeta>()
|
||||
val incrementalChanges = mutableListOf<com.gzzn.omms.msgexchange.domain.FlightChange>()
|
||||
|
||||
override fun upsertSnapshotBatch(day: String, flights: List<Pair<String, FlightFields>>, now: Instant) {
|
||||
this.flights.putAll(flights)
|
||||
}
|
||||
override fun upsertIncremental(changes: List<com.gzzn.omms.msgexchange.domain.FlightChange>, now: Instant) {
|
||||
incrementalChanges.addAll(changes)
|
||||
changes.forEach { flights[it.flid] = it.fields }
|
||||
}
|
||||
override fun deleteDiffByDay(day: String, delFlids: Collection<String>): Int = 0
|
||||
override fun findByFlid(flid: String): FlightFields? = flights[flid]
|
||||
override fun findByFlids(flids: Collection<String>): Map<String, FlightFields> =
|
||||
|
||||
-4
@@ -70,8 +70,6 @@ class V2PipelineWritePathContractTest {
|
||||
|
||||
assertEquals(ProcStatus.SUCCEEDED, procState.snapshotOf(headId)!!.state)
|
||||
assertEquals(1, flightSchd.persistNextStatesCalls)
|
||||
assertEquals(0, flightSchd.legacySnapshotCalls)
|
||||
assertEquals(0, flightSchd.legacyIncrementalCalls)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -109,7 +107,5 @@ class V2PipelineWritePathContractTest {
|
||||
|
||||
assertEquals(ProcStatus.SUCCEEDED, procState.snapshotOf(headId)!!.state)
|
||||
assertEquals(1, flightSchd.persistNextStatesCalls)
|
||||
assertEquals(0, flightSchd.legacySnapshotCalls)
|
||||
assertEquals(0, flightSchd.legacyIncrementalCalls)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.gzzn.omms.msgexchange.support
|
||||
|
||||
import com.gzzn.omms.msgexchange.domain.FlightChange
|
||||
import com.gzzn.omms.msgexchange.domain.flight.FlightStateEngine
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.FlightFields
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.FlightSchdRepository
|
||||
import java.time.Instant
|
||||
|
||||
/** 测试种子助手:v2 写路径(persistNextStates)等价替代已退场的 legacy upsert(P4)。 */
|
||||
fun seedSnapshot(
|
||||
repo: FlightSchdRepository,
|
||||
day: String,
|
||||
flights: List<Pair<String, FlightFields>>,
|
||||
now: Instant = Instant.now(),
|
||||
) {
|
||||
repo.persistNextStates(
|
||||
day,
|
||||
flights.map { (flid, fields) ->
|
||||
FlightStateEngine.apply(
|
||||
null,
|
||||
FlightStateEngine.commandsFromFields(flid, fields, snapshotReplace = true),
|
||||
"seed-$day",
|
||||
bumpVersion = false,
|
||||
).copy(stateVersion = 1L)
|
||||
},
|
||||
snapshotReplace = true,
|
||||
now = now,
|
||||
)
|
||||
}
|
||||
|
||||
fun seedIncremental(repo: FlightSchdRepository, changes: List<FlightChange>, messageId: String = "seed-inc") {
|
||||
repo.persistNextStates(
|
||||
null,
|
||||
changes.map { change ->
|
||||
FlightStateEngine.apply(
|
||||
repo.findNextStateByFlid(change.flid),
|
||||
FlightStateEngine.commandsFromFields(change.flid, change.fields, snapshotReplace = false),
|
||||
messageId,
|
||||
bumpVersion = true,
|
||||
)
|
||||
},
|
||||
snapshotReplace = false,
|
||||
)
|
||||
}
|
||||
@@ -1,46 +1,17 @@
|
||||
package com.gzzn.omms.msgexchange.support
|
||||
|
||||
import com.gzzn.omms.msgexchange.domain.FlightChange
|
||||
import com.gzzn.omms.msgexchange.domain.flight.FlightNextState
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.FlightFields
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.FlightSchdRepository
|
||||
import com.gzzn.omms.msgexchange.infra.stub.StubFlightSchd
|
||||
import java.time.Instant
|
||||
|
||||
/** V1.2.0 宽表集合存储列;v2 persistNextStates 不得写入。 */
|
||||
val LEGACY_COLLECTION_STORAGE_COLUMNS: List<String> = listOf(
|
||||
"gate1", "pgot1", "pgct1", "gotm1", "gctm1", "gtyp1",
|
||||
"gate2", "pgot2", "pgct2", "gotm2", "gctm2", "gtyp2",
|
||||
"chkc1", "ccls1", "pcot1", "pcct1", "cotm1", "cctm1", "ctyp1",
|
||||
"chkc2", "ccls2", "pcot2", "pcct2", "cotm2", "cctm2", "ctyp2",
|
||||
"chkc3", "ccls3", "pcot3", "pcct3", "cotm3", "cctm3", "ctyp3",
|
||||
"belt1", "bcls1", "bpcot1", "bpcct1", "fbag1", "lbag1", "btyp1",
|
||||
"belt2", "bcls2", "bpcot2", "bpcct2", "fbag2", "lbag2", "btyp2",
|
||||
"psst1", "stst1", "stet1", "psst2", "stst2", "stet2",
|
||||
"chut1", "chcls1", "pcbt1", "pcet1", "cbtm1", "cetm1", "chtyp1",
|
||||
"chut2", "chcls2", "pcbt2", "pcet2", "cbtm2", "cetm2", "chtyp2",
|
||||
"dely_code", "dely_strt", "dely_dura", "dely_remc",
|
||||
"abtm_a", "abtm_d", "chot_on", "chot_off",
|
||||
"rout_path", "erut_path",
|
||||
)
|
||||
|
||||
/** v2 写路径守卫(P4 退场后):persistNextStates 是唯一写路径,legacy upsert 入口已从接口删除。 */
|
||||
class GuardedFlightSchd(
|
||||
private val inner: FlightSchdRepository = StubFlightSchd(),
|
||||
) : FlightSchdRepository {
|
||||
var legacySnapshotCalls = 0
|
||||
var legacyIncrementalCalls = 0
|
||||
var persistNextStatesCalls = 0
|
||||
|
||||
override fun upsertSnapshotBatch(day: String, flights: List<Pair<String, FlightFields>>, now: Instant) {
|
||||
legacySnapshotCalls++
|
||||
throw AssertionError("v2 path must not call upsertSnapshotBatch")
|
||||
}
|
||||
|
||||
override fun upsertIncremental(changes: List<FlightChange>, now: Instant) {
|
||||
legacyIncrementalCalls++
|
||||
throw AssertionError("v2 path must not call upsertIncremental")
|
||||
}
|
||||
|
||||
override fun persistNextStates(day: String?, states: List<FlightNextState>, snapshotReplace: Boolean, now: Instant) {
|
||||
persistNextStatesCalls++
|
||||
inner.persistNextStates(day, states, snapshotReplace, now)
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
package com.gzzn.omms.msgexchange.tools
|
||||
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.jdbc.FlightSchdReadAssembler
|
||||
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
|
||||
|
||||
/**
|
||||
* FS7 v2 双读对拍:明细表组装 vs legacy 槽位列组装。
|
||||
* 反例来自 review-flight-state-2026-09-08 §12.1 / SIS §3.34.3(G28/G33/G23)。
|
||||
*/
|
||||
class FlightStoreDualReadTest {
|
||||
|
||||
private val tool = FlightStoreDiffTool()
|
||||
private val flid = "TEST_DUAL_READ"
|
||||
|
||||
private fun detailView(vararg collections: Pair<String, String>) = mapOf(
|
||||
flid to linkedMapOf(
|
||||
"FLID" to flid,
|
||||
*collections,
|
||||
),
|
||||
)
|
||||
|
||||
private fun legacyView(vararg collections: Pair<String, String>) = mapOf(
|
||||
flid to linkedMapOf(
|
||||
"FLID" to flid,
|
||||
*collections,
|
||||
),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `Dual read GREEN when detail and legacy slot views agree within slot capacity`() {
|
||||
val gtdt = """[{"GTNO":"1","GATE":"G28","PGOT":"07SEP260800"},{"GTNO":"2","GATE":"G33"}]"""
|
||||
val detail = detailView("GTDT" to gtdt)
|
||||
val legacy = legacyView("GTDT" to gtdt)
|
||||
|
||||
val report = tool.diffDualRead(detail, legacy)
|
||||
assertTrue(report.isGreen)
|
||||
assertEquals(1, report.matchedCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Dual read RED for SIS three-gate sample G28 G33 G23 - third gate must not be ignored`() {
|
||||
val detailGtdt = """[
|
||||
{"GTNO":"1","GATE":"G28"},
|
||||
{"GTNO":"2","GATE":"G33"},
|
||||
{"GTNO":"3","GATE":"G23"}
|
||||
]""".replace("\n", "").replace(" ", "")
|
||||
val legacyGtdt = """[{"GTNO":"1","GATE":"G28"},{"GTNO":"2","GATE":"G33"}]"""
|
||||
|
||||
val report = tool.diffDualRead(
|
||||
detailView("GTDT" to detailGtdt),
|
||||
legacyView("GTDT" to legacyGtdt),
|
||||
)
|
||||
assertFalse(report.isGreen)
|
||||
assertTrue(report.unexpectedDeviations.any { it.path == "GTDT" })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Dual read RED for non-contiguous GTNO=3 - must not rewrite to GTNO=1`() {
|
||||
val detailGtdt = """[{"GTNO":"3","GATE":"G28"}]"""
|
||||
val legacyGtdt = """[{"GTNO":"1","GATE":"G28"}]"""
|
||||
|
||||
val report = tool.diffDualRead(
|
||||
detailView("GTDT" to detailGtdt),
|
||||
legacyView("GTDT" to legacyGtdt),
|
||||
)
|
||||
assertFalse(report.isGreen)
|
||||
assertTrue(report.unexpectedDeviations.any { it.path?.startsWith("GTDT") == true })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Dual read RED for duplicate CHKC different cabin classes - legacy dedupe loses row`() {
|
||||
val detailCkdt = """[
|
||||
{"CKNO":"1","CHKC":"01","CCLS":"F"},
|
||||
{"CKNO":"2","CHKC":"01","CCLS":"Y"}
|
||||
]""".replace("\n", "").replace(" ", "")
|
||||
val legacyCkdt = """[{"CKNO":"1","CHKC":"01","CCLS":"F"}]"""
|
||||
|
||||
val report = tool.diffDualRead(
|
||||
detailView("CKDT" to detailCkdt),
|
||||
legacyView("CKDT" to legacyCkdt),
|
||||
)
|
||||
assertFalse(report.isGreen)
|
||||
assertTrue(report.unexpectedDeviations.any { it.path == "CKDT" })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Dual read RED for two bridge ops with ABDG - legacy milestone view drops bridge id`() {
|
||||
val detailAbtm = """[
|
||||
{"ASNO":"1","ABDG":"B12","ABOP":"A","AOTM":"07SEP260730"},
|
||||
{"ASNO":"2","ABDG":"B15","ABOP":"D","AOTM":"07SEP261200"}
|
||||
]""".replace("\n", "").replace(" ", "")
|
||||
val legacyAbtm = """[
|
||||
{"ASNO":"1","ABOP":"A","AOTM":"07SEP260730"},
|
||||
{"ASNO":"2","ABOP":"D","AOTM":"07SEP261200"}
|
||||
]""".replace("\n", "").replace(" ", "")
|
||||
|
||||
val report = tool.diffDualRead(
|
||||
detailView("ABTM" to detailAbtm),
|
||||
legacyView("ABTM" to legacyAbtm),
|
||||
)
|
||||
assertFalse(report.isGreen)
|
||||
assertTrue(report.unexpectedDeviations.any { it.path?.contains("ABDG") == true || it.path?.startsWith("ABTM") == true })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Dual read RED for two DELY reasons - legacy keeps only first`() {
|
||||
val detailDely = """[
|
||||
{"DLNO":"1","CODE":"01","STRT":"07SEP260800","DURA":"30"},
|
||||
{"DLNO":"2","CODE":"02","STRT":"07SEP260900","DURA":"15"}
|
||||
]""".replace("\n", "").replace(" ", "")
|
||||
val legacyDely = """[{"CODE":"01","STRT":"07SEP260800","DURA":"30"}]"""
|
||||
|
||||
val report = tool.diffDualRead(
|
||||
detailView("DELY" to detailDely),
|
||||
legacyView("DELY" to legacyDely),
|
||||
)
|
||||
assertFalse(report.isGreen)
|
||||
assertTrue(report.unexpectedDeviations.any { it.path == "DELY" })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `FlightSchdReadAssembler exposes lossy legacy slot view for three-gate SIS sample`() {
|
||||
val row = mapOf(
|
||||
"GATE1" to "G28",
|
||||
"GATE2" to "G33",
|
||||
)
|
||||
val detailCollections = mapOf(
|
||||
"GTDT" to """[
|
||||
{"GTNO":"1","GATE":"G28"},
|
||||
{"GTNO":"2","GATE":"G33"},
|
||||
{"GTNO":"3","GATE":"G23"}
|
||||
]""".replace("\n", "").replace(" ", ""),
|
||||
)
|
||||
|
||||
val detailView = FlightSchdReadAssembler.assembleDetailOnly(flid, row, detailCollections)
|
||||
val legacyView = FlightSchdReadAssembler.assembleLegacySlotsOnly(flid, row)
|
||||
|
||||
val report = tool.diffDualRead(mapOf(flid to detailView), mapOf(flid to legacyView))
|
||||
assertFalse(report.isGreen)
|
||||
assertTrue(legacyView["GTDT"]!!.contains("G23").not())
|
||||
assertTrue(detailView["GTDT"]!!.contains("G23"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `FlightSchdReadAssembler merged view follows detail table when present`() {
|
||||
val row = mapOf("GATE1" to "OLD")
|
||||
val detailCollections = mapOf(
|
||||
"GTDT" to """[{"GTNO":"1","GATE":"G28"}]""",
|
||||
)
|
||||
val merged = FlightSchdReadAssembler.assembleMerged(flid, row, detailCollections)
|
||||
assertEquals(detailCollections["GTDT"], merged["GTDT"])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user