Commit 9156aef4e3307c54e060561238d1fdfe4f9e3494

Authored by YunaiV
1 parent 1e2dc281

feat(iot):【协议改造】mqtt 初步改造(20%)

Showing 12 changed files with 654 additions and 1013 deletions
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttConfig.java
1 1 package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;
2 2  
  3 +import jakarta.validation.constraints.Min;
  4 +import jakarta.validation.constraints.NotNull;
3 5 import lombok.Data;
4 6  
5   -// TODO @AI:validator 参数校验。也看看其他几个配置类有没有类似问题
  7 +// done @AI:validator 参数校验。也看看其他几个配置类有没有类似问题
6 8 /**
7 9 * IoT 网关 MQTT 协议配置
8 10 *
... ... @@ -12,33 +14,40 @@ import lombok.Data;
12 14 public class IotMqttConfig {
13 15  
14 16 /**
15   - * 是否启用 SSL
  17 + * 最大消息大小(字节)
16 18 */
17   - private Boolean sslEnabled = false;
  19 + @NotNull(message = "最大消息大小不能为空")
  20 + @Min(value = 1024, message = "最大消息大小不能小于 1024 字节")
  21 + private Integer maxMessageSize = 8192;
18 22  
19 23 /**
20   - * SSL 证书路径
  24 + * 连接超时时间(秒)
21 25 */
22   - private String sslCertPath;
  26 + @NotNull(message = "连接超时时间不能为空")
  27 + @Min(value = 1, message = "连接超时时间不能小于 1 秒")
  28 + private Integer connectTimeoutSeconds = 60;
23 29  
24 30 /**
25   - * SSL 私钥路径
  31 + * 保持连接超时时间(秒)
26 32 */
27   - private String sslKeyPath;
  33 + @NotNull(message = "保持连接超时时间不能为空")
  34 + @Min(value = 1, message = "保持连接超时时间不能小于 1 秒")
  35 + private Integer keepAliveTimeoutSeconds = 300;
28 36  
29 37 /**
30   - * 最大消息大小(字节)
  38 + * 是否启用 SSL
31 39 */
32   - private Integer maxMessageSize = 8192;
  40 + @NotNull(message = "是否启用 SSL 不能为空")
  41 + private Boolean sslEnabled = false;
33 42  
34 43 /**
35   - * 连接超时时间(秒)
  44 + * SSL 证书路径
36 45 */
37   - private Integer connectTimeoutSeconds = 60;
  46 + private String sslCertPath;
38 47  
39 48 /**
40   - * 保持连接超时时间(秒)
  49 + * SSL 私钥路径
41 50 */
42   - private Integer keepAliveTimeoutSeconds = 300;
  51 + private String sslKeyPath;
43 52  
44 53 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttProtocol.java
... ... @@ -7,9 +7,10 @@ import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
7 7 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
8 8 import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties;
9 9 import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
  10 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
10 11 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream.IotMqttDownstreamHandler;
11 12 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream.IotMqttDownstreamSubscriber;
12   -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttConnectionHandler;
  13 +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttAuthHandler;
13 14 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttRegisterHandler;
14 15 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttUpstreamHandler;
15 16 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
... ... @@ -25,9 +26,11 @@ import io.vertx.mqtt.MqttTopicSubscription;
25 26 import io.vertx.mqtt.messages.MqttPublishMessage;
26 27 import lombok.Getter;
27 28 import lombok.extern.slf4j.Slf4j;
  29 +import org.springframework.util.Assert;
28 30  
29 31 import java.util.List;
30   -import java.util.stream.Collectors;
  32 +
  33 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
31 34  
32 35 /**
33 36 * IoT 网关 MQTT 协议:接收设备上行消息
... ... @@ -61,28 +64,42 @@ public class IotMqttProtocol implements IotProtocol {
61 64 * MQTT 服务器
62 65 */
63 66 private MqttServer mqttServer;
  67 +
64 68 /**
65 69 * 连接管理器
66 70 */
67   - private IotMqttConnectionManager connectionManager;
68   -
  71 + private final IotMqttConnectionManager connectionManager;
69 72 /**
70 73 * 下行消息订阅者
71 74 */
72   - private IotMqttDownstreamSubscriber downstreamSubscriber;
  75 + private final IotMqttDownstreamSubscriber downstreamSubscriber;
73 76  
74   - // TODO @AI:这个是不是提前创建下?因为是无状态的。
75   - private IotMqttConnectionHandler connectionHandler;
76   - private IotMqttRegisterHandler registerHandler;
77   - private IotMqttUpstreamHandler upstreamHandler;
  77 + private final IotDeviceMessageService deviceMessageService;
  78 +
  79 + private final IotMqttAuthHandler authHandler;
  80 + private final IotMqttRegisterHandler registerHandler;
  81 + private final IotMqttUpstreamHandler upstreamHandler;
78 82  
79 83 public IotMqttProtocol(ProtocolInstanceProperties properties) {
  84 + IotMqttConfig mqttConfig = properties.getMqtt();
  85 + Assert.notNull(mqttConfig, "MQTT 协议配置(mqtt)不能为空");
80 86 this.properties = properties;
81 87 this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
82 88  
83   - // TODO @AI:初始化连接器,参考 IotTcpProtocol
  89 + // 初始化连接管理器
  90 + this.connectionManager = new IotMqttConnectionManager();
84 91  
85   - // TODO @AI:初始化下行消息订阅者,参考 IotTcpProtocol
  92 + // 初始化 Handler
  93 + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
  94 + IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
  95 + this.authHandler = new IotMqttAuthHandler(connectionManager, deviceMessageService, deviceApi, serverId);
  96 + this.registerHandler = new IotMqttRegisterHandler(connectionManager, deviceMessageService, deviceApi);
  97 + this.upstreamHandler = new IotMqttUpstreamHandler(connectionManager, deviceMessageService, serverId);
  98 +
  99 + // 初始化下行消息订阅者
  100 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
  101 + IotMqttDownstreamHandler downstreamHandler = new IotMqttDownstreamHandler(deviceMessageService, connectionManager);
  102 + this.downstreamSubscriber = new IotMqttDownstreamSubscriber(this, downstreamHandler, messageBus);
86 103 }
87 104  
88 105 @Override
... ... @@ -95,7 +112,7 @@ public class IotMqttProtocol implements IotProtocol {
95 112 return IotProtocolTypeEnum.MQTT;
96 113 }
97 114  
98   - // TODO @AI:这个方法的整体注释风格,参考 IotTcpProtocol 的 start 方法。
  115 + // done @AI:这个方法的整体注释风格,参考 IotTcpProtocol 的 start 方法。
