feat(flight): UNMAPPED_FIELD、SRVT/VIPF 明细与白名单内未映射留档(ACM2-89/99/100)

V5 迁移;集合段出现才写;白名单 7 类内未落态写入 UNMAPPED_FIELD,不扩白名单。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
windyboy
2026-09-21 15:54:55 +08:00
co-authored by Cursor
parent 54b383fa9d
commit 02b0b2f189
14 changed files with 464 additions and 13 deletions
@@ -0,0 +1,148 @@
package com.gzzn.omms.msgexchange.processing
import com.fasterxml.jackson.databind.ObjectMapper
import com.gzzn.omms.msgexchange.codec.FlopPayload
import com.gzzn.omms.msgexchange.codec.JacksonXmlCodec
import com.gzzn.omms.msgexchange.domain.DecodedMessage
import com.gzzn.omms.msgexchange.domain.MetaFields
import com.gzzn.omms.msgexchange.domain.MsgKind
import com.gzzn.omms.msgexchange.domain.ProcState
import com.gzzn.omms.msgexchange.domain.ProcStatus
import com.gzzn.omms.msgexchange.domain.flight.FlightState
import com.gzzn.omms.msgexchange.domain.flight.UnmappedFieldEntry
import com.gzzn.omms.msgexchange.infra.stub.StubFlightProjectionPort
import com.gzzn.omms.msgexchange.infra.stub.StubFlightState
import com.gzzn.omms.msgexchange.infra.stub.StubMsgEvents
import com.gzzn.omms.msgexchange.infra.stub.StubPipelineLock
import com.gzzn.omms.msgexchange.infra.stub.StubPipelineTx
import com.gzzn.omms.msgexchange.infra.stub.StubProcState
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import java.time.LocalDate
class SrvtVipfUnmappedTest {
private val msgId = 42L
private val flid = "900001"
private fun head() = ProcState(msgId, ProcStatus.PENDING, updatedAt = java.time.Instant.EPOCH)
private fun flopProcessor(flights: StubFlightState): FlopProcessor {
val commit = FlightCommit(
StubPipelineTx(),
StubPipelineLock(),
StubProcState(),
StubMsgEvents(),
StubFlightProjectionPort(),
java.time.Clock.systemUTC(),
)
return FlopProcessor(commit, flights, StubMsgEvents(), ObjectMapper(), java.time.Clock.systemUTC())
}
private fun seedFlight(flights: StubFlightState) {
flights.persistFullState(
com.gzzn.omms.msgexchange.domain.flight.FlightSnapshot(
flid,
LocalDate.of(2026, 12, 15),
FlightState.ACTIVE,
1,
mapOf("FLNO" to "CA101"),
emptyMap(),
),
msgId = 1,
now = java.time.Instant.EPOCH,
)
}
@Test
fun `SRVT present writes detail absent keeps prior rows`() {
val flights = StubFlightState()
seedFlight(flights)
val proc = flopProcessor(flights)
val srvt = listOf(mapOf("OPER" to "ADD", "SRTC" to "SR01"))
proc.apply(
head(),
flopMsg("GTDT"),
FlopPayload(flid, collections = mapOf("SRVT" to srvt)),
)
assertEquals(srvt, flights.srvtByFlid[flid])
proc.apply(
head(),
flopMsg("GTDT"),
FlopPayload(flid, collections = mapOf("GTDT" to listOf(mapOf("GTNO" to "1", "GATE" to "D01")))),
)
assertEquals(srvt, flights.srvtByFlid[flid], "absent SRVT must not clear Q2 default")
}
@Test
fun `VIPF present and reprocess replaces segment idempotently`() {
val flights = StubFlightState()
seedFlight(flights)
val proc = flopProcessor(flights)
val vipf = listOf(mapOf("VPCD" to "V001", "VFES" to "3"))
val payload = FlopPayload(flid, collections = mapOf("VIPF" to vipf))
proc.apply(head(), flopMsg("GTDT"), payload)
proc.apply(head(), flopMsg("GTDT"), payload)
assertEquals(1, flights.vipfByFlid[flid]!!.size)
assertEquals("V001", flights.vipfByFlid[flid]!![0]["VPCD"])
}
@Test
fun `whitelisted FLOP with FDIV stores unmapped fields`() {
val codec = JacksonXmlCodec()
val raw = """
<MSG><META><SNDR>AODB</SNDR><SEQN>1</SEQN><DTTM>20021010090311</DTTM>
<TYPE>FLOP</TYPE><STYP>GTDT</STYP></META>
<FLOP><FLID>$flid</FLID>
<GTDT GTNO="1"><GATE>D08</GATE></GTDT>
<FDIV DDES="CTU" DDIR="TO">note</FDIV>
</FLOP></MSG>
""".trimIndent()
val decoded = (codec.decode(raw) as com.gzzn.omms.msgexchange.codec.DecodeResult.Ok).message
val body = decoded.body as FlopPayload
assertTrue(body.unmapped.any { it.path == "FDIV/@DDES" && it.raw == "CTU" })
assertTrue(body.unmapped.any { it.path == "FDIV/@DDIR" && it.raw == "TO" })
assertTrue(body.unmapped.any { it.path == "FDIV" && it.raw == "note" })
val flights = StubFlightState()
seedFlight(flights)
flopProcessor(flights).apply(head(), decoded, body)
assertEquals(body.unmapped, flights.unmappedByMsgId[msgId])
}
@Test
fun `reprocess rewrites same unmapped rows`() {
val flights = StubFlightState()
seedFlight(flights)
val proc = flopProcessor(flights)
val unmapped = listOf(
UnmappedFieldEntry("FRET/@REID", 1, "G"),
UnmappedFieldEntry("FRET", 1, ""),
)
val payload = FlopPayload(flid, unmapped = unmapped)
proc.apply(head(), flopMsg("DELY"), payload)
proc.apply(head(), flopMsg("DELY"), payload)
assertEquals(unmapped, flights.unmappedByMsgId[msgId])
}
@Test
fun `FLOP FDIV subtype stays unsupported not unmapped`() {
val codec = JacksonXmlCodec()
val raw = """
<MSG><META><SNDR>AODB</SNDR><SEQN>2</SEQN><DTTM>20021010090311</DTTM>
<TYPE>FLOP</TYPE><STYP>FDIV</STYP></META>
<FLOP><FLID>$flid</FLID><FDIV DDES="CTU"/></FLOP></MSG>
""".trimIndent()
val kind = (codec.decode(raw) as com.gzzn.omms.msgexchange.codec.DecodeResult.Ok).message.kind
assertEquals(MsgKind.Unsupported("FLOP-FDIV"), kind)
}
private fun flopMsg(styp: String) = DecodedMessage(
meta = MetaFields("AODB", "FLOP", styp, 1L, 1L),
kind = MsgKind.Flop(styp),
rawXml = "<MSG/>",
body = FlopPayload(flid),
)
}