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:
@@ -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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user