chore: initial import of lingniu-vehicle-ingest

Multi-module Spring Boot ingest service for vehicle telemetry. Modules:

- ingest-api / ingest-core / ingest-codec-common: shared SPI, dispatcher,
  Disruptor event bus, BCC/BCD codec helpers
- protocol-gb32960: GB/T 32960.3 inbound (Netty + per-version parser
  packages v2016/v2025), platform login auth, VIN whitelist, idle handler
- protocol-jt808 / protocol-jt1078 / protocol-jsatl12: JT/T inbound
- inbound-mqtt / inbound-xinda-push: alternative ingest channels
- session-core: per-channel session state
- sink-archive / sink-mq: persistence sinks (local file / Kafka)
- command-gateway: terminal control command gateway
- bootstrap-all: aggregator Spring Boot app
- observability: Micrometer / Actuator wiring

Includes hex-dump golden samples under protocol-gb32960/src/test/resources
and the GB/T 32960.3-2016 / 2025 reference PDFs under reference/.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
lingniu-dev
2026-04-15 16:08:57 +08:00
commit 064ecc479c
220 changed files with 11874 additions and 0 deletions

29
ingest-api/pom.xml Normal file
View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.lingniu.ingest</groupId>
<artifactId>lingniu-vehicle-ingest</artifactId>
<version>0.1.0-SNAPSHOT</version>
</parent>
<artifactId>ingest-api</artifactId>
<name>ingest-api</name>
<description>SPI、sealed 领域事件、注解定义。零 Spring 依赖。</description>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,14 @@
package com.lingniu.ingest.api;
/**
* 协议标识,作为所有 SPI / 注解 / 路由 key 的统一枚举。
* 新增协议需要在此处登记,避免散落的字符串常量。
*/
public enum ProtocolId {
GB32960,
JT808,
JT1078,
JSATL12,
MQTT_YUTONG,
XINDA_PUSH
}

View File

@@ -0,0 +1,22 @@
package com.lingniu.ingest.api.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 批量异步处理:框架把多次同类调用聚合成 {@code List} 交给方法。
*
* <p>处理方法签名需为 {@code void foo(List<T> list)} 或返回 {@code List<VehicleEvent>}。
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AsyncBatch {
int size() default 1000;
long waitMs() default 500;
int poolSize() default 2;
}

View File

@@ -0,0 +1,17 @@
package com.lingniu.ingest.api.annotation;
import com.lingniu.ingest.api.event.VehicleEvent;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 声明当前 Handler 方法产出的事件类型。用于文档化和静态校验。
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface EventEmit {
Class<? extends VehicleEvent>[] value();
}

View File

@@ -0,0 +1,17 @@
package com.lingniu.ingest.api.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 声明幂等键,由 Dedup 拦截器使用。支持 SpEL 表达式,上下文根对象为 Handler 参数。
*
* <p>示例:{@code @IdempotentKey("#msg.vin + ':' + #msg.seq + ':' + #msg.eventTime")}
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface IdempotentKey {
String value();
}

View File