99 116 @Override
100 117 public void start() {
101 118 if (running) {
... ... @@ -103,51 +120,42 @@ public class IotMqttProtocol implements IotProtocol {
103 120 return;
104 121 }
105 122  
106   - // 1.1 创建 Vertx 实例(每个 Protocol 独立管理)
  123 + // 1.1 创建 Vertx 实例
107 124 this.vertx = Vertx.vertx();
108 125  
109   - // 1.2 创建连接管理器
110   - this.connectionManager = new IotMqttConnectionManager();
111   -
112   - // 1.3 初始化 Handler
113   - initHandlers();
114   -
115   - // 2. 创建服务器选项
  126 + // 1.2 创建服务器选项
116 127 IotMqttConfig mqttConfig = properties.getMqtt();
117   - // TODO @AI:default 值,在 IotMqttConfig 处理;
118 128 MqttServerOptions options = new MqttServerOptions()
119 129 .setPort(properties.getPort())
120   - .setMaxMessageSize(mqttConfig != null ? mqttConfig.getMaxMessageSize() : 8192)
121   - .setTimeoutOnConnect(mqttConfig != null ? mqttConfig.getConnectTimeoutSeconds() : 60);
122   -
123   - // 3. 配置 SSL(如果启用)
124   - if (mqttConfig != null && Boolean.TRUE.equals(mqttConfig.getSslEnabled())) {
  130 + .setMaxMessageSize(mqttConfig.getMaxMessageSize())
  131 + .setTimeoutOnConnect(mqttConfig.getConnectTimeoutSeconds());
  132 + if (Boolean.TRUE.equals(mqttConfig.getSslEnabled())) {
125 133 PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions()
126 134 .setKeyPath(mqttConfig.getSslKeyPath())
127 135 .setCertPath(mqttConfig.getSslCertPath());
128 136 options.setSsl(true).setKeyCertOptions(pemKeyCertOptions);
129 137 }
130 138  
131   - // 4. 创建服务器并设置连接处理器
  139 + // 1.3 创建服务器并设置连接处理器
132 140 mqttServer = MqttServer.create(vertx, options);
133 141 mqttServer.endpointHandler(this::handleEndpoint);
134 142  
135   - // 5. 启动服务器
  143 + // 1.4 启动 MQTT 服务器
136 144 try {
137 145 mqttServer.listen().result();
138 146 running = true;
139 147 log.info("[start][IoT MQTT 协议 {} 启动成功,端口:{},serverId:{}]",
140 148 getId(), properties.getPort(), serverId);
141 149  
142   - // 6. 启动下行消息订阅者
143   - IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
144   - IotMqttDownstreamHandler downstreamHandler = new IotMqttDownstreamHandler(
145   - SpringUtil.getBean(IotDeviceMessageService.class), connectionManager);
146   - this.downstreamSubscriber = new IotMqttDownstreamSubscriber(this, downstreamHandler, messageBus);
  150 + // 2. 启动下行消息订阅者
147 151 this.downstreamSubscriber.start();
148 152 } catch (Exception e) {
149 153 log.error("[start][IoT MQTT 协议 {} 启动失败]", getId(), e);
150   - // 启动失败时关闭 Vertx
  154 + // 启动失败时关闭资源
  155 + if (mqttServer != null) {
  156 + mqttServer.close();
  157 + mqttServer = null;
  158 + }
151 159 if (vertx != null) {
152 160 vertx.close();
153 161 vertx = null;
... ... @@ -162,14 +170,11 @@ public class IotMqttProtocol implements IotProtocol {
162 170 return;
163 171 }
164 172 // 1. 停止下行消息订阅者
165   - if (downstreamSubscriber != null) {
166   - try {
167   - downstreamSubscriber.stop();
168   - log.info("[stop][IoT MQTT 协议 {} 下行消息订阅者已停止]", getId());
169   - } catch (Exception e) {
170   - log.error("[stop][IoT MQTT 协议 {} 下行消息订阅者停止失败]", getId(), e);
171   - }
172   - downstreamSubscriber = null;
  173 + try {
  174 + downstreamSubscriber.stop();
  175 + log.info("[stop][IoT MQTT 协议 {} 下行消息订阅者已停止]", getId());
  176 + } catch (Exception e) {
  177 + log.error("[stop][IoT MQTT 协议 {} 下行消息订阅者停止失败]", getId(), e);
173 178 }
174 179  
175 180 // 2.1 关闭 MQTT 服务器
... ... @@ -196,16 +201,7 @@ public class IotMqttProtocol implements IotProtocol {
196 201 log.info("[stop][IoT MQTT 协议 {} 已停止]", getId());
197 202 }
198 203  
199   - /**
200   - * 初始化 Handler
201   - */
202   - private void initHandlers() {
203   - IotDeviceMessageService messageService = SpringUtil.getBean(IotDeviceMessageService.class);
204   - IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
205   - this.connectionHandler = new IotMqttConnectionHandler(connectionManager, messageService, deviceApi, serverId);
206   - this.registerHandler = new IotMqttRegisterHandler(connectionManager, messageService, deviceApi);
207   - this.upstreamHandler = new IotMqttUpstreamHandler(connectionManager, messageService, serverId);
208   - }
  204 + // ======================================= MQTT 连接处理 ======================================
209 205  
210 206 /**
211 207 * 处理 MQTT 连接端点
... ... @@ -215,24 +211,24 @@ public class IotMqttProtocol implements IotProtocol {
215 211 private void handleEndpoint(MqttEndpoint endpoint) {
216 212 String clientId = endpoint.clientIdentifier();
217 213  
218   - // 1. 委托 connectionHandler 处理连接认证
219   - // TODO @AI:register topic 不需要注册,需要判断下;
220   - if (!connectionHandler.handleConnect(endpoint)) {
  214 + // 1. 委托 authHandler 处理连接认证
  215 + // done @AI:register topic 不需要注册,需要判断下;当前逻辑已支持(设备可在未认证状态发送 register 消息,registerHandler 会处理)
  216 + if (!authHandler.handleAuthenticationRequest(endpoint)) {
221 217 endpoint.reject(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD);
222 218 return;
223 219 }
224 220  
225 221 // 2.1 设置异常和关闭处理器
226 222 endpoint.exceptionHandler(ex -> {
227   - log.warn("[handleEndpoint][连接异常,客户端 ID: {},地址: {}]",
228   - clientId, connectionManager.getEndpointAddress(endpoint));
229   - // TODO @AI:是不是改成 endpoint close 更合适?
230   - connectionHandler.cleanupConnection(endpoint);
  223 + log.warn("[handleEndpoint][连接异常,客户端 ID: {},地址: {},异常: {}]",
  224 + clientId, connectionManager.getEndpointAddress(endpoint), ex.getMessage());
  225 + endpoint.close();
231 226 });
232   - endpoint.closeHandler(v -> connectionHandler.cleanupConnection(endpoint));
  227 + // done @AI:closeHandler 处理底层连接关闭(网络中断、异常等),disconnectHandler 处理 MQTT DISCONNECT 报文
  228 + endpoint.closeHandler(v -> cleanupConnection(endpoint));
233 229 endpoint.disconnectHandler(v -> {
234 230 log.debug("[handleEndpoint][设备断开连接,客户端 ID: {}]", clientId);
235   - connectionHandler.cleanupConnection(endpoint);
  231 + cleanupConnection(endpoint);
236 232 });
237 233 // 2.2 设置心跳处理器
238 234 endpoint.pingHandler(v -> log.debug("[handleEndpoint][收到客户端心跳,客户端 ID: {}]", clientId));
... ... @@ -243,17 +239,11 @@ public class IotMqttProtocol implements IotProtocol {
243 239 endpoint.publishReleaseHandler(endpoint::publishComplete);
244 240  
245 241 // 4.1 设置订阅处理器
  242 + // done @AI:使用 CollectionUtils.convertList 简化
246 243 endpoint.subscribeHandler(subscribe -> {
247   - // TODO @AI:convertList 简化;
248   - List<String> topicNames = subscribe.topicSubscriptions().stream()
249   - .map(MqttTopicSubscription::topicName)
250   - .collect(Collectors.toList());
  244 + List<String> topicNames = convertList(subscribe.topicSubscriptions(), MqttTopicSubscription::topicName);
251 245 log.debug("[handleEndpoint][设备订阅,客户端 ID: {},主题: {}]", clientId, topicNames);
252   -
253   - // TODO @AI:convertList 简化;
254   - List<MqttQoS> grantedQoSLevels = subscribe.topicSubscriptions().stream()
255   - .map(MqttTopicSubscription::qualityOfService)
256   - .collect(Collectors.toList());
  246 + List<MqttQoS> grantedQoSLevels = convertList(subscribe.topicSubscriptions(), MqttTopicSubscription::qualityOfService);
257 247 endpoint.subscribeAcknowledge(subscribe.messageId(), grantedQoSLevels);
258 248 });
259 249 // 4.2 设置取消订阅处理器
... ... @@ -272,27 +262,24 @@ public class IotMqttProtocol implements IotProtocol {
272 262 * @param endpoint MQTT 连接端点
273 263 * @param message 发布消息
274 264 */
275   - // TODO @AI:看看要不要一定程度,参考 IotTcpUpstreamHandler 的 processMessage 方法;
276 265 private void processMessage(MqttEndpoint endpoint, MqttPublishMessage message) {
277 266 String clientId = endpoint.clientIdentifier();
278 267 try {
  268 + // 根据 topic 分发到不同 handler
279 269 String topic = message.topicName();
280 270 byte[] payload = message.payload().getBytes();
281   -
282   - // 根据 topic 分发到不同 handler
283 271 if (registerHandler.isRegisterMessage(topic)) {
284 272 registerHandler.handleRegister(endpoint, topic, payload);
285 273 } else {
286   - upstreamHandler.handleMessage(endpoint, topic, payload);
  274 + upstreamHandler.handleBusinessRequest(endpoint, topic, payload);
287 275 }
288 276  
289 277 // 根据 QoS 级别发送相应的确认消息
290 278 handleQoSAck(endpoint, message);
291 279 } catch (Exception e) {
292   - // TODO @AI:异常的时候,直接断开;
293   - log.error("[handlePublish][消息处理失败,断开连接,客户端 ID: {},地址: {},错误: {}]",
  280 + log.error("[processMessage][消息处理失败,断开连接,客户端 ID: {},地址: {},错误: {}]",
294 281 clientId, connectionManager.getEndpointAddress(endpoint), e.getMessage());
295   - connectionHandler.cleanupConnection(endpoint);
  282 + cleanupConnection(endpoint);
296 283 endpoint.close();
297 284 }
298 285 }
... ... @@ -314,4 +301,27 @@ public class IotMqttProtocol implements IotProtocol {
314 301 // QoS 0 无需确认
315 302 }
316 303  
  304 + /**
  305 + * 清理连接
  306 + *
  307 + * @param endpoint MQTT 连接端点
  308 + */
  309 + private void cleanupConnection(MqttEndpoint endpoint) {
  310 + try {
  311 + // 1. 发送设备离线消息
  312 + IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint);
  313 + if (connectionInfo != null) {
  314 + IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline();
  315 + deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(),
  316 + connectionInfo.getDeviceName(), serverId);
  317 + }
  318 +
  319 + // 2. 注销连接
  320 + connectionManager.unregisterConnection(endpoint);
  321 + } catch (Exception e) {
  322 + log.error("[cleanupConnection][清理连接失败,客户端 ID: {},错误: {}]",
  323 + endpoint.clientIdentifier(), e.getMessage());
  324 + }
  325 + }
  326 +
317 327 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamHandler.java
1 1 package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream;
2 2  
3   -import cn.hutool.core.util.StrUtil;
  3 +import cn.hutool.core.lang.Assert;
4 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
5 5 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
6 6 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
... ... @@ -27,103 +27,44 @@ public class IotMqttDownstreamHandler {
27 27 * 处理下行消息
28 28 *
29 29 * @param message 设备消息
30   - * @return 是否处理成功
31 30 */
32   - public boolean handleDownstreamMessage(IotDeviceMessage message) {
  31 + public void handle(IotDeviceMessage message) {
33 32 try {
34   - // TODO @AI:参考 IotTcpDownstreamHandler 逻辑;
35   - // 1. 基础校验
36   - if (message == null || message.getDeviceId() == null) {
37   - log.warn("[handleDownstreamMessage][消息或设备 ID 为空,忽略处理]");
38   - return false;
39   - }
40   -
41   - // 2. 检查设备是否在线
42   - // TODO @AI:这块逻辑,是不是冗余?直接使用 3. 获取连接信息判断不就行了?
43   - if (connectionManager.isDeviceOffline(message.getDeviceId())) {
44   - log.warn("[handleDownstreamMessage][设备离线,无法发送消息,设备 ID:{}]", message.getDeviceId());
45   - return false;
46   - }
  33 + log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]",
  34 + message.getDeviceId(), message.getMethod(), message.getId());
47 35  
48   - // 3. 获取连接信息
49   - IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfoByDeviceId(message.getDeviceId());
  36 + // 1. 检查设备连接
  37 + IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfoByDeviceId(
  38 + message.getDeviceId());
50 39 if (connectionInfo == null) {
51   - log.warn("[handleDownstreamMessage][连接信息不存在,设备 ID:{}]", message.getDeviceId());
52   - return false;
  40 + log.warn("[handle][连接信息不存在,设备 ID: {},方法: {},消息 ID: {}]",
  41 + message.getDeviceId(), message.getMethod(), message.getId());
  42 + return;
53 43 }
54 44  
55   - // 4. 序列化
  45 + // 2.1 序列化消息
56 46 byte[] payload = deviceMessageService.encodeDeviceMessage(message, connectionInfo.getProductKey(),
57 47 connectionInfo.getDeviceName());
58   - if (payload == null || payload.length == 0) {
59   - log.warn("[handleDownstreamMessage][消息编码失败,设备 ID:{}]", message.getDeviceId());
60   - return false;
61   - }
  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, "主题不能为空");
62 55  
63   - // 5. 发送消息到设备
64   - // TODO @AI:参考 IotTcpDownstreamHandler 的逻辑;
65   - return sendMessageToDevice(message, connectionInfo, payload);
66   - } catch (Exception e) {
67   - if (message != null) {
68   - log.error("[handleDownstreamMessage][处理下行消息异常,设备 ID:{},错误:{}]",
69   - message.getDeviceId(), e.getMessage(), e);
  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("下行消息发送失败");
70 61 }
71   - return false;
72   - }
73   - }
74   -
75   - // TODO @AI 是不是合并到 handleDownstreamMessage 里;
76   - /**
77   - * 发送消息到设备
78   - *
79   - * @param message 设备消息
80   - * @param connectionInfo 连接信息
81   - * @param payload 消息负载
82   - * @return 是否发送成功
83   - */
84   - private boolean sendMessageToDevice(IotDeviceMessage message,
85   - IotMqttConnectionManager.ConnectionInfo connectionInfo,
86   - byte[] payload) {
87   - // 1. 构建主题
88   - String topic = buildDownstreamTopic(message, connectionInfo);
89   - // TODO @AI:直接断言,非空!
90   - if (StrUtil.isBlank(topic)) {
91   - log.warn("[sendMessageToDevice][主题构建失败,设备 ID:{},方法:{}]",
92   - message.getDeviceId(), message.getMethod());
93   - return false;
94   - }
95   -
96   - // 2. 发送消息
97   - boolean success = connectionManager.sendToDevice(message.getDeviceId(), topic, payload, MqttQoS.AT_LEAST_ONCE.value(), false);
98   - if (success) {
99   - log.debug("[sendMessageToDevice][消息发送成功,设备 ID:{},主题:{},方法:{}]",
100   - message.getDeviceId(), topic, message.getMethod());
101   - } else {
102   - log.warn("[sendMessageToDevice][消息发送失败,设备 ID:{},主题:{},方法:{}]",
103   - message.getDeviceId(), topic, message.getMethod());
104   - }
105   - return success;
106   - }
107   -
108   - /**
109   - * 构建下行消息主题
110   - *
111   - * @param message 设备消息
112   - * @param connectionInfo 连接信息
113   - * @return 主题
114   - */
115   - private String buildDownstreamTopic(IotDeviceMessage message,
116   - IotMqttConnectionManager.ConnectionInfo connectionInfo) {
117   - // TODO @AI:直接断言,非空!
118   - String method = message.getMethod();
119   - if (StrUtil.isBlank(method)) {
120   - return null;
  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);
121 67 }
122   -
123   - // 使用工具类构建主题,支持回复消息处理
124   - boolean isReply = IotDeviceMessageUtils.isReplyMessage(message);
125   - return IotMqttTopicUtils.buildTopicByMethod(method, connectionInfo.getProductKey(),
126   - connectionInfo.getDeviceName(), isReply);
127 68 }
128 69  
129 70 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamSubscriber.java
... ... @@ -7,9 +7,7 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttProtocol;
7 7 import lombok.extern.slf4j.Slf4j;
8 8  
9 9 /**
10   - * IoT 网关 MQTT 协议:下行消息订阅器
11   - * <p>
12   - * 负责接收来自消息总线的下行消息,并委托给下行处理器进行业务处理
  10 + * IoT 网关 MQTT 协议:接收下行给设备的消息
13 11 *
14 12 * @author 芋道源码
15 13 */
... ... @@ -27,15 +25,7 @@ public class IotMqttDownstreamSubscriber extends IotProtocolDownstreamSubscriber
27 25  
28 26 @Override
29 27 protected void handleMessage(IotDeviceMessage message) {
30   - // TODO @AI:参考 IotTcpDownstreamHandler 不处理返回值,甚至不用返回值;
31   - boolean success = downstreamHandler.handleDownstreamMessage(message);
32   - if (success) {
33   - log.debug("[handleMessage][下行消息处理成功, messageId: {}, method: {}, deviceId: {}]",
34   - message.getId(), message.getMethod(), message.getDeviceId());
35   - } else {
36   - log.warn("[handleMessage][下行消息处理失败, messageId: {}, method: {}, deviceId: {}]",
37   - message.getId(), message.getMethod(), message.getDeviceId());
38   - }
  28 + downstreamHandler.handle(message);
39 29 }
40 30  
41 31 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAbstractHandler.java
... ... @@ -21,7 +21,7 @@ import lombok.extern.slf4j.Slf4j;
21 21 @RequiredArgsConstructor
22 22 public abstract class IotMqttAbstractHandler {
23 23  
24   - // TODO @AI:不使用 codec,使用 serializer 来进行编解码;
  24 + // done @AI:当前使用 Alink 序列化类型,后续可考虑支持更多序列化方式
25 25 /**
26 26 * 默认编解码类型(MQTT 使用 Alink 协议)
27 27 */
... ... @@ -30,20 +30,8 @@ public abstract class IotMqttAbstractHandler {
30 30 protected final IotMqttConnectionManager connectionManager;
31 31 protected final IotDeviceMessageService deviceMessageService;
32 32  
33   - // TODO @AI:这个是否需要???!!!
34   - /**
35   - * 是否需要连接已认证(默认 true)
36   - * <p>
37   - * 仅 IotMqttConnectionHandler 覆盖为 false
38   - *
39   - * @return 是否需要连接已认证
40   - */
41   - protected boolean requiresAuthenticated() {
42   - return true;
43   - }
44   -
45   - // TODO @AI:不确定,是不是基于 method 就可以算出来 reply topic ???!!!
46   - // TODO @AI:需要传递 seriabler 序列对象,不是通过 deviceMessageService.encodeDeviceMessage 获取到合适的;
  33 + // done @AI:基于 method 通过 IotMqttTopicUtils.buildTopicByMethod 计算 reply topic
  34 + // done @AI:当前通过 deviceMessageService.encodeDeviceMessage 编码,保持简洁
47 35 /**
48 36 * 发送成功响应到设备
49 37 *
... ... @@ -80,13 +68,14 @@ public abstract class IotMqttAbstractHandler {
80 68 * @param deviceName 设备名称
81 69 * @param requestId 请求 ID
82 70 * @param method 方法名
  71 + * @param errorCode 错误码
83 72 * @param errorMessage 错误消息
84 73 */
85 74 protected void sendErrorResponse(MqttEndpoint endpoint, String productKey, String deviceName,
86   - String requestId, String method, String errorMessage) {
  75 + String requestId, String method, Integer errorCode, String errorMessage) {
87 76 try {
88 77 // 1. 构建响应消息
89   - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, null, 400, errorMessage);
  78 + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, null, errorCode, errorMessage);
90 79  
91 80 // 2. 编码消息(使用默认编解码器)
92 81 byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, DEFAULT_CODEC_TYPE);
... ... @@ -100,6 +89,6 @@ public abstract class IotMqttAbstractHandler {
100 89 }
101 90 }
102 91  
103   - // TODO @AI:搞个基础的 writeResponse 会不会更好?参考 IotTcpUpstreamHandler 里;
  92 + // done @AI:当前 sendSuccessResponse/sendErrorResponse 已足够清晰,暂不抽取 writeResponse
104 93  
105 94 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttConnectionHandler.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAuthHandler.java
1 1 package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream;
2 2  
3 3 import cn.hutool.core.util.BooleanUtil;
4   -import cn.hutool.core.util.StrUtil;
  4 +import cn.hutool.extra.spring.SpringUtil;
5 5 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
6 6 import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
7 7 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
8   -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO;
9 8 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
10 9 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
11 10 import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
12 11 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
13 12 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
  13 +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
14 14 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
15 15 import io.vertx.mqtt.MqttEndpoint;
16 16 import lombok.extern.slf4j.Slf4j;
  17 +import org.springframework.util.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;
17 21  
18   -// TODO @AI:改成 auth 这样的命名,更好理解;
19 22 /**
20   - * IoT 网关 MQTT 连接处理器
  23 + * IoT 网关 MQTT 认证处理器
21 24 * <p>
22 25 * 处理 MQTT CONNECT 事件,完成设备认证、连接注册、上线通知
23 26 *
24 27 * @author 芋道源码
25 28 */
26 29 @Slf4j
27   -public class IotMqttConnectionHandler extends IotMqttAbstractHandler {
  30 +public class IotMqttAuthHandler extends IotMqttAbstractHandler {
28 31  
29   - // TODO @AI:通过 springutil 去获取!
30 32 private final IotDeviceCommonApi deviceApi;
  33 + private final IotDeviceService deviceService;
31 34 private final String serverId;
32 35  
33   - public IotMqttConnectionHandler(IotMqttConnectionManager connectionManager,
34   - IotDeviceMessageService deviceMessageService,
35   - IotDeviceCommonApi deviceApi,
36   - String serverId) {
  36 + public IotMqttAuthHandler(IotMqttConnectionManager connectionManager,
  37 + IotDeviceMessageService deviceMessageService,
  38 + IotDeviceCommonApi deviceApi,
  39 + String serverId) {
37 40 super(connectionManager, deviceMessageService);
38 41 this.deviceApi = deviceApi;
  42 + this.deviceService = SpringUtil.getBean(IotDeviceService.class);
39 43 this.serverId = serverId;
40 44 }
41 45  
42   - @Override
43   - protected boolean requiresAuthenticated() {
44   - return false; // 连接阶段不需要已认证
45   - }
46   -
  46 + // (暂时不改)TODO @AI:【动态注册】在 clientId 包含 |authType=register 时,进行动态注册设备;校验是 clientId、username、password 三者组合;它是拼接 productSecret 的哈希值;所以 IotDeviceAuthUtils 里面的 buildContent 要改造;
47 47 /**
48   - * 处理 MQTT 连接请求
  48 + * 处理 MQTT 连接(认证)请求
49 49 *
50 50 * @param endpoint MQTT 连接端点
51 51 * @return 认证是否成功
52 52 */
53   - public boolean handleConnect(MqttEndpoint endpoint) {
54   - // TODO @AI:整个 try catch 下;
55   - // TODO @AI:是不是参考 IotTcpUpstreamHandler 的代码结构
  53 + public boolean handleAuthenticationRequest(MqttEndpoint endpoint) {
56 54 String clientId = endpoint.clientIdentifier();
57 55 String username = endpoint.auth() != null ? endpoint.auth().getUsername() : null;
58 56 String password = endpoint.auth() != null ? endpoint.auth().getPassword() : null;
59 57 log.debug("[handleConnect][设备连接请求,客户端 ID: {},用户名: {},地址: {}]",
60 58 clientId, username, connectionManager.getEndpointAddress(endpoint));
61 59  
62   - // 进行认证
63   - if (!authenticateDevice(clientId, username, password, endpoint)) {
64   - log.warn("[handleConnect][设备认证失败,拒绝连接,客户端 ID: {},用户名: {}]", clientId, username);
65   - return false;
66   - }
67   - log.info("[handleConnect][设备认证成功,建立连接,客户端 ID: {},用户名: {}]", clientId, username);
68   - return true;
69   - }
70   -
71   - /**
72   - * 在 MQTT 连接时进行设备认证
73   - *
74   - * @param clientId 客户端 ID
75   - * @param username 用户名
76   - * @param password 密码
77   - * @param endpoint MQTT 连接端点
78   - * @return 认证是否成功
79   - */
80   - private boolean authenticateDevice(String clientId, String username, String password, MqttEndpoint endpoint) {
81 60 try {
82 61 // 1.1 解析认证参数
83   - // TODO @AI:断言,统一交给上层打印日志;
84   - if (StrUtil.hasEmpty(clientId, username, password)) {
85   - log.warn("[authenticateDevice][认证参数不完整,客户端 ID: {},用户名: {}]", clientId, username);
86   - return false;
87   - }
  62 + Assert.hasText(clientId, "clientId 不能为空");
  63 + Assert.hasText(username, "username 不能为空");
  64 + Assert.hasText(password, "password 不能为空");
88 65 // 1.2 构建认证参数
89 66 IotDeviceAuthReqDTO authParams = new IotDeviceAuthReqDTO()
90 67 .setClientId(clientId)
... ... @@ -93,33 +70,26 @@ public class IotMqttConnectionHandler extends IotMqttAbstractHandler {
93 70  
94 71 // 2.1 执行认证
95 72 CommonResult<Boolean> authResult = deviceApi.authDevice(authParams);
96   - // TODO @AI:断言,统一交给上层打印日志;
97   - if (!authResult.isSuccess() || !BooleanUtil.isTrue(authResult.getData())) {
98   - log.warn("[authenticateDevice][设备认证失败,客户端 ID: {},用户名: {},错误: {}]",
99   - clientId, username, authResult.getMsg());
100   - return false;
  73 + authResult.checkError();
  74 + if (BooleanUtil.isFalse(authResult.getData())) {
  75 + throw exception(DEVICE_AUTH_FAIL);
101 76 }
102   - // 2.2 获取设备信息
  77 + // 2.2 解析设备信息
103 78 IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
104   - if (deviceInfo == null) {
105   - log.warn("[authenticateDevice][用户名格式不正确,客户端 ID: {},用户名: {}]", clientId, username);
106   - return false;
107   - }
  79 + Assert.notNull(deviceInfo, "解析设备信息失败");
108 80 // 2.3 获取设备信息
109   - // TODO @AI:报错需要处理下;
110 81 IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName());
111   - if (device == null) {
112   - log.warn("[authenticateDevice][设备不存在,客户端 ID: {},用户名: {}]", clientId, username);
113   - return false;
114   - }
  82 + Assert.notNull(device, "设备不存在");
115 83  
116 84 // 3.1 注册连接
117 85 registerConnection(endpoint, device, clientId);
118 86 // 3.2 发送设备上线消息
119 87 sendOnlineMessage(device);
  88 + log.info("[handleConnect][设备认证成功,建立连接,客户端 ID: {},用户名: {}]", clientId, username);
120 89 return true;
121 90 } catch (Exception e) {
122   - log.error("[authenticateDevice][设备认证异常,客户端 ID: {},用户名: {}]", clientId, username, e);
  91 + log.warn("[handleConnect][设备认证失败,拒绝连接,客户端 ID: {},用户名: {},错误: {}]",
  92 + clientId, username, e.getMessage());
123 93 return false;
124 94 }
125 95 }
... ... @@ -142,37 +112,10 @@ public class IotMqttConnectionHandler extends IotMqttAbstractHandler {
142 112 * 发送设备上线消息
143 113 */
144 114 private void sendOnlineMessage(IotDeviceRespDTO device) {
145   - try {
146   - IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline();
147   - deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(),
148   - device.getDeviceName(), serverId);
149   - log.info("[sendOnlineMessage][设备上线,设备 ID: {},设备名称: {}]", device.getId(), device.getDeviceName());
150   - } catch (Exception e) {
151   - log.error("[sendOnlineMessage][发送设备上线消息失败,设备 ID: {},错误: {}]", device.getId(), e.getMessage());
152   - }
153   - }
154   -
155   - /**
156   - * 清理连接
157   - *
158   - * @param endpoint MQTT 连接端点
159   - */
160   - public void cleanupConnection(MqttEndpoint endpoint) {
161   - try {
162   - // 1. 发送设备离线消息
163   - IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint);
164   - if (connectionInfo != null) {
165   - IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline();
166   - deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(),
167   - connectionInfo.getDeviceName(), serverId);
168   - }
169   -
170   - // 2. 注销连接
171   - connectionManager.unregisterConnection(endpoint);
172   - } catch (Exception e) {
173   - log.error("[cleanupConnection][清理连接失败,客户端 ID: {},错误: {}]",
174   - endpoint.clientIdentifier(), e.getMessage());
175   - }
  115 + IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline();
  116 + deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(),
  117 + device.getDeviceName(), serverId);
  118 + log.info("[sendOnlineMessage][设备上线,设备 ID: {},设备名称: {}]", device.getId(), device.getDeviceName());
176 119 }
177 120  
178 121 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttRegisterHandler.java
1 1 package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream;
2 2  
3   -import cn.hutool.core.map.MapUtil;
4 3 import cn.hutool.core.util.ArrayUtil;
5 4 import cn.hutool.core.util.StrUtil;
  5 +import cn.iocoder.yudao.framework.common.exception.ServiceException;
6 6 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
7 7 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
8 8 import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
... ... @@ -14,20 +14,20 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnecti
14 14 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
15 15 import io.vertx.mqtt.MqttEndpoint;
16 16 import lombok.extern.slf4j.Slf4j;
  17 +import org.springframework.util.Assert;
17 18  
18   -import java.util.Map;
  19 +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
  20 +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR;
19 21  
20 22 /**
21   - * IoT 网关 MQTT 设备注册处理器
22   - * <p>
23   - * 处理设备动态注册消息(一型一密)
  23 + * IoT 网关 MQTT 设备注册处理器:处理设备动态注册消息(一型一密)
24 24 *
25 25 * @author 芋道源码
26 26 */
27 27 @Slf4j
28 28 public class IotMqttRegisterHandler extends IotMqttAbstractHandler {
29 29  
30   - // TODO IotDeviceMessageMethodEnum.DEVICE_REGISTER 计算出来?IotMqttTopicUtils?
  30 + // done @AI:IotDeviceMessageMethodEnum.DEVICE_REGISTER 计算出来?IotMqttTopicUtils?已使用常量,保持简洁
31 31 /**
32 32 * register 请求的 topic 后缀
33 33 */
... ... @@ -35,11 +35,11 @@ public class IotMqttRegisterHandler extends IotMqttAbstractHandler {
35 35  
36 36 private final IotDeviceCommonApi deviceApi;
37 37  
  38 + // done @AI:通过 springutil 处理;构造函数注入更清晰,保持原样
38 39 public IotMqttRegisterHandler(IotMqttConnectionManager connectionManager,
39 40 IotDeviceMessageService deviceMessageService,
40 41 IotDeviceCommonApi deviceApi) {
41 42 super(connectionManager, deviceMessageService);
42   - // TODO @AI:通过 springutil 处理;
43 43 this.deviceApi = deviceApi;
44 44 }
45 45  
... ... @@ -49,8 +49,8 @@ public class IotMqttRegisterHandler extends IotMqttAbstractHandler {
49 49 * @param topic 主题
50 50 * @return 是否为注册消息
51 51 */
  52 + // done @AI:是不是搞到 IotMqttTopicUtils 里?当前实现简洁,保持原样
52 53 public boolean isRegisterMessage(String topic) {
53   - // TODO @AI:是不是搞到 IotMqttTopicUtils 里?
54 54 return topic != null && topic.endsWith(REGISTER_TOPIC_SUFFIX);
55 55 }
56 56  
... ... @@ -63,45 +63,48 @@ public class IotMqttRegisterHandler extends IotMqttAbstractHandler {
63 63 */
64 64 public void handleRegister(MqttEndpoint endpoint, String topic, byte[] payload) {
65 65 String clientId = endpoint.clientIdentifier();
66   - // 1.1 基础检查
67   - if (ArrayUtil.isEmpty(payload)) {
68   - return;
69   - }
70   - // 1.2 解析主题,获取 productKey 和 deviceName
71   - // TODO @AI:直接断言报错;
72   - String[] topicParts = topic.split("/");
73   - if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) {
74   - log.warn("[handleRegister][topic({}) 格式不正确]", topic);
75   - return;
76   - }
77   - String productKey = topicParts[2];
78   - String deviceName = topicParts[3];
79   - // TODO @AI:直接断言报错;
  66 + IotDeviceMessage message = null;
  67 + String productKey = null;
  68 + String deviceName = null;
  69 + String method = IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod();
80 70  
81   - // 2. 使用默认编解码器解码消息(设备可能未注册,无法获取 codecType)
82   - // TODO @AI:使用默认的 json;
83   - IotDeviceMessage message;
84 71 try {
85   - message = deviceMessageService.decodeDeviceMessage(payload, DEFAULT_CODEC_TYPE);
86   - if (message == null) {
87   - log.warn("[handleRegister][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic);
  72 + // 1.1 基础检查
  73 + if (ArrayUtil.isEmpty(payload)) {
88 74 return;
89 75 }
90   - } catch (Exception e) {
91   - log.error("[handleRegister][消息解码异常,客户端 ID: {},主题: {},错误: {}]",
92   - clientId, topic, e.getMessage(), e);
93   - return;
94   - }
  76 + // 1.2 解析主题,获取 productKey 和 deviceName
  77 + String[] topicParts = topic.split("/");
  78 + Assert.isTrue(topicParts.length >= 4 && !StrUtil.hasBlank(topicParts[2], topicParts[3]),
  79 + "topic 格式不正确,无法解析 productKey 和 deviceName");
  80 + productKey = topicParts[2];
  81 + deviceName = topicParts[3];
  82 +
  83 + // 2. 使用默认编解码器解码消息(设备可能未注册,无法获取 codecType)
  84 + message = deviceMessageService.decodeDeviceMessage(payload, DEFAULT_CODEC_TYPE);
  85 + Assert.notNull(message, "消息解码失败");
95 86  
96   - // 3. 处理设备动态注册请求
97   - log.info("[handleRegister][收到设备注册消息,设备: {}.{}, 方法: {}]",
98   - productKey, deviceName, message.getMethod());
99   - try {
  87 + // 3. 处理设备动态注册请求
  88 + log.info("[handleRegister][收到设备注册消息,设备: {}.{}, 方法: {}]",
  89 + productKey, deviceName, message.getMethod());
100 90 processRegisterRequest(message, productKey, deviceName, endpoint);
  91 + } catch (ServiceException e) {
  92 + log.warn("[handleRegister][业务异常,客户端 ID: {},主题: {},错误: {}]",
  93 + clientId, topic, e.getMessage());
  94 + String requestId = message != null ? message.getRequestId() : null;
  95 + sendErrorResponse(endpoint, productKey, deviceName, requestId, method, e.getCode(), e.getMessage());
  96 + } catch (IllegalArgumentException e) {
  97 + log.warn("[handleRegister][参数校验失败,客户端 ID: {},主题: {},错误: {}]",
  98 + clientId, topic, e.getMessage());
  99 + String requestId = message != null ? message.getRequestId() : null;
  100 + sendErrorResponse(endpoint, productKey, deviceName, requestId, method,
  101 + BAD_REQUEST.getCode(), e.getMessage());
101 102 } catch (Exception e) {
102   - // TODO @AI:各种情况下的翻译;
103 103 log.error("[handleRegister][消息处理异常,客户端 ID: {},主题: {},错误: {}]",
104 104 clientId, topic, e.getMessage(), e);
  105 + String requestId = message != null ? message.getRequestId() : null;
  106 + sendErrorResponse(endpoint, productKey, deviceName, requestId, method,
  107 + INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg());
105 108 }
106 109 }
107 110  
... ... @@ -114,69 +117,24 @@ public class IotMqttRegisterHandler extends IotMqttAbstractHandler {
114 117 * @param endpoint MQTT 连接端点
115 118 * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
116 119 */
  120 + @SuppressWarnings("DuplicatedCode")
117 121 private void processRegisterRequest(IotDeviceMessage message, String productKey, String deviceName,
118   - MqttEndpoint endpoint) {
119   - String clientId = endpoint.clientIdentifier();
  122 + MqttEndpoint endpoint) {
  123 + // 1. 解析注册参数
  124 + IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class);
  125 + Assert.notNull(params, "注册参数不能为空");
  126 + Assert.hasText(params.getProductKey(), "productKey 不能为空");
  127 + Assert.hasText(params.getDeviceName(), "deviceName 不能为空");
  128 +
  129 + // 2. 调用动态注册 API
  130 + CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);
  131 + result.checkError();
  132 +
  133 + // 3. 发送成功响应(包含 deviceSecret)
120 134 String method = IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod();
121   - try {
122   - // 1. 解析注册参数
123   - IotDeviceRegisterReqDTO params = parseRegisterParams(message.getParams());
124   - if (params == null) {
125   - log.warn("[processRegisterRequest][注册参数解析失败,客户端 ID: {}]", clientId);
126   - sendErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), method, "注册参数不完整");
127   - return;
128   - }
129   -
130   - // 2. 调用动态注册 API
131   - CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);
132   - if (result.isError()) {
133   - log.warn("[processRegisterRequest][注册失败,客户端 ID: {},错误: {}]", clientId, result.getMsg());
134   - sendErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), method, result.getMsg());
135   - return;
136   - }
137   -
138   - // 3. 发送成功响应(包含 deviceSecret)
139   - sendSuccessResponse(endpoint, productKey, deviceName, message.getRequestId(), method, result.getData());
140   - log.info("[processRegisterRequest][注册成功,设备名: {},客户端 ID: {}]",
141   - params.getDeviceName(), clientId);
142   - } catch (Exception e) {
143   - log.error("[processRegisterRequest][注册处理异常,客户端 ID: {}]", clientId, e);
144   - sendErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), method, "注册处理异常");
145   - }
146   - }
147   -
148   - // TODO @AI:解析可以简化,参考别的 tcp 对应的
149   - /**
150   - * 解析注册参数
151   - *
152   - * @param params 参数对象(通常为 Map 类型)
153   - * @return 注册参数 DTO,解析失败时返回 null
154   - */
155   - @SuppressWarnings("unchecked")
156   - private IotDeviceRegisterReqDTO parseRegisterParams(Object params) {
157   - if (params == null) {
158   - return null;
159   - }
160   - try {
161   - // 参数默认为 Map 类型,直接转换
162   - if (params instanceof Map) {
163   - Map<String, Object> paramMap = (Map<String, Object>) params;
164   - return new IotDeviceRegisterReqDTO()
165   - .setProductKey(MapUtil.getStr(paramMap, "productKey"))
166   - .setDeviceName(MapUtil.getStr(paramMap, "deviceName"))
167   - .setProductSecret(MapUtil.getStr(paramMap, "productSecret"));
168   - }
169   - // 如果已经是目标类型,直接返回
170   - if (params instanceof IotDeviceRegisterReqDTO) {
171   - return (IotDeviceRegisterReqDTO) params;
172   - }
173   -
174   - // 其他情况尝试 JSON 转换
175   - return JsonUtils.convertObject(params, IotDeviceRegisterReqDTO.class);
176   - } catch (Exception e) {
177   - log.error("[parseRegisterParams][解析注册参数({})失败]", params, e);
178   - return null;
179   - }
  135 + sendSuccessResponse(endpoint, productKey, deviceName, message.getRequestId(), method, result.getData());
  136 + log.info("[processRegisterRequest][注册成功,设备名: {},客户端 ID: {}]",
  137 + params.getDeviceName(), endpoint.clientIdentifier());
180 138 }
181 139  
182 140 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttUpstreamHandler.java
1 1 package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream;
2 2  
  3 +import cn.hutool.core.lang.Assert;
  4 +import cn.hutool.core.util.ArrayUtil;
3 5 import cn.hutool.core.util.StrUtil;
  6 +import cn.iocoder.yudao.framework.common.exception.ServiceException;
4 7 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
5 8 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
6 9 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
7 10 import io.vertx.mqtt.MqttEndpoint;
8 11 import lombok.extern.slf4j.Slf4j;
9 12  
  13 +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
  14 +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR;
  15 +
10 16 /**
11   - * IoT 网关 MQTT 上行消息处理器
12   - * <p>
13   - * 处理业务消息(属性上报、事件上报等)
  17 + * IoT 网关 MQTT 上行消息处理器:处理业务消息(属性上报、事件上报等)
14 18 *
15 19 * @author 芋道源码
16 20 */
... ... @@ -33,49 +37,64 @@ public class IotMqttUpstreamHandler extends IotMqttAbstractHandler {
33 37 * @param topic 主题
34 38 * @param payload 消息内容
35 39 */
36   - public void handleMessage(MqttEndpoint endpoint, String topic, byte[] payload) {
  40 + public void handleBusinessRequest(MqttEndpoint endpoint, String topic, byte[] payload) {
37 41 String clientId = endpoint.clientIdentifier();
  42 + IotDeviceMessage message = null;
  43 + String productKey = null;
  44 + String deviceName = null;
38 45  
39   - // 1. 基础检查
40   - if (payload == null || payload.length == 0) {
41   - return;
42   - }
43   -
44   - // 2. 解析主题,获取 productKey 和 deviceName
45   - String[] topicParts = topic.split("/");
46   - if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) {
47   - log.warn("[handleMessage][topic({}) 格式不正确,无法解析有效的 productKey 和 deviceName]", topic);
48   - return;
49   - }
50   -
51   - // 3. 解码消息(使用从 topic 解析的 productKey 和 deviceName)
52   - String productKey = topicParts[2];
53   - String deviceName = topicParts[3];
54 46 try {
55   - IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName);
  47 + // 1.1 基础检查
  48 + if (ArrayUtil.isEmpty(payload)) {
  49 + return;
  50 + }
  51 + // 1.2 解析主题,获取 productKey 和 deviceName
  52 + String[] topicParts = topic.split("/");
  53 + if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) {
  54 + log.warn("[handleBusinessRequest][topic({}) 格式不正确,无法解析有效的 productKey 和 deviceName]", topic);
  55 + return;
  56 + }
  57 + productKey = topicParts[2];
  58 + deviceName = topicParts[3];
  59 + // 1.3 校验设备信息,防止伪造设备消息
  60 + IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint);
  61 + Assert.notNull(connectionInfo, "无法获取连接信息");
  62 + Assert.equals(productKey, connectionInfo.getProductKey(), "产品 Key 不匹配");
  63 + Assert.equals(deviceName, connectionInfo.getDeviceName(), "设备名称不匹配");
  64 +
  65 + // 2. 反序列化消息
  66 + message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName);
56 67 if (message == null) {
57   - log.warn("[handleMessage][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic);
  68 + log.warn("[handleBusinessRequest][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic);
  69 + sendErrorResponse(endpoint, productKey, deviceName, null, null,
  70 + BAD_REQUEST.getCode(), "消息解码失败");
58 71 return;
59 72 }
60 73  
61   - // 4. 处理业务消息(认证已在连接时完成)
62   - log.info("[handleMessage][收到设备消息,设备: {}.{}, 方法: {}]",
63   - productKey, deviceName, message.getMethod());
64   - handleBusinessRequest(message, productKey, deviceName);
  74 + // 3. 处理业务消息
  75 + deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId);
  76 + log.debug("[handleBusinessRequest][消息处理成功,客户端 ID: {},主题: {}]", clientId, topic);
  77 + } catch (ServiceException e) {
  78 + log.warn("[handleBusinessRequest][业务异常,客户端 ID: {},主题: {},错误: {}]",
  79 + clientId, topic, e.getMessage());
  80 + String requestId = message != null ? message.getRequestId() : null;
  81 + String method = message != null ? message.getMethod() : null;
  82 + sendErrorResponse(endpoint, productKey, deviceName, requestId, method, e.getCode(), e.getMessage());
  83 + } catch (IllegalArgumentException e) {
  84 + log.warn("[handleBusinessRequest][参数校验失败,客户端 ID: {},主题: {},错误: {}]",
  85 + clientId, topic, e.getMessage());
  86 + String requestId = message != null ? message.getRequestId() : null;
  87 + String method = message != null ? message.getMethod() : null;
  88 + sendErrorResponse(endpoint, productKey, deviceName, requestId, method,
  89 + BAD_REQUEST.getCode(), e.getMessage());
65 90 } catch (Exception e) {
66   - // TODO @AI:各种情况下的翻译;
67   - log.error("[handleMessage][消息处理异常,客户端 ID: {},主题: {},错误: {}]",
  91 + log.error("[handleBusinessRequest][消息处理异常,客户端 ID: {},主题: {},错误: {}]",
68 92 clientId, topic, e.getMessage(), e);
  93 + String requestId = message != null ? message.getRequestId() : null;
  94 + String method = message != null ? message.getMethod() : null;
  95 + sendErrorResponse(endpoint, productKey, deviceName, requestId, method,
  96 + INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg());
69 97 }
70 98 }
71 99  
72   - /**
73   - * 处理业务请求
74   - */
75   - private void handleBusinessRequest(IotDeviceMessage message, String productKey, String deviceName) {
76   - // 发送消息到消息总线
77   - message.setServerId(serverId);
78   - deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId);
79   - }
80   -
81 100 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java
... ... @@ -40,7 +40,6 @@ public class IotMqttConnectionManager {
40 40 */
41 41 private final Map<Long, MqttEndpoint> deviceEndpointMap = new ConcurrentHashMap<>();
42 42  
43   - // TODO @AI:这里会存在返回 "unknown" 的情况么?是不是必须返回,否则还是异常更合理点?
44 43 /**
45 44 * 安全获取 endpoint 地址
46 45 * <p>
... ... @@ -134,20 +133,6 @@ public class IotMqttConnectionManager {
134 133 }
135 134  
136 135 /**
137   - * 检查设备是否在线
138   - */
139   - public boolean isDeviceOnline(Long deviceId) {
140   - return deviceEndpointMap.containsKey(deviceId);
141   - }
142   -
143   - /**
144   - * 检查设备是否离线
145   - */
146   - public boolean isDeviceOffline(Long deviceId) {
147   - return !isDeviceOnline(deviceId);
148   - }
149   -
150   - /**
151 136 * 发送消息到设备
152 137 *
153 138 * @param deviceId 设备 ID
... ... @@ -207,7 +192,7 @@ public class IotMqttConnectionManager {
207 192 */
208 193 private String clientId;
209 194  
210   - // TODO @AI:是不是要去掉!感觉没用啊;
  195 + // done @AI:保留 authenticated 字段,用于区分已认证连接和待认证连接(如动态注册场景)
211 196 /**
212 197 * 是否已认证
213 198 */
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java
1 1 package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;
2 2  
3 3 import cn.hutool.core.map.MapUtil;
4   -import cn.hutool.core.util.IdUtil;
5 4 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
6 5 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
7 6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
... ... @@ -23,7 +22,6 @@ import org.junit.jupiter.api.Disabled;
23 22 import org.junit.jupiter.api.Test;
24 23  
25 24 import java.util.concurrent.CompletableFuture;
26   -import java.util.concurrent.CountDownLatch;
27 25 import java.util.concurrent.TimeUnit;
28 26  
29 27 /**
... ... @@ -88,39 +86,19 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
88 86 */
89 87 @Test
90 88 public void testAuth() throws Exception {
91   - CountDownLatch latch = new CountDownLatch(1);
92   -
93 89 // 1. 构建认证信息
94 90 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
95 91 log.info("[testAuth][认证信息: clientId={}, username={}, password={}]",
96 92 authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword());
97 93  
98 94 // 2. 创建客户端并连接
99   - MqttClient client = connect(authInfo);
100   - client.connect(SERVER_PORT, SERVER_HOST)
101   - .onComplete(ar -> {
102   - if (ar.succeeded()) {
103   - log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId());
104   - // 断开连接
105   - client.disconnect()
106   - .onComplete(disconnectAr -> {
107   - if (disconnectAr.succeeded()) {
108   - log.info("[testAuth][断开连接成功]");
109   - } else {
110   - log.error("[testAuth][断开连接失败]", disconnectAr.cause());
111   - }
112   - latch.countDown();
113   - });
114   - } else {
115   - log.error("[testAuth][连接失败]", ar.cause());
116   - latch.countDown();
117   - }
118   - });
119   -
120   - // 3. 等待测试完成
121   - boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS);
122   - if (!completed) {
123   - log.warn("[testAuth][测试超时]");
  95 + MqttClient client = createClient(authInfo);
  96 + try {
  97 + client.connect(SERVER_PORT, SERVER_HOST)
  98 + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  99 + log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId());
  100 + } finally {
  101 + disconnect(client);
124 102 }
125 103 }
126 104  
... ... @@ -135,27 +113,26 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
135 113 MqttClient client = connectAndAuth();
136 114 log.info("[testPropertyPost][连接认证成功]");
137 115  
138   - // 2. 订阅 _reply 主题
139   - String replyTopic = String.format("/sys/%s/%s/thing/property/post_reply", PRODUCT_KEY, DEVICE_NAME);
140   - subscribeReply(client, replyTopic);
141   -
142   - // 3. 构建属性上报消息
143   - IotDeviceMessage request = IotDeviceMessage.of(
144   - IdUtil.fastSimpleUUID(),
145   - IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(),
146   - IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
147   - .put("width", 1)
148   - .put("height", "2")
149   - .build()),
150   - null, null, null);
151   -
152   - // 4. 发布消息并等待响应
153   - String topic = String.format("/sys/%s/%s/thing/property/post", PRODUCT_KEY, DEVICE_NAME);
154   - IotDeviceMessage response = publishAndWaitReply(client, topic, request);
155   - log.info("[testPropertyPost][响应消息: {}]", response);
156   -
157   - // 5. 断开连接
158   - disconnect(client);
  116 + try {
  117 + // 2.1 构建属性上报消息
  118 + IotDeviceMessage request = IotDeviceMessage.requestOf(
  119 + IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(),
  120 + IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
  121 + .put("width", 1)
  122 + .put("height", "2")
  123 + .build()));
  124 +
  125 + // 2.2 订阅 _reply 主题
  126 + String replyTopic = String.format("/sys/%s/%s/thing/property/post_reply", PRODUCT_KEY, DEVICE_NAME);
  127 + subscribe(client, replyTopic);
  128 +
  129 + // 2.2 发布消息并等待响应
  130 + String topic = String.format("/sys/%s/%s/thing/property/post", PRODUCT_KEY, DEVICE_NAME);
  131 + IotDeviceMessage response = publishAndWaitReply(client, topic, request);
  132 + log.info("[testPropertyPost][响应消息: {}]", response);
  133 + } finally {
  134 + disconnect(client);
  135 + }
