fix(iot): jt808消息应答机制调整-每条消息都需应答
Some checks failed
Java CI with Maven / build (11) (push) Has been cancelled
Java CI with Maven / build (17) (push) Has been cancelled
Java CI with Maven / build (8) (push) Has been cancelled

This commit is contained in:
lzh
2026-01-21 14:48:16 +08:00
parent 3d9079cbaf
commit ed127c3b29
2 changed files with 52 additions and 60 deletions

View File

@@ -137,7 +137,7 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec {
* 根据 JT808 消息ID获取物模型标准方法名
*
* 映射关系:
* - 0x0002 心跳 -> thing.state.update设备状态更新
* - 0x0002 心跳 -> jt808.terminal.heartbeatJT808 专用,不映射到物模型
* - 0x0200 位置上报 -> thing.property.post属性上报
* - 0x0704 批量位置上报 -> thing.property.post属性上报
* - 0x0006 按键事件 -> thing.event.post事件上报
@@ -147,8 +147,8 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec {
*/
private String getStandardMethodName(int msgId) {
return switch (msgId) {
// 设备状态类
case 0x0002 -> IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod(); // 心跳 -> 状态更新
// JT808 协议专用消息(不映射到物模型,避免被 REPLY_DISABLED 影响)
case 0x0002 -> "jt808.terminal.heartbeat"; // 心跳 -> JT808 专用方法
// 属性上报类
case 0x0200 -> IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(); // 位置信息汇报 -> 属性上报
@@ -179,8 +179,8 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec {
// thing.property.post - 返回 properties
case 0x0200, 0x0704 -> parseLocationInfoAsProperties(dataPack);
// thing.state.update - 返回 state 信
case 0x0002 -> parseHeartbeatAsState();
// jt808.terminal.heartbeat - 心跳消
case 0x0002 -> parseHeartbeat(dataPack);
// thing.event.post - 返回 event 信息
case 0x0006 -> parseButtonEventAsEvent(dataPack);
@@ -193,11 +193,12 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec {
}
/**
* 解析心跳为状态信息thing.state.update
* 解析心跳消息jt808.terminal.heartbeat
* <p>
* JT808 心跳消息体为空,仅用于保活,平台必须回复 0x8001 通用应答
*/
private Map<String, Object> parseHeartbeatAsState() {
private Map<String, Object> parseHeartbeat(Jt808DataPack dataPack) {
Map<String, Object> result = new HashMap<>();
result.put("state", "online");
result.put("timestamp", System.currentTimeMillis());
return result;
}

View File

@@ -1,6 +1,7 @@
package com.viewsh.module.iot.gateway.protocol.tcp.handler;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.viewsh.framework.common.pojo.CommonResult;
@@ -25,19 +26,18 @@ import java.util.concurrent.TimeUnit;
* JT808 协议处理器
* <p>
* 实现 JT808 部标协议的完整认证流程:
* 1. 终端注册0x0100设备首次连接时注册平台返回鉴权码
* 1. 终端注册0x0100设备首次连接时注册平台生成并返回随机鉴权码
* 2. 终端鉴权0x0102设备使用鉴权码进行鉴权鉴权成功后可发送业务消息
* <p>
* 认证策略(基于设备配置的 authType
* - SECRET一机一密使用设备的 deviceSecret 作为鉴权码
* - PRODUCT_SECRET一型一密使用产品的 productSecret 作为鉴权码
* - NONE免鉴权使用终端手机号作为鉴权码兼容模式
* 鉴权码机制
* - 注册时生成随机 token32位UUID大写形式
* - 鉴权码永久有效,缓存在 Redis 中
* - 鉴权成功后保留鉴权码,设备断线重连时可重复使用
* - 基于连接的信任:鉴权成功后,该 TCP 连接上的后续消息无需再鉴权
* <p>
* 设计说明:
* - 基于 demo 项目com/iot/transport/jt808的实现逻辑
* - 注册和鉴权分两步完成,符合 JT808 标准流程
* - 鉴权码在注册阶段生成并缓存,鉴权阶段验证后清除
* - 支持设备级和产品级认证类型配置(设备级优先)
* - 符合 JT808 标准流程:注册 → 获取鉴权码 → 鉴权 → 发送业务数据
* - 鉴权码是"进门密码",进门一次即可,断线重连时需重新验证
*
* @author lzh
*/
@@ -54,12 +54,18 @@ public class Jt808ProtocolHandler extends AbstractProtocolHandler {
private static final String METHOD_AUTH = "jt808.terminal.auth"; // 终端鉴权 (0x0102)
/**
* 鉴权码缓存过期时间(分钟
* 鉴权码缓存过期时间(
* <p>
* 设备在注册后需要在此时间内完成鉴权,否则需要重新注册
* Redis 会自动清理过期的鉴权码,无需手动管理。
* 设置较长的过期时间,支持设备断线重连时重复使用鉴权码
*/
private static final int AUTH_TOKEN_EXPIRE_DAYS = 30;
/**
* 鉴权码缓存 Key 前缀
* <p>
* 鉴权码在注册时生成并缓存到 Redis过期时间30天。
* 鉴权成功后保留鉴权码,设备断线重连时可重复使用。
*/
private static final int AUTH_TOKEN_EXPIRE_MINUTES = 5;
private final String JT808_AUTH_TOKEN = "iot:jt808_auth_token:%s";
/**
* Redis 模板(用于鉴权码缓存)
@@ -88,7 +94,7 @@ public class Jt808ProtocolHandler extends AbstractProtocolHandler {
@Override
public AuthResult handleAuthentication(String clientId, IotDeviceMessage message,
String codecType, NetSocket socket) {
String codecType, NetSocket socket) {
String method = message.getMethod();
// 路由到不同的认证处理方法
@@ -104,8 +110,8 @@ public class Jt808ProtocolHandler extends AbstractProtocolHandler {
@Override
public void handleBusinessMessage(String clientId, IotDeviceMessage message,
String codecType, NetSocket socket,
String productKey, String deviceName, String serverId) {
String codecType, NetSocket socket,
String productKey, String deviceName, String serverId) {
try {
// 1. 发送消息到消息总线
deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId);
@@ -117,7 +123,7 @@ public class Jt808ProtocolHandler extends AbstractProtocolHandler {
// 根据 JT808 协议规范,平台需要对终端上报的业务消息进行应答,包括:
// - 0x0200 位置信息汇报
// - 0x0002 终端心跳
// - 0x0704 批量位置上报
// - 0x0704 批量位置补报上报
// - 等其他业务消息
if (!IotDeviceMessageMethodEnum.isReplyDisabled(message.getMethod())) {
// 提取终端手机号、流水号和原始消息ID
@@ -145,7 +151,7 @@ public class Jt808ProtocolHandler extends AbstractProtocolHandler {
@Override
public void sendResponse(NetSocket socket, IotDeviceMessage originalMessage,
boolean success, String message, String codecType) {
boolean success, String message, String codecType) {
// JT808 协议的响应由具体的处理方法发送(注册应答、通用应答等)
// 此方法不需要实现,保留空实现
log.debug("[sendResponse][JT808 协议响应由具体方法发送,跳过通用响应]");
@@ -172,7 +178,7 @@ public class Jt808ProtocolHandler extends AbstractProtocolHandler {
* @return 认证结果PENDING 表示注册成功但需要继续鉴权)
*/
private AuthResult handleRegister(String clientId, IotDeviceMessage message,
String codecType, NetSocket socket) {
String codecType, NetSocket socket) {
try {
// 1. 提取终端手机号
String terminalPhone = extractTerminalPhone(message);
@@ -195,13 +201,13 @@ public class Jt808ProtocolHandler extends AbstractProtocolHandler {
// 3. 生成鉴权码(根据设备的认证类型)
String authToken = generateAuthToken(terminalPhone, device);
// 4. 缓存鉴权码到 Redis设置过期时间
// 4. 缓存鉴权码到 Redis30天过期
String redisKey = String.format(JT808_AUTH_TOKEN, terminalPhone);
stringRedisTemplate.opsForValue().set(redisKey, authToken,
AUTH_TOKEN_EXPIRE_MINUTES, TimeUnit.MINUTES);
AUTH_TOKEN_EXPIRE_DAYS, TimeUnit.DAYS);
log.debug("[handleRegister][鉴权码已缓存到 Redis终端手机号: {}, 过期时间: {} 分钟]",
terminalPhone, AUTH_TOKEN_EXPIRE_MINUTES);
log.debug("[handleRegister][鉴权码已缓存到 Redis终端手机号: {}, 过期时间: {} ]",
terminalPhone, AUTH_TOKEN_EXPIRE_DAYS);
// 5. 发送注册应答成功result_code=0
sendRegisterResp(socket, terminalPhone, flowId, (byte) 0, authToken, codecType, message.getRequestId());
@@ -244,7 +250,7 @@ public class Jt808ProtocolHandler extends AbstractProtocolHandler {
* @return 认证结果SUCCESS 表示鉴权成功,可以发送业务消息)
*/
private AuthResult handleAuth(String clientId, IotDeviceMessage message,
String codecType, NetSocket socket) {
String codecType, NetSocket socket) {
try {
// 1. 提取终端手机号和鉴权码
String terminalPhone = extractTerminalPhone(message);
@@ -292,8 +298,7 @@ public class Jt808ProtocolHandler extends AbstractProtocolHandler {
// 4. 发送通用应答成功result_code=0
sendCommonResp(socket, terminalPhone, flowId, 0x0102, (byte) 0, codecType, message.getRequestId());
// 5. 从 Redis 删除鉴权码
stringRedisTemplate.delete(redisKey);
// 注意:鉴权码保留在 Redis 中,设备断线重连时可重复使用
log.info("[handleAuth][JT808 鉴权成功,终端手机号: {}, 设备ID: {}]",
terminalPhone, device.getId());
@@ -320,41 +325,27 @@ public class Jt808ProtocolHandler extends AbstractProtocolHandler {
/**
* 生成鉴权码
* <p>
* 使用系统标准认证 API 生成鉴权码,保持与标准协议认证的一致性
* 鉴权码的生成策略由系统统一管理,支持:
* - SECRET一机一密使用设备的 deviceSecret
* - PRODUCT_SECRET一型一密使用产品的 productSecret
* - NONE免鉴权使用终端手机号
* <p>
* 注意:此方法基于 demo 的实现RegisterHandler:48demo 使用手机号作为鉴权码
* 生成随机 token 作为鉴权码,缓存在 Redis 中过期时间AUTH_TOKEN_EXPIRE_DAYS
* 终端使用此 token 进行鉴权,鉴权成功后清除缓存。
*
* @param terminalPhone 终端手机号
* @param device 设备信息
* @return 鉴权码
* @return 随机鉴权码32位十六进制字符串
*/
private String generateAuthToken(String terminalPhone, IotDeviceRespDTO device) {
try {
// 构建认证参数username 格式productKey.deviceName
// String username = IotDeviceAuthUtils.buildUsername(device.getProductKey(),
// device.getDeviceName());
// 生成 32 位随机十六进制字符串作为鉴权码
String authToken = IdUtil.simpleUUID().toUpperCase();
// 调用系统标准认证 API 获取认证信息
// 注意:这里只是为了获取密钥信息,不是真正的认证
// 实际的鉴权码应该是设备的密钥deviceSecret 或 productSecret
// 但由于当前 API 不返回密钥,我们使用终端手机号作为鉴权码(与 demo 保持一致)
log.debug("[generateAuthToken][生成鉴权码,终端手机号: {}, 设备: {}, 鉴权码: {}]",
terminalPhone, device.getDeviceName(), authToken);
log.debug("[generateAuthToken][生成鉴权码,终端手机号: {}, 设备: {}, 认证类型: {}]",
terminalPhone, device.getDeviceName(),
StrUtil.isNotBlank(device.getAuthType()) ? device.getAuthType() : device.getProductAuthType());
// 使用终端手机号作为鉴权码(与 demo 保持一致)
// TODO: 后续可以根据 authType 从系统获取真实的密钥
return terminalPhone;
return authToken;
} catch (Exception e) {
log.error("[generateAuthToken][生成鉴权码异常,终端手机号: {}]", terminalPhone, e);
// 异常时使用终端手机号兜底
return terminalPhone;
// 异常时使用时间戳兜底
return Long.toHexString(System.currentTimeMillis()).toUpperCase();
}
}
@@ -377,7 +368,7 @@ public class Jt808ProtocolHandler extends AbstractProtocolHandler {
* @param requestId 请求ID
*/
private void sendRegisterResp(NetSocket socket, String phone, int replyFlowId, byte replyCode,
String authToken, String codecType, String requestId) {
String authToken, String codecType, String requestId) {
try {
// 生成平台流水号
int flowId = (int) (System.currentTimeMillis() % 65535) + 1;
@@ -427,7 +418,7 @@ public class Jt808ProtocolHandler extends AbstractProtocolHandler {
* @param requestId 请求ID
*/
private void sendCommonResp(NetSocket socket, String phone, int replyFlowId, int replyId,
byte replyCode, String codecType, String requestId) {
byte replyCode, String codecType, String requestId) {
try {
// 生成平台流水号
int flowId = (int) (System.currentTimeMillis() % 65535) + 1;