chore: wire split apps deployment config
Some checks failed
ci/woodpecker/manual/woodpecker Pipeline was canceled

This commit is contained in:
lingniu
2026-06-24 14:38:59 +08:00
parent a70f8f39ad
commit 22468e78b4
23 changed files with 241 additions and 952 deletions

5
.dockerignore Normal file
View File

@@ -0,0 +1,5 @@
.git
.idea
.worktrees
archive
**/.DS_Store

View File

@@ -36,11 +36,11 @@
- **Decision**: 第一批迁移即覆盖 JT1078 信令 + JSATL12 报警附件上传
- **Consequences**: Phase 2 工期相应拉长JSATL12 需要对象存储后端(本地 / S3 / OSS
## ADR-006 部署形态:all-in-one
- **Status**: Accepted
- **Context**: 当前规模无需按协议拆 Pod
- **Decision**: 生产默认 `bootstrap-all`,通过配置开关按需启停各协议模块
- **Consequences**: 单进程资源需求较高;未来若规模扩大可切换到 `bootstrap-slim`(保留能力,不交付)
## ADR-006 部署形态:GB32960 三应用拆分
- **Status**: Superseded
- **Context**: `bootstrap-all` 一体化启动已被 GB32960 拆分部署替代
- **Decision**: 生产默认部署 `gb32960-ingest-app``vehicle-history-app``vehicle-analytics-app` 三个应用;旧 `bootstrap-all` 模块删除
- **Consequences**: CI/CD 分别构建三个镜像;协议接入、历史查询、统计消费可以独立发布和回滚
## ADR-007 Java 25 + 虚拟线程 + Disruptor
- **Status**: Accepted

15
Dockerfile Normal file
View File

@@ -0,0 +1,15 @@
FROM eclipse-temurin:25-jre
ARG APP_NAME
ARG APP_VERSION
ENV APP_NAME=${APP_NAME}
ENV APP_VERSION=${APP_VERSION}
ENV JAVA_OPTS=""
WORKDIR /app
COPY modules/apps/${APP_NAME}/target/${APP_NAME}.jar /app/app.jar
EXPOSE 20100 20200 20300 32960
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /app/app.jar"]

View File

@@ -57,7 +57,9 @@ lingniu-vehicle-ingest/
│ │ └── vehicle-stat-service/ Kafka 全字段事件消费 + 可配置日统计
│ └── apps/
│ ├── command-gateway/ HTTP → 设备下行命令808 位置/参数/属性/控制/区域删除/报警确认 + 1078 音视频控制)
── bootstrap-all/ 一体化启动
── gb32960-ingest-app/ GB32960 TCP 接入 + Kafka 投递
│ ├── vehicle-history-app/ 历史查询 + 原始归档索引消费
│ └── vehicle-analytics-app/ 车辆状态/日统计消费
├── docs/ 架构文档、模块图、实施计划
└── reference/ 参考资料
```
@@ -68,7 +70,7 @@ lingniu-vehicle-ingest/
# 要求JDK 25, Maven 3.9+
mvn -v
mvn clean install -DskipTests
mvn -pl :bootstrap-all spring-boot:run
mvn -pl :gb32960-ingest-app -am spring-boot:run
```
## 迁移说明

View File

@@ -180,10 +180,10 @@ If the split deployment is unhealthy:
1. Stop `gb32960-ingest-app` first so new GB32960 ACKs are not issued against an unhealthy Kafka boundary.
2. Keep Kafka topics intact for replay unless storage or privacy policy requires deletion.
3. Restart or roll back `vehicle-history-app` and `vehicle-analytics-app` independently; their failures do not require protocol ACK rollback because they consume from Kafka offsets.
4. If Kafka itself is unavailable, route GB32960 traffic back to the previous all-in-one runtime only if that runtime is configured with its known-good durability behavior.
4. If Kafka itself is unavailable, stop `gb32960-ingest-app` or route GB32960 traffic to a previously verified release that preserves the Kafka durability boundary.
5. After rollback, compare Kafka consumer group lag and DLQ contents before resuming the split services.
Rollback commands depend on the deployment environment. For local testing, stop the three Java processes and restart the previous `bootstrap-all` workflow if needed.
Rollback commands depend on the deployment environment. For local testing, stop the three Java processes and restart the previous verified app jars or images.
## Task 8 Verification Notes

View File

