feat(persistence): v2 persistNextStates dual-write and detail-table reads
Wire findNextStateByFlid, CAS last_message_id, detail-table CRUD, and stub parity for incremental/snapshot nextState persistence without legacy slot writes.
This commit is contained in:
@@ -97,6 +97,7 @@ interface FlightSchdRepository {
|
|||||||
val fday: String,
|
val fday: String,
|
||||||
val version: Long,
|
val version: Long,
|
||||||
val flids: Set<String>,
|
val flids: Set<String>,
|
||||||
|
val lastMessageId: String? = null,
|
||||||
val updatedAt: Instant = Instant.now(),
|
val updatedAt: Instant = Instant.now(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -112,6 +113,9 @@ interface FlightSchdRepository {
|
|||||||
/** 点查单航班字段集;无字段行(含航班不存在)返回 null。 */
|
/** 点查单航班字段集;无字段行(含航班不存在)返回 null。 */
|
||||||
fun findByFlid(flid: String): FlightFields?
|
fun findByFlid(flid: String): FlightFields?
|
||||||
|
|
||||||
|
/** v2:点查单航班 nextState(含 state_version / last_message_id 追踪字段)。 */
|
||||||
|
fun findNextStateByFlid(flid: String): com.gzzn.omms.msgexchange.domain.flight.FlightNextState?
|
||||||
|
|
||||||
/** 点查多航班字段集:FLID → 字段集,仅含实际存在的航班。 */
|
/** 点查多航班字段集:FLID → 字段集,仅含实际存在的航班。 */
|
||||||
fun findByFlids(flids: Collection<String>): Map<String, FlightFields>
|
fun findByFlids(flids: Collection<String>): Map<String, FlightFields>
|
||||||
|
|
||||||
@@ -140,6 +144,17 @@ interface FlightSchdRepository {
|
|||||||
* 历史代清理:清理 cutoffDay 之前的历史代记录(与 FLIGHT_SCHD 历史清场生命周期对齐)。
|
* 历史代清理:清理 cutoffDay 之前的历史代记录(与 FLIGHT_SCHD 历史清场生命周期对齐)。
|
||||||
*/
|
*/
|
||||||
fun deleteGenBefore(cutoffDay: String): Int
|
fun deleteGenBefore(cutoffDay: String): Int
|
||||||
|
|
||||||
|
/**
|
||||||
|
* v2 无损明细:将 nextState 写入宽表 + 明细表,并更新追踪字段。
|
||||||
|
* 必须在 PipelineTransactionManager 事务内调用。
|
||||||
|
*/
|
||||||
|
fun persistNextStates(
|
||||||
|
day: String?,
|
||||||
|
states: List<com.gzzn.omms.msgexchange.domain.flight.FlightNextState>,
|
||||||
|
snapshotReplace: Boolean,
|
||||||
|
now: Instant = Instant.now(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 事务管理器抽象:自有 PG 单事务原子保障。 */
|
/** 事务管理器抽象:自有 PG 单事务原子保障。 */
|
||||||
|
|||||||
+179
@@ -0,0 +1,179 @@
|
|||||||
|
package com.gzzn.omms.msgexchange.infra.persistence.jdbc
|
||||||
|
|
||||||
|
import com.gzzn.omms.msgexchange.domain.flight.FlightNextState
|
||||||
|
import java.sql.Timestamp
|
||||||
|
import java.time.Instant
|
||||||
|
import javax.sql.DataSource
|
||||||
|
|
||||||
|
/**
|
||||||
|
* v2 明细表读写(flight-state-design-v2 §3.2)。
|
||||||
|
* 与宽表双写;读路径优先明细表(完整顺序/源序号保真)。
|
||||||
|
*/
|
||||||
|
internal object FlightDetailTables {
|
||||||
|
/** 由明细表承载的集合键(v2 读写的权威来源)。 */
|
||||||
|
val DETAIL_COLLECTION_KEYS: Set<String> = setOf(
|
||||||
|
"GTDT", "CKDT", "CLDT", "PSDT", "CHDT", "DELY", "ABTM", "CHOT", "ROUT", "ERUT",
|
||||||
|
)
|
||||||
|
|
||||||
|
private data class TableSpec(
|
||||||
|
val table: String,
|
||||||
|
val collectionKey: String,
|
||||||
|
val seqAttr: String,
|
||||||
|
val columns: List<Pair<String, String>>, // json attr -> db column
|
||||||
|
)
|
||||||
|
|
||||||
|
private val SPECS = listOf(
|
||||||
|
TableSpec("flight_gate", "GTDT", "GTNO", listOf(
|
||||||
|
"GATE" to "gate", "PGOT" to "pgot", "PGCT" to "pgct", "GOTM" to "gotm", "GCTM" to "gctm", "GTYP" to "gtyp",
|
||||||
|
)),
|
||||||
|
TableSpec("flight_checkin", "CKDT", "CKNO", listOf(
|
||||||
|
"CHKC" to "chkc", "CCLS" to "ccls", "PCOT" to "pcot", "PCCT" to "pcct", "COTM" to "cotm", "CCTM" to "cctm", "CTYP" to "ctyp",
|
||||||
|
)),
|
||||||
|
TableSpec("flight_belt", "CLDT", "CLNO", listOf(
|
||||||
|
"BELT" to "belt", "BCLS" to "bcls", "PCOT" to "pcot", "PCCT" to "pcct", "FBAG" to "fbag", "LBAG" to "lbag", "BTYP" to "btyp",
|
||||||
|
)),
|
||||||
|
TableSpec("flight_stand_plan", "PSDT", "PSNO", listOf(
|
||||||
|
"PSST" to "psst", "STST" to "stst", "STET" to "stet",
|
||||||
|
)),
|
||||||
|
TableSpec("flight_chute", "CHDT", "CHNO", listOf(
|
||||||
|
"CHUT" to "chut", "CHCLS" to "chcls", "PCBT" to "pcbt", "PCET" to "pcet", "CBTM" to "cbtm", "CETM" to "cetm", "CHTYP" to "chtyp",
|
||||||
|
)),
|
||||||
|
TableSpec("flight_delay", "DELY", "DLNO", listOf(
|
||||||
|
"CODE" to "code", "STRT" to "strt", "DURA" to "dura", "REMC" to "remc",
|
||||||
|
)),
|
||||||
|
TableSpec("flight_bridge_op", "ABTM", "ASNO", listOf(
|
||||||
|
"ABDG" to "abdg", "ABOP" to "abop", "AOTM" to "aotm",
|
||||||
|
)),
|
||||||
|
TableSpec("flight_chock_op", "CHOT", "CSNO", listOf(
|
||||||
|
"CHID" to "chid", "CHST" to "chst", "CHTM" to "chtm",
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun replaceAll(ds: DataSource, state: FlightNextState, now: Instant) {
|
||||||
|
val ts = now.toSqlTimestamp()
|
||||||
|
val conn = ds.obtainConnection()
|
||||||
|
try {
|
||||||
|
for (spec in SPECS) {
|
||||||
|
ds.update("DELETE FROM ${spec.table} WHERE flid = ?") { ps -> ps.setString(1, state.flid) }
|
||||||
|
val items = state.collections[spec.collectionKey] ?: continue
|
||||||
|
if (items.isEmpty()) continue
|
||||||
|
val colNames = listOf("flid", "ordinal", "source_seq", "record_version") +
|
||||||
|
spec.columns.map { it.second } + listOf("created_at", "updated_at")
|
||||||
|
val sql = "INSERT INTO ${spec.table} (${colNames.joinToString(", ")}) VALUES (${
|
||||||
|
colNames.joinToString(", ") { "?" }
|
||||||
|
})"
|
||||||
|
conn.prepareStatement(sql).use { ps ->
|
||||||
|
items.forEachIndexed { index, item ->
|
||||||
|
var i = 1
|
||||||
|
ps.setString(i++, state.flid)
|
||||||
|
ps.setInt(i++, index + 1)
|
||||||
|
ps.setString(i++, item[spec.seqAttr])
|
||||||
|
ps.setLong(i++, state.stateVersion)
|
||||||
|
spec.columns.forEach { (attr, _) -> ps.setString(i++, item[attr]) }
|
||||||
|
ps.setTimestamp(i++, ts)
|
||||||
|
ps.setTimestamp(i++, ts)
|
||||||
|
ps.addBatch()
|
||||||
|
}
|
||||||
|
ps.executeBatch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
replaceRoutes(ds, state, now)
|
||||||
|
} finally {
|
||||||
|
conn.releaseIfNotInTransaction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun replaceRoutes(ds: DataSource, state: FlightNextState, now: Instant) {
|
||||||
|
val ts = now.toSqlTimestamp()
|
||||||
|
ds.update("DELETE FROM flight_route_point WHERE flid = ?") { ps -> ps.setString(1, state.flid) }
|
||||||
|
listOf("ROUT" to "ROUT", "ERUT" to "ERUT").forEach { (key, kind) ->
|
||||||
|
val items = state.collections[key] ?: return@forEach
|
||||||
|
if (items.isEmpty()) return@forEach
|
||||||
|
val conn = ds.obtainConnection()
|
||||||
|
try {
|
||||||
|
conn.prepareStatement(
|
||||||
|
"INSERT INTO flight_route_point (flid, ordinal, source_seq, record_version, route_kind, apcd, scat, scdt, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
).use { ps ->
|
||||||
|
items.forEachIndexed { index, item ->
|
||||||
|
ps.setString(1, state.flid)
|
||||||
|
ps.setInt(2, index + 1)
|
||||||
|
ps.setString(3, item["RTNO"])
|
||||||
|
ps.setLong(4, state.stateVersion)
|
||||||
|
ps.setString(5, kind)
|
||||||
|
ps.setString(6, item["APCD"])
|
||||||
|
ps.setString(7, item["SCAT"])
|
||||||
|
ps.setString(8, item["SCDT"])
|
||||||
|
ps.setTimestamp(9, ts)
|
||||||
|
ps.setTimestamp(10, ts)
|
||||||
|
ps.addBatch()
|
||||||
|
}
|
||||||
|
ps.executeBatch()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
conn.releaseIfNotInTransaction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteForFlids(ds: DataSource, flids: Collection<String>) {
|
||||||
|
if (flids.isEmpty()) return
|
||||||
|
val tables = SPECS.map { it.table } + "flight_route_point"
|
||||||
|
for (table in tables) {
|
||||||
|
for (chunk in flids.chunked(200)) {
|
||||||
|
val placeholders = chunk.joinToString(",") { "?" }
|
||||||
|
ds.update("DELETE FROM $table WHERE flid IN ($placeholders)") { ps ->
|
||||||
|
chunk.forEachIndexed { i, flid -> ps.setString(i + 1, flid) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadCollections(ds: DataSource, flid: String): Map<String, String> {
|
||||||
|
val out = linkedMapOf<String, String>()
|
||||||
|
val mapper = com.fasterxml.jackson.databind.ObjectMapper()
|
||||||
|
for (spec in SPECS) {
|
||||||
|
val rows = ds.query(
|
||||||
|
"SELECT * FROM ${spec.table} WHERE flid = ? ORDER BY ordinal ASC",
|
||||||
|
{ ps -> ps.setString(1, flid) },
|
||||||
|
) { rs ->
|
||||||
|
spec.columns.associate { (attr, col) -> attr to rs.getString(col) }.toMutableMap().apply {
|
||||||
|
put(spec.seqAttr, rs.getString("source_seq"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (rows.isNotEmpty()) {
|
||||||
|
out[spec.collectionKey] = mapper.writeValueAsString(rows)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
listOf("ROUT" to "ROUT", "ERUT" to "ERUT").forEach { (key, kind) ->
|
||||||
|
val rows = ds.query(
|
||||||
|
"SELECT source_seq, apcd, scat, scdt FROM flight_route_point WHERE flid = ? AND route_kind = ? ORDER BY ordinal ASC",
|
||||||
|
{ ps -> ps.setString(1, flid); ps.setString(2, kind) },
|
||||||
|
) { rs ->
|
||||||
|
mapOf(
|
||||||
|
"RTNO" to rs.getString("source_seq"),
|
||||||
|
"APCD" to rs.getString("apcd"),
|
||||||
|
"SCAT" to rs.getString("scat"),
|
||||||
|
"SCDT" to rs.getString("scdt"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (rows.isNotEmpty()) {
|
||||||
|
out[key] = mapper.writeValueAsString(rows)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasAnyDetailRows(ds: DataSource, flid: String): Boolean {
|
||||||
|
for (spec in SPECS) {
|
||||||
|
val count = ds.queryOne(
|
||||||
|
"SELECT 1 FROM ${spec.table} WHERE flid = ? LIMIT 1",
|
||||||
|
{ ps -> ps.setString(1, flid) },
|
||||||
|
) { 1 }
|
||||||
|
if (count != null) return true
|
||||||
|
}
|
||||||
|
return ds.queryOne(
|
||||||
|
"SELECT 1 FROM flight_route_point WHERE flid = ? LIMIT 1",
|
||||||
|
{ ps -> ps.setString(1, flid) },
|
||||||
|
) { 1 } != null
|
||||||
|
}
|
||||||
|
}
|
||||||
+153
-17
@@ -394,6 +394,9 @@ class JdbcFlightSchdRepository(
|
|||||||
/** 无界集合紧凑 JSON 数组字串列(键名去 _TEXT 后缀即 legacy 视图键,直存直取)。 */
|
/** 无界集合紧凑 JSON 数组字串列(键名去 _TEXT 后缀即 legacy 视图键,直存直取)。 */
|
||||||
private val TEXT_COLUMNS: List<String> = listOf("SRVT_TEXT", "VIPF_TEXT", "MAFL_TEXT")
|
private val TEXT_COLUMNS: List<String> = listOf("SRVT_TEXT", "VIPF_TEXT", "MAFL_TEXT")
|
||||||
|
|
||||||
|
/** v2 主表写入列:标量 + 异常 + 无界文本;集合由明细表承载,不经槽位/里程碑列。 */
|
||||||
|
private val SCALAR_WRITE_COLUMNS: List<String> = SCALAR_COLUMNS + EXCEPTION_COLUMNS + TEXT_COLUMNS
|
||||||
|
|
||||||
/** 库列名全集(读侧 SELECT 与视图重建的稳定顺序,与 V1.2.0 迁移一致)。 */
|
/** 库列名全集(读侧 SELECT 与视图重建的稳定顺序,与 V1.2.0 迁移一致)。 */
|
||||||
private val ALL_COLUMNS: List<String> = SCALAR_COLUMNS + EXCEPTION_COLUMNS +
|
private val ALL_COLUMNS: List<String> = SCALAR_COLUMNS + EXCEPTION_COLUMNS +
|
||||||
OCCURRENCE_COLLECTIONS.flatMap { c -> c.slots.flatMap(Map<String, String>::values) } +
|
OCCURRENCE_COLLECTIONS.flatMap { c -> c.slots.flatMap(Map<String, String>::values) } +
|
||||||
@@ -442,7 +445,22 @@ class JdbcFlightSchdRepository(
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
/** FDIV/FRET/FLAB:1:0..1 单值异常对象 → 前缀标量列(自由文本取首个命中的文本键)。 */
|
/** v2:仅标量/异常/文本列 flatten;集合键由明细表读写,不经过槽位列。 */
|
||||||
|
private fun flattenScalarsOnly(scalars: Map<String, String>): Map<String, String?> {
|
||||||
|
val fields = linkedMapOf<String, String>()
|
||||||
|
scalars.forEach { (key, value) -> fields[key] = value }
|
||||||
|
val out = mutableMapOf<String, String?>()
|
||||||
|
scalars.forEach { (key, value) ->
|
||||||
|
when {
|
||||||
|
key in SCALAR_KEY_SET -> out[key] = value
|
||||||
|
key in setOf("SRVT", "VIPF", "MAFL") -> out["${key}_TEXT"] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flattenExceptions(fields, out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
/** FDIV/FRET/FLAB:1:0..1 单值异常对象 → 前缀标量列(自由文本取首个命中的文本键)。 */
|
||||||
private fun flattenExceptions(fields: FlightFields, out: MutableMap<String, String?>) {
|
private fun flattenExceptions(fields: FlightFields, out: MutableMap<String, String?>) {
|
||||||
fun flatten(key: String, mappings: Map<String, String>, textTarget: String) {
|
fun flatten(key: String, mappings: Map<String, String>, textTarget: String) {
|
||||||
val raw = fields[key] ?: return
|
val raw = fields[key] ?: return
|
||||||
@@ -627,6 +645,7 @@ class JdbcFlightSchdRepository(
|
|||||||
|
|
||||||
override fun deleteDiffByDay(day: String, delFlids: Collection<String>): Int {
|
override fun deleteDiffByDay(day: String, delFlids: Collection<String>): Int {
|
||||||
if (delFlids.isEmpty()) return 0
|
if (delFlids.isEmpty()) return 0
|
||||||
|
FlightDetailTables.deleteForFlids(ds, delFlids)
|
||||||
var totalDeleted = 0
|
var totalDeleted = 0
|
||||||
val sqlDate = toSqlDate(day)
|
val sqlDate = toSqlDate(day)
|
||||||
for (chunk in delFlids.chunked(200)) {
|
for (chunk in delFlids.chunked(200)) {
|
||||||
@@ -660,12 +679,17 @@ class JdbcFlightSchdRepository(
|
|||||||
?.let { fields["FRET"] = it }
|
?.let { fields["FRET"] = it }
|
||||||
exceptionView("FLAB", listOf("ARES" to "FLAB_ARES", "RSN" to "FLAB_RSN"), row)
|
exceptionView("FLAB", listOf("ARES" to "FLAB_ARES", "RSN" to "FLAB_RSN"), row)
|
||||||
?.let { fields["FLAB"] = it }
|
?.let { fields["FLAB"] = it }
|
||||||
delayView(row)?.let { fields["DELY"] = it }
|
val detailCollections = FlightDetailTables.loadCollections(ds, flid)
|
||||||
airbridgeView(row)?.let { fields["ABTM"] = it }
|
val detailKeys = detailCollections.keys
|
||||||
chocksView(row)?.let { fields["CHOT"] = it }
|
if ("DELY" !in detailKeys) delayView(row)?.let { fields["DELY"] = it }
|
||||||
row["ROUT_PATH"]?.let { fields["ROUT"] = routeView(it) }
|
if ("ABTM" !in detailKeys) airbridgeView(row)?.let { fields["ABTM"] = it }
|
||||||
row["ERUT_PATH"]?.let { fields["ERUT"] = routeView(it) }
|
if ("CHOT" !in detailKeys) chocksView(row)?.let { fields["CHOT"] = it }
|
||||||
OCCURRENCE_COLLECTIONS.forEach { c -> slotView(c, row)?.let { fields[c.key] = it } }
|
if ("ROUT" !in detailKeys) row["ROUT_PATH"]?.let { fields["ROUT"] = routeView(it) }
|
||||||
|
if ("ERUT" !in detailKeys) row["ERUT_PATH"]?.let { fields["ERUT"] = routeView(it) }
|
||||||
|
OCCURRENCE_COLLECTIONS.forEach { c ->
|
||||||
|
if (c.key !in detailKeys) slotView(c, row)?.let { fields[c.key] = it }
|
||||||
|
}
|
||||||
|
detailCollections.forEach { (key, value) -> fields[key] = value }
|
||||||
return flid to fields
|
return flid to fields
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -740,6 +764,20 @@ class JdbcFlightSchdRepository(
|
|||||||
::mapFlightRow,
|
::mapFlightRow,
|
||||||
)?.second
|
)?.second
|
||||||
|
|
||||||
|
override fun findNextStateByFlid(flid: String): com.gzzn.omms.msgexchange.domain.flight.FlightNextState? =
|
||||||
|
ds.queryOne(
|
||||||
|
"SELECT flid, state_version, last_message_id, ${ALL_COLUMNS.joinToString(", ")} FROM flight_schd WHERE flid = ?",
|
||||||
|
{ ps -> ps.setString(1, flid) },
|
||||||
|
) { rs ->
|
||||||
|
val (id, fields) = mapFlightRow(rs)
|
||||||
|
com.gzzn.omms.msgexchange.domain.flight.FlightStateEngine.fromFlightFields(
|
||||||
|
id,
|
||||||
|
fields,
|
||||||
|
rs.getLong("state_version"),
|
||||||
|
rs.getString("last_message_id") ?: "",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override fun findByFlids(flids: Collection<String>): Map<String, FlightFields> {
|
override fun findByFlids(flids: Collection<String>): Map<String, FlightFields> {
|
||||||
if (flids.isEmpty()) return emptyMap()
|
if (flids.isEmpty()) return emptyMap()
|
||||||
val result = linkedMapOf<String, FlightFields>()
|
val result = linkedMapOf<String, FlightFields>()
|
||||||
@@ -769,6 +807,7 @@ class JdbcFlightSchdRepository(
|
|||||||
|
|
||||||
override fun deleteByFlids(flids: Set<String>): Int {
|
override fun deleteByFlids(flids: Set<String>): Int {
|
||||||
if (flids.isEmpty()) return 0
|
if (flids.isEmpty()) return 0
|
||||||
|
FlightDetailTables.deleteForFlids(ds, flids)
|
||||||
var totalDeleted = 0
|
var totalDeleted = 0
|
||||||
for (chunk in flids.chunked(200)) {
|
for (chunk in flids.chunked(200)) {
|
||||||
val placeholders = chunk.joinToString(",") { "?" }
|
val placeholders = chunk.joinToString(",") { "?" }
|
||||||
@@ -782,9 +821,11 @@ class JdbcFlightSchdRepository(
|
|||||||
|
|
||||||
override fun getGen(day: String): FlightSchdRepository.GenMeta? {
|
override fun getGen(day: String): FlightSchdRepository.GenMeta? {
|
||||||
val meta = ds.queryOne(
|
val meta = ds.queryOne(
|
||||||
"SELECT version, updated_at FROM schd_gen WHERE fday = ?",
|
"SELECT version, last_message_id, updated_at FROM schd_gen WHERE fday = ?",
|
||||||
{ ps -> ps.setDate(1, toSqlDate(day)) },
|
{ ps -> ps.setDate(1, toSqlDate(day)) },
|
||||||
) { rs -> rs.getLong("version") to (rs.getInstant("updated_at") ?: Instant.now()) }
|
) { rs ->
|
||||||
|
Triple(rs.getLong("version"), rs.getString("last_message_id"), rs.getInstant("updated_at") ?: Instant.now())
|
||||||
|
}
|
||||||
?: return null
|
?: return null
|
||||||
val flids = ds.query(
|
val flids = ds.query(
|
||||||
"SELECT flid FROM schd_gen_flid WHERE fday = ? ORDER BY flid ASC",
|
"SELECT flid FROM schd_gen_flid WHERE fday = ? ORDER BY flid ASC",
|
||||||
@@ -794,7 +835,8 @@ class JdbcFlightSchdRepository(
|
|||||||
fday = day,
|
fday = day,
|
||||||
version = meta.first,
|
version = meta.first,
|
||||||
flids = flids,
|
flids = flids,
|
||||||
updatedAt = meta.second,
|
lastMessageId = meta.second,
|
||||||
|
updatedAt = meta.third,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -832,14 +874,15 @@ class JdbcFlightSchdRepository(
|
|||||||
if (expected == 0L) {
|
if (expected == 0L) {
|
||||||
val inserted = ds.update(
|
val inserted = ds.update(
|
||||||
"""
|
"""
|
||||||
INSERT INTO schd_gen (fday, version, updated_at)
|
INSERT INTO schd_gen (fday, version, last_message_id, updated_at)
|
||||||
VALUES (?, ?, ?)
|
VALUES (?, ?, ?, ?)
|
||||||
ON CONFLICT (fday) DO NOTHING
|
ON CONFLICT (fday) DO NOTHING
|
||||||
""".trimIndent(),
|
""".trimIndent(),
|
||||||
) { ps ->
|
) { ps ->
|
||||||
ps.setDate(1, sqlDate)
|
ps.setDate(1, sqlDate)
|
||||||
ps.setLong(2, newGen.version)
|
ps.setLong(2, newGen.version)
|
||||||
ps.setTimestamp(3, sqlTimestamp)
|
ps.setString(3, newGen.lastMessageId)
|
||||||
|
ps.setTimestamp(4, sqlTimestamp)
|
||||||
}
|
}
|
||||||
if (inserted == 1) {
|
if (inserted == 1) {
|
||||||
replaceGenFlids(day, newGen.flids)
|
replaceGenFlids(day, newGen.flids)
|
||||||
@@ -850,14 +893,15 @@ class JdbcFlightSchdRepository(
|
|||||||
val updated = ds.update(
|
val updated = ds.update(
|
||||||
"""
|
"""
|
||||||
UPDATE schd_gen
|
UPDATE schd_gen
|
||||||
SET version = ?, updated_at = ?
|
SET version = ?, last_message_id = ?, updated_at = ?
|
||||||
WHERE fday = ? AND version = ?
|
WHERE fday = ? AND version = ?
|
||||||
""".trimIndent(),
|
""".trimIndent(),
|
||||||
) { ps ->
|
) { ps ->
|
||||||
ps.setLong(1, newGen.version)
|
ps.setLong(1, newGen.version)
|
||||||
ps.setTimestamp(2, sqlTimestamp)
|
ps.setString(2, newGen.lastMessageId)
|
||||||
ps.setDate(3, sqlDate)
|
ps.setTimestamp(3, sqlTimestamp)
|
||||||
ps.setLong(4, expected)
|
ps.setDate(4, sqlDate)
|
||||||
|
ps.setLong(5, expected)
|
||||||
}
|
}
|
||||||
if (updated == 1) {
|
if (updated == 1) {
|
||||||
replaceGenFlids(day, newGen.flids)
|
replaceGenFlids(day, newGen.flids)
|
||||||
@@ -866,6 +910,98 @@ class JdbcFlightSchdRepository(
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** v2 主表行存在性保障(增量路径新插 FDAY=NULL)。 */
|
||||||
|
private fun ensureFlightRow(flid: String, now: Instant) {
|
||||||
|
val ts = now.toSqlTimestamp()
|
||||||
|
ds.update(
|
||||||
|
"INSERT INTO flight_schd (flid, created_at, updated_at) VALUES (?, ?, ?) " +
|
||||||
|
"ON CONFLICT (flid) DO UPDATE SET updated_at = EXCLUDED.updated_at",
|
||||||
|
) { ps ->
|
||||||
|
ps.setString(1, flid)
|
||||||
|
ps.setTimestamp(2, ts)
|
||||||
|
ps.setTimestamp(3, ts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** v2 快照:标量列全量替换(集合不写槽位/里程碑列)。 */
|
||||||
|
private fun upsertSnapshotScalars(day: String, state: com.gzzn.omms.msgexchange.domain.flight.FlightNextState, now: Instant) {
|
||||||
|
val stored = flattenScalarsOnly(state.scalars)
|
||||||
|
val sqlDate = toSqlDate(day)
|
||||||
|
val sqlTimestamp = now.toSqlTimestamp()
|
||||||
|
val sql = buildString {
|
||||||
|
append("INSERT INTO flight_schd (flid, fday, ")
|
||||||
|
append(SCALAR_WRITE_COLUMNS.joinToString(", "))
|
||||||
|
append(", last_message_id, state_version, created_at, updated_at) VALUES (?, ?, ")
|
||||||
|
append(SCALAR_WRITE_COLUMNS.joinToString(", ") { "?" })
|
||||||
|
append(", ?, ?, ?, ?) ON CONFLICT (flid) DO UPDATE SET fday = EXCLUDED.fday, ")
|
||||||
|
append(SCALAR_WRITE_COLUMNS.joinToString(", ") { "$it = EXCLUDED.$it" })
|
||||||
|
append(", last_message_id = EXCLUDED.last_message_id, state_version = EXCLUDED.state_version, updated_at = EXCLUDED.updated_at")
|
||||||
|
}
|
||||||
|
ds.update(sql) { ps ->
|
||||||
|
var i = 1
|
||||||
|
ps.setString(i++, state.flid)
|
||||||
|
ps.setDate(i++, sqlDate)
|
||||||
|
for (column in SCALAR_WRITE_COLUMNS) {
|
||||||
|
ps.setString(i++, stored[column])
|
||||||
|
}
|
||||||
|
ps.setString(i++, state.lastMessageId)
|
||||||
|
ps.setLong(i++, state.stateVersion)
|
||||||
|
ps.setTimestamp(i++, sqlTimestamp)
|
||||||
|
ps.setTimestamp(i++, sqlTimestamp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** v2 增量:仅更新出现的标量列 + 追踪字段。 */
|
||||||
|
private fun upsertIncrementalScalars(state: com.gzzn.omms.msgexchange.domain.flight.FlightNextState, now: Instant) {
|
||||||
|
val stored = flattenScalarsOnly(state.scalars)
|
||||||
|
if (stored.isEmpty()) {
|
||||||
|
updateTrackingFields(state, now)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val assignments = stored.keys.map { "$it = ?" }.toMutableList()
|
||||||
|
assignments += listOf("last_message_id = ?", "state_version = ?", "updated_at = ?")
|
||||||
|
val sql = "UPDATE flight_schd SET ${assignments.joinToString(", ")} WHERE flid = ?"
|
||||||
|
val sqlTimestamp = now.toSqlTimestamp()
|
||||||
|
ds.update(sql) { ps ->
|
||||||
|
var i = 1
|
||||||
|
stored.values.forEach { ps.setString(i++, it) }
|
||||||
|
ps.setString(i++, state.lastMessageId)
|
||||||
|
ps.setLong(i++, state.stateVersion)
|
||||||
|
ps.setTimestamp(i++, sqlTimestamp)
|
||||||
|
ps.setString(i, state.flid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateTrackingFields(state: com.gzzn.omms.msgexchange.domain.flight.FlightNextState, now: Instant) {
|
||||||
|
ds.update(
|
||||||
|
"UPDATE flight_schd SET last_message_id = ?, state_version = ?, updated_at = ? WHERE flid = ?",
|
||||||
|
) { ps ->
|
||||||
|
ps.setString(1, state.lastMessageId)
|
||||||
|
ps.setLong(2, state.stateVersion)
|
||||||
|
ps.setTimestamp(3, now.toSqlTimestamp())
|
||||||
|
ps.setString(4, state.flid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun persistNextStates(
|
||||||
|
day: String?,
|
||||||
|
states: List<com.gzzn.omms.msgexchange.domain.flight.FlightNextState>,
|
||||||
|
snapshotReplace: Boolean,
|
||||||
|
now: Instant,
|
||||||
|
) {
|
||||||
|
if (states.isEmpty()) return
|
||||||
|
for (state in states) {
|
||||||
|
if (snapshotReplace && day != null) {
|
||||||
|
upsertSnapshotScalars(day, state, now)
|
||||||
|
} else {
|
||||||
|
ensureFlightRow(state.flid, now)
|
||||||
|
upsertIncrementalScalars(state, now)
|
||||||
|
}
|
||||||
|
FlightDetailTables.replaceAll(ds, state, now)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun deleteGenBefore(cutoffDay: String): Int =
|
override fun deleteGenBefore(cutoffDay: String): Int =
|
||||||
ds.update(
|
ds.update(
|
||||||
"DELETE FROM schd_gen WHERE fday < ?",
|
"DELETE FROM schd_gen WHERE fday < ?",
|
||||||
|
|||||||
@@ -203,15 +203,19 @@ class StubFlightSchd : FlightSchdRepository {
|
|||||||
val flid: String,
|
val flid: String,
|
||||||
val fday: String?,
|
val fday: String?,
|
||||||
val fields: FlightFields,
|
val fields: FlightFields,
|
||||||
|
val stateVersion: Long = 0L,
|
||||||
|
val lastMessageId: String = "",
|
||||||
val createdAt: Instant,
|
val createdAt: Instant,
|
||||||
val updatedAt: Instant,
|
val updatedAt: Instant,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val records = linkedMapOf<String, Record>()
|
private val records = linkedMapOf<String, Record>()
|
||||||
|
private val detailCollections = mutableMapOf<String, Map<String, List<Map<String, String>>>>()
|
||||||
private val gens = mutableMapOf<String, FlightSchdRepository.GenMeta>()
|
private val gens = mutableMapOf<String, FlightSchdRepository.GenMeta>()
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
records.clear()
|
records.clear()
|
||||||
|
detailCollections.clear()
|
||||||
gens.clear()
|
gens.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,28 +249,56 @@ class StubFlightSchd : FlightSchdRepository {
|
|||||||
if (delFlids.isEmpty()) return 0
|
if (delFlids.isEmpty()) return 0
|
||||||
val delSet = delFlids.toSet()
|
val delSet = delFlids.toSet()
|
||||||
val toRemove = records.filter { (flid, rec) -> rec.fday == day && flid in delSet }.keys
|
val toRemove = records.filter { (flid, rec) -> rec.fday == day && flid in delSet }.keys
|
||||||
toRemove.forEach { records.remove(it) }
|
toRemove.forEach { flid ->
|
||||||
|
records.remove(flid)
|
||||||
|
detailCollections.remove(flid)
|
||||||
|
}
|
||||||
return toRemove.size
|
return toRemove.size
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun findByFlid(flid: String): FlightFields? = records[flid]?.fields
|
override fun findByFlid(flid: String): FlightFields? {
|
||||||
|
val rec = records[flid] ?: return null
|
||||||
|
return mergeFields(rec)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mergeFields(rec: Record): FlightFields {
|
||||||
|
val out = linkedMapOf<String, String>()
|
||||||
|
out.putAll(rec.fields)
|
||||||
|
val mapper = com.fasterxml.jackson.databind.ObjectMapper()
|
||||||
|
detailCollections[rec.flid]?.forEach { (key, items) ->
|
||||||
|
out[key] = mapper.writeValueAsString(items)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun findNextStateByFlid(flid: String): com.gzzn.omms.msgexchange.domain.flight.FlightNextState? {
|
||||||
|
val rec = records[flid] ?: return null
|
||||||
|
return com.gzzn.omms.msgexchange.domain.flight.FlightNextState(
|
||||||
|
flid = rec.flid,
|
||||||
|
scalars = rec.fields.filterKeys { it != "FLID" },
|
||||||
|
collections = detailCollections[flid] ?: emptyMap(),
|
||||||
|
stateVersion = rec.stateVersion,
|
||||||
|
lastMessageId = rec.lastMessageId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override fun findByFlids(flids: Collection<String>): Map<String, FlightFields> =
|
override fun findByFlids(flids: Collection<String>): Map<String, FlightFields> =
|
||||||
flids.mapNotNull { flid -> records[flid]?.let { flid to it.fields } }.toMap()
|
flids.mapNotNull { flid -> records[flid]?.let { flid to mergeFields(it) } }.toMap()
|
||||||
|
|
||||||
override fun findByDay(day: String): List<Pair<String, FlightFields>> =
|
override fun findByDay(day: String): List<Pair<String, FlightFields>> =
|
||||||
records.values.filter { it.fday == day }
|
records.values.filter { it.fday == day }
|
||||||
.sortedBy { it.flid }
|
.sortedBy { it.flid }
|
||||||
.map { it.flid to it.fields }
|
.map { it.flid to mergeFields(it) }
|
||||||
|
|
||||||
override fun findAll(): Map<String, FlightFields> =
|
override fun findAll(): Map<String, FlightFields> =
|
||||||
records.mapValues { it.value.fields }
|
records.mapValues { mergeFields(it.value) }
|
||||||
|
|
||||||
override fun deleteByFlids(flids: Set<String>): Int {
|
override fun deleteByFlids(flids: Set<String>): Int {
|
||||||
if (flids.isEmpty()) return 0
|
if (flids.isEmpty()) return 0
|
||||||
var count = 0
|
var count = 0
|
||||||
for (flid in flids) {
|
for (flid in flids) {
|
||||||
if (records.remove(flid) != null) count++
|
if (records.remove(flid) != null) count++
|
||||||
|
detailCollections.remove(flid)
|
||||||
}
|
}
|
||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
@@ -280,7 +312,48 @@ class StubFlightSchd : FlightSchdRepository {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun deleteGenBefore(cutoffDay: String): Int {
|
override fun persistNextStates(
|
||||||
|
day: String?,
|
||||||
|
states: List<com.gzzn.omms.msgexchange.domain.flight.FlightNextState>,
|
||||||
|
snapshotReplace: Boolean,
|
||||||
|
now: Instant,
|
||||||
|
) {
|
||||||
|
for (state in states) {
|
||||||
|
val scalarFields = linkedMapOf("FLID" to state.flid)
|
||||||
|
state.scalars.forEach { (k, v) -> scalarFields[k] = v }
|
||||||
|
val existing = records[state.flid]
|
||||||
|
if (snapshotReplace && day != null) {
|
||||||
|
records[state.flid] = Record(
|
||||||
|
flid = state.flid,
|
||||||
|
fday = day,
|
||||||
|
fields = scalarFields,
|
||||||
|
stateVersion = state.stateVersion,
|
||||||
|
lastMessageId = state.lastMessageId,
|
||||||
|
createdAt = existing?.createdAt ?: now,
|
||||||
|
updatedAt = now,
|
||||||
|
)
|
||||||
|
detailCollections[state.flid] = state.collections
|
||||||
|
} else {
|
||||||
|
val mergedScalars = (existing?.fields?.filterKeys {
|
||||||
|
it == "FLID" || it !in com.gzzn.omms.msgexchange.infra.persistence.jdbc.FlightDetailTables.DETAIL_COLLECTION_KEYS
|
||||||
|
} ?: emptyMap()) + scalarFields
|
||||||
|
records[state.flid] = Record(
|
||||||
|
flid = state.flid,
|
||||||
|
fday = existing?.fday,
|
||||||
|
fields = mergedScalars,
|
||||||
|
stateVersion = state.stateVersion,
|
||||||
|
lastMessageId = state.lastMessageId,
|
||||||
|
createdAt = existing?.createdAt ?: now,
|
||||||
|
updatedAt = now,
|
||||||
|
)
|
||||||
|
val mergedCollections = (detailCollections[state.flid] ?: emptyMap()).toMutableMap()
|
||||||
|
state.collections.forEach { (key, items) -> mergedCollections[key] = items }
|
||||||
|
detailCollections[state.flid] = mergedCollections
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deleteGenBefore(cutoffDay: String): Int {
|
||||||
val toRemove = gens.keys.filter { it < cutoffDay }
|
val toRemove = gens.keys.filter { it < cutoffDay }
|
||||||
toRemove.forEach { gens.remove(it) }
|
toRemove.forEach { gens.remove(it) }
|
||||||
return toRemove.size
|
return toRemove.size
|
||||||
|
|||||||
Reference in New Issue
Block a user