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

View File

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

View File

@@ -2,11 +2,11 @@
## Scope ## 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 ## 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: 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 ```text
KAFKA_TOPIC_JT808_EVENT=vehicle.event.jt808.v1 KAFKA_TOPIC_JT808_EVENT=vehicle.event.jt808.v1
VEHICLE_STAT_JT808_MILEAGE_ENABLED=true VEHICLE_STAT_JT808_MILEAGE_ENABLED=true
VEHICLE_STAT_REPOSITORY_TYPE=jdbc
VEHICLE_STAT_JT808_STATE_STORE=redis VEHICLE_STAT_JT808_STATE_STORE=redis
MYSQL_JDBC_URL=<jdbc-url> MYSQL_JDBC_URL=<jdbc-url>
MYSQL_USERNAME=<user> MYSQL_USERNAME=<user>

View File

@@ -118,7 +118,7 @@ deploy/local/launchctl/
JT808 注册身份绑定: JT808 注册身份绑定:
- 默认可以使用 `VEHICLE_IDENTITY_STORE=file``memory` - 生产默认使用 `VEHICLE_IDENTITY_STORE=mysql`,不再使用 file/memory 作为运行时身份绑定仓储
- 生产需要把 0x0100 注册信息维护到 MySQL 时,设置 `VEHICLE_IDENTITY_STORE=mysql` - 生产需要把 0x0100 注册信息维护到 MySQL 时,设置 `VEHICLE_IDENTITY_STORE=mysql`
- 需要提供 `VEHICLE_IDENTITY_MYSQL_JDBC_URL``VEHICLE_IDENTITY_MYSQL_USERNAME``VEHICLE_IDENTITY_MYSQL_PASSWORD` - 需要提供 `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 注册字段。 - 服务启动时自动创建 `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` - `jt808_locations` 中位置行数已超过 `8783`
- 在旧的字段宽表开启模式下,`telemetry_fields``protocol='JT808'` 的字段行数曾超过 `15000`;当前高吞吐默认不要求该表持续增长。 - 在旧的字段宽表开启模式下,`telemetry_fields``protocol='JT808'` 的字段行数曾超过 `15000`;当前高吞吐默认不要求该表持续增长。
- 分页 API 已用终端号 `13079963301` 验证第一页和 nextCursor 第二页,样例位置包含 `longitude=118.913846``latitude=31.927309``statusFlag=3``archive://2026/06/29/JT808/...` 原始帧引用。 - 分页 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 / 轻压测工具 ## JT808 本机 smoke / 轻压测工具

View File

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

View File

@@ -78,9 +78,13 @@ lingniu:
store: ${SESSION_STORE:memory} store: ${SESSION_STORE:memory}
ttl: ${SESSION_TTL:30m} ttl: ${SESSION_TTL:30m}
identity: identity:
store: ${VEHICLE_IDENTITY_STORE:file} store: ${VEHICLE_IDENTITY_STORE:mysql}
file: mysql:
path: ${VEHICLE_IDENTITY_FILE:./data/vehicle-identity.jsonl} 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: sink:
mq: mq:
enabled: ${KAFKA_ENABLED:true} enabled: ${KAFKA_ENABLED:true}

View File

