diff --git a/modules/inbound/inbound-xinda-push/src/main/java/com/lingniu/ingest/inbound/xinda/XindaPushClient.java b/modules/inbound/inbound-xinda-push/src/main/java/com/lingniu/ingest/inbound/xinda/XindaPushClient.java index 60a81896..76d043d2 100644 --- a/modules/inbound/inbound-xinda-push/src/main/java/com/lingniu/ingest/inbound/xinda/XindaPushClient.java +++ b/modules/inbound/inbound-xinda-push/src/main/java/com/lingniu/ingest/inbound/xinda/XindaPushClient.java @@ -198,11 +198,12 @@ public class XindaPushClient extends PushClient { // 保留外部字段和内部 VIN 的双轨元数据,方便定位绑定错误或第三方字段变更。 meta.put("source", "xinda-push"); meta.put("cmd", payload.command()); + putRawFields(meta, identityFields); meta.put("vin", identity.identity().vin()); meta.put("externalVin", firstNonBlank(identityFields, "vin")); - meta.put("phone", firstNonBlank(identityFields, "sim", "phone", "mobile")); - meta.put("deviceId", firstNonBlank(identityFields, "deviceId", "terminalId", "imei")); - meta.put("plateNo", firstNonBlank(identityFields, "plateNo", "carNo")); + meta.put("phone", phone(identityFields)); + meta.put("deviceId", deviceId(identityFields)); + meta.put("plateNo", plateNo(identityFields)); meta.put("identityResolved", Boolean.toString(identity.identity().resolved())); meta.put("identitySource", identity.identity().source().name()); if (identity.errorMessage() != null) { @@ -298,9 +299,9 @@ public class XindaPushClient extends PushClient { return new IdentityResolution(identityResolver.resolve(new VehicleIdentityLookup( ProtocolId.XINDA_PUSH, firstNonBlank(json, "vin"), - firstNonBlank(json, "sim", "phone", "mobile"), - firstNonBlank(json, "deviceId", "terminalId", "imei"), - firstNonBlank(json, "plateNo", "carNo"))), null); + phone(json), + deviceId(json), + plateNo(json))), null); } catch (RuntimeException e) { return new IdentityResolution( new VehicleIdentity("unknown", false, VehicleIdentitySource.UNKNOWN), @@ -308,6 +309,60 @@ public class XindaPushClient extends PushClient { } } + private static String plateNo(JSONObject node) { + return firstNonBlank(node, "plateNo", "carNo", "carName"); + } + + private static String phone(JSONObject node) { + return firstNonBlank(node, "sim", "phone", "mobile", "simCode", "tmnKey"); + } + + private static String deviceId(JSONObject node) { + return firstNonBlank(node, "deviceId", "terminalId", "imei", "tmnKey", "tmnCode"); + } + + private static void putRawFields(Map meta, JSONObject json) { + if (meta == null || json == null || json.isEmpty()) { + return; + } + for (Map.Entry entry : json.entrySet()) { + if (entry.getKey() == null || entry.getKey().isBlank() || entry.getValue() == null) { + continue; + } + Object value = entry.getValue(); + if (value instanceof JSONObject object) { + for (Map.Entry nested : object.entrySet()) { + if (nested.getKey() != null && !nested.getKey().isBlank() && nested.getValue() != null) { + meta.put("raw." + entry.getKey() + "." + nested.getKey(), nested.getValue().toString()); + } + } + } else { + String text = value.toString(); + JSONObject nestedJson = parseObjectOrNull(text); + if (nestedJson != null) { + for (Map.Entry nested : nestedJson.entrySet()) { + if (nested.getKey() != null && !nested.getKey().isBlank() && nested.getValue() != null) { + meta.put("raw." + entry.getKey() + "." + nested.getKey(), nested.getValue().toString()); + } + } + } else { + meta.put("raw." + entry.getKey(), text); + } + } + } + } + + private static JSONObject parseObjectOrNull(String value) { + if (value == null || value.isBlank() || !value.trim().startsWith("{")) { + return null; + } + try { + return JSON.parseObject(value); + } catch (Exception ignored) { + return null; + } + } + private static JSONObject parseQuietly(String rawJson) { if (rawJson == null || rawJson.isBlank()) { return new JSONObject(); diff --git a/modules/inbound/inbound-xinda-push/src/test/java/com/lingniu/ingest/inbound/xinda/XindaPushClientTest.java b/modules/inbound/inbound-xinda-push/src/test/java/com/lingniu/ingest/inbound/xinda/XindaPushClientTest.java index c0788618..a299d2fd 100644 --- a/modules/inbound/inbound-xinda-push/src/test/java/com/lingniu/ingest/inbound/xinda/XindaPushClientTest.java +++ b/modules/inbound/inbound-xinda-push/src/test/java/com/lingniu/ingest/inbound/xinda/XindaPushClientTest.java @@ -100,6 +100,36 @@ class XindaPushClientTest { .containsEntry("identitySource", "BOUND_DEVICE_ID"); } + @Test + void businessMessageRawFrameResolvesIdentityByCarNamePlateAlias() throws Exception { + Dispatcher dispatcher = mock(Dispatcher.class); + InMemoryVehicleIdentityService identities = new InMemoryVehicleIdentityService(); + identities.bind(new VehicleIdentityBinding( + ProtocolId.XINDA_PUSH, + "LB9A32A24P0LS1270", + "", + "", + "粤BD12345")); + XindaPushClient client = new XindaPushClient(new XindaPushProperties(), identities, dispatcher); + PushMsg msg = new PushMsg(); + msg.init("0200", "{\"carName\":\"粤BD12345\",\"tmnKey\":\"XD-TMN-001\",\"lng\":113.1,\"lat\":23.1}"); + + client.messageReceived(null, msg); + + ArgumentCaptor captor = ArgumentCaptor.forClass(RawFrame.class); + verify(dispatcher).dispatch(captor.capture()); + RawFrame frame = captor.getValue(); + assertThat(frame.sourceMeta()) + .containsEntry("vin", "LB9A32A24P0LS1270") + .containsEntry("plateNo", "粤BD12345") + .containsEntry("deviceId", "XD-TMN-001") + .containsEntry("raw.carName", "粤BD12345") + .containsEntry("raw.tmnKey", "XD-TMN-001") + .containsEntry("raw.lng", "113.1") + .containsEntry("identityResolved", "true") + .containsEntry("identitySource", "BOUND_PLATE"); + } + @Test void malformedCommandIsStillDispatchedForArchiveAndPassthrough() throws Exception { Dispatcher dispatcher = mock(Dispatcher.class); diff --git a/modules/services/event-history-service/src/main/java/com/lingniu/ingest/eventhistory/RawFrameHistoryController.java b/modules/services/event-history-service/src/main/java/com/lingniu/ingest/eventhistory/RawFrameHistoryController.java index 11b5cb55..4d48d07b 100644 --- a/modules/services/event-history-service/src/main/java/com/lingniu/ingest/eventhistory/RawFrameHistoryController.java +++ b/modules/services/event-history-service/src/main/java/com/lingniu/ingest/eventhistory/RawFrameHistoryController.java @@ -1,5 +1,7 @@ package com.lingniu.ingest.eventhistory; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; import com.lingniu.ingest.tdenginehistory.TdengineHistoryReader; import com.lingniu.ingest.tdenginehistory.TdenginePage; import com.lingniu.ingest.tdenginehistory.TdenginePageCursor; @@ -22,6 +24,7 @@ import java.io.IOException; import java.time.Instant; import java.util.List; import java.util.Locale; +import java.util.Map; @RestController @ConditionalOnProperty(prefix = "lingniu.ingest.event-history", name = "enabled", havingValue = "true") @@ -30,6 +33,9 @@ import java.util.Locale; @Tag(name = "Raw Frame API", description = "原始帧索引分页查询接口。") public final class RawFrameHistoryController { + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final TypeReference> MAP_TYPE = new TypeReference<>() {}; + private final TdengineHistoryReader reader; public RawFrameHistoryController(TdengineHistoryReader reader) { @@ -165,6 +171,7 @@ public final class RawFrameHistoryController { String parseError, String peer, String metadataJson, + Map metadata, String protocol, String vehicleKey, String vin, @@ -185,10 +192,22 @@ public final class RawFrameHistoryController { row.parseError(), row.peer(), row.metadataJson(), + metadata(row.metadataJson()), row.protocol(), row.vehicleKey(), row.vin(), row.phone()); } + + private static Map metadata(String metadataJson) { + if (metadataJson == null || metadataJson.isBlank()) { + return Map.of(); + } + try { + return OBJECT_MAPPER.readValue(metadataJson, MAP_TYPE); + } catch (Exception ignored) { + return Map.of("_raw", metadataJson); + } + } } } diff --git a/modules/services/event-history-service/src/test/java/com/lingniu/ingest/eventhistory/RawFrameHistoryControllerTest.java b/modules/services/event-history-service/src/test/java/com/lingniu/ingest/eventhistory/RawFrameHistoryControllerTest.java index cc9231d4..dbf40e44 100644 --- a/modules/services/event-history-service/src/test/java/com/lingniu/ingest/eventhistory/RawFrameHistoryControllerTest.java +++ b/modules/services/event-history-service/src/test/java/com/lingniu/ingest/eventhistory/RawFrameHistoryControllerTest.java @@ -57,6 +57,10 @@ class RawFrameHistoryControllerTest { assertThat(item.messageIdHex()).isEqualTo("0x0200"); assertThat(item.rawUri()).contains("XINDA_PUSH"); assertThat(item.metadataJson()).contains("rawArchiveUri"); + assertThat(item.metadata()) + .containsEntry("rawArchiveUri", item.rawUri()) + .containsEntry("plateNo", "粤BD12345") + .containsEntry("rawJson", "{\"carName\":\"粤BD12345\"}"); assertThat(response.nextCursor()).isEqualTo(new RawFrameHistoryController.CursorResponse( "2026-06-29T12:00:01Z", "frame-1")); } @@ -98,7 +102,8 @@ class RawFrameHistoryControllerTest { "NOT_PARSED", "", "xinda", - "{\"rawArchiveUri\":\"archive://2026/06/29/XINDA_PUSH/unknown/" + frameId + ".bin\"}", + "{\"rawArchiveUri\":\"archive://2026/06/29/XINDA_PUSH/unknown/" + frameId + + ".bin\",\"plateNo\":\"粤BD12345\",\"rawJson\":\"{\\\"carName\\\":\\\"粤BD12345\\\"}\"}", "XINDA_PUSH", "LB9A32A24P0LS1270", "LB9A32A24P0LS1270",