@@ -1,128 +0,0 @@
<?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>
<relativePath>../../../pom.xml</relativePath>
</parent>
<artifactId>bootstrap-all</artifactId>
<name>bootstrap-all</name>
<description>一体化启动:默认集成全部协议与 Sink。生产环境通过配置开关按需启停。</description>
<dependencies>
<!-- Core -->
<dependency>
<groupId>com.lingniu.ingest</groupId>
<artifactId>ingest-core</artifactId>
</dependency>
<dependency>
<groupId>com.lingniu.ingest</groupId>
<artifactId>session-core</artifactId>
</dependency>
<dependency>
<groupId>com.lingniu.ingest</groupId>
<artifactId>observability</artifactId>
</dependency>
<!-- Sinks -->
<dependency>
<groupId>com.lingniu.ingest</groupId>
<artifactId>sink-mq</artifactId>
</dependency>
<dependency>
<groupId>com.lingniu.ingest</groupId>
<artifactId>sink-archive</artifactId>
</dependency>
<dependency>
<groupId>com.lingniu.ingest</groupId>
<artifactId>event-file-store</artifactId>
</dependency>
<dependency>
<groupId>com.lingniu.ingest</groupId>
<artifactId>event-history-service</artifactId>
</dependency>
<dependency>
<groupId>com.lingniu.ingest</groupId>
<artifactId>vehicle-state-service</artifactId>
</dependency>
<dependency>
<groupId>com.lingniu.ingest</groupId>
<artifactId>vehicle-stat-service</artifactId>
</dependency>
<!-- Protocols -->
<dependency>
<groupId>com.lingniu.ingest</groupId>
<artifactId>protocol-gb32960</artifactId>
</dependency>
<dependency>
<groupId>com.lingniu.ingest</groupId>
<artifactId>protocol-jt808</artifactId>
</dependency>
<dependency>
<groupId>com.lingniu.ingest</groupId>
<artifactId>protocol-jt1078</artifactId>
</dependency>
<dependency>
<groupId>com.lingniu.ingest</groupId>
<artifactId>protocol-jsatl12</artifactId>
</dependency>
<!-- Inbound -->
<dependency>
<groupId>com.lingniu.ingest</groupId>
<artifactId>inbound-mqtt</artifactId>
</dependency>
<dependency>
<groupId>com.lingniu.ingest</groupId>
<artifactId>inbound-xinda-push</artifactId>
</dependency>
<!-- Command Gateway (HTTP) -->
<dependency>
<groupId>com.lingniu.ingest</groupId>
<artifactId>command-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>lingniu-vehicle-ingest</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<!-- JDK 24+ 对所有调用 sun.misc.Unsafe 的库jctools、byte-buddy 等)
打弃用警告,每进程打一次,纯日志噪声。显式 allow 让 JVM 闭嘴,
等上游库迁移到 VarHandle 后即可去掉这个参数。 -->
<jvmArguments>--sun-misc-unsafe-memory-access=allow</jvmArguments>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -1,30 +0,0 @@
package com.lingniu.ingest.bootstrap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 一体化启动入口。
* 各协议 / Sink 模块通过 Spring Boot AutoConfiguration + @ConditionalOnProperty 按需加载。
*
* <p><b>启动失败兜底</b>:某个 Netty server 绑定端口失败时Spring 会尝试销毁已初始化的 Bean。
* 但如果 {@code destroy()} 过程中再次失败或卡住,残留的 Netty boss/worker 线程(非守护线程)
* 会让 JVM 永远不退出,表现为进程仍在监听端口,下次启动时出现 {@code Address already in use}。
* 这里 main 捕获 Throwable 后强制 {@link System#exit(int)},确保进程干净退出。
*/
@SpringBootApplication(scanBasePackages = "com.lingniu.ingest")
public class IngestApplication {
private static final Logger log = LoggerFactory.getLogger(IngestApplication.class);
public static void main(String[] args) {
try {
SpringApplication.run(IngestApplication.class, args);
} catch (Throwable t) {
log.error("Ingest application failed to start, forcing JVM exit", t);
System.exit(1);
}
}
}

View File

@@ -1,237 +0,0 @@
spring:
application:
name: lingniu-vehicle-ingest
threads:
virtual:
enabled: true
springdoc:
swagger-ui:
path: /swagger-ui.html
api-docs:
path: /v3/api-docs
server:
port: 20000
lingniu:
ingest:
gb32960:
enabled: true
port: ${GB32960_PORT:32960}
boss-threads: 1
worker-threads: 0 # 0 = Runtime.availableProcessors() * 2
# VIN 白名单认证内存配置。GB/T 32960 设备没有账号密码,身份由 VIN 决定。
auth:
enabled: ${GB32960_AUTH_ENABLED:false} # true 启用白名单false 放行所有 VIN
case-sensitive: false # 默认大小写不敏感
whitelist: # VIN 列表,未列入的 0x01 登入返回 0x04 VIN_NOT_EXIST
# - LTEST000000000001
# - LGWEF4A58RE123456
# 平台登入 0x05 账号列表GB/T 32960.3 表 29
# 列表为空时放行所有平台登入(兼容旧行为),否则 username/password 必须匹配,
# 失败返回 0x02 OTHER_ERROR 应答并关闭连接。
platforms:
# 现代 (XIANDAI)
- username: ${GB32960_PLATFORM_USER_1:lingniu}
password: ${GB32960_PLATFORM_PWD_1:Lingniu.2024#}
allowed-ips: # 可选:限制来源 IP为空不限制
description: 外部下级平台 - 现代 (原旧服务客户端)
# 现代平台真实 GB32960 接入账号
- username: ${GB32960_PLATFORM_USER_HYUNDAI:Hyundai}
password: ${GB32960_PLATFORM_PWD_HYUNDAI:f2e3445d7cda409fb4f278f6fb890734}
allowed-ips:
- ${GB32960_PLATFORM_IP_HYUNDAI:8.134.95.166}
description: 外部下级平台 - Hyundai
# 飞驰 (FEICHI)
- username: ${GB32960_PLATFORM_USER_2:ln@feichi}
password: ${GB32960_PLATFORM_PWD_2:Lingniu.2024#}
allowed-ips:
description: 外部下级平台 - 飞驰
# 海珀特 (HAIPOTE)
- username: ${GB32960_PLATFORM_USER_3:hbt@lingniu}
password: ${GB32960_PLATFORM_PWD_3:sgmi88b@&9}
allowed-ips:
description: 外部下级平台 - 海珀特
# 跃进 (YUEJIN)
- username: ${GB32960_PLATFORM_USER_4:yj01@lingniu}
password: ${GB32960_PLATFORM_PWD_4:01234567890123456789}
allowed-ips:
description: 外部下级平台 - 跃进
# TLS 双向认证(可选)。启用后 Netty pipeline 前置 SslHandler 并强制客户端证书。
tls:
enabled: ${GB32960_TLS_ENABLED:false}
cert-path: ${GB32960_TLS_CERT:} # 服务端证书 PEM 路径
key-path: ${GB32960_TLS_KEY:} # 服务端私钥 PEM 路径PKCS#8 未加密)
trust-cert-path: ${GB32960_TLS_CA:} # 受信 CA 证书 PEM用于校验客户端证书
require-client-auth: true # 强制双向false=单向 TLS
# ====================================================================
# 厂商扩展协议路由vendor extensions
# 命中规则按 list 顺序首匹first-match-wins命中后用对应套件的 parser 集合
# 在标准 0x01..0x09 之上叠加。已支持的套件:
# - guangdong-fc广东燃料电池汽车示范规范 0x30~0x34、0x80
# 来源reference/广东燃料电池汽车示范应用城市群综合监管平台-...pdf
# ====================================================================
vendor-extensions:
- name: guangdong-fc
match:
# 平台账号匹配精确匹配case-insensitive。这里和上面 auth.platforms
# 中的 username 保持一致,确保 lingniu 账号登入后所有 0x02 帧都走广东 profile。
platform-accounts:
- lingniu
- Hyundai
- ln@feichi
# VIN 前缀匹配startsWith空 list = 不参与匹配
vin-prefixes: []
# VIN 精确匹配
vins: []
# 报文解析容错。默认启用单块异常隔离:某个信息块解析失败时兜成
# InfoBlock.Raw 后继续解析下一个块,不再整帧丢弃。仅在需要严格失败语义
# (灰度回滚或抓虫)时设为 false。
parse:
lenient-block-failure: true
diagnostics:
max-logged-frame-keys-per-channel: ${GB32960_DIAGNOSTICS_MAX_LOGGED_FRAME_KEYS_PER_CHANNEL:4096}
jt808:
# 1078 信令和 JSATL12 嵌入 JT808 信令都依赖本模块;一体化启动默认开启,
# 需要只跑 GB32960 时可用 JT808_ENABLED=false 显式关闭。
enabled: ${JT808_ENABLED:true}
port: ${JT808_PORT:10808}
max-frame-length: ${JT808_MAX_FRAME_LENGTH:1048}
jt1078:
# 1078 媒体流归档可独立启用1078 信令事件复用 JT808 mapper
# 只有同时启用 jt808.enabled=true 时才会装配信令桥接 handler。
enabled: ${JT1078_ENABLED:true}
media-stream:
enabled: ${JT1078_MEDIA_STREAM_ENABLED:true}
tcp-enabled: ${JT1078_MEDIA_STREAM_TCP_ENABLED:true}
udp-enabled: ${JT1078_MEDIA_STREAM_UDP_ENABLED:false}
port: ${JT1078_MEDIA_STREAM_PORT:11078}
udp-port: ${JT1078_MEDIA_STREAM_UDP_PORT:11078}
worker-threads: ${JT1078_MEDIA_STREAM_WORKERS:0}
max-packet-length: ${JT1078_MEDIA_STREAM_MAX_PACKET_LENGTH:1048576}
segment-seconds: ${JT1078_MEDIA_STREAM_SEGMENT_SECONDS:300}
jsatl12:
# JSATL12 附件流依赖 ArchiveStore嵌入的 JT808 信令解析依赖
# jt808.enabled=true。缺任一依赖时不会装配附件接入 server。
enabled: ${JSATL12_ENABLED:true}
port: ${JSATL12_PORT:7612}
worker-threads: ${JSATL12_WORKERS:0}
max-frame-length: ${JSATL12_MAX_FRAME_LENGTH:65536}
file-store: ${JSATL12_FILE_STORE:file:///var/lingniu/alarm-files}
mqtt:
enabled: ${MQTT_ENABLED:false}
endpoints:
- name: yutong
uri: ${MQTT_URI:ssl://cpxlm.axxc.cn:38883}
topic: ${MQTT_TOPIC:/ytforward/shln/+}
qos: 2
clean-session: false
keep-alive-seconds: 20
connection-timeout-seconds: 10
client-id: ${MQTT_CLIENT_ID:shlnClientId}
username: ${MQTT_USER:shln}
password: ${MQTT_PWD:}
tls:
ca-pem: ${MQTT_CA_PEM:/opt/lingniuServices/certificate/yutong/vehicledatareception/ca.crt}
client-pem: ${MQTT_CLIENT_PEM:/opt/lingniuServices/certificate/yutong/vehicledatareception/client.crt}
client-key: ${MQTT_CLIENT_KEY:/opt/lingniuServices/certificate/yutong/vehicledatareception/client-key-pkcs8.pem}
xinda-push:
enabled: ${XINDA_PUSH_ENABLED:false}
host: ${XINDA_HOST:}
port: ${XINDA_PORT:10100}
username: ${XINDA_USER:}
password: ${XINDA_PWD:}
subscribe-msg-ids: ${XINDA_SUBSCRIBE_MSG_IDS:0200,0300,0401}
client-desc: ${XINDA_CLIENT_DESC:lingniu-ingest}
pipeline:
disruptor:
ring-buffer-size: 131072 # 2^17
wait-strategy: yielding # blocking | sleeping | yielding | busy-spin
producer-type: multi
dedup:
enabled: true
cache-size: 200000
ttl-seconds: 600
rate-limit:
per-vin-qps: 50
session:
# memory | redis。生产默认 Redis会话索引用于多实例/重启后的下行命令解析。
store: ${SESSION_STORE:redis}
ttl: ${SESSION_TTL:30m}
identity:
# memory 适合本地开发file 是生产最低可用持久化后端,后续可替换 DB/配置中心实现。
store: ${VEHICLE_IDENTITY_STORE:file}
file:
path: ${VEHICLE_IDENTITY_FILE:./data/vehicle-identity.jsonl}
sink:
mq:
# MQ Sink 总开关。true=装配 Kafka Producer 并投递事件false=完全不装配(适合本地开发或 Kafka 不可达)。
enabled: ${KAFKA_ENABLED:false}
type: kafka # 后端选择kafka | (rocketmq | pulsar 预留)
bootstrap-servers: ${KAFKA_BROKERS:114.55.58.251:9092}
compression-type: zstd
linger-ms: 20
batch-size: 65536
acks: all
enable-idempotence: true
topics:
realtime: vehicle.realtime
location: vehicle.location
alarm: vehicle.alarm
session: vehicle.session
media-meta: vehicle.media.meta
raw-archive: vehicle.raw.archive
dlq: vehicle.dlq
consumer:
# true=启动服务侧 Kafka Envelope 消费 worker默认绑定 event-history/state/stat 三类 processor。
enabled: ${KAFKA_CONSUMER_ENABLED:false}
auto-startup: true
client-id-prefix: ${KAFKA_CONSUMER_CLIENT_ID_PREFIX:lingniu-envelope-consumer}
poll-timeout-millis: ${KAFKA_CONSUMER_POLL_TIMEOUT_MILLIS:1000}
loop-backoff-millis: ${KAFKA_CONSUMER_LOOP_BACKOFF_MILLIS:1000}
auto-offset-reset: ${KAFKA_CONSUMER_AUTO_OFFSET_RESET:earliest}
max-poll-records: ${KAFKA_CONSUMER_MAX_POLL_RECORDS:500}
archive:
enabled: true
type: local # local | s3 | oss
path: ./archive/
event-file-store:
enabled: ${EVENT_FILE_STORE_ENABLED:true}
# 32960 历史索引库根路径。这里保存 Parquet 分区和 DuckDB sidecar 索引;
# 全字段查询仍会按 rawArchiveUri 回读 archive 根目录下的 .bin 原始包。
path: ${EVENT_FILE_STORE_PATH:./target/event-store/}
zone-id: ${EVENT_FILE_STORE_ZONE_ID:Asia/Shanghai}
batch-size: ${EVENT_FILE_STORE_BATCH_SIZE:500}
flush-interval-millis: ${EVENT_FILE_STORE_FLUSH_INTERVAL_MILLIS:1000}
event-history:
enabled: ${EVENT_HISTORY_ENABLED:true}
vehicle-state:
enabled: ${VEHICLE_STATE_ENABLED:false}
vehicle-stat:
enabled: ${VEHICLE_STAT_ENABLED:true}
file-path: ${VEHICLE_STAT_FILE_PATH:./target/vehicle-stat/}
zone-id: ${VEHICLE_STAT_ZONE_ID:Asia/Shanghai}
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus,env
metrics:
tags:
application: lingniu-vehicle-ingest
logging:
level:
root: INFO
com.lingniu.ingest: INFO
com.lingniu.ingest.protocol.gb32960.inbound.Gb32960ChannelHandler: INFO
com.lingniu.ingest.protocol.gb32960.codec.Gb32960FrameDecoder: INFO
com.lingniu.ingest.protocol.gb32960.codec.Gb32960BodyParser: INFO
# 信达 push 诊断:打开下面一行可以看到每一条收发的原始字节 hex dump
com.lingniu.ingest.inbound.xinda: INFO

View File

@@ -1,134 +0,0 @@
package com.lingniu.ingest.bootstrap;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
import org.springframework.core.io.ClassPathResource;
import java.util.Properties;
import static org.assertj.core.api.Assertions.assertThat;
class BootstrapApplicationDefaultsTest {
@Test
void defaultConfigEnablesJt808WhenDependentProtocolsAreEnabledByDefault() {
Properties props = loadApplicationProperties();
assertThat(defaultEnabled(props.getProperty("lingniu.ingest.jsatl12.enabled"))).isTrue();
assertThat(defaultEnabled(props.getProperty("lingniu.ingest.jt1078.enabled"))).isTrue();
assertThat(defaultEnabled(props.getProperty("lingniu.ingest.jt808.enabled")))
.as("JSATL12 and JT1078 signaling depend on JT808 decoder/mapper, so bootstrap defaults must not disable JT808")
.isTrue();
}
@Test
void productionProtocolEntrypointsAreControlledByEnvironmentFlags() {
Properties props = loadApplicationProperties();
assertThat(props.getProperty("lingniu.ingest.jt808.enabled")).isEqualTo("${JT808_ENABLED:true}");
assertThat(props.getProperty("lingniu.ingest.jt1078.enabled")).isEqualTo("${JT1078_ENABLED:true}");
assertThat(props.getProperty("lingniu.ingest.jsatl12.enabled")).isEqualTo("${JSATL12_ENABLED:true}");
assertThat(props.getProperty("lingniu.ingest.mqtt.enabled")).isEqualTo("${MQTT_ENABLED:false}");
assertThat(props.getProperty("lingniu.ingest.xinda-push.enabled")).isEqualTo("${XINDA_PUSH_ENABLED:false}");
}
@Test
void defaultConfigKeepsLegacyProductionIngressPortsAndYutongMqttShape() {
Properties props = loadApplicationProperties();
assertThat(props.getProperty("lingniu.ingest.gb32960.port")).isEqualTo("${GB32960_PORT:32960}");
assertThat(props.getProperty("lingniu.ingest.jt808.port")).isEqualTo("${JT808_PORT:10808}");
assertThat(props.getProperty("lingniu.ingest.jsatl12.port")).isEqualTo("${JSATL12_PORT:7612}");
assertThat(props.getProperty("lingniu.ingest.jt1078.media-stream.port"))
.isEqualTo("${JT1078_MEDIA_STREAM_PORT:11078}");
assertThat(props.getProperty("lingniu.ingest.jt1078.media-stream.udp-port"))
.isEqualTo("${JT1078_MEDIA_STREAM_UDP_PORT:11078}");
assertThat(props.getProperty("lingniu.ingest.mqtt.endpoints[0].name")).isEqualTo("yutong");
assertThat(props.getProperty("lingniu.ingest.mqtt.endpoints[0].uri"))
.isEqualTo("${MQTT_URI:ssl://cpxlm.axxc.cn:38883}");
assertThat(props.getProperty("lingniu.ingest.mqtt.endpoints[0].topic"))
.isEqualTo("${MQTT_TOPIC:/ytforward/shln/+}");
assertThat(props.getProperty("lingniu.ingest.mqtt.endpoints[0].qos")).isEqualTo("2");
assertThat(props.getProperty("lingniu.ingest.mqtt.endpoints[0].clean-session")).isEqualTo("false");
assertThat(props.getProperty("lingniu.ingest.mqtt.endpoints[0].keep-alive-seconds")).isEqualTo("20");
assertThat(props.getProperty("lingniu.ingest.mqtt.endpoints[0].connection-timeout-seconds")).isEqualTo("10");
assertThat(props.getProperty("lingniu.ingest.mqtt.endpoints[0].client-id"))
.isEqualTo("${MQTT_CLIENT_ID:shlnClientId}");
assertThat(props.getProperty("lingniu.ingest.mqtt.endpoints[0].username"))
.isEqualTo("${MQTT_USER:shln}");
assertThat(props.getProperty("lingniu.ingest.mqtt.endpoints[0].password"))
.isEqualTo("${MQTT_PWD:}");
assertThat(props.getProperty("lingniu.ingest.mqtt.endpoints[0].tls.ca-pem"))
.isEqualTo("${MQTT_CA_PEM:/opt/lingniuServices/certificate/yutong/vehicledatareception/ca.crt}");
assertThat(props.getProperty("lingniu.ingest.mqtt.endpoints[0].tls.client-pem"))
.isEqualTo("${MQTT_CLIENT_PEM:/opt/lingniuServices/certificate/yutong/vehicledatareception/client.crt}");
assertThat(props.getProperty("lingniu.ingest.mqtt.endpoints[0].tls.client-key"))
.isEqualTo("${MQTT_CLIENT_KEY:/opt/lingniuServices/certificate/yutong/vehicledatareception/client-key-pkcs8.pem}");
assertThat(props.getProperty("lingniu.ingest.sink.mq.bootstrap-servers"))
.isEqualTo("${KAFKA_BROKERS:114.55.58.251:9092}");
assertThat(props.getProperty("lingniu.ingest.session.store")).isEqualTo("${SESSION_STORE:redis}");
assertThat(props.getProperty("lingniu.ingest.session.ttl")).isEqualTo("${SESSION_TTL:30m}");
}
@Test
void productionEventHistoryQueryApiIsEnabledByDefault() {
Properties props = loadApplicationProperties();
assertThat(props.getProperty("lingniu.ingest.event-history.enabled"))
.isEqualTo("${EVENT_HISTORY_ENABLED:true}");
}
@Test
void productionVehicleStatisticsAreEnabledByDefault() {
Properties props = loadApplicationProperties();
assertThat(props.getProperty("lingniu.ingest.vehicle-stat.enabled"))
.isEqualTo("${VEHICLE_STAT_ENABLED:true}");
assertThat(props.getProperty("lingniu.ingest.vehicle-stat.file-path"))
.isEqualTo("${VEHICLE_STAT_FILE_PATH:./target/vehicle-stat/}");
assertThat(props.getProperty("lingniu.ingest.vehicle-stat.zone-id"))
.isEqualTo("${VEHICLE_STAT_ZONE_ID:Asia/Shanghai}");
}
@Test
void swaggerUiAndOpenApiDocsUseStableDefaultPaths() {
Properties props = loadApplicationProperties();
assertThat(props.getProperty("springdoc.swagger-ui.path")).isEqualTo("/swagger-ui.html");
assertThat(props.getProperty("springdoc.api-docs.path")).isEqualTo("/v3/api-docs");
}
@Test
void hyundaiPlatformUsesGuangdongFuelCellVendorProfileByDefault() {
Properties props = loadApplicationProperties();
assertThat(props.getProperty("lingniu.ingest.gb32960.vendor-extensions[0].name"))
.isEqualTo("guangdong-fc");
assertThat(props.getProperty("lingniu.ingest.gb32960.vendor-extensions[0].match.platform-accounts[1]"))
.isEqualTo("Hyundai");
}
private static Properties loadApplicationProperties() {
YamlPropertiesFactoryBean yaml = new YamlPropertiesFactoryBean();
yaml.setResources(new ClassPathResource("application.yml"));
Properties props = yaml.getObject();
assertThat(props).isNotNull();
return props;
}
private static boolean defaultEnabled(String value) {
if (value == null) {
return false;
}
String normalized = value.trim();
if ("true".equalsIgnoreCase(normalized)) {
return true;
}
if ("false".equalsIgnoreCase(normalized)) {
return false;
}
return normalized.matches("\\$\\{[^:}]+:true}");
}
}

View File

@@ -1,166 +0,0 @@
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));
}
}

View File

@@ -1,73 +0,0 @@
package com.lingniu.ingest.bootstrap;
import com.lingniu.ingest.core.config.IngestCoreAutoConfiguration;
import com.lingniu.ingest.identity.config.VehicleIdentityAutoConfiguration;
import com.lingniu.ingest.inbound.mqtt.client.MqttEndpointManager;
import com.lingniu.ingest.inbound.mqtt.config.MqttInboundAutoConfiguration;
import com.lingniu.ingest.inbound.xinda.XindaPushClient;
import com.lingniu.ingest.inbound.xinda.config.XindaPushAutoConfiguration;
import com.lingniu.ingest.protocol.jsatl12.config.Jsatl12AutoConfiguration;
import com.lingniu.ingest.protocol.jsatl12.inbound.Jsatl12NettyServer;
import com.lingniu.ingest.protocol.jt1078.config.Jt1078AutoConfiguration;
import com.lingniu.ingest.protocol.jt1078.config.Jt1078SignalAutoConfiguration;
import com.lingniu.ingest.protocol.jt1078.handler.Jt1078MediaSignalHandler;
import com.lingniu.ingest.protocol.jt1078.media.Jt1078MediaArchiveService;
import com.lingniu.ingest.protocol.jt1078.media.Jt1078MediaStreamServer;
import com.lingniu.ingest.protocol.jt808.config.Jt808AutoConfiguration;
import com.lingniu.ingest.protocol.jt808.inbound.Jt808NettyServer;
import com.lingniu.ingest.session.config.SessionCoreAutoConfiguration;
import com.lingniu.ingest.sink.archive.config.SinkArchiveAutoConfiguration;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
class ProtocolAutoConfigurationCompositionTest {
@TempDir
Path archiveRoot;
@Test
void nonGb32960ProtocolAdaptersCanBeComposedWithoutExternalConnections() {
new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(
IngestCoreAutoConfiguration.class,
SessionCoreAutoConfiguration.class,
VehicleIdentityAutoConfiguration.class,
SinkArchiveAutoConfiguration.class,
Jt1078AutoConfiguration.class,
Jt808AutoConfiguration.class,
Jt1078SignalAutoConfiguration.class,
Jsatl12AutoConfiguration.class,
MqttInboundAutoConfiguration.class,
XindaPushAutoConfiguration.class))
.withPropertyValues(
"lingniu.ingest.sink.archive.enabled=true",
"lingniu.ingest.sink.archive.type=local",
"lingniu.ingest.sink.archive.path=" + archiveRoot,
"lingniu.ingest.jt808.enabled=true",
"lingniu.ingest.jt808.port=0",
"lingniu.ingest.jt1078.enabled=true",
"lingniu.ingest.jt1078.media-stream.enabled=true",
"lingniu.ingest.jt1078.media-stream.tcp-enabled=true",
"lingniu.ingest.jt1078.media-stream.udp-enabled=false",
"lingniu.ingest.jt1078.media-stream.port=0",
"lingniu.ingest.jsatl12.enabled=true",
"lingniu.ingest.jsatl12.port=0",
"lingniu.ingest.mqtt.enabled=true",
"lingniu.ingest.xinda-push.enabled=true")
.run(context -> {
assertThat(context).hasSingleBean(Jt808NettyServer.class);
assertThat(context).hasSingleBean(Jt1078MediaSignalHandler.class);
assertThat(context).hasSingleBean(Jt1078MediaArchiveService.class);
assertThat(context).hasSingleBean(Jt1078MediaStreamServer.class);
assertThat(context).hasSingleBean(Jsatl12NettyServer.class);
assertThat(context).hasSingleBean(MqttEndpointManager.class);
assertThat(context).hasSingleBean(XindaPushClient.class);
});
}
}

