docs: add detailed 32960 pipeline comments

This commit is contained in:
kkfluous
2026-06-23 13:17:37 +08:00
parent ba68ffe061
commit a096e4ce0e
125 changed files with 493 additions and 14 deletions

View File

@@ -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();

View File

@@ -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(),

View File

@@ -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', '_');
}
}

View File

@@ -27,6 +27,7 @@ public final class VehicleStatEventProcessor {
return;
}
// 统计消费的是已经标准化后的 telemetry_snapshot不重新解析 RAW .bin。
OptionalDouble totalMileage = totalMileage(envelope);
if (totalMileage.isEmpty()) {
return;

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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() {