@@ -1,6 +1,8 @@
package com.lingniu.ingest.gb32960app; package com.lingniu.ingest.gb32960app;
import com.lingniu.ingest.core.config.IngestCoreAutoConfiguration; 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.codec.Gb32960MessageDecoder;
import com.lingniu.ingest.protocol.gb32960.config.Gb32960AutoConfiguration; import com.lingniu.ingest.protocol.gb32960.config.Gb32960AutoConfiguration;
import com.lingniu.ingest.protocol.gb32960.inbound.Gb32960NettyServer; import com.lingniu.ingest.protocol.gb32960.inbound.Gb32960NettyServer;
@@ -27,6 +29,7 @@ class Gb32960IngestAppCompositionTest {
.withConfiguration(AutoConfigurations.of( .withConfiguration(AutoConfigurations.of(
IngestCoreAutoConfiguration.class, IngestCoreAutoConfiguration.class,
SessionCoreAutoConfiguration.class, SessionCoreAutoConfiguration.class,
VehicleIdentityAutoConfiguration.class,
SinkMqAutoConfiguration.class, SinkMqAutoConfiguration.class,
SinkArchiveAutoConfiguration.class, SinkArchiveAutoConfiguration.class,
Gb32960AutoConfiguration.class)) Gb32960AutoConfiguration.class))
@@ -36,6 +39,8 @@ class Gb32960IngestAppCompositionTest {
"lingniu.ingest.gb32960.enabled=true", "lingniu.ingest.gb32960.enabled=true",
"lingniu.ingest.gb32960.server.enabled=true", "lingniu.ingest.gb32960.server.enabled=true",
"lingniu.ingest.gb32960.port=0", "lingniu.ingest.gb32960.port=0",
"lingniu.ingest.identity.store=mysql",
"lingniu.ingest.identity.mysql.initialize-schema=false",
"lingniu.ingest.session.store=memory", "lingniu.ingest.session.store=memory",
"lingniu.ingest.sink.mq.enabled=true", "lingniu.ingest.sink.mq.enabled=true",
"lingniu.ingest.sink.mq.type=kafka", "lingniu.ingest.sink.mq.type=kafka",
@@ -53,6 +58,7 @@ class Gb32960IngestAppCompositionTest {
contextRunner.run(context -> { contextRunner.run(context -> {
assertThat(context).hasSingleBean(Gb32960MessageDecoder.class); assertThat(context).hasSingleBean(Gb32960MessageDecoder.class);
assertThat(context).hasSingleBean(Gb32960NettyServer.class); assertThat(context).hasSingleBean(Gb32960NettyServer.class);
assertThat(context).hasSingleBean(MySqlVehicleIdentityService.class);
assertThat(context).hasSingleBean(KafkaEventSink.class); assertThat(context).hasSingleBean(KafkaEventSink.class);
assertThat(context).hasSingleBean(KafkaEnvelopeDeadLetterSink.class); assertThat(context).hasSingleBean(KafkaEnvelopeDeadLetterSink.class);
assertThat(context).hasSingleBean(ArchiveStore.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.mq.consumer.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.sink.archive.path", "${SINK_ARCHIVE_PATH:./archive/}") .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-file-store.enabled", false)
.containsEntry("lingniu.ingest.event-history.enabled", false) .containsEntry("lingniu.ingest.event-history.enabled", false)
.containsEntry("lingniu.ingest.vehicle-state.enabled", false) .containsEntry("lingniu.ingest.vehicle-state.enabled", false)

View File

@@ -48,9 +48,7 @@ lingniu:
store: ${SESSION_STORE:memory} store: ${SESSION_STORE:memory}
ttl: ${SESSION_TTL:30m} ttl: ${SESSION_TTL:30m}
identity: identity:
store: ${VEHICLE_IDENTITY_STORE:file} store: ${VEHICLE_IDENTITY_STORE:mysql}
file:
path: ${VEHICLE_IDENTITY_FILE:./data/vehicle-identity.jsonl}
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} 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} username: ${VEHICLE_IDENTITY_MYSQL_USERNAME:root}

View File

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

View File

@@ -78,8 +78,6 @@ lingniu:
enabled: ${VEHICLE_STATE_ENABLED:false} enabled: ${VEHICLE_STATE_ENABLED:false}
vehicle-stat: vehicle-stat:
enabled: ${VEHICLE_STAT_ENABLED:true} 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} zone-id: ${VEHICLE_STAT_ZONE_ID:Asia/Shanghai}
jt808: jt808:
enabled: ${VEHICLE_STAT_JT808_MILEAGE_ENABLED:false} 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.VehicleStateEnvelopeIngestor;
import com.lingniu.ingest.vehiclestate.config.VehicleStateAutoConfiguration; import com.lingniu.ingest.vehiclestate.config.VehicleStateAutoConfiguration;
import com.lingniu.ingest.vehiclestat.DailyVehicleStatService; 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.VehicleStatController;
import com.lingniu.ingest.vehiclestat.VehicleStatEnvelopeIngestor; import com.lingniu.ingest.vehiclestat.VehicleStatEnvelopeIngestor;
import com.lingniu.ingest.vehiclestat.VehicleStatEventProcessor; import com.lingniu.ingest.vehiclestat.VehicleStatEventProcessor;
@@ -16,22 +16,17 @@ import com.lingniu.ingest.vehiclestat.VehicleStatRepository;
import com.lingniu.ingest.vehiclestat.config.VehicleStatAutoConfiguration; import com.lingniu.ingest.vehiclestat.config.VehicleStatAutoConfiguration;
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.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.util.ClassUtils; import org.springframework.util.ClassUtils;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
class VehicleAnalyticsAppCompositionTest { class VehicleAnalyticsAppCompositionTest {
@TempDir
Path tempDir;
@Test @Test
void createsStatsBeansWithoutProtocolListenerOrEventFileStore() { void createsStatsBeansWithoutProtocolListenerOrEventFileStore() {
new ApplicationContextRunner() new ApplicationContextRunner()
@@ -41,6 +36,7 @@ class VehicleAnalyticsAppCompositionTest {
VehicleStatAutoConfiguration.class)) VehicleStatAutoConfiguration.class))
.withAllowBeanDefinitionOverriding(true) .withAllowBeanDefinitionOverriding(true)
.withBean("kafkaProducer", KafkaProducer.class, VehicleAnalyticsAppCompositionTest::kafkaProducer) .withBean("kafkaProducer", KafkaProducer.class, VehicleAnalyticsAppCompositionTest::kafkaProducer)
.withBean(JdbcTemplate.class, () -> mock(JdbcTemplate.class))
.withPropertyValues( .withPropertyValues(
"lingniu.ingest.sink.mq.enabled=true", "lingniu.ingest.sink.mq.enabled=true",
"lingniu.ingest.sink.mq.type=kafka", "lingniu.ingest.sink.mq.type=kafka",
@@ -48,13 +44,12 @@ class VehicleAnalyticsAppCompositionTest {
"lingniu.ingest.sink.mq.consumer.enabled=false", "lingniu.ingest.sink.mq.consumer.enabled=false",
"lingniu.ingest.vehicle-state.enabled=false", "lingniu.ingest.vehicle-state.enabled=false",
"lingniu.ingest.vehicle-stat.enabled=true", "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-file-store.enabled=false",
"lingniu.ingest.event-history.enabled=false", "lingniu.ingest.event-history.enabled=false",
"lingniu.ingest.gb32960.enabled=false") "lingniu.ingest.gb32960.enabled=false")
.run(context -> { .run(context -> {
assertThat(context).hasSingleBean(VehicleStatRepository.class); assertThat(context).hasSingleBean(VehicleStatRepository.class);
assertThat(context).hasSingleBean(FileVehicleStatRepository.class); assertThat(context).hasSingleBean(JdbcVehicleStatMetricRepository.class);
assertThat(context).hasSingleBean(DailyVehicleStatService.class); assertThat(context).hasSingleBean(DailyVehicleStatService.class);
assertThat(context).hasSingleBean(VehicleStatEventProcessor.class); assertThat(context).hasSingleBean(VehicleStatEventProcessor.class);
assertThat(context).hasSingleBean(VehicleStatEnvelopeIngestor.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-file-store.enabled", false)
.containsEntry("lingniu.ingest.event-history.enabled", false) .containsEntry("lingniu.ingest.event-history.enabled", false)
.containsEntry("lingniu.ingest.vehicle-stat.enabled", "${VEHICLE_STAT_ENABLED:true}") .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.vehicle-state.enabled", "${VEHICLE_STATE_ENABLED:false}")
.containsEntry("lingniu.ingest.sink.mq.consumer.enabled", "${KAFKA_CONSUMER_ENABLED:true}") .containsEntry("lingniu.ingest.sink.mq.consumer.enabled", "${KAFKA_CONSUMER_ENABLED:true}")
.containsEntry( .containsEntry(

View File

@@ -60,16 +60,13 @@ lingniu:
rate-limit: rate-limit:
per-vin-qps: 50 per-vin-qps: 50
identity: identity:
store: ${VEHICLE_IDENTITY_STORE:file} store: ${VEHICLE_IDENTITY_STORE:mysql}
file:
path: ${VEHICLE_IDENTITY_FILE:./data/vehicle-identity.jsonl}
mysql: mysql:
table: ${VEHICLE_IDENTITY_MYSQL_TABLE:vehicle_identity_binding} 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}
jdbc-url: ${VEHICLE_IDENTITY_MYSQL_JDBC_URL:} username: ${VEHICLE_IDENTITY_MYSQL_USERNAME:root}
username: ${VEHICLE_IDENTITY_MYSQL_USERNAME:}
password: ${VEHICLE_IDENTITY_MYSQL_PASSWORD:} password: ${VEHICLE_IDENTITY_MYSQL_PASSWORD:}
driver-class-name: ${VEHICLE_IDENTITY_MYSQL_DRIVER_CLASS_NAME:com.mysql.cj.jdbc.Driver} table-name: ${VEHICLE_IDENTITY_MYSQL_TABLE:vehicle_identity_bindings}
refresh-interval: ${VEHICLE_IDENTITY_MYSQL_REFRESH_INTERVAL:60s} initialize-schema: ${VEHICLE_IDENTITY_MYSQL_INITIALIZE_SCHEMA:true}
sink: sink:
mq: mq:
enabled: ${KAFKA_ENABLED:true} enabled: ${KAFKA_ENABLED:true}

View File

@@ -1,6 +1,7 @@
package com.lingniu.ingest.yutongmqttapp; package com.lingniu.ingest.yutongmqttapp;
import com.lingniu.ingest.core.config.IngestCoreAutoConfiguration; import com.lingniu.ingest.core.config.IngestCoreAutoConfiguration;
import com.lingniu.ingest.identity.MySqlVehicleIdentityService;
import com.lingniu.ingest.identity.config.VehicleIdentityAutoConfiguration; import com.lingniu.ingest.identity.config.VehicleIdentityAutoConfiguration;
import com.lingniu.ingest.inbound.mqtt.client.MqttEndpointManager; import com.lingniu.ingest.inbound.mqtt.client.MqttEndpointManager;
import com.lingniu.ingest.inbound.mqtt.config.MqttInboundAutoConfiguration; 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].uri=tcp://127.0.0.1:1883",
"lingniu.ingest.mqtt.endpoints[0].topic=/yutong/#", "lingniu.ingest.mqtt.endpoints[0].topic=/yutong/#",
"lingniu.ingest.mqtt.endpoints[0].profile=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.enabled=true",
"lingniu.ingest.sink.mq.type=kafka", "lingniu.ingest.sink.mq.type=kafka",
"lingniu.ingest.sink.mq.bootstrap-servers=localhost:9092", "lingniu.ingest.sink.mq.bootstrap-servers=localhost:9092",
@@ -54,6 +57,7 @@ class YutongMqttAppCompositionTest {
assertThat(context).hasSingleBean(MqttEndpointManager.class); assertThat(context).hasSingleBean(MqttEndpointManager.class);
assertThat(context).hasSingleBean(MqttProfileRegistry.class); assertThat(context).hasSingleBean(MqttProfileRegistry.class);
assertThat(context).hasSingleBean(MqttRealtimeHandler.class); assertThat(context).hasSingleBean(MqttRealtimeHandler.class);
assertThat(context).hasSingleBean(MySqlVehicleIdentityService.class);
assertThat(context).hasSingleBean(KafkaEventSink.class); assertThat(context).hasSingleBean(KafkaEventSink.class);
assertThat(context).hasSingleBean(KafkaEnvelopeDeadLetterSink.class); assertThat(context).hasSingleBean(KafkaEnvelopeDeadLetterSink.class);
assertThat(context).hasSingleBean(ArchiveStore.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.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.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.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.store", "${VEHICLE_IDENTITY_STORE:mysql}")
.containsEntry("lingniu.ingest.identity.mysql.table", "${VEHICLE_IDENTITY_MYSQL_TABLE:vehicle_identity_binding}") .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.sink.archive.enabled", "${SINK_ARCHIVE_ENABLED:true}")
.containsEntry("lingniu.ingest.event-file-store.enabled", false) .containsEntry("lingniu.ingest.event-file-store.enabled", false)
.containsEntry("lingniu.ingest.event-history.enabled", false) .containsEntry("lingniu.ingest.event-history.enabled", false)

View File

@@ -1,8 +1,6 @@
package com.lingniu.ingest.identity.config; package com.lingniu.ingest.identity.config;
import com.fasterxml.jackson.databind.ObjectMapper; 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.MySqlVehicleIdentityService;
import com.lingniu.ingest.identity.VehicleIdentityRegistry; import com.lingniu.ingest.identity.VehicleIdentityRegistry;
import com.lingniu.ingest.identity.VehicleIdentityResolver; import com.lingniu.ingest.identity.VehicleIdentityResolver;
@@ -14,7 +12,6 @@ import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.datasource.DriverManagerDataSource; import org.springframework.jdbc.datasource.DriverManagerDataSource;
import javax.sql.DataSource; import javax.sql.DataSource;
import java.nio.file.Path;
@AutoConfiguration @AutoConfiguration
@EnableConfigurationProperties(VehicleIdentityProperties.class) @EnableConfigurationProperties(VehicleIdentityProperties.class)
@@ -28,16 +25,8 @@ public class VehicleIdentityAutoConfiguration {
@Bean @Bean
@ConditionalOnMissingBean({VehicleIdentityResolver.class, VehicleIdentityRegistry.class}) @ConditionalOnMissingBean({VehicleIdentityResolver.class, VehicleIdentityRegistry.class})
@ConditionalOnProperty(prefix = "lingniu.ingest.identity", name = "store", havingValue = "file") @ConditionalOnProperty(prefix = "lingniu.ingest.identity", name = "store", havingValue = "mysql",
public FileVehicleIdentityService fileVehicleIdentityService(VehicleIdentityProperties properties, matchIfMissing = true)
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")
public MySqlVehicleIdentityService mysqlVehicleIdentityService(VehicleIdentityProperties properties, public MySqlVehicleIdentityService mysqlVehicleIdentityService(VehicleIdentityProperties properties,
ObjectMapper objectMapper) { ObjectMapper objectMapper) {
VehicleIdentityProperties.Mysql mysql = properties.getMysql(); VehicleIdentityProperties.Mysql mysql = properties.getMysql();
@@ -45,12 +34,4 @@ public class VehicleIdentityAutoConfiguration {
mysql.getJdbcUrl(), mysql.getUsername(), mysql.getPassword()); mysql.getJdbcUrl(), mysql.getUsername(), mysql.getPassword());
return new MySqlVehicleIdentityService(dataSource, mysql, objectMapper); 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") @ConfigurationProperties(prefix = "lingniu.ingest.identity")
public class VehicleIdentityProperties { public class VehicleIdentityProperties {
private String store = "memory"; private String store = "mysql";
private File file = new File();
private Mysql mysql = new Mysql(); private Mysql mysql = new Mysql();
public String getStore() { return store; } public String getStore() { return store; }
public void setStore(String store) { this.store = 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 Mysql getMysql() { return mysql; }
public void setMysql(Mysql mysql) { this.mysql = 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 { 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 jdbcUrl = "jdbc:mysql://127.0.0.1:3306/lingniu_vehicle?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai";
private String username = "root"; 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.VehicleIdentityRegistry;
import com.lingniu.ingest.identity.VehicleIdentityResolver; import com.lingniu.ingest.identity.VehicleIdentityResolver;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
class VehicleIdentityAutoConfigurationTest { class VehicleIdentityAutoConfigurationTest {
@TempDir
Path tempDir;
private final ApplicationContextRunner runner = new ApplicationContextRunner() private final ApplicationContextRunner runner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(VehicleIdentityAutoConfiguration.class)); .withConfiguration(AutoConfigurations.of(VehicleIdentityAutoConfiguration.class));
@Test @Test
void createsDefaultIdentityService() { void createsMysqlIdentityServiceByDefault() {
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() {
runner runner
.withPropertyValues( .withPropertyValues(
"lingniu.ingest.identity.store=file", "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.file.path=" + tempDir.resolve("identity.jsonl")) "lingniu.ingest.identity.mysql.username=sa",
"lingniu.ingest.identity.mysql.password=",
"lingniu.ingest.identity.mysql.table-name=vehicle_identity_bindings")
.run(context -> { .run(context -> {
assertThat(context).hasSingleBean(VehicleIdentityResolver.class); 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()) assertThat(context.getBean(VehicleIdentityResolver.class).getClass().getSimpleName())
.isEqualTo("FileVehicleIdentityService"); .isEqualTo("MySqlVehicleIdentityService");
}); });
} }
@@ -60,4 +49,14 @@ class VehicleIdentityAutoConfigurationTest {
.isEqualTo("MySqlVehicleIdentityService"); .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.DailyMileageCalculator;
import com.lingniu.ingest.vehiclestat.DailyMileageStrategy; import com.lingniu.ingest.vehiclestat.DailyMileageStrategy;
import com.lingniu.ingest.vehiclestat.DailyVehicleStatService; import com.lingniu.ingest.vehiclestat.DailyVehicleStatService;
import com.lingniu.ingest.vehiclestat.FileVehicleStatRepository;
import com.lingniu.ingest.vehiclestat.JdbcVehicleStatMetricRepository; import com.lingniu.ingest.vehiclestat.JdbcVehicleStatMetricRepository;
import com.lingniu.ingest.vehiclestat.VehicleStatController; import com.lingniu.ingest.vehiclestat.VehicleStatController;
import com.lingniu.ingest.vehiclestat.VehicleStatEnvelopeIngestor; 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.data.redis.core.StringRedisTemplate;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import java.nio.file.Path;
import java.time.Duration; import java.time.Duration;
import java.time.ZoneId; import java.time.ZoneId;
@@ -48,20 +46,11 @@ public class VehicleStatAutoConfiguration {
@Bean @Bean
@ConditionalOnBean(JdbcTemplate.class) @ConditionalOnBean(JdbcTemplate.class)
@ConditionalOnProperty(prefix = "lingniu.ingest.vehicle-stat", name = "repository-type",
havingValue = "jdbc", matchIfMissing = true)
@ConditionalOnMissingBean @ConditionalOnMissingBean
public VehicleStatRepository jdbcVehicleStatMetricRepository(JdbcTemplate jdbcTemplate) { public VehicleStatRepository jdbcVehicleStatMetricRepository(JdbcTemplate jdbcTemplate) {
return new JdbcVehicleStatMetricRepository(jdbcTemplate); return new JdbcVehicleStatMetricRepository(jdbcTemplate);
} }
@Bean
@ConditionalOnMissingBean
public VehicleStatRepository vehicleStatRepository(VehicleStatProperties props) {
// 无 JDBC 指标库时才回落本地文件,主要用于开发和单元验证。
return new FileVehicleStatRepository(Path.of(props.getFilePath()));
}
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
public VehicleStatRuleRepository vehicleStatRuleRepository() { public VehicleStatRuleRepository vehicleStatRuleRepository() {

View File

@@ -5,33 +5,11 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "lingniu.ingest.vehicle-stat") @ConfigurationProperties(prefix = "lingniu.ingest.vehicle-stat")
public class VehicleStatProperties { 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 String zoneId = "Asia/Shanghai";
private Jt808 jt808 = new Jt808(); 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() { public String getZoneId() {
return zoneId; 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.DailyMileageCalculator;
import com.lingniu.ingest.vehiclestat.DailyMileageStrategy; import com.lingniu.ingest.vehiclestat.DailyMileageStrategy;
import com.lingniu.ingest.vehiclestat.DailyVehicleStatService; import com.lingniu.ingest.vehiclestat.DailyVehicleStatService;
import com.lingniu.ingest.vehiclestat.FileVehicleStatRepository;
import com.lingniu.ingest.vehiclestat.JdbcVehicleStatMetricRepository; import com.lingniu.ingest.vehiclestat.JdbcVehicleStatMetricRepository;
import com.lingniu.ingest.vehiclestat.MileagePoint; import com.lingniu.ingest.vehiclestat.MileagePoint;
import com.lingniu.ingest.vehiclestat.VehicleStatController; import com.lingniu.ingest.vehiclestat.VehicleStatController;
@@ -71,17 +70,14 @@ class VehicleStatAutoConfigurationTest {
} }
@Test @Test
void createsFileRepositoryWhenNoRepositoryBeanExists() { void doesNotCreateRepositoryWithoutJdbcMetricStore() {
new ApplicationContextRunner() new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(VehicleStatAutoConfiguration.class)) .withConfiguration(AutoConfigurations.of(VehicleStatAutoConfiguration.class))
.withPropertyValues("lingniu.ingest.vehicle-stat.enabled=true") .withPropertyValues("lingniu.ingest.vehicle-stat.enabled=true")
.run(context -> { .run(context -> {
assertThat(context).hasSingleBean(VehicleStatRepository.class); assertThat(context).doesNotHaveBean(VehicleStatRepository.class);
assertThat(context).hasSingleBean(FileVehicleStatRepository.class); assertThat(context).doesNotHaveBean(DailyVehicleStatService.class);
assertThat(context).hasSingleBean(VehicleStatRuleRepository.class); assertThat(context).doesNotHaveBean(VehicleStatEventProcessor.class);
assertThat(context).hasSingleBean(DailyMileageCalculator.class);
assertThat(context).hasSingleBean(DailyVehicleStatService.class);
assertThat(context).hasSingleBean(VehicleStatEventProcessor.class);
}); });
} }
@@ -142,7 +138,7 @@ class VehicleStatAutoConfigurationTest {
} }
@Test @Test
void canForceFileRepositoryEvenWhenJdbcTemplateExists() { void ignoresFileRepositoryTypeAndKeepsJdbcMetricRepository() {
new ApplicationContextRunner() new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of( .withConfiguration(AutoConfigurations.of(
JdbcTemplateAutoConfiguration.class, JdbcTemplateAutoConfiguration.class,
@@ -156,8 +152,7 @@ class VehicleStatAutoConfigurationTest {
"lingniu.ingest.vehicle-stat.repository-type=file") "lingniu.ingest.vehicle-stat.repository-type=file")
.run(context -> { .run(context -> {
assertThat(context).hasSingleBean(VehicleStatRepository.class); assertThat(context).hasSingleBean(VehicleStatRepository.class);
assertThat(context).hasSingleBean(FileVehicleStatRepository.class); assertThat(context).hasSingleBean(JdbcVehicleStatMetricRepository.class);
assertThat(context).doesNotHaveBean(JdbcVehicleStatMetricRepository.class);
}); });
} }