feat(refdata): Q22 basicdata 映射与 ReferenceDataProcessor(ACM2-93)

闭合 Q22;V6 建 basicdata 表组;RefData 走真处理器(DNLD/RESP/ADD/UPD/DEL/RSTA)。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
windyboy
2026-09-21 16:01:06 +08:00
co-authored by Cursor
parent 02b0b2f189
commit e5449a5cb1
24 changed files with 1442 additions and 34 deletions
@@ -67,8 +67,7 @@ class JacksonXmlCodec : XmlCodec {
is MsgKind.Schd -> msg.schd?.let(SisWireMapper::scheduleBody)
is MsgKind.Flop -> msg.flop?.let(SisWireMapper::flopPayload)
MsgKind.Fdel -> msg.flop?.let(SisWireMapper::flopPayload)
// 参考数据类别报文的正文结构随 13 类各异(ACM2-93 实装 ReferenceDataProcessor 时绑定)
is MsgKind.RefData -> null
is MsgKind.RefData -> RefDataWireMapper.body(type, styp, msg)
MsgKind.Eror -> msg.eror?.let {
val seqs = it.seqs ?: return DecodeResult.Err(DecodeFailure(ErrorClass.MALFORMED, "missing-eror-seqs"))
val typs = it.typs?.trim()?.takeIf { t -> t.isNotEmpty() }
@@ -0,0 +1,205 @@
package com.gzzn.omms.msgexchange.codec
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty
import com.gzzn.omms.msgexchange.domain.ref.RefDataBody
import com.gzzn.omms.msgexchange.domain.ref.RefDataRecord
import com.gzzn.omms.msgexchange.domain.ref.ResourceStatusRecord
@JsonIgnoreProperties(ignoreUnknown = true)
data class CoulRecordXml(
@param:JacksonXmlProperty(localName = "COUC") val couc: String? = null,
@param:JacksonXmlProperty(localName = "COUN") val coun: String? = null,
@param:JacksonXmlProperty(localName = "CNMC") val cnmc: String? = null,
)
@JsonIgnoreProperties(ignoreUnknown = true)
data class ArptRecordXml(
@param:JacksonXmlProperty(localName = "ITCD") val itcd: String? = null,
@param:JacksonXmlProperty(localName = "ICCD") val iccd: String? = null,
@param:JacksonXmlProperty(localName = "ANAM") val anam: String? = null,
@param:JacksonXmlProperty(localName = "ANMC") val anmc: String? = null,
@param:JacksonXmlProperty(localName = "CTRY") val ctry: String? = null,
@param:JacksonXmlProperty(localName = "ACTY") val acty: String? = null,
@param:JacksonXmlProperty(localName = "ATYP") val atyp: String? = null,
@param:JacksonXmlProperty(localName = "HAUL") val haul: String? = null,
)
@JsonIgnoreProperties(ignoreUnknown = true)
data class AirlRecordXml(
@param:JacksonXmlProperty(localName = "ITOP") val itop: String? = null,
@param:JacksonXmlProperty(localName = "ICOP") val icop: String? = null,
@param:JacksonXmlProperty(localName = "ONAM") val onam: String? = null,
@param:JacksonXmlProperty(localName = "ONMC") val onmc: String? = null,
@param:JacksonXmlProperty(localName = "CTRY") val ctry: String? = null,
@param:JacksonXmlProperty(localName = "DORI") val dori: String? = null,
)
@JsonIgnoreProperties(ignoreUnknown = true)
data class AircRecordXml(
@param:JacksonXmlProperty(localName = "ITAT") val itat: String? = null,
@param:JacksonXmlProperty(localName = "ICAT") val icat: String? = null,
@param:JacksonXmlProperty(localName = "DESC") val desc: String? = null,
@param:JacksonXmlProperty(localName = "CDSC") val cdsc: String? = null,
@param:JacksonXmlProperty(localName = "CHAP") val chap: String? = null,
@param:JacksonXmlProperty(localName = "MAXP") val maxp: String? = null,
@param:JacksonXmlProperty(localName = "MFWT") val mfwt: String? = null,
@param:JacksonXmlProperty(localName = "MTWT") val mtwt: String? = null,
@param:JacksonXmlProperty(localName = "MABR") val mabr: String? = null,
)
@JsonIgnoreProperties(ignoreUnknown = true)
data class RegnRecordXml(
@param:JacksonXmlProperty(localName = "RNUM") val rnum: String? = null,
@param:JacksonXmlProperty(localName = "ITAT") val itat: String? = null,
@param:JacksonXmlProperty(localName = "OWID") val owid: String? = null,
@param:JacksonXmlProperty(localName = "ACAL") val acal: String? = null,
@param:JacksonXmlProperty(localName = "MAXP") val maxp: String? = null,
@param:JacksonXmlProperty(localName = "MFWT") val mfwt: String? = null,
@param:JacksonXmlProperty(localName = "MTWT") val mtwt: String? = null,
)
@JsonIgnoreProperties(ignoreUnknown = true)
data class OrgnRecordXml(
@param:JacksonXmlProperty(localName = "OGID") val ogid: String? = null,
@param:JacksonXmlProperty(localName = "ONAM") val onam: String? = null,
@param:JacksonXmlProperty(localName = "ONMC") val onmc: String? = null,
)
@JsonIgnoreProperties(ignoreUnknown = true)
data class FltlRecordXml(
@param:JacksonXmlProperty(localName = "FTYP") val ftyp: String? = null,
@param:JacksonXmlProperty(localName = "CTYP") val ctyp: String? = null,
@param:JacksonXmlProperty(localName = "FDES") val fdes: String? = null,
@param:JacksonXmlProperty(localName = "FDSC") val fdsc: String? = null,
@param:JacksonXmlProperty(localName = "FCML") val fcml: String? = null,
)
@JsonIgnoreProperties(ignoreUnknown = true)
data class TlstRecordXml(
@param:JacksonXmlProperty(localName = "TCOD") val tcod: String? = null,
@param:JacksonXmlProperty(localName = "TNAM") val tnam: String? = null,
@param:JacksonXmlProperty(localName = "TNMC") val tnmc: String? = null,
)
@JsonIgnoreProperties(ignoreUnknown = true)
data class GlstRecordXml(
@param:JacksonXmlProperty(localName = "GCOD") val gcod: String? = null,
@param:JacksonXmlProperty(localName = "GTNM") val gtnm: String? = null,
@param:JacksonXmlProperty(localName = "GNMC") val gnmc: String? = null,
@param:JacksonXmlProperty(localName = "GCAT") val gcat: String? = null,
@param:JacksonXmlProperty(localName = "GTML") val gtml: String? = null,
)
@JsonIgnoreProperties(ignoreUnknown = true)
data class SlstRecordXml(
@param:JacksonXmlProperty(localName = "SCOD") val scod: String? = null,
@param:JacksonXmlProperty(localName = "STNM") val stnm: String? = null,
@param:JacksonXmlProperty(localName = "SNMC") val snmc: String? = null,
@param:JacksonXmlProperty(localName = "STML") val stml: String? = null,
@param:JacksonXmlProperty(localName = "SATC") val satc: String? = null,
)
@JsonIgnoreProperties(ignoreUnknown = true)
data class ClstRecordXml(
@param:JacksonXmlProperty(localName = "CCOD") val ccod: String? = null,
@param:JacksonXmlProperty(localName = "CTNM") val ctnm: String? = null,
@param:JacksonXmlProperty(localName = "CNMC") val cnmc: String? = null,
@param:JacksonXmlProperty(localName = "CCAT") val ccat: String? = null,
@param:JacksonXmlProperty(localName = "CTML") val ctml: String? = null,
@param:JacksonXmlProperty(localName = "CTRA") val ctra: String? = null,
)
@JsonIgnoreProperties(ignoreUnknown = true)
data class BlstRecordXml(
@param:JacksonXmlProperty(localName = "BCOD") val bcod: String? = null,
@param:JacksonXmlProperty(localName = "BTNM") val btnm: String? = null,
@param:JacksonXmlProperty(localName = "BNMC") val bnmc: String? = null,
@param:JacksonXmlProperty(localName = "BCAT") val bcat: String? = null,
@param:JacksonXmlProperty(localName = "BTML") val btml: String? = null,
)
@JsonIgnoreProperties(ignoreUnknown = true)
data class ChltRecordXml(
@param:JacksonXmlProperty(localName = "CCOD") val ccod: String? = null,
@param:JacksonXmlProperty(localName = "CHNM") val chnm: String? = null,
@param:JacksonXmlProperty(localName = "CNMC") val cnmc: String? = null,
@param:JacksonXmlProperty(localName = "CTML") val ctml: String? = null,
@param:JacksonXmlProperty(localName = "CCAT") val ccat: String? = null,
)
@JsonIgnoreProperties(ignoreUnknown = true)
data class RstaRecordXml(
@param:JacksonXmlProperty(localName = "RTYP") val rtyp: String? = null,
@param:JacksonXmlProperty(localName = "RSID") val rsid: String? = null,
@param:JacksonXmlProperty(localName = "STAT") val stat: String? = null,
@param:JacksonXmlProperty(localName = "RDST") val rdst: String? = null,
@param:JacksonXmlProperty(localName = "RDET") val rdet: String? = null,
)
object RefDataWireMapper {
fun body(category: String, styp: String, msg: SisMessageXml): RefDataBody {
val records = when (category) {
"COUL" -> msg.coul.map { recordOf("COUC" to it.couc, "COUN" to it.coun, "CNMC" to it.cnmc) }
"ARPT" -> msg.arpt.map {
recordOf(
"ITCD" to it.itcd, "ICCD" to it.iccd, "ANAM" to it.anam, "ANMC" to it.anmc,
"CTRY" to it.ctry, "ACTY" to it.acty, "ATYP" to it.atyp, "HAUL" to it.haul,
)
}
"AIRL" -> msg.airl.map {
recordOf("ITOP" to it.itop, "ICOP" to it.icop, "ONAM" to it.onam, "ONMC" to it.onmc, "CTRY" to it.ctry, "DORI" to it.dori)
}
"AIRC" -> msg.airc.map {
recordOf(
"ITAT" to it.itat, "ICAT" to it.icat, "DESC" to it.desc, "CDSC" to it.cdsc,
"CHAP" to it.chap, "MAXP" to it.maxp, "MFWT" to it.mfwt, "MTWT" to it.mtwt, "MABR" to it.mabr,
)
}
"REGN" -> msg.regn.map {
recordOf(
"RNUM" to it.rnum, "ITAT" to it.itat, "OWID" to it.owid, "ACAL" to it.acal,
"MAXP" to it.maxp, "MFWT" to it.mfwt, "MTWT" to it.mtwt,
)
}
"ORGN" -> msg.orgn.map { recordOf("OGID" to it.ogid, "ONAM" to it.onam, "ONMC" to it.onmc) }
"FLTL" -> msg.fltl.map {
recordOf("FTYP" to it.ftyp, "CTYP" to it.ctyp, "FDES" to it.fdes, "FDSC" to it.fdsc, "FCML" to it.fcml)
}
"TLST" -> msg.tlst.map { recordOf("TCOD" to it.tcod, "TNAM" to it.tnam, "TNMC" to it.tnmc) }
"GLST" -> msg.glst.map {
recordOf("GCOD" to it.gcod, "GTNM" to it.gtnm, "GNMC" to it.gnmc, "GCAT" to it.gcat, "GTML" to it.gtml)
}
"SLST" -> msg.slst.map {
recordOf("SCOD" to it.scod, "STNM" to it.stnm, "SNMC" to it.snmc, "STML" to it.stml, "SATC" to it.satc)
}
"CLST" -> msg.clst.map {
recordOf(
"CCOD" to it.ccod, "CTNM" to it.ctnm, "CNMC" to it.cnmc,
"CCAT" to it.ccat, "CTML" to it.ctml, "CTRA" to it.ctra,
)
}
"BLST" -> msg.blst.map {
recordOf("BCOD" to it.bcod, "BTNM" to it.btnm, "BNMC" to it.bnmc, "BCAT" to it.bcat, "BTML" to it.btml)
}
"CHLT" -> msg.chlt.map {
recordOf("CCOD" to it.ccod, "CHNM" to it.chnm, "CNMC" to it.cnmc, "CTML" to it.ctml, "CCAT" to it.ccat)
}
"RSTA" -> emptyList()
else -> emptyList()
}
val rsta = msg.rsta.mapNotNull {
val rtyp = it.rtyp?.trim()?.uppercase()?.takeIf { r -> r.isNotEmpty() } ?: return@mapNotNull null
val rsid = it.rsid?.trim()?.takeIf { r -> r.isNotEmpty() } ?: return@mapNotNull null
val stat = it.stat?.trim()?.uppercase()?.takeIf { r -> r.isNotEmpty() } ?: return@mapNotNull null
ResourceStatusRecord(rtyp, rsid, stat, blankToNull(it.rdst), blankToNull(it.rdet))
}
return RefDataBody(category, styp, records, rsta)
}
private fun recordOf(vararg pairs: Pair<String, String?>): RefDataRecord =
RefDataRecord(pairs.associate { (k, v) -> k to (v ?: "") })
private fun blankToNull(v: String?) = v?.trim()?.takeIf { it.isNotEmpty() }
}
@@ -59,6 +59,20 @@ data class SisMessageXml(
@param:JacksonXmlProperty(localName = "EROR") val eror: ErorXml? = null,
@param:JacksonXmlProperty(localName = "RQFD") val rqfd: RqfdXml? = null,
@param:JacksonXmlProperty(localName = "RQRD") val rqrd: RqrdXml? = null,
@param:JacksonXmlElementWrapper(useWrapping = false) @param:JacksonXmlProperty(localName = "COUL") val coul: List<CoulRecordXml> = emptyList(),
@param:JacksonXmlElementWrapper(useWrapping = false) @param:JacksonXmlProperty(localName = "ARPT") val arpt: List<ArptRecordXml> = emptyList(),
@param:JacksonXmlElementWrapper(useWrapping = false) @param:JacksonXmlProperty(localName = "AIRL") val airl: List<AirlRecordXml> = emptyList(),
@param:JacksonXmlElementWrapper(useWrapping = false) @param:JacksonXmlProperty(localName = "AIRC") val airc: List<AircRecordXml> = emptyList(),
@param:JacksonXmlElementWrapper(useWrapping = false) @param:JacksonXmlProperty(localName = "REGN") val regn: List<RegnRecordXml> = emptyList(),
@param:JacksonXmlElementWrapper(useWrapping = false) @param:JacksonXmlProperty(localName = "ORGN") val orgn: List<OrgnRecordXml> = emptyList(),
@param:JacksonXmlElementWrapper(useWrapping = false) @param:JacksonXmlProperty(localName = "FLTL") val fltl: List<FltlRecordXml> = emptyList(),
@param:JacksonXmlElementWrapper(useWrapping = false) @param:JacksonXmlProperty(localName = "TLST") val tlst: List<TlstRecordXml> = emptyList(),
@param:JacksonXmlElementWrapper(useWrapping = false) @param:JacksonXmlProperty(localName = "GLST") val glst: List<GlstRecordXml> = emptyList(),
@param:JacksonXmlElementWrapper(useWrapping = false) @param:JacksonXmlProperty(localName = "SLST") val slst: List<SlstRecordXml> = emptyList(),
@param:JacksonXmlElementWrapper(useWrapping = false) @param:JacksonXmlProperty(localName = "CLST") val clst: List<ClstRecordXml> = emptyList(),
@param:JacksonXmlElementWrapper(useWrapping = false) @param:JacksonXmlProperty(localName = "BLST") val blst: List<BlstRecordXml> = emptyList(),
@param:JacksonXmlElementWrapper(useWrapping = false) @param:JacksonXmlProperty(localName = "CHLT") val chlt: List<ChltRecordXml> = emptyList(),
@param:JacksonXmlElementWrapper(useWrapping = false) @param:JacksonXmlProperty(localName = "RSTA") val rsta: List<RstaRecordXml> = emptyList(),
)
@JsonIgnoreProperties(ignoreUnknown = true)
@@ -0,0 +1,36 @@
package com.gzzn.omms.msgexchange.domain.ref
/** 解码后的静态参考数据载荷(`US-13`);一条 MSG 对应一个类别码 [category]。 */
data class RefDataBody(
val category: String,
val styp: String,
val records: List<RefDataRecord>,
val resourceStatuses: List<ResourceStatusRecord> = emptyList(),
)
/** 一条参考记录:键为 SIS 叶子标签,空串表示「无值」语义(`US-13` AC4)。 */
data class RefDataRecord(val fields: Map<String, String>)
/** `TYPE=RSTA` 资源状态行(`SIS:3.14`)。 */
data class ResourceStatusRecord(
val rtyp: String,
val rsid: String,
val stat: String,
val rdst: String? = null,
val rdet: String? = null,
)
enum class RefDataMode {
FULL_REPLACE,
ADD,
UPDATE,
DELETE,
}
fun refDataModeOf(styp: String): RefDataMode? = when (styp.uppercase()) {
"DNLD", "RESP" -> RefDataMode.FULL_REPLACE
"ADD" -> RefDataMode.ADD
"UPD" -> RefDataMode.UPDATE
"DEL" -> RefDataMode.DELETE
else -> null
}
@@ -0,0 +1,37 @@
package com.gzzn.omms.msgexchange.domain.ref
/** SIS 标签 → `basicdata` 列(`Q22` 映射表;列名取自 admin-api 实体注解)。 */
internal object RefDataFieldMapping {
fun businessKey(category: String, rec: RefDataRecord): String = when (category) {
"COUL" -> tag(rec, "COUC")
"ARPT" -> tag(rec, "ITCD")
"AIRL" -> tag(rec, "ITOP")
"AIRC" -> tag(rec, "ITAT")
"REGN" -> tag(rec, "RNUM")
"ORGN" -> tag(rec, "OGID")
"FLTL" -> tag(rec, "FTYP")
"TLST" -> tag(rec, "TCOD")
"GLST" -> tag(rec, "GCOD")
"SLST" -> tag(rec, "SCOD")
"CLST" -> tag(rec, "CCOD")
"BLST" -> tag(rec, "BCOD")
"CHLT" -> tag(rec, "CCOD")
else -> error("no key for $category")
}
fun stableNumericId(category: String, key: String): Long {
val h = "$category:$key".hashCode().toLong()
return (if (h == Long.MIN_VALUE) 1L else kotlin.math.abs(h)).coerceAtLeast(1L)
}
private fun tag(rec: RefDataRecord, name: String) = rec.fields[name]?.trim().orEmpty()
fun blankToNull(raw: String?): String? = raw?.trim()?.takeIf { it.isNotEmpty() }
fun domIntFlag(code: String?): java.math.BigDecimal? = when (blankToNull(code)?.uppercase()) {
"I" -> java.math.BigDecimal.ONE
"D" -> java.math.BigDecimal.ZERO
"R" -> java.math.BigDecimal.valueOf(2)
else -> null
}
}
@@ -0,0 +1,50 @@
package com.gzzn.omms.msgexchange.domain.ref
/** 类别校验:失败时整类不写入(`US-13` AC2)。 */
object RefDataValidator {
private val KEY_TAG = mapOf(
"COUL" to "COUC",
"ARPT" to "ITCD",
"AIRL" to "ITOP",
"AIRC" to "ITAT",
"REGN" to "RNUM",
"ORGN" to "OGID",
"FLTL" to "FTYP",
"TLST" to "TCOD",
"GLST" to "GCOD",
"SLST" to "SCOD",
"CLST" to "CCOD",
"BLST" to "BCOD",
"CHLT" to "CCOD",
)
private val ALLOWED_RSTA_RTYP = setOf("BELT", "CNTR", "GATE", "STND", "CHUT")
private val ALLOWED_STAT = setOf("E", "D")
fun validate(category: String, body: RefDataBody, mode: RefDataMode): String? {
if (category == "RSTA") {
if (body.resourceStatuses.isEmpty()) return "rsta-empty"
body.resourceStatuses.forEach { r ->
if (r.rtyp !in ALLOWED_RSTA_RTYP) return "rsta-rtyp:${r.rtyp}"
if (r.stat !in ALLOWED_STAT) return "rsta-stat:${r.stat}"
if (r.rsid.isBlank()) return "rsta-rsid-missing"
}
return null
}
val keyTag = KEY_TAG[category] ?: return "unknown-category:$category"
if (mode == RefDataMode.FULL_REPLACE) {
body.records.forEach { rec ->
validateRecord(keyTag, rec)?.let { return it }
}
return null
}
if (body.records.size != 1) return "incremental-needs-single-record"
return validateRecord(keyTag, body.records.single())
}
private fun validateRecord(keyTag: String, rec: RefDataRecord): String? {
val key = rec.fields[keyTag]?.trim().orEmpty()
if (key.isEmpty()) return "missing-key:$keyTag"
return null
}
}
@@ -0,0 +1,15 @@
package com.gzzn.omms.msgexchange.infra.persistence
import com.gzzn.omms.msgexchange.domain.ref.RefDataRecord
import com.gzzn.omms.msgexchange.domain.ref.ResourceStatusRecord
/** 静态参考数据表组(schema `basicdata`)写入端口(`US-13`、`C-10`)。 */
interface RefDataRepository {
fun replaceCategory(category: String, records: List<RefDataRecord>)
fun upsertCategory(category: String, record: RefDataRecord)
fun deleteCategory(category: String, businessKey: String)
fun applyResourceStatuses(updates: List<ResourceStatusRecord>)
}
@@ -0,0 +1,419 @@
package com.gzzn.omms.msgexchange.infra.persistence.jdbc
import com.gzzn.omms.msgexchange.domain.ref.RefDataFieldMapping
import com.gzzn.omms.msgexchange.domain.ref.RefDataRecord
import com.gzzn.omms.msgexchange.domain.ref.ResourceStatusRecord
import com.gzzn.omms.msgexchange.infra.persistence.RefDataRepository
import io.micronaut.context.annotation.Requires
import jakarta.inject.Singleton
import java.math.BigDecimal
import javax.sql.DataSource
@Singleton
@Requires(property = "datasources.default.enabled", value = "true")
@Requires(missingProperty = "msgx.stubs")
class JdbcRefDataRepository(
private val ds: DataSource,
) : RefDataRepository {
override fun replaceCategory(category: String, records: List<RefDataRecord>) {
ds.withTransaction {
deleteAll(category)
records.forEach { upsertCategory(category, it) }
}
}
override fun upsertCategory(category: String, record: RefDataRecord) {
when (category) {
"COUL" -> upsertCountry(record)
"ARPT" -> upsertAirport(record)
"AIRL" -> upsertAirline(record)
"AIRC" -> upsertAircraftType(record)
"REGN" -> upsertAircraft(record)
"ORGN" -> upsertFlightAgent(record)
"FLTL" -> upsertFlightType(record)
"TLST" -> upsertTerminal(record)
"GLST" -> upsertGate(record)
"SLST" -> upsertStand(record)
"CLST" -> upsertCheckinDesk(record)
"BLST" -> upsertCarousel(record)
"CHLT" -> upsertChut(record)
else -> error("unsupported category $category")
}
}
override fun deleteCategory(category: String, businessKey: String) {
val sql = when (category) {
"COUL" -> "DELETE FROM basicdata.sys_country WHERE COUNTRY_CODE = ?"
"ARPT" -> "DELETE FROM basicdata.sys_airport WHERE AIRPORT_CODE_IATA = ?"
"AIRL" -> "DELETE FROM basicdata.sys_airline WHERE AIRLINE_CODE_IATA = ?"
"AIRC" -> "DELETE FROM basicdata.sys_aircrafttype WHERE AIRCRAFT_TYPE_CODE = ?"
"REGN" -> "DELETE FROM basicdata.sys_aircraft WHERE AIRCRAFT_NUMBER = ?"
"ORGN" -> "DELETE FROM basicdata.sys_flightagent WHERE FLIGHTAGENT_ID = ?"
"FLTL" -> "DELETE FROM basicdata.sys_flight_type WHERE FLIGHT_TYPE_CODE = ?"
"TLST" -> "DELETE FROM basicdata.orms_terminal WHERE TERMINAL_CODE = ?"
"GLST" -> "DELETE FROM basicdata.orms_gate WHERE GATE_CODE = ?"
"SLST" -> "DELETE FROM basicdata.orms_stand WHERE STAND_CODE = ?"
"CLST" -> "DELETE FROM basicdata.orms_checkindesk WHERE CHECKINDESK_CODE = ?"
"BLST" -> "DELETE FROM basicdata.orms_carousel WHERE CAROUSEL_CODE = ?"
"CHLT" -> "DELETE FROM basicdata.orms_chut WHERE CHUT_CODE = ?"
else -> error("unsupported category $category")
}
ds.update(sql) { ps -> ps.setString(1, businessKey) }
}
override fun applyResourceStatuses(updates: List<ResourceStatusRecord>) {
updates.forEach { u ->
when (u.rtyp) {
"GATE" -> ds.update(
"UPDATE basicdata.orms_gate SET GATE_STATUS = ? WHERE GATE_CODE = ?",
) { ps ->
ps.setBigDecimal(1, numericStatus(u.stat))
ps.setString(2, u.rsid)
}
"STND" -> ds.update(
"UPDATE basicdata.orms_stand SET STAND_STATUS = ? WHERE STAND_CODE = ?",
) { ps ->
ps.setBigDecimal(1, numericStatus(u.stat))
ps.setString(2, u.rsid)
}
"BELT" -> ds.update(
"UPDATE basicdata.orms_carousel SET CAROUSEL_STATUS = ? WHERE CAROUSEL_CODE = ?",
) { ps ->
ps.setString(1, u.stat)
ps.setString(2, u.rsid)
}
"CNTR" -> ds.update(
"UPDATE basicdata.orms_checkindesk SET CHECKINDESK_STATUS = ? WHERE CHECKINDESK_CODE = ?",
) { ps ->
ps.setString(1, u.stat)
ps.setString(2, u.rsid)
}
"CHUT" -> ds.update(
"UPDATE basicdata.orms_chut SET CHUT_STATUS = ? WHERE CHUT_CODE = ?",
) { ps ->
ps.setString(1, u.stat)
ps.setString(2, u.rsid)
}
}
}
}
private fun deleteAll(category: String) {
val sql = when (category) {
"COUL" -> "DELETE FROM basicdata.sys_country"
"ARPT" -> "DELETE FROM basicdata.sys_airport"
"AIRL" -> "DELETE FROM basicdata.sys_airline"
"AIRC" -> "DELETE FROM basicdata.sys_aircrafttype"
"REGN" -> "DELETE FROM basicdata.sys_aircraft"
"ORGN" -> "DELETE FROM basicdata.sys_flightagent"
"FLTL" -> "DELETE FROM basicdata.sys_flight_type"
"TLST" -> "DELETE FROM basicdata.orms_terminal"
"GLST" -> "DELETE FROM basicdata.orms_gate"
"SLST" -> "DELETE FROM basicdata.orms_stand"
"CLST" -> "DELETE FROM basicdata.orms_checkindesk"
"BLST" -> "DELETE FROM basicdata.orms_carousel"
"CHLT" -> "DELETE FROM basicdata.orms_chut"
else -> error("unsupported category $category")
}
ds.update(sql) {}
}
private fun upsertCountry(rec: RefDataRecord) {
val f = rec.fields
val code = tag(f, "COUC") ?: return
val id = RefDataFieldMapping.stableNumericId("COUL", code)
ds.update(
"""
INSERT INTO basicdata.sys_country (COUNTRY_ID, COUNTRY_CODE, COUNTRY_NAME_ENG, COUNTRY_NAME_CHN)
VALUES (?, ?, ?, ?)
ON CONFLICT (COUNTRY_ID) DO UPDATE SET
COUNTRY_CODE = EXCLUDED.COUNTRY_CODE,
COUNTRY_NAME_ENG = EXCLUDED.COUNTRY_NAME_ENG,
COUNTRY_NAME_CHN = EXCLUDED.COUNTRY_NAME_CHN
""".trimIndent(),
) { ps ->
ps.setLong(1, id)
ps.setString(2, code)
ps.setString(3, tag(f, "COUN"))
ps.setString(4, tag(f, "CNMC"))
}
}
private fun upsertAirport(rec: RefDataRecord) {
val f = rec.fields
val iata = tag(f, "ITCD") ?: return
val id = RefDataFieldMapping.stableNumericId("ARPT", iata)
ds.update(
"""
INSERT INTO basicdata.sys_airport (
AIRPORT_ID, AIRPORT_CODE_IATA, AIRPORT_CODE_ICAO, AIRPORT_NAME_ENG, AIRPORT_NAME_CHN, HAUL, INTERNATIONAL_FLAG
) VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (AIRPORT_ID) DO UPDATE SET
AIRPORT_CODE_IATA = EXCLUDED.AIRPORT_CODE_IATA,
AIRPORT_CODE_ICAO = EXCLUDED.AIRPORT_CODE_ICAO,
AIRPORT_NAME_ENG = EXCLUDED.AIRPORT_NAME_ENG,
AIRPORT_NAME_CHN = EXCLUDED.AIRPORT_NAME_CHN,
HAUL = EXCLUDED.HAUL,
INTERNATIONAL_FLAG = EXCLUDED.INTERNATIONAL_FLAG
""".trimIndent(),
) { ps ->
ps.setLong(1, id)
ps.setString(2, iata)
ps.setString(3, tag(f, "ICCD"))
ps.setString(4, tag(f, "ANAM"))
ps.setString(5, tag(f, "ANMC"))
ps.setString(6, tag(f, "HAUL"))
ps.setObject(7, RefDataFieldMapping.domIntFlag(f["ATYP"]))
}
}
private fun upsertAirline(rec: RefDataRecord) {
val f = rec.fields
val iata = tag(f, "ITOP") ?: return
val id = RefDataFieldMapping.stableNumericId("AIRL", iata)
ds.update(
"""
INSERT INTO basicdata.sys_airline (
AIRLINE_ID, AIRLINE_CODE_IATA, AIRLINE_CODE_ICAO, AIRLINE_NAME_ENG, BRIEF_NAME_CHN, INTERNATIONAL_FLAG
) VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT (AIRLINE_ID) DO UPDATE SET
AIRLINE_CODE_IATA = EXCLUDED.AIRLINE_CODE_IATA,
AIRLINE_CODE_ICAO = EXCLUDED.AIRLINE_CODE_ICAO,
AIRLINE_NAME_ENG = EXCLUDED.AIRLINE_NAME_ENG,
BRIEF_NAME_CHN = EXCLUDED.BRIEF_NAME_CHN,
INTERNATIONAL_FLAG = EXCLUDED.INTERNATIONAL_FLAG
""".trimIndent(),
) { ps ->
ps.setLong(1, id)
ps.setString(2, iata)
ps.setString(3, tag(f, "ICOP"))
ps.setString(4, tag(f, "ONAM"))
ps.setString(5, tag(f, "ONMC"))
ps.setObject(6, RefDataFieldMapping.domIntFlag(f["DORI"]))
}
}
private fun upsertAircraftType(rec: RefDataRecord) {
val f = rec.fields
val code = tag(f, "ITAT") ?: return
ds.update(
"""
INSERT INTO basicdata.sys_aircrafttype (
AIRCRAFT_TYPE_CODE, AIRCRAFT_TYPE_CODE_IATA, AIRCRAFT_TYPE_CODE_ICAO, AIRCRAFT_TYPE_NAME,
CHAP, MAX_PASSENGERS, MAX_FREIGHT_WEIGHT, MAX_TAKEOFF_WEIGHT, MAX_AIRBRIDGES
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (AIRCRAFT_TYPE_CODE) DO UPDATE SET
AIRCRAFT_TYPE_CODE_IATA = EXCLUDED.AIRCRAFT_TYPE_CODE_IATA,
AIRCRAFT_TYPE_CODE_ICAO = EXCLUDED.AIRCRAFT_TYPE_CODE_ICAO,
AIRCRAFT_TYPE_NAME = EXCLUDED.AIRCRAFT_TYPE_NAME,
CHAP = EXCLUDED.CHAP,
MAX_PASSENGERS = EXCLUDED.MAX_PASSENGERS,
MAX_FREIGHT_WEIGHT = EXCLUDED.MAX_FREIGHT_WEIGHT,
MAX_TAKEOFF_WEIGHT = EXCLUDED.MAX_TAKEOFF_WEIGHT,
MAX_AIRBRIDGES = EXCLUDED.MAX_AIRBRIDGES
""".trimIndent(),
) { ps ->
ps.setString(1, code)
ps.setString(2, code)
ps.setString(3, tag(f, "ICAT"))
ps.setString(4, tag(f, "DESC"))
ps.setString(5, tag(f, "CHAP"))
ps.setObject(6, tag(f, "MAXP")?.toBigDecimalOrNull())
ps.setObject(7, tag(f, "MFWT")?.toBigDecimalOrNull())
ps.setObject(8, tag(f, "MTWT")?.toBigDecimalOrNull())
ps.setObject(9, tag(f, "MABR")?.toBigDecimalOrNull())
}
}
private fun upsertAircraft(rec: RefDataRecord) {
val f = rec.fields
val tail = tag(f, "RNUM") ?: return
val id = RefDataFieldMapping.stableNumericId("REGN", tail)
ds.update(
"""
INSERT INTO basicdata.sys_aircraft (
AIRCRAFT_ID, AIRCRAFT_NUMBER, AIRCRAFT_TYPE_CODE, AIRLINE_CODE, OWID,
MAX_PASSENGERS, MAX_FREIGHT_WEIGHT, MAX_TAKEOFF_WEIGHT
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (AIRCRAFT_ID) DO UPDATE SET
AIRCRAFT_NUMBER = EXCLUDED.AIRCRAFT_NUMBER,
AIRCRAFT_TYPE_CODE = EXCLUDED.AIRCRAFT_TYPE_CODE,
AIRLINE_CODE = EXCLUDED.AIRLINE_CODE,
OWID = EXCLUDED.OWID,
MAX_PASSENGERS = EXCLUDED.MAX_PASSENGERS,
MAX_FREIGHT_WEIGHT = EXCLUDED.MAX_FREIGHT_WEIGHT,
MAX_TAKEOFF_WEIGHT = EXCLUDED.MAX_TAKEOFF_WEIGHT
""".trimIndent(),
) { ps ->
ps.setLong(1, id)
ps.setString(2, tail)
ps.setString(3, tag(f, "ITAT"))
ps.setString(4, tag(f, "ACAL"))
ps.setObject(5, tag(f, "OWID")?.toBigDecimalOrNull())
ps.setObject(6, tag(f, "MAXP")?.toBigDecimalOrNull())
ps.setObject(7, tag(f, "MFWT")?.toBigDecimalOrNull())
ps.setObject(8, tag(f, "MTWT")?.toBigDecimalOrNull())
}
}
private fun upsertFlightAgent(rec: RefDataRecord) {
val f = rec.fields
val ogid = tag(f, "OGID") ?: return
ds.update(
"""
INSERT INTO basicdata.sys_flightagent (FLIGHTAGENT_ID, OGID, FLIGHTAGENT_NAME)
VALUES (?, ?, ?)
ON CONFLICT (FLIGHTAGENT_ID) DO UPDATE SET
OGID = EXCLUDED.OGID,
FLIGHTAGENT_NAME = EXCLUDED.FLIGHTAGENT_NAME
""".trimIndent(),
) { ps ->
ps.setString(1, ogid)
ps.setString(2, ogid)
ps.setString(3, tag(f, "ONAM"))
}
}
private fun upsertFlightType(rec: RefDataRecord) {
val f = rec.fields
val code = tag(f, "FTYP") ?: return
ds.update(
"""
INSERT INTO basicdata.sys_flight_type (
FLIGHT_TYPE_CODE, FLIGHT_TYPE_CAA_CODE, FLIGHT_TYPE_NAME, FLIGHT_TYPE_NAME_CN, C_TAG
) VALUES (?, ?, ?, ?, ?)
ON CONFLICT (FLIGHT_TYPE_CODE) DO UPDATE SET
FLIGHT_TYPE_CAA_CODE = EXCLUDED.FLIGHT_TYPE_CAA_CODE,
FLIGHT_TYPE_NAME = EXCLUDED.FLIGHT_TYPE_NAME,
FLIGHT_TYPE_NAME_CN = EXCLUDED.FLIGHT_TYPE_NAME_CN,
C_TAG = EXCLUDED.C_TAG
""".trimIndent(),
) { ps ->
ps.setString(1, code)
ps.setString(2, tag(f, "CTYP"))
ps.setString(3, tag(f, "FDES"))
ps.setString(4, tag(f, "FDSC"))
ps.setString(5, tag(f, "FCML"))
}
}
private fun upsertTerminal(rec: RefDataRecord) {
val f = rec.fields
val code = tag(f, "TCOD") ?: return
ds.update(
"""
INSERT INTO basicdata.orms_terminal (TERMINAL_CODE, TERMINAL_NAME)
VALUES (?, ?)
ON CONFLICT (TERMINAL_CODE) DO UPDATE SET TERMINAL_NAME = EXCLUDED.TERMINAL_NAME
""".trimIndent(),
) { ps ->
ps.setString(1, code)
ps.setString(2, tag(f, "TNAM"))
}
}
private fun upsertGate(rec: RefDataRecord) {
val f = rec.fields
val code = tag(f, "GCOD") ?: return
ds.update(
"""
INSERT INTO basicdata.orms_gate (GATE_CODE, GATE_NAME, GATE_CATEGORY, GTML, GATE_STATUS)
VALUES (?, ?, ?, ?, 0)
ON CONFLICT (GATE_CODE) DO UPDATE SET
GATE_NAME = EXCLUDED.GATE_NAME,
GATE_CATEGORY = EXCLUDED.GATE_CATEGORY,
GTML = EXCLUDED.GTML
""".trimIndent(),
) { ps ->
ps.setString(1, code)
ps.setString(2, tag(f, "GTNM"))
ps.setString(3, tag(f, "GCAT"))
ps.setString(4, tag(f, "GTML"))
}
}
private fun upsertStand(rec: RefDataRecord) {
val f = rec.fields
val code = tag(f, "SCOD") ?: return
ds.update(
"""
INSERT INTO basicdata.orms_stand (STAND_CODE, STAND_NAME, STML, SATC, STAND_STATUS)
VALUES (?, ?, ?, ?, 0)
ON CONFLICT (STAND_CODE) DO UPDATE SET
STAND_NAME = EXCLUDED.STAND_NAME,
STML = EXCLUDED.STML,
SATC = EXCLUDED.SATC
""".trimIndent(),
) { ps ->
ps.setString(1, code)
ps.setString(2, tag(f, "STNM"))
ps.setString(3, tag(f, "STML"))
ps.setString(4, tag(f, "SATC"))
}
}
private fun upsertCheckinDesk(rec: RefDataRecord) {
val f = rec.fields
val code = tag(f, "CCOD") ?: return
ds.update(
"""
INSERT INTO basicdata.orms_checkindesk (CHECKINDESK_CODE, CHECKINDESK_NAME, CCAT, CHECKINDESK_STATUS)
VALUES (?, ?, ?, 'E')
ON CONFLICT (CHECKINDESK_CODE) DO UPDATE SET
CHECKINDESK_NAME = EXCLUDED.CHECKINDESK_NAME,
CCAT = EXCLUDED.CCAT
""".trimIndent(),
) { ps ->
ps.setString(1, code)
ps.setString(2, tag(f, "CTNM"))
ps.setString(3, tag(f, "CCAT"))
}
}
private fun upsertCarousel(rec: RefDataRecord) {
val f = rec.fields
val code = tag(f, "BCOD") ?: return
ds.update(
"""
INSERT INTO basicdata.orms_carousel (CAROUSEL_CODE, CAROUSEL_NAME, BCAT, BTML, CAROUSEL_STATUS)
VALUES (?, ?, ?, ?, 'E')
ON CONFLICT (CAROUSEL_CODE) DO UPDATE SET
CAROUSEL_NAME = EXCLUDED.CAROUSEL_NAME,
BCAT = EXCLUDED.BCAT,
BTML = EXCLUDED.BTML
""".trimIndent(),
) { ps ->
ps.setString(1, code)
ps.setString(2, tag(f, "BTNM"))
ps.setString(3, tag(f, "BCAT"))
ps.setString(4, tag(f, "BTML"))
}
}
private fun upsertChut(rec: RefDataRecord) {
val f = rec.fields
val code = tag(f, "CCOD") ?: return
ds.update(
"""
INSERT INTO basicdata.orms_chut (CHUT_CODE, CHUT_NAME, BCAT, BTML, CHUT_STATUS)
VALUES (?, ?, ?, ?, 'E')
ON CONFLICT (CHUT_CODE) DO UPDATE SET
CHUT_NAME = EXCLUDED.CHUT_NAME,
BCAT = EXCLUDED.BCAT,
BTML = EXCLUDED.BTML
""".trimIndent(),
) { ps ->
ps.setString(1, code)
ps.setString(2, tag(f, "CHNM"))
ps.setString(3, tag(f, "CCAT"))
ps.setString(4, tag(f, "CTML"))
}
}
private fun tag(fields: Map<String, String>, name: String): String? =
RefDataFieldMapping.blankToNull(fields[name])
private fun numericStatus(stat: String): BigDecimal =
if (stat == "D") BigDecimal.ONE else BigDecimal.ZERO
}
@@ -0,0 +1,177 @@
package com.gzzn.omms.msgexchange.infra.stub
import com.gzzn.omms.msgexchange.domain.ref.RefDataFieldMapping
import com.gzzn.omms.msgexchange.domain.ref.RefDataRecord
import com.gzzn.omms.msgexchange.domain.ref.ResourceStatusRecord
import com.gzzn.omms.msgexchange.infra.persistence.RefDataRepository
import io.micronaut.context.annotation.Requires
import jakarta.inject.Singleton
import java.math.BigDecimal
import java.util.concurrent.ConcurrentHashMap
@Singleton
@Requires(property = "msgx.stubs", value = "true")
class StubRefDataRepository : RefDataRepository {
val countries = ConcurrentHashMap<String, MutableMap<String, Any?>>()
val airports = ConcurrentHashMap<String, MutableMap<String, Any?>>()
val airlines = ConcurrentHashMap<String, MutableMap<String, Any?>>()
val aircraftTypes = ConcurrentHashMap<String, MutableMap<String, Any?>>()
val aircraft = ConcurrentHashMap<String, MutableMap<String, Any?>>()
val flightAgents = ConcurrentHashMap<String, MutableMap<String, Any?>>()
val flightTypes = ConcurrentHashMap<String, MutableMap<String, Any?>>()
val terminals = ConcurrentHashMap<String, MutableMap<String, Any?>>()
val gates = ConcurrentHashMap<String, MutableMap<String, Any?>>()
val stands = ConcurrentHashMap<String, MutableMap<String, Any?>>()
val checkinDesks = ConcurrentHashMap<String, MutableMap<String, Any?>>()
val carousels = ConcurrentHashMap<String, MutableMap<String, Any?>>()
val chuts = ConcurrentHashMap<String, MutableMap<String, Any?>>()
override fun replaceCategory(category: String, records: List<RefDataRecord>) {
tableOf(category).clear()
records.forEach { upsertCategory(category, it) }
}
override fun upsertCategory(category: String, record: RefDataRecord) {
val key = RefDataFieldMapping.businessKey(category, record)
val row = mapRow(category, record).toMutableMap()
tableOf(category)[key] = row
}
override fun deleteCategory(category: String, businessKey: String) {
tableOf(category).remove(businessKey)
}
override fun applyResourceStatuses(updates: List<ResourceStatusRecord>) {
updates.forEach { u ->
when (u.rtyp) {
"GATE" -> gates[u.rsid]?.let { it["GATE_STATUS"] = gateStatus(u.stat) }
"STND" -> stands[u.rsid]?.let { it["STAND_STATUS"] = gateStatus(u.stat) }
"BELT" -> carousels[u.rsid]?.let { it["CAROUSEL_STATUS"] = u.stat }
"CNTR" -> checkinDesks[u.rsid]?.let { it["CHECKINDESK_STATUS"] = u.stat }
"CHUT" -> chuts[u.rsid]?.let { it["CHUT_STATUS"] = u.stat }
}
}
}
private fun tableOf(category: String): ConcurrentHashMap<String, MutableMap<String, Any?>> = when (category) {
"COUL" -> countries
"ARPT" -> airports
"AIRL" -> airlines
"AIRC" -> aircraftTypes
"REGN" -> aircraft
"ORGN" -> flightAgents
"FLTL" -> flightTypes
"TLST" -> terminals
"GLST" -> gates
"SLST" -> stands
"CLST" -> checkinDesks
"BLST" -> carousels
"CHLT" -> chuts
else -> error("unsupported category $category")
}
private fun mapRow(category: String, rec: RefDataRecord): Map<String, Any?> {
val f = rec.fields
fun s(tag: String) = RefDataFieldMapping.blankToNull(f[tag])
return when (category) {
"COUL" -> mapOf(
"COUNTRY_ID" to RefDataFieldMapping.stableNumericId(category, s("COUC")!!),
"COUNTRY_CODE" to s("COUC"),
"COUNTRY_NAME_ENG" to s("COUN"),
"COUNTRY_NAME_CHN" to s("CNMC"),
)
"ARPT" -> mapOf(
"AIRPORT_ID" to RefDataFieldMapping.stableNumericId(category, s("ITCD")!!),
"AIRPORT_CODE_IATA" to s("ITCD"),
"AIRPORT_CODE_ICAO" to s("ICCD"),
"AIRPORT_NAME_ENG" to s("ANAM"),
"AIRPORT_NAME_CHN" to s("ANMC"),
"HAUL" to s("HAUL"),
"INTERNATIONAL_FLAG" to RefDataFieldMapping.domIntFlag(s("ATYP")),
)
"AIRL" -> mapOf(
"AIRLINE_ID" to RefDataFieldMapping.stableNumericId(category, s("ITOP")!!),
"AIRLINE_CODE_IATA" to s("ITOP"),
"AIRLINE_CODE_ICAO" to s("ICOP"),
"AIRLINE_NAME_ENG" to s("ONAM"),
"BRIEF_NAME_CHN" to s("ONMC"),
"INTERNATIONAL_FLAG" to RefDataFieldMapping.domIntFlag(s("DORI")),
)
"AIRC" -> mapOf(
"AIRCRAFT_TYPE_CODE" to s("ITAT"),
"AIRCRAFT_TYPE_CODE_IATA" to s("ITAT"),
"AIRCRAFT_TYPE_CODE_ICAO" to s("ICAT"),
"AIRCRAFT_TYPE_NAME" to s("DESC"),
"CHAP" to s("CHAP"),
"MAX_PASSENGERS" to s("MAXP")?.toBigDecimalOrNull(),
"MAX_FREIGHT_WEIGHT" to s("MFWT")?.toBigDecimalOrNull(),
"MAX_TAKEOFF_WEIGHT" to s("MTWT")?.toBigDecimalOrNull(),
"MAX_AIRBRIDGES" to s("MABR")?.toBigDecimalOrNull(),
)
"REGN" -> mapOf(
"AIRCRAFT_ID" to RefDataFieldMapping.stableNumericId(category, s("RNUM")!!),
"AIRCRAFT_NUMBER" to s("RNUM"),
"AIRCRAFT_TYPE_CODE" to s("ITAT"),
"AIRLINE_CODE" to s("ACAL"),
"OWID" to s("OWID")?.toBigDecimalOrNull(),
"MAX_PASSENGERS" to s("MAXP")?.toBigDecimalOrNull(),
"MAX_FREIGHT_WEIGHT" to s("MFWT")?.toBigDecimalOrNull(),
"MAX_TAKEOFF_WEIGHT" to s("MTWT")?.toBigDecimalOrNull(),
)
"ORGN" -> mapOf(
"FLIGHTAGENT_ID" to s("OGID"),
"OGID" to s("OGID"),
"FLIGHTAGENT_NAME" to s("ONAM"),
)
"FLTL" -> mapOf(
"FLIGHT_TYPE_CODE" to s("FTYP"),
"FLIGHT_TYPE_CAA_CODE" to s("CTYP"),
"FLIGHT_TYPE_NAME" to s("FDES"),
"FLIGHT_TYPE_NAME_CN" to s("FDSC"),
"C_TAG" to s("FCML"),
)
"TLST" -> mapOf(
"TERMINAL_CODE" to s("TCOD"),
"TERMINAL_NAME" to s("TNAM"),
)
"GLST" -> mapOf(
"GATE_CODE" to s("GCOD"),
"GATE_NAME" to s("GTNM"),
"GATE_CATEGORY" to s("GCAT"),
"GTML" to s("GTML"),
"GATE_STATUS" to BigDecimal.ZERO,
)
"SLST" -> mapOf(
"STAND_CODE" to s("SCOD"),
"STAND_NAME" to s("STNM"),
"STML" to s("STML"),
"SATC" to s("SATC"),
"STAND_STATUS" to BigDecimal.ZERO,
)
"CLST" -> mapOf(
"CHECKINDESK_CODE" to s("CCOD"),
"CHECKINDESK_NAME" to s("CTNM"),
"CCAT" to s("CCAT"),
"CHECKINDESK_STATUS" to "E",
)
"BLST" -> mapOf(
"CAROUSEL_CODE" to s("BCOD"),
"CAROUSEL_NAME" to s("BTNM"),
"BCAT" to s("BCAT"),
"BTML" to s("BTML"),
"CAROUSEL_STATUS" to "E",
)
"CHLT" -> mapOf(
"CHUT_CODE" to s("CCOD"),
"CHUT_NAME" to s("CHNM"),
"BCAT" to s("CCAT"),
"BTML" to s("CTML"),
"CHUT_STATUS" to "E",
)
else -> error("unsupported category $category")
}
}
private fun gateStatus(stat: String): BigDecimal =
if (stat == "D") BigDecimal.ONE else BigDecimal.ZERO
}
@@ -69,6 +69,9 @@ class OutboundRequestService(
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 ->
@@ -124,6 +124,7 @@ class MessageProcessor(
private val flopProcessor: FlopProcessor,
private val fdelProcessor: FdelProcessor,
private val adftProcessor: AdftProcessor,
private val referenceDataProcessor: ReferenceDataProcessor,
private val outbound: OutboundRequestService,
private val procFailure: ProcFailure,
private val props: PipelineProps,
@@ -237,10 +238,9 @@ class MessageProcessor(
flopProcessor.apply(head, decoded, payload)
}
is MsgKind.RefData -> {
// 参考数据必须走 US-13implementation.md「分派」),不得按"不支持"跳过;
// ReferenceDataProcessor 尚未实装(ACM2-93,待 Q22),先按可重试失败留队并告警。
log.warn("refdata without handler -> FAILED(UNSUPPORTED) msgId={} type={}", head.msgId, kind.type)
return procFailure.fail(head, ErrorClass.UNSUPPORTED, "refdata-pending:${kind.type}")
val body = decoded.body as? com.gzzn.omms.msgexchange.domain.ref.RefDataBody // validated below
if (body == null) return deadMalformed(head, "missing-refdata-body")
referenceDataProcessor.apply(head, decoded, kind.type)
}
MsgKind.Eror -> {
val payload = decoded.body as? ErorPayload
@@ -0,0 +1,28 @@
package com.gzzn.omms.msgexchange.processing
import com.gzzn.omms.msgexchange.domain.ProcState
import com.gzzn.omms.msgexchange.domain.ProcStatus
import com.gzzn.omms.msgexchange.infra.persistence.PipelineLockRepository
import com.gzzn.omms.msgexchange.infra.persistence.PipelineTransactionManager
import com.gzzn.omms.msgexchange.infra.persistence.ProcStateRepository
import jakarta.inject.Singleton
import java.time.Clock
/** 静态参考数据单事务提交:落库 + 终态 + 回填意图(无 Redis / MSG_EVENT`US-13`)。 */
@Singleton
class RefDataCommit(
private val txManager: PipelineTransactionManager,
private val lock: PipelineLockRepository,
private val procState: ProcStateRepository,
private val clock: Clock,
) {
fun commit(head: ProcState, work: () -> Unit) {
lock.holdAcrossTransactions {
txManager.inTransaction {
lock.lock()
work()
procState.markTerminal(head.msgId, ProcStatus.SUCCEEDED, now = clock.instant())
}
}
}
}
@@ -0,0 +1,52 @@
package com.gzzn.omms.msgexchange.processing
import com.gzzn.omms.msgexchange.domain.DecodedMessage
import com.gzzn.omms.msgexchange.domain.ProcState
import com.gzzn.omms.msgexchange.domain.ref.RefDataBody
import com.gzzn.omms.msgexchange.domain.ref.RefDataFieldMapping
import com.gzzn.omms.msgexchange.domain.ref.RefDataMode
import com.gzzn.omms.msgexchange.domain.ref.RefDataValidator
import com.gzzn.omms.msgexchange.domain.ref.refDataModeOf
import com.gzzn.omms.msgexchange.infra.persistence.RefDataRepository
import jakarta.inject.Singleton
import org.slf4j.LoggerFactory
@Singleton
class ReferenceDataProcessor(
private val commit: RefDataCommit,
private val refData: RefDataRepository,
private val outbound: OutboundRequestService,
) {
private val log = LoggerFactory.getLogger(ReferenceDataProcessor::class.java)
fun apply(head: ProcState, msg: DecodedMessage, category: String): ApplyResult {
val body = msg.body as? RefDataBody ?: return ApplyResult.DeadProtocol("missing-refdata-body")
val mode = refDataModeOf(body.styp) ?: return ApplyResult.DeadProtocol("refdata-styp:${body.styp}")
val reason = RefDataValidator.validate(category, body, mode)
if (reason != null) {
log.warn("refdata validation failed msgId={} category={} reason={}", head.msgId, category, reason)
return ApplyResult.DeadProtocol(reason)
}
commit.commit(head) {
if (category == "RSTA") {
refData.applyResourceStatuses(body.resourceStatuses)
} else {
when (mode) {
RefDataMode.FULL_REPLACE -> refData.replaceCategory(category, body.records)
RefDataMode.ADD, RefDataMode.UPDATE ->
body.records.forEach { refData.upsertCategory(category, it) }
RefDataMode.DELETE ->
body.records.forEach {
refData.deleteCategory(category, RefDataFieldMapping.businessKey(category, it))
}
}
}
}
if (body.styp.equals("RESP", ignoreCase = true)) {
outbound.completeRqrdResponse()
}
return ApplyResult.Succeeded
}
}