refactor: store runtime identity and mileage in mysql metrics

This commit is contained in:
lingniu
2026-07-01 01:56:16 +08:00
parent b1190cd7c7
commit 0fc91f512c
23 changed files with 75 additions and 132 deletions

View File

@@ -37,7 +37,7 @@ lingniu-vehicle-ingest/
│ │ ├── ingest-codec-common/ 公共编解码工具BCD/CRC/BCC/bit utils
│ │ ├── ingest-core/ Pipeline / Dispatcher / Disruptor / Session 桥
│ │ ├── session-core/ 设备会话 + 鉴权 + Token + Redis/Memory SessionStore
│ │ ├── vehicle-identity/ 跨协议车辆身份解析 + 外部标识绑定memory/file/mysql
│ │ ├── vehicle-identity/ 跨协议车辆身份解析 + MySQL 外部标识绑定
│ │ └── observability/ metrics / tracing / health
│ ├── protocols/
│ │ ├── protocol-gb32960/ GB/T 32960

View File

@@ -110,7 +110,6 @@ Vehicle analytics:
```bash
KAFKA_BROKERS=127.0.0.1:9092 \
HTTP_PORT=20300 \
VEHICLE_STAT_FILE_PATH=./target/split-vehicle-stat \
java --sun-misc-unsafe-memory-access=allow \
-jar modules/apps/vehicle-analytics-app/target/vehicle-analytics-app.jar
```

View File

@@ -2,11 +2,11 @@
## Scope
The analytics app can calculate daily mileage from JT808 telemetry only. It consumes `vehicle.event.jt808.v1`, keeps the rolling per-vehicle daily state in Redis or memory, and saves the current daily result into the common `vehicle-stat` metric repository.
The analytics app can calculate daily mileage from JT808 telemetry only. It consumes `vehicle.event.jt808.v1`, keeps the rolling per-vehicle daily state in Redis, and saves the current daily result into the common `vehicle-stat` metric repository.
## Storage
There is no separate JT808 daily-mileage table. Runtime state is stored under the configured JT808 state store so the stream can continue after restart. The derived metric is written through `VehicleStatRepository.saveDailyStat(...)`; production uses the common JDBC/MySQL metric table `vehicle_stat_metric`. Local development can force the file fallback with `VEHICLE_STAT_REPOSITORY_TYPE=file`, which writes `daily-stats.tsv` under `VEHICLE_STAT_FILE_PATH`.
There is no separate JT808 daily-mileage table. Runtime state is stored under the configured JT808 state store so the stream can continue after restart. The derived metric is written through `VehicleStatRepository.saveDailyStat(...)` into the common JDBC/MySQL metric table `vehicle_stat_metric`.
The JT808 daily-mileage value is calculated from the GPS total mileage reported in location additional information:
@@ -23,7 +23,6 @@ Set these in Portainer or Nacos, without committing secrets:
```text
KAFKA_TOPIC_JT808_EVENT=vehicle.event.jt808.v1
VEHICLE_STAT_JT808_MILEAGE_ENABLED=true
VEHICLE_STAT_REPOSITORY_TYPE=jdbc
VEHICLE_STAT_JT808_STATE_STORE=redis
MYSQL_JDBC_URL=<jdbc-url>
MYSQL_USERNAME=<user>

View File

@@ -118,7 +118,7 @@ deploy/local/launchctl/
JT808 注册身份绑定:
- 默认可以使用 `VEHICLE_IDENTITY_STORE=file``memory`
- 生产默认使用 `VEHICLE_IDENTITY_STORE=mysql`,不再使用 file/memory 作为运行时身份绑定仓储
- 生产需要把 0x0100 注册信息维护到 MySQL 时,设置 `VEHICLE_IDENTITY_STORE=mysql`
- 需要提供 `VEHICLE_IDENTITY_MYSQL_JDBC_URL``VEHICLE_IDENTITY_MYSQL_USERNAME``VEHICLE_IDENTITY_MYSQL_PASSWORD`
- 服务启动时自动创建 `vehicle_identity_binding``vehicle_identity_binding_registration` 两张表。`vehicle_identity_binding` 只维护 `plate``vin` 两列;`vehicle_identity_binding_registration``protocol + phone` 保存 JT808 0x0100 注册字段。
@@ -263,7 +263,7 @@ JT808 真实转发链路已验证:
- `jt808_locations` 中位置行数已超过 `8783`
- 在旧的字段宽表开启模式下,`telemetry_fields``protocol='JT808'` 的字段行数曾超过 `15000`;当前高吞吐默认不要求该表持续增长。
- 分页 API 已用终端号 `13079963301` 验证第一页和 nextCursor 第二页,样例位置包含 `longitude=118.913846``latitude=31.927309``statusFlag=3``archive://2026/06/29/JT808/...` 原始帧引用。
- 注册帧样例:终端号 `13079963320`、deviceId `9963320`、deviceType `SEG-9888G`、plate `沪A61559F`、maker `70112`、province `31`、city `113`、plateColor `2`当前本机 live 进程仍使用早期生成的 `memory` 身份存储,正式行里 `vin` 仍为 `unknown`;模板已改为 `file`生产 VIN 反写仍需要 MySQL identity store。
- 注册帧样例:终端号 `13079963320`、deviceId `9963320`、deviceType `SEG-9888G`、plate `沪A61559F`、maker `70112`、province `31`、city `113`、plateColor `2`。生产 VIN 反写依赖 MySQL identity store,后续 raw/event 的 VIN 解析也从 MySQL 绑定读取
## JT808 本机 smoke / 轻压测工具

