fix(acm2-107): finalize /all/flights contract and FLTR JSON payload

Close Q21 into C-11 and emit day-schedule FLTR-shaped JSON for Redis, schd, and query responses.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
windyboy
2026-09-23 11:23:37 +08:00
co-authored by Cursor
parent 43bb561f47
commit f45a0ac898
10 changed files with 103 additions and 50 deletions
@@ -15,8 +15,8 @@ import io.micronaut.http.annotation.Produces
* 数据只从投影来,不回落 PG:查询与网页客户端必须读同一份,不然两边会看到不同的航班
* (`INV-11`)。投影读不出来就返回错误——空列表会被前端当成"现在没有航班"`US-12` AC2)。
*
* 响应口径暂定(`Q21`):成功裸 JSON 数组、不套旧系统的 `ResponseDto`;失败503 加一个
* 错误对象。投影载荷的形状与 `KAFKA:schd` 同一份,尚未按旧系统 `SCHD.FLTR` 逐字对拍
* 成功裸 JSON 数组、不套旧系统的 `ResponseDto`;失败503 加错误对象(`C-11`)。
* 数组元素与 `KAFKA:schd` 同形,都是日计划 `FLTR` 转成的 JSON`C-9`、`C-11`
*/
@Controller("/all/flights")
class FlightQueryController(
@@ -45,7 +45,7 @@ class FlightQueryController(
private fun nonSharedFlights(): List<com.fasterxml.jackson.databind.JsonNode> =
projection.readAll()
.map { mapper.readTree(it) }
.filter { it.path("scalars").path("MAID").asText("").isBlank() }
.filter { it.path("MAID").asText("").isBlank() }
private companion object {
private const val PROJECTION_UNAVAILABLE = "FLIGHT_PROJECTION_UNAVAILABLE"
@@ -189,13 +189,22 @@ class AdftProcessor(
// 共享小工具(处理器层私有约定)
// =====================================================================
/** 航班整态载荷:`KAFKA:schd` 事件与 Redis 投影共用这一份形状。 */
internal fun flightPayload(next: FlightSnapshot): Map<String, Any> = linkedMapOf(
"flid" to next.flid,
"stateVersion" to next.stateVersion,
"scalars" to next.scalars,
"collections" to next.collections,
)
/**
* 航班整态载荷:`KAFKA:schd` 事件与 Redis 投影共用。
*
* 形状就是日计划一条 `FLTR` 转成 JSON(字段名与 SIS/XSD 一致,`C-9`、`C-11`):先 `FLID`,再标量,再集合段。
* 空串与空集合不输出;内部的 `stateVersion` 不进载荷,只用于投影/事件代次守卫。
*/
internal fun flightPayload(next: FlightSnapshot): Map<String, Any> {
val out = linkedMapOf<String, Any>("FLID" to next.flid)
next.scalars.forEach { (key, value) ->
if (value.isNotEmpty()) out[key] = value
}
next.collections.forEach { (key, rows) ->
if (rows.isNotEmpty()) out[key] = rows
}
return out
}
/**
* 删除通知:只走 `KAFKA:msg``schd` 不发 tombstone`C-9`)。
@@ -46,7 +46,7 @@ interface FlightProjectionPort {
class FlightProjectionFailure(msgId: Long, cause: Throwable) :
RuntimeException("flight projection write failed msgId=$msgId: ${cause.message ?: cause.javaClass.simpleName}", cause)
/** 投影载荷与 `KAFKA:schd` 整态同一份 JSON:投影和通知同源,对账时不用比两种形状(`C-11`)。 */
/** 投影载荷与 `KAFKA:schd` 整态同一份:都是 `FLTR` 转 JSON`C-9`、`C-11`)。 */
internal fun projectionOf(snapshot: FlightSnapshot, mapper: ObjectMapper): FlightProjectionWrite.Upsert =
FlightProjectionWrite.Upsert(
flid = snapshot.flid,
@@ -8,7 +8,7 @@ import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
/** `GET /all/flights`:投影读、过滤共享航班、失败时 503(`US-12`、`C-11`、`Q21` 暂定)。 */
/** `GET /all/flights`:投影读、过滤共享航班、失败时 503(`US-12`、`C-11`)。 */
class FlightQueryControllerTest {
private val mapper = ObjectMapper()
@@ -16,8 +16,8 @@ class FlightQueryControllerTest {
@Test
fun `returns bare JSON array of non-shared flights`() {
val projection = StubFlightProjectionPort()
projection.snapshots["1"] = """{"flid":"1","scalars":{"MAID":""}}"""
projection.snapshots["2"] = """{"flid":"2","scalars":{"MAID":"1"}}"""
projection.snapshots["1"] = """{"FLID":"1","FLNO":"CA001"}"""
projection.snapshots["2"] = """{"FLID":"2","FLNO":"CA002","MAID":"1"}"""
val controller = FlightQueryController(projection, mapper)
val response = controller.all()
@@ -26,7 +26,8 @@ class FlightQueryControllerTest {
val root = mapper.readTree(response.body()!!)
assertTrue(root.isArray)
assertEquals(1, root.size())
assertEquals("1", root[0].path("flid").asText())
assertEquals("1", root[0].path("FLID").asText())
assertEquals("CA001", root[0].path("FLNO").asText())
}
@Test
@@ -0,0 +1,45 @@
package com.gzzn.omms.msgexchange.processing
import com.gzzn.omms.msgexchange.domain.flight.FlightSnapshot
import com.gzzn.omms.msgexchange.domain.flight.FlightState
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
/** 投影/`schd` 载荷是日计划 `FLTR` 转 JSON`C-9`、`C-11`)。 */
class FlightPayloadTest {
@Test
fun `emits FLTR-shaped JSON and omits empty fields and internal version`() {
val snapshot = FlightSnapshot(
flid = "121112312",
operationDay = null,
state = FlightState.ACTIVE,
stateVersion = 7,
scalars = mapOf(
"FLNO" to "UA724",
"MAID" to "",
"REMC" to "keep",
),
collections = mapOf(
"GTDT" to listOf(mapOf("GATE" to "G28")),
"DELY" to emptyList(),
),
)
val payload = flightPayload(snapshot)
assertEquals("121112312", payload["FLID"])
assertEquals("UA724", payload["FLNO"])
assertEquals("keep", payload["REMC"])
assertEquals(listOf(mapOf("GATE" to "G28")), payload["GTDT"])
assertFalse(payload.containsKey("MAID"))
assertFalse(payload.containsKey("DELY"))
assertFalse(payload.containsKey("stateVersion"))
assertFalse(payload.containsKey("scalars"))
assertFalse(payload.containsKey("collections"))
assertFalse(payload.containsKey("flid"))
assertTrue(payload.keys.first() == "FLID")
}
}