refactor(flight-schd): 全宽表零子表收敛 16 个集合并定案 PIPELINE_LOCK 行锁 (ACM2-29)

依据 ACM2-29 新定案【全宽表·零子表】,废除「*_TXT 过渡 + 按需升独立子表」
原规划(对 XSD maxOccurs=99 的过度设计),结合报文样例与成都现场地服实际
规律(登机门 1~2、值机柜台 1~3、转盘 1~2、延误单有效、靠撤桥/轮挡各 1 次)
将 16 个明细集合全部收敛为 FLIGHT_SCHD 宽表标量列或紧凑 VARCHAR 字串。

行为变更:
- V1.2.0 迁移:新增集合平铺槽位列(GTDT×2/CKDT×3/CLDT×2/PSDT×2/CHDT×2,
  跨集合同名属性按 B 前缀/CH 前缀消解)、里程碑标量列(DELY_*、ABTM_A/D、
  CHOT_ON/OFF)、异常前缀标量列(FDIV/FRET/FLAB)、紧凑航路字串
  (ROUT_PATH/ERUT_PATH)与无界集合 JSON 字串列(SRVT/VIPF/MAFL_TEXT);
  删除全部 16 个 *_TXT 文本列;不建任何子表、零 CLOB;
- 仓储层:删除子表替换/回查机制,写侧集合键平铺为列(序号属性 0 = 显式
  删除标记,分舱复用条目抹平去重,超界按定案丢弃),读侧由平铺列重建
  16 个集合键,视图与 legacy flightInfo hash 保持同构(KAFKA_SCHD 线格式
  与 FS7 Diff 逐字段比对不受存储形态影响);
- I5 单写者锁修正:PIPELINE_LOCK 单行 SELECT ... FOR UPDATE 取代
  PG advisory lock,PG/Oracle 11g 同构,无 DBMS_LOCK DBA 特权依赖;
- FlightFieldsJson 对契约结构字段解析 JSON tree 输出原生数组/对象,
  杜绝集合被再次编码成字符串的双重转义。

不变量:消息严格 FIFO、单写者互斥、增量=字段级合并(集合键出现=单资源
集合级全量快照替换)、快照=整体替换、按代域化差删均不变。

迁移影响:本地开发库为一次性测试数据,已重置并由 Flyway 全新应用
V1.0.0→V1.2.0(同文件名内容变更,沿用旧库会触发校验和不匹配)。

