Commit 006ef40c4b325b32e1aad54f55d7176e23554ad6

Authored by 安浩浩
1 parent 4cefea68

【功能完善】IoT: 添加 MQTT 主题非法错误码,重构设备服务调用和属性设置逻辑,优化 MQTT 消息处理流程

yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java
@@ -67,4 +67,7 @@ public interface ErrorCodeConstants { @@ -67,4 +67,7 @@ public interface ErrorCodeConstants {
67 ErrorCode OTA_UPGRADE_RECORD_DUPLICATE = new ErrorCode(1_050_008_007, "升级记录重复"); 67 ErrorCode OTA_UPGRADE_RECORD_DUPLICATE = new ErrorCode(1_050_008_007, "升级记录重复");
68 ErrorCode OTA_UPGRADE_RECORD_CANNOT_RETRY = new ErrorCode(1_050_008_008, "升级记录不能重试"); 68 ErrorCode OTA_UPGRADE_RECORD_CANNOT_RETRY = new ErrorCode(1_050_008_008, "升级记录不能重试");
69 69
  70 + // ========== MQTT 通信相关 1-050-009-000 ==========
  71 + ErrorCode MQTT_TOPIC_ILLEGAL = new ErrorCode(1_050_009_000, "topic illegal");
  72 +
70 } 73 }
71 \ No newline at end of file 74 \ No newline at end of file
yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/config/IotPluginEmqxAutoConfiguration.java
1 package cn.iocoder.yudao.module.iot.plugin.emqx.config; 1 package cn.iocoder.yudao.module.iot.plugin.emqx.config;
2 2
  3 +import cn.hutool.core.util.IdUtil;
3 import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; 4 import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
4 import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamHandler; 5 import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamHandler;
5 import cn.iocoder.yudao.module.iot.plugin.emqx.downstream.IotDeviceDownstreamHandlerImpl; 6 import cn.iocoder.yudao.module.iot.plugin.emqx.downstream.IotDeviceDownstreamHandlerImpl;
6 import cn.iocoder.yudao.module.iot.plugin.emqx.upstream.IotDeviceUpstreamServer; 7 import cn.iocoder.yudao.module.iot.plugin.emqx.upstream.IotDeviceUpstreamServer;
  8 +import io.vertx.core.Vertx;
  9 +import io.vertx.mqtt.MqttClient;
  10 +import io.vertx.mqtt.MqttClientOptions;
  11 +import lombok.extern.slf4j.Slf4j;
7 import org.springframework.boot.context.properties.EnableConfigurationProperties; 12 import org.springframework.boot.context.properties.EnableConfigurationProperties;
8 import org.springframework.context.annotation.Bean; 13 import org.springframework.context.annotation.Bean;
9 import org.springframework.context.annotation.Configuration; 14 import org.springframework.context.annotation.Configuration;
@@ -13,19 +18,37 @@ import org.springframework.context.annotation.Configuration; @@ -13,19 +18,37 @@ import org.springframework.context.annotation.Configuration;
13 * 18 *
14 * @author haohao 19 * @author haohao
15 */ 20 */
  21 +@Slf4j
16 @Configuration 22 @Configuration
17 @EnableConfigurationProperties(IotPluginEmqxProperties.class) 23 @EnableConfigurationProperties(IotPluginEmqxProperties.class)
18 public class IotPluginEmqxAutoConfiguration { 24 public class IotPluginEmqxAutoConfiguration {
19 25
  26 + @Bean
  27 + public Vertx vertx() {
  28 + return Vertx.vertx();
  29 + }
  30 +
  31 + @Bean
  32 + public MqttClient mqttClient(Vertx vertx, IotPluginEmqxProperties emqxProperties) {
  33 + MqttClientOptions options = new MqttClientOptions()
  34 + .setClientId("yudao-iot-downstream-" + IdUtil.fastSimpleUUID())
  35 + .setUsername(emqxProperties.getMqttUsername())
  36 + .setPassword(emqxProperties.getMqttPassword())
  37 + .setSsl(emqxProperties.isMqttSsl());
  38 + return MqttClient.create(vertx, options);
  39 + }
  40 +
20 @Bean(initMethod = "start", destroyMethod = "stop") 41 @Bean(initMethod = "start", destroyMethod = "stop")
21 public IotDeviceUpstreamServer deviceUpstreamServer(IotDeviceUpstreamApi deviceUpstreamApi, 42 public IotDeviceUpstreamServer deviceUpstreamServer(IotDeviceUpstreamApi deviceUpstreamApi,
22 - IotPluginEmqxProperties emqxProperties) {  
23 - return new IotDeviceUpstreamServer(emqxProperties, deviceUpstreamApi); 43 + IotPluginEmqxProperties emqxProperties,
  44 + Vertx vertx,
  45 + MqttClient mqttClient) {
  46 + return new IotDeviceUpstreamServer(emqxProperties, deviceUpstreamApi, vertx, mqttClient);
24 } 47 }
25 48
26 @Bean 49 @Bean
27 - public IotDeviceDownstreamHandler deviceDownstreamHandler() {  
28 - return new IotDeviceDownstreamHandlerImpl(); 50 + public IotDeviceDownstreamHandler deviceDownstreamHandler(MqttClient mqttClient) {
  51 + return new IotDeviceDownstreamHandlerImpl(mqttClient);
29 } 52 }
30 53
31 -} 54 -}
  55 +}
32 \ No newline at end of file 56 \ No newline at end of file
yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/downstream/IotDeviceDownstreamHandlerImpl.java
1 package cn.iocoder.yudao.module.iot.plugin.emqx.downstream; 1 package cn.iocoder.yudao.module.iot.plugin.emqx.downstream;
2 2
  3 +import cn.hutool.core.util.IdUtil;
  4 +import cn.hutool.json.JSONObject;
  5 +import cn.hutool.json.JSONUtil;
