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,7 +75,7 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi {
75 return success(BeanUtils.toBean(device, IotDeviceRespDTO.class, deviceDTO -> { 75 return success(BeanUtils.toBean(device, IotDeviceRespDTO.class, deviceDTO -> {
76 IotProductDO product = productService.getProductFromCache(deviceDTO.getProductId()); 76 IotProductDO product = productService.getProductFromCache(deviceDTO.getProductId());
77 if (product != null) { 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,10 +67,15 @@ public class IotProductRespVO {
67 @DictFormat(DictTypeConstants.NET_TYPE) 67 @DictFormat(DictTypeConstants.NET_TYPE)
68 private Integer netType; 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 @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) 80 @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
76 @ExcelProperty("创建时间") 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 package cn.iocoder.yudao.module.iot.controller.admin.product.vo.product; 1 package cn.iocoder.yudao.module.iot.controller.admin.product.vo.product;
2 2
3 import cn.iocoder.yudao.framework.common.validation.InEnum; 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 import cn.iocoder.yudao.module.iot.enums.product.IotNetTypeEnum; 6 import cn.iocoder.yudao.module.iot.enums.product.IotNetTypeEnum;
5 import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum; 7 import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum;
6 import io.swagger.v3.oas.annotations.media.Schema; 8 import io.swagger.v3.oas.annotations.media.Schema;
@@ -44,9 +46,15 @@ public class IotProductSaveReqVO { @@ -44,9 +46,15 @@ public class IotProductSaveReqVO {
44 @InEnum(value = IotNetTypeEnum.class, message = "联网方式必须是 {value}") 46 @InEnum(value = IotNetTypeEnum.class, message = "联网方式必须是 {value}")
45 private Integer netType; 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 @Schema(description = "是否开启动态注册", example = "false") 59 @Schema(description = "是否开启动态注册", example = "false")
52 @NotNull(message = "是否开启动态注册不能为空") 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,12 +78,16 @@ public class IotProductDO extends TenantBaseDO {
78 */ 78 */
79 private Integer netType; 79 private Integer netType;
80 /** 80 /**
81 - * 数据格式(编解码器类型) 81 + * 协议类型
82 * <p> 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 \ No newline at end of file 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 +8,8 @@ package cn.iocoder.yudao.module.iot.enums;
8 public class DictTypeConstants { 8 public class DictTypeConstants {
9 9
10 public static final String NET_TYPE = "iot_net_type"; 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 public static final String PRODUCT_STATUS = "iot_product_status"; 14 public static final String PRODUCT_STATUS = "iot_product_status";
15 public static final String PRODUCT_DEVICE_TYPE = "iot_product_device_type"; 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,6 +29,7 @@ import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoChangeReqDTO;
29 import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO; 29 import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO;
30 import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetRespDTO; 30 import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetRespDTO;
31 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; 31 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  32 +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils;
32 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; 33 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
33 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceGroupDO; 34 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceGroupDO;
34 import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; 35 import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
@@ -819,8 +820,9 @@ public class IotDeviceServiceImpl implements IotDeviceService { @@ -819,8 +820,9 @@ public class IotDeviceServiceImpl implements IotDeviceService {
819 if (BooleanUtil.isFalse(product.getRegisterEnabled())) { 820 if (BooleanUtil.isFalse(product.getRegisterEnabled())) {
820 throw exception(DEVICE_REGISTER_DISABLED); 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 throw exception(DEVICE_REGISTER_SECRET_INVALID); 826 throw exception(DEVICE_REGISTER_SECRET_INVALID);
825 } 827 }
826 return TenantUtils.execute(product.getTenantId(), () -> { 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,8 +34,12 @@ public class IotDeviceRespDTO {
34 */ 34 */
35 private Long productId; 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 \ No newline at end of file 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,9 +27,11 @@ public class IotDeviceRegisterReqDTO {
27 private String deviceName; 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 \ No newline at end of file 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 \ No newline at end of file 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,6 +17,15 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttDownstreamSubscr
17 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol; 17 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol;
18 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; 18 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
19 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler; 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 import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; 29 import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager;
21 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; 30 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
22 import io.vertx.core.Vertx; 31 import io.vertx.core.Vertx;
@@ -24,14 +33,18 @@ import lombok.extern.slf4j.Slf4j; @@ -24,14 +33,18 @@ import lombok.extern.slf4j.Slf4j;
24 import org.redisson.api.RedissonClient; 33 import org.redisson.api.RedissonClient;
25 import org.springframework.beans.factory.annotation.Qualifier; 34 import org.springframework.beans.factory.annotation.Qualifier;
26 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 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 import org.springframework.boot.context.properties.EnableConfigurationProperties; 42 import org.springframework.boot.context.properties.EnableConfigurationProperties;
28 import org.springframework.context.annotation.Bean; 43 import org.springframework.context.annotation.Bean;
29 import org.springframework.context.annotation.Configuration; 44 import org.springframework.context.annotation.Configuration;
30 45
31 -  
32 @Configuration 46 @Configuration
33 @EnableConfigurationProperties(IotGatewayProperties.class) 47 @EnableConfigurationProperties(IotGatewayProperties.class)
34 -@Slf4j  
35 public class IotGatewayConfiguration { 48 public class IotGatewayConfiguration {
36 49
37 @Bean 50 @Bean
@@ -45,75 +58,6 @@ public class IotGatewayConfiguration { @@ -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 * IoT 网关 Modbus TCP 协议配置类 61 * IoT 网关 Modbus TCP 协议配置类
118 */ 62 */
119 @Configuration 63 @Configuration
@@ -188,8 +132,8 @@ public class IotGatewayConfiguration { @@ -188,8 +132,8 @@ public class IotGatewayConfiguration {
188 132
189 @Bean 133 @Bean
190 public IotModbusTcpDownstreamSubscriber iotModbusTcpDownstreamSubscriber(IotModbusTcpUpstreamProtocol upstreamProtocol, 134 public IotModbusTcpDownstreamSubscriber iotModbusTcpDownstreamSubscriber(IotModbusTcpUpstreamProtocol upstreamProtocol,
191 - IotModbusTcpDownstreamHandler downstreamHandler,  
192 - IotMessageBus messageBus) { 135 + IotModbusTcpDownstreamHandler downstreamHandler,
  136 + IotMessageBus messageBus) {
193 return new IotModbusTcpDownstreamSubscriber(upstreamProtocol, downstreamHandler, messageBus); 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,12 +2,12 @@ package cn.iocoder.yudao.module.iot.gateway.config;
2 2
3 import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; 3 import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
4 import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapConfig; 4 import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapConfig;
  5 +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxConfig;
5 import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpConfig; 6 import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpConfig;
  7 +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttConfig;
6 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig; 8 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig;
7 import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpConfig; 9 import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpConfig;
8 import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketConfig; 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 import jakarta.validation.Valid; 11 import jakarta.validation.Valid;
12 import jakarta.validation.constraints.NotEmpty; 12 import jakarta.validation.constraints.NotEmpty;
13 import jakarta.validation.constraints.NotNull; 13 import jakarta.validation.constraints.NotNull;
@@ -33,14 +33,9 @@ public class IotGatewayProperties { @@ -33,14 +33,9 @@ public class IotGatewayProperties {
33 private TokenProperties token; 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 @Data 40 @Data
46 public static class RpcProperties { 41 public static class RpcProperties {
@@ -79,367 +74,155 @@ public class IotGatewayProperties { @@ -79,367 +74,155 @@ public class IotGatewayProperties {
79 74
80 } 75 }
81 76
  77 + /**
  78 + * 协议实例配置
  79 + */
82 @Data 80 @Data
83 public static class ProtocolProperties { 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 * <p> 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 @Data 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 @Data 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,7 +5,9 @@ import cn.hutool.core.util.BooleanUtil;
5 import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; 5 import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
6 import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; 6 import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
7 import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol; 7 import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol;
  8 +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol;
8 import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol; 9 import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol;
  10 +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttProtocol;
9 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpProtocol; 11 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpProtocol;
10 import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpProtocol; 12 import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpProtocol;
11 import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketProtocol; 13 import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketProtocol;
@@ -43,13 +45,13 @@ public class IotProtocolManager implements SmartLifecycle { @@ -43,13 +45,13 @@ public class IotProtocolManager implements SmartLifecycle {
43 if (running) { 45 if (running) {
44 return; 46 return;
45 } 47 }
46 - List<IotGatewayProperties.ProtocolInstanceProperties> protocolConfigs = gatewayProperties.getProtocols(); 48 + List<IotGatewayProperties.ProtocolProperties> protocolConfigs = gatewayProperties.getProtocols();
47 if (CollUtil.isEmpty(protocolConfigs)) { 49 if (CollUtil.isEmpty(protocolConfigs)) {
48 log.info("[start][没有配置协议实例,跳过启动]"); 50 log.info("[start][没有配置协议实例,跳过启动]");
49 return; 51 return;
50 } 52 }
51 53
52 - for (IotGatewayProperties.ProtocolInstanceProperties config : protocolConfigs) { 54 + for (IotGatewayProperties.ProtocolProperties config : protocolConfigs) {
53 if (BooleanUtil.isFalse(config.getEnabled())) { 55 if (BooleanUtil.isFalse(config.getEnabled())) {
54 log.info("[start][协议实例 {} 未启用,跳过]", config.getId()); 56 log.info("[start][协议实例 {} 未启用,跳过]", config.getId());
55 continue; 57 continue;
@@ -89,10 +91,10 @@ public class IotProtocolManager implements SmartLifecycle { @@ -89,10 +91,10 @@ public class IotProtocolManager implements SmartLifecycle {
89 * @return 协议实例 91 * @return 协议实例
90 */ 92 */
91 @SuppressWarnings({"EnhancedSwitchMigration"}) 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 if (protocolType == null) { 96 if (protocolType == null) {
95 - log.error("[createProtocol][协议实例 {} 的协议类型 {} 不存在]", config.getId(), config.getType()); 97 + log.error("[createProtocol][协议实例 {} 的协议类型 {} 不存在]", config.getId(), config.getProtocol());
96 return null; 98 return null;
97 } 99 }
98 switch (protocolType) { 100 switch (protocolType) {
@@ -106,6 +108,10 @@ public class IotProtocolManager implements SmartLifecycle { @@ -106,6 +108,10 @@ public class IotProtocolManager implements SmartLifecycle {
106 return createCoapProtocol(config); 108 return createCoapProtocol(config);
107 case WEBSOCKET: 109 case WEBSOCKET:
108 return createWebSocketProtocol(config); 110 return createWebSocketProtocol(config);
  111 + case MQTT:
  112 + return createMqttProtocol(config);
  113 + case EMQX:
  114 + return createEmqxProtocol(config);
109 default: 115 default:
110 throw new IllegalArgumentException(String.format( 116 throw new IllegalArgumentException(String.format(
111 "[createProtocol][协议实例 %s 的协议类型 %s 暂不支持]", config.getId(), protocolType)); 117 "[createProtocol][协议实例 %s 的协议类型 %s 暂不支持]", config.getId(), protocolType));
@@ -118,7 +124,7 @@ public class IotProtocolManager implements SmartLifecycle { @@ -118,7 +124,7 @@ public class IotProtocolManager implements SmartLifecycle {
118 * @param config 协议实例配置 124 * @param config 协议实例配置
119 * @return HTTP 协议实例 125 * @return HTTP 协议实例
120 */ 126 */
121 - private IotHttpProtocol createHttpProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { 127 + private IotHttpProtocol createHttpProtocol(IotGatewayProperties.ProtocolProperties config) {
122 return new IotHttpProtocol(config); 128 return new IotHttpProtocol(config);
123 } 129 }
124 130
@@ -128,7 +134,7 @@ public class IotProtocolManager implements SmartLifecycle { @@ -128,7 +134,7 @@ public class IotProtocolManager implements SmartLifecycle {
128 * @param config 协议实例配置 134 * @param config 协议实例配置
129 * @return TCP 协议实例 135 * @return TCP 协议实例
130 */ 136 */
131 - private IotTcpProtocol createTcpProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { 137 + private IotTcpProtocol createTcpProtocol(IotGatewayProperties.ProtocolProperties config) {
132 return new IotTcpProtocol(config); 138 return new IotTcpProtocol(config);
133 } 139 }
134 140
@@ -138,7 +144,7 @@ public class IotProtocolManager implements SmartLifecycle { @@ -138,7 +144,7 @@ public class IotProtocolManager implements SmartLifecycle {
138 * @param config 协议实例配置 144 * @param config 协议实例配置
139 * @return UDP 协议实例 145 * @return UDP 协议实例
140 */ 146 */
141 - private IotUdpProtocol createUdpProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { 147 + private IotUdpProtocol createUdpProtocol(IotGatewayProperties.ProtocolProperties config) {
142 return new IotUdpProtocol(config); 148 return new IotUdpProtocol(config);
143 } 149 }
144 150
@@ -148,7 +154,7 @@ public class IotProtocolManager implements SmartLifecycle { @@ -148,7 +154,7 @@ public class IotProtocolManager implements SmartLifecycle {
148 * @param config 协议实例配置 154 * @param config 协议实例配置
149 * @return CoAP 协议实例 155 * @return CoAP 协议实例
150 */ 156 */
151 - private IotCoapProtocol createCoapProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { 157 + private IotCoapProtocol createCoapProtocol(IotGatewayProperties.ProtocolProperties config) {
152 return new IotCoapProtocol(config); 158 return new IotCoapProtocol(config);
153 } 159 }
154 160
@@ -158,8 +164,28 @@ public class IotProtocolManager implements SmartLifecycle { @@ -158,8 +164,28 @@ public class IotProtocolManager implements SmartLifecycle {
158 * @param config 协议实例配置 164 * @param config 协议实例配置
159 * @return WebSocket 协议实例 165 * @return WebSocket 协议实例
160 */ 166 */
161 - private IotWebSocketProtocol createWebSocketProtocol(IotGatewayProperties.ProtocolInstanceProperties config) { 167 + private IotWebSocketProtocol createWebSocketProtocol(IotGatewayProperties.ProtocolProperties config) {
162 return new IotWebSocketProtocol(config); 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,7 +4,7 @@ import cn.hutool.extra.spring.SpringUtil;
4 import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; 4 import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
5 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; 5 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
6 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; 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 import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; 8 import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
9 import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.downstream.IotCoapDownstreamSubscriber; 9 import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.downstream.IotCoapDownstreamSubscriber;
10 import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapAuthHandler; 10 import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapAuthHandler;
@@ -21,7 +21,7 @@ import org.eclipse.californium.core.CoapResource; @@ -21,7 +21,7 @@ import org.eclipse.californium.core.CoapResource;
21 import org.eclipse.californium.core.CoapServer; 21 import org.eclipse.californium.core.CoapServer;
22 import org.eclipse.californium.core.config.CoapConfig; 22 import org.eclipse.californium.core.config.CoapConfig;
23 import org.eclipse.californium.elements.config.Configuration; 23 import org.eclipse.californium.elements.config.Configuration;
24 -import org.springframework.util.Assert; 24 +import cn.hutool.core.lang.Assert;
25 25
26 import java.util.concurrent.TimeUnit; 26 import java.util.concurrent.TimeUnit;
27 27
@@ -43,7 +43,7 @@ public class IotCoapProtocol implements IotProtocol { @@ -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 * 服务器 ID(用于消息追踪,全局唯一) 48 * 服务器 ID(用于消息追踪,全局唯一)
49 */ 49 */
@@ -66,7 +66,7 @@ public class IotCoapProtocol implements IotProtocol { @@ -66,7 +66,7 @@ public class IotCoapProtocol implements IotProtocol {
66 */ 66 */
67 private final IotCoapDownstreamSubscriber downstreamSubscriber; 67 private final IotCoapDownstreamSubscriber downstreamSubscriber;
68 68
69 - public IotCoapProtocol(ProtocolInstanceProperties properties) { 69 + public IotCoapProtocol(ProtocolProperties properties) {
70 IotCoapConfig coapConfig = properties.getCoap(); 70 IotCoapConfig coapConfig = properties.getCoap();
71 Assert.notNull(coapConfig, "CoAP 协议配置(coap)不能为空"); 71 Assert.notNull(coapConfig, "CoAP 协议配置(coap)不能为空");
72 this.properties = properties; 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,7 +33,7 @@ public class IotCoapRegisterHandler extends IotCoapAbstractHandler {
33 Assert.notNull(request, "请求体不能为空"); 33 Assert.notNull(request, "请求体不能为空");
34 Assert.notBlank(request.getProductKey(), "productKey 不能为空"); 34 Assert.notBlank(request.getProductKey(), "productKey 不能为空");
35 Assert.notBlank(request.getDeviceName(), "deviceName 不能为空"); 35 Assert.notBlank(request.getDeviceName(), "deviceName 不能为空");
36 - Assert.notBlank(request.getProductSecret(), "productSecret 不能为空"); 36 + Assert.notBlank(request.getSign(), "sign 不能为空");
37 37
38 // 2. 调用动态注册 38 // 2. 调用动态注册
39 CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(request); 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 \ No newline at end of file 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 \ No newline at end of file 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 import cn.hutool.core.util.StrUtil; 3 import cn.hutool.core.util.StrUtil;
4 import cn.hutool.extra.spring.SpringUtil; 4 import cn.hutool.extra.spring.SpringUtil;
5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; 5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
7 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; 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 import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; 9 import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
10 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; 10 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
11 import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; 11 import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
@@ -21,13 +21,13 @@ import lombok.extern.slf4j.Slf4j; @@ -21,13 +21,13 @@ import lombok.extern.slf4j.Slf4j;
21 @Slf4j 21 @Slf4j
22 public class IotEmqxDownstreamHandler { 22 public class IotEmqxDownstreamHandler {
23 23
24 - private final IotEmqxUpstreamProtocol protocol; 24 + private final IotEmqxProtocol protocol;
25 25
26 private final IotDeviceService deviceService; 26 private final IotDeviceService deviceService;
27 27
28 private final IotDeviceMessageService deviceMessageService; 28 private final IotDeviceMessageService deviceMessageService;
29 29
30 - public IotEmqxDownstreamHandler(IotEmqxUpstreamProtocol protocol) { 30 + public IotEmqxDownstreamHandler(IotEmqxProtocol protocol) {
31 this.protocol = protocol; 31 this.protocol = protocol;
32 this.deviceService = SpringUtil.getBean(IotDeviceService.class); 32 this.deviceService = SpringUtil.getBean(IotDeviceService.class);
33 this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); 33 this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
@@ -53,9 +53,10 @@ public class IotEmqxDownstreamHandler { @@ -53,9 +53,10 @@ public class IotEmqxDownstreamHandler {
53 return; 53 return;
54 } 54 }
55 // 2.2 构建载荷 55 // 2.2 构建载荷
56 - byte[] payload = deviceMessageService.encodeDeviceMessage(message, deviceInfo.getProductKey(), 56 + byte[] payload = deviceMessageService.serializeDeviceMessage(message, deviceInfo.getProductKey(),
57 deviceInfo.getDeviceName()); 57 deviceInfo.getDeviceName());
58 - // 2.3 发布消息 58 +
  59 + // 3. 发布消息
59 protocol.publishMessage(topic, payload); 60 protocol.publishMessage(topic, payload);
60 } 61 }
61 62
@@ -74,4 +75,4 @@ public class IotEmqxDownstreamHandler { @@ -74,4 +75,4 @@ public class IotEmqxDownstreamHandler {
74 return IotMqttTopicUtils.buildTopicByMethod(message.getMethod(), productKey, deviceName, isReply); 75 return IotMqttTopicUtils.buildTopicByMethod(message.getMethod(), productKey, deviceName, isReply);
75 } 76 }
76 77
77 -}  
78 \ No newline at end of file 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 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; 3 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
5 -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;  
6 import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; 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 import lombok.extern.slf4j.Slf4j; 7 import lombok.extern.slf4j.Slf4j;
9 8
10 /** 9 /**
@@ -17,7 +16,7 @@ public class IotEmqxDownstreamSubscriber extends IotProtocolDownstreamSubscriber @@ -17,7 +16,7 @@ public class IotEmqxDownstreamSubscriber extends IotProtocolDownstreamSubscriber
17 16
18 private final IotEmqxDownstreamHandler downstreamHandler; 17 private final IotEmqxDownstreamHandler downstreamHandler;
19 18
20 - public IotEmqxDownstreamSubscriber(IotEmqxUpstreamProtocol protocol, IotMessageBus messageBus) { 19 + public IotEmqxDownstreamSubscriber(IotEmqxProtocol protocol, IotMessageBus messageBus) {
21 super(protocol, messageBus); 20 super(protocol, messageBus);
22 this.downstreamHandler = new IotEmqxDownstreamHandler(protocol); 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 import cn.hutool.core.util.BooleanUtil; 4 import cn.hutool.core.util.BooleanUtil;
4 import cn.hutool.core.util.StrUtil; 5 import cn.hutool.core.util.StrUtil;
5 import cn.hutool.extra.spring.SpringUtil; 6 import cn.hutool.extra.spring.SpringUtil;
6 import cn.iocoder.yudao.framework.common.pojo.CommonResult; 7 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
7 import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; 8 import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
8 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; 9 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
  10 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
9 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 11 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
10 import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; 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 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; 15 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  16 +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol;
12 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; 17 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
  18 +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
13 import io.vertx.core.json.JsonObject; 19 import io.vertx.core.json.JsonObject;
14 import io.vertx.ext.web.RoutingContext; 20 import io.vertx.ext.web.RoutingContext;
15 import lombok.extern.slf4j.Slf4j; 21 import lombok.extern.slf4j.Slf4j;
16 22
  23 +import java.util.Locale;
  24 +
17 /** 25 /**
18 * IoT 网关 EMQX 认证事件处理器 26 * IoT 网关 EMQX 认证事件处理器
19 * <p> 27 * <p>
20 * 为 EMQX 提供 HTTP 接口服务,包括: 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 * @author 芋道源码 34 * @author 芋道源码
25 */ 35 */
@@ -45,30 +55,43 @@ public class IotEmqxAuthEventHandler { @@ -45,30 +55,43 @@ public class IotEmqxAuthEventHandler {
45 private static final String RESULT_IGNORE = "ignore"; 55 private static final String RESULT_IGNORE = "ignore";
46 56
47 /** 57 /**
48 - * EMQX 事件类型常量 58 + * EMQX 事件类型常量 - 客户端连接
49 */ 59 */
50 private static final String EVENT_CLIENT_CONNECTED = "client.connected"; 60 private static final String EVENT_CLIENT_CONNECTED = "client.connected";
  61 + /**
  62 + * EMQX 事件类型常量 - 客户端断开连接
  63 + */
51 private static final String EVENT_CLIENT_DISCONNECTED = "client.disconnected"; 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 private final String serverId; 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 private final IotDeviceCommonApi deviceApi; 76 private final IotDeviceCommonApi deviceApi;
58 77
59 - public IotEmqxAuthEventHandler(String serverId) { 78 + public IotEmqxAuthEventHandler(String serverId, IotEmqxProtocol protocol) {
60 this.serverId = serverId; 79 this.serverId = serverId;
  80 + this.protocol = protocol;
61 this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); 81 this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
62 this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); 82 this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
63 } 83 }
64 84
  85 + // ========== 认证处理 ==========
  86 +
65 /** 87 /**
66 * EMQX 认证接口 88 * EMQX 认证接口
67 */ 89 */
68 public void handleAuth(RoutingContext context) { 90 public void handleAuth(RoutingContext context) {
  91 + JsonObject body = null;
69 try { 92 try {
70 // 1. 参数校验 93 // 1. 参数校验
71 - JsonObject body = parseRequestBody(context); 94 + body = parseRequestBody(context);
72 if (body == null) { 95 if (body == null) {
73 return; 96 return;
74 } 97 }
@@ -82,7 +105,13 @@ public class IotEmqxAuthEventHandler { @@ -82,7 +105,13 @@ public class IotEmqxAuthEventHandler {
82 return; 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 boolean authResult = handleDeviceAuth(clientId, username, password); 115 boolean authResult = handleDeviceAuth(clientId, username, password);
87 log.info("[handleAuth][设备认证结果: {} -> {}]", username, authResult); 116 log.info("[handleAuth][设备认证结果: {} -> {}]", username, authResult);
88 if (authResult) { 117 if (authResult) {
@@ -91,12 +120,180 @@ public class IotEmqxAuthEventHandler { @@ -91,12 +120,180 @@ public class IotEmqxAuthEventHandler {
91 sendAuthResponse(context, RESULT_DENY); 120 sendAuthResponse(context, RESULT_DENY);
92 } 121 }
93 } catch (Exception e) { 122 } catch (Exception e) {
94 - log.error("[handleAuth][设备认证异常]", e); 123 + log.error("[handleAuth][设备认证异常][body={}]", body, e);
95 sendAuthResponse(context, RESULT_IGNORE); 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 * EMQX 统一事件处理接口:根据 EMQX 官方 Webhook 设计,统一处理所有客户端事件 297 * EMQX 统一事件处理接口:根据 EMQX 官方 Webhook 设计,统一处理所有客户端事件
101 * 支持的事件类型:client.connected、client.disconnected 等 298 * 支持的事件类型:client.connected、client.disconnected 等
102 */ 299 */
@@ -124,59 +321,16 @@ public class IotEmqxAuthEventHandler { @@ -124,59 +321,16 @@ public class IotEmqxAuthEventHandler {
124 break; 321 break;
125 } 322 }
126 323
127 - // EMQX Webhook 只需要 200 状态码,无需响应体 324 + // 3. EMQX Webhook 只需要 200 状态码,无需响应体
128 context.response().setStatusCode(SUCCESS_STATUS_CODE).end(); 325 context.response().setStatusCode(SUCCESS_STATUS_CODE).end();
129 } catch (Exception e) { 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 context.response().setStatusCode(SUCCESS_STATUS_CODE).end(); 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 * <p> 335 * <p>
182 * 事件接口解析失败时仅返回 200 状态码,无响应体(符合 EMQX Webhook 规范) 336 * 事件接口解析失败时仅返回 200 状态码,无响应体(符合 EMQX Webhook 规范)
@@ -201,23 +355,22 @@ public class IotEmqxAuthEventHandler { @@ -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,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 \ No newline at end of file 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 import cn.hutool.core.util.StrUtil; 4 import cn.hutool.core.util.StrUtil;
4 import cn.hutool.extra.spring.SpringUtil; 5 import cn.hutool.extra.spring.SpringUtil;
5 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
6 -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol;  
7 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; 7 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
8 import io.vertx.mqtt.messages.MqttPublishMessage; 8 import io.vertx.mqtt.messages.MqttPublishMessage;
9 import lombok.extern.slf4j.Slf4j; 9 import lombok.extern.slf4j.Slf4j;
@@ -20,41 +20,40 @@ public class IotEmqxUpstreamHandler { @@ -20,41 +20,40 @@ public class IotEmqxUpstreamHandler {
20 20
21 private final String serverId; 21 private final String serverId;
22 22
23 - public IotEmqxUpstreamHandler(IotEmqxUpstreamProtocol protocol) { 23 + public IotEmqxUpstreamHandler(String serverId) {
24 this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); 24 this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
25 - this.serverId = protocol.getServerId(); 25 + this.serverId = serverId;
26 } 26 }
27 27
28 /** 28 /**
29 * 处理 MQTT 发布消息 29 * 处理 MQTT 发布消息
30 */ 30 */
31 public void handle(MqttPublishMessage mqttMessage) { 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 String topic = mqttMessage.topicName(); 33 String topic = mqttMessage.topicName();
34 byte[] payload = mqttMessage.payload().getBytes(); 34 byte[] payload = mqttMessage.payload().getBytes();
35 try { 35 try {
36 // 1. 解析主题,一次性获取所有信息 36 // 1. 解析主题,一次性获取所有信息
37 String[] topicParts = topic.split("/"); 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 log.warn("[handle][topic({}) 格式不正确,无法解析有效的 productKey 和 deviceName]", topic); 41 log.warn("[handle][topic({}) 格式不正确,无法解析有效的 productKey 和 deviceName]", topic);
40 return; 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 if (message == null) { 47 if (message == null) {
49 log.warn("[handle][topic({}) payload({}) 消息解码失败]", topic, new String(payload)); 48 log.warn("[handle][topic({}) payload({}) 消息解码失败]", topic, new String(payload));
50 return; 49 return;
51 } 50 }
52 51
53 - // 4. 发送消息到队列 52 + // 3. 发送消息到队列
54 deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); 53 deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId);
55 } catch (Exception e) { 54 } catch (Exception e) {
56 log.error("[handle][topic({}) payload({}) 处理异常]", topic, new String(payload), e); 55 log.error("[handle][topic({}) payload({}) 处理异常]", topic, new String(payload), e);
57 } 56 }
58 } 57 }
59 58
60 -}  
61 \ No newline at end of file 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,19 +10,4 @@ import lombok.Data;
10 @Data 10 @Data
11 public class IotHttpConfig { 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,7 +4,8 @@ import cn.hutool.extra.spring.SpringUtil;
4 import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; 4 import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
5 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; 5 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
6 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; 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 import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; 9 import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
9 import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.downstream.IotHttpDownstreamSubscriber; 10 import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.downstream.IotHttpDownstreamSubscriber;
10 import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpAuthHandler; 11 import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpAuthHandler;
@@ -33,7 +34,7 @@ public class IotHttpProtocol implements IotProtocol { @@ -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 * 服务器 ID(用于消息追踪,全局唯一) 39 * 服务器 ID(用于消息追踪,全局唯一)
39 */ 40 */
@@ -60,7 +61,7 @@ public class IotHttpProtocol implements IotProtocol { @@ -60,7 +61,7 @@ public class IotHttpProtocol implements IotProtocol {
60 */ 61 */
61 private IotHttpDownstreamSubscriber downstreamSubscriber; 62 private IotHttpDownstreamSubscriber downstreamSubscriber;
62 63
63 - public IotHttpProtocol(ProtocolInstanceProperties properties) { 64 + public IotHttpProtocol(ProtocolProperties properties) {
64 this.properties = properties; 65 this.properties = properties;
65 this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); 66 this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
66 67
@@ -104,12 +105,12 @@ public class IotHttpProtocol implements IotProtocol { @@ -104,12 +105,12 @@ public class IotHttpProtocol implements IotProtocol {
104 router.post(IotHttpUpstreamHandler.PATH).handler(upstreamHandler); 105 router.post(IotHttpUpstreamHandler.PATH).handler(upstreamHandler);
105 106
106 // 1.4 启动 HTTP 服务器 107 // 1.4 启动 HTTP 服务器
107 - IotHttpConfig httpConfig = properties.getHttp();  
108 HttpServerOptions options = new HttpServerOptions().setPort(properties.getPort()); 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 PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() 111 PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions()
111 - .setKeyPath(httpConfig.getSslKeyPath())  
112 - .setCertPath(httpConfig.getSslCertPath()); 112 + .setKeyPath(sslConfig.getSslKeyPath())
  113 + .setCertPath(sslConfig.getSslCertPath());
113 options = options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); 114 options = options.setSsl(true).setKeyCertOptions(pemKeyCertOptions);
114 } 115 }
115 try { 116 try {
@@ -125,7 +126,11 @@ public class IotHttpProtocol implements IotProtocol { @@ -125,7 +126,11 @@ public class IotHttpProtocol implements IotProtocol {
125 this.downstreamSubscriber.start(); 126 this.downstreamSubscriber.start();
126 } catch (Exception e) { 127 } catch (Exception e) {
127 log.error("[start][IoT HTTP 协议 {} 启动失败]", getId(), e); 128 log.error("[start][IoT HTTP 协议 {} 启动失败]", getId(), e);
128 - // 启动失败时关闭 Vertx 129 + // 启动失败时关闭资源
  130 + if (httpServer != null) {
  131 + httpServer.close();
  132 + httpServer = null;
  133 + }
129 if (vertx != null) { 134 if (vertx != null) {
130 vertx.close(); 135 vertx.close();
131 vertx = null; 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,7 +35,7 @@ public class IotHttpRegisterHandler extends IotHttpAbstractHandler {
35 Assert.notNull(request, "请求参数不能为空"); 35 Assert.notNull(request, "请求参数不能为空");
36 Assert.notBlank(request.getProductKey(), "productKey 不能为空"); 36 Assert.notBlank(request.getProductKey(), "productKey 不能为空");
37 Assert.notBlank(request.getDeviceName(), "deviceName 不能为空"); 37 Assert.notBlank(request.getDeviceName(), "deviceName 不能为空");
38 - Assert.notBlank(request.getProductSecret(), "productSecret 不能为空"); 38 + Assert.notBlank(request.getSign(), "sign 不能为空");
39 39
40 // 2. 调用动态注册 40 // 2. 调用动态注册
41 CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(request); 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 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; 3 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
5 import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; 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 import lombok.extern.slf4j.Slf4j; 7 import lombok.extern.slf4j.Slf4j;
8 8
9 /** 9 /**
10 - * IoT 网关 MQTT 协议:下行消息订阅器  
11 - * <p>  
12 - * 负责接收来自消息总线的下行消息,并委托给下行处理器进行业务处理 10 + * IoT 网关 MQTT 协议:接收下行给设备的消息
13 * 11 *
14 * @author 芋道源码 12 * @author 芋道源码
15 */ 13 */
@@ -18,7 +16,7 @@ public class IotMqttDownstreamSubscriber extends IotProtocolDownstreamSubscriber @@ -18,7 +16,7 @@ public class IotMqttDownstreamSubscriber extends IotProtocolDownstreamSubscriber
18 16
19 private final IotMqttDownstreamHandler downstreamHandler; 17 private final IotMqttDownstreamHandler downstreamHandler;
20 18
21 - public IotMqttDownstreamSubscriber(IotMqttUpstreamProtocol protocol, 19 + public IotMqttDownstreamSubscriber(IotMqttProtocol protocol,
22 IotMqttDownstreamHandler downstreamHandler, 20 IotMqttDownstreamHandler downstreamHandler,
23 IotMessageBus messageBus) { 21 IotMessageBus messageBus) {
24 super(protocol, messageBus); 22 super(protocol, messageBus);
@@ -27,14 +25,7 @@ public class IotMqttDownstreamSubscriber extends IotProtocolDownstreamSubscriber @@ -27,14 +25,7 @@ public class IotMqttDownstreamSubscriber extends IotProtocolDownstreamSubscriber
27 25
28 @Override 26 @Override
29 protected void handleMessage(IotDeviceMessage message) { 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,7 +66,6 @@ public class IotMqttConnectionManager {
66 } catch (Exception ignored) { 66 } catch (Exception ignored) {
67 // 连接已关闭,忽略异常 67 // 连接已关闭,忽略异常
68 } 68 }
69 -  
70 return realTimeAddress; 69 return realTimeAddress;
71 } 70 }
72 71
@@ -74,24 +73,24 @@ public class IotMqttConnectionManager { @@ -74,24 +73,24 @@ public class IotMqttConnectionManager {
74 * 注册设备连接(包含认证信息) 73 * 注册设备连接(包含认证信息)
75 * 74 *
76 * @param endpoint MQTT 连接端点 75 * @param endpoint MQTT 连接端点
77 - * @param deviceId 设备 ID  
78 * @param connectionInfo 连接信息 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 MqttEndpoint oldEndpoint = deviceEndpointMap.get(deviceId); 81 MqttEndpoint oldEndpoint = deviceEndpointMap.get(deviceId);
83 if (oldEndpoint != null && oldEndpoint != endpoint) { 82 if (oldEndpoint != null && oldEndpoint != endpoint) {
84 log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]", 83 log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]",
85 deviceId, getEndpointAddress(oldEndpoint)); 84 deviceId, getEndpointAddress(oldEndpoint));
86 - oldEndpoint.close();  
87 - // 清理旧连接的映射 85 + // 先清理映射,再关闭连接(避免旧连接处理器干扰)
88 connectionMap.remove(oldEndpoint); 86 connectionMap.remove(oldEndpoint);
  87 + oldEndpoint.close();
89 } 88 }
90 89
91 // 注册新连接 90 // 注册新连接
92 connectionMap.put(endpoint, connectionInfo); 91 connectionMap.put(endpoint, connectionInfo);
93 deviceEndpointMap.put(deviceId, endpoint); 92 deviceEndpointMap.put(deviceId, endpoint);
94 - log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {},product key: {},device name: {}]", 93 + log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {},productKey: {},deviceName: {}]",
95 deviceId, getEndpointAddress(endpoint), connectionInfo.getProductKey(), connectionInfo.getDeviceName()); 94 deviceId, getEndpointAddress(endpoint), connectionInfo.getProductKey(), connectionInfo.getDeviceName());
96 } 95 }
97 96
@@ -129,26 +128,11 @@ public class IotMqttConnectionManager { @@ -129,26 +128,11 @@ public class IotMqttConnectionManager {
129 if (endpoint == null) { 128 if (endpoint == null) {
130 return null; 129 return null;
131 } 130 }
132 -  
133 // 获取连接信息 131 // 获取连接信息
134 return getConnectionInfo(endpoint); 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 * @param deviceId 设备 ID 138 * @param deviceId 设备 ID
@@ -192,28 +176,16 @@ public class IotMqttConnectionManager { @@ -192,28 +176,16 @@ public class IotMqttConnectionManager {
192 * 设备 ID 176 * 设备 ID
193 */ 177 */
194 private Long deviceId; 178 private Long deviceId;
195 -  
196 /** 179 /**
197 * 产品 Key 180 * 产品 Key
198 */ 181 */
199 private String productKey; 182 private String productKey;
200 -  
201 /** 183 /**
202 * 设备名称 184 * 设备名称
203 */ 185 */
204 private String deviceName; 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 private String remoteAddress; 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 \ No newline at end of file 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,20 +28,6 @@ public class IotTcpConfig {
28 private Long keepAliveTimeoutMs = 30000L; 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 @Valid 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,7 +5,8 @@ import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
5 import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; 5 import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum;
6 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; 6 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
7 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; 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 import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; 10 import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
10 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; 11 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec;
11 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodecFactory; 12 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodecFactory;
@@ -21,7 +22,7 @@ import io.vertx.core.net.NetServerOptions; @@ -21,7 +22,7 @@ import io.vertx.core.net.NetServerOptions;
21 import io.vertx.core.net.PemKeyCertOptions; 22 import io.vertx.core.net.PemKeyCertOptions;
22 import lombok.Getter; 23 import lombok.Getter;
23 import lombok.extern.slf4j.Slf4j; 24 import lombok.extern.slf4j.Slf4j;
24 -import org.springframework.util.Assert; 25 +import cn.hutool.core.lang.Assert;
25 26
26 /** 27 /**
27 * IoT TCP 协议实现 28 * IoT TCP 协议实现
@@ -36,7 +37,7 @@ public class IotTcpProtocol implements IotProtocol { @@ -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 * 服务器 ID(用于消息追踪,全局唯一) 42 * 服务器 ID(用于消息追踪,全局唯一)
42 */ 43 */
@@ -76,7 +77,7 @@ public class IotTcpProtocol implements IotProtocol { @@ -76,7 +77,7 @@ public class IotTcpProtocol implements IotProtocol {
76 */ 77 */
77 private final IotTcpFrameCodec frameCodec; 78 private final IotTcpFrameCodec frameCodec;
78 79
79 - public IotTcpProtocol(ProtocolInstanceProperties properties) { 80 + public IotTcpProtocol(ProtocolProperties properties) {
80 IotTcpConfig tcpConfig = properties.getTcp(); 81 IotTcpConfig tcpConfig = properties.getTcp();
81 Assert.notNull(tcpConfig, "TCP 协议配置(tcp)不能为空"); 82 Assert.notNull(tcpConfig, "TCP 协议配置(tcp)不能为空");
82 Assert.notNull(tcpConfig.getCodec(), "TCP 拆包配置(tcp.codec)不能为空"); 83 Assert.notNull(tcpConfig.getCodec(), "TCP 拆包配置(tcp.codec)不能为空");
@@ -128,10 +129,11 @@ public class IotTcpProtocol implements IotProtocol { @@ -128,10 +129,11 @@ public class IotTcpProtocol implements IotProtocol {
128 .setTcpNoDelay(true) 129 .setTcpNoDelay(true)
129 .setReuseAddress(true) 130 .setReuseAddress(true)
130 .setIdleTimeout((int) (tcpConfig.getKeepAliveTimeoutMs() / 1000)); // 设置空闲超时 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 PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() 134 PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions()
133 - .setKeyPath(tcpConfig.getSslKeyPath())  
134 - .setCertPath(tcpConfig.getSslCertPath()); 135 + .setKeyPath(sslConfig.getSslKeyPath())
  136 + .setCertPath(sslConfig.getSslCertPath());
135 options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); 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,7 +8,7 @@ import io.vertx.core.Handler;
8 import io.vertx.core.buffer.Buffer; 8 import io.vertx.core.buffer.Buffer;
9 import io.vertx.core.parsetools.RecordParser; 9 import io.vertx.core.parsetools.RecordParser;
10 import lombok.extern.slf4j.Slf4j; 10 import lombok.extern.slf4j.Slf4j;
11 -import org.springframework.util.Assert; 11 +import cn.hutool.core.lang.Assert;
12 12
13 /** 13 /**
14 * IoT TCP 分隔符帧编解码器 14 * IoT TCP 分隔符帧编解码器
@@ -39,7 +39,7 @@ public class IotTcpDelimiterFrameCodec implements IotTcpFrameCodec { @@ -39,7 +39,7 @@ public class IotTcpDelimiterFrameCodec implements IotTcpFrameCodec {
39 private final byte[] delimiterBytes; 39 private final byte[] delimiterBytes;
40 40
41 public IotTcpDelimiterFrameCodec(IotTcpConfig.CodecConfig config) { 41 public IotTcpDelimiterFrameCodec(IotTcpConfig.CodecConfig config) {
42 - Assert.hasText(config.getDelimiter(), "delimiter 不能为空"); 42 + Assert.notBlank(config.getDelimiter(), "delimiter 不能为空");
43 this.delimiterBytes = parseDelimiter(config.getDelimiter()); 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 +7,7 @@ import io.vertx.core.Handler;
7 import io.vertx.core.buffer.Buffer; 7 import io.vertx.core.buffer.Buffer;
8 import io.vertx.core.parsetools.RecordParser; 8 import io.vertx.core.parsetools.RecordParser;
9 import lombok.extern.slf4j.Slf4j; 9 import lombok.extern.slf4j.Slf4j;
10 -import org.springframework.util.Assert; 10 +import cn.hutool.core.lang.Assert;
11 11
12 /** 12 /**
13 * IoT TCP 定长帧编解码器 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 +7,7 @@ import io.vertx.core.Handler;
7 import io.vertx.core.buffer.Buffer; 7 import io.vertx.core.buffer.Buffer;
8 import io.vertx.core.parsetools.RecordParser; 8 import io.vertx.core.parsetools.RecordParser;
9 import lombok.extern.slf4j.Slf4j; 9 import lombok.extern.slf4j.Slf4j;
10 -import org.springframework.util.Assert; 10 +import cn.hutool.core.lang.Assert;
11 11
12 import java.util.concurrent.atomic.AtomicReference; 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,8 +45,8 @@ public class IotTcpDownstreamHandler {
45 } 45 }
46 46
47 // 2. 序列化 + 帧编码 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 // 3. 发送到设备 51 // 3. 发送到设备
52 boolean success = connectionManager.sendToDevice(message.getDeviceId(), frameData.getBytes()); 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,7 +24,7 @@ import io.vertx.core.buffer.Buffer;
24 import io.vertx.core.net.NetSocket; 24 import io.vertx.core.net.NetSocket;
25 import io.vertx.core.parsetools.RecordParser; 25 import io.vertx.core.parsetools.RecordParser;
26 import lombok.extern.slf4j.Slf4j; 26 import lombok.extern.slf4j.Slf4j;
27 -import org.springframework.util.Assert; 27 +import cn.hutool.core.lang.Assert;
28 28
29 import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; 29 import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*;
30 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 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,8 +167,8 @@ public class IotTcpUpstreamHandler implements Handler&lt;NetSocket&gt; {
167 // 1. 解析认证参数 167 // 1. 解析认证参数
168 IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class); 168 IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class);
169 Assert.notNull(authParams, "认证参数不能为空"); 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 // 2.1 执行认证 173 // 2.1 执行认证
174 CommonResult<Boolean> authResult = deviceApi.authDevice(authParams); 174 CommonResult<Boolean> authResult = deviceApi.authDevice(authParams);
@@ -204,8 +204,9 @@ public class IotTcpUpstreamHandler implements Handler&lt;NetSocket&gt; { @@ -204,8 +204,9 @@ public class IotTcpUpstreamHandler implements Handler&lt;NetSocket&gt; {
204 // 1. 解析注册参数 204 // 1. 解析注册参数
205 IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class); 205 IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class);
206 Assert.notNull(params, "注册参数不能为空"); 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 // 2. 调用动态注册 211 // 2. 调用动态注册
211 CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params); 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,8 +47,8 @@ public class IotTcpConnectionManager {
47 * @param deviceId 设备 ID 47 * @param deviceId 设备 ID
48 * @param connectionInfo 连接信息 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 if (connectionMap.size() >= maxConnections) { 52 if (connectionMap.size() >= maxConnections) {
53 throw new IllegalStateException("连接数已达上限: " + maxConnections); 53 throw new IllegalStateException("连接数已达上限: " + maxConnections);
54 } 54 }
@@ -57,9 +57,9 @@ public class IotTcpConnectionManager { @@ -57,9 +57,9 @@ public class IotTcpConnectionManager {
57 if (oldSocket != null && oldSocket != socket) { 57 if (oldSocket != null && oldSocket != socket) {
58 log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]", 58 log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]",
59 deviceId, oldSocket.remoteAddress()); 59 deviceId, oldSocket.remoteAddress());
60 - oldSocket.close();  
61 - // 清理旧连接的映射 60 + // 先清理映射,再关闭连接
62 connectionMap.remove(oldSocket); 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,7 +4,7 @@ import cn.hutool.extra.spring.SpringUtil;
4 import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; 4 import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
5 import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; 5 import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum;
6 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; 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 import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; 8 import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
9 import cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.downstream.IotUdpDownstreamHandler; 9 import cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.downstream.IotUdpDownstreamHandler;
10 import cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.downstream.IotUdpDownstreamSubscriber; 10 import cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.downstream.IotUdpDownstreamSubscriber;
@@ -18,7 +18,7 @@ import io.vertx.core.datagram.DatagramSocket; @@ -18,7 +18,7 @@ import io.vertx.core.datagram.DatagramSocket;
18 import io.vertx.core.datagram.DatagramSocketOptions; 18 import io.vertx.core.datagram.DatagramSocketOptions;
19 import lombok.Getter; 19 import lombok.Getter;
20 import lombok.extern.slf4j.Slf4j; 20 import lombok.extern.slf4j.Slf4j;
21 -import org.springframework.util.Assert; 21 +import cn.hutool.core.lang.Assert;
22 22
23 /** 23 /**
24 * IoT UDP 协议实现 24 * IoT UDP 协议实现
@@ -33,7 +33,7 @@ public class IotUdpProtocol implements IotProtocol { @@ -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 * 服务器 ID(用于消息追踪,全局唯一) 38 * 服务器 ID(用于消息追踪,全局唯一)
39 */ 39 */
@@ -70,7 +70,7 @@ public class IotUdpProtocol implements IotProtocol { @@ -70,7 +70,7 @@ public class IotUdpProtocol implements IotProtocol {
70 */ 70 */
71 private final IotMessageSerializer serializer; 71 private final IotMessageSerializer serializer;
72 72
73 - public IotUdpProtocol(ProtocolInstanceProperties properties) { 73 + public IotUdpProtocol(ProtocolProperties properties) {
74 IotUdpConfig udpConfig = properties.getUdp(); 74 IotUdpConfig udpConfig = properties.getUdp();
75 Assert.notNull(udpConfig, "UDP 协议配置(udp)不能为空"); 75 Assert.notNull(udpConfig, "UDP 协议配置(udp)不能为空");
76 this.properties = properties; 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,7 +27,7 @@ import io.vertx.core.buffer.Buffer;
27 import io.vertx.core.datagram.DatagramPacket; 27 import io.vertx.core.datagram.DatagramPacket;
28 import io.vertx.core.datagram.DatagramSocket; 28 import io.vertx.core.datagram.DatagramSocket;
29 import lombok.extern.slf4j.Slf4j; 29 import lombok.extern.slf4j.Slf4j;
30 -import org.springframework.util.Assert; 30 +import cn.hutool.core.lang.Assert;
31 31
32 import java.net.InetSocketAddress; 32 import java.net.InetSocketAddress;
33 import java.util.Map; 33 import java.util.Map;
@@ -173,8 +173,8 @@ public class IotUdpUpstreamHandler { @@ -173,8 +173,8 @@ public class IotUdpUpstreamHandler {
173 // 1. 解析认证参数 173 // 1. 解析认证参数
174 IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class); 174 IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class);
175 Assert.notNull(authParams, "认证参数不能为空"); 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 // 2.1 执行认证 179 // 2.1 执行认证
180 CommonResult<Boolean> authResult = deviceApi.authDevice(authParams); 180 CommonResult<Boolean> authResult = deviceApi.authDevice(authParams);
@@ -218,8 +218,9 @@ public class IotUdpUpstreamHandler { @@ -218,8 +218,9 @@ public class IotUdpUpstreamHandler {
218 // 1. 解析注册参数 218 // 1. 解析注册参数
219 IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class); 219 IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class);
220 Assert.notNull(params, "注册参数不能为空"); 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 // 2. 调用动态注册 225 // 2. 调用动态注册
225 CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params); 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,6 +9,7 @@ import lombok.Data;
9 import lombok.extern.slf4j.Slf4j; 9 import lombok.extern.slf4j.Slf4j;
10 10
11 import java.net.InetSocketAddress; 11 import java.net.InetSocketAddress;
  12 +import java.util.concurrent.CompletableFuture;
12 import java.util.concurrent.TimeUnit; 13 import java.util.concurrent.TimeUnit;
13 14
14 /** 15 /**
@@ -47,8 +48,8 @@ public class IotUdpSessionManager { @@ -47,8 +48,8 @@ public class IotUdpSessionManager {
47 * @param deviceId 设备 ID 48 * @param deviceId 设备 ID
48 * @param sessionInfo 会话信息 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 if (deviceSessionCache.getIfPresent(deviceId) == null 53 if (deviceSessionCache.getIfPresent(deviceId) == null
53 && deviceSessionCache.size() >= maxSessions) { 54 && deviceSessionCache.size() >= maxSessions) {
54 throw new IllegalStateException("会话数已达上限: " + maxSessions); 55 throw new IllegalStateException("会话数已达上限: " + maxSessions);
@@ -113,16 +114,21 @@ public class IotUdpSessionManager { @@ -113,16 +114,21 @@ public class IotUdpSessionManager {
113 } 114 }
114 InetSocketAddress address = sessionInfo.getAddress(); 115 InetSocketAddress address = sessionInfo.getAddress();
115 try { 116 try {
  117 + // 使用 CompletableFuture 同步等待发送结果
  118 + CompletableFuture<Boolean> future = new CompletableFuture<>();
116 socket.send(Buffer.buffer(data), address.getPort(), address.getHostString(), result -> { 119 socket.send(Buffer.buffer(data), address.getPort(), address.getHostString(), result -> {
117 if (result.succeeded()) { 120 if (result.succeeded()) {
118 log.debug("[sendToDevice][发送消息成功,设备 ID: {},地址: {},数据长度: {} 字节]", 121 log.debug("[sendToDevice][发送消息成功,设备 ID: {},地址: {},数据长度: {} 字节]",
119 deviceId, buildAddressKey(address), data.length); 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 } catch (Exception e) { 132 } catch (Exception e) {
127 log.error("[sendToDevice][发送消息异常,设备 ID: {}]", deviceId, e); 133 log.error("[sendToDevice][发送消息异常,设备 ID: {}]", deviceId, e);
128 return false; 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,20 +35,4 @@ public class IotWebSocketConfig {
35 @NotNull(message = "空闲超时时间不能为空") 35 @NotNull(message = "空闲超时时间不能为空")
36 private Integer idleTimeoutSeconds = 60; 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,7 +6,8 @@ import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
6 import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; 6 import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum;
7 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; 7 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
8 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; 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 import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; 11 import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
11 import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.downstream.IotWebSocketDownstreamSubscriber; 12 import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.downstream.IotWebSocketDownstreamSubscriber;
12 import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.downstream.IotWebSocketDownstreamHandler; 13 import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.downstream.IotWebSocketDownstreamHandler;
@@ -20,7 +21,7 @@ import io.vertx.core.http.HttpServerOptions; @@ -20,7 +21,7 @@ import io.vertx.core.http.HttpServerOptions;
20 import io.vertx.core.net.PemKeyCertOptions; 21 import io.vertx.core.net.PemKeyCertOptions;
21 import lombok.Getter; 22 import lombok.Getter;
22 import lombok.extern.slf4j.Slf4j; 23 import lombok.extern.slf4j.Slf4j;
23 -import org.springframework.util.Assert; 24 +import cn.hutool.core.lang.Assert;
24 25
25 /** 26 /**
26 * IoT WebSocket 协议实现 27 * IoT WebSocket 协议实现
@@ -35,7 +36,7 @@ public class IotWebSocketProtocol implements IotProtocol { @@ -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 * 服务器 ID(用于消息追踪,全局唯一) 41 * 服务器 ID(用于消息追踪,全局唯一)
41 */ 42 */
@@ -71,7 +72,7 @@ public class IotWebSocketProtocol implements IotProtocol { @@ -71,7 +72,7 @@ public class IotWebSocketProtocol implements IotProtocol {
71 */ 72 */
72 private final IotMessageSerializer serializer; 73 private final IotMessageSerializer serializer;
73 74
74 - public IotWebSocketProtocol(ProtocolInstanceProperties properties) { 75 + public IotWebSocketProtocol(ProtocolProperties properties) {
75 Assert.notNull(properties, "协议实例配置不能为空"); 76 Assert.notNull(properties, "协议实例配置不能为空");
76 Assert.notNull(properties.getWebsocket(), "WebSocket 协议配置(websocket)不能为空"); 77 Assert.notNull(properties.getWebsocket(), "WebSocket 协议配置(websocket)不能为空");
77 this.properties = properties; 78 this.properties = properties;
@@ -120,10 +121,11 @@ public class IotWebSocketProtocol implements IotProtocol { @@ -120,10 +121,11 @@ public class IotWebSocketProtocol implements IotProtocol {
120 .setIdleTimeout(wsConfig.getIdleTimeoutSeconds()) 121 .setIdleTimeout(wsConfig.getIdleTimeoutSeconds())
121 .setMaxWebSocketFrameSize(wsConfig.getMaxFrameSize()) 122 .setMaxWebSocketFrameSize(wsConfig.getMaxFrameSize())
122 .setMaxWebSocketMessageSize(wsConfig.getMaxMessageSize()); 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 PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() 126 PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions()
125 - .setKeyPath(wsConfig.getSslKeyPath())  
126 - .setCertPath(wsConfig.getSslCertPath()); 127 + .setKeyPath(sslConfig.getSslKeyPath())
  128 + .setCertPath(sslConfig.getSslCertPath());
127 options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); 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,7 +23,7 @@ import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessa
23 import io.vertx.core.Handler; 23 import io.vertx.core.Handler;
24 import io.vertx.core.http.ServerWebSocket; 24 import io.vertx.core.http.ServerWebSocket;
25 import lombok.extern.slf4j.Slf4j; 25 import lombok.extern.slf4j.Slf4j;
26 -import org.springframework.util.Assert; 26 +import cn.hutool.core.lang.Assert;
27 27
28 import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; 28 import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*;
29 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 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,7 +109,7 @@ public class IotWebSocketUpstreamHandler implements Handler&lt;ServerWebSocket&gt; {
109 // 1.2 解码消息 109 // 1.2 解码消息
110 message = serializer.deserialize(payload); 110 message = serializer.deserialize(payload);
111 Assert.notNull(message, "消息反序列化失败"); 111 Assert.notNull(message, "消息反序列化失败");
112 - Assert.hasText(message.getMethod(), "method 不能为空"); 112 + Assert.notBlank(message.getMethod(), "method 不能为空");
113 113
114 // 2. 根据消息类型路由处理 114 // 2. 根据消息类型路由处理
115 if (AUTH_METHOD.equals(message.getMethod())) { 115 if (AUTH_METHOD.equals(message.getMethod())) {
@@ -150,8 +150,8 @@ public class IotWebSocketUpstreamHandler implements Handler&lt;ServerWebSocket&gt; { @@ -150,8 +150,8 @@ public class IotWebSocketUpstreamHandler implements Handler&lt;ServerWebSocket&gt; {
150 // 1. 解析认证参数 150 // 1. 解析认证参数
151 IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class); 151 IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class);
152 Assert.notNull(authParams, "认证参数不能为空"); 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 // 2.1 执行认证 156 // 2.1 执行认证
157 CommonResult<Boolean> authResult = deviceApi.authDevice(authParams); 157 CommonResult<Boolean> authResult = deviceApi.authDevice(authParams);
@@ -187,8 +187,9 @@ public class IotWebSocketUpstreamHandler implements Handler&lt;ServerWebSocket&gt; { @@ -187,8 +187,9 @@ public class IotWebSocketUpstreamHandler implements Handler&lt;ServerWebSocket&gt; {
187 // 1. 解析注册参数 187 // 1. 解析注册参数
188 IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class); 188 IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class);
189 Assert.notNull(params, "注册参数不能为空"); 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 // 2. 调用动态注册 194 // 2. 调用动态注册
194 CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params); 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 package cn.iocoder.yudao.module.iot.gateway.service.device.message; 1 package cn.iocoder.yudao.module.iot.gateway.service.device.message;
2 2
  3 +import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum;
3 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 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,45 +11,45 @@ import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
10 public interface IotDeviceMessageService { 11 public interface IotDeviceMessageService {
11 12
12 /** 13 /**
13 - * 编码消息 14 + * 序列化消息
14 * 15 *
15 * @param message 消息 16 * @param message 消息
16 * @param productKey 产品 Key 17 * @param productKey 产品 Key
17 * @param deviceName 设备名称 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 * @param bytes 消息内容 37 * @param bytes 消息内容
37 * @param productKey 产品 Key 38 * @param productKey 产品 Key
38 * @param deviceName 设备名称 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 package cn.iocoder.yudao.module.iot.gateway.service.device.message; 1 package cn.iocoder.yudao.module.iot.gateway.service.device.message;
2 2
  3 +import cn.hutool.core.lang.Assert;
3 import cn.hutool.core.util.StrUtil; 4 import cn.hutool.core.util.StrUtil;
4 -import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;  
5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; 5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
  6 +import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum;
6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 7 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
7 import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer; 8 import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer;
8 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; 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 import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; 12 import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
11 import jakarta.annotation.Resource; 13 import jakarta.annotation.Resource;
12 import lombok.extern.slf4j.Slf4j; 14 import lombok.extern.slf4j.Slf4j;
13 import org.springframework.stereotype.Service; 15 import org.springframework.stereotype.Service;
14 16
15 import java.time.LocalDateTime; 17 import java.time.LocalDateTime;
16 -import java.util.List;  
17 -import java.util.Map;  
18 18
19 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 19 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
20 import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_NOT_EXISTS; 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,80 +28,70 @@ import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVIC
28 @Slf4j 28 @Slf4j
29 public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { 29 public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
30 30
31 - /**  
32 - * 编解码器  
33 - */  
34 - private final Map<String, IotDeviceMessageCodec> codes;  
35 -  
36 @Resource 31 @Resource
37 private IotDeviceService deviceService; 32 private IotDeviceService deviceService;
38 33
39 @Resource 34 @Resource
40 private IotDeviceMessageProducer deviceMessageProducer; 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 @Override 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 // 1.1 获取设备信息 43 // 1.1 获取设备信息
50 IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceName); 44 IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceName);
51 if (device == null) { 45 if (device == null) {
52 throw exception(DEVICE_NOT_EXISTS, productKey, deviceName); 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 @Override 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 @Override 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 // 1.1 获取设备信息 72 // 1.1 获取设备信息
81 IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceName); 73 IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceName);
82 if (device == null) { 74 if (device == null) {
83 throw exception(DEVICE_NOT_EXISTS, productKey, deviceName); 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 @Override 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 @Override 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,6 +38,12 @@ public final class IotMqttTopicUtils {
38 */ 38 */
39 public static final String MQTT_EVENT_PATH = "/mqtt/event"; 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,4 +69,50 @@ public final class IotMqttTopicUtils {
63 return SYS_TOPIC_PREFIX + productKey + "/" + deviceName + "/" + topicSuffix; 69 return SYS_TOPIC_PREFIX + productKey + "/" + deviceName + "/" + topicSuffix;
64 } 70 }
65 71
66 -}  
67 \ No newline at end of file 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,7 +48,7 @@ yudao:
48 # 针对引入的 HTTP 组件的配置 48 # 针对引入的 HTTP 组件的配置
49 # ==================================== 49 # ====================================
50 - id: http-json 50 - id: http-json
51 - type: http 51 + protocol: http
52 port: 8092 52 port: 8092
53 enabled: false 53 enabled: false
54 http: 54 http:
@@ -57,9 +57,9 @@ yudao: @@ -57,9 +57,9 @@ yudao:
57 # 针对引入的 TCP 组件的配置 57 # 针对引入的 TCP 组件的配置
58 # ==================================== 58 # ====================================
59 - id: tcp-json 59 - id: tcp-json
60 - type: tcp  
61 - port: 8091  
62 enabled: false 60 enabled: false
  61 + protocol: tcp
  62 + port: 8091
63 serialize: json 63 serialize: json
64 tcp: 64 tcp:
65 max-connections: 1000 65 max-connections: 1000
@@ -79,9 +79,9 @@ yudao: @@ -79,9 +79,9 @@ yudao:
79 # 针对引入的 UDP 组件的配置 79 # 针对引入的 UDP 组件的配置
80 # ==================================== 80 # ====================================
81 - id: udp-json 81 - id: udp-json
82 - type: udp  
83 - port: 8093  
84 enabled: false 82 enabled: false
  83 + protocol: udp
  84 + port: 8093
85 serialize: json 85 serialize: json
86 udp: 86 udp:
87 max-sessions: 1000 # 最大会话数 87 max-sessions: 1000 # 最大会话数
@@ -92,9 +92,9 @@ yudao: @@ -92,9 +92,9 @@ yudao:
92 # 针对引入的 WebSocket 组件的配置 92 # 针对引入的 WebSocket 组件的配置
93 # ==================================== 93 # ====================================
94 - id: websocket-json 94 - id: websocket-json
95 - type: websocket 95 + enabled: false
  96 + protocol: websocket
96 port: 8094 97 port: 8094
97 - enabled: true  
98 serialize: json 98 serialize: json
99 websocket: 99 websocket:
100 path: /ws 100 path: /ws
@@ -106,21 +106,68 @@ yudao: @@ -106,21 +106,68 @@ yudao:
106 # 针对引入的 CoAP 组件的配置 106 # 针对引入的 CoAP 组件的配置
107 # ==================================== 107 # ====================================
108 - id: coap-json 108 - id: coap-json
109 - type: coap 109 + enabled: false
  110 + protocol: coap
110 port: 5683 111 port: 5683
111 - enabled: true  
112 coap: 112 coap:
113 max-message-size: 1024 # 最大消息大小(字节) 113 max-message-size: 1024 # 最大消息大小(字节)
114 ack-timeout-ms: 2000 # ACK 超时时间(毫秒) 114 ack-timeout-ms: 2000 # ACK 超时时间(毫秒)
115 max-retransmit: 4 # 最大重传次数 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 # 针对引入的 EMQX 组件的配置 129 # 针对引入的 EMQX 组件的配置
121 # ==================================== 130 # ====================================
122 - emqx: 131 + - id: emqx-1
123 enabled: false 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 http-port: 8090 # MQTT HTTP 服务端口 171 http-port: 8090 # MQTT HTTP 服务端口
125 mqtt-host: 127.0.0.1 # MQTT Broker 地址 172 mqtt-host: 127.0.0.1 # MQTT Broker 地址
126 mqtt-port: 1883 # MQTT Broker 端口 173 mqtt-port: 1883 # MQTT Broker 端口
@@ -153,25 +200,8 @@ yudao: @@ -153,25 +200,8 @@ yudao:
153 # ==================================== 200 # ====================================
154 # 针对引入的 MQTT 组件的配置 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 modbus-tcp: 205 modbus-tcp:
176 enabled: true 206 enabled: true
177 config-refresh-interval: 30 # 配置刷新间隔(秒) 207 config-refresh-interval: 30 # 配置刷新间隔(秒)
@@ -194,7 +224,6 @@ logging: @@ -194,7 +224,6 @@ logging:
194 cn.iocoder.yudao.module.iot.gateway.protocol.emqx: DEBUG 224 cn.iocoder.yudao.module.iot.gateway.protocol.emqx: DEBUG
195 cn.iocoder.yudao.module.iot.gateway.protocol.http: DEBUG 225 cn.iocoder.yudao.module.iot.gateway.protocol.http: DEBUG
196 cn.iocoder.yudao.module.iot.gateway.protocol.mqtt: DEBUG 226 cn.iocoder.yudao.module.iot.gateway.protocol.mqtt: DEBUG
197 - cn.iocoder.yudao.module.iot.gateway.protocol.mqttws: DEBUG  
198 cn.iocoder.yudao.module.iot.gateway.protocol.coap: DEBUG 227 cn.iocoder.yudao.module.iot.gateway.protocol.coap: DEBUG
199 cn.iocoder.yudao.module.iot.gateway.protocol.websocket: DEBUG 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,6 +9,7 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
9 import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; 9 import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
10 import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; 10 import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
11 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; 11 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  12 +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils;
12 import lombok.extern.slf4j.Slf4j; 13 import lombok.extern.slf4j.Slf4j;
13 import org.eclipse.californium.core.CoapClient; 14 import org.eclipse.californium.core.CoapClient;
14 import org.eclipse.californium.core.CoapResponse; 15 import org.eclipse.californium.core.CoapResponse;
@@ -203,10 +204,13 @@ public class IotDirectDeviceCoapProtocolIntegrationTest { @@ -203,10 +204,13 @@ public class IotDirectDeviceCoapProtocolIntegrationTest {
203 // 1.1 构建请求 204 // 1.1 构建请求
204 String uri = String.format("coap://%s:%d/auth/register/device", SERVER_HOST, SERVER_PORT); 205 String uri = String.format("coap://%s:%d/auth/register/device", SERVER_HOST, SERVER_PORT);
205 // 1.2 构建请求参数 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 String payload = JsonUtils.toJsonString(reqDTO); 214 String payload = JsonUtils.toJsonString(reqDTO);
211 // 1.3 输出请求 215 // 1.3 输出请求
212 log.info("[testDeviceRegister][请求 URI: {}]", uri); 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,6 +10,7 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
10 import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; 10 import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
11 import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; 11 import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
12 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; 12 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  13 +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils;
13 import lombok.extern.slf4j.Slf4j; 14 import lombok.extern.slf4j.Slf4j;
14 import org.junit.jupiter.api.Disabled; 15 import org.junit.jupiter.api.Disabled;
15 import org.junit.jupiter.api.Test; 16 import org.junit.jupiter.api.Test;
@@ -158,10 +159,13 @@ public class IotDirectDeviceHttpProtocolIntegrationTest { @@ -158,10 +159,13 @@ public class IotDirectDeviceHttpProtocolIntegrationTest {
158 // 1.1 构建请求 159 // 1.1 构建请求
159 String url = String.format("http://%s:%d/auth/register/device", SERVER_HOST, SERVER_PORT); 160 String url = String.format("http://%s:%d/auth/register/device", SERVER_HOST, SERVER_PORT);
160 // 1.2 构建请求参数 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 IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO() 165 IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO()
162 .setProductKey(PRODUCT_KEY) 166 .setProductKey(PRODUCT_KEY)
163 - .setDeviceName("test-" + System.currentTimeMillis())  
164 - .setProductSecret("test-product-secret"); 167 + .setDeviceName(deviceName)
  168 + .setSign(sign);
165 String payload = JsonUtils.toJsonString(reqDTO); 169 String payload = JsonUtils.toJsonString(reqDTO);
166 // 1.3 输出请求 170 // 1.3 输出请求
167 log.info("[testDeviceRegister][请求 URL: {}]", url); 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 package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; 1 package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;
2 2
3 import cn.hutool.core.map.MapUtil; 3 import cn.hutool.core.map.MapUtil;
4 -import cn.hutool.core.util.IdUtil;  
5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; 4 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
6 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; 5 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
7 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
8 -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;  
9 import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; 7 import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
10 import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; 8 import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
11 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; 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 import io.netty.handler.codec.mqtt.MqttQoS; 13 import io.netty.handler.codec.mqtt.MqttQoS;
15 import io.vertx.core.Vertx; 14 import io.vertx.core.Vertx;
16 import io.vertx.core.buffer.Buffer; 15 import io.vertx.core.buffer.Buffer;
@@ -23,7 +22,6 @@ import org.junit.jupiter.api.Disabled; @@ -23,7 +22,6 @@ import org.junit.jupiter.api.Disabled;
23 import org.junit.jupiter.api.Test; 22 import org.junit.jupiter.api.Test;
24 23
25 import java.util.concurrent.CompletableFuture; 24 import java.util.concurrent.CompletableFuture;
26 -import java.util.concurrent.CountDownLatch;  
27 import java.util.concurrent.TimeUnit; 25 import java.util.concurrent.TimeUnit;
28 26
29 /** 27 /**
@@ -59,9 +57,9 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { @@ -59,9 +57,9 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
59 57
60 private static Vertx vertx; 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 // ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询) ===================== 64 // ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询) =====================
67 65
@@ -88,39 +86,19 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { @@ -88,39 +86,19 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
88 */ 86 */
89 @Test 87 @Test
90 public void testAuth() throws Exception { 88 public void testAuth() throws Exception {
91 - CountDownLatch latch = new CountDownLatch(1);  
92 -  
93 // 1. 构建认证信息 89 // 1. 构建认证信息
94 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); 90 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
95 log.info("[testAuth][认证信息: clientId={}, username={}, password={}]", 91 log.info("[testAuth][认证信息: clientId={}, username={}, password={}]",
96 authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword()); 92 authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword());
97 93
98 // 2. 创建客户端并连接 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,27 +113,26 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
135 MqttClient client = connectAndAuth(); 113 MqttClient client = connectAndAuth();
136 log.info("[testPropertyPost][连接认证成功]"); 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,27 +146,26 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
169 MqttClient client = connectAndAuth(); 146 MqttClient client = connectAndAuth();
170 log.info("[testEventPost][连接认证成功]"); 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,37 +173,58 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
197 /** 173 /**
198 * 直连设备动态注册测试(一型一密) 174 * 直连设备动态注册测试(一型一密)
199 * <p> 175 * <p>
200 - * 使用产品密钥(productSecret)验证身份,成功后返回设备密钥(deviceSecret) 176 + * 认证方式:
  177 + * - clientId: 任意值 + "|authType=register|" 后缀
  178 + * - username: {deviceName}&{productKey}(与普通认证相同)
  179 + * - password: 签名(使用 productSecret 对 "deviceName" + deviceName + "productKey" + productKey 进行 HMAC-SHA256)
201 * <p> 180 * <p>
202 - * 注意:此接口不需要认证 181 + * 成功后返回设备密钥(deviceSecret),可用于后续一机一密认证
203 */ 182 */
204 @Test 183 @Test
205 public void testDeviceRegister() throws Exception { 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,44 +234,25 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
237 */ 234 */
238 @Test 235 @Test
239 public void testSubscribe() throws Exception { 236 public void testSubscribe() throws Exception {
240 - CountDownLatch latch = new CountDownLatch(1);  
241 -  
242 // 1. 连接并认证 237 // 1. 连接并认证
243 MqttClient client = connectAndAuth(); 238 MqttClient client = connectAndAuth();
244 log.info("[testSubscribe][连接认证成功]"); 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,7 +264,7 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
286 * @param authInfo 认证信息 264 * @param authInfo 认证信息
287 * @return MQTT 客户端 265 * @return MQTT 客户端
288 */ 266 */
289 - private MqttClient connect(IotDeviceAuthReqDTO authInfo) { 267 + private MqttClient createClient(IotDeviceAuthReqDTO authInfo) {
290 MqttClientOptions options = new MqttClientOptions() 268 MqttClientOptions options = new MqttClientOptions()
291 .setClientId(authInfo.getClientId()) 269 .setClientId(authInfo.getClientId())
292 .setUsername(authInfo.getUsername()) 270 .setUsername(authInfo.getUsername())
@@ -302,44 +280,23 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { @@ -302,44 +280,23 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
302 * @return 已认证的 MQTT 客户端 280 * @return 已认证的 MQTT 客户端
303 */ 281 */
304 private MqttClient connectAndAuth() throws Exception { 282 private MqttClient connectAndAuth() throws Exception {
305 - // 1. 创建客户端并连接  
306 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); 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 client.connect(SERVER_PORT, SERVER_HOST) 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,34 +307,28 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
350 * @param request 请求消息 307 * @param request 请求消息
351 * @return 响应消息 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 // 1. 设置消息处理器,接收响应 312 // 1. 设置消息处理器,接收响应
355 - CompletableFuture<IotDeviceMessage> future = new CompletableFuture<>(); 313 + CompletableFuture<IotDeviceMessage> responseFuture = new CompletableFuture<>();
356 client.publishHandler(message -> { 314 client.publishHandler(message -> {
357 log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]", 315 log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]",
358 message.topicName(), message.payload().toString()); 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 client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false) 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 try { 330 try {
380 - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); 331 + return responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
381 } catch (Exception e) { 332 } catch (Exception e) {
382 log.warn("[publishAndWaitReply][等待响应超时或失败]"); 333 log.warn("[publishAndWaitReply][等待响应超时或失败]");
383 return null; 334 return null;
@@ -390,19 +341,9 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { @@ -390,19 +341,9 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
390 * @param client MQTT 客户端 341 * @param client MQTT 客户端
391 */ 342 */
392 private void disconnect(MqttClient client) throws Exception { 343 private void disconnect(MqttClient client) throws Exception {
393 - // 1. 断开连接  
394 - CompletableFuture<Void> future = new CompletableFuture<>();  
395 client.disconnect() 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,7 +2,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;
2 2
3 import cn.hutool.core.collection.ListUtil; 3 import cn.hutool.core.collection.ListUtil;
4 import cn.hutool.core.map.MapUtil; 4 import cn.hutool.core.map.MapUtil;
5 -import cn.hutool.core.util.IdUtil;  
6 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; 5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
7 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; 6 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
8 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 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,8 +12,8 @@ import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO;
13 import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO; 12 import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO;
14 import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetReqDTO; 13 import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetReqDTO;
15 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; 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 import io.netty.handler.codec.mqtt.MqttQoS; 17 import io.netty.handler.codec.mqtt.MqttQoS;
19 import io.vertx.core.Vertx; 18 import io.vertx.core.Vertx;
20 import io.vertx.core.buffer.Buffer; 19 import io.vertx.core.buffer.Buffer;
@@ -27,10 +26,8 @@ import org.junit.jupiter.api.Disabled; @@ -27,10 +26,8 @@ import org.junit.jupiter.api.Disabled;
27 import org.junit.jupiter.api.Test; 26 import org.junit.jupiter.api.Test;
28 27
29 import java.util.Collections; 28 import java.util.Collections;
30 -import java.util.List;  
31 import java.util.Map; 29 import java.util.Map;
32 import java.util.concurrent.CompletableFuture; 30 import java.util.concurrent.CompletableFuture;
33 -import java.util.concurrent.CountDownLatch;  
34 import java.util.concurrent.TimeUnit; 31 import java.util.concurrent.TimeUnit;
35 32
36 /** 33 /**
@@ -68,9 +65,9 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { @@ -68,9 +65,9 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
68 65
69 private static Vertx vertx; 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 // ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) ===================== 72 // ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) =====================
76 73
@@ -103,8 +100,6 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { @@ -103,8 +100,6 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
103 */ 100 */
104 @Test 101 @Test
105 public void testAuth() throws Exception { 102 public void testAuth() throws Exception {
106 - CountDownLatch latch = new CountDownLatch(1);  
107 -  
108 // 1. 构建认证信息 103 // 1. 构建认证信息
109 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( 104 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(
110 GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET); 105 GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET);
@@ -112,31 +107,13 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { @@ -112,31 +107,13 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
112 authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword()); 107 authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword());
113 108
114 // 2. 创建客户端并连接 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,36 +130,35 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
153 MqttClient client = connectAndAuth(); 130 MqttClient client = connectAndAuth();
154 log.info("[testTopoAdd][连接认证成功]"); 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,29 +172,28 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
196 MqttClient client = connectAndAuth(); 172 MqttClient client = connectAndAuth();
197 log.info("[testTopoDelete][连接认证成功]"); 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,27 +207,26 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
232 MqttClient client = connectAndAuth(); 207 MqttClient client = connectAndAuth();
233 log.info("[testTopoGet][连接认证成功]"); 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,29 +244,28 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
270 MqttClient client = connectAndAuth(); 244 MqttClient client = connectAndAuth();
271 log.info("[testSubDeviceRegister][连接认证成功]"); 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,64 +281,63 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
308 MqttClient client = connectAndAuth(); 281 MqttClient client = connectAndAuth();
309 log.info("[testPropertyPackPost][连接认证成功]"); 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,7 +348,7 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
376 * @param authInfo 认证信息 348 * @param authInfo 认证信息
377 * @return MQTT 客户端 349 * @return MQTT 客户端
378 */ 350 */
379 - private MqttClient connect(IotDeviceAuthReqDTO authInfo) { 351 + private MqttClient createClient(IotDeviceAuthReqDTO authInfo) {
380 MqttClientOptions options = new MqttClientOptions() 352 MqttClientOptions options = new MqttClientOptions()
381 .setClientId(authInfo.getClientId()) 353 .setClientId(authInfo.getClientId())
382 .setUsername(authInfo.getUsername()) 354 .setUsername(authInfo.getUsername())
@@ -392,45 +364,24 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { @@ -392,45 +364,24 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
392 * @return 已认证的 MQTT 客户端 364 * @return 已认证的 MQTT 客户端
393 */ 365 */
394 private MqttClient connectAndAuth() throws Exception { 366 private MqttClient connectAndAuth() throws Exception {
395 - // 1. 创建客户端并连接  
396 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( 367 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(
397 GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET); 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 client.connect(SERVER_PORT, SERVER_HOST) 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,34 +392,28 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
441 * @param request 请求消息 392 * @param request 请求消息
442 * @return 响应消息 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 // 1. 设置消息处理器,接收响应 397 // 1. 设置消息处理器,接收响应
446 - CompletableFuture<IotDeviceMessage> future = new CompletableFuture<>(); 398 + CompletableFuture<IotDeviceMessage> responseFuture = new CompletableFuture<>();
447 client.publishHandler(message -> { 399 client.publishHandler(message -> {
448 log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]", 400 log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]",
449 message.topicName(), message.payload().toString()); 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 client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false) 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 try { 415 try {
471 - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); 416 + return responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
472 } catch (Exception e) { 417 } catch (Exception e) {
473 log.warn("[publishAndWaitReply][等待响应超时或失败]"); 418 log.warn("[publishAndWaitReply][等待响应超时或失败]");
474 return null; 419 return null;
@@ -481,19 +426,9 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { @@ -481,19 +426,9 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
481 * @param client MQTT 客户端 426 * @param client MQTT 客户端
482 */ 427 */
483 private void disconnect(MqttClient client) throws Exception { 428 private void disconnect(MqttClient client) throws Exception {
484 - // 1. 断开连接  
485 - CompletableFuture<Void> future = new CompletableFuture<>();  
486 client.disconnect() 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 package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; 1 package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;
2 2
3 import cn.hutool.core.map.MapUtil; 3 import cn.hutool.core.map.MapUtil;
4 -import cn.hutool.core.util.IdUtil;  
5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; 4 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
6 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; 5 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
7 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
8 import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; 7 import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
9 import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; 8 import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
10 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; 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 import io.netty.handler.codec.mqtt.MqttQoS; 12 import io.netty.handler.codec.mqtt.MqttQoS;
14 import io.vertx.core.Vertx; 13 import io.vertx.core.Vertx;
15 import io.vertx.core.buffer.Buffer; 14 import io.vertx.core.buffer.Buffer;
@@ -22,7 +21,6 @@ import org.junit.jupiter.api.Disabled; @@ -22,7 +21,6 @@ import org.junit.jupiter.api.Disabled;
22 import org.junit.jupiter.api.Test; 21 import org.junit.jupiter.api.Test;
23 22
24 import java.util.concurrent.CompletableFuture; 23 import java.util.concurrent.CompletableFuture;
25 -import java.util.concurrent.CountDownLatch;  
26 import java.util.concurrent.TimeUnit; 24 import java.util.concurrent.TimeUnit;
27 25
28 /** 26 /**
@@ -61,9 +59,9 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { @@ -61,9 +59,9 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest {
61 59
62 private static Vertx vertx; 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 // ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) ===================== 66 // ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
69 67
@@ -90,39 +88,19 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { @@ -90,39 +88,19 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest {
90 */ 88 */
91 @Test 89 @Test
92 public void testAuth() throws Exception { 90 public void testAuth() throws Exception {
93 - CountDownLatch latch = new CountDownLatch(1);  
94 -  
95 // 1. 构建认证信息 91 // 1. 构建认证信息
96 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); 92 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
97 log.info("[testAuth][认证信息: clientId={}, username={}, password={}]", 93 log.info("[testAuth][认证信息: clientId={}, username={}, password={}]",
98 authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword()); 94 authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword());
99 95
100 // 2. 创建客户端并连接 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,28 +116,27 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest {
138 log.info("[testPropertyPost][连接认证成功]"); 116 log.info("[testPropertyPost][连接认证成功]");
139 log.info("[testPropertyPost][子设备属性上报 - 请求实际由 Gateway 代为转发]"); 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,32 +151,31 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest {
174 log.info("[testEventPost][连接认证成功]"); 151 log.info("[testEventPost][连接认证成功]");
175 log.info("[testEventPost][子设备事件上报 - 请求实际由 Gateway 代为转发]"); 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,7 +186,7 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest {
210 * @param authInfo 认证信息 186 * @param authInfo 认证信息
211 * @return MQTT 客户端 187 * @return MQTT 客户端
212 */ 188 */
213 - private MqttClient connect(IotDeviceAuthReqDTO authInfo) { 189 + private MqttClient createClient(IotDeviceAuthReqDTO authInfo) {
214 MqttClientOptions options = new MqttClientOptions() 190 MqttClientOptions options = new MqttClientOptions()
215 .setClientId(authInfo.getClientId()) 191 .setClientId(authInfo.getClientId())
216 .setUsername(authInfo.getUsername()) 192 .setUsername(authInfo.getUsername())
@@ -226,44 +202,23 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { @@ -226,44 +202,23 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest {
226 * @return 已认证的 MQTT 客户端 202 * @return 已认证的 MQTT 客户端
227 */ 203 */
228 private MqttClient connectAndAuth() throws Exception { 204 private MqttClient connectAndAuth() throws Exception {
229 - // 1. 创建客户端并连接  
230 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); 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 client.connect(SERVER_PORT, SERVER_HOST) 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,34 +229,28 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest {
274 * @param request 请求消息 229 * @param request 请求消息
275 * @return 响应消息 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 // 1. 设置消息处理器,接收响应 234 // 1. 设置消息处理器,接收响应
279 - CompletableFuture<IotDeviceMessage> future = new CompletableFuture<>(); 235 + CompletableFuture<IotDeviceMessage> responseFuture = new CompletableFuture<>();
280 client.publishHandler(message -> { 236 client.publishHandler(message -> {
281 log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]", 237 log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]",
282 message.topicName(), message.payload().toString()); 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 client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false) 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 try { 252 try {
304 - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); 253 + return responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
305 } catch (Exception e) { 254 } catch (Exception e) {
306 log.warn("[publishAndWaitReply][等待响应超时或失败]"); 255 log.warn("[publishAndWaitReply][等待响应超时或失败]");
307 return null; 256 return null;
@@ -314,19 +263,9 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { @@ -314,19 +263,9 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest {
314 * @param client MQTT 客户端 263 * @param client MQTT 客户端
315 */ 264 */
316 private void disconnect(MqttClient client) throws Exception { 265 private void disconnect(MqttClient client) throws Exception {
317 - // 1. 断开连接  
318 - CompletableFuture<Void> future = new CompletableFuture<>();  
319 client.disconnect() 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,6 +8,7 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
8 import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; 8 import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
9 import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; 9 import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
10 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; 10 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  11 +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils;
11 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum; 12 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum;
12 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; 13 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec;
13 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodecFactory; 14 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodecFactory;
@@ -146,10 +147,13 @@ public class IotDirectDeviceTcpProtocolIntegrationTest { @@ -146,10 +147,13 @@ public class IotDirectDeviceTcpProtocolIntegrationTest {
146 @Test 147 @Test
147 public void testDeviceRegister() throws Exception { 148 public void testDeviceRegister() throws Exception {
148 // 1. 构建注册消息 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 IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO() 153 IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO()
150 .setProductKey(PRODUCT_KEY) 154 .setProductKey(PRODUCT_KEY)
151 - .setDeviceName("test-tcp-" + System.currentTimeMillis())  
152 - .setProductSecret("test-product-secret"); 155 + .setDeviceName(deviceName)
  156 + .setSign(sign);
153 IotDeviceMessage request = IotDeviceMessage.requestOf( 157 IotDeviceMessage request = IotDeviceMessage.requestOf(
154 IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO); 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,6 +8,7 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
8 import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; 8 import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
9 import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; 9 import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
10 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; 10 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  11 +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.IotMessageSerializer;
12 import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; 13 import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer;
13 import lombok.extern.slf4j.Slf4j; 14 import lombok.extern.slf4j.Slf4j;
@@ -100,10 +101,13 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { @@ -100,10 +101,13 @@ public class IotDirectDeviceUdpProtocolIntegrationTest {
100 @Test 101 @Test
101 public void testDeviceRegister() throws Exception { 102 public void testDeviceRegister() throws Exception {
102 // 1. 构建注册消息 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 IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO() 107 IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO()
104 .setProductKey(PRODUCT_KEY) 108 .setProductKey(PRODUCT_KEY)
105 - .setDeviceName("test-udp-" + System.currentTimeMillis())  
106 - .setProductSecret("test-product-secret"); 109 + .setDeviceName(deviceName)
  110 + .setSign(sign);
107 IotDeviceMessage request = IotDeviceMessage.requestOf( 111 IotDeviceMessage request = IotDeviceMessage.requestOf(
108 IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO); 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,6 +10,7 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
10 import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; 10 import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
11 import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; 11 import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
12 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; 12 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  13 +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils;
13 import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; 14 import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
14 import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; 15 import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer;
15 import io.vertx.core.Vertx; 16 import io.vertx.core.Vertx;
@@ -131,10 +132,13 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest { @@ -131,10 +132,13 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest {
131 @Test 132 @Test
132 public void testDeviceRegister() throws Exception { 133 public void testDeviceRegister() throws Exception {
133 // 1.1 构建注册消息 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 IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), 142 IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(),
139 IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO, null, null, null); 143 IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO, null, null, null);
140 // 1.2 序列化 144 // 1.2 序列化