diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/codec/SisMessageBody.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/codec/SisMessageBody.kt
index b04e5fc..6b8a7fe 100644
--- a/src/main/kotlin/com/gzzn/omms/msgexchange/codec/SisMessageBody.kt
+++ b/src/main/kotlin/com/gzzn/omms/msgexchange/codec/SisMessageBody.kt
@@ -187,12 +187,13 @@ data class PsdtXml(
data class ChdtXml(
@param:JacksonXmlProperty(isAttribute = true, localName = "CHNO") val chno: String? = null,
@param:JacksonXmlProperty(localName = "CHUT") val chut: String? = null,
- @param:JacksonXmlProperty(localName = "CHCLS") val chcls: String? = null,
+ // SIS 的 CHDT 类字段固定为 CCLS/CTYP(implementation.md「动态运行事件」);曾误绑 CHCLS/CHTYP
+ @param:JacksonXmlProperty(localName = "CCLS") val ccls: String? = null,
@param:JacksonXmlProperty(localName = "PCBT") val pcbt: String? = null,
@param:JacksonXmlProperty(localName = "PCET") val pcet: String? = null,
@param:JacksonXmlProperty(localName = "CBTM") val cbtm: String? = null,
@param:JacksonXmlProperty(localName = "CETM") val cetm: String? = null,
- @param:JacksonXmlProperty(localName = "CHTYP") val chtyp: String? = null,
+ @param:JacksonXmlProperty(localName = "CTYP") val ctyp: String? = null,
)
@JsonIgnoreProperties(ignoreUnknown = true)
diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/codec/SisWireMapper.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/codec/SisWireMapper.kt
index 5ba0390..771f009 100644
--- a/src/main/kotlin/com/gzzn/omms/msgexchange/codec/SisWireMapper.kt
+++ b/src/main/kotlin/com/gzzn/omms/msgexchange/codec/SisWireMapper.kt
@@ -46,7 +46,7 @@ internal object SisWireMapper {
"CKDT" to ckdt.map { it.toMap("CKNO" to it.ckno, "CHKC" to it.chkc, "CCLS" to it.ccls, "PCOT" to it.pcot, "PCCT" to it.pcct, "COTM" to it.cotm, "CCTM" to it.cctm, "CTYP" to it.ctyp) },
"CLDT" to cldt.map { it.toMap("CLNO" to it.clno, "BELT" to it.belt, "BCLS" to it.bcls, "PCOT" to it.pcot, "PCCT" to it.pcct, "FBAG" to it.fbag, "LBAG" to it.lbag, "BTYP" to it.btyp) },
"PSDT" to psdt.map { it.toMap("PSNO" to it.psno, "PSST" to it.psst, "STST" to it.stst, "STET" to it.stet) },
- "CHDT" to chdt.map { it.toMap("CHNO" to it.chno, "CHUT" to it.chut, "CHCLS" to it.chcls, "PCBT" to it.pcbt, "PCET" to it.pcet, "CBTM" to it.cbtm, "CETM" to it.cetm, "CHTYP" to it.chtyp) },
+ "CHDT" to chdt.map { it.toMap("CHNO" to it.chno, "CHUT" to it.chut, "CCLS" to it.ccls, "PCBT" to it.pcbt, "PCET" to it.pcet, "CBTM" to it.cbtm, "CETM" to it.cetm, "CTYP" to it.ctyp) },
"DELY" to dely.map { it.toMap("CODE" to it.code, "STRT" to it.strt, "DURA" to it.dura, "REMC" to it.text) },
"ABTM" to abtm.map { it.toMap("ASNO" to it.asno, "ABDG" to it.abdg, "ABOP" to it.abop, "AOTM" to it.aotm) },
"CHOT" to chot.map { it.toMap("CSNO" to it.csno, "CHID" to it.chid, "CHST" to it.chst, "CHTM" to it.chtm) },
diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/infra/persistence/jdbc/JdbcPgRepositories.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/infra/persistence/jdbc/JdbcPgRepositories.kt
index 102eed6..869e511 100644
--- a/src/main/kotlin/com/gzzn/omms/msgexchange/infra/persistence/jdbc/JdbcPgRepositories.kt
+++ b/src/main/kotlin/com/gzzn/omms/msgexchange/infra/persistence/jdbc/JdbcPgRepositories.kt
@@ -872,7 +872,7 @@ class JdbcFlightStateRepository(
"CKDT" to DetailSpec("flight_checkin", listOf("chkc", "ccls", "pcot", "pcct", "cotm", "cctm", "ctyp"), "CKNO"),
"CLDT" to DetailSpec("flight_belt", listOf("belt", "bcls", "pcot", "pcct", "fbag", "lbag", "btyp"), "CLNO"),
"PSDT" to DetailSpec("flight_stand_plan", listOf("psst", "stst", "stet"), "PSNO"),
- "CHDT" to DetailSpec("flight_chute", listOf("chut", "chcls", "pcbt", "pcet", "cbtm", "cetm", "chtyp"), "CHNO"),
+ "CHDT" to DetailSpec("flight_chute", listOf("chut", "ccls", "pcbt", "pcet", "cbtm", "cetm", "ctyp"), "CHNO"),
"DELY" to DetailSpec("flight_delay", listOf("code", "strt", "dura", "remc"), null),
"ABTM" to DetailSpec("flight_bridge_op", listOf("abdg", "abop", "aotm"), "ASNO"),
"CHOT" to DetailSpec("flight_chock_op", listOf("chid", "chst", "chtm"), "CSNO"),
diff --git a/src/main/resources/db/migration/V2__flight_chute_class_type_rename.sql b/src/main/resources/db/migration/V2__flight_chute_class_type_rename.sql
new file mode 100644
index 0000000..ad596c1
--- /dev/null
+++ b/src/main/resources/db/migration/V2__flight_chute_class_type_rename.sql
@@ -0,0 +1,12 @@
+-- =====================================================================
+-- FLIGHT_CHUTE 类字段更名:CHCLS/CHTYP → CCLS/CTYP
+-- ---------------------------------------------------------------------
+-- SIS 的 CHDT 类字段固定为 CCLS/CTYP(docs/implementation.md「动态运行事件」)。
+-- V1 基线已发布不可改写,列名偏差以本新增迁移修正;只作用于自有 PostgreSQL,
+-- 共享 MySQL 信箱不受影响(C-2)。wire DTO 与映射键已同步改为 CCLS/CTYP,
+-- 本迁移保证库端列名与代码读出的键一致(存量行原值随列更名保留)。
+-- =====================================================================
+
+ALTER TABLE flight_chute RENAME COLUMN chcls TO ccls;
+
+ALTER TABLE flight_chute RENAME COLUMN chtyp TO ctyp;
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 6cc77f3..9a8b54a 100644
--- a/src/test/kotlin/com/gzzn/omms/msgexchange/codec/JacksonXmlCodecTest.kt
+++ b/src/test/kotlin/com/gzzn/omms/msgexchange/codec/JacksonXmlCodecTest.kt
@@ -1,6 +1,10 @@
package com.gzzn.omms.msgexchange.codec
import com.gzzn.omms.msgexchange.domain.MsgKind
+import com.gzzn.omms.msgexchange.domain.flight.FlightSnapshot
+import com.gzzn.omms.msgexchange.domain.flight.FlightState
+import com.gzzn.omms.msgexchange.domain.flight.FlightStateEngine
+import com.gzzn.omms.msgexchange.domain.flight.MergeChange
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertInstanceOf
@@ -271,4 +275,36 @@ class JacksonXmlCodecTest {
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)
}
+
+ @Test
+ fun `CHDT round trip keeps SIS class fields CCLS and CTYP from decode to merged state`() {
+ val raw = """
+ AODB920021010090311
+ FLOPCHDT
+ 121112312
+ C01AIN
+
+ """.trimIndent()
+
+ val payload = ((codec.decode(raw) as DecodeResult.Ok).message.body as FlopPayload)
+
+ // wire 层:CCLS/CTYP 键在、CHCLS/CHTYP 键不在
+ val row = payload.collections.getValue("CHDT").single()
+ assertEquals("A", row["CCLS"])
+ assertEquals("IN", row["CTYP"])
+ assertFalse(row.containsKey("CHCLS"))
+ assertFalse(row.containsKey("CHTYP"))
+
+ // 合并层:MergeChange 带进航班快照的 CHDT 明细
+ val current = FlightSnapshot(
+ "121112312", java.time.LocalDate.of(2026, 12, 15), FlightState.ACTIVE, 1,
+ scalars = mapOf("FLNO" to "CA001"),
+ collections = emptyMap(),
+ )
+ val next = FlightStateEngine.mergedState(
+ current,
+ MergeChange("121112312", scalars = emptyMap(), collections = payload.collections),
+ )
+ assertEquals(listOf(row), next.collections["CHDT"])
+ }
}
diff --git a/src/test/kotlin/com/gzzn/omms/msgexchange/infra/persistence/jdbc/FlywayMigrationTest.kt b/src/test/kotlin/com/gzzn/omms/msgexchange/infra/persistence/jdbc/FlywayMigrationTest.kt
index 433d001..be8f116 100644
--- a/src/test/kotlin/com/gzzn/omms/msgexchange/infra/persistence/jdbc/FlywayMigrationTest.kt
+++ b/src/test/kotlin/com/gzzn/omms/msgexchange/infra/persistence/jdbc/FlywayMigrationTest.kt
@@ -10,10 +10,11 @@ import org.junit.jupiter.api.Test
import java.sql.DriverManager
/**
- * 在真实 PostgreSQL 上跑一遍迁移,确认结果符合预期:单基线 `V1__flight_state_baseline.sql`
- * 执行成功且是唯一的迁移记录,该建的表和单行种子(PIPELINE_LOCK、INBOX_CURSOR)都在,
- * 回填事实与收报水位都落在基线里,`BACKFILL_TODO`、`idx_evt_flid`、`PROC_STATE` 的处理开始
- * 时间列都不复存在——这些正是原 V2–V10 合并后的净结构。
+ * 在真实 PostgreSQL 上跑一遍迁移链,确认结果符合预期:`V1__flight_state_baseline.sql`
+ * 基线加 `V2__flight_chute_class_type_rename.sql` 更名依序执行成功,该建的表和单行种子
+ * (PIPELINE_LOCK、INBOX_CURSOR)都在,回填事实与收报水位都落在基线里,`BACKFILL_TODO`、
+ * `idx_evt_flid`、`PROC_STATE` 的处理开始时间列都不复存在;FLIGHT_CHUTE 的类字段列
+ * 已由 V2 更名为 CCLS/CTYP(SIS 口径)。
*
* 没有可用的 PostgreSQL 时跳过(不假装通过)。
*/
@@ -42,7 +43,7 @@ class FlywayMigrationTest {
DriverManager.getConnection(url, user, pass).use { conn ->
conn.createStatement().use { stmt ->
- // 单基线:原 V1–V10 已合并为一条迁移,历史里只应留下 V1
+ // 迁移链:V1 基线 + V2 类字段更名,依序成功
stmt.executeQuery(
"SELECT version, script, success FROM flyway_schema_history ORDER BY installed_rank ASC",
).use { rs ->
@@ -50,12 +51,24 @@ class FlywayMigrationTest {
while (rs.next()) {
records.add(Triple(rs.getString("version"), rs.getString("script"), rs.getBoolean("success")))
}
- assertEquals(1, records.size, "迁移链必须收敛为单基线,不得再有 V2+ 条目")
+ assertEquals(2, records.size, "迁移链应为 V1 基线 + V2 更名两条")
assertEquals("1", records[0].first)
assertEquals("V1__flight_state_baseline.sql", records[0].second)
+ assertEquals("2", records[1].first)
+ assertEquals("V2__flight_chute_class_type_rename.sql", records[1].second)
assertTrue(records.all { it.third })
}
+ // V2 更名后 FLIGHT_CHUTE 类字段为 SIS 口径的 ccls/ctyp,CHCLS/CHTYP 不复存在
+ stmt.executeQuery(
+ "SELECT column_name FROM information_schema.columns WHERE table_name = 'flight_chute' " +
+ "AND column_name IN ('ccls', 'ctyp', 'chcls', 'chtyp')",
+ ).use { rs ->
+ val cols = mutableSetOf()
+ while (rs.next()) cols.add(rs.getString("column_name"))
+ assertEquals(setOf("ccls", "ctyp"), cols, "FLIGHT_CHUTE 类字段必须已更名为 ccls/ctyp")
+ }
+
// 全表就绪(回填事实并入 PROC_STATE,基线不再建 BACKFILL_TODO)
stmt.executeQuery(
"SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'",