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 67 ErrorCode OTA_UPGRADE_RECORD_DUPLICATE = new ErrorCode(1_050_008_007, "升级记录重复");
68 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 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 1 package cn.iocoder.yudao.module.iot.plugin.emqx.config;
2 2  
  3 +import cn.hutool.core.util.IdUtil;
3 4 import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
4 5 import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamHandler;
5 6 import cn.iocoder.yudao.module.iot.plugin.emqx.downstream.IotDeviceDownstreamHandlerImpl;
6 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 12 import org.springframework.boot.context.properties.EnableConfigurationProperties;
8 13 import org.springframework.context.annotation.Bean;
9 14 import org.springframework.context.annotation.Configuration;
... ... @@ -13,19 +18,37 @@ import org.springframework.context.annotation.Configuration;
13 18 *
14 19 * @author haohao
15 20 */
  21 +@Slf4j
16 22 @Configuration
17 23 @EnableConfigurationProperties(IotPluginEmqxProperties.class)
18 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 41 @Bean(initMethod = "start", destroyMethod = "stop")
21 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 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 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 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 6 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
4 7 import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.*;
5 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 19 * EMQX 插件的 {@link IotDeviceDownstreamHandler} 实现类
... ... @@ -10,14 +21,61 @@ import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamH
10 21 *
11 22 * @author 芋道源码
12 23 */
  24 +@Slf4j
13 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 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 81 @Override
... ... @@ -26,11 +84,34 @@ public class IotDeviceDownstreamHandlerImpl implements IotDeviceDownstreamHandle
26 84 }
27 85  
28 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 117 @Override
... ... @@ -43,4 +124,60 @@ public class IotDeviceDownstreamHandlerImpl implements IotDeviceDownstreamHandle
43 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 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 1 package cn.iocoder.yudao.module.iot.plugin.emqx.upstream;
2 2  
3   -import cn.hutool.core.util.IdUtil;
4 3 import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
5 4 import cn.iocoder.yudao.module.iot.plugin.emqx.config.IotPluginEmqxProperties;
6 5 import cn.iocoder.yudao.module.iot.plugin.emqx.upstream.router.IotDeviceAuthVertxHandler;
7 6 import cn.iocoder.yudao.module.iot.plugin.emqx.upstream.router.IotDeviceMqttMessageHandler;
8 7 import io.netty.handler.codec.mqtt.MqttQoS;
  8 +import io.vertx.core.Future;
9 9 import io.vertx.core.Vertx;
10 10 import io.vertx.core.http.HttpServer;
11 11 import io.vertx.ext.web.Router;
12 12 import io.vertx.ext.web.handler.BodyHandler;
13 13 import io.vertx.mqtt.MqttClient;
14   -import io.vertx.mqtt.MqttClientOptions;
15 14 import lombok.extern.slf4j.Slf4j;
16 15  
  16 +import java.util.concurrent.CompletableFuture;
  17 +import java.util.concurrent.TimeUnit;
  18 +
