feat: make gb32960 archive history query production ready

This commit is contained in:
kkfluous
2026-06-23 11:55:44 +08:00
parent b14871ff1c
commit ba68ffe061
462 changed files with 23639 additions and 2341 deletions

View File

@@ -0,0 +1,72 @@
package com.lingniu.ingest.vehiclestate;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.Optional;
public final class RedisVehicleStateRepository implements VehicleStateRepository {
private final StringRedisTemplate redis;
public RedisVehicleStateRepository(StringRedisTemplate redis) {
if (redis == null) {
throw new IllegalArgumentException("redis must not be null");
}
this.redis = redis;
}
@Override
public void putState(String vin, String json) {
put(key("vehicle:state:", vin), json);
}
@Override
public void putLocation(String vin, String json) {
put(key("vehicle:location:", vin), json);
}
@Override
public void putSafety(String vin, String json) {
put(key("vehicle:safety:", vin), json);
}
@Override
public void putLastEvent(String vin, String json) {
put(key("vehicle:event:last:", vin), json);
}
@Override
public Optional<String> getState(String vin) {
return get(key("vehicle:state:", vin));
}
@Override
public Optional<String> getLocation(String vin) {
return get(key("vehicle:location:", vin));
}
@Override
public Optional<String> getSafety(String vin) {
return get(key("vehicle:safety:", vin));
}
@Override
public Optional<String> getLastEvent(String vin) {
return get(key("vehicle:event:last:", vin));
}
private void put(String key, String json) {
redis.opsForValue().set(key, json == null ? "{}" : json);
}
private Optional<String> get(String key) {
return Optional.ofNullable(redis.opsForValue().get(key));
}
private static String key(String prefix, String vin) {
if (vin == null || vin.isBlank()) {
throw new IllegalArgumentException("vin must not be blank");
}
return prefix + vin;
}
}

View File

@@ -0,0 +1,55 @@
package com.lingniu.ingest.vehiclestate;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
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.RestController;
import java.util.Optional;
@RestController
@ConditionalOnProperty(prefix = "lingniu.ingest.vehicle-state", name = "enabled", havingValue = "true")
@ConditionalOnBean(VehicleStateRepository.class)
@RequestMapping(path = "/api/vehicle-state", produces = MediaType.APPLICATION_JSON_VALUE)
public final class VehicleStateController {
private final VehicleStateRepository repository;
public VehicleStateController(VehicleStateRepository repository) {
if (repository == null) {
throw new IllegalArgumentException("repository must not be null");
}
this.repository = repository;
}
@GetMapping("/{vin}")
public ResponseEntity<String> state(@PathVariable String vin) {
return json(repository.getState(vin));
}
@GetMapping("/{vin}/location")
public ResponseEntity<String> location(@PathVariable String vin) {
return json(repository.getLocation(vin));
}
@GetMapping("/{vin}/safety")
public ResponseEntity<String> safety(@PathVariable String vin) {
return json(repository.getSafety(vin));
}
@GetMapping("/{vin}/last-event")
public ResponseEntity<String> lastEvent(@PathVariable String vin) {
return json(repository.getLastEvent(vin));
}
private static ResponseEntity<String> json(Optional<String> value) {
return value.map(json -> ResponseEntity.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(json))
.orElseGet(() -> ResponseEntity.notFound().build());
}
}

View File

@@ -0,0 +1,47 @@
package com.lingniu.ingest.vehiclestate;
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 VehicleStateEnvelopeIngestor implements EnvelopeIngestor {
private final VehicleStateUpdater updater;
public VehicleStateEnvelopeIngestor(VehicleStateUpdater updater) {
if (updater == null) {
throw new IllegalArgumentException("updater must not be null");
}
this.updater = updater;
}
public void ingest(byte[] kafkaValue) {
updater.update(parse(kafkaValue));
}
@Override
public EnvelopeIngestResult tryIngest(byte[] kafkaValue) {
VehicleEnvelope envelope = null;
try {
envelope = parse(kafkaValue);
updater.update(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);
}
}
}

