fix: enrich xinda raw frame metadata
This commit is contained in:
@@ -198,11 +198,12 @@ public class XindaPushClient extends PushClient {
|
|||||||
// 保留外部字段和内部 VIN 的双轨元数据,方便定位绑定错误或第三方字段变更。
|
// 保留外部字段和内部 VIN 的双轨元数据,方便定位绑定错误或第三方字段变更。
|
||||||
meta.put("source", "xinda-push");
|
meta.put("source", "xinda-push");
|
||||||
meta.put("cmd", payload.command());
|
meta.put("cmd", payload.command());
|
||||||
|
putRawFields(meta, identityFields);
|
||||||
meta.put("vin", identity.identity().vin());
|
meta.put("vin", identity.identity().vin());
|
||||||
meta.put("externalVin", firstNonBlank(identityFields, "vin"));
|
meta.put("externalVin", firstNonBlank(identityFields, "vin"));
|
||||||
meta.put("phone", firstNonBlank(identityFields, "sim", "phone", "mobile"));
|
meta.put("phone", phone(identityFields));
|
||||||
meta.put("deviceId", firstNonBlank(identityFields, "deviceId", "terminalId", "imei"));
|
meta.put("deviceId", deviceId(identityFields));
|
||||||
meta.put("plateNo", firstNonBlank(identityFields, "plateNo", "carNo"));
|
meta.put("plateNo", plateNo(identityFields));
|
||||||
meta.put("identityResolved", Boolean.toString(identity.identity().resolved()));
|
meta.put("identityResolved", Boolean.toString(identity.identity().resolved()));
|
||||||
meta.put("identitySource", identity.identity().source().name());
|
meta.put("identitySource", identity.identity().source().name());
|
||||||
if (identity.errorMessage() != null) {
|
if (identity.errorMessage() != null) {
|
||||||
@@ -298,9 +299,9 @@ public class XindaPushClient extends PushClient {
|
|||||||
return new IdentityResolution(identityResolver.resolve(new VehicleIdentityLookup(
|
return new IdentityResolution(identityResolver.resolve(new VehicleIdentityLookup(
|
||||||
ProtocolId.XINDA_PUSH,
|
ProtocolId.XINDA_PUSH,
|
||||||
firstNonBlank(json, "vin"),
|
firstNonBlank(json, "vin"),
|
||||||
firstNonBlank(json, "sim", "phone", "mobile"),
|
phone(json),
|
||||||
firstNonBlank(json, "deviceId", "terminalId", "imei"),
|
deviceId(json),
|
||||||
firstNonBlank(json, "plateNo", "carNo"))), null);
|
plateNo(json))), null);
|
||||||
} catch (RuntimeException e) {
|
} catch (RuntimeException e) {
|
||||||
return new IdentityResolution(
|
return new IdentityResolution(
|
||||||
new VehicleIdentity("unknown", false, VehicleIdentitySource.UNKNOWN),
|
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<String, String> meta, JSONObject json) {
|
||||||
|
if (meta == null || json == null || json.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (Map.Entry<String, Object> 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<String, Object> 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<String, Object> 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) {
|
private static JSONObject parseQuietly(String rawJson) {
|
||||||
if (rawJson == null || rawJson.isBlank()) {
|
if (rawJson == null || rawJson.isBlank()) {
|
||||||
return new JSONObject();
|
return new JSONObject();
|
||||||
|
|||||||
@@ -100,6 +100,36 @@ class XindaPushClientTest {
|
|||||||
.containsEntry("identitySource", "BOUND_DEVICE_ID");
|
.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<RawFrame> 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
|
@Test
|
||||||
void malformedCommandIsStillDispatchedForArchiveAndPassthrough() throws Exception {
|
void malformedCommandIsStillDispatchedForArchiveAndPassthrough() throws Exception {
|
||||||
Dispatcher dispatcher = mock(Dispatcher.class);
|
Dispatcher dispatcher = mock(Dispatcher.class);
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.lingniu.ingest.eventhistory;
|
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.TdengineHistoryReader;
|
||||||
import com.lingniu.ingest.tdenginehistory.TdenginePage;
|
import com.lingniu.ingest.tdenginehistory.TdenginePage;
|
||||||
import com.lingniu.ingest.tdenginehistory.TdenginePageCursor;
|
import com.lingniu.ingest.tdenginehistory.TdenginePageCursor;
|
||||||
@@ -22,6 +24,7 @@ import java.io.IOException;
|
|||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@ConditionalOnProperty(prefix = "lingniu.ingest.event-history", name = "enabled", havingValue = "true")
|
@ConditionalOnProperty(prefix = "lingniu.ingest.event-history", name = "enabled", havingValue = "true")
|
||||||
@@ -30,6 +33,9 @@ import java.util.Locale;
|
|||||||
@Tag(name = "Raw Frame API", description = "原始帧索引分页查询接口。")
|
@Tag(name = "Raw Frame API", description = "原始帧索引分页查询接口。")
|
||||||
public final class RawFrameHistoryController {
|
public final class RawFrameHistoryController {
|
||||||
|
|
||||||
|
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||||
|
private static final TypeReference<Map<String, Object>> MAP_TYPE = new TypeReference<>() {};
|
||||||
|
|
||||||
private final TdengineHistoryReader reader;
|
private final TdengineHistoryReader reader;
|
||||||
|
|
||||||
public RawFrameHistoryController(TdengineHistoryReader reader) {
|
public RawFrameHistoryController(TdengineHistoryReader reader) {
|
||||||
@@ -165,6 +171,7 @@ public final class RawFrameHistoryController {
|
|||||||
String parseError,
|
String parseError,
|
||||||
String peer,
|
String peer,
|
||||||
String metadataJson,
|
String metadataJson,
|
||||||
|
Map<String, Object> metadata,
|
||||||
String protocol,
|
String protocol,
|
||||||
String vehicleKey,
|
String vehicleKey,
|
||||||
String vin,
|
String vin,
|
||||||
@@ -185,10 +192,22 @@ public final class RawFrameHistoryController {
|
|||||||
row.parseError(),
|
row.parseError(),
|
||||||
row.peer(),
|
row.peer(),
|
||||||
row.metadataJson(),
|
row.metadataJson(),
|
||||||
|
metadata(row.metadataJson()),
|
||||||
row.protocol(),
|
row.protocol(),
|
||||||
row.vehicleKey(),
|
row.vehicleKey(),
|
||||||
row.vin(),
|
row.vin(),
|
||||||
row.phone());
|
row.phone());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Map<String, Object> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,10 @@ class RawFrameHistoryControllerTest {
|
|||||||
assertThat(item.messageIdHex()).isEqualTo("0x0200");
|
assertThat(item.messageIdHex()).isEqualTo("0x0200");
|
||||||
assertThat(item.rawUri()).contains("XINDA_PUSH");
|
assertThat(item.rawUri()).contains("XINDA_PUSH");
|
||||||
assertThat(item.metadataJson()).contains("rawArchiveUri");
|
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(
|
assertThat(response.nextCursor()).isEqualTo(new RawFrameHistoryController.CursorResponse(
|
||||||
"2026-06-29T12:00:01Z", "frame-1"));
|
"2026-06-29T12:00:01Z", "frame-1"));
|
||||||
}
|
}
|
||||||
@@ -98,7 +102,8 @@ class RawFrameHistoryControllerTest {
|
|||||||
"NOT_PARSED",
|
"NOT_PARSED",
|
||||||
"",
|
"",
|
||||||
"xinda",
|
"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",
|
"XINDA_PUSH",
|
||||||
"LB9A32A24P0LS1270",
|
"LB9A32A24P0LS1270",
|
||||||
"LB9A32A24P0LS1270",
|
"LB9A32A24P0LS1270",
|
||||||
|
|||||||
Reference in New Issue
Block a user