docs: add detailed 32960 pipeline comments
This commit is contained in:
@@ -31,6 +31,8 @@ public final class DailyMileageCalculator {
|
||||
return OptionalDouble.empty();
|
||||
}
|
||||
|
||||
// 两种策略都只基于同一 VIN 的里程点计算,不能跨车辆 join;
|
||||
// 上层 repository 负责按 VIN 过滤,避免大车队查询时扩大扫描面。
|
||||
double value = switch (strategy) {
|
||||
case CURRENT_LAST_MINUS_PREVIOUS_LAST -> currentLastMinusPreviousLast(statDate, points);
|
||||
case DAY_MAX_MINUS_DAY_MIN -> dayMaxMinusDayMin(statDate, points);
|
||||
@@ -39,6 +41,7 @@ public final class DailyMileageCalculator {
|
||||
}
|
||||
|
||||
private double currentLastMinusPreviousLast(LocalDate statDate, List<MileagePoint> points) {
|
||||
// 适用于累计里程单调递增的设备:当天最后一帧减前一日最后一帧。
|
||||
Optional<MileagePoint> previousLast = points.stream()
|
||||
.filter(point -> localDate(point).isBefore(statDate))
|
||||
.max(Comparator.comparing(MileagePoint::eventTime));
|
||||
@@ -53,6 +56,7 @@ public final class DailyMileageCalculator {
|
||||
}
|
||||
|
||||
private double dayMaxMinusDayMin(LocalDate statDate, List<MileagePoint> points) {
|
||||
// 兜底策略:仅使用当天数据,适合缺少前一日最后点但当天点足够的场景。
|
||||
List<MileagePoint> currentDay = points.stream()
|
||||
.filter(point -> localDate(point).isEqual(statDate))
|
||||
.toList();
|
||||
|
||||
@@ -29,6 +29,7 @@ public final class DailyVehicleStatService {
|
||||
|
||||
public Optional<VehicleDailyStatResult> calculateAndSave(String vin, LocalDate statDate) {
|
||||
VehicleStatRule rule = ruleRepository.ruleFor(vin);
|
||||
// 统计是从里程点二次计算出来的派生数据,不作为 32960 全字段历史查询的数据源。
|
||||
OptionalDouble dailyMileage = mileageCalculator.calculate(
|
||||
statDate,
|
||||
rule.dailyMileageStrategy(),
|
||||
|
||||
@@ -22,6 +22,7 @@ public final class FileVehicleStatRepository implements VehicleStatRepository {
|
||||
throw new IllegalArgumentException("root must not be null");
|
||||
}
|
||||
Path absoluteRoot = root.toAbsolutePath();
|
||||
// 当前实现是轻量本地 TSV,适合开发和小规模派生统计;生产历史全字段查询走 DuckDB RAW 索引。
|
||||
this.pointsFile = absoluteRoot.resolve("mileage-points.tsv");
|
||||
this.dailyStatsFile = absoluteRoot.resolve("daily-stats.tsv");
|
||||
}
|
||||
@@ -44,6 +45,7 @@ public final class FileVehicleStatRepository implements VehicleStatRepository {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
// statDate 由计算器按策略判断;这里返回该 VIN 的点,避免仓储层混入业务口径。
|
||||
out.add(new MileagePoint(Instant.parse(parts[1]), Double.parseDouble(parts[2])));
|
||||
} catch (RuntimeException ignored) {
|
||||
// Ignore corrupt rows instead of making the whole statistics API unavailable.
|
||||
@@ -118,6 +120,7 @@ public final class FileVehicleStatRepository implements VehicleStatRepository {
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new IllegalArgumentException("vin must not be blank");
|
||||
}
|
||||
// TSV 文件没有转义层,VIN 中的控制字符统一替换,防止破坏行结构。
|
||||
return value.trim().replace('\t', '_').replace('\n', '_').replace('\r', '_');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ public final class VehicleStatEventProcessor {
|
||||
return;
|
||||
}
|
||||
|
||||
// 统计消费的是已经标准化后的 telemetry_snapshot,不重新解析 RAW .bin。
|
||||
OptionalDouble totalMileage = totalMileage(envelope);
|
||||
if (totalMileage.isEmpty()) {
|
||||
return;
|
||||
|
||||
@@ -34,6 +34,7 @@ public final class VehicleStatEventSink implements EventSink {
|
||||
|
||||
@Override
|
||||
public boolean accepts(VehicleEvent event) {
|
||||
// 直接 EventSink 路径只服务老的 Realtime 派生统计;32960 RAW 历史链路不依赖它。
|
||||
return event instanceof VehicleEvent.Realtime realtime
|
||||
&& realtime.payload() != null
|
||||
&& realtime.payload().totalMileageKm() != null;
|
||||
|
||||
@@ -31,6 +31,7 @@ public class VehicleStatAutoConfiguration {
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public VehicleStatRepository vehicleStatRepository(VehicleStatProperties props) {
|
||||
// 派生统计默认落本地文件,便于独立开关;主历史库仍由 event-file-store 管理。
|
||||
return new FileVehicleStatRepository(Path.of(props.getFilePath()));
|
||||
}
|
||||
|
||||
@@ -91,6 +92,7 @@ public class VehicleStatAutoConfiguration {
|
||||
@ConditionalOnMissingBean(name = "vehicleStatEnvelopeConsumerProcessor")
|
||||
public EnvelopeConsumerProcessor vehicleStatEnvelopeConsumerProcessor(VehicleStatEnvelopeIngestor ingestor,
|
||||
EnvelopeDeadLetterSink deadLetterSink) {
|
||||
// Bean 名必须和 sink-mq 默认 binding 对齐,KafkaEnvelopeConsumerFactory 才能自动创建 worker。
|
||||
return new EnvelopeConsumerProcessor("vehicle-stat", ingestor, deadLetterSink);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@ 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/";
|
||||
|
||||
/** 统计自然日口径,默认按国内业务使用东八区。 */
|
||||
private String zoneId = "Asia/Shanghai";
|
||||
|
||||
public String getFilePath() {
|
||||
|
||||
Reference in New Issue
Block a user