refactor(flight-state): 按 flight-state.md 审计定稿全量重构脚手架与 SQL (ACM2-31)

- SQL 基线 V1__flight_state_baseline.sql 整体取代 V1.0.0–V1.4.0:
  PIPELINE_LOCK/PROC_STATE/MSG_EVENT/REQ_TRACK/BACKFILL_TODO/FLIGHT_SCHD
  + 8 张资源明细表 + FLIGHT_ROUTE_POINT + SCHD_SNAP_LOG 留痕层
- 废除 FDAY 日代/SCHD_GEN/名单差删:OPERATION_DAY 不可变(应用层校验 +
  条件更新强化 §7.4),STATE 仅 ACTIVE/DELETED,物理清除只在历史归档后
- 处理器化:applyScheduleRecords(§5.1 七步同一事务,重放判定/整包
  DEAD(PROTOCOL)/归属冲突不落地)+ FLOP/FDEL/ADFT(tombstone 仅
  ACTIVE→DELETED,重复 FDEL 幂等不推进版本)
- 投递:KAFKA_SCHD 同 FLID 按最新 STATE_VERSION 合并,被压掉事件关闭,
  TOMBSTONE 发 null 值消息(键缺失=删除旧值 §7.3)
- 回填待办改为业务事务内预登记,消除提交后写待办的崩溃窗口(§7.2/§10)
- XML 解码改为 jackson-dataformat-xml 数据类直接映射(SIS 信封强类型,
  FLTR 开放标签泛型承载)
- 历史归档/物理清除顺序不可颠倒:归档确认成功集才物理删除,未接通删 0 条
- 移除 PUMP_JOB 队列/ReferenceService/FlightStoreDiffTool 等旧机制与测试,
  新增运营日/引擎/快照/FDEL/归档顺序不变性回归测试