View File

@@ -1,174 +0,0 @@
package com.lingniu.ingest.bootstrap;
import com.lingniu.ingest.codec.BccChecksum;
import com.lingniu.ingest.protocol.gb32960.inbound.Gb32960NettyServer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import java.io.ByteArrayOutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.Instant;
import java.util.List;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
/**
* 端到端验证原始报文冷存链路:
*
* <pre>
* TCP → Gb32960 Netty → Dispatcher 发 RawArchive 事件
* → Disruptor 虚拟线程 → ArchiveEventSink.publish
* → LocalArchiveStore.put → 文件系统落盘
* </pre>
*
* <p>与 {@link IngestEndToEndTest} 互补:前者验证业务事件路径;这里验证 raw archive
* 路径。两个测试跑在独立 Spring 上下文里archive 是否启用的配置不同)。
*/
@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",
"lingniu.ingest.sink.mq.enabled=false",
// 启用 archive sink路径由 @DynamicPropertySource 注入
"lingniu.ingest.sink.archive.enabled=true",
"lingniu.ingest.sink.archive.type=local"
})
class RawArchiveEndToEndTest {
@TempDir
static Path archiveRoot;
@DynamicPropertySource
static void properties(DynamicPropertyRegistry registry) {
registry.add("lingniu.ingest.sink.archive.path", archiveRoot::toString);
}
@Autowired
private Gb32960NettyServer server;
@Test
void gb32960FrameIsArchivedToDisk() throws Exception {
int port = server.getBoundPort();
assertThat(port).isGreaterThan(0);
String vin = "LTEST0000ARCH0001";
byte[] frame = buildRealtimeFrame(vin);
try (Socket socket = new Socket("127.0.0.1", port)) {
socket.getOutputStream().write(frame);
socket.getOutputStream().flush();
}
// 预期落盘目录:<root>/yyyy/MM/dd/GB32960/<vin>/
// ingestTime 在 Dispatcher 里用 receivedAt = Instant.now(),跨天边界理论上今天/昨天两个候选都可能命中,
// 为避免边界脆弱,扫描 root 下所有 .bin 文件匹配 vin。
Path file = awaitArchivedFile(archiveRoot, vin, 3000);
assertThat(file)
.as("expected an archived frame for vin=%s under %s within 3s", vin, archiveRoot)
.isNotNull();
assertThat(Files.readAllBytes(file)).containsExactly(frame);
// 额外key 的日期分片应是 ingestTime 的 UTC 当天(允许跨天时两者都可接受)
String today = DateTimeFormatter.ofPattern("yyyy/MM/dd").withZone(ZoneOffset.UTC).format(Instant.now());
String yesterday = DateTimeFormatter.ofPattern("yyyy/MM/dd").withZone(ZoneOffset.UTC)
.format(Instant.now().minusSeconds(86400));
String rel = archiveRoot.relativize(file).toString().replace('\\', '/');
assertThat(rel).satisfiesAnyOf(
r -> assertThat(r).startsWith(today + "/GB32960/" + vin + "/"),
r -> assertThat(r).startsWith(yesterday + "/GB32960/" + vin + "/"));
assertThat(rel).endsWith(".bin");
}
private static Path awaitArchivedFile(Path root, String vin, long timeoutMs) throws Exception {
long deadline = System.currentTimeMillis() + timeoutMs;
while (System.currentTimeMillis() < deadline) {
Path hit = findFirst(root, vin);
if (hit != null) return hit;
Thread.sleep(50);
}
return null;
}
private static Path findFirst(Path root, String vin) throws Exception {
if (!Files.exists(root)) return null;
try (Stream<Path> walk = Files.walk(root)) {
List<Path> hits = walk
.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".bin"))
.filter(p -> p.toString().contains("/" + vin + "/") || p.toString().contains("\\" + vin + "\\"))
.toList();
return hits.isEmpty() ? null : hits.get(0);
}
}
// ===== frame builder (inlined copy of IngestEndToEndTest#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);
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);
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));
}
}

