fix(gb32960): emit platform login/logout events instead of swallowing them

Gb32960RealtimeHandler.onPlatform 之前返回 List.of() 吞掉所有 0x05/0x06 事件,
而 Gb32960EventMapper.toEvents 里 PLATFORM_LOGIN / PLATFORM_LOGOUT 分支实际构造了
VehicleEvent.Login/Logout(vin="platform:<username>",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) <noreply@anthropic.com>
This commit is contained in:
kkfluous
2026-04-20 15:27:52 +08:00
parent ce87c8ee26
commit da28a0c8d5
2 changed files with 93 additions and 9 deletions

View File

@@ -7,18 +7,19 @@ import com.lingniu.ingest.api.annotation.ProtocolHandler;
import com.lingniu.ingest.api.annotation.RateLimited; import com.lingniu.ingest.api.annotation.RateLimited;
import com.lingniu.ingest.api.event.VehicleEvent; import com.lingniu.ingest.api.event.VehicleEvent;
import com.lingniu.ingest.protocol.gb32960.mapper.Gb32960EventMapper; 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 com.lingniu.ingest.protocol.gb32960.model.Gb32960Message;
import java.util.List; import java.util.List;
/** /**
* 示例 Handler演示注解驱动的消息路由与事件产出。 * GB/T 32960 协议 HandlerDispatcher 路由到此的所有 0x01~0x07 命令都委托给
* {@link Gb32960EventMapper} 转成 {@link VehicleEvent}。
* *
* <p>构造器注入 {@link Gb32960EventMapper},全部业务逻辑限制在纯函数里,不触碰数据库 / 不做 IO。 * <p>这是 GB32960 协议在通用 Dispatcher 管线里的**唯一入口** Bean——移除将导致所有
* 产出的事件由 Dispatcher 统一投递到 Disruptor → Kafka。 * GB32960 帧产出的事件丢失。构造器注入 {@link Gb32960EventMapper},全部业务逻辑限制
* 在纯函数里,不触碰数据库 / 不做 IO。产出的事件由 Dispatcher 统一投递到 Disruptor → Kafka。
*/ */
@ProtocolHandler(protocol = ProtocolId.GB32960, version = "2017") @ProtocolHandler(protocol = ProtocolId.GB32960)
public class Gb32960RealtimeHandler { public class Gb32960RealtimeHandler {
private final Gb32960EventMapper mapper; private final Gb32960EventMapper mapper;
@@ -42,11 +43,15 @@ public class Gb32960RealtimeHandler {
return mapper.toEvents(msg); return mapper.toEvents(msg);
} }
/** 平台登入 / 登出:仅用于 OEM 平台间握手,不产出业务事件。 */ /**
* 平台登入 / 登出0x05 / 0x06外部下级平台握手产出
* {@link VehicleEvent.Login} / {@link VehicleEvent.Logout}
* vin 字段形如 {@code platform:<username>}metadata 带 {@code kind=platform}
* 下游消费者可据此区分车辆会话与平台会话。
*/
@MessageMapping(command = {0x05, 0x06}, desc = "平台登入/登出") @MessageMapping(command = {0x05, 0x06}, desc = "平台登入/登出")
@EventEmit({VehicleEvent.Login.class, VehicleEvent.Logout.class})
public List<VehicleEvent> onPlatform(Gb32960Message msg) { public List<VehicleEvent> onPlatform(Gb32960Message msg) {
CommandType cmd = msg.header().command(); return mapper.toEvents(msg);
// 留个 hook后续可在此触发 session-core 的平台会话事件
return List.of();
} }
} }

View File

@@ -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<VehicleEvent> 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<VehicleEvent> 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));
}
}