From da28a0c8d522bb9b745fd42e714d857329656ec4 Mon Sep 17 00:00:00 2001 From: kkfluous Date: Mon, 20 Apr 2026 15:27:52 +0800 Subject: [PATCH] fix(gb32960): emit platform login/logout events instead of swallowing them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gb32960RealtimeHandler.onPlatform 之前返回 List.of() 吞掉所有 0x05/0x06 事件, 而 Gb32960EventMapper.toEvents 里 PLATFORM_LOGIN / PLATFORM_LOGOUT 分支实际构造了 VehicleEvent.Login/Logout(vin="platform:",metadata.kind="platform")。 两边语义冲突导致平台登入事件永远不会上送下游——mapper 里那段是死代码。 修正: - onPlatform 改为委托 mapper.toEvents(msg),让平台会话事件真正发布到 vehicle.session topic,下游按 metadata.kind 区分车辆/平台会话 - 加 @EventEmit 声明 Login/Logout 事件类,保持元数据一致 - 去掉 @ProtocolHandler(version = "2017"):2017 不是真实的 GB/T 32960 版本(标准 仅 2016 / 2025),且 version 属性在 HandlerRegistry 路由里未使用,纯噪音 - JavaDoc 从"示例 Handler:演示..."改为生产路径描述,说明它是 GB32960 在 Dispatcher 管线里的唯一入口 新增 Gb32960RealtimeHandlerTest 2 case 覆盖平台登入/登出事件产出。 全模块 41 tests 全绿(原 39 + 新 2),无回归。 Co-Authored-By: Claude Opus 4.7 (1M context) --- .../handler/Gb32960RealtimeHandler.java | 23 +++--- .../handler/Gb32960RealtimeHandlerTest.java | 79 +++++++++++++++++++ 2 files changed, 93 insertions(+), 9 deletions(-) create mode 100644 protocol-gb32960/src/test/java/com/lingniu/ingest/protocol/gb32960/handler/Gb32960RealtimeHandlerTest.java diff --git a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/handler/Gb32960RealtimeHandler.java b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/handler/Gb32960RealtimeHandler.java index 2a870318..e622988d 100644 --- a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/handler/Gb32960RealtimeHandler.java +++ b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/handler/Gb32960RealtimeHandler.java @@ -7,18 +7,19 @@ import com.lingniu.ingest.api.annotation.ProtocolHandler; import com.lingniu.ingest.api.annotation.RateLimited; import com.lingniu.ingest.api.event.VehicleEvent; import com.lingniu.ingest.protocol.gb32960.mapper.Gb32960EventMapper; -import com.lingniu.ingest.protocol.gb32960.model.CommandType; import com.lingniu.ingest.protocol.gb32960.model.Gb32960Message; import java.util.List; /** - * 示例 Handler:演示注解驱动的消息路由与事件产出。 + * GB/T 32960 协议 Handler:Dispatcher 路由到此的所有 0x01~0x07 命令都委托给 + * {@link Gb32960EventMapper} 转成 {@link VehicleEvent}。 * - *

构造器注入 {@link Gb32960EventMapper},全部业务逻辑限制在纯函数里,不触碰数据库 / 不做 IO。 - * 产出的事件由 Dispatcher 统一投递到 Disruptor → Kafka。 + *

这是 GB32960 协议在通用 Dispatcher 管线里的**唯一入口** Bean——移除将导致所有 + * GB32960 帧产出的事件丢失。构造器注入 {@link Gb32960EventMapper},全部业务逻辑限制 + * 在纯函数里,不触碰数据库 / 不做 IO。产出的事件由 Dispatcher 统一投递到 Disruptor → Kafka。 */ -@ProtocolHandler(protocol = ProtocolId.GB32960, version = "2017") +@ProtocolHandler(protocol = ProtocolId.GB32960) public class Gb32960RealtimeHandler { private final Gb32960EventMapper mapper; @@ -42,11 +43,15 @@ public class Gb32960RealtimeHandler { return mapper.toEvents(msg); } - /** 平台登入 / 登出:仅用于 OEM 平台间握手,不产出业务事件。 */ + /** + * 平台登入 / 登出(0x05 / 0x06):外部下级平台握手,产出 + * {@link VehicleEvent.Login} / {@link VehicleEvent.Logout}, + * vin 字段形如 {@code platform:},metadata 带 {@code kind=platform}, + * 下游消费者可据此区分车辆会话与平台会话。 + */ @MessageMapping(command = {0x05, 0x06}, desc = "平台登入/登出") + @EventEmit({VehicleEvent.Login.class, VehicleEvent.Logout.class}) public List onPlatform(Gb32960Message msg) { - CommandType cmd = msg.header().command(); - // 留个 hook:后续可在此触发 session-core 的平台会话事件 - return List.of(); + return mapper.toEvents(msg); } } diff --git a/protocol-gb32960/src/test/java/com/lingniu/ingest/protocol/gb32960/handler/Gb32960RealtimeHandlerTest.java b/protocol-gb32960/src/test/java/com/lingniu/ingest/protocol/gb32960/handler/Gb32960RealtimeHandlerTest.java new file mode 100644 index 00000000..b46862d3 --- /dev/null +++ b/protocol-gb32960/src/test/java/com/lingniu/ingest/protocol/gb32960/handler/Gb32960RealtimeHandlerTest.java @@ -0,0 +1,79 @@ +package com.lingniu.ingest.protocol.gb32960.handler; + +import com.lingniu.ingest.api.event.VehicleEvent; +import com.lingniu.ingest.protocol.gb32960.mapper.Gb32960EventMapper; +import com.lingniu.ingest.protocol.gb32960.model.CommandBody; +import com.lingniu.ingest.protocol.gb32960.model.CommandType; +import com.lingniu.ingest.protocol.gb32960.model.EncryptType; +import com.lingniu.ingest.protocol.gb32960.model.Gb32960Header; +import com.lingniu.ingest.protocol.gb32960.model.Gb32960Message; +import com.lingniu.ingest.protocol.gb32960.model.ProtocolVersion; +import com.lingniu.ingest.protocol.gb32960.model.ResponseFlag; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * 保障 {@link Gb32960RealtimeHandler} 正确委托给 {@link Gb32960EventMapper}, + * 不吞掉任何需要上送下游的事件。 + */ +class Gb32960RealtimeHandlerTest { + + private final Gb32960RealtimeHandler handler = new Gb32960RealtimeHandler(new Gb32960EventMapper()); + + @Test + void onPlatform_login_emitsLoginEventWithKindPlatform() { + Gb32960Message msg = buildPlatformLoginMessage("lingniu"); + + List events = handler.onPlatform(msg); + + assertThat(events).hasSize(1); + VehicleEvent first = events.get(0); + assertThat(first).isInstanceOf(VehicleEvent.Login.class); + assertThat(first.vin()).isEqualTo("platform:lingniu"); + assertThat(first.metadata()) + .containsEntry("kind", "platform") + .containsEntry("username", "lingniu"); + } + + @Test + void onPlatform_logout_emitsLogoutEventWithKindPlatform() { + Gb32960Message msg = new Gb32960Message( + new Gb32960Header( + ProtocolVersion.V2016, + CommandType.PLATFORM_LOGOUT, + ResponseFlag.COMMAND, + "00000000000000000", + EncryptType.UNENCRYPTED, + 0, + null), + new CommandBody.PlatformLogout(Instant.parse("2026-04-20T10:00:00Z"), 1)); + + List events = handler.onPlatform(msg); + + assertThat(events).hasSize(1); + assertThat(events.get(0)).isInstanceOf(VehicleEvent.Logout.class); + assertThat(events.get(0).metadata()).containsEntry("kind", "platform"); + } + + private static Gb32960Message buildPlatformLoginMessage(String username) { + return new Gb32960Message( + new Gb32960Header( + ProtocolVersion.V2016, + CommandType.PLATFORM_LOGIN, + ResponseFlag.COMMAND, + "00000000000000000", + EncryptType.UNENCRYPTED, + 0, + null), + new CommandBody.PlatformLogin( + Instant.parse("2026-04-20T10:00:00Z"), + 1, + username, + "secret", + EncryptType.UNENCRYPTED)); + } +}