View File

@@ -37,6 +37,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
@@ -57,6 +61,7 @@
<execution>
<goals>
<goal>repackage</goal>
<goal>build-info</goal>
</goals>
</execution>
</executions>

View File

@@ -1,6 +1,19 @@
spring:
application:
name: gb32960-ingest-app
config:
import:
- optional:nacos:${spring.application.name}.${NACOS_CONFIG_FILE_EXTENSION:yml}?group=${NACOS_GROUP:DEFAULT_GROUP}&refreshEnabled=${NACOS_REFRESH_ENABLED:true}
cloud:
nacos:
config:
enabled: ${NACOS_CONFIG_ENABLED:true}
server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8848}
namespace: ${NACOS_NAMESPACE:}
group: ${NACOS_GROUP:DEFAULT_GROUP}
file-extension: ${NACOS_CONFIG_FILE_EXTENSION:yml}
username: ${NACOS_USERNAME:}
password: ${NACOS_PASSWORD:}
threads:
virtual:
enabled: true

View File

@@ -16,6 +16,10 @@ class Gb32960IngestAppDefaultsTest {
assertThat(properties)
.containsEntry("spring.application.name", "gb32960-ingest-app")
.containsEntry(
"spring.config.import[0]",
"optional:nacos:${spring.application.name}.${NACOS_CONFIG_FILE_EXTENSION:yml}?group=${NACOS_GROUP:DEFAULT_GROUP}&refreshEnabled=${NACOS_REFRESH_ENABLED:true}")
.containsEntry("spring.cloud.nacos.config.server-addr", "${NACOS_SERVER_ADDR:127.0.0.1:8848}")
.containsEntry("lingniu.ingest.gb32960.enabled", true)
.containsEntry("lingniu.ingest.gb32960.server.enabled", true)
.containsEntry("lingniu.ingest.gb32960.port", "${GB32960_PORT:32960}")

View File

@@ -37,6 +37,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
@@ -57,6 +61,7 @@
<execution>
<goals>
<goal>repackage</goal>
<goal>build-info</goal>
</goals>
</execution>
</executions>

View File

@@ -1,6 +1,19 @@
spring:
application:
name: vehicle-analytics-app
config:
import:
- optional:nacos:${spring.application.name}.${NACOS_CONFIG_FILE_EXTENSION:yml}?group=${NACOS_GROUP:DEFAULT_GROUP}&refreshEnabled=${NACOS_REFRESH_ENABLED:true}
cloud:
nacos:
config:
enabled: ${NACOS_CONFIG_ENABLED:true}
server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8848}
namespace: ${NACOS_NAMESPACE:}
group: ${NACOS_GROUP:DEFAULT_GROUP}
file-extension: ${NACOS_CONFIG_FILE_EXTENSION:yml}
username: ${NACOS_USERNAME:}
password: ${NACOS_PASSWORD:}
threads:
virtual:
enabled: true

