Commit e275cb57e9519878557323924aef9c7a75c5f7f9

Authored by YunaiV
2 parents 3a26b2e3 eb9d27ae

Merge branch 'feature/iot-protocol' of https://gitee.com/zhijiantianya/ruoyi-vue…

…-pro into feature/iot-modbus

# Conflicts:
#	yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java
#	yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java
#	yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java
#	yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java
#	yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml
Showing 71 changed files with 2877 additions and 3161 deletions
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java
... ... @@ -75,7 +75,7 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi {
75 75 return success(BeanUtils.toBean(device, IotDeviceRespDTO.class, deviceDTO -> {
76 76 IotProductDO product = productService.getProductFromCache(deviceDTO.getProductId());
77 77 if (product != null) {
78   - deviceDTO.setCodecType(product.getCodecType());
  78 + deviceDTO.setProtocolType(product.getProtocolType()).setSerializeType(product.getSerializeType());
79 79 }
80 80 }));
81 81 }
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java
... ... @@ -67,10 +67,15 @@ public class IotProductRespVO {
67 67 @DictFormat(DictTypeConstants.NET_TYPE)
68 68 private Integer netType;
69 69  
70   - @Schema(description = "数据格式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
71   - @ExcelProperty(value = "数据格式", converter = DictConvert.class)
72   - @DictFormat(DictTypeConstants.CODEC_TYPE)
73   - private String codecType;
  70 + @Schema(description = "协议类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "mqtt")
  71 + @ExcelProperty(value = "协议类型", converter = DictConvert.class)
  72 + @DictFormat(DictTypeConstants.PROTOCOL_TYPE)
  73 + private String protocolType;
  74 +
  75 + @Schema(description = "序列化类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "json")
  76 + @ExcelProperty(value = "序列化类型", converter = DictConvert.class)
  77 + @DictFormat(DictTypeConstants.SERIALIZE_TYPE)
  78 + private String serializeType;
74 79  
75 80 @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
76 81 @ExcelProperty("创建时间")
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductSaveReqVO.java
1 1 package cn.iocoder.yudao.module.iot.controller.admin.product.vo.product;
2 2  
3 3 import cn.iocoder.yudao.framework.common.validation.InEnum;
  4 +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
  5 +import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum;
4 6 import cn.iocoder.yudao.module.iot.enums.product.IotNetTypeEnum;
5 7 import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum;
6 8 import io.swagger.v3.oas.annotations.media.Schema;
... ... @@ -44,9 +46,15 @@ public class IotProductSaveReqVO {
44 46 @InEnum(value = IotNetTypeEnum.class, message = "联网方式必须是 {value}")
45 47 private Integer netType;
46 48  
47   - @Schema(description = "数据格式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
48   - @NotEmpty(message = "数据格式不能为空")
49   - private String codecType;
  49 + @Schema(description = "协议类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "mqtt")
  50 + @InEnum(value = IotProtocolTypeEnum.class, message = "协议类型必须是 {value}")
  51 + @NotEmpty(message = "协议类型不能为空")
  52 + private String protocolType;
  53 +
  54 + @Schema(description = "序列化类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "json")
  55 + @InEnum(value = IotSerializeTypeEnum.class, message = "序列化类型必须是 {value}")
  56 + @NotEmpty(message = "序列化类型不能为空")
  57 + private String serializeType;
50 58  
51 59 @Schema(description = "是否开启动态注册", example = "false")
52 60 @NotNull(message = "是否开启动态注册不能为空")
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductDO.java
... ... @@ -78,12 +78,16 @@ public class IotProductDO extends TenantBaseDO {
78 78 */
79 79 private Integer netType;
80 80 /**
81   - * 数据格式(编解码器类型)
  81 + * 协议类型
82 82 * <p>
83   - * 字典 {@link cn.iocoder.yudao.module.iot.enums.DictTypeConstants#CODEC_TYPE}
84   - *
85   - * 目的:用于 gateway-server 解析消息格式
  83 + * 枚举 {@link cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum}
86 84 */
87   - private String codecType;
  85 + private String protocolType;
  86 + /**
  87 + * 序列化类型
  88 + * <p>
  89 + * 枚举 {@link cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum}
  90 + */
  91 + private String serializeType;
88 92  
89 93 }
90 94 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/DictTypeConstants.java
... ... @@ -8,8 +8,8 @@ package cn.iocoder.yudao.module.iot.enums;
8 8 public class DictTypeConstants {
9 9  
10 10 public static final String NET_TYPE = "iot_net_type";
11   - public static final String LOCATION_TYPE = "iot_location_type";
12   - public static final String CODEC_TYPE = "iot_codec_type";
  11 + public static final String PROTOCOL_TYPE = "iot_protocol_type";
  12 + public static final String SERIALIZE_TYPE = "iot_serialize_type";
13 13  
14 14 public static final String PRODUCT_STATUS = "iot_product_status";
15 15 public static final String PRODUCT_DEVICE_TYPE = "iot_product_device_type";
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java
... ... @@ -29,6 +29,7 @@ import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoChangeReqDTO;
29 29 import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO;
30 30 import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetRespDTO;
31 31 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  32 +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils;
32 33 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
33 34 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceGroupDO;
34 35 import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
... ... @@ -819,8 +820,9 @@ public class IotDeviceServiceImpl implements IotDeviceService {
819 820 if (BooleanUtil.isFalse(product.getRegisterEnabled())) {
820 821 throw exception(DEVICE_REGISTER_DISABLED);
821 822 }
822   - // 1.3 验证 productSecret
823   - if (ObjUtil.notEqual(product.getProductSecret(), reqDTO.getProductSecret())) {
  823 + // 1.3 【重要!!!】验证签名
  824 + if (!IotProductAuthUtils.verifySign(reqDTO.getProductKey(), reqDTO.getDeviceName(),
  825 + product.getProductSecret(), reqDTO.getSign())) {
824 826 throw exception(DEVICE_REGISTER_SECRET_INVALID);
825 827 }
826 828 return TenantUtils.execute(product.getTenantId(), () -> {
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotDeviceRespDTO.java
... ... @@ -34,8 +34,12 @@ public class IotDeviceRespDTO {
34 34 */
35 35 private Long productId;
36 36 /**
37   - * 编解码器类型
  37 + * 协议类型
38 38 */
39   - private String codecType;
  39 + private String protocolType;
  40 + /**
  41 + * 序列化类型
  42 + */
  43 + private String serializeType;
40 44  
41 45 }
42 46 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterReqDTO.java
... ... @@ -27,9 +27,11 @@ public class IotDeviceRegisterReqDTO {
27 27 private String deviceName;
28 28  
29 29 /**
30   - * 产品密钥
  30 + * 注册签名
  31 + *
  32 + * @see cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils#buildSign(String, String, String)
31 33 */
32   - @NotEmpty(message = "产品密钥不能为空")
33   - private String productSecret;
  34 + @NotEmpty(message = "签名不能为空")
  35 + private String sign;
34 36  
35 37 }
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotProductAuthUtils.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.util;
  2 +
  3 +import cn.hutool.core.util.StrUtil;
  4 +import cn.hutool.crypto.digest.DigestUtil;
  5 +import cn.hutool.crypto.digest.HmacAlgorithm;
  6 +
  7 +/**
  8 + * IoT 产品【动态注册】认证工具类
  9 + * <p>
  10 + * 用于一型一密场景,使用 productSecret 生成签名
  11 + *
  12 + * @author 芋道源码
  13 + */
  14 +public class IotProductAuthUtils {
  15 +
  16 + /**
  17 + * 生成设备动态注册签名
  18 + *
  19 + * @param productKey 产品标识
  20 + * @param deviceName 设备名称
  21 + * @param productSecret 产品密钥
  22 + * @return 签名
  23 + */
  24 + public static String buildSign(String productKey, String deviceName, String productSecret) {
  25 + String content = buildContent(productKey, deviceName);
  26 + return DigestUtil.hmac(HmacAlgorithm.HmacSHA256, StrUtil.utf8Bytes(productSecret))
  27 + .digestHex(content);
  28 + }
  29 +
  30 + /**
  31 + * 验证设备动态注册签名
  32 + *
  33 + * @param productKey 产品标识
  34 + * @param deviceName 设备名称
  35 + * @param productSecret 产品密钥
  36 + * @param sign 待验证的签名
  37 + * @return 是否验证通过
  38 + */
  39 + public static boolean verifySign(String productKey, String deviceName, String productSecret, String sign) {
  40 + String expectedSign = buildSign(productKey, deviceName, productSecret);
  41 + return expectedSign.equals(sign);
  42 + }
  43 +
  44 + /**
  45 + * 构建签名内容
  46 + *
  47 + * @param productKey 产品标识
  48 + * @param deviceName 设备名称
  49 + * @return 签名内容
  50 + */
  51 + private static String buildContent(String productKey, String deviceName) {
  52 + return "deviceName" + deviceName + "productKey" + productKey;
  53 + }
  54 +
  55 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/IotDeviceMessageCodec.java deleted
1   -package cn.iocoder.yudao.module.iot.gateway.codec;
2   -
3   -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
4   -
5   -/**
6   - * {@link cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage} 的编解码器
7   - *
8   - * @author 芋道源码
9   - */
10   -public interface IotDeviceMessageCodec {
11   -
12   - /**
13   - * 编码消息
14   - *
15   - * @param message 消息
16   - * @return 编码后的消息内容
17   - */
18   - byte[] encode(IotDeviceMessage message);
19   -
20   - /**
21   - * 解码消息
22   - *
23   - * @param bytes 消息内容
24   - * @return 解码后的消息内容
25   - */
26   - IotDeviceMessage decode(byte[] bytes);
27   -
28   - /**
29   - * @return 数据格式(编码器类型)
30   - */
31   - String type();
32   -
33   -}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/alink/IotAlinkDeviceMessageCodec.java deleted
1   -package cn.iocoder.yudao.module.iot.gateway.codec.alink;
2   -
3   -import cn.hutool.core.lang.Assert;
4   -import cn.iocoder.yudao.framework.common.pojo.CommonResult;
5   -import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
6   -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
7   -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
8   -import lombok.AllArgsConstructor;
9   -import lombok.Data;
10   -import lombok.NoArgsConstructor;
11   -import org.springframework.stereotype.Component;
12   -
13   -/**
14   - * 阿里云 Alink {@link IotDeviceMessage} 的编解码器
15   - *
16   - * @author 芋道源码
17   - */
18   -@Component
19   -public class IotAlinkDeviceMessageCodec implements IotDeviceMessageCodec {
20   -
21   - public static final String TYPE = "Alink";
22   -
23   - @Data
24   - @NoArgsConstructor
25   - @AllArgsConstructor
26   - private static class AlinkMessage {
27   -
28   - public static final String VERSION_1 = "1.0";
29   -
30   - /**
31   - * 消息 ID,且每个消息 ID 在当前设备具有唯一性
32   - */
33   - private String id;
34   -
35   - /**
36   - * 版本号
37   - */
38   - private String version;
39   -
40   - /**
41   - * 请求方法
42   - */
43   - private String method;
44   -
45   - /**
46   - * 请求参数
47   - */
48   - private Object params;
49   -
50   - /**
51   - * 响应结果
52   - */
53   - private Object data;
54   - /**
55   - * 响应错误码
56   - */
57   - private Integer code;
58   - /**
59   - * 响应提示
60   - *
61   - * 特殊:这里阿里云是 message,为了保持和项目的 {@link CommonResult#getMsg()} 一致。
62   - */
63   - private String msg;
64   -
65   - }
66   -
67   - @Override
68   - public String type() {
69   - return TYPE;
70   - }
71   -
72   - @Override
73   - public byte[] encode(IotDeviceMessage message) {
74   - AlinkMessage alinkMessage = new AlinkMessage(message.getRequestId(), AlinkMessage.VERSION_1,
75   - message.getMethod(), message.getParams(), message.getData(), message.getCode(), message.getMsg());
76   - return JsonUtils.toJsonByte(alinkMessage);
77   - }
78   -
79   - @Override
80   - @SuppressWarnings("DataFlowIssue")
81   - public IotDeviceMessage decode(byte[] bytes) {
82   - AlinkMessage alinkMessage = JsonUtils.parseObject(bytes, AlinkMessage.class);
83   - Assert.notNull(alinkMessage, "消息不能为空");
84   - Assert.equals(alinkMessage.getVersion(), AlinkMessage.VERSION_1, "消息版本号必须是 1.0");
85   - return IotDeviceMessage.of(alinkMessage.getId(), alinkMessage.getMethod(), alinkMessage.getParams(),
86   - alinkMessage.getData(), alinkMessage.getCode(), alinkMessage.getMsg());
87   - }
88   -
89   -}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/package-info.java deleted
1   -/**
2   - * 提供设备接入的各种数据(请求、响应)的编解码
3   - */
4   -package cn.iocoder.yudao.module.iot.gateway.codec;
5 0 \ No newline at end of file
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java deleted
1   -package cn.iocoder.yudao.module.iot.gateway.codec.tcp;
2   -
3   -import cn.hutool.core.lang.Assert;
4   -import cn.hutool.core.util.StrUtil;
5   -import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
6   -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
7   -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
8   -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
9   -import io.vertx.core.buffer.Buffer;
10   -import lombok.extern.slf4j.Slf4j;
11   -import org.springframework.stereotype.Component;
12   -
13   -import java.nio.charset.StandardCharsets;
14   -
15   -/**
16   - * TCP/UDP 二进制格式 {@link IotDeviceMessage} 编解码器
17   - * <p>
18   - * 二进制协议格式(所有数值使用大端序):
19   - *
20   - * <pre>
21   - * +--------+--------+--------+---------------------------+--------+--------+
22   - * | 魔术字 | 版本号 | 消息类型| 消息长度(4 字节) |
23   - * +--------+--------+--------+---------------------------+--------+--------+
24   - * | 消息 ID 长度(2 字节) | 消息 ID (变长字符串) |
25   - * +--------+--------+--------+--------+--------+--------+--------+--------+
26   - * | 方法名长度(2 字节) | 方法名(变长字符串) |
27   - * +--------+--------+--------+--------+--------+--------+--------+--------+
28   - * | 消息体数据(变长) |
29   - * +--------+--------+--------+--------+--------+--------+--------+--------+
30   - * </pre>
31   - * <p>
32   - * 消息体格式:
33   - * - 请求消息:params 数据(JSON)
34   - * - 响应消息:code (4字节) + msg 长度(2字节) + msg 字符串 + data 数据(JSON)
35   - * <p>
36   - * 注意:deviceId 不包含在协议中,由服务器根据连接上下文自动设置
37   - *
38   - * @author 芋道源码
39   - */
40   -@Slf4j
41   -@Component
42   -public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec {
43   -
44   - public static final String TYPE = "TcpBinary";
45   -
46   - /**
47   - * 协议魔术字,用于协议识别
48   - */
49   - private static final byte MAGIC_NUMBER = (byte) 0x7E;
50   -
51   - /**
52   - * 协议版本号
53   - */
54   - private static final byte PROTOCOL_VERSION = (byte) 0x01;
55   -
56   - /**
57   - * 请求消息类型
58   - */
59   - private static final byte REQUEST = (byte) 0x01;
60   -
61   - /**
62   - * 响应消息类型
63   - */
64   - private static final byte RESPONSE = (byte) 0x02;
65   -
66   - /**
67   - * 协议头部固定长度(魔术字 + 版本号 + 消息类型 + 消息长度)
68   - */
69   - private static final int HEADER_FIXED_LENGTH = 7;
70   -
71   - /**
72   - * 最小消息长度(头部 + 消息ID长度 + 方法名长度)
73   - */
74   - private static final int MIN_MESSAGE_LENGTH = HEADER_FIXED_LENGTH + 4;
75   -
76   - @Override
77   - public String type() {
78   - return TYPE;
79   - }
80   -
81   - @Override
82   - public byte[] encode(IotDeviceMessage message) {
83   - Assert.notNull(message, "消息不能为空");
84   - Assert.notBlank(message.getMethod(), "消息方法不能为空");
85   - try {
86   - // 1. 确定消息类型
87   - byte messageType = determineMessageType(message);
88   - // 2. 构建消息体
89   - byte[] bodyData = buildMessageBody(message, messageType);
90   - // 3. 构建完整消息
91   - return buildCompleteMessage(message, messageType, bodyData);
92   - } catch (Exception e) {
93   - log.error("[encode][TCP 二进制消息编码失败,消息: {}]", message, e);
94   - throw new RuntimeException("TCP 二进制消息编码失败: " + e.getMessage(), e);
95   - }
96   - }
97   -
98   - @Override
99   - public IotDeviceMessage decode(byte[] bytes) {
100   - Assert.notNull(bytes, "待解码数据不能为空");
101   - Assert.isTrue(bytes.length >= MIN_MESSAGE_LENGTH, "数据包长度不足");
102   - try {
103   - Buffer buffer = Buffer.buffer(bytes);
104   - // 解析协议头部和消息内容
105   - int index = 0;
106   - // 1. 验证魔术字
107   - byte magic = buffer.getByte(index++);
108   - Assert.isTrue(magic == MAGIC_NUMBER, "无效的协议魔术字: " + magic);
109   -
110   - // 2. 验证版本号
111   - byte version = buffer.getByte(index++);
112   - Assert.isTrue(version == PROTOCOL_VERSION, "不支持的协议版本: " + version);
113   -
114   - // 3. 读取消息类型
115   - byte messageType = buffer.getByte(index++);
116   - // 直接验证消息类型,无需抽取方法
117   - Assert.isTrue(messageType == REQUEST || messageType == RESPONSE,
118   - "无效的消息类型: " + messageType);
119   -
120   - // 4. 读取消息长度
121   - int messageLength = buffer.getInt(index);
122   - index += 4;
123   - Assert.isTrue(messageLength == buffer.length(),
124   - "消息长度不匹配,期望: " + messageLength + ", 实际: " + buffer.length());
125   -
126   - // 5. 读取消息 ID
127   - short messageIdLength = buffer.getShort(index);
128   - index += 2;
129   - String messageId = buffer.getString(index, index + messageIdLength, StandardCharsets.UTF_8.name());
130   - index += messageIdLength;
131   -
132   - // 6. 读取方法名
133   - short methodLength = buffer.getShort(index);
134   - index += 2;
135   - String method = buffer.getString(index, index + methodLength, StandardCharsets.UTF_8.name());
136   - index += methodLength;
137   -
138   - // 7. 解析消息体
139   - return parseMessageBody(buffer, index, messageType, messageId, method);
140   - } catch (Exception e) {
141   - log.error("[decode][TCP 二进制消息解码失败,数据长度: {}]", bytes.length, e);
142   - throw new RuntimeException("TCP 二进制消息解码失败: " + e.getMessage(), e);
143   - }
144   - }
145   -
146   - /**
147   - * 确定消息类型
148   - * 优化后的判断逻辑:有响应字段就是响应消息,否则就是请求消息
149   - */
150   - private byte determineMessageType(IotDeviceMessage message) {
151   - // 判断是否为响应消息:有响应码或响应消息时为响应
152   - if (message.getCode() != null) {
153   - return RESPONSE;
154   - }
155   - // 默认为请求消息
156   - return REQUEST;
157   - }
158   -
159   - /**
160   - * 构建消息体
161   - */
162   - private byte[] buildMessageBody(IotDeviceMessage message, byte messageType) {
163   - Buffer bodyBuffer = Buffer.buffer();
164   - if (messageType == RESPONSE) {
165   - // code
166   - bodyBuffer.appendInt(message.getCode() != null ? message.getCode() : 0);
167   - // msg
168   - String msg = message.getMsg() != null ? message.getMsg() : "";
169   - byte[] msgBytes = StrUtil.utf8Bytes(msg);
170   - bodyBuffer.appendShort((short) msgBytes.length);
171   - bodyBuffer.appendBytes(msgBytes);
172   - // data
173   - if (message.getData() != null) {
174   - bodyBuffer.appendBytes(JsonUtils.toJsonByte(message.getData()));
175   - }
176   - } else {
177   - // 请求消息只处理 params 参数
178   - // TODO @haohao:如果为空,是不是得写个长度 0 哈?
179   - if (message.getParams() != null) {
180   - bodyBuffer.appendBytes(JsonUtils.toJsonByte(message.getParams()));
181   - }
182   - }
183   - return bodyBuffer.getBytes();
184   - }
185   -
186   - /**
187   - * 构建完整消息
188   - */
189   - private byte[] buildCompleteMessage(IotDeviceMessage message, byte messageType, byte[] bodyData) {
190   - Buffer buffer = Buffer.buffer();
191   - // 1. 写入协议头部
192   - buffer.appendByte(MAGIC_NUMBER);
193   - buffer.appendByte(PROTOCOL_VERSION);
194   - buffer.appendByte(messageType);
195   - // 2. 预留消息长度位置(在 5. 更新消息长度)
196   - int lengthPosition = buffer.length();
197   - buffer.appendInt(0);
198   - // 3. 写入消息 ID
199   - String messageId = StrUtil.isNotBlank(message.getRequestId()) ? message.getRequestId()
200   - : IotDeviceMessageUtils.generateMessageId();
201   - byte[] messageIdBytes = StrUtil.utf8Bytes(messageId);
202   - buffer.appendShort((short) messageIdBytes.length);
203   - buffer.appendBytes(messageIdBytes);
204   - // 4. 写入方法名
205   - byte[] methodBytes = StrUtil.utf8Bytes(message.getMethod());
206   - buffer.appendShort((short) methodBytes.length);
207   - buffer.appendBytes(methodBytes);
208   - // 5. 写入消息体
209   - buffer.appendBytes(bodyData);
210   - // 6. 更新消息长度
211   - buffer.setInt(lengthPosition, buffer.length());
212   - return buffer.getBytes();
213   - }
214   -
215   - /**
216   - * 解析消息体
217   - */
218   - private IotDeviceMessage parseMessageBody(Buffer buffer, int startIndex, byte messageType,
219   - String messageId, String method) {
220   - if (startIndex >= buffer.length()) {
221   - // 空消息体
222   - return IotDeviceMessage.of(messageId, method, null, null, null, null);
223   - }
224   -
225   - if (messageType == RESPONSE) {
226   - // 响应消息:解析 code + msg + data
227   - return parseResponseMessage(buffer, startIndex, messageId, method);
228   - } else {
229   - // 请求消息:解析 payload
230   - Object payload = parseJsonData(buffer, startIndex, buffer.length());
231   - return IotDeviceMessage.of(messageId, method, payload, null, null, null);
232   - }
233   - }
234   -
235   - /**
236   - * 解析响应消息
237   - */
238   - private IotDeviceMessage parseResponseMessage(Buffer buffer, int startIndex, String messageId, String method) {
239   - int index = startIndex;
240   -
241   - // 1. 读取响应码
242   - Integer code = buffer.getInt(index);
243   - index += 4;
244   -
245   - // 2. 读取响应消息
246   - short msgLength = buffer.getShort(index);
247   - index += 2;
248   - String msg = msgLength > 0 ? buffer.getString(index, index + msgLength, StandardCharsets.UTF_8.name()) : null;
249   - index += msgLength;
250   -
251   - // 3. 读取响应数据
252   - Object data = null;
253   - if (index < buffer.length()) {
254   - data = parseJsonData(buffer, index, buffer.length());
255   - }
256   -
257   - return IotDeviceMessage.of(messageId, method, null, data, code, msg);
258   - }
259   -
260   - /**
261   - * 解析 JSON 数据
262   - */
263   - private Object parseJsonData(Buffer buffer, int startIndex, int endIndex) {
264   - if (startIndex >= endIndex) {
265   - return null;
266   - }
267   - try {
268   - String jsonStr = buffer.getString(startIndex, endIndex, StandardCharsets.UTF_8.name());
269   - return JsonUtils.parseObject(jsonStr, Object.class);
270   - } catch (Exception e) {
271   - log.warn("[parseJsonData][JSON 解析失败,返回原始字符串]", e);
272   - return buffer.getString(startIndex, endIndex, StandardCharsets.UTF_8.name());
273   - }
274   - }
275   -
276   - /**
277   - * 快速检测是否为二进制格式
278   - *
279   - * @param data 数据
280   - * @return 是否为二进制格式
281   - */
282   - public static boolean isBinaryFormatQuick(byte[] data) {
283   - return data != null && data.length >= 1 && data[0] == MAGIC_NUMBER;
284   - }
285   -
286   -}
287 0 \ No newline at end of file
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java deleted
1   -package cn.iocoder.yudao.module.iot.gateway.codec.tcp;
2   -
3   -import cn.hutool.core.lang.Assert;
4   -import cn.hutool.core.util.StrUtil;
5   -import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
6   -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
7   -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
8   -import lombok.AllArgsConstructor;
9   -import lombok.Data;
10   -import lombok.NoArgsConstructor;
11   -import org.springframework.stereotype.Component;
12   -
13   -/**
14   - * TCP/UDP JSON 格式 {@link IotDeviceMessage} 编解码器
15   - *
16   - * 采用纯 JSON 格式传输,格式如下:
17   - * {
18   - * "id": "消息 ID",
19   - * "method": "消息方法",
20   - * "params": {...}, // 请求参数
21   - * "data": {...}, // 响应结果
22   - * "code": 200, // 响应错误码
23   - * "msg": "success", // 响应提示
24   - * "timestamp": 时间戳
25   - * }
26   - *
27   - * @author 芋道源码
28   - */
29   -@Component
30   -public class IotTcpJsonDeviceMessageCodec implements IotDeviceMessageCodec {
31   -
32   - public static final String TYPE = "TcpJson";
33   -
34   - @Data
35   - @NoArgsConstructor
36   - @AllArgsConstructor
37   - private static class TcpJsonMessage {
38   -
39   - /**
40   - * 消息 ID,且每个消息 ID 在当前设备具有唯一性
41   - */
42   - private String id;
43   -
44   - /**
45   - * 请求方法
46   - */
47   - private String method;
48   -
49   - /**
50   - * 请求参数
51   - */
52   - private Object params;
53   -
54   - /**
55   - * 响应结果
56   - */
57   - private Object data;
58   -
59   - /**
60   - * 响应错误码
61   - */
62   - private Integer code;
63   -
64   - /**
65   - * 响应提示
66   - */
67   - private String msg;
68   -
69   - /**
70   - * 时间戳
71   - */
72   - private Long timestamp;
73   -
74   - }
75   -
76   - @Override
77   - public String type() {
78   - return TYPE;
79   - }
80   -
81   - @Override
82   - public byte[] encode(IotDeviceMessage message) {
83   - TcpJsonMessage tcpJsonMessage = new TcpJsonMessage(
84   - message.getRequestId(),
85   - message.getMethod(),
86   - message.getParams(),
87   - message.getData(),
88   - message.getCode(),
89   - message.getMsg(),
90   - System.currentTimeMillis());
91   - return JsonUtils.toJsonByte(tcpJsonMessage);
92   - }
93   -
94   - @Override
95   - @SuppressWarnings("DataFlowIssue")
96   - public IotDeviceMessage decode(byte[] bytes) {
97   - String jsonStr = StrUtil.utf8Str(bytes).trim();
98   - TcpJsonMessage tcpJsonMessage = JsonUtils.parseObject(jsonStr, TcpJsonMessage.class);
99   - Assert.notNull(tcpJsonMessage, "消息不能为空");
100   - Assert.notBlank(tcpJsonMessage.getMethod(), "消息方法不能为空");
101   - return IotDeviceMessage.of(
102   - tcpJsonMessage.getId(),
103   - tcpJsonMessage.getMethod(),
104   - tcpJsonMessage.getParams(),
105   - tcpJsonMessage.getData(),
106   - tcpJsonMessage.getCode(),
107   - tcpJsonMessage.getMsg());
108   - }
109   -
110   -}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java
... ... @@ -17,6 +17,15 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttDownstreamSubscr
17 17 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol;
18 18 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
19 19 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler;
  20 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.IotModbusTcpDownstreamSubscriber;
  21 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.IotModbusTcpUpstreamProtocol;
  22 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.client.IotModbusTcpClient;
  23 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.codec.IotModbusDataConverter;
  24 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpConfigCacheService;
  25 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpConnectionManager;
  26 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpPollScheduler;
  27 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.router.IotModbusTcpDownstreamHandler;
  28 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.router.IotModbusTcpUpstreamHandler;
20 29 import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager;
21 30 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
22 31 import io.vertx.core.Vertx;
... ... @@ -24,14 +33,18 @@ import lombok.extern.slf4j.Slf4j;
24 33 import org.redisson.api.RedissonClient;
25 34 import org.springframework.beans.factory.annotation.Qualifier;
26 35 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
  36 +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
  37 +import io.vertx.core.Vertx;
  38 +import lombok.extern.slf4j.Slf4j;
  39 +import org.redisson.api.RedissonClient;
  40 +import org.springframework.beans.factory.annotation.Qualifier;
  41 +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
27 42 import org.springframework.boot.context.properties.EnableConfigurationProperties;
28 43 import org.springframework.context.annotation.Bean;
29 44 import org.springframework.context.annotation.Configuration;
30 45  
31   -
32 46 @Configuration
33 47 @EnableConfigurationProperties(IotGatewayProperties.class)
34   -@Slf4j
35 48 public class IotGatewayConfiguration {
36 49  
37 50 @Bean
... ... @@ -45,75 +58,6 @@ public class IotGatewayConfiguration {
45 58 }
46 59  
47 60 /**
48   - * IoT 网关 EMQX 协议配置类
49   - */
50   - @Configuration
51   - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.emqx", name = "enabled", havingValue = "true")
52   - @Slf4j
53   - public static class EmqxProtocolConfiguration {
54   -
55   - @Bean(name = "emqxVertx", destroyMethod = "close")
56   - public Vertx emqxVertx() {
57   - return Vertx.vertx();
58   - }
59   -
60   - @Bean
61   - public IotEmqxAuthEventProtocol iotEmqxAuthEventProtocol(IotGatewayProperties gatewayProperties,
62   - @Qualifier("emqxVertx") Vertx emqxVertx) {
63   - return new IotEmqxAuthEventProtocol(gatewayProperties.getProtocol().getEmqx(), emqxVertx);
64   - }
65   -
66   - @Bean
67   - public IotEmqxUpstreamProtocol iotEmqxUpstreamProtocol(IotGatewayProperties gatewayProperties,
68   - @Qualifier("emqxVertx") Vertx emqxVertx) {
69   - return new IotEmqxUpstreamProtocol(gatewayProperties.getProtocol().getEmqx(), emqxVertx);
70   - }
71   -
72   - @Bean
73   - public IotEmqxDownstreamSubscriber iotEmqxDownstreamSubscriber(IotEmqxUpstreamProtocol mqttUpstreamProtocol,
74   - IotMessageBus messageBus) {
75   - return new IotEmqxDownstreamSubscriber(mqttUpstreamProtocol, messageBus);
76   - }
77   - }
78   -
79   - /**
80   - * IoT 网关 MQTT 协议配置类
81   - */
82   - @Configuration
83   - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.mqtt", name = "enabled", havingValue = "true")
84   - @Slf4j
85   - public static class MqttProtocolConfiguration {
86   -
87   - @Bean(name = "mqttVertx", destroyMethod = "close")
88   - public Vertx mqttVertx() {
89   - return Vertx.vertx();
90   - }
91   -
92   - @Bean
93   - public IotMqttUpstreamProtocol iotMqttUpstreamProtocol(IotGatewayProperties gatewayProperties,
94   - IotDeviceMessageService messageService,
95   - IotMqttConnectionManager connectionManager,
96   - @Qualifier("mqttVertx") Vertx mqttVertx) {
97   - return new IotMqttUpstreamProtocol(gatewayProperties.getProtocol().getMqtt(), messageService,
98   - connectionManager, mqttVertx);
99   - }
100   -
101   - @Bean
102   - public IotMqttDownstreamHandler iotMqttDownstreamHandler(IotDeviceMessageService messageService,
103   - IotMqttConnectionManager connectionManager) {
104   - return new IotMqttDownstreamHandler(messageService, connectionManager);
105   - }
106   -
107   - @Bean
108   - public IotMqttDownstreamSubscriber iotMqttDownstreamSubscriber(IotMqttUpstreamProtocol mqttUpstreamProtocol,
109   - IotMqttDownstreamHandler downstreamHandler,
110   - IotMessageBus messageBus) {
111   - return new IotMqttDownstreamSubscriber(mqttUpstreamProtocol, downstreamHandler, messageBus);
112   - }
113   -
114   - }
115   -
116   - /**
117 61 * IoT 网关 Modbus TCP 协议配置类
118 62 */
119 63 @Configuration
... ... @@ -188,8 +132,8 @@ public class IotGatewayConfiguration {
188 132  
189 133 @Bean
190 134 public IotModbusTcpDownstreamSubscriber iotModbusTcpDownstreamSubscriber(IotModbusTcpUpstreamProtocol upstreamProtocol,
191   - IotModbusTcpDownstreamHandler downstreamHandler,
192   - IotMessageBus messageBus) {
  135 + IotModbusTcpDownstreamHandler downstreamHandler,
  136 + IotMessageBus messageBus) {
193 137 return new IotModbusTcpDownstreamSubscriber(upstreamProtocol, downstreamHandler, messageBus);
194 138 }
195 139  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java
... ... @@ -2,12 +2,12 @@ package cn.iocoder.yudao.module.iot.gateway.config;
2 2  
3 3 import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
4 4 import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapConfig;
  5 +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxConfig;
5 6 import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpConfig;
  7 +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttConfig;
6 8 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig;
7 9 import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpConfig;
8 10 import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketConfig;
9   -import io.vertx.core.net.KeyCertOptions;
10   -import io.vertx.core.net.TrustOptions;
11 11 import jakarta.validation.Valid;
12 12 import jakarta.validation.constraints.NotEmpty;
13 13 import jakarta.validation.constraints.NotNull;
... ... @@ -33,14 +33,9 @@ public class IotGatewayProperties {
33 33 private TokenProperties token;
34 34  
35 35 /**
36   - * 协议配置(旧版,保持兼容)
37   - */
38   - private ProtocolProperties protocol;
39   -
40   - /**
41 36 * 协议实例列表
42 37 */
43   - private List<ProtocolInstanceProperties> protocols;
  38 + private List<ProtocolProperties> protocols;
44 39  
45 40 @Data
46 41 public static class RpcProperties {
... ... @@ -79,367 +74,155 @@ public class IotGatewayProperties {
79 74  
80 75 }
81 76  
  77 + /**
  78 + * 协议实例配置
  79 + */
82 80 @Data
83 81 public static class ProtocolProperties {
84 82  
85 83 /**
86   - * EMQX 组件配置
87   - */
88   - private EmqxProperties emqx;
89   -
90   - /**
91   - * MQTT 组件配置
  84 + * 协议实例 ID,如 "http-alink"、"tcp-binary"
92 85 */
93   - private MqttProperties mqtt;
94   -
  86 + @NotEmpty(message = "协议实例 ID 不能为空")
  87 + private String id;
95 88 /**
96   - * Modbus TCP 组件配置
  89 + * 是否启用
97 90 */
98   - private ModbusTcpProperties modbusTcp;
99   -
100   - }
101   -
102   - @Data
103   - public static class HttpProperties {
104   -
  91 + @NotNull(message = "是否启用不能为空")
  92 + private Boolean enabled = true;
105 93 /**
106   - * 是否开启
  94 + * 协议类型
  95 + *
  96 + * @see cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum
107 97 */
108   - @NotNull(message = "是否开启不能为空")
109   - private Boolean enabled;
  98 + @NotEmpty(message = "协议类型不能为空")
  99 + private String protocol;
110 100 /**
111 101 * 服务端口
112   - */
113   - private Integer serverPort;
114   -
115   - /**
116   - * 是否开启 SSL
117   - */
118   - @NotNull(message = "是否开启 SSL 不能为空")
119   - private Boolean sslEnabled = false;
120   -
121   - /**
122   - * SSL 证书路径
123   - */
124   - private String sslKeyPath;
125   - /**
126   - * SSL 证书路径
127   - */
128   - private String sslCertPath;
129   -
130   - }
131   -
132   - @Data
133   - public static class EmqxProperties {
134   -
135   - /**
136   - * 是否开启
137   - */
138   - @NotNull(message = "是否开启不能为空")
139   - private Boolean enabled;
140   -
141   - /**
142   - * HTTP 服务端口(默认:8090)
143   - */
144   - private Integer httpPort = 8090;
145   -
146   - /**
147   - * MQTT 服务器地址
148   - */
149   - @NotEmpty(message = "MQTT 服务器地址不能为空")
150   - private String mqttHost;
151   -
152   - /**
153   - * MQTT 服务器端口(默认:1883)
154   - */
155   - @NotNull(message = "MQTT 服务器端口不能为空")
156   - private Integer mqttPort = 1883;
157   -
158   - /**
159   - * MQTT 用户名
160   - */
161   - @NotEmpty(message = "MQTT 用户名不能为空")
162   - private String mqttUsername;
163   -
164   - /**
165   - * MQTT 密码
166   - */
167   - @NotEmpty(message = "MQTT 密码不能为空")
168   - private String mqttPassword;
169   -
170   - /**
171   - * MQTT 客户端的 SSL 开关
172   - */
173   - @NotNull(message = "MQTT 是否开启 SSL 不能为空")
174   - private Boolean mqttSsl = false;
175   -
176   - /**
177   - * MQTT 客户端 ID(如果为空,系统将自动生成)
178   - */
179   - @NotEmpty(message = "MQTT 客户端 ID 不能为空")
180   - private String mqttClientId;
181   -
182   - /**
183   - * MQTT 订阅的主题
184   - */
185   - @NotEmpty(message = "MQTT 主题不能为空")
186   - private List<@NotEmpty(message = "MQTT 主题不能为空") String> mqttTopics;
187   -
188   - /**
189   - * 默认 QoS 级别
190 102 * <p>
191   - * 0 - 最多一次
192   - * 1 - 至少一次
193   - * 2 - 刚好一次
  103 + * 不同协议含义不同:
  104 + * 1. TCP/UDP/HTTP/WebSocket/MQTT/CoAP:对应网关自身监听的服务端口
  105 + * 2. EMQX:对应网关提供给 EMQX 回调的 HTTP Hook 端口(/mqtt/auth、/mqtt/acl、/mqtt/event)
194 106 */
195   - private Integer mqttQos = 1;
196   -
  107 + @NotNull(message = "服务端口不能为空")
  108 + private Integer port;
197 109 /**
198   - * 连接超时时间(秒)
  110 + * 序列化类型(可选)
  111 + *
  112 + * @see cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum
  113 + *
  114 + * 为什么是可选的呢?
  115 + * 1. {@link IotProtocolTypeEnum#HTTP}、{@link IotProtocolTypeEnum#COAP} 协议,目前强制是 JSON 格式
  116 + * 2. {@link IotProtocolTypeEnum#EMQX} 协议,目前支持根据产品(设备)配置的序列化类型来解析
199 117 */
200   - private Integer connectTimeoutSeconds = 10;
  118 + private String serialize;
201 119  
202   - /**
203   - * 重连延迟时间(毫秒)
204   - */
205   - private Long reconnectDelayMs = 5000L;
  120 + // ========== SSL 配置 ==========
206 121  
207 122 /**
208   - * 是否启用 Clean Session (清理会话)
209   - * true: 每次连接都是新会话,Broker 不保留离线消息和订阅关系。
210   - * 对于网关这类“永远在线”且会主动重新订阅的应用,建议为 true。
  123 + * SSL 配置(可选,配置文件中不配置则为 null)
211 124 */
212   - private Boolean cleanSession = true;
  125 + @Valid
  126 + private SslConfig ssl;
  127 +
  128 + // ========== 各协议配置 ==========
213 129  
214 130 /**
215   - * 心跳间隔(秒)
216   - * 用于保持连接活性,及时发现网络中断。
  131 + * HTTP 协议配置
217 132 */
218   - private Integer keepAliveIntervalSeconds = 60;
219   -
  133 + @Valid
  134 + private IotHttpConfig http;
220 135 /**
221   - * 最大未确认消息队列大小
222   - * 限制已发送但未收到 Broker 确认的 QoS 1/2 消息数量,用于流量控制。
  136 + * WebSocket 协议配置
223 137 */
224   - private Integer maxInflightQueue = 10000;
  138 + @Valid
  139 + private IotWebSocketConfig websocket;
225 140  
226 141 /**
227   - * 是否信任所有 SSL 证书
228   - * 警告:此配置会绕过证书验证,仅建议在开发和测试环境中使用!
229   - * 在生产环境中,应设置为 false,并配置正确的信任库。
  142 + * TCP 协议配置
230 143 */
231   - private Boolean trustAll = false;
232   -
  144 + @Valid
  145 + private IotTcpConfig tcp;
233 146 /**
234   - * 遗嘱消息配置 (用于网关异常下线时通知其他系统)
  147 + * UDP 协议配置
235 148 */
236   - private final Will will = new Will();
  149 + @Valid
  150 + private IotUdpConfig udp;
237 151  
238 152 /**
239   - * 高级 SSL/TLS 配置 (用于生产环境)
  153 + * CoAP 协议配置
240 154 */
241   - private final Ssl sslOptions = new Ssl();
  155 + @Valid
  156 + private IotCoapConfig coap;
242 157  
243 158 /**
244   - * 遗嘱消息 (Last Will and Testament)
  159 + * MQTT 协议配置
245 160 */
246   - @Data
247   - public static class Will {
248   -
249   - /**
250   - * 是否启用遗嘱消息
251   - */
252   - private boolean enabled = false;
253   - /**
254   - * 遗嘱消息主题
255   - */
256   - private String topic;
257   - /**
258   - * 遗嘱消息内容
259   - */
260   - private String payload;
261   - /**
262   - * 遗嘱消息 QoS 等级
263   - */
264   - private Integer qos = 1;
265   - /**
266   - * 遗嘱消息是否作为保留消息发布
267   - */
268   - private boolean retain = true;
269   -
270   - }
271   -
  161 + @Valid
  162 + private IotMqttConfig mqtt;
272 163 /**
273   - * 高级 SSL/TLS 配置
  164 + * EMQX 协议配置
274 165 */
275   - @Data
276   - public static class Ssl {
277   -
278   - /**
279   - * 密钥库(KeyStore)路径,例如:classpath:certs/client.jks
280   - * 包含客户端自己的证书和私钥,用于向服务端证明身份(双向认证)。
281   - */
282   - private String keyStorePath;
283   - /**
284   - * 密钥库密码
285   - */
286   - private String keyStorePassword;
287   - /**
288   - * 信任库(TrustStore)路径,例如:classpath:certs/trust.jks
289   - * 包含服务端信任的 CA 证书,用于验证服务端的身份,防止中间人攻击。
290   - */
291   - private String trustStorePath;
292   - /**
293   - * 信任库密码
294   - */
295   - private String trustStorePassword;
296   -
297   - }
  166 + @Valid
  167 + private IotEmqxConfig emqx;
298 168  
299 169 }
300 170  
  171 + /**
  172 + * SSL 配置
  173 + */
301 174 @Data
302   - public static class MqttProperties {
  175 + public static class SslConfig {
303 176  
304 177 /**
305   - * 是否开启
  178 + * 是否启用 SSL
306 179 */
307   - @NotNull(message = "是否开启不能为空")
308   - private Boolean enabled;
  180 + @NotNull(message = "是否启用 SSL 不能为空")
  181 + private Boolean ssl = false;
309 182  
310 183 /**
311   - * 服务器端口
  184 + * SSL 证书路径
312 185 */
313   - private Integer port = 1883;
  186 + @NotEmpty(message = "SSL 证书路径不能为空")
  187 + private String sslCertPath;
314 188  
315 189 /**
316   - * 最大消息大小(字节)
  190 + * SSL 私钥路径
317 191 */
318   - private Integer maxMessageSize = 8192;
  192 + @NotEmpty(message = "SSL 私钥路径不能为空")
  193 + private String sslKeyPath;
319 194  
320 195 /**
321   - * 连接超时时间(秒)
  196 + * 密钥库(KeyStore)路径
  197 + * <p>
  198 + * 包含客户端自己的证书和私钥,用于向服务端证明身份(双向认证)
322 199 */
323   - private Integer connectTimeoutSeconds = 60;
  200 + private String keyStorePath;
324 201 /**
325   - * 保持连接超时时间(秒)
  202 + * 密钥库密码
326 203 */
327   - private Integer keepAliveTimeoutSeconds = 300;
  204 + private String keyStorePassword;
328 205  
329   - // NOTE:SSL 相关参数后续统一到 protocol 层级(优先级低)
330   - /**
331   - * 是否启用 SSL
332   - */
333   - private Boolean sslEnabled = false;
334 206 /**
335   - * SSL 配置
  207 + * 信任库(TrustStore)路径
  208 + * <p>
  209 + * 包含服务端信任的 CA 证书,用于验证服务端的身份
336 210 */
337   - private SslOptions sslOptions = new SslOptions();
338   -
  211 + private String trustStorePath;
339 212 /**
340   - * SSL 配置选项
  213 + * 信任库密码
341 214 */
342   - @Data
343   - public static class SslOptions {
344   -
345   - /**
346   - * 密钥证书选项
347   - */
348   - private KeyCertOptions keyCertOptions;
349   - /**
350   - * 信任选项
351   - */
352   - private TrustOptions trustOptions;
353   - /**
354   - * SSL 证书路径
355   - */
356   - private String certPath;
357   - /**
358   - * SSL 私钥路径
359   - */
360   - private String keyPath;
361   - /**
362   - * 信任存储路径
363   - */
364   - private String trustStorePath;
365   - /**
366   - * 信任存储密码
367   - */
368   - private String trustStorePassword;
369   -
370   - }
  215 + private String trustStorePassword;
371 216  
372 217 }
373 218  
374   - // NOTE:暂未统一为 ProtocolProperties,待协议改造完成再调整
375   - /**
376   - * 协议实例配置
377   - */
378 219 @Data
379   - public static class ProtocolInstanceProperties {
380   -
381   - /**
382   - * 协议实例 ID,如 "http-alink"、"tcp-binary"
383   - */
384   - @NotEmpty(message = "协议实例 ID 不能为空")
385   - private String id;
386   - /**
387   - * 是否启用
388   - */
389   - @NotNull(message = "是否启用不能为空")
390   - private Boolean enabled = true;
391   - /**
392   - * 协议类型
393   - *
394   - * @see cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum
395   - */
396   - @NotEmpty(message = "协议类型不能为空")
397   - private String type;
398   - /**
399   - * 服务端口
400   - */
401   - @NotNull(message = "服务端口不能为空")
402   - private Integer port;
403   - /**
404   - * 序列化类型(可选)
405   - *
406   - * @see cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum
407   - *
408   - * 为什么是可选的呢?
409   - * 1. {@link IotProtocolTypeEnum#HTTP}、${@link IotProtocolTypeEnum#COAP} 协议,目前强制是 JSON 格式
410   - * 2. {@link IotProtocolTypeEnum#EMQX} 协议,目前支持根据产品(设备)配置的序列化类型来解析
411   - */
412   - private String serialize;
413   -
414   - /**
415   - * HTTP 协议配置
416   - */
417   - @Valid
418   - private IotHttpConfig http;
419   -
420   - /**
421   - * TCP 协议配置
422   - */
423   - @Valid
424   - private IotTcpConfig tcp;
425   -
426   - /**
427   - * UDP 协议配置
428   - */
429   - @Valid
430   - private IotUdpConfig udp;
431   -
432   - /**
433   - * CoAP 协议配置
434   - */
435   - @Valid
436   - private IotCoapConfig coap;
  220 + public static class ProtocolProperties {
437 221  
438 222 /**
439   - * WebSocket 协议配置
  223 + * Modbus TCP 组件配置
440 224 */
441   - @Valid
442   - private IotWebSocketConfig websocket;
  225 + private ModbusTcpProperties modbusTcp;
443 226  
444 227 }
445 228  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java
... ... @@ -5,7 +5,9 @@ import cn.hutool.core.util.BooleanUtil;
5 5 import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
6 6 import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
7 7 import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol;
  8 +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol;
8 9 import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol;
  10 +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttProtocol;
9 11 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpProtocol;
10 12 import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpProtocol;
11 13 import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketProtocol;
... ... @@ -43,13 +45,13 @@ public class IotProtocolManager implements SmartLifecycle {
43 45 if (running) {
44 46 return;
45 47 }
46   - List<IotGatewayProperties.ProtocolInstanceProperties> protocolConfigs = gatewayProperties.getProtocols();
  48 + List<IotGatewayProperties.ProtocolProperties> protocolConfigs = gatewayProperties.getProtocols();
47 49 if (CollUtil.isEmpty(protocolConfigs)) {
48 50 log.info("[start][没有配置协议实例,跳过启动]");
49 51 return;
50 52 }
51 53  
52   - for (IotGatewayProperties.ProtocolInstanceProperties config : protocolConfigs) {
  54 + for (IotGatewayProperties.ProtocolProperties config : protocolConfigs) {
53 55 if (BooleanUtil.isFalse(config.getEnabled())) {
54 56 log.info("[start][协议实例 {} 未启用,跳过]", config.getId());
55 57 continue;
... ... @@ -89,10 +91,10 @@ public class IotProtocolManager implements SmartLifecycle {
89 91 * @return 协议实例
90 92 */
91 93 @SuppressWarnings({"EnhancedSwitchMigration"})
92   - private IotProtocol createProtocol(IotGatewayProperties.ProtocolInstanceProperties config) {
93   - IotProtocolTypeEnum protocolType = IotProtocolTypeEnum.of(config.getType());
  94 + private IotProtocol createProtocol(IotGatewayProperties.ProtocolProperties config) {
  95 + IotProtocolTypeEnum protocolType = IotProtocolTypeEnum.of(config.getProtocol());
94 96 if (protocolType == null) {
95   - log.error("[createProtocol][协议实例 {} 的协议类型 {} 不存在]", config.getId(), config.getType());
  97 + log.error("[createProtocol][协议实例 {} 的协议类型 {} 不存在]", config.getId(), config.getProtocol());
96 98 return null;
97 99 }
98 100 switch (protocolType) {
... ... @@ -106,6 +108,10 @@ public class IotProtocolManager implements SmartLifecycle {
106 108 return createCoapProtocol(config);
107 109 case WEBSOCKET:
108 110 return createWebSocketProtocol(config);
  111 + case MQTT:
  112 + return createMqttProtocol(config);
  113 + case EMQX:
  114 + return createEmqxProtocol(config);
109 115 default:
110 116 throw new IllegalArgumentException(String.format(
111 117 "[createProtocol][协议实例 %s 的协议类型 %s 暂不支持]", config.getId(), protocolType));
... ... @@ -118,7 +124,7 @@ public class IotProtocolManager implements SmartLifecycle {
118 124 * @param config 协议实例配置
119 125 * @return HTTP 协议实例
120 126 */
121   - private IotHttpProtocol createHttpProtocol(IotGatewayProperties.ProtocolInstanceProperties config) {
  127 + private IotHttpProtocol createHttpProtocol(IotGatewayProperties.ProtocolProperties config) {
122 128 return new IotHttpProtocol(config);
123 129 }
124 130  
... ... @@ -128,7 +134,7 @@ public class IotProtocolManager implements SmartLifecycle {
128 134 * @param config 协议实例配置
129 135 * @return TCP 协议实例
130 136 */
131   - private IotTcpProtocol createTcpProtocol(IotGatewayProperties.ProtocolInstanceProperties config) {
  137 + private IotTcpProtocol createTcpProtocol(IotGatewayProperties.ProtocolProperties config) {
132 138 return new IotTcpProtocol(config);
133 139 }
134 140  
... ... @@ -138,7 +144,7 @@ public class IotProtocolManager implements SmartLifecycle {
138 144 * @param config 协议实例配置
139 145 * @return UDP 协议实例
140 146 */
141   - private IotUdpProtocol createUdpProtocol(IotGatewayProperties.ProtocolInstanceProperties config) {
  147 + private IotUdpProtocol createUdpProtocol(IotGatewayProperties.ProtocolProperties config) {
142 148 return new IotUdpProtocol(config);
143 149 }
144 150  
... ... @@ -148,7 +154,7 @@ public class IotProtocolManager implements SmartLifecycle {
148 154 * @param config 协议实例配置
149 155 * @return CoAP 协议实例
150 156 */
151   - private IotCoapProtocol createCoapProtocol(IotGatewayProperties.ProtocolInstanceProperties config) {
  157 + private IotCoapProtocol createCoapProtocol(IotGatewayProperties.ProtocolProperties config) {
152 158 return new IotCoapProtocol(config);
153 159 }
154 160  
... ... @@ -158,8 +164,28 @@ public class IotProtocolManager implements SmartLifecycle {
158 164 * @param config 协议实例配置
159 165 * @return WebSocket 协议实例
160 166 */
161   - private IotWebSocketProtocol createWebSocketProtocol(IotGatewayProperties.ProtocolInstanceProperties config) {
  167 + private IotWebSocketProtocol createWebSocketProtocol(IotGatewayProperties.ProtocolProperties config) {
162 168 return new IotWebSocketProtocol(config);
163 169 }
164 170  
  171 + /**
  172 + * 创建 MQTT 协议实例
  173 + *
  174 + * @param config 协议实例配置
  175 + * @return MQTT 协议实例
  176 + */
  177 + private IotMqttProtocol createMqttProtocol(IotGatewayProperties.ProtocolProperties config) {
  178 + return new IotMqttProtocol(config);
  179 + }
  180 +
  181 + /**
  182 + * 创建 EMQX 协议实例
  183 + *
  184 + * @param config 协议实例配置
  185 + * @return EMQX 协议实例
  186 + */
  187 + private IotEmqxProtocol createEmqxProtocol(IotGatewayProperties.ProtocolProperties config) {
  188 + return new IotEmqxProtocol(config);
  189 + }
  190 +
165 191 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapProtocol.java
... ... @@ -4,7 +4,7 @@ import cn.hutool.extra.spring.SpringUtil;
4 4 import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
5 5 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
6 6 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
7   -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties;
  7 +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties;
8 8 import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
9 9 import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.downstream.IotCoapDownstreamSubscriber;
10 10 import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapAuthHandler;
... ... @@ -21,7 +21,7 @@ import org.eclipse.californium.core.CoapResource;
21 21 import org.eclipse.californium.core.CoapServer;
22 22 import org.eclipse.californium.core.config.CoapConfig;
23 23 import org.eclipse.californium.elements.config.Configuration;
24   -import org.springframework.util.Assert;
  24 +import cn.hutool.core.lang.Assert;
25 25  
26 26 import java.util.concurrent.TimeUnit;
27 27  
... ... @@ -43,7 +43,7 @@ public class IotCoapProtocol implements IotProtocol {
43 43 /**
44 44 * 协议配置
45 45 */
46   - private final ProtocolInstanceProperties properties;
  46 + private final ProtocolProperties properties;
47 47 /**
48 48 * 服务器 ID(用于消息追踪,全局唯一)
49 49 */
... ... @@ -66,7 +66,7 @@ public class IotCoapProtocol implements IotProtocol {
66 66 */
67 67 private final IotCoapDownstreamSubscriber downstreamSubscriber;
68 68  
69   - public IotCoapProtocol(ProtocolInstanceProperties properties) {
  69 + public IotCoapProtocol(ProtocolProperties properties) {
70 70 IotCoapConfig coapConfig = properties.getCoap();
71 71 Assert.notNull(coapConfig, "CoAP 协议配置(coap)不能为空");
72 72 this.properties = properties;
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterHandler.java
... ... @@ -33,7 +33,7 @@ public class IotCoapRegisterHandler extends IotCoapAbstractHandler {
33 33 Assert.notNull(request, "请求体不能为空");
34 34 Assert.notBlank(request.getProductKey(), "productKey 不能为空");
35 35 Assert.notBlank(request.getDeviceName(), "deviceName 不能为空");
36   - Assert.notBlank(request.getProductSecret(), "productSecret 不能为空");
  36 + Assert.notBlank(request.getSign(), "sign 不能为空");
37 37  
38 38 // 2. 调用动态注册
39 39 CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(request);
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxAuthEventProtocol.java deleted
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx;
2   -
3   -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
4   -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
5   -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router.IotEmqxAuthEventHandler;
6   -import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
7   -import io.vertx.core.Vertx;
8   -import io.vertx.core.http.HttpServer;
9   -import io.vertx.ext.web.Router;
10   -import io.vertx.ext.web.handler.BodyHandler;
11   -import jakarta.annotation.PostConstruct;
12   -import jakarta.annotation.PreDestroy;
13   -import lombok.extern.slf4j.Slf4j;
14   -
15   -/**
16   - * IoT 网关 EMQX 认证事件协议服务
17   - * <p>
18   - * 为 EMQX 提供 HTTP 接口服务,包括:
19   - * 1. 设备认证接口 - 对应 EMQX HTTP 认证插件
20   - * 2. 设备事件处理接口 - 对应 EMQX Webhook 事件通知
21   - *
22   - * @author 芋道源码
23   - */
24   -@Slf4j
25   -public class IotEmqxAuthEventProtocol {
26   -
27   - private final IotGatewayProperties.EmqxProperties emqxProperties;
28   -
29   - private final String serverId;
30   -
31   - private final Vertx vertx;
32   -
33   - private HttpServer httpServer;
34   -
35   - public IotEmqxAuthEventProtocol(IotGatewayProperties.EmqxProperties emqxProperties,
36   - Vertx vertx) {
37   - this.emqxProperties = emqxProperties;
38   - this.vertx = vertx;
39   - this.serverId = IotDeviceMessageUtils.generateServerId(emqxProperties.getMqttPort());
40   - }
41   -
42   - @PostConstruct
43   - public void start() {
44   - try {
45   - startHttpServer();
46   - log.info("[start][IoT 网关 EMQX 认证事件协议服务启动成功, 端口: {}]", emqxProperties.getHttpPort());
47   - } catch (Exception e) {
48   - log.error("[start][IoT 网关 EMQX 认证事件协议服务启动失败]", e);
49   - throw e;
50   - }
51   - }
52   -
53   - @PreDestroy
54   - public void stop() {
55   - stopHttpServer();
56   - log.info("[stop][IoT 网关 EMQX 认证事件协议服务已停止]");
57   - }
58   -
59   - /**
60   - * 启动 HTTP 服务器
61   - */
62   - private void startHttpServer() {
63   - int port = emqxProperties.getHttpPort();
64   -
65   - // 1. 创建路由
66   - Router router = Router.router(vertx);
67   - router.route().handler(BodyHandler.create());
68   -
69   - // 2. 创建处理器,传入 serverId
70   - IotEmqxAuthEventHandler handler = new IotEmqxAuthEventHandler(serverId);
71   - router.post(IotMqttTopicUtils.MQTT_AUTH_PATH).handler(handler::handleAuth);
72   - router.post(IotMqttTopicUtils.MQTT_EVENT_PATH).handler(handler::handleEvent);
73   - // TODO @haohao:/mqtt/acl 需要处理么?
74   - // TODO @芋艿:已在 EMQX 处理,如果是“设备直连”模式需要处理
75   -
76   - // 3. 启动 HTTP 服务器
77   - try {
78   - httpServer = vertx.createHttpServer()
79   - .requestHandler(router)
80   - .listen(port)
81   - .result();
82   - } catch (Exception e) {
83   - log.error("[startHttpServer][HTTP 服务器启动失败, 端口: {}]", port, e);
84   - throw e;
85   - }
86   - }
87   -
88   - /**
89   - * 停止 HTTP 服务器
90   - */
91   - private void stopHttpServer() {
92   - if (httpServer == null) {
93   - return;
94   - }
95   -
96   - try {
97   - httpServer.close().result();
98   - log.info("[stopHttpServer][HTTP 服务器已停止]");
99   - } catch (Exception e) {
100   - log.error("[stopHttpServer][HTTP 服务器停止失败]", e);
101   - }
102   - }
103   -
104   -}
105 0 \ No newline at end of file
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxConfig.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx;
  2 +
  3 +import jakarta.validation.Valid;
  4 +import jakarta.validation.constraints.Max;
  5 +import jakarta.validation.constraints.Min;
  6 +import jakarta.validation.constraints.NotEmpty;
  7 +import jakarta.validation.constraints.NotNull;
  8 +import lombok.Data;
  9 +
  10 +import java.util.List;
  11 +
  12 +/**
  13 + * IoT EMQX 协议配置
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +@Data
  18 +public class IotEmqxConfig {
  19 +
  20 + // ========== MQTT Client 配置(连接 EMQX Broker) ==========
  21 +
  22 + /**
  23 + * MQTT 服务器地址
  24 + */
  25 + @NotEmpty(message = "MQTT 服务器地址不能为空")
  26 + private String mqttHost;
  27 +
  28 + /**
  29 + * MQTT 服务器端口(默认:1883)
  30 + */
  31 + @NotNull(message = "MQTT 服务器端口不能为空")
  32 + private Integer mqttPort = 1883;
  33 +
  34 + /**
  35 + * MQTT 用户名
  36 + */
  37 + @NotEmpty(message = "MQTT 用户名不能为空")
  38 + private String mqttUsername;
  39 +
  40 + /**
  41 + * MQTT 密码
  42 + */
  43 + @NotEmpty(message = "MQTT 密码不能为空")
  44 + private String mqttPassword;
  45 +
  46 + /**
  47 + * MQTT 客户端的 SSL 开关
  48 + */
  49 + @NotNull(message = "MQTT 是否开启 SSL 不能为空")
  50 + private Boolean mqttSsl = false;
  51 +
  52 + /**
  53 + * MQTT 客户端 ID
  54 + */
  55 + @NotEmpty(message = "MQTT 客户端 ID 不能为空")
  56 + private String mqttClientId;
  57 +
  58 + /**
  59 + * MQTT 订阅的主题
  60 + */
  61 + @NotEmpty(message = "MQTT 主题不能为空")
  62 + private List<@NotEmpty(message = "MQTT 主题不能为空") String> mqttTopics;
  63 +
  64 + /**
  65 + * 默认 QoS 级别
  66 + * <p>
  67 + * 0 - 最多一次
  68 + * 1 - 至少一次
  69 + * 2 - 刚好一次
  70 + */
  71 + @NotNull(message = "MQTT QoS 不能为空")
  72 + @Min(value = 0, message = "MQTT QoS 不能小于 0")
  73 + @Max(value = 2, message = "MQTT QoS 不能大于 2")
  74 + private Integer mqttQos = 1;
  75 +
  76 + /**
  77 + * 连接超时时间(秒)
  78 + */
  79 + @NotNull(message = "连接超时时间不能为空")
  80 + @Min(value = 1, message = "连接超时时间不能小于 1 秒")
  81 + private Integer connectTimeoutSeconds = 10;
  82 +
  83 + /**
  84 + * 重连延迟时间(毫秒)
  85 + */
  86 + @NotNull(message = "重连延迟时间不能为空")
  87 + @Min(value = 0, message = "重连延迟时间不能小于 0 毫秒")
  88 + private Long reconnectDelayMs = 5000L;
  89 +
  90 + /**
  91 + * 是否启用 Clean Session (清理会话)
  92 + * true: 每次连接都是新会话,Broker 不保留离线消息和订阅关系。
  93 + * 对于网关这类“永远在线”且会主动重新订阅的应用,建议为 true。
  94 + */
  95 + @NotNull(message = "是否启用 Clean Session 不能为空")
  96 + private Boolean cleanSession = true;
  97 +
  98 + /**
  99 + * 心跳间隔(秒)
  100 + * 用于保持连接活性,及时发现网络中断。
  101 + */
  102 + @NotNull(message = "心跳间隔不能为空")
  103 + @Min(value = 1, message = "心跳间隔不能小于 1 秒")
  104 + private Integer keepAliveIntervalSeconds = 60;
  105 +
  106 + /**
  107 + * 最大未确认消息队列大小
  108 + * 限制已发送但未收到 Broker 确认的 QoS 1/2 消息数量,用于流量控制。
  109 + */
  110 + @NotNull(message = "最大未确认消息队列大小不能为空")
  111 + @Min(value = 1, message = "最大未确认消息队列大小不能小于 1")
  112 + private Integer maxInflightQueue = 10000;
  113 +
  114 + /**
  115 + * 是否信任所有 SSL 证书
  116 + * 警告:此配置会绕过证书验证,仅建议在开发和测试环境中使用!
  117 + * 在生产环境中,应设置为 false,并配置正确的信任库。
  118 + */
  119 + @NotNull(message = "是否信任所有 SSL 证书不能为空")
  120 + private Boolean trustAll = false;
  121 +
  122 + // ========== MQTT Will / SSL 高级配置 ==========
  123 +
  124 + /**
  125 + * 遗嘱消息配置 (用于网关异常下线时通知其他系统)
  126 + */
  127 + @Valid
  128 + private Will will = new Will();
  129 +
  130 + /**
  131 + * 高级 SSL/TLS 配置 (用于生产环境)
  132 + */
  133 + @Valid
  134 + private Ssl sslOptions = new Ssl();
  135 +
  136 + // ========== HTTP Hook 配置(网关提供给 EMQX 调用) ==========
  137 +
  138 + /**
  139 + * HTTP Hook 服务配置(用于 /mqtt/auth、/mqtt/event)
  140 + */
  141 + @Valid
  142 + private Http http = new Http();
  143 +
  144 + /**
  145 + * 遗嘱消息 (Last Will and Testament)
  146 + */
  147 + @Data
  148 + public static class Will {
  149 +
  150 + /**
  151 + * 是否启用遗嘱消息
  152 + */
  153 + private boolean enabled = false;
  154 + /**
  155 + * 遗嘱消息主题
  156 + */
  157 + private String topic;
  158 + /**
  159 + * 遗嘱消息内容
  160 + */
  161 + private String payload;
  162 + /**
  163 + * 遗嘱消息 QoS 等级
  164 + */
  165 + @Min(value = 0, message = "遗嘱消息 QoS 不能小于 0")
  166 + @Max(value = 2, message = "遗嘱消息 QoS 不能大于 2")
  167 + private Integer qos = 1;
  168 + /**
  169 + * 遗嘱消息是否作为保留消息发布
  170 + */
  171 + private boolean retain = true;
  172 +
  173 + }
  174 +
  175 + /**
  176 + * 高级 SSL/TLS 配置
  177 + */
  178 + @Data
  179 + public static class Ssl {
  180 +
  181 + /**
  182 + * 密钥库(KeyStore)路径,例如:classpath:certs/client.jks
  183 + * 包含客户端自己的证书和私钥,用于向服务端证明身份(双向认证)。
  184 + */
  185 + private String keyStorePath;
  186 + /**
  187 + * 密钥库密码
  188 + */
  189 + private String keyStorePassword;
  190 + /**
  191 + * 信任库(TrustStore)路径,例如:classpath:certs/trust.jks
  192 + * 包含服务端信任的 CA 证书,用于验证服务端的身份,防止中间人攻击。
  193 + */
  194 + private String trustStorePath;
  195 + /**
  196 + * 信任库密码
  197 + */
  198 + private String trustStorePassword;
  199 +
  200 + }
  201 +
  202 + /**
  203 + * HTTP Hook 服务 SSL 配置
  204 + */
  205 + @Data
  206 + public static class Http {
  207 +
  208 + /**
  209 + * 是否启用 SSL
  210 + */
  211 + private Boolean sslEnabled = false;
  212 +
  213 + /**
  214 + * SSL 证书路径
  215 + */
  216 + private String sslCertPath;
  217 +
  218 + /**
  219 + * SSL 私钥路径
  220 + */
  221 + private String sslKeyPath;
  222 +
  223 + }
  224 +
  225 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxProtocol.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.lang.Assert;
  5 +import cn.hutool.core.util.StrUtil;
  6 +import cn.hutool.extra.spring.SpringUtil;
  7 +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
  8 +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
  9 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
  10 +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties;
  11 +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
  12 +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.downstream.IotEmqxDownstreamSubscriber;
  13 +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream.IotEmqxAuthEventHandler;
  14 +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream.IotEmqxUpstreamHandler;
  15 +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
  16 +import io.netty.handler.codec.mqtt.MqttQoS;
  17 +import io.vertx.core.Vertx;
  18 +import io.vertx.core.buffer.Buffer;
  19 +import io.vertx.core.http.HttpServer;
  20 +import io.vertx.core.http.HttpServerOptions;
  21 +import io.vertx.core.net.JksOptions;
  22 +import io.vertx.core.net.PemKeyCertOptions;
  23 +import io.vertx.ext.web.Router;
  24 +import io.vertx.ext.web.handler.BodyHandler;
  25 +import io.vertx.mqtt.MqttClient;
  26 +import io.vertx.mqtt.MqttClientOptions;
  27 +import lombok.Getter;
  28 +import lombok.extern.slf4j.Slf4j;
  29 +
  30 +import java.util.List;
  31 +import java.util.Map;
  32 +import java.util.concurrent.TimeUnit;
  33 +
  34 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
  35 +
  36 +/**
  37 + * IoT 网关 EMQX 协议实现:
  38 + * <p>
  39 + * 1. 提供 HTTP Hook 服务(/mqtt/auth、/mqtt/acl、/mqtt/event)给 EMQX 调用
  40 + * 2. 通过 MQTT Client 订阅设备上行消息,并发布下行消息到 Broker
  41 + *
  42 + * @author 芋道源码
  43 + */
  44 +@Slf4j
  45 +public class IotEmqxProtocol implements IotProtocol {
  46 +
  47 + /**
  48 + * 协议配置
  49 + */
  50 + private final ProtocolProperties properties;
  51 + /**
  52 + * EMQX 配置
  53 + */
  54 + private final IotEmqxConfig emqxConfig;
  55 + /**
  56 + * 服务器 ID
  57 + */
  58 + @Getter
  59 + private final String serverId;
  60 +
  61 + /**
  62 + * 运行状态
  63 + */
  64 + @Getter
  65 + private volatile boolean running = false;
  66 +
  67 + /**
  68 + * Vert.x 实例
  69 + */
  70 + private Vertx vertx;
  71 + /**
  72 + * HTTP Hook 服务器
  73 + */
  74 + private HttpServer httpServer;
  75 +
  76 + /**
  77 + * MQTT Client
  78 + */
  79 + private volatile MqttClient mqttClient;
  80 + /**
  81 + * MQTT 重连定时器 ID
  82 + */
  83 + private volatile Long reconnectTimerId;
  84 +
  85 + /**
  86 + * 上行消息处理器
  87 + */
  88 + private final IotEmqxUpstreamHandler upstreamHandler;
  89 +
  90 + /**
  91 + * 下行消息订阅者
  92 + */
  93 + private final IotEmqxDownstreamSubscriber downstreamSubscriber;
  94 +
  95 + public IotEmqxProtocol(ProtocolProperties properties) {
  96 + Assert.notNull(properties, "协议实例配置不能为空");
  97 + Assert.notNull(properties.getEmqx(), "EMQX 协议配置(emqx)不能为空");
  98 + this.properties = properties;
  99 + this.emqxConfig = properties.getEmqx();
  100 + Assert.notNull(emqxConfig.getConnectTimeoutSeconds(),
  101 + "MQTT 连接超时时间(emqx.connect-timeout-seconds)不能为空");
  102 + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
  103 + this.upstreamHandler = new IotEmqxUpstreamHandler(serverId);
  104 +
  105 + // 初始化下行消息订阅者
  106 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
  107 + this.downstreamSubscriber = new IotEmqxDownstreamSubscriber(this, messageBus);
  108 + }
  109 +
  110 + @Override
  111 + public String getId() {
  112 + return properties.getId();
  113 + }
  114 +
  115 + @Override
  116 + public IotProtocolTypeEnum getType() {
  117 + return IotProtocolTypeEnum.EMQX;
  118 + }
  119 +
  120 + @Override
  121 + public void start() {
  122 + if (running) {
  123 + log.warn("[start][IoT EMQX 协议 {} 已经在运行中]", getId());
  124 + return;
  125 + }
  126 +
  127 + // 1.1 创建 Vertx 实例
  128 + this.vertx = Vertx.vertx();
  129 +
  130 + try {
  131 + // 1.2 启动 HTTP Hook 服务
  132 + startHttpServer();
  133 +
  134 + // 1.3 启动 MQTT Client
  135 + startMqttClient();
  136 + running = true;
  137 + log.info("[start][IoT EMQX 协议 {} 启动成功,hookPort:{},serverId:{}]",
  138 + getId(), properties.getPort(), serverId);
  139 +
  140 + // 2. 启动下行消息订阅者
  141 + this.downstreamSubscriber.start();
  142 + } catch (Exception e) {
  143 + log.error("[start][IoT EMQX 协议 {} 启动失败]", getId(), e);
  144 + // 启动失败时,关闭资源
  145 + stop0();
  146 + throw e;
  147 + }
  148 + }
  149 +
  150 + @Override
  151 + public void stop() {
  152 + if (!running) {
  153 + return;
  154 + }
  155 + stop0();
  156 + }
  157 +
  158 + private void stop0() {
  159 + // 1. 停止下行消息订阅者
  160 + try {
  161 + downstreamSubscriber.stop();
  162 + log.info("[stop][IoT EMQX 协议 {} 下行消息订阅者已停止]", getId());
  163 + } catch (Exception e) {
  164 + log.error("[stop][IoT EMQX 协议 {} 下行消息订阅者停止失败]", getId(), e);
  165 + }
  166 +
  167 + // 2.1 先置为 false:避免 closeHandler 触发重连
  168 + running = false;
  169 + stopMqttClientReconnectChecker();
  170 + // 2.2 停止 MQTT Client
  171 + stopMqttClient();
  172 +
  173 + // 2.3 停止 HTTP Hook 服务
  174 + stopHttpServer();
  175 +
  176 + // 2.4 关闭 Vertx
  177 + if (vertx != null) {
  178 + try {
  179 + vertx.close().toCompletionStage().toCompletableFuture()
  180 + .get(10, TimeUnit.SECONDS);
  181 + log.info("[stop][IoT EMQX 协议 {} Vertx 已关闭]", getId());
  182 + } catch (Exception e) {
  183 + log.error("[stop][IoT EMQX 协议 {} Vertx 关闭失败]", getId(), e);
  184 + }
  185 + vertx = null;
  186 + }
  187 +
  188 + log.info("[stop][IoT EMQX 协议 {} 已停止]", getId());
  189 + }
  190 +
  191 + // ======================================= HTTP Hook Server =======================================
  192 +
  193 + /**
  194 + * 启动 HTTP Hook 服务(/mqtt/auth、/mqtt/acl、/mqtt/event)
  195 + */
  196 + private void startHttpServer() {
  197 + // 1. 创建路由
  198 + Router router = Router.router(vertx);
  199 + router.route().handler(BodyHandler.create().setBodyLimit(1024 * 1024)); // 限制 body 大小为 1MB,防止大包攻击
  200 +
  201 + // 2. 创建处理器
  202 + IotEmqxAuthEventHandler handler = new IotEmqxAuthEventHandler(serverId, this);
  203 + router.post(IotMqttTopicUtils.MQTT_AUTH_PATH).handler(handler::handleAuth);
  204 + router.post(IotMqttTopicUtils.MQTT_ACL_PATH).handler(handler::handleAcl);
  205 + router.post(IotMqttTopicUtils.MQTT_EVENT_PATH).handler(handler::handleEvent);
  206 +
  207 + // 3. 启动 HTTP Server(支持 HTTPS)
  208 + IotEmqxConfig.Http httpConfig = emqxConfig.getHttp();
  209 + HttpServerOptions options = new HttpServerOptions().setPort(properties.getPort());
  210 + if (httpConfig != null && Boolean.TRUE.equals(httpConfig.getSslEnabled())) {
  211 + Assert.notBlank(httpConfig.getSslCertPath(), "EMQX HTTP SSL 证书路径(emqx.http.ssl-cert-path)不能为空");
  212 + Assert.notBlank(httpConfig.getSslKeyPath(), "EMQX HTTP SSL 私钥路径(emqx.http.ssl-key-path)不能为空");
  213 + PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions()
  214 + .setKeyPath(httpConfig.getSslKeyPath())
  215 + .setCertPath(httpConfig.getSslCertPath());
  216 + options.setSsl(true).setKeyCertOptions(pemKeyCertOptions);
  217 + }
  218 + try {
  219 + httpServer = vertx.createHttpServer(options)
  220 + .requestHandler(router)
  221 + .listen()
  222 + .toCompletionStage().toCompletableFuture()
  223 + .get(10, TimeUnit.SECONDS);
  224 + log.info("[startHttpServer][IoT EMQX 协议 {} HTTP Hook 服务启动成功, port: {}, ssl: {}]",
  225 + getId(), properties.getPort(), httpConfig != null && Boolean.TRUE.equals(httpConfig.getSslEnabled()));
  226 + } catch (Exception e) {
  227 + log.error("[startHttpServer][IoT EMQX 协议 {} HTTP Hook 服务启动失败, port: {}]", getId(), properties.getPort(), e);
  228 + throw new RuntimeException("HTTP Hook 服务启动失败", e);
  229 + }
  230 + }
  231 +
  232 + private void stopHttpServer() {
  233 + if (httpServer == null) {
  234 + return;
  235 + }
  236 + try {
  237 + httpServer.close().toCompletionStage().toCompletableFuture()
  238 + .get(5, TimeUnit.SECONDS);
  239 + log.info("[stopHttpServer][IoT EMQX 协议 {} HTTP Hook 服务已停止]", getId());
  240 + } catch (Exception e) {
  241 + log.error("[stopHttpServer][IoT EMQX 协议 {} HTTP Hook 服务停止失败]", getId(), e);
  242 + } finally {
  243 + httpServer = null;
  244 + }
  245 + }
  246 +
  247 + // ======================================= MQTT Client ======================================
  248 +
  249 + private void startMqttClient() {
  250 + // 1.1 创建 MQTT Client
  251 + MqttClient client = createMqttClient();
  252 + this.mqttClient = client;
  253 + // 1.2 连接 MQTT Broker
  254 + if (!connectMqttClient(client)) {
  255 + throw new RuntimeException("MQTT Client 启动失败: 连接 Broker 失败");
  256 + }
  257 +
  258 + // 2. 启动定时重连检查
  259 + startMqttClientReconnectChecker();
  260 + }
  261 +
  262 + private void stopMqttClient() {
  263 + MqttClient client = this.mqttClient;
  264 + this.mqttClient = null; // 先清理引用
  265 + if (client == null) {
  266 + return;
  267 + }
  268 +
  269 + // 1. 批量取消订阅(仅在连接时)
  270 + if (client.isConnected()) {
  271 + List<String> topicList = emqxConfig.getMqttTopics();
  272 + if (CollUtil.isNotEmpty(topicList)) {
  273 + try {
  274 + client.unsubscribe(topicList).toCompletionStage().toCompletableFuture()
  275 + .get(5, TimeUnit.SECONDS);
  276 + } catch (Exception e) {
  277 + log.warn("[stopMqttClient][IoT EMQX 协议 {} 取消订阅异常]", getId(), e);
  278 + }
  279 + }
  280 + }
  281 +
  282 + // 2. 断开 MQTT 连接
  283 + try {
  284 + client.disconnect().toCompletionStage().toCompletableFuture()
  285 + .get(5, TimeUnit.SECONDS);
  286 + } catch (Exception e) {
  287 + log.warn("[stopMqttClient][IoT EMQX 协议 {} 断开连接异常]", getId(), e);
  288 + }
  289 + }
  290 +
  291 + // ======================================= MQTT 基础方法 ======================================
  292 +
  293 + /**
  294 + * 创建 MQTT 客户端
  295 + *
  296 + * @return 新创建的 MqttClient
  297 + */
  298 + private MqttClient createMqttClient() {
  299 + // 1.1 基础配置
  300 + MqttClientOptions options = new MqttClientOptions()
  301 + .setClientId(emqxConfig.getMqttClientId())
  302 + .setUsername(emqxConfig.getMqttUsername())
  303 + .setPassword(emqxConfig.getMqttPassword())
  304 + .setSsl(Boolean.TRUE.equals(emqxConfig.getMqttSsl()))
  305 + .setCleanSession(Boolean.TRUE.equals(emqxConfig.getCleanSession()))
  306 + .setKeepAliveInterval(emqxConfig.getKeepAliveIntervalSeconds())
  307 + .setMaxInflightQueue(emqxConfig.getMaxInflightQueue());
  308 + options.setConnectTimeout(emqxConfig.getConnectTimeoutSeconds() * 1000); // Vert.x 需要毫秒
  309 + options.setTrustAll(Boolean.TRUE.equals(emqxConfig.getTrustAll()));
  310 + // 1.2 配置遗嘱消息
  311 + IotEmqxConfig.Will will = emqxConfig.getWill();
  312 + if (will != null && will.isEnabled()) {
  313 + Assert.notBlank(will.getTopic(), "遗嘱消息主题(emqx.will.topic)不能为空");
  314 + Assert.notNull(will.getPayload(), "遗嘱消息内容(emqx.will.payload)不能为空");
  315 + options.setWillFlag(true)
  316 + .setWillTopic(will.getTopic())
  317 + .setWillMessageBytes(Buffer.buffer(will.getPayload()))
  318 + .setWillQoS(will.getQos())
  319 + .setWillRetain(will.isRetain());
  320 + }
  321 + // 1.3 配置高级 SSL/TLS(仅在启用 SSL 且不信任所有证书时生效,且需要 sslOptions 非空)
  322 + IotEmqxConfig.Ssl sslOptions = emqxConfig.getSslOptions();
  323 + if (Boolean.TRUE.equals(emqxConfig.getMqttSsl())
  324 + && Boolean.FALSE.equals(emqxConfig.getTrustAll())
  325 + && sslOptions != null) {
  326 + if (StrUtil.isNotBlank(sslOptions.getTrustStorePath())) {
  327 + options.setTrustStoreOptions(new JksOptions()
  328 + .setPath(sslOptions.getTrustStorePath())
  329 + .setPassword(sslOptions.getTrustStorePassword()));
  330 + }
  331 + if (StrUtil.isNotBlank(sslOptions.getKeyStorePath())) {
  332 + options.setKeyStoreOptions(new JksOptions()
  333 + .setPath(sslOptions.getKeyStorePath())
  334 + .setPassword(sslOptions.getKeyStorePassword()));
  335 + }
  336 + }
  337 +
  338 + // 2. 创建客户端
  339 + return MqttClient.create(vertx, options);
  340 + }
  341 +
  342 + /**
  343 + * 连接 MQTT Broker(同步等待)
  344 + *
  345 + * @param client MQTT 客户端
  346 + * @return 连接成功返回 true,失败返回 false
  347 + */
  348 + @SuppressWarnings("BooleanMethodIsAlwaysInverted")
  349 + private synchronized boolean connectMqttClient(MqttClient client) {
  350 + String host = emqxConfig.getMqttHost();
  351 + int port = emqxConfig.getMqttPort();
  352 + int timeoutSeconds = emqxConfig.getConnectTimeoutSeconds();
  353 + try {
  354 + // 1. 连接 Broker
  355 + client.connect(port, host).toCompletionStage().toCompletableFuture()
  356 + .get(timeoutSeconds, TimeUnit.SECONDS);
  357 + log.info("[connectMqttClient][IoT EMQX 协议 {} 连接成功, host: {}, port: {}]",
  358 + getId(), host, port);
  359 +
  360 + // 2. 设置处理器
  361 + setupMqttClientHandlers(client);
  362 + subscribeMqttClientTopics(client);
  363 + return true;
  364 + } catch (Exception e) {
  365 + log.error("[connectMqttClient][IoT EMQX 协议 {} 连接发生异常]", getId(), e);
  366 + return false;
  367 + }
  368 + }
  369 +
  370 + /**
  371 + * 关闭 MQTT 客户端
  372 + */
  373 + private void closeMqttClient() {
  374 + MqttClient oldClient = this.mqttClient;
  375 + this.mqttClient = null; // 先清理引用
  376 + if (oldClient == null) {
  377 + return;
  378 + }
  379 + // 尽力释放(无论是否连接都尝试 disconnect)
  380 + try {
  381 + oldClient.disconnect().toCompletionStage().toCompletableFuture()
  382 + .get(5, TimeUnit.SECONDS);
  383 + } catch (Exception ignored) {
  384 + }
  385 + }
  386 +
  387 + // ======================================= MQTT 重连机制 ======================================
  388 +
  389 + /**
  390 + * 启动 MQTT Client 周期性重连检查器
  391 + */
  392 + private void startMqttClientReconnectChecker() {
  393 + long interval = emqxConfig.getReconnectDelayMs();
  394 + this.reconnectTimerId = vertx.setPeriodic(interval, timerId -> {
  395 + if (!running) {
  396 + return;
  397 + }
  398 + if (mqttClient != null && mqttClient.isConnected()) {
  399 + return;
  400 + }
  401 + log.info("[startMqttClientReconnectChecker][IoT EMQX 协议 {} 检测到断开,尝试重连]", getId());
  402 + // 用 executeBlocking 避免阻塞 event-loop(tryReconnectMqttClient 内部有同步等待)
  403 + vertx.executeBlocking(() -> {
  404 + tryReconnectMqttClient();
  405 + return null;
  406 + });
  407 + });
  408 + }
  409 +
  410 + /**
  411 + * 停止 MQTT Client 重连检查器
  412 + */
  413 + private void stopMqttClientReconnectChecker() {
  414 + if (reconnectTimerId != null && vertx != null) {
  415 + try {
  416 + vertx.cancelTimer(reconnectTimerId);
  417 + } catch (Exception ignored) {
  418 + }
  419 + reconnectTimerId = null;
  420 + }
  421 + }
  422 +
  423 + /**
  424 + * 尝试重连 MQTT Client
  425 + */
  426 + private synchronized void tryReconnectMqttClient() {
  427 + // 1. 前置检查
  428 + if (!running) {
  429 + return;
  430 + }
  431 + if (mqttClient != null && mqttClient.isConnected()) {
  432 + return;
  433 + }
  434 +
  435 + log.info("[tryReconnectMqttClient][IoT EMQX 协议 {} 开始重连]", getId());
  436 + try {
  437 + // 2. 关闭旧客户端
  438 + closeMqttClient();
  439 +
  440 + // 3.1 创建新客户端
  441 + MqttClient client = createMqttClient();
  442 + this.mqttClient = client;
  443 + // 3.2 连接(失败只打印日志,等下次定时)
  444 + if (!connectMqttClient(client)) {
  445 + log.warn("[tryReconnectMqttClient][IoT EMQX 协议 {} 重连失败,等待下次重试]", getId());
  446 + }
  447 + } catch (Exception e) {
  448 + log.error("[tryReconnectMqttClient][IoT EMQX 协议 {} 重连异常]", getId(), e);
  449 + }
  450 + }
  451 +
  452 + // ======================================= MQTT Handler ======================================
  453 +
  454 + /**
  455 + * 设置 MQTT Client 事件处理器
  456 + */
  457 + private void setupMqttClientHandlers(MqttClient client) {
  458 + // 1. 断开重连监听
  459 + client.closeHandler(closeEvent -> {
  460 + if (!running) {
  461 + return;
  462 + }
  463 + log.warn("[setupMqttClientHandlers][IoT EMQX 协议 {} 连接断开,立即尝试重连]", getId());
  464 + // 用 executeBlocking 避免阻塞 event-loop(tryReconnectMqttClient 内部有同步等待)
  465 + vertx.executeBlocking(() -> {
  466 + tryReconnectMqttClient();
  467 + return null;
  468 + });
  469 + });
  470 +
  471 + // 2. 异常处理
  472 + client.exceptionHandler(exception ->
  473 + log.error("[setupMqttClientHandlers][IoT EMQX 协议 {} MQTT Client 异常]", getId(), exception));
  474 +
  475 + // 3. 上行消息处理
  476 + client.publishHandler(upstreamHandler::handle);
  477 + }
  478 +
  479 + /**
  480 + * 订阅 MQTT Client 主题(同步等待)
  481 + */
  482 + private void subscribeMqttClientTopics(MqttClient client) {
  483 + List<String> topicList = emqxConfig.getMqttTopics();
  484 + if (!client.isConnected()) {
  485 + log.warn("[subscribeMqttClientTopics][IoT EMQX 协议 {} MQTT Client 未连接, 跳过订阅]", getId());
  486 + return;
  487 + }
  488 + if (CollUtil.isEmpty(topicList)) {
  489 + log.warn("[subscribeMqttClientTopics][IoT EMQX 协议 {} 未配置订阅主题, 跳过订阅]", getId());
  490 + return;
  491 + }
  492 + // 执行订阅
  493 + Map<String, Integer> topics = convertMap(emqxConfig.getMqttTopics(), topic -> topic,
  494 + topic -> emqxConfig.getMqttQos());
  495 + try {
  496 + client.subscribe(topics).toCompletionStage().toCompletableFuture()
  497 + .get(10, TimeUnit.SECONDS);
  498 + log.info("[subscribeMqttClientTopics][IoT EMQX 协议 {} 订阅成功, 共 {} 个主题]", getId(), topicList.size());
  499 + } catch (Exception e) {
  500 + log.error("[subscribeMqttClientTopics][IoT EMQX 协议 {} 订阅失败]", getId(), e);
  501 + }
  502 + }
  503 +
  504 + /**
  505 + * 发布消息到 MQTT Broker
  506 + *
  507 + * @param topic 主题
  508 + * @param payload 消息内容
  509 + */
  510 + public void publishMessage(String topic, byte[] payload) {
  511 + if (mqttClient == null || !mqttClient.isConnected()) {
  512 + log.warn("[publishMessage][IoT EMQX 协议 {} MQTT Client 未连接, 无法发布消息]", getId());
  513 + return;
  514 + }
  515 + MqttQoS qos = MqttQoS.valueOf(emqxConfig.getMqttQos());
  516 + mqttClient.publish(topic, Buffer.buffer(payload), qos, false, false)
  517 + .onFailure(e -> log.error("[publishMessage][IoT EMQX 协议 {} 发布失败, topic: {}]", getId(), topic, e));
  518 + }
  519 +
  520 + /**
  521 + * 延迟发布消息到 MQTT Broker
  522 + *
  523 + * @param topic 主题
  524 + * @param payload 消息内容
  525 + * @param delayMs 延迟时间(毫秒)
  526 + */
  527 + public void publishDelayMessage(String topic, byte[] payload, long delayMs) {
  528 + vertx.setTimer(delayMs, id -> publishMessage(topic, payload));
  529 + }
  530 +
  531 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxUpstreamProtocol.java deleted
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx;
2   -
3   -import cn.hutool.core.lang.Assert;
4   -import cn.hutool.core.util.StrUtil;
5   -import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
6   -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
7   -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
8   -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
9   -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router.IotEmqxUpstreamHandler;
10   -import io.netty.handler.codec.mqtt.MqttQoS;
11   -import io.vertx.core.Vertx;
12   -import io.vertx.core.buffer.Buffer;
13   -import io.vertx.core.net.JksOptions;
14   -import io.vertx.mqtt.MqttClient;
15   -import io.vertx.mqtt.MqttClientOptions;
16   -import jakarta.annotation.PostConstruct;
17   -import jakarta.annotation.PreDestroy;
18   -import lombok.Getter;
19   -import lombok.extern.slf4j.Slf4j;
20   -
21   -import java.util.HashMap;
22   -import java.util.List;
23   -import java.util.Map;
24   -import java.util.concurrent.CountDownLatch;
25   -import java.util.concurrent.atomic.AtomicBoolean;
26   -
27   -/**
28   - * IoT 网关 EMQX 协议:接收设备上行消息
29   - *
30   - * @author 芋道源码
31   - */
32   -@Slf4j
33   -public class IotEmqxUpstreamProtocol implements IotProtocol {
34   -
35   - private static final String ID = "emqx";
36   -
37   - private final IotGatewayProperties.EmqxProperties emqxProperties;
38   -
39   - private volatile boolean running = false;
40   -
41   - private final Vertx vertx;
42   -
43   - @Getter
44   - private final String serverId;
45   -
46   - private MqttClient mqttClient;
47   -
48   - private IotEmqxUpstreamHandler upstreamHandler;
49   -
50   - public IotEmqxUpstreamProtocol(IotGatewayProperties.EmqxProperties emqxProperties,
51   - Vertx vertx) {
52   - this.emqxProperties = emqxProperties;
53   - this.serverId = IotDeviceMessageUtils.generateServerId(emqxProperties.getMqttPort());
54   - this.vertx = vertx;
55   - }
56   -
57   - @Override
58   - public String getId() {
59   - return ID;
60   - }
61   -
62   - @Override
63   - public IotProtocolTypeEnum getType() {
64   - return IotProtocolTypeEnum.EMQX;
65   - }
66   -
67   - @Override
68   - @PostConstruct
69   - public void start() {
70   - if (running) {
71   - return;
72   - }
73   -
74   - try {
75   - // 1. 启动 MQTT 客户端
76   - startMqttClient();
77   -
78   - // 2. 标记服务为运行状态
79   - running = true;
80   - log.info("[start][IoT 网关 EMQX 协议启动成功]");
81   - } catch (Exception e) {
82   - log.error("[start][IoT 网关 EMQX 协议服务启动失败,应用将关闭]", e);
83   - stop();
84   -
85   - // 异步关闭应用
86   - Thread shutdownThread = new Thread(() -> {
87   - try {
88   - // 确保日志输出完成,使用更优雅的方式
89   - log.error("[start][由于 MQTT 连接失败,正在关闭应用]");
90   - // 等待日志输出完成
91   - Thread.sleep(1000);
92   - } catch (InterruptedException ie) {
93   - Thread.currentThread().interrupt();
94   - log.warn("[start][应用关闭被中断]");
95   - }
96   - System.exit(1);
97   - });
98   - shutdownThread.setDaemon(true);
99   - shutdownThread.setName("emergency-shutdown");
100   - shutdownThread.start();
101   -
102   - throw e;
103   - }
104   - }
105   -
106   - @Override
107   - @PreDestroy
108   - public void stop() {
109   - if (!running) {
110   - return;
111   - }
112   -
113   - // 1. 停止 MQTT 客户端
114   - stopMqttClient();
115   -
116   - // 2. 标记服务为停止状态
117   - running = false;
118   - log.info("[stop][IoT 网关 MQTT 协议服务已停止]");
119   - }
120   -
121   - @Override
122   - public boolean isRunning() {
123   - return running;
124   - }
125   -
126   - /**
127   - * 启动 MQTT 客户端
128   - */
129   - private void startMqttClient() {
130   - try {
131   - // 1. 初始化消息处理器
132   - this.upstreamHandler = new IotEmqxUpstreamHandler(this);
133   -
134   - // 2. 创建 MQTT 客户端
135   - createMqttClient();
136   -
137   - // 3. 同步连接 MQTT Broker
138   - connectMqttSync();
139   - } catch (Exception e) {
140   - log.error("[startMqttClient][MQTT 客户端启动失败]", e);
141   - throw new RuntimeException("MQTT 客户端启动失败: " + e.getMessage(), e);
142   - }
143   - }
144   -
145   - /**
146   - * 同步连接 MQTT Broker
147   - */
148   - private void connectMqttSync() {
149   - String host = emqxProperties.getMqttHost();
150   - int port = emqxProperties.getMqttPort();
151   - // 1. 连接 MQTT Broker
152   - CountDownLatch latch = new CountDownLatch(1);
153   - AtomicBoolean success = new AtomicBoolean(false);
154   - mqttClient.connect(port, host, connectResult -> {
155   - if (connectResult.succeeded()) {
156   - log.info("[connectMqttSync][MQTT 客户端连接成功, host: {}, port: {}]", host, port);
157   - setupMqttHandlers();
158   - subscribeToTopics();
159   - success.set(true);
160   - } else {
161   - log.error("[connectMqttSync][连接 MQTT Broker 失败, host: {}, port: {}]",
162   - host, port, connectResult.cause());
163   - }
164   - latch.countDown();
165   - });
166   -
167   - // 2. 等待连接结果
168   - try {
169   - // 应用层超时控制:防止启动过程无限阻塞,与MQTT客户端的网络超时是不同层次的控制
170   - boolean awaitResult = latch.await(10, java.util.concurrent.TimeUnit.SECONDS);
171   - if (!awaitResult) {
172   - log.error("[connectMqttSync][等待连接结果超时]");
173   - throw new RuntimeException("连接 MQTT Broker 超时");
174   - }
175   - if (!success.get()) {
176   - throw new RuntimeException(String.format("首次连接 MQTT Broker 失败,地址: %s, 端口: %d", host, port));
177   - }
178   - } catch (InterruptedException e) {
179   - Thread.currentThread().interrupt();
180   - log.error("[connectMqttSync][等待连接结果被中断]", e);
181   - throw new RuntimeException("连接 MQTT Broker 被中断", e);
182   - }
183   - }
184   -
185   - /**
186   - * 异步连接 MQTT Broker
187   - */
188   - private void connectMqttAsync() {
189   - String host = emqxProperties.getMqttHost();
190   - int port = emqxProperties.getMqttPort();
191   - mqttClient.connect(port, host, connectResult -> {
192   - if (connectResult.succeeded()) {
193   - log.info("[connectMqttAsync][MQTT 客户端重连成功]");
194   - setupMqttHandlers();
195   - subscribeToTopics();
196   - } else {
197   - log.error("[connectMqttAsync][连接 MQTT Broker 失败, host: {}, port: {}]",
198   - host, port, connectResult.cause());
199   - log.warn("[connectMqttAsync][重连失败,将再次尝试]");
200   - reconnectWithDelay();
201   - }
202   - });
203   - }
204   -
205   - /**
206   - * 延迟重连
207   - */
208   - private void reconnectWithDelay() {
209   - if (!running) {
210   - return;
211   - }
212   - if (mqttClient != null && mqttClient.isConnected()) {
213   - return;
214   - }
215   -
216   - long delay = emqxProperties.getReconnectDelayMs();
217   - log.info("[reconnectWithDelay][将在 {} 毫秒后尝试重连 MQTT Broker]", delay);
218   - vertx.setTimer(delay, timerId -> {
219   - if (!running) {
220   - return;
221   - }
222   - if (mqttClient != null && mqttClient.isConnected()) {
223   - return;
224   - }
225   -
226   - log.info("[reconnectWithDelay][开始重连 MQTT Broker]");
227   - try {
228   - createMqttClient();
229   - connectMqttAsync();
230   - } catch (Exception e) {
231   - log.error("[reconnectWithDelay][重连过程中发生异常]", e);
232   - vertx.setTimer(delay, t -> reconnectWithDelay());
233   - }
234   - });
235   - }
236   -
237   - /**
238   - * 停止 MQTT 客户端
239   - */
240   - private void stopMqttClient() {
241   - if (mqttClient == null) {
242   - return;
243   - }
244   - try {
245   - if (mqttClient.isConnected()) {
246   - // 1. 取消订阅所有主题
247   - List<String> topicList = emqxProperties.getMqttTopics();
248   - for (String topic : topicList) {
249   - try {
250   - mqttClient.unsubscribe(topic);
251   - } catch (Exception e) {
252   - log.warn("[stopMqttClient][取消订阅主题({})异常]", topic, e);
253   - }
254   - }
255   -
256   - // 2. 断开 MQTT 客户端连接
257   - try {
258   - CountDownLatch disconnectLatch = new CountDownLatch(1);
259   - mqttClient.disconnect(ar -> disconnectLatch.countDown());
260   - if (!disconnectLatch.await(5, java.util.concurrent.TimeUnit.SECONDS)) {
261   - log.warn("[stopMqttClient][断开 MQTT 连接超时]");
262   - }
263   - } catch (Exception e) {
264   - log.warn("[stopMqttClient][关闭 MQTT 客户端异常]", e);
265   - }
266   - }
267   - } catch (Exception e) {
268   - log.warn("[stopMqttClient][停止 MQTT 客户端过程中发生异常]", e);
269   - } finally {
270   - mqttClient = null;
271   - }
272   - }
273   -
274   - /**
275   - * 创建 MQTT 客户端
276   - */
277   - private void createMqttClient() {
278   - // 1.1 创建基础配置
279   - MqttClientOptions options = (MqttClientOptions) new MqttClientOptions()
280   - .setClientId(emqxProperties.getMqttClientId())
281   - .setUsername(emqxProperties.getMqttUsername())
282   - .setPassword(emqxProperties.getMqttPassword())
283   - .setSsl(emqxProperties.getMqttSsl())
284   - .setCleanSession(emqxProperties.getCleanSession())
285   - .setKeepAliveInterval(emqxProperties.getKeepAliveIntervalSeconds())
286   - .setMaxInflightQueue(emqxProperties.getMaxInflightQueue())
287   - .setConnectTimeout(emqxProperties.getConnectTimeoutSeconds() * 1000) // Vert.x 需要毫秒
288   - .setTrustAll(emqxProperties.getTrustAll());
289   - // 1.2 配置遗嘱消息
290   - IotGatewayProperties.EmqxProperties.Will will = emqxProperties.getWill();
291   - if (will.isEnabled()) {
292   - Assert.notBlank(will.getTopic(), "遗嘱消息主题(will.topic)不能为空");
293   - Assert.notNull(will.getPayload(), "遗嘱消息内容(will.payload)不能为空");
294   - options.setWillFlag(true)
295   - .setWillTopic(will.getTopic())
296   - .setWillMessageBytes(Buffer.buffer(will.getPayload()))
297   - .setWillQoS(will.getQos())
298   - .setWillRetain(will.isRetain());
299   - }
300   - // 1.3 配置高级 SSL/TLS (仅在启用 SSL 且不信任所有证书时生效)
301   - if (Boolean.TRUE.equals(emqxProperties.getMqttSsl()) && !Boolean.TRUE.equals(emqxProperties.getTrustAll())) {
302   - IotGatewayProperties.EmqxProperties.Ssl sslOptions = emqxProperties.getSslOptions();
303   - if (StrUtil.isNotBlank(sslOptions.getTrustStorePath())) {
304   - options.setTrustStoreOptions(new JksOptions()
305   - .setPath(sslOptions.getTrustStorePath())
306   - .setPassword(sslOptions.getTrustStorePassword()));
307   - }
308   - if (StrUtil.isNotBlank(sslOptions.getKeyStorePath())) {
309   - options.setKeyStoreOptions(new JksOptions()
310   - .setPath(sslOptions.getKeyStorePath())
311   - .setPassword(sslOptions.getKeyStorePassword()));
312   - }
313   - }
314   - // 1.4 安全警告日志
315   - if (Boolean.TRUE.equals(emqxProperties.getTrustAll())) {
316   - log.warn("[createMqttClient][安全警告:当前配置信任所有 SSL 证书(trustAll=true),这在生产环境中存在严重安全风险!]");
317   - }
318   -
319   - // 2. 创建客户端实例
320   - this.mqttClient = MqttClient.create(vertx, options);
321   - }
322   -
323   - /**
324   - * 设置 MQTT 处理器
325   - */
326   - private void setupMqttHandlers() {
327   - // 1. 设置断开重连监听器
328   - mqttClient.closeHandler(closeEvent -> {
329   - if (!running) {
330   - return;
331   - }
332   - log.warn("[closeHandler][MQTT 连接已断开, 准备重连]");
333   - reconnectWithDelay();
334   - });
335   -
336   - // 2. 设置异常处理器
337   - mqttClient.exceptionHandler(exception ->
338   - log.error("[exceptionHandler][MQTT 客户端异常]", exception));
339   -
340   - // 3. 设置消息处理器
341   - mqttClient.publishHandler(upstreamHandler::handle);
342   - }
343   -
344   - /**
345   - * 订阅设备上行消息主题
346   - */
347   - private void subscribeToTopics() {
348   - // 1. 校验 MQTT 客户端是否连接
349   - List<String> topicList = emqxProperties.getMqttTopics();
350   - if (mqttClient == null || !mqttClient.isConnected()) {
351   - log.warn("[subscribeToTopics][MQTT 客户端未连接, 跳过订阅]");
352   - return;
353   - }
354   -
355   - // 2. 批量订阅所有主题
356   - Map<String, Integer> topics = new HashMap<>();
357   - int qos = emqxProperties.getMqttQos();
358   - for (String topic : topicList) {
359   - topics.put(topic, qos);
360   - }
361   - mqttClient.subscribe(topics, subscribeResult -> {
362   - if (subscribeResult.succeeded()) {
363   - log.info("[subscribeToTopics][订阅主题成功, 共 {} 个主题]", topicList.size());
364   - } else {
365   - log.error("[subscribeToTopics][订阅主题失败, 共 {} 个主题, 原因: {}]",
366   - topicList.size(), subscribeResult.cause().getMessage(), subscribeResult.cause());
367   - }
368   - });
369   - }
370   -
371   - /**
372   - * 发布消息到 MQTT Broker
373   - *
374   - * @param topic 主题
375   - * @param payload 消息内容
376   - */
377   - public void publishMessage(String topic, byte[] payload) {
378   - if (mqttClient == null || !mqttClient.isConnected()) {
379   - log.warn("[publishMessage][MQTT 客户端未连接, 无法发布消息]");
380   - return;
381   - }
382   - MqttQoS qos = MqttQoS.valueOf(emqxProperties.getMqttQos());
383   - mqttClient.publish(topic, Buffer.buffer(payload), qos, false, false);
384   - }
385   -
386   -}
387 0 \ No newline at end of file
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxDownstreamHandler.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamHandler.java
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router;
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.downstream;
2 2  
3 3 import cn.hutool.core.util.StrUtil;
4 4 import cn.hutool.extra.spring.SpringUtil;
5 5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
6 6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
7 7 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
8   -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol;
  8 +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol;
9 9 import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
10 10 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
11 11 import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
... ... @@ -21,13 +21,13 @@ import lombok.extern.slf4j.Slf4j;
21 21 @Slf4j
22 22 public class IotEmqxDownstreamHandler {
23 23  
24   - private final IotEmqxUpstreamProtocol protocol;
  24 + private final IotEmqxProtocol protocol;
25 25  
26 26 private final IotDeviceService deviceService;
27 27  
28 28 private final IotDeviceMessageService deviceMessageService;
29 29  
30   - public IotEmqxDownstreamHandler(IotEmqxUpstreamProtocol protocol) {
  30 + public IotEmqxDownstreamHandler(IotEmqxProtocol protocol) {
31 31 this.protocol = protocol;
32 32 this.deviceService = SpringUtil.getBean(IotDeviceService.class);
33 33 this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
... ... @@ -53,9 +53,10 @@ public class IotEmqxDownstreamHandler {
53 53 return;
54 54 }
55 55 // 2.2 构建载荷
56   - byte[] payload = deviceMessageService.encodeDeviceMessage(message, deviceInfo.getProductKey(),
  56 + byte[] payload = deviceMessageService.serializeDeviceMessage(message, deviceInfo.getProductKey(),
57 57 deviceInfo.getDeviceName());
58   - // 2.3 发布消息
  58 +
  59 + // 3. 发布消息
59 60 protocol.publishMessage(topic, payload);
60 61 }
61 62  
... ... @@ -74,4 +75,4 @@ public class IotEmqxDownstreamHandler {
74 75 return IotMqttTopicUtils.buildTopicByMethod(message.getMethod(), productKey, deviceName, isReply);
75 76 }
76 77  
77   -}
78 78 \ No newline at end of file
  79 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxDownstreamSubscriber.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamSubscriber.java
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx;
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.downstream;
2 2  
3 3 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
4 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
5   -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
6 5 import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber;
7   -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router.IotEmqxDownstreamHandler;
  6 +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol;
8 7 import lombok.extern.slf4j.Slf4j;
9 8  
10 9 /**
... ... @@ -17,7 +16,7 @@ public class IotEmqxDownstreamSubscriber extends IotProtocolDownstreamSubscriber
17 16  
18 17 private final IotEmqxDownstreamHandler downstreamHandler;
19 18  
20   - public IotEmqxDownstreamSubscriber(IotEmqxUpstreamProtocol protocol, IotMessageBus messageBus) {
  19 + public IotEmqxDownstreamSubscriber(IotEmqxProtocol protocol, IotMessageBus messageBus) {
21 20 super(protocol, messageBus);
22 21 this.downstreamHandler = new IotEmqxDownstreamHandler(protocol);
23 22 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxAuthEventHandler.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxAuthEventHandler.java
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router;
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream;
2 2  
  3 +import cn.hutool.core.lang.Assert;
3 4 import cn.hutool.core.util.BooleanUtil;
4 5 import cn.hutool.core.util.StrUtil;
5 6 import cn.hutool.extra.spring.SpringUtil;
6 7 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
7 8 import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
8 9 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
  10 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
9 11 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
10 12 import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
  13 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
  14 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
11 15 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  16 +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol;
12 17 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
  18 +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
13 19 import io.vertx.core.json.JsonObject;
14 20 import io.vertx.ext.web.RoutingContext;
15 21 import lombok.extern.slf4j.Slf4j;
16 22  
  23 +import java.util.Locale;
  24 +
17 25 /**
18 26 * IoT 网关 EMQX 认证事件处理器
19 27 * <p>
20 28 * 为 EMQX 提供 HTTP 接口服务,包括:
21   - * 1. 设备认证接口 - 对应 EMQX HTTP 认证插件
22   - * 2. 设备事件处理接口 - 对应 EMQX Webhook 事件通知
  29 + * 1. 设备认证接口 - 对应 EMQX HTTP 认证插件 {@link #handleAuth(RoutingContext)}
  30 + * 2. 设备事件处理接口 - 对应 EMQX Webhook 事件通知 {@link #handleEvent(RoutingContext)}
  31 + * 3. 设备 ACL 权限接口 - 对应 EMQX HTTP ACL 插件 {@link #handleAcl(RoutingContext)}
  32 + * 4. 设备注册接口 - 集成一型一密设备注册 {@link #handleDeviceRegister(RoutingContext, String, String)}
23 33 *
24 34 * @author 芋道源码
25 35 */
... ... @@ -45,30 +55,43 @@ public class IotEmqxAuthEventHandler {
45 55 private static final String RESULT_IGNORE = "ignore";
46 56  
47 57 /**
48   - * EMQX 事件类型常量
  58 + * EMQX 事件类型常量 - 客户端连接
49 59 */
50 60 private static final String EVENT_CLIENT_CONNECTED = "client.connected";
  61 + /**
  62 + * EMQX 事件类型常量 - 客户端断开连接
  63 + */
51 64 private static final String EVENT_CLIENT_DISCONNECTED = "client.disconnected";
52 65  
  66 + /**
  67 + * 认证类型标识 - 设备注册
  68 + */
  69 + private static final String AUTH_TYPE_REGISTER = "|authType=register|";
  70 +
53 71 private final String serverId;
54 72  
55   - private final IotDeviceMessageService deviceMessageService;
  73 + private final IotEmqxProtocol protocol;
56 74  
  75 + private final IotDeviceMessageService deviceMessageService;
57 76 private final IotDeviceCommonApi deviceApi;
58 77  
59   - public IotEmqxAuthEventHandler(String serverId) {
  78 + public IotEmqxAuthEventHandler(String serverId, IotEmqxProtocol protocol) {
60 79 this.serverId = serverId;
  80 + this.protocol = protocol;
61 81 this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
62 82 this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
63 83 }
64 84  
  85 + // ========== 认证处理 ==========
  86 +
65 87 /**
66 88 * EMQX 认证接口
67 89 */
68 90 public void handleAuth(RoutingContext context) {
  91 + JsonObject body = null;
69 92 try {
70 93 // 1. 参数校验
71   - JsonObject body = parseRequestBody(context);
  94 + body = parseRequestBody(context);
72 95 if (body == null) {
73 96 return;
74 97 }
... ... @@ -82,7 +105,13 @@ public class IotEmqxAuthEventHandler {
82 105 return;
83 106 }
84 107  
85   - // 2. 执行认证
  108 + // 2.1 情况一:判断是否为注册请求
  109 + if (StrUtil.endWith(clientId, AUTH_TYPE_REGISTER)) {
  110 + handleDeviceRegister(context, username, password);
  111 + return;
  112 + }
  113 +
  114 + // 2.2 情况二:执行认证
86 115 boolean authResult = handleDeviceAuth(clientId, username, password);
87 116 log.info("[handleAuth][设备认证结果: {} -> {}]", username, authResult);
88 117 if (authResult) {
... ... @@ -91,12 +120,180 @@ public class IotEmqxAuthEventHandler {
91 120 sendAuthResponse(context, RESULT_DENY);
92 121 }
93 122 } catch (Exception e) {
94   - log.error("[handleAuth][设备认证异常]", e);
  123 + log.error("[handleAuth][设备认证异常][body={}]", body, e);
95 124 sendAuthResponse(context, RESULT_IGNORE);
96 125 }
97 126 }
98 127  
99 128 /**
  129 + * 解析认证接口请求体
  130 + * <p>
  131 + * 认证接口解析失败时返回 JSON 格式响应(包含 result 字段)
  132 + *
  133 + * @param context 路由上下文
  134 + * @return 请求体JSON对象,解析失败时返回null
  135 + */
  136 + private JsonObject parseRequestBody(RoutingContext context) {
  137 + try {
  138 + JsonObject body = context.body().asJsonObject();
  139 + if (body == null) {
  140 + log.info("[parseRequestBody][请求体为空]");
  141 + sendAuthResponse(context, RESULT_IGNORE);
  142 + return null;
  143 + }
  144 + return body;
  145 + } catch (Exception e) {
  146 + log.error("[parseRequestBody][body({}) 解析请求体失败]", context.body().asString(), e);
  147 + sendAuthResponse(context, RESULT_IGNORE);
  148 + return null;
  149 + }
  150 + }
  151 +
  152 + /**
  153 + * 执行设备认证
  154 + *
  155 + * @param clientId 客户端ID
  156 + * @param username 用户名
  157 + * @param password 密码
  158 + * @return 认证是否成功
  159 + */
  160 + private boolean handleDeviceAuth(String clientId, String username, String password) {
  161 + try {
  162 + CommonResult<Boolean> result = deviceApi.authDevice(new IotDeviceAuthReqDTO()
  163 + .setClientId(clientId).setUsername(username).setPassword(password));
  164 + result.checkError();
  165 + return BooleanUtil.isTrue(result.getData());
  166 + } catch (Exception e) {
  167 + log.error("[handleDeviceAuth][设备({}) 认证接口调用失败]", username, e);
  168 + throw e;
  169 + }
  170 + }
  171 +
  172 + /**
  173 + * 发送 EMQX 认证响应
  174 + * 根据 EMQX 官方文档要求,必须返回 JSON 格式响应
  175 + *
  176 + * @param context 路由上下文
  177 + * @param result 认证结果:allow、deny、ignore
  178 + */
  179 + private void sendAuthResponse(RoutingContext context, String result) {
  180 + // 构建符合 EMQX 官方规范的响应
  181 + JsonObject response = new JsonObject()
  182 + .put("result", result)
  183 + .put("is_superuser", false);
  184 + // 可以根据业务需求添加客户端属性
  185 + // response.put("client_attrs", new JsonObject().put("role", "device"));
  186 + // 可以添加认证过期时间(可选)
  187 + // response.put("expire_at", System.currentTimeMillis() / 1000 + 3600);
  188 +
  189 + // 回复响应
  190 + context.response()
  191 + .setStatusCode(SUCCESS_STATUS_CODE)
  192 + .putHeader("Content-Type", "application/json; charset=utf-8")
  193 + .end(response.encode());
  194 + }
  195 +
  196 + // ========== ACL 处理 ==========
  197 +
  198 + /**
  199 + * EMQX ACL 接口
  200 + * <p>
  201 + * 用于 EMQX 的 HTTP ACL 插件校验设备的 publish/subscribe 权限。
  202 + * 若请求参数无法识别,则返回 ignore 交给 EMQX 自身 ACL 规则处理。
  203 + */
  204 + public void handleAcl(RoutingContext context) {
  205 + JsonObject body = null;
  206 + try {
  207 + // 1.1 解析请求体
  208 + body = parseRequestBody(context);
  209 + if (body == null) {
  210 + return;
  211 + }
  212 + String username = body.getString("username");
  213 + String topic = body.getString("topic");
  214 + if (StrUtil.hasBlank(username, topic)) {
  215 + log.info("[handleAcl][ACL 参数不完整: username={}, topic={}]", username, topic);
  216 + sendAuthResponse(context, RESULT_IGNORE);
  217 + return;
  218 + }
  219 + // 1.2 解析设备身份
  220 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
  221 + if (deviceInfo == null) {
  222 + sendAuthResponse(context, RESULT_IGNORE);
  223 + return;
  224 + }
  225 + // 1.3 解析 ACL 动作(兼容多种 EMQX 版本/插件字段)
  226 + Boolean subscribe = parseAclSubscribeFlag(body);
  227 + if (subscribe == null) {
  228 + sendAuthResponse(context, RESULT_IGNORE);
  229 + return;
  230 + }
  231 +
  232 + // 2. 执行 ACL 校验
  233 + boolean allowed = subscribe
  234 + ? IotMqttTopicUtils.isTopicSubscribeAllowed(topic, deviceInfo.getProductKey(), deviceInfo.getDeviceName())
  235 + : IotMqttTopicUtils.isTopicPublishAllowed(topic, deviceInfo.getProductKey(), deviceInfo.getDeviceName());
  236 + sendAuthResponse(context, allowed ? RESULT_ALLOW : RESULT_DENY);
  237 + } catch (Exception e) {
  238 + log.error("[handleAcl][ACL 处理失败][body={}]", body, e);
  239 + sendAuthResponse(context, RESULT_IGNORE);
  240 + }
  241 + }
  242 +
  243 + /**
  244 + * 解析 ACL 动作类型:订阅/发布
  245 + *
  246 + * @param body ACL 请求体
  247 + * @return true 订阅;false 发布;null 不识别
  248 + */
  249 + private static Boolean parseAclSubscribeFlag(JsonObject body) {
  250 + // 1. action 字段(常见为 publish/subscribe)
  251 + String action = body.getString("action");
  252 + if (StrUtil.isNotBlank(action)) {
  253 + String lower = action.toLowerCase(Locale.ROOT);
  254 + if (lower.contains("sub")) {
  255 + return true;
  256 + }
  257 + if (lower.contains("pub")) {
  258 + return false;
  259 + }
  260 + }
  261 +
  262 + // 2. access 字段:可能是数字或字符串
  263 + Integer access = body.getInteger("access");
  264 + if (access != null) {
  265 + if (access == 1) {
  266 + return true;
  267 + }
  268 + if (access == 2) {
  269 + return false;
  270 + }
  271 + }
  272 + String accessText = body.getString("access");
  273 + if (StrUtil.isNotBlank(accessText)) {
  274 + String lower = accessText.toLowerCase(Locale.ROOT);
  275 + if (lower.contains("sub")) {
  276 + return true;
  277 + }
  278 + if (lower.contains("pub")) {
  279 + return false;
  280 + }
  281 + if (StrUtil.isNumeric(accessText)) {
  282 + int value = Integer.parseInt(accessText);
  283 + if (value == 1) {
  284 + return true;
  285 + }
  286 + if (value == 2) {
  287 + return false;
  288 + }
  289 + }
  290 + }
  291 + return null;
  292 + }
  293 +
  294 + // ========== 事件处理 ==========
  295 +
  296 + /**
100 297 * EMQX 统一事件处理接口:根据 EMQX 官方 Webhook 设计,统一处理所有客户端事件
101 298 * 支持的事件类型:client.connected、client.disconnected 等
102 299 */
... ... @@ -124,59 +321,16 @@ public class IotEmqxAuthEventHandler {
124 321 break;
125 322 }
126 323  
127   - // EMQX Webhook 只需要 200 状态码,无需响应体
  324 + // 3. EMQX Webhook 只需要 200 状态码,无需响应体
128 325 context.response().setStatusCode(SUCCESS_STATUS_CODE).end();
129 326 } catch (Exception e) {
130   - log.error("[handleEvent][事件处理失败][body={}]", body != null ? body.encode() : "null", e);
131   - // 即使处理失败,也返回 200 避免EMQX重试
  327 + log.error("[handleEvent][事件处理失败][body={}]", body, e);
  328 + // 即使处理失败,也返回 200 避免 EMQX 重试
132 329 context.response().setStatusCode(SUCCESS_STATUS_CODE).end();
133 330 }
134 331 }
135 332  
136 333 /**
137   - * 处理客户端连接事件
138   - */
139   - private void handleClientConnected(JsonObject body) {
140   - String username = body.getString("username");
141   - log.info("[handleClientConnected][设备上线: {}]", username);
142   - handleDeviceStateChange(username, true);
143   - }
144   -
145   - /**
146   - * 处理客户端断开连接事件
147   - */
148   - private void handleClientDisconnected(JsonObject body) {
149   - String username = body.getString("username");
150   - String reason = body.getString("reason");
151   - log.info("[handleClientDisconnected][设备下线: {} ({})]", username, reason);
152   - handleDeviceStateChange(username, false);
153   - }
154   -
155   - /**
156   - * 解析认证接口请求体
157   - * <p>
158   - * 认证接口解析失败时返回 JSON 格式响应(包含 result 字段)
159   - *
160   - * @param context 路由上下文
161   - * @return 请求体JSON对象,解析失败时返回null
162   - */
163   - private JsonObject parseRequestBody(RoutingContext context) {
164   - try {
165   - JsonObject body = context.body().asJsonObject();
166   - if (body == null) {
167   - log.info("[parseRequestBody][请求体为空]");
168   - sendAuthResponse(context, RESULT_IGNORE);
169   - return null;
170   - }
171   - return body;
172   - } catch (Exception e) {
173   - log.error("[parseRequestBody][body({}) 解析请求体失败]", context.body().asString(), e);
174   - sendAuthResponse(context, RESULT_IGNORE);
175   - return null;
176   - }
177   - }
178   -
179   - /**
180 334 * 解析事件接口请求体
181 335 * <p>
182 336 * 事件接口解析失败时仅返回 200 状态码,无响应体(符合 EMQX Webhook 规范)
... ... @@ -201,23 +355,22 @@ public class IotEmqxAuthEventHandler {
201 355 }
202 356  
203 357 /**
204   - * 执行设备认证
205   - *
206   - * @param clientId 客户端ID
207   - * @param username 用户名
208   - * @param password 密码
209   - * @return 认证是否成功
  358 + * 处理客户端连接事件
210 359 */
211   - private boolean handleDeviceAuth(String clientId, String username, String password) {
212   - try {
213   - CommonResult<Boolean> result = deviceApi.authDevice(new IotDeviceAuthReqDTO()
214   - .setClientId(clientId).setUsername(username).setPassword(password));
215   - result.checkError();
216   - return BooleanUtil.isTrue(result.getData());
217   - } catch (Exception e) {
218   - log.error("[handleDeviceAuth][设备({}) 认证接口调用失败]", username, e);
219   - throw e;
220   - }
  360 + private void handleClientConnected(JsonObject body) {
  361 + String username = body.getString("username");
  362 + log.info("[handleClientConnected][设备上线: {}]", username);
  363 + handleDeviceStateChange(username, true);
  364 + }
  365 +
  366 + /**
  367 + * 处理客户端断开连接事件
  368 + */
  369 + private void handleClientDisconnected(JsonObject body) {
  370 + String username = body.getString("username");
  371 + String reason = body.getString("reason");
  372 + log.info("[handleClientDisconnected][设备下线: {} ({})]", username, reason);
  373 + handleDeviceStateChange(username, false);
221 374 }
222 375  
223 376 /**
... ... @@ -247,29 +400,74 @@ public class IotEmqxAuthEventHandler {
247 400 }
248 401 }
249 402  
  403 + // ========= 注册处理 =========
  404 +
250 405 /**
251   - * 发送 EMQX 认证响应
252   - * 根据 EMQX 官方文档要求,必须返回 JSON 格式响应
  406 + * 处理设备注册请求(一型一密)
253 407 *
254   - * @param context 路由上下文
255   - * @param result 认证结果:allow、deny、ignore
  408 + * @param context 路由上下文
  409 + * @param username 用户名
  410 + * @param password 密码(签名)
256 411 */
257   - private void sendAuthResponse(RoutingContext context, String result) {
258   - // 构建符合 EMQX 官方规范的响应
259   - JsonObject response = new JsonObject()
260   - .put("result", result)
261   - .put("is_superuser", false);
  412 + private void handleDeviceRegister(RoutingContext context, String username, String password) {
  413 + try {
  414 + // 1. 解析设备信息
  415 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
  416 + if (deviceInfo == null) {
  417 + log.warn("[handleDeviceRegister][设备注册失败: 无法解析 username={}]", username);
  418 + sendAuthResponse(context, RESULT_DENY);
  419 + return;
  420 + }
262 421  
263   - // 可以根据业务需求添加客户端属性
264   - // response.put("client_attrs", new JsonObject().put("role", "device"));
  422 + // 2. 调用注册 API
  423 + IotDeviceRegisterReqDTO params = new IotDeviceRegisterReqDTO()
  424 + .setProductKey(deviceInfo.getProductKey())
  425 + .setDeviceName(deviceInfo.getDeviceName())
  426 + .setSign(password);
  427 + CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);
  428 + result.checkError();
265 429  
266   - // 可以添加认证过期时间(可选)
267   - // response.put("expire_at", System.currentTimeMillis() / 1000 + 3600);
  430 + // 3. 允许连接
  431 + log.info("[handleDeviceRegister][设备注册成功: {}]", username);
  432 + sendAuthResponse(context, RESULT_ALLOW);
268 433  
269   - context.response()
270   - .setStatusCode(SUCCESS_STATUS_CODE)
271   - .putHeader("Content-Type", "application/json; charset=utf-8")
272   - .end(response.encode());
  434 + // 4. 延迟 5 秒发送注册结果(等待设备连接成功并完成订阅)
  435 + sendRegisterResultMessage(username, result.getData());
  436 + } catch (Exception e) {
  437 + log.warn("[handleDeviceRegister][设备注册失败: {}, 错误: {}]", username, e.getMessage());
  438 + sendAuthResponse(context, RESULT_DENY);
  439 + }
  440 + }
  441 +
  442 + /**
  443 + * 发送注册结果消息给设备
  444 + * <p>
  445 + * 注意:延迟 5 秒发送,等待设备连接成功并完成订阅。
  446 + *
  447 + * @param username 用户名
  448 + * @param result 注册结果
  449 + */
  450 + @SuppressWarnings("DataFlowIssue")
  451 + private void sendRegisterResultMessage(String username, IotDeviceRegisterRespDTO result) {
  452 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
  453 + Assert.notNull(deviceInfo, "设备信息不能为空");
  454 + try {
  455 + // 1.1 构建响应消息
  456 + String method = IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod();
  457 + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(null, method, result, 0, null);
  458 + // 1.2 序列化消息
  459 + byte[] encodedData = deviceMessageService.serializeDeviceMessage(responseMessage,
  460 + cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum.JSON);
  461 + // 1.3 构建响应主题
  462 + String replyTopic = IotMqttTopicUtils.buildTopicByMethod(method,
  463 + deviceInfo.getProductKey(), deviceInfo.getDeviceName(), true);
  464 +
  465 + // 2. 构建响应主题,并延迟发布(等待设备连接成功并完成订阅)
  466 + protocol.publishDelayMessage(replyTopic, encodedData, 5000);
  467 + log.info("[sendRegisterResultMessage][发送注册结果: topic={}]", replyTopic);
  468 + } catch (Exception e) {
  469 + log.error("[sendRegisterResultMessage][发送注册结果失败: {}]", username, e);
  470 + }
273 471 }
274 472  
275   -}
276 473 \ No newline at end of file
  474 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxUpstreamHandler.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxUpstreamHandler.java
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router;
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream;
2 2  
  3 +import cn.hutool.core.util.ArrayUtil;
3 4 import cn.hutool.core.util.StrUtil;
4 5 import cn.hutool.extra.spring.SpringUtil;
5 6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
6   -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol;
7 7 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
8 8 import io.vertx.mqtt.messages.MqttPublishMessage;
9 9 import lombok.extern.slf4j.Slf4j;
... ... @@ -20,41 +20,40 @@ public class IotEmqxUpstreamHandler {
20 20  
21 21 private final String serverId;
22 22  
23   - public IotEmqxUpstreamHandler(IotEmqxUpstreamProtocol protocol) {
  23 + public IotEmqxUpstreamHandler(String serverId) {
24 24 this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
25   - this.serverId = protocol.getServerId();
  25 + this.serverId = serverId;
26 26 }
27 27  
28 28 /**
29 29 * 处理 MQTT 发布消息
30 30 */
31 31 public void handle(MqttPublishMessage mqttMessage) {
32   - log.info("[handle][收到 MQTT 消息, topic: {}, payload: {}]", mqttMessage.topicName(), mqttMessage.payload());
  32 + log.debug("[handle][收到 MQTT 消息, topic: {}, payload: {}]", mqttMessage.topicName(), mqttMessage.payload());
33 33 String topic = mqttMessage.topicName();
34 34 byte[] payload = mqttMessage.payload().getBytes();
35 35 try {
36 36 // 1. 解析主题,一次性获取所有信息
37 37 String[] topicParts = topic.split("/");
38   - if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) {
  38 + String productKey = ArrayUtil.get(topicParts, 2);
  39 + String deviceName = ArrayUtil.get(topicParts, 3);
  40 + if (topicParts.length < 4 || StrUtil.hasBlank(productKey, deviceName)) {
39 41 log.warn("[handle][topic({}) 格式不正确,无法解析有效的 productKey 和 deviceName]", topic);
40 42 return;
41 43 }
42 44  
43   - String productKey = topicParts[2];
44   - String deviceName = topicParts[3];
45   -
46   - // 3. 解码消息
47   - IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName);
  45 + // 2. 反序列化消息
  46 + IotDeviceMessage message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName);
48 47 if (message == null) {
49 48 log.warn("[handle][topic({}) payload({}) 消息解码失败]", topic, new String(payload));
50 49 return;
51 50 }
52 51  
53   - // 4. 发送消息到队列
  52 + // 3. 发送消息到队列
54 53 deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId);
55 54 } catch (Exception e) {
56 55 log.error("[handle][topic({}) payload({}) 处理异常]", topic, new String(payload), e);
57 56 }
58 57 }
59 58  
60   -}
61 59 \ No newline at end of file
  60 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpConfig.java
... ... @@ -10,19 +10,4 @@ import lombok.Data;
10 10 @Data
11 11 public class IotHttpConfig {
12 12  
13   - /**
14   - * 是否启用 SSL
15   - */
16   - private Boolean sslEnabled = false;
17   -
18   - /**
19   - * SSL 证书路径
20   - */
21   - private String sslCertPath;
22   -
23   - /**
24   - * SSL 私钥路径
25   - */
26   - private String sslKeyPath;
27   -
28 13 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpProtocol.java
... ... @@ -4,7 +4,8 @@ import cn.hutool.extra.spring.SpringUtil;
4 4 import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
5 5 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
6 6 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
7   -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties;
  7 +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
  8 +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties;
8 9 import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
9 10 import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.downstream.IotHttpDownstreamSubscriber;
10 11 import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpAuthHandler;
... ... @@ -33,7 +34,7 @@ public class IotHttpProtocol implements IotProtocol {
33 34 /**
34 35 * 协议配置
35 36 */
36   - private final ProtocolInstanceProperties properties;
  37 + private final ProtocolProperties properties;
37 38 /**
38 39 * 服务器 ID(用于消息追踪,全局唯一)
39 40 */
... ... @@ -60,7 +61,7 @@ public class IotHttpProtocol implements IotProtocol {
60 61 */
61 62 private IotHttpDownstreamSubscriber downstreamSubscriber;
62 63  
63   - public IotHttpProtocol(ProtocolInstanceProperties properties) {
  64 + public IotHttpProtocol(ProtocolProperties properties) {
64 65 this.properties = properties;
65 66 this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
66 67  
... ... @@ -104,12 +105,12 @@ public class IotHttpProtocol implements IotProtocol {
104 105 router.post(IotHttpUpstreamHandler.PATH).handler(upstreamHandler);
105 106  
106 107 // 1.4 启动 HTTP 服务器
107   - IotHttpConfig httpConfig = properties.getHttp();
108 108 HttpServerOptions options = new HttpServerOptions().setPort(properties.getPort());
109   - if (httpConfig != null && Boolean.TRUE.equals(httpConfig.getSslEnabled())) {
  109 + IotGatewayProperties.SslConfig sslConfig = properties.getSsl();
  110 + if (sslConfig != null && Boolean.TRUE.equals(sslConfig.getSsl())) {
110 111 PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions()
111   - .setKeyPath(httpConfig.getSslKeyPath())
112   - .setCertPath(httpConfig.getSslCertPath());
  112 + .setKeyPath(sslConfig.getSslKeyPath())
  113 + .setCertPath(sslConfig.getSslCertPath());
113 114 options = options.setSsl(true).setKeyCertOptions(pemKeyCertOptions);
114 115 }
115 116 try {
... ... @@ -125,7 +126,11 @@ public class IotHttpProtocol implements IotProtocol {
125 126 this.downstreamSubscriber.start();
126 127 } catch (Exception e) {
127 128 log.error("[start][IoT HTTP 协议 {} 启动失败]", getId(), e);
128   - // 启动失败时关闭 Vertx
  129 + // 启动失败时关闭资源
  130 + if (httpServer != null) {
  131 + httpServer.close();
  132 + httpServer = null;
  133 + }
129 134 if (vertx != null) {
130 135 vertx.close();
131 136 vertx = null;
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpRegisterHandler.java
... ... @@ -35,7 +35,7 @@ public class IotHttpRegisterHandler extends IotHttpAbstractHandler {
35 35 Assert.notNull(request, "请求参数不能为空");
36 36 Assert.notBlank(request.getProductKey(), "productKey 不能为空");
37 37 Assert.notBlank(request.getDeviceName(), "deviceName 不能为空");
38   - Assert.notBlank(request.getProductSecret(), "productSecret 不能为空");
  38 + Assert.notBlank(request.getSign(), "sign 不能为空");
39 39  
40 40 // 2. 调用动态注册
41 41 CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(request);
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttConfig.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;
  2 +
  3 +import jakarta.validation.constraints.Min;
  4 +import jakarta.validation.constraints.NotNull;
  5 +import lombok.Data;
  6 +
  7 +/**
  8 + * IoT 网关 MQTT 协议配置
  9 + *
  10 + * @author 芋道源码
  11 + */
  12 +@Data
  13 +public class IotMqttConfig {
  14 +
  15 + /**
  16 + * 最大消息大小(字节)
  17 + */
  18 + @NotNull(message = "最大消息大小不能为空")
  19 + @Min(value = 1024, message = "最大消息大小不能小于 1024 字节")
  20 + private Integer maxMessageSize = 8192;
  21 +
  22 + /**
  23 + * 连接超时时间(秒)
  24 + */
  25 + @NotNull(message = "连接超时时间不能为空")
  26 + @Min(value = 1, message = "连接超时时间不能小于 1 秒")
  27 + private Integer connectTimeoutSeconds = 60;
  28 +
  29 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttProtocol.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;
  2 +
  3 +import cn.hutool.core.lang.Assert;
  4 +import cn.hutool.core.util.StrUtil;
  5 +import cn.hutool.extra.spring.SpringUtil;
  6 +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
  7 +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
  8 +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
  9 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  10 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
  11 +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
  12 +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties;
  13 +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
  14 +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream.IotMqttDownstreamHandler;
  15 +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream.IotMqttDownstreamSubscriber;
  16 +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttAuthHandler;
  17 +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttRegisterHandler;
  18 +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttUpstreamHandler;
  19 +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
  20 +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
  21 +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
  22 +import io.netty.handler.codec.mqtt.MqttConnectReturnCode;
  23 +import io.netty.handler.codec.mqtt.MqttQoS;
  24 +import io.vertx.core.Vertx;
  25 +import io.vertx.core.net.PemKeyCertOptions;
  26 +import io.vertx.mqtt.MqttEndpoint;
  27 +import io.vertx.mqtt.MqttServer;
  28 +import io.vertx.mqtt.MqttServerOptions;
  29 +import io.vertx.mqtt.MqttTopicSubscription;
  30 +import io.vertx.mqtt.messages.MqttPublishMessage;
  31 +import lombok.Getter;
  32 +import lombok.extern.slf4j.Slf4j;
  33 +
  34 +import java.util.ArrayList;
  35 +import java.util.List;
  36 +
  37 +/**
  38 + * IoT 网关 MQTT 协议:接收设备上行消息
  39 + *
  40 + * @author 芋道源码
  41 + */
  42 +@Slf4j
  43 +public class IotMqttProtocol implements IotProtocol {
  44 +
  45 + /**
  46 + * 注册连接的 clientId 标识
  47 + *
  48 + * @see #handleEndpoint(MqttEndpoint)
  49 + */
  50 + private static final String AUTH_TYPE_REGISTER = "|authType=register|";
  51 +
  52 + /**
  53 + * 协议配置
  54 + */
  55 + private final ProtocolProperties properties;
  56 + /**
  57 + * 服务器 ID(用于消息追踪,全局唯一)
  58 + */
  59 + @Getter
  60 + private final String serverId;
  61 +
  62 + /**
  63 + * 运行状态
  64 + */
  65 + @Getter
  66 + private volatile boolean running = false;
  67 +
  68 + /**
  69 + * Vert.x 实例
  70 + */
  71 + private Vertx vertx;
  72 + /**
  73 + * MQTT 服务器
  74 + */
  75 + private MqttServer mqttServer;
  76 +
  77 + /**
  78 + * 连接管理器
  79 + */
  80 + private final IotMqttConnectionManager connectionManager;
  81 + /**
  82 + * 下行消息订阅者
  83 + */
  84 + private final IotMqttDownstreamSubscriber downstreamSubscriber;
  85 +
  86 + private final IotDeviceMessageService deviceMessageService;
  87 +
  88 + private final IotMqttAuthHandler authHandler;
  89 + private final IotMqttRegisterHandler registerHandler;
  90 + private final IotMqttUpstreamHandler upstreamHandler;
  91 +
  92 + public IotMqttProtocol(ProtocolProperties properties) {
  93 + IotMqttConfig mqttConfig = properties.getMqtt();
  94 + Assert.notNull(mqttConfig, "MQTT 协议配置(mqtt)不能为空");
  95 + this.properties = properties;
  96 + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
  97 +
  98 + // 初始化连接管理器
  99 + this.connectionManager = new IotMqttConnectionManager();
  100 +
  101 + // 初始化 Handler
  102 + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
  103 + IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
  104 + this.authHandler = new IotMqttAuthHandler(connectionManager, deviceMessageService, deviceApi, serverId);
  105 + this.registerHandler = new IotMqttRegisterHandler(connectionManager, deviceMessageService);
  106 + this.upstreamHandler = new IotMqttUpstreamHandler(connectionManager, deviceMessageService, serverId);
  107 +
  108 + // 初始化下行消息订阅者
  109 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
  110 + IotMqttDownstreamHandler downstreamHandler = new IotMqttDownstreamHandler(deviceMessageService, connectionManager);
  111 + this.downstreamSubscriber = new IotMqttDownstreamSubscriber(this, downstreamHandler, messageBus);
  112 + }
  113 +
  114 + @Override
  115 + public String getId() {
  116 + return properties.getId();
  117 + }
  118 +
  119 + @Override
  120 + public IotProtocolTypeEnum getType() {
  121 + return IotProtocolTypeEnum.MQTT;
  122 + }
  123 +
  124 + @Override
  125 + public void start() {
  126 + if (running) {
  127 + log.warn("[start][IoT MQTT 协议 {} 已经在运行中]", getId());
  128 + return;
  129 + }
  130 +
  131 + // 1.1 创建 Vertx 实例
  132 + this.vertx = Vertx.vertx();
  133 +
  134 + // 1.2 创建服务器选项
  135 + IotMqttConfig mqttConfig = properties.getMqtt();
  136 + MqttServerOptions options = new MqttServerOptions()
  137 + .setPort(properties.getPort())
  138 + .setMaxMessageSize(mqttConfig.getMaxMessageSize())
  139 + .setTimeoutOnConnect(mqttConfig.getConnectTimeoutSeconds());
  140 + IotGatewayProperties.SslConfig sslConfig = properties.getSsl();
  141 + if (sslConfig != null && Boolean.TRUE.equals(sslConfig.getSsl())) {
  142 + PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions()
  143 + .setKeyPath(sslConfig.getSslKeyPath())
  144 + .setCertPath(sslConfig.getSslCertPath());
  145 + options.setSsl(true).setKeyCertOptions(pemKeyCertOptions);
  146 + }
  147 +
  148 + // 1.3 创建服务器并设置连接处理器
  149 + mqttServer = MqttServer.create(vertx, options);
  150 + mqttServer.endpointHandler(this::handleEndpoint);
  151 +
  152 + // 1.4 启动 MQTT 服务器
  153 + try {
  154 + mqttServer.listen().result();
  155 + running = true;
  156 + log.info("[start][IoT MQTT 协议 {} 启动成功,端口:{},serverId:{}]",
  157 + getId(), properties.getPort(), serverId);
  158 +
  159 + // 2. 启动下行消息订阅者
  160 + this.downstreamSubscriber.start();
  161 + } catch (Exception e) {
  162 + log.error("[start][IoT MQTT 协议 {} 启动失败]", getId(), e);
  163 + // 启动失败时关闭资源
  164 + if (mqttServer != null) {
  165 + mqttServer.close();
  166 + mqttServer = null;
  167 + }
  168 + if (vertx != null) {
  169 + vertx.close();
  170 + vertx = null;
  171 + }
  172 + throw e;
  173 + }
  174 + }
  175 +
  176 + @Override
  177 + public void stop() {
  178 + if (!running) {
  179 + return;
  180 + }
  181 + // 1. 停止下行消息订阅者
  182 + try {
  183 + downstreamSubscriber.stop();
  184 + log.info("[stop][IoT MQTT 协议 {} 下行消息订阅者已停止]", getId());
  185 + } catch (Exception e) {
  186 + log.error("[stop][IoT MQTT 协议 {} 下行消息订阅者停止失败]", getId(), e);
  187 + }
  188 +
  189 + // 2.1 关闭 MQTT 服务器
  190 + if (mqttServer != null) {
  191 + try {
  192 + mqttServer.close().result();
  193 + log.info("[stop][IoT MQTT 协议 {} 服务器已停止]", getId());
  194 + } catch (Exception e) {
  195 + log.error("[stop][IoT MQTT 协议 {} 服务器停止失败]", getId(), e);
  196 + }
  197 + mqttServer = null;
  198 + }
  199 + // 2.2 关闭 Vertx 实例
  200 + if (vertx != null) {
  201 + try {
  202 + vertx.close().result();
  203 + log.info("[stop][IoT MQTT 协议 {} Vertx 已关闭]", getId());
  204 + } catch (Exception e) {
  205 + log.error("[stop][IoT MQTT 协议 {} Vertx 关闭失败]", getId(), e);
  206 + }
  207 + vertx = null;
  208 + }
  209 + running = false;
  210 + log.info("[stop][IoT MQTT 协议 {} 已停止]", getId());
  211 + }
  212 +
  213 + // ======================================= MQTT 连接处理 ======================================
  214 +
  215 + /**
  216 + * 处理 MQTT 连接端点
  217 + *
  218 + * @param endpoint MQTT 连接端点
  219 + */
  220 + private void handleEndpoint(MqttEndpoint endpoint) {
  221 + // 1. 如果是注册请求,注册待认证连接;否则走正常认证流程
  222 + String clientId = endpoint.clientIdentifier();
  223 + if (StrUtil.endWith(clientId, AUTH_TYPE_REGISTER)) {
  224 + // 情况一:设备注册请求
  225 + registerHandler.handleRegister(endpoint);
  226 + return;
  227 + } else {
  228 + // 情况二:普通认证请求
  229 + if (!authHandler.handleAuthenticationRequest(endpoint)) {
  230 + endpoint.reject(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD);
  231 + return;
  232 + }
  233 + }
  234 +
  235 + // 2.1 设置异常和关闭处理器
  236 + endpoint.exceptionHandler(ex -> {
  237 + log.warn("[handleEndpoint][连接异常,客户端 ID: {},地址: {},异常: {}]",
  238 + clientId, connectionManager.getEndpointAddress(endpoint), ex.getMessage());
  239 + endpoint.close();
  240 + });
  241 + endpoint.closeHandler(v -> cleanupConnection(endpoint)); // 处理底层连接关闭(网络中断、异常等)
  242 + endpoint.disconnectHandler(v -> { // 处理 MQTT DISCONNECT 报文
  243 + log.debug("[handleEndpoint][设备断开连接,客户端 ID: {}]", clientId);
  244 + cleanupConnection(endpoint);
  245 + });
  246 + // 2.2 设置心跳处理器
  247 + endpoint.pingHandler(v -> log.debug("[handleEndpoint][收到客户端心跳,客户端 ID: {}]", clientId));
  248 +
  249 + // 3.1 设置消息处理器
  250 + endpoint.publishHandler(message -> processMessage(endpoint, message));
  251 + // 3.2 设置 QoS 2 消息的 PUBREL 处理器
  252 + endpoint.publishReleaseHandler(endpoint::publishComplete);
  253 +
  254 + // 4.1 设置订阅处理器(带 ACL 校验)
  255 + endpoint.subscribeHandler(subscribe -> {
  256 + IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint);
  257 + List<MqttQoS> grantedQoSLevels = new ArrayList<>();
  258 + for (MqttTopicSubscription sub : subscribe.topicSubscriptions()) {
  259 + String topicName = sub.topicName();
  260 + // 校验主题是否属于当前设备
  261 + if (connectionInfo != null && IotMqttTopicUtils.isTopicSubscribeAllowed(
  262 + topicName, connectionInfo.getProductKey(), connectionInfo.getDeviceName())) {
  263 + grantedQoSLevels.add(sub.qualityOfService());
  264 + log.debug("[handleEndpoint][订阅成功,客户端 ID: {},主题: {}]", clientId, topicName);
  265 + } else {
  266 + log.warn("[handleEndpoint][订阅被拒绝,客户端 ID: {},主题: {}]", clientId, topicName);
  267 + grantedQoSLevels.add(MqttQoS.FAILURE);
  268 + }
  269 + }
  270 + endpoint.subscribeAcknowledge(subscribe.messageId(), grantedQoSLevels);
  271 + });
  272 + // 4.2 设置取消订阅处理器
  273 + endpoint.unsubscribeHandler(unsubscribe -> {
  274 + log.debug("[handleEndpoint][设备取消订阅,客户端 ID: {},主题: {}]", clientId, unsubscribe.topics());
  275 + endpoint.unsubscribeAcknowledge(unsubscribe.messageId());
  276 + });
  277 +
  278 + // 5. 接受连接
  279 + endpoint.accept(false);
  280 + }
  281 +
  282 + /**
  283 + * 处理消息(发布)
  284 + *
  285 + * @param endpoint MQTT 连接端点
  286 + * @param message 发布消息
  287 + */
  288 + private void processMessage(MqttEndpoint endpoint, MqttPublishMessage message) {
  289 + String clientId = endpoint.clientIdentifier();
  290 + try {
  291 + // 1. 处理业务消息
  292 + String topic = message.topicName();
  293 + byte[] payload = message.payload().getBytes();
  294 + upstreamHandler.handleBusinessRequest(endpoint, topic, payload);
  295 +
  296 + // 2. 根据 QoS 级别发送相应的确认消息
  297 + handleQoSAck(endpoint, message);
  298 + } catch (Exception e) {
  299 + log.error("[processMessage][消息处理失败,断开连接,客户端 ID: {},地址: {},错误: {}]",
  300 + clientId, connectionManager.getEndpointAddress(endpoint), e.getMessage());
  301 + endpoint.close();
  302 + }
  303 + }
  304 +
  305 + /**
  306 + * 处理 QoS 确认
  307 + *
  308 + * @param endpoint MQTT 连接端点
  309 + * @param message 发布消息
  310 + */
  311 + private void handleQoSAck(MqttEndpoint endpoint, MqttPublishMessage message) {
  312 + if (message.qosLevel() == MqttQoS.AT_LEAST_ONCE) {
  313 + // QoS 1: 发送 PUBACK 确认
  314 + endpoint.publishAcknowledge(message.messageId());
  315 + } else if (message.qosLevel() == MqttQoS.EXACTLY_ONCE) {
  316 + // QoS 2: 发送 PUBREC 确认
  317 + endpoint.publishReceived(message.messageId());
  318 + }
  319 + // QoS 0 无需确认
  320 + }
  321 +
  322 + /**
  323 + * 清理连接
  324 + *
  325 + * @param endpoint MQTT 连接端点
  326 + */
  327 + private void cleanupConnection(MqttEndpoint endpoint) {
  328 + try {
  329 + // 1. 发送设备离线消息
  330 + IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint);
  331 + if (connectionInfo != null) {
  332 + IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline();
  333 + deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(),
  334 + connectionInfo.getDeviceName(), serverId);
  335 + }
  336 +
  337 + // 2. 注销连接
  338 + connectionManager.unregisterConnection(endpoint);
  339 + } catch (Exception e) {
  340 + log.error("[cleanupConnection][清理连接失败,客户端 ID: {},错误: {}]",
  341 + endpoint.clientIdentifier(), e.getMessage());
  342 + }
  343 + }
  344 +
  345 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttUpstreamProtocol.java deleted
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;
2   -
3   -import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
4   -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
5   -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
6   -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
7   -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
8   -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttUpstreamHandler;
9   -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
10   -import io.vertx.core.Vertx;
11   -import io.vertx.mqtt.MqttServer;
12   -import io.vertx.mqtt.MqttServerOptions;
13   -import jakarta.annotation.PostConstruct;
14   -import jakarta.annotation.PreDestroy;
15   -import lombok.Getter;
16   -import lombok.extern.slf4j.Slf4j;
17   -
18   -/**
19   - * IoT 网关 MQTT 协议:接收设备上行消息
20   - *
21   - * @author 芋道源码
22   - */
23   -@Slf4j
24   -public class IotMqttUpstreamProtocol implements IotProtocol {
25   -
26   - private static final String ID = "mqtt";
27   -
28   - private volatile boolean running = false;
29   -
30   - private final IotGatewayProperties.MqttProperties mqttProperties;
31   -
32   - private final IotDeviceMessageService messageService;
33   -
34   - private final IotMqttConnectionManager connectionManager;
35   -
36   - private final Vertx vertx;
37   -
38   - @Getter
39   - private final String serverId;
40   -
41   - private MqttServer mqttServer;
42   -
43   - public IotMqttUpstreamProtocol(IotGatewayProperties.MqttProperties mqttProperties,
44   - IotDeviceMessageService messageService,
45   - IotMqttConnectionManager connectionManager,
46   - Vertx vertx) {
47   - this.mqttProperties = mqttProperties;
48   - this.messageService = messageService;
49   - this.connectionManager = connectionManager;
50   - this.vertx = vertx;
51   - this.serverId = IotDeviceMessageUtils.generateServerId(mqttProperties.getPort());
52   - }
53   -
54   - @Override
55   - public String getId() {
56   - return ID;
57   - }
58   -
59   - @Override
60   - public IotProtocolTypeEnum getType() {
61   - return IotProtocolTypeEnum.MQTT;
62   - }
63   -
64   - @Override
65   - public boolean isRunning() {
66   - return running;
67   - }
68   -
69   - // TODO @haohao:这里的编写,是不是和 tcp 对应的,风格保持一致哈;
70   - @Override
71   - @PostConstruct
72   - public void start() {
73   - // 创建服务器选项
74   - MqttServerOptions options = new MqttServerOptions()
75   - .setPort(mqttProperties.getPort())
76   - .setMaxMessageSize(mqttProperties.getMaxMessageSize())
77   - .setTimeoutOnConnect(mqttProperties.getConnectTimeoutSeconds());
78   -
79   - // 配置 SSL(如果启用)
80   - if (Boolean.TRUE.equals(mqttProperties.getSslEnabled())) {
81   - options.setSsl(true)
82   - .setKeyCertOptions(mqttProperties.getSslOptions().getKeyCertOptions())
83   - .setTrustOptions(mqttProperties.getSslOptions().getTrustOptions());
84   - }
85   -
86   - // 创建服务器并设置连接处理器
87   - mqttServer = MqttServer.create(vertx, options);
88   - mqttServer.endpointHandler(endpoint -> {
89   - IotMqttUpstreamHandler handler = new IotMqttUpstreamHandler(this, messageService, connectionManager);
90   - handler.handle(endpoint);
91   - });
92   -
93   - // 启动服务器
94   - try {
95   - mqttServer.listen().result();
96   - running = true;
97   - log.info("[start][IoT 网关 MQTT 协议启动成功,端口:{}]", mqttProperties.getPort());
98   - } catch (Exception e) {
99   - log.error("[start][IoT 网关 MQTT 协议启动失败]", e);
100   - throw e;
101   - }
102   - }
103   -
104   - @Override
105   - @PreDestroy
106   - public void stop() {
107   - if (mqttServer != null) {
108   - try {
109   - mqttServer.close().result();
110   - running = false;
111   - log.info("[stop][IoT 网关 MQTT 协议已停止]");
112   - } catch (Exception e) {
113   - log.error("[stop][IoT 网关 MQTT 协议停止失败]", e);
114   - }
115   - }
116   - }
117   -}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream;
  2 +
  3 +import cn.hutool.core.lang.Assert;
  4 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  5 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
  6 +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
  7 +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
  8 +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
  9 +import io.netty.handler.codec.mqtt.MqttQoS;
  10 +import lombok.RequiredArgsConstructor;
  11 +import lombok.extern.slf4j.Slf4j;
  12 +
  13 +/**
  14 + * IoT 网关 MQTT 协议:下行消息处理器
  15 + *
  16 + * @author 芋道源码
  17 + */
  18 +@Slf4j
  19 +@RequiredArgsConstructor
  20 +public class IotMqttDownstreamHandler {
  21 +
  22 + private final IotDeviceMessageService deviceMessageService;
  23 +
  24 + private final IotMqttConnectionManager connectionManager;
  25 +
  26 + /**
  27 + * 处理下行消息
  28 + *
  29 + * @param message 设备消息
  30 + */
  31 + public void handle(IotDeviceMessage message) {
  32 + try {
  33 + log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]",
  34 + message.getDeviceId(), message.getMethod(), message.getId());
  35 +
  36 + // 1. 检查设备连接
  37 + IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfoByDeviceId(
  38 + message.getDeviceId());
  39 + if (connectionInfo == null) {
  40 + log.warn("[handle][连接信息不存在,设备 ID: {},方法: {},消息 ID: {}]",
  41 + message.getDeviceId(), message.getMethod(), message.getId());
  42 + return;
  43 + }
  44 +
  45 + // 2.1 序列化消息
  46 + byte[] payload = deviceMessageService.serializeDeviceMessage(message, connectionInfo.getProductKey(),
  47 + connectionInfo.getDeviceName());
  48 + Assert.isTrue(payload != null && payload.length > 0, "消息编码结果不能为空");
  49 + // 2.2 构建主题
  50 + Assert.notBlank(message.getMethod(), "消息方法不能为空");
  51 + boolean isReply = IotDeviceMessageUtils.isReplyMessage(message);
  52 + String topic = IotMqttTopicUtils.buildTopicByMethod(message.getMethod(), connectionInfo.getProductKey(),
  53 + connectionInfo.getDeviceName(), isReply);
  54 + Assert.notBlank(topic, "主题不能为空");
  55 +
  56 + // 3. 发送到设备
  57 + boolean success = connectionManager.sendToDevice(message.getDeviceId(), topic, payload,
  58 + MqttQoS.AT_LEAST_ONCE.value(), false);
  59 + if (!success) {
  60 + throw new RuntimeException("下行消息发送失败");
  61 + }
  62 + log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},主题: {},数据长度: {} 字节]",
  63 + message.getDeviceId(), message.getMethod(), message.getId(), topic, payload.length);
  64 + } catch (Exception e) {
  65 + log.error("[handle][处理下行消息失败,设备 ID: {},方法: {},消息内容: {}]",
  66 + message.getDeviceId(), message.getMethod(), message, e);
  67 + }
  68 + }
  69 +
  70 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttDownstreamSubscriber.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamSubscriber.java
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream;
2 2  
3 3 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
4 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
5 5 import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber;
6   -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler;
  6 +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttProtocol;
7 7 import lombok.extern.slf4j.Slf4j;
8 8  
9 9 /**
10   - * IoT 网关 MQTT 协议:下行消息订阅器
11   - * <p>
12   - * 负责接收来自消息总线的下行消息,并委托给下行处理器进行业务处理
  10 + * IoT 网关 MQTT 协议:接收下行给设备的消息
13 11 *
14 12 * @author 芋道源码
15 13 */
... ... @@ -18,7 +16,7 @@ public class IotMqttDownstreamSubscriber extends IotProtocolDownstreamSubscriber
18 16  
19 17 private final IotMqttDownstreamHandler downstreamHandler;
20 18  
21   - public IotMqttDownstreamSubscriber(IotMqttUpstreamProtocol protocol,
  19 + public IotMqttDownstreamSubscriber(IotMqttProtocol protocol,
22 20 IotMqttDownstreamHandler downstreamHandler,
23 21 IotMessageBus messageBus) {
24 22 super(protocol, messageBus);
... ... @@ -27,14 +25,7 @@ public class IotMqttDownstreamSubscriber extends IotProtocolDownstreamSubscriber
27 25  
28 26 @Override
29 27 protected void handleMessage(IotDeviceMessage message) {
30   - boolean success = downstreamHandler.handleDownstreamMessage(message);
31   - if (success) {
32   - log.debug("[handleMessage][下行消息处理成功, messageId: {}, method: {}, deviceId: {}]",
33   - message.getId(), message.getMethod(), message.getDeviceId());
34   - } else {
35   - log.warn("[handleMessage][下行消息处理失败, messageId: {}, method: {}, deviceId: {}]",
36   - message.getId(), message.getMethod(), message.getDeviceId());
37   - }
  28 + downstreamHandler.handle(message);
38 29 }
39 30  
40 31 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAbstractHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  4 +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
  5 +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
  6 +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
  7 +import io.netty.handler.codec.mqtt.MqttQoS;
  8 +import io.vertx.core.buffer.Buffer;
  9 +import io.vertx.mqtt.MqttEndpoint;
  10 +import lombok.RequiredArgsConstructor;
  11 +import lombok.extern.slf4j.Slf4j;
  12 +
  13 +/**
  14 + * IoT 网关 MQTT 协议的处理器抽象基类
  15 + * <p>
  16 + * 提供通用的连接校验、响应发送等功能
  17 + *
  18 + * @author 芋道源码
  19 + */
  20 +@Slf4j
  21 +@RequiredArgsConstructor
  22 +public abstract class IotMqttAbstractHandler {
  23 +
  24 + protected final IotMqttConnectionManager connectionManager;
  25 + protected final IotDeviceMessageService deviceMessageService;
  26 +
  27 + /**
  28 + * 发送成功响应到设备
  29 + *
  30 + * @param endpoint MQTT 连接端点
  31 + * @param productKey 产品 Key
  32 + * @param deviceName 设备名称
  33 + * @param requestId 请求 ID
  34 + * @param method 方法名
  35 + * @param data 响应数据
  36 + */
  37 + @SuppressWarnings("SameParameterValue")
  38 + protected void sendSuccessResponse(MqttEndpoint endpoint, String productKey, String deviceName,
  39 + String requestId, String method, Object data) {
  40 + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, data, 0, null);
  41 + writeResponse(endpoint, productKey, deviceName, method, responseMessage);
  42 + }
  43 +
  44 + /**
  45 + * 发送错误响应到设备
  46 + *
  47 + * @param endpoint MQTT 连接端点
  48 + * @param productKey 产品 Key
  49 + * @param deviceName 设备名称
  50 + * @param requestId 请求 ID
  51 + * @param method 方法名
  52 + * @param errorCode 错误码
  53 + * @param errorMessage 错误消息
  54 + */
  55 + protected void sendErrorResponse(MqttEndpoint endpoint, String productKey, String deviceName,
  56 + String requestId, String method, Integer errorCode, String errorMessage) {
  57 + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, null, errorCode, errorMessage);
  58 + writeResponse(endpoint, productKey, deviceName, method, responseMessage);
  59 + }
  60 +
  61 + /**
  62 + * 写入响应消息到设备
  63 + *
  64 + * @param endpoint MQTT 连接端点
  65 + * @param productKey 产品 Key
  66 + * @param deviceName 设备名称
  67 + * @param method 方法名
  68 + * @param responseMessage 响应消息
  69 + */
  70 + private void writeResponse(MqttEndpoint endpoint, String productKey, String deviceName,
  71 + String method, IotDeviceMessage responseMessage) {
  72 + try {
  73 + // 1.1 序列化消息(根据设备配置的序列化类型)
  74 + byte[] encodedData = deviceMessageService.serializeDeviceMessage(responseMessage, productKey, deviceName);
  75 + // 1.2 构建响应主题
  76 + String replyTopic = IotMqttTopicUtils.buildTopicByMethod(method, productKey, deviceName, true);
  77 +
  78 + // 2. 发送响应消息
  79 + endpoint.publish(replyTopic, Buffer.buffer(encodedData), MqttQoS.AT_LEAST_ONCE, false, false);
  80 + log.debug("[writeResponse][发送响应,主题: {},code: {}]", replyTopic, responseMessage.getCode());
  81 + } catch (Exception e) {
  82 + log.error("[writeResponse][发送响应异常,客户端 ID: {}]", endpoint.clientIdentifier(), e);
  83 + }
  84 + }
  85 +
  86 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAuthHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream;
  2 +
  3 +import cn.hutool.core.util.BooleanUtil;
  4 +import cn.hutool.extra.spring.SpringUtil;
  5 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  6 +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
  7 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
  8 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
  9 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  10 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
  11 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  12 +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
  13 +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
  14 +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
  15 +import io.vertx.mqtt.MqttEndpoint;
  16 +import lombok.extern.slf4j.Slf4j;
  17 +import cn.hutool.core.lang.Assert;
  18 +
  19 +import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL;
  20 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  21 +
  22 +/**
  23 + * IoT 网关 MQTT 认证处理器
  24 + * <p>
  25 + * 处理 MQTT CONNECT 事件,完成设备认证、连接注册、上线通知
  26 + *
  27 + * @author 芋道源码
  28 + */
  29 +@Slf4j
  30 +public class IotMqttAuthHandler extends IotMqttAbstractHandler {
  31 +
  32 + private final IotDeviceCommonApi deviceApi;
  33 + private final IotDeviceService deviceService;
  34 + private final String serverId;
  35 +
  36 + public IotMqttAuthHandler(IotMqttConnectionManager connectionManager,
  37 + IotDeviceMessageService deviceMessageService,
  38 + IotDeviceCommonApi deviceApi,
  39 + String serverId) {
  40 + super(connectionManager, deviceMessageService);
  41 + this.deviceApi = deviceApi;
  42 + this.deviceService = SpringUtil.getBean(IotDeviceService.class);
  43 + this.serverId = serverId;
  44 + }
  45 +
  46 + /**
  47 + * 处理 MQTT 连接(认证)请求
  48 + *
  49 + * @param endpoint MQTT 连接端点
  50 + * @return 认证是否成功
  51 + */
  52 + @SuppressWarnings("DataFlowIssue")
  53 + public boolean handleAuthenticationRequest(MqttEndpoint endpoint) {
  54 + String clientId = endpoint.clientIdentifier();
  55 + String username = endpoint.auth() != null ? endpoint.auth().getUsername() : null;
  56 + String password = endpoint.auth() != null ? endpoint.auth().getPassword() : null;
  57 + log.debug("[handleConnect][设备连接请求,客户端 ID: {},用户名: {},地址: {}]",
  58 + clientId, username, connectionManager.getEndpointAddress(endpoint));
  59 +
  60 + try {
  61 + // 1.1 解析认证参数
  62 + Assert.notBlank(clientId, "clientId 不能为空");
  63 + Assert.notBlank(username, "username 不能为空");
  64 + Assert.notBlank(password, "password 不能为空");
  65 + // 1.2 构建认证参数
  66 + IotDeviceAuthReqDTO authParams = new IotDeviceAuthReqDTO()
  67 + .setClientId(clientId)
  68 + .setUsername(username)
  69 + .setPassword(password);
  70 +
  71 + // 2.1 执行认证
  72 + CommonResult<Boolean> authResult = deviceApi.authDevice(authParams);
  73 + authResult.checkError();
  74 + if (BooleanUtil.isFalse(authResult.getData())) {
  75 + throw exception(DEVICE_AUTH_FAIL);
  76 + }
  77 + // 2.2 解析设备信息
  78 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
  79 + Assert.notNull(deviceInfo, "解析设备信息失败");
  80 + // 2.3 获取设备信息
  81 + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName());
  82 + Assert.notNull(device, "设备不存在");
  83 +
  84 + // 3.1 注册连接
  85 + registerConnection(endpoint, device, clientId);
  86 + // 3.2 发送设备上线消息
  87 + sendOnlineMessage(device);
  88 + log.info("[handleConnect][设备认证成功,建立连接,客户端 ID: {},用户名: {}]", clientId, username);
  89 + return true;
  90 + } catch (Exception e) {
  91 + log.warn("[handleConnect][设备认证失败,拒绝连接,客户端 ID: {},用户名: {},错误: {}]",
  92 + clientId, username, e.getMessage());
  93 + return false;
  94 + }
  95 + }
  96 +
  97 + /**
  98 + * 注册连接
  99 + */
  100 + private void registerConnection(MqttEndpoint endpoint, IotDeviceRespDTO device, String clientId) {
  101 + IotMqttConnectionManager.ConnectionInfo connectionInfo = new IotMqttConnectionManager.ConnectionInfo()
  102 + .setDeviceId(device.getId())
  103 + .setProductKey(device.getProductKey())
  104 + .setDeviceName(device.getDeviceName())
  105 + .setRemoteAddress(connectionManager.getEndpointAddress(endpoint));
  106 + connectionManager.registerConnection(endpoint, connectionInfo);
  107 + }
  108 +
  109 + /**
  110 + * 发送设备上线消息
  111 + */
  112 + private void sendOnlineMessage(IotDeviceRespDTO device) {
  113 + IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline();
  114 + deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(),
  115 + device.getDeviceName(), serverId);
  116 + log.info("[sendOnlineMessage][设备上线,设备 ID: {},设备名称: {}]", device.getId(), device.getDeviceName());
  117 + }
  118 +
  119 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttRegisterHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream;
  2 +
  3 +import cn.hutool.core.lang.Assert;
  4 +import cn.hutool.extra.spring.SpringUtil;
  5 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  6 +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
  7 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
  8 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
  9 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
  10 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
  11 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  12 +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
  13 +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
  14 +import io.vertx.mqtt.MqttEndpoint;
  15 +import lombok.extern.slf4j.Slf4j;
  16 +
  17 +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR;
  18 +
  19 +/**
  20 + * IoT 网关 MQTT 设备注册处理器:处理设备动态注册消息(一型一密)
  21 + *
  22 + * @author 芋道源码
  23 + */
  24 +@Slf4j
  25 +public class IotMqttRegisterHandler extends IotMqttAbstractHandler {
  26 +
  27 + private final IotDeviceCommonApi deviceApi;
  28 +
  29 + public IotMqttRegisterHandler(IotMqttConnectionManager connectionManager,
  30 + IotDeviceMessageService deviceMessageService) {
  31 + super(connectionManager, deviceMessageService);
  32 + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
  33 + }
  34 +
  35 + /**
  36 + * 处理注册连接
  37 + * <p>
  38 + * 通过 MQTT 连接的 username 解析设备信息,password 作为签名,直接处理设备注册
  39 + *
  40 + * @param endpoint MQTT 连接端点
  41 + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
  42 + */
  43 + @SuppressWarnings("DataFlowIssue")
  44 + public void handleRegister(MqttEndpoint endpoint) {
  45 + String clientId = endpoint.clientIdentifier();
  46 + String username = endpoint.auth() != null ? endpoint.auth().getUsername() : null;
  47 + String password = endpoint.auth() != null ? endpoint.auth().getPassword() : null;
  48 + String method = IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod();
  49 + String productKey = null;
  50 + String deviceName = null;
  51 +
  52 + try {
  53 + // 1.1 校验参数
  54 + Assert.notBlank(clientId, "clientId 不能为空");
  55 + Assert.notBlank(username, "username 不能为空");
  56 + Assert.notBlank(password, "password 不能为空");
  57 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
  58 + Assert.notNull(deviceInfo, "解析设备信息失败");
  59 + productKey = deviceInfo.getProductKey();
  60 + deviceName = deviceInfo.getDeviceName();
  61 + log.info("[handleRegister][设备注册连接,客户端 ID: {},设备: {}.{}]",
  62 + clientId, productKey, deviceName);
  63 + // 1.2 构建注册参数
  64 + IotDeviceRegisterReqDTO params = new IotDeviceRegisterReqDTO()
  65 + .setProductKey(productKey)
  66 + .setDeviceName(deviceName)
  67 + .setSign(password);
  68 +
  69 + // 2. 调用动态注册 API
  70 + CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);
  71 + result.checkError();
  72 +
  73 + // 3. 接受连接,并发送成功响应
  74 + endpoint.accept(false);
  75 + sendSuccessResponse(endpoint, productKey, deviceName, null, method, result.getData());
  76 + log.info("[handleRegister][注册成功,设备: {}.{},客户端 ID: {}]", productKey, deviceName, clientId);
  77 + } catch (Exception e) {
  78 + log.warn("[handleRegister][注册失败,客户端 ID: {},错误: {}]", clientId, e.getMessage());
  79 + // 接受连接,并发送错误响应
  80 + endpoint.accept(false);
  81 + sendErrorResponse(endpoint, productKey, deviceName, null, method,
  82 + INTERNAL_SERVER_ERROR.getCode(), e.getMessage());
  83 + } finally {
  84 + // 注册完成后关闭连接(一型一密只用于获取 deviceSecret,不保持连接)
  85 + endpoint.close();
  86 + }
  87 + }
  88 +
  89 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttUpstreamHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream;
  2 +
  3 +import cn.hutool.core.lang.Assert;
  4 +import cn.hutool.core.util.ArrayUtil;
  5 +import cn.iocoder.yudao.framework.common.exception.ServiceException;
  6 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  7 +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
  8 +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
  9 +import io.vertx.mqtt.MqttEndpoint;
  10 +import lombok.extern.slf4j.Slf4j;
  11 +
  12 +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
  13 +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR;
  14 +
  15 +/**
  16 + * IoT 网关 MQTT 上行消息处理器:处理业务消息(属性上报、事件上报等)
  17 + *
  18 + * @author 芋道源码
  19 + */
  20 +@Slf4j
  21 +public class IotMqttUpstreamHandler extends IotMqttAbstractHandler {
  22 +
  23 + private final String serverId;
  24 +
  25 + public IotMqttUpstreamHandler(IotMqttConnectionManager connectionManager,
  26 + IotDeviceMessageService deviceMessageService,
  27 + String serverId) {
  28 + super(connectionManager, deviceMessageService);
  29 + this.serverId = serverId;
  30 + }
  31 +
  32 + /**
  33 + * 处理业务消息
  34 + *
  35 + * @param endpoint MQTT 连接端点
  36 + * @param topic 主题
  37 + * @param payload 消息内容
  38 + */
  39 + public void handleBusinessRequest(MqttEndpoint endpoint, String topic, byte[] payload) {
  40 + String clientId = endpoint.clientIdentifier();
  41 + IotDeviceMessage message = null;
  42 + String productKey = null;
  43 + String deviceName = null;
  44 +
  45 + try {
  46 + // 1.1 基础检查
  47 + if (ArrayUtil.isEmpty(payload)) {
  48 + return;
  49 + }
  50 + // 1.2 解析主题,获取 productKey 和 deviceName
  51 + String[] topicParts = topic.split("/");
  52 + productKey = ArrayUtil.get(topicParts, 2);
  53 + deviceName = ArrayUtil.get(topicParts, 3);
  54 + Assert.notBlank(productKey, "产品 Key 不能为空");
  55 + Assert.notBlank(deviceName, "设备名称不能为空");
  56 + // 1.3 校验设备信息,防止伪造设备消息
  57 + IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint);
  58 + Assert.notNull(connectionInfo, "无法获取连接信息");
  59 + Assert.equals(productKey, connectionInfo.getProductKey(), "产品 Key 不匹配");
  60 + Assert.equals(deviceName, connectionInfo.getDeviceName(), "设备名称不匹配");
  61 +
  62 + // 2. 反序列化消息
  63 + message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName);
  64 + if (message == null) {
  65 + log.warn("[handleBusinessRequest][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic);
  66 + sendErrorResponse(endpoint, productKey, deviceName, null, null,
  67 + BAD_REQUEST.getCode(), "消息解码失败");
  68 + return;
  69 + }
  70 +
  71 + // 3. 处理业务消息
  72 + deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId);
  73 + log.debug("[handleBusinessRequest][消息处理成功,客户端 ID: {},主题: {}]", clientId, topic);
  74 + } catch (ServiceException e) {
  75 + log.warn("[handleBusinessRequest][业务异常,客户端 ID: {},主题: {},错误: {}]",
  76 + clientId, topic, e.getMessage());
  77 + String requestId = message != null ? message.getRequestId() : null;
  78 + String method = message != null ? message.getMethod() : null;
  79 + sendErrorResponse(endpoint, productKey, deviceName, requestId, method, e.getCode(), e.getMessage());
  80 + } catch (IllegalArgumentException e) {
  81 + log.warn("[handleBusinessRequest][参数校验失败,客户端 ID: {},主题: {},错误: {}]",
  82 + clientId, topic, e.getMessage());
  83 + String requestId = message != null ? message.getRequestId() : null;
  84 + String method = message != null ? message.getMethod() : null;
  85 + sendErrorResponse(endpoint, productKey, deviceName, requestId, method,
  86 + BAD_REQUEST.getCode(), e.getMessage());
  87 + } catch (Exception e) {
  88 + log.error("[handleBusinessRequest][消息处理异常,客户端 ID: {},主题: {},错误: {}]",
  89 + clientId, topic, e.getMessage(), e);
  90 + String requestId = message != null ? message.getRequestId() : null;
  91 + String method = message != null ? message.getMethod() : null;
  92 + sendErrorResponse(endpoint, productKey, deviceName, requestId, method,
  93 + INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg());
  94 + }
  95 + }
  96 +
  97 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java
... ... @@ -66,7 +66,6 @@ public class IotMqttConnectionManager {
66 66 } catch (Exception ignored) {
67 67 // 连接已关闭,忽略异常
68 68 }
69   -
70 69 return realTimeAddress;
71 70 }
72 71  
... ... @@ -74,24 +73,24 @@ public class IotMqttConnectionManager {
74 73 * 注册设备连接(包含认证信息)
75 74 *
76 75 * @param endpoint MQTT 连接端点
77   - * @param deviceId 设备 ID
78 76 * @param connectionInfo 连接信息
79 77 */
80   - public void registerConnection(MqttEndpoint endpoint, Long deviceId, ConnectionInfo connectionInfo) {
  78 + public void registerConnection(MqttEndpoint endpoint, ConnectionInfo connectionInfo) {
  79 + Long deviceId = connectionInfo.getDeviceId();
81 80 // 如果设备已有其他连接,先清理旧连接
82 81 MqttEndpoint oldEndpoint = deviceEndpointMap.get(deviceId);
83 82 if (oldEndpoint != null && oldEndpoint != endpoint) {
84 83 log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]",
85 84 deviceId, getEndpointAddress(oldEndpoint));
86   - oldEndpoint.close();
87   - // 清理旧连接的映射
  85 + // 先清理映射,再关闭连接(避免旧连接处理器干扰)
88 86 connectionMap.remove(oldEndpoint);
  87 + oldEndpoint.close();
89 88 }
90 89  
91 90 // 注册新连接
92 91 connectionMap.put(endpoint, connectionInfo);
93 92 deviceEndpointMap.put(deviceId, endpoint);
94   - log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {},product key: {},device name: {}]",
  93 + log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {},productKey: {},deviceName: {}]",
95 94 deviceId, getEndpointAddress(endpoint), connectionInfo.getProductKey(), connectionInfo.getDeviceName());
96 95 }
97 96  
... ... @@ -129,26 +128,11 @@ public class IotMqttConnectionManager {
129 128 if (endpoint == null) {
130 129 return null;
131 130 }
132   -
133 131 // 获取连接信息
134 132 return getConnectionInfo(endpoint);
135 133 }
136 134  
137 135 /**
138   - * 检查设备是否在线
139   - */
140   - public boolean isDeviceOnline(Long deviceId) {
141   - return deviceEndpointMap.containsKey(deviceId);
142   - }
143   -
144   - /**
145   - * 检查设备是否离线
146   - */
147   - public boolean isDeviceOffline(Long deviceId) {
148   - return !isDeviceOnline(deviceId);
149   - }
150   -
151   - /**
152 136 * 发送消息到设备
153 137 *
154 138 * @param deviceId 设备 ID
... ... @@ -192,28 +176,16 @@ public class IotMqttConnectionManager {
192 176 * 设备 ID
193 177 */
194 178 private Long deviceId;
195   -
196 179 /**
197 180 * 产品 Key
198 181 */
199 182 private String productKey;
200   -
201 183 /**
202 184 * 设备名称
203 185 */
204 186 private String deviceName;
205 187  
206 188 /**
207   - * 客户端 ID
208   - */
209   - private String clientId;
210   -
211   - /**
212   - * 是否已认证
213   - */
214   - private boolean authenticated;
215   -
216   - /**
217 189 * 连接地址
218 190 */
219 191 private String remoteAddress;
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttDownstreamHandler.java deleted
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router;
2   -
3   -import cn.hutool.core.util.StrUtil;
4   -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
5   -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
6   -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
7   -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
8   -import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
9   -import io.netty.handler.codec.mqtt.MqttQoS;
10   -import lombok.extern.slf4j.Slf4j;
11   -
12   -/**
13   - * IoT 网关 MQTT 协议:下行消息处理器
14   - * <p>
15   - * 专门处理下行消息的业务逻辑,包括:
16   - * 1. 消息编码
17   - * 2. 主题构建
18   - * 3. 消息发送
19   - *
20   - * @author 芋道源码
21   - */
22   -@Slf4j
23   -public class IotMqttDownstreamHandler {
24   -
25   - private final IotDeviceMessageService deviceMessageService;
26   -
27   - private final IotMqttConnectionManager connectionManager;
28   -
29   - public IotMqttDownstreamHandler(IotDeviceMessageService deviceMessageService,
30   - IotMqttConnectionManager connectionManager) {
31   - this.deviceMessageService = deviceMessageService;
32   - this.connectionManager = connectionManager;
33   - }
34   -
35   - /**
36   - * 处理下行消息
37   - *
38   - * @param message 设备消息
39   - * @return 是否处理成功
40   - */
41   - public boolean handleDownstreamMessage(IotDeviceMessage message) {
42   - try {
43   - // 1. 基础校验
44   - if (message == null || message.getDeviceId() == null) {
45   - log.warn("[handleDownstreamMessage][消息或设备 ID 为空,忽略处理]");
46   - return false;
47   - }
48   -
49   - // 2. 检查设备是否在线
50   - if (connectionManager.isDeviceOffline(message.getDeviceId())) {
51   - log.warn("[handleDownstreamMessage][设备离线,无法发送消息,设备 ID:{}]", message.getDeviceId());
52   - return false;
53   - }
54   -
55   - // 3. 获取连接信息
56   - IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfoByDeviceId(message.getDeviceId());
57   - if (connectionInfo == null) {
58   - log.warn("[handleDownstreamMessage][连接信息不存在,设备 ID:{}]", message.getDeviceId());
59   - return false;
60   - }
61   -
62   - // 4. 编码消息
63   - byte[] payload = deviceMessageService.encodeDeviceMessage(message, connectionInfo.getProductKey(),
64   - connectionInfo.getDeviceName());
65   - if (payload == null || payload.length == 0) {
66   - log.warn("[handleDownstreamMessage][消息编码失败,设备 ID:{}]", message.getDeviceId());
67   - return false;
68   - }
69   -
70   - // 5. 发送消息到设备
71   - return sendMessageToDevice(message, connectionInfo, payload);
72   - } catch (Exception e) {
73   - if (message != null) {
74   - log.error("[handleDownstreamMessage][处理下行消息异常,设备 ID:{},错误:{}]",
75   - message.getDeviceId(), e.getMessage(), e);
76   - }
77   - return false;
78   - }
79   - }
80   -
81   - /**
82   - * 发送消息到设备
83   - *
84   - * @param message 设备消息
85   - * @param connectionInfo 连接信息
86   - * @param payload 消息负载
87   - * @return 是否发送成功
88   - */
89   - private boolean sendMessageToDevice(IotDeviceMessage message,
90   - IotMqttConnectionManager.ConnectionInfo connectionInfo,
91   - byte[] payload) {
92   - // 1. 构建主题
93   - String topic = buildDownstreamTopic(message, connectionInfo);
94   - if (StrUtil.isBlank(topic)) {
95   - log.warn("[sendMessageToDevice][主题构建失败,设备 ID:{},方法:{}]",
96   - message.getDeviceId(), message.getMethod());
97   - return false;
98   - }
99   -
100   - // 2. 发送消息
101   - boolean success = connectionManager.sendToDevice(message.getDeviceId(), topic, payload, MqttQoS.AT_LEAST_ONCE.value(), false);
102   - if (success) {
103   - log.debug("[sendMessageToDevice][消息发送成功,设备 ID:{},主题:{},方法:{}]",
104   - message.getDeviceId(), topic, message.getMethod());
105   - } else {
106   - log.warn("[sendMessageToDevice][消息发送失败,设备 ID:{},主题:{},方法:{}]",
107   - message.getDeviceId(), topic, message.getMethod());
108   - }
109   - return success;
110   - }
111   -
112   - /**
113   - * 构建下行消息主题
114   - *
115   - * @param message 设备消息
116   - * @param connectionInfo 连接信息
117   - * @return 主题
118   - */
119   - private String buildDownstreamTopic(IotDeviceMessage message,
120   - IotMqttConnectionManager.ConnectionInfo connectionInfo) {
121   - String method = message.getMethod();
122   - if (StrUtil.isBlank(method)) {
123   - return null;
124   - }
125   -
126   - // 使用工具类构建主题,支持回复消息处理
127   - boolean isReply = IotDeviceMessageUtils.isReplyMessage(message);
128   - return IotMqttTopicUtils.buildTopicByMethod(method, connectionInfo.getProductKey(),
129   - connectionInfo.getDeviceName(), isReply);
130   - }
131   -
132   -}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttUpstreamHandler.java deleted
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router;
2   -
3   -import cn.hutool.core.map.MapUtil;
4   -import cn.hutool.core.util.ArrayUtil;
5   -import cn.hutool.core.util.BooleanUtil;
6   -import cn.hutool.core.util.StrUtil;
7   -import cn.hutool.extra.spring.SpringUtil;
8   -import cn.iocoder.yudao.framework.common.pojo.CommonResult;
9   -import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
10   -import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
11   -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
12   -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO;
13   -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
14   -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
15   -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
16   -import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
17   -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
18   -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
19   -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
20   -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol;
21   -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
22   -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
23   -import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
24   -import io.netty.handler.codec.mqtt.MqttConnectReturnCode;
25   -import io.netty.handler.codec.mqtt.MqttQoS;
26   -import io.vertx.mqtt.MqttEndpoint;
27   -import io.vertx.mqtt.MqttTopicSubscription;
28   -import lombok.extern.slf4j.Slf4j;
29   -
30   -import java.util.List;
31   -import java.util.Map;
32   -
33   -/**
34   - * MQTT 上行消息处理器
35   - *
36   - * @author 芋道源码
37   - */
38   -@Slf4j
39   -public class IotMqttUpstreamHandler {
40   -
41   - /**
42   - * 默认编解码类型(MQTT 使用 Alink 协议)
43   - */
44   - private static final String DEFAULT_CODEC_TYPE = "Alink";
45   -
46   - /**
47   - * register 请求的 topic 后缀
48   - */
49   - private static final String REGISTER_TOPIC_SUFFIX = "/thing/auth/register";
50   -
51   - private final IotDeviceMessageService deviceMessageService;
52   -
53   - private final IotMqttConnectionManager connectionManager;
54   -
55   - private final IotDeviceCommonApi deviceApi;
56   -
57   - private final String serverId;
58   -
59   - public IotMqttUpstreamHandler(IotMqttUpstreamProtocol protocol,
60   - IotDeviceMessageService deviceMessageService,
61   - IotMqttConnectionManager connectionManager) {
62   - this.deviceMessageService = deviceMessageService;
63   - this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
64   - this.connectionManager = connectionManager;
65   - this.serverId = protocol.getServerId();
66   - }
67   -
68   - /**
69   - * 处理 MQTT 连接
70   - *
71   - * @param endpoint MQTT 连接端点
72   - */
73   - public void handle(MqttEndpoint endpoint) {
74   - String clientId = endpoint.clientIdentifier();
75   - String username = endpoint.auth() != null ? endpoint.auth().getUsername() : null;
76   - String password = endpoint.auth() != null ? endpoint.auth().getPassword() : null;
77   -
78   - log.debug("[handle][设备连接请求,客户端 ID: {},用户名: {},地址: {}]",
79   - clientId, username, connectionManager.getEndpointAddress(endpoint));
80   -
81   - // 1. 先进行认证
82   - if (!authenticateDevice(clientId, username, password, endpoint)) {
83   - log.warn("[handle][设备认证失败,拒绝连接,客户端 ID: {},用户名: {}]", clientId, username);
84   - endpoint.reject(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD);
85   - return;
86   - }
87   -
88   - log.info("[handle][设备认证成功,建立连接,客户端 ID: {},用户名: {}]", clientId, username);
89   -
90   - // 2. 设置心跳处理器(监听客户端的 PINGREQ 消息)
91   - endpoint.pingHandler(v -> {
92   - log.debug("[handle][收到客户端心跳,客户端 ID: {}]", clientId);
93   - // Vert.x 会自动发送 PINGRESP 响应,无需手动处理
94   - });
95   -
96   - // 3. 设置异常和关闭处理器
97   - endpoint.exceptionHandler(ex -> {
98   - log.warn("[handle][连接异常,客户端 ID: {},地址: {}]", clientId, connectionManager.getEndpointAddress(endpoint));
99   - cleanupConnection(endpoint);
100   - });
101   - endpoint.closeHandler(v -> {
102   - cleanupConnection(endpoint);
103   - });
104   -
105   - // 4. 设置消息处理器
106   - endpoint.publishHandler(mqttMessage -> {
107   - try {
108   - // 4.1 根据 topic 判断是否为 register 请求
109   - String topic = mqttMessage.topicName();
110   - byte[] payload = mqttMessage.payload().getBytes();
111   - if (topic.endsWith(REGISTER_TOPIC_SUFFIX)) {
112   - // register 请求:使用默认编解码器处理(设备可能未注册)
113   - processRegisterMessage(clientId, topic, payload, endpoint);
114   - } else {
115   - // 业务请求:正常处理
116   - processMessage(clientId, topic, payload);
117   - }
118   -
119   - // 4.2 根据 QoS 级别发送相应的确认消息
120   - if (mqttMessage.qosLevel() == MqttQoS.AT_LEAST_ONCE) {
121   - // QoS 1: 发送 PUBACK 确认
122   - endpoint.publishAcknowledge(mqttMessage.messageId());
123   - } else if (mqttMessage.qosLevel() == MqttQoS.EXACTLY_ONCE) {
124   - // QoS 2: 发送 PUBREC 确认
125   - endpoint.publishReceived(mqttMessage.messageId());
126   - }
127   - // QoS 0 无需确认
128   - } catch (Exception e) {
129   - log.error("[handle][消息解码失败,断开连接,客户端 ID: {},地址: {},错误: {}]",
130   - clientId, connectionManager.getEndpointAddress(endpoint), e.getMessage());
131   - cleanupConnection(endpoint);
132   - endpoint.close();
133   - }
134   - });
135   -
136   - // 5. 设置订阅处理器
137   - endpoint.subscribeHandler(subscribe -> {
138   - // 提取主题名称列表用于日志显示
139   - List<String> topicNames = subscribe.topicSubscriptions().stream()
140   - .map(MqttTopicSubscription::topicName)
141   - .collect(java.util.stream.Collectors.toList());
142   - log.debug("[handle][设备订阅,客户端 ID: {},主题: {}]", clientId, topicNames);
143   -
144   - // 提取 QoS 列表
145   - List<MqttQoS> grantedQoSLevels = subscribe.topicSubscriptions().stream()
146   - .map(MqttTopicSubscription::qualityOfService)
147   - .collect(java.util.stream.Collectors.toList());
148   - endpoint.subscribeAcknowledge(subscribe.messageId(), grantedQoSLevels);
149   - });
150   -
151   - // 6. 设置取消订阅处理器
152   - endpoint.unsubscribeHandler(unsubscribe -> {
153   - log.debug("[handle][设备取消订阅,客户端 ID: {},主题: {}]", clientId, unsubscribe.topics());
154   - endpoint.unsubscribeAcknowledge(unsubscribe.messageId());
155   - });
156   -
157   - // 7. 设置 QoS 2消息的 PUBREL 处理器
158   - endpoint.publishReleaseHandler(endpoint::publishComplete);
159   -
160   - // 8. 设置断开连接处理器
161   - endpoint.disconnectHandler(v -> {
162   - log.debug("[handle][设备断开连接,客户端 ID: {}]", clientId);
163   - cleanupConnection(endpoint);
164   - });
165   -
166   - // 9. 接受连接
167   - endpoint.accept(false);
168   - }
169   -
170   - /**
171   - * 处理消息
172   - *
173   - * @param clientId 客户端 ID
174   - * @param topic 主题
175   - * @param payload 消息内容
176   - */
177   - private void processMessage(String clientId, String topic, byte[] payload) {
178   - // 1. 基础检查
179   - if (payload == null || payload.length == 0) {
180   - return;
181   - }
182   -
183   - // 2. 解析主题,获取 productKey 和 deviceName
184   - String[] topicParts = topic.split("/");
185   - if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) {
186   - log.warn("[processMessage][topic({}) 格式不正确,无法解析有效的 productKey 和 deviceName]", topic);
187   - return;
188   - }
189   -
190   - // 3. 解码消息(使用从 topic 解析的 productKey 和 deviceName)
191   - String productKey = topicParts[2];
192   - String deviceName = topicParts[3];
193   - try {
194   - IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName);
195   - if (message == null) {
196   - log.warn("[processMessage][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic);
197   - return;
198   - }
199   -
200   - // 4. 处理业务消息(认证已在连接时完成)
201   - log.info("[processMessage][收到设备消息,设备: {}.{}, 方法: {}]",
202   - productKey, deviceName, message.getMethod());
203   - handleBusinessRequest(message, productKey, deviceName);
204   - } catch (Exception e) {
205   - log.error("[processMessage][消息处理异常,客户端 ID: {},主题: {},错误: {}]",
206   - clientId, topic, e.getMessage(), e);
207   - }
208   - }
209   -
210   - /**
211   - * 在 MQTT 连接时进行设备认证
212   - *
213   - * @param clientId 客户端 ID
214   - * @param username 用户名
215   - * @param password 密码
216   - * @param endpoint MQTT 连接端点
217   - * @return 认证是否成功
218   - */
219   - private boolean authenticateDevice(String clientId, String username, String password, MqttEndpoint endpoint) {
220   - try {
221   - // 1. 参数校验
222   - if (StrUtil.hasEmpty(clientId, username, password)) {
223   - log.warn("[authenticateDevice][认证参数不完整,客户端 ID: {},用户名: {}]", clientId, username);
224   - return false;
225   - }
226   -
227   - // 2. 构建认证参数
228   - IotDeviceAuthReqDTO authParams = new IotDeviceAuthReqDTO()
229   - .setClientId(clientId)
230   - .setUsername(username)
231   - .setPassword(password);
232   -
233   - // 3. 调用设备认证 API
234   - CommonResult<Boolean> authResult = deviceApi.authDevice(authParams);
235   - if (!authResult.isSuccess() || !BooleanUtil.isTrue(authResult.getData())) {
236   - log.warn("[authenticateDevice][设备认证失败,客户端 ID: {},用户名: {},错误: {}]",
237   - clientId, username, authResult.getMsg());
238   - return false;
239   - }
240   -
241   - // 4. 获取设备信息
242   - IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
243   - if (deviceInfo == null) {
244   - log.warn("[authenticateDevice][用户名格式不正确,客户端 ID: {},用户名: {}]", clientId, username);
245   - return false;
246   - }
247   -
248   - IotDeviceGetReqDTO getReqDTO = new IotDeviceGetReqDTO()
249   - .setProductKey(deviceInfo.getProductKey())
250   - .setDeviceName(deviceInfo.getDeviceName());
251   -
252   - CommonResult<IotDeviceRespDTO> deviceResult = deviceApi.getDevice(getReqDTO);
253   - if (!deviceResult.isSuccess() || deviceResult.getData() == null) {
254   - log.warn("[authenticateDevice][获取设备信息失败,客户端 ID: {},用户名: {},错误: {}]",
255   - clientId, username, deviceResult.getMsg());
256   - return false;
257   - }
258   -
259   - // 5. 注册连接
260   - IotDeviceRespDTO device = deviceResult.getData();
261   - registerConnection(endpoint, device, clientId);
262   -
263   - // 6. 发送设备上线消息
264   - sendOnlineMessage(device);
265   -
266   - return true;
267   - } catch (Exception e) {
268   - log.error("[authenticateDevice][设备认证异常,客户端 ID: {},用户名: {}]", clientId, username, e);
269   - return false;
270   - }
271   - }
272   -
273   - /**
274   - * 处理 register 消息(设备动态注册,使用默认编解码器)
275   - *
276   - * @param clientId 客户端 ID
277   - * @param topic 主题
278   - * @param payload 消息内容
279   - * @param endpoint MQTT 连接端点
280   - */
281   - private void processRegisterMessage(String clientId, String topic, byte[] payload, MqttEndpoint endpoint) {
282   - // 1.1 基础检查
283   - if (ArrayUtil.isEmpty(payload)) {
284   - return;
285   - }
286   - // 1.2 解析主题,获取 productKey 和 deviceName
287   - String[] topicParts = topic.split("/");
288   - if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) {
289   - log.warn("[processRegisterMessage][topic({}) 格式不正确]", topic);
290   - return;
291   - }
292   - String productKey = topicParts[2];
293   - String deviceName = topicParts[3];
294   -
295   - // 2. 使用默认编解码器解码消息(设备可能未注册,无法获取 codecType)
296   - IotDeviceMessage message;
297   - try {
298   - message = deviceMessageService.decodeDeviceMessage(payload, DEFAULT_CODEC_TYPE);
299   - if (message == null) {
300   - log.warn("[processRegisterMessage][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic);
301   - return;
302   - }
303   - } catch (Exception e) {
304   - log.error("[processRegisterMessage][消息解码异常,客户端 ID: {},主题: {},错误: {}]",
305   - clientId, topic, e.getMessage(), e);
306   - return;
307   - }
308   -
309   - // 3. 处理设备动态注册请求
310   - log.info("[processRegisterMessage][收到设备注册消息,设备: {}.{}, 方法: {}]",
311   - productKey, deviceName, message.getMethod());
312   - try {
313   - handleRegisterRequest(message, productKey, deviceName, endpoint);
314   - } catch (Exception e) {
315   - log.error("[processRegisterMessage][消息处理异常,客户端 ID: {},主题: {},错误: {}]",
316   - clientId, topic, e.getMessage(), e);
317   - }
318   - }
319   -
320   - /**
321   - * 处理设备动态注册请求(一型一密,不需要 deviceSecret)
322   - *
323   - * @param message 消息信息
324   - * @param productKey 产品 Key
325   - * @param deviceName 设备名称
326   - * @param endpoint MQTT 连接端点
327   - * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
328   - */
329   - private void handleRegisterRequest(IotDeviceMessage message, String productKey, String deviceName, MqttEndpoint endpoint) {
330   - String clientId = endpoint.clientIdentifier();
331   - try {
332   - // 1. 解析注册参数
333   - IotDeviceRegisterReqDTO params = parseRegisterParams(message.getParams());
334   - if (params == null) {
335   - log.warn("[handleRegisterRequest][注册参数解析失败,客户端 ID: {}]", clientId);
336   - sendRegisterErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), "注册参数不完整");
337   - return;
338   - }
339   -
340   - // 2. 调用动态注册 API
341   - CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);
342   - if (result.isError()) {
343   - log.warn("[handleRegisterRequest][注册失败,客户端 ID: {},错误: {}]", clientId, result.getMsg());
344   - sendRegisterErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), result.getMsg());
345   - return;
346   - }
347   -
348   - // 3. 发送成功响应(包含 deviceSecret)
349   - sendRegisterSuccessResponse(endpoint, productKey, deviceName, message.getRequestId(), result.getData());
350   - log.info("[handleRegisterRequest][注册成功,设备名: {},客户端 ID: {}]",
351   - params.getDeviceName(), clientId);
352   - } catch (Exception e) {
353   - log.error("[handleRegisterRequest][注册处理异常,客户端 ID: {}]", clientId, e);
354   - sendRegisterErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), "注册处理异常");
355   - }
356   - }
357   -
358   - /**
359   - * 解析注册参数
360   - *
361   - * @param params 参数对象(通常为 Map 类型)
362   - * @return 注册参数 DTO,解析失败时返回 null
363   - */
364   - @SuppressWarnings({"unchecked", "DuplicatedCode"})
365   - private IotDeviceRegisterReqDTO parseRegisterParams(Object params) {
366   - if (params == null) {
367   - return null;
368   - }
369   - try {
370   - // 参数默认为 Map 类型,直接转换
371   - if (params instanceof Map) {
372   - Map<String, Object> paramMap = (Map<String, Object>) params;
373   - return new IotDeviceRegisterReqDTO()
374   - .setProductKey(MapUtil.getStr(paramMap, "productKey"))
375   - .setDeviceName(MapUtil.getStr(paramMap, "deviceName"))
376   - .setProductSecret(MapUtil.getStr(paramMap, "productSecret"));
377   - }
378   - // 如果已经是目标类型,直接返回
379   - if (params instanceof IotDeviceRegisterReqDTO) {
380   - return (IotDeviceRegisterReqDTO) params;
381   - }
382   -
383   - // 其他情况尝试 JSON 转换
384   - return JsonUtils.convertObject(params, IotDeviceRegisterReqDTO.class);
385   - } catch (Exception e) {
386   - log.error("[parseRegisterParams][解析注册参数({})失败]", params, e);
387   - return null;
388   - }
389   - }
390   -
391   - /**
392   - * 发送注册成功响应(包含 deviceSecret)
393   - *
394   - * @param endpoint MQTT 连接端点
395   - * @param productKey 产品 Key
396   - * @param deviceName 设备名称
397   - * @param requestId 请求 ID
398   - * @param registerResp 注册响应
399   - */
400   - private void sendRegisterSuccessResponse(MqttEndpoint endpoint, String productKey, String deviceName,
401   - String requestId, IotDeviceRegisterRespDTO registerResp) {
402   - try {
403   - // 1. 构建响应消息(参考 HTTP 返回格式,直接返回 IotDeviceRegisterRespDTO)
404   - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId,
405   - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerResp, 0, null);
406   -
407   - // 2. 编码消息(使用默认编解码器,因为设备可能还未注册)
408   - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, DEFAULT_CODEC_TYPE);
409   -
410   - // 3. 构建响应主题并发送(格式:/sys/{productKey}/{deviceName}/thing/auth/register_reply)
411   - String replyTopic = IotMqttTopicUtils.buildTopicByMethod(
412   - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), productKey, deviceName, true);
413   - endpoint.publish(replyTopic, io.vertx.core.buffer.Buffer.buffer(encodedData),
414   - MqttQoS.AT_LEAST_ONCE, false, false);
415   - log.debug("[sendRegisterSuccessResponse][发送注册成功响应,主题: {}]", replyTopic);
416   - } catch (Exception e) {
417   - log.error("[sendRegisterSuccessResponse][发送注册成功响应异常,客户端 ID: {}]",
418   - endpoint.clientIdentifier(), e);
419   - }
420   - }
421   -
422   - /**
423   - * 发送注册错误响应
424   - *
425   - * @param endpoint MQTT 连接端点
426   - * @param productKey 产品 Key
427   - * @param deviceName 设备名称
428   - * @param requestId 请求 ID
429   - * @param errorMessage 错误消息
430   - */
431   - private void sendRegisterErrorResponse(MqttEndpoint endpoint, String productKey, String deviceName,
432   - String requestId, String errorMessage) {
433   - try {
434   - // 1. 构建响应消息
435   - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId,
436   - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), null, 400, errorMessage);
437   -
438   - // 2. 编码消息(使用默认编解码器,因为设备可能还未注册)
439   - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, DEFAULT_CODEC_TYPE);
440   -
441   - // 3. 构建响应主题并发送(格式:/sys/{productKey}/{deviceName}/thing/auth/register_reply)
442   - String replyTopic = IotMqttTopicUtils.buildTopicByMethod(
443   - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), productKey, deviceName, true);
444   - endpoint.publish(replyTopic, io.vertx.core.buffer.Buffer.buffer(encodedData),
445   - MqttQoS.AT_LEAST_ONCE, false, false);
446   - log.debug("[sendRegisterErrorResponse][发送注册错误响应,主题: {}]", replyTopic);
447   - } catch (Exception e) {
448   - log.error("[sendRegisterErrorResponse][发送注册错误响应异常,客户端 ID: {}]",
449   - endpoint.clientIdentifier(), e);
450   - }
451   - }
452   -
453   - /**
454   - * 处理业务请求
455   - */
456   - private void handleBusinessRequest(IotDeviceMessage message, String productKey, String deviceName) {
457   - // 发送消息到消息总线
458   - message.setServerId(serverId);
459   - deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId);
460   - }
461   -
462   - /**
463   - * 注册连接
464   - */
465   - private void registerConnection(MqttEndpoint endpoint, IotDeviceRespDTO device, String clientId) {
466   - IotMqttConnectionManager.ConnectionInfo connectionInfo = new IotMqttConnectionManager.ConnectionInfo()
467   - .setDeviceId(device.getId())
468   - .setProductKey(device.getProductKey())
469   - .setDeviceName(device.getDeviceName())
470   - .setClientId(clientId)
471   - .setAuthenticated(true)
472   - .setRemoteAddress(connectionManager.getEndpointAddress(endpoint));
473   - connectionManager.registerConnection(endpoint, device.getId(), connectionInfo);
474   - }
475   -
476   - /**
477   - * 发送设备上线消息
478   - */
479   - private void sendOnlineMessage(IotDeviceRespDTO device) {
480   - try {
481   - IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline();
482   - deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(),
483   - device.getDeviceName(), serverId);
484   - log.info("[sendOnlineMessage][设备上线,设备 ID: {},设备名称: {}]", device.getId(), device.getDeviceName());
485   - } catch (Exception e) {
486   - log.error("[sendOnlineMessage][发送设备上线消息失败,设备 ID: {},错误: {}]", device.getId(), e.getMessage());
487   - }
488   - }
489   -
490   - /**
491   - * 清理连接
492   - */
493   - private void cleanupConnection(MqttEndpoint endpoint) {
494   - try {
495   - IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint);
496   - if (connectionInfo != null) {
497   - // 发送设备离线消息
498   - IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline();
499   - deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(),
500   - connectionInfo.getDeviceName(), serverId);
501   - log.info("[cleanupConnection][设备离线,设备 ID: {},设备名称: {}]", connectionInfo.getDeviceId(), connectionInfo.getDeviceName());
502   - }
503   -
504   - // 注销连接
505   - connectionManager.unregisterConnection(endpoint);
506   - } catch (Exception e) {
507   - log.error("[cleanupConnection][清理连接失败,客户端 ID: {},错误: {}]", endpoint.clientIdentifier(), e.getMessage());
508   - }
509   - }
510   -
511   -}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/package-info.java
1 1 /**
2   - * 提供设备接入的各种协议的实现
  2 + * 设备接入协议:MQTT、EMQX、HTTP、TCP 等协议的实现
3 3 */
4   -package cn.iocoder.yudao.module.iot.gateway.protocol;
5 4 \ No newline at end of file
  5 +package cn.iocoder.yudao.module.iot.gateway.protocol;
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpConfig.java
... ... @@ -28,20 +28,6 @@ public class IotTcpConfig {
28 28 private Long keepAliveTimeoutMs = 30000L;
29 29  
30 30 /**
31   - * 是否启用 SSL
32   - */
33   - @NotNull(message = "是否启用 SSL 不能为空")
34   - private Boolean sslEnabled = false;
35   - /**
36   - * SSL 证书路径
37   - */
38   - private String sslCertPath;
39   - /**
40   - * SSL 私钥路径
41   - */
42   - private String sslKeyPath;
43   -
44   - /**
45 31 * 拆包配置
46 32 */
47 33 @Valid
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java
... ... @@ -5,7 +5,8 @@ import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
5 5 import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum;
6 6 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
7 7 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
8   -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties;
  8 +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
  9 +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties;
9 10 import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
10 11 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec;
11 12 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodecFactory;
... ... @@ -21,7 +22,7 @@ import io.vertx.core.net.NetServerOptions;
21 22 import io.vertx.core.net.PemKeyCertOptions;
22 23 import lombok.Getter;
23 24 import lombok.extern.slf4j.Slf4j;
24   -import org.springframework.util.Assert;
  25 +import cn.hutool.core.lang.Assert;
25 26  
26 27 /**
27 28 * IoT TCP 协议实现
... ... @@ -36,7 +37,7 @@ public class IotTcpProtocol implements IotProtocol {
36 37 /**
37 38 * 协议配置
38 39 */
39   - private final ProtocolInstanceProperties properties;
  40 + private final ProtocolProperties properties;
40 41 /**
41 42 * 服务器 ID(用于消息追踪,全局唯一)
42 43 */
... ... @@ -76,7 +77,7 @@ public class IotTcpProtocol implements IotProtocol {
76 77 */
77 78 private final IotTcpFrameCodec frameCodec;
78 79  
79   - public IotTcpProtocol(ProtocolInstanceProperties properties) {
  80 + public IotTcpProtocol(ProtocolProperties properties) {
80 81 IotTcpConfig tcpConfig = properties.getTcp();
81 82 Assert.notNull(tcpConfig, "TCP 协议配置(tcp)不能为空");
82 83 Assert.notNull(tcpConfig.getCodec(), "TCP 拆包配置(tcp.codec)不能为空");
... ... @@ -128,10 +129,11 @@ public class IotTcpProtocol implements IotProtocol {
128 129 .setTcpNoDelay(true)
129 130 .setReuseAddress(true)
130 131 .setIdleTimeout((int) (tcpConfig.getKeepAliveTimeoutMs() / 1000)); // 设置空闲超时
131   - if (Boolean.TRUE.equals(tcpConfig.getSslEnabled())) {
  132 + IotGatewayProperties.SslConfig sslConfig = properties.getSsl();
  133 + if (sslConfig != null && Boolean.TRUE.equals(sslConfig.getSsl())) {
132 134 PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions()
133   - .setKeyPath(tcpConfig.getSslKeyPath())
134   - .setCertPath(tcpConfig.getSslCertPath());
  135 + .setKeyPath(sslConfig.getSslKeyPath())
  136 + .setCertPath(sslConfig.getSslCertPath());
135 137 options.setSsl(true).setKeyCertOptions(pemKeyCertOptions);
136 138 }
137 139  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/delimiter/IotTcpDelimiterFrameCodec.java
... ... @@ -8,7 +8,7 @@ import io.vertx.core.Handler;
8 8 import io.vertx.core.buffer.Buffer;
9 9 import io.vertx.core.parsetools.RecordParser;
10 10 import lombok.extern.slf4j.Slf4j;
11   -import org.springframework.util.Assert;
  11 +import cn.hutool.core.lang.Assert;
12 12  
13 13 /**
14 14 * IoT TCP 分隔符帧编解码器
... ... @@ -39,7 +39,7 @@ public class IotTcpDelimiterFrameCodec implements IotTcpFrameCodec {
39 39 private final byte[] delimiterBytes;
40 40  
41 41 public IotTcpDelimiterFrameCodec(IotTcpConfig.CodecConfig config) {
42   - Assert.hasText(config.getDelimiter(), "delimiter 不能为空");
  42 + Assert.notBlank(config.getDelimiter(), "delimiter 不能为空");
43 43 this.delimiterBytes = parseDelimiter(config.getDelimiter());
44 44 }
45 45  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpFixedLengthFrameCodec.java
... ... @@ -7,7 +7,7 @@ import io.vertx.core.Handler;
7 7 import io.vertx.core.buffer.Buffer;
8 8 import io.vertx.core.parsetools.RecordParser;
9 9 import lombok.extern.slf4j.Slf4j;
10   -import org.springframework.util.Assert;
  10 +import cn.hutool.core.lang.Assert;
11 11  
12 12 /**
13 13 * IoT TCP 定长帧编解码器
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpLengthFieldFrameCodec.java
... ... @@ -7,7 +7,7 @@ import io.vertx.core.Handler;
7 7 import io.vertx.core.buffer.Buffer;
8 8 import io.vertx.core.parsetools.RecordParser;
9 9 import lombok.extern.slf4j.Slf4j;
10   -import org.springframework.util.Assert;
  10 +import cn.hutool.core.lang.Assert;
11 11  
12 12 import java.util.concurrent.atomic.AtomicReference;
13 13  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamHandler.java
... ... @@ -45,8 +45,8 @@ public class IotTcpDownstreamHandler {
45 45 }
46 46  
47 47 // 2. 序列化 + 帧编码
48   - byte[] serializedData = serializer.serialize(message);
49   - Buffer frameData = codec.encode(serializedData);
  48 + byte[] payload = serializer.serialize(message);
  49 + Buffer frameData = codec.encode(payload);
50 50  
51 51 // 3. 发送到设备
52 52 boolean success = connectionManager.sendToDevice(message.getDeviceId(), frameData.getBytes());
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java
... ... @@ -24,7 +24,7 @@ import io.vertx.core.buffer.Buffer;
24 24 import io.vertx.core.net.NetSocket;
25 25 import io.vertx.core.parsetools.RecordParser;
26 26 import lombok.extern.slf4j.Slf4j;
27   -import org.springframework.util.Assert;
  27 +import cn.hutool.core.lang.Assert;
28 28  
29 29 import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*;
30 30 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
... ... @@ -167,8 +167,8 @@ public class IotTcpUpstreamHandler implements Handler&lt;NetSocket&gt; {
167 167 // 1. 解析认证参数
168 168 IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class);
169 169 Assert.notNull(authParams, "认证参数不能为空");
170   - Assert.hasText(authParams.getUsername(), "username 不能为空");
171   - Assert.hasText(authParams.getPassword(), "password 不能为空");
  170 + Assert.notBlank(authParams.getUsername(), "username 不能为空");
  171 + Assert.notBlank(authParams.getPassword(), "password 不能为空");
172 172  
173 173 // 2.1 执行认证
174 174 CommonResult<Boolean> authResult = deviceApi.authDevice(authParams);
... ... @@ -204,8 +204,9 @@ public class IotTcpUpstreamHandler implements Handler&lt;NetSocket&gt; {
204 204 // 1. 解析注册参数
205 205 IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class);
206 206 Assert.notNull(params, "注册参数不能为空");
207   - Assert.hasText(params.getProductKey(), "productKey 不能为空");
208   - Assert.hasText(params.getDeviceName(), "deviceName 不能为空");
  207 + Assert.notBlank(params.getProductKey(), "productKey 不能为空");
  208 + Assert.notBlank(params.getDeviceName(), "deviceName 不能为空");
  209 + Assert.notBlank(params.getSign(), "sign 不能为空");
209 210  
210 211 // 2. 调用动态注册
211 212 CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java
... ... @@ -47,8 +47,8 @@ public class IotTcpConnectionManager {
47 47 * @param deviceId 设备 ID
48 48 * @param connectionInfo 连接信息
49 49 */
50   - public void registerConnection(NetSocket socket, Long deviceId, ConnectionInfo connectionInfo) {
51   - // 检查连接数是否已达上限
  50 + public synchronized void registerConnection(NetSocket socket, Long deviceId, ConnectionInfo connectionInfo) {
  51 + // 检查连接数是否已达上限(同步方法确保检查和注册的原子性)
52 52 if (connectionMap.size() >= maxConnections) {
53 53 throw new IllegalStateException("连接数已达上限: " + maxConnections);
54 54 }
... ... @@ -57,9 +57,9 @@ public class IotTcpConnectionManager {
57 57 if (oldSocket != null && oldSocket != socket) {
58 58 log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]",
59 59 deviceId, oldSocket.remoteAddress());
60   - oldSocket.close();
61   - // 清理旧连接的映射
  60 + // 先清理映射,再关闭连接
62 61 connectionMap.remove(oldSocket);
  62 + oldSocket.close();
63 63 }
64 64  
65 65 // 注册新连接
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpProtocol.java
... ... @@ -4,7 +4,7 @@ import cn.hutool.extra.spring.SpringUtil;
4 4 import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
5 5 import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum;
6 6 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
7   -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties;
  7 +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties;
8 8 import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
9 9 import cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.downstream.IotUdpDownstreamHandler;
10 10 import cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.downstream.IotUdpDownstreamSubscriber;
... ... @@ -18,7 +18,7 @@ import io.vertx.core.datagram.DatagramSocket;
18 18 import io.vertx.core.datagram.DatagramSocketOptions;
19 19 import lombok.Getter;
20 20 import lombok.extern.slf4j.Slf4j;
21   -import org.springframework.util.Assert;
  21 +import cn.hutool.core.lang.Assert;
22 22  
23 23 /**
24 24 * IoT UDP 协议实现
... ... @@ -33,7 +33,7 @@ public class IotUdpProtocol implements IotProtocol {
33 33 /**
34 34 * 协议配置
35 35 */
36   - private final ProtocolInstanceProperties properties;
  36 + private final ProtocolProperties properties;
37 37 /**
38 38 * 服务器 ID(用于消息追踪,全局唯一)
39 39 */
... ... @@ -70,7 +70,7 @@ public class IotUdpProtocol implements IotProtocol {
70 70 */
71 71 private final IotMessageSerializer serializer;
72 72  
73   - public IotUdpProtocol(ProtocolInstanceProperties properties) {
  73 + public IotUdpProtocol(ProtocolProperties properties) {
74 74 IotUdpConfig udpConfig = properties.getUdp();
75 75 Assert.notNull(udpConfig, "UDP 协议配置(udp)不能为空");
76 76 this.properties = properties;
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/upstream/IotUdpUpstreamHandler.java
... ... @@ -27,7 +27,7 @@ import io.vertx.core.buffer.Buffer;
27 27 import io.vertx.core.datagram.DatagramPacket;
28 28 import io.vertx.core.datagram.DatagramSocket;
29 29 import lombok.extern.slf4j.Slf4j;
30   -import org.springframework.util.Assert;
  30 +import cn.hutool.core.lang.Assert;
31 31  
32 32 import java.net.InetSocketAddress;
33 33 import java.util.Map;
... ... @@ -173,8 +173,8 @@ public class IotUdpUpstreamHandler {
173 173 // 1. 解析认证参数
174 174 IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class);
175 175 Assert.notNull(authParams, "认证参数不能为空");
176   - Assert.hasText(authParams.getUsername(), "username 不能为空");
177   - Assert.hasText(authParams.getPassword(), "password 不能为空");
  176 + Assert.notBlank(authParams.getUsername(), "username 不能为空");
  177 + Assert.notBlank(authParams.getPassword(), "password 不能为空");
178 178  
179 179 // 2.1 执行认证
180 180 CommonResult<Boolean> authResult = deviceApi.authDevice(authParams);
... ... @@ -218,8 +218,9 @@ public class IotUdpUpstreamHandler {
218 218 // 1. 解析注册参数
219 219 IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class);
220 220 Assert.notNull(params, "注册参数不能为空");
221   - Assert.hasText(params.getProductKey(), "productKey 不能为空");
222   - Assert.hasText(params.getDeviceName(), "deviceName 不能为空");
  221 + Assert.notBlank(params.getProductKey(), "productKey 不能为空");
  222 + Assert.notBlank(params.getDeviceName(), "deviceName 不能为空");
  223 + Assert.notBlank(params.getSign(), "sign 不能为空");
223 224  
224 225 // 2. 调用动态注册
225 226 CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/manager/IotUdpSessionManager.java
... ... @@ -9,6 +9,7 @@ import lombok.Data;
9 9 import lombok.extern.slf4j.Slf4j;
10 10  
11 11 import java.net.InetSocketAddress;
  12 +import java.util.concurrent.CompletableFuture;
12 13 import java.util.concurrent.TimeUnit;
13 14  
14 15 /**
... ... @@ -47,8 +48,8 @@ public class IotUdpSessionManager {
47 48 * @param deviceId 设备 ID
48 49 * @param sessionInfo 会话信息
49 50 */
50   - public void registerSession(Long deviceId, SessionInfo sessionInfo) {
51   - // 检查是否为新设备,且会话数已达上限
  51 + public synchronized void registerSession(Long deviceId, SessionInfo sessionInfo) {
  52 + // 检查是否为新设备,且会话数已达上限(同步方法确保检查和注册的原子性)
52 53 if (deviceSessionCache.getIfPresent(deviceId) == null
53 54 && deviceSessionCache.size() >= maxSessions) {
54 55 throw new IllegalStateException("会话数已达上限: " + maxSessions);
... ... @@ -113,16 +114,21 @@ public class IotUdpSessionManager {
113 114 }
114 115 InetSocketAddress address = sessionInfo.getAddress();
115 116 try {
  117 + // 使用 CompletableFuture 同步等待发送结果
  118 + CompletableFuture<Boolean> future = new CompletableFuture<>();
116 119 socket.send(Buffer.buffer(data), address.getPort(), address.getHostString(), result -> {
117 120 if (result.succeeded()) {
118 121 log.debug("[sendToDevice][发送消息成功,设备 ID: {},地址: {},数据长度: {} 字节]",
119 122 deviceId, buildAddressKey(address), data.length);
120   - return;
  123 + future.complete(true);
  124 + } else {
  125 + log.error("[sendToDevice][发送消息失败,设备 ID: {},地址: {}]",
  126 + deviceId, buildAddressKey(address), result.cause());
  127 + future.complete(false);
121 128 }
122   - log.error("[sendToDevice][发送消息失败,设备 ID: {},地址: {}]",
123   - deviceId, buildAddressKey(address), result.cause());
124 129 });
125   - return true;
  130 + // 同步等待结果,超时 5 秒
  131 + return future.get(5, TimeUnit.SECONDS);
126 132 } catch (Exception e) {
127 133 log.error("[sendToDevice][发送消息异常,设备 ID: {}]", deviceId, e);
128 134 return false;
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketConfig.java
... ... @@ -35,20 +35,4 @@ public class IotWebSocketConfig {
35 35 @NotNull(message = "空闲超时时间不能为空")
36 36 private Integer idleTimeoutSeconds = 60;
37 37  
38   - /**
39   - * 是否启用 SSL(wss://)
40   - */
41   - @NotNull(message = "是否启用 SSL 不能为空")
42   - private Boolean sslEnabled = false;
43   -
44   - /**
45   - * SSL 证书路径
46   - */
47   - private String sslCertPath;
48   -
49   - /**
50   - * SSL 私钥路径
51   - */
52   - private String sslKeyPath;
53   -
54 38 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketProtocol.java
... ... @@ -6,7 +6,8 @@ import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
6 6 import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum;
7 7 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
8 8 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
9   -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties;
  9 +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
  10 +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties;
10 11 import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
11 12 import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.downstream.IotWebSocketDownstreamSubscriber;
12 13 import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.downstream.IotWebSocketDownstreamHandler;
... ... @@ -20,7 +21,7 @@ import io.vertx.core.http.HttpServerOptions;
20 21 import io.vertx.core.net.PemKeyCertOptions;
21 22 import lombok.Getter;
22 23 import lombok.extern.slf4j.Slf4j;
23   -import org.springframework.util.Assert;
  24 +import cn.hutool.core.lang.Assert;
24 25  
25 26 /**
26 27 * IoT WebSocket 协议实现
... ... @@ -35,7 +36,7 @@ public class IotWebSocketProtocol implements IotProtocol {
35 36 /**
36 37 * 协议配置
37 38 */
38   - private final ProtocolInstanceProperties properties;
  39 + private final ProtocolProperties properties;
39 40 /**
40 41 * 服务器 ID(用于消息追踪,全局唯一)
41 42 */
... ... @@ -71,7 +72,7 @@ public class IotWebSocketProtocol implements IotProtocol {
71 72 */
72 73 private final IotMessageSerializer serializer;
73 74  
74   - public IotWebSocketProtocol(ProtocolInstanceProperties properties) {
  75 + public IotWebSocketProtocol(ProtocolProperties properties) {
75 76 Assert.notNull(properties, "协议实例配置不能为空");
76 77 Assert.notNull(properties.getWebsocket(), "WebSocket 协议配置(websocket)不能为空");
77 78 this.properties = properties;
... ... @@ -120,10 +121,11 @@ public class IotWebSocketProtocol implements IotProtocol {
120 121 .setIdleTimeout(wsConfig.getIdleTimeoutSeconds())
121 122 .setMaxWebSocketFrameSize(wsConfig.getMaxFrameSize())
122 123 .setMaxWebSocketMessageSize(wsConfig.getMaxMessageSize());
123   - if (Boolean.TRUE.equals(wsConfig.getSslEnabled())) {
  124 + IotGatewayProperties.SslConfig sslConfig = properties.getSsl();
  125 + if (sslConfig != null && Boolean.TRUE.equals(sslConfig.getSsl())) {
124 126 PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions()
125   - .setKeyPath(wsConfig.getSslKeyPath())
126   - .setCertPath(wsConfig.getSslCertPath());
  127 + .setKeyPath(sslConfig.getSslKeyPath())
  128 + .setCertPath(sslConfig.getSslCertPath());
127 129 options.setSsl(true).setKeyCertOptions(pemKeyCertOptions);
128 130 }
129 131  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/upstream/IotWebSocketUpstreamHandler.java
... ... @@ -23,7 +23,7 @@ import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessa
23 23 import io.vertx.core.Handler;
24 24 import io.vertx.core.http.ServerWebSocket;
25 25 import lombok.extern.slf4j.Slf4j;
26   -import org.springframework.util.Assert;
  26 +import cn.hutool.core.lang.Assert;
27 27  
28 28 import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*;
29 29 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
... ... @@ -109,7 +109,7 @@ public class IotWebSocketUpstreamHandler implements Handler&lt;ServerWebSocket&gt; {
109 109 // 1.2 解码消息
110 110 message = serializer.deserialize(payload);
111 111 Assert.notNull(message, "消息反序列化失败");
112   - Assert.hasText(message.getMethod(), "method 不能为空");
  112 + Assert.notBlank(message.getMethod(), "method 不能为空");
113 113  
114 114 // 2. 根据消息类型路由处理
115 115 if (AUTH_METHOD.equals(message.getMethod())) {
... ... @@ -150,8 +150,8 @@ public class IotWebSocketUpstreamHandler implements Handler&lt;ServerWebSocket&gt; {
150 150 // 1. 解析认证参数
151 151 IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class);
152 152 Assert.notNull(authParams, "认证参数不能为空");
153   - Assert.hasText(authParams.getUsername(), "username 不能为空");
154   - Assert.hasText(authParams.getPassword(), "password 不能为空");
  153 + Assert.notBlank(authParams.getUsername(), "username 不能为空");
  154 + Assert.notBlank(authParams.getPassword(), "password 不能为空");
155 155  
156 156 // 2.1 执行认证
157 157 CommonResult<Boolean> authResult = deviceApi.authDevice(authParams);
... ... @@ -187,8 +187,9 @@ public class IotWebSocketUpstreamHandler implements Handler&lt;ServerWebSocket&gt; {
187 187 // 1. 解析注册参数
188 188 IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class);
189 189 Assert.notNull(params, "注册参数不能为空");
190   - Assert.hasText(params.getProductKey(), "productKey 不能为空");
191   - Assert.hasText(params.getDeviceName(), "deviceName 不能为空");
  190 + Assert.notBlank(params.getProductKey(), "productKey 不能为空");
  191 + Assert.notBlank(params.getDeviceName(), "deviceName 不能为空");
  192 + Assert.notBlank(params.getSign(), "sign 不能为空");
192 193  
193 194 // 2. 调用动态注册
194 195 CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/package-info.java 0 → 100644
  1 +/**
  2 + * 消息序列化:将设备消息转换为字节数组(JSON、二进制等格式)
  3 + *
  4 + * @see cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer
  5 + */
  6 +package cn.iocoder.yudao.module.iot.gateway.serialize;
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageService.java
1 1 package cn.iocoder.yudao.module.iot.gateway.service.device.message;
2 2  
  3 +import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum;
3 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
4 5  
5 6 /**
... ... @@ -10,45 +11,45 @@ import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
10 11 public interface IotDeviceMessageService {
11 12  
12 13 /**
13   - * 编码消息
  14 + * 序列化消息
14 15 *
15 16 * @param message 消息
16 17 * @param productKey 产品 Key
17 18 * @param deviceName 设备名称
18   - * @return 编码后的消息内容
  19 + * @return 序列化后的消息内容
19 20 */
20   - byte[] encodeDeviceMessage(IotDeviceMessage message,
21   - String productKey, String deviceName);
  21 + byte[] serializeDeviceMessage(IotDeviceMessage message,
  22 + String productKey, String deviceName);
22 23  
23 24 /**
24   - * 编码消息
  25 + * 序列化消息
25 26 *
26   - * @param message 消息
27   - * @param codecType 编解码器类型
28   - * @return 编码后的消息内容
  27 + * @param message 消息
  28 + * @param serializeType 序列化类型
  29 + * @return 序列化后的消息内容
29 30 */
30   - byte[] encodeDeviceMessage(IotDeviceMessage message,
31   - String codecType);
  31 + byte[] serializeDeviceMessage(IotDeviceMessage message,
  32 + IotSerializeTypeEnum serializeType);
32 33  
33 34 /**
34   - * 解码消息
  35 + * 反序列化消息
35 36 *
36 37 * @param bytes 消息内容
37 38 * @param productKey 产品 Key
38 39 * @param deviceName 设备名称
39   - * @return 解码后的消息内容
  40 + * @return 反序列化后的消息内容
40 41 */
41   - IotDeviceMessage decodeDeviceMessage(byte[] bytes,
42   - String productKey, String deviceName);
  42 + IotDeviceMessage deserializeDeviceMessage(byte[] bytes,
  43 + String productKey, String deviceName);
43 44  
44 45 /**
45   - * 解码消息
  46 + * 反序列化消息
46 47 *
47   - * @param bytes 消息内容
48   - * @param codecType 编解码器类型
49   - * @return 解码后的消息内容
  48 + * @param bytes 消息内容
  49 + * @param serializeType 序列化类型
  50 + * @return 反序列化后的消息内容
50 51 */
51   - IotDeviceMessage decodeDeviceMessage(byte[] bytes, String codecType);
  52 + IotDeviceMessage deserializeDeviceMessage(byte[] bytes, IotSerializeTypeEnum serializeType);
52 53  
53 54 /**
54 55 * 发送消息
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageServiceImpl.java
1 1 package cn.iocoder.yudao.module.iot.gateway.service.device.message;
2 2  
  3 +import cn.hutool.core.lang.Assert;
3 4 import cn.hutool.core.util.StrUtil;
4   -import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
5 5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
  6 +import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum;
6 7 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
7 8 import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer;
8 9 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
9   -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
  10 +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
  11 +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager;
10 12 import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
11 13 import jakarta.annotation.Resource;
12 14 import lombok.extern.slf4j.Slf4j;
13 15 import org.springframework.stereotype.Service;
14 16  
15 17 import java.time.LocalDateTime;
16   -import java.util.List;
17   -import java.util.Map;
18 18  
19 19 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
20 20 import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_NOT_EXISTS;
... ... @@ -28,80 +28,70 @@ import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVIC
28 28 @Slf4j
29 29 public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
30 30  
31   - /**
32   - * 编解码器
33   - */
34   - private final Map<String, IotDeviceMessageCodec> codes;
35   -
36 31 @Resource
37 32 private IotDeviceService deviceService;
38 33  
39 34 @Resource
40 35 private IotDeviceMessageProducer deviceMessageProducer;
41 36  
42   - public IotDeviceMessageServiceImpl(List<IotDeviceMessageCodec> codes) {
43   - this.codes = CollectionUtils.convertMap(codes, IotDeviceMessageCodec::type);
44   - }
  37 + @Resource
  38 + private IotMessageSerializerManager messageSerializerManager;
45 39  
46 40 @Override
47   - public byte[] encodeDeviceMessage(IotDeviceMessage message,
48   - String productKey, String deviceName) {
  41 + public byte[] serializeDeviceMessage(IotDeviceMessage message,
  42 + String productKey, String deviceName) {
49 43 // 1.1 获取设备信息
50 44 IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceName);
51 45 if (device == null) {
52 46 throw exception(DEVICE_NOT_EXISTS, productKey, deviceName);
53 47 }
54   - // 1.2 获取编解码器
55   - IotDeviceMessageCodec codec = codes.get(device.getCodecType());
56   - if (codec == null) {
57   - throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", device.getCodecType()));
58   - }
  48 + // 1.2 获取序列化器
  49 + IotSerializeTypeEnum serializeType = IotSerializeTypeEnum.of(device.getSerializeType());
  50 + Assert.notNull(serializeType, "设备序列化类型不能为空");
59 51  
60   - // 2. 编码消息
61   - return codec.encode(message);
  52 + // 2. 序列化消息
  53 + return serializeDeviceMessage(message, serializeType);
62 54 }
63 55  
64 56 @Override
65   - public byte[] encodeDeviceMessage(IotDeviceMessage message,
66   - String codecType) {
67   - // 1. 获取编解码器
68   - IotDeviceMessageCodec codec = codes.get(codecType);
69   - if (codec == null) {
70   - throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", codecType));
  57 + public byte[] serializeDeviceMessage(IotDeviceMessage message,
  58 + IotSerializeTypeEnum serializeType) {
  59 + // 1. 获取序列化器
  60 + IotMessageSerializer serializer = messageSerializerManager.get(serializeType);
  61 + if (serializer == null) {
  62 + throw new IllegalArgumentException(StrUtil.format("序列化器({}) 不存在", serializeType));
71 63 }
72 64  
73   - // 2. 编码消息
74   - return codec.encode(message);
  65 + // 2. 序列化消息
  66 + return serializer.serialize(message);
75 67 }
76 68  
77 69 @Override
78   - public IotDeviceMessage decodeDeviceMessage(byte[] bytes,
79   - String productKey, String deviceName) {
  70 + public IotDeviceMessage deserializeDeviceMessage(byte[] bytes,
  71 + String productKey, String deviceName) {
80 72 // 1.1 获取设备信息
81 73 IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceName);
82 74 if (device == null) {
83 75 throw exception(DEVICE_NOT_EXISTS, productKey, deviceName);
84 76 }
85   - // 1.2 获取编解码器
86   - IotDeviceMessageCodec codec = codes.get(device.getCodecType());
87   - if (codec == null) {
88   - throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", device.getCodecType()));
89   - }
  77 + // 1.2 获取序列化器
  78 + IotSerializeTypeEnum serializeType = IotSerializeTypeEnum.of(device.getSerializeType());
  79 + Assert.notNull(serializeType, "设备序列化类型不能为空");
90 80  
91   - // 2. 解码消息
92   - return codec.decode(bytes);
  81 + // 2. 反序列化消息
  82 + return deserializeDeviceMessage(bytes, serializeType);
93 83 }
94 84  
95 85 @Override
96   - public IotDeviceMessage decodeDeviceMessage(byte[] bytes, String codecType) {
97   - // 1. 获取编解码器
98   - IotDeviceMessageCodec codec = codes.get(codecType);
99   - if (codec == null) {
100   - throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", codecType));
  86 + public IotDeviceMessage deserializeDeviceMessage(byte[] bytes, IotSerializeTypeEnum serializeType) {
  87 + // 1. 获取序列化器
  88 + IotMessageSerializer serializer = messageSerializerManager.get(serializeType);
  89 + if (serializer == null) {
  90 + throw new IllegalArgumentException(StrUtil.format("序列化器({}) 不存在", serializeType));
101 91 }
102 92  
103   - // 2. 解码消息
104   - return codec.decode(bytes);
  93 + // 2. 反序列化消息
  94 + return serializer.deserialize(bytes);
105 95 }
106 96  
107 97 @Override
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/util/IotMqttTopicUtils.java
... ... @@ -38,6 +38,12 @@ public final class IotMqttTopicUtils {
38 38 */
39 39 public static final String MQTT_EVENT_PATH = "/mqtt/event";
40 40  
  41 + /**
  42 + * MQTT ACL 接口路径
  43 + * 对应 EMQX HTTP ACL 插件的 ACL 请求接口
  44 + */
  45 + public static final String MQTT_ACL_PATH = "/mqtt/acl";
  46 +
41 47 // ========== 工具方法 ==========
42 48  
43 49 /**
... ... @@ -63,4 +69,50 @@ public final class IotMqttTopicUtils {
63 69 return SYS_TOPIC_PREFIX + productKey + "/" + deviceName + "/" + topicSuffix;
64 70 }
65 71  
66   -}
67 72 \ No newline at end of file
  73 + /**
  74 + * 校验主题是否允许订阅
  75 + * <p>
  76 + * 规则:主题必须以 /sys/{productKey}/{deviceName}/ 开头,
  77 + * 或者是通配符形式 /sys/{productKey}/{deviceName}/#
  78 + *
  79 + * @param topic 订阅的主题
  80 + * @param productKey 产品 Key
  81 + * @param deviceName 设备名称
  82 + * @return 是否允许订阅
  83 + */
  84 + public static boolean isTopicSubscribeAllowed(String topic, String productKey, String deviceName) {
  85 + if (!StrUtil.isAllNotBlank(topic, productKey, deviceName)) {
  86 + return false;
  87 + }
  88 + // 构建设备主题前缀
  89 + String deviceTopicPrefix = SYS_TOPIC_PREFIX + productKey + "/" + deviceName + "/";
  90 + // 主题必须以设备前缀开头,或者是设备前缀的通配符形式
  91 + return topic.startsWith(deviceTopicPrefix)
  92 + || topic.equals(SYS_TOPIC_PREFIX + productKey + "/" + deviceName + "/#");
  93 + }
  94 +
  95 + /**
  96 + * 校验主题是否允许发布
  97 + * <p>
  98 + * 规则:主题必须以 /sys/{productKey}/{deviceName}/ 开头,且不允许包含通配符(+/#)。
  99 + *
  100 + * @param topic 发布的主题
  101 + * @param productKey 产品 Key
  102 + * @param deviceName 设备名称
  103 + * @return 是否允许发布
  104 + */
  105 + // TODO DONE @AI:这个逻辑,是不是 mqtt 协议,也要使用???答:是通用工具方法,MQTT 协议可按需调用;
  106 + // TODO @AI:那你改下 mqtt,也调用!!!
  107 + public static boolean isTopicPublishAllowed(String topic, String productKey, String deviceName) {
  108 + if (!StrUtil.isAllNotBlank(topic, productKey, deviceName)) {
  109 + return false;
  110 + }
  111 + // MQTT publish topic 不允许包含通配符,但这里做一次兜底校验
  112 + if (topic.contains("#") || topic.contains("+")) {
  113 + return false;
  114 + }
  115 + String deviceTopicPrefix = SYS_TOPIC_PREFIX + productKey + "/" + deviceName + "/";
  116 + return topic.startsWith(deviceTopicPrefix);
  117 + }
  118 +
  119 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml
... ... @@ -48,7 +48,7 @@ yudao:
48 48 # 针对引入的 HTTP 组件的配置
49 49 # ====================================
50 50 - id: http-json
51   - type: http
  51 + protocol: http
52 52 port: 8092
53 53 enabled: false
54 54 http:
... ... @@ -57,9 +57,9 @@ yudao:
57 57 # 针对引入的 TCP 组件的配置
58 58 # ====================================
59 59 - id: tcp-json
60   - type: tcp
61   - port: 8091
62 60 enabled: false
  61 + protocol: tcp
  62 + port: 8091
63 63 serialize: json
64 64 tcp:
65 65 max-connections: 1000
... ... @@ -79,9 +79,9 @@ yudao:
79 79 # 针对引入的 UDP 组件的配置
80 80 # ====================================
81 81 - id: udp-json
82   - type: udp
83   - port: 8093
84 82 enabled: false
  83 + protocol: udp
  84 + port: 8093
85 85 serialize: json
86 86 udp:
87 87 max-sessions: 1000 # 最大会话数
... ... @@ -92,9 +92,9 @@ yudao:
92 92 # 针对引入的 WebSocket 组件的配置
93 93 # ====================================
94 94 - id: websocket-json
95   - type: websocket
  95 + enabled: false
  96 + protocol: websocket
96 97 port: 8094
97   - enabled: true
98 98 serialize: json
99 99 websocket:
100 100 path: /ws
... ... @@ -106,21 +106,68 @@ yudao:
106 106 # 针对引入的 CoAP 组件的配置
107 107 # ====================================
108 108 - id: coap-json
109   - type: coap
  109 + enabled: false
  110 + protocol: coap
110 111 port: 5683
111   - enabled: true
112 112 coap:
113 113 max-message-size: 1024 # 最大消息大小(字节)
114 114 ack-timeout-ms: 2000 # ACK 超时时间(毫秒)
115 115 max-retransmit: 4 # 最大重传次数
116   -
117   - # 协议配置(旧版,保持兼容)
118   - protocol:
  116 + # ====================================
  117 + # 针对引入的 MQTT 组件的配置
  118 + # ====================================
  119 + - id: mqtt-json
  120 + enabled: true
  121 + protocol: mqtt
  122 + port: 1883
  123 + serialize: json
  124 + mqtt:
  125 + max-message-size: 8192 # 最大消息大小(字节)
  126 + connect-timeout-seconds: 60 # 连接超时时间(秒)
  127 + ssl-enabled: false # 是否启用 SSL
119 128 # ====================================
120 129 # 针对引入的 EMQX 组件的配置
121 130 # ====================================
122   - emqx:
  131 + - id: emqx-1
123 132 enabled: false
  133 + protocol: emqx
  134 + port: 8090 # EMQX HTTP Hook 端口(/mqtt/auth、/mqtt/event)
  135 + emqx:
  136 + mqtt-host: 127.0.0.1 # MQTT Broker 地址
  137 + mqtt-port: 1883 # MQTT Broker 端口
  138 + mqtt-username: admin # MQTT 用户名
  139 + mqtt-password: public # MQTT 密码
  140 + mqtt-client-id: iot-gateway-mqtt # MQTT 客户端 ID
  141 + mqtt-ssl: false # 是否开启 SSL
  142 + mqtt-topics:
  143 + - "/sys/#" # 系统主题
  144 + mqtt-qos: 1 # 默认 QoS
  145 + clean-session: true # 是否启用 Clean Session (默认: true)
  146 + keep-alive-interval-seconds: 60 # 心跳间隔,单位秒 (默认: 60)
  147 + max-inflight-queue: 10000 # 最大飞行消息队列,单位:条
  148 + connect-timeout-seconds: 10 # 连接超时,单位:秒
  149 + reconnect-delay-ms: 5000 # 重连延迟,单位:毫秒
  150 + # 是否信任所有 SSL 证书 (默认: false)。警告:生产环境必须为 false!
  151 + # 仅在开发环境或内网测试时,如果使用了自签名证书,可以临时设置为 true
  152 + trust-all: true # 在 dev 环境可以设为 true
  153 + # EMQX HTTP Hook 回调网关的 HTTPS 配置(可选)
  154 + http:
  155 + ssl-enabled: false
  156 + # ssl-cert-path: "path/to/server.crt"
  157 + # ssl-key-path: "path/to/server.key"
  158 + # 遗嘱消息配置 (用于网关异常下线时通知其他系统)
  159 + will:
  160 + enabled: true # 生产环境强烈建议开启
  161 + topic: "gateway/status/iot-gateway-mqtt" # 遗嘱消息主题
  162 + payload: "offline" # 遗嘱消息负载
  163 + qos: 1 # 遗嘱消息 QoS
  164 + retain: true # 遗嘱消息是否保留
  165 + # 高级 SSL/TLS 配置 (当 trust-all: false 且 mqtt-ssl: true 时生效)
  166 + ssl-options:
  167 + key-store-path: "classpath:certs/client.jks" # 客户端证书库路径
  168 + key-store-password: "your-keystore-password" # 客户端证书库密码
  169 + trust-store-path: "classpath:certs/trust.jks" # 信任的 CA 证书库路径
  170 + trust-store-password: "your-truststore-password" # 信任的 CA 证书库密码
124 171 http-port: 8090 # MQTT HTTP 服务端口
125 172 mqtt-host: 127.0.0.1 # MQTT Broker 地址
126 173 mqtt-port: 1883 # MQTT Broker 端口
... ... @@ -153,25 +200,8 @@ yudao:
153 200 # ====================================
154 201 # 针对引入的 MQTT 组件的配置
155 202 # ====================================
156   - mqtt:
157   - enabled: false
158   - port: 1883
159   - max-message-size: 8192
160   - connect-timeout-seconds: 60
161   - ssl-enabled: false
162   - # ====================================
163   - # 针对引入的 MQTT WebSocket 组件的配置
164   - # ====================================
165   - mqtt-ws:
166   - enabled: true # 是否启用 MQTT WebSocket
167   - port: 8083 # WebSocket 服务端口
168   - path: /mqtt # WebSocket 路径
169   - max-message-size: 8192 # 最大消息大小(字节)
170   - max-frame-size: 65536 # 最大帧大小(字节)
171   - connect-timeout-seconds: 60 # 连接超时时间(秒)
172   - keep-alive-timeout-seconds: 300 # 保持连接超时时间(秒)
173   - ssl-enabled: false # 是否启用 SSL(wss://)
174   - sub-protocol: mqtt # WebSocket 子协议
  203 + # 协议配置(旧版,保持兼容)
  204 + protocol:
175 205 modbus-tcp:
176 206 enabled: true
177 207 config-refresh-interval: 30 # 配置刷新间隔(秒)
... ... @@ -194,7 +224,6 @@ logging:
194 224 cn.iocoder.yudao.module.iot.gateway.protocol.emqx: DEBUG
195 225 cn.iocoder.yudao.module.iot.gateway.protocol.http: DEBUG
196 226 cn.iocoder.yudao.module.iot.gateway.protocol.mqtt: DEBUG
197   - cn.iocoder.yudao.module.iot.gateway.protocol.mqttws: DEBUG
198 227 cn.iocoder.yudao.module.iot.gateway.protocol.coap: DEBUG
199 228 cn.iocoder.yudao.module.iot.gateway.protocol.websocket: DEBUG
200 229 # 根日志级别
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotDirectDeviceCoapProtocolIntegrationTest.java
... ... @@ -9,6 +9,7 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
9 9 import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
10 10 import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
11 11 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  12 +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils;
12 13 import lombok.extern.slf4j.Slf4j;
13 14 import org.eclipse.californium.core.CoapClient;
14 15 import org.eclipse.californium.core.CoapResponse;
... ... @@ -203,10 +204,13 @@ public class IotDirectDeviceCoapProtocolIntegrationTest {
203 204 // 1.1 构建请求
204 205 String uri = String.format("coap://%s:%d/auth/register/device", SERVER_HOST, SERVER_PORT);
205 206 // 1.2 构建请求参数
206   - IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO();
207   - reqDTO.setProductKey(PRODUCT_KEY);
208   - reqDTO.setDeviceName("test-" + System.currentTimeMillis());
209   - reqDTO.setProductSecret("test-product-secret");
  207 + String deviceName = "test-" + System.currentTimeMillis();
  208 + String productSecret = "test-product-secret"; // 替换为实际的 productSecret
  209 + String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret);
  210 + IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO()
  211 + .setProductKey(PRODUCT_KEY)
  212 + .setDeviceName(deviceName)
  213 + .setSign(sign);
210 214 String payload = JsonUtils.toJsonString(reqDTO);
211 215 // 1.3 输出请求
212 216 log.info("[testDeviceRegister][请求 URI: {}]", uri);
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/package-info.java 0 → 100644
  1 +/**
  2 + * IoT 网关 EMQX 协议集成测试包
  3 + *
  4 + * <p>
  5 + * 测试类直接使用 mqtt 包下的单测即可,因为设备都是通过 MQTT 协议连接 EMQX Broker。
  6 + *
  7 + * @see cn.iocoder.yudao.module.iot.gateway.protocol.mqtt
  8 + *
  9 + * <h2>架构</h2>
  10 + * <pre>
  11 + * +--------+ MQTT +-------------+ HTTP Hook +---------+
  12 + * | 设备 | --------------> | EMQX Broker | ----------------> | 网关 |
  13 + * +--------+ +-------------+ +---------+
  14 + * </pre>
  15 + *
  16 + * @author 芋道源码
  17 + */
  18 +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx;
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotDirectDeviceHttpProtocolIntegrationTest.java
... ... @@ -10,6 +10,7 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
10 10 import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
11 11 import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
12 12 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  13 +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils;
13 14 import lombok.extern.slf4j.Slf4j;
14 15 import org.junit.jupiter.api.Disabled;
15 16 import org.junit.jupiter.api.Test;
... ... @@ -158,10 +159,13 @@ public class IotDirectDeviceHttpProtocolIntegrationTest {
158 159 // 1.1 构建请求
159 160 String url = String.format("http://%s:%d/auth/register/device", SERVER_HOST, SERVER_PORT);
160 161 // 1.2 构建请求参数
  162 + String deviceName = "test-" + System.currentTimeMillis();
  163 + String productSecret = "test-product-secret"; // 替换为实际的 productSecret
  164 + String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret);
161 165 IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO()
162 166 .setProductKey(PRODUCT_KEY)
163   - .setDeviceName("test-" + System.currentTimeMillis())
164   - .setProductSecret("test-product-secret");
  167 + .setDeviceName(deviceName)
  168 + .setSign(sign);
165 169 String payload = JsonUtils.toJsonString(reqDTO);
166 170 // 1.3 输出请求
167 171 log.info("[testDeviceRegister][请求 URL: {}]", url);
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java
1 1 package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;
2 2  
3 3 import cn.hutool.core.map.MapUtil;
4   -import cn.hutool.core.util.IdUtil;
5 4 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
6 5 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
7 6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
8   -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
9 7 import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
10 8 import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
11 9 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
12   -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
13   -import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec;
  10 +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils;
  11 +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
  12 +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer;
14 13 import io.netty.handler.codec.mqtt.MqttQoS;
15 14 import io.vertx.core.Vertx;
16 15 import io.vertx.core.buffer.Buffer;
... ... @@ -23,7 +22,6 @@ import org.junit.jupiter.api.Disabled;
23 22 import org.junit.jupiter.api.Test;
24 23  
25 24 import java.util.concurrent.CompletableFuture;
26   -import java.util.concurrent.CountDownLatch;
27 25 import java.util.concurrent.TimeUnit;
28 26  
29 27 /**
... ... @@ -59,9 +57,9 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
59 57  
60 58 private static Vertx vertx;
61 59  
62   - // ===================== 编解码器(MQTT 使用 Alink 协议) =====================
  60 + // ===================== 序列化器 =====================
63 61  
64   - private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec();
  62 + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer();
65 63  
66 64 // ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询) =====================
67 65  
... ... @@ -88,39 +86,19 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
88 86 */
89 87 @Test
90 88 public void testAuth() throws Exception {
91   - CountDownLatch latch = new CountDownLatch(1);
92   -
93 89 // 1. 构建认证信息
94 90 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
95 91 log.info("[testAuth][认证信息: clientId={}, username={}, password={}]",
96 92 authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword());
97 93  
98 94 // 2. 创建客户端并连接
99   - MqttClient client = connect(authInfo);
100   - client.connect(SERVER_PORT, SERVER_HOST)
101   - .onComplete(ar -> {
102   - if (ar.succeeded()) {
103   - log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId());
104   - // 断开连接
105   - client.disconnect()
106   - .onComplete(disconnectAr -> {
107   - if (disconnectAr.succeeded()) {
108   - log.info("[testAuth][断开连接成功]");
109   - } else {
110   - log.error("[testAuth][断开连接失败]", disconnectAr.cause());
111   - }
112   - latch.countDown();
113   - });
114   - } else {
115   - log.error("[testAuth][连接失败]", ar.cause());
116   - latch.countDown();
117   - }
118   - });
119   -
120   - // 3. 等待测试完成
121   - boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS);
122   - if (!completed) {
123   - log.warn("[testAuth][测试超时]");
  95 + MqttClient client = createClient(authInfo);
  96 + try {
  97 + client.connect(SERVER_PORT, SERVER_HOST)
  98 + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  99 + log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId());
  100 + } finally {
  101 + disconnect(client);
124 102 }
125 103 }
126 104  
... ... @@ -135,27 +113,26 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
135 113 MqttClient client = connectAndAuth();
136 114 log.info("[testPropertyPost][连接认证成功]");
137 115  
138   - // 2. 订阅 _reply 主题
139   - String replyTopic = String.format("/sys/%s/%s/thing/property/post_reply", PRODUCT_KEY, DEVICE_NAME);
140   - subscribeReply(client, replyTopic);
141   -
142   - // 3. 构建属性上报消息
143   - IotDeviceMessage request = IotDeviceMessage.of(
144   - IdUtil.fastSimpleUUID(),
145   - IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(),
146   - IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
147   - .put("width", 1)
148   - .put("height", "2")
149   - .build()),
150   - null, null, null);
151   -
152   - // 4. 发布消息并等待响应
153   - String topic = String.format("/sys/%s/%s/thing/property/post", PRODUCT_KEY, DEVICE_NAME);
154   - IotDeviceMessage response = publishAndWaitReply(client, topic, request);
155   - log.info("[testPropertyPost][响应消息: {}]", response);
156   -
157   - // 5. 断开连接
158   - disconnect(client);
  116 + try {
  117 + // 2.1 构建属性上报消息
  118 + IotDeviceMessage request = IotDeviceMessage.requestOf(
  119 + IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(),
  120 + IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
  121 + .put("width", 1)
  122 + .put("height", "2")
  123 + .build()));
  124 +
  125 + // 2.2 订阅 _reply 主题
  126 + String replyTopic = String.format("/sys/%s/%s/thing/property/post_reply", PRODUCT_KEY, DEVICE_NAME);
  127 + subscribe(client, replyTopic);
  128 +
  129 + // 2.2 发布消息并等待响应
  130 + String topic = String.format("/sys/%s/%s/thing/property/post", PRODUCT_KEY, DEVICE_NAME);
  131 + IotDeviceMessage response = publishAndWaitReply(client, topic, request);
  132 + log.info("[testPropertyPost][响应消息: {}]", response);
  133 + } finally {
  134 + disconnect(client);
  135 + }
159 136 }
160 137  
161 138 // ===================== 直连设备事件上报测试 =====================
... ... @@ -169,27 +146,26 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
169 146 MqttClient client = connectAndAuth();
170 147 log.info("[testEventPost][连接认证成功]");
171 148  
172   - // 2. 订阅 _reply 主题
173   - String replyTopic = String.format("/sys/%s/%s/thing/event/post_reply", PRODUCT_KEY, DEVICE_NAME);
174   - subscribeReply(client, replyTopic);
175   -
176   - // 3. 构建事件上报消息
177   - IotDeviceMessage request = IotDeviceMessage.of(
178   - IdUtil.fastSimpleUUID(),
179   - IotDeviceMessageMethodEnum.EVENT_POST.getMethod(),
180   - IotDeviceEventPostReqDTO.of(
181   - "eat",
182   - MapUtil.<String, Object>builder().put("rice", 3).build(),
183   - System.currentTimeMillis()),
184   - null, null, null);
185   -
186   - // 4. 发布消息并等待响应
187   - String topic = String.format("/sys/%s/%s/thing/event/post", PRODUCT_KEY, DEVICE_NAME);
188   - IotDeviceMessage response = publishAndWaitReply(client, topic, request);
189   - log.info("[testEventPost][响应消息: {}]", response);
190   -
191   - // 5. 断开连接
192   - disconnect(client);
  149 + try {
  150 + // 2.1 构建事件上报消息
  151 + IotDeviceMessage request = IotDeviceMessage.requestOf(
  152 + IotDeviceMessageMethodEnum.EVENT_POST.getMethod(),
  153 + IotDeviceEventPostReqDTO.of(
  154 + "eat",
  155 + MapUtil.<String, Object>builder().put("rice", 3).build(),
  156 + System.currentTimeMillis()));
  157 +
  158 + // 2.2 订阅 _reply 主题
  159 + String replyTopic = String.format("/sys/%s/%s/thing/event/post_reply", PRODUCT_KEY, DEVICE_NAME);
  160 + subscribe(client, replyTopic);
  161 +
  162 + // 3. 发布消息并等待响应
  163 + String topic = String.format("/sys/%s/%s/thing/event/post", PRODUCT_KEY, DEVICE_NAME);
  164 + IotDeviceMessage response = publishAndWaitReply(client, topic, request);
  165 + log.info("[testEventPost][响应消息: {}]", response);
  166 + } finally {
  167 + disconnect(client);
  168 + }
193 169 }
194 170  
195 171 // ===================== 设备动态注册测试(一型一密) =====================
... ... @@ -197,37 +173,58 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
197 173 /**
198 174 * 直连设备动态注册测试(一型一密)
199 175 * <p>
200   - * 使用产品密钥(productSecret)验证身份,成功后返回设备密钥(deviceSecret)
  176 + * 认证方式:
  177 + * - clientId: 任意值 + "|authType=register|" 后缀
  178 + * - username: {deviceName}&{productKey}(与普通认证相同)
  179 + * - password: 签名(使用 productSecret 对 "deviceName" + deviceName + "productKey" + productKey 进行 HMAC-SHA256)
201 180 * <p>
202   - * 注意:此接口不需要认证
  181 + * 成功后返回设备密钥(deviceSecret),可用于后续一机一密认证
203 182 */
204 183 @Test
205 184 public void testDeviceRegister() throws Exception {
206   - // 1. 连接并认证(使用已有设备连接)
207   - MqttClient client = connectAndAuth();
208   - log.info("[testDeviceRegister][连接认证成功]");
209   -
210   - // 2.1 构建注册消息
211   - IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO();
212   - registerReqDTO.setProductKey(PRODUCT_KEY);
213   - registerReqDTO.setDeviceName("test-mqtt-" + System.currentTimeMillis());
214   - registerReqDTO.setProductSecret("test-product-secret");
215   - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(),
216   - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO, null, null, null);
217   - // 2.2 订阅 _reply 主题
218   - String replyTopic = String.format("/sys/%s/%s/thing/auth/register_reply",
219   - registerReqDTO.getProductKey(), registerReqDTO.getDeviceName());
220   - subscribeReply(client, replyTopic);
221   -
222   - // 3. 发布消息并等待响应
223   - String topic = String.format("/sys/%s/%s/thing/auth/register",
224   - registerReqDTO.getProductKey(), registerReqDTO.getDeviceName());
225   - IotDeviceMessage response = publishAndWaitReply(client, topic, request);
226   - log.info("[testDeviceRegister][响应消息: {}]", response);
227   - log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]");
228   -
229   - // 4. 断开连接
230   - disconnect(client);
  185 + // 1.1 构建注册参数
  186 + String deviceName = "test-mqtt-" + System.currentTimeMillis();
  187 + String productSecret = "test-product-secret"; // 替换为实际的 productSecret
  188 + String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret);
  189 + // 1.2 构建 MQTT 连接参数(clientId 需要添加 |authType=register| 后缀)
  190 + String clientId = IotDeviceAuthUtils.buildClientId(PRODUCT_KEY, deviceName) + "|authType=register|";
  191 + String username = IotDeviceAuthUtils.buildUsername(PRODUCT_KEY, deviceName);
  192 + log.info("[testDeviceRegister][注册参数: clientId={}, username={}, sign={}]",
  193 + clientId, username, sign);
  194 + // 1.3 创建客户端并连接(连接时服务端自动处理注册)
  195 + MqttClientOptions options = new MqttClientOptions()
  196 + .setClientId(clientId)
  197 + .setUsername(username)
  198 + .setPassword(sign)
  199 + .setCleanSession(true)
  200 + .setKeepAliveInterval(60);
  201 + MqttClient client = MqttClient.create(vertx, options);
  202 +
  203 + try {
  204 + // 2. 连接服务器(连接成功后服务端会自动处理注册并发送响应)
  205 + client.connect(SERVER_PORT, SERVER_HOST)
  206 + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  207 + log.info("[testDeviceRegister][连接成功,等待注册响应...]");
  208 +
  209 + // 3.1 设置消息处理器,接收注册响应
  210 + CompletableFuture<IotDeviceMessage> responseFuture = new CompletableFuture<>();
  211 + client.publishHandler(message -> {
  212 + log.info("[testDeviceRegister][收到响应: topic={}, payload={}]",
  213 + message.topicName(), message.payload().toString());
  214 + IotDeviceMessage response = SERIALIZER.deserialize(message.payload().getBytes());
  215 + responseFuture.complete(response);
  216 + });
  217 + // 3.2 订阅 _reply 主题
  218 + String replyTopic = String.format("/sys/%s/%s/thing/auth/register_reply", PRODUCT_KEY, deviceName);
  219 + subscribe(client, replyTopic);
  220 +
  221 + // 4. 等待注册响应
  222 + IotDeviceMessage response = responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  223 + log.info("[testDeviceRegister][注册响应: {}]", response);
  224 + log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]");
  225 + } finally {
  226 + disconnect(client);
  227 + }
231 228 }
232 229  
233 230 // ===================== 订阅下行消息测试 =====================
... ... @@ -237,44 +234,25 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
237 234 */
238 235 @Test
239 236 public void testSubscribe() throws Exception {
240   - CountDownLatch latch = new CountDownLatch(1);
241   -
242 237 // 1. 连接并认证
243 238 MqttClient client = connectAndAuth();
244 239 log.info("[testSubscribe][连接认证成功]");
245 240  
246   - // 2. 设置消息处理器
247   - client.publishHandler(message -> {
248   - log.info("[testSubscribe][收到消息: topic={}, payload={}]",
249   - message.topicName(), message.payload().toString());
250   - });
251   -
252   - // 3. 订阅下行主题
253   - String topic = String.format("/sys/%s/%s/thing/service/#", PRODUCT_KEY, DEVICE_NAME);
254   - log.info("[testSubscribe][订阅主题: {}]", topic);
255   -
256   - client.subscribe(topic, MqttQoS.AT_LEAST_ONCE.value())
257   - .onComplete(subscribeAr -> {
258   - if (subscribeAr.succeeded()) {
259   - log.info("[testSubscribe][订阅成功,等待下行消息... (30秒后自动断开)]");
260   - // 保持连接 30 秒等待消息
261   - vertx.setTimer(30000, id -> {
262   - client.disconnect()
263   - .onComplete(disconnectAr -> {
264   - log.info("[testSubscribe][断开连接]");
265   - latch.countDown();
266   - });
267   - });
268   - } else {
269   - log.error("[testSubscribe][订阅失败]", subscribeAr.cause());
270   - latch.countDown();
271   - }
272   - });
273   -
274   - // 4. 等待测试完成
275   - boolean completed = latch.await(60, TimeUnit.SECONDS);
276   - if (!completed) {
277   - log.warn("[testSubscribe][测试超时]");
  241 + try {
  242 + // 2. 设置消息处理器
  243 + client.publishHandler(message -> log.info("[testSubscribe][收到消息: topic={}, payload={}]",
  244 + message.topicName(), message.payload().toString()));
  245 +
  246 + // 3. 订阅下行主题
  247 + String topic = String.format("/sys/%s/%s/thing/service/#", PRODUCT_KEY, DEVICE_NAME);
  248 + log.info("[testSubscribe][订阅主题: {}]", topic);
  249 + subscribe(client, topic);
  250 + log.info("[testSubscribe][订阅成功,等待下行消息... (30秒后自动断开)]");
  251 +
  252 + // 4. 保持连接 30 秒等待消息
  253 + Thread.sleep(30000);
  254 + } finally {
  255 + disconnect(client);
278 256 }
279 257 }
280 258  
... ... @@ -286,7 +264,7 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
286 264 * @param authInfo 认证信息
287 265 * @return MQTT 客户端
288 266 */
289   - private MqttClient connect(IotDeviceAuthReqDTO authInfo) {
  267 + private MqttClient createClient(IotDeviceAuthReqDTO authInfo) {
290 268 MqttClientOptions options = new MqttClientOptions()
291 269 .setClientId(authInfo.getClientId())
292 270 .setUsername(authInfo.getUsername())
... ... @@ -302,44 +280,23 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
302 280 * @return 已认证的 MQTT 客户端
303 281 */
304 282 private MqttClient connectAndAuth() throws Exception {
305   - // 1. 创建客户端并连接
306 283 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
307   - MqttClient client = connect(authInfo);
308   -
309   - // 2.1 连接
310   - CompletableFuture<MqttClient> future = new CompletableFuture<>();
  284 + MqttClient client = createClient(authInfo);
311 285 client.connect(SERVER_PORT, SERVER_HOST)
312   - .onComplete(ar -> {
313   - if (ar.succeeded()) {
314   - future.complete(client);
315   - } else {
316   - future.completeExceptionally(ar.cause());
317   - }
318   - });
319   - // 2.2 等待连接结果
320   - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  286 + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  287 + return client;
321 288 }
322 289  
323 290 /**
324   - * 订阅响应主题
  291 + * 订阅主题
325 292 *
326   - * @param client MQTT 客户端
327   - * @param replyTopic 响应主题
  293 + * @param client MQTT 客户端
  294 + * @param topic 主题
328 295 */
329   - private void subscribeReply(MqttClient client, String replyTopic) throws Exception {
330   - // 1. 订阅响应主题
331   - CompletableFuture<Void> future = new CompletableFuture<>();
332   - client.subscribe(replyTopic, MqttQoS.AT_LEAST_ONCE.value())
333   - .onComplete(ar -> {
334   - if (ar.succeeded()) {
335   - log.info("[subscribeReply][订阅响应主题成功: {}]", replyTopic);
336   - future.complete(null);
337   - } else {
338   - future.completeExceptionally(ar.cause());
339   - }
340   - });
341   - // 2. 等待订阅结果
342   - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  296 + private void subscribe(MqttClient client, String topic) throws Exception {
  297 + client.subscribe(topic, MqttQoS.AT_LEAST_ONCE.value())
  298 + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  299 + log.info("[subscribe][订阅主题成功: {}]", topic);
343 300 }
344 301  
345 302 /**
... ... @@ -350,34 +307,28 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
350 307 * @param request 请求消息
351 308 * @return 响应消息
352 309 */
353   - private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) {
  310 + private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request)
  311 + throws Exception {
354 312 // 1. 设置消息处理器,接收响应
355   - CompletableFuture<IotDeviceMessage> future = new CompletableFuture<>();
  313 + CompletableFuture<IotDeviceMessage> responseFuture = new CompletableFuture<>();
356 314 client.publishHandler(message -> {
357 315 log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]",
358 316 message.topicName(), message.payload().toString());
359   - IotDeviceMessage response = CODEC.decode(message.payload().getBytes());
360   - future.complete(response);
  317 + IotDeviceMessage response = SERIALIZER.deserialize(message.payload().getBytes());
  318 + responseFuture.complete(response);
361 319 });
362 320  
363   - // 2. 编码并发布消息
364   - byte[] payload = CODEC.encode(request);
365   - log.info("[publishAndWaitReply][Codec: {}, 发送消息: topic={}, payload={}]",
366   - CODEC.type(), topic, new String(payload));
367   -
  321 + // 2. 序列化并发布消息
  322 + byte[] payload = SERIALIZER.serialize(request);
  323 + log.info("[publishAndWaitReply][Serializer: {}, 发送消息: topic={}, payload={}]",
  324 + SERIALIZER.getType(), topic, new String(payload));
368 325 client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false)
369   - .onComplete(ar -> {
370   - if (ar.succeeded()) {
371   - log.info("[publishAndWaitReply][消息发布成功,messageId={}]", ar.result());
372   - } else {
373   - log.error("[publishAndWaitReply][消息发布失败]", ar.cause());
374   - future.completeExceptionally(ar.cause());
375   - }
376   - });
377   -
378   - // 3. 等待响应(超时返回 null)
  326 + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  327 + log.info("[publishAndWaitReply][消息发布成功]");
  328 +
  329 + // 3. 等待响应
379 330 try {
380   - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  331 + return responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
381 332 } catch (Exception e) {
382 333 log.warn("[publishAndWaitReply][等待响应超时或失败]");
383 334 return null;
... ... @@ -390,19 +341,9 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
390 341 * @param client MQTT 客户端
391 342 */
392 343 private void disconnect(MqttClient client) throws Exception {
393   - // 1. 断开连接
394   - CompletableFuture<Void> future = new CompletableFuture<>();
395 344 client.disconnect()
396   - .onComplete(ar -> {
397   - if (ar.succeeded()) {
398   - log.info("[disconnect][断开连接成功]");
399   - future.complete(null);
400   - } else {
401   - future.completeExceptionally(ar.cause());
402   - }
403   - });
404   - // 2. 等待断开结果
405   - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  345 + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  346 + log.info("[disconnect][断开连接成功]");
406 347 }
407 348  
408 349 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewayDeviceMqttProtocolIntegrationTest.java
... ... @@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;
2 2  
3 3 import cn.hutool.core.collection.ListUtil;
4 4 import cn.hutool.core.map.MapUtil;
5   -import cn.hutool.core.util.IdUtil;
6 5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
7 6 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
8 7 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
... ... @@ -13,8 +12,8 @@ import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO;
13 12 import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO;
14 13 import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetReqDTO;
15 14 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
16   -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
17   -import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec;
  15 +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
  16 +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer;
18 17 import io.netty.handler.codec.mqtt.MqttQoS;
19 18 import io.vertx.core.Vertx;
20 19 import io.vertx.core.buffer.Buffer;
... ... @@ -27,10 +26,8 @@ import org.junit.jupiter.api.Disabled;
27 26 import org.junit.jupiter.api.Test;
28 27  
29 28 import java.util.Collections;
30   -import java.util.List;
31 29 import java.util.Map;
32 30 import java.util.concurrent.CompletableFuture;
33   -import java.util.concurrent.CountDownLatch;
34 31 import java.util.concurrent.TimeUnit;
35 32  
36 33 /**
... ... @@ -68,9 +65,9 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
68 65  
69 66 private static Vertx vertx;
70 67  
71   - // ===================== 编解码器(MQTT 使用 Alink 协议) =====================
  68 + // ===================== 序列化器 =====================
72 69  
73   - private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec();
  70 + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer();
74 71  
75 72 // ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) =====================
76 73  
... ... @@ -103,8 +100,6 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
103 100 */
104 101 @Test
105 102 public void testAuth() throws Exception {
106   - CountDownLatch latch = new CountDownLatch(1);
107   -
108 103 // 1. 构建认证信息
109 104 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(
110 105 GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET);
... ... @@ -112,31 +107,13 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
112 107 authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword());
113 108  
114 109 // 2. 创建客户端并连接
115   - MqttClient client = connect(authInfo);
116   - client.connect(SERVER_PORT, SERVER_HOST)
117   - .onComplete(ar -> {
118   - if (ar.succeeded()) {
119   - log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId());
120   - // 断开连接
121   - client.disconnect()
122   - .onComplete(disconnectAr -> {
123   - if (disconnectAr.succeeded()) {
124   - log.info("[testAuth][断开连接成功]");
125   - } else {
126   - log.error("[testAuth][断开连接失败]", disconnectAr.cause());
127   - }
128   - latch.countDown();
129   - });
130   - } else {
131   - log.error("[testAuth][连接失败]", ar.cause());
132   - latch.countDown();
133   - }
134   - });
135   -
136   - // 3. 等待测试完成
137   - boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS);
138   - if (!completed) {
139   - log.warn("[testAuth][测试超时]");
  110 + MqttClient client = createClient(authInfo);
  111 + try {
  112 + client.connect(SERVER_PORT, SERVER_HOST)
  113 + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  114 + log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId());
  115 + } finally {
  116 + disconnect(client);
140 117 }
141 118 }
142 119  
... ... @@ -153,36 +130,35 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
153 130 MqttClient client = connectAndAuth();
154 131 log.info("[testTopoAdd][连接认证成功]");
155 132  
156   - // 2.1 订阅 _reply 主题
157   - String replyTopic = String.format("/sys/%s/%s/thing/topo/add_reply",
158   - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
159   - subscribeReply(client, replyTopic);
160   -
161   - // 2.2 构建子设备认证信息
162   - IotDeviceAuthReqDTO subAuthInfo = IotDeviceAuthUtils.getAuthInfo(
163   - SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET);
164   - IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO()
165   - .setClientId(subAuthInfo.getClientId())
166   - .setUsername(subAuthInfo.getUsername())
167   - .setPassword(subAuthInfo.getPassword());
168   -
169   - // 2.3 构建请求消息
170   - IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO();
171   - params.setSubDevices(Collections.singletonList(subDeviceAuth));
172   - IotDeviceMessage request = IotDeviceMessage.of(
173   - IdUtil.fastSimpleUUID(),
174   - IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(),
175   - params,
176   - null, null, null);
177   -
178   - // 3. 发布消息并等待响应
179   - String topic = String.format("/sys/%s/%s/thing/topo/add",
180   - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
181   - IotDeviceMessage response = publishAndWaitReply(client, topic, request);
182   - log.info("[testTopoAdd][响应消息: {}]", response);
183   -
184   - // 4. 断开连接
185   - disconnect(client);
  133 + try {
  134 + // 2.1 构建子设备认证信息
  135 + IotDeviceAuthReqDTO subAuthInfo = IotDeviceAuthUtils.getAuthInfo(
  136 + SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET);
  137 + IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO()
  138 + .setClientId(subAuthInfo.getClientId())
  139 + .setUsername(subAuthInfo.getUsername())
  140 + .setPassword(subAuthInfo.getPassword());
  141 +
  142 + // 2.2 构建请求消息
  143 + IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO()
  144 + .setSubDevices(Collections.singletonList(subDeviceAuth));
  145 + IotDeviceMessage request = IotDeviceMessage.requestOf(
  146 + IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(),
  147 + params);
  148 +
  149 + // 2.3 订阅 _reply 主题
  150 + String replyTopic = String.format("/sys/%s/%s/thing/topo/add_reply",
  151 + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
  152 + subscribe(client, replyTopic);
  153 +
  154 + // 3. 发布消息并等待响应
  155 + String topic = String.format("/sys/%s/%s/thing/topo/add",
  156 + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
  157 + IotDeviceMessage response = publishAndWaitReply(client, topic, request);
  158 + log.info("[testTopoAdd][响应消息: {}]", response);
  159 + } finally {
  160 + disconnect(client);
  161 + }
186 162 }
187 163  
188 164 /**
... ... @@ -196,29 +172,28 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
196 172 MqttClient client = connectAndAuth();
197 173 log.info("[testTopoDelete][连接认证成功]");
198 174  
199   - // 2.1 订阅 _reply 主题
200   - String replyTopic = String.format("/sys/%s/%s/thing/topo/delete_reply",
201   - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
202   - subscribeReply(client, replyTopic);
203   -
204   - // 2.2 构建请求消息
205   - IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO();
206   - params.setSubDevices(Collections.singletonList(
207   - new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)));
208   - IotDeviceMessage request = IotDeviceMessage.of(
209   - IdUtil.fastSimpleUUID(),
210   - IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(),
211   - params,
212   - null, null, null);
213   -
214   - // 3. 发布消息并等待响应
215   - String topic = String.format("/sys/%s/%s/thing/topo/delete",
216   - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
217   - IotDeviceMessage response = publishAndWaitReply(client, topic, request);
218   - log.info("[testTopoDelete][响应消息: {}]", response);
219   -
220   - // 4. 断开连接
221   - disconnect(client);
  175 + try {
  176 + // 2.1 构建请求消息
  177 + IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO()
  178 + .setSubDevices(Collections.singletonList(
  179 + new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)));
  180 + IotDeviceMessage request = IotDeviceMessage.requestOf(
  181 + IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(),
  182 + params);
  183 +
  184 + // 2.2 订阅 _reply 主题
  185 + String replyTopic = String.format("/sys/%s/%s/thing/topo/delete_reply",
  186 + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
  187 + subscribe(client, replyTopic);
  188 +
  189 + // 3. 发布消息并等待响应
  190 + String topic = String.format("/sys/%s/%s/thing/topo/delete",
  191 + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
  192 + IotDeviceMessage response = publishAndWaitReply(client, topic, request);
  193 + log.info("[testTopoDelete][响应消息: {}]", response);
  194 + } finally {
  195 + disconnect(client);
  196 + }
222 197 }
223 198  
224 199 /**
... ... @@ -232,27 +207,26 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
232 207 MqttClient client = connectAndAuth();
233 208 log.info("[testTopoGet][连接认证成功]");
234 209  
235   - // 2.1 订阅 _reply 主题
236   - String replyTopic = String.format("/sys/%s/%s/thing/topo/get_reply",
237   - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
238   - subscribeReply(client, replyTopic);
239   -
240   - // 2.2 构建请求消息
241   - IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO();
242   - IotDeviceMessage request = IotDeviceMessage.of(
243   - IdUtil.fastSimpleUUID(),
244   - IotDeviceMessageMethodEnum.TOPO_GET.getMethod(),
245   - params,
246   - null, null, null);
247   -
248   - // 3. 发布消息并等待响应
249   - String topic = String.format("/sys/%s/%s/thing/topo/get",
250   - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
251   - IotDeviceMessage response = publishAndWaitReply(client, topic, request);
252   - log.info("[testTopoGet][响应消息: {}]", response);
253   -
254   - // 4. 断开连接
255   - disconnect(client);
  210 + try {
  211 + // 2.1 构建请求消息
  212 + IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO();
  213 + IotDeviceMessage request = IotDeviceMessage.requestOf(
  214 + IotDeviceMessageMethodEnum.TOPO_GET.getMethod(),
  215 + params);
  216 +
  217 + // 2.2 订阅 _reply 主题
  218 + String replyTopic = String.format("/sys/%s/%s/thing/topo/get_reply",
  219 + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
  220 + subscribe(client, replyTopic);
  221 +
  222 + // 3. 发布消息并等待响应
  223 + String topic = String.format("/sys/%s/%s/thing/topo/get",
  224 + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
  225 + IotDeviceMessage response = publishAndWaitReply(client, topic, request);
  226 + log.info("[testTopoGet][响应消息: {}]", response);
  227 + } finally {
  228 + disconnect(client);
  229 + }
256 230 }
257 231  
258 232 // ===================== 子设备注册测试 =====================
... ... @@ -270,29 +244,28 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
270 244 MqttClient client = connectAndAuth();
271 245 log.info("[testSubDeviceRegister][连接认证成功]");
272 246  
273   - // 2.1 订阅 _reply 主题
274   - String replyTopic = String.format("/sys/%s/%s/thing/auth/sub-device/register_reply",
275   - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
276   - subscribeReply(client, replyTopic);
277   -
278   - // 2.2 构建请求消息
279   - IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO();
280   - subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY);
281   - subDevice.setDeviceName("mougezishebei-mqtt");
282   - IotDeviceMessage request = IotDeviceMessage.of(
283   - IdUtil.fastSimpleUUID(),
284   - IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod(),
285   - Collections.singletonList(subDevice),
286   - null, null, null);
287   -
288   - // 3. 发布消息并等待响应
289   - String topic = String.format("/sys/%s/%s/thing/auth/sub-device/register",
290   - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
291   - IotDeviceMessage response = publishAndWaitReply(client, topic, request);
292   - log.info("[testSubDeviceRegister][响应消息: {}]", response);
293   -
294   - // 4. 断开连接
295   - disconnect(client);
  247 + try {
  248 + // 2.1 构建请求消息
  249 + IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO()
  250 + .setProductKey(SUB_DEVICE_PRODUCT_KEY)
  251 + .setDeviceName("mougezishebei-mqtt");
  252 + IotDeviceMessage request = IotDeviceMessage.requestOf(
  253 + IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod(),
  254 + Collections.singletonList(subDevice));
  255 +
  256 + // 2.2 订阅 _reply 主题
  257 + String replyTopic = String.format("/sys/%s/%s/thing/auth/sub-device/register_reply",
  258 + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
  259 + subscribe(client, replyTopic);
  260 +
  261 + // 3. 发布消息并等待响应
  262 + String topic = String.format("/sys/%s/%s/thing/auth/sub-device/register",
  263 + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
  264 + IotDeviceMessage response = publishAndWaitReply(client, topic, request);
  265 + log.info("[testSubDeviceRegister][响应消息: {}]", response);
  266 + } finally {
  267 + disconnect(client);
  268 + }
296 269 }
297 270  
298 271 // ===================== 批量上报测试 =====================
... ... @@ -308,64 +281,63 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
308 281 MqttClient client = connectAndAuth();
309 282 log.info("[testPropertyPackPost][连接认证成功]");
310 283  
311   - // 2.1 订阅 _reply 主题
312   - String replyTopic = String.format("/sys/%s/%s/thing/event/property/pack/post_reply",
313   - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
314   - subscribeReply(client, replyTopic);
315   -
316   - // 2.2 构建【网关设备】自身属性
317   - Map<String, Object> gatewayProperties = MapUtil.<String, Object>builder()
318   - .put("temperature", 25.5)
319   - .build();
320   -
321   - // 2.3 构建【网关设备】自身事件
322   - IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue();
323   - gatewayEvent.setValue(MapUtil.builder().put("message", "gateway started").build());
324   - gatewayEvent.setTime(System.currentTimeMillis());
325   - Map<String, IotDevicePropertyPackPostReqDTO.EventValue> gatewayEvents = MapUtil
326   - .<String, IotDevicePropertyPackPostReqDTO.EventValue>builder()
327   - .put("statusReport", gatewayEvent)
328   - .build();
329   -
330   - // 2.4 构建【网关子设备】属性
331   - Map<String, Object> subDeviceProperties = MapUtil.<String, Object>builder()
332   - .put("power", 100)
333   - .build();
334   -
335   - // 2.5 构建【网关子设备】事件
336   - IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue();
337   - subDeviceEvent.setValue(MapUtil.builder().put("errorCode", 0).build());
338   - subDeviceEvent.setTime(System.currentTimeMillis());
339   - Map<String, IotDevicePropertyPackPostReqDTO.EventValue> subDeviceEvents = MapUtil
340   - .<String, IotDevicePropertyPackPostReqDTO.EventValue>builder()
341   - .put("healthCheck", subDeviceEvent)
342   - .build();
343   -
344   - // 2.6 构建子设备数据
345   - IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData();
346   - subDeviceData.setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME));
347   - subDeviceData.setProperties(subDeviceProperties);
348   - subDeviceData.setEvents(subDeviceEvents);
349   -
350   - // 2.7 构建请求消息
351   - IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO();
352   - params.setProperties(gatewayProperties);
353   - params.setEvents(gatewayEvents);
354   - params.setSubDevices(ListUtil.of(subDeviceData));
355   - IotDeviceMessage request = IotDeviceMessage.of(
356   - IdUtil.fastSimpleUUID(),
357   - IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(),
358   - params,
359   - null, null, null);
360   -
361   - // 3. 发布消息并等待响应
362   - String topic = String.format("/sys/%s/%s/thing/event/property/pack/post",
363   - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
364   - IotDeviceMessage response = publishAndWaitReply(client, topic, request);
365   - log.info("[testPropertyPackPost][响应消息: {}]", response);
366   -
367   - // 4. 断开连接
368   - disconnect(client);
  284 + try {
  285 + // 2.1 构建【网关设备】自身属性
  286 + Map<String, Object> gatewayProperties = MapUtil.<String, Object>builder()
  287 + .put("temperature", 25.5)
  288 + .build();
  289 +
  290 + // 2.2 构建【网关设备】自身事件
  291 + IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue()
  292 + .setValue(MapUtil.builder().put("message", "gateway started").build())
  293 + .setTime(System.currentTimeMillis());
  294 + Map<String, IotDevicePropertyPackPostReqDTO.EventValue> gatewayEvents = MapUtil
  295 + .<String, IotDevicePropertyPackPostReqDTO.EventValue>builder()
  296 + .put("statusReport", gatewayEvent)
  297 + .build();
  298 +
  299 + // 2.3 构建【网关子设备】属性
  300 + Map<String, Object> subDeviceProperties = MapUtil.<String, Object>builder()
  301 + .put("power", 100)
  302 + .build();
  303 +
  304 + // 2.4 构建【网关子设备】事件
  305 + IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue()
  306 + .setValue(MapUtil.builder().put("errorCode", 0).build())
  307 + .setTime(System.currentTimeMillis());
  308 + Map<String, IotDevicePropertyPackPostReqDTO.EventValue> subDeviceEvents = MapUtil
  309 + .<String, IotDevicePropertyPackPostReqDTO.EventValue>builder()
  310 + .put("healthCheck", subDeviceEvent)
  311 + .build();
  312 +
  313 + // 2.5 构建子设备数据
  314 + IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData()
  315 + .setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME))
  316 + .setProperties(subDeviceProperties)
  317 + .setEvents(subDeviceEvents);
  318 +
  319 + // 2.6 构建请求消息
  320 + IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO()
  321 + .setProperties(gatewayProperties)
  322 + .setEvents(gatewayEvents)
  323 + .setSubDevices(ListUtil.of(subDeviceData));
  324 + IotDeviceMessage request = IotDeviceMessage.requestOf(
  325 + IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(),
  326 + params);
  327 +
  328 + // 2.7 订阅 _reply 主题
  329 + String replyTopic = String.format("/sys/%s/%s/thing/event/property/pack/post_reply",
  330 + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
  331 + subscribe(client, replyTopic);
  332 +
  333 + // 3. 发布消息并等待响应
  334 + String topic = String.format("/sys/%s/%s/thing/event/property/pack/post",
  335 + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
  336 + IotDeviceMessage response = publishAndWaitReply(client, topic, request);
  337 + log.info("[testPropertyPackPost][响应消息: {}]", response);
  338 + } finally {
  339 + disconnect(client);
  340 + }
369 341 }
370 342  
371 343 // ===================== 辅助方法 =====================
... ... @@ -376,7 +348,7 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
376 348 * @param authInfo 认证信息
377 349 * @return MQTT 客户端
378 350 */
379   - private MqttClient connect(IotDeviceAuthReqDTO authInfo) {
  351 + private MqttClient createClient(IotDeviceAuthReqDTO authInfo) {
380 352 MqttClientOptions options = new MqttClientOptions()
381 353 .setClientId(authInfo.getClientId())
382 354 .setUsername(authInfo.getUsername())
... ... @@ -392,45 +364,24 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
392 364 * @return 已认证的 MQTT 客户端
393 365 */
394 366 private MqttClient connectAndAuth() throws Exception {
395   - // 1. 创建客户端并连接
396 367 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(
397 368 GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET);
398   - MqttClient client = connect(authInfo);
399   -
400   - // 2.1 连接
401   - CompletableFuture<MqttClient> future = new CompletableFuture<>();
  369 + MqttClient client = createClient(authInfo);
402 370 client.connect(SERVER_PORT, SERVER_HOST)
403   - .onComplete(ar -> {
404   - if (ar.succeeded()) {
405   - future.complete(client);
406   - } else {
407   - future.completeExceptionally(ar.cause());
408   - }
409   - });
410   - // 2.2 等待连接结果
411   - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  371 + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  372 + return client;
412 373 }
413 374  
414 375 /**
415   - * 订阅响应主题
  376 + * 订阅主题
416 377 *
417   - * @param client MQTT 客户端
418   - * @param replyTopic 响应主题
  378 + * @param client MQTT 客户端
  379 + * @param topic 主题
419 380 */
420   - private void subscribeReply(MqttClient client, String replyTopic) throws Exception {
421   - // 1. 订阅响应主题
422   - CompletableFuture<Void> future = new CompletableFuture<>();
423   - client.subscribe(replyTopic, MqttQoS.AT_LEAST_ONCE.value())
424   - .onComplete(ar -> {
425   - if (ar.succeeded()) {
426   - log.info("[subscribeReply][订阅响应主题成功: {}]", replyTopic);
427   - future.complete(null);
428   - } else {
429   - future.completeExceptionally(ar.cause());
430   - }
431   - });
432   - // 2. 等待订阅结果
433   - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  381 + private void subscribe(MqttClient client, String topic) throws Exception {
  382 + client.subscribe(topic, MqttQoS.AT_LEAST_ONCE.value())
  383 + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  384 + log.info("[subscribe][订阅主题成功: {}]", topic);
434 385 }
435 386  
436 387 /**
... ... @@ -441,34 +392,28 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
441 392 * @param request 请求消息
442 393 * @return 响应消息
443 394 */
444   - private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) {
  395 + private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request)
  396 + throws Exception {
445 397 // 1. 设置消息处理器,接收响应
446   - CompletableFuture<IotDeviceMessage> future = new CompletableFuture<>();
  398 + CompletableFuture<IotDeviceMessage> responseFuture = new CompletableFuture<>();
447 399 client.publishHandler(message -> {
448 400 log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]",
449 401 message.topicName(), message.payload().toString());
450   - IotDeviceMessage response = CODEC.decode(message.payload().getBytes());
451   - future.complete(response);
  402 + IotDeviceMessage response = SERIALIZER.deserialize(message.payload().getBytes());
  403 + responseFuture.complete(response);
452 404 });
453 405  
454   - // 2. 编码并发布消息
455   - byte[] payload = CODEC.encode(request);
456   - log.info("[publishAndWaitReply][Codec: {}, 发送消息: topic={}, payload={}]",
457   - CODEC.type(), topic, new String(payload));
458   -
  406 + // 2. 序列化并发布消息
  407 + byte[] payload = SERIALIZER.serialize(request);
  408 + log.info("[publishAndWaitReply][Serializer: {}, 发送消息: topic={}, payload={}]",
  409 + SERIALIZER.getType(), topic, new String(payload));
459 410 client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false)
460   - .onComplete(ar -> {
461   - if (ar.succeeded()) {
462   - log.info("[publishAndWaitReply][消息发布成功,messageId={}]", ar.result());
463   - } else {
464   - log.error("[publishAndWaitReply][消息发布失败]", ar.cause());
465   - future.completeExceptionally(ar.cause());
466   - }
467   - });
468   -
469   - // 3. 等待响应(超时返回 null)
  411 + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  412 + log.info("[publishAndWaitReply][消息发布成功]");
  413 +
  414 + // 3. 等待响应
470 415 try {
471   - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  416 + return responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
472 417 } catch (Exception e) {
473 418 log.warn("[publishAndWaitReply][等待响应超时或失败]");
474 419 return null;
... ... @@ -481,19 +426,9 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
481 426 * @param client MQTT 客户端
482 427 */
483 428 private void disconnect(MqttClient client) throws Exception {
484   - // 1. 断开连接
485   - CompletableFuture<Void> future = new CompletableFuture<>();
486 429 client.disconnect()
487   - .onComplete(ar -> {
488   - if (ar.succeeded()) {
489   - log.info("[disconnect][断开连接成功]");
490   - future.complete(null);
491   - } else {
492   - future.completeExceptionally(ar.cause());
493   - }
494   - });
495   - // 2. 等待断开结果
496   - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  430 + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  431 + log.info("[disconnect][断开连接成功]");
497 432 }
498 433  
499 434 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewaySubDeviceMqttProtocolIntegrationTest.java
1 1 package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;
2 2  
3 3 import cn.hutool.core.map.MapUtil;
4   -import cn.hutool.core.util.IdUtil;
5 4 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
6 5 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
7 6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
8 7 import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
9 8 import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
10 9 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
11   -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
12   -import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec;
  10 +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
  11 +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer;
13 12 import io.netty.handler.codec.mqtt.MqttQoS;
14 13 import io.vertx.core.Vertx;
15 14 import io.vertx.core.buffer.Buffer;
... ... @@ -22,7 +21,6 @@ import org.junit.jupiter.api.Disabled;
22 21 import org.junit.jupiter.api.Test;
23 22  
24 23 import java.util.concurrent.CompletableFuture;
25   -import java.util.concurrent.CountDownLatch;
26 24 import java.util.concurrent.TimeUnit;
27 25  
28 26 /**
... ... @@ -61,9 +59,9 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest {
61 59  
62 60 private static Vertx vertx;
63 61  
64   - // ===================== 编解码器(MQTT 使用 Alink 协议) =====================
  62 + // ===================== 序列化器 =====================
65 63  
66   - private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec();
  64 + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer();
67 65  
68 66 // ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
69 67  
... ... @@ -90,39 +88,19 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest {
90 88 */
91 89 @Test
92 90 public void testAuth() throws Exception {
93   - CountDownLatch latch = new CountDownLatch(1);
94   -
95 91 // 1. 构建认证信息
96 92 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
97 93 log.info("[testAuth][认证信息: clientId={}, username={}, password={}]",
98 94 authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword());
99 95  
100 96 // 2. 创建客户端并连接
101   - MqttClient client = connect(authInfo);
102   - client.connect(SERVER_PORT, SERVER_HOST)
103   - .onComplete(ar -> {
104   - if (ar.succeeded()) {
105   - log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId());
106   - // 断开连接
107   - client.disconnect()
108   - .onComplete(disconnectAr -> {
109   - if (disconnectAr.succeeded()) {
110   - log.info("[testAuth][断开连接成功]");
111   - } else {
112   - log.error("[testAuth][断开连接失败]", disconnectAr.cause());
113   - }
114   - latch.countDown();
115   - });
116   - } else {
117   - log.error("[testAuth][连接失败]", ar.cause());
118   - latch.countDown();
119   - }
120   - });
121   -
122   - // 3. 等待测试完成
123   - boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS);
124   - if (!completed) {
125   - log.warn("[testAuth][测试超时]");
  97 + MqttClient client = createClient(authInfo);
  98 + try {
  99 + client.connect(SERVER_PORT, SERVER_HOST)
  100 + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  101 + log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId());
  102 + } finally {
  103 + disconnect(client);
126 104 }
127 105 }
128 106  
... ... @@ -138,28 +116,27 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest {
138 116 log.info("[testPropertyPost][连接认证成功]");
139 117 log.info("[testPropertyPost][子设备属性上报 - 请求实际由 Gateway 代为转发]");
140 118  
141   - // 2. 订阅 _reply 主题
142   - String replyTopic = String.format("/sys/%s/%s/thing/property/post_reply", PRODUCT_KEY, DEVICE_NAME);
143   - subscribeReply(client, replyTopic);
144   -
145   - // 3. 构建属性上报消息
146   - IotDeviceMessage request = IotDeviceMessage.of(
147   - IdUtil.fastSimpleUUID(),
148   - IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(),
149   - IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
150   - .put("power", 100)
151   - .put("status", "online")
152   - .put("temperature", 36.5)
153   - .build()),
154   - null, null, null);
155   -
156   - // 4. 发布消息并等待响应
157   - String topic = String.format("/sys/%s/%s/thing/property/post", PRODUCT_KEY, DEVICE_NAME);
158   - IotDeviceMessage response = publishAndWaitReply(client, topic, request);
159   - log.info("[testPropertyPost][响应消息: {}]", response);
160   -
161   - // 5. 断开连接
162   - disconnect(client);
  119 + try {
  120 + // 2.1 构建属性上报消息
  121 + IotDeviceMessage request = IotDeviceMessage.requestOf(
  122 + IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(),
  123 + IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
  124 + .put("power", 100)
  125 + .put("status", "online")
  126 + .put("temperature", 36.5)
  127 + .build()));
  128 +
  129 + // 2.2 订阅 _reply 主题
  130 + String replyTopic = String.format("/sys/%s/%s/thing/property/post_reply", PRODUCT_KEY, DEVICE_NAME);
  131 + subscribe(client, replyTopic);
  132 +
  133 + // 3. 发布消息并等待响应
  134 + String topic = String.format("/sys/%s/%s/thing/property/post", PRODUCT_KEY, DEVICE_NAME);
  135 + IotDeviceMessage response = publishAndWaitReply(client, topic, request);
  136 + log.info("[testPropertyPost][响应消息: {}]", response);
  137 + } finally {
  138 + disconnect(client);
  139 + }
163 140 }
164 141  
165 142 // ===================== 子设备事件上报测试 =====================
... ... @@ -174,32 +151,31 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest {
174 151 log.info("[testEventPost][连接认证成功]");
175 152 log.info("[testEventPost][子设备事件上报 - 请求实际由 Gateway 代为转发]");
176 153  
177   - // 2. 订阅 _reply 主题
178   - String replyTopic = String.format("/sys/%s/%s/thing/event/post_reply", PRODUCT_KEY, DEVICE_NAME);
179   - subscribeReply(client, replyTopic);
180   -
181   - // 3. 构建事件上报消息
182   - IotDeviceMessage request = IotDeviceMessage.of(
183   - IdUtil.fastSimpleUUID(),
184   - IotDeviceMessageMethodEnum.EVENT_POST.getMethod(),
185   - IotDeviceEventPostReqDTO.of(
186   - "alarm",
187   - MapUtil.<String, Object>builder()
188   - .put("level", "warning")
189   - .put("message", "temperature too high")
190   - .put("threshold", 40)
191   - .put("current", 42)
192   - .build(),
193   - System.currentTimeMillis()),
194   - null, null, null);
195   -
196   - // 4. 发布消息并等待响应
197   - String topic = String.format("/sys/%s/%s/thing/event/post", PRODUCT_KEY, DEVICE_NAME);
198   - IotDeviceMessage response = publishAndWaitReply(client, topic, request);
199   - log.info("[testEventPost][响应消息: {}]", response);
200   -
201   - // 5. 断开连接
202   - disconnect(client);
  154 + try {
  155 + // 2.1 构建事件上报消息
  156 + IotDeviceMessage request = IotDeviceMessage.requestOf(
  157 + IotDeviceMessageMethodEnum.EVENT_POST.getMethod(),
  158 + IotDeviceEventPostReqDTO.of(
  159 + "alarm",
  160 + MapUtil.<String, Object>builder()
  161 + .put("level", "warning")
  162 + .put("message", "temperature too high")
  163 + .put("threshold", 40)
  164 + .put("current", 42)
  165 + .build(),
  166 + System.currentTimeMillis()));
  167 +
  168 + // 2.2 订阅 _reply 主题
  169 + String replyTopic = String.format("/sys/%s/%s/thing/event/post_reply", PRODUCT_KEY, DEVICE_NAME);
  170 + subscribe(client, replyTopic);
  171 +
  172 + // 3. 发布消息并等待响应
  173 + String topic = String.format("/sys/%s/%s/thing/event/post", PRODUCT_KEY, DEVICE_NAME);
  174 + IotDeviceMessage response = publishAndWaitReply(client, topic, request);
  175 + log.info("[testEventPost][响应消息: {}]", response);
  176 + } finally {
  177 + disconnect(client);
  178 + }
203 179 }
204 180  
205 181 // ===================== 辅助方法 =====================
... ... @@ -210,7 +186,7 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest {
210 186 * @param authInfo 认证信息
211 187 * @return MQTT 客户端
212 188 */
213   - private MqttClient connect(IotDeviceAuthReqDTO authInfo) {
  189 + private MqttClient createClient(IotDeviceAuthReqDTO authInfo) {
214 190 MqttClientOptions options = new MqttClientOptions()
215 191 .setClientId(authInfo.getClientId())
216 192 .setUsername(authInfo.getUsername())
... ... @@ -226,44 +202,23 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest {
226 202 * @return 已认证的 MQTT 客户端
227 203 */
228 204 private MqttClient connectAndAuth() throws Exception {
229   - // 1. 创建客户端并连接
230 205 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
231   - MqttClient client = connect(authInfo);
232   -
233   - // 2.1 连接
234   - CompletableFuture<MqttClient> future = new CompletableFuture<>();
  206 + MqttClient client = createClient(authInfo);
235 207 client.connect(SERVER_PORT, SERVER_HOST)
236   - .onComplete(ar -> {
237   - if (ar.succeeded()) {
238   - future.complete(client);
239   - } else {
240   - future.completeExceptionally(ar.cause());
241   - }
242   - });
243   - // 2.2 等待连接结果
244   - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  208 + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  209 + return client;
245 210 }
246 211  
247 212 /**
248   - * 订阅响应主题
  213 + * 订阅主题
249 214 *
250   - * @param client MQTT 客户端
251   - * @param replyTopic 响应主题
  215 + * @param client MQTT 客户端
  216 + * @param topic 主题
252 217 */
253   - private void subscribeReply(MqttClient client, String replyTopic) throws Exception {
254   - // 1. 订阅响应主题
255   - CompletableFuture<Void> future = new CompletableFuture<>();
256   - client.subscribe(replyTopic, MqttQoS.AT_LEAST_ONCE.value())
257   - .onComplete(ar -> {
258   - if (ar.succeeded()) {
259   - log.info("[subscribeReply][订阅响应主题成功: {}]", replyTopic);
260   - future.complete(null);
261   - } else {
262   - future.completeExceptionally(ar.cause());
263   - }
264   - });
265   - // 2. 等待订阅结果
266   - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  218 + private void subscribe(MqttClient client, String topic) throws Exception {
  219 + client.subscribe(topic, MqttQoS.AT_LEAST_ONCE.value())
  220 + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  221 + log.info("[subscribe][订阅主题成功: {}]", topic);
267 222 }
268 223  
269 224 /**
... ... @@ -274,34 +229,28 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest {
274 229 * @param request 请求消息
275 230 * @return 响应消息
276 231 */
277   - private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) {
  232 + private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request)
  233 + throws Exception {
278 234 // 1. 设置消息处理器,接收响应
279   - CompletableFuture<IotDeviceMessage> future = new CompletableFuture<>();
  235 + CompletableFuture<IotDeviceMessage> responseFuture = new CompletableFuture<>();
280 236 client.publishHandler(message -> {
281 237 log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]",
282 238 message.topicName(), message.payload().toString());
283   - IotDeviceMessage response = CODEC.decode(message.payload().getBytes());
284   - future.complete(response);
  239 + IotDeviceMessage response = SERIALIZER.deserialize(message.payload().getBytes());
  240 + responseFuture.complete(response);
285 241 });
286 242  
287   - // 2. 编码并发布消息
288   - byte[] payload = CODEC.encode(request);
289   - log.info("[publishAndWaitReply][Codec: {}, 发送消息: topic={}, payload={}]",
290   - CODEC.type(), topic, new String(payload));
291   -
  243 + // 2. 序列化并发布消息
  244 + byte[] payload = SERIALIZER.serialize(request);
  245 + log.info("[publishAndWaitReply][Serializer: {}, 发送消息: topic={}, payload={}]",
  246 + SERIALIZER.getType(), topic, new String(payload));
292 247 client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false)
293   - .onComplete(ar -> {
294   - if (ar.succeeded()) {
295   - log.info("[publishAndWaitReply][消息发布成功,messageId={}]", ar.result());
296   - } else {
297   - log.error("[publishAndWaitReply][消息发布失败]", ar.cause());
298   - future.completeExceptionally(ar.cause());
299   - }
300   - });
301   -
302   - // 3. 等待响应(超时返回 null)
  248 + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  249 + log.info("[publishAndWaitReply][消息发布成功]");
  250 +
  251 + // 3. 等待响应
303 252 try {
304   - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  253 + return responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
305 254 } catch (Exception e) {
306 255 log.warn("[publishAndWaitReply][等待响应超时或失败]");
307 256 return null;
... ... @@ -314,19 +263,9 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest {
314 263 * @param client MQTT 客户端
315 264 */
316 265 private void disconnect(MqttClient client) throws Exception {
317   - // 1. 断开连接
318   - CompletableFuture<Void> future = new CompletableFuture<>();
319 266 client.disconnect()
320   - .onComplete(ar -> {
321   - if (ar.succeeded()) {
322   - log.info("[disconnect][断开连接成功]");
323   - future.complete(null);
324   - } else {
325   - future.completeExceptionally(ar.cause());
326   - }
327   - });
328   - // 2. 等待断开结果
329   - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  267 + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  268 + log.info("[disconnect][断开连接成功]");
330 269 }
331 270  
332 271 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotDirectDeviceTcpProtocolIntegrationTest.java
... ... @@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
8 8 import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
9 9 import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
10 10 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  11 +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils;
11 12 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum;
12 13 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec;
13 14 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodecFactory;
... ... @@ -146,10 +147,13 @@ public class IotDirectDeviceTcpProtocolIntegrationTest {
146 147 @Test
147 148 public void testDeviceRegister() throws Exception {
148 149 // 1. 构建注册消息
  150 + String deviceName = "test-tcp-" + System.currentTimeMillis();
  151 + String productSecret = "test-product-secret"; // 替换为实际的 productSecret
  152 + String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret);
149 153 IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO()
150 154 .setProductKey(PRODUCT_KEY)
151   - .setDeviceName("test-tcp-" + System.currentTimeMillis())
152   - .setProductSecret("test-product-secret");
  155 + .setDeviceName(deviceName)
  156 + .setSign(sign);
153 157 IotDeviceMessage request = IotDeviceMessage.requestOf(
154 158 IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO);
155 159  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotDirectDeviceUdpProtocolIntegrationTest.java
... ... @@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
8 8 import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
9 9 import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
10 10 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  11 +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils;
11 12 import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
12 13 import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer;
13 14 import lombok.extern.slf4j.Slf4j;
... ... @@ -100,10 +101,13 @@ public class IotDirectDeviceUdpProtocolIntegrationTest {
100 101 @Test
101 102 public void testDeviceRegister() throws Exception {
102 103 // 1. 构建注册消息
  104 + String deviceName = "test-udp-" + System.currentTimeMillis();
  105 + String productSecret = "test-product-secret"; // 替换为实际的 productSecret
  106 + String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret);
103 107 IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO()
104 108 .setProductKey(PRODUCT_KEY)
105   - .setDeviceName("test-udp-" + System.currentTimeMillis())
106   - .setProductSecret("test-product-secret");
  109 + .setDeviceName(deviceName)
  110 + .setSign(sign);
107 111 IotDeviceMessage request = IotDeviceMessage.requestOf(
108 112 IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO);
109 113  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotDirectDeviceWebSocketProtocolIntegrationTest.java
... ... @@ -10,6 +10,7 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
10 10 import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
11 11 import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
12 12 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  13 +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils;
13 14 import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
14 15 import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer;
15 16 import io.vertx.core.Vertx;
... ... @@ -131,10 +132,13 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest {
131 132 @Test
132 133 public void testDeviceRegister() throws Exception {
133 134 // 1.1 构建注册消息
134   - IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO();
135   - registerReqDTO.setProductKey(PRODUCT_KEY);
136   - registerReqDTO.setDeviceName("test-ws-" + System.currentTimeMillis());
137   - registerReqDTO.setProductSecret("test-product-secret");
  135 + String deviceName = "test-ws-" + System.currentTimeMillis();
  136 + String productSecret = "test-product-secret"; // 替换为实际的 productSecret
  137 + String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret);
  138 + IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO()
  139 + .setProductKey(PRODUCT_KEY)
  140 + .setDeviceName(deviceName)
  141 + .setSign(sign);
138 142 IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(),
139 143 IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO, null, null, null);
140 144 // 1.2 序列化
... ...