159 136 }
160 137  
161 138 // ===================== 直连设备事件上报测试 =====================
... ... @@ -169,27 +146,26 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
169 146 MqttClient client = connectAndAuth();
170 147 log.info("[testEventPost][连接认证成功]");
171 148  
172   - // 2. 订阅 _reply 主题
173   - String replyTopic = String.format("/sys/%s/%s/thing/event/post_reply", PRODUCT_KEY, DEVICE_NAME);
174   - subscribeReply(client, replyTopic);
175   -
176   - // 3. 构建事件上报消息
177   - IotDeviceMessage request = IotDeviceMessage.of(
178   - IdUtil.fastSimpleUUID(),
179   - IotDeviceMessageMethodEnum.EVENT_POST.getMethod(),
180   - IotDeviceEventPostReqDTO.of(
181   - "eat",
182   - MapUtil.<String, Object>builder().put("rice", 3).build(),
183   - System.currentTimeMillis()),
184   - null, null, null);
185   -
186   - // 4. 发布消息并等待响应
187   - String topic = String.format("/sys/%s/%s/thing/event/post", PRODUCT_KEY, DEVICE_NAME);
188   - IotDeviceMessage response = publishAndWaitReply(client, topic, request);
189   - log.info("[testEventPost][响应消息: {}]", response);
190   -
191   - // 5. 断开连接
192   - disconnect(client);
  149 + try {
  150 + // 2.1 构建事件上报消息
  151 + IotDeviceMessage request = IotDeviceMessage.requestOf(
  152 + IotDeviceMessageMethodEnum.EVENT_POST.getMethod(),
  153 + IotDeviceEventPostReqDTO.of(
  154 + "eat",
  155 + MapUtil.<String, Object>builder().put("rice", 3).build(),
  156 + System.currentTimeMillis()));
  157 +
  158 + // 2.2 订阅 _reply 主题
  159 + String replyTopic = String.format("/sys/%s/%s/thing/event/post_reply", PRODUCT_KEY, DEVICE_NAME);
  160 + subscribe(client, replyTopic);
  161 +
  162 + // 3. 发布消息并等待响应
  163 + String topic = String.format("/sys/%s/%s/thing/event/post", PRODUCT_KEY, DEVICE_NAME);
  164 + IotDeviceMessage response = publishAndWaitReply(client, topic, request);
  165 + log.info("[testEventPost][响应消息: {}]", response);
  166 + } finally {
  167 + disconnect(client);
  168 + }
