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
|
## 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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
Reference in New Issue
Block a user