Commit 2737ffa1162dbb92041119ebe0db4de8756f6780
1 parent
569eef4a
reactor:【IoT 物联网】重构 MQTT 客户端连接和订阅主题流程,更新配置属性
Showing
7 changed files
with
308 additions
and
127 deletions
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.gateway.config; |
| 2 | 2 | |
| 3 | +import cn.hutool.core.util.StrUtil; | |
| 3 | 4 | import jakarta.validation.constraints.NotEmpty; |
| 4 | 5 | import jakarta.validation.constraints.NotNull; |
| 5 | 6 | import lombok.Data; |
| ... | ... | @@ -175,7 +176,7 @@ public class IotGatewayProperties { |
| 175 | 176 | * @return MQTT 客户端 ID |
| 176 | 177 | */ |
| 177 | 178 | public String getMqttClientId() { |
| 178 | - if (cn.hutool.core.util.StrUtil.isBlank(mqttClientId)) { | |
| 179 | + if (StrUtil.isBlank(mqttClientId)) { | |
| 179 | 180 | mqttClientId = "iot-gateway-mqtt-" + System.currentTimeMillis(); |
| 180 | 181 | } |
| 181 | 182 | return mqttClientId; | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttDownstreamSubscriber.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; |
| 2 | 2 | |
| 3 | -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | |
| 4 | 3 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| 5 | 4 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; |
| 6 | 5 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| ... | ... | @@ -45,13 +44,13 @@ public class IotMqttDownstreamSubscriber implements IotMessageSubscriber<IotDevi |
| 45 | 44 | |
| 46 | 45 | @Override |
| 47 | 46 | public void onMessage(IotDeviceMessage message) { |
| 48 | - log.info("[onMessage][接收到下行消息][messageId: {}][method: {}][deviceId: {}]", | |
| 47 | + log.debug("[onMessage][接收到下行消息, messageId: {}, method: {}, deviceId: {}]", | |
| 49 | 48 | message.getId(), message.getMethod(), message.getDeviceId()); |
| 50 | 49 | try { |
| 51 | 50 | // 1. 校验 |
| 52 | 51 | String method = message.getMethod(); |
| 53 | 52 | if (method == null) { |
| 54 | - log.warn("[onMessage][消息方法为空][messageId: {}][deviceId: {}]", | |
| 53 | + log.warn("[onMessage][消息方法为空, messageId: {}, deviceId: {}]", | |
| 55 | 54 | message.getId(), message.getDeviceId()); |
| 56 | 55 | return; |
| 57 | 56 | } |
| ... | ... | @@ -59,20 +58,9 @@ public class IotMqttDownstreamSubscriber implements IotMessageSubscriber<IotDevi |
| 59 | 58 | // 2. 处理下行消息 |
| 60 | 59 | downstreamHandler.handle(message); |
| 61 | 60 | } catch (Exception e) { |
| 62 | - log.error("[onMessage][处理下行消息失败][messageId: {}][method: {}][deviceId: {}]", | |
| 61 | + log.error("[onMessage][处理下行消息失败, messageId: {}, method: {}, deviceId: {}]", | |
| 63 | 62 | message.getId(), message.getMethod(), message.getDeviceId(), e); |
| 64 | 63 | } |
| 65 | 64 | } |
| 66 | 65 | |
| 67 | - /** | |
| 68 | - * 判断是否为上行消息 | |
| 69 | - * | |
| 70 | - * @param method 消息方法 | |
| 71 | - * @return 是否为上行消息 | |
| 72 | - */ | |
| 73 | - private boolean isUpstreamMessage(String method) { | |
| 74 | - IotDeviceMessageMethodEnum methodEnum = IotDeviceMessageMethodEnum.of(method); | |
| 75 | - return methodEnum != null && methodEnum.getUpstream(); | |
| 76 | - } | |
| 77 | - | |
| 78 | 66 | } |
| 79 | 67 | \ No newline at end of file | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttUpstreamProtocol.java
| ... | ... | @@ -21,7 +21,6 @@ import lombok.Getter; |
| 21 | 21 | import lombok.extern.slf4j.Slf4j; |
| 22 | 22 | |
| 23 | 23 | import java.util.List; |
| 24 | -import java.util.concurrent.TimeUnit; | |
| 25 | 24 | |
| 26 | 25 | /** |
| 27 | 26 | * IoT 网关 MQTT 协议:接收设备上行消息 |
| ... | ... | @@ -61,10 +60,10 @@ public class IotMqttUpstreamProtocol { |
| 61 | 60 | @PostConstruct |
| 62 | 61 | public void start() { |
| 63 | 62 | if (isRunning) { |
| 64 | - log.warn("[start][MQTT 统一协议服务已经在运行中,请勿重复启动]"); | |
| 63 | + log.warn("[start][MQTT 协议服务已经在运行中,请勿重复启动]"); | |
| 65 | 64 | return; |
| 66 | 65 | } |
| 67 | - log.info("[start][开始启动 MQTT 统一协议服务]"); | |
| 66 | + log.info("[start][启动 MQTT 协议服务]"); | |
| 68 | 67 | |
| 69 | 68 | try { |
| 70 | 69 | // 1. 创建共享的 Vertx 实例 |
| ... | ... | @@ -77,9 +76,9 @@ public class IotMqttUpstreamProtocol { |
| 77 | 76 | startMqttClient(); |
| 78 | 77 | |
| 79 | 78 | isRunning = true; |
| 80 | - log.info("[start][MQTT 统一协议服务启动完成]"); | |
| 79 | + log.info("[start][MQTT 协议服务启动完成]"); | |
| 81 | 80 | } catch (Exception e) { |
| 82 | - log.error("[start][MQTT 统一协议服务启动失败]", e); | |
| 81 | + log.error("[start][MQTT 协议服务启动失败]", e); | |
| 83 | 82 | // 启动失败时清理资源 |
| 84 | 83 | stop(); |
| 85 | 84 | throw e; |
| ... | ... | @@ -89,10 +88,10 @@ public class IotMqttUpstreamProtocol { |
| 89 | 88 | @PreDestroy |
| 90 | 89 | public void stop() { |
| 91 | 90 | if (!isRunning) { |
| 92 | - log.warn("[stop][MQTT 统一协议服务已经停止,无需再次停止]"); | |
| 91 | + log.warn("[stop][MQTT 协议服务已经停止,无需再次停止]"); | |
| 93 | 92 | return; |
| 94 | 93 | } |
| 95 | - log.info("[stop][开始停止 MQTT 统一协议服务]"); | |
| 94 | + log.info("[stop][停止 MQTT 协议服务]"); | |
| 96 | 95 | |
| 97 | 96 | // 1. 停止 MQTT 客户端 |
| 98 | 97 | stopMqttClient(); |
| ... | ... | @@ -104,28 +103,28 @@ public class IotMqttUpstreamProtocol { |
| 104 | 103 | if (vertx != null) { |
| 105 | 104 | try { |
| 106 | 105 | vertx.close(); |
| 107 | - log.info("[stop][Vertx 实例已关闭]"); | |
| 106 | + log.debug("[stop][Vertx 实例已关闭]"); | |
| 108 | 107 | } catch (Exception e) { |
| 109 | 108 | log.warn("[stop][关闭 Vertx 实例失败]", e); |
| 110 | 109 | } |
| 111 | 110 | } |
| 112 | 111 | |
| 113 | 112 | isRunning = false; |
| 114 | - log.info("[stop][MQTT 统一协议服务已停止]"); | |
| 113 | + log.info("[stop][MQTT 协议服务已停止]"); | |
| 115 | 114 | } |
| 116 | 115 | |
| 117 | 116 | /** |
| 118 | 117 | * 启动 HTTP 认证服务 |
| 119 | 118 | */ |
| 120 | 119 | private void startHttpAuthServer() { |
| 121 | - log.info("[startHttpAuthServer][开始启动 HTTP 认证服务]"); | |
| 120 | + log.info("[startHttpAuthServer][启动 HTTP 认证服务]"); | |
| 122 | 121 | |
| 123 | 122 | // 创建路由 |
| 124 | 123 | Router router = Router.router(vertx); |
| 125 | 124 | router.route().handler(BodyHandler.create()); |
| 126 | 125 | |
| 127 | 126 | // 创建认证处理器 |
| 128 | - IotMqttHttpAuthHandler authHandler = new IotMqttHttpAuthHandler(); | |
| 127 | + IotMqttHttpAuthHandler authHandler = new IotMqttHttpAuthHandler(this); | |
| 129 | 128 | router.post(IotMqttTopicUtils.MQTT_AUTH_AUTHENTICATE_PATH).handler(authHandler::authenticate); |
| 130 | 129 | router.post(IotMqttTopicUtils.MQTT_AUTH_CONNECTED_PATH).handler(authHandler::connected); |
| 131 | 130 | router.post(IotMqttTopicUtils.MQTT_AUTH_DISCONNECTED_PATH).handler(authHandler::disconnected); |
| ... | ... | @@ -137,7 +136,7 @@ public class IotMqttUpstreamProtocol { |
| 137 | 136 | .requestHandler(router) |
| 138 | 137 | .listen(authPort) |
| 139 | 138 | .result(); |
| 140 | - log.info("[startHttpAuthServer][HTTP 认证服务启动成功,端口:{}]", authPort); | |
| 139 | + log.info("[startHttpAuthServer][HTTP 认证服务启动成功, 端口: {}]", authPort); | |
| 141 | 140 | } catch (Exception e) { |
| 142 | 141 | log.error("[startHttpAuthServer][HTTP 认证服务启动失败]", e); |
| 143 | 142 | throw e; |
| ... | ... | @@ -163,23 +162,24 @@ public class IotMqttUpstreamProtocol { |
| 163 | 162 | * 启动 MQTT 客户端 |
| 164 | 163 | */ |
| 165 | 164 | private void startMqttClient() { |
| 166 | - log.info("[startMqttClient][开始启动 MQTT 客户端]"); | |
| 165 | + log.info("[startMqttClient][启动 MQTT 客户端]"); | |
| 167 | 166 | |
| 168 | - // 1. 初始化消息处理器 | |
| 169 | - this.upstreamHandler = new IotMqttUpstreamHandler(this); | |
| 167 | + try { | |
| 168 | + // 1. 初始化消息处理器 | |
| 169 | + this.upstreamHandler = new IotMqttUpstreamHandler(this); | |
| 170 | 170 | |
| 171 | - // 2. 创建 MQTT 客户端 | |
| 172 | - log.info("[startMqttClient][使用 MQTT 客户端 ID: {}]", emqxProperties.getMqttClientId()); | |
| 171 | + // 2. 创建 MQTT 客户端 | |
| 172 | + log.info("[startMqttClient][使用 MQTT 客户端 ID: {}]", emqxProperties.getMqttClientId()); | |
| 173 | + createMqttClient(); | |
| 173 | 174 | |
| 174 | - MqttClientOptions options = new MqttClientOptions() | |
| 175 | - .setClientId(emqxProperties.getMqttClientId()) | |
| 176 | - .setUsername(emqxProperties.getMqttUsername()) | |
| 177 | - .setPassword(emqxProperties.getMqttPassword()) | |
| 178 | - .setSsl(emqxProperties.getMqttSsl()); | |
| 179 | - this.mqttClient = MqttClient.create(vertx, options); | |
| 175 | + // 3. 连接 MQTT Broker(异步连接,不会抛出异常) | |
| 176 | + connectMqtt(); | |
| 180 | 177 | |
| 181 | - // 3. 连接 MQTT Broker | |
| 182 | - connectMqtt(); | |
| 178 | + log.info("[startMqttClient][MQTT 客户端启动完成,正在异步连接中...]"); | |
| 179 | + } catch (Exception e) { | |
| 180 | + log.error("[startMqttClient][MQTT 客户端启动失败]", e); | |
| 181 | + throw new RuntimeException("MQTT 客户端启动失败", e); | |
| 182 | + } | |
| 183 | 183 | } |
| 184 | 184 | |
| 185 | 185 | /** |
| ... | ... | @@ -195,7 +195,7 @@ public class IotMqttUpstreamProtocol { |
| 195 | 195 | mqttClient.unsubscribe(topic); |
| 196 | 196 | log.debug("[stopMqttClient][取消订阅主题: {}]", topic); |
| 197 | 197 | } catch (Exception e) { |
| 198 | - log.warn("[stopMqttClient][取消订阅主题异常: {}]", topic, e); | |
| 198 | + log.warn("[stopMqttClient][取消订阅主题({})异常]", topic, e); | |
| 199 | 199 | } |
| 200 | 200 | } |
| 201 | 201 | } |
| ... | ... | @@ -216,43 +216,107 @@ public class IotMqttUpstreamProtocol { |
| 216 | 216 | * 连接 MQTT Broker 并订阅主题 |
| 217 | 217 | */ |
| 218 | 218 | private void connectMqtt() { |
| 219 | + connectMqtt(false); | |
| 220 | + } | |
| 221 | + | |
| 222 | + /** | |
| 223 | + * 连接 MQTT Broker 并订阅主题 | |
| 224 | + * | |
| 225 | + * @param isReconnect 是否为重连 | |
| 226 | + */ | |
| 227 | + private void connectMqtt(boolean isReconnect) { | |
| 219 | 228 | // 1. 参数校验 |
| 220 | 229 | String host = emqxProperties.getMqttHost(); |
| 221 | 230 | Integer port = emqxProperties.getMqttPort(); |
| 222 | 231 | if (StrUtil.isBlank(host)) { |
| 223 | - log.error("[connectMqtt][MQTT Host 为空,无法连接]"); | |
| 232 | + log.error("[connectMqtt][MQTT Host 为空, 无法连接]"); | |
| 224 | 233 | throw new IllegalArgumentException("MQTT Host 不能为空"); |
| 225 | 234 | } |
| 226 | 235 | if (port == null || port <= 0) { |
| 227 | - log.error("[connectMqtt][MQTT Port 无效:{}]", port); | |
| 236 | + log.error("[connectMqtt][MQTT Port({}) 无效]", port); | |
| 228 | 237 | throw new IllegalArgumentException("MQTT Port 必须为正整数"); |
| 229 | 238 | } |
| 230 | - log.info("[connectMqtt][开始连接 MQTT Broker][host: {}][port: {}]", host, port); | |
| 231 | 239 | |
| 232 | - // 2. 连接 | |
| 233 | - try { | |
| 234 | - mqttClient.connect(port, host) | |
| 235 | - .toCompletionStage() | |
| 236 | - .toCompletableFuture() | |
| 237 | - .get(emqxProperties.getConnectTimeoutSeconds(), TimeUnit.SECONDS); | |
| 238 | - log.info("[connectMqtt][MQTT 客户端连接成功][host: {}][port: {}]", host, port); | |
| 239 | - | |
| 240 | - // 3. 设置处理器 | |
| 241 | - // 3.1 设置断开重连监听器 | |
| 242 | - mqttClient.closeHandler(closeEvent -> { | |
| 243 | - log.warn("[closeHandler][MQTT 连接已断开,准备重连]"); | |
| 244 | - reconnectWithDelay(); | |
| 245 | - }); | |
| 246 | - // 3.2 设置消息处理器 | |
| 247 | - mqttClient.publishHandler(upstreamHandler::handle); | |
| 248 | - log.debug("[setupMessageHandler][MQTT 消息处理器设置完成]"); | |
| 240 | + if (isReconnect) { | |
| 241 | + log.info("[connectMqtt][开始重连 MQTT Broker, host: {}, port: {}]", host, port); | |
| 242 | + // 重连时重新创建客户端实例 | |
| 243 | + createMqttClient(); | |
| 244 | + } else { | |
| 245 | + log.info("[connectMqtt][开始连接 MQTT Broker, host: {}, port: {}]", host, port); | |
| 246 | + } | |
| 249 | 247 | |
| 250 | - // 4. 订阅主题 | |
| 251 | - subscribeToTopics(); | |
| 252 | - } catch (Exception e) { | |
| 253 | - log.error("[connectMqtt][连接 MQTT Broker 失败][host: {}][port: {}]", host, port, e); | |
| 254 | - reconnectWithDelay(); // 连接失败时,也要尝试重连 | |
| 255 | - throw new RuntimeException("MQTT 客户端启动失败", e); | |
| 248 | + // 2. 异步连接 | |
| 249 | + mqttClient.connect(port, host, connectResult -> { | |
| 250 | + if (connectResult.succeeded()) { | |
| 251 | + if (isReconnect) { | |
| 252 | + log.info("[connectMqtt][MQTT 客户端重连成功, host: {}, port: {}]", host, port); | |
| 253 | + } else { | |
| 254 | + log.info("[connectMqtt][MQTT 客户端连接成功, host: {}, port: {}]", host, port); | |
| 255 | + } | |
| 256 | + | |
| 257 | + // 3. 设置处理器 | |
| 258 | + setupMqttHandlers(); | |
| 259 | + | |
| 260 | + // 4. 订阅主题 | |
| 261 | + subscribeToTopics(); | |
| 262 | + | |
| 263 | + } else { | |
| 264 | + log.error("[connectMqtt][连接 MQTT Broker 失败, host: {}, port: {}, isReconnect: {}]", | |
| 265 | + host, port, isReconnect, connectResult.cause()); | |
| 266 | + | |
| 267 | + if (!isReconnect) { | |
| 268 | + // 首次连接失败时,也要尝试重连 | |
| 269 | + log.warn("[connectMqtt][首次连接失败,将开始重连机制]"); | |
| 270 | + reconnectWithDelay(); | |
| 271 | + } else { | |
| 272 | + // 重连失败时,继续尝试重连 | |
| 273 | + reconnectWithDelay(); | |
| 274 | + } | |
| 275 | + } | |
| 276 | + }); | |
| 277 | + } | |
| 278 | + | |
| 279 | + /** | |
| 280 | + * 创建 MQTT 客户端 | |
| 281 | + */ | |
| 282 | + private void createMqttClient() { | |
| 283 | + log.debug("[createMqttClient][创建 MQTT 客户端, clientId: {}]", emqxProperties.getMqttClientId()); | |
| 284 | + | |
| 285 | + MqttClientOptions options = new MqttClientOptions() | |
| 286 | + .setClientId(emqxProperties.getMqttClientId()) | |
| 287 | + .setUsername(emqxProperties.getMqttUsername()) | |
| 288 | + .setPassword(emqxProperties.getMqttPassword()) | |
| 289 | + .setSsl(emqxProperties.getMqttSsl()); | |
| 290 | + | |
| 291 | + this.mqttClient = MqttClient.create(vertx, options); | |
| 292 | + } | |
| 293 | + | |
| 294 | + /** | |
| 295 | + * 设置 MQTT 处理器 | |
| 296 | + */ | |
| 297 | + private void setupMqttHandlers() { | |
| 298 | + if (mqttClient == null) { | |
| 299 | + log.warn("[setupMqttHandlers][MQTT 客户端为空,跳过处理器设置]"); | |
| 300 | + return; | |
| 301 | + } | |
| 302 | + | |
| 303 | + // 设置断开重连监听器 | |
| 304 | + mqttClient.closeHandler(closeEvent -> { | |
| 305 | + log.warn("[closeHandler][MQTT 连接已断开, 准备重连]"); | |
| 306 | + reconnectWithDelay(); | |
| 307 | + }); | |
| 308 | + | |
| 309 | + // 设置异常处理器 | |
| 310 | + mqttClient.exceptionHandler(exception -> { | |
| 311 | + log.error("[exceptionHandler][MQTT 客户端异常]", exception); | |
| 312 | + }); | |
| 313 | + | |
| 314 | + // 设置消息处理器 | |
| 315 | + if (upstreamHandler != null) { | |
| 316 | + mqttClient.publishHandler(upstreamHandler::handle); | |
| 317 | + log.debug("[setupMqttHandlers][MQTT 消息处理器设置完成]"); | |
| 318 | + } else { | |
| 319 | + log.warn("[setupMqttHandlers][上行消息处理器为空,跳过设置]"); | |
| 256 | 320 | } |
| 257 | 321 | } |
| 258 | 322 | |
| ... | ... | @@ -261,15 +325,43 @@ public class IotMqttUpstreamProtocol { |
| 261 | 325 | */ |
| 262 | 326 | private void subscribeToTopics() { |
| 263 | 327 | List<String> topicList = emqxProperties.getMqttTopics(); |
| 328 | + if (CollUtil.isEmpty(topicList)) { | |
| 329 | + log.warn("[subscribeToTopics][订阅主题列表为空, 跳过订阅]"); | |
| 330 | + return; | |
| 331 | + } | |
| 332 | + | |
| 333 | + if (mqttClient == null || !mqttClient.isConnected()) { | |
| 334 | + log.warn("[subscribeToTopics][MQTT 客户端未连接, 跳过订阅]"); | |
| 335 | + return; | |
| 336 | + } | |
| 337 | + | |
| 264 | 338 | int qos = emqxProperties.getMqttQos(); |
| 339 | + log.info("[subscribeToTopics][开始订阅主题, 共 {} 个, QoS: {}]", topicList.size(), qos); | |
| 340 | + | |
| 341 | + int[] successCount = {0}; // 使用数组以便在 lambda 中修改 | |
| 342 | + int[] failCount = {0}; | |
| 265 | 343 | |
| 266 | - log.info("[subscribeToTopics][开始订阅主题,共 {} 个]", topicList.size()); | |
| 267 | 344 | for (String topic : topicList) { |
| 268 | 345 | mqttClient.subscribe(topic, qos, subscribeResult -> { |
| 269 | 346 | if (subscribeResult.succeeded()) { |
| 270 | - log.info("[subscribeToTopics][订阅主题成功: {}][QoS: {}]", topic, qos); | |
| 347 | + successCount[0]++; | |
| 348 | + log.debug("[subscribeToTopics][订阅主题成功, topic: {}, qos: {}]", topic, qos); | |
| 349 | + | |
| 350 | + // 当所有主题都处理完成时,记录汇总日志 | |
| 351 | + if (successCount[0] + failCount[0] == topicList.size()) { | |
| 352 | + log.info("[subscribeToTopics][主题订阅完成, 成功: {}, 失败: {}, 总计: {}]", | |
| 353 | + successCount[0], failCount[0], topicList.size()); | |
| 354 | + } | |
| 271 | 355 | } else { |
| 272 | - log.error("[subscribeToTopics][订阅主题失败: {}]", topic, subscribeResult.cause()); | |
| 356 | + failCount[0]++; | |
| 357 | + log.error("[subscribeToTopics][订阅主题失败, topic: {}, qos: {}, 原因: {}]", | |
| 358 | + topic, qos, subscribeResult.cause().getMessage(), subscribeResult.cause()); | |
| 359 | + | |
| 360 | + // 当所有主题都处理完成时,记录汇总日志 | |
| 361 | + if (successCount[0] + failCount[0] == topicList.size()) { | |
| 362 | + log.info("[subscribeToTopics][主题订阅完成, 成功: {}, 失败: {}, 总计: {}]", | |
| 363 | + successCount[0], failCount[0], topicList.size()); | |
| 364 | + } | |
| 273 | 365 | } |
| 274 | 366 | }); |
| 275 | 367 | } |
| ... | ... | @@ -281,15 +373,21 @@ public class IotMqttUpstreamProtocol { |
| 281 | 373 | private void reconnectWithDelay() { |
| 282 | 374 | long delay = emqxProperties.getReconnectDelayMs(); |
| 283 | 375 | vertx.setTimer(delay, timerId -> { |
| 284 | - if (!isRunning || (mqttClient != null && mqttClient.isConnected())) { | |
| 376 | + if (!isRunning) { | |
| 377 | + log.debug("[reconnectWithDelay][服务已停止, 取消重连]"); | |
| 378 | + return; | |
| 379 | + } | |
| 380 | + // 检查连接状态,如果已连接则无需重连 | |
| 381 | + if (mqttClient != null && mqttClient.isConnected()) { | |
| 382 | + log.debug("[reconnectWithDelay][MQTT 客户端已连接, 无需重连]"); | |
| 285 | 383 | return; |
| 286 | 384 | } |
| 287 | - log.info("[reconnectWithDelay][开始重连 MQTT Broker,延迟 {} 毫秒]", delay); | |
| 385 | + log.info("[reconnectWithDelay][开始重连 MQTT Broker, 延迟: {} ms]", delay); | |
| 288 | 386 | try { |
| 289 | - connectMqtt(); | |
| 387 | + connectMqtt(true); // 标记为重连 | |
| 290 | 388 | } catch (Exception e) { |
| 291 | - log.error("[reconnectWithDelay][重连失败,将继续尝试重连]", e); | |
| 292 | - reconnectWithDelay(); // 失败后,继续尝试 | |
| 389 | + log.error("[reconnectWithDelay][重连失败, 将继续尝试]", e); | |
| 390 | + // 重连失败时,不需要重复调用,因为 connectMqtt(true) 内部已经处理了重连逻辑 | |
| 293 | 391 | } |
| 294 | 392 | }); |
| 295 | 393 | } |
| ... | ... | @@ -302,7 +400,7 @@ public class IotMqttUpstreamProtocol { |
| 302 | 400 | */ |
| 303 | 401 | public void publishMessage(String topic, String payload) { |
| 304 | 402 | if (mqttClient == null || !mqttClient.isConnected()) { |
| 305 | - log.warn("[publishMessage][MQTT 客户端未连接,无法发布消息][topic: {}]", topic); | |
| 403 | + log.warn("[publishMessage][MQTT 客户端未连接, 无法发布消息到 topic({})]", topic); | |
| 306 | 404 | return; |
| 307 | 405 | } |
| 308 | 406 | MqttQoS qos = MqttQoS.valueOf(emqxProperties.getMqttQos()); | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttDownstreamHandler.java
| ... | ... | @@ -41,14 +41,14 @@ public class IotMqttDownstreamHandler { |
| 41 | 41 | // 1. 获取设备信息(使用缓存) |
| 42 | 42 | IotDeviceRespDTO deviceInfo = deviceService.getDeviceFromCache(message.getDeviceId()); |
| 43 | 43 | if (deviceInfo == null) { |
| 44 | - log.warn("[handle][设备信息不存在][deviceId: {}]", message.getDeviceId()); | |
| 44 | + log.warn("[handle][设备信息不存在, deviceId: {}]", message.getDeviceId()); | |
| 45 | 45 | return; |
| 46 | 46 | } |
| 47 | 47 | |
| 48 | 48 | // 2. 根据方法构建主题 |
| 49 | 49 | String topic = buildTopicByMethod(message.getMethod(), deviceInfo.getProductKey(), deviceInfo.getDeviceName()); |
| 50 | 50 | if (StrUtil.isBlank(topic)) { |
| 51 | - log.warn("[handle][未知的消息方法:{}]", message.getMethod()); | |
| 51 | + log.warn("[handle][未知的消息方法: {}]", message.getMethod()); | |
| 52 | 52 | return; |
| 53 | 53 | } |
| 54 | 54 | |
| ... | ... | @@ -57,7 +57,7 @@ public class IotMqttDownstreamHandler { |
| 57 | 57 | |
| 58 | 58 | // 4. 发布消息 |
| 59 | 59 | protocol.publishMessage(topic, payload.toString()); |
| 60 | - log.info("[handle][发布下行消息成功][method: {}][topic: {}]", message.getMethod(), topic); | |
| 60 | + log.debug("[handle][发布下行消息成功, method: {}, topic: {}]", message.getMethod(), topic); | |
| 61 | 61 | } |
| 62 | 62 | |
| 63 | 63 | /** | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttHttpAuthHandler.java
| ... | ... | @@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; |
| 8 | 8 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; |
| 9 | 9 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 10 | 10 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; |
| 11 | +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol; | |
| 11 | 12 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 12 | 13 | import io.vertx.core.json.JsonObject; |
| 13 | 14 | import io.vertx.ext.web.RoutingContext; |
| ... | ... | @@ -46,14 +47,27 @@ public class IotMqttHttpAuthHandler { |
| 46 | 47 | private static final int INTERNAL_ERROR_STATUS_CODE = 500; |
| 47 | 48 | |
| 48 | 49 | /** |
| 50 | + * MQTT 协议实例,用于获取服务器ID | |
| 51 | + */ | |
| 52 | + private final IotMqttUpstreamProtocol protocol; | |
| 53 | + | |
| 54 | + /** | |
| 55 | + * 构造器 | |
| 56 | + * | |
| 57 | + * @param protocol MQTT 协议实例 | |
| 58 | + */ | |
| 59 | + public IotMqttHttpAuthHandler(IotMqttUpstreamProtocol protocol) { | |
| 60 | + this.protocol = protocol; | |
| 61 | + } | |
| 62 | + | |
| 63 | + /** | |
| 49 | 64 | * EMQX 认证接口 |
| 50 | 65 | */ |
| 51 | 66 | public void authenticate(RoutingContext context) { |
| 52 | 67 | try { |
| 53 | 68 | // 解析请求体 |
| 54 | - JsonObject body = context.body().asJsonObject(); | |
| 69 | + JsonObject body = parseRequestBody(context); | |
| 55 | 70 | if (body == null) { |
| 56 | - sendErrorResponse(context, 400, "请求体不能为空"); | |
| 57 | 71 | return; |
| 58 | 72 | } |
| 59 | 73 | |
| ... | ... | @@ -61,34 +75,23 @@ public class IotMqttHttpAuthHandler { |
| 61 | 75 | String username = body.getString("username"); |
| 62 | 76 | String password = body.getString("password"); |
| 63 | 77 | |
| 64 | - log.info("[authenticate][EMQX 设备认证请求][clientId: {}][username: {}]", clientid, username); | |
| 78 | + log.debug("[authenticate][EMQX 设备认证, clientId: {}, username: {}]", clientid, username); | |
| 65 | 79 | |
| 66 | 80 | // 参数校验 |
| 67 | - if (StrUtil.isEmpty(clientid) || StrUtil.isEmpty(username) || StrUtil.isEmpty(password)) { | |
| 68 | - log.warn("[authenticate][认证参数不完整][clientId: {}][username: {}]", clientid, username); | |
| 69 | - sendErrorResponse(context, BAD_REQUEST_STATUS_CODE, "认证参数不完整"); | |
| 81 | + if (!validateAuthParams(context, clientid, username, password)) { | |
| 70 | 82 | return; |
| 71 | 83 | } |
| 72 | 84 | |
| 73 | 85 | // 执行设备认证 |
| 74 | - IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); | |
| 75 | - CommonResult<Boolean> result = deviceApi.authDevice(new IotDeviceAuthReqDTO() | |
| 76 | - .setClientId(clientid) | |
| 77 | - .setUsername(username) | |
| 78 | - .setPassword(password)); | |
| 79 | - | |
| 80 | - result.checkError(); | |
| 81 | - if (!BooleanUtil.isTrue(result.getData())) { | |
| 82 | - log.warn("[authenticate][设备认证失败][clientId: {}][username: {}]", clientid, username); | |
| 83 | - sendErrorResponse(context, UNAUTHORIZED_STATUS_CODE, DEVICE_AUTH_FAIL.getMsg()); | |
| 86 | + if (!performDeviceAuth(context, clientid, username, password)) { | |
| 84 | 87 | return; |
| 85 | 88 | } |
| 86 | 89 | |
| 87 | - log.info("[authenticate][设备认证成功][clientId: {}][username: {}]", clientid, username); | |
| 90 | + log.debug("[authenticate][设备认证成功, clientId: {}, username: {}]", clientid, username); | |
| 88 | 91 | sendSuccessResponse(context, "认证成功"); |
| 89 | 92 | |
| 90 | 93 | } catch (Exception e) { |
| 91 | - log.error("[authenticate][设备认证异常]", e); | |
| 94 | + log.error("[authenticate][设备认证异常, 详细信息: {}]", e.getMessage(), e); | |
| 92 | 95 | sendErrorResponse(context, INTERNAL_ERROR_STATUS_CODE, "认证服务异常"); |
| 93 | 96 | } |
| 94 | 97 | } |
| ... | ... | @@ -99,9 +102,8 @@ public class IotMqttHttpAuthHandler { |
| 99 | 102 | public void connected(RoutingContext context) { |
| 100 | 103 | try { |
| 101 | 104 | // 解析请求体 |
| 102 | - JsonObject body = context.body().asJsonObject(); | |
| 105 | + JsonObject body = parseRequestBody(context); | |
| 103 | 106 | if (body == null) { |
| 104 | - sendErrorResponse(context, BAD_REQUEST_STATUS_CODE, "请求体不能为空"); | |
| 105 | 107 | return; |
| 106 | 108 | } |
| 107 | 109 | |
| ... | ... | @@ -109,13 +111,14 @@ public class IotMqttHttpAuthHandler { |
| 109 | 111 | String username = body.getString("username"); |
| 110 | 112 | Long timestamp = body.getLong("timestamp"); |
| 111 | 113 | |
| 112 | - log.info("[connected][设备连接事件][clientId: {}][username: {}]", clientid, username); | |
| 114 | + log.debug("[connected][设备连接, clientId: {}, username: {}, timestamp: {}]", | |
| 115 | + clientid, username, timestamp); | |
| 113 | 116 | |
| 114 | - handleDeviceStateChange(username, true); | |
| 117 | + handleDeviceStateChange(username, true, "设备连接"); | |
| 115 | 118 | sendSuccessResponse(context, "处理成功"); |
| 116 | 119 | |
| 117 | 120 | } catch (Exception e) { |
| 118 | - log.error("[connected][处理设备连接事件失败]", e); | |
| 121 | + log.error("[connected][处理设备连接事件失败, 详细信息: {}]", e.getMessage(), e); | |
| 119 | 122 | sendErrorResponse(context, INTERNAL_ERROR_STATUS_CODE, "处理失败"); |
| 120 | 123 | } |
| 121 | 124 | } |
| ... | ... | @@ -126,9 +129,8 @@ public class IotMqttHttpAuthHandler { |
| 126 | 129 | public void disconnected(RoutingContext context) { |
| 127 | 130 | try { |
| 128 | 131 | // 解析请求体 |
| 129 | - JsonObject body = context.body().asJsonObject(); | |
| 132 | + JsonObject body = parseRequestBody(context); | |
| 130 | 133 | if (body == null) { |
| 131 | - sendErrorResponse(context, BAD_REQUEST_STATUS_CODE, "请求体不能为空"); | |
| 132 | 134 | return; |
| 133 | 135 | } |
| 134 | 136 | |
| ... | ... | @@ -137,58 +139,144 @@ public class IotMqttHttpAuthHandler { |
| 137 | 139 | String reason = body.getString("reason"); |
| 138 | 140 | Long timestamp = body.getLong("timestamp"); |
| 139 | 141 | |
| 140 | - log.info("[disconnected][设备断开连接事件][clientId: {}][username: {}][reason: {}]", | |
| 141 | - clientid, username, reason); | |
| 142 | + log.debug("[disconnected][设备断开连接, clientId: {}, username: {}, reason: {}, timestamp: {}]", | |
| 143 | + clientid, username, reason, timestamp); | |
| 142 | 144 | |
| 143 | - handleDeviceStateChange(username, false); | |
| 145 | + handleDeviceStateChange(username, false, "设备断开连接,原因:" + reason); | |
| 144 | 146 | sendSuccessResponse(context, "处理成功"); |
| 145 | 147 | |
| 146 | 148 | } catch (Exception e) { |
| 147 | - log.error("[disconnected][处理设备断开连接事件失败]", e); | |
| 149 | + log.error("[disconnected][处理设备断开连接事件失败, 详细信息: {}]", e.getMessage(), e); | |
| 148 | 150 | sendErrorResponse(context, INTERNAL_ERROR_STATUS_CODE, "处理失败"); |
| 149 | 151 | } |
| 150 | 152 | } |
| 151 | 153 | |
| 152 | 154 | /** |
| 153 | - * 处理设备状态变化 | |
| 155 | + * 解析请求体 | |
| 156 | + * | |
| 157 | + * @param context 路由上下文 | |
| 158 | + * @return 请求体JSON对象,解析失败时返回null | |
| 159 | + */ | |
| 160 | + private JsonObject parseRequestBody(RoutingContext context) { | |
| 161 | + try { | |
| 162 | + JsonObject body = context.body().asJsonObject(); | |
| 163 | + if (body == null) { | |
| 164 | + log.warn("[parseRequestBody][请求体为空]"); | |
| 165 | + sendErrorResponse(context, BAD_REQUEST_STATUS_CODE, "请求体不能为空"); | |
| 166 | + return null; | |
| 167 | + } | |
| 168 | + return body; | |
| 169 | + } catch (Exception e) { | |
| 170 | + log.error("[parseRequestBody][解析请求体失败]", e); | |
| 171 | + sendErrorResponse(context, BAD_REQUEST_STATUS_CODE, "请求体格式错误"); | |
| 172 | + return null; | |
| 173 | + } | |
| 174 | + } | |
| 175 | + | |
| 176 | + /** | |
| 177 | + * 验证认证参数 | |
| 178 | + * | |
| 179 | + * @param context 路由上下文 | |
| 180 | + * @param clientid 客户端ID | |
| 181 | + * @param username 用户名 | |
| 182 | + * @param password 密码 | |
| 183 | + * @return 验证是否通过 | |
| 184 | + */ | |
| 185 | + private boolean validateAuthParams(RoutingContext context, String clientid, String username, String password) { | |
| 186 | + if (StrUtil.hasEmpty(clientid, username, password)) { | |
| 187 | + log.warn("[validateAuthParams][认证参数不完整, clientId: {}, username: {}, password: {}]", | |
| 188 | + clientid, username, StrUtil.isNotEmpty(password) ? "***" : "空"); | |
| 189 | + sendErrorResponse(context, BAD_REQUEST_STATUS_CODE, "认证参数不完整"); | |
| 190 | + return false; | |
| 191 | + } | |
| 192 | + return true; | |
| 193 | + } | |
| 194 | + | |
| 195 | + /** | |
| 196 | + * 执行设备认证 | |
| 154 | 197 | * |
| 198 | + * @param context 路由上下文 | |
| 199 | + * @param clientid 客户端ID | |
| 155 | 200 | * @param username 用户名 |
| 156 | - * @param online 是否在线 | |
| 201 | + * @param password 密码 | |
| 202 | + * @return 认证是否成功 | |
| 157 | 203 | */ |
| 158 | - private void handleDeviceStateChange(String username, boolean online) { | |
| 204 | + private boolean performDeviceAuth(RoutingContext context, String clientid, String username, String password) { | |
| 205 | + try { | |
| 206 | + IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); | |
| 207 | + CommonResult<Boolean> result = deviceApi.authDevice(new IotDeviceAuthReqDTO() | |
| 208 | + .setClientId(clientid) | |
| 209 | + .setUsername(username) | |
| 210 | + .setPassword(password)); | |
| 211 | + | |
| 212 | + result.checkError(); | |
| 213 | + if (!BooleanUtil.isTrue(result.getData())) { | |
| 214 | + log.warn("[performDeviceAuth][设备认证失败, clientId: {}, username: {}]", clientid, username); | |
| 215 | + sendErrorResponse(context, UNAUTHORIZED_STATUS_CODE, DEVICE_AUTH_FAIL.getMsg()); | |
| 216 | + return false; | |
| 217 | + } | |
| 218 | + return true; | |
| 219 | + } catch (Exception e) { | |
| 220 | + log.error("[performDeviceAuth][设备认证异常, clientId: {}, username: {}]", clientid, username, e); | |
| 221 | + sendErrorResponse(context, INTERNAL_ERROR_STATUS_CODE, "认证服务异常"); | |
| 222 | + return false; | |
| 223 | + } | |
| 224 | + } | |
| 225 | + | |
| 226 | + /** | |
| 227 | + * 处理设备状态变化 | |
| 228 | + * | |
| 229 | + * @param username 用户名 | |
| 230 | + * @param online 是否在线 | |
| 231 | + * @param actionDesc 操作描述 | |
| 232 | + */ | |
| 233 | + private void handleDeviceStateChange(String username, boolean online, String actionDesc) { | |
| 159 | 234 | if (StrUtil.isEmpty(username) || "undefined".equals(username)) { |
| 160 | - log.warn("[handleDeviceStateChange][用户名为空或未定义][username: {}]", username); | |
| 235 | + log.warn("[handleDeviceStateChange][用户名为空或'undefined', username: {}, action: {}]", | |
| 236 | + username, actionDesc); | |
| 161 | 237 | return; |
| 162 | 238 | } |
| 163 | 239 | |
| 164 | 240 | // 解析设备信息 |
| 165 | 241 | IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(username); |
| 166 | 242 | if (deviceInfo == null) { |
| 167 | - log.warn("[handleDeviceStateChange][无法解析设备信息][username: {}]", username); | |
| 243 | + log.warn("[handleDeviceStateChange][无法从 username({}) 解析设备信息, action: {}]", | |
| 244 | + username, actionDesc); | |
| 168 | 245 | return; |
| 169 | 246 | } |
| 170 | 247 | |
| 171 | 248 | try { |
| 172 | - // 获取服务器 ID | |
| 173 | - String serverId = "mqtt_auth_gateway"; | |
| 249 | + // 从协议实例获取服务器 ID | |
| 250 | + String serverId = protocol.getServerId(); | |
| 251 | + if (StrUtil.isEmpty(serverId)) { | |
| 252 | + log.error("[handleDeviceStateChange][获取服务器ID失败, username: {}, action: {}]", | |
| 253 | + username, actionDesc); | |
| 254 | + return; | |
| 255 | + } | |
| 174 | 256 | |
| 175 | 257 | // 构建设备状态消息 |
| 176 | 258 | IotDeviceMessageService deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); |
| 177 | 259 | IotDeviceMessage message; |
| 178 | 260 | if (online) { |
| 179 | 261 | message = IotDeviceMessage.buildStateOnline(); |
| 180 | - log.info("[handleDeviceStateChange][发送设备上线消息成功][username: {}]", username); | |
| 262 | + log.debug("[handleDeviceStateChange][发送设备上线消息, username: {}, serverId: {}]", | |
| 263 | + username, serverId); | |
| 181 | 264 | } else { |
| 182 | 265 | message = IotDeviceMessage.buildStateOffline(); |
| 183 | - log.info("[handleDeviceStateChange][发送设备下线消息成功][username: {}]", username); | |
| 266 | + log.debug("[handleDeviceStateChange][发送设备下线消息, username: {}, serverId: {}]", | |
| 267 | + username, serverId); | |
| 184 | 268 | } |
| 185 | 269 | |
| 186 | 270 | // 发送消息到消息总线 |
| 187 | 271 | deviceMessageService.sendDeviceMessage(message, |
| 188 | 272 | deviceInfo.getProductKey(), deviceInfo.getDeviceName(), serverId); |
| 273 | + | |
| 274 | + log.info("[handleDeviceStateChange][{}处理成功, productKey: {}, deviceName: {}, serverId: {}]", | |
| 275 | + actionDesc, deviceInfo.getProductKey(), deviceInfo.getDeviceName(), serverId); | |
| 276 | + | |
| 189 | 277 | } catch (Exception e) { |
| 190 | - log.error("[handleDeviceStateChange][发送设备状态消息失败][username: {}][online: {}]", | |
| 191 | - username, online, e); | |
| 278 | + log.error("[handleDeviceStateChange][发送设备状态消息失败, username: {}, online: {}, action: {}]", | |
| 279 | + username, online, actionDesc, e); | |
| 192 | 280 | } |
| 193 | 281 | } |
| 194 | 282 | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttUpstreamHandler.java
| ... | ... | @@ -9,6 +9,8 @@ import io.vertx.mqtt.messages.MqttPublishMessage; |
| 9 | 9 | import lombok.extern.slf4j.Slf4j; |
| 10 | 10 | import org.springframework.util.Assert; |
| 11 | 11 | |
| 12 | +import java.util.Arrays; | |
| 13 | + | |
| 12 | 14 | /** |
| 13 | 15 | * IoT 网关 MQTT 上行消息处理器 |
| 14 | 16 | * |
| ... | ... | @@ -32,12 +34,12 @@ public class IotMqttUpstreamHandler { |
| 32 | 34 | String topic = message.topicName(); |
| 33 | 35 | byte[] payload = message.payload().getBytes(); |
| 34 | 36 | |
| 35 | - log.debug("[handle][收到 MQTT 消息][topic: {}]", topic); | |
| 37 | + log.debug("[handle][收到 MQTT 消息, topic: {}]", topic); | |
| 36 | 38 | |
| 37 | 39 | try { |
| 38 | 40 | // 1. 前置校验 |
| 39 | 41 | if (StrUtil.isBlank(topic)) { |
| 40 | - log.warn("[validateInput][主题为空,忽略消息]"); | |
| 42 | + log.warn("[handle][主题为空, 忽略消息]"); | |
| 41 | 43 | return; |
| 42 | 44 | } |
| 43 | 45 | // 注意:payload 可以为空 |
| ... | ... | @@ -45,7 +47,7 @@ public class IotMqttUpstreamHandler { |
| 45 | 47 | // 2. 识别并验证消息类型 |
| 46 | 48 | String messageType = getMessageType(topic); |
| 47 | 49 | Assert.notNull(messageType, String.format("未知的消息类型, topic(%s)", topic)); |
| 48 | - log.info("[handle][接收到{}][topic: {}]", messageType, topic); | |
| 50 | + log.debug("[handle][接收到上行消息({}), topic: {}]", messageType, topic); | |
| 49 | 51 | |
| 50 | 52 | // 3. 解析主题,获取 productKey 和 deviceName |
| 51 | 53 | String[] topicParts = topic.split("/"); |
| ... | ... | @@ -72,7 +74,7 @@ public class IotMqttUpstreamHandler { |
| 72 | 74 | deviceMessageService.sendDeviceMessage(deviceMessage, productKey, deviceName, serverId); |
| 73 | 75 | |
| 74 | 76 | // 6. 记录成功日志 |
| 75 | - log.info("[handle][处理{}成功,已转发到 MQ][topic: {}]", messageType, topic); | |
| 77 | + log.debug("[handle][处理上行消息({})成功, topic: {}]", messageType, topic); | |
| 76 | 78 | } catch (Exception e) { |
| 77 | 79 | log.error("[handle][处理 MQTT 消息失败][topic: {}][payload: {}]", topic, new String(payload), e); |
| 78 | 80 | } |
| ... | ... | @@ -86,10 +88,13 @@ public class IotMqttUpstreamHandler { |
| 86 | 88 | */ |
| 87 | 89 | private String getMessageType(String topic) { |
| 88 | 90 | String[] topicParts = topic.split("/"); |
| 89 | - if (topicParts.length < 7) { | |
| 90 | - return null; | |
| 91 | + // 约定:topic 第 4 个部分开始为消息类型 | |
| 92 | + // 例如:/sys/{productKey}/{deviceName}/thing/property/post -> | |
| 93 | + // thing/property/post | |
| 94 | + if (topicParts.length > 4) { | |
| 95 | + return String.join("/", Arrays.copyOfRange(topicParts, 4, topicParts.length)); | |
| 91 | 96 | } |
| 92 | - return topicParts[3]; | |
| 97 | + return topicParts[topicParts.length - 1]; | |
| 93 | 98 | } |
| 94 | 99 | |
| 95 | 100 | } |
| 96 | 101 | \ No newline at end of file | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application-local.yaml