Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 016a1e18b7 | |||
| 5489125cfa | |||
| 2f4c836d1b | |||
| d1d7eec5c9 | |||
| 1886afc150 | |||
| 861a78c092 | |||
| 03a569c269 | |||
| 9d6b53c4f6 | |||
| 4066d65da2 |
@@ -23,8 +23,6 @@ import com.viewshanghai.module.iot.dal.tdengine.IotDeviceMessageMapper;
|
||||
import com.viewshanghai.module.iot.service.device.IotDeviceService;
|
||||
import com.viewshanghai.module.iot.service.device.property.IotDevicePropertyService;
|
||||
import com.viewshanghai.module.iot.service.ota.IotOtaTaskRecordService;
|
||||
import com.viewshanghai.module.iot.service.product.IotProductService;
|
||||
import com.viewshanghai.module.iot.dal.dataobject.product.IotProductDO;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.google.common.base.Objects;
|
||||
@@ -67,9 +65,6 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
|
||||
|
||||
@Resource
|
||||
private IotDeviceMessageProducer deviceMessageProducer;
|
||||
|
||||
@Resource
|
||||
private IotProductService productService;
|
||||
|
||||
@Override
|
||||
public void defineDeviceMessageStable() {
|
||||
@@ -177,21 +172,11 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
|
||||
// 条件1:防止对"回复消息"再次回复,避免无限循环
|
||||
// 条件2:某些特定的method不需要回复(如设备状态变更、OTA进度上报)
|
||||
// 条件3(新增):HTTP短连接场景,因为已经在请求中直接响应了,不需要再通过消息总线发送回复
|
||||
// 条件4(新增):JT808 协议由协议处理器自动生成通用应答(0x8001),业务层不需要生成回复消息
|
||||
if (IotDeviceMessageUtils.isReplyMessage(message)
|
||||
|| IotDeviceMessageMethodEnum.isReplyDisabled(message.getMethod())
|
||||
|| StrUtil.isEmpty(message.getServerId())) {
|
||||
return; // serverId 为空,不记录回复消息
|
||||
}
|
||||
|
||||
// 检查设备的编解码类型,如果是 JT808,则跳过回复消息生成
|
||||
// JT808 协议的回复由协议处理器(Jt808ProtocolHandler)自动生成通用应答
|
||||
IotProductDO product = productService.getProductFromCache(device.getProductId());
|
||||
if (product != null && "JT808".equals(product.getCodecType())) {
|
||||
log.debug("[handleUpstreamDeviceMessage][JT808 协议消息,跳过业务层回复消息生成,由协议处理器自动生成通用应答]");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
IotDeviceMessage replyMessage = IotDeviceMessage.replyOf(message.getRequestId(), message.getMethod(), replyData,
|
||||
serviceException != null ? serviceException.getCode() : null,
|
||||
|
||||
@@ -58,7 +58,7 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec {
|
||||
|
||||
// 根据方法名路由到不同的编码器
|
||||
return switch (method) {
|
||||
// === 标准物模型方法(下行) ===
|
||||
// === 标准物模型方法 ===
|
||||
case "thing.service.invoke" -> encodeServiceInvoke(message, phone, flowId); // 服务调用
|
||||
case "thing.property.set" -> encodePropertySet(message, phone, flowId); // 属性设置
|
||||
|
||||
@@ -67,18 +67,8 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec {
|
||||
case "jt808.platform.registerResp", "registerResp" -> encodeRegisterResp(message, phone, flowId);
|
||||
case "jt808.platform.textDown", "textDown" -> encodeTextDown(message, phone, flowId);
|
||||
|
||||
// === 上行消息方法(不应用于下行) ===
|
||||
// 注意:正常情况下不应该到达这里,因为:
|
||||
// 1. 业务层已跳过 JT808 协议的回复消息生成
|
||||
// 2. 协议处理器会自动生成通用应答
|
||||
// 这里保留作为防御性检查
|
||||
case "thing.property.post", "thing.property.report" -> {
|
||||
log.warn("[encode][JT808 协议不支持下行属性上报方法: {},请使用 thing.property.set 进行属性设置。如果这是回复消息,应使用 jt808.platform.commonResp 方法]", method);
|
||||
yield new byte[0];
|
||||
}
|
||||
|
||||
default -> {
|
||||
log.warn("[encode][不支持的消息方法: {},JT808 协议仅支持下行方法: thing.service.invoke, thing.property.set]", method);
|
||||
log.warn("[encode][不支持的消息方法: {}]", method);
|
||||
yield new byte[0];
|
||||
}
|
||||
};
|
||||
@@ -87,42 +77,20 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec {
|
||||
throw new RuntimeException("JT808 消息编码失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为上行消息方法
|
||||
*/
|
||||
private boolean isUpstreamMethod(String method) {
|
||||
return "thing.property.post".equals(method)
|
||||
|| "thing.property.report".equals(method)
|
||||
|| "thing.event.post".equals(method)
|
||||
|| "thing.state.update".equals(method);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IotDeviceMessage decode(byte[] bytes) {
|
||||
Assert.notNull(bytes, "待解码数据不能为空");
|
||||
Assert.isTrue(bytes.length >= 12, "数据包长度不足");
|
||||
|
||||
|
||||
try {
|
||||
// 1. 验证并去除首尾标识符 0x7e
|
||||
// JT808格式: 0x7e + 消息头 + 消息体 + 校验码 + 0x7e
|
||||
if (bytes[0] != (byte) 0x7e || bytes[bytes.length - 1] != (byte) 0x7e) {
|
||||
throw new IllegalArgumentException("JT808消息格式错误:缺少首尾标识符0x7e");
|
||||
}
|
||||
|
||||
// 2. 反转义处理(处理0x7d01->0x7d, 0x7d02->0x7e)
|
||||
// doEscape4Receive会保留start之前和end之后的字节,所以传入1和length-1来处理中间部分
|
||||
// 1. 反转义(去除首尾标识符)
|
||||
byte[] unescapedBytes = protocolUtil.doEscape4Receive(bytes, 1, bytes.length - 1);
|
||||
|
||||
// 3. 从反转义后的数据中提取实际内容(去掉首尾的0x7e)
|
||||
// doEscape4Receive保留了首尾的0x7e,需要再次去除
|
||||
byte[] actualData = new byte[unescapedBytes.length - 2];
|
||||
System.arraycopy(unescapedBytes, 1, actualData, 0, actualData.length);
|
||||
|
||||
// 4. 解析为 JT808 数据包(此时数据已经是纯净的消息内容,不含0x7e)
|
||||
Jt808DataPack dataPack = decoder.bytes2PackageData(actualData);
|
||||
|
||||
// 5. 转换为统一的 IotDeviceMessage
|
||||
|
||||
// 2. 解析为 JT808 数据包
|
||||
Jt808DataPack dataPack = decoder.bytes2PackageData(unescapedBytes);
|
||||
|
||||
// 3. 转换为统一的 IotDeviceMessage
|
||||
return convertToIotDeviceMessage(dataPack);
|
||||
} catch (Exception e) {
|
||||
log.error("[decode][JT808 消息解码失败,数据长度: {}]", bytes.length, e);
|
||||
@@ -132,74 +100,34 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec {
|
||||
|
||||
/**
|
||||
* 将 JT808 数据包转换为 IotDeviceMessage
|
||||
*
|
||||
* @param dataPack JT808 数据包
|
||||
* @return IotDeviceMessage 消息对象
|
||||
*/
|
||||
private IotDeviceMessage convertToIotDeviceMessage(Jt808DataPack dataPack) {
|
||||
Assert.notNull(dataPack, "JT808 数据包不能为空");
|
||||
|
||||
Jt808DataPack.PackHead head = dataPack.getPackHead();
|
||||
Assert.notNull(head, "JT808 数据包头部不能为空");
|
||||
|
||||
int msgId = head.getId();
|
||||
String terminalPhone = head.getTerminalPhone();
|
||||
int flowId = head.getFlowId();
|
||||
|
||||
// 生成请求ID(用于追踪和关联,格式:终端手机号_流水号_时间戳)
|
||||
// 添加时间戳确保唯一性,避免同一终端同一流水号重复
|
||||
String requestId = String.format("%s_%d_%d", terminalPhone, flowId, System.currentTimeMillis());
|
||||
// 生成消息ID(使用流水号作为标识)
|
||||
String messageId = head.getTerminalPhone() + "_" + head.getFlowId();
|
||||
|
||||
// 根据消息ID确定物模型标准方法名
|
||||
String method = getStandardMethodName(msgId);
|
||||
|
||||
// 解析消息参数
|
||||
Object params = parseMessageParams(dataPack, msgId);
|
||||
|
||||
// 构建元数据并添加到 params 中(用于调试和追踪)
|
||||
params = enrichParamsWithMetadata(params, msgId, terminalPhone, flowId, head.getEncryptionType());
|
||||
|
||||
// 创建 IotDeviceMessage(of 方法会自动生成 id 和 reportTime)
|
||||
return IotDeviceMessage.of(requestId, method, params, null, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为参数对象添加元数据
|
||||
*
|
||||
* @param params 原始参数对象
|
||||
* @param msgId JT808 消息ID
|
||||
* @param terminalPhone 终端手机号
|
||||
* @param flowId 流水号
|
||||
* @param encryptionType 加密类型
|
||||
* @return 包含元数据的参数对象
|
||||
*/
|
||||
private Object enrichParamsWithMetadata(Object params, int msgId, String terminalPhone,
|
||||
int flowId, int encryptionType) {
|
||||
// 构建元数据 Map
|
||||
Map<String, Object> metadata = new HashMap<>(4);
|
||||
metadata.put("jt808MsgId", String.format("0x%04X", msgId));
|
||||
metadata.put("terminalPhone", terminalPhone);
|
||||
metadata.put("flowId", flowId);
|
||||
metadata.put("encryptionType", encryptionType);
|
||||
|
||||
// 如果 params 是 Map 类型,则添加元数据
|
||||
// 构建元数据(保留在 params 中,用于调试和追踪)
|
||||
if (params instanceof Map) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> paramsMap = (Map<String, Object>) params;
|
||||
|
||||
// 创建新的可变 Map,确保可以添加元数据
|
||||
// 注意:某些解析方法可能返回不可变的 Map(如 Map.of()),需要转换为可变 Map
|
||||
Map<String, Object> enrichedParams = new HashMap<>(paramsMap);
|
||||
enrichedParams.put("_metadata", metadata);
|
||||
return enrichedParams;
|
||||
Map<String, Object> metadata = new HashMap<>();
|
||||
metadata.put("jt808MsgId", String.format("0x%04X", msgId));
|
||||
metadata.put("terminalPhone", head.getTerminalPhone());
|
||||
metadata.put("flowId", head.getFlowId());
|
||||
metadata.put("encryptionType", head.getEncryptionType());
|
||||
paramsMap.put("_metadata", metadata);
|
||||
}
|
||||
|
||||
// 如果 params 不是 Map,则包装为包含元数据的 Map
|
||||
// 保留原始 params 在 _data 字段中
|
||||
Map<String, Object> wrapper = new HashMap<>(2);
|
||||
wrapper.put("_data", params);
|
||||
wrapper.put("_metadata", metadata);
|
||||
return wrapper;
|
||||
// 创建 IotDeviceMessage
|
||||
IotDeviceMessage message = IotDeviceMessage.of(messageId, method, params, null, null, null);
|
||||
message.setReportTime(LocalDateTime.now());
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -273,41 +201,38 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec {
|
||||
|
||||
/**
|
||||
* 解析按键事件为事件上报格式(thing.event.post)
|
||||
* <p>
|
||||
* 根据 JT808 协议:
|
||||
* - 短按:keyId = 0x01-0x0A(对应1-10号键),keyState = 按键次数
|
||||
* - 长按:keyId = 0x0B-0x0E(对应长按1-4号键),keyState = 按键次数
|
||||
* <p>
|
||||
* 物模型标准格式(简化版,只保留 keyId 和 keyState):
|
||||
*
|
||||
* 物模型标准格式:
|
||||
* {
|
||||
* "identifier": "button_event", // 事件标识符,用于存储到数据库的 identifier 字段
|
||||
* "eventId": "button_event",
|
||||
* "eventTime": 1234567890,
|
||||
* "params": {
|
||||
* "keyId": 1, // 0x01=短按1号键, 0x0B=长按1号键
|
||||
* "keyState": 1 // 按键次数(0x01=按键一次)
|
||||
* "keyId": 1,
|
||||
* "keyState": 1,
|
||||
* "keyType": "short_press",
|
||||
* "keyNumber": 1,
|
||||
* "isLongPress": false
|
||||
* }
|
||||
* }
|
||||
* <p>
|
||||
* 判断长按/短按:通过 keyId 判断
|
||||
* - keyId < 0x0B:短按
|
||||
* - keyId >= 0x0B:长按
|
||||
*/
|
||||
private Map<String, Object> parseButtonEventAsEvent(Jt808DataPack dataPack) {
|
||||
Jt808ButtonEvent event = decoder.toButtonEventMsg(dataPack);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
// 使用 identifier 字段(符合物模型标准格式),用于存储到数据库的 identifier 字段
|
||||
result.put("identifier", "button_event");
|
||||
// 统一使用一个事件标识符,通过 isLongPress 参数区分短按和长按
|
||||
result.put("eventId", "button_event");
|
||||
|
||||
// 事件时间戳
|
||||
result.put("eventTime", System.currentTimeMillis());
|
||||
|
||||
// 事件参数(只保留 keyId 和 keyState 两个字段)
|
||||
// 通过 keyId 可以判断是长按还是短按:keyId >= 0x0B 为长按
|
||||
// 事件参数(包含 isLongPress 字段用于区分短按和长按)
|
||||
Map<String, Object> eventParams = new HashMap<>();
|
||||
eventParams.put("keyId", event.getKeyId());
|
||||
eventParams.put("keyState", event.getKeyState());
|
||||
eventParams.put("keyType", event.getKeyType());
|
||||
eventParams.put("keyNumber", event.getKeyNumber());
|
||||
eventParams.put("isLongPress", event.getIsLongPress());
|
||||
result.put("params", eventParams);
|
||||
|
||||
return result;
|
||||
@@ -621,163 +546,98 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec {
|
||||
|
||||
/**
|
||||
* 提取终端手机号
|
||||
*
|
||||
*
|
||||
* 优先级:
|
||||
* 1. 从 params._deviceName 中获取(下发场景,IotTcpDownstreamHandler 自动注入)
|
||||
* 2. 从 params._metadata.terminalPhone 中获取(上行消息回复场景)
|
||||
* 3. 从 params.phone 中获取(手动指定,向下兼容,不推荐)
|
||||
* 4. 从 data.phone 中获取(应答消息场景,使用 replyOf 构建的消息)
|
||||
*/
|
||||
private String extractPhoneNumber(IotDeviceMessage message) {
|
||||
// 尝试从 params 提取
|
||||
if (message.getParams() instanceof Map) {
|
||||
Map<?, ?> params = (Map<?, ?>) message.getParams();
|
||||
String phone = extractPhoneFromMap(params);
|
||||
if (phone != null) {
|
||||
return phone;
|
||||
}
|
||||
if (!(message.getParams() instanceof Map)) {
|
||||
log.error("[extractPhoneNumber][params 不是 Map 类型,消息: {}]", message);
|
||||
throw new IllegalArgumentException("消息参数格式错误,params 必须是 Map 类型");
|
||||
}
|
||||
|
||||
// 尝试从 data 提取(应答消息使用 replyOf 时,参数在 data 字段)
|
||||
if (message.getData() instanceof Map) {
|
||||
Map<?, ?> data = (Map<?, ?>) message.getData();
|
||||
String phone = extractPhoneFromMap(data);
|
||||
if (phone != null) {
|
||||
return phone;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果都获取不到,抛出异常
|
||||
log.error("[extractPhoneNumber][无法提取终端手机号,params: {}, data: {}]",
|
||||
message.getParams(), message.getData());
|
||||
throw new IllegalArgumentException(
|
||||
"消息中缺少终端手机号。请确保设备的 deviceName 为终端手机号(纯数字),例如: \"13800138000\"");
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Map 中提取终端手机号
|
||||
*/
|
||||
private String extractPhoneFromMap(Map<?, ?> map) {
|
||||
if (map == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
Map<?, ?> params = (Map<?, ?>) message.getParams();
|
||||
|
||||
// 1. 优先从 _deviceName 获取(下发场景,由 IotTcpDownstreamHandler 注入)
|
||||
Object deviceName = map.get("_deviceName");
|
||||
Object deviceName = params.get("_deviceName");
|
||||
if (deviceName != null && StrUtil.isNotBlank(deviceName.toString())) {
|
||||
String deviceNameStr = deviceName.toString().trim();
|
||||
// 验证是否为数字(终端手机号应该是纯数字)
|
||||
if (deviceNameStr.matches("\\d+")) {
|
||||
return deviceNameStr;
|
||||
} else {
|
||||
log.warn("[extractPhoneFromMap][_deviceName 不是纯数字: {}]", deviceNameStr);
|
||||
log.warn("[extractPhoneNumber][_deviceName 不是纯数字: {}]", deviceNameStr);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 2. 从 metadata 中获取(上行消息回复场景)
|
||||
if (map.get("_metadata") instanceof Map) {
|
||||
Map<?, ?> metadata = (Map<?, ?>) map.get("_metadata");
|
||||
if (params.get("_metadata") instanceof Map) {
|
||||
Map<?, ?> metadata = (Map<?, ?>) params.get("_metadata");
|
||||
Object terminalPhone = metadata.get("terminalPhone");
|
||||
if (terminalPhone != null && StrUtil.isNotBlank(terminalPhone.toString())) {
|
||||
return terminalPhone.toString();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 3. 从 phone 字段获取(向下兼容,不推荐)
|
||||
Object phone = map.get("phone");
|
||||
Object phone = params.get("phone");
|
||||
if (phone != null && StrUtil.isNotBlank(phone.toString())) {
|
||||
String phoneStr = phone.toString().trim();
|
||||
if (phoneStr.matches("\\d+")) {
|
||||
return phoneStr;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
// 4. 如果都获取不到,抛出异常
|
||||
log.error("[extractPhoneNumber][无法提取终端手机号,params: {}]", params);
|
||||
throw new IllegalArgumentException(
|
||||
"消息中缺少终端手机号。请确保设备的 deviceName 为终端手机号(纯数字),例如: \"13800138000\"");
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取流水号
|
||||
*
|
||||
*
|
||||
* 对于下发消息,如果没有指定流水号,则生成一个随机流水号
|
||||
*/
|
||||
private int extractFlowId(IotDeviceMessage message) {
|
||||
// 尝试从 params 提取
|
||||
if (message.getParams() instanceof Map) {
|
||||
Map<?, ?> params = (Map<?, ?>) message.getParams();
|
||||
Integer flowId = extractFlowIdFromMap(params);
|
||||
if (flowId != null) {
|
||||
return flowId;
|
||||
|
||||
// 尝试获取显式指定的流水号
|
||||
Object flowId = params.get("flowId");
|
||||
if (flowId instanceof Number) {
|
||||
return ((Number) flowId).intValue();
|
||||
}
|
||||
|
||||
// 尝试从 metadata 中获取(上行消息的流水号)
|
||||
if (params.get("_metadata") instanceof Map) {
|
||||
Map<?, ?> metadata = (Map<?, ?>) params.get("_metadata");
|
||||
Object metaFlowId = metadata.get("flowId");
|
||||
if (metaFlowId instanceof Number) {
|
||||
return ((Number) metaFlowId).intValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试从 data 提取(应答消息使用 replyOf 时,参数在 data 字段)
|
||||
if (message.getData() instanceof Map) {
|
||||
Map<?, ?> data = (Map<?, ?>) message.getData();
|
||||
Integer flowId = extractFlowIdFromMap(data);
|
||||
if (flowId != null) {
|
||||
return flowId;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 生成随机流水号(1-65535)
|
||||
return (int) (System.currentTimeMillis() % 65535) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Map 中提取流水号
|
||||
*/
|
||||
private Integer extractFlowIdFromMap(Map<?, ?> map) {
|
||||
if (map == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 尝试获取显式指定的流水号
|
||||
Object flowId = map.get("flowId");
|
||||
if (flowId instanceof Number) {
|
||||
return ((Number) flowId).intValue();
|
||||
}
|
||||
|
||||
// 尝试从 metadata 中获取(上行消息的流水号)
|
||||
if (map.get("_metadata") instanceof Map) {
|
||||
Map<?, ?> metadata = (Map<?, ?>) map.get("_metadata");
|
||||
Object metaFlowId = metadata.get("flowId");
|
||||
if (metaFlowId instanceof Number) {
|
||||
return ((Number) metaFlowId).intValue();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取参数为Map
|
||||
*
|
||||
* 优先从 params 获取,如果 params 为空则从 data 获取(应答消息场景)
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> getParamsAsMap(IotDeviceMessage message) {
|
||||
// 优先从 params 获取
|
||||
if (message.getParams() instanceof Map) {
|
||||
return (Map<String, Object>) message.getParams();
|
||||
}
|
||||
|
||||
// 从 data 获取(应答消息使用 replyOf 时,参数在 data 字段)
|
||||
if (message.getData() instanceof Map) {
|
||||
return (Map<String, Object>) message.getData();
|
||||
}
|
||||
|
||||
// 尝试JSON转换 params
|
||||
// 尝试JSON转换
|
||||
if (message.getParams() != null) {
|
||||
String json = JsonUtils.toJsonString(message.getParams());
|
||||
return JsonUtils.parseObject(json, Map.class);
|
||||
}
|
||||
|
||||
// 尝试JSON转换 data
|
||||
if (message.getData() != null) {
|
||||
String json = JsonUtils.toJsonString(message.getData());
|
||||
return JsonUtils.parseObject(json, Map.class);
|
||||
}
|
||||
|
||||
return new HashMap<>();
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import com.viewshanghai.framework.common.pojo.CommonResult;
|
||||
import com.viewshanghai.module.iot.core.biz.IotDeviceCommonApi;
|
||||
import com.viewshanghai.module.iot.core.biz.dto.IotDeviceGetReqDTO;
|
||||
import com.viewshanghai.module.iot.core.biz.dto.IotDeviceRespDTO;
|
||||
import com.viewshanghai.module.iot.core.enums.IotAuthTypeEnum;
|
||||
import com.viewshanghai.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import com.viewshanghai.module.iot.gateway.codec.jt808.IotJt808DeviceMessageCodec;
|
||||
import com.viewshanghai.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
@@ -113,108 +112,10 @@ public class Jt808ProtocolHandler extends AbstractProtocolHandler {
|
||||
log.info("[handleBusinessMessage][JT808 业务消息已发送,clientId: {}, method: {}, messageId: {}]",
|
||||
clientId, message.getMethod(), message.getId());
|
||||
|
||||
// JT808 协议要求:对每个上行消息都要回复通用应答(0x8001)
|
||||
// 从消息的 requestId 中提取流水号和消息ID(格式:terminalPhone_flowId_timestamp)
|
||||
// 从消息的 metadata 中提取终端手机号和原始消息ID
|
||||
String terminalPhone = extractTerminalPhone(message);
|
||||
int flowId = extractFlowId(message);
|
||||
int msgId = extractJt808MsgId(message);
|
||||
|
||||
if (terminalPhone != null && flowId > 0 && msgId > 0) {
|
||||
// 发送通用应答(成功)
|
||||
sendCommonResp(socket, terminalPhone, flowId, msgId, (byte) 0, codecType, message.getRequestId());
|
||||
} else {
|
||||
log.warn("[handleBusinessMessage][无法提取 JT808 消息信息,跳过通用应答,terminalPhone: {}, flowId: {}, msgId: {}]",
|
||||
terminalPhone, flowId, msgId);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[handleBusinessMessage][JT808 业务消息处理异常,clientId: {}]", clientId, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从消息中提取 JT808 流水号
|
||||
* <p>
|
||||
* 提取优先级:
|
||||
* 1. 从 metadata.flowId 中提取(最可靠)
|
||||
* 2. 从 requestId 中提取(格式:terminalPhone_flowId_timestamp)
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private int extractFlowId(IotDeviceMessage message) {
|
||||
// 优先从 metadata 中提取
|
||||
if (message.getParams() instanceof Map) {
|
||||
Map<?, ?> params = (Map<?, ?>) message.getParams();
|
||||
if (params.get("_metadata") instanceof Map) {
|
||||
Map<?, ?> metadata = (Map<?, ?>) params.get("_metadata");
|
||||
Object flowId = metadata.get("flowId");
|
||||
if (flowId instanceof Number) {
|
||||
return ((Number) flowId).intValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果 metadata 中没有,尝试从 requestId 中提取
|
||||
String requestId = message.getRequestId();
|
||||
if (requestId != null && requestId.contains("_")) {
|
||||
try {
|
||||
String[] parts = requestId.split("_");
|
||||
if (parts.length >= 2) {
|
||||
return Integer.parseInt(parts[1]);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("[extractFlowId][从 requestId 提取流水号失败: {}]", requestId);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从消息中提取 JT808 消息ID
|
||||
* 从 metadata 中获取原始 JT808 消息ID(格式:0x%04X,如 "0x0200")
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private int extractJt808MsgId(IotDeviceMessage message) {
|
||||
Object params = message.getParams();
|
||||
if (params instanceof Map) {
|
||||
Map<String, Object> paramsMap = (Map<String, Object>) params;
|
||||
// 尝试从 _metadata 中获取 jt808MsgId(格式:0x%04X)
|
||||
Object metadata = paramsMap.get("_metadata");
|
||||
if (metadata instanceof Map) {
|
||||
Map<String, Object> metadataMap = (Map<String, Object>) metadata;
|
||||
Object jt808MsgIdObj = metadataMap.get("jt808MsgId");
|
||||
if (jt808MsgIdObj instanceof String) {
|
||||
String jt808MsgIdStr = (String) jt808MsgIdObj;
|
||||
try {
|
||||
// 解析 "0x0200" 格式的字符串
|
||||
if (jt808MsgIdStr.startsWith("0x") || jt808MsgIdStr.startsWith("0X")) {
|
||||
return Integer.parseInt(jt808MsgIdStr.substring(2), 16);
|
||||
} else {
|
||||
return Integer.parseInt(jt808MsgIdStr, 16);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
log.debug("[extractJt808MsgId][解析 jt808MsgId 失败: {}]", jt808MsgIdStr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 如果无法从 metadata 中提取,尝试根据 method 映射到 JT808 消息ID
|
||||
String method = message.getMethod();
|
||||
return mapMethodToJt808MsgId(method);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将物模型方法名映射到 JT808 消息ID
|
||||
*/
|
||||
private int mapMethodToJt808MsgId(String method) {
|
||||
return switch (method) {
|
||||
case "thing.property.post", "thing.property.report" -> 0x0200; // 位置信息汇报
|
||||
case "thing.event.post" -> 0x0006; // 按键事件
|
||||
case "thing.state.update" -> 0x0002; // 心跳
|
||||
default -> 0x0001; // 默认使用通用应答
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendResponse(NetSocket socket, IotDeviceMessage originalMessage,
|
||||
@@ -344,30 +245,22 @@ public class Jt808ProtocolHandler extends AbstractProtocolHandler {
|
||||
return AuthResult.failure("设备不存在");
|
||||
}
|
||||
|
||||
// 3. 从 Redis 获取鉴权码(优先使用注册时缓存的鉴权码)
|
||||
// 3. 从 Redis 获取鉴权码
|
||||
String redisKey = String.format(JT808_AUTH_TOKEN, terminalPhone);
|
||||
String cachedAuthToken = stringRedisTemplate.opsForValue().get(redisKey);
|
||||
|
||||
boolean authSuccess = false;
|
||||
|
||||
if (StrUtil.isNotBlank(cachedAuthToken)) {
|
||||
// 3.1 如果找到缓存,验证鉴权码是否匹配
|
||||
if (authCode.equals(cachedAuthToken)) {
|
||||
authSuccess = true;
|
||||
log.debug("[handleAuth][使用缓存的鉴权码验证成功,终端手机号: {}]", terminalPhone);
|
||||
} else {
|
||||
log.warn("[handleAuth][缓存的鉴权码验证失败,终端手机号: {}, 期望: {}, 实际: {}]",
|
||||
terminalPhone, cachedAuthToken, authCode);
|
||||
}
|
||||
} else {
|
||||
// 3.2 如果未找到缓存,尝试直接验证(支持跳过注册步骤)
|
||||
log.info("[handleAuth][未找到鉴权码缓存,尝试直接验证,终端手机号: {}]", terminalPhone);
|
||||
authSuccess = validateAuthCodeDirectly(device, authCode, terminalPhone);
|
||||
if (StrUtil.isBlank(cachedAuthToken)) {
|
||||
log.warn("[handleAuth][未找到鉴权码缓存,终端手机号: {},可能未注册或缓存已过期]", terminalPhone);
|
||||
sendCommonResp(socket, terminalPhone, flowId, 0x0102, (byte) 1, codecType, message.getRequestId());
|
||||
return AuthResult.failure("未找到鉴权码,请先注册");
|
||||
}
|
||||
|
||||
if (!authSuccess) {
|
||||
// 验证鉴权码是否匹配(Redis 自动处理过期,无需手动检查)
|
||||
if (!authCode.equals(cachedAuthToken)) {
|
||||
log.warn("[handleAuth][鉴权码验证失败,终端手机号: {}, 期望: {}, 实际: {}]",
|
||||
terminalPhone, cachedAuthToken, authCode);
|
||||
sendCommonResp(socket, terminalPhone, flowId, 0x0102, (byte) 1, codecType, message.getRequestId());
|
||||
return AuthResult.failure("鉴权码验证失败");
|
||||
return AuthResult.failure("鉴权码错误");
|
||||
}
|
||||
|
||||
// 4. 发送通用应答(成功,result_code=0)
|
||||
@@ -396,58 +289,7 @@ public class Jt808ProtocolHandler extends AbstractProtocolHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 鉴权码生成和验证策略 ==========
|
||||
|
||||
/**
|
||||
* 直接验证鉴权码(跳过注册步骤)
|
||||
* <p>
|
||||
* 当设备未注册或缓存过期时,根据设备的认证类型直接验证鉴权码
|
||||
*
|
||||
* @param device 设备信息
|
||||
* @param authCode 设备发送的鉴权码
|
||||
* @param terminalPhone 终端手机号
|
||||
* @return true-验证成功,false-验证失败
|
||||
*/
|
||||
private boolean validateAuthCodeDirectly(IotDeviceRespDTO device, String authCode, String terminalPhone) {
|
||||
try {
|
||||
// 获取生效的认证类型(设备级优先,否则使用产品级)
|
||||
String effectiveAuthType = StrUtil.isNotBlank(device.getAuthType())
|
||||
? device.getAuthType()
|
||||
: device.getProductAuthType();
|
||||
|
||||
if (StrUtil.isBlank(effectiveAuthType)) {
|
||||
log.warn("[validateAuthCodeDirectly][认证类型为空,使用默认策略(终端手机号),终端手机号: {}]", terminalPhone);
|
||||
// 默认策略:使用终端手机号作为鉴权码
|
||||
return terminalPhone.equals(authCode);
|
||||
}
|
||||
|
||||
// 根据认证类型验证
|
||||
if (IotAuthTypeEnum.NONE.getType().equals(effectiveAuthType)) {
|
||||
// 免鉴权:直接通过(或使用终端手机号验证)
|
||||
log.debug("[validateAuthCodeDirectly][免鉴权模式,终端手机号: {}]", terminalPhone);
|
||||
return true;
|
||||
} else if (IotAuthTypeEnum.SECRET.getType().equals(effectiveAuthType)) {
|
||||
// 一机一密:使用设备的 deviceSecret
|
||||
// String deviceSecret = device.getDeviceSecret();
|
||||
return terminalPhone.equals(authCode);
|
||||
} else if (IotAuthTypeEnum.PRODUCT_SECRET.getType().equals(effectiveAuthType)) {
|
||||
// 一型一密:使用产品的 productSecret
|
||||
// 注意:设备信息中可能没有 productSecret,需要从产品获取
|
||||
// 这里暂时使用终端手机号作为回退策略
|
||||
log.warn("[validateAuthCodeDirectly][一型一密需要产品密钥,当前暂不支持直接验证,终端手机号: {},使用终端手机号作为回退", terminalPhone);
|
||||
return terminalPhone.equals(authCode);
|
||||
} else {
|
||||
// 其他类型:使用终端手机号作为回退策略
|
||||
log.warn("[validateAuthCodeDirectly][未知认证类型: {},使用终端手机号作为回退,终端手机号: {}]",
|
||||
effectiveAuthType, terminalPhone);
|
||||
return terminalPhone.equals(authCode);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[validateAuthCodeDirectly][直接验证鉴权码异常,终端手机号: {}]", terminalPhone, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// ========== 鉴权码生成策略 ==========
|
||||
|
||||
/**
|
||||
* 生成鉴权码
|
||||
@@ -620,6 +462,28 @@ public class Jt808ProtocolHandler extends AbstractProtocolHandler {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从消息中提取流水号
|
||||
* <p>
|
||||
* 流水号存储在消息的 _metadata.flowId 字段中
|
||||
*
|
||||
* @param message 设备消息
|
||||
* @return 流水号,提取失败返回 1
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private int extractFlowId(IotDeviceMessage message) {
|
||||
if (message.getParams() instanceof Map) {
|
||||
Map<?, ?> params = (Map<?, ?>) message.getParams();
|
||||
if (params.get("_metadata") instanceof Map) {
|
||||
Map<?, ?> metadata = (Map<?, ?>) params.get("_metadata");
|
||||
Object flowId = metadata.get("flowId");
|
||||
if (flowId instanceof Number) {
|
||||
return ((Number) flowId).intValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从消息中提取鉴权码
|
||||
|
||||
Reference in New Issue
Block a user