From 57bb5ea233d6308779ab920d55003b19beb2e8d6 Mon Sep 17 00:00:00 2001 From: windyboy Date: Tue, 8 Sep 2026 11:26:16 +0800 Subject: [PATCH] feat(flight-state): add V1.3.0 detail tables and domain command engine Introduce lossless detail-table schema and FlightStateEngine command model per flight-state-design-v2 (FieldCommand, FlightNextState, collection semantics). --- .../msgexchange/domain/flight/FieldCommand.kt | 23 +++ .../domain/flight/FlightNextState.kt | 22 +++ .../domain/flight/FlightStateEngine.kt | 142 +++++++++++++++++ .../V1.3.0__flight_detail_tables.sql | 146 ++++++++++++++++++ 4 files changed, 333 insertions(+) create mode 100644 src/main/kotlin/com/gzzn/omms/msgexchange/domain/flight/FieldCommand.kt create mode 100644 src/main/kotlin/com/gzzn/omms/msgexchange/domain/flight/FlightNextState.kt create mode 100644 src/main/kotlin/com/gzzn/omms/msgexchange/domain/flight/FlightStateEngine.kt create mode 100644 src/main/resources/db/migration/V1.3.0__flight_detail_tables.sql diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/domain/flight/FieldCommand.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/domain/flight/FieldCommand.kt new file mode 100644 index 0000000..b1f1753 --- /dev/null +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/domain/flight/FieldCommand.kt @@ -0,0 +1,23 @@ +package com.gzzn.omms.msgexchange.domain.flight + +/** + * 显式字段命令(flight-state-design-v2 §4)。 + */ +sealed interface ScalarCommand { + data object Unchanged : ScalarCommand + data class Set(val value: String) : ScalarCommand + data object Clear : ScalarCommand +} + +sealed interface CollectionCommand { + data object Unchanged : CollectionCommand + data class Replace(val items: List>) : CollectionCommand + data object Clear : CollectionCommand + data class Apply(val item: Map, val sourceSeq: String) : CollectionCommand +} + +data class FlightFieldCommands( + val flid: String, + val scalars: Map = emptyMap(), + val collections: Map = emptyMap(), +) diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/domain/flight/FlightNextState.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/domain/flight/FlightNextState.kt new file mode 100644 index 0000000..2e6a903 --- /dev/null +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/domain/flight/FlightNextState.kt @@ -0,0 +1,22 @@ +package com.gzzn.omms.msgexchange.domain.flight + +import com.fasterxml.jackson.databind.ObjectMapper +import com.gzzn.omms.msgexchange.infra.persistence.FlightFields + +data class FlightNextState( + val flid: String, + val scalars: Map, + val collections: Map>>, + val stateVersion: Long, + val lastMessageId: String, +) { + fun toFlightFields(mapper: ObjectMapper = ObjectMapper()): FlightFields { + val out = linkedMapOf() + out["FLID"] = flid + scalars.forEach { (k, v) -> out[k] = v } + collections.forEach { (key, items) -> + out[key] = mapper.writeValueAsString(items) + } + return out + } +} diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/domain/flight/FlightStateEngine.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/domain/flight/FlightStateEngine.kt new file mode 100644 index 0000000..6c6a552 --- /dev/null +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/domain/flight/FlightStateEngine.kt @@ -0,0 +1,142 @@ +package com.gzzn.omms.msgexchange.domain.flight + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import com.gzzn.omms.msgexchange.infra.persistence.FlightFields + +/** + * 从 legacy 字段视图解析/合并命令,并计算 nextState(v2 §4–§5)。 + */ +object FlightStateEngine { + private val mapper = ObjectMapper() + + private val COLLECTION_KEYS = setOf( + "GTDT", "CKDT", "CLDT", "PSDT", "CHDT", "DELY", "ABTM", "CHOT", "ROUT", "ERUT", + ) + + private val SEQ_ATTR = mapOf( + "GTDT" to "GTNO", + "CKDT" to "CKNO", + "CLDT" to "CLNO", + "PSDT" to "PSNO", + "CHDT" to "CHNO", + "DELY" to "DLNO", + "ABTM" to "ASNO", + "CHOT" to "CSNO", + "ROUT" to "RTNO", + "ERUT" to "RTNO", + ) + + /** DNLD/FLOP 字段集 → 命令(出现即 Set/Replace;未出现即 Unchanged)。 */ + fun commandsFromFields(flid: String, fields: FlightFields, snapshotReplace: Boolean): FlightFieldCommands { + val scalars = linkedMapOf() + val collections = linkedMapOf() + fields.forEach { (key, value) -> + if (key == "FLID") return@forEach + when { + key in COLLECTION_KEYS -> { + val items = parseCollection(value) + val seqAttr = SEQ_ATTR[key] + if (items.isEmpty() && seqAttr != null && fieldsContainsClearOnly(value)) { + collections[key] = CollectionCommand.Clear + } else { + collections[key] = CollectionCommand.Replace(items) + } + } + snapshotReplace -> scalars[key] = ScalarCommand.Set(value) + else -> scalars[key] = ScalarCommand.Set(value) + } + } + return FlightFieldCommands(flid, scalars, collections) + } + + fun apply( + current: FlightNextState?, + commands: FlightFieldCommands, + messageId: String, + bumpVersion: Boolean, + ): FlightNextState { + val baseScalars = current?.scalars?.toMutableMap() ?: mutableMapOf() + val baseCollections = current?.collections?.mapValues { it.value.toMutableList() } + ?.toMutableMap() ?: mutableMapOf() + val baseVersion = current?.stateVersion ?: 0L + + commands.scalars.forEach { (key, cmd) -> + when (cmd) { + ScalarCommand.Unchanged -> Unit + is ScalarCommand.Set -> baseScalars[key] = cmd.value + ScalarCommand.Clear -> baseScalars.remove(key) + } + } + + commands.collections.forEach { (key, cmd) -> + when (cmd) { + CollectionCommand.Unchanged -> Unit + is CollectionCommand.Replace -> baseCollections[key] = cmd.items.toMutableList() + CollectionCommand.Clear -> baseCollections.remove(key) + is CollectionCommand.Apply -> { + val list = baseCollections.getOrPut(key) { mutableListOf() }.toMutableList() + val idx = list.indexOfFirst { it[SEQ_ATTR[key] ?: ""] == cmd.sourceSeq } + if (idx >= 0) { + list[idx] = cmd.item + } else { + list.add(cmd.item) + } + baseCollections[key] = list + } + } + } + + val nextVersion = if (bumpVersion) baseVersion + 1 else baseVersion + return FlightNextState( + flid = commands.flid, + scalars = baseScalars, + collections = baseCollections.mapValues { it.value.toList() }, + stateVersion = nextVersion, + lastMessageId = messageId, + ) + } + + fun parseCollection(raw: String): List> { + val node = mapper.readTree(raw) + val items = when { + node.isNull -> emptyList() + node.isArray -> node.toList() + node.isObject -> listOf(node) + else -> throw IllegalArgumentException("collection value must be array or object") + } + return items.map { item -> + item.properties().associate { (k, v) -> + k to when { + v.isNull -> "" + v.isValueNode -> v.asText() + else -> v.toString() + } + } + } + } + + private fun fieldsContainsClearOnly(raw: String): Boolean { + val node = runCatching { mapper.readTree(raw) }.getOrNull() ?: return false + if (node.isArray && node.size() == 1) { + val seq = node[0].fields().asSequence().firstOrNull { it.key.endsWith("NO") }?.value?.asText() + return seq == "0" + } + return false + } + + /** 当前库态 → FlightNextState(供增量合并)。 */ + fun fromFlightFields(flid: String, fields: FlightFields, stateVersion: Long = 0L, lastMessageId: String = ""): FlightNextState { + val scalars = linkedMapOf() + val collections = linkedMapOf>>() + fields.forEach { (key, value) -> + if (key == "FLID") return@forEach + if (key in COLLECTION_KEYS) { + collections[key] = parseCollection(value) + } else { + scalars[key] = value + } + } + return FlightNextState(flid, scalars, collections, stateVersion, lastMessageId) + } +} diff --git a/src/main/resources/db/migration/V1.3.0__flight_detail_tables.sql b/src/main/resources/db/migration/V1.3.0__flight_detail_tables.sql new file mode 100644 index 0000000..764e730 --- /dev/null +++ b/src/main/resources/db/migration/V1.3.0__flight_detail_tables.sql @@ -0,0 +1,146 @@ +-- flight-state-design-v2 (ACM2-29): lossless detail tables + tracking columns + +ALTER TABLE flight_schd + ADD COLUMN IF NOT EXISTS last_message_id VARCHAR(64), + ADD COLUMN IF NOT EXISTS state_version BIGINT NOT NULL DEFAULT 0; + +ALTER TABLE schd_gen + ADD COLUMN IF NOT EXISTS last_message_id VARCHAR(64); + +CREATE TABLE IF NOT EXISTS flight_gate ( + flid VARCHAR(32) NOT NULL, + ordinal INTEGER NOT NULL, + source_seq VARCHAR(64), + record_version BIGINT NOT NULL DEFAULT 0, + gate VARCHAR(64), + pgot VARCHAR(64), + pgct VARCHAR(64), + gotm VARCHAR(64), + gctm VARCHAR(64), + gtyp VARCHAR(64), + created_at TIMESTAMP(6) WITH TIME ZONE NOT NULL, + updated_at TIMESTAMP(6) WITH TIME ZONE NOT NULL, + PRIMARY KEY (flid, ordinal) +); + +CREATE TABLE IF NOT EXISTS flight_checkin ( + flid VARCHAR(32) NOT NULL, + ordinal INTEGER NOT NULL, + source_seq VARCHAR(64), + record_version BIGINT NOT NULL DEFAULT 0, + chkc VARCHAR(64), + ccls VARCHAR(64), + pcot VARCHAR(64), + pcct VARCHAR(64), + cotm VARCHAR(64), + cctm VARCHAR(64), + ctyp VARCHAR(64), + created_at TIMESTAMP(6) WITH TIME ZONE NOT NULL, + updated_at TIMESTAMP(6) WITH TIME ZONE NOT NULL, + PRIMARY KEY (flid, ordinal) +); + +CREATE TABLE IF NOT EXISTS flight_belt ( + flid VARCHAR(32) NOT NULL, + ordinal INTEGER NOT NULL, + source_seq VARCHAR(64), + record_version BIGINT NOT NULL DEFAULT 0, + belt VARCHAR(64), + bcls VARCHAR(64), + pcot VARCHAR(64), + pcct VARCHAR(64), + fbag VARCHAR(64), + lbag VARCHAR(64), + btyp VARCHAR(64), + created_at TIMESTAMP(6) WITH TIME ZONE NOT NULL, + updated_at TIMESTAMP(6) WITH TIME ZONE NOT NULL, + PRIMARY KEY (flid, ordinal) +); + +CREATE TABLE IF NOT EXISTS flight_stand_plan ( + flid VARCHAR(32) NOT NULL, + ordinal INTEGER NOT NULL, + source_seq VARCHAR(64), + record_version BIGINT NOT NULL DEFAULT 0, + psst VARCHAR(64), + stst VARCHAR(64), + stet VARCHAR(64), + created_at TIMESTAMP(6) WITH TIME ZONE NOT NULL, + updated_at TIMESTAMP(6) WITH TIME ZONE NOT NULL, + PRIMARY KEY (flid, ordinal) +); + +CREATE TABLE IF NOT EXISTS flight_chute ( + flid VARCHAR(32) NOT NULL, + ordinal INTEGER NOT NULL, + source_seq VARCHAR(64), + record_version BIGINT NOT NULL DEFAULT 0, + chut VARCHAR(64), + chcls VARCHAR(64), + pcbt VARCHAR(64), + pcet VARCHAR(64), + cbtm VARCHAR(64), + cetm VARCHAR(64), + chtyp VARCHAR(64), + created_at TIMESTAMP(6) WITH TIME ZONE NOT NULL, + updated_at TIMESTAMP(6) WITH TIME ZONE NOT NULL, + PRIMARY KEY (flid, ordinal) +); + +CREATE TABLE IF NOT EXISTS flight_delay ( + flid VARCHAR(32) NOT NULL, + ordinal INTEGER NOT NULL, + source_seq VARCHAR(64), + record_version BIGINT NOT NULL DEFAULT 0, + code VARCHAR(64), + strt VARCHAR(64), + dura VARCHAR(64), + remc VARCHAR(80), + created_at TIMESTAMP(6) WITH TIME ZONE NOT NULL, + updated_at TIMESTAMP(6) WITH TIME ZONE NOT NULL, + PRIMARY KEY (flid, ordinal) +); + +CREATE TABLE IF NOT EXISTS flight_bridge_op ( + flid VARCHAR(32) NOT NULL, + ordinal INTEGER NOT NULL, + source_seq VARCHAR(64), + record_version BIGINT NOT NULL DEFAULT 0, + abdg VARCHAR(64), + abop VARCHAR(64), + aotm VARCHAR(64), + created_at TIMESTAMP(6) WITH TIME ZONE NOT NULL, + updated_at TIMESTAMP(6) WITH TIME ZONE NOT NULL, + PRIMARY KEY (flid, ordinal) +); + +CREATE TABLE IF NOT EXISTS flight_chock_op ( + flid VARCHAR(32) NOT NULL, + ordinal INTEGER NOT NULL, + source_seq VARCHAR(64), + record_version BIGINT NOT NULL DEFAULT 0, + chid VARCHAR(64), + chst VARCHAR(64), + chtm VARCHAR(64), + created_at TIMESTAMP(6) WITH TIME ZONE NOT NULL, + updated_at TIMESTAMP(6) WITH TIME ZONE NOT NULL, + PRIMARY KEY (flid, ordinal) +); + +CREATE TABLE IF NOT EXISTS flight_route_point ( + flid VARCHAR(32) NOT NULL, + ordinal INTEGER NOT NULL, + source_seq VARCHAR(64), + record_version BIGINT NOT NULL DEFAULT 0, + route_kind VARCHAR(8) NOT NULL, + apcd VARCHAR(64), + scat VARCHAR(64), + scdt VARCHAR(64), + created_at TIMESTAMP(6) WITH TIME ZONE NOT NULL, + updated_at TIMESTAMP(6) WITH TIME ZONE NOT NULL, + PRIMARY KEY (flid, ordinal) +); + +CREATE INDEX IF NOT EXISTS idx_flight_gate_flid ON flight_gate (flid); +CREATE INDEX IF NOT EXISTS idx_flight_checkin_flid ON flight_checkin (flid); +CREATE INDEX IF NOT EXISTS idx_flight_belt_flid ON flight_belt (flid);