3 import cn.iocoder.yudao.framework.common.pojo.CommonResult; 6 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
4 import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.*; 7 import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.*;
5 import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamHandler; 8 import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamHandler;
  9 +import io.netty.handler.codec.mqtt.MqttQoS;
  10 +import io.vertx.core.buffer.Buffer;
  11 +import io.vertx.mqtt.MqttClient;
  12 +import lombok.extern.slf4j.Slf4j;
  13 +
  14 +import java.util.Map;
  15 +
  16 +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.MQTT_TOPIC_ILLEGAL;
6 17
7 /** 18 /**
8 * EMQX 插件的 {@link IotDeviceDownstreamHandler} 实现类 19 * EMQX 插件的 {@link IotDeviceDownstreamHandler} 实现类
@@ -10,14 +21,61 @@ import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamH @@ -10,14 +21,61 @@ import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamH
10 * 21 *
11 * @author 芋道源码 22 * @author 芋道源码
12 */ 23 */
  24 +@Slf4j
13 public class IotDeviceDownstreamHandlerImpl implements IotDeviceDownstreamHandler { 25 public class IotDeviceDownstreamHandlerImpl implements IotDeviceDownstreamHandler {
14 26
  27 + private static final String SYS_TOPIC_PREFIX = "/sys/";
  28 +
  29 + // 设备服务调用 标准 JSON
  30 + // 请求Topic:/sys/${productKey}/${deviceName}/thing/service/${tsl.service.identifier}
  31 + // 响应Topic:/sys/${productKey}/${deviceName}/thing/service/${tsl.service.identifier}_reply
  32 + private static final String SERVICE_TOPIC_PREFIX = "/thing/service/";
  33 +
  34 + // 设置设备属性 标准 JSON
  35 + // 请求Topic:/sys/${productKey}/${deviceName}/thing/service/property/set
  36 + // 响应Topic:/sys/${productKey}/${deviceName}/thing/service/property/set_reply
  37 + private static final String PROPERTY_SET_TOPIC = "/thing/service/property/set";
  38 +
  39 + private final MqttClient mqttClient;
  40 +
  41 + /**
  42 + * 构造函数
  43 + *
  44 + * @param mqttClient MQTT客户端
  45 + */
  46 + public IotDeviceDownstreamHandlerImpl(MqttClient mqttClient) {
  47 + this.mqttClient = mqttClient;
  48 + }
  49 +
15 @Override 50 @Override
16 - public CommonResult<Boolean> invokeDeviceService(IotDeviceServiceInvokeReqDTO invokeReqDTO) {  
17 - // 设备服务调用  
18 - // 请求Topic:/sys/${productKey}/${deviceName}/thing/service/${tsl.service.identifier}  
19 - // 响应Topic:/sys/${productKey}/${deviceName}/thing/service/${tsl.service.identifier}_reply  
20 - return CommonResult.success(true); 51 + public CommonResult<Boolean> invokeDeviceService(IotDeviceServiceInvokeReqDTO reqDTO) {
  52 + log.info("[invokeService][开始调用设备服务][reqDTO: {}]", JSONUtil.toJsonStr(reqDTO));
  53 +
  54 + // 验证参数
  55 + if (reqDTO.getProductKey() == null || reqDTO.getDeviceName() == null || reqDTO.getIdentifier() == null) {
  56 + log.error("[invokeService][参数不完整][reqDTO: {}]", JSONUtil.toJsonStr(reqDTO));
  57 + return CommonResult.error(MQTT_TOPIC_ILLEGAL.getCode(), MQTT_TOPIC_ILLEGAL.getMsg());
  58 + }
  59 +
  60 + try {
  61 + // 构建请求主题
  62 + String topic = buildServiceTopic(reqDTO.getProductKey(), reqDTO.getDeviceName(), reqDTO.getIdentifier());
  63 +
  64 + // 生成请求ID(如果没有提供)
  65 + String requestId = reqDTO.getRequestId() != null ? reqDTO.getRequestId() : generateRequestId();
  66 +
  67 + // 构建请求消息
  68 + JSONObject request = buildServiceRequest(requestId, reqDTO.getIdentifier(), reqDTO.getParams());
  69 +
  70 + // 发送消息
  71 + publishMessage(topic, request);
  72 +
  73 + log.info("[invokeService][调用设备服务成功][requestId: {}][topic: {}]", requestId, topic);
  74 + return CommonResult.success(true);
  75 + } catch (Exception e) {
  76 + log.error("[invokeService][调用设备服务异常][reqDTO: {}]", JSONUtil.toJsonStr(reqDTO), e);
  77 + return CommonResult.error(MQTT_TOPIC_ILLEGAL.getCode(), MQTT_TOPIC_ILLEGAL.getMsg());
  78 + }
21 } 79 }
22 80
23 @Override 81 @Override
@@ -26,11 +84,34 @@ public class IotDeviceDownstreamHandlerImpl implements IotDeviceDownstreamHandle @@ -26,11 +84,34 @@ public class IotDeviceDownstreamHandlerImpl implements IotDeviceDownstreamHandle
26 } 84 }
27 85
28 @Override 86 @Override
29 - public CommonResult<Boolean> setDeviceProperty(IotDevicePropertySetReqDTO setReqDTO) {  
30 - // 设置设备属性 标准 JSON  
31 - // 请求Topic:/sys/${productKey}/${deviceName}/thing/service/property/set  
32 - // 响应Topic:/sys/${productKey}/${deviceName}/thing/service/property/set_reply  
33 - return CommonResult.success(true); 87 + public CommonResult<Boolean> setDeviceProperty(IotDevicePropertySetReqDTO reqDTO) {
  88 + log.info("[setProperty][开始设置设备属性][reqDTO: {}]", JSONUtil.toJsonStr(reqDTO));
  89 +
  90 + // 验证参数
  91 + if (reqDTO.getProductKey() == null || reqDTO.getDeviceName() == null) {
  92 + log.error("[setProperty][参数不完整][reqDTO: {}]", JSONUtil.toJsonStr(reqDTO));
  93 + return CommonResult.error(MQTT_TOPIC_ILLEGAL.getCode(), MQTT_TOPIC_ILLEGAL.getMsg());
  94 + }
  95 +
  96 + try {
  97 + // 构建请求主题
  98 + String topic = buildPropertySetTopic(reqDTO.getProductKey(), reqDTO.getDeviceName());
  99 +
  100 + // 生成请求ID(如果没有提供)
  101 + String requestId = reqDTO.getRequestId() != null ? reqDTO.getRequestId() : generateRequestId();
  102 +
  103 + // 构建请求消息
  104 + JSONObject request = buildPropertySetRequest(requestId, reqDTO.getProperties());
  105 +
  106 + // 发送消息
  107 + publishMessage(topic, request);
  108 +
  109 + log.info("[setProperty][设置设备属性成功][requestId: {}][topic: {}]", requestId, topic);
  110 + return CommonResult.success(true);
  111 + } catch (Exception e) {
  112 + log.error("[setProperty][设置设备属性异常][reqDTO: {}]", JSONUtil.toJsonStr(reqDTO), e);
  113 + return CommonResult.error(MQTT_TOPIC_ILLEGAL.getCode(), MQTT_TOPIC_ILLEGAL.getMsg());
  114 + }
34 } 115 }
35 116
36 @Override 117 @Override
@@ -43,4 +124,60 @@ public class IotDeviceDownstreamHandlerImpl implements IotDeviceDownstreamHandle @@ -43,4 +124,60 @@ public class IotDeviceDownstreamHandlerImpl implements IotDeviceDownstreamHandle
43 return CommonResult.success(true); 124 return CommonResult.success(true);
44 } 125 }
45 126
  127 + /**
  128 + * 构建服务调用主题
  129 + */
  130 + private String buildServiceTopic(String productKey, String deviceName, String serviceIdentifier) {
  131 + return SYS_TOPIC_PREFIX + productKey + "/" + deviceName + SERVICE_TOPIC_PREFIX + serviceIdentifier;
  132 + }
  133 +
  134 + /**
  135 + * 构建属性设置主题
  136 + */
  137 + private String buildPropertySetTopic(String productKey, String deviceName) {
  138 + return SYS_TOPIC_PREFIX + productKey + "/" + deviceName + PROPERTY_SET_TOPIC;
  139 + }
  140 +
  141 + /**
  142 + * 构建服务调用请求
  143 + */
  144 + private JSONObject buildServiceRequest(String requestId, String serviceIdentifier, Map<String, Object> params) {
  145 + return new JSONObject()
  146 + .set("id", requestId)
  147 + .set("version", "1.0")
  148 + .set("method", "thing.service." + serviceIdentifier)
  149 + .set("params", params != null ? params : new JSONObject());
  150 + }
  151 +
  152 + /**
  153 + * 构建属性设置请求
  154 + */
  155 + private JSONObject buildPropertySetRequest(String requestId, Map<String, Object> properties) {
  156 + return new JSONObject()
  157 + .set("id", requestId)
  158 + .set("version", "1.0")
  159 + .set("method", "thing.service.property.set")
  160 + .set("params", properties);
  161 + }
  162 +
  163 + /**
  164 + * 发布MQTT消息
  165 + */
  166 + private void publishMessage(String topic, JSONObject payload) {
  167 + mqttClient.publish(
  168 + topic,
  169 + Buffer.buffer(payload.toString()),
  170 + MqttQoS.AT_LEAST_ONCE,
  171 + false,
  172 + false);
  173 + log.info("[publishMessage][发送消息成功][topic: {}][payload: {}]", topic, payload);
  174 + }
  175 +
  176 + /**
  177 + * 生成请求ID
  178 + */
  179 + private String generateRequestId() {
  180 + return IdUtil.fastSimpleUUID();
  181 + }
  182 +
