refactor: store jt808 mileage as metric diff
This commit is contained in:
@@ -610,10 +610,10 @@ flowchart LR
|
||||
</tr>
|
||||
<tr>
|
||||
<td>modules/services/vehicle-stat-service</td>
|
||||
<td>消费 Kafka 全字段事件,抽取统计源点,按车辆规则计算每日里程等日统计;生产消费可使用 <code>tryIngest</code> 和自动装配的 <code>EnvelopeConsumerProcessor</code> 隔离坏 protobuf,缺少 telemetry_snapshot 的消息会标记为 SKIPPED 并进入死信出口。</td>
|
||||
<td>Kafka Protobuf Envelope、车辆统计规则。</td>
|
||||
<td>按 <code>vin + stat_date</code> 保存的日统计结果。</td>
|
||||
<td><span class="tag green">每日里程</span><span class="tag">规则配置</span></td>
|
||||
<td>消费 Kafka 全字段事件,808 每日里程只使用 0x0200 附加项 0x01 上报的总里程做当日首末差值;生产消费可使用 <code>tryIngest</code> 和自动装配的 <code>EnvelopeConsumerProcessor</code> 隔离坏 protobuf,缺少 telemetry_snapshot 的消息会标记为 SKIPPED 并进入死信出口。</td>
|
||||
<td>Kafka Protobuf Envelope、808 <code>total_mileage_km</code>。</td>
|
||||
<td><code>vehicle_stat_metric</code>:<code>metric_key=daily_mileage_km</code>、<code>calculation_method=JT808_TOTAL_MILEAGE_DIFF</code>。</td>
|
||||
<td><span class="tag green">每日里程</span><span class="tag">指标表</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>modules/apps/command-gateway</td>
|
||||
|
||||
@@ -12,6 +12,7 @@ The JT808 daily-mileage value is calculated from the GPS total mileage reported
|
||||
|
||||
```text
|
||||
daily_mileage_km = last_total_mileage_km - first_total_mileage_km
|
||||
calculation_method = JT808_TOTAL_MILEAGE_DIFF
|
||||
```
|
||||
|
||||
The metric is saved only after at least two ordered JT808 location points with valid `total_mileage_km` have been received for the same vehicle and local day. If the difference is negative, no metric row is saved for that state update.
|
||||
|
||||
@@ -8,11 +8,11 @@ Consume JT808 Kafka location events, keep rolling daily state, and write the der
|
||||
|
||||
- Source topic: `vehicle.event.jt808.v1`
|
||||
- Runtime app: `vehicle-analytics-app`
|
||||
- State: Redis or in-memory `Jt808MileageStateStore`
|
||||
- Runtime state: Redis `Jt808MileageStateStore`
|
||||
- Metric output: `VehicleStatRepository.saveDailyStat(...)`
|
||||
- Production metric storage: JDBC/MySQL `vehicle_stat_metric`
|
||||
- Local fallback storage: `VEHICLE_STAT_FILE_PATH/daily-stats.tsv`
|
||||
- Date boundary: `Asia/Shanghai`
|
||||
- Calculation method: `JT808_TOTAL_MILEAGE_DIFF`
|
||||
|
||||
JT808 daily mileage is calculated only from the GPS total mileage reported by JT808 location additional information:
|
||||
|
||||
|
||||
@@ -269,28 +269,20 @@ MYSQL_USERNAME
|
||||
MYSQL_PASSWORD
|
||||
```
|
||||
|
||||
`vehicle_daily_stat`
|
||||
Daily derived metrics are stored in the common metric table, not in a
|
||||
protocol-specific daily-mileage table.
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS vehicle_daily_stat (
|
||||
vin VARCHAR(32) NOT NULL,
|
||||
CREATE TABLE IF NOT EXISTS vehicle_stat_metric (
|
||||
vin VARCHAR(64) NOT NULL,
|
||||
stat_date DATE NOT NULL,
|
||||
platform_account VARCHAR(64),
|
||||
total_mileage_km DECIMAL(14,3),
|
||||
daily_mileage_km DECIMAL(14,3),
|
||||
daily_power_kwh DECIMAL(14,3),
|
||||
daily_hydrogen_kg DECIMAL(14,4),
|
||||
daily_hydrogen_kg_per_100km DECIMAL(14,4),
|
||||
first_event_time DATETIME(3),
|
||||
last_event_time DATETIME(3),
|
||||
first_total_mileage_km DECIMAL(14,3),
|
||||
last_total_mileage_km DECIMAL(14,3),
|
||||
last_soc_percent DECIMAL(8,3),
|
||||
sample_count BIGINT NOT NULL DEFAULT 0,
|
||||
late_sample_count BIGINT NOT NULL DEFAULT 0,
|
||||
calculation_quality VARCHAR(32) NOT NULL,
|
||||
updated_at DATETIME(3) NOT NULL,
|
||||
PRIMARY KEY (vin, stat_date)
|
||||
metric_key VARCHAR(64) NOT NULL,
|
||||
metric_value DECIMAL(18,6) NULL,
|
||||
metric_unit VARCHAR(16) NOT NULL,
|
||||
calculation_method VARCHAR(64) NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (vin, stat_date, metric_key)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
```
|
||||
|
||||
|
||||
@@ -159,15 +159,16 @@ Responsibilities:
|
||||
- Consume normalized telemetry events from Kafka.
|
||||
- Maintain day-level vehicle point state needed for statistics.
|
||||
- Run scheduled and event-triggered calculations.
|
||||
- Store daily results in a statistics database.
|
||||
- Support per-vehicle calculation rules.
|
||||
- Store derived results in the common metric table.
|
||||
|
||||
Initial daily mileage strategies:
|
||||
Current JT808 daily mileage strategy:
|
||||
|
||||
| Strategy | Formula |
|
||||
|---|---|
|
||||
| `CURRENT_LAST_MINUS_PREVIOUS_LAST` | Current day last valid total mileage minus previous day last valid total mileage |
|
||||
| `DAY_MAX_MINUS_DAY_MIN` | Current day max valid total mileage minus current day min valid total mileage |
|
||||
| `JT808_TOTAL_MILEAGE_DIFF` | Current local day last valid JT808 GPS total mileage minus first valid JT808 GPS total mileage |
|
||||
|
||||
The derived value is stored as `metric_key = daily_mileage_km` in
|
||||
`vehicle_stat_metric`; there is no protocol-specific JT808 daily-mileage table.
|
||||
|
||||
Hydrogen and electricity statistics use internal fields from the same event
|
||||
contract. Refueling and charging resets must be handled in statistics logic, not
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
package com.lingniu.ingest.vehiclestat;
|
||||
|
||||
public enum DailyMileageStrategy {
|
||||
CURRENT_LAST_MINUS_PREVIOUS_LAST,
|
||||
DAY_MAX_MINUS_DAY_MIN
|
||||
JT808_TOTAL_MILEAGE_DIFF;
|
||||
|
||||
public static DailyMileageStrategy fromStorage(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return JT808_TOTAL_MILEAGE_DIFF;
|
||||
}
|
||||
String normalized = value.trim();
|
||||
if ("CURRENT_LAST_MINUS_PREVIOUS_LAST".equals(normalized)) {
|
||||
return JT808_TOTAL_MILEAGE_DIFF;
|
||||
}
|
||||
return DailyMileageStrategy.valueOf(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ public final class JdbcVehicleStatMetricRepository implements VehicleStatReposit
|
||||
rs.getBigDecimal("metric_value") == null
|
||||
? OptionalDouble.empty()
|
||||
: OptionalDouble.of(rs.getBigDecimal("metric_value").doubleValue()),
|
||||
DailyMileageStrategy.valueOf(rs.getString("calculation_method"))),
|
||||
DailyMileageStrategy.fromStorage(rs.getString("calculation_method"))),
|
||||
normalizedVin, Date.valueOf(statDate), DAILY_MILEAGE_KEY);
|
||||
return rows.stream().findFirst();
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ public record VehicleDailyStatResult(String vin,
|
||||
dailyMileageKm = OptionalDouble.empty();
|
||||
}
|
||||
if (dailyMileageStrategy == null) {
|
||||
dailyMileageStrategy = DailyMileageStrategy.CURRENT_LAST_MINUS_PREVIOUS_LAST;
|
||||
dailyMileageStrategy = DailyMileageStrategy.JT808_TOTAL_MILEAGE_DIFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ public final class Jt808DailyMileageState {
|
||||
statVehicleId(),
|
||||
statDate,
|
||||
dailyMileage,
|
||||
DailyMileageStrategy.CURRENT_LAST_MINUS_PREVIOUS_LAST));
|
||||
DailyMileageStrategy.JT808_TOTAL_MILEAGE_DIFF));
|
||||
}
|
||||
|
||||
private void updateOdometer(Jt808LocationPoint point) {
|
||||
|
||||
@@ -23,12 +23,12 @@ class JdbcVehicleStatMetricRepositoryTest {
|
||||
"VIN001",
|
||||
LocalDate.of(2026, 6, 30),
|
||||
OptionalDouble.of(12.345),
|
||||
DailyMileageStrategy.CURRENT_LAST_MINUS_PREVIOUS_LAST));
|
||||
DailyMileageStrategy.JT808_TOTAL_MILEAGE_DIFF));
|
||||
repository.saveDailyStat(new VehicleDailyStatResult(
|
||||
"VIN001",
|
||||
LocalDate.of(2026, 6, 30),
|
||||
OptionalDouble.of(15.5),
|
||||
DailyMileageStrategy.CURRENT_LAST_MINUS_PREVIOUS_LAST));
|
||||
DailyMileageStrategy.JT808_TOTAL_MILEAGE_DIFF));
|
||||
|
||||
assertThat(repository.findDailyStat("VIN001", LocalDate.of(2026, 6, 30)))
|
||||
.isPresent()
|
||||
@@ -40,5 +40,33 @@ class JdbcVehicleStatMetricRepositoryTest {
|
||||
assertThat(jdbcTemplate.queryForObject(
|
||||
"SELECT metric_key FROM vehicle_stat_metric WHERE vin = 'VIN001'",
|
||||
String.class)).isEqualTo("daily_mileage_km");
|
||||
assertThat(jdbcTemplate.queryForObject(
|
||||
"SELECT calculation_method FROM vehicle_stat_metric WHERE vin = 'VIN001'",
|
||||
String.class)).isEqualTo("JT808_TOTAL_MILEAGE_DIFF");
|
||||
assertThat(jdbcTemplate.queryForObject("""
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.tables
|
||||
WHERE table_name = 'vehicle_daily_stat'
|
||||
""", Integer.class)).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void readsLegacyCalculationMethodAsTotalMileageDifference() {
|
||||
JdbcTemplate jdbcTemplate = new JdbcTemplate(new DriverManagerDataSource(
|
||||
"jdbc:h2:mem:vehicle_stat_metric_legacy;MODE=MySQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1",
|
||||
"sa",
|
||||
""));
|
||||
JdbcVehicleStatMetricRepository repository = new JdbcVehicleStatMetricRepository(jdbcTemplate);
|
||||
jdbcTemplate.update("""
|
||||
INSERT INTO vehicle_stat_metric
|
||||
(vin, stat_date, metric_key, metric_value, metric_unit, calculation_method)
|
||||
VALUES ('VIN001', DATE '2026-06-30', 'daily_mileage_km', 8.5, 'km', 'CURRENT_LAST_MINUS_PREVIOUS_LAST')
|
||||
""");
|
||||
|
||||
assertThat(repository.findDailyStat("VIN001", LocalDate.of(2026, 6, 30)))
|
||||
.isPresent()
|
||||
.get()
|
||||
.extracting(VehicleDailyStatResult::dailyMileageStrategy)
|
||||
.isEqualTo(DailyMileageStrategy.JT808_TOTAL_MILEAGE_DIFF);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,14 +21,15 @@ class VehicleStatControllerTest {
|
||||
"VIN001",
|
||||
LocalDate.parse("2026-06-22"),
|
||||
OptionalDouble.of(35.5),
|
||||
DailyMileageStrategy.CURRENT_LAST_MINUS_PREVIOUS_LAST));
|
||||
DailyMileageStrategy.JT808_TOTAL_MILEAGE_DIFF));
|
||||
MockMvc mvc = standaloneSetup(new VehicleStatController(repository)).build();
|
||||
|
||||
mvc.perform(get("/api/vehicle-stat/VIN001/daily").param("date", "2026-06-22"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.vin").value("VIN001"))
|
||||
.andExpect(jsonPath("$.statDate").value("2026-06-22"))
|
||||
.andExpect(jsonPath("$.dailyMileageKm").value(35.5));
|
||||
.andExpect(jsonPath("$.dailyMileageKm").value(35.5))
|
||||
.andExpect(jsonPath("$.dailyMileageStrategy").value("JT808_TOTAL_MILEAGE_DIFF"));
|
||||
}
|
||||
|
||||
private static final class InMemoryVehicleStatRepository implements VehicleStatRepository {
|
||||
|
||||
@@ -36,7 +36,7 @@ class Jt808MileageStreamProcessorTest {
|
||||
assertThat(latest.vin()).isEqualTo("VIN001");
|
||||
assertThat(latest.statDate()).isEqualTo(LocalDate.of(2026, 6, 30));
|
||||
assertThat(latest.dailyMileageKm()).hasValue(1.0);
|
||||
assertThat(latest.dailyMileageStrategy()).isEqualTo(DailyMileageStrategy.CURRENT_LAST_MINUS_PREVIOUS_LAST);
|
||||
assertThat(latest.dailyMileageStrategy()).isEqualTo(DailyMileageStrategy.JT808_TOTAL_MILEAGE_DIFF);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user