diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..08b6fcca --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +.git +.idea +.worktrees +archive +**/.DS_Store diff --git a/DECISIONS.md b/DECISIONS.md index 528f8341..fef0d63d 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -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 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..015fa950 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/README.md b/README.md index 33524d7d..d4ba0b9e 100644 --- a/README.md +++ b/README.md @@ -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 ``` ## 迁移说明 diff --git a/docs/operations/gb32960-service-split-runbook.md b/docs/operations/gb32960-service-split-runbook.md index 07a02f46..481b35c6 100644 --- a/docs/operations/gb32960-service-split-runbook.md +++ b/docs/operations/gb32960-service-split-runbook.md @@ -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 diff --git a/modules/apps/bootstrap-all/pom.xml b/modules/apps/bootstrap-all/pom.xml deleted file mode 100644 index c63df75c..00000000 --- a/modules/apps/bootstrap-all/pom.xml +++ /dev/null @@ -1,128 +0,0 @@ - - - 4.0.0 - - com.lingniu.ingest - lingniu-vehicle-ingest - 0.1.0-SNAPSHOT - ../../../pom.xml - - bootstrap-all - bootstrap-all - 一体化启动:默认集成全部协议与 Sink。生产环境通过配置开关按需启停。 - - - - - com.lingniu.ingest - ingest-core - - - com.lingniu.ingest - session-core - - - com.lingniu.ingest - observability - - - - - com.lingniu.ingest - sink-mq - - - com.lingniu.ingest - sink-archive - - - com.lingniu.ingest - event-file-store - - - com.lingniu.ingest - event-history-service - - - com.lingniu.ingest - vehicle-state-service - - - com.lingniu.ingest - vehicle-stat-service - - - - - com.lingniu.ingest - protocol-gb32960 - - - com.lingniu.ingest - protocol-jt808 - - - com.lingniu.ingest - protocol-jt1078 - - - com.lingniu.ingest - protocol-jsatl12 - - - - - com.lingniu.ingest - inbound-mqtt - - - com.lingniu.ingest - inbound-xinda-push - - - - - com.lingniu.ingest - command-gateway - - - - org.springframework.boot - spring-boot-starter - - - org.springdoc - springdoc-openapi-starter-webmvc-ui - - - - - org.springframework.boot - spring-boot-starter-test - test - - - - - lingniu-vehicle-ingest - - - org.springframework.boot - spring-boot-maven-plugin - - - --sun-misc-unsafe-memory-access=allow - - - - - repackage - - - - - - - diff --git a/modules/apps/bootstrap-all/src/main/java/com/lingniu/ingest/bootstrap/IngestApplication.java b/modules/apps/bootstrap-all/src/main/java/com/lingniu/ingest/bootstrap/IngestApplication.java deleted file mode 100644 index 5ffb76c0..00000000 --- a/modules/apps/bootstrap-all/src/main/java/com/lingniu/ingest/bootstrap/IngestApplication.java +++ /dev/null @@ -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 按需加载。 - * - *

启动失败兜底:某个 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); - } - } -} diff --git a/modules/apps/bootstrap-all/src/main/resources/application.yml b/modules/apps/bootstrap-all/src/main/resources/application.yml deleted file mode 100644 index eb6a8e76..00000000 --- a/modules/apps/bootstrap-all/src/main/resources/application.yml +++ /dev/null @@ -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 diff --git a/modules/apps/bootstrap-all/src/test/java/com/lingniu/ingest/bootstrap/BootstrapApplicationDefaultsTest.java b/modules/apps/bootstrap-all/src/test/java/com/lingniu/ingest/bootstrap/BootstrapApplicationDefaultsTest.java deleted file mode 100644 index 421d6db1..00000000 --- a/modules/apps/bootstrap-all/src/test/java/com/lingniu/ingest/bootstrap/BootstrapApplicationDefaultsTest.java +++ /dev/null @@ -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}"); - } -} diff --git a/modules/apps/bootstrap-all/src/test/java/com/lingniu/ingest/bootstrap/IngestEndToEndTest.java b/modules/apps/bootstrap-all/src/test/java/com/lingniu/ingest/bootstrap/IngestEndToEndTest.java deleted file mode 100644 index 7bbfaf95..00000000 --- a/modules/apps/bootstrap-all/src/test/java/com/lingniu/ingest/bootstrap/IngestEndToEndTest.java +++ /dev/null @@ -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; - -/** - * 端到端集成测试: - *

- *   test client → TCP → Gb32960 Netty server → frame decoder → dispatcher
- *   → InterceptorChain → Handler → DisruptorEventBus → TestEventSink
- * 
- * - *

关闭了所有非 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 sink:TestEventSink 成为唯一 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 queue = new LinkedBlockingQueue<>(); - - @Override public String name() { return "test"; } - - @Override - public CompletableFuture publish(VehicleEvent event) { - queue.add(event); - return CompletableFuture.completedFuture(null); - } - - @Override - public CompletableFuture publishBatch(List 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)); - } -} diff --git a/modules/apps/bootstrap-all/src/test/java/com/lingniu/ingest/bootstrap/ProtocolAutoConfigurationCompositionTest.java b/modules/apps/bootstrap-all/src/test/java/com/lingniu/ingest/bootstrap/ProtocolAutoConfigurationCompositionTest.java deleted file mode 100644 index f59644cc..00000000 --- a/modules/apps/bootstrap-all/src/test/java/com/lingniu/ingest/bootstrap/ProtocolAutoConfigurationCompositionTest.java +++ /dev/null @@ -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); - }); - } -} diff --git a/modules/apps/bootstrap-all/src/test/java/com/lingniu/ingest/bootstrap/RawArchiveEndToEndTest.java b/modules/apps/bootstrap-all/src/test/java/com/lingniu/ingest/bootstrap/RawArchiveEndToEndTest.java deleted file mode 100644 index 59f04ffe..00000000 --- a/modules/apps/bootstrap-all/src/test/java/com/lingniu/ingest/bootstrap/RawArchiveEndToEndTest.java +++ /dev/null @@ -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; - -/** - * 端到端验证原始报文冷存链路: - * - *

- *   TCP → Gb32960 Netty → Dispatcher 发 RawArchive 事件
- *       → Disruptor 虚拟线程 → ArchiveEventSink.publish
- *       → LocalArchiveStore.put → 文件系统落盘
- * 
- * - *

与 {@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(); - } - - // 预期落盘目录:/yyyy/MM/dd/GB32960// - // 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 walk = Files.walk(root)) { - List 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)); - } -} diff --git a/modules/apps/gb32960-ingest-app/pom.xml b/modules/apps/gb32960-ingest-app/pom.xml index d428c5fd..2409f6fd 100644 --- a/modules/apps/gb32960-ingest-app/pom.xml +++ b/modules/apps/gb32960-ingest-app/pom.xml @@ -37,6 +37,10 @@ org.springframework.boot spring-boot-starter-web + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + org.springframework.boot spring-boot-starter-test @@ -57,6 +61,7 @@ repackage + build-info diff --git a/modules/apps/gb32960-ingest-app/src/main/resources/application.yml b/modules/apps/gb32960-ingest-app/src/main/resources/application.yml index acef6c76..a30fc25c 100644 --- a/modules/apps/gb32960-ingest-app/src/main/resources/application.yml +++ b/modules/apps/gb32960-ingest-app/src/main/resources/application.yml @@ -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 diff --git a/modules/apps/gb32960-ingest-app/src/test/java/com/lingniu/ingest/gb32960app/Gb32960IngestAppDefaultsTest.java b/modules/apps/gb32960-ingest-app/src/test/java/com/lingniu/ingest/gb32960app/Gb32960IngestAppDefaultsTest.java index 52e96193..765d218a 100644 --- a/modules/apps/gb32960-ingest-app/src/test/java/com/lingniu/ingest/gb32960app/Gb32960IngestAppDefaultsTest.java +++ b/modules/apps/gb32960-ingest-app/src/test/java/com/lingniu/ingest/gb32960app/Gb32960IngestAppDefaultsTest.java @@ -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}") diff --git a/modules/apps/vehicle-analytics-app/pom.xml b/modules/apps/vehicle-analytics-app/pom.xml index 7aec22a3..d904e775 100644 --- a/modules/apps/vehicle-analytics-app/pom.xml +++ b/modules/apps/vehicle-analytics-app/pom.xml @@ -37,6 +37,10 @@ org.springframework.boot spring-boot-starter-web + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + org.springframework.boot spring-boot-starter-test @@ -57,6 +61,7 @@ repackage + build-info diff --git a/modules/apps/vehicle-analytics-app/src/main/resources/application.yml b/modules/apps/vehicle-analytics-app/src/main/resources/application.yml index 8581f3d1..9435221f 100644 --- a/modules/apps/vehicle-analytics-app/src/main/resources/application.yml +++ b/modules/apps/vehicle-analytics-app/src/main/resources/application.yml @@ -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 diff --git a/modules/apps/vehicle-analytics-app/src/test/java/com/lingniu/ingest/analyticsapp/VehicleAnalyticsAppDefaultsTest.java b/modules/apps/vehicle-analytics-app/src/test/java/com/lingniu/ingest/analyticsapp/VehicleAnalyticsAppDefaultsTest.java index 8cfd28d5..5319e64a 100644 --- a/modules/apps/vehicle-analytics-app/src/test/java/com/lingniu/ingest/analyticsapp/VehicleAnalyticsAppDefaultsTest.java +++ b/modules/apps/vehicle-analytics-app/src/test/java/com/lingniu/ingest/analyticsapp/VehicleAnalyticsAppDefaultsTest.java @@ -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) diff --git a/modules/apps/vehicle-history-app/pom.xml b/modules/apps/vehicle-history-app/pom.xml index f9dfeedb..c31f0509 100644 --- a/modules/apps/vehicle-history-app/pom.xml +++ b/modules/apps/vehicle-history-app/pom.xml @@ -45,6 +45,10 @@ org.springframework.boot spring-boot-starter-web + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + org.springframework.boot spring-boot-starter-test @@ -65,6 +69,7 @@ repackage + build-info diff --git a/modules/apps/vehicle-history-app/src/main/resources/application.yml b/modules/apps/vehicle-history-app/src/main/resources/application.yml index ff706add..2f8d38e5 100644 --- a/modules/apps/vehicle-history-app/src/main/resources/application.yml +++ b/modules/apps/vehicle-history-app/src/main/resources/application.yml @@ -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 diff --git a/modules/apps/vehicle-history-app/src/test/java/com/lingniu/ingest/historyapp/VehicleHistoryAppDefaultsTest.java b/modules/apps/vehicle-history-app/src/test/java/com/lingniu/ingest/historyapp/VehicleHistoryAppDefaultsTest.java index 47bb2238..463cc328 100644 --- a/modules/apps/vehicle-history-app/src/test/java/com/lingniu/ingest/historyapp/VehicleHistoryAppDefaultsTest.java +++ b/modules/apps/vehicle-history-app/src/test/java/com/lingniu/ingest/historyapp/VehicleHistoryAppDefaultsTest.java @@ -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}") diff --git a/pom.xml b/pom.xml index ab3285a8..6985812f 100644 --- a/pom.xml +++ b/pom.xml @@ -39,7 +39,6 @@ modules/apps/gb32960-ingest-app modules/apps/vehicle-history-app modules/apps/vehicle-analytics-app - modules/apps/bootstrap-all @@ -51,6 +50,7 @@ 25 3.5.3 + 2025.0.0.0 4.2.9.Final 4.0.0 4.28.3 @@ -93,6 +93,13 @@ pom import + + com.alibaba.cloud + spring-cloud-alibaba-dependencies + ${spring-cloud-alibaba.version} + pom + import + io.github.resilience4j resilience4j-bom diff --git a/woodpecker.yml b/woodpecker.yml new file mode 100644 index 00000000..34afba80 --- /dev/null +++ b/woodpecker.yml @@ -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 < /root/.docker/config.json < /root/.docker/config.json <