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