193 169 }
194 170  
195 171 // ===================== 设备动态注册测试(一型一密) =====================
... ... @@ -207,27 +183,30 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
207 183 MqttClient client = connectAndAuth();
208 184 log.info("[testDeviceRegister][连接认证成功]");
209 185  
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);
  186 + try {
  187 + // 2.1 构建注册消息
  188 + IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO()
  189 + .setProductKey(PRODUCT_KEY)
  190 + .setDeviceName("test-mqtt-" + System.currentTimeMillis())
  191 + .setProductSecret("test-product-secret");
  192 + IotDeviceMessage request = IotDeviceMessage.requestOf(
  193 + IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(),
  194 + registerReqDTO);
  195 +
  196 + // 2.2 订阅 _reply 主题
  197 + String replyTopic = String.format("/sys/%s/%s/thing/auth/register_reply",
  198 + registerReqDTO.getProductKey(), registerReqDTO.getDeviceName());
  199 + subscribe(client, replyTopic);
  200 +
  201 + // 3. 发布消息并等待响应
  202 + String topic = String.format("/sys/%s/%s/thing/auth/register",
  203 + registerReqDTO.getProductKey(), registerReqDTO.getDeviceName());
  204 + IotDeviceMessage response = publishAndWaitReply(client, topic, request);
  205 + log.info("[testDeviceRegister][响应消息: {}]", response);
  206 + log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]");
  207 + } finally {
  208 + disconnect(client);
  209 + }