46 } 183 }
47 \ No newline at end of file 184 \ No newline at end of file
yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/IotDeviceUpstreamServer.java
1 package cn.iocoder.yudao.module.iot.plugin.emqx.upstream; 1 package cn.iocoder.yudao.module.iot.plugin.emqx.upstream;
2 2
3 -import cn.hutool.core.util.IdUtil;  
4 import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; 3 import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
5 import cn.iocoder.yudao.module.iot.plugin.emqx.config.IotPluginEmqxProperties; 4 import cn.iocoder.yudao.module.iot.plugin.emqx.config.IotPluginEmqxProperties;
6 import cn.iocoder.yudao.module.iot.plugin.emqx.upstream.router.IotDeviceAuthVertxHandler; 5 import cn.iocoder.yudao.module.iot.plugin.emqx.upstream.router.IotDeviceAuthVertxHandler;
7 import cn.iocoder.yudao.module.iot.plugin.emqx.upstream.router.IotDeviceMqttMessageHandler; 6 import cn.iocoder.yudao.module.iot.plugin.emqx.upstream.router.IotDeviceMqttMessageHandler;
8 import io.netty.handler.codec.mqtt.MqttQoS; 7 import io.netty.handler.codec.mqtt.MqttQoS;
  8 +import io.vertx.core.Future;
9 import io.vertx.core.Vertx; 9 import io.vertx.core.Vertx;
10 import io.vertx.core.http.HttpServer; 10 import io.vertx.core.http.HttpServer;
11 import io.vertx.ext.web.Router; 11 import io.vertx.ext.web.Router;
12 import io.vertx.ext.web.handler.BodyHandler; 12 import io.vertx.ext.web.handler.BodyHandler;
13 import io.vertx.mqtt.MqttClient; 13 import io.vertx.mqtt.MqttClient;
14 -import io.vertx.mqtt.MqttClientOptions;  
15 import lombok.extern.slf4j.Slf4j; 14 import lombok.extern.slf4j.Slf4j;
16 15
  16 +import java.util.concurrent.CompletableFuture;
  17 +import java.util.concurrent.TimeUnit;
  18 +
