fix: calculate jt808 daily mileage from metric endpoints

This commit is contained in:
lingniu
2026-07-01 09:52:24 +08:00
parent bb051391c2
commit 0236884f81
7 changed files with 53 additions and 15 deletions

View File

@@ -6,16 +6,16 @@ The analytics app can calculate daily mileage from JT808 telemetry only. It cons
## Storage ## Storage
There is no separate JT808 daily-mileage table and no Redis mileage state. The derived value is stored as one `daily_mileage_km` metric row in the common JDBC/MySQL table `vehicle_stat_metric`; the first and latest GPS total mileage values are kept on that same row as calculation source columns. There is no separate JT808 daily-mileage table and no Redis mileage state. The derived value is stored as one `daily_mileage_km` metric row in the common JDBC/MySQL table `vehicle_stat_metric`; the local-day minimum and maximum GPS total mileage values are kept on that same row as calculation source columns.
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:
```text ```text
daily_mileage_km = last_total_mileage_km - first_total_mileage_km daily_mileage_km = max_total_mileage_km - min_total_mileage_km
calculation_method = JT808_TOTAL_MILEAGE_DIFF calculation_method = JT808_TOTAL_MILEAGE_DIFF
``` ```
The first valid JT808 location point for a vehicle and local day stores the day start total on the metric row and writes `daily_mileage_km=0.0`. Later ordered points update the latest total on the same row and rewrite the derived `daily_mileage_km`. If a later total mileage is lower than the stored latest total, the previous latest total remains. The first valid JT808 location point for a vehicle and local day stores both calculation endpoints on the metric row and writes `daily_mileage_km=0.0`. Later ordered or replayed points update the lower endpoint when the reported GPS total mileage is smaller, update the upper endpoint when it is larger, and rewrite the derived `daily_mileage_km` on the same row.
## Runtime Settings ## Runtime Settings

View File

@@ -17,17 +17,17 @@ Consume JT808 Kafka location events and write the derived daily mileage into the
JT808 daily mileage is calculated only from the GPS total mileage reported by JT808 location additional information: JT808 daily mileage is calculated only from the GPS total mileage reported by JT808 location additional information:
```text ```text
daily_mileage_km = last_total_mileage_km - first_total_mileage_km daily_mileage_km = max_total_mileage_km - min_total_mileage_km
``` ```
The first valid local-day sample writes `daily_mileage_km=0.0`. Later samples update the same metric row with: The first valid local-day sample writes `daily_mileage_km=0.0`. Later ordered or replayed samples update the same metric row with:
```text ```text
metric_value = latest_total_mileage_km - first_total_mileage_km metric_value = max_total_mileage_km - min_total_mileage_km
metric_key = daily_mileage_km metric_key = daily_mileage_km
``` ```
The first and latest GPS total mileage values stay on that same metric row as calculation source columns so restarts recover from MySQL without Redis or another state table. The local-day minimum and maximum GPS total mileage values stay on that same metric row as calculation source columns so restarts recover from MySQL without Redis or another state table.
## Runtime Properties ## Runtime Properties

View File

@@ -344,11 +344,12 @@ Validation:
### Daily Mileage ### Daily Mileage
JT808 daily mileage uses the GPS total mileage reported in location additional JT808 daily mileage uses the GPS total mileage reported in location additional
information. The first valid sample of the local day is the baseline and the information. The local-day minimum and maximum GPS total mileage values rewrite
latest valid total mileage rewrites the same metric row. the same metric row, so ordered streaming and historical replay use one
calculation path.
```text ```text
dailyMileage = today.lastTotalMileage - today.firstTotalMileage dailyMileage = today.maxTotalMileage - today.minTotalMileage
calculationMethod = JT808_TOTAL_MILEAGE_DIFF calculationMethod = JT808_TOTAL_MILEAGE_DIFF
``` ```

View File

@@ -125,13 +125,14 @@ Responsibilities:
- Calculate 808 daily mileage from the reported GPS total mileage only: - Calculate 808 daily mileage from the reported GPS total mileage only:
```text ```text
daily_mileage_km = last_total_mileage_km - first_total_mileage_km daily_mileage_km = max_total_mileage_km - min_total_mileage_km
calculation_method = JT808_TOTAL_MILEAGE_DIFF calculation_method = JT808_TOTAL_MILEAGE_DIFF
``` ```
- Store the metric in `vehicle_stat_metric` with - Store the metric in `vehicle_stat_metric` with
`metric_key = daily_mileage_km`; the first and latest GPS total mileage used `metric_key = daily_mileage_km`; the local-day minimum and maximum GPS total
for the subtraction stay on the same metric row as calculation source columns. mileage used for the subtraction stay on the same metric row as calculation
source columns.
There is no JT808-specific daily mileage table and no in-memory production There is no JT808-specific daily mileage table and no in-memory production
state-store mode. Restart recovery reads the same metric row, so it does not state-store mode. Restart recovery reads the same metric row, so it does not

View File