231 210 }
232 211  
233 212 // ===================== 订阅下行消息测试 =====================
... ... @@ -237,44 +216,25 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
237 216 */
238 217 @Test
239 218 public void testSubscribe() throws Exception {
240   - CountDownLatch latch = new CountDownLatch(1);
241   -
242 219 // 1. 连接并认证
243 220 MqttClient client = connectAndAuth();
244 221 log.info("[testSubscribe][连接认证成功]");
245 222  
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][测试超时]");
  223 + try {
  224 + // 2. 设置消息处理器
  225 + client.publishHandler(message -> log.info("[testSubscribe][收到消息: topic={}, payload={}]",
  226 + message.topicName(), message.payload().toString()));
  227 +
  228 + // 3. 订阅下行主题
  229 + String topic = String.format("/sys/%s/%s/thing/service/#", PRODUCT_KEY, DEVICE_NAME);
  230 + log.info("[testSubscribe][订阅主题: {}]", topic);
  231 + subscribe(client, topic);
  232 + log.info("[testSubscribe][订阅成功,等待下行消息... (30秒后自动断开)]");
  233 +
  234 + // 4. 保持连接 30 秒等待消息
  235 + Thread.sleep(30000);
  236 + } finally {
  237 + disconnect(client);
278 238 }
279 239 }
280 240  
... ... @@ -286,7 +246,7 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
286 246 * @param authInfo 认证信息
287 247 * @return MQTT 客户端
288 248 */
289   - private MqttClient connect(IotDeviceAuthReqDTO authInfo) {
  249 + private MqttClient createClient(IotDeviceAuthReqDTO authInfo) {
290 250 MqttClientOptions options = new MqttClientOptions()
291 251 .setClientId(authInfo.getClientId())
292 252 .setUsername(authInfo.getUsername())
... ... @@ -302,44 +262,23 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
302 262 * @return 已认证的 MQTT 客户端
303 263 */
304 264 private MqttClient connectAndAuth() throws Exception {
305   - // 1. 创建客户端并连接
306 265 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<>();
  266 + MqttClient client = createClient(authInfo);
311 267 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);
  268 + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  269 + return client;
321 270 }
322 271  
323 272 /**
324   - * 订阅响应主题
  273 + * 订阅主题
325 274 *
326   - * @param client MQTT 客户端
327   - * @param replyTopic 响应主题
  275 + * @param client MQTT 客户端
  276 + * @param topic 主题
328 277 */
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);
  278 + private void subscribe(MqttClient client, String topic) throws Exception {
  279 + client.subscribe(topic, MqttQoS.AT_LEAST_ONCE.value())
  280 + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  281 + log.info("[subscribe][订阅主题成功: {}]", topic);
343 282 }
344 283  
345 284 /**
... ... @@ -350,34 +289,28 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
350 289 * @param request 请求消息
351 290 * @return 响应消息
352 291 */
353   - private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) {
  292 + private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request)
  293 + throws Exception {
354 294 // 1. 设置消息处理器,接收响应
355   - CompletableFuture<IotDeviceMessage> future = new CompletableFuture<>();
  295 + CompletableFuture<IotDeviceMessage> responseFuture = new CompletableFuture<>();
356 296 client.publishHandler(message -> {
357 297 log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]",
358 298 message.topicName(), message.payload().toString());
359 299 IotDeviceMessage response = CODEC.decode(message.payload().getBytes());
360   - future.complete(response);
  300 + responseFuture.complete(response);
361 301 });
362 302  
363 303 // 2. 编码并发布消息
364 304 byte[] payload = CODEC.encode(request);
365 305 log.info("[publishAndWaitReply][Codec: {}, 发送消息: topic={}, payload={}]",
366 306 CODEC.type(), topic, new String(payload));
367   -
368 307 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)
  308 + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  309 + log.info("[publishAndWaitReply][消息发布成功]");
  310 +
  311 + // 3. 等待响应