17 19 /**
18 20 * IoT 设备下行服务端,接收来自 device 设备的请求,转发给 server 服务器
19 21 * <p>
... ... @@ -24,20 +26,26 @@ import lombok.extern.slf4j.Slf4j;
24 26 @Slf4j
25 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 34 private final Vertx vertx;
30 35 private final HttpServer server;
31 36 private final MqttClient client;
32 37 private final IotPluginEmqxProperties emqxProperties;
33 38 private final IotDeviceMqttMessageHandler mqttMessageHandler;
  39 + private volatile boolean isRunning = false; // 服务运行状态标志
34 40  
35 41 public IotDeviceUpstreamServer(IotPluginEmqxProperties emqxProperties,
36   - IotDeviceUpstreamApi deviceUpstreamApi) {
  42 + IotDeviceUpstreamApi deviceUpstreamApi,
  43 + Vertx vertx,
  44 + MqttClient client) {
  45 + this.vertx = vertx;
37 46 this.emqxProperties = emqxProperties;
  47 + this.client = client;
38 48  
39   - // 创建 Vertx 实例
40   - this.vertx = Vertx.vertx();
41 49 // 创建 Router 实例
42 50 Router router = Router.router(vertx);
43 51 router.route().handler(BodyHandler.create()); // 处理 Body
... ... @@ -45,14 +53,6 @@ public class IotDeviceUpstreamServer {
45 53 .handler(new IotDeviceAuthVertxHandler(deviceUpstreamApi));
46 54 // 创建 HttpServer 实例
47 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 56 this.mqttMessageHandler = new IotDeviceMqttMessageHandler(deviceUpstreamApi, client);
57 57 }
58 58  
... ... @@ -60,25 +60,45 @@ public class IotDeviceUpstreamServer {
60 60 * 启动 HTTP 服务器、MQTT 客户端
61 61 */
62 62 public void start() {
  63 + if (isRunning) {
  64 + log.warn("服务已经在运行中,请勿重复启动");
  65 + return;
  66 + }
  67 +
  68 + log.info("[start] 开始启动服务");
  69 +
63 70 // 1. 启动 HTTP 服务器
64   - log.info("[start][开始启动]");
65   - server.listen(emqxProperties.getAuthPort())
  71 + CompletableFuture<Void> httpFuture = server.listen(emqxProperties.getAuthPort())
66 72 .toCompletionStage()
67 73 .toCompletableFuture()
68   - .join();
69   - log.info("[start][HTTP服务器启动完成,端口({})]", this.server.actualPort());
  74 + .thenAccept(v -> log.info("[start] HTTP服务器启动完成,端口: {}", server.actualPort()));
70 75  
71 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 106 */
87 107 private void setupMessageHandler() {
88 108 client.publishHandler(mqttMessageHandler::handle);
  109 + log.debug("[setupMessageHandler] MQTT消息处理器设置完成");
89 110 }
90 111  
91 112 /**
92 113 * 重连 MQTT 客户端
93 114 */
94 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 123 connectMqtt();
98 124 });
99 125 }
100 126  
101 127 /**
102 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 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 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 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 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 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 import lombok.extern.slf4j.Slf4j;
14 14  
15 15 import java.time.LocalDateTime;
  16 +import java.util.Arrays;
16 17  
17 18 /**
18 19 * IoT 设备 MQTT 消息处理器
19 20 * <p>
20 21 * 参考:
21 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 26 @Slf4j
25 27 public class IotDeviceMqttMessageHandler {
... ... @@ -35,6 +37,12 @@ public class IotDeviceMqttMessageHandler {
35 37 private static final String PROPERTY_POST_TOPIC = "/thing/event/property/post";
36 38 private static final String EVENT_POST_TOPIC_PREFIX = "/thing/event/";
37 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 47 private final IotDeviceUpstreamApi deviceUpstreamApi;
40 48 private final MqttClient mqttClient;
... ... @@ -44,18 +52,33 @@ public class IotDeviceMqttMessageHandler {
44 52 this.mqttClient = mqttClient;
45 53 }
46 54  
  55 + /**
  56 + * 处理MQTT消息
  57 + *
  58 + * @param message MQTT发布消息
  59 + */
47 60 public void handle(MqttPublishMessage message) {
48 61 String topic = message.topicName();
49 62 String payload = message.payload().toString();
50 63 log.info("[messageHandler][接收到消息][topic: {}][payload: {}]", topic, payload);
51 64  
52 65 try {
  66 + if (payload == null || payload.isEmpty()) {
  67 + log.warn("[messageHandler][消息内容为空][topic: {}]", topic);
  68 + return;
  69 + }
53 70 handleMessage(topic, payload);
54 71 } catch (Exception e) {
55 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 82 private void handleMessage(String topic, String payload) {
60 83 // 校验前缀
61 84 if (!topic.startsWith(SYS_TOPIC_PREFIX)) {
... ... @@ -88,34 +111,26 @@ public class IotDeviceMqttMessageHandler {
88 111 * @param payload 消息内容
89 112 */
90 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 140 * @param payload 消息内容
126 141 */
127 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 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 216 JSONObject response = new JSONObject()
144 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 240 * @param topicParts 主题部分
164 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 259 * @return 设备事件上报请求对象
183 260 */
184 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 273 \ No newline at end of file
... ...