@@ -60,7 +60,7 @@ public final class JdbcVehicleStatMetricRepository implements VehicleStatReposit
DailyMileageState existingState = findDailyMileageState(normalizedVin, date).orElse(null); DailyMileageState existingState = findDailyMileageState(normalizedVin, date).orElse(null);
double firstTotalMileage = existingState == null || !Double.isFinite(existingState.firstTotalMileageKm()) double firstTotalMileage = existingState == null || !Double.isFinite(existingState.firstTotalMileageKm())
? totalMileageKm ? totalMileageKm
: existingState.firstTotalMileageKm(); : Math.min(existingState.firstTotalMileageKm(), totalMileageKm);
double latestTotalMileage = existingState == null || !Double.isFinite(existingState.latestTotalMileageKm()) double latestTotalMileage = existingState == null || !Double.isFinite(existingState.latestTotalMileageKm())
? totalMileageKm ? totalMileageKm
: Math.max(existingState.latestTotalMileageKm(), totalMileageKm); : Math.max(existingState.latestTotalMileageKm(), totalMileageKm);

View File

@@ -116,6 +116,37 @@ class JdbcVehicleStatMetricRepositoryTest {
""", Double.class)).isEqualTo(9.5); """, Double.class)).isEqualTo(9.5);
} }
@Test
void recalculatesJt808MileageWhenEarlierLowerMileageArrivesAfterRestart() {
JdbcTemplate jdbcTemplate = new JdbcTemplate(new DriverManagerDataSource(
"jdbc:h2:mem:vehicle_stat_metric_jt808_replay_lower;MODE=MySQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1",
"sa",
""));
JdbcVehicleStatMetricRepository repository = new JdbcVehicleStatMetricRepository(jdbcTemplate);
repository.recordDailyMileageSample("VIN001", LocalDate.of(2026, 7, 1), 1010.0).orElseThrow();
JdbcVehicleStatMetricRepository restartedRepository = new JdbcVehicleStatMetricRepository(jdbcTemplate);
VehicleDailyStatResult replayedEarlierSample = restartedRepository.recordDailyMileageSample(
"VIN001", LocalDate.of(2026, 7, 1), 1000.5).orElseThrow();
assertThat(replayedEarlierSample.dailyMileageKm()).hasValue(9.5);
assertThat(jdbcTemplate.queryForObject("""
SELECT first_total_mileage_km
FROM vehicle_stat_metric
WHERE vin = 'VIN001' AND metric_key = 'daily_mileage_km'
""", Double.class)).isEqualTo(1000.5);
assertThat(jdbcTemplate.queryForObject("""
SELECT latest_total_mileage_km
FROM vehicle_stat_metric
WHERE vin = 'VIN001' AND metric_key = 'daily_mileage_km'
""", Double.class)).isEqualTo(1010.0);
assertThat(jdbcTemplate.queryForObject("""
SELECT COUNT(*)
FROM vehicle_stat_metric
WHERE vin = 'VIN001' AND stat_date = DATE '2026-07-01'
""", Integer.class)).isOne();
}
@Test @Test
void ignoresLegacySplitMileageStateMetricsWhenRecordingNewGpsTotalMileage() { void ignoresLegacySplitMileageStateMetricsWhenRecordingNewGpsTotalMileage() {
JdbcTemplate jdbcTemplate = new JdbcTemplate(new DriverManagerDataSource( JdbcTemplate jdbcTemplate = new JdbcTemplate(new DriverManagerDataSource(
@@ -145,6 +176,11 @@ class JdbcVehicleStatMetricRepositoryTest {
FROM vehicle_stat_metric FROM vehicle_stat_metric
WHERE vin = 'VIN001' AND metric_key = 'daily_mileage_km' WHERE vin = 'VIN001' AND metric_key = 'daily_mileage_km'
""", Double.class)).isEqualTo(110.0); """, Double.class)).isEqualTo(110.0);
assertThat(jdbcTemplate.queryForObject("""
SELECT COUNT(*)
FROM vehicle_stat_metric
WHERE vin = 'VIN001' AND stat_date = DATE '2026-07-01'
""", Integer.class)).isOne();
} }
@Test @Test

View File

@@ -26,7 +26,7 @@ class VehicleStatRepositoryContractTest {
assertThat(plan) assertThat(plan)
.contains("Runtime state: none outside `vehicle_stat_metric`") .contains("Runtime state: none outside `vehicle_stat_metric`")
.contains("daily_mileage_km = last_total_mileage_km - first_total_mileage_km") .contains("daily_mileage_km = max_total_mileage_km - min_total_mileage_km")
.contains("Do not create or write a protocol-specific JT808 daily-mileage table.") .contains("Do not create or write a protocol-specific JT808 daily-mileage table.")
.doesNotContain("Redis `Jt808MileageStateStore`") .doesNotContain("Redis `Jt808MileageStateStore`")
.doesNotContain("REDIS_HOST") .doesNotContain("REDIS_HOST")