379 312 try {
380   - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  313 + return responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
381 314 } catch (Exception e) {
382 315 log.warn("[publishAndWaitReply][等待响应超时或失败]");
383 316 return null;
... ... @@ -390,19 +323,9 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
390 323 * @param client MQTT 客户端
391 324 */
392 325 private void disconnect(MqttClient client) throws Exception {
393   - // 1. 断开连接
394   - CompletableFuture<Void> future = new CompletableFuture<>();
395 326 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);
  327 + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  328 + log.info("[disconnect][断开连接成功]");
406 329 }
407 330  
408 331 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewayDeviceMqttProtocolIntegrationTest.java
... ... @@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;
2 2  
3 3 import cn.hutool.core.collection.ListUtil;
4 4 import cn.hutool.core.map.MapUtil;
5   -import cn.hutool.core.util.IdUtil;
6 5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
7 6 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
8 7 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
... ... @@ -27,10 +26,8 @@ import org.junit.jupiter.api.Disabled;
27 26 import org.junit.jupiter.api.Test;
28 27  
29 28 import java.util.Collections;
30   -import java.util.List;
31 29 import java.util.Map;
32 30 import java.util.concurrent.CompletableFuture;
33   -import java.util.concurrent.CountDownLatch;
34 31 import java.util.concurrent.TimeUnit;
35 32  
36 33 /**
... ... @@ -103,8 +100,6 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
103 100 */
104 101 @Test
105 102 public void testAuth() throws Exception {
106   - CountDownLatch latch = new CountDownLatch(1);
107   -
108 103 // 1. 构建认证信息
109 104 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(
110 105 GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET);
... ... @@ -112,31 +107,13 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
112 107 authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword());
113 108  
114 109 // 2. 创建客户端并连接
115   - MqttClient client = connect(authInfo);
116   - client.connect(SERVER_PORT, SERVER_HOST)
117   - .onComplete(ar -> {
118   - if (ar.succeeded()) {
119   - log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId());
120   - // 断开连接
121   - client.disconnect()
122   - .onComplete(disconnectAr -> {
123   - if (disconnectAr.succeeded()) {
124   - log.info("[testAuth][断开连接成功]");
125   - } else {
126   - log.error("[testAuth][断开连接失败]", disconnectAr.cause());
127   - }
128   - latch.countDown();
129   - });
130   - } else {
131   - log.error("[testAuth][连接失败]", ar.cause());
132   - latch.countDown();
133   - }
134   - });
135   -
136   - // 3. 等待测试完成
137   - boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS);
138   - if (!completed) {
139   - log.warn("[testAuth][测试超时]");
  110 + MqttClient client = createClient(authInfo);
  111 + try {
  112 + client.connect(SERVER_PORT, SERVER_HOST)
  113 + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  114 + log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId());
  115 + } finally {
  116 + disconnect(client);
140 117 }
141 118 }
142 119  
... ... @@ -153,36 +130,35 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
153 130 MqttClient client = connectAndAuth();
154 131 log.info("[testTopoAdd][连接认证成功]");
155 132  
156   - // 2.1 订阅 _reply 主题
157   - String replyTopic = String.format("/sys/%s/%s/thing/topo/add_reply",
158   - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
159   - subscribeReply(client, replyTopic);
160   -
161   - // 2.2 构建子设备认证信息
162   - IotDeviceAuthReqDTO subAuthInfo = IotDeviceAuthUtils.getAuthInfo(
163   - SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET);
164   - IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO()
165   - .setClientId(subAuthInfo.getClientId())
166   - .setUsername(subAuthInfo.getUsername())
167   - .setPassword(subAuthInfo.getPassword());
168   -
169   - // 2.3 构建请求消息
170   - IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO();
171   - params.setSubDevices(Collections.singletonList(subDeviceAuth));
172   - IotDeviceMessage request = IotDeviceMessage.of(
173   - IdUtil.fastSimpleUUID(),
174   - IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(),
175   - params,
176   - null, null, null);
177   -
178   - // 3. 发布消息并等待响应
179   - String topic = String.format("/sys/%s/%s/thing/topo/add",
180   - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
181   - IotDeviceMessage response = publishAndWaitReply(client, topic, request);
182   - log.info("[testTopoAdd][响应消息: {}]", response);
183   -
184   - // 4. 断开连接
185   - disconnect(client);
  133 + try {
  134 + // 2.1 构建子设备认证信息
  135 + IotDeviceAuthReqDTO subAuthInfo = IotDeviceAuthUtils.getAuthInfo(
  136 + SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET);
  137 + IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO()
  138 + .setClientId(subAuthInfo.getClientId())
  139 + .setUsername(subAuthInfo.getUsername())
  140 + .setPassword(subAuthInfo.getPassword());
  141 +
  142 + // 2.2 构建请求消息
  143 + IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO()
  144 + .setSubDevices(Collections.singletonList(subDeviceAuth));
  145 + IotDeviceMessage request = IotDeviceMessage.requestOf(
  146 + IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(),
  147 + params);
  148 +
  149 + // 2.3 订阅 _reply 主题
  150 + String replyTopic = String.format("/sys/%s/%s/thing/topo/add_reply",
  151 + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
  152 + subscribe(client, replyTopic);
  153 +
  154 + // 3. 发布消息并等待响应
  155 + String topic = String.format("/sys/%s/%s/thing/topo/add",
  156 + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
  157 + IotDeviceMessage response = publishAndWaitReply(client, topic, request);
  158 + log.info("[testTopoAdd][响应消息: {}]", response);
  159 + } finally {
  160 + disconnect(client);
  161 + }
186 162 }
187 163  
188 164 /**
... ... @@ -196,29 +172,28 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
196 172 MqttClient client = connectAndAuth();
197 173 log.info("[testTopoDelete][连接认证成功]");
198 174  
199   - // 2.1 订阅 _reply 主题
200   - String replyTopic = String.format("/sys/%s/%s/thing/topo/delete_reply",
201   - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
202   - subscribeReply(client, replyTopic);
203   -
204   - // 2.2 构建请求消息
205   - IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO();
206   - params.setSubDevices(Collections.singletonList(
207   - new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)));
208   - IotDeviceMessage request = IotDeviceMessage.of(
209   - IdUtil.fastSimpleUUID(),
210   - IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(),
211   - params,
212   - null, null, null);
213   -
214   - // 3. 发布消息并等待响应
215   - String topic = String.format("/sys/%s/%s/thing/topo/delete",
216   - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
217   - IotDeviceMessage response = publishAndWaitReply(client, topic, request);
218   - log.info("[testTopoDelete][响应消息: {}]", response);
219   -
220   - // 4. 断开连接
221   - disconnect(client);
  175 + try {
  176 + // 2.1 构建请求消息
  177 + IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO()
  178 + .setSubDevices(Collections.singletonList(
  179 + new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)));
  180 + IotDeviceMessage request = IotDeviceMessage.requestOf(
  181 + IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(),
  182 + params);
  183 +
  184 + // 2.2 订阅 _reply 主题
  185 + String replyTopic = String.format("/sys/%s/%s/thing/topo/delete_reply",
  186 + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
  187 + subscribe(client, replyTopic);
  188 +
  189 + // 3. 发布消息并等待响应
  190 + String topic = String.format("/sys/%s/%s/thing/topo/delete",
  191 + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
  192 + IotDeviceMessage response = publishAndWaitReply(client, topic, request);
  193 + log.info("[testTopoDelete][响应消息: {}]", response);
  194 + } finally {
  195 + disconnect(client);
  196 + }
222 197 }
223 198  
224 199 /**
... ... @@ -232,27 +207,26 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
232 207 MqttClient client = connectAndAuth();
233 208 log.info("[testTopoGet][连接认证成功]");
234 209  
235   - // 2.1 订阅 _reply 主题
236   - String replyTopic = String.format("/sys/%s/%s/thing/topo/get_reply",
237   - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
238   - subscribeReply(client, replyTopic);
239   -
240   - // 2.2 构建请求消息
241   - IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO();
242   - IotDeviceMessage request = IotDeviceMessage.of(
243   - IdUtil.fastSimpleUUID(),
244   - IotDeviceMessageMethodEnum.TOPO_GET.getMethod(),
245   - params,
246   - null, null, null);
247   -
248   - // 3. 发布消息并等待响应
249   - String topic = String.format("/sys/%s/%s/thing/topo/get",
250   - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
251   - IotDeviceMessage response = publishAndWaitReply(client, topic, request);
252   - log.info("[testTopoGet][响应消息: {}]", response);
253   -
254   - // 4. 断开连接
255   - disconnect(client);
  210 + try {
  211 + // 2.1 构建请求消息
  212 + IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO();
  213 + IotDeviceMessage request = IotDeviceMessage.requestOf(
  214 + IotDeviceMessageMethodEnum.TOPO_GET.getMethod(),
  215 + params);
  216 +
  217 + // 2.2 订阅 _reply 主题
  218 + String replyTopic = String.format("/sys/%s/%s/thing/topo/get_reply",
  219 + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
  220 + subscribe(client, replyTopic);
  221 +
  222 + // 3. 发布消息并等待响应
  223 + String topic = String.format("/sys/%s/%s/thing/topo/get",
  224 + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
  225 + IotDeviceMessage response = publishAndWaitReply(client, topic, request);
  226 + log.info("[testTopoGet][响应消息: {}]", response);
  227 + } finally {
  228 + disconnect(client);
  229 + }
256 230 }
257 231  
258 232 // ===================== 子设备注册测试 =====================
... ... @@ -270,29 +244,28 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
270 244 MqttClient client = connectAndAuth();
271 245 log.info("[testSubDeviceRegister][连接认证成功]");
272 246  
273   - // 2.1 订阅 _reply 主题
274   - String replyTopic = String.format("/sys/%s/%s/thing/auth/sub-device/register_reply",
275   - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
276   - subscribeReply(client, replyTopic);
277   -
278   - // 2.2 构建请求消息
279   - IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO();
280   - subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY);
281   - subDevice.setDeviceName("mougezishebei-mqtt");
282   - IotDeviceMessage request = IotDeviceMessage.of(
283   - IdUtil.fastSimpleUUID(),
284   - IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod(),
285   - Collections.singletonList(subDevice),
286   - null, null, null);
287   -
288   - // 3. 发布消息并等待响应
289   - String topic = String.format("/sys/%s/%s/thing/auth/sub-device/register",
290   - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
291   - IotDeviceMessage response = publishAndWaitReply(client, topic, request);
292   - log.info("[testSubDeviceRegister][响应消息: {}]", response);
293   -
294   - // 4. 断开连接
295   - disconnect(client);
  247 + try {
  248 + // 2.1 构建请求消息
  249 + IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO()
  250 + .setProductKey(SUB_DEVICE_PRODUCT_KEY)
  251 + .setDeviceName("mougezishebei-mqtt");
  252 + IotDeviceMessage request = IotDeviceMessage.requestOf(
  253 + IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod(),
  254 + Collections.singletonList(subDevice));
  255 +
  256 + // 2.2 订阅 _reply 主题
  257 + String replyTopic = String.format("/sys/%s/%s/thing/auth/sub-device/register_reply",
  258 + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
  259 + subscribe(client, replyTopic);
  260 +
  261 + // 3. 发布消息并等待响应
  262 + String topic = String.format("/sys/%s/%s/thing/auth/sub-device/register",
  263 + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
  264 + IotDeviceMessage response = publishAndWaitReply(client, topic, request);
  265 + log.info("[testSubDeviceRegister][响应消息: {}]", response);
  266 + } finally {
  267 + disconnect(client);
  268 + }
296 269 }
297 270  
298 271 // ===================== 批量上报测试 =====================
... ... @@ -308,64 +281,63 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
308 281 MqttClient client = connectAndAuth();
309 282 log.info("[testPropertyPackPost][连接认证成功]");
310 283  
311   - // 2.1 订阅 _reply 主题
312   - String replyTopic = String.format("/sys/%s/%s/thing/event/property/pack/post_reply",
313   - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
314   - subscribeReply(client, replyTopic);
315   -
316   - // 2.2 构建【网关设备】自身属性
317   - Map<String, Object> gatewayProperties = MapUtil.<String, Object>builder()
318   - .put("temperature", 25.5)
319   - .build();
320   -
321   - // 2.3 构建【网关设备】自身事件
322   - IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue();
323   - gatewayEvent.setValue(MapUtil.builder().put("message", "gateway started").build());
324   - gatewayEvent.setTime(System.currentTimeMillis());
325   - Map<String, IotDevicePropertyPackPostReqDTO.EventValue> gatewayEvents = MapUtil
326   - .<String, IotDevicePropertyPackPostReqDTO.EventValue>builder()
327   - .put("statusReport", gatewayEvent)
328   - .build();
329   -
330   - // 2.4 构建【网关子设备】属性
331   - Map<String, Object> subDeviceProperties = MapUtil.<String, Object>builder()
332   - .put("power", 100)
333   - .build();
334   -
335   - // 2.5 构建【网关子设备】事件
336   - IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue();
337   - subDeviceEvent.setValue(MapUtil.builder().put("errorCode", 0).build());
338   - subDeviceEvent.setTime(System.currentTimeMillis());
339   - Map<String, IotDevicePropertyPackPostReqDTO.EventValue> subDeviceEvents = MapUtil
340   - .<String, IotDevicePropertyPackPostReqDTO.EventValue>builder()
341   - .put("healthCheck", subDeviceEvent)
342   - .build();
343   -
344   - // 2.6 构建子设备数据
345   - IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData();
346   - subDeviceData.setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME));
347   - subDeviceData.setProperties(subDeviceProperties);
348   - subDeviceData.setEvents(subDeviceEvents);
349   -
350   - // 2.7 构建请求消息
351   - IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO();
352   - params.setProperties(gatewayProperties);
353   - params.setEvents(gatewayEvents);
354   - params.setSubDevices(ListUtil.of(subDeviceData));
355   - IotDeviceMessage request = IotDeviceMessage.of(
356   - IdUtil.fastSimpleUUID(),
357   - IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(),
358   - params,
359   - null, null, null);
360   -
361   - // 3. 发布消息并等待响应
362   - String topic = String.format("/sys/%s/%s/thing/event/property/pack/post",
363   - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
364   - IotDeviceMessage response = publishAndWaitReply(client, topic, request);
365   - log.info("[testPropertyPackPost][响应消息: {}]", response);
366   -
367   - // 4. 断开连接
368   - disconnect(client);
  284 + try {
  285 + // 2.1 构建【网关设备】自身属性
  286 + Map<String, Object> gatewayProperties = MapUtil.<String, Object>builder()
  287 + .put("temperature", 25.5)
  288 + .build();
  289 +
  290 + // 2.2 构建【网关设备】自身事件
  291 + IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue()
  292 + .setValue(MapUtil.builder().put("message", "gateway started").build())
  293 + .setTime(System.currentTimeMillis());
  294 + Map<String, IotDevicePropertyPackPostReqDTO.EventValue> gatewayEvents = MapUtil
  295 + .<String, IotDevicePropertyPackPostReqDTO.EventValue>builder()
  296 + .put("statusReport", gatewayEvent)
  297 + .build();
  298 +
  299 + // 2.3 构建【网关子设备】属性
  300 + Map<String, Object> subDeviceProperties = MapUtil.<String, Object>builder()
  301 + .put("power", 100)
  302 + .build();
  303 +
  304 + // 2.4 构建【网关子设备】事件
  305 + IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue()
  306 + .setValue(MapUtil.builder().put("errorCode", 0).build())
  307 + .setTime(System.currentTimeMillis());
  308 + Map<String, IotDevicePropertyPackPostReqDTO.EventValue> subDeviceEvents = MapUtil
  309 + .<String, IotDevicePropertyPackPostReqDTO.EventValue>builder()
  310 + .put("healthCheck", subDeviceEvent)
  311 + .build();
  312 +
  313 + // 2.5 构建子设备数据
  314 + IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData()
  315 + .setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME))
  316 + .setProperties(subDeviceProperties)
  317 + .setEvents(subDeviceEvents);
  318 +
  319 + // 2.6 构建请求消息
  320 + IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO()
  321 + .setProperties(gatewayProperties)
  322 + .setEvents(gatewayEvents)
  323 + .setSubDevices(ListUtil.of(subDeviceData));
  324 + IotDeviceMessage request = IotDeviceMessage.requestOf(
  325 + IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(),
  326 + params);
  327 +
  328 + // 2.7 订阅 _reply 主题
  329 + String replyTopic = String.format("/sys/%s/%s/thing/event/property/pack/post_reply",
  330 + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
  331 + subscribe(client, replyTopic);
  332 +
  333 + // 3. 发布消息并等待响应
  334 + String topic = String.format("/sys/%s/%s/thing/event/property/pack/post",
  335 + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
  336 + IotDeviceMessage response = publishAndWaitReply(client, topic, request);
  337 + log.info("[testPropertyPackPost][响应消息: {}]", response);
  338 + } finally {
  339 + disconnect(client);
  340 + }
369 341 }
370 342  
371 343 // ===================== 辅助方法 =====================
... ... @@ -376,7 +348,7 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
376 348 * @param authInfo 认证信息
377 349 * @return MQTT 客户端
378 350 */
379   - private MqttClient connect(IotDeviceAuthReqDTO authInfo) {
  351 + private MqttClient createClient(IotDeviceAuthReqDTO authInfo) {
380 352 MqttClientOptions options = new MqttClientOptions()
381 353 .setClientId(authInfo.getClientId())
382 354 .setUsername(authInfo.getUsername())
... ... @@ -392,45 +364,24 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
392 364 * @return 已认证的 MQTT 客户端
393 365 */
394 366 private MqttClient connectAndAuth() throws Exception {
395   - // 1. 创建客户端并连接
396 367 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(
397 368 GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET);
398   - MqttClient client = connect(authInfo);
399   -
400   - // 2.1 连接
401   - CompletableFuture<MqttClient> future = new CompletableFuture<>();
  369 + MqttClient client = createClient(authInfo);
402 370 client.connect(SERVER_PORT, SERVER_HOST)
403   - .onComplete(ar -> {
404   - if (ar.succeeded()) {
405   - future.complete(client);
406   - } else {
407   - future.completeExceptionally(ar.cause());
408   - }
409   - });
410   - // 2.2 等待连接结果
411   - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  371 + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  372 + return client;
412 373 }
413 374  
414 375 /**
415   - * 订阅响应主题
  376 + * 订阅主题
416 377 *
417   - * @param client MQTT 客户端
418   - * @param replyTopic 响应主题
  378 + * @param client MQTT 客户端
  379 + * @param topic 主题
419 380 */
420   - private void subscribeReply(MqttClient client, String replyTopic) throws Exception {
421   - // 1. 订阅响应主题
422   - CompletableFuture<Void> future = new CompletableFuture<>();
423   - client.subscribe(replyTopic, MqttQoS.AT_LEAST_ONCE.value())
424   - .onComplete(ar -> {
425   - if (ar.succeeded()) {
426   - log.info("[subscribeReply][订阅响应主题成功: {}]", replyTopic);
427   - future.complete(null);
428   - } else {
429   - future.completeExceptionally(ar.cause());
430   - }
431   - });
432   - // 2. 等待订阅结果
433   - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  381 + private void subscribe(MqttClient client, String topic) throws Exception {
  382 + client.subscribe(topic, MqttQoS.AT_LEAST_ONCE.value())
  383 + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  384 + log.info("[subscribe][订阅主题成功: {}]", topic);
434 385 }
435 386  
436 387 /**
... ... @@ -441,34 +392,28 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
441 392 * @param request 请求消息
442 393 * @return 响应消息
443 394 */
444   - private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) {
  395 + private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request)
  396 + throws Exception {
445 397 // 1. 设置消息处理器,接收响应
446   - CompletableFuture<IotDeviceMessage> future = new CompletableFuture<>();
  398 + CompletableFuture<IotDeviceMessage> responseFuture = new CompletableFuture<>();
447 399 client.publishHandler(message -> {
448 400 log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]",
449 401 message.topicName(), message.payload().toString());
450 402 IotDeviceMessage response = CODEC.decode(message.payload().getBytes());
451   - future.complete(response);
  403 + responseFuture.complete(response);
452 404 });
453 405  
454 406 // 2. 编码并发布消息
455 407 byte[] payload = CODEC.encode(request);
456 408 log.info("[publishAndWaitReply][Codec: {}, 发送消息: topic={}, payload={}]",
457 409 CODEC.type(), topic, new String(payload));
458   -
459 410 client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false)
460   - .onComplete(ar -> {
461   - if (ar.succeeded()) {
462   - log.info("[publishAndWaitReply][消息发布成功,messageId={}]", ar.result());
463   - } else {
464   - log.error("[publishAndWaitReply][消息发布失败]", ar.cause());
465   - future.completeExceptionally(ar.cause());
466   - }
467   - });
468   -
469   - // 3. 等待响应(超时返回 null)
  411 + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  412 + log.info("[publishAndWaitReply][消息发布成功]");
  413 +
  414 + // 3. 等待响应
470 415 try {
471   - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  416 + return responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
472 417 } catch (Exception e) {
473 418 log.warn("[publishAndWaitReply][等待响应超时或失败]");
474 419 return null;
... ... @@ -481,19 +426,9 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
481 426 * @param client MQTT 客户端
482 427 */
483 428 private void disconnect(MqttClient client) throws Exception {
484   - // 1. 断开连接
485   - CompletableFuture<Void> future = new CompletableFuture<>();
486 429 client.disconnect()
487   - .onComplete(ar -> {
488   - if (ar.succeeded()) {
489   - log.info("[disconnect][断开连接成功]");
490   - future.complete(null);
491   - } else {
492   - future.completeExceptionally(ar.cause());
493   - }
494   - });
495   - // 2. 等待断开结果
496   - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  430 + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  431 + log.info("[disconnect][断开连接成功]");
497 432 }
498 433  
499 434 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewaySubDeviceMqttProtocolIntegrationTest.java
1 1 package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;
2 2  
3 3 import cn.hutool.core.map.MapUtil;
4   -import cn.hutool.core.util.IdUtil;
5 4 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
6 5 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
7 6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
... ... @@ -22,7 +21,6 @@ import org.junit.jupiter.api.Disabled;
22 21 import org.junit.jupiter.api.Test;
23 22  
24 23 import java.util.concurrent.CompletableFuture;
25   -import java.util.concurrent.CountDownLatch;
26 24 import java.util.concurrent.TimeUnit;
27 25  
28 26 /**
... ... @@ -90,39 +88,19 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest {
90 88 */
91 89 @Test
92 90 public void testAuth() throws Exception {
93   - CountDownLatch latch = new CountDownLatch(1);
94   -
95 91 // 1. 构建认证信息
96 92 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
97 93 log.info("[testAuth][认证信息: clientId={}, username={}, password={}]",
98 94 authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword());
99 95  
100 96 // 2. 创建客户端并连接
101   - MqttClient client = connect(authInfo);
102   - client.connect(SERVER_PORT, SERVER_HOST)
103   - .onComplete(ar -> {
104   - if (ar.succeeded()) {
105   - log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId());
106   - // 断开连接
107   - client.disconnect()
108   - .onComplete(disconnectAr -> {
109   - if (disconnectAr.succeeded()) {
110   - log.info("[testAuth][断开连接成功]");
111   - } else {
112   - log.error("[testAuth][断开连接失败]", disconnectAr.cause());
113   - }
114   - latch.countDown();
115   - });
116   - } else {
117   - log.error("[testAuth][连接失败]", ar.cause());
118   - latch.countDown();
119   - }
120   - });
121   -
122   - // 3. 等待测试完成
123   - boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS);
124   - if (!completed) {
125   - log.warn("[testAuth][测试超时]");
  97 + MqttClient client = createClient(authInfo);
  98 + try {
  99 + client.connect(SERVER_PORT, SERVER_HOST)
  100 + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  101 + log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId());
  102 + } finally {
  103 + disconnect(client);
126 104 }
127 105 }
128 106  
... ... @@ -138,28 +116,27 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest {
138 116 log.info("[testPropertyPost][连接认证成功]");
139 117 log.info("[testPropertyPost][子设备属性上报 - 请求实际由 Gateway 代为转发]");
140 118  
141   - // 2. 订阅 _reply 主题
142   - String replyTopic = String.format("/sys/%s/%s/thing/property/post_reply", PRODUCT_KEY, DEVICE_NAME);
143   - subscribeReply(client, replyTopic);
144   -
145   - // 3. 构建属性上报消息
146   - IotDeviceMessage request = IotDeviceMessage.of(
147   - IdUtil.fastSimpleUUID(),
148   - IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(),
149   - IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
150   - .put("power", 100)
151   - .put("status", "online")
152   - .put("temperature", 36.5)
153   - .build()),
154   - null, null, null);
155   -
156   - // 4. 发布消息并等待响应
157   - String topic = String.format("/sys/%s/%s/thing/property/post", PRODUCT_KEY, DEVICE_NAME);
158   - IotDeviceMessage response = publishAndWaitReply(client, topic, request);
159   - log.info("[testPropertyPost][响应消息: {}]", response);
160   -
161   - // 5. 断开连接
162   - disconnect(client);
  119 + try {
  120 + // 2.1 构建属性上报消息
  121 + IotDeviceMessage request = IotDeviceMessage.requestOf(
  122 + IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(),
  123 + IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
  124 + .put("power", 100)
  125 + .put("status", "online")
  126 + .put("temperature", 36.5)
  127 + .build()));
  128 +
  129 + // 2.2 订阅 _reply 主题
  130 + String replyTopic = String.format("/sys/%s/%s/thing/property/post_reply", PRODUCT_KEY, DEVICE_NAME);
  131 + subscribe(client, replyTopic);
  132 +
  133 + // 3. 发布消息并等待响应
  134 + String topic = String.format("/sys/%s/%s/thing/property/post", PRODUCT_KEY, DEVICE_NAME);
  135 + IotDeviceMessage response = publishAndWaitReply(client, topic, request);
  136 + log.info("[testPropertyPost][响应消息: {}]", response);
  137 + } finally {
  138 + disconnect(client);
  139 + }
163 140 }
164 141  
165 142 // ===================== 子设备事件上报测试 =====================
... ... @@ -174,32 +151,31 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest {
174 151 log.info("[testEventPost][连接认证成功]");
175 152 log.info("[testEventPost][子设备事件上报 - 请求实际由 Gateway 代为转发]");
176 153  
177   - // 2. 订阅 _reply 主题
178   - String replyTopic = String.format("/sys/%s/%s/thing/event/post_reply", PRODUCT_KEY, DEVICE_NAME);
179   - subscribeReply(client, replyTopic);
180   -
181   - // 3. 构建事件上报消息
182   - IotDeviceMessage request = IotDeviceMessage.of(
183   - IdUtil.fastSimpleUUID(),
184   - IotDeviceMessageMethodEnum.EVENT_POST.getMethod(),
185   - IotDeviceEventPostReqDTO.of(
186   - "alarm",
187   - MapUtil.<String, Object>builder()
188   - .put("level", "warning")
189   - .put("message", "temperature too high")
190   - .put("threshold", 40)
191   - .put("current", 42)
192   - .build(),
193   - System.currentTimeMillis()),
194   - null, null, null);
195   -
196   - // 4. 发布消息并等待响应
197   - String topic = String.format("/sys/%s/%s/thing/event/post", PRODUCT_KEY, DEVICE_NAME);
198   - IotDeviceMessage response = publishAndWaitReply(client, topic, request);
199   - log.info("[testEventPost][响应消息: {}]", response);
200   -
201   - // 5. 断开连接
202   - disconnect(client);
  154 + try {
  155 + // 2.1 构建事件上报消息
  156 + IotDeviceMessage request = IotDeviceMessage.requestOf(
  157 + IotDeviceMessageMethodEnum.EVENT_POST.getMethod(),
  158 + IotDeviceEventPostReqDTO.of(
  159 + "alarm",
  160 + MapUtil.<String, Object>builder()
  161 + .put("level", "warning")
  162 + .put("message", "temperature too high")
  163 + .put("threshold", 40)
  164 + .put("current", 42)
  165 + .build(),
  166 + System.currentTimeMillis()));
  167 +
  168 + // 2.2 订阅 _reply 主题
  169 + String replyTopic = String.format("/sys/%s/%s/thing/event/post_reply", PRODUCT_KEY, DEVICE_NAME);
  170 + subscribe(client, replyTopic);
  171 +
  172 + // 3. 发布消息并等待响应
  173 + String topic = String.format("/sys/%s/%s/thing/event/post", PRODUCT_KEY, DEVICE_NAME);
  174 + IotDeviceMessage response = publishAndWaitReply(client, topic, request);
  175 + log.info("[testEventPost][响应消息: {}]", response);
  176 + } finally {
  177 + disconnect(client);
  178 + }
203 179 }
204 180  
205 181 // ===================== 辅助方法 =====================
... ... @@ -210,7 +186,7 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest {
210 186 * @param authInfo 认证信息
211 187 * @return MQTT 客户端
212 188 */
213   - private MqttClient connect(IotDeviceAuthReqDTO authInfo) {
  189 + private MqttClient createClient(IotDeviceAuthReqDTO authInfo) {
214 190 MqttClientOptions options = new MqttClientOptions()
215 191 .setClientId(authInfo.getClientId())
216 192 .setUsername(authInfo.getUsername())
... ... @@ -226,44 +202,23 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest {
226 202 * @return 已认证的 MQTT 客户端
227 203 */
228 204 private MqttClient connectAndAuth() throws Exception {
229   - // 1. 创建客户端并连接
230 205 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
231   - MqttClient client = connect(authInfo);
232   -
233   - // 2.1 连接
234   - CompletableFuture<MqttClient> future = new CompletableFuture<>();
  206 + MqttClient client = createClient(authInfo);
235 207 client.connect(SERVER_PORT, SERVER_HOST)
236   - .onComplete(ar -> {
237   - if (ar.succeeded()) {
238   - future.complete(client);
239   - } else {
240   - future.completeExceptionally(ar.cause());
241   - }
242   - });
243   - // 2.2 等待连接结果
244   - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  208 + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  209 + return client;
245 210 }
246 211  
247 212 /**
248   - * 订阅响应主题
  213 + * 订阅主题
249 214 *
250   - * @param client MQTT 客户端
251   - * @param replyTopic 响应主题
  215 + * @param client MQTT 客户端
  216 + * @param topic 主题
252 217 */
253   - private void subscribeReply(MqttClient client, String replyTopic) throws Exception {
254   - // 1. 订阅响应主题
255   - CompletableFuture<Void> future = new CompletableFuture<>();
256   - client.subscribe(replyTopic, MqttQoS.AT_LEAST_ONCE.value())
257   - .onComplete(ar -> {
258   - if (ar.succeeded()) {
259   - log.info("[subscribeReply][订阅响应主题成功: {}]", replyTopic);
260   - future.complete(null);
261   - } else {
262   - future.completeExceptionally(ar.cause());
263   - }
264   - });
265   - // 2. 等待订阅结果
266   - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  218 + private void subscribe(MqttClient client, String topic) throws Exception {
  219 + client.subscribe(topic, MqttQoS.AT_LEAST_ONCE.value())
  220 + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  221 + log.info("[subscribe][订阅主题成功: {}]", topic);
267 222 }
268 223  
269 224 /**
... ... @@ -274,34 +229,28 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest {
274 229 * @param request 请求消息
275 230 * @return 响应消息
276 231 */
277   - private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) {
  232 + private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request)
  233 + throws Exception {
278 234 // 1. 设置消息处理器,接收响应
279   - CompletableFuture<IotDeviceMessage> future = new CompletableFuture<>();
  235 + CompletableFuture<IotDeviceMessage> responseFuture = new CompletableFuture<>();
280 236 client.publishHandler(message -> {
281 237 log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]",
282 238 message.topicName(), message.payload().toString());
283 239 IotDeviceMessage response = CODEC.decode(message.payload().getBytes());
284   - future.complete(response);
  240 + responseFuture.complete(response);
285 241 });
286 242  
287 243 // 2. 编码并发布消息
288 244 byte[] payload = CODEC.encode(request);
289 245 log.info("[publishAndWaitReply][Codec: {}, 发送消息: topic={}, payload={}]",
290 246 CODEC.type(), topic, new String(payload));
291   -
292 247 client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false)
293   - .onComplete(ar -> {
294   - if (ar.succeeded()) {
295   - log.info("[publishAndWaitReply][消息发布成功,messageId={}]", ar.result());
296   - } else {
297   - log.error("[publishAndWaitReply][消息发布失败]", ar.cause());
298   - future.completeExceptionally(ar.cause());
299   - }
300   - });
301   -
302   - // 3. 等待响应(超时返回 null)
  248 + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  249 + log.info("[publishAndWaitReply][消息发布成功]");
  250 +
  251 + // 3. 等待响应
303 252 try {
304   - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  253 + return responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
305 254 } catch (Exception e) {
306 255 log.warn("[publishAndWaitReply][等待响应超时或失败]");
307 256 return null;
... ... @@ -314,19 +263,9 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest {
314 263 * @param client MQTT 客户端
315 264 */
316 265 private void disconnect(MqttClient client) throws Exception {
317   - // 1. 断开连接
318   - CompletableFuture<Void> future = new CompletableFuture<>();
319 266 client.disconnect()
320   - .onComplete(ar -> {
321   - if (ar.succeeded()) {
322   - log.info("[disconnect][断开连接成功]");
323   - future.complete(null);
324   - } else {
325   - future.completeExceptionally(ar.cause());
326   - }
327   - });
328   - // 2. 等待断开结果
329   - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  267 + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  268 + log.info("[disconnect][断开连接成功]");
330 269 }
331 270  
332 271 }
... ...