17 /** 19 /**
18 * IoT 设备下行服务端,接收来自 device 设备的请求,转发给 server 服务器 20 * IoT 设备下行服务端,接收来自 device 设备的请求,转发给 server 服务器
19 * <p> 21 * <p>
@@ -24,20 +26,26 @@ import lombok.extern.slf4j.Slf4j; @@ -24,20 +26,26 @@ import lombok.extern.slf4j.Slf4j;
24 @Slf4j 26 @Slf4j
25 public class IotDeviceUpstreamServer { 27 public class IotDeviceUpstreamServer {
26 28
27 - private static final int RECONNECT_DELAY = 5000; // 重连延迟时间(毫秒) 29 + private static final int RECONNECT_DELAY_MS = 5000; // 重连延迟时间(毫秒)
  30 + private static final int CONNECTION_TIMEOUT_MS = 10000; // 连接超时时间(毫秒)
  31 + private static final String TOPIC_SEPARATOR = ","; // 主题分隔符
  32 + private static final MqttQoS DEFAULT_QOS = MqttQoS.AT_LEAST_ONCE; // 默认QoS级别
28 33
29 private final Vertx vertx; 34 private final Vertx vertx;
30 private final HttpServer server; 35 private final HttpServer server;
31 private final MqttClient client; 36 private final MqttClient client;
32 private final IotPluginEmqxProperties emqxProperties; 37 private final IotPluginEmqxProperties emqxProperties;
33 private final IotDeviceMqttMessageHandler mqttMessageHandler; 38 private final IotDeviceMqttMessageHandler mqttMessageHandler;
  39 + private volatile boolean isRunning = false; // 服务运行状态标志
34 40
35 public IotDeviceUpstreamServer(IotPluginEmqxProperties emqxProperties, 41 public IotDeviceUpstreamServer(IotPluginEmqxProperties emqxProperties,
36 - IotDeviceUpstreamApi deviceUpstreamApi) { 42 + IotDeviceUpstreamApi deviceUpstreamApi,
  43 + Vertx vertx,
  44 + MqttClient client) {
  45 + this.vertx = vertx;
37 this.emqxProperties = emqxProperties; 46 this.emqxProperties = emqxProperties;
  47 + this.client = client;
38 48
39 - // 创建 Vertx 实例  
40 - this.vertx = Vertx.vertx();  
41 // 创建 Router 实例 49 // 创建 Router 实例
42 Router router = Router.router(vertx); 50 Router router = Router.router(vertx);
43 router.route().handler(BodyHandler.create()); // 处理 Body 51 router.route().handler(BodyHandler.create()); // 处理 Body
@@ -45,14 +53,6 @@ public class IotDeviceUpstreamServer { @@ -45,14 +53,6 @@ public class IotDeviceUpstreamServer {
45 .handler(new IotDeviceAuthVertxHandler(deviceUpstreamApi)); 53 .handler(new IotDeviceAuthVertxHandler(deviceUpstreamApi));
46 // 创建 HttpServer 实例 54 // 创建 HttpServer 实例
47 this.server = vertx.createHttpServer().requestHandler(router); 55 this.server = vertx.createHttpServer().requestHandler(router);
48 -  
49 - // 创建 MQTT 客户端  
50 - MqttClientOptions options = new MqttClientOptions()  
51 - .setClientId("yudao-iot-server-" + IdUtil.fastSimpleUUID())  
52 - .setUsername(emqxProperties.getMqttUsername())  
53 - .setPassword(emqxProperties.getMqttPassword())  
54 - .setSsl(emqxProperties.isMqttSsl());  
55 - client = MqttClient.create(vertx, options);  
56 this.mqttMessageHandler = new IotDeviceMqttMessageHandler(deviceUpstreamApi, client); 56 this.mqttMessageHandler = new IotDeviceMqttMessageHandler(deviceUpstreamApi, client);
57 } 57 }
58 58
@@ -60,25 +60,45 @@ public class IotDeviceUpstreamServer { @@ -60,25 +60,45 @@ public class IotDeviceUpstreamServer {
60 * 启动 HTTP 服务器、MQTT 客户端 60 * 启动 HTTP 服务器、MQTT 客户端
61 */ 61 */
62 public void start() { 62 public void start() {
  63 + if (isRunning) {
  64 + log.warn("服务已经在运行中,请勿重复启动");
  65 + return;
  66 + }
  67 +
  68 + log.info("[start] 开始启动服务");
  69 +
63 // 1. 启动 HTTP 服务器 70 // 1. 启动 HTTP 服务器
64 - log.info("[start][开始启动]");  
65 - server.listen(emqxProperties.getAuthPort()) 71 + CompletableFuture<Void> httpFuture = server.listen(emqxProperties.getAuthPort())
66 .toCompletionStage() 72 .toCompletionStage()
67 .toCompletableFuture() 73 .toCompletableFuture()
68 - .join();  
69 - log.info("[start][HTTP服务器启动完成,端口({})]", this.server.actualPort()); 74 + .thenAccept(v -> log.info("[start] HTTP服务器启动完成,端口: {}", server.actualPort()));
70 75
71 // 2. 连接 MQTT Broker 76 // 2. 连接 MQTT Broker
72 - connectMqtt();  
73 -  
74 - // 3. 添加 MQTT 断开重连监听器  
75 - client.closeHandler(v -> {  
76 - log.warn("[closeHandler][MQTT 连接已断开,准备重连]");  
77 - reconnectWithDelay();  
78 - }); 77 + CompletableFuture<Void> mqttFuture = connectMqtt()
  78 + .toCompletionStage()
  79 + .toCompletableFuture()
  80 + .thenAccept(v -> {
  81 + // 3. 添加 MQTT 断开重连监听器
  82 + client.closeHandler(closeEvent -> {
  83 + log.warn("[closeHandler] MQTT连接已断开,准备重连");
  84 + reconnectWithDelay();
  85 + });
  86 +
  87 + // 4. 设置 MQTT 消息处理器
  88 + setupMessageHandler();
  89 + });
79 90
80 - // 4. 设置 MQTT 消息处理器  
81 - setupMessageHandler(); 91 + // 等待所有服务启动完成
  92 + CompletableFuture.allOf(httpFuture, mqttFuture)
  93 + .orTimeout(CONNECTION_TIMEOUT_MS, TimeUnit.MILLISECONDS)
  94 + .whenComplete((result, error) -> {
  95 + if (error != null) {
  96 + log.error("[start] 服务启动失败", error);
  97 + } else {
  98 + isRunning = true;
  99 + log.info("[start] 所有服务启动完成");
  100 + }
  101 + });
82 } 102 }
83 103
84 /** 104 /**
@@ -86,79 +106,118 @@ public class IotDeviceUpstreamServer { @@ -86,79 +106,118 @@ public class IotDeviceUpstreamServer {
86 */ 106 */
87 private void setupMessageHandler() { 107 private void setupMessageHandler() {
88 client.publishHandler(mqttMessageHandler::handle); 108 client.publishHandler(mqttMessageHandler::handle);
  109 + log.debug("[setupMessageHandler] MQTT消息处理器设置完成");
89 } 110 }
90 111
91 /** 112 /**
92 * 重连 MQTT 客户端 113 * 重连 MQTT 客户端
93 */ 114 */
94 private void reconnectWithDelay() { 115 private void reconnectWithDelay() {
95 - vertx.setTimer(RECONNECT_DELAY, id -> {  
96 - log.info("[reconnectWithDelay][开始重新连接 MQTT]"); 116 + if (!isRunning) {
  117 + log.info("[reconnectWithDelay] 服务已停止,不再尝试重连");
  118 + return;
  119 + }
  120 +
  121 + vertx.setTimer(RECONNECT_DELAY_MS, id -> {
  122 + log.info("[reconnectWithDelay] 开始重新连接MQTT");
97 connectMqtt(); 123 connectMqtt();
98 }); 124 });
99 } 125 }
100 126
101 /** 127 /**
102 * 连接 MQTT Broker 并订阅主题 128 * 连接 MQTT Broker 并订阅主题
  129 + *
  130 + * @return 连接结果的Future
103 */ 131 */
104 - private void connectMqtt() {  
105 - client.connect(emqxProperties.getMqttPort(), emqxProperties.getMqttHost())  
106 - .onSuccess(connAck -> {  
107 - log.info("[connectMqtt][MQTT客户端连接成功]");  
108 - subscribeToTopics(); 132 + private Future<Void> connectMqtt() {
  133 + return client.connect(emqxProperties.getMqttPort(), emqxProperties.getMqttHost())
  134 + .compose(connAck -> {
  135 + log.info("[connectMqtt] MQTT客户端连接成功");
  136 + return subscribeToTopics();
109 }) 137 })
110 - .onFailure(err -> {  
111 - log.error("[connectMqtt][连接 MQTT Broker 失败]", err); 138 + .recover(err -> {
  139 + log.error("[connectMqtt] 连接MQTT Broker失败: {}", err.getMessage());
112 reconnectWithDelay(); 140 reconnectWithDelay();
  141 + return Future.failedFuture(err);
113 }); 142 });
114 } 143 }
115 144
116 /** 145 /**
117 * 订阅设备上行消息主题 146 * 订阅设备上行消息主题
  147 + *
  148 + * @return 订阅结果的Future
118 */ 149 */
119 - private void subscribeToTopics() {  
120 - String[] topics = emqxProperties.getMqttTopics().split(","); 150 + private Future<Void> subscribeToTopics() {
  151 + String topicsStr = emqxProperties.getMqttTopics();
  152 + if (topicsStr == null || topicsStr.trim().isEmpty()) {
  153 + log.warn("[subscribeToTopics] 未配置MQTT主题,跳过订阅");
  154 + return Future.succeededFuture();
  155 + }
  156 +
  157 + log.info("[subscribeToTopics] 开始订阅设备上行消息主题");
  158 +
  159 + String[] topics = topicsStr.split(TOPIC_SEPARATOR);
  160 + Future<Void> compositeFuture = Future.succeededFuture();
  161 +
121 for (String topic : topics) { 162 for (String topic : topics) {
122 - client.subscribe(topic, MqttQoS.AT_LEAST_ONCE.value())  
123 - .onSuccess(v -> log.info("[subscribeToTopics][成功订阅主题: {}]", topic))  
124 - .onFailure(err -> log.error("[subscribeToTopics][订阅主题失败: {}]", topic, err)); 163 + String trimmedTopic = topic.trim();
  164 + if (trimmedTopic.isEmpty()) {
  165 + continue;
  166 + }
  167 +
  168 + compositeFuture = compositeFuture.compose(v -> client.subscribe(trimmedTopic, DEFAULT_QOS.value())
  169 + .<Void>map(ack -> {
  170 + log.info("[subscribeToTopics] 成功订阅主题: {}", trimmedTopic);
  171 + return null;
  172 + })
  173 + .recover(err -> {
  174 + log.error("[subscribeToTopics] 订阅主题失败: {}, 原因: {}",
  175 + trimmedTopic, err.getMessage());
  176 + return Future.<Void>succeededFuture(); // 继续订阅其他主题
  177 + }));
125 } 178 }
126 - log.info("[subscribeToTopics][开始订阅设备上行消息主题]"); 179 +
  180 + return compositeFuture;
127 } 181 }
128 182
129 /** 183 /**
130 - * 停止所有 184 + * 停止所有服务
131 */ 185 */
132 public void stop() { 186 public void stop() {
133 - log.info("[stop][开始关闭]");  
134 - try {  
135 - // 关闭 HTTP 服务器  
136 - if (server != null) {  
137 - server.close()  
138 - .toCompletionStage()  
139 - .toCompletableFuture()  
140 - .join();  
141 - } 187 + if (!isRunning) {
  188 + log.warn("[stop] 服务未运行,无需停止");
  189 + return;
  190 + }
142 191
143 - // 关闭 MQTT 客户端  
144 - if (client != null) {  
145 - client.disconnect()  
146 - .toCompletionStage()  
147 - .toCompletableFuture()  
148 - .join();  
149 - } 192 + log.info("[stop] 开始关闭服务");
  193 + isRunning = false;
150 194
151 - // 关闭 Vertx 实例  
152 - if (vertx != null) {  
153 - vertx.close()  
154 - .toCompletionStage()  
155 - .toCompletableFuture()  
156 - .join();  
157 - }  
158 - log.info("[stop][关闭完成]"); 195 + try {
  196 + CompletableFuture<Void> serverFuture = server != null
  197 + ? server.close().toCompletionStage().toCompletableFuture()
  198 + : CompletableFuture.completedFuture(null);
  199 +
  200 + CompletableFuture<Void> clientFuture = client != null
  201 + ? client.disconnect().toCompletionStage().toCompletableFuture()
  202 + : CompletableFuture.completedFuture(null);
  203 +
  204 + CompletableFuture<Void> vertxFuture = vertx != null
  205 + ? vertx.close().toCompletionStage().toCompletableFuture()
  206 + : CompletableFuture.completedFuture(null);
  207 +
  208 + // 等待所有资源关闭
  209 + CompletableFuture.allOf(serverFuture, clientFuture, vertxFuture)
  210 + .orTimeout(CONNECTION_TIMEOUT_MS, TimeUnit.MILLISECONDS)
  211 + .whenComplete((result, error) -> {
  212 + if (error != null) {
  213 + log.error("[stop] 服务关闭过程中发生异常", error);
  214 + } else {
  215 + log.info("[stop] 所有服务关闭完成");
  216 + }
  217 + });
159 } catch (Exception e) { 218 } catch (Exception e) {
160 - log.error("[stop][关闭异常]", e);  
161 - throw new RuntimeException(e); 219 + log.error("[stop] 关闭服务异常", e);
  220 + throw new RuntimeException("关闭IoT设备上行服务失败", e);
162 } 221 }
163 } 222 }
164 } 223 }
165 \ No newline at end of file 224 \ No newline at end of file
yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/router/IotDeviceMqttMessageHandler.java
@@ -13,13 +13,15 @@ import io.vertx.mqtt.messages.MqttPublishMessage; @@ -13,13 +13,15 @@ import io.vertx.mqtt.messages.MqttPublishMessage;
13 import lombok.extern.slf4j.Slf4j; 13 import lombok.extern.slf4j.Slf4j;
14 14
15 import java.time.LocalDateTime; 15 import java.time.LocalDateTime;
  16 +import java.util.Arrays;
