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:
41
session-core/pom.xml
Normal file
41
session-core/pom.xml
Normal file
@@ -0,0 +1,41 @@
|
||||
<?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>session-core</artifactId>
|
||||
<name>session-core</name>
|
||||
<description>设备会话 + 鉴权 + Token + Redis 兜底。</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.lingniu.ingest</groupId>
|
||||
<artifactId>ingest-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-autoconfigure</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||
<artifactId>caffeine</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>
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.lingniu.ingest.session;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* 下行命令调度器 SPI。
|
||||
*
|
||||
* <p>用于 command-gateway 把 HTTP 请求转为设备命令,等待设备应答。真正的实现需要与协议
|
||||
* 模块(如 jt808)的 Netty 通道绑定:按 sessionId 找到 Channel → 发送命令字节 → 用
|
||||
* {@code CompletableFuture + 序列号 map} 实现同步请求/应答。
|
||||
*
|
||||
* <p>当前阶段提供接口 + 占位实现,具体 Netty 绑定在 command-gateway / protocol-jt808
|
||||
* 的下一批次落地。
|
||||
*/
|
||||
public interface CommandDispatcher {
|
||||
|
||||
/** 异步下发命令,不等应答。 */
|
||||
CompletableFuture<Void> notify(String sessionId, Object command);
|
||||
|
||||
/** 同步下发命令并等待应答。 */
|
||||
<T> CompletableFuture<T> request(String sessionId, Object command, Class<T> responseType, Duration timeout);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.lingniu.ingest.session;
|
||||
|
||||
import com.lingniu.ingest.api.ProtocolId;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 设备会话领域模型。不可变;更新通过 {@link SessionStore#update} 原子替换。
|
||||
*
|
||||
* @param sessionId 会话 ID(通常由 Netty channel id + 终端号生成)
|
||||
* @param protocol 协议 ID
|
||||
* @param vin 车架号(注册/鉴权后补齐,注册前为 null)
|
||||
* @param phone 终端手机号 / 设备号
|
||||
* @param token 鉴权 Token(平台下发)
|
||||
* @param peerAddress 对端 IP:port
|
||||
* @param registeredAt 注册时间
|
||||
* @param lastSeenAt 最后活跃时间
|
||||
* @param attributes 协议特定属性(协议版本、型号等)
|
||||
*/
|
||||
public record DeviceSession(
|
||||
String sessionId,
|
||||
ProtocolId protocol,
|
||||
String vin,
|
||||
String phone,
|
||||
String token,
|
||||
String peerAddress,
|
||||
Instant registeredAt,
|
||||
Instant lastSeenAt,
|
||||
Map<String, String> attributes
|
||||
) {
|
||||
public DeviceSession touch(Instant now) {
|
||||
return new DeviceSession(sessionId, protocol, vin, phone, token, peerAddress,
|
||||
registeredAt, now, attributes);
|
||||
}
|
||||
|
||||
public DeviceSession withVin(String vin) {
|
||||
return new DeviceSession(sessionId, protocol, vin, phone, token, peerAddress,
|
||||
registeredAt, lastSeenAt, attributes);
|
||||
}
|
||||
|
||||
public DeviceSession withToken(String token) {
|
||||
return new DeviceSession(sessionId, protocol, vin, phone, token, peerAddress,
|
||||
registeredAt, lastSeenAt, attributes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.lingniu.ingest.session;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
/**
|
||||
* 纯内存会话存储。支持按 sessionId / vin / phone 三种 key 查询。
|
||||
*
|
||||
* <p>失活清理:基于 {@link Caffeine} 的 {@code expireAfterAccess 30 分钟}。
|
||||
* 生产环境若需多节点一致性,可用 Redis 实现替换此 Bean。
|
||||
*/
|
||||
public final class InMemorySessionStore implements SessionStore {
|
||||
|
||||
private final Cache<String, DeviceSession> byId;
|
||||
private final ConcurrentMap<String, String> vinToId = new ConcurrentHashMap<>();
|
||||
private final ConcurrentMap<String, String> phoneToId = new ConcurrentHashMap<>();
|
||||
|
||||
public InMemorySessionStore() {
|
||||
this.byId = Caffeine.newBuilder()
|
||||
.expireAfterAccess(Duration.ofMinutes(30))
|
||||
.removalListener((String sid, DeviceSession s, com.github.benmanes.caffeine.cache.RemovalCause c) -> {
|
||||
if (s == null) return;
|
||||
if (s.vin() != null) vinToId.remove(s.vin(), sid);
|
||||
if (s.phone() != null) phoneToId.remove(s.phone(), sid);
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void put(DeviceSession session) {
|
||||
byId.put(session.sessionId(), session);
|
||||
if (session.vin() != null) vinToId.put(session.vin(), session.sessionId());
|
||||
if (session.phone() != null) phoneToId.put(session.phone(), session.sessionId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<DeviceSession> findBySessionId(String sessionId) {
|
||||
return Optional.ofNullable(byId.getIfPresent(sessionId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<DeviceSession> findByVin(String vin) {
|
||||
String sid = vinToId.get(vin);
|
||||
return sid == null ? Optional.empty() : findBySessionId(sid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<DeviceSession> findByPhone(String phone) {
|
||||
String sid = phoneToId.get(phone);
|
||||
return sid == null ? Optional.empty() : findBySessionId(sid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized Optional<DeviceSession> update(String sessionId, UnaryOperator<DeviceSession> updater) {
|
||||
DeviceSession old = byId.getIfPresent(sessionId);
|
||||
if (old == null) return Optional.empty();
|
||||
DeviceSession next = updater.apply(old);
|
||||
put(next);
|
||||
return Optional.of(next);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(String sessionId) {
|
||||
DeviceSession s = byId.getIfPresent(sessionId);
|
||||
if (s != null) {
|
||||
byId.invalidate(sessionId);
|
||||
if (s.vin() != null) vinToId.remove(s.vin(), sessionId);
|
||||
if (s.phone() != null) phoneToId.remove(s.phone(), sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return (int) byId.estimatedSize();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.lingniu.ingest.session;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* 占位命令调度器:返回 {@link UnsupportedOperationException},
|
||||
* 待 command-gateway 与协议模块的下行通道打通后替换为真实实现。
|
||||
*/
|
||||
public final class NoopCommandDispatcher implements CommandDispatcher {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(NoopCommandDispatcher.class);
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> notify(String sessionId, Object command) {
|
||||
log.warn("[noop] notify sessionId={} command={} dropped", sessionId, command);
|
||||
return CompletableFuture.failedFuture(new UnsupportedOperationException(
|
||||
"CommandDispatcher not yet implemented; configure a real one in protocol module."));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> CompletableFuture<T> request(String sessionId, Object command, Class<T> responseType, Duration timeout) {
|
||||
log.warn("[noop] request sessionId={} command={} responseType={} dropped",
|
||||
sessionId, command, responseType.getSimpleName());
|
||||
return CompletableFuture.failedFuture(new UnsupportedOperationException(
|
||||
"CommandDispatcher not yet implemented; configure a real one in protocol module."));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.lingniu.ingest.session;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
/**
|
||||
* 设备会话存储 SPI。默认实现是内存 + Caffeine,生产可替换为 Redis 兜底。
|
||||
*/
|
||||
public interface SessionStore {
|
||||
|
||||
void put(DeviceSession session);
|
||||
|
||||
Optional<DeviceSession> findBySessionId(String sessionId);
|
||||
|
||||
Optional<DeviceSession> findByVin(String vin);
|
||||
|
||||
Optional<DeviceSession> findByPhone(String phone);
|
||||
|
||||
/** 原子更新:读 → 修改 → 写。返回更新后的会话;若原会话不存在返回 empty。 */
|
||||
Optional<DeviceSession> update(String sessionId, UnaryOperator<DeviceSession> updater);
|
||||
|
||||
void remove(String sessionId);
|
||||
|
||||
int size();
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.lingniu.ingest.session.config;
|
||||
|
||||
import com.lingniu.ingest.session.CommandDispatcher;
|
||||
import com.lingniu.ingest.session.InMemorySessionStore;
|
||||
import com.lingniu.ingest.session.NoopCommandDispatcher;
|
||||
import com.lingniu.ingest.session.SessionStore;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
@AutoConfiguration
|
||||
public class SessionCoreAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public SessionStore sessionStore() {
|
||||
return new InMemorySessionStore();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public CommandDispatcher commandDispatcher() {
|
||||
return new NoopCommandDispatcher();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
com.lingniu.ingest.session.config.SessionCoreAutoConfiguration
|
||||
Reference in New Issue
Block a user