diff --git a/DECISIONS.md b/DECISIONS.md index 908cf277..a5315d41 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -91,14 +91,14 @@ 3. 默认优化和验证优先覆盖活跃接入、TDengine 历史和 MySQL 指标链路。 - **Consequences**: 默认构建和部署边界继续收窄;最新状态能力保持源码可用,但其测试需要通过 `-Poptional-latest-state` 显式运行。 -## ADR-014 raw-archive-store 原型实现:Optional only +## ADR-014 raw-archive-store 原型已删除 - **Status**: Accepted -- **Context**: 默认生产 raw bytes 写入由 `sink-archive` 负责,历史查询通过 TDengine `raw_frames` 和 `archive://...` 引用追溯;`raw-archive-store` 当前只是独立读写接口和本地实现原型,没有默认 app 或 service 依赖。 +- **Context**: 默认生产 raw bytes 写入由 `sink-archive` 负责,历史查询通过 TDengine `raw_frames` 和 `archive://...` 引用追溯;`raw-archive-store` 只是未部署的独立读写原型,继续保留会造成 raw archive 路径歧义。 - **Decision**: - 1. `raw-archive-store` 不进入默认 Maven reactor。 - 2. raw-archive-store 仅保留在 `optional-raw-archive-store` profile,需要验证本地读写原型时显式构建。 - 3. 默认生产链路继续以 `sink-archive`、Kafka raw topic、TDengine raw_frames 为准。 -- **Consequences**: 默认构建少一个未部署模块;本地 raw archive 原型仍可通过 `-Poptional-raw-archive-store` 保留和验证。 + 1. 删除 `raw-archive-store` 模块和对应 optional profile。 + 2. 默认生产链路只保留 `sink-archive`、Kafka raw topic、TDengine raw_frames 这条 raw 路径。 + 3. 如需新的 archive store 能力,先以生产链路需求重新设计,不恢复旧原型。 +- **Consequences**: 默认构建面和可选构建面继续收窄;raw bytes 写入职责集中在 `sink-archive`。 ## ADR-015 文件型事件索引:Removed - **Status**: Accepted diff --git a/docs/target-architecture.md b/docs/target-architecture.md index 8e1a3fad..6669a511 100644 --- a/docs/target-architecture.md +++ b/docs/target-architecture.md @@ -187,8 +187,8 @@ flowchart LR `optional-attachments`. - vehicle-state-service is absent from the default Maven reactor and available through `optional-latest-state`. -- raw-archive-store is absent from the default Maven reactor and available - through `optional-raw-archive-store`. +- raw-archive-store prototype has been removed; raw bytes are written through + `sink-archive`. - History APIs read from TDengine; file-based event indexes are not maintained. - Raw frame queries can return complete parsed JSON through `payloadJson.parsed`. - Location queries page over compact rows and reference raw records instead of diff --git a/modules/apps/vehicle-history-app/src/test/java/com/lingniu/ingest/historyapp/MavenModuleProfileTest.java b/modules/apps/vehicle-history-app/src/test/java/com/lingniu/ingest/historyapp/MavenModuleProfileTest.java index d8545e43..47aada88 100644 --- a/modules/apps/vehicle-history-app/src/test/java/com/lingniu/ingest/historyapp/MavenModuleProfileTest.java +++ b/modules/apps/vehicle-history-app/src/test/java/com/lingniu/ingest/historyapp/MavenModuleProfileTest.java @@ -53,14 +53,17 @@ class MavenModuleProfileTest { } @Test - void rawArchiveStorePrototypeIsOptionalAndOutsideDefaultProductionReactor() throws Exception { + void rawArchiveStorePrototypeIsRemovedFromBuildSurface() throws Exception { Document pom = rootPom(); assertThat(defaultModules(pom)) .doesNotContain("modules/sinks/raw-archive-store"); assertThat(profileModules(pom, "optional-raw-archive-store")) - .containsExactly("modules/sinks/raw-archive-store"); + .as("raw bytes are written by sink-archive; raw-archive-store was an unused prototype") + .isEmpty(); + assertThat(Files.exists(repositoryRoot().resolve("modules/sinks/raw-archive-store"))) + .isFalse(); } @Test @@ -153,7 +156,7 @@ class MavenModuleProfileTest { assertThat(profileDependencyManagementArtifacts(pom, "optional-latest-state")) .containsExactly("vehicle-state-service"); assertThat(profileDependencyManagementArtifacts(pom, "optional-raw-archive-store")) - .containsExactly("raw-archive-store"); + .isEmpty(); assertThat(profileDependencyManagementArtifacts(pom, "optional-event-file-store")) .isEmpty(); assertThat(profileDependencyManagementArtifacts(pom, "legacy-xinda")) @@ -328,11 +331,12 @@ class MavenModuleProfileTest { .contains("Xinda Push 仅保留在 `legacy-xinda` profile") .contains("JSATL12 仅保留在 `optional-attachments` profile") .contains("vehicle-state-service 仅保留在 `optional-latest-state` profile") - .contains("raw-archive-store 仅保留在 `optional-raw-archive-store` profile") + .contains("raw-archive-store 原型已删除") .contains("协议接入应用不直接写业务库") .contains("`vehicle-analytics-app` 写入 MySQL `vehicle_stat_metric`") .doesNotContain("event-file-store") .doesNotContain("optional-event-file-store") + .doesNotContain("optional-raw-archive-store") .doesNotContain("## ADR-004 信达 Push:保留模块但彻底重写\n- **Status**: Accepted") .doesNotContain("Spring Boot 3.4.x"); } diff --git a/modules/sinks/raw-archive-store/pom.xml b/modules/sinks/raw-archive-store/pom.xml deleted file mode 100644 index e0179e52..00000000 --- a/modules/sinks/raw-archive-store/pom.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - 4.0.0 - - com.lingniu.ingest - lingniu-vehicle-ingest - 0.1.0-SNAPSHOT - ../../../pom.xml - - raw-archive-store - raw-archive-store - Raw frame archive writer and reader contracts. - - - - com.lingniu.ingest - ingest-api - - - org.junit.jupiter - junit-jupiter - test - - - org.assertj - assertj-core - test - - - diff --git a/modules/sinks/raw-archive-store/src/main/java/com/lingniu/ingest/rawarchive/LocalRawArchiveStore.java b/modules/sinks/raw-archive-store/src/main/java/com/lingniu/ingest/rawarchive/LocalRawArchiveStore.java deleted file mode 100644 index 8ba89823..00000000 --- a/modules/sinks/raw-archive-store/src/main/java/com/lingniu/ingest/rawarchive/LocalRawArchiveStore.java +++ /dev/null @@ -1,110 +0,0 @@ -package com.lingniu.ingest.rawarchive; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.LinkOption; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.time.ZoneId; -import java.util.HexFormat; - -public final class LocalRawArchiveStore implements RawArchiveWriter, RawArchiveReader { - - private static final ZoneId PARTITION_ZONE = ZoneId.of("Asia/Shanghai"); - - private final Path root; - private final String uriPrefix; - - public LocalRawArchiveStore(Path root, String uriPrefix) { - if (root == null) { - throw new IllegalArgumentException("root must not be null"); - } - this.root = root.toAbsolutePath().normalize(); - String prefix = uriPrefix == null || uriPrefix.isBlank() ? "archive://raw" : uriPrefix.trim(); - this.uriPrefix = prefix.endsWith("/") ? prefix.substring(0, prefix.length() - 1) : prefix; - } - - @Override - public RawArchiveReceipt write(RawArchiveWriteRequest request) throws IOException { - String key = key(request); - Path target = resolveKey(key); - Files.createDirectories(target.getParent()); - rejectSymlinkPath(target); - byte[] bytes = request.rawBytes(); - Files.write(target, bytes, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE); - return new RawArchiveReceipt(uriPrefix + "/" + key, checksum(bytes), bytes.length); - } - - @Override - public byte[] read(String rawUri) throws IOException { - String key = keyFromUri(rawUri); - Path target = resolveKey(key); - rejectSymlinkPath(target); - return Files.readAllBytes(target); - } - - private String key(RawArchiveWriteRequest request) { - var date = request.receivedAt().atZone(PARTITION_ZONE).toLocalDate(); - return "%04d/%02d/%02d/%s/%s/%s.bin".formatted( - date.getYear(), - date.getMonthValue(), - date.getDayOfMonth(), - request.protocol().name(), - safeSegment(request.vehicleKey()), - safeSegment(request.frameId())); - } - - private String keyFromUri(String rawUri) { - if (rawUri == null || rawUri.isBlank()) { - throw new IllegalArgumentException("rawUri must not be blank"); - } - String normalized = rawUri.trim(); - String expected = uriPrefix + "/"; - if (!normalized.startsWith(expected)) { - throw new IllegalArgumentException("rawUri does not use expected prefix: " + rawUri); - } - return normalized.substring(expected.length()); - } - - private Path resolveKey(String key) { - Path target = root.resolve(key).normalize(); - if (!target.startsWith(root)) { - throw new IllegalArgumentException("raw archive key escapes root: " + key); - } - return target; - } - - private void rejectSymlinkPath(Path target) throws IOException { - Path current = root; - Path relative = root.relativize(target); - for (Path segment : relative) { - current = current.resolve(segment); - if (Files.isSymbolicLink(current)) { - throw new IOException("raw archive path traverses symlink: " + current); - } - if (Files.notExists(current, LinkOption.NOFOLLOW_LINKS)) { - return; - } - } - } - - private static String safeSegment(String value) { - String cleaned = value == null ? "" : value.trim(); - if (cleaned.isBlank()) { - return "unknown"; - } - return cleaned.replaceAll("[^A-Za-z0-9._-]", "_"); - } - - private static String checksum(byte[] bytes) { - try { - MessageDigest digest = MessageDigest.getInstance("SHA-256"); - return "sha256:" + HexFormat.of().formatHex(digest.digest(bytes)); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException("SHA-256 is unavailable", e); - } - } -} diff --git a/modules/sinks/raw-archive-store/src/main/java/com/lingniu/ingest/rawarchive/RawArchiveReader.java b/modules/sinks/raw-archive-store/src/main/java/com/lingniu/ingest/rawarchive/RawArchiveReader.java deleted file mode 100644 index f5aad3ad..00000000 --- a/modules/sinks/raw-archive-store/src/main/java/com/lingniu/ingest/rawarchive/RawArchiveReader.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.lingniu.ingest.rawarchive; - -import java.io.IOException; - -public interface RawArchiveReader { - byte[] read(String rawUri) throws IOException; -} diff --git a/modules/sinks/raw-archive-store/src/main/java/com/lingniu/ingest/rawarchive/RawArchiveReceipt.java b/modules/sinks/raw-archive-store/src/main/java/com/lingniu/ingest/rawarchive/RawArchiveReceipt.java deleted file mode 100644 index e65326c0..00000000 --- a/modules/sinks/raw-archive-store/src/main/java/com/lingniu/ingest/rawarchive/RawArchiveReceipt.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.lingniu.ingest.rawarchive; - -public record RawArchiveReceipt(String rawUri, String checksum, long sizeBytes) { - public RawArchiveReceipt { - rawUri = required(rawUri, "rawUri"); - checksum = required(checksum, "checksum"); - if (sizeBytes < 0) { - throw new IllegalArgumentException("sizeBytes must not be negative"); - } - } - - private static String required(String value, String field) { - String cleaned = value == null ? "" : value.trim(); - if (cleaned.isBlank()) { - throw new IllegalArgumentException(field + " must not be blank"); - } - return cleaned; - } -} diff --git a/modules/sinks/raw-archive-store/src/main/java/com/lingniu/ingest/rawarchive/RawArchiveWriteRequest.java b/modules/sinks/raw-archive-store/src/main/java/com/lingniu/ingest/rawarchive/RawArchiveWriteRequest.java deleted file mode 100644 index bf42aba3..00000000 --- a/modules/sinks/raw-archive-store/src/main/java/com/lingniu/ingest/rawarchive/RawArchiveWriteRequest.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.lingniu.ingest.rawarchive; - -import com.lingniu.ingest.api.ProtocolId; - -import java.time.Instant; - -public record RawArchiveWriteRequest( - ProtocolId protocol, - String vehicleKey, - String frameId, - Instant receivedAt, - byte[] rawBytes -) { - public RawArchiveWriteRequest { - if (protocol == null) { - throw new IllegalArgumentException("protocol must not be null"); - } - vehicleKey = required(vehicleKey, "vehicleKey"); - frameId = required(frameId, "frameId"); - if (receivedAt == null) { - throw new IllegalArgumentException("receivedAt must not be null"); - } - if (rawBytes == null || rawBytes.length == 0) { - throw new IllegalArgumentException("rawBytes must not be empty"); - } - rawBytes = rawBytes.clone(); - } - - @Override - public byte[] rawBytes() { - return rawBytes.clone(); - } - - private static String required(String value, String field) { - String cleaned = value == null ? "" : value.trim(); - if (cleaned.isBlank()) { - throw new IllegalArgumentException(field + " must not be blank"); - } - return cleaned; - } -} diff --git a/modules/sinks/raw-archive-store/src/main/java/com/lingniu/ingest/rawarchive/RawArchiveWriter.java b/modules/sinks/raw-archive-store/src/main/java/com/lingniu/ingest/rawarchive/RawArchiveWriter.java deleted file mode 100644 index 655026fd..00000000 --- a/modules/sinks/raw-archive-store/src/main/java/com/lingniu/ingest/rawarchive/RawArchiveWriter.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.lingniu.ingest.rawarchive; - -import java.io.IOException; - -public interface RawArchiveWriter { - RawArchiveReceipt write(RawArchiveWriteRequest request) throws IOException; -} diff --git a/modules/sinks/raw-archive-store/src/test/java/com/lingniu/ingest/rawarchive/LocalRawArchiveStoreTest.java b/modules/sinks/raw-archive-store/src/test/java/com/lingniu/ingest/rawarchive/LocalRawArchiveStoreTest.java deleted file mode 100644 index 626a7aae..00000000 --- a/modules/sinks/raw-archive-store/src/test/java/com/lingniu/ingest/rawarchive/LocalRawArchiveStoreTest.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.lingniu.ingest.rawarchive; - -import com.lingniu.ingest.api.ProtocolId; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -import java.nio.file.Path; -import java.time.Instant; -import java.util.HexFormat; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -class LocalRawArchiveStoreTest { - - @TempDir - Path tempDir; - - @Test - void writesReadableRawObjectWithArchiveUriAndChecksum() throws Exception { - LocalRawArchiveStore store = new LocalRawArchiveStore(tempDir, "archive://raw"); - byte[] bytes = HexFormat.of().parseHex("7e020000007e"); - - RawArchiveReceipt receipt = store.write(new RawArchiveWriteRequest( - ProtocolId.JT808, - "jt808:013912345678", - "frame-1", - Instant.parse("2026-06-29T00:00:00Z"), - bytes)); - - assertThat(receipt.rawUri()).isEqualTo("archive://raw/2026/06/29/JT808/jt808_013912345678/frame-1.bin"); - assertThat(receipt.checksum()).startsWith("sha256:"); - assertThat(receipt.sizeBytes()).isEqualTo(bytes.length); - assertThat(store.read(receipt.rawUri())).containsExactly(bytes); - } - - @Test - void rejectsBlankFrameId() { - LocalRawArchiveStore store = new LocalRawArchiveStore(tempDir, "archive://raw"); - - assertThatThrownBy(() -> store.write(new RawArchiveWriteRequest( - ProtocolId.GB32960, - "VIN001", - "", - Instant.parse("2026-06-29T00:00:00Z"), - new byte[]{1}))) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("frameId"); - } -} diff --git a/pom.xml b/pom.xml index 61f1ab0c..4a1eb98c 100644 --- a/pom.xml +++ b/pom.xml @@ -91,21 +91,6 @@ - - optional-raw-archive-store - - modules/sinks/raw-archive-store - - - - - com.lingniu.ingest - raw-archive-store - ${project.version} - - - - legacy-xinda