View File

@@ -16,6 +16,10 @@ class VehicleAnalyticsAppDefaultsTest {
assertThat(properties)
.containsEntry("spring.application.name", "vehicle-analytics-app")
.containsEntry(
"spring.config.import[0]",
"optional:nacos:${spring.application.name}.${NACOS_CONFIG_FILE_EXTENSION:yml}?group=${NACOS_GROUP:DEFAULT_GROUP}&refreshEnabled=${NACOS_REFRESH_ENABLED:true}")
.containsEntry("spring.cloud.nacos.config.server-addr", "${NACOS_SERVER_ADDR:127.0.0.1:8848}")
.containsEntry("lingniu.ingest.gb32960.enabled", false)
.containsEntry("lingniu.ingest.sink.archive.enabled", false)
.containsEntry("lingniu.ingest.event-file-store.enabled", false)

View File

@@ -45,6 +45,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
@@ -65,6 +69,7 @@
<execution>
<goals>
<goal>repackage</goal>
<goal>build-info</goal>
</goals>
</execution>
</executions>

View File

@@ -1,6 +1,19 @@
spring:
application:
name: vehicle-history-app
config:
import:
- optional:nacos:${spring.application.name}.${NACOS_CONFIG_FILE_EXTENSION:yml}?group=${NACOS_GROUP:DEFAULT_GROUP}&refreshEnabled=${NACOS_REFRESH_ENABLED:true}
cloud:
nacos:
config:
enabled: ${NACOS_CONFIG_ENABLED:true}
server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8848}
namespace: ${NACOS_NAMESPACE:}
group: ${NACOS_GROUP:DEFAULT_GROUP}
file-extension: ${NACOS_CONFIG_FILE_EXTENSION:yml}
username: ${NACOS_USERNAME:}
password: ${NACOS_PASSWORD:}
threads:
virtual:
enabled: true