This commit is contained in:
windyboy
2026-09-09 17:53:08 +08:00
parent 6af292d103
commit 879d658159
83 changed files with 3435 additions and 6355 deletions
@@ -0,0 +1,47 @@
package com.gzzn.omms.msgexchange.domain
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Test
import java.time.LocalDate
import java.time.ZoneId
/**
* 运营日计算(docs/flight-state.md §3.5):SODTddMMMyyHHmm+ 机场时区 + 业务切日边界。
*/
class OperationDayTest {
private val zone: ZoneId = ZoneId.of("Asia/Shanghai")
@Test
fun `computes operation day from SODT with zero cutoff`() {
val calc = OperationDayCalculator(zone, cutoffHour = 0)
assertEquals(LocalDate.of(2026, 12, 15), calc.compute("15DEC261723"))
}
@Test
fun `time before cutoff rolls back to previous operation day`() {
val calc = OperationDayCalculator(zone, cutoffHour = 4)
// 04:00 之前 → 前一运营日
assertEquals(LocalDate.of(2026, 12, 14), calc.compute("15DEC260230"))
// 04:00 整点起归属当日
assertEquals(LocalDate.of(2026, 12, 15), calc.compute("15DEC260400"))
}
@Test
fun `invalid or missing SODT returns null`() {
val calc = OperationDayCalculator(zone, 0)
assertNull(calc.compute(null))
assertNull(calc.compute(""))
assertNull(calc.compute("garbage"))
assertNull(calc.compute("99XXC12345"))
}
@Test
fun `cutoff outside 0 to 23 is clamped to 23`() {
val calc = OperationDayCalculator(zone, cutoffHour = 99)
// 越界收敛为 23:当日 00:00 早于 23 点 → 归属前一运营日
assertEquals(LocalDate.of(2026, 12, 14), calc.compute("15DEC260000"))
}
}
@@ -1,202 +1,113 @@
package com.gzzn.omms.msgexchange.domain.flight
import com.gzzn.omms.msgexchange.domain.OperationDayCalculator
import com.gzzn.omms.msgexchange.domain.SnapshotFlag
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
import java.time.LocalDate
import java.time.ZoneId
/**
* 航班状态引擎(docs/flight-state.md §3.3/§5.2/§5.4/§6.1)不变量:
* 快照整体替换(缺失=清除)、DELETED 不被 SCHD 恢复、FLOP 增量保留缺失字段、
* §5.2 五项校验整包拒绝语义。
*/
class FlightStateEngineTest {
private val opDay = OperationDayCalculator(ZoneId.of("Asia/Shanghai"), 0)
private val day = LocalDate.of(2026, 12, 15)
@Test
fun `scalar unchanged does not modify existing value`() {
val current = FlightNextState("F1", mapOf("FLNO" to "CA100"), emptyMap(), 1L, "m1")
val next = FlightStateEngine.apply(
fun `snapshot replaces scalars and clears absent ones`() {
val current = FlightSnapshot(
"121", day, FlightState.ACTIVE, 5,
scalars = mapOf("FLNO" to "CA001", "REMC" to "old-note"),
collections = mapOf("GTDT" to listOf(mapOf("GATE" to "G1"))),
)
val next = FlightStateEngine.snapshotState(
current,
FlightFieldCommands("F1", scalars = emptyMap()),
"m2",
bumpVersion = true,
ScheduleRecord("121", scalars = mapOf("FLNO" to "CA002")),
operationDay = day,
keepDeleted = false,
)
assertEquals("CA100", next.scalars["FLNO"])
assertEquals(2L, next.stateVersion)
assertEquals("CA002", next.scalars["FLNO"])
assertFalse(next.scalars.containsKey("REMC")) // 缺失 = Clear(§5.4
assertEquals(emptyList<Map<String, String>>(), next.collections["GTDT"]) // 缺失 = Replace 空集
assertEquals(6, next.stateVersion) // 每次成功写入 +1(§4
assertEquals(FlightState.ACTIVE, next.state)
}
@Test
fun `collection replace preserves order and source sequence`() {
val items = listOf(
mapOf("GTNO" to "1", "GATE" to "A1"),
mapOf("GTNO" to "3", "GATE" to "B2"),
mapOf("GTNO" to "7", "GATE" to "C3"),
fun `snapshot keeps DELETED state and flags revive conflict by caller`() {
val current = FlightSnapshot("121", day, FlightState.DELETED, 3, mapOf(), emptyMap())
val next = FlightStateEngine.snapshotState(
current, ScheduleRecord("121", mapOf("FLNO" to "CA001")), day, keepDeleted = true,
)
val next = FlightStateEngine.apply(
null,
FlightFieldCommands("F1", collections = mapOf("GTDT" to CollectionCommand.Replace(items))),
"m1",
bumpVersion = true,
)
assertEquals(3, next.collections["GTDT"]!!.size)
assertEquals("3", next.collections["GTDT"]!![1]["GTNO"])
assertEquals("C3", next.collections["GTDT"]!![2]["GATE"])
assertEquals(FlightState.DELETED, next.state) // §5.1 步骤 6:普通 SCHD 不恢复
assertEquals(4, next.stateVersion)
}
@Test
fun `collection clear removes prior entries`() {
val current = FlightNextState(
"F1",
emptyMap(),
mapOf("GTDT" to listOf(mapOf("GTNO" to "1", "GATE" to "A1"))),
1L,
"m1",
fun `flop merge retains absent scalars and collections`() {
val current = FlightSnapshot(
"121", day, FlightState.ACTIVE, 1,
scalars = mapOf("FLNO" to "CA001", "ESTT" to "15DEC261807"),
collections = mapOf("GTDT" to listOf(mapOf("GATE" to "G1"))),
)
val next = FlightStateEngine.apply(
val next = FlightStateEngine.mergedState(
current,
FlightFieldCommands("F1", collections = mapOf("GTDT" to CollectionCommand.Clear)),
"m2",
bumpVersion = true,
MergeChange("121", scalars = mapOf("ESTT" to "15DEC261900")),
)
assertFalse(next.collections.containsKey("GTDT"))
assertEquals("15DEC261900", next.scalars["ESTT"])
assertEquals("CA001", next.scalars["FLNO"]) // 缺失 = 保留(§6.1
assertEquals(listOf(mapOf("GATE" to "G1")), next.collections["GTDT"])
assertEquals(2, next.stateVersion)
}
@Test
fun `same gate number different attributes both retained on replace`() {
val items = listOf(
mapOf("GTNO" to "1", "GATE" to "A1", "GTYP" to "D"),
mapOf("GTNO" to "1", "GATE" to "A1", "GTYP" to "I"),
fun `validation rejects RECS mismatch and duplicate flid and day outside scope`() {
val ok = FlightStateEngine.validateMessage(
1, listOf(ScheduleRecord("121", mapOf("SODT" to "15DEC261723"))), day, day, opDay,
)
val next = FlightStateEngine.apply(
null,
FlightFieldCommands("F1", collections = mapOf("GTDT" to CollectionCommand.Replace(items))),
"m1",
bumpVersion = true,
)
assertEquals("D", next.collections["GTDT"]!![0]["GTYP"])
assertEquals("I", next.collections["GTDT"]!![1]["GTYP"])
}
assertTrue(ok is SnapshotValidation.Ok)
@Test
fun `incremental apply bumps state version from persisted value`() {
val current = FlightNextState("F1", mapOf("FLNO" to "CA100"), emptyMap(), 5L, "m1")
val next = FlightStateEngine.apply(
current,
FlightFieldCommands("F1", scalars = mapOf("STAT" to ScalarCommand.Set("DEP"))),
"m2",
bumpVersion = true,
)
assertEquals(6L, next.stateVersion)
}
val recsMismatch = FlightStateEngine.validateMessage(
2, listOf(ScheduleRecord("121", mapOf("SODT" to "15DEC261723"))), day, day, opDay,
) as SnapshotValidation.Invalid
assertTrue(recsMismatch.flags.contains(SnapshotFlag.RECS_DROP))
@Test
fun `apply by source sequence updates single row`() {
val current = FlightNextState(
"F1",
emptyMap(),
mapOf("GTDT" to listOf(mapOf("GTNO" to "2", "GATE" to "OLD"))),
1L,
"m1",
)
val next = FlightStateEngine.apply(
current,
FlightFieldCommands(
"F1",
collections = mapOf(
"GTDT" to CollectionCommand.Apply(mapOf("GTNO" to "2", "GATE" to "NEW"), "2"),
),
val duplicate = FlightStateEngine.validateMessage(
2,
listOf(
ScheduleRecord("121", mapOf("SODT" to "15DEC261723")),
ScheduleRecord("121", mapOf("SODT" to "15DEC261723")),
),
"m2",
bumpVersion = true,
day, day, opDay,
)
assertEquals("NEW", next.collections["GTDT"]!![0]["GATE"])
assertTrue(duplicate is SnapshotValidation.Invalid)
val outOfScope = FlightStateEngine.validateMessage(
1, listOf(ScheduleRecord("121", mapOf("SODT" to "20DEC261723"))), day, day, opDay,
) as SnapshotValidation.Invalid
assertTrue(outOfScope.flags.contains(SnapshotFlag.DAY_MISMATCH)) // §5.2 覆盖范围校验
val empty = FlightStateEngine.validateMessage(0, emptyList(), day, day, opDay) as SnapshotValidation.Ok
assertTrue(empty.flags.contains(SnapshotFlag.EMPTY))
}
@Test
fun `zero sequence marker alone translates to explicit clear`() {
val commands = FlightStateEngine.commandsFromFields(
"F1",
mapOf("GTDT" to """[{"GTNO":"0"}]"""),
fun `validation rejects non numeric or oversized flid`() {
val bad = FlightStateEngine.validateMessage(
1, listOf(ScheduleRecord("ABC", mapOf("SODT" to "15DEC261723"))), day, day, opDay,
)
assertEquals(CollectionCommand.Clear, commands.collections["GTDT"])
}
@Test
fun `zero sequence marker mixed with regular items fails fast instead of guessing`() {
val error = runCatching {
FlightStateEngine.commandsFromFields(
"F1",
mapOf("GTDT" to """[{"GTNO":"1","GATE":"A1"},{"GTNO":"0"}]"""),
)
}.exceptionOrNull()
assertTrue(error is IllegalArgumentException)
}
@Test
fun `empty array replaces collection with empty set keeping key semantics`() {
val current = FlightNextState(
"F1",
emptyMap(),
mapOf("GTDT" to listOf(mapOf("GTNO" to "1", "GATE" to "A1"))),
1L,
"m1",
assertTrue(bad is SnapshotValidation.Invalid)
val tooLong = FlightStateEngine.validateMessage(
1, listOf(ScheduleRecord("1234567890123", mapOf("SODT" to "15DEC261723"))), day, day, opDay,
)
val commands = FlightStateEngine.commandsFromFields("F1", mapOf("GTDT" to "[]"))
val next = FlightStateEngine.apply(current, commands, "m2", bumpVersion = true)
assertEquals(emptyList<Map<String, String>>(), next.collections["GTDT"])
}
@Test
fun `apply on DELY fails fast because protocol defines no sequence attribute`() {
val current = FlightNextState(
"F1",
emptyMap(),
mapOf("DELY" to listOf(mapOf("CODE" to "YY"))),
1L,
"m1",
)
val error = runCatching {
FlightStateEngine.apply(
current,
FlightFieldCommands(
"F1",
collections = mapOf("DELY" to CollectionCommand.Apply(mapOf("CODE" to "ZZ"), "1")),
),
"m2",
bumpVersion = true,
)
}.exceptionOrNull()
assertTrue(error is IllegalArgumentException)
}
@Test
fun `exception null payload translates to explicit clear and apply records cleared key`() {
val current = FlightNextState(
"F1",
mapOf("FLNO" to "CA100", "FRET" to """{"REID":"R1","RSN":"diverted"}"""),
emptyMap(),
3L,
"m1",
)
val commands = FlightStateEngine.commandsFromFields("F1", mapOf("FRET" to "null"))
assertEquals(ScalarCommand.Clear, commands.scalars["FRET"])
val next = FlightStateEngine.apply(current, commands, "m2", bumpVersion = true)
assertFalse(next.scalars.containsKey("FRET"))
assertTrue(next.clearedKeys.contains("FRET"))
assertEquals("CA100", next.scalars["FLNO"])
// 非空载荷仍为 Set,且清空后的下一跳 Set 恢复键(clearedKeys 不跨消息携带)
val restored = FlightStateEngine.apply(
next,
FlightStateEngine.commandsFromFields("F1", mapOf("FRET" to """{"REID":"R2"}""")),
"m3",
bumpVersion = true,
)
assertEquals("""{"REID":"R2"}""", restored.scalars["FRET"])
assertFalse(restored.clearedKeys.contains("FRET"))
}
@Test
fun `exception clear accepts null literal empty object and empty string`() {
for (payload in listOf("null", "{}", "")) {
val commands = FlightStateEngine.commandsFromFields("F1", mapOf("FDIV" to payload))
assertEquals(ScalarCommand.Clear, commands.scalars["FDIV"], "payload=[$payload] must be Clear")
}
assertTrue(tooLong is SnapshotValidation.Invalid) // SIS §3.16.2 Number(1-12)
}
}