fix(persistence): 差删先按 FDAY 圈定删除成员,修复保留航班明细误删 (ACM2-30)

This commit is contained in:
windyboy
2026-09-08 18:50:02 +08:00
parent 7c2d22e8f1
commit 1fc2b2c895
9 changed files with 56 additions and 65 deletions
@@ -39,7 +39,7 @@ object FlightStateEngine {
// 注意:DELY 无协议序号属性(DLNO 非法)→ 不支持 Apply;清除走空数组 Replace(空集)
/** DNLD/FLOP 字段集 → 命令(出现即 Set/Replace;未出现即 Unchanged;序号 0 条目 = 显式清除标记)。 */
fun commandsFromFields(flid: String, fields: FlightFields, snapshotReplace: Boolean): FlightFieldCommands {
fun commandsFromFields(flid: String, fields: FlightFields): FlightFieldCommands {
val scalars = linkedMapOf<String, ScalarCommand>()
val collections = linkedMapOf<String, CollectionCommand>()
fields.forEach { (key, value) ->
@@ -1,20 +1,14 @@
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,
@@ -165,17 +159,4 @@ internal object FlightDetailTables {
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
}
}
@@ -396,26 +396,10 @@ class JdbcFlightSchdRepository(
companion object {
/** SCHD.FLTR 标量列 + legacy 派生列(与 V1.1.0 迁移一致,全部可空 VARCHAR)。 */
private val SCALAR_COLUMNS: List<String> = listOf(
"ALCD", "ALSC", "FLNO", "MVIN", "SODT", "FLTY", "FLIN", "ACFT", "RENO",
"TAOP", "TAFL", "TAID", "TRML", "MAXP", "CSOP", "CSFT", "MAID",
"ESTT", "ACTT", "STND", "PHAG", "CNCL", "REMC", "BOTM", "LACL", "FINT",
"APPT", "EGSR", "EGST", "FHAG", "MHAG", "VIPP", "VIPR", "LBNO", "LBWT",
"PAXC", "EXSC", "EXSR", "FTSS", "PEDT", "NEAT", "PADT", "NAAT",
"ABDG", "LPSDT", "ABN",
)
/** 异常明细前缀标量列(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",
)
/** 无界集合紧凑 JSON 数组字串列(键名去 _TEXT 后缀即 legacy 视图键,直存直取)。 */
private val TEXT_COLUMNS: List<String> = listOf("SRVT_TEXT", "VIPF_TEXT", "MAFL_TEXT")
private val SCALAR_COLUMNS: List<String> = FlightSchdReadAssembler.SCALAR_COLUMNS
/** v2 主表写入列:标量 + 异常 + 无界文本;集合由明细表承载(P4 起槽位/里程碑/航路列已退场)。 */
private val SCALAR_WRITE_COLUMNS: List<String> = SCALAR_COLUMNS + EXCEPTION_COLUMNS + TEXT_COLUMNS
private val SCALAR_WRITE_COLUMNS: List<String> = FlightSchdReadAssembler.ALL_COLUMNS
/** 库列名全集(读侧 SELECT 稳定顺序;P4 退场后 = 无损承载列,与 V1.4.0 后表结构一致)。 */
private val ALL_COLUMNS: List<String> = FlightSchdReadAssembler.ALL_COLUMNS
@@ -441,7 +425,7 @@ class JdbcFlightSchdRepository(
return out
}
/** FDIV/FRET/FLAB1:0..1 单值异常对象 → 前缀标量列(自由文本取首个命中的文本键)。 */
/** 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
@@ -462,11 +446,19 @@ class JdbcFlightSchdRepository(
override fun deleteDiffByDay(day: String, delFlids: Collection<String>): Int {
if (delFlids.isEmpty()) return 0
FlightDetailTables.deleteForFlids(ds, delFlids)
var totalDeleted = 0
val sqlDate = toSqlDate(day)
for (chunk in delFlids.chunked(200)) {
val placeholders = chunk.joinToString(",") { "?" }
// 与主表使用相同日期域;跨日迁移和 FDAY=NULL 的航班必须连同明细保留。
val ownedFlids = ds.query(
"SELECT flid FROM flight_schd WHERE fday = ? AND flid IN ($placeholders)",
{ ps ->
ps.setDate(1, sqlDate)
chunk.forEachIndexed { i, flid -> ps.setString(i + 2, flid) }
},
) { rs -> rs.getString("flid") }
FlightDetailTables.deleteForFlids(ds, ownedFlids)
val sql = "DELETE FROM flight_schd WHERE fday = ? AND flid IN ($placeholders)"
totalDeleted += ds.update(sql) { ps ->
ps.setDate(1, sqlDate)
@@ -874,4 +866,3 @@ class JdbcReqTrackRepository(
}
}
}
@@ -232,7 +232,7 @@ class MessageProcessor(
if (decision.flightChanges.isNotEmpty()) {
val nextStates = decision.flightChanges.map { change ->
val current = flightSchd.findNextStateByFlid(change.flid)
val commands = FlightStateEngine.commandsFromFields(change.flid, change.fields, snapshotReplace = false)
val commands = FlightStateEngine.commandsFromFields(change.flid, change.fields)
FlightStateEngine.apply(current, commands, messageId, bumpVersion = true)
}
flightSchd.persistNextStates(null, nextStates, snapshotReplace = false)
@@ -78,7 +78,7 @@ class SnapshotFlow(
// 锁内读取当前航班态,计算 nextState(单写者互斥下读到的一定是已提交最新态)
val nextStates = normalized.map { (flid, fields) ->
val current = flightSchd.findNextStateByFlid(flid)
val commands = FlightStateEngine.commandsFromFields(flid, fields, snapshotReplace = true)
val commands = FlightStateEngine.commandsFromFields(flid, fields)
FlightStateEngine.apply(current, commands, messageId, bumpVersion = true)
}
@@ -32,7 +32,7 @@ class GtdtHandler(
fields["GTDT"] = mapper.writeValueAsString(gtdtItems)
val current = flightView[body.flid]?.let { FlightStateEngine.fromFlightFields(body.flid, it) }
val commands = FlightStateEngine.commandsFromFields(body.flid, fields, snapshotReplace = false)
val commands = FlightStateEngine.commandsFromFields(body.flid, fields)
val preview = FlightStateEngine.apply(current, commands, messageId = "", bumpVersion = false)
val payloadJson = FlightFieldsJson.toJson(preview.toFlightFields(mapper))