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:
@@ -2,20 +2,24 @@ package com.gzzn.omms.msgexchange.config
|
||||
|
||||
import io.micronaut.context.annotation.ConfigurationProperties
|
||||
|
||||
/** 航班归档删除的时间窗口和留痕保留期,对应配置里的 `msgx.history.*`;窗口按机场时区算。 */
|
||||
/** 历史清理候选的时间窗口(`US-14` AC2 五类已结束条件)和留痕保留期,对应 `msgx.history.*`;窗口按机场时区算。 */
|
||||
@ConfigurationProperties("msgx.history")
|
||||
class HistoryProps {
|
||||
/** 取消时间字段(CNCL)非空,且已经过去这么多小时。 */
|
||||
var cancelledHours: Long = 48
|
||||
/** `US-14` AC2 条件 1:计划时间(SODT)早于当前超过这么多天。 */
|
||||
var plannedAgeDays: Long = 3
|
||||
|
||||
/** 到达/离开终态字段(NAAT/NEAT)非空且已过去这么多小时;这两个字段的确切含义还要跟业务确认。 */
|
||||
var terminalHours: Long = 48
|
||||
/** `US-14` AC2 条件 2:取消时间(CNCL)早于当前超过这么多小时。 */
|
||||
var cancelledHours: Long = 1
|
||||
|
||||
/** 航班已标记删除(STATE = DELETED)并且过了这么多小时。 */
|
||||
var deletedHours: Long = 48
|
||||
/**
|
||||
* `US-14` AC2 条件 3 的窗口:备降(应降本场、实际降其他机场)且计划时间早于当前超过这么多小时。
|
||||
* 备降依据是 `FDIV` 段(`DDES`/`DDIR`),该段尚未解码落库(`Q3`、ACM2-100);
|
||||
* 落地前本条件不参与候选判定,参数先行登记。
|
||||
*/
|
||||
var divertedHours: Long = 1
|
||||
|
||||
/** 兜底期限:航班既没取消也没到终态时,最后一次有效更新超过这么多小时就算静默,可以清理。 */
|
||||
var idleHours: Long = 24 * 7
|
||||
/** `US-14` AC2 条件 5:实际到港时间(NAAT)早于当前超过这么多小时。 */
|
||||
var arrivedHours: Long = 1
|
||||
|
||||
/** 日计划留痕表 SCHD_SNAP_LOG 保留多少天,按(快照覆盖截止日, 接收时间)删旧行。 */
|
||||
var snapLogRetentionDays: Long = 90
|
||||
|
||||
@@ -64,13 +64,49 @@ data class FlightSnapshot(
|
||||
val collections: Map<String, List<Map<String, String>>>,
|
||||
)
|
||||
|
||||
/** 判定一个航班可以归档删除的四条时间窗口,单位是小时,按机场所在时区算;具体取值走业务配置。 */
|
||||
/**
|
||||
* 历史清理候选判据(`US-14` AC2 的五类已结束条件;窗口按机场时区算,取值走业务配置):
|
||||
*
|
||||
* 1. 计划时间(SODT)早于当前超过 [plannedAgeDays] 天;
|
||||
* 2. 取消时间(CNCL)早于当前超过 [cancelledHours] 小时;
|
||||
* 3. 备降(应降本场、实际降其他机场)且计划时间早于当前超过 [divertedHours] 小时
|
||||
* ——**当前不可判**:备降依据是 `FDIV` 段的 `DDES`/`DDIR`,该段不在现行 FLOP 白名单、
|
||||
* 也无 DTO 与存储列(`Q3`、ACM2-100),落地前本条件恒不命中(漏删优于误删);
|
||||
* 4. 离港:计划日期早于当日、有实际离港时间(NEAT)且不晚于当前;
|
||||
* 5. 到港:计划日期早于当日、有实际到港时间(NAAT)且早于当前超过 [arrivedHours] 小时。
|
||||
*/
|
||||
data class HistoryRules(
|
||||
val cancelledHours: Long = 48, // 取消时间字段(CNCL)非空且已过 N 小时
|
||||
val terminalHours: Long = 48, // 到达/离开终态字段(NAAT/NEAT)非空且已过 N 小时
|
||||
val deletedHours: Long = 48, // 已标记删除(STATE = DELETED)且已过 N 小时
|
||||
val idleHours: Long = 24 * 7, // 既没有终态也没有取消:最后更新距今超过这个兜底期限
|
||||
)
|
||||
val plannedAgeDays: Long = 3,
|
||||
val cancelledHours: Long = 1,
|
||||
val divertedHours: Long = 1,
|
||||
val arrivedHours: Long = 1,
|
||||
) {
|
||||
/**
|
||||
* 五类条件的共享求值:JDBC 候选查询与内存 stub 都走这里,避免两份实现漂移。
|
||||
* 时间字段解析不出来(空、格式错)一律视为条件不满足——宁漏删不误删。
|
||||
*/
|
||||
fun isEnded(
|
||||
plannedAt: java.time.Instant?,
|
||||
cancelledAt: java.time.Instant?,
|
||||
arrivalAt: java.time.Instant?,
|
||||
departureAt: java.time.Instant?,
|
||||
zone: java.time.ZoneId,
|
||||
now: java.time.Instant,
|
||||
): Boolean {
|
||||
val plannedDate = plannedAt?.atZone(zone)?.toLocalDate()
|
||||
// 「当日」由调用方传入的 now 推导(运营日时区),不用系统时钟——测试与回放需要确定性行为
|
||||
val beforeToday = plannedDate != null && plannedDate.isBefore(now.atZone(zone).toLocalDate())
|
||||
return when {
|
||||
plannedAt != null && plannedAt.isBefore(now.minusSeconds(plannedAgeDays * 86400)) -> true // 条件 1
|
||||
cancelledAt != null && cancelledAt.isBefore(now.minusSeconds(cancelledHours * 3600)) -> true // 条件 2
|
||||
// 条件 3(备降)待 FDIV 落地,见类注释
|
||||
beforeToday && departureAt != null && !departureAt.isAfter(now) -> true // 条件 4
|
||||
beforeToday && arrivalAt != null &&
|
||||
arrivalAt.isBefore(now.minusSeconds(arrivedHours * 3600)) -> true // 条件 5
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 被生命周期判定选中、准备归档并物理删除的航班。顺序固定是"先归档、再删除",
|
||||
|
||||
+14
-17
@@ -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.isEnded(US-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 {
|
||||
|
||||
@@ -58,7 +58,7 @@ class HistorySweepJob(
|
||||
return SweepOutcome(selected = 0, archived = 0, purged = 0, snapLogPurged = snapLogPurged)
|
||||
}
|
||||
|
||||
val rules = HistoryRules(props.cancelledHours, props.terminalHours, props.deletedHours, props.idleHours)
|
||||
val rules = HistoryRules(props.plannedAgeDays, props.cancelledHours, props.divertedHours, props.arrivedHours)
|
||||
val zone = operationDayProps.zoneId() // 保留期窗口按 PARAM:msgx.operation-day.zone 算,不用 UTC
|
||||
val candidates = flightState.findHistoryCandidates(rules, zone, now)
|
||||
if (candidates.isEmpty()) return SweepOutcome(0, 0, 0, snapLogPurged = snapLogPurged)
|
||||
|
||||
Reference in New Issue
Block a user