fix: calculate jt808 daily mileage from metric endpoints
This commit is contained in:
@@ -6,16 +6,16 @@ The analytics app can calculate daily mileage from JT808 telemetry only. It cons
|
||||
|
||||
## 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:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
```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
|
||||
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
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -344,11 +344,12 @@ Validation:
|
||||
### Daily Mileage
|
||||
|
||||
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
|
||||
latest valid total mileage rewrites the same metric row.
|
||||
information. The local-day minimum and maximum GPS total mileage values rewrite
|
||||
the same metric row, so ordered streaming and historical replay use one
|
||||
calculation path.
|
||||
|
||||
```text
|
||||
dailyMileage = today.lastTotalMileage - today.firstTotalMileage
|
||||
dailyMileage = today.maxTotalMileage - today.minTotalMileage
|
||||
calculationMethod = JT808_TOTAL_MILEAGE_DIFF
|
||||
```
|
||||
|
||||
|
||||
@@ -125,13 +125,14 @@ Responsibilities:
|
||||
- Calculate 808 daily mileage from the reported GPS total mileage only:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
- Store the metric in `vehicle_stat_metric` with
|
||||
`metric_key = daily_mileage_km`; the first and latest GPS total mileage used
|
||||
for the subtraction stay on the same metric row as calculation source columns.
|
||||
`metric_key = daily_mileage_km`; the local-day minimum and maximum GPS total
|
||||
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
|
||||
state-store mode. Restart recovery reads the same metric row, so it does not
|
||||
|
||||
@@ -60,7 +60,7 @@ public final class JdbcVehicleStatMetricRepository implements VehicleStatReposit
|
||||
DailyMileageState existingState = findDailyMileageState(normalizedVin, date).orElse(null);
|
||||
double firstTotalMileage = existingState == null || !Double.isFinite(existingState.firstTotalMileageKm())
|
||||
? totalMileageKm
|
||||
: existingState.firstTotalMileageKm();
|
||||
: Math.min(existingState.firstTotalMileageKm(), totalMileageKm);
|
||||
double latestTotalMileage = existingState == null || !Double.isFinite(existingState.latestTotalMileageKm())
|
||||
? totalMileageKm
|
||||
: Math.max(existingState.latestTotalMileageKm(), totalMileageKm);
|
||||
|
||||
@@ -116,6 +116,37 @@ class JdbcVehicleStatMetricRepositoryTest {
|
||||
""", 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
|
||||
void ignoresLegacySplitMileageStateMetricsWhenRecordingNewGpsTotalMileage() {
|
||||
JdbcTemplate jdbcTemplate = new JdbcTemplate(new DriverManagerDataSource(
|
||||
@@ -145,6 +176,11 @@ class JdbcVehicleStatMetricRepositoryTest {
|
||||
FROM vehicle_stat_metric
|
||||
WHERE vin = 'VIN001' AND metric_key = 'daily_mileage_km'
|
||||
""", 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
|
||||
|
||||
@@ -26,7 +26,7 @@ class VehicleStatRepositoryContractTest {
|
||||
|
||||
assertThat(plan)
|
||||
.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.")
|
||||
.doesNotContain("Redis `Jt808MileageStateStore`")
|
||||
.doesNotContain("REDIS_HOST")
|
||||
|
||||
Reference in New Issue
Block a user