View File

@@ -0,0 +1,22 @@
package com.lingniu.ingest.vehiclestate;
import java.util.Optional;
public interface VehicleStateRepository {
void putState(String vin, String json);
void putLocation(String vin, String json);
void putSafety(String vin, String json);
void putLastEvent(String vin, String json);
Optional<String> getState(String vin);
Optional<String> getLocation(String vin);
Optional<String> getSafety(String vin);
Optional<String> getLastEvent(String vin);
}

View File

@@ -0,0 +1,142 @@
package com.lingniu.ingest.vehiclestate;
import com.lingniu.ingest.sink.mq.proto.TelemetryField;
import com.lingniu.ingest.sink.mq.proto.VehicleEnvelope;
import java.util.LinkedHashMap;
import java.util.Map;
public final class VehicleStateUpdater {
private final VehicleStateRepository repository;
public VehicleStateUpdater(VehicleStateRepository repository) {
if (repository == null) {
throw new IllegalArgumentException("repository must not be null");
}
this.repository = repository;
}
public void update(VehicleEnvelope envelope) {
if (envelope == null) {
throw new IllegalArgumentException("envelope must not be null");
}
if (!envelope.hasTelemetrySnapshot()) {
throw new IllegalArgumentException("envelope telemetry_snapshot is required");
}
Map<String, String> fields = fields(envelope);
String vin = envelope.getVin();
repository.putState(vin, json(base(envelope, fields)));
repository.putLastEvent(vin, json(lastEvent(envelope)));
if (fields.containsKey("longitude") && fields.containsKey("latitude")) {
repository.putLocation(vin, json(location(envelope, fields)));
}
if (hasSafetyFields(fields)) {
repository.putSafety(vin, json(safety(envelope, fields)));
}
}
private static Map<String, String> fields(VehicleEnvelope envelope) {
Map<String, String> fields = new LinkedHashMap<>();
for (TelemetryField field : envelope.getTelemetrySnapshot().getFieldsList()) {
fields.put(field.getKey(), field.getValue());
}
return fields;
}
private static Map<String, Object> base(VehicleEnvelope envelope, Map<String, String> fields) {
Map<String, Object> map = eventIdentity(envelope);
map.put("eventType", envelope.getTelemetrySnapshot().getEventType());
map.put("fields", fields);
return map;
}
private static Map<String, Object> location(VehicleEnvelope envelope, Map<String, String> fields) {
Map<String, Object> map = eventIdentity(envelope);
putIfPresent(map, fields, "longitude");
putIfPresent(map, fields, "latitude");
putIfPresent(map, fields, "altitude_m");
putIfPresent(map, fields, "direction_deg");
putIfPresent(map, fields, "speed_kmh");
return map;
}
private static Map<String, Object> safety(VehicleEnvelope envelope, Map<String, String> fields) {
Map<String, Object> map = eventIdentity(envelope);
putIfPresent(map, fields, "safety_category");
putIfPresent(map, fields, "hydrogen_leak_detected");
putIfPresent(map, fields, "hydrogen_leak_level");
putIfPresent(map, fields, "hydrogen_leak_action_required");
putIfPresent(map, fields, "alarm_level");
return map;
}
private static Map<String, Object> lastEvent(VehicleEnvelope envelope) {
return eventIdentity(envelope);
}
private static Map<String, Object> eventIdentity(VehicleEnvelope envelope) {
Map<String, Object> map = new LinkedHashMap<>();
map.put("eventId", envelope.getEventId());
map.put("traceId", envelope.getTraceId());
map.put("vin", envelope.getVin());
map.put("source", envelope.getSource());
map.put("eventTimeMs", Long.toString(envelope.getEventTimeMs()));
map.put("ingestTimeMs", Long.toString(envelope.getIngestTimeMs()));
return map;
}
private static boolean hasSafetyFields(Map<String, String> fields) {
return fields.containsKey("safety_category")
|| fields.containsKey("hydrogen_leak_detected")
|| fields.containsKey("hydrogen_leak_level")
|| fields.containsKey("hydrogen_leak_action_required")
|| fields.containsKey("alarm_level");
}
private static void putIfPresent(Map<String, Object> map, Map<String, String> fields, String key) {
if (fields.containsKey(key)) {
map.put(key, fields.get(key));
}
}
private static String json(Map<String, ?> map) {
StringBuilder out = new StringBuilder("{");
boolean first = true;
for (Map.Entry<String, ?> entry : map.entrySet()) {
if (!first) {
out.append(',');
}
first = false;
out.append('"').append(escape(entry.getKey())).append('"').append(':');
Object value = entry.getValue();
if (value instanceof Map<?, ?> nested) {
out.append(jsonObject(nested));
} else {
out.append('"').append(escape(String.valueOf(value))).append('"');
}
}
return out.append('}').toString();
}
private static String jsonObject(Map<?, ?> map) {
StringBuilder out = new StringBuilder("{");
boolean first = true;
for (Map.Entry<?, ?> entry : map.entrySet()) {
if (!first) {
out.append(',');
}
first = false;
out.append('"').append(escape(String.valueOf(entry.getKey()))).append('"')
.append(':')
.append('"').append(escape(String.valueOf(entry.getValue()))).append('"');
}
return out.append('}').toString();
}
private static String escape(String value) {
return value.replace("\\", "\\\\").replace("\"", "\\\"");
}
}