View File

@@ -16,6 +16,10 @@ class VehicleHistoryAppDefaultsTest {
assertThat(properties)
.containsEntry("spring.application.name", "vehicle-history-app")
.containsEntry(
"spring.config.import[0]",
"optional:nacos:${spring.application.name}.${NACOS_CONFIG_FILE_EXTENSION:yml}?group=${NACOS_GROUP:DEFAULT_GROUP}&refreshEnabled=${NACOS_REFRESH_ENABLED:true}")
.containsEntry("spring.cloud.nacos.config.server-addr", "${NACOS_SERVER_ADDR:127.0.0.1:8848}")
.containsEntry("lingniu.ingest.gb32960.enabled", true)
.containsEntry("lingniu.ingest.gb32960.server.enabled", false)
.containsEntry("lingniu.ingest.sink.archive.enabled", "${SINK_ARCHIVE_ENABLED:true}")

View File

@@ -39,7 +39,6 @@
<module>modules/apps/gb32960-ingest-app</module>
<module>modules/apps/vehicle-history-app</module>
<module>modules/apps/vehicle-analytics-app</module>
<module>modules/apps/bootstrap-all</module>
</modules>
<properties>
@@ -51,6 +50,7 @@
<maven.compiler.release>25</maven.compiler.release>
<spring-boot.version>3.5.3</spring-boot.version>
<spring-cloud-alibaba.version>2025.0.0.0</spring-cloud-alibaba.version>
<netty.version>4.2.9.Final</netty.version>
<disruptor.version>4.0.0</disruptor.version>
<protobuf.version>4.28.3</protobuf.version>
@@ -93,6 +93,13 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-bom</artifactId>

136
woodpecker.yml Normal file
View File

@@ -0,0 +1,136 @@
steps:
- name: maven-build
image: maven:3.9.16-eclipse-temurin-25
when:
event:
- push
- pull_request
- manual
branch:
- master
- develop
- main
commands: |
cd $CI_WORKSPACE
mvn -B -ntp -am -pl :gb32960-ingest-app,:vehicle-history-app,:vehicle-analytics-app package
BRANCH_NAME=$(echo ${CI_COMMIT_BRANCH:-local} | tr / -)
PROJECT_VERSION=$(mvn -q -DforceStdout help:evaluate -Dexpression=project.version)
SHORT_SHA=$(echo ${CI_COMMIT_SHA:-manual} | cut -c1-8)
IMAGE_VERSION="$BRANCH_NAME-$PROJECT_VERSION-$SHORT_SHA"
echo "Project version: $PROJECT_VERSION"
echo "Image version: $IMAGE_VERSION"
echo "$IMAGE_VERSION" > $CI_WORKSPACE/project_version.txt
- name: docker-build-gb32960-ingest-app
image: docker:24.0.5-cli
when:
event:
- push
- pull_request
- manual
branch:
- master
- develop
- main
volumes:
- /var/run/docker.sock:/var/run/docker.sock
commands: |
cd $CI_WORKSPACE
APP_NAME=gb32960-ingest-app
APP_VERSION=$(cat $CI_WORKSPACE/project_version.txt)
IMAGE=harbor.lnh2e.com/lingniu-v1/$APP_NAME:$APP_VERSION
docker build \
--build-arg APP_NAME=$APP_NAME \
--build-arg APP_VERSION=$APP_VERSION \
-t $IMAGE .
mkdir -p /root/.docker
cat > /root/.docker/config.json <<EOF
{
"auths": {
"harbor.lnh2e.com": {
"auth": "$(echo Y2ljZDpMbkBjaWNkMDE=)"
}
}
}
EOF
docker push $IMAGE
- name: docker-build-vehicle-history-app
image: docker:24.0.5-cli
when:
event:
- push
- pull_request
- manual
branch:
- master
- develop
- main
volumes:
- /var/run/docker.sock:/var/run/docker.sock
commands: |
cd $CI_WORKSPACE
APP_NAME=vehicle-history-app
APP_VERSION=$(cat $CI_WORKSPACE/project_version.txt)
IMAGE=harbor.lnh2e.com/lingniu-v1/$APP_NAME:$APP_VERSION
docker build \
--build-arg APP_NAME=$APP_NAME \
--build-arg APP_VERSION=$APP_VERSION \
-t $IMAGE .
mkdir -p /root/.docker
cat > /root/.docker/config.json <<EOF
{
"auths": {
"harbor.lnh2e.com": {
"auth": "$(echo Y2ljZDpMbkBjaWNkMDE=)"
}
}
}
EOF
docker push $IMAGE
- name: docker-build-vehicle-analytics-app
image: docker:24.0.5-cli
when:
event:
- push
- pull_request
- manual
branch:
- master
- develop
- main
volumes:
- /var/run/docker.sock:/var/run/docker.sock
commands: |
cd $CI_WORKSPACE
APP_NAME=vehicle-analytics-app
APP_VERSION=$(cat $CI_WORKSPACE/project_version.txt)
IMAGE=harbor.lnh2e.com/lingniu-v1/$APP_NAME:$APP_VERSION
docker build \
--build-arg APP_NAME=$APP_NAME \
--build-arg APP_VERSION=$APP_VERSION \
-t $IMAGE .
mkdir -p /root/.docker
cat > /root/.docker/config.json <<EOF
{
"auths": {
"harbor.lnh2e.com": {
"auth": "$(echo Y2ljZDpMbkBjaWNkMDE=)"
}
}
}
EOF
docker push $IMAGE