refactor(flight-state): SCHD 日计划收敛为缺失保留合并语义并对齐现行设计文档

按现行 docs/flight-state.md(111 行版,§1-§7)全面对齐 domain 包及其消费方,
删除整套指向已退役长版文档(§5.x-§10)的引用与死代码:

- 语义:FlightStateEngine.snapshotState 由"完整快照整体替换"改为 §3.1 合并语义
  ——出现 Set/Replace、缺失保留、标量空串显式清空;DELETED 不被日计划恢复(§3.3)。
- 校验:validateMessage 移除从未接线的报文覆盖范围(scope)参数,只保留 §4 步骤 2
  的声明数量/航班标识/运营日推导校验;ScheduleBody 删除 scopeStart/scopeEnd。
- 删除死代码:domain/Decision.kt、FlightModel 的 SnapshotPatch/UpsertOutcome、
  ProcState.isTerminal/archivable、SnapshotFlag.SEQN_REGRESSION、
  ScheduleRecord.seqn(及 wire FlightRecordXml.SEQN);codec 移除未消费 FFID。
- 删除未接线且引用已退役列(fday/last_message_id)的 SqlDialect 方言脚手架,
  oracle11g README 改为按 V1 现列重建的口径。
- 注释/测试:domain、processing、infra 仓储与 jobs/delivery、配置类及对应测试的
  KDoc 章节引用全部对齐现行 flight-state.md/design.md;FlightStateEngineTest
  重写为合并语义(62/62 通过)。

