fix(flight): 历史清理判据对齐 US-14 AC2 五类已结束条件(ACM2-91)

- HistoryRules.isEnded 共享求值器:计划超 3 天/取消超 1h/离港/到港;JDBC 与 stub 同源不漂移
- 删除无依据的 idle/deleted/terminal 三个窗口(reference.md 四行冲突参数重写为 AC2 口径)
- 备降条件依赖 FDIV(DDES/DDIR),落地(Q3、ACM2-100)前显式不命中,参数先行登记
- 修「当日」取系统时钟的缺陷,改由传入 now 推导(测试抓出)
- implementation.md 12.5 与配置、yml 同批对齐
This commit is contained in:
windyboy
2026-09-21 13:39:18 +08:00
parent 1d95926e06
commit f9d51399da
11 changed files with 178 additions and 63 deletions
@@ -718,37 +718,34 @@ class JdbcFlightStateRepository(
) == 1
override fun findHistoryCandidates(rules: HistoryRules, zone: ZoneId, now: Instant): List<HistoryCandidate> {
// 四条清理判据在应用层算:里面的时间字段是 SIS 格式字符串,没法交给 SQL 比较;
// 判据在应用层算:时间字段是 SIS 格式字符串ddMMMyyHHmm,交给 SQL 比较不了
// 不按 updated_at 粗筛——取消/终态时间可能早于最近一次更新,粗筛会漏删。
val rows = ds.query(
"SELECT flid, state, state_version, operation_day, cncl, naat, neat, updated_at FROM flight_schd",
"SELECT flid, state, state_version, sodt, cncl, naat, neat FROM flight_schd",
{},
) { rs ->
object {
val flid = rs.getString("flid")
val state = FlightState.valueOf(rs.getString("state"))
val version = rs.getLong("state_version")
val sodt = rs.getString("sodt")
val cncl = rs.getString("cncl")
val naat = rs.getString("naat")
val neat = rs.getString("neat")
val updatedAt = rs.getInstant("updated_at")
}
}
return rows.mapNotNull { r ->
val cancelledAt = parseSisTime(r.cncl, zone)
val terminalAt = listOfNotNull(parseSisTime(r.naat, zone), parseSisTime(r.neat, zone)).maxOrNull()
val hasTerminalField = listOf(r.cncl, r.naat, r.neat).any { !it.isNullOrBlank() }
val hit = when {
cancelledAt != null && cancelledAt < now.minus(Duration.ofHours(rules.cancelledHours)) -> true
terminalAt != null && terminalAt < now.minus(Duration.ofHours(rules.terminalHours)) -> true
r.state == FlightState.DELETED && r.updatedAt != null &&
r.updatedAt < now.minus(Duration.ofHours(rules.deletedHours)) -> true
!hasTerminalField && r.updatedAt != null &&
r.updatedAt < now.minus(Duration.ofHours(rules.idleHours)) -> true
else -> false
}
// NAAT/NEAT 的业务含义还没确认,所以解析不出来时一律当成"不满足条件",宁可漏删
if (hit) HistoryCandidate(r.flid, r.state, r.version, wasNeverFdel = r.state == FlightState.ACTIVE) else null
// US-14 AC2 五类已结束条件共享求值(HistoryRules.isEnded);备降条件待 FDIV 落地,
// 见 HistoryRules 注释。解析不出来一律不命中:宁漏删不误删。
val ended = rules.isEnded(
plannedAt = parseSisTime(r.sodt, zone),
cancelledAt = parseSisTime(r.cncl, zone),
arrivalAt = parseSisTime(r.naat, zone),
departureAt = parseSisTime(r.neat, zone),
zone = zone,
now = now,
)
if (ended) HistoryCandidate(r.flid, r.state, r.version, wasNeverFdel = r.state == FlightState.ACTIVE) else null
}
}
@@ -385,16 +385,21 @@ class StubFlightState : FlightStateRepository {
override fun findHistoryCandidates(rules: HistoryRules, zone: ZoneId, now: Instant): List<HistoryCandidate> =
mains.values.mapNotNull { main ->
val snap = snapshots[main.flid]
val cancelled = snap?.scalars?.get("CNCL")
val hasTerminalField = listOf("CNCL", "NAAT", "NEAT").any { !snap?.scalars?.get(it).isNullOrBlank() }
val idleHit = !hasTerminalField && main.updatedAt < now.minusSeconds(rules.idleHours * 3600)
val deletedHit = main.state == FlightState.DELETED && main.updatedAt < now.minusSeconds(rules.deletedHours * 3600)
if (idleHit || deletedHit) {
HistoryCandidate(main.flid, main.state, main.stateVersion, wasNeverFdel = main.state == FlightState.ACTIVE)
} else {
null
val snap = snapshots[main.flid] ?: return@mapNotNull null
fun sisTime(key: String): Instant? {
val raw = snap.scalars[key]?.trim()?.takeIf { it.isNotEmpty() } ?: return null
return com.gzzn.omms.msgexchange.domain.OperationDayCalculator.parseSodt(raw)?.atZone(zone)?.toInstant()
}
// 与 JDBC 候选查询同走 HistoryRules.isEndedUS-14 AC2 五类条件)
val ended = rules.isEnded(
plannedAt = sisTime("SODT"),
cancelledAt = sisTime("CNCL"),
arrivalAt = sisTime("NAAT"),
departureAt = sisTime("NEAT"),
zone = zone,
now = now,
)
if (ended) HistoryCandidate(main.flid, main.state, main.stateVersion, wasNeverFdel = main.state == FlightState.ACTIVE) else null
}
override fun purgeArchived(candidates: List<HistoryCandidate>): Int {