feat: add tdengine history store foundation
This commit is contained in:
@@ -33,6 +33,10 @@
|
|||||||
<groupId>com.lingniu.ingest</groupId>
|
<groupId>com.lingniu.ingest</groupId>
|
||||||
<artifactId>event-file-store</artifactId>
|
<artifactId>event-file-store</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.lingniu.ingest</groupId>
|
||||||
|
<artifactId>tdengine-history-store</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.lingniu.ingest</groupId>
|
<groupId>com.lingniu.ingest</groupId>
|
||||||
<artifactId>event-history-service</artifactId>
|
<artifactId>event-history-service</artifactId>
|
||||||
|
|||||||
@@ -66,6 +66,9 @@ lingniu:
|
|||||||
zone-id: ${EVENT_FILE_STORE_ZONE_ID:Asia/Shanghai}
|
zone-id: ${EVENT_FILE_STORE_ZONE_ID:Asia/Shanghai}
|
||||||
batch-size: ${EVENT_FILE_STORE_BATCH_SIZE:500}
|
batch-size: ${EVENT_FILE_STORE_BATCH_SIZE:500}
|
||||||
flush-interval-millis: ${EVENT_FILE_STORE_FLUSH_INTERVAL_MILLIS:1000}
|
flush-interval-millis: ${EVENT_FILE_STORE_FLUSH_INTERVAL_MILLIS:1000}
|
||||||
|
tdengine-history:
|
||||||
|
enabled: ${TDENGINE_HISTORY_ENABLED:false}
|
||||||
|
database: ${TDENGINE_HISTORY_DATABASE:vehicle_history}
|
||||||
event-history:
|
event-history:
|
||||||
enabled: true
|
enabled: true
|
||||||
vehicle-state:
|
vehicle-state:
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import com.lingniu.ingest.sink.archive.config.SinkArchiveAutoConfiguration;
|
|||||||
import com.lingniu.ingest.sink.mq.KafkaEnvelopeDeadLetterSink;
|
import com.lingniu.ingest.sink.mq.KafkaEnvelopeDeadLetterSink;
|
||||||
import com.lingniu.ingest.sink.mq.KafkaEventSink;
|
import com.lingniu.ingest.sink.mq.KafkaEventSink;
|
||||||
import com.lingniu.ingest.sink.mq.SinkMqAutoConfiguration;
|
import com.lingniu.ingest.sink.mq.SinkMqAutoConfiguration;
|
||||||
|
import com.lingniu.ingest.tdenginehistory.TdengineHistorySchema;
|
||||||
|
import com.lingniu.ingest.tdenginehistory.config.TdengineHistoryAutoConfiguration;
|
||||||
import org.apache.kafka.clients.producer.KafkaProducer;
|
import org.apache.kafka.clients.producer.KafkaProducer;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.io.TempDir;
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
@@ -39,6 +41,7 @@ class VehicleHistoryAppCompositionTest {
|
|||||||
.withConfiguration(AutoConfigurations.of(
|
.withConfiguration(AutoConfigurations.of(
|
||||||
SinkArchiveAutoConfiguration.class,
|
SinkArchiveAutoConfiguration.class,
|
||||||
EventFileStoreAutoConfiguration.class,
|
EventFileStoreAutoConfiguration.class,
|
||||||
|
TdengineHistoryAutoConfiguration.class,
|
||||||
SinkMqAutoConfiguration.class,
|
SinkMqAutoConfiguration.class,
|
||||||
Gb32960AutoConfiguration.class,
|
Gb32960AutoConfiguration.class,
|
||||||
EventHistoryAutoConfiguration.class))
|
EventHistoryAutoConfiguration.class))
|
||||||
@@ -50,6 +53,8 @@ class VehicleHistoryAppCompositionTest {
|
|||||||
"lingniu.ingest.sink.archive.path=" + tempDir.resolve("archive"),
|
"lingniu.ingest.sink.archive.path=" + tempDir.resolve("archive"),
|
||||||
"lingniu.ingest.event-file-store.enabled=true",
|
"lingniu.ingest.event-file-store.enabled=true",
|
||||||
"lingniu.ingest.event-file-store.path=" + tempDir.resolve("event-store"),
|
"lingniu.ingest.event-file-store.path=" + tempDir.resolve("event-store"),
|
||||||
|
"lingniu.ingest.tdengine-history.enabled=true",
|
||||||
|
"lingniu.ingest.tdengine-history.database=vehicle_history_test",
|
||||||
"lingniu.ingest.event-history.enabled=true",
|
"lingniu.ingest.event-history.enabled=true",
|
||||||
"lingniu.ingest.gb32960.enabled=true",
|
"lingniu.ingest.gb32960.enabled=true",
|
||||||
"lingniu.ingest.gb32960.server.enabled=false",
|
"lingniu.ingest.gb32960.server.enabled=false",
|
||||||
@@ -64,6 +69,7 @@ class VehicleHistoryAppCompositionTest {
|
|||||||
assertThat(context).hasSingleBean(RawArchiveEventSink.class);
|
assertThat(context).hasSingleBean(RawArchiveEventSink.class);
|
||||||
assertThat(context).hasSingleBean(EventFileStore.class);
|
assertThat(context).hasSingleBean(EventFileStore.class);
|
||||||
assertThat(context).hasSingleBean(EventFileStoreSink.class);
|
assertThat(context).hasSingleBean(EventFileStoreSink.class);
|
||||||
|
assertThat(context).hasSingleBean(TdengineHistorySchema.class);
|
||||||
assertThat(context).hasSingleBean(KafkaEventSink.class);
|
assertThat(context).hasSingleBean(KafkaEventSink.class);
|
||||||
assertThat(context).hasSingleBean(KafkaEnvelopeDeadLetterSink.class);
|
assertThat(context).hasSingleBean(KafkaEnvelopeDeadLetterSink.class);
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ class VehicleHistoryAppDefaultsTest {
|
|||||||
.containsEntry("lingniu.ingest.gb32960.server.enabled", false)
|
.containsEntry("lingniu.ingest.gb32960.server.enabled", false)
|
||||||
.containsEntry("lingniu.ingest.sink.archive.enabled", "${SINK_ARCHIVE_ENABLED:true}")
|
.containsEntry("lingniu.ingest.sink.archive.enabled", "${SINK_ARCHIVE_ENABLED:true}")
|
||||||
.containsEntry("lingniu.ingest.event-file-store.enabled", "${EVENT_FILE_STORE_ENABLED:true}")
|
.containsEntry("lingniu.ingest.event-file-store.enabled", "${EVENT_FILE_STORE_ENABLED:true}")
|
||||||
|
.containsEntry("lingniu.ingest.tdengine-history.enabled", "${TDENGINE_HISTORY_ENABLED:false}")
|
||||||
|
.containsEntry("lingniu.ingest.tdengine-history.database", "${TDENGINE_HISTORY_DATABASE:vehicle_history}")
|
||||||
.containsEntry("lingniu.ingest.event-history.enabled", true)
|
.containsEntry("lingniu.ingest.event-history.enabled", true)
|
||||||
.containsEntry("lingniu.ingest.vehicle-state.enabled", false)
|
.containsEntry("lingniu.ingest.vehicle-state.enabled", false)
|
||||||
.containsEntry("lingniu.ingest.vehicle-stat.enabled", false)
|
.containsEntry("lingniu.ingest.vehicle-stat.enabled", false)
|
||||||
|
|||||||
52
modules/sinks/tdengine-history-store/pom.xml
Normal file
52
modules/sinks/tdengine-history-store/pom.xml
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>com.lingniu.ingest</groupId>
|
||||||
|
<artifactId>lingniu-vehicle-ingest</artifactId>
|
||||||
|
<version>0.1.0-SNAPSHOT</version>
|
||||||
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
|
</parent>
|
||||||
|
<artifactId>tdengine-history-store</artifactId>
|
||||||
|
<name>tdengine-history-store</name>
|
||||||
|
<description>TDengine-backed hot history store schema and Kafka envelope mapping.</description>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.lingniu.ingest</groupId>
|
||||||
|
<artifactId>sink-mq</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-autoconfigure</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.assertj</groupId>
|
||||||
|
<artifactId>assertj-core</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.lingniu.ingest.tdenginehistory;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
public record TdengineBatchStatement(
|
||||||
|
String createChildTableSql,
|
||||||
|
String insertSql,
|
||||||
|
List<Object> values
|
||||||
|
) {
|
||||||
|
public TdengineBatchStatement {
|
||||||
|
values = values == null ? List.of() : Collections.unmodifiableList(new ArrayList<>(values));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package com.lingniu.ingest.tdenginehistory;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.lingniu.ingest.sink.mq.proto.LocationPayload;
|
||||||
|
import com.lingniu.ingest.sink.mq.proto.RawFrameFactPayload;
|
||||||
|
import com.lingniu.ingest.sink.mq.proto.VehicleEnvelope;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public final class TdengineEnvelopeRows {
|
||||||
|
|
||||||
|
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||||
|
|
||||||
|
private TdengineEnvelopeRows() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Optional<TdengineRawFrameRow> rawFrame(VehicleEnvelope envelope) {
|
||||||
|
if (envelope == null || !envelope.hasRawFrameFact()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
RawFrameFactPayload raw = envelope.getRawFrameFact();
|
||||||
|
Map<String, String> metadata = new LinkedHashMap<>();
|
||||||
|
metadata.putAll(envelope.getMetadataMap());
|
||||||
|
metadata.putAll(raw.getMetadataMap());
|
||||||
|
return Optional.of(new TdengineRawFrameRow(
|
||||||
|
instant(envelope.getEventTimeMs()),
|
||||||
|
raw.getFrameId(),
|
||||||
|
instant(envelope.getIngestTimeMs()),
|
||||||
|
raw.getMessageId(),
|
||||||
|
raw.getSubType(),
|
||||||
|
instant(envelope.getEventTimeMs()),
|
||||||
|
raw.getRawUri(),
|
||||||
|
raw.getChecksum(),
|
||||||
|
raw.getRawSizeBytes(),
|
||||||
|
raw.getParseStatus().name().replace("PARSE_STATUS_", ""),
|
||||||
|
raw.getParseError(),
|
||||||
|
raw.getPeer(),
|
||||||
|
json(metadata),
|
||||||
|
protocol(envelope),
|
||||||
|
firstNonBlank(raw.getVehicleKey(), envelope.getMetadataOrDefault("vehicle_key", ""), envelope.getVin()),
|
||||||
|
firstNonBlank(raw.getVin(), envelope.getVin()),
|
||||||
|
raw.getPhone()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Optional<TdengineLocationRow> location(VehicleEnvelope envelope) {
|
||||||
|
if (envelope == null || !envelope.hasLocation()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
LocationPayload location = envelope.getLocation();
|
||||||
|
String rawUri = envelope.hasRawArchive() ? envelope.getRawArchive().getUri() : "";
|
||||||
|
String frameId = envelope.getMetadataOrDefault("frame_id", envelope.getEventId());
|
||||||
|
return Optional.of(new TdengineLocationRow(
|
||||||
|
instant(envelope.getEventTimeMs()),
|
||||||
|
envelope.getEventId(),
|
||||||
|
frameId,
|
||||||
|
instant(envelope.getIngestTimeMs()),
|
||||||
|
location.getLongitude(),
|
||||||
|
location.getLatitude(),
|
||||||
|
location.getAltitudeM(),
|
||||||
|
location.getSpeedKmh(),
|
||||||
|
location.getDirectionDeg(),
|
||||||
|
location.getAlarmFlag(),
|
||||||
|
location.getStatusFlag(),
|
||||||
|
totalMileage(envelope),
|
||||||
|
rawUri,
|
||||||
|
json(envelope.getMetadataMap()),
|
||||||
|
protocol(envelope),
|
||||||
|
firstNonBlank(envelope.getMetadataOrDefault("vehicle_key", ""), envelope.getVin()),
|
||||||
|
envelope.getVin(),
|
||||||
|
envelope.getMetadataOrDefault("phone", "")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Instant instant(long epochMillis) {
|
||||||
|
return Instant.ofEpochMilli(epochMillis);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String protocol(VehicleEnvelope envelope) {
|
||||||
|
return firstNonBlank(envelope.getSource(), "UNKNOWN");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Double totalMileage(VehicleEnvelope envelope) {
|
||||||
|
String value = envelope.getMetadataOrDefault("total_mileage_km", "");
|
||||||
|
if (value.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Double.parseDouble(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String json(Map<String, String> metadata) {
|
||||||
|
try {
|
||||||
|
return OBJECT_MAPPER.writeValueAsString(metadata == null ? Map.of() : metadata);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
throw new IllegalArgumentException("metadata cannot be serialized as json", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String firstNonBlank(String... values) {
|
||||||
|
for (String value : values) {
|
||||||
|
if (value != null && !value.isBlank()) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package com.lingniu.ingest.tdenginehistory;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SQL model for the TDengine hot history store.
|
||||||
|
*/
|
||||||
|
public final class TdengineHistorySchema {
|
||||||
|
|
||||||
|
private final String database;
|
||||||
|
|
||||||
|
public TdengineHistorySchema(String database) {
|
||||||
|
this.database = TdengineIdentifier.database(database);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> bootstrapSql() {
|
||||||
|
return List.of(
|
||||||
|
"CREATE DATABASE IF NOT EXISTS " + database + " PRECISION 'ms'",
|
||||||
|
"USE " + database,
|
||||||
|
rawFramesStableSql(),
|
||||||
|
vehicleLocationsStableSql()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String rawFrameTable(String protocol, String vehicleKey) {
|
||||||
|
return "raw_" + TdengineIdentifier.fragment(protocol) + "_" + TdengineIdentifier.hash16(vehicleKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String locationTable(String protocol, String vehicleKey) {
|
||||||
|
return "loc_" + TdengineIdentifier.fragment(protocol) + "_" + TdengineIdentifier.hash16(vehicleKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String rawFramesStableSql() {
|
||||||
|
return """
|
||||||
|
CREATE STABLE IF NOT EXISTS raw_frames (
|
||||||
|
ts TIMESTAMP,
|
||||||
|
frame_id NCHAR(64),
|
||||||
|
received_at TIMESTAMP,
|
||||||
|
message_id INT,
|
||||||
|
sub_type INT,
|
||||||
|
event_time TIMESTAMP,
|
||||||
|
raw_uri NCHAR(512),
|
||||||
|
checksum NCHAR(128),
|
||||||
|
raw_size_bytes BIGINT,
|
||||||
|
parse_status NCHAR(16),
|
||||||
|
parse_error NCHAR(512),
|
||||||
|
peer NCHAR(128),
|
||||||
|
metadata_json NCHAR(4096)
|
||||||
|
) TAGS (
|
||||||
|
protocol NCHAR(16),
|
||||||
|
vehicle_key NCHAR(128),
|
||||||
|
vin NCHAR(64),
|
||||||
|
phone NCHAR(32)
|
||||||
|
)""";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String vehicleLocationsStableSql() {
|
||||||
|
return """
|
||||||
|
CREATE STABLE IF NOT EXISTS vehicle_locations (
|
||||||
|
ts TIMESTAMP,
|
||||||
|
fact_id NCHAR(64),
|
||||||
|
frame_id NCHAR(64),
|
||||||
|
received_at TIMESTAMP,
|
||||||
|
longitude DOUBLE,
|
||||||
|
latitude DOUBLE,
|
||||||
|
altitude_m DOUBLE,
|
||||||
|
speed_kmh DOUBLE,
|
||||||
|
direction_deg DOUBLE,
|
||||||
|
alarm_flag BIGINT,
|
||||||
|
status_flag BIGINT,
|
||||||
|
total_mileage_km DOUBLE,
|
||||||
|
raw_uri NCHAR(512),
|
||||||
|
metadata_json NCHAR(4096)
|
||||||
|
) TAGS (
|
||||||
|
protocol NCHAR(16),
|
||||||
|
vehicle_key NCHAR(128),
|
||||||
|
vin NCHAR(64),
|
||||||
|
phone NCHAR(32)
|
||||||
|
)""";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package com.lingniu.ingest.tdenginehistory;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public final class TdengineHistoryStatements {
|
||||||
|
|
||||||
|
private static final String RAW_FRAME_COLUMNS = "ts, frame_id, received_at, message_id, sub_type, event_time, "
|
||||||
|
+ "raw_uri, checksum, raw_size_bytes, parse_status, parse_error, peer, metadata_json";
|
||||||
|
private static final String RAW_FRAME_PLACEHOLDERS = "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?";
|
||||||
|
|
||||||
|
private static final String LOCATION_COLUMNS = "ts, fact_id, frame_id, received_at, longitude, latitude, "
|
||||||
|
+ "altitude_m, speed_kmh, direction_deg, alarm_flag, status_flag, total_mileage_km, raw_uri, metadata_json";
|
||||||
|
private static final String LOCATION_PLACEHOLDERS = "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?";
|
||||||
|
|
||||||
|
private final TdengineHistorySchema schema;
|
||||||
|
|
||||||
|
public TdengineHistoryStatements(TdengineHistorySchema schema) {
|
||||||
|
if (schema == null) {
|
||||||
|
throw new IllegalArgumentException("schema must not be null");
|
||||||
|
}
|
||||||
|
this.schema = schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TdengineBatchStatement rawFrame(TdengineRawFrameRow row) {
|
||||||
|
String table = schema.rawFrameTable(row.protocol(), row.vehicleKey());
|
||||||
|
return new TdengineBatchStatement(
|
||||||
|
"CREATE TABLE IF NOT EXISTS " + table + " USING raw_frames TAGS ("
|
||||||
|
+ tags(row.protocol(), row.vehicleKey(), row.vin(), row.phone()) + ")",
|
||||||
|
"INSERT INTO " + table + " (" + RAW_FRAME_COLUMNS + ") VALUES (" + RAW_FRAME_PLACEHOLDERS + ")",
|
||||||
|
values(
|
||||||
|
row.ts(), row.frameId(), row.receivedAt(), row.messageId(), row.subType(),
|
||||||
|
row.eventTime(), row.rawUri(), row.checksum(), row.rawSizeBytes(), row.parseStatus(),
|
||||||
|
row.parseError(), row.peer(), row.metadataJson()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TdengineBatchStatement location(TdengineLocationRow row) {
|
||||||
|
String table = schema.locationTable(row.protocol(), row.vehicleKey());
|
||||||
|
return new TdengineBatchStatement(
|
||||||
|
"CREATE TABLE IF NOT EXISTS " + table + " USING vehicle_locations TAGS ("
|
||||||
|
+ tags(row.protocol(), row.vehicleKey(), row.vin(), row.phone()) + ")",
|
||||||
|
"INSERT INTO " + table + " (" + LOCATION_COLUMNS + ") VALUES (" + LOCATION_PLACEHOLDERS + ")",
|
||||||
|
values(
|
||||||
|
row.ts(), row.factId(), row.frameId(), row.receivedAt(), row.longitude(), row.latitude(),
|
||||||
|
row.altitudeM(), row.speedKmh(), row.directionDeg(), row.alarmFlag(), row.statusFlag(),
|
||||||
|
row.totalMileageKm(), row.rawUri(), row.metadataJson()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String tags(String protocol, String vehicleKey, String vin, String phone) {
|
||||||
|
return quote(protocol) + ", " + quote(vehicleKey) + ", " + quote(vin) + ", " + quote(phone);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String quote(String value) {
|
||||||
|
return "'" + (value == null ? "" : value.replace("'", "''")) + "'";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Object> values(Object... values) {
|
||||||
|
return Arrays.asList(values);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.lingniu.ingest.tdenginehistory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface TdengineHistoryWriter {
|
||||||
|
|
||||||
|
default void appendRawFrame(TdengineRawFrameRow row) throws IOException {
|
||||||
|
appendRawFrames(List.of(row));
|
||||||
|
}
|
||||||
|
|
||||||
|
void appendRawFrames(List<TdengineRawFrameRow> rows) throws IOException;
|
||||||
|
|
||||||
|
default void appendLocation(TdengineLocationRow row) throws IOException {
|
||||||
|
appendLocations(List.of(row));
|
||||||
|
}
|
||||||
|
|
||||||
|
void appendLocations(List<TdengineLocationRow> rows) throws IOException;
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.lingniu.ingest.tdenginehistory;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.HexFormat;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
final class TdengineIdentifier {
|
||||||
|
|
||||||
|
private TdengineIdentifier() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static String database(String value) {
|
||||||
|
String fragment = fragment(value);
|
||||||
|
if (fragment.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("database must contain identifier characters");
|
||||||
|
}
|
||||||
|
return fragment;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String fragment(String value) {
|
||||||
|
if (value == null) {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
String normalized = value.trim().toLowerCase(Locale.ROOT)
|
||||||
|
.replaceAll("[^a-z0-9_]+", "_")
|
||||||
|
.replaceAll("_+", "_")
|
||||||
|
.replaceAll("^_|_$", "");
|
||||||
|
return normalized.isBlank() ? "unknown" : normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String hash16(String value) {
|
||||||
|
try {
|
||||||
|
byte[] digest = MessageDigest.getInstance("SHA-256")
|
||||||
|
.digest(String.valueOf(value).getBytes(StandardCharsets.UTF_8));
|
||||||
|
return HexFormat.of().formatHex(digest, 0, 8);
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new IllegalStateException("SHA-256 is not available", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.lingniu.ingest.tdenginehistory;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
public record TdengineLocationRow(
|
||||||
|
Instant ts,
|
||||||
|
String factId,
|
||||||
|
String frameId,
|
||||||
|
Instant receivedAt,
|
||||||
|
double longitude,
|
||||||
|
double latitude,
|
||||||
|
double altitudeM,
|
||||||
|
double speedKmh,
|
||||||
|
double directionDeg,
|
||||||
|
long alarmFlag,
|
||||||
|
long statusFlag,
|
||||||
|
Double totalMileageKm,
|
||||||
|
String rawUri,
|
||||||
|
String metadataJson,
|
||||||
|
String protocol,
|
||||||
|
String vehicleKey,
|
||||||
|
String vin,
|
||||||
|
String phone
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.lingniu.ingest.tdenginehistory;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
public record TdengineRawFrameRow(
|
||||||
|
Instant ts,
|
||||||
|
String frameId,
|
||||||
|
Instant receivedAt,
|
||||||
|
int messageId,
|
||||||
|
int subType,
|
||||||
|
Instant eventTime,
|
||||||
|
String rawUri,
|
||||||
|
String checksum,
|
||||||
|
long rawSizeBytes,
|
||||||
|
String parseStatus,
|
||||||
|
String parseError,
|
||||||
|
String peer,
|
||||||
|
String metadataJson,
|
||||||
|
String protocol,
|
||||||
|
String vehicleKey,
|
||||||
|
String vin,
|
||||||
|
String phone
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.lingniu.ingest.tdenginehistory.config;
|
||||||
|
|
||||||
|
import com.lingniu.ingest.tdenginehistory.TdengineHistorySchema;
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
|
||||||
|
@AutoConfiguration
|
||||||
|
@EnableConfigurationProperties(TdengineHistoryProperties.class)
|
||||||
|
public class TdengineHistoryAutoConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
@ConditionalOnProperty(
|
||||||
|
prefix = "lingniu.ingest.tdengine-history",
|
||||||
|
name = "enabled",
|
||||||
|
havingValue = "true")
|
||||||
|
public TdengineHistorySchema tdengineHistorySchema(TdengineHistoryProperties properties) {
|
||||||
|
return new TdengineHistorySchema(properties.getDatabase());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.lingniu.ingest.tdenginehistory.config;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
|
||||||
|
@ConfigurationProperties(prefix = "lingniu.ingest.tdengine-history")
|
||||||
|
public class TdengineHistoryProperties {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认关闭,避免未配置 TDengine 连接时影响现有历史查询服务启动。
|
||||||
|
*/
|
||||||
|
private boolean enabled = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TDengine 历史库名。
|
||||||
|
*/
|
||||||
|
private String database = "vehicle_history";
|
||||||
|
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEnabled(boolean enabled) {
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDatabase() {
|
||||||
|
return database;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDatabase(String database) {
|
||||||
|
this.database = database;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
com.lingniu.ingest.tdenginehistory.config.TdengineHistoryAutoConfiguration
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package com.lingniu.ingest.tdenginehistory;
|
||||||
|
|
||||||
|
import com.lingniu.ingest.sink.mq.proto.LocationPayload;
|
||||||
|
import com.lingniu.ingest.sink.mq.proto.ParseStatusProto;
|
||||||
|
import com.lingniu.ingest.sink.mq.proto.RawArchiveRef;
|
||||||
|
import com.lingniu.ingest.sink.mq.proto.RawFrameFactPayload;
|
||||||
|
import com.lingniu.ingest.sink.mq.proto.VehicleEnvelope;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class TdengineEnvelopeRowsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void mapsRawFrameFactEnvelopeToRawFrameRow() {
|
||||||
|
VehicleEnvelope envelope = VehicleEnvelope.newBuilder()
|
||||||
|
.setEventId("evt-raw-1")
|
||||||
|
.setVin("VIN123")
|
||||||
|
.setSource("JT808")
|
||||||
|
.setEventTimeMs(1_772_000_001_234L)
|
||||||
|
.setIngestTimeMs(1_772_000_001_999L)
|
||||||
|
.putMetadata("channel", "tcp-808")
|
||||||
|
.setRawFrameFact(RawFrameFactPayload.newBuilder()
|
||||||
|
.setFrameId("frame-1")
|
||||||
|
.setVehicleKey("jt808:g7gps")
|
||||||
|
.setPhone("013800000000")
|
||||||
|
.setMessageId(0x0200)
|
||||||
|
.setSubType(0)
|
||||||
|
.setRawUri("archive://jt808/2026/06/29/frame-1.bin")
|
||||||
|
.setChecksum("sha256:abc")
|
||||||
|
.setRawSizeBytes(67)
|
||||||
|
.setParseStatus(ParseStatusProto.PARSE_STATUS_SUCCEEDED)
|
||||||
|
.setPeer("10.0.0.8:30001")
|
||||||
|
.putMetadata("auth", "passed"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
TdengineRawFrameRow row = TdengineEnvelopeRows.rawFrame(envelope).orElseThrow();
|
||||||
|
|
||||||
|
assertThat(row.ts()).isEqualTo(Instant.ofEpochMilli(1_772_000_001_234L));
|
||||||
|
assertThat(row.receivedAt()).isEqualTo(Instant.ofEpochMilli(1_772_000_001_999L));
|
||||||
|
assertThat(row.protocol()).isEqualTo("JT808");
|
||||||
|
assertThat(row.vehicleKey()).isEqualTo("jt808:g7gps");
|
||||||
|
assertThat(row.vin()).isEqualTo("VIN123");
|
||||||
|
assertThat(row.phone()).isEqualTo("013800000000");
|
||||||
|
assertThat(row.messageId()).isEqualTo(0x0200);
|
||||||
|
assertThat(row.rawUri()).isEqualTo("archive://jt808/2026/06/29/frame-1.bin");
|
||||||
|
assertThat(row.metadataJson()).contains("\"channel\":\"tcp-808\"", "\"auth\":\"passed\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void mapsLocationEnvelopeToVehicleLocationRow() {
|
||||||
|
VehicleEnvelope envelope = VehicleEnvelope.newBuilder()
|
||||||
|
.setEventId("evt-location-1")
|
||||||
|
.setVin("VIN32960")
|
||||||
|
.setSource("GB32960")
|
||||||
|
.setEventTimeMs(1_772_000_002_000L)
|
||||||
|
.setIngestTimeMs(1_772_000_002_321L)
|
||||||
|
.putMetadata("vehicle_key", "vin:VIN32960")
|
||||||
|
.putMetadata("frame_id", "frame-location-1")
|
||||||
|
.setRawArchive(RawArchiveRef.newBuilder()
|
||||||
|
.setUri("archive://gb32960/2026/06/29/frame-location-1.bin")
|
||||||
|
.setChecksum("sha256:def")
|
||||||
|
.setSizeBytes(98))
|
||||||
|
.setLocation(LocationPayload.newBuilder()
|
||||||
|
.setLongitude(113.12)
|
||||||
|
.setLatitude(23.45)
|
||||||
|
.setAltitudeM(8.0)
|
||||||
|
.setSpeedKmh(42.5)
|
||||||
|
.setDirectionDeg(91.0)
|
||||||
|
.setAlarmFlag(1)
|
||||||
|
.setStatusFlag(3))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
TdengineLocationRow row = TdengineEnvelopeRows.location(envelope).orElseThrow();
|
||||||
|
|
||||||
|
assertThat(row.ts()).isEqualTo(Instant.ofEpochMilli(1_772_000_002_000L));
|
||||||
|
assertThat(row.factId()).isEqualTo("evt-location-1");
|
||||||
|
assertThat(row.frameId()).isEqualTo("frame-location-1");
|
||||||
|
assertThat(row.protocol()).isEqualTo("GB32960");
|
||||||
|
assertThat(row.vehicleKey()).isEqualTo("vin:VIN32960");
|
||||||
|
assertThat(row.longitude()).isEqualTo(113.12);
|
||||||
|
assertThat(row.latitude()).isEqualTo(23.45);
|
||||||
|
assertThat(row.speedKmh()).isEqualTo(42.5);
|
||||||
|
assertThat(row.totalMileageKm()).isNull();
|
||||||
|
assertThat(row.rawUri()).isEqualTo("archive://gb32960/2026/06/29/frame-location-1.bin");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.lingniu.ingest.tdenginehistory;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class TdengineHistorySchemaTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void bootstrapSqlCreatesRawAndLocationStablesFromSpec() {
|
||||||
|
TdengineHistorySchema schema = new TdengineHistorySchema("vehicle_history");
|
||||||
|
|
||||||
|
assertThat(schema.bootstrapSql())
|
||||||
|
.first()
|
||||||
|
.isEqualTo("CREATE DATABASE IF NOT EXISTS vehicle_history PRECISION 'ms'");
|
||||||
|
assertThat(schema.bootstrapSql())
|
||||||
|
.contains("USE vehicle_history");
|
||||||
|
assertThat(schema.bootstrapSql().get(2))
|
||||||
|
.contains("CREATE STABLE IF NOT EXISTS raw_frames")
|
||||||
|
.contains("frame_id NCHAR(64)")
|
||||||
|
.contains("raw_uri NCHAR(512)")
|
||||||
|
.contains("metadata_json NCHAR(4096)")
|
||||||
|
.contains("TAGS (")
|
||||||
|
.contains("protocol NCHAR(16)")
|
||||||
|
.contains("vehicle_key NCHAR(128)")
|
||||||
|
.contains("phone NCHAR(32)");
|
||||||
|
assertThat(schema.bootstrapSql().get(3))
|
||||||
|
.contains("CREATE STABLE IF NOT EXISTS vehicle_locations")
|
||||||
|
.contains("longitude DOUBLE")
|
||||||
|
.contains("total_mileage_km DOUBLE")
|
||||||
|
.contains("raw_uri NCHAR(512)");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void childTableNamesAreStableAndSanitized() {
|
||||||
|
TdengineHistorySchema schema = new TdengineHistorySchema("vehicle_history");
|
||||||
|
|
||||||
|
assertThat(schema.rawFrameTable("JT808", "jt808:g7gps/013800000000"))
|
||||||
|
.matches("raw_jt808_[0-9a-f]{16}");
|
||||||
|
assertThat(schema.locationTable("GB32960", "VIN WITH SPACE"))
|
||||||
|
.matches("loc_gb32960_[0-9a-f]{16}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package com.lingniu.ingest.tdenginehistory;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class TdengineHistoryStatementsTest {
|
||||||
|
|
||||||
|
private final TdengineHistorySchema schema = new TdengineHistorySchema("vehicle_history");
|
||||||
|
private final TdengineHistoryStatements statements = new TdengineHistoryStatements(schema);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rawFrameBatchStatementCreatesChildTableAndParameterizedInsert() {
|
||||||
|
TdengineRawFrameRow row = new TdengineRawFrameRow(
|
||||||
|
Instant.parse("2026-06-29T05:00:01Z"),
|
||||||
|
"frame-1",
|
||||||
|
Instant.parse("2026-06-29T05:00:02Z"),
|
||||||
|
0x0200,
|
||||||
|
0,
|
||||||
|
Instant.parse("2026-06-29T05:00:01Z"),
|
||||||
|
"archive://jt808/frame-1.bin",
|
||||||
|
"sha256:abc",
|
||||||
|
67,
|
||||||
|
"SUCCEEDED",
|
||||||
|
"",
|
||||||
|
"10.0.0.1:808",
|
||||||
|
"{\"auth\":\"passed\"}",
|
||||||
|
"JT808",
|
||||||
|
"jt808:g7gps",
|
||||||
|
"VIN123",
|
||||||
|
"013800000000");
|
||||||
|
|
||||||
|
TdengineBatchStatement batch = statements.rawFrame(row);
|
||||||
|
|
||||||
|
assertThat(batch.createChildTableSql())
|
||||||
|
.startsWith("CREATE TABLE IF NOT EXISTS raw_jt808_")
|
||||||
|
.contains(" USING raw_frames TAGS ('JT808', 'jt808:g7gps', 'VIN123', '013800000000')");
|
||||||
|
assertThat(batch.insertSql())
|
||||||
|
.startsWith("INSERT INTO raw_jt808_")
|
||||||
|
.contains("(ts, frame_id, received_at, message_id, sub_type, event_time, raw_uri, checksum, raw_size_bytes, parse_status, parse_error, peer, metadata_json)")
|
||||||
|
.endsWith("VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||||
|
assertThat(batch.values())
|
||||||
|
.containsExactly(
|
||||||
|
row.ts(), row.frameId(), row.receivedAt(), row.messageId(), row.subType(),
|
||||||
|
row.eventTime(), row.rawUri(), row.checksum(), row.rawSizeBytes(), row.parseStatus(),
|
||||||
|
row.parseError(), row.peer(), row.metadataJson());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void locationBatchStatementEscapesTags() {
|
||||||
|
TdengineLocationRow row = new TdengineLocationRow(
|
||||||
|
Instant.parse("2026-06-29T05:00:01Z"),
|
||||||
|
"fact-1",
|
||||||
|
"frame-1",
|
||||||
|
Instant.parse("2026-06-29T05:00:02Z"),
|
||||||
|
113.1,
|
||||||
|
23.4,
|
||||||
|
8.0,
|
||||||
|
42.0,
|
||||||
|
90.0,
|
||||||
|
1,
|
||||||
|
3,
|
||||||
|
null,
|
||||||
|
"archive://gb32960/frame-1.bin",
|
||||||
|
"{}",
|
||||||
|
"GB32960",
|
||||||
|
"vin:O'HARE",
|
||||||
|
"O'HARE",
|
||||||
|
"");
|
||||||
|
|
||||||
|
TdengineBatchStatement batch = statements.location(row);
|
||||||
|
|
||||||
|
assertThat(batch.createChildTableSql())
|
||||||
|
.startsWith("CREATE TABLE IF NOT EXISTS loc_gb32960_")
|
||||||
|
.contains(" USING vehicle_locations TAGS ('GB32960', 'vin:O''HARE', 'O''HARE', '')");
|
||||||
|
assertThat(batch.values())
|
||||||
|
.containsExactly(
|
||||||
|
row.ts(), row.factId(), row.frameId(), row.receivedAt(), row.longitude(), row.latitude(),
|
||||||
|
row.altitudeM(), row.speedKmh(), row.directionDeg(), row.alarmFlag(), row.statusFlag(),
|
||||||
|
row.totalMileageKm(), row.rawUri(), row.metadataJson());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.lingniu.ingest.tdenginehistory.config;
|
||||||
|
|
||||||
|
import com.lingniu.ingest.tdenginehistory.TdengineHistorySchema;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||||
|
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class TdengineHistoryAutoConfigurationTest {
|
||||||
|
|
||||||
|
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||||
|
.withConfiguration(AutoConfigurations.of(TdengineHistoryAutoConfiguration.class));
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void staysOffByDefault() {
|
||||||
|
contextRunner.run(context -> {
|
||||||
|
assertThat(context).doesNotHaveBean(TdengineHistorySchema.class);
|
||||||
|
assertThat(context.getBean(TdengineHistoryProperties.class).isEnabled()).isFalse();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createsSchemaWhenEnabled() {
|
||||||
|
contextRunner
|
||||||
|
.withPropertyValues(
|
||||||
|
"lingniu.ingest.tdengine-history.enabled=true",
|
||||||
|
"lingniu.ingest.tdengine-history.database=vehicle_history_hot")
|
||||||
|
.run(context -> {
|
||||||
|
assertThat(context).hasSingleBean(TdengineHistorySchema.class);
|
||||||
|
assertThat(context.getBean(TdengineHistorySchema.class).bootstrapSql().getFirst())
|
||||||
|
.isEqualTo("CREATE DATABASE IF NOT EXISTS vehicle_history_hot PRECISION 'ms'");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
6
pom.xml
6
pom.xml
@@ -28,6 +28,7 @@
|
|||||||
<module>modules/sinks/sink-archive</module>
|
<module>modules/sinks/sink-archive</module>
|
||||||
<module>modules/sinks/raw-archive-store</module>
|
<module>modules/sinks/raw-archive-store</module>
|
||||||
<module>modules/sinks/event-file-store</module>
|
<module>modules/sinks/event-file-store</module>
|
||||||
|
<module>modules/sinks/tdengine-history-store</module>
|
||||||
<module>modules/services/event-history-service</module>
|
<module>modules/services/event-history-service</module>
|
||||||
<module>modules/services/vehicle-state-service</module>
|
<module>modules/services/vehicle-state-service</module>
|
||||||
<module>modules/services/vehicle-stat-service</module>
|
<module>modules/services/vehicle-stat-service</module>
|
||||||
@@ -195,6 +196,11 @@
|
|||||||
<artifactId>event-file-store</artifactId>
|
<artifactId>event-file-store</artifactId>
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.lingniu.ingest</groupId>
|
||||||
|
<artifactId>tdengine-history-store</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.lingniu.ingest</groupId>
|
<groupId>com.lingniu.ingest</groupId>
|
||||||
<artifactId>event-history-service</artifactId>
|
<artifactId>event-history-service</artifactId>
|
||||||
|
|||||||
Reference in New Issue
Block a user