diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/codec/JacksonXmlCodec.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/codec/JacksonXmlCodec.kt
index d6af86b..bcfb631 100644
--- a/src/main/kotlin/com/gzzn/omms/msgexchange/codec/JacksonXmlCodec.kt
+++ b/src/main/kotlin/com/gzzn/omms/msgexchange/codec/JacksonXmlCodec.kt
@@ -88,6 +88,8 @@ class JacksonXmlCodec : XmlCodec {
* 子类型分派必须是**白名单**:未知的 SCHD 子类型不能静默当成全量日计划(DNLD)——
* 那会让一条本该"暂不支持、等人工处置"的报文走最重的整包合并写入路径。
* 空 STYP 按 legacy 约定视为 DNLD(该约定待与 SIS 逐类对拍确认,见 Q3)。
+ * FLOP 同样按白名单:仅现行七类(`implementation.md`「动态运行事件」`US-05` AC1)
+ * 走合并路径,其余 STYP 是 `Q3` 闭合目标,落 `Unsupported`(`FDEL` 单独成类,不参与白名单)。
* 静态参考数据类别码(`implementation.md`「静态参考数据」`SIS:3.1`~`3.14`)单列成
* [MsgKind.RefData]:它们必须走参考数据处理(US-13),不得混入"不支持"跳过。
*/
@@ -98,11 +100,18 @@ class JacksonXmlCodec : XmlCodec {
"DNLD", "" -> MsgKind.Schd(MsgKind.SchdSubtype.DNLD)
else -> MsgKind.Unsupported("SCHD-$styp")
}
- "FLOP" -> if (styp == "FDEL") MsgKind.Fdel else MsgKind.Flop(styp)
+ "FLOP" -> when {
+ styp == "FDEL" -> MsgKind.Fdel
+ styp in FLOP_SUBTYPES -> MsgKind.Flop(styp)
+ else -> MsgKind.Unsupported("FLOP-$styp")
+ }
in REF_DATA_TYPES -> MsgKind.RefData(type)
else -> MsgKind.Unsupported("$type-$styp")
}
+ /** 现行 FLOP 白名单 7 类(`US-05` AC1);表内其余行是 `Q3` 闭合目标,不是现行白名单。 */
+ private val FLOP_SUBTYPES = setOf("ABTM", "DELY", "PSDT", "CKDT", "CLDT", "CHDT", "GTDT")
+
private val REF_DATA_TYPES = setOf(
"COUL", "ARPT", "AIRL", "AIRC", "REGN", "ORGN", "FLTL",
"TLST", "GLST", "SLST", "CLST", "BLST", "CHLT", "RSTA",
diff --git a/src/test/kotlin/com/gzzn/omms/msgexchange/codec/JacksonXmlCodecTest.kt b/src/test/kotlin/com/gzzn/omms/msgexchange/codec/JacksonXmlCodecTest.kt
index 8f71ec3..6cc77f3 100644
--- a/src/test/kotlin/com/gzzn/omms/msgexchange/codec/JacksonXmlCodecTest.kt
+++ b/src/test/kotlin/com/gzzn/omms/msgexchange/codec/JacksonXmlCodecTest.kt
@@ -250,4 +250,25 @@ class JacksonXmlCodecTest {
val unknown = (codec.decode(raw("XYZZ", "TEST")) as DecodeResult.Ok).message
assertEquals(MsgKind.Unsupported("XYZZ-TEST"), unknown.kind)
}
+
+ @Test
+ fun `FLOP subtype whitelist sends only the seven current types to FlopProcessor`() {
+ fun raw(styp: String) = """
+ AODB920021010090311
+ FLOP$styp
+ """.trimIndent()
+
+ // 现行白名单 7 类(US-05 AC1)
+ for (styp in listOf("ABTM", "DELY", "PSDT", "CKDT", "CLDT", "CHDT", "GTDT")) {
+ assertEquals(MsgKind.Flop(styp), (codec.decode(raw(styp)) as DecodeResult.Ok).message.kind)
+ }
+
+ // FDEL 独立分派,不参与白名单判定
+ assertEquals(MsgKind.Fdel, (codec.decode(raw("FDEL")) as DecodeResult.Ok).message.kind)
+
+ // 白名单外子类型(ACTT/CNCL 是 Q3 闭合目标)与空 STYP 都落 Unsupported,不走合并路径
+ assertEquals(MsgKind.Unsupported("FLOP-ACTT"), (codec.decode(raw("ACTT")) as DecodeResult.Ok).message.kind)
+ assertEquals(MsgKind.Unsupported("FLOP-CNCL"), (codec.decode(raw("CNCL")) as DecodeResult.Ok).message.kind)
+ assertEquals(MsgKind.Unsupported("FLOP-"), (codec.decode(raw("")) as DecodeResult.Ok).message.kind)
+ }
}
diff --git a/src/test/kotlin/com/gzzn/omms/msgexchange/processing/IgnoreBranchTest.kt b/src/test/kotlin/com/gzzn/omms/msgexchange/processing/IgnoreBranchTest.kt
index d356de6..2f5e285 100644
--- a/src/test/kotlin/com/gzzn/omms/msgexchange/processing/IgnoreBranchTest.kt
+++ b/src/test/kotlin/com/gzzn/omms/msgexchange/processing/IgnoreBranchTest.kt
@@ -24,6 +24,7 @@ import com.gzzn.omms.msgexchange.infra.stub.StubSnapshotLog
import com.fasterxml.jackson.databind.ObjectMapper
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
+import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import java.time.Clock
import java.time.Instant
@@ -212,6 +213,28 @@ class IgnoreBranchTest {
assertEquals("refdata-pending:ARPT", row.lastError)
}
+ @Test
+ fun `flop subtype outside whitelist is skipped without touching flights`() {
+ val proc = StubProcState()
+ proc.insertIfAbsent(msgId, null)
+ val inbox = StubInbox()
+ inbox.raws[msgId] = ""
+ val msg = DecodedMessage(
+ meta = MetaFields("AODB", "FLOP", "ACTT", 500L, 1L),
+ kind = MsgKind.Unsupported("FLOP-ACTT"),
+ rawXml = "",
+ )
+ val flights = StubFlightState()
+ val p = processor(proc = proc, inbox = inbox, codec = codecReturning(msg))
+
+ p.processOne(head())
+
+ val row = proc.find(msgId)!!
+ assertEquals(ProcStatus.SKIPPED, row.state)
+ assertEquals("unsupported:FLOP-ACTT", row.lastError)
+ assertTrue(flights.snapshots.isEmpty()) // 白名单外子类型不改航班(US-05 AC1)
+ }
+
@Test
fun `ignore increments counter`() {
val proc = StubProcState()