16 17
17 /** 18 /**
18 * IoT 设备 MQTT 消息处理器 19 * IoT 设备 MQTT 消息处理器
19 * <p> 20 * <p>
20 * 参考: 21 * 参考:
21 * <p> 22 * <p>
22 - * "<a href="https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services?spm=a2c4g.11186623.0.0.97a72915vRck44#section-g4j-5zg-12b">...</a>"> 23 + * "<a href=
  24 + * "https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services?spm=a2c4g.11186623.0.0.97a72915vRck44#section-g4j-5zg-12b">...</a>">
23 */ 25 */
24 @Slf4j 26 @Slf4j
25 public class IotDeviceMqttMessageHandler { 27 public class IotDeviceMqttMessageHandler {
@@ -35,6 +37,12 @@ public class IotDeviceMqttMessageHandler { @@ -35,6 +37,12 @@ public class IotDeviceMqttMessageHandler {
35 private static final String PROPERTY_POST_TOPIC = "/thing/event/property/post"; 37 private static final String PROPERTY_POST_TOPIC = "/thing/event/property/post";
36 private static final String EVENT_POST_TOPIC_PREFIX = "/thing/event/"; 38 private static final String EVENT_POST_TOPIC_PREFIX = "/thing/event/";
37 private static final String EVENT_POST_TOPIC_SUFFIX = "/post"; 39 private static final String EVENT_POST_TOPIC_SUFFIX = "/post";
  40 + private static final String REPLY_SUFFIX = "_reply";
  41 + private static final int SUCCESS_CODE = 200;
  42 + private static final String SUCCESS_MESSAGE = "success";
  43 + private static final String PROPERTY_METHOD = "thing.event.property.post";
  44 + private static final String EVENT_METHOD_PREFIX = "thing.event.";
  45 + private static final String EVENT_METHOD_SUFFIX = ".post";
38 46
39 private final IotDeviceUpstreamApi deviceUpstreamApi; 47 private final IotDeviceUpstreamApi deviceUpstreamApi;
40 private final MqttClient mqttClient; 48 private final MqttClient mqttClient;
@@ -44,18 +52,33 @@ public class IotDeviceMqttMessageHandler { @@ -44,18 +52,33 @@ public class IotDeviceMqttMessageHandler {
44 this.mqttClient = mqttClient; 52 this.mqttClient = mqttClient;
45 } 53 }
46 54
  55 + /**
  56 + * 处理MQTT消息
  57 + *
  58 + * @param message MQTT发布消息
  59 + */
47 public void handle(MqttPublishMessage message) { 60 public void handle(MqttPublishMessage message) {
48 String topic = message.topicName(); 61 String topic = message.topicName();
49 String payload = message.payload().toString(); 62 String payload = message.payload().toString();
50 log.info("[messageHandler][接收到消息][topic: {}][payload: {}]", topic, payload); 63 log.info("[messageHandler][接收到消息][topic: {}][payload: {}]", topic, payload);
51 64
52 try { 65 try {
  66 + if (payload == null || payload.isEmpty()) {
  67 + log.warn("[messageHandler][消息内容为空][topic: {}]", topic);
  68 + return;
  69 + }
53 handleMessage(topic, payload); 70 handleMessage(topic, payload);
54 } catch (Exception e) { 71 } catch (Exception e) {
55 log.error("[messageHandler][处理消息失败][topic: {}][payload: {}]", topic, payload, e); 72 log.error("[messageHandler][处理消息失败][topic: {}][payload: {}]", topic, payload, e);
56 } 73 }
57 } 74 }
58 75
  76 + /**
  77 + * 根据主题类型处理消息
  78 + *
  79 + * @param topic 主题
  80 + * @param payload 消息内容
  81 + */
59 private void handleMessage(String topic, String payload) { 82 private void handleMessage(String topic, String payload) {
60 // 校验前缀 83 // 校验前缀
61 if (!topic.startsWith(SYS_TOPIC_PREFIX)) { 84 if (!topic.startsWith(SYS_TOPIC_PREFIX)) {
@@ -88,34 +111,26 @@ public class IotDeviceMqttMessageHandler { @@ -88,34 +111,26 @@ public class IotDeviceMqttMessageHandler {
88 * @param payload 消息内容 111 * @param payload 消息内容
89 */ 112 */
90 private void handlePropertyPost(String topic, String payload) { 113 private void handlePropertyPost(String topic, String payload) {
91 - // 解析消息内容  
92 - JSONObject jsonObject = JSONUtil.parseObj(payload);  
93 - String[] topicParts = topic.split("/"); 114 + try {
  115 + // 解析消息内容
  116 + JSONObject jsonObject = JSONUtil.parseObj(payload);
  117 + String[] topicParts = parseTopic(topic);
  118 + if (topicParts == null) {
  119 + return;
  120 + }
94 121
95 - // 构建设备属性上报请求对象  
96 - IotDevicePropertyReportReqDTO reportReqDTO = buildPropertyReportDTO(jsonObject, topicParts); 122 + // 构建设备属性上报请求对象
  123 + IotDevicePropertyReportReqDTO reportReqDTO = buildPropertyReportDTO(jsonObject, topicParts);
97 124
98 - // 调用上游 API 处理设备上报数据  
99 - deviceUpstreamApi.reportDeviceProperty(reportReqDTO);  
100 - log.info("[handlePropertyPost][处理设备上行消息成功][topic: {}][reportReqDTO: {}]",  
101 - topic, JSONUtil.toJsonStr(reportReqDTO)); 125 + // 调用上游 API 处理设备上报数据
  126 + deviceUpstreamApi.reportDeviceProperty(reportReqDTO);
  127 + log.info("[handlePropertyPost][处理设备属性上报成功][topic: {}]", topic);
102 128
103 - // 发送响应消息  
104 - String replyTopic = topic + "_reply";  
105 - JSONObject response = new JSONObject()  
106 - .set("id", jsonObject.getStr("id"))  
107 - .set("code", 200)  
108 - .set("data", new JSONObject())  
109 - .set("message", "success")  
110 - .set("method", "thing.event.property.post");  
111 -  
112 - mqttClient.publish(replyTopic,  
113 - Buffer.buffer(response.toString()),  
114 - MqttQoS.AT_LEAST_ONCE,  
115 - false,  
116 - false);  
117 - log.info("[handlePropertyPost][发送响应消息成功][topic: {}][response: {}]",  
118 - replyTopic, response.toString()); 129 + // 发送响应消息
  130 + sendResponse(topic, jsonObject, PROPERTY_METHOD, null);
  131 + } catch (Exception e) {
  132 + log.error("[handlePropertyPost][处理设备属性上报失败][topic: {}][payload: {}]", topic, payload, e);
  133 + }
119 } 134 }
120 135
121 /** 136 /**
@@ -125,35 +140,97 @@ public class IotDeviceMqttMessageHandler { @@ -125,35 +140,97 @@ public class IotDeviceMqttMessageHandler {
125 * @param payload 消息内容 140 * @param payload 消息内容
126 */ 141 */
127 private void handleEventPost(String topic, String payload) { 142 private void handleEventPost(String topic, String payload) {
128 - // 解析消息内容  
129 - JSONObject jsonObject = JSONUtil.parseObj(payload); 143 + try {
  144 + // 解析消息内容
  145 + JSONObject jsonObject = JSONUtil.parseObj(payload);
  146 + String[] topicParts = parseTopic(topic);
  147 + if (topicParts == null) {
  148 + return;
  149 + }
  150 +
  151 + // 构建设备事件上报请求对象
  152 + IotDeviceEventReportReqDTO reportReqDTO = buildEventReportDTO(jsonObject, topicParts);
  153 +
  154 + // 调用上游 API 处理设备上报数据
  155 + deviceUpstreamApi.reportDeviceEvent(reportReqDTO);
  156 + log.info("[handleEventPost][处理设备事件上报成功][topic: {}]", topic);
  157 +
  158 + // 从 topic 中获取事件标识符
  159 + String eventIdentifier = getEventIdentifier(topicParts, topic);
  160 + if (eventIdentifier == null) {
  161 + return;
  162 + }
  163 +
  164 + // 发送响应消息
  165 + String method = EVENT_METHOD_PREFIX + eventIdentifier + EVENT_METHOD_SUFFIX;
  166 + sendResponse(topic, jsonObject, method, null);
  167 + } catch (Exception e) {
  168 + log.error("[handleEventPost][处理设备事件上报失败][topic: {}][payload: {}]", topic, payload, e);
  169 + }
  170 + }
  171 +
  172 + /**
  173 + * 解析主题,获取主题各部分
  174 + *
  175 + * @param topic 主题
  176 + * @return 主题各部分数组,如果解析失败返回null
  177 + */
  178 + private String[] parseTopic(String topic) {
130 String[] topicParts = topic.split("/"); 179 String[] topicParts = topic.split("/");
  180 + if (topicParts.length < 7) {
  181 + log.warn("[parseTopic][主题格式不正确][topic: {}]", topic);
  182 + return null;
  183 + }
  184 + return topicParts;
  185 + }
131 186
132 - // 构建设备事件上报请求对象  
133 - IotDeviceEventReportReqDTO reportReqDTO = buildEventReportDTO(jsonObject, topicParts); 187 + /**
  188 + * 从主题部分中获取事件标识符
  189 + *
  190 + * @param topicParts 主题各部分
  191 + * @param topic 原始主题,用于日志
  192 + * @return 事件标识符,如果获取失败返回null
  193 + */
  194 + private String getEventIdentifier(String[] topicParts, String topic) {
  195 + try {
  196 + return topicParts[6];
  197 + } catch (ArrayIndexOutOfBoundsException e) {
  198 + log.warn("[getEventIdentifier][无法从主题中获取事件标识符][topic: {}][topicParts: {}]",
  199 + topic, Arrays.toString(topicParts));
  200 + return null;
  201 + }
  202 + }
134 203
135 - // 调用上游 API 处理设备上报数据  
136 - deviceUpstreamApi.reportDeviceEvent(reportReqDTO);  
137 - log.info("[handleEventPost][处理设备上行消息成功][topic: {}][reportReqDTO: {}]",  
138 - topic, JSONUtil.toJsonStr(reportReqDTO)); 204 + /**
  205 + * 发送响应消息
  206 + *
  207 + * @param topic 原始主题
  208 + * @param jsonObject 原始消息JSON对象
  209 + * @param method 响应方法
  210 + * @param customData 自定义数据,可为null
  211 + */
  212 + private void sendResponse(String topic, JSONObject jsonObject, String method, JSONObject customData) {
  213 + String replyTopic = topic + REPLY_SUFFIX;
  214 + JSONObject data = customData != null ? customData : new JSONObject();
139 215
140 - // 发送响应消息  
141 - String replyTopic = topic + "_reply";  
142 - String eventIdentifier = topicParts[6]; // 从 topic 中获取事件标识符  
143 JSONObject response = new JSONObject() 216 JSONObject response = new JSONObject()
144 .set("id", jsonObject.getStr("id")) 217 .set("id", jsonObject.getStr("id"))
145 - .set("code", 200)  
146 - .set("data", new JSONObject())  
147 - .set("message", "success")  
148 - .set("method", "thing.event." + eventIdentifier + ".post");  
149 -  
150 - mqttClient.publish(replyTopic,  
151 - Buffer.buffer(response.toString()),  
152 - MqttQoS.AT_LEAST_ONCE,  
153 - false,  
154 - false);  
155 - log.info("[handleEventPost][发送响应消息成功][topic: {}][response: {}]",  
156 - replyTopic, response.toString()); 218 + .set("code", SUCCESS_CODE)
  219 + .set("data", data)
  220 + .set("message", SUCCESS_MESSAGE)
  221 + .set("method", method);
  222 +
  223 + try {
  224 + mqttClient.publish(replyTopic,
  225 + Buffer.buffer(response.toString()),
  226 + MqttQoS.AT_LEAST_ONCE,
  227 + false,
  228 + false);
  229 + log.info("[sendResponse][发送响应消息成功][topic: {}]", replyTopic);
  230 + } catch (Exception e) {
  231 + log.error("[sendResponse][发送响应消息失败][topic: {}][response: {}]",
  232 + replyTopic, response.toString(), e);
  233 + }
157 } 234 }
158 235
159 /** 236 /**
@@ -163,15 +240,15 @@ public class IotDeviceMqttMessageHandler { @@ -163,15 +240,15 @@ public class IotDeviceMqttMessageHandler {
163 * @param topicParts 主题部分 240 * @param topicParts 主题部分
164 * @return 设备属性上报请求对象 241 * @return 设备属性上报请求对象
165 */ 242 */
166 - private IotDevicePropertyReportReqDTO buildPropertyReportDTO(JSONObject jsonObject,  
167 - String[] topicParts) {  
168 - return ((IotDevicePropertyReportReqDTO) new IotDevicePropertyReportReqDTO()  
169 - .setRequestId(jsonObject.getStr("id"))  
170 - .setProcessId(IotPluginCommonUtils.getProcessId())  
171 - .setReportTime(LocalDateTime.now())  
172 - .setProductKey(topicParts[2])  
173 - .setDeviceName(topicParts[3]))  
174 - .setProperties(jsonObject.getJSONObject("params")); 243 + private IotDevicePropertyReportReqDTO buildPropertyReportDTO(JSONObject jsonObject, String[] topicParts) {
  244 + IotDevicePropertyReportReqDTO reportReqDTO = new IotDevicePropertyReportReqDTO();
  245 + reportReqDTO.setRequestId(jsonObject.getStr("id"));
  246 + reportReqDTO.setProcessId(IotPluginCommonUtils.getProcessId());
  247 + reportReqDTO.setReportTime(LocalDateTime.now());
  248 + reportReqDTO.setProductKey(topicParts[2]);
  249 + reportReqDTO.setDeviceName(topicParts[3]);
  250 + reportReqDTO.setProperties(jsonObject.getJSONObject("params"));
  251 + return reportReqDTO;
175 } 252 }
176 253
177 /** 254 /**
@@ -182,13 +259,14 @@ public class IotDeviceMqttMessageHandler { @@ -182,13 +259,14 @@ public class IotDeviceMqttMessageHandler {
182 * @return 设备事件上报请求对象 259 * @return 设备事件上报请求对象
183 */ 260 */
184 private IotDeviceEventReportReqDTO buildEventReportDTO(JSONObject jsonObject, String[] topicParts) { 261 private IotDeviceEventReportReqDTO buildEventReportDTO(JSONObject jsonObject, String[] topicParts) {
185 - return ((IotDeviceEventReportReqDTO) new IotDeviceEventReportReqDTO()  
186 - .setRequestId(jsonObject.getStr("id"))  
187 - .setProcessId(IotPluginCommonUtils.getProcessId())  
188 - .setReportTime(LocalDateTime.now())  
189 - .setProductKey(topicParts[2])  
190 - .setDeviceName(topicParts[3]))  
191 - .setIdentifier(topicParts[4])  
192 - .setParams(jsonObject.getJSONObject("params")); 262 + IotDeviceEventReportReqDTO reportReqDTO = new IotDeviceEventReportReqDTO();
  263 + reportReqDTO.setRequestId(jsonObject.getStr("id"));
  264 + reportReqDTO.setProcessId(IotPluginCommonUtils.getProcessId());
  265 + reportReqDTO.setReportTime(LocalDateTime.now());
  266 + reportReqDTO.setProductKey(topicParts[2]);
  267 + reportReqDTO.setDeviceName(topicParts[3]);
  268 + reportReqDTO.setIdentifier(topicParts[6]);
  269 + reportReqDTO.setParams(jsonObject.getJSONObject("params"));
  270 + return reportReqDTO;
193 } 271 }
194 } 272 }
195 \ No newline at end of file 273 \ No newline at end of file