diff --git a/viewsh-module-iot/viewsh-module-iot-core/src/main/java/com/viewsh/module/iot/core/biz/dto/IotDeviceRespDTO.java b/viewsh-module-iot/viewsh-module-iot-core/src/main/java/com/viewsh/module/iot/core/biz/dto/IotDeviceRespDTO.java
index bac75eb..10e83ae 100644
--- a/viewsh-module-iot/viewsh-module-iot-core/src/main/java/com/viewsh/module/iot/core/biz/dto/IotDeviceRespDTO.java
+++ b/viewsh-module-iot/viewsh-module-iot-core/src/main/java/com/viewsh/module/iot/core/biz/dto/IotDeviceRespDTO.java
@@ -38,4 +38,16 @@ public class IotDeviceRespDTO {
*/
private String codecType;
+ /**
+ * 设备级认证类型
+ * 枚举 {@link com.viewsh.module.iot.core.enums.IotAuthTypeEnum}
+ */
+ private String authType;
+
+ /**
+ * 产品级认证类型 (兜底策略)
+ * 枚举 {@link com.viewsh.module.iot.core.enums.IotAuthTypeEnum}
+ */
+ private String productAuthType;
+
}
\ No newline at end of file
diff --git a/viewsh-module-iot/viewsh-module-iot-core/src/main/java/com/viewsh/module/iot/core/enums/IotAuthTypeEnum.java b/viewsh-module-iot/viewsh-module-iot-core/src/main/java/com/viewsh/module/iot/core/enums/IotAuthTypeEnum.java
new file mode 100644
index 0000000..10478a1
--- /dev/null
+++ b/viewsh-module-iot/viewsh-module-iot-core/src/main/java/com/viewsh/module/iot/core/enums/IotAuthTypeEnum.java
@@ -0,0 +1,58 @@
+package com.viewsh.module.iot.core.enums;
+
+import com.viewsh.framework.common.core.ArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * IoT 认证类型枚举
+ *
+ * 用于产品(Product)和设备(Device)的认证策略配置
+ *
+ * @author 芋道源码
+ */
+@AllArgsConstructor
+@Getter
+public enum IotAuthTypeEnum implements ArrayValuable {
+
+ /**
+ * 一机一密:每个设备有独立的 DeviceSecret
+ */
+ SECRET("SECRET", "一机一密"),
+
+ /**
+ * 一型一密:同产品下所有设备共用 ProductSecret
+ */
+ PRODUCT_SECRET("PRODUCT_SECRET", "一型一密"),
+
+ /**
+ * 动态注册:设备初次连接时自动创建,通常配合一型一密或免鉴权使用
+ * 注意:这通常是一个"能力开关"而非单纯的"认证方式",但在某些简单模型下可作为一种策略标识
+ * TODO: 暂未实现动态注册
+ */
+ DYNAMIC("DYNAMIC", "动态注册"),
+
+ /**
+ * 免鉴权:仅校验 ProductKey 和 DeviceName 存在
+ */
+ NONE("NONE", "免鉴权");
+
+ public static final String[] ARRAYS = Arrays.stream(values()).map(IotAuthTypeEnum::getType).toArray(String[]::new);
+
+ /**
+ * 类型代码
+ */
+ private final String type;
+ /**
+ * 名字
+ */
+ private final String name;
+
+ @Override
+ public String[] array() {
+ return ARRAYS;
+ }
+
+}
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
new file mode 100644
index 0000000..1c13d5b
--- /dev/null
+++ b/viewsh-module-iot/viewsh-module-iot-gateway/src/main/java/com/viewsh/module/iot/gateway/codec/jt808/IotJt808DeviceMessageCodec.java
@@ -0,0 +1,672 @@
+package com.viewsh.module.iot.gateway.codec.jt808;
+
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.util.StrUtil;
+import com.viewsh.framework.common.util.json.JsonUtils;
+import com.viewsh.module.iot.core.enums.IotDeviceMessageMethodEnum;
+import com.viewsh.module.iot.core.mq.message.IotDeviceMessage;
+import com.viewsh.module.iot.gateway.codec.IotDeviceMessageCodec;
+import com.viewsh.module.iot.gateway.codec.jt808.entity.*;
+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;
+import java.util.Map;
+
+/**
+ * JT808 协议 {@link IotDeviceMessage} 编解码器
+ *
+ * 将 JT808 标准协议转换为统一的 IotDeviceMessage 格式
+ *
+ * @author lzh
+ */
+@Slf4j
+@Component
+public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec {
+
+ public static final String TYPE = "JT808";
+
+ private final Jt808Decoder decoder;
+ private final Jt808Encoder encoder;
+ private final Jt808ProtocolUtil protocolUtil;
+
+ public IotJt808DeviceMessageCodec() {
+ this.decoder = new Jt808Decoder();
+ this.encoder = new Jt808Encoder();
+ this.protocolUtil = new Jt808ProtocolUtil();
+ }
+
+ @Override
+ public String type() {
+ return TYPE;
+ }
+
+ @Override
+ public byte[] encode(IotDeviceMessage message) {
+ Assert.notNull(message, "消息不能为空");
+ Assert.notBlank(message.getMethod(), "消息方法不能为空");
+
+ try {
+ // 从消息中提取必要信息
+ String phone = extractPhoneNumber(message);
+ int flowId = extractFlowId(message);
+ String method = message.getMethod();
+
+ // 根据方法名路由到不同的编码器
+ return switch (method) {
+ // === 标准物模型方法 ===
+ case "thing.service.invoke" -> encodeServiceInvoke(message, phone, flowId); // 服务调用
+ case "thing.property.set" -> encodePropertySet(message, phone, flowId); // 属性设置
+
+ // === JT808 内部协议方法(向下兼容) ===
+ case "jt808.platform.commonResp", "commonResp" -> encodeCommonResp(message, phone, flowId);
+ case "jt808.platform.registerResp", "registerResp" -> encodeRegisterResp(message, phone, flowId);
+ case "jt808.platform.textDown", "textDown" -> encodeTextDown(message, phone, flowId);
+
+ default -> {
+ log.warn("[encode][不支持的消息方法: {}]", method);
+ yield new byte[0];
+ }
+ };
+ } catch (Exception e) {
+ log.error("[encode][JT808 消息编码失败,消息: {}]", message, e);
+ throw new RuntimeException("JT808 消息编码失败: " + e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public IotDeviceMessage decode(byte[] bytes) {
+ Assert.notNull(bytes, "待解码数据不能为空");
+ Assert.isTrue(bytes.length >= 12, "数据包长度不足");
+
+ try {
+ // 1. 反转义(去除首尾标识符)
+ byte[] unescapedBytes = protocolUtil.doEscape4Receive(bytes, 1, bytes.length - 1);
+
+ // 2. 解析为 JT808 数据包
+ Jt808DataPack dataPack = decoder.bytes2PackageData(unescapedBytes);
+
+ // 3. 转换为统一的 IotDeviceMessage
+ return convertToIotDeviceMessage(dataPack);
+ } catch (Exception e) {
+ log.error("[decode][JT808 消息解码失败,数据长度: {}]", bytes.length, e);
+ throw new RuntimeException("JT808 消息解码失败: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * 将 JT808 数据包转换为 IotDeviceMessage
+ */
+ private IotDeviceMessage convertToIotDeviceMessage(Jt808DataPack dataPack) {
+ Jt808DataPack.PackHead head = dataPack.getPackHead();
+ int msgId = head.getId();
+
+ // 生成消息ID(使用流水号作为标识)
+ String messageId = head.getTerminalPhone() + "_" + head.getFlowId();
+
+ // 根据消息ID确定物模型标准方法名
+ String method = getStandardMethodName(msgId);
+ Object params = parseMessageParams(dataPack, msgId);
+
+ // 构建元数据(保留在 params 中,用于调试和追踪)
+ if (params instanceof Map) {
+ @SuppressWarnings("unchecked")
+ Map paramsMap = (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);
+ }
+
+ // 创建 IotDeviceMessage
+ IotDeviceMessage message = IotDeviceMessage.of(messageId, method, params, null, null, null);
+ message.setReportTime(LocalDateTime.now());
+ return message;
+ }
+
+ /**
+ * 根据 JT808 消息ID获取物模型标准方法名
+ *
+ * 映射关系:
+ * - 0x0002 心跳 -> thing.state.update(设备状态更新)
+ * - 0x0200 位置上报 -> thing.property.post(属性上报)
+ * - 0x0704 批量位置上报 -> thing.property.post(属性上报)
+ * - 0x0006 按键事件 -> thing.event.post(事件上报)
+ * - 0x0100 注册 -> 内部认证流程,不使用标准方法
+ * - 0x0102 鉴权 -> 内部认证流程,不使用标准方法
+ * - 0x0001 通用应答 -> 内部应答,不使用标准方法
+ */
+ private String getStandardMethodName(int msgId) {
+ return switch (msgId) {
+ // 设备状态类
+ case 0x0002 -> IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod(); // 心跳 -> 状态更新
+
+ // 属性上报类
+ case 0x0200 -> IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(); // 位置信息汇报 -> 属性上报
+ case 0x0704 -> IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(); // 批量位置上传 -> 属性上报
+
+ // 事件上报类
+ case 0x0006 -> IotDeviceMessageMethodEnum.EVENT_POST.getMethod(); // 按键事件 -> 事件上报
+
+ // 内部协议类(不映射到标准方法,使用 JT808 特定方法名)
+ case 0x0001 -> "jt808.terminal.commonResp"; // 终端通用应答
+ case 0x0100 -> "jt808.terminal.register"; // 终端注册
+ case 0x0102 -> "jt808.terminal.auth"; // 终端鉴权
+ case 0x0003 -> "jt808.terminal.logout"; // 终端注销
+
+ // 未知消息
+ default -> "jt808.unknown.0x" + Integer.toHexString(msgId);
+ };
+ }
+
+ /**
+ * 解析消息参数(根据物模型标准格式)
+ */
+ private Object parseMessageParams(Jt808DataPack dataPack, int msgId) {
+ byte[] bodyBytes = dataPack.getBodyBytes();
+
+ // 针对不同消息类型进行特殊解析,返回符合物模型标准的格式
+ return switch (msgId) {
+ // thing.property.post - 返回 properties
+ case 0x0200, 0x0704 -> parseLocationInfoAsProperties(dataPack);
+
+ // thing.state.update - 返回 state 信息
+ case 0x0002 -> parseHeartbeatAsState();
+
+ // thing.event.post - 返回 event 信息
+ case 0x0006 -> parseButtonEventAsEvent(dataPack);
+
+ // JT808 内部协议消息 - 保持原有格式
+ case 0x0100 -> parseRegisterInfo(dataPack);
+ case 0x0102 -> parseAuthInfo(bodyBytes);
+ default -> parseGenericParams(bodyBytes);
+ };
+ }
+
+ /**
+ * 解析心跳为状态信息(thing.state.update)
+ */
+ private Map parseHeartbeatAsState() {
+ Map result = new HashMap<>();
+ result.put("state", "online");
+ result.put("timestamp", System.currentTimeMillis());
+ return result;
+ }
+
+ /**
+ * 解析按键事件为事件上报格式(thing.event.post)
+ *
+ * 物模型标准格式:
+ * {
+ * "eventId": "button_event",
+ * "eventTime": 1234567890,
+ * "params": {
+ * "keyId": 1,
+ * "keyState": 1,
+ * "keyType": "short_press",
+ * "keyNumber": 1,
+ * "isLongPress": false
+ * }
+ * }
+ */
+ private Map parseButtonEventAsEvent(Jt808DataPack dataPack) {
+ Jt808ButtonEvent event = decoder.toButtonEventMsg(dataPack);
+
+ Map result = new HashMap<>();
+
+ // 统一使用一个事件标识符,通过 isLongPress 参数区分短按和长按
+ result.put("eventId", "button_event");
+
+ // 事件时间戳
+ result.put("eventTime", System.currentTimeMillis());
+
+ // 事件参数(包含 isLongPress 字段用于区分短按和长按)
+ 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;
+ }
+
+ /**
+ * 解析位置信息为属性上报格式(thing.property.post)
+ *
+ * 物模型标准格式:params 直接就是属性键值对
+ * {
+ * "latitude": 31.123456,
+ * "longitude": 121.123456,
+ * "batteryLevel": 80,
+ * ...
+ * }
+ */
+ private Map parseLocationInfoAsProperties(Jt808DataPack dataPack) {
+ Jt808LocationInfo locationInfo = decoder.toLocationInfoUploadMsg(dataPack);
+
+ // 物模型属性集合(所有属性平铺在同一层)
+ Map properties = new HashMap<>();
+
+ // === 基础位置信息(核心属性) ===
+ properties.put("latitude", locationInfo.getLatitude());
+ properties.put("longitude", locationInfo.getLongitude());
+ properties.put("elevation", locationInfo.getElevation());
+ properties.put("speed", locationInfo.getSpeed());
+ properties.put("direction", locationInfo.getDirection());
+ properties.put("time", locationInfo.getTime());
+
+ // 状态和告警字段(转为整数便于处理)
+ properties.put("warningFlag", locationInfo.getWarningFlagField());
+ properties.put("status", locationInfo.getStatusField());
+
+ // === 扩展信息(物模型属性) ===
+
+ // 电池信息
+ if (locationInfo.getBatteryInfo() != null) {
+ Jt808BatteryInfo batteryInfo = locationInfo.getBatteryInfo();
+ if (batteryInfo.getBatteryLevel() != null) {
+ properties.put("batteryLevel", batteryInfo.getBatteryLevel());
+ }
+ if (batteryInfo.getVersion() != null) {
+ properties.put("firmwareVersion", batteryInfo.getVersion());
+ }
+ if (batteryInfo.getIccid() != null) {
+ properties.put("iccid", batteryInfo.getIccid());
+ }
+ }
+
+ // 里程
+ if (locationInfo.getMileage() != null) {
+ properties.put("mileage", locationInfo.getMileage());
+ }
+
+ // 信号强度
+ if (locationInfo.getSignalStrength() != null) {
+ properties.put("signalStrength", locationInfo.getSignalStrength());
+ }
+
+ // 蓝牙设备列表
+ if (locationInfo.getBluetoothInfos() != null && !locationInfo.getBluetoothInfos().isEmpty()) {
+ List