From 0fc91f512c368d82ba6877ed3120fd11d5ba99c5 Mon Sep 17 00:00:00 2001 From: lingniu Date: Wed, 1 Jul 2026 01:56:16 +0800 Subject: [PATCH] refactor: store runtime identity and mileage in mysql metrics --- README.md | 2 +- .../gb32960-service-split-runbook.md | 1 - .../operations/jt808-daily-mileage-runbook.md | 5 +-- .../vehicle-ingest-tdengine-verification.md | 4 +- modules/apps/gb32960-ingest-app/pom.xml | 4 ++ .../src/main/resources/application.yml | 10 +++-- .../Gb32960IngestAppCompositionTest.java | 6 +++ .../Gb32960IngestAppDefaultsTest.java | 3 ++ .../src/main/resources/application.yml | 4 +- .../Jt808IngestAppCompositionTest.java | 4 ++ .../jt808app/Jt808IngestAppDefaultsTest.java | 2 +- .../src/main/resources/application.yml | 2 - .../VehicleAnalyticsAppCompositionTest.java | 13 ++----- .../VehicleAnalyticsAppDefaultsTest.java | 1 - .../src/main/resources/application.yml | 13 +++---- .../YutongMqttAppCompositionTest.java | 4 ++ .../YutongMqttAppDefaultsTest.java | 5 ++- .../VehicleIdentityAutoConfiguration.java | 23 +---------- .../config/VehicleIdentityProperties.java | 12 +----- .../VehicleIdentityAutoConfigurationTest.java | 39 +++++++++---------- .../config/VehicleStatAutoConfiguration.java | 11 ------ .../config/VehicleStatProperties.java | 22 ----------- .../VehicleStatAutoConfigurationTest.java | 17 +++----- 23 files changed, 75 insertions(+), 132 deletions(-) diff --git a/README.md b/README.md index 48bbbba5..a381985f 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/operations/gb32960-service-split-runbook.md b/docs/operations/gb32960-service-split-runbook.md index 4ccc345b..a5b87e78 100644 --- a/docs/operations/gb32960-service-split-runbook.md +++ b/docs/operations/gb32960-service-split-runbook.md @@ -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 ``` diff --git a/docs/operations/jt808-daily-mileage-runbook.md b/docs/operations/jt808-daily-mileage-runbook.md index 52d5b2ad..2b97eedc 100644 --- a/docs/operations/jt808-daily-mileage-runbook.md +++ b/docs/operations/jt808-daily-mileage-runbook.md @@ -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= MYSQL_USERNAME= diff --git a/docs/operations/vehicle-ingest-tdengine-verification.md b/docs/operations/vehicle-ingest-tdengine-verification.md index 82c1e3d8..73369a39 100644 --- a/docs/operations/vehicle-ingest-tdengine-verification.md +++ b/docs/operations/vehicle-ingest-tdengine-verification.md @@ -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 / 轻压测工具 diff --git a/modules/apps/gb32960-ingest-app/pom.xml b/modules/apps/gb32960-ingest-app/pom.xml index c8a70b82..dda16f3e 100644 --- a/modules/apps/gb32960-ingest-app/pom.xml +++ b/modules/apps/gb32960-ingest-app/pom.xml @@ -25,6 +25,10 @@ com.lingniu.ingest observability + + com.lingniu.ingest + vehicle-identity + com.lingniu.ingest protocol-gb32960 diff --git a/modules/apps/gb32960-ingest-app/src/main/resources/application.yml b/modules/apps/gb32960-ingest-app/src/main/resources/application.yml index f8af34cc..b5a87a97 100644 --- a/modules/apps/gb32960-ingest-app/src/main/resources/application.yml +++ b/modules/apps/gb32960-ingest-app/src/main/resources/application.yml @@ -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} diff --git a/modules/apps/gb32960-ingest-app/src/test/java/com/lingniu/ingest/gb32960app/Gb32960IngestAppCompositionTest.java b/modules/apps/gb32960-ingest-app/src/test/java/com/lingniu/ingest/gb32960app/Gb32960IngestAppCompositionTest.java index cc521c67..90296655 100644 --- a/modules/apps/gb32960-ingest-app/src/test/java/com/lingniu/ingest/gb32960app/Gb32960IngestAppCompositionTest.java +++ b/modules/apps/gb32960-ingest-app/src/test/java/com/lingniu/ingest/gb32960app/Gb32960IngestAppCompositionTest.java @@ -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); diff --git a/modules/apps/gb32960-ingest-app/src/test/java/com/lingniu/ingest/gb32960app/Gb32960IngestAppDefaultsTest.java b/modules/apps/gb32960-ingest-app/src/test/java/com/lingniu/ingest/gb32960app/Gb32960IngestAppDefaultsTest.java index 4e3c32bf..f277d699 100644 --- a/modules/apps/gb32960-ingest-app/src/test/java/com/lingniu/ingest/gb32960app/Gb32960IngestAppDefaultsTest.java +++ b/modules/apps/gb32960-ingest-app/src/test/java/com/lingniu/ingest/gb32960app/Gb32960IngestAppDefaultsTest.java @@ -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) diff --git a/modules/apps/jt808-ingest-app/src/main/resources/application.yml b/modules/apps/jt808-ingest-app/src/main/resources/application.yml index 3b849c94..533c5dc2 100644 --- a/modules/apps/jt808-ingest-app/src/main/resources/application.yml +++ b/modules/apps/jt808-ingest-app/src/main/resources/application.yml @@ -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} diff --git a/modules/apps/jt808-ingest-app/src/test/java/com/lingniu/ingest/jt808app/Jt808IngestAppCompositionTest.java b/modules/apps/jt808-ingest-app/src/test/java/com/lingniu/ingest/jt808app/Jt808IngestAppCompositionTest.java index b26c87b1..52a1c6e5 100644 --- a/modules/apps/jt808-ingest-app/src/test/java/com/lingniu/ingest/jt808app/Jt808IngestAppCompositionTest.java +++ b/modules/apps/jt808-ingest-app/src/test/java/com/lingniu/ingest/jt808app/Jt808IngestAppCompositionTest.java @@ -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); diff --git a/modules/apps/jt808-ingest-app/src/test/java/com/lingniu/ingest/jt808app/Jt808IngestAppDefaultsTest.java b/modules/apps/jt808-ingest-app/src/test/java/com/lingniu/ingest/jt808app/Jt808IngestAppDefaultsTest.java index 5e292ef3..fffa7651 100644 --- a/modules/apps/jt808-ingest-app/src/test/java/com/lingniu/ingest/jt808app/Jt808IngestAppDefaultsTest.java +++ b/modules/apps/jt808-ingest-app/src/test/java/com/lingniu/ingest/jt808app/Jt808IngestAppDefaultsTest.java @@ -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", diff --git a/modules/apps/vehicle-analytics-app/src/main/resources/application.yml b/modules/apps/vehicle-analytics-app/src/main/resources/application.yml index 55e7af8a..026b10a2 100644 --- a/modules/apps/vehicle-analytics-app/src/main/resources/application.yml +++ b/modules/apps/vehicle-analytics-app/src/main/resources/application.yml @@ -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} diff --git a/modules/apps/vehicle-analytics-app/src/test/java/com/lingniu/ingest/analyticsapp/VehicleAnalyticsAppCompositionTest.java b/modules/apps/vehicle-analytics-app/src/test/java/com/lingniu/ingest/analyticsapp/VehicleAnalyticsAppCompositionTest.java index 110345cf..d6257f30 100644 --- a/modules/apps/vehicle-analytics-app/src/test/java/com/lingniu/ingest/analyticsapp/VehicleAnalyticsAppCompositionTest.java +++ b/modules/apps/vehicle-analytics-app/src/test/java/com/lingniu/ingest/analyticsapp/VehicleAnalyticsAppCompositionTest.java @@ -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); diff --git a/modules/apps/vehicle-analytics-app/src/test/java/com/lingniu/ingest/analyticsapp/VehicleAnalyticsAppDefaultsTest.java b/modules/apps/vehicle-analytics-app/src/test/java/com/lingniu/ingest/analyticsapp/VehicleAnalyticsAppDefaultsTest.java index 66bad7b6..5319e64a 100644 --- a/modules/apps/vehicle-analytics-app/src/test/java/com/lingniu/ingest/analyticsapp/VehicleAnalyticsAppDefaultsTest.java +++ b/modules/apps/vehicle-analytics-app/src/test/java/com/lingniu/ingest/analyticsapp/VehicleAnalyticsAppDefaultsTest.java @@ -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( diff --git a/modules/apps/yutong-mqtt-app/src/main/resources/application.yml b/modules/apps/yutong-mqtt-app/src/main/resources/application.yml index a6efb018..fd10d874 100644 --- a/modules/apps/yutong-mqtt-app/src/main/resources/application.yml +++ b/modules/apps/yutong-mqtt-app/src/main/resources/application.yml @@ -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} diff --git a/modules/apps/yutong-mqtt-app/src/test/java/com/lingniu/ingest/yutongmqttapp/YutongMqttAppCompositionTest.java b/modules/apps/yutong-mqtt-app/src/test/java/com/lingniu/ingest/yutongmqttapp/YutongMqttAppCompositionTest.java index 11068ab4..019aff20 100644 --- a/modules/apps/yutong-mqtt-app/src/test/java/com/lingniu/ingest/yutongmqttapp/YutongMqttAppCompositionTest.java +++ b/modules/apps/yutong-mqtt-app/src/test/java/com/lingniu/ingest/yutongmqttapp/YutongMqttAppCompositionTest.java @@ -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); diff --git a/modules/apps/yutong-mqtt-app/src/test/java/com/lingniu/ingest/yutongmqttapp/YutongMqttAppDefaultsTest.java b/modules/apps/yutong-mqtt-app/src/test/java/com/lingniu/ingest/yutongmqttapp/YutongMqttAppDefaultsTest.java index efcafb4c..f30e1ded 100644 --- a/modules/apps/yutong-mqtt-app/src/test/java/com/lingniu/ingest/yutongmqttapp/YutongMqttAppDefaultsTest.java +++ b/modules/apps/yutong-mqtt-app/src/test/java/com/lingniu/ingest/yutongmqttapp/YutongMqttAppDefaultsTest.java @@ -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) diff --git a/modules/core/vehicle-identity/src/main/java/com/lingniu/ingest/identity/config/VehicleIdentityAutoConfiguration.java b/modules/core/vehicle-identity/src/main/java/com/lingniu/ingest/identity/config/VehicleIdentityAutoConfiguration.java index c45397fe..59257be4 100644 --- a/modules/core/vehicle-identity/src/main/java/com/lingniu/ingest/identity/config/VehicleIdentityAutoConfiguration.java +++ b/modules/core/vehicle-identity/src/main/java/com/lingniu/ingest/identity/config/VehicleIdentityAutoConfiguration.java @@ -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(); - } } diff --git a/modules/core/vehicle-identity/src/main/java/com/lingniu/ingest/identity/config/VehicleIdentityProperties.java b/modules/core/vehicle-identity/src/main/java/com/lingniu/ingest/identity/config/VehicleIdentityProperties.java index 38aeb325..c47f7cc5 100644 --- a/modules/core/vehicle-identity/src/main/java/com/lingniu/ingest/identity/config/VehicleIdentityProperties.java +++ b/modules/core/vehicle-identity/src/main/java/com/lingniu/ingest/identity/config/VehicleIdentityProperties.java @@ -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"; diff --git a/modules/core/vehicle-identity/src/test/java/com/lingniu/ingest/identity/config/VehicleIdentityAutoConfigurationTest.java b/modules/core/vehicle-identity/src/test/java/com/lingniu/ingest/identity/config/VehicleIdentityAutoConfigurationTest.java index 530646b7..79bfe1f1 100644 --- a/modules/core/vehicle-identity/src/test/java/com/lingniu/ingest/identity/config/VehicleIdentityAutoConfigurationTest.java +++ b/modules/core/vehicle-identity/src/test/java/com/lingniu/ingest/identity/config/VehicleIdentityAutoConfigurationTest.java @@ -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); + }); + } } diff --git a/modules/services/vehicle-stat-service/src/main/java/com/lingniu/ingest/vehiclestat/config/VehicleStatAutoConfiguration.java b/modules/services/vehicle-stat-service/src/main/java/com/lingniu/ingest/vehiclestat/config/VehicleStatAutoConfiguration.java index cfdfd54b..8e6e2739 100644 --- a/modules/services/vehicle-stat-service/src/main/java/com/lingniu/ingest/vehiclestat/config/VehicleStatAutoConfiguration.java +++ b/modules/services/vehicle-stat-service/src/main/java/com/lingniu/ingest/vehiclestat/config/VehicleStatAutoConfiguration.java @@ -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() { diff --git a/modules/services/vehicle-stat-service/src/main/java/com/lingniu/ingest/vehiclestat/config/VehicleStatProperties.java b/modules/services/vehicle-stat-service/src/main/java/com/lingniu/ingest/vehiclestat/config/VehicleStatProperties.java index a5fdaadd..c184797f 100644 --- a/modules/services/vehicle-stat-service/src/main/java/com/lingniu/ingest/vehiclestat/config/VehicleStatProperties.java +++ b/modules/services/vehicle-stat-service/src/main/java/com/lingniu/ingest/vehiclestat/config/VehicleStatProperties.java @@ -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; } diff --git a/modules/services/vehicle-stat-service/src/test/java/com/lingniu/ingest/vehiclestat/config/VehicleStatAutoConfigurationTest.java b/modules/services/vehicle-stat-service/src/test/java/com/lingniu/ingest/vehiclestat/config/VehicleStatAutoConfigurationTest.java index 9ccaf3be..8f467cb1 100644 --- a/modules/services/vehicle-stat-service/src/test/java/com/lingniu/ingest/vehiclestat/config/VehicleStatAutoConfigurationTest.java +++ b/modules/services/vehicle-stat-service/src/test/java/com/lingniu/ingest/vehiclestat/config/VehicleStatAutoConfigurationTest.java @@ -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); }); }