View File

@@ -0,0 +1,56 @@
package com.lingniu.ingest.vehiclestate.config;
import com.lingniu.ingest.api.consumer.EnvelopeConsumerProcessor;
import com.lingniu.ingest.api.consumer.EnvelopeDeadLetterSink;
import com.lingniu.ingest.vehiclestate.RedisVehicleStateRepository;
import com.lingniu.ingest.vehiclestate.VehicleStateController;
import com.lingniu.ingest.vehiclestate.VehicleStateEnvelopeIngestor;
import com.lingniu.ingest.vehiclestate.VehicleStateRepository;
import com.lingniu.ingest.vehiclestate.VehicleStateUpdater;
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.context.annotation.Bean;
import org.springframework.data.redis.core.StringRedisTemplate;
@AutoConfiguration
@ConditionalOnProperty(prefix = "lingniu.ingest.vehicle-state", name = "enabled", havingValue = "true")
public class VehicleStateAutoConfiguration {
@Bean
@ConditionalOnBean(StringRedisTemplate.class)
@ConditionalOnMissingBean
public VehicleStateRepository vehicleStateRepository(StringRedisTemplate redis) {
return new RedisVehicleStateRepository(redis);
}
@Bean
@ConditionalOnBean(VehicleStateRepository.class)
@ConditionalOnMissingBean
public VehicleStateUpdater vehicleStateUpdater(VehicleStateRepository repository) {
return new VehicleStateUpdater(repository);
}
@Bean
@ConditionalOnBean(VehicleStateUpdater.class)
@ConditionalOnMissingBean
public VehicleStateEnvelopeIngestor vehicleStateEnvelopeIngestor(VehicleStateUpdater updater) {
return new VehicleStateEnvelopeIngestor(updater);
}
@Bean
@ConditionalOnBean({VehicleStateEnvelopeIngestor.class, EnvelopeDeadLetterSink.class})
@ConditionalOnMissingBean(name = "vehicleStateEnvelopeConsumerProcessor")
public EnvelopeConsumerProcessor vehicleStateEnvelopeConsumerProcessor(VehicleStateEnvelopeIngestor ingestor,
EnvelopeDeadLetterSink deadLetterSink) {
return new EnvelopeConsumerProcessor("vehicle-state", ingestor, deadLetterSink);
}
@Bean
@ConditionalOnBean(VehicleStateRepository.class)
@ConditionalOnMissingBean
public VehicleStateController vehicleStateController(VehicleStateRepository repository) {
return new VehicleStateController(repository);
}
}

View File

@@ -0,0 +1 @@
com.lingniu.ingest.vehiclestate.config.VehicleStateAutoConfiguration