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).
This commit is contained in:
windyboy
2026-09-08 11:26:16 +08:00
parent cd5de56937
commit 57bb5ea233
4 changed files with 333 additions and 0 deletions
@@ -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<Map<String, String>>) : CollectionCommand
data object Clear : CollectionCommand
data class Apply(val item: Map<String, String>, val sourceSeq: String) : CollectionCommand
}
data class FlightFieldCommands(
val flid: String,
val scalars: Map<String, ScalarCommand> = emptyMap(),
val collections: Map<String, CollectionCommand> = emptyMap(),
)
@@ -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<String, String>,
val collections: Map<String, List<Map<String, String>>>,
val stateVersion: Long,
val lastMessageId: String,
) {
fun toFlightFields(mapper: ObjectMapper = ObjectMapper()): FlightFields {
val out = linkedMapOf<String, String>()
out["FLID"] = flid
scalars.forEach { (k, v) -> out[k] = v }
collections.forEach { (key, items) ->
out[key] = mapper.writeValueAsString(items)
}
return out
}
}
@@ -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<String, ScalarCommand>()
val collections = linkedMapOf<String, CollectionCommand>()
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<Map<String, String>> {
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<String, String>()
val collections = linkedMapOf<String, List<Map<String, String>>>()
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)
}
}
@@ -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);