V1__flight_state_baseline.sql 保留原样(内容注释仍带旧章节号,改动会破坏
已应用迁移的 Flyway checksum,待重建基线或 V2 净迁移时收敛)。
This commit is contained in:
windyboy
2026-09-09 22:17:04 +08:00
parent b75f566276
commit 99a0f5f738
35 changed files with 320 additions and 370 deletions
@@ -14,22 +14,22 @@ import java.time.LocalDate
import java.time.ZoneId
// =====================================================================
// 仓储契约(docs/flight-state.md §3.2 表职责)。
// 仓储契约(docs/flight-state.md §2 权威模型/§3 写入语义,design.md §2.1)。
// 决策层写路径约定:所有 FLIGHT_SCHD 及明细写操作必须发生在
// 「持有 PIPELINE_LOCK 的同一事务」内(§1:锁只串行化 DB 事务)。
// 「持有 PIPELINE_LOCK 的同一事务」内(§1/§4:锁只串行化 DB 事务)。
// =====================================================================
/** 自有 PG 单事务原子保障(§1 目标 3:状态、事件、处理终态同事务提交)。 */
/** 自有 PG 单事务原子保障(flight-state.md §1/§4:状态、事件、处理终态同事务提交)。 */
interface PipelineTransactionManager {
fun <T> inTransaction(block: () -> T): T
}
/** 单行锁(§3.2 PIPELINE_LOCK:事务内第一步 SELECT ... FOR UPDATE,串行化状态写事务。 */
/** 单行锁 PIPELINE_LOCK:事务内第一步 SELECT ... FOR UPDATE,串行化状态写事务(§1/§4。 */
interface PipelineLockRepository {
fun lock()
}
/** PROC_STATE:每消息一行;兼作快照重放判定(§5.1 步骤 2:MSG_ID 已有成功终态 → 重放)。 */
/** PROC_STATE:每消息一行design.md §2.1);SUCCEEDED 终态兼作日计划重放判定。 */
interface ProcStateRepository {
fun insert(msgId: Long, state: ProcStatus = ProcStatus.PENDING)
@@ -61,7 +61,7 @@ interface ProcStateRepository {
fun requeueByErrorClasses(errorClasses: List<ErrorClass>): Int
}
/** MSG_EVENT outbox§7.3)。KAFKA_SCHD 合并同 FLID 未发事件按最新 STATE_VERSION 输出。 */
/** MSG_EVENT outboxflight-state.md §5)。KAFKA_SCHD 合并同 FLID 未发事件按最新 STATE_VERSION 输出。 */
interface MsgEventRepository {
fun insertAll(events: List<MsgEvent>): List<Long>
@@ -69,7 +69,7 @@ interface MsgEventRepository {
fun claimBatch(target: String, limit: Int): List<MsgEvent>
/** 同 FLID 未发 KAFKA_SCHD 事件合并:每 FLID 取最新 STATE_VERSION 一条(§7.3)。 */
/** 同 FLID 未发 KAFKA_SCHD 事件合并:每 FLID 取最新 STATE_VERSION 一条(§5)。 */
fun mergePendingSchd(limit: Int): List<MsgEvent>
fun markSent(eventId: Long)
@@ -81,62 +81,63 @@ interface MsgEventRepository {
fun markDead(eventId: Long, errorClass: ErrorClass, lastError: String, attempts: Int? = null)
}
/** 完整态落库结果:DAY_GUARD_VIOLATION = OPERATION_DAY 不可变条件更新未命中(§7.4)。 */
/** 完整态落库结果:DAY_GUARD_VIOLATION = OPERATION_DAY 不可变条件更新未命中(§2.1)。 */
enum class PersistOutcome { INSERTED, UPDATED, DAY_GUARD_VIOLATION }
/**
* 航班当前态权威(§3.2 FLIGHT_SCHD + 9 张明细表)。
* 航班当前态权威(§2 FLIGHT_SCHD + 9 张明细表)。
* 唯一写路径 = persistFullState:写入前在内存生成完整新状态(引擎产物)再落库,
* 明细集合按组先删后插(§3.3)。
* 明细集合按组先删后插(§2.2 完整合并结果为准)。
*/
interface FlightStateRepository {
/** 主行点查(身份/版本判定;批量用于快照归属校验 §5.3)。 */
/** 主行点查(身份/版本判定;批量用于日计划归属校验 §2.1)。 */
fun findMainRow(flid: String): FlightMainRow?
fun findMainRows(flids: Collection<String>): Map<String, FlightMainRow>
/** 完整当前态:主行 + 全部明细(一致性读边界由调用方事务保证,§9)。 */
/** 完整当前态:主行 + 全部明细(一致性读边界由调用方事务保证,§5)。 */
fun loadFullSnapshot(flid: String): FlightSnapshot?
/**
* 完整当前态落库(§3.3):主行 upsert + 全部明细按组先删后插;
* 完整当前态落库(§3):主行 upsert + 全部明细按组先删后插;
* STATE_VERSION 以 snapshot.stateVersion 落库。
* SCHD整体替换/清除)、FLOP/ADFT(合并后全量写)共用此唯一写路径。
* 条件更新带 `WHERE operation_day IS NULL OR operation_day = :day`(§7.4 不可变强化)。
* SCHD§3.1)、FLOP/ADFT§3.2/§3.3合并后全量结果共用此唯一写路径。
* 条件更新带 `WHERE operation_day IS NULL OR operation_day = :day`(§2.1 不可变强化)。
*/
fun persistFullState(snapshot: FlightSnapshot, msgId: Long, now: Instant): PersistOutcome
/**
* FDEL(§6.2):ACTIVE → 置 DELETED、推进版本、明细保留、返回 true(发布删除事件);
* FDEL(§3.3):ACTIVE → 置 DELETED、推进版本、明细保留、返回 true(发布删除事件);
* 已 DELETED 或不存在 → 返回 false(幂等成功,不推进版本不重复发布)。
*/
fun markDeleted(flid: String, msgId: Long, now: Instant): Boolean
/** ADFT 生命周期重激活(§6.3):DELETED → ACTIVE,推进版本;非 DELETED 返回 false。 */
/** ADFT 生命周期重激活(§3.3):DELETED → ACTIVE,推进版本;非 DELETED 返回 false。 */
fun revive(flid: String, msgId: Long, now: Instant): Boolean
/** §8.1:按窗口规则选出历史候选(含 DELETED候选时间按机场时区折算)。 */
/** §6:按保留期与终态/静默判据选出历史候选(含 DELETED窗口按机场时区折算)。 */
fun findHistoryCandidates(rules: HistoryRules, zone: ZoneId, now: Instant): List<HistoryCandidate>
/**
* §8.2 步骤 3:物理删除主行与明细(仅历史存储确认成功后调用;
* §6:物理删除主行与明细(仅历史存储确认成功后调用;
* 历史存储未接通时调用方必须传空集合——删 0 条)。
*/
fun purgeArchived(flids: Collection<String>): Int
/** §8.3 观测:OPERATION_DAY 仍为 NULL 的航班数(只增不删,终止规则未定 §10)。 */
/** §2.1 观测:OPERATION_DAY 仍为 NULL 的航班数(只增不删,终止规则未定 §6)。 */
fun countOperationDayNull(): Int
}
/** §5.5 SCHD_SNAP_LOG:事务外追加留痕,不参与决策;一行 = 一次尝试(重放也记)。 */
/** SCHD_SNAP_LOGdesign.md §6.2:事务外追加留痕,不参与决策;一行 = 一次尝试(重放也记)。 */
interface SnapshotLogRepository {
fun append(entry: SnapshotLogEntry)
}
/**
* 请求状态机(§7.1):只有 RESP 完成 RQFD 请求,按(运营日、发送方、请求类型)
* 匹配最新一条 PENDING同类请求只留一条有效,新请求置旧为 EXPIRED。
* 请求状态机 REQ_TRACKdesign.md §4.2 目标机制,尚无运行时协调器):
* 只有 RESP 完成 RQFD 请求,按(运营日、发送方、请求类型)匹配最新一条 PENDING
* 同类请求只留一条有效,新请求置旧为 EXPIRED。
*/
interface ReqTrackRepository {
enum class ReqState { PENDING, SENT, DONE, EXPIRED }
@@ -156,7 +157,7 @@ interface ReqTrackRepository {
fun findLatest(reqType: String, operationDay: LocalDate, sender: String, states: List<ReqState>): Req?
/** RESP 完成请求(§7.1):匹配最新一条 PENDING/SENT;无匹配返回 false(迟到不报错)。 */
/** RESP 完成请求(design.md §4.2):匹配最新一条 PENDING/SENT;无匹配返回 false(迟到不报错)。 */
fun completeLatest(reqType: String, operationDay: LocalDate, sender: String): Boolean
fun linkCoutmsgs(reqId: Long, coutmsgsId: Long)
@@ -167,7 +168,7 @@ interface ReqTrackRepository {
}
/**
* 共享信箱回填补偿待办(§7.2):业务事务内预登记(消除崩溃窗口 §10 偏差)
* 共享信箱回填补偿待办(design.md §3.3/§6.1):业务事务内预登记
* 提交后由 BackfillSweepJob 重试;回填失败不得把 SUCCEEDED 改回 FAILED。
*/
interface BackfillTodoRepository {
@@ -1,72 +0,0 @@
package com.gzzn.omms.msgexchange.infra.persistence.dialect
/**
* v2 §6ACM2-29 P3-B):数据库方言接缝。
*
* 原则(v2 §1/§6):同一业务模型支持 PostgreSQL 与 Oracle 11g,方言只存在于基础设施适配层,
* 业务层与仓储语义不按数据库类型分支。
*
* 接缝范围(随仓储 SQL 演进扩充):
* - 航班主行快照 upsertPG `INSERT .. ON CONFLICT` vs 11g `MERGE INTO`11g 无 ON CONFLICT
* - 增量路径主行存在性保障:同上
*
* 激活门控(本接缝为编译级交付,**未经目标库验证**):
* Oracle 11.2 补丁级别 × JDK 25 × ojdbc 驱动 × Flywaydb/migration/oracle11g location
* × 连接池组合必须在现场 11g 实测通过后,才允许把生产方言切到 [Oracle11gDialect]
* MERGE 绑定顺序适配、CLOBSRVT/VIPF/MAFL_TEXT)与空串=NULL 语义回归一并纳入激活清单。
* 验收证据要求见 docs/flight-state.md §8。
*/
interface SqlDialect {
/** 航班主行快照 upsert:按 SCALAR_WRITE_COLUMNS 生成整体替换 SQL。 */
fun flightSnapshotUpsertSql(columns: List<String>): String
/** 增量路径主行存在性保障(新插 FDAY=NULL,已有行仅推进 updated_at)。 */
fun flightRowEnsureSql(): String
}
/** PostgreSQL 方言:当前生产路径。 */
object PostgreSqlDialect : SqlDialect {
override fun flightSnapshotUpsertSql(columns: List<String>): String = buildString {
append("INSERT INTO flight_schd (flid, fday, ")
append(columns.joinToString(", "))
append(", last_message_id, state_version, created_at, updated_at) VALUES (?, ?, ")
append(columns.joinToString(", ") { "?" })
append(", ?, ?, ?, ?) ON CONFLICT (flid) DO UPDATE SET fday = EXCLUDED.fday, ")
append(columns.joinToString(", ") { "$it = EXCLUDED.$it" })
append(", last_message_id = EXCLUDED.last_message_id, state_version = EXCLUDED.state_version, updated_at = EXCLUDED.updated_at")
}
override fun flightRowEnsureSql(): String =
"INSERT INTO flight_schd (flid, created_at, updated_at) VALUES (?, ?, ?) " +
"ON CONFLICT (flid) DO UPDATE SET updated_at = EXCLUDED.updated_at"
}
/**
* Oracle 11g 方言:`MERGE INTO` 单语句 upsert11g 无 `ON CONFLICT`)。
*
* **编译级交付,未在目标 11g 上验收**(见 [SqlDialect] 激活门控)。
* 绑定顺序与 PG 不同(USING 子句的 flid 绑定 + MATCHED/NOT MATCHED 两份列绑定),
* 激活时需同步实现 11g 绑定适配,禁止直接复用 PG 绑定序列。
*/
object Oracle11gDialect : SqlDialect {
override fun flightSnapshotUpsertSql(columns: List<String>): String = buildString {
append("MERGE INTO flight_schd t ")
append("USING (SELECT ? AS FLID FROM dual) s ON (t.flid = s.FLID) ")
append("WHEN MATCHED THEN UPDATE SET t.fday = ?, ")
append(columns.joinToString(", ") { "t.$it = ?" })
append(", t.last_message_id = ?, t.state_version = ?, t.updated_at = ? ")
append("WHEN NOT MATCHED THEN INSERT (flid, fday, ")
append(columns.joinToString(", "))
append(", last_message_id, state_version, created_at, updated_at) VALUES (")
append((listOf("?", "?") + columns.map { "?" } + listOf("?", "?", "?")).joinToString(", "))
append(")")
}
override fun flightRowEnsureSql(): String =
"MERGE INTO flight_schd t " +
"USING (SELECT ? AS FLID FROM dual) s ON (t.flid = s.FLID) " +
"WHEN MATCHED THEN UPDATE SET t.updated_at = ? " +
"WHEN NOT MATCHED THEN INSERT (flid, created_at, updated_at) VALUES (?, ?, ?)"
}
@@ -36,9 +36,9 @@ import java.util.Locale
import javax.sql.DataSource
// =====================================================================
// 自有 PostgreSQL 仓储实现(docs/flight-state.md §3.2 表职责schema 见
// 自有 PostgreSQL 仓储实现(docs/flight-state.md §2 权威模型/§3 写入语义schema 见
// db/migration/V1__flight_state_baseline.sql)。全部写路径约定在
// withTransaction + PIPELINE_LOCK 内调用(§1 目标 3/§3.2)。
// withTransaction + PIPELINE_LOCK 内调用(§1/§4)。
// =====================================================================
@Singleton
@@ -56,7 +56,7 @@ class JdbcPipelineTransactionManager(
class JdbcPipelineLockRepository(
private val ds: DataSource,
) : PipelineLockRepository {
/** 事务内第一步:单行 FOR UPDATE 串行化状态写事务(§3.2 PIPELINE_LOCK)。 */
/** 事务内第一步:单行 FOR UPDATE 串行化状态写事务(§1/§4 PIPELINE_LOCK)。 */
override fun lock() {
ds.queryOne("SELECT lock_id FROM pipeline_lock WHERE lock_id = 1 FOR UPDATE", {}) { 1 } ?: error("PIPELINE_LOCK row missing")
}
@@ -211,7 +211,7 @@ class JdbcMsgEventRepository(
::mapEvent,
)
/** §7.3:同 FLID 未发事件按最新 STATE_VERSION 合并;PG DISTINCT ON 方言(注释明示)。 */
/** §5:同 FLID 未发事件按最新 STATE_VERSION 合并;PG DISTINCT ON 方言(注释明示)。 */
override fun mergePendingSchd(limit: Int): List<MsgEvent> =
ds.query(
"""
@@ -348,7 +348,7 @@ class JdbcFlightStateRepository(
},
)
if (updated == 0) {
// §7.4 不可变强化:条件更新未命中 = 归属日冲突
// §2.1 不可变强化:条件更新未命中 = 归属日冲突
return if (existed != null) PersistOutcome.DAY_GUARD_VIOLATION else PersistOutcome.UPDATED
}
replaceDetails(snapshot, now)
@@ -370,7 +370,7 @@ class JdbcFlightStateRepository(
) == 1
override fun findHistoryCandidates(rules: HistoryRules, zone: ZoneId, now: Instant): List<HistoryCandidate> {
// §8.1 四条判定在应用层执行(SIS 时间串解析无法下推 SQL);
// §6 四条判定在应用层执行(SIS 时间串解析无法下推 SQL);
// 不按 updated_at 粗筛——取消/终态时间可能早于最近一次更新,粗筛会漏删。
val rows = ds.query(
"SELECT flid, state, state_version, operation_day, cncl, naat, neat, updated_at FROM flight_schd",
@@ -399,7 +399,7 @@ class JdbcFlightStateRepository(
r.updatedAt < now.minus(Duration.ofHours(rules.idleHours)) -> true
else -> false
}
// NAAT/NEAT 业务含义待术语表确认(§10):解析失败视为不命中,不误删
// NAAT/NEAT 业务含义待术语表确认(§6 开放项):解析失败视为不命中,不误删
if (hit) HistoryCandidate(r.flid, r.state, r.version, wasNeverFdel = r.state == FlightState.ACTIVE) else null
}
}
@@ -456,7 +456,7 @@ class JdbcFlightStateRepository(
items.forEachIndexed { ordinal, item ->
val cols = mutableListOf("flid", "ordinal", "source_seq")
val vals = mutableListOf<Any?>(flid)
vals.add(ordinal + 1) // ORDINAL 保留输入顺序(§3.2
vals.add(ordinal + 1) // ORDINAL 保留输入顺序(§2.2
vals.add(item[spec.seqAttr])
spec.columns.forEach { col ->
cols.add(col)
@@ -488,7 +488,7 @@ class JdbcFlightStateRepository(
private fun loadDetails(flid: String, key: String, spec: DetailSpec): List<Map<String, String>> {
val sql = if (spec.routeKind != null) {
// ROUT/ERUT 共表:按 ROUTE_KIND 过滤(§3.2
// ROUT/ERUT 共表:按 ROUTE_KIND 过滤(§2.2
"SELECT * FROM ${spec.table} WHERE flid = ? AND route_kind = ? ORDER BY ordinal ASC"
} else {
"SELECT * FROM ${spec.table} WHERE flid = ? ORDER BY ordinal ASC"
@@ -544,7 +544,7 @@ class JdbcFlightStateRepository(
"flight_delay", "flight_bridge_op", "flight_chock_op", "flight_route_point",
)
/** 10 类集合 ↔ 明细表/列映射(§3.2;列名与基线一致)。 */
/** 10 类集合 ↔ 明细表/列映射(§2.2;列名与基线一致)。 */
internal val COLLECTIONS: Map<String, DetailSpec> = mapOf(
"GTDT" to DetailSpec("flight_gate", listOf("gate", "pgot", "pgct", "gotm", "gctm", "gtyp"), "GTNO"),
"CKDT" to DetailSpec("flight_checkin", listOf("chkc", "ccls", "pcot", "pcct", "cotm", "cctm", "ctyp"), "CKNO"),
@@ -566,7 +566,7 @@ class JdbcFlightStateRepository(
class JdbcSnapshotLogRepository(
private val ds: DataSource,
) : SnapshotLogRepository {
/** §5.5 只追加;写失败由调用方捕获记指标(append 自身不抛出)。 */
/** design.md §6.2只追加;写失败由调用方捕获记指标(append 自身不抛出)。 */
override fun append(entry: SnapshotLogEntry) {
ds.update(
"""
@@ -628,7 +628,7 @@ class JdbcReqTrackRepository(
)
}
/** §7.1 RESP 完成请求:匹配最新一条 PENDING/SENT;无匹配返回 false(迟到不报错)。 */
/** design.md §4.2 RESP 完成请求:匹配最新一条 PENDING/SENT;无匹配返回 false(迟到不报错)。 */
override fun completeLatest(reqType: String, operationDay: LocalDate, sender: String): Boolean =
ds.update(
"""
@@ -6,7 +6,7 @@ import jakarta.inject.Singleton
/**
* stub 适配层——DeliveryPort 内存实现,仅在 msgx.stubs=true 时生效。
* 记录 (topic, key, payload)payload = null 表示 TOMBSTONE§7.3 键缺失=删除旧值)。
* 记录 (topic, key, payload)payload = null 表示 TOMBSTONEflight-state.md §5 键缺失=删除旧值)。
*/
@Requires(property = "msgx.stubs", value = "true")
@Singleton
@@ -132,7 +132,7 @@ class StubMsgEvents : MsgEventRepository {
override fun claimBatch(target: String, limit: Int): List<MsgEvent> =
rows.values.filter { it.target == target && it.state == EventStatus.PENDING }.sortedBy { it.eventId!! }.take(limit)
/** §7.3:同 FLID 取最新 STATE_VERSION,按事件序输出。 */
/** §5:同 FLID 取最新 STATE_VERSION,按事件序输出。 */
override fun mergePendingSchd(limit: Int): List<MsgEvent> =
rows.values
.filter { it.target == "KAFKA:schd" && it.state == EventStatus.PENDING }
@@ -178,7 +178,7 @@ class StubFlightState : FlightStateRepository {
override fun loadFullSnapshot(flid: String): FlightSnapshot? = snapshots[flid]
/** §7.4 不可变条件:已有非空 OPERATION_DAY 且与新值不同 → DAY_GUARD_VIOLATION。 */
/** §2.1 不可变条件:已有非空 OPERATION_DAY 且与新值不同 → DAY_GUARD_VIOLATION。 */
override fun persistFullState(snapshot: FlightSnapshot, msgId: Long, now: Instant): PersistOutcome {
val existing = mains[snapshot.flid]
if (existing?.operationDay != null && existing.operationDay != snapshot.operationDay) {
@@ -260,7 +260,7 @@ class StubReqTrack : ReqTrackRepository {
fun clear() = rows.clear()
override fun insert(reqType: String, operationDay: LocalDate, sender: String): Long {
// §7.1:同类(类型+运营日+发送方)旧有效请求先置 EXPIRED
// design.md §4.2:同类(类型+运营日+发送方)旧有效请求先置 EXPIRED
rows.values.filter {
it.reqType == reqType && it.operationDay == operationDay && it.sender == sender &&
(it.state == ReqTrackRepository.ReqState.PENDING || it.state == ReqTrackRepository.ReqState.SENT)