fix(processing): 接通 Redis 投影并修正验证缺口

This commit is contained in:
windyboy
2026-09-21 17:03:26 +08:00
parent 6980926a7c
commit 43bb561f47
17 changed files with 162 additions and 43 deletions
@@ -121,7 +121,7 @@ class PipelineProps {
/** 出站请求落信后等待应答的最长时限(`US-09`;具体取值待 Q)。 */
var responseTimeout: Duration = Duration.ofMinutes(30)
var routingRqfd: String = "OMMSRQFD"
var routingRqfd: String = "OSH5RQFD"
var routingRqrd: String = "OMMSRQRD"
}
@@ -5,7 +5,7 @@ import io.micronaut.context.annotation.ConfigurationProperties
/** 航班查询投影所在的 Redis,对应 `msgx.redis.*`;键位沿用旧系统(`C-11`、`Q6`)。 */
@ConfigurationProperties("msgx.redis")
class RedisProps {
/** 有没有 Redis 可用。关着的时候投影写是空操作`G-REDIS-PROJECTION`。 */
/** 有没有 Redis 可用。关着的时候投影写是空操作。 */
var enabled: Boolean = false
/** 航班快照所在的哈希键,字段名是 `FLID`。 */
@@ -965,11 +965,18 @@ class JdbcFlightStateRepository(
} else {
"SELECT * FROM ${spec.table} WHERE flid = ? ORDER BY ordinal ASC"
}
return ds.query(sql, { ps -> ps.setString(1, flid); spec.routeKind?.let { ps.setString(2, it) } }, ::detailItem)
return ds.query(
sql,
{ ps -> ps.setString(1, flid); spec.routeKind?.let { ps.setString(2, it) } },
{ rs -> detailItem(rs, spec) },
)
}
private fun detailItem(rs: ResultSet): Map<String, String> {
private fun detailItem(rs: ResultSet, spec: DetailSpec): Map<String, String> {
val item = linkedMapOf<String, String>()
spec.seqAttr?.let { seqAttr ->
rs.getString("source_seq")?.takeIf { it.isNotEmpty() }?.let { item[seqAttr] = it }
}
val md = rs.metaData
for (i in 1..md.columnCount) {
val col = md.getColumnName(i).uppercase()
@@ -997,7 +1004,7 @@ class JdbcFlightStateRepository(
/** 明细读取时排除的追踪列(业务键以报文线格式大写键回传)。 */
private val IGNORED_DETAIL_COLUMNS =
setOf("CREATED_AT", "UPDATED_AT", "FLID", "ORDINAL", "ROUTE_KIND")
setOf("CREATED_AT", "UPDATED_AT", "FLID", "ORDINAL", "ROUTE_KIND", "SOURCE_SEQ")
private const val SELECT_MAIN =
"SELECT flid, operation_day, state, state_version, last_msg_id, updated_at FROM flight_schd"
@@ -4,42 +4,52 @@ import com.gzzn.omms.msgexchange.config.RedisProps
import com.gzzn.omms.msgexchange.processing.FlightProjectionPort
import com.gzzn.omms.msgexchange.processing.FlightProjectionWrite
import io.micronaut.context.annotation.Requires
import io.lettuce.core.api.StatefulRedisConnection
import jakarta.inject.Singleton
/**
* Redis 投影适配器骨架:键位与写法在这里定死,**但客户端还没接进来**`G-REDIS-PROJECTION`
* Redis 投影适配器:一个哈希键保存全部航班,批次用 Redis 事务原子提交
*
* 写入形态沿用旧系统:一个哈希键(`PARAM:msgx.redis.flight-key`)装全部航班,
* field 是 `FLID`value 是整态 JSON,不设过期;删除航班就删掉这个 field(`INV-8`)。
*
* 开了 `msgx.redis.enabled` 却没有客户端时,写入直接抛异常而不是假装成功:
* 按 `INV-10`,这条消息就停在未完成、下轮重试,不会被当成已处理写回信箱。
* Redis 调用失败会直接抛异常;按 `INV-10`,消息保持未完成并在下轮重试。
*/
@Requires(property = "msgx.stubs", notEquals = "true")
@Requires(property = "msgx.redis.enabled", value = "true")
@Singleton
class RedisFlightProjectionPort(
private val props: RedisProps,
private val connection: StatefulRedisConnection<String, String>,
) : FlightProjectionPort {
override fun write(writes: List<FlightProjectionWrite>) {
throw UnsupportedOperationException(
"redis client not wired yet (G-REDIS-PROJECTION); pending writes=${writes.size} key=${props.flightKey}",
)
if (writes.isEmpty()) return
val commands = connection.sync()
commands.multi()
try {
writes.forEach { write ->
when (write) {
is FlightProjectionWrite.Upsert -> commands.hset(props.flightKey, write.flid, write.payloadJson)
is FlightProjectionWrite.Delete -> commands.hdel(props.flightKey, write.flid)
}
}
commands.exec()
} catch (failure: RuntimeException) {
runCatching { commands.discard() }
throw failure
}
}
override fun readAll(): List<String> = throw UnsupportedOperationException(
"redis client not wired yet (G-REDIS-PROJECTION); key=${props.flightKey}",
)
override fun readAll(): List<String> = connection.sync().hvals(props.flightKey)
override fun ping(): Boolean = false
override fun ping(): Boolean = connection.sync().ping().equals("PONG", ignoreCase = true)
}
/**
* 没接 Redis 时的投影出口:什么都不做。
*
* 这不是"投影写成功"的承诺——`INV-10` 在真实客户端接通之前只能空转(`G-REDIS-PROJECTION`
* 摆这个 bean 是为了让三步提交的时序在没有 Redis 的环境里也照常跑,而不是让每条航班报文都失败。
* 这不是"投影写成功"的承诺。这个 bean 只让明确关闭投影的环境继续运行
*/
@Requires(property = "msgx.stubs", notEquals = "true")
@Requires(property = "msgx.redis.enabled", notEquals = "true")
@@ -48,7 +58,7 @@ class NoopFlightProjectionPort : FlightProjectionPort {
private val log = org.slf4j.LoggerFactory.getLogger(NoopFlightProjectionPort::class.java)
override fun write(writes: List<FlightProjectionWrite>) {
log.debug("redis projection disabled, skipping {} write(s) [G-REDIS-PROJECTION]", writes.size)
log.debug("redis projection disabled, skipping {} write(s)", writes.size)
}
/** 没有投影可读时报错而不是回空列表:空列表会被查询方当成"现在没有航班"`INV-11`)。 */
@@ -14,7 +14,7 @@ import java.time.ZoneId
import java.time.format.DateTimeFormatter
/**
* 出站 REQ_TRACK 协调:登记、COUTMSGS 落信、超时与 EROR 失败(`G-REQ-TRACK`、`C-4`)。
* 出站 REQ_TRACK 协调:登记、COUTMSGS 落信、超时与 EROR 失败(`C-4`)。
*/
@Singleton
class OutboundRequestService(
@@ -206,7 +206,7 @@ class MessageProcessor(
scheduleProcessor.applyScheduleRecords(head, decoded)
MsgKind.SchdSubtype.RESP -> {
if (!outbound.hasOpenSentRqfd()) {
log.info("SCHD-RESP without open RQFD -> SKIPPED msgId={} [G-RESP-GUARD]", head.msgId)
log.info("SCHD-RESP without open RQFD -> SKIPPED msgId={}", head.msgId)
procState.markTerminal(
head.msgId, ProcStatus.SKIPPED,
lastError = "resp-guard:no-open-req",