feat(outbound): REQ_TRACK/COUTMSGS/schd-sync 与 RESP 守卫(ACM2-92/86)

出站请求状态机与编码;SCHD-RESP 须匹配未过期开放请求;EROR 结案匹配 SENT。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
windyboy
2026-09-21 15:50:14 +08:00
co-authored by Cursor
parent b545fd2b58
commit 54b383fa9d
23 changed files with 843 additions and 26 deletions
@@ -1,5 +1,6 @@
package com.gzzn.omms.msgexchange.processing
import com.gzzn.omms.msgexchange.codec.ErorPayload
import com.gzzn.omms.msgexchange.codec.FlopPayload
import com.gzzn.omms.msgexchange.codec.ScheduleBody
import com.gzzn.omms.msgexchange.codec.XmlCodec
@@ -123,6 +124,7 @@ class MessageProcessor(
private val flopProcessor: FlopProcessor,
private val fdelProcessor: FdelProcessor,
private val adftProcessor: AdftProcessor,
private val outbound: OutboundRequestService,
private val procFailure: ProcFailure,
private val props: PipelineProps,
private val clock: Clock,
@@ -201,8 +203,24 @@ class MessageProcessor(
val body = decoded.body as? ScheduleBody
if (body == null) return deadMalformed(head, "missing-schd-body")
when (kind.subtype) {
MsgKind.SchdSubtype.DNLD, MsgKind.SchdSubtype.RESP ->
MsgKind.SchdSubtype.DNLD ->
scheduleProcessor.applyScheduleRecords(head, decoded)
MsgKind.SchdSubtype.RESP -> {
if (!outbound.hasOpenSentRqfd()) {
log.info("SCHD-RESP without open RQFD -> SKIPPED msgId={} [G-RESP-GUARD]", head.msgId)
procState.markTerminal(
head.msgId, ProcStatus.SKIPPED,
lastError = "resp-guard:no-open-req",
now = clock.instant(),
)
return
}
val respResult = scheduleProcessor.applyScheduleRecords(head, decoded)
if (respResult is ApplyResult.Succeeded || respResult is ApplyResult.ReplaySkipped) {
outbound.completeRqfdResponse()
}
respResult
}
MsgKind.SchdSubtype.ADFT -> {
val record = body.records.singleOrNull()
if (record == null) return deadMalformed(head, "adft-needs-single-fltr")
@@ -227,9 +245,20 @@ class MessageProcessor(
return procFailure.fail(head, ErrorClass.UNSUPPORTED, "refdata-pending:${kind.type}")
}
MsgKind.Eror -> {
// US-09 AC3:须匹配出站请求;REQ_TRACK 协调器落地前(ACM2-92)先按可重试失败留队。
log.warn("eror without handler -> FAILED(UNSUPPORTED) msgId={}", head.msgId)
return procFailure.fail(head, ErrorClass.UNSUPPORTED, "eror-pending")
val payload = decoded.body as? ErorPayload
?: return deadMalformed(head, "missing-eror-body")
val matched = outbound.failFromEror(payload.seqs, payload.typs, payload.stys)
if (!matched) {
log.info("EROR without matching open outbound -> SKIPPED msgId={}", head.msgId)
procState.markTerminal(
head.msgId, ProcStatus.SKIPPED,
lastError = "eror:no-matching-req",
now = clock.instant(),
)
} else {
procState.markTerminal(head.msgId, ProcStatus.SUCCEEDED, now = clock.instant())
}
return
}
is MsgKind.Unsupported -> {
// 合法但不支持的类型:跳过留档记 SKIPPEDUS-03 AC2markTerminal 同一条 UPDATE