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

View File

@@ -0,0 +1,166 @@
package com.lingniu.ingest.bootstrap;
import com.lingniu.ingest.api.event.VehicleEvent;
import com.lingniu.ingest.api.sink.EventSink;
import com.lingniu.ingest.codec.BccChecksum;
import com.lingniu.ingest.protocol.gb32960.inbound.Gb32960NettyServer;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import java.io.ByteArrayOutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
/**
* 端到端集成测试:
* <pre>
* test client → TCP → Gb32960 Netty server → frame decoder → dispatcher
* → InterceptorChain → Handler → DisruptorEventBus → TestEventSink
* </pre>
*
* <p>关闭了所有非 gb32960 的协议与 Kafka sink只测核心链路连通性。
*/
@SpringBootTest(
classes = IngestApplication.class,
webEnvironment = SpringBootTest.WebEnvironment.NONE,
properties = {
"lingniu.ingest.gb32960.enabled=true",
"lingniu.ingest.gb32960.port=0",
"lingniu.ingest.jt808.enabled=false",
"lingniu.ingest.jt1078.enabled=false",
"lingniu.ingest.jsatl12.enabled=false",
"lingniu.ingest.mqtt.enabled=false",
"lingniu.ingest.xinda-push.enabled=false",
"lingniu.ingest.command-gateway.enabled=false",
// 总开关关闭 Kafka sinkTestEventSink 成为唯一 Sink
"lingniu.ingest.sink.mq.enabled=false",
"lingniu.ingest.sink.archive.enabled=false",
"spring.main.allow-bean-definition-overriding=true"
})
@Import(IngestEndToEndTest.TestConfig.class)
class IngestEndToEndTest {
@Autowired
private Gb32960NettyServer server;
@Autowired
private TestEventSink testSink;
@Test
void gb32960FrameReachesEventSink() throws Exception {
int port = server.getBoundPort();
assertThat(port).isGreaterThan(0);
byte[] frame = buildRealtimeFrame("LTEST0000000E2E01");
try (Socket socket = new Socket("127.0.0.1", port)) {
socket.getOutputStream().write(frame);
socket.getOutputStream().flush();
}
// DisruptorEventBus + Virtual threads have some jitter; poll up to 3s.
VehicleEvent captured = null;
long deadline = System.currentTimeMillis() + 3000;
while (System.currentTimeMillis() < deadline) {
captured = testSink.queue.poll(100, TimeUnit.MILLISECONDS);
if (captured instanceof VehicleEvent.Realtime) break;
if (captured != null) continue;
}
assertThat(captured).isInstanceOf(VehicleEvent.Realtime.class);
assertThat(captured.vin()).isEqualTo("LTEST0000000E2E01");
}
// ===== test config =====
@TestConfiguration
static class TestConfig {
@Bean
TestEventSink testEventSink() {
return new TestEventSink();
}
}
/** 收集所有 publish 的事件。 */
static final class TestEventSink implements EventSink {
final BlockingQueue<VehicleEvent> queue = new LinkedBlockingQueue<>();
@Override public String name() { return "test"; }
@Override
public CompletableFuture<Void> publish(VehicleEvent event) {
queue.add(event);
return CompletableFuture.completedFuture(null);
}
@Override
public CompletableFuture<Void> publishBatch(List<VehicleEvent> batch) {
queue.addAll(batch);
return CompletableFuture.completedFuture(null);
}
}
// ===== frame builder (inlined copy of Gb32960DecoderTest#buildRealtimeFrame) =====
private static byte[] buildRealtimeFrame(String vin) {
ByteArrayOutputStream body = new ByteArrayOutputStream();
body.write(24); body.write(1); body.write(2); body.write(3); body.write(4); body.write(5);
// 0x01 整车 20 字节
body.write(0x01);
body.write(0x01); body.write(0x03); body.write(0x01);
writeU16(body, 523);
writeU32(body, 1234567);
writeU16(body, 6000);
writeU16(body, 1100);
body.write(70);
body.write(0x01);
body.write(0x0F);
writeU16(body, 500);
body.write(30);
body.write(0);
// 0x05 位置 9 字节
body.write(0x05);
body.write(0x00);
writeU32(body, 116_397_128L);
writeU32(body, 39_916_527L);
byte[] bodyBytes = body.toByteArray();
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(0x23); out.write(0x23);
out.write(0x02);
out.write(0xFE);
byte[] vinBytes = vin.getBytes(StandardCharsets.US_ASCII);
out.write(vinBytes, 0, 17);
out.write(0x01);
writeU16(out, bodyBytes.length);
out.write(bodyBytes, 0, bodyBytes.length);
byte[] almost = out.toByteArray();
byte bcc = BccChecksum.compute(almost, 2, almost.length - 2);
out.write(bcc & 0xFF);
return out.toByteArray();
}
private static void writeU16(ByteArrayOutputStream os, int v) {
os.write((v >> 8) & 0xFF);
os.write(v & 0xFF);
}
private static void writeU32(ByteArrayOutputStream os, long v) {
os.write((int) ((v >> 24) & 0xFF));
os.write((int) ((v >> 16) & 0xFF));
os.write((int) ((v >> 8) & 0xFF));
os.write((int) (v & 0xFF));
}
}