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:
@@ -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>)
|
||||
}
|
||||
+419
@@ -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-13(implementation.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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user