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() } }