删除会被误用的系统时间默认值:MsgEvent.createdAt、ProcState.updatedAt、InboxPoller.pollOnce(now)、HistorySweepJob.run(now)、Identity.of(day)。Identity 的日期边界改由 MessageProcessor 按 PARAM:msgx.operation-day.zone 算出后传入(边界关闭时身份值不变);StubProcState/StubInbox 改注入 Clock(测试可传假时钟),全部调用点显式传时间。 验证:./gradlew test 145 tests / 0 fail / 0 skipped。
91 lines
3.6 KiB
Kotlin
91 lines
3.6 KiB
Kotlin
package com.gzzn.omms.msgexchange.domain
|
||
|
||
import java.time.Instant
|
||
|
||
/**
|
||
* 一条入站消息的处理记录,一行对应共享信箱 CMINMSGS 里的一条报文。
|
||
*
|
||
* 这张表同时承担四件事:
|
||
* 1. 防重复入队——主键是信箱 ID,同一条报文只会有一行;
|
||
* 2. 防业务重复——IDENTITY_KEY 唯一,同一条业务报文只处理一次;
|
||
* 3. 记录处理进度——状态、重试次数、下次重试时间和失败原因;
|
||
* 4. 记录回填进度——处理完要把"已处理"标记写回共享信箱,写成功之前一直留着待办意图。
|
||
*
|
||
* 第 4 条和终态写在同一条 UPDATE 里,所以不会出现"业务处理完了,却没人记得去回填"。
|
||
*/
|
||
enum class ProcStatus {
|
||
/** 已入队,等待处理。 */
|
||
PENDING,
|
||
|
||
/** 处理失败,等退避时间到了再重试。 */
|
||
FAILED,
|
||
|
||
/** 处理成功。 */
|
||
SUCCEEDED,
|
||
|
||
/** 判定为业务重复,跳过不处理。 */
|
||
SKIPPED,
|
||
|
||
/** 处理失败且不再重试,等人工处置。 */
|
||
DEAD,
|
||
}
|
||
|
||
/** 失败原因分类,决定失败后是重试还是直接进死信。 */
|
||
enum class ErrorClass {
|
||
/** 报文本身不合法,重试也没用,直接进死信。 */
|
||
MALFORMED,
|
||
|
||
/** 整包被拒绝(运营日冲突、声明条数不符等),整包不落地,直接进死信。 */
|
||
PROTOCOL,
|
||
|
||
/** 解码逻辑的问题;修好 codec 之后可以重放。 */
|
||
CODEC_ERROR,
|
||
|
||
/** 重试次数用尽(尝试上限);人工复核后可以重放。 */
|
||
EXHAUSTED,
|
||
|
||
/** 数据库、网络等基础设施抖动,重试通常就能过。 */
|
||
INFRA,
|
||
|
||
/** 报文类型还没有对应处理器;属于能力未实现,先退避重试等补齐。 */
|
||
UNSUPPORTED,
|
||
}
|
||
|
||
data class ProcState(
|
||
/** 信箱 CMINMSGS_ID,也是本表主键。 */
|
||
val msgId: Long,
|
||
val state: ProcStatus,
|
||
/** 业务身份 SNDR|TYPE|STYP|SEQN;解码成功后绑定一次,重试不会重绑。 */
|
||
val identityKey: String? = null,
|
||
/** 处理失败次数,用来算退避档位和判断是否已到上限。 */
|
||
val attempts: Int = 0,
|
||
/** FAILED 状态下,下次可以重试的时刻。 */
|
||
val nextAttemptAt: Instant? = null,
|
||
val errorClass: ErrorClass? = null,
|
||
/** 最近一次失败的原因(截断后落库,供排查)。 */
|
||
val lastError: String? = null,
|
||
/** 信箱里的接收时间,来自**库方时钟**、**可能为 NULL**;仅用于对账与展示,不作任何判据。 */
|
||
val receivedAt: Instant? = null,
|
||
/**
|
||
* 本地入队时间(本系统写入,非空)。超期补写期限 `R` **只比较它**:与判据用的本地
|
||
* `NOW` 同源,不受库方时钟偏斜影响(`PRE-4`)。
|
||
*/
|
||
val enqueuedAt: Instant? = null,
|
||
/** 非空表示已确认信箱行带上了处理标记。 */
|
||
val backfillAt: Instant? = null,
|
||
/** 非空表示还欠一次回填:写终态时置为当前时间,失败后退避推后。 */
|
||
val backfillNextAt: Instant? = null,
|
||
val backfillAttempts: Int = 0,
|
||
val backfillError: String? = null,
|
||
/**
|
||
* 非空表示已判定"不必再回填":信箱行不存在,或暂时性故障持续到 `R` 仍未打标。
|
||
*
|
||
* **它不等于标记已确认**:`backfillAt` 仍为空,所以不满足"边界内全部行已打标"的清除条件。
|
||
* 停止重试与"已满足清除前提"是两件事,不能互相替代。
|
||
*/
|
||
val backfillAbandonedAt: Instant? = null,
|
||
/** 放弃原因(`MISSING_ROW` / `TRANSIENT_DEADLINE`),供人工对账与恢复判断。 */
|
||
val backfillAbandonedReason: String? = null,
|
||
val updatedAt: Instant,
|
||
)
|