View File

@@ -25,6 +25,10 @@
<groupId>com.lingniu.ingest</groupId>
<artifactId>observability</artifactId>
</dependency>
<dependency>
<groupId>com.lingniu.ingest</groupId>
<artifactId>vehicle-identity</artifactId>
</dependency>
<dependency>
<groupId>com.lingniu.ingest</groupId>
<artifactId>protocol-gb32960</artifactId>

View File

@@ -78,9 +78,13 @@ lingniu:
store: ${SESSION_STORE:memory}
ttl: ${SESSION_TTL:30m}
identity:
store: ${VEHICLE_IDENTITY_STORE:file}
file:
path: ${VEHICLE_IDENTITY_FILE:./data/vehicle-identity.jsonl}
store: ${VEHICLE_IDENTITY_STORE:mysql}
mysql:
jdbc-url: ${VEHICLE_IDENTITY_MYSQL_JDBC_URL:jdbc:mysql://127.0.0.1:3306/lingniu_vehicle?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai}
username: ${VEHICLE_IDENTITY_MYSQL_USERNAME:root}
password: ${VEHICLE_IDENTITY_MYSQL_PASSWORD:}
table-name: ${VEHICLE_IDENTITY_MYSQL_TABLE:vehicle_identity_bindings}
initialize-schema: ${VEHICLE_IDENTITY_MYSQL_INITIALIZE_SCHEMA:true}
sink:
mq:
enabled: ${KAFKA_ENABLED:true}

View File

@@ -1,6 +1,8 @@
package com.lingniu.ingest.gb32960app;
import com.lingniu.ingest.core.config.IngestCoreAutoConfiguration;
import com.lingniu.ingest.identity.MySqlVehicleIdentityService;
import com.lingniu.ingest.identity.config.VehicleIdentityAutoConfiguration;
import com.lingniu.ingest.protocol.gb32960.codec.Gb32960MessageDecoder;
import com.lingniu.ingest.protocol.gb32960.config.Gb32960AutoConfiguration;
import com.lingniu.ingest.protocol.gb32960.inbound.Gb32960NettyServer;
@@ -27,6 +29,7 @@ class Gb32960IngestAppCompositionTest {
.withConfiguration(AutoConfigurations.of(
IngestCoreAutoConfiguration.class,
SessionCoreAutoConfiguration.class,
VehicleIdentityAutoConfiguration.class,
SinkMqAutoConfiguration.class,
SinkArchiveAutoConfiguration.class,
Gb32960AutoConfiguration.class))
@@ -36,6 +39,8 @@ class Gb32960IngestAppCompositionTest {
"lingniu.ingest.gb32960.enabled=true",
"lingniu.ingest.gb32960.server.enabled=true",
"lingniu.ingest.gb32960.port=0",
"lingniu.ingest.identity.store=mysql",
"lingniu.ingest.identity.mysql.initialize-schema=false",
"lingniu.ingest.session.store=memory",
"lingniu.ingest.sink.mq.enabled=true",
"lingniu.ingest.sink.mq.type=kafka",
@@ -53,6 +58,7 @@ class Gb32960IngestAppCompositionTest {
contextRunner.run(context -> {
assertThat(context).hasSingleBean(Gb32960MessageDecoder.class);
assertThat(context).hasSingleBean(Gb32960NettyServer.class);
assertThat(context).hasSingleBean(MySqlVehicleIdentityService.class);
assertThat(context).hasSingleBean(KafkaEventSink.class);
assertThat(context).hasSingleBean(KafkaEnvelopeDeadLetterSink.class);
assertThat(context).hasSingleBean(ArchiveStore.class);

View File

@@ -27,6 +27,9 @@ class Gb32960IngestAppDefaultsTest {
.containsEntry("lingniu.ingest.sink.mq.consumer.enabled", false)
.containsEntry("lingniu.ingest.sink.archive.enabled", "${SINK_ARCHIVE_ENABLED:true}")
.containsEntry("lingniu.ingest.sink.archive.path", "${SINK_ARCHIVE_PATH:./archive/}")
.containsEntry("lingniu.ingest.identity.store", "${VEHICLE_IDENTITY_STORE:mysql}")
.containsEntry("lingniu.ingest.identity.mysql.table-name",
"${VEHICLE_IDENTITY_MYSQL_TABLE:vehicle_identity_bindings}")
.containsEntry("lingniu.ingest.event-file-store.enabled", false)
.containsEntry("lingniu.ingest.event-history.enabled", false)
.containsEntry("lingniu.ingest.vehicle-state.enabled", false)

View File

@@ -48,9 +48,7 @@ lingniu:
store: ${SESSION_STORE:memory}
ttl: ${SESSION_TTL:30m}
identity:
store: ${VEHICLE_IDENTITY_STORE:file}
file:
path: ${VEHICLE_IDENTITY_FILE:./data/vehicle-identity.jsonl}
store: ${VEHICLE_IDENTITY_STORE:mysql}
mysql:
jdbc-url: ${VEHICLE_IDENTITY_MYSQL_JDBC_URL:jdbc:mysql://127.0.0.1:3306/lingniu_vehicle?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai}
username: ${VEHICLE_IDENTITY_MYSQL_USERNAME:root}

View File

@@ -1,6 +1,7 @@
package com.lingniu.ingest.jt808app;
import com.lingniu.ingest.core.config.IngestCoreAutoConfiguration;
import com.lingniu.ingest.identity.MySqlVehicleIdentityService;
import com.lingniu.ingest.identity.config.VehicleIdentityAutoConfiguration;
import com.lingniu.ingest.protocol.jt808.codec.Jt808MessageDecoder;
import com.lingniu.ingest.protocol.jt808.config.Jt808AutoConfiguration;
@@ -37,6 +38,8 @@ class Jt808IngestAppCompositionTest {
.withPropertyValues(
"lingniu.ingest.jt808.enabled=true",
"lingniu.ingest.jt808.port=0",
"lingniu.ingest.identity.store=mysql",
"lingniu.ingest.identity.mysql.initialize-schema=false",
"lingniu.ingest.session.store=memory",
"lingniu.ingest.sink.mq.enabled=true",
"lingniu.ingest.sink.mq.type=kafka",
@@ -54,6 +57,7 @@ class Jt808IngestAppCompositionTest {
contextRunner.run(context -> {
assertThat(context).hasSingleBean(Jt808MessageDecoder.class);
assertThat(context).hasSingleBean(Jt808NettyServer.class);
assertThat(context).hasSingleBean(MySqlVehicleIdentityService.class);
assertThat(context).hasSingleBean(KafkaEventSink.class);
assertThat(context).hasSingleBean(KafkaEnvelopeDeadLetterSink.class);
assertThat(context).hasSingleBean(ArchiveStore.class);

View File

@@ -28,7 +28,7 @@ class Jt808IngestAppDefaultsTest {
.containsEntry("lingniu.ingest.sink.mq.topics.realtime", "${KAFKA_TOPIC_JT808_EVENT:vehicle.event.jt808.v1}")
.containsEntry("lingniu.ingest.sink.mq.topics.raw-archive", "${KAFKA_TOPIC_JT808_RAW:vehicle.raw.jt808.v1}")
.containsEntry("lingniu.ingest.sink.mq.topics.dlq", "${KAFKA_TOPIC_JT808_DLQ:vehicle.dlq.jt808.v1}")
.containsEntry("lingniu.ingest.identity.store", "${VEHICLE_IDENTITY_STORE:file}")
.containsEntry("lingniu.ingest.identity.store", "${VEHICLE_IDENTITY_STORE:mysql}")
.containsEntry("lingniu.ingest.identity.mysql.table-name",
"${VEHICLE_IDENTITY_MYSQL_TABLE:vehicle_identity_bindings}")
.containsEntry("lingniu.ingest.identity.mysql.jdbc-url",

View File

@@ -78,8 +78,6 @@ lingniu:
enabled: ${VEHICLE_STATE_ENABLED:false}
vehicle-stat:
enabled: ${VEHICLE_STAT_ENABLED:true}
repository-type: ${VEHICLE_STAT_REPOSITORY_TYPE:jdbc}
file-path: ${VEHICLE_STAT_FILE_PATH:./target/vehicle-stat/}
zone-id: ${VEHICLE_STAT_ZONE_ID:Asia/Shanghai}
jt808:
enabled: ${VEHICLE_STAT_JT808_MILEAGE_ENABLED:false}

View File

@@ -7,7 +7,7 @@ import com.lingniu.ingest.sink.mq.SinkMqAutoConfiguration;
import com.lingniu.ingest.vehiclestate.VehicleStateEnvelopeIngestor;
import com.lingniu.ingest.vehiclestate.config.VehicleStateAutoConfiguration;
import com.lingniu.ingest.vehiclestat.DailyVehicleStatService;
import com.lingniu.ingest.vehiclestat.FileVehicleStatRepository;
import com.lingniu.ingest.vehiclestat.JdbcVehicleStatMetricRepository;
import com.lingniu.ingest.vehiclestat.VehicleStatController;
import com.lingniu.ingest.vehiclestat.VehicleStatEnvelopeIngestor;
import com.lingniu.ingest.vehiclestat.VehicleStatEventProcessor;
@@ -16,22 +16,17 @@ import com.lingniu.ingest.vehiclestat.VehicleStatRepository;
import com.lingniu.ingest.vehiclestat.config.VehicleStatAutoConfiguration;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.util.ClassUtils;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
class VehicleAnalyticsAppCompositionTest {
@TempDir
Path tempDir;
@Test
void createsStatsBeansWithoutProtocolListenerOrEventFileStore() {
new ApplicationContextRunner()
@@ -41,6 +36,7 @@ class VehicleAnalyticsAppCompositionTest {
VehicleStatAutoConfiguration.class))
.withAllowBeanDefinitionOverriding(true)
.withBean("kafkaProducer", KafkaProducer.class, VehicleAnalyticsAppCompositionTest::kafkaProducer)
.withBean(JdbcTemplate.class, () -> mock(JdbcTemplate.class))
.withPropertyValues(
"lingniu.ingest.sink.mq.enabled=true",
"lingniu.ingest.sink.mq.type=kafka",
@@ -48,13 +44,12 @@ class VehicleAnalyticsAppCompositionTest {
"lingniu.ingest.sink.mq.consumer.enabled=false",
"lingniu.ingest.vehicle-state.enabled=false",
"lingniu.ingest.vehicle-stat.enabled=true",
"lingniu.ingest.vehicle-stat.file-path=" + tempDir.resolve("vehicle-stat"),
"lingniu.ingest.event-file-store.enabled=false",
"lingniu.ingest.event-history.enabled=false",
"lingniu.ingest.gb32960.enabled=false")
.run(context -> {
assertThat(context).hasSingleBean(VehicleStatRepository.class);
assertThat(context).hasSingleBean(FileVehicleStatRepository.class);
assertThat(context).hasSingleBean(JdbcVehicleStatMetricRepository.class);
assertThat(context).hasSingleBean(DailyVehicleStatService.class);
assertThat(context).hasSingleBean(VehicleStatEventProcessor.class);
assertThat(context).hasSingleBean(VehicleStatEnvelopeIngestor.class);

View File

@@ -25,7 +25,6 @@ class VehicleAnalyticsAppDefaultsTest {
.containsEntry("lingniu.ingest.event-file-store.enabled", false)
.containsEntry("lingniu.ingest.event-history.enabled", false)
.containsEntry("lingniu.ingest.vehicle-stat.enabled", "${VEHICLE_STAT_ENABLED:true}")
.containsEntry("lingniu.ingest.vehicle-stat.repository-type", "${VEHICLE_STAT_REPOSITORY_TYPE:jdbc}")
.containsEntry("lingniu.ingest.vehicle-state.enabled", "${VEHICLE_STATE_ENABLED:false}")
.containsEntry("lingniu.ingest.sink.mq.consumer.enabled", "${KAFKA_CONSUMER_ENABLED:true}")
.containsEntry(

View File

@@ -60,16 +60,13 @@ lingniu:
rate-limit:
per-vin-qps: 50
identity:
store: ${VEHICLE_IDENTITY_STORE:file}
file:
path: ${VEHICLE_IDENTITY_FILE:./data/vehicle-identity.jsonl}
store: ${VEHICLE_IDENTITY_STORE:mysql}
mysql:
table: ${VEHICLE_IDENTITY_MYSQL_TABLE:vehicle_identity_binding}
jdbc-url: ${VEHICLE_IDENTITY_MYSQL_JDBC_URL:}
username: ${VEHICLE_IDENTITY_MYSQL_USERNAME:}
jdbc-url: ${VEHICLE_IDENTITY_MYSQL_JDBC_URL:jdbc:mysql://127.0.0.1:3306/lingniu_vehicle?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai}
username: ${VEHICLE_IDENTITY_MYSQL_USERNAME:root}
password: ${VEHICLE_IDENTITY_MYSQL_PASSWORD:}
driver-class-name: ${VEHICLE_IDENTITY_MYSQL_DRIVER_CLASS_NAME:com.mysql.cj.jdbc.Driver}
refresh-interval: ${VEHICLE_IDENTITY_MYSQL_REFRESH_INTERVAL:60s}
table-name: ${VEHICLE_IDENTITY_MYSQL_TABLE:vehicle_identity_bindings}
initialize-schema: ${VEHICLE_IDENTITY_MYSQL_INITIALIZE_SCHEMA:true}
sink:
mq:
enabled: ${KAFKA_ENABLED:true}

View File

@@ -1,6 +1,7 @@
package com.lingniu.ingest.yutongmqttapp;
import com.lingniu.ingest.core.config.IngestCoreAutoConfiguration;
import com.lingniu.ingest.identity.MySqlVehicleIdentityService;
import com.lingniu.ingest.identity.config.VehicleIdentityAutoConfiguration;
import com.lingniu.ingest.inbound.mqtt.client.MqttEndpointManager;
import com.lingniu.ingest.inbound.mqtt.config.MqttInboundAutoConfiguration;
@@ -37,6 +38,8 @@ class YutongMqttAppCompositionTest {
"lingniu.ingest.mqtt.endpoints[0].uri=tcp://127.0.0.1:1883",
"lingniu.ingest.mqtt.endpoints[0].topic=/yutong/#",
"lingniu.ingest.mqtt.endpoints[0].profile=yutong",
"lingniu.ingest.identity.store=mysql",
"lingniu.ingest.identity.mysql.initialize-schema=false",
"lingniu.ingest.sink.mq.enabled=true",
"lingniu.ingest.sink.mq.type=kafka",
"lingniu.ingest.sink.mq.bootstrap-servers=localhost:9092",
@@ -54,6 +57,7 @@ class YutongMqttAppCompositionTest {
assertThat(context).hasSingleBean(MqttEndpointManager.class);
assertThat(context).hasSingleBean(MqttProfileRegistry.class);
assertThat(context).hasSingleBean(MqttRealtimeHandler.class);
assertThat(context).hasSingleBean(MySqlVehicleIdentityService.class);
assertThat(context).hasSingleBean(KafkaEventSink.class);
assertThat(context).hasSingleBean(KafkaEnvelopeDeadLetterSink.class);
assertThat(context).hasSingleBean(ArchiveStore.class);

View File

@@ -30,8 +30,9 @@ class YutongMqttAppDefaultsTest {
.containsEntry("lingniu.ingest.sink.mq.topics.realtime", "${KAFKA_TOPIC_YUTONG_MQTT_EVENT:vehicle.event.mqtt-yutong.v1}")
.containsEntry("lingniu.ingest.sink.mq.topics.raw-archive", "${KAFKA_TOPIC_YUTONG_MQTT_RAW:vehicle.raw.mqtt-yutong.v1}")
.containsEntry("lingniu.ingest.sink.mq.topics.dlq", "${KAFKA_TOPIC_YUTONG_MQTT_DLQ:vehicle.dlq.mqtt-yutong.v1}")
.containsEntry("lingniu.ingest.identity.store", "${VEHICLE_IDENTITY_STORE:file}")
.containsEntry("lingniu.ingest.identity.mysql.table", "${VEHICLE_IDENTITY_MYSQL_TABLE:vehicle_identity_binding}")
.containsEntry("lingniu.ingest.identity.store", "${VEHICLE_IDENTITY_STORE:mysql}")
.containsEntry("lingniu.ingest.identity.mysql.table-name",
"${VEHICLE_IDENTITY_MYSQL_TABLE:vehicle_identity_bindings}")
.containsEntry("lingniu.ingest.sink.archive.enabled", "${SINK_ARCHIVE_ENABLED:true}")
.containsEntry("lingniu.ingest.event-file-store.enabled", false)
.containsEntry("lingniu.ingest.event-history.enabled", false)

View File

@@ -1,8 +1,6 @@
package com.lingniu.ingest.identity.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.lingniu.ingest.identity.FileVehicleIdentityService;
import com.lingniu.ingest.identity.InMemoryVehicleIdentityService;
import com.lingniu.ingest.identity.MySqlVehicleIdentityService;
import com.lingniu.ingest.identity.VehicleIdentityRegistry;
import com.lingniu.ingest.identity.VehicleIdentityResolver;
@@ -14,7 +12,6 @@ import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import javax.sql.DataSource;
import java.nio.file.Path;
@AutoConfiguration
@EnableConfigurationProperties(VehicleIdentityProperties.class)
@@ -28,16 +25,8 @@ public class VehicleIdentityAutoConfiguration {
@Bean
@ConditionalOnMissingBean({VehicleIdentityResolver.class, VehicleIdentityRegistry.class})
@ConditionalOnProperty(prefix = "lingniu.ingest.identity", name = "store", havingValue = "file")
public FileVehicleIdentityService fileVehicleIdentityService(VehicleIdentityProperties properties,
ObjectMapper objectMapper) {
// 文件实现同时提供 resolver 和 registry协议层既能查绑定也能在注册/鉴权时补写绑定。
return new FileVehicleIdentityService(Path.of(properties.getFile().getPath()), objectMapper);
}
@Bean
@ConditionalOnMissingBean({VehicleIdentityResolver.class, VehicleIdentityRegistry.class})
@ConditionalOnProperty(prefix = "lingniu.ingest.identity", name = "store", havingValue = "mysql")
@ConditionalOnProperty(prefix = "lingniu.ingest.identity", name = "store", havingValue = "mysql",
matchIfMissing = true)
public MySqlVehicleIdentityService mysqlVehicleIdentityService(VehicleIdentityProperties properties,
ObjectMapper objectMapper) {
VehicleIdentityProperties.Mysql mysql = properties.getMysql();
@@ -45,12 +34,4 @@ public class VehicleIdentityAutoConfiguration {
mysql.getJdbcUrl(), mysql.getUsername(), mysql.getPassword());
return new MySqlVehicleIdentityService(dataSource, mysql, objectMapper);
}
@Bean
@ConditionalOnMissingBean({VehicleIdentityResolver.class, VehicleIdentityRegistry.class})
@ConditionalOnProperty(prefix = "lingniu.ingest.identity", name = "store", havingValue = "memory",
matchIfMissing = true)
public InMemoryVehicleIdentityService vehicleIdentityService() {
return new InMemoryVehicleIdentityService();
}
}

View File

@@ -5,24 +5,14 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "lingniu.ingest.identity")
public class VehicleIdentityProperties {
private String store = "memory";
private File file = new File();
private String store = "mysql";
private Mysql mysql = new Mysql();
public String getStore() { return store; }
public void setStore(String store) { this.store = store; }
public File getFile() { return file; }
public void setFile(File file) { this.file = file; }
public Mysql getMysql() { return mysql; }
public void setMysql(Mysql mysql) { this.mysql = mysql; }
public static class File {
private String path = "./data/vehicle-identity.jsonl";
public String getPath() { return path; }
public void setPath(String path) { this.path = path; }
}
public static class Mysql {
private String jdbcUrl = "jdbc:mysql://127.0.0.1:3306/lingniu_vehicle?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai";
private String username = "root";

View File

@@ -3,42 +3,31 @@ package com.lingniu.ingest.identity.config;
import com.lingniu.ingest.identity.VehicleIdentityRegistry;
import com.lingniu.ingest.identity.VehicleIdentityResolver;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
class VehicleIdentityAutoConfigurationTest {
@TempDir
Path tempDir;
private final ApplicationContextRunner runner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(VehicleIdentityAutoConfiguration.class));
@Test
void createsDefaultIdentityService() {
runner.run(context -> {
assertThat(context).hasSingleBean(VehicleIdentityResolver.class);
assertThat(context).hasSingleBean(VehicleIdentityRegistry.class);
assertThat(context.getBean(VehicleIdentityResolver.class))
.isSameAs(context.getBean(VehicleIdentityRegistry.class));
});
}
@Test
void createsFileIdentityServiceWhenConfigured() {
void createsMysqlIdentityServiceByDefault() {
runner
.withPropertyValues(
"lingniu.ingest.identity.store=file",
"lingniu.ingest.identity.file.path=" + tempDir.resolve("identity.jsonl"))
"lingniu.ingest.identity.mysql.jdbc-url=jdbc:h2:mem:auto-identity-default;MODE=MySQL;DATABASE_TO_UPPER=false;DB_CLOSE_DELAY=-1",
"lingniu.ingest.identity.mysql.username=sa",
"lingniu.ingest.identity.mysql.password=",
"lingniu.ingest.identity.mysql.table-name=vehicle_identity_bindings")
.run(context -> {
assertThat(context).hasSingleBean(VehicleIdentityResolver.class);
assertThat(context).hasSingleBean(VehicleIdentityRegistry.class);
assertThat(context.getBean(VehicleIdentityResolver.class))
.isSameAs(context.getBean(VehicleIdentityRegistry.class));
assertThat(context.getBean(VehicleIdentityResolver.class).getClass().getSimpleName())
.isEqualTo("FileVehicleIdentityService");
.isEqualTo("MySqlVehicleIdentityService");
});
}
@@ -60,4 +49,14 @@ class VehicleIdentityAutoConfigurationTest {
.isEqualTo("MySqlVehicleIdentityService");
});
}
@Test
void unsupportedStoreModeDoesNotCreateRuntimeFallback() {
runner
.withPropertyValues("lingniu.ingest.identity.store=file")
.run(context -> {
assertThat(context).doesNotHaveBean(VehicleIdentityResolver.class);
assertThat(context).doesNotHaveBean(VehicleIdentityRegistry.class);
});
}
}

View File

@@ -5,7 +5,6 @@ import com.lingniu.ingest.api.consumer.EnvelopeDeadLetterSink;
import com.lingniu.ingest.vehiclestat.DailyMileageCalculator;
import com.lingniu.ingest.vehiclestat.DailyMileageStrategy;
import com.lingniu.ingest.vehiclestat.DailyVehicleStatService;
import com.lingniu.ingest.vehiclestat.FileVehicleStatRepository;
import com.lingniu.ingest.vehiclestat.JdbcVehicleStatMetricRepository;
import com.lingniu.ingest.vehiclestat.VehicleStatController;
import com.lingniu.ingest.vehiclestat.VehicleStatEnvelopeIngestor;
@@ -33,7 +32,6 @@ import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.jdbc.core.JdbcTemplate;
import java.nio.file.Path;
import java.time.Duration;
import java.time.ZoneId;
@@ -48,20 +46,11 @@ public class VehicleStatAutoConfiguration {
@Bean
@ConditionalOnBean(JdbcTemplate.class)
@ConditionalOnProperty(prefix = "lingniu.ingest.vehicle-stat", name = "repository-type",
havingValue = "jdbc", matchIfMissing = true)
@ConditionalOnMissingBean
public VehicleStatRepository jdbcVehicleStatMetricRepository(JdbcTemplate jdbcTemplate) {
return new JdbcVehicleStatMetricRepository(jdbcTemplate);
}
@Bean
@ConditionalOnMissingBean
public VehicleStatRepository vehicleStatRepository(VehicleStatProperties props) {
// 无 JDBC 指标库时才回落本地文件,主要用于开发和单元验证。
return new FileVehicleStatRepository(Path.of(props.getFilePath()));
}
@Bean
@ConditionalOnMissingBean
public VehicleStatRuleRepository vehicleStatRuleRepository() {

View File

@@ -5,33 +5,11 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "lingniu.ingest.vehicle-stat")
public class VehicleStatProperties {
/** 派生统计本地文件根目录;不是 32960 RAW archive 或 DuckDB 历史库目录。 */
private String filePath = "./target/vehicle-stat/";
/** 指标仓储类型:有 JDBC 数据源时生产使用 jdbc本地无数据库时回落 file。 */
private String repositoryType = "jdbc";
/** 统计自然日口径,默认按国内业务使用东八区。 */
private String zoneId = "Asia/Shanghai";
private Jt808 jt808 = new Jt808();
public String getFilePath() {
return filePath;
}
public void setFilePath(String filePath) {
this.filePath = filePath;
}
public String getRepositoryType() {
return repositoryType;
}
public void setRepositoryType(String repositoryType) {
this.repositoryType = repositoryType;
}
public String getZoneId() {
return zoneId;
}

View File

@@ -5,7 +5,6 @@ import com.lingniu.ingest.api.consumer.EnvelopeDeadLetterSink;
import com.lingniu.ingest.vehiclestat.DailyMileageCalculator;
import com.lingniu.ingest.vehiclestat.DailyMileageStrategy;
import com.lingniu.ingest.vehiclestat.DailyVehicleStatService;
import com.lingniu.ingest.vehiclestat.FileVehicleStatRepository;
import com.lingniu.ingest.vehiclestat.JdbcVehicleStatMetricRepository;
import com.lingniu.ingest.vehiclestat.MileagePoint;
import com.lingniu.ingest.vehiclestat.VehicleStatController;
@@ -71,17 +70,14 @@ class VehicleStatAutoConfigurationTest {
}
@Test
void createsFileRepositoryWhenNoRepositoryBeanExists() {
void doesNotCreateRepositoryWithoutJdbcMetricStore() {
new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(VehicleStatAutoConfiguration.class))
.withPropertyValues("lingniu.ingest.vehicle-stat.enabled=true")
.run(context -> {
assertThat(context).hasSingleBean(VehicleStatRepository.class);
assertThat(context).hasSingleBean(FileVehicleStatRepository.class);
assertThat(context).hasSingleBean(VehicleStatRuleRepository.class);
assertThat(context).hasSingleBean(DailyMileageCalculator.class);
assertThat(context).hasSingleBean(DailyVehicleStatService.class);
assertThat(context).hasSingleBean(VehicleStatEventProcessor.class);
assertThat(context).doesNotHaveBean(VehicleStatRepository.class);
assertThat(context).doesNotHaveBean(DailyVehicleStatService.class);
assertThat(context).doesNotHaveBean(VehicleStatEventProcessor.class);
});
}
@@ -142,7 +138,7 @@ class VehicleStatAutoConfigurationTest {
}
@Test
void canForceFileRepositoryEvenWhenJdbcTemplateExists() {
void ignoresFileRepositoryTypeAndKeepsJdbcMetricRepository() {
new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(
JdbcTemplateAutoConfiguration.class,
@@ -156,8 +152,7 @@ class VehicleStatAutoConfigurationTest {
"lingniu.ingest.vehicle-stat.repository-type=file")
.run(context -> {
assertThat(context).hasSingleBean(VehicleStatRepository.class);
assertThat(context).hasSingleBean(FileVehicleStatRepository.class);
assertThat(context).doesNotHaveBean(JdbcVehicleStatMetricRepository.class);
assertThat(context).hasSingleBean(JdbcVehicleStatMetricRepository.class);
});
}