验证:./gradlew test 全绿(66 个用例,含本地 PG 真实方言集成:集合槽位
替换/清除、里程碑与航路平铺回读、PIPELINE_LOCK NOWAIT 互斥与提交释放)。
This commit is contained in:
windyboy
2026-09-08 08:48:24 +08:00
parent ff6cec08c8
commit cd5de56937
8 changed files with 651 additions and 45 deletions
@@ -1,7 +1,6 @@
package com.gzzn.omms.msgexchange.infra.persistence
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.node.ObjectNode
/**
* 航班字段集(FLIGHT_SCHD 宽表字段)→ KAFKA_SCHD 事件载荷序列化。
@@ -11,9 +10,26 @@ import com.fasterxml.jackson.databind.node.ObjectNode
object FlightFieldsJson {
private val mapper = ObjectMapper()
/** Fields whose legacy wire representation is a JSON array/object rather than a JSON string. */
private val STRUCTURED_FIELDS = setOf(
"ROUT", "ERUT", "CHDT", "GTDT", "PSDT", "CKDT", "CLDT", "DELY",
"CHOT", "ABTM", "SRVT", "VIPF", "MAFL", "FDIV", "FRET", "FLAB",
)
fun toJson(fields: Map<String, String>): String {
val node = mapper.createObjectNode()
fields.toSortedMap().forEach { (k, v) -> node.put(k, v) }
fields.toSortedMap().forEach { (key, value) ->
if (key in STRUCTURED_FIELDS) {
val parsed = runCatching { mapper.readTree(value) }.getOrNull()
if (parsed != null && (parsed.isArray || parsed.isObject || parsed.isNull)) {
node.set<com.fasterxml.jackson.databind.JsonNode>(key, parsed)
} else {
node.put(key, value)
}
} else {
node.put(key, value)
}
}
return node.toString()
}
}
@@ -70,9 +70,12 @@ interface MsgEventRepository {
}
/**
* 阶段 A 运营航班权威与日计划代(ACM2-28 定案 + 11g 修订:废除 FLTR_JSON JSONB 整文档存储):
* 阶段 A 运营航班权威与日计划代(ACM2-28 定案 + ACM2-29 全宽表·零子表定案):
* - 表 FLIGHT_SCHD:一行一航班的运营航班宽表(FLID 主键 + FDAY 可空所属代 + SCHD.FLTR 标量字段列
* + 1:N 明细集合序列化文本列),字段即列、天然可索引可直查,PG/Oracle 11g 方言一致;
* + 集合平铺标量列/紧凑 VARCHAR 字串列),零子表、零 CLOB,字段即列、天然可索引可直查,
* PG/Oracle 11g 方言一致;读侧视图由平铺列重建集合键,与 legacy flightInfo hash 同构;
* - 运营资源消息语义 = 单资源集合级全量快照替换;外层航班增量仍为字段级合并(仅新增/覆盖);
* - 表 PIPELINE_LOCK:单写者行级互斥(SELECT ... FOR UPDATEPG/11g 同构,无 DBMS_LOCK 特权依赖);
* - 表 SCHD_GEN:各日代版本(SQL CAS);表 SCHD_GEN_FLID:当前代有效航班 FLID 集合(差删依据);
* - Redis 退出动态权威与全部写路径;事务 2 与快照发布全在自有库内单事务原子提交;
* - 增量更新(FLOP/ADFT):新插 FDAY=NULL,已有行保留原 FDAY,按字段列更新
@@ -1,5 +1,8 @@
package com.gzzn.omms.msgexchange.infra.persistence.jdbc
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.node.ObjectNode
import com.gzzn.omms.msgexchange.domain.ErrorClass
import com.gzzn.omms.msgexchange.domain.EventStatus
import com.gzzn.omms.msgexchange.domain.MsgEvent
@@ -296,7 +299,16 @@ class JdbcPumpJobRepository(
class JdbcPipelineTransactionManager(
private val ds: DataSource,
) : PipelineTransactionManager {
override fun <T> inTransaction(block: () -> T): T = ds.withTransaction(block)
override fun <T> inTransaction(block: () -> T): T = ds.withTransaction {
// Standard row lock works on PostgreSQL and Oracle 11g without DBMS_LOCK privileges.
// The lock is connection-scoped and is released automatically on commit, rollback, or crash.
ds.queryOne(
"SELECT lock_key FROM pipeline_lock WHERE lock_key = ? FOR UPDATE",
{ ps -> ps.setString(1, "FLIGHT_SCHD_WRITER") },
) { rs -> rs.getString("lock_key") }
?: error("missing FLIGHT_SCHD_WRITER row in PIPELINE_LOCK")
block()
}
}
@Singleton
@@ -317,35 +329,214 @@ class JdbcFlightSchdRepository(
"ABDG", "LPSDT", "ABN",
)
/** 1:N 明细集合键(库列名 = 键 + _TXT 后缀,序列化文本)。 */
private val COLLECTION_KEYS: List<String> = listOf(
"ROUT", "ERUT", "CHDT", "GTDT", "PSDT", "CKDT", "CLDT", "DELY",
"CHOT", "ABTM", "SRVT", "VIPF", "MAFL", "FDIV", "FRET", "FLAB",
/** 异常明细前缀标量列(FDIV/FRET/FLAB:规范 1:0..1 单值异常)。 */
private val EXCEPTION_COLUMNS: List<String> = listOf(
"FDIV_DDES", "FDIV_DDIR", "FDIV_REMC", "FRET_REID", "FRET_RSN",
"FLAB_ARES", "FLAB_RSN",
)
/** 库列名全集(标量原样;集合列带 _TXT 后缀)。 */
private val ALL_COLUMNS: List<String> = SCALAR_COLUMNS + COLLECTION_KEYS.map { "${it}_TXT" }
/**
* 平铺出现槽位集合(ACM2-29 零子表定案):槽位序号即报文序号属性(GTNO/CKNO/...),
* 列名 = legacy 属性名 + 槽位号;跨集合同名属性按 V1.2.0 注释加专属前缀
* (转盘时刻 BPCOT/BPCCT、通道等级/类型 CHCLS/CHTYP)。槽位数按现场地服
* 规律定界(登机门 2、值机柜台 3、转盘 2、计划机位 2、离港通道 2),
* 超界条目按定案丢弃,不再为 XSD maxOccurs=99 的理论上限设计。
*/
private data class SlotCollection(
val key: String,
val seqAttr: String,
val dedupeAttr: String,
val slots: List<Map<String, String>>, // 报文属性 → 库列名
)
/** 字段键 → 库列名白名单;未知键拒绝写入(fail fast,经处理边界落 FAILED(INFRA))。
* FLID 即主键列:写侧忽略该键(主键已承载),读侧合成返回,视图与 legacy hash 同构。 */
private val FIELD_TO_COLUMN: Map<String, String> =
SCALAR_COLUMNS.associateWith { it } + COLLECTION_KEYS.associateWith { "${it}_TXT" }
private fun slotCollection(
key: String,
seqAttr: String,
dedupeAttr: String,
attrs: List<String>,
count: Int,
renames: Map<String, String> = emptyMap(),
): SlotCollection = SlotCollection(
key = key,
seqAttr = seqAttr,
dedupeAttr = dedupeAttr,
slots = (1..count).map { i -> attrs.associateWith { attr -> (renames[attr] ?: attr) + i } },
)
private fun columnToFieldKey(column: String): String =
if (column.endsWith("_TXT")) column.removeSuffix("_TXT") else column
private val OCCURRENCE_COLLECTIONS: List<SlotCollection> = listOf(
slotCollection("GTDT", "GTNO", "GATE", listOf("GATE", "PGOT", "PGCT", "GOTM", "GCTM", "GTYP"), 2),
slotCollection(
"CKDT", "CKNO", "CHKC",
listOf("CHKC", "CCLS", "PCOT", "PCCT", "COTM", "CCTM", "CTYP"), 3,
),
slotCollection(
"CLDT", "CLNO", "BELT",
listOf("BELT", "BCLS", "PCOT", "PCCT", "FBAG", "LBAG", "BTYP"), 2,
renames = mapOf("PCOT" to "BPCOT", "PCCT" to "BPCCT"),
),
slotCollection("PSDT", "PSNO", "PSST", listOf("PSST", "STST", "STET"), 2),
slotCollection(
"CHDT", "CHNO", "CHUT",
listOf("CHUT", "CCLS", "PCBT", "PCET", "CBTM", "CETM", "CTYP"), 2,
renames = mapOf("CCLS" to "CHCLS", "CTYP" to "CHTYP"),
),
)
/** 里程碑/单值集合列(延误单有效覆盖语义、靠撤桥/轮挡时刻)。 */
private val MILESTONE_COLUMNS: List<String> = listOf(
"DELY_CODE", "DELY_STRT", "DELY_DURA", "DELY_REMC",
"ABTM_A", "ABTM_D", "CHOT_ON", "CHOT_OFF",
)
/** 紧凑航路字串列("APCD/SCAT/SCDT,...",≤7 站)。 */
private val PATH_COLUMNS: List<String> = listOf("ROUT_PATH", "ERUT_PATH")
/** 无界集合紧凑 JSON 数组字串列(键名去 _TEXT 后缀即 legacy 视图键,直存直取)。 */
private val TEXT_COLUMNS: List<String> = listOf("SRVT_TEXT", "VIPF_TEXT", "MAFL_TEXT")
/** 库列名全集(读侧 SELECT 与视图重建的稳定顺序,与 V1.2.0 迁移一致)。 */
private val ALL_COLUMNS: List<String> = SCALAR_COLUMNS + EXCEPTION_COLUMNS +
OCCURRENCE_COLLECTIONS.flatMap { c -> c.slots.flatMap(Map<String, String>::values) } +
MILESTONE_COLUMNS + PATH_COLUMNS + TEXT_COLUMNS
private val SCALAR_KEY_SET: Set<String> = SCALAR_COLUMNS.toSet()
/** 16 个集合视图键:写侧消费平铺、读侧重建,视图与 legacy flightInfo hash 同构。 */
private val COLLECTION_KEYS: Set<String> = OCCURRENCE_COLLECTIONS.map { it.key }.toSet() +
setOf("DELY", "ABTM", "CHOT", "ROUT", "ERUT", "SRVT", "VIPF", "MAFL", "FDIV", "FRET", "FLAB")
}
private val mapper = ObjectMapper()
private fun toSqlDate(day: String): java.sql.Date =
java.sql.Date.valueOf(day.trim().take(10))
private fun requireColumns(fields: FlightFields): List<Pair<String, String>> =
fields.mapNotNull { (key, value) ->
if (key == "FLID") return@mapNotNull null // 主键列:写侧忽略
val column = FIELD_TO_COLUMN[key]
?: throw IllegalArgumentException("unknown flight field: $key")
column to value
/** 集合键平铺 + 标量键校验 → (列名, 值);未知键拒绝写入(fail fast,经处理边界落 FAILED(INFRA))。
* FLID 即主键列:写侧忽略该键(主键已承载),读侧合成返回。 */
private fun storagePairs(fields: FlightFields): List<Pair<String, String?>> =
flattenForStorage(fields).mapNotNull { (key, value) ->
if (key == "FLID") null else key to value
}
/** 字段集 → 列名键值(标量键同名直取;16 个集合键消费为平铺列/紧凑字串列;
* 未知键 fail fast)。值为 null 表示显式清空(单资源集合级全量快照替换语义)。 */
private fun flattenForStorage(fields: FlightFields): Map<String, String?> {
val out = mutableMapOf<String, String?>()
fields.forEach { (key, value) ->
when {
key in SCALAR_KEY_SET -> out[key] = value
key == "FLID" || key in COLLECTION_KEYS -> Unit // 主键跳过;集合键由下方平铺消费
else -> throw IllegalArgumentException("unknown flight field: $key")
}
}
flattenExceptions(fields, out)
OCCURRENCE_COLLECTIONS.forEach { c -> flattenSlots(c, fields[c.key], out) }
flattenDelay(fields["DELY"], out)
flattenAirbridge(fields["ABTM"], out)
flattenChocks(fields["CHOT"], out)
fields["ROUT"]?.let { out["ROUT_PATH"] = routeToPath(it) }
fields["ERUT"]?.let { out["ERUT_PATH"] = routeToPath(it) }
fields["SRVT"]?.let { out["SRVT_TEXT"] = it }
fields["VIPF"]?.let { out["VIPF_TEXT"] = it }
fields["MAFL"]?.let { out["MAFL_TEXT"] = it }
return out
}
/** FDIV/FRET/FLAB1:0..1 单值异常对象 → 前缀标量列(自由文本取首个命中的文本键)。 */
private fun flattenExceptions(fields: FlightFields, out: MutableMap<String, String?>) {
fun flatten(key: String, mappings: Map<String, String>, textTarget: String) {
val raw = fields[key] ?: return
val node = runCatching { mapper.readTree(raw) }.getOrNull() ?: return
if (!node.isObject) return
mappings.forEach { (source, target) ->
node[source]?.takeUnless(JsonNode::isNull)?.asText()?.let { out[target] = it }
}
listOf("REMC", "RSN", "value", "#text").asSequence()
.mapNotNull { name -> node[name]?.takeUnless(JsonNode::isNull)?.asText() }
.firstOrNull()
?.let { out[textTarget] = it }
}
flatten("FDIV", mapOf("DDES" to "FDIV_DDES", "DDIR" to "FDIV_DDIR"), "FDIV_REMC")
flatten("FRET", mapOf("REID" to "FRET_REID"), "FRET_RSN")
flatten("FLAB", mapOf("ARES" to "FLAB_ARES"), "FLAB_RSN")
}
/** 出现槽位集合 → 槽位序号列。序号属性 "0" = 显式删除标记;按资源号抹平复用冗余;
* 超出现场定界槽位数的条目按定案丢弃;集合键出现但条目为空 = 全槽位清空。 */
private fun flattenSlots(c: SlotCollection, raw: String?, out: MutableMap<String, String?>) {
if (raw == null) return
val seen = mutableSetOf<String>()
val items = collectionItems(raw)
.filterNot { it[c.seqAttr]?.takeUnless(JsonNode::isNull)?.asText() == "0" }
.filter { item ->
val dedupeKey = item[c.dedupeAttr]?.takeUnless(JsonNode::isNull)?.asText()
dedupeKey == null || seen.add(dedupeKey)
}
.take(c.slots.size)
c.slots.forEachIndexed { index, slot ->
val item = items.getOrNull(index)
slot.forEach { (attr, column) ->
out[column] = item?.get(attr)?.takeUnless(JsonNode::isNull)?.asText()
}
}
}
/** 延误:业务上任意时刻仅 1 个有效延误(覆盖语义)→ 首条延误平铺。 */
private fun flattenDelay(raw: String?, out: MutableMap<String, String?>) {
if (raw == null) return
val first = collectionItems(raw).firstOrNull()
out["DELY_CODE"] = first?.get("CODE")?.takeUnless(JsonNode::isNull)?.asText()
out["DELY_STRT"] = first?.get("STRT")?.takeUnless(JsonNode::isNull)?.asText()
out["DELY_DURA"] = first?.get("DURA")?.takeUnless(JsonNode::isNull)?.asText()
out["DELY_REMC"] = first?.let { f ->
listOf("REMC", "value", "#text").asSequence()
.mapNotNull { name -> f[name]?.takeUnless(JsonNode::isNull)?.asText() }
.firstOrNull()
}
}
/** 靠撤桥:按 ABOP 取靠桥(A)/撤桥(D) 时刻;桥号沿用派生列 ABDG。 */
private fun flattenAirbridge(raw: String?, out: MutableMap<String, String?>) {
if (raw == null) return
val items = collectionItems(raw)
fun timeOf(op: String): String? = items.firstOrNull { it["ABOP"]?.asText() == op }
?.get("AOTM")?.takeUnless(JsonNode::isNull)?.asText()
out["ABTM_A"] = timeOf("A")
out["ABTM_D"] = timeOf("D")
}
/** 轮挡:按 CHID 取上轮挡(ON)/下轮挡(OFF) 时刻;机位沿用派生列 STND。 */
private fun flattenChocks(raw: String?, out: MutableMap<String, String?>) {
if (raw == null) return
val items = collectionItems(raw)
fun timeOf(id: String): String? = items.firstOrNull { it["CHID"]?.asText() == id }
?.get("CHTM")?.takeUnless(JsonNode::isNull)?.asText()
out["CHOT_ON"] = timeOf("ON")
out["CHOT_OFF"] = timeOf("OFF")
}
/** 航路集合 → 紧凑字串 "APCD/SCAT/SCDT,..."(空属性保留空槽位;空集合 → NULL = 显式清空)。 */
private fun routeToPath(raw: String): String? {
val items = collectionItems(raw)
if (items.isEmpty()) return null
return items.joinToString(",") { item ->
listOf("APCD", "SCAT", "SCDT").joinToString("/") { attr ->
item[attr]?.takeUnless(JsonNode::isNull)?.asText() ?: ""
}
}
}
/** 集合值统一形状:JSON 数组(多值)/对象(单值)/null(清空);其余形状 fail fast。 */
private fun collectionItems(raw: String): List<JsonNode> {
val node = mapper.readTree(raw)
return when {
node.isNull -> emptyList()
node.isArray -> node.toList()
node.isObject -> listOf(node)
else -> throw IllegalArgumentException("collection value must be a JSON array or object")
}
}
override fun upsertSnapshotBatch(day: String, flights: List<Pair<String, FlightFields>>, now: Instant) {
if (flights.isEmpty()) return
// 快照 = 航班字段整体替换:缺省字段置 NULL,批量 SQL 形状对全部行一致
@@ -364,11 +555,12 @@ class JdbcFlightSchdRepository(
conn.prepareStatement(sql).use { ps ->
var count = 0
for ((flid, fields) in flights) {
val stored = flattenForStorage(fields)
var i = 1
ps.setString(i++, flid)
ps.setDate(i++, sqlDate)
for (column in ALL_COLUMNS) {
ps.setString(i++, fields[columnToFieldKey(column)])
ps.setString(i++, stored[column])
}
ps.setTimestamp(i++, sqlTimestamp)
ps.setTimestamp(i++, sqlTimestamp)
@@ -412,18 +604,20 @@ class JdbcFlightSchdRepository(
ps.executeBatch()
}
}
// 字段列更新(legacy flightInfo hash 同语义:仅新增/覆盖,不删除缺失字段
// 字段列更新(legacy flightInfo hash 同语义:仅新增/覆盖,不删除缺失字段
// 集合键出现 = 单资源集合级全量快照替换,缺槽位置 NULL)
for (c in changes) {
val columns = requireColumns(c.fields)
if (columns.isEmpty()) continue
val sql = "UPDATE flight_schd SET " +
columns.joinToString(", ") { "${it.first} = ?" } +
", updated_at = ? WHERE flid = ?"
ds.update(sql) { ps ->
var i = 1
columns.forEach { (_, value) -> ps.setString(i++, value) }
ps.setTimestamp(i++, sqlTimestamp)
ps.setString(i, c.flid)
val columns = storagePairs(c.fields)
if (columns.isNotEmpty()) {
val sql = "UPDATE flight_schd SET " +
columns.joinToString(", ") { "${it.first} = ?" } +
", updated_at = ? WHERE flid = ?"
ds.update(sql) { ps ->
var i = 1
columns.forEach { (_, value) -> ps.setString(i++, value) }
ps.setTimestamp(i++, sqlTimestamp)
ps.setString(i, c.flid)
}
}
}
} finally {
@@ -446,25 +640,105 @@ class JdbcFlightSchdRepository(
return totalDeleted
}
/** 行 → 字段视图:标量列直读,16 个集合键由平铺列重建,与 legacy flightInfo hash 同构
* KAFKA_SCHD 线格式与 FS7 对拍逐字段比对均依赖该同构视图)。 */
private fun mapFlightRow(rs: java.sql.ResultSet): Pair<String, FlightFields> {
val flid = rs.getString("flid")
val fields = linkedMapOf<String, String>()
fields["FLID"] = flid // 视图与 legacy flightInfo hash 同构:FLID 字段常在
val row = linkedMapOf<String, String?>()
for (column in ALL_COLUMNS) {
val value = rs.getString(column) ?: continue
fields[columnToFieldKey(column)] = value
row[column] = rs.getString(column)
}
SCALAR_COLUMNS.forEach { column -> row[column]?.let { fields[column] = it } }
TEXT_COLUMNS.forEach { column -> row[column]?.let { fields[column.removeSuffix("_TEXT")] = it } }
exceptionView("FDIV", listOf("DDES" to "FDIV_DDES", "DDIR" to "FDIV_DDIR", "REMC" to "FDIV_REMC"), row)
?.let { fields["FDIV"] = it }
exceptionView("FRET", listOf("REID" to "FRET_REID", "RSN" to "FRET_RSN"), row)
?.let { fields["FRET"] = it }
exceptionView("FLAB", listOf("ARES" to "FLAB_ARES", "RSN" to "FLAB_RSN"), row)
?.let { fields["FLAB"] = it }
delayView(row)?.let { fields["DELY"] = it }
airbridgeView(row)?.let { fields["ABTM"] = it }
chocksView(row)?.let { fields["CHOT"] = it }
row["ROUT_PATH"]?.let { fields["ROUT"] = routeView(it) }
row["ERUT_PATH"]?.let { fields["ERUT"] = routeView(it) }
OCCURRENCE_COLLECTIONS.forEach { c -> slotView(c, row)?.let { fields[c.key] = it } }
return flid to fields
}
override fun findByFlid(flid: String): FlightFields? {
val row = ds.queryOne(
/** 前缀标量列 → 单值异常对象视图(缺失属性省略;全空 → 无键)。 */
private fun exceptionView(key: String, attrs: List<Pair<String, String>>, row: Map<String, String?>): String? {
val node = mapper.createObjectNode()
attrs.forEach { (attr, column) -> row[column]?.let { node.put(attr, it) } }
return if (node.size() == 0) null else mapper.writeValueAsString(node)
}
/** 延误列 → 0..1 条延误对象数组视图(legacy DELY 为可重复标签的数组形状)。 */
private fun delayView(row: Map<String, String?>): String? {
val node = mapper.createObjectNode()
listOf("CODE" to "DELY_CODE", "STRT" to "DELY_STRT", "DURA" to "DELY_DURA", "REMC" to "DELY_REMC")
.forEach { (attr, column) -> row[column]?.let { node.put(attr, it) } }
return if (node.size() == 0) null else mapper.writeValueAsString(arrayOf(node))
}
/** 靠撤桥时刻 → 保障里程碑数组视图(ASNO/ABOP 为操作序合成;桥号见派生列 ABDG)。 */
private fun airbridgeView(row: Map<String, String?>): String? {
val items = mutableListOf<ObjectNode>()
row["ABTM_A"]?.let {
items += mapper.createObjectNode().apply { put("ASNO", "1"); put("ABOP", "A"); put("AOTM", it) }
}
row["ABTM_D"]?.let {
items += mapper.createObjectNode().apply { put("ASNO", "2"); put("ABOP", "D"); put("AOTM", it) }
}
return if (items.isEmpty()) null else mapper.writeValueAsString(items)
}
/** 轮挡时刻 → 保障里程碑数组视图(CHID 标识上/下轮挡;机位见派生列 STND)。 */
private fun chocksView(row: Map<String, String?>): String? {
val items = mutableListOf<ObjectNode>()
row["CHOT_ON"]?.let {
items += mapper.createObjectNode().apply { put("CSNO", "1"); put("CHID", "ON"); put("CHTM", it) }
}
row["CHOT_OFF"]?.let {
items += mapper.createObjectNode().apply { put("CSNO", "1"); put("CHID", "OFF"); put("CHTM", it) }
}
return if (items.isEmpty()) null else mapper.writeValueAsString(items)
}
/** 紧凑航路字串 → legacy ROUTEDAILY 数组视图(RTNO 按段序合成;空槽位属性省略)。 */
private fun routeView(path: String): String {
val items = path.split(",").mapIndexed { index, segment ->
val parts = segment.split("/", limit = 3)
mapper.createObjectNode().apply {
put("RTNO", (index + 1).toString())
listOf("APCD", "SCAT", "SCDT").forEachIndexed { i, attr ->
if (i < parts.size && parts[i].isNotEmpty()) put(attr, parts[i])
}
}
}
return mapper.writeValueAsString(items)
}
/** 出现槽位列 → legacy 集合数组视图(空槽位跳过;序号属性按槽位合成)。 */
private fun slotView(c: SlotCollection, row: Map<String, String?>): String? {
val items = c.slots.mapIndexed { index, slot ->
mapper.createObjectNode().apply {
put(c.seqAttr, (index + 1).toString())
slot.forEach { (attr, column) -> row[column]?.let { put(attr, it) } }
}
}.filterNot { it.size() <= 1 }
return if (items.isEmpty()) null else mapper.writeValueAsString(items)
}
override fun findByFlid(flid: String): FlightFields? =
ds.queryOne(
"SELECT flid, ${ALL_COLUMNS.joinToString(", ")} FROM flight_schd WHERE flid = ?",
{ ps -> ps.setString(1, flid) },
::mapFlightRow,
)
return row?.second
}
)?.second
override fun findByFlids(flids: Collection<String>): Map<String, FlightFields> {
if (flids.isEmpty()) return emptyMap()
@@ -0,0 +1,118 @@
-- =====================================================================
-- ACM2-29 · 全宽表·零子表(Zero-Subtable Wide Table)定案落地
-- ---------------------------------------------------------------------
-- 废除「16 个明细集合保留 *_TXT 并逐个升独立子表」的原规划(对 XSD
-- maxOccurs=99 的过度设计)。结合 docs/legacy/SIS_AODB_RMS-V0.1.md 报文
-- 样例与成都现场(双流/天府)地服实际规律——登机门 1~2 个、值机柜台专属
-- 1~3 个、转盘 1~2 个、延误单有效、靠撤桥/轮挡各 1 次——全部集合收敛为
-- FLIGHT_SCHD 宽表的标准 VARCHAR 标量列或紧凑格式化字串,零子表、零 CLOB:
-- · 平铺标量列(属性名 + 出现槽位序号,可按需直接追加普通 B-tree,
-- 11g/PG 零方言成本):
-- GTDT → GATE/PGOT/PGCT/GOTM/GCTM/GTYP ×2 组(登机门 1~2 个)
-- CKDT → CHKC/CCLS/PCOT/PCCT/COTM/CCTM/CTYP ×3 组
-- (值机柜台 1~3 个;同柜台分舱复用条目在写侧抹平去重)
-- CLDT → BELT/BCLS/BPCOT/BPCCT/FBAG/LBAG/BTYP ×2 组(转盘 1~2 个;
-- B 前缀时刻 = BELT 专属,避免与值机 PCOT/PCCT 同名)
-- PSDT → PSST/STST/STET ×2 组(计划机位 1~2 个)
-- CHDT → CHUT/CHCLS/PCBT/PCET/CBTM/CETM/CHTYP ×2 组(离港通道 1~2 个;
-- CH 前缀 = CHUTE 专属,避免与值机 CCLS/CTYP 同名)
-- DELY → DELY_CODE/DELY_STRT/DELY_DURA/DELY_REMC(单有效覆盖语义)
-- ABTM → ABTM_A/ABTM_D(靠桥/撤桥时刻;桥号沿用派生列 ABDG)
-- CHOT → CHOT_ON/CHOT_OFF(上/下轮挡时刻;机位沿用派生列 STND)
-- FDIV/FRET/FLAB → 前缀标量列(规范 1:0..1 单值异常)
-- 超出现场定界的条目按定案丢弃(不再为 XSD 理论上限设计)。
-- · 紧凑 VARCHAR 字串列(无界/航路型集合,11g VARCHAR2 内联存储,非 CLOB):
-- ROUT/ERUT → ROUT_PATH/ERUT_PATH(≤7 站紧凑航路 "APCD/SCAT/SCDT,..."
-- SRVT/VIPF/MAFL → *_TEXT(服务/VIP/共享列表条数无 XSD 上限,存 JSON 数组;
-- 超长写侧 fail fast,不允许静默截断)
-- · 值语义保持 legacy 字符串原样;读侧视图由标量列重建集合键,
-- 与 legacy flightInfo hash 同构(KAFKA_SCHD 线格式与 FS7 对拍不变)。
-- · 运营资源消息语义 = 单资源集合级全量快照替换;外层航班字段仍为
-- 字段级增量合并(集合键出现即整集合覆盖,缺槽位置 NULL)。
-- ---------------------------------------------------------------------
-- 交叉实例单写者互斥(I5 修正定案):PIPELINE_LOCK 行级锁。主泵写事务
-- 持有 FLIGHT_SCHD_WRITER 行 SELECT ... FOR UPDATE;连接断开、提交或回滚
-- 时由数据库释放,PG 与 Oracle 11g 同构,无 DBMS_LOCK DBA 特权依赖。
-- =====================================================================
ALTER TABLE FLIGHT_SCHD
-- 登机门 GTDT(现场 1~2 个)
ADD COLUMN GATE1 VARCHAR(64), ADD COLUMN PGOT1 VARCHAR(64), ADD COLUMN PGCT1 VARCHAR(64),
ADD COLUMN GOTM1 VARCHAR(64), ADD COLUMN GCTM1 VARCHAR(64), ADD COLUMN GTYP1 VARCHAR(64),
ADD COLUMN GATE2 VARCHAR(64), ADD COLUMN PGOT2 VARCHAR(64), ADD COLUMN PGCT2 VARCHAR(64),
ADD COLUMN GOTM2 VARCHAR(64), ADD COLUMN GCTM2 VARCHAR(64), ADD COLUMN GTYP2 VARCHAR(64),
-- 值机柜台 CKDT(现场专属 1~3 个,分舱复用条目写侧抹平)
ADD COLUMN CHKC1 VARCHAR(64), ADD COLUMN CCLS1 VARCHAR(64),
ADD COLUMN PCOT1 VARCHAR(64), ADD COLUMN PCCT1 VARCHAR(64),
ADD COLUMN COTM1 VARCHAR(64), ADD COLUMN CCTM1 VARCHAR(64), ADD COLUMN CTYP1 VARCHAR(64),
ADD COLUMN CHKC2 VARCHAR(64), ADD COLUMN CCLS2 VARCHAR(64),
ADD COLUMN PCOT2 VARCHAR(64), ADD COLUMN PCCT2 VARCHAR(64),
ADD COLUMN COTM2 VARCHAR(64), ADD COLUMN CCTM2 VARCHAR(64), ADD COLUMN CTYP2 VARCHAR(64),
ADD COLUMN CHKC3 VARCHAR(64), ADD COLUMN CCLS3 VARCHAR(64),
ADD COLUMN PCOT3 VARCHAR(64), ADD COLUMN PCCT3 VARCHAR(64),
ADD COLUMN COTM3 VARCHAR(64), ADD COLUMN CCTM3 VARCHAR(64), ADD COLUMN CTYP3 VARCHAR(64),
-- 行李转盘 CLDT(现场 1~2 个;B 前缀时刻 = BELT 专属)
ADD COLUMN BELT1 VARCHAR(64), ADD COLUMN BCLS1 VARCHAR(64),
ADD COLUMN BPCOT1 VARCHAR(64), ADD COLUMN BPCCT1 VARCHAR(64),
ADD COLUMN FBAG1 VARCHAR(64), ADD COLUMN LBAG1 VARCHAR(64), ADD COLUMN BTYP1 VARCHAR(64),
ADD COLUMN BELT2 VARCHAR(64), ADD COLUMN BCLS2 VARCHAR(64),
ADD COLUMN BPCOT2 VARCHAR(64), ADD COLUMN BPCCT2 VARCHAR(64),
ADD COLUMN FBAG2 VARCHAR(64), ADD COLUMN LBAG2 VARCHAR(64), ADD COLUMN BTYP2 VARCHAR(64),
-- 计划机位 PSDT(现场 1~2 个)
ADD COLUMN PSST1 VARCHAR(64), ADD COLUMN STST1 VARCHAR(64), ADD COLUMN STET1 VARCHAR(64),
ADD COLUMN PSST2 VARCHAR(64), ADD COLUMN STST2 VARCHAR(64), ADD COLUMN STET2 VARCHAR(64),
-- 离港行李通道 CHDT(现场 1~2 个;CH 前缀 = CHUTE 专属)
ADD COLUMN CHUT1 VARCHAR(64), ADD COLUMN CHCLS1 VARCHAR(64),
ADD COLUMN PCBT1 VARCHAR(64), ADD COLUMN PCET1 VARCHAR(64),
ADD COLUMN CBTM1 VARCHAR(64), ADD COLUMN CETM1 VARCHAR(64), ADD COLUMN CHTYP1 VARCHAR(64),
ADD COLUMN CHUT2 VARCHAR(64), ADD COLUMN CHCLS2 VARCHAR(64),
ADD COLUMN PCBT2 VARCHAR(64), ADD COLUMN PCET2 VARCHAR(64),
ADD COLUMN CBTM2 VARCHAR(64), ADD COLUMN CETM2 VARCHAR(64), ADD COLUMN CHTYP2 VARCHAR(64),
-- 延误 DELY(单有效覆盖语义)
ADD COLUMN DELY_CODE VARCHAR(64),
ADD COLUMN DELY_STRT VARCHAR(64),
ADD COLUMN DELY_DURA VARCHAR(64),
ADD COLUMN DELY_REMC VARCHAR(80),
-- 靠撤桥 ABTM / 轮挡 CHOT(保障里程碑时刻)
ADD COLUMN ABTM_A VARCHAR(64),
ADD COLUMN ABTM_D VARCHAR(64),
ADD COLUMN CHOT_ON VARCHAR(64),
ADD COLUMN CHOT_OFF VARCHAR(64),
-- 异常明细 FDIV/FRET/FLAB(规范 1:0..1 单值异常)
ADD COLUMN FDIV_DDES VARCHAR(64),
ADD COLUMN FDIV_DDIR VARCHAR(64),
ADD COLUMN FDIV_REMC VARCHAR(80),
ADD COLUMN FRET_REID VARCHAR(64),
ADD COLUMN FRET_RSN VARCHAR(80),
ADD COLUMN FLAB_ARES VARCHAR(64),
ADD COLUMN FLAB_RSN VARCHAR(80),
-- 紧凑航路字串(≤7 站,免除多表关联)
ADD COLUMN ROUT_PATH VARCHAR(256),
ADD COLUMN ERUT_PATH VARCHAR(256),
-- 无界集合紧凑 JSON 数组字串(11g VARCHAR2 内联,非 CLOB;超长写侧 fail fast
ADD COLUMN SRVT_TEXT VARCHAR(2000),
ADD COLUMN VIPF_TEXT VARCHAR(512),
ADD COLUMN MAFL_TEXT VARCHAR(512);
-- 16 个集合的过渡 *_TXT 文本列全清场
ALTER TABLE FLIGHT_SCHD
DROP COLUMN ROUT_TXT, DROP COLUMN ERUT_TXT, DROP COLUMN CHDT_TXT,
DROP COLUMN GTDT_TXT, DROP COLUMN PSDT_TXT, DROP COLUMN CKDT_TXT,
DROP COLUMN CLDT_TXT, DROP COLUMN DELY_TXT, DROP COLUMN CHOT_TXT,
DROP COLUMN ABTM_TXT, DROP COLUMN SRVT_TXT, DROP COLUMN VIPF_TXT,
DROP COLUMN MAFL_TXT, DROP COLUMN FDIV_TXT, DROP COLUMN FRET_TXT,
DROP COLUMN FLAB_TXT;
-- 运营查询索引按需逐列追加示例(11g/PG 均为普通 B-tree,零方言成本):
-- CREATE INDEX idx_flight_schd_gate1 ON FLIGHT_SCHD (GATE1);
-- CREATE INDEX idx_flight_schd_chkc1 ON FLIGHT_SCHD (CHKC1);
-- CREATE INDEX idx_flight_schd_dely_code ON FLIGHT_SCHD (DELY_CODE);
-- 交叉实例单写者互斥行(I5SELECT ... FOR UPDATE 行级锁)
CREATE TABLE PIPELINE_LOCK (
LOCK_KEY VARCHAR(32) NOT NULL PRIMARY KEY,
OWNER_INFO VARCHAR(128),
UPDATED_AT TIMESTAMP(6) WITH TIME ZONE NOT NULL
);
INSERT INTO PIPELINE_LOCK (LOCK_KEY, OWNER_INFO, UPDATED_AT)
VALUES ('FLIGHT_SCHD_WRITER', NULL, CURRENT_TIMESTAMP);