From aed023043c172e2183eef7dc84d8f803866796e6 Mon Sep 17 00:00:00 2001 From: lingniu Date: Mon, 29 Jun 2026 12:53:08 +0800 Subject: [PATCH] feat: add vehicle key derivation --- .../com/lingniu/ingest/facts/VehicleKey.java | 46 +++++++++++++++++++ .../lingniu/ingest/facts/VehicleKeyTest.java | 42 +++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 modules/core/ingest-facts/src/main/java/com/lingniu/ingest/facts/VehicleKey.java create mode 100644 modules/core/ingest-facts/src/test/java/com/lingniu/ingest/facts/VehicleKeyTest.java diff --git a/modules/core/ingest-facts/src/main/java/com/lingniu/ingest/facts/VehicleKey.java b/modules/core/ingest-facts/src/main/java/com/lingniu/ingest/facts/VehicleKey.java new file mode 100644 index 00000000..ae93a68f --- /dev/null +++ b/modules/core/ingest-facts/src/main/java/com/lingniu/ingest/facts/VehicleKey.java @@ -0,0 +1,46 @@ +package com.lingniu.ingest.facts; + +import com.lingniu.ingest.api.ProtocolId; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; + +public final class VehicleKey { + + private VehicleKey() { + } + + public static String derive(ProtocolId protocol, String vin, String phone, String rawIdentitySeed) { + if (protocol == null) { + throw new IllegalArgumentException("protocol must not be null"); + } + String normalizedVin = clean(vin); + if (!normalizedVin.isBlank()) { + return normalizedVin; + } + String normalizedPhone = clean(phone); + if (protocol == ProtocolId.JT808 && !normalizedPhone.isBlank()) { + return "jt808:" + normalizedPhone; + } + String seed = clean(rawIdentitySeed); + if (seed.isBlank()) { + seed = protocol.name(); + } + return "unknown:" + protocol.name() + ":" + sha256(seed).substring(0, 16); + } + + private static String clean(String value) { + return value == null ? "" : value.trim(); + } + + private static String sha256(String value) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + return HexFormat.of().formatHex(digest.digest(value.getBytes(StandardCharsets.UTF_8))); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("SHA-256 is unavailable", e); + } + } +} diff --git a/modules/core/ingest-facts/src/test/java/com/lingniu/ingest/facts/VehicleKeyTest.java b/modules/core/ingest-facts/src/test/java/com/lingniu/ingest/facts/VehicleKeyTest.java new file mode 100644 index 00000000..43820676 --- /dev/null +++ b/modules/core/ingest-facts/src/test/java/com/lingniu/ingest/facts/VehicleKeyTest.java @@ -0,0 +1,42 @@ +package com.lingniu.ingest.facts; + +import com.lingniu.ingest.api.ProtocolId; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class VehicleKeyTest { + + @Test + void usesVinForGb32960() { + assertThat(VehicleKey.derive(ProtocolId.GB32960, " LB9A32A20P0LS1257 ", "", "abc")) + .isEqualTo("LB9A32A20P0LS1257"); + } + + @Test + void usesResolvedVinForJt808WhenPresent() { + assertThat(VehicleKey.derive(ProtocolId.JT808, "VIN001", "013912345678", "abc")) + .isEqualTo("VIN001"); + } + + @Test + void usesPhoneForJt808WhenVinIsMissing() { + assertThat(VehicleKey.derive(ProtocolId.JT808, "", "013912345678", "abc")) + .isEqualTo("jt808:013912345678"); + } + + @Test + void usesStableUnknownHashWhenVehicleIdentifiersAreMissing() { + assertThat(VehicleKey.derive(ProtocolId.JT808, "", "", "abc")) + .isEqualTo(VehicleKey.derive(ProtocolId.JT808, null, null, "abc")) + .startsWith("unknown:JT808:"); + } + + @Test + void rejectsMissingProtocol() { + assertThatThrownBy(() -> VehicleKey.derive(null, "VIN001", "", "abc")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("protocol"); + } +}