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:
@@ -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);
|
||||||
Reference in New Issue
Block a user