From 4e387e410cb097015b663c8ab1c108f030cba021 Mon Sep 17 00:00:00 2001 From: lzh Date: Wed, 21 Jan 2026 22:15:12 +0800 Subject: [PATCH] =?UTF-8?q?refactor(iot-gateway):=20JT808=E7=BC=96?= =?UTF-8?q?=E8=A7=A3=E7=A0=81=E5=99=A8=E4=BB=A3=E7=A0=81=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 提取 enrichParamsWithMetadata 方法统一处理元数据 - 提取 extractPhoneFromMap 和 extractFlowIdFromMap 方法 - 优化 getParamsAsMap 方法,同时支持 params 和 data 字段 - 优化代码注释格式 Co-Authored-By: Claude Opus 4.5 --- .../jt808/IotJt808DeviceMessageCodec.java | 251 ++++++++++++------ 1 file changed, 168 insertions(+), 83 deletions(-) diff --git a/viewsh-module-iot/viewsh-module-iot-gateway/src/main/java/com/viewsh/module/iot/gateway/codec/jt808/IotJt808DeviceMessageCodec.java b/viewsh-module-iot/viewsh-module-iot-gateway/src/main/java/com/viewsh/module/iot/gateway/codec/jt808/IotJt808DeviceMessageCodec.java index 6d6f7ba..5129ae3 100644 --- a/viewsh-module-iot/viewsh-module-iot-gateway/src/main/java/com/viewsh/module/iot/gateway/codec/jt808/IotJt808DeviceMessageCodec.java +++ b/viewsh-module-iot/viewsh-module-iot-gateway/src/main/java/com/viewsh/module/iot/gateway/codec/jt808/IotJt808DeviceMessageCodec.java @@ -11,7 +11,6 @@ import com.viewsh.module.iot.gateway.codec.jt808.util.Jt808ProtocolUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; -import java.time.LocalDateTime; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -115,36 +114,23 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec { int msgId = head.getId(); // 生成消息ID(使用流水号作为标识) - String messageId = head.getTerminalPhone() + "_" + head.getFlowId(); + String terminalPhone = head.getTerminalPhone(); + int flowId = head.getFlowId(); + String messageId = terminalPhone + "_" + flowId; // 根据消息ID确定物模型标准方法名 String method = getStandardMethodName(msgId); Object params = parseMessageParams(dataPack, msgId); - // 构建元数据(保留在 params 中,用于调试和追踪) - if (params instanceof Map) { - @SuppressWarnings("unchecked") - // 确保 paramsMap 是可变的,防止 parseMessageParams 返回不可变 Map 导致 put 报错 - Map paramsMap = new HashMap<>((Map) params); - Map 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 引用,确保使用的是包含 metadata 的可变 Map - params = paramsMap; - } + // 构建元数据并添加到 params 中(用于调试和追踪) + params = enrichParamsWithMetadata(params, msgId, terminalPhone, flowId, head.getEncryptionType()); - // 创建 IotDeviceMessage - IotDeviceMessage message = IotDeviceMessage.of(messageId, method, params, null, null, null); - message.setReportTime(LocalDateTime.now()); - return message; + return IotDeviceMessage.of(messageId, method, params, null, null, null); } /** * 根据 JT808 消息ID获取物模型标准方法名 - * + *

* 映射关系: * - 0x0002 心跳 -> jt808.terminal.heartbeat(JT808 专用,不映射到物模型) * - 0x0200 位置上报 -> thing.property.post(属性上报) @@ -214,18 +200,18 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec { /** * 解析按键事件为事件上报格式(thing.event.post) - * + *

* 物模型标准格式: * { - * "identifier": "button_event", - * "eventTime": 1234567890, - * "params": { - * "keyId": 1, - * "keyState": 1, - * "keyType": "short_press", - * "keyNumber": 1, - * "isLongPress": false - * } + * "identifier": "button_event", + * "eventTime": 1234567890, + * "params": { + * "keyId": 1, + * "keyState": 1, + * "keyType": "short_press", + * "keyNumber": 1, + * "isLongPress": false + * } * } */ private Map parseButtonEventAsEvent(Jt808DataPack dataPack) { @@ -233,20 +219,17 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec { Map result = new HashMap<>(); - // 统一使用一个事件标识符,通过 isLongPress 参数区分短按和长按 - // 注意:必须使用 "identifier" 字段,以便 IotDeviceMessageUtils.getIdentifier() 正确提取 + // 使用 identifier 字段(符合物模型标准格式),用于存储到数据库的 identifier 字段 result.put("identifier", "button_event"); // 事件时间戳 result.put("eventTime", System.currentTimeMillis()); - // 事件参数(包含 isLongPress 字段用于区分短按和长按) + // 事件参数(只保留 keyId 和 keyState 两个字段) + // 通过 keyId 可以判断是长按还是短按:keyId >= 0x0B 为长按 Map 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; @@ -254,13 +237,13 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec { /** * 解析位置信息为属性上报格式(thing.property.post) - * + *

* 物模型标准格式:params 直接就是属性键值对 * { - * "latitude": 31.123456, - * "longitude": 121.123456, - * "batteryLevel": 80, - * ... + * "latitude": 31.123456, + * "longitude": 121.123456, + * "batteryLevel": 80, + * ... * } */ private Map parseLocationInfoAsProperties(Jt808DataPack dataPack) { @@ -430,15 +413,15 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec { /** * 编码服务调用(thing.service.invoke) - * + *

* 根据服务标识符映射到不同的 JT808 指令 - * + *

* 消息格式: * { - * "identifier": "服务标识符", - * "params": { - * // 服务参数 - * } + * "identifier": "服务标识符", + * "params": { + * // 服务参数 + * } * } */ private byte[] encodeServiceInvoke(IotDeviceMessage message, String phone, int flowId) { @@ -483,15 +466,15 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec { /** * 编码属性设置(thing.property.set) - * + *

* 属性设置映射到 JT808 的参数设置指令 (0x8103) - * + *

* 消息格式: * { - * "properties": { - * "identifier1": value1, - * "identifier2": value2 - * } + * "properties": { + * "identifier1": value1, + * "identifier2": value2 + * } * } */ private byte[] encodePropertySet(IotDeviceMessage message, String phone, int flowId) { @@ -520,7 +503,7 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec { /** * 将物模型属性映射到 JT808 参数 - * + *

* 映射关系参考 JT808 协议标准: * - 0x0029: 心跳发送间隔 * - 0x0027: 位置汇报间隔 @@ -560,35 +543,61 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec { /** * 提取终端手机号 - * + *

* 优先级: * 1. 从 params._deviceName 中获取(下发场景,IotTcpDownstreamHandler 自动注入) * 2. 从 params._metadata.terminalPhone 中获取(上行消息回复场景) * 3. 从 params.phone 中获取(手动指定,向下兼容,不推荐) */ private String extractPhoneNumber(IotDeviceMessage message) { - if (!(message.getParams() instanceof Map)) { - log.error("[extractPhoneNumber][params 不是 Map 类型,消息: {}]", message); - throw new IllegalArgumentException("消息参数格式错误,params 必须是 Map 类型"); + // 尝试从 params 提取 + if (message.getParams() instanceof Map) { + Map params = (Map) message.getParams(); + String phone = extractPhoneFromMap(params); + if (phone != null) { + return phone; + } } - Map params = (Map) message.getParams(); + // 尝试从 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; + } // 1. 优先从 _deviceName 获取(下发场景,由 IotTcpDownstreamHandler 注入) - Object deviceName = params.get("_deviceName"); + Object deviceName = map.get("_deviceName"); if (deviceName != null && StrUtil.isNotBlank(deviceName.toString())) { String deviceNameStr = deviceName.toString().trim(); // 验证是否为数字(终端手机号应该是纯数字) if (deviceNameStr.matches("\\d+")) { return deviceNameStr; } else { - log.warn("[extractPhoneNumber][_deviceName 不是纯数字: {}]", deviceNameStr); + log.warn("[extractPhoneFromMap][_deviceName 不是纯数字: {}]", deviceNameStr); } } // 2. 从 metadata 中获取(上行消息回复场景) - if (params.get("_metadata") instanceof Map) { - Map metadata = (Map) params.get("_metadata"); + if (map.get("_metadata") instanceof Map) { + Map metadata = (Map) map.get("_metadata"); Object terminalPhone = metadata.get("terminalPhone"); if (terminalPhone != null && StrUtil.isNotBlank(terminalPhone.toString())) { return terminalPhone.toString(); @@ -596,7 +605,7 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec { } // 3. 从 phone 字段获取(向下兼容,不推荐) - Object phone = params.get("phone"); + Object phone = map.get("phone"); if (phone != null && StrUtil.isNotBlank(phone.toString())) { String phoneStr = phone.toString().trim(); if (phoneStr.matches("\\d+")) { @@ -604,34 +613,30 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec { } } - // 4. 如果都获取不到,抛出异常 - log.error("[extractPhoneNumber][无法提取终端手机号,params: {}]", params); - throw new IllegalArgumentException( - "消息中缺少终端手机号。请确保设备的 deviceName 为终端手机号(纯数字),例如: \"13800138000\""); + return null; } /** * 提取流水号 - * + *

* 对于下发消息,如果没有指定流水号,则生成一个随机流水号 */ private int extractFlowId(IotDeviceMessage message) { + // 尝试从 params 提取 if (message.getParams() instanceof Map) { Map params = (Map) message.getParams(); - - // 尝试获取显式指定的流水号 - Object flowId = params.get("flowId"); - if (flowId instanceof Number) { - return ((Number) flowId).intValue(); + Integer flowId = extractFlowIdFromMap(params); + if (flowId != null) { + return flowId; } + } - // 尝试从 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; } } @@ -639,19 +644,61 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec { 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 getParamsAsMap(IotDeviceMessage message) { + // 优先从 params 获取 if (message.getParams() instanceof Map) { return (Map) message.getParams(); } - // 尝试JSON转换 + + // 从 data 获取(应答消息使用 replyOf 时,参数在 data 字段) + if (message.getData() instanceof Map) { + return (Map) message.getData(); + } + + // 尝试JSON转换 params 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<>(); } @@ -682,4 +729,42 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec { && data[data.length - 1] == (byte) Jt808Constants.PKG_DELIMITER; } + /** + * 为参数对象添加元数据 + * + * @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 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 类型,则添加元数据 + if (params instanceof Map) { + @SuppressWarnings("unchecked") + Map paramsMap = (Map) params; + + // 创建新的可变 Map,确保可以添加元数据 + // 注意:某些解析方法可能返回不可变的 Map(如 Map.of()),需要转换为可变 Map + Map enrichedParams = new HashMap<>(paramsMap); + enrichedParams.put("_metadata", metadata); + return enrichedParams; + } + + // 如果 params 不是 Map,则包装为包含元数据的 Map + // 保留原始 params 在 _data 字段中 + Map wrapper = new HashMap<>(2); + wrapper.put("_data", params); + wrapper.put("_metadata", metadata); + return wrapper; + } } \ No newline at end of file