141 lines
5.7 KiB
Kotlin
141 lines
5.7 KiB
Kotlin
package com.gzzn.omms.msgexchange.processing
|
|
|
|
import com.gzzn.omms.msgexchange.codec.XmlCodec
|
|
import com.gzzn.omms.msgexchange.config.OperationDayProps
|
|
import com.gzzn.omms.msgexchange.config.PipelineProps
|
|
import com.gzzn.omms.msgexchange.domain.OutboundRequestKeys
|
|
import com.gzzn.omms.msgexchange.infra.persistence.CoutmsgOutboxRepository
|
|
import com.gzzn.omms.msgexchange.infra.persistence.ReqTrackRepository
|
|
import jakarta.inject.Singleton
|
|
import org.slf4j.LoggerFactory
|
|
import java.time.Clock
|
|
import java.time.LocalDate
|
|
import java.time.ZoneId
|
|
import java.time.format.DateTimeFormatter
|
|
|
|
/**
|
|
* 出站 REQ_TRACK 协调:登记、COUTMSGS 落信、超时与 EROR 失败(`C-4`)。
|
|
*/
|
|
@Singleton
|
|
class OutboundRequestService(
|
|
private val reqTrack: ReqTrackRepository,
|
|
private val outbox: CoutmsgOutboxRepository,
|
|
private val codec: XmlCodec,
|
|
private val props: PipelineProps,
|
|
operationDayProps: OperationDayProps,
|
|
private val clock: Clock,
|
|
) {
|
|
private val log = LoggerFactory.getLogger(OutboundRequestService::class.java)
|
|
private val zone: ZoneId = operationDayProps.zoneId()
|
|
private val cutoffHour = operationDayProps.cutoffHour.coerceIn(0, 23)
|
|
private val dttmFmt = DateTimeFormatter.ofPattern("yyyyMMddHHmmss")
|
|
|
|
sealed interface RegisterOutcome {
|
|
data class Registered(val reqId: Long) : RegisterOutcome
|
|
data object OpenExists : RegisterOutcome
|
|
}
|
|
|
|
fun currentOperationDay(): LocalDate {
|
|
val airport = clock.withZone(zone)
|
|
val now = java.time.LocalDateTime.now(airport)
|
|
val day = now.toLocalDate()
|
|
return if (now.hour < cutoffHour) day.minusDays(1) else day
|
|
}
|
|
|
|
fun hasOpenRequest(reqType: String, operationDay: LocalDate = currentOperationDay()): Boolean =
|
|
reqTrack.findLatest(
|
|
reqType,
|
|
operationDay,
|
|
OutboundRequestKeys.SENDER,
|
|
listOf(ReqTrackRepository.ReqState.PENDING, ReqTrackRepository.ReqState.SENT),
|
|
) != null
|
|
|
|
fun registerRqfdSync(): RegisterOutcome {
|
|
val day = currentOperationDay()
|
|
if (hasOpenRequest(OutboundRequestKeys.RQFD_REQ_TYPE, day)) return RegisterOutcome.OpenExists
|
|
val reqId = reqTrack.insert(OutboundRequestKeys.RQFD_REQ_TYPE, day, OutboundRequestKeys.SENDER)
|
|
dispatchPending(limit = 1)
|
|
return RegisterOutcome.Registered(reqId)
|
|
}
|
|
|
|
fun hasOpenSentRqfd(operationDay: LocalDate = currentOperationDay()): Boolean =
|
|
reqTrack.findLatest(
|
|
OutboundRequestKeys.RQFD_REQ_TYPE,
|
|
operationDay,
|
|
OutboundRequestKeys.SENDER,
|
|
listOf(ReqTrackRepository.ReqState.SENT),
|
|
) != null
|
|
|
|
fun completeRqfdResponse(operationDay: LocalDate = currentOperationDay()): Boolean =
|
|
reqTrack.completeLatest(OutboundRequestKeys.RQFD_REQ_TYPE, operationDay, OutboundRequestKeys.SENDER)
|
|
|
|
fun completeRqrdResponse(operationDay: LocalDate = currentOperationDay()): Boolean =
|
|
reqTrack.completeLatest(OutboundRequestKeys.RQRD_REQ_TYPE, operationDay, OutboundRequestKeys.SENDER)
|
|
|
|
fun dispatchPending(limit: Int = 10): Int {
|
|
var sent = 0
|
|
reqTrack.listPendingDispatch(limit).forEach { req ->
|
|
if (dispatchOne(req)) sent++
|
|
}
|
|
return sent
|
|
}
|
|
|
|
fun expireTimedOut(): Int {
|
|
val cutoff = clock.instant().minus(props.outbound.responseTimeout)
|
|
return reqTrack.expireSentOlderThan(cutoff)
|
|
}
|
|
|
|
fun failFromEror(seqs: Long, typs: String, stys: String): Boolean {
|
|
val reqType = OutboundRequestKeys.reqTypeFromEror(typs, stys) ?: return false
|
|
val day = currentOperationDay()
|
|
val open = reqTrack.findOpenSentBySeqn(reqType, day, OutboundRequestKeys.SENDER, seqs) ?: return false
|
|
reqTrack.markFailed(open.reqId)
|
|
log.warn(
|
|
"outbound request FAILED from EROR reqId={} type={} seqn={} typs={} stys={}",
|
|
open.reqId, reqType, seqs, typs, stys,
|
|
)
|
|
return true
|
|
}
|
|
|
|
private fun dispatchOne(req: ReqTrackRepository.Req): Boolean {
|
|
if (req.state != ReqTrackRepository.ReqState.PENDING || req.writeUncertain) return false
|
|
val seqn = outbox.nextOutboundSeqn()
|
|
val dttm = beijingMetaDttm()
|
|
val xml = when (req.reqType) {
|
|
OutboundRequestKeys.RQFD_REQ_TYPE -> codec.encodeOutboundRqfd(seqn, dttm)
|
|
OutboundRequestKeys.RQRD_REQ_TYPE -> codec.encodeOutboundRqrd("AIRL", seqn, dttm)
|
|
else -> {
|
|
log.warn("unknown req_type for dispatch reqId={} type={}", req.reqId, req.reqType)
|
|
return false
|
|
}
|
|
}
|
|
val routing = when (req.reqType) {
|
|
OutboundRequestKeys.RQFD_REQ_TYPE -> props.outbound.routingRqfd
|
|
else -> props.outbound.routingRqrd
|
|
}
|
|
val inserted = outbox.insertMessage(xml, routing)
|
|
return when (inserted.outcome) {
|
|
CoutmsgOutboxRepository.InsertOutcome.CONFIRMED -> {
|
|
val id = inserted.coutmsgsId ?: return false
|
|
reqTrack.linkCoutmsgs(req.reqId, id, seqn)
|
|
reqTrack.markSent(req.reqId, clock.instant())
|
|
log.info("outbound COUTMSGS SENT reqId={} coutmsgsId={} seqn={}", req.reqId, id, seqn)
|
|
true
|
|
}
|
|
CoutmsgOutboxRepository.InsertOutcome.AMBIGUOUS -> {
|
|
reqTrack.markWriteUncertain(req.reqId)
|
|
log.error(
|
|
"outbound COUTMSGS write AMBIGUOUS reqId={} seqn={} — recorded, no blind resend",
|
|
req.reqId, seqn,
|
|
)
|
|
false
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun beijingMetaDttm(): Long {
|
|
val ldt = java.time.LocalDateTime.ofInstant(clock.instant(), zone)
|
|
return dttmFmt.format(ldt).toLong()
|
|
}
|
|
}
|