feat: make gb32960 archive history query production ready
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
package com.lingniu.ingest.vehiclestat;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalDouble;
|
||||
|
||||
public final class DailyMileageCalculator {
|
||||
|
||||
private final ZoneId zoneId;
|
||||
|
||||
public DailyMileageCalculator(ZoneId zoneId) {
|
||||
if (zoneId == null) {
|
||||
throw new IllegalArgumentException("zoneId must not be null");
|
||||
}
|
||||
this.zoneId = zoneId;
|
||||
}
|
||||
|
||||
public OptionalDouble calculate(LocalDate statDate,
|
||||
DailyMileageStrategy strategy,
|
||||
List<MileagePoint> points) {
|
||||
if (statDate == null) {
|
||||
throw new IllegalArgumentException("statDate must not be null");
|
||||
}
|
||||
if (strategy == null) {
|
||||
throw new IllegalArgumentException("strategy must not be null");
|
||||
}
|
||||
if (points == null || points.isEmpty()) {
|
||||
return OptionalDouble.empty();
|
||||
}
|
||||
|
||||
double value = switch (strategy) {
|
||||
case CURRENT_LAST_MINUS_PREVIOUS_LAST -> currentLastMinusPreviousLast(statDate, points);
|
||||
case DAY_MAX_MINUS_DAY_MIN -> dayMaxMinusDayMin(statDate, points);
|
||||
};
|
||||
return Double.isFinite(value) && value >= 0 ? OptionalDouble.of(value) : OptionalDouble.empty();
|
||||
}
|
||||
|
||||
private double currentLastMinusPreviousLast(LocalDate statDate, List<MileagePoint> points) {
|
||||
Optional<MileagePoint> previousLast = points.stream()
|
||||
.filter(point -> localDate(point).isBefore(statDate))
|
||||
.max(Comparator.comparing(MileagePoint::eventTime));
|
||||
Optional<MileagePoint> currentLast = points.stream()
|
||||
.filter(point -> localDate(point).isEqual(statDate))
|
||||
.max(Comparator.comparing(MileagePoint::eventTime));
|
||||
|
||||
if (previousLast.isEmpty() || currentLast.isEmpty()) {
|
||||
return Double.NaN;
|
||||
}
|
||||
return currentLast.get().totalMileageKm() - previousLast.get().totalMileageKm();
|
||||
}
|
||||
|
||||
private double dayMaxMinusDayMin(LocalDate statDate, List<MileagePoint> points) {
|
||||
List<MileagePoint> currentDay = points.stream()
|
||||
.filter(point -> localDate(point).isEqual(statDate))
|
||||
.toList();
|
||||
if (currentDay.size() < 2) {
|
||||
return Double.NaN;
|
||||
}
|
||||
|
||||
double min = currentDay.stream().mapToDouble(MileagePoint::totalMileageKm).min().orElse(Double.NaN);
|
||||
double max = currentDay.stream().mapToDouble(MileagePoint::totalMileageKm).max().orElse(Double.NaN);
|
||||
return max - min;
|
||||
}
|
||||
|
||||
private LocalDate localDate(MileagePoint point) {
|
||||
return point.eventTime().atZone(zoneId).toLocalDate();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.lingniu.ingest.vehiclestat;
|
||||
|
||||
public enum DailyMileageStrategy {
|
||||
CURRENT_LAST_MINUS_PREVIOUS_LAST,
|
||||
DAY_MAX_MINUS_DAY_MIN
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.lingniu.ingest.vehiclestat;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalDouble;
|
||||
|
||||
public final class DailyVehicleStatService {
|
||||
|
||||
private final VehicleStatRepository repository;
|
||||
private final VehicleStatRuleRepository ruleRepository;
|
||||
private final DailyMileageCalculator mileageCalculator;
|
||||
|
||||
public DailyVehicleStatService(VehicleStatRepository repository,
|
||||
VehicleStatRuleRepository ruleRepository,
|
||||
DailyMileageCalculator mileageCalculator) {
|
||||
if (repository == null) {
|
||||
throw new IllegalArgumentException("repository must not be null");
|
||||
}
|
||||
if (ruleRepository == null) {
|
||||
throw new IllegalArgumentException("ruleRepository must not be null");
|
||||
}
|
||||
if (mileageCalculator == null) {
|
||||
throw new IllegalArgumentException("mileageCalculator must not be null");
|
||||
}
|
||||
this.repository = repository;
|
||||
this.ruleRepository = ruleRepository;
|
||||
this.mileageCalculator = mileageCalculator;
|
||||
}
|
||||
|
||||
public Optional<VehicleDailyStatResult> calculateAndSave(String vin, LocalDate statDate) {
|
||||
VehicleStatRule rule = ruleRepository.ruleFor(vin);
|
||||
OptionalDouble dailyMileage = mileageCalculator.calculate(
|
||||
statDate,
|
||||
rule.dailyMileageStrategy(),
|
||||
repository.mileagePoints(vin, statDate));
|
||||
if (dailyMileage.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
VehicleDailyStatResult result = new VehicleDailyStatResult(
|
||||
vin,
|
||||
statDate,
|
||||
dailyMileage,
|
||||
rule.dailyMileageStrategy());
|
||||
repository.saveDailyStat(result);
|
||||
return Optional.of(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.lingniu.ingest.vehiclestat;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalDouble;
|
||||
|
||||
public final class FileVehicleStatRepository implements VehicleStatRepository {
|
||||
|
||||
private final Path pointsFile;
|
||||
private final Path dailyStatsFile;
|
||||
|
||||
public FileVehicleStatRepository(Path root) {
|
||||
if (root == null) {
|
||||
throw new IllegalArgumentException("root must not be null");
|
||||
}
|
||||
Path absoluteRoot = root.toAbsolutePath();
|
||||
this.pointsFile = absoluteRoot.resolve("mileage-points.tsv");
|
||||
this.dailyStatsFile = absoluteRoot.resolve("daily-stats.tsv");
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void appendMileagePoint(String vin, MileagePoint point) {
|
||||
if (point == null) {
|
||||
throw new IllegalArgumentException("point must not be null");
|
||||
}
|
||||
appendLine(pointsFile, clean(vin) + '\t' + point.eventTime() + '\t' + point.totalMileageKm());
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized List<MileagePoint> mileagePoints(String vin, LocalDate statDate) {
|
||||
String normalizedVin = clean(vin);
|
||||
List<MileagePoint> out = new ArrayList<>();
|
||||
for (String line : readLines(pointsFile)) {
|
||||
String[] parts = line.split("\\t", -1);
|
||||
if (parts.length != 3 || !normalizedVin.equals(parts[0])) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
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.
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void saveDailyStat(VehicleDailyStatResult result) {
|
||||
if (result == null) {
|
||||
throw new IllegalArgumentException("result must not be null");
|
||||
}
|
||||
String mileage = result.dailyMileageKm().isPresent()
|
||||
? Double.toString(result.dailyMileageKm().getAsDouble())
|
||||
: "";
|
||||
appendLine(dailyStatsFile,
|
||||
clean(result.vin()) + '\t'
|
||||
+ result.statDate() + '\t'
|
||||
+ result.dailyMileageStrategy().name() + '\t'
|
||||
+ mileage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized Optional<VehicleDailyStatResult> findDailyStat(String vin, LocalDate statDate) {
|
||||
String normalizedVin = clean(vin);
|
||||
VehicleDailyStatResult latest = null;
|
||||
for (String line : readLines(dailyStatsFile)) {
|
||||
String[] parts = line.split("\\t", -1);
|
||||
if (parts.length != 4 || !normalizedVin.equals(parts[0])) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
LocalDate rowDate = LocalDate.parse(parts[1]);
|
||||
if (!rowDate.equals(statDate)) {
|
||||
continue;
|
||||
}
|
||||
DailyMileageStrategy strategy = DailyMileageStrategy.valueOf(parts[2]);
|
||||
OptionalDouble mileage = parts[3].isBlank()
|
||||
? OptionalDouble.empty()
|
||||
: OptionalDouble.of(Double.parseDouble(parts[3]));
|
||||
latest = new VehicleDailyStatResult(normalizedVin, rowDate, mileage, strategy);
|
||||
} catch (RuntimeException ignored) {
|
||||
// Ignore corrupt rows; later valid rows can still provide the answer.
|
||||
}
|
||||
}
|
||||
return Optional.ofNullable(latest);
|
||||
}
|
||||
|
||||
private static void appendLine(Path file, String line) {
|
||||
try {
|
||||
Files.createDirectories(file.getParent());
|
||||
Files.writeString(file, line + System.lineSeparator(), StandardCharsets.UTF_8,
|
||||
StandardOpenOption.CREATE, StandardOpenOption.APPEND);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("write vehicle stat file failed: " + file, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<String> readLines(Path file) {
|
||||
if (!Files.isRegularFile(file)) {
|
||||
return List.of();
|
||||
}
|
||||
try {
|
||||
return Files.readAllLines(file, StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("read vehicle stat file failed: " + file, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String clean(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new IllegalArgumentException("vin must not be blank");
|
||||
}
|
||||
return value.trim().replace('\t', '_').replace('\n', '_').replace('\r', '_');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.lingniu.ingest.vehiclestat;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
public record MileagePoint(Instant eventTime, double totalMileageKm) {
|
||||
|
||||
public MileagePoint {
|
||||
if (eventTime == null) {
|
||||
throw new IllegalArgumentException("eventTime must not be null");
|
||||
}
|
||||
if (!Double.isFinite(totalMileageKm)) {
|
||||
throw new IllegalArgumentException("totalMileageKm must be finite");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.lingniu.ingest.vehiclestat;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.OptionalDouble;
|
||||
|
||||
public record VehicleDailyStatResult(String vin,
|
||||
LocalDate statDate,
|
||||
OptionalDouble dailyMileageKm,
|
||||
DailyMileageStrategy dailyMileageStrategy) {
|
||||
|
||||
public VehicleDailyStatResult {
|
||||
if (vin == null || vin.isBlank()) {
|
||||
throw new IllegalArgumentException("vin must not be blank");
|
||||
}
|
||||
if (statDate == null) {
|
||||
throw new IllegalArgumentException("statDate must not be null");
|
||||
}
|
||||
if (dailyMileageKm == null) {
|
||||
dailyMileageKm = OptionalDouble.empty();
|
||||
}
|
||||
if (dailyMileageStrategy == null) {
|
||||
dailyMileageStrategy = DailyMileageStrategy.CURRENT_LAST_MINUS_PREVIOUS_LAST;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.lingniu.ingest.vehiclestat;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@ConditionalOnProperty(prefix = "lingniu.ingest.vehicle-stat", name = "enabled", havingValue = "true")
|
||||
@ConditionalOnBean(DailyVehicleStatService.class)
|
||||
@RequestMapping(path = "/api/vehicle-stat", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public final class VehicleStatController {
|
||||
|
||||
private final VehicleStatRepository repository;
|
||||
private final DailyVehicleStatService dailyStatService;
|
||||
|
||||
public VehicleStatController(VehicleStatRepository repository,
|
||||
DailyVehicleStatService dailyStatService) {
|
||||
if (repository == null) {
|
||||
throw new IllegalArgumentException("repository must not be null");
|
||||
}
|
||||
if (dailyStatService == null) {
|
||||
throw new IllegalArgumentException("dailyStatService must not be null");
|
||||
}
|
||||
this.repository = repository;
|
||||
this.dailyStatService = dailyStatService;
|
||||
}
|
||||
|
||||
@GetMapping("/{vin}/daily")
|
||||
public ResponseEntity<Map<String, Object>> daily(
|
||||
@PathVariable String vin,
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date,
|
||||
@RequestParam(defaultValue = "true") boolean calculateIfMissing) {
|
||||
var result = repository.findDailyStat(vin, date)
|
||||
.or(() -> calculateIfMissing ? dailyStatService.calculateAndSave(vin, date) : java.util.Optional.empty());
|
||||
return result.map(stat -> ResponseEntity.ok(toJson(stat)))
|
||||
.orElseGet(() -> ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
private static Map<String, Object> toJson(VehicleDailyStatResult stat) {
|
||||
Map<String, Object> json = new LinkedHashMap<>();
|
||||
json.put("vin", stat.vin());
|
||||
json.put("statDate", stat.statDate().toString());
|
||||
json.put("dailyMileageKm", stat.dailyMileageKm().isPresent()
|
||||
? stat.dailyMileageKm().getAsDouble()
|
||||
: null);
|
||||
json.put("dailyMileageStrategy", stat.dailyMileageStrategy().name());
|
||||
return json;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.lingniu.ingest.vehiclestat;
|
||||
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
import com.lingniu.ingest.api.consumer.EnvelopeIngestResult;
|
||||
import com.lingniu.ingest.api.consumer.EnvelopeIngestor;
|
||||
import com.lingniu.ingest.sink.mq.proto.VehicleEnvelope;
|
||||
|
||||
public final class VehicleStatEnvelopeIngestor implements EnvelopeIngestor {
|
||||
|
||||
private final VehicleStatEventProcessor processor;
|
||||
|
||||
public VehicleStatEnvelopeIngestor(VehicleStatEventProcessor processor) {
|
||||
if (processor == null) {
|
||||
throw new IllegalArgumentException("processor must not be null");
|
||||
}
|
||||
this.processor = processor;
|
||||
}
|
||||
|
||||
public void ingest(byte[] kafkaValue) {
|
||||
processor.process(parse(kafkaValue));
|
||||
}
|
||||
|
||||
@Override
|
||||
public EnvelopeIngestResult tryIngest(byte[] kafkaValue) {
|
||||
VehicleEnvelope envelope = null;
|
||||
try {
|
||||
envelope = parse(kafkaValue);
|
||||
if (!envelope.hasTelemetrySnapshot()) {
|
||||
return EnvelopeIngestResult.skipped(
|
||||
envelope.getEventId(), envelope.getVin(), "envelope telemetry_snapshot is required");
|
||||
}
|
||||
processor.process(envelope);
|
||||
return EnvelopeIngestResult.processed(envelope.getEventId(), envelope.getVin());
|
||||
} catch (IllegalArgumentException ex) {
|
||||
return envelope == null
|
||||
? EnvelopeIngestResult.invalid(ex.getMessage())
|
||||
: EnvelopeIngestResult.skipped(envelope.getEventId(), envelope.getVin(), ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static VehicleEnvelope parse(byte[] kafkaValue) {
|
||||
if (kafkaValue == null || kafkaValue.length == 0) {
|
||||
throw new IllegalArgumentException("VehicleEnvelope bytes must not be empty");
|
||||
}
|
||||
try {
|
||||
return VehicleEnvelope.parseFrom(kafkaValue);
|
||||
} catch (InvalidProtocolBufferException ex) {
|
||||
throw new IllegalArgumentException("VehicleEnvelope bytes are invalid", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.lingniu.ingest.vehiclestat;
|
||||
|
||||
import com.lingniu.ingest.sink.mq.proto.TelemetryField;
|
||||
import com.lingniu.ingest.sink.mq.proto.VehicleEnvelope;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.OptionalDouble;
|
||||
|
||||
public final class VehicleStatEventProcessor {
|
||||
|
||||
private static final String TOTAL_MILEAGE_KEY = "total_mileage_km";
|
||||
|
||||
private final VehicleStatRepository repository;
|
||||
|
||||
public VehicleStatEventProcessor(VehicleStatRepository repository) {
|
||||
if (repository == null) {
|
||||
throw new IllegalArgumentException("repository must not be null");
|
||||
}
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
public void process(VehicleEnvelope envelope) {
|
||||
if (envelope == null) {
|
||||
throw new IllegalArgumentException("envelope must not be null");
|
||||
}
|
||||
if (!envelope.hasTelemetrySnapshot()) {
|
||||
return;
|
||||
}
|
||||
|
||||
OptionalDouble totalMileage = totalMileage(envelope);
|
||||
if (totalMileage.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
repository.appendMileagePoint(
|
||||
envelope.getVin(),
|
||||
new MileagePoint(Instant.ofEpochMilli(envelope.getEventTimeMs()), totalMileage.getAsDouble()));
|
||||
}
|
||||
|
||||
private static OptionalDouble totalMileage(VehicleEnvelope envelope) {
|
||||
for (TelemetryField field : envelope.getTelemetrySnapshot().getFieldsList()) {
|
||||
if (!TOTAL_MILEAGE_KEY.equals(field.getKey())) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
double value = Double.parseDouble(field.getValue());
|
||||
return Double.isFinite(value) ? OptionalDouble.of(value) : OptionalDouble.empty();
|
||||
} catch (NumberFormatException ex) {
|
||||
return OptionalDouble.empty();
|
||||
}
|
||||
}
|
||||
return OptionalDouble.empty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.lingniu.ingest.vehiclestat;
|
||||
|
||||
import com.lingniu.ingest.api.event.VehicleEvent;
|
||||
import com.lingniu.ingest.api.sink.EventSink;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public final class VehicleStatEventSink implements EventSink {
|
||||
|
||||
private final VehicleStatRepository repository;
|
||||
private final DailyVehicleStatService dailyStatService;
|
||||
private final ZoneId zoneId;
|
||||
|
||||
public VehicleStatEventSink(VehicleStatRepository repository,
|
||||
DailyVehicleStatService dailyStatService,
|
||||
ZoneId zoneId) {
|
||||
if (repository == null) {
|
||||
throw new IllegalArgumentException("repository must not be null");
|
||||
}
|
||||
if (dailyStatService == null) {
|
||||
throw new IllegalArgumentException("dailyStatService must not be null");
|
||||
}
|
||||
this.repository = repository;
|
||||
this.dailyStatService = dailyStatService;
|
||||
this.zoneId = zoneId == null ? ZoneId.of("Asia/Shanghai") : zoneId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "vehicle-stat";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean accepts(VehicleEvent event) {
|
||||
return event instanceof VehicleEvent.Realtime realtime
|
||||
&& realtime.payload() != null
|
||||
&& realtime.payload().totalMileageKm() != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> publish(VehicleEvent event) {
|
||||
if (!accepts(event)) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
CompletableFuture<Void> future = new CompletableFuture<>();
|
||||
try {
|
||||
VehicleEvent.Realtime realtime = (VehicleEvent.Realtime) event;
|
||||
repository.appendMileagePoint(
|
||||
realtime.vin(),
|
||||
new MileagePoint(realtime.eventTime(), realtime.payload().totalMileageKm()));
|
||||
LocalDate statDate = LocalDate.ofInstant(realtime.eventTime(), zoneId);
|
||||
dailyStatService.calculateAndSave(realtime.vin(), statDate);
|
||||
future.complete(null);
|
||||
} catch (RuntimeException e) {
|
||||
future.completeExceptionally(e);
|
||||
}
|
||||
return future;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.lingniu.ingest.vehiclestat;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface VehicleStatRepository {
|
||||
|
||||
void appendMileagePoint(String vin, MileagePoint point);
|
||||
|
||||
List<MileagePoint> mileagePoints(String vin, LocalDate statDate);
|
||||
|
||||
void saveDailyStat(VehicleDailyStatResult result);
|
||||
|
||||
Optional<VehicleDailyStatResult> findDailyStat(String vin, LocalDate statDate);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.lingniu.ingest.vehiclestat;
|
||||
|
||||
public record VehicleStatRule(String vin, DailyMileageStrategy dailyMileageStrategy) {
|
||||
|
||||
public VehicleStatRule {
|
||||
if (vin == null || vin.isBlank()) {
|
||||
throw new IllegalArgumentException("vin must not be blank");
|
||||
}
|
||||
if (dailyMileageStrategy == null) {
|
||||
dailyMileageStrategy = DailyMileageStrategy.CURRENT_LAST_MINUS_PREVIOUS_LAST;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.lingniu.ingest.vehiclestat;
|
||||
|
||||
public interface VehicleStatRuleRepository {
|
||||
|
||||
VehicleStatRule ruleFor(String vin);
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.lingniu.ingest.vehiclestat.config;
|
||||
|
||||
import com.lingniu.ingest.api.consumer.EnvelopeConsumerProcessor;
|
||||
import com.lingniu.ingest.api.consumer.EnvelopeDeadLetterSink;
|
||||
import com.lingniu.ingest.vehiclestat.DailyMileageCalculator;
|
||||
import com.lingniu.ingest.vehiclestat.DailyMileageStrategy;
|
||||
import com.lingniu.ingest.vehiclestat.DailyVehicleStatService;
|
||||
import com.lingniu.ingest.vehiclestat.FileVehicleStatRepository;
|
||||
import com.lingniu.ingest.vehiclestat.VehicleStatController;
|
||||
import com.lingniu.ingest.vehiclestat.VehicleStatEnvelopeIngestor;
|
||||
import com.lingniu.ingest.vehiclestat.VehicleStatEventSink;
|
||||
import com.lingniu.ingest.vehiclestat.VehicleStatEventProcessor;
|
||||
import com.lingniu.ingest.vehiclestat.VehicleStatRepository;
|
||||
import com.lingniu.ingest.vehiclestat.VehicleStatRule;
|
||||
import com.lingniu.ingest.vehiclestat.VehicleStatRuleRepository;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.time.ZoneId;
|
||||
|
||||
@AutoConfiguration
|
||||
@EnableConfigurationProperties(VehicleStatProperties.class)
|
||||
@ConditionalOnProperty(prefix = "lingniu.ingest.vehicle-stat", name = "enabled", havingValue = "true")
|
||||
public class VehicleStatAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public VehicleStatRepository vehicleStatRepository(VehicleStatProperties props) {
|
||||
return new FileVehicleStatRepository(Path.of(props.getFilePath()));
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public VehicleStatRuleRepository vehicleStatRuleRepository() {
|
||||
return vin -> new VehicleStatRule(vin, DailyMileageStrategy.CURRENT_LAST_MINUS_PREVIOUS_LAST);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public DailyMileageCalculator dailyMileageCalculator(VehicleStatProperties props) {
|
||||
return new DailyMileageCalculator(ZoneId.of(props.getZoneId()));
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBean(VehicleStatRepository.class)
|
||||
@ConditionalOnMissingBean
|
||||
public DailyVehicleStatService dailyVehicleStatService(VehicleStatRepository repository,
|
||||
VehicleStatRuleRepository ruleRepository,
|
||||
DailyMileageCalculator mileageCalculator) {
|
||||
return new DailyVehicleStatService(repository, ruleRepository, mileageCalculator);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBean(VehicleStatRepository.class)
|
||||
@ConditionalOnMissingBean
|
||||
public VehicleStatEventProcessor vehicleStatEventProcessor(VehicleStatRepository repository) {
|
||||
return new VehicleStatEventProcessor(repository);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBean(VehicleStatEventProcessor.class)
|
||||
@ConditionalOnMissingBean
|
||||
public VehicleStatEnvelopeIngestor vehicleStatEnvelopeIngestor(VehicleStatEventProcessor processor) {
|
||||
return new VehicleStatEnvelopeIngestor(processor);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBean({VehicleStatRepository.class, DailyVehicleStatService.class})
|
||||
@ConditionalOnMissingBean
|
||||
public VehicleStatEventSink vehicleStatEventSink(VehicleStatRepository repository,
|
||||
DailyVehicleStatService dailyStatService,
|
||||
VehicleStatProperties props) {
|
||||
return new VehicleStatEventSink(repository, dailyStatService, ZoneId.of(props.getZoneId()));
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBean(DailyVehicleStatService.class)
|
||||
@ConditionalOnMissingBean
|
||||
public VehicleStatController vehicleStatController(VehicleStatRepository repository,
|
||||
DailyVehicleStatService dailyStatService) {
|
||||
return new VehicleStatController(repository, dailyStatService);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBean({VehicleStatEnvelopeIngestor.class, EnvelopeDeadLetterSink.class})
|
||||
@ConditionalOnMissingBean(name = "vehicleStatEnvelopeConsumerProcessor")
|
||||
public EnvelopeConsumerProcessor vehicleStatEnvelopeConsumerProcessor(VehicleStatEnvelopeIngestor ingestor,
|
||||
EnvelopeDeadLetterSink deadLetterSink) {
|
||||
return new EnvelopeConsumerProcessor("vehicle-stat", ingestor, deadLetterSink);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.lingniu.ingest.vehiclestat.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@ConfigurationProperties(prefix = "lingniu.ingest.vehicle-stat")
|
||||
public class VehicleStatProperties {
|
||||
|
||||
private String filePath = "./target/vehicle-stat/";
|
||||
|
||||
private String zoneId = "Asia/Shanghai";
|
||||
|
||||
public String getFilePath() {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
public void setFilePath(String filePath) {
|
||||
this.filePath = filePath;
|
||||
}
|
||||
|
||||
public String getZoneId() {
|
||||
return zoneId;
|
||||
}
|
||||
|
||||
public void setZoneId(String zoneId) {
|
||||
this.zoneId = zoneId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
com.lingniu.ingest.vehiclestat.config.VehicleStatAutoConfiguration
|
||||
Reference in New Issue
Block a user