@@ -0,0 +1,27 @@
package com.lingniu.ingest.api.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 方法级路由注解:声明此方法处理哪些协议消息。
*
* <p>{@link #command} 和 {@link #infoType} 的含义由各协议自行解释Dispatcher 只负责匹配:
* <ul>
* <li>GB/T 32960{@code command} = 0x01 车辆登入 / 0x02 实时上报 / ...{@code infoType} = 信息体 ID
* <li>JT/T 808{@code command} = 消息 ID0x0100 / 0x0200 / ...{@code infoType} 留空
* <li>MQTT{@code command} 可为 topic hash 或忽略
* </ul>
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MessageMapping {
int[] command() default {};
int[] infoType() default {};
String desc() default "";
}

View File

@@ -0,0 +1,30 @@
package com.lingniu.ingest.api.annotation;
import com.lingniu.ingest.api.ProtocolId;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 标记一个类为协议 Handler Bean由 {@code AnnotationHandlerProcessor} 自动扫描注册。
*
* <p>典型用法:
* <pre>{@code
* @ProtocolHandler(protocol = ProtocolId.GB32960)
* public class Gb32960RealtimeHandler {
* @MessageMapping(command = 0x02)
* public VehicleEvent.Realtime handle(Gb32960Message msg) { ... }
* }
* }</pre>
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ProtocolHandler {
ProtocolId protocol();
/** 可选:子协议版本(如 32960 的 2011 / 2017。 */
String version() default "";
}

View File

@@ -0,0 +1,20 @@
package com.lingniu.ingest.api.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 单 VIN 速率限制。超限消息直接进 DLQ 并打点。
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RateLimited {
/** 单 VIN 每秒最大消息数。 */
int perVin() default 50;
/** 全局 QPS 上限;<=0 表示不设限。 */
int global() default -1;
}

View File

@@ -0,0 +1,15 @@
package com.lingniu.ingest.api.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 为 Handler 方法开启 OpenTelemetry span。
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface TraceSpan {
String value();
}

View File

@@ -0,0 +1,37 @@
package com.lingniu.ingest.api.event;
import java.util.List;
import java.util.Set;
/**
* 报警事件载荷,跨协议归一。
*
* @param level 报警最高等级
* @param alarmTypeCode 协议原始 alarm type 编码(按协议自定义),可用于联查协议规范
* @param alarmTypeName 报警类型名称,如 {@code GB32960_ALARM}
* @param faultCodes 故障代码列表(协议私有编码字符串化,便于下游去重/聚合)
* @param activeBits 结构化的通用报警位集合2016 版0~152025 版0~27对照 GB/T 32960.3 表 24
* @param longitude 经度(可选,若事件伴随位置)
* @param latitude 纬度(可选)
*/
public record AlarmPayload(
AlarmLevel level,
int alarmTypeCode,
String alarmTypeName,
List<String> faultCodes,
Set<String> activeBits,
Double longitude,
Double latitude
) {
/**
* 报警等级(归一化的跨协议分类)。
*
* <ul>
* <li>{@link #INFO}:提示级(对应 GB/T 32960.3 0/无故障)
* <li>{@link #MINOR}1 级 —— 不影响车辆正常行驶
* <li>{@link #MAJOR}2 级 —— 影响车辆性能,需驾驶员限制行驶
* <li>{@link #CRITICAL}3 级(驾驶员应立即停车)或 4 级(热事件最高级)
* </ul>
*/
public enum AlarmLevel { INFO, MINOR, MAJOR, CRITICAL }
}

View File

@@ -0,0 +1,12 @@
package com.lingniu.ingest.api.event;
/** 位置事件载荷。坐标已统一转换为 WGS84 十进制度。 */
public record LocationPayload(
double longitude,
double latitude,
double altitudeM,
double speedKmh,
double directionDeg,
long alarmFlag,
long statusFlag
) {}

View File

@@ -0,0 +1,49 @@
package com.lingniu.ingest.api.event;
/**
* 实时数据载荷(扁平 record字段用 {@code Double} / {@code Integer} 允许 null表达"未上报")。
*
* <p>不再复刻旧服务 {@code VehicleDataActual} 的 273 字段巨型类 —— 协议特有细节通过
* {@link VehicleEvent#metadata()} 传递。此处只保留跨协议通用字段。
*/
public record RealtimePayload(
// ===== 动力 & 能源 =====
Double speedKmh,
Double totalMileageKm,
Double batterySoc,
Double batteryVoltageV,
Double batteryCurrentA,
// ===== 燃料电池 / 氢能 =====
Double fcVoltageV,
Double fcCurrentA,
Double fcTempC,
Double hydrogenRemainingKg,
Double hydrogenHighPressureMpa,
Double hydrogenLowPressureMpa,
// ===== 车辆状态 =====
VehicleState vehicleState,
ChargingState chargingState,
RunningMode runningMode,
Integer gearLevel,
Double acceleratorPedal,
Double brakePedal,
// ===== 位置(冗余一份便于单主题消费)=====
Double longitude,
Double latitude,
Double altitudeM,
Double directionDeg,
// ===== 环境 =====
Double ambientTempC,
Double coolantTempC
) {
public enum VehicleState { STARTED, SHUTDOWN, OTHER, INVALID }
public enum ChargingState { UNCHARGED, PARKED_CHARGING, DRIVING_CHARGING, CHARGED, OTHER, INVALID }
public enum RunningMode { ELECTRIC, HYBRID, FUEL, OTHER, INVALID }
}

View File

@@ -0,0 +1,141 @@
package com.lingniu.ingest.api.event;
import com.lingniu.ingest.api.ProtocolId;
import java.time.Instant;
import java.util.Map;
/**
* 领域事件根类型。所有协议归一到这里。
*
* <p>使用 sealed + record 表达有界闭合的事件集合,下游消费方可用 {@code switch} 模式匹配穷尽处理。
*
* <p>字段约定:
* <ul>
* <li>{@link #vin} 作为 Kafka 分区 key保证单车顺序
* <li>{@link #eventTime} 设备上报时间(而非服务器接收时间)
* <li>{@link #ingestTime} 服务器接收时间,用于延迟监控
* <li>{@link #traceId} W3C traceparent 上下文
* </ul>
*/
public sealed interface VehicleEvent
permits VehicleEvent.Realtime,
VehicleEvent.Location,
VehicleEvent.Alarm,
VehicleEvent.Login,
VehicleEvent.Logout,
VehicleEvent.Heartbeat,
VehicleEvent.MediaMeta,
VehicleEvent.Passthrough {
String eventId();
String vin();
ProtocolId source();
Instant eventTime();
Instant ingestTime();
String traceId();
/** 通用元数据:协议版本、终端手机号、网关 IP 等。 */
Map<String, String> metadata();
// ===== 具体事件类型 =====
/** 整车实时数据32960 实时上报 / MQTT 宇通 / JT808 位置扩展等都归一到这里)。 */
record Realtime(
String eventId,
String vin,
ProtocolId source,
Instant eventTime,
Instant ingestTime,
String traceId,
Map<String, String> metadata,
RealtimePayload payload
) implements VehicleEvent {}
/** 纯位置事件808 的 0x0200 / Xinda 0200 映射到此)。 */
record Location(
String eventId,
String vin,
ProtocolId source,
Instant eventTime,
Instant ingestTime,
String traceId,
Map<String, String> metadata,
LocationPayload payload
) implements VehicleEvent {}
/** 报警事件。 */
record Alarm(
String eventId,
String vin,
ProtocolId source,
Instant eventTime,
Instant ingestTime,
String traceId,
Map<String, String> metadata,
AlarmPayload payload
) implements VehicleEvent {}
/** 设备登入 / 车辆登入。 */
record Login(
String eventId,
String vin,
ProtocolId source,
Instant eventTime,
Instant ingestTime,
String traceId,
Map<String, String> metadata,
String iccid,
String protocolVersion
) implements VehicleEvent {}
/** 设备登出。 */
record Logout(
String eventId,
String vin,
ProtocolId source,
Instant eventTime,
Instant ingestTime,
String traceId,
Map<String, String> metadata
) implements VehicleEvent {}
/** 心跳 / 链路保活。 */
record Heartbeat(
String eventId,
String vin,
ProtocolId source,
Instant eventTime,
Instant ingestTime,
String traceId,
Map<String, String> metadata
) implements VehicleEvent {}
/** 多媒体元数据(大文件本体走对象存储,通过 archiveRef 引用)。 */
record MediaMeta(
String eventId,
String vin,
ProtocolId source,
Instant eventTime,
Instant ingestTime,
String traceId,
Map<String, String> metadata,
String mediaId,
String mediaType,
long sizeBytes,
String archiveRef
) implements VehicleEvent {}
/** 透传 / 自定义扩展消息。 */
record Passthrough(
String eventId,
String vin,
ProtocolId source,
Instant eventTime,
Instant ingestTime,
String traceId,
Map<String, String> metadata,
int passthroughType,
byte[] data
) implements VehicleEvent {}
}

View File

@@ -0,0 +1,13 @@
/**
* lingniu-vehicle-ingest 公共 API 模块。零 Spring 依赖,只定义 SPI、sealed 领域事件、注解。
*
* <p>分包:
* <ul>
* <li>{@code annotation} - Handler 注解体系
* <li>{@code event} - sealed {@code VehicleEvent} + payload records
* <li>{@code pipeline} - RawFrame / IngestContext / IngestInterceptor
* <li>{@code sink} - EventSink SPI
* <li>{@code spi} - ProtocolPlugin / FrameDecoder / FrameEncoder / EventMapper
* </ul>
*/
package com.lingniu.ingest.api;

View File

@@ -0,0 +1,46 @@
package com.lingniu.ingest.api.pipeline;
import java.util.HashMap;
import java.util.Map;
/**
* 一次消息处理的上下文,贯穿整个拦截链与 Handler。线程不共享不需要同步。
*/
public final class IngestContext {
private final String traceId;
private final Map<String, Object> attributes = new HashMap<>(8);
private volatile boolean aborted;
private volatile String abortReason;
public IngestContext(String traceId) {
this.traceId = traceId;
}
public String traceId() {
return traceId;
}
public <T> T attr(String key) {
@SuppressWarnings("unchecked")
T v = (T) attributes.get(key);
return v;
}
public void attr(String key, Object value) {
attributes.put(key, value);
}
public void abort(String reason) {
this.aborted = true;
this.abortReason = reason;
}
public boolean aborted() {
return aborted;
}
public String abortReason() {
return abortReason;
}
}

View File

@@ -0,0 +1,21 @@
package com.lingniu.ingest.api.pipeline;
import com.lingniu.ingest.api.event.VehicleEvent;
/**
* 拦截器 SPI。内置实现Auth / Dedup / RateLimit / CoordTransform / Tracing。
* 实现 {@link org.springframework.core.Ordered}(或使用 {@code @Order})控制顺序。
*/
public interface IngestInterceptor {
/** 解码后、分发到 Handler 之前触发。返回 false 表示终止后续处理。 */
default boolean before(RawFrame frame, IngestContext ctx) {
return true;
}
/** Handler 成功产出事件后触发,允许修饰事件或取消投递。 */
default void after(VehicleEvent event, IngestContext ctx) {}
/** 处理链任一环节抛出异常时触发。 */
default void onError(Throwable error, IngestContext ctx) {}
}

View File

@@ -0,0 +1,27 @@
package com.lingniu.ingest.api.pipeline;
import com.lingniu.ingest.api.ProtocolId;
import java.time.Instant;
import java.util.Map;
/**
* 进入 Pipeline 的原始帧描述。
*
* @param protocolId 来源协议
* @param command 协议主命令 / 消息 ID由各协议自行定义
* @param infoType 可选子类型(如 32960 的信息体 ID
* @param payload 已经被 {@link com.lingniu.ingest.api.spi.FrameDecoder} 解析出的协议对象
* @param rawBytes 原始字节,用于冷存与排障(可能为 null按配置开关
* @param sourceMeta 来源元数据peer ip、端口、session id、topic 等
* @param receivedAt 服务器接收时刻
*/
public record RawFrame(
ProtocolId protocolId,
int command,
int infoType,
Object payload,
byte[] rawBytes,
Map<String, String> sourceMeta,
Instant receivedAt
) {}

View File

@@ -0,0 +1,28 @@
package com.lingniu.ingest.api.sink;
import com.lingniu.ingest.api.event.VehicleEvent;
import java.util.List;
import java.util.concurrent.CompletableFuture;
/**
* 事件出口 SPI。Kafka、本地归档、指标打点都是一种 Sink。
*
* <p>实现应当线程安全、非阻塞(内部异步 IO。由 Disruptor EventBus 扇出调用。
*/
public interface EventSink {
String name();
CompletableFuture<Void> publish(VehicleEvent event);
default CompletableFuture<Void> publishBatch(List<VehicleEvent> batch) {
CompletableFuture<?>[] all = batch.stream().map(this::publish).toArray(CompletableFuture[]::new);
return CompletableFuture.allOf(all);
}
/** 是否接受此事件类型。默认全部接受。 */
default boolean accepts(VehicleEvent event) {
return true;
}
}

View File

@@ -0,0 +1,12 @@
package com.lingniu.ingest.api.spi;
/** 解码阶段异常,指示数据体本身违反协议。应被 Dispatcher 路由到 DLQ。 */
public class DecodeException extends RuntimeException {
public DecodeException(String message) {
super(message);
}
public DecodeException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,16 @@
package com.lingniu.ingest.api.spi;
import com.lingniu.ingest.api.event.VehicleEvent;
import java.util.List;
/**
* 协议消息 → 领域事件的映射器Adapter 模式)。
*
* <p>实现类负责把"我这个协议特有的 Java 对象"翻译成统一的 {@link VehicleEvent}。
* 允许一条协议消息展开为多条领域事件(例如 32960 的一次实时上报同时产出 Realtime + Location
*/
@FunctionalInterface
public interface EventMapper<I> {
List<VehicleEvent> toEvents(I message);
}

View File

@@ -0,0 +1,13 @@
package com.lingniu.ingest.api.spi;
import java.nio.ByteBuffer;
/**
* 协议帧解码器:字节流 → 协议消息对象。
*
* <p>实现类应是无状态的(拆包/粘包由上游 Netty 的帧解码器处理),此处只负责一条完整帧的解析。
*/
@FunctionalInterface
public interface FrameDecoder<I> {
I decode(ByteBuffer buffer) throws DecodeException;
}

View File

@@ -0,0 +1,9 @@
package com.lingniu.ingest.api.spi;
import java.nio.ByteBuffer;
/** 下行帧编码器。无下行的协议返回 null 即可。 */
@FunctionalInterface
public interface FrameEncoder<O> {
ByteBuffer encode(O message);
}

View File

@@ -0,0 +1,16 @@
package com.lingniu.ingest.api.spi;
import com.lingniu.ingest.api.event.VehicleEvent;
import java.util.List;
/**
* 编程式 Handler 接口。推荐使用 {@code @ProtocolHandler + @MessageMapping} 注解方式代替。
* 此接口保留是为了 SPI 场景下的脚本式扩展(例如动态加载的 groovy handler
*/
public interface MessageHandler<I> {
boolean supports(I message);
List<VehicleEvent> handle(I message);
}

View File

@@ -0,0 +1,31 @@
package com.lingniu.ingest.api.spi;
import com.lingniu.ingest.api.ProtocolId;
import java.util.List;
/**
* 协议插件 SPI。每个协议模块提供一个实现由 {@code ProtocolPluginRegistry} 通过 ServiceLoader
* 或 Spring Bean 方式发现并装配。
*
* @param <I> 协议解码后的消息类型
* @param <O> 下行消息类型(无下行的协议用 {@link Void}
*/
public interface ProtocolPlugin<I, O> {
ProtocolId id();
FrameDecoder<I> decoder();
FrameEncoder<O> encoder();
EventMapper<I> eventMapper();
/**
* 插件自带的协议特定 Handler 列表(可选)。
* 通常推荐使用 {@code @ProtocolHandler} + Spring Bean 的方式注册,此处返回空列表。
*/
default List<MessageHandler<I>> builtinHandlers() {
return List.of();
}
}