From 69e25eeaaccfa6aa81b252ea4de7c7159fe5aec0 Mon Sep 17 00:00:00 2001 From: haohao <1036606149@qq.com> Date: Sat, 14 Jun 2025 20:44:55 +0800 Subject: [PATCH] reactor:【IoT 物联网】重构 EMQX 协议处理逻辑,新增认证和下行消息处理器 --- yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java | 20 +++++++++++++------- yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxAuthEventProtocol.java | 113 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxDownstreamSubscriber.java | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxUpstreamProtocol.java | 328 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxAuthEventHandler.java | 252 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxDownstreamHandler.java | 96 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxUpstreamHandler.java | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttDownstreamSubscriber.java | 66 ------------------------------------------------------------------ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttUpstreamProtocol.java | 430 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttDownstreamHandler.java | 96 ------------------------------------------------------------------------------------------------ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttHttpAuthHandler.java | 250 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttUpstreamHandler.java | 60 ------------------------------------------------------------ 12 files changed, 930 insertions(+), 909 deletions(-) create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxAuthEventProtocol.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxDownstreamSubscriber.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxUpstreamProtocol.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxAuthEventHandler.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxDownstreamHandler.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxUpstreamHandler.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttDownstreamSubscriber.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttUpstreamProtocol.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttDownstreamHandler.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttHttpAuthHandler.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttUpstreamHandler.java diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java index d730e92..e9b0001 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java @@ -1,10 +1,11 @@ package cn.iocoder.yudao.module.iot.gateway.config; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxAuthEventProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttDownstreamSubscriber; -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -37,7 +38,7 @@ public class IotGatewayConfiguration { } /** - * IoT 网关 MQTT 协议配置类 + * IoT 网关 EMQX 协议配置类 */ @Configuration @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.emqx", name = "enabled", havingValue = "true") @@ -45,14 +46,19 @@ public class IotGatewayConfiguration { public static class MqttProtocolConfiguration { @Bean - public IotMqttUpstreamProtocol iotMqttUpstreamProtocol(IotGatewayProperties gatewayProperties) { - return new IotMqttUpstreamProtocol(gatewayProperties.getProtocol().getEmqx()); + public IotEmqxAuthEventProtocol iotEmqxAuthEventProtocol(IotGatewayProperties gatewayProperties) { + return new IotEmqxAuthEventProtocol(gatewayProperties.getProtocol().getEmqx()); } @Bean - public IotMqttDownstreamSubscriber iotMqttDownstreamSubscriber(IotMqttUpstreamProtocol mqttUpstreamProtocol, + public IotEmqxUpstreamProtocol iotEmqxUpstreamProtocol(IotGatewayProperties gatewayProperties) { + return new IotEmqxUpstreamProtocol(gatewayProperties.getProtocol().getEmqx()); + } + + @Bean + public IotEmqxDownstreamSubscriber iotEmqxDownstreamSubscriber(IotEmqxUpstreamProtocol mqttUpstreamProtocol, IotMessageBus messageBus) { - return new IotMqttDownstreamSubscriber(mqttUpstreamProtocol, messageBus); + return new IotEmqxDownstreamSubscriber(mqttUpstreamProtocol, messageBus); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxAuthEventProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxAuthEventProtocol.java new file mode 100644 index 0000000..2ba902c --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxAuthEventProtocol.java @@ -0,0 +1,113 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx; + +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router.IotEmqxAuthEventHandler; +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpServer; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.handler.BodyHandler; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT 网关 EMQX 认证事件协议服务 + *
+ * 为 EMQX 提供 HTTP 接口服务,包括:
+ * 1. 设备认证接口 - 对应 EMQX HTTP 认证插件
+ * 2. 设备事件处理接口 - 对应 EMQX Webhook 事件通知
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public class IotEmqxAuthEventProtocol {
+
+ private final IotGatewayProperties.EmqxProperties emqxProperties;
+
+ private final String serverId;
+
+ private Vertx vertx;
+
+ private HttpServer httpServer;
+
+ public IotEmqxAuthEventProtocol(IotGatewayProperties.EmqxProperties emqxProperties) {
+ this.emqxProperties = emqxProperties;
+ this.serverId = IotDeviceMessageUtils.generateServerId(emqxProperties.getMqttPort());
+ }
+
+ @PostConstruct
+ public void start() {
+ try {
+ // 创建 Vertx 实例
+ this.vertx = Vertx.vertx();
+ startHttpServer();
+ log.info("[start][IoT 网关 EMQX 认证事件协议服务启动成功, 端口: {}]", emqxProperties.getHttpPort());
+ } catch (Exception e) {
+ log.error("[start][IoT 网关 EMQX 认证事件协议服务启动失败]", e);
+ throw e;
+ }
+ }
+
+ @PreDestroy
+ public void stop() {
+ stopHttpServer();
+
+ // 关闭 Vertx 实例
+ if (vertx != null) {
+ try {
+ vertx.close();
+ log.debug("[stop][Vertx 实例已关闭]");
+ } catch (Exception e) {
+ log.warn("[stop][关闭 Vertx 实例失败]", e);
+ }
+ }
+
+ log.info("[stop][IoT 网关 EMQX 认证事件协议服务已停止]");
+ }
+
+ /**
+ * 启动 HTTP 服务器
+ */
+ private void startHttpServer() {
+ int port = emqxProperties.getHttpPort();
+
+ // 1. 创建路由
+ Router router = Router.router(vertx);
+ router.route().handler(BodyHandler.create());
+
+ // 2. 创建处理器,传入 serverId
+ IotEmqxAuthEventHandler handler = new IotEmqxAuthEventHandler(serverId);
+ router.post(IotMqttTopicUtils.MQTT_AUTH_PATH).handler(handler::handleAuth);
+ router.post(IotMqttTopicUtils.MQTT_EVENT_PATH).handler(handler::handleEvent);
+
+ // 3. 启动 HTTP 服务器
+ try {
+ httpServer = vertx.createHttpServer()
+ .requestHandler(router)
+ .listen(port)
+ .result();
+ } catch (Exception e) {
+ log.error("[startHttpServer][HTTP 服务器启动失败, 端口: {}]", port, e);
+ throw e;
+ }
+ }
+
+ /**
+ * 停止 HTTP 服务器
+ */
+ private void stopHttpServer() {
+ if (httpServer == null) {
+ return;
+ }
+
+ try {
+ httpServer.close().result();
+ log.info("[stopHttpServer][HTTP 服务器已停止]");
+ } catch (Exception e) {
+ log.error("[stopHttpServer][HTTP 服务器停止失败]", e);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxDownstreamSubscriber.java
new file mode 100644
index 0000000..61bf123
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxDownstreamSubscriber.java
@@ -0,0 +1,68 @@
+package cn.iocoder.yudao.module.iot.gateway.protocol.emqx;
+
+import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
+import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;
+import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
+import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
+import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router.IotEmqxDownstreamHandler;
+import jakarta.annotation.PostConstruct;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * IoT 网关 EMQX 订阅者:接收下行给设备的消息
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public class IotEmqxDownstreamSubscriber implements IotMessageSubscriber
+ * 为 EMQX 提供 HTTP 接口服务,包括:
+ * 1. 设备认证接口 - 对应 EMQX HTTP 认证插件
+ * 2. 设备事件处理接口 - 对应 EMQX Webhook 事件通知
+ * 提供统一的错误处理和参数校验
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public class IotEmqxAuthEventHandler {
+
+ /**
+ * HTTP 成功状态码(EMQX 要求固定使用 200)
+ */
+ private static final int SUCCESS_STATUS_CODE = 200;
+
+ /**
+ * 认证允许结果
+ */
+ private static final String RESULT_ALLOW = "allow";
+ /**
+ * 认证拒绝结果
+ */
+ private static final String RESULT_DENY = "deny";
+ /**
+ * 认证忽略结果
+ */
+ private static final String RESULT_IGNORE = "ignore";
+
+ /**
+ * EMQX 事件类型常量
+ */
+ private static final String EVENT_CLIENT_CONNECTED = "client.connected";
+ private static final String EVENT_CLIENT_DISCONNECTED = "client.disconnected";
+
+ private final String serverId;
+
+ private final IotDeviceMessageService deviceMessageService;
+
+ private final IotDeviceCommonApi deviceApi;
+
+ public IotEmqxAuthEventHandler(String serverId) {
+ this.serverId = serverId;
+ this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
+ this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
+ }
+
+ /**
+ * EMQX 认证接口
+ */
+ public void handleAuth(RoutingContext context) {
+ try {
+ // 1. 参数校验
+ JsonObject body = parseRequestBody(context);
+ if (body == null) {
+ return;
+ }
+ String clientId = body.getString("clientid");
+ String username = body.getString("username");
+ String password = body.getString("password");
+ log.debug("[handleAuth][设备认证请求: clientId={}, username={}]", clientId, username);
+ if (StrUtil.hasEmpty(clientId, username, password)) {
+ log.info("[handleAuth][认证参数不完整: clientId={}, username={}]", clientId, username);
+ sendAuthResponse(context, RESULT_DENY);
+ return;
+ }
+
+ // 2. 执行认证
+ boolean authResult = performDeviceAuth(clientId, username, password);
+ log.info("[handleAuth][设备认证结果: {} -> {}]", username, authResult);
+ if (authResult) {
+ sendAuthResponse(context, RESULT_ALLOW);
+ } else {
+ sendAuthResponse(context, RESULT_DENY);
+ }
+ } catch (Exception e) {
+ log.error("[handleAuth][设备认证异常]", e);
+ sendAuthResponse(context, RESULT_IGNORE);
+ }
+ }
+
+ /**
+ * EMQX 统一事件处理接口
+ * 根据 EMQX 官方 Webhook 设计,统一处理所有客户端事件
+ * 支持的事件类型:client.connected、client.disconnected 等
+ */
+ public void handleEvent(RoutingContext context) {
+ JsonObject body = null;
+ try {
+ // 1. 解析请求体
+ body = parseRequestBody(context);
+ if (body == null) {
+ return;
+ }
+ String event = body.getString("event");
+ String username = body.getString("username");
+ log.debug("[handleEvent][收到事件: {} - {}]", event, username);
+
+ // 2. 根据事件类型进行分发处理
+ switch (event) {
+ case EVENT_CLIENT_CONNECTED:
+ handleClientConnected(body);
+ break;
+ case EVENT_CLIENT_DISCONNECTED:
+ handleClientDisconnected(body);
+ break;
+ default:
+ break;
+ }
+
+ // EMQX Webhook 只需要 200 状态码,无需响应体
+ context.response().setStatusCode(SUCCESS_STATUS_CODE).end();
+ } catch (Exception e) {
+ log.error("[handleEvent][事件处理失败][body={}]", body != null ? body.encode() : "null", e);
+ // 即使处理失败,也返回 200 避免EMQX重试
+ context.response().setStatusCode(SUCCESS_STATUS_CODE).end();
+ }
+ }
+
+ /**
+ * 处理客户端连接事件
+ */
+ private void handleClientConnected(JsonObject body) {
+ String username = body.getString("username");
+ log.info("[handleClientConnected][设备上线: {}]", username);
+ handleDeviceStateChange(username, true);
+ }
+
+ /**
+ * 处理客户端断开连接事件
+ */
+ private void handleClientDisconnected(JsonObject body) {
+ String username = body.getString("username");
+ String reason = body.getString("reason");
+ log.info("[handleClientDisconnected][设备下线: {} ({})]", username, reason);
+ handleDeviceStateChange(username, false);
+ }
+
+ /**
+ * 解析请求体
+ *
+ * @param context 路由上下文
+ * @return 请求体JSON对象,解析失败时返回null
+ */
+ private JsonObject parseRequestBody(RoutingContext context) {
+ String rawBody = null;
+ try {
+ rawBody = context.body().asString();
+ JsonObject body = context.body().asJsonObject();
+ if (body == null) {
+ log.info("[parseRequestBody][请求体为空][rawBody={}]", rawBody);
+ sendAuthResponse(context, RESULT_IGNORE);
+ return null;
+ }
+ return body;
+ } catch (Exception e) {
+ log.error("[parseRequestBody][解析请求体失败][rawBody={}]", rawBody, e);
+ sendAuthResponse(context, RESULT_IGNORE);
+ return null;
+ }
+ }
+
+ /**
+ * 执行设备认证
+ *
+ * @param clientId 客户端ID
+ * @param username 用户名
+ * @param password 密码
+ * @return 认证是否成功
+ */
+ private boolean performDeviceAuth(String clientId, String username, String password) {
+ try {
+ CommonResult
+ * 从消息总线接收到下行消息,然后发布到 MQTT Broker,从而被设备所接收
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public class IotEmqxDownstreamHandler {
+
+ private final IotEmqxUpstreamProtocol protocol;
+
+ private final IotDeviceService deviceService;
+
+ private final IotDeviceMessageService deviceMessageService;
+
+ public IotEmqxDownstreamHandler(IotEmqxUpstreamProtocol protocol) {
+ this.protocol = protocol;
+ this.deviceService = SpringUtil.getBean(IotDeviceService.class);
+ this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
+ }
+
+ /**
+ * 处理下行消息
+ *
+ * @param message 设备消息
+ */
+ public void handle(IotDeviceMessage message) {
+ // 1. 获取设备信息(使用缓存)
+ IotDeviceRespDTO deviceInfo = deviceService.getDeviceFromCache(message.getDeviceId());
+ if (deviceInfo == null) {
+ log.error("[handle][设备信息({})不存在]", message.getDeviceId());
+ return;
+ }
+
+ // 2.1 根据方法构建主题
+ String topic = buildTopicByMethod(message, deviceInfo.getProductKey(), deviceInfo.getDeviceName());
+ if (StrUtil.isBlank(topic)) {
+ log.warn("[handle][未知的消息方法: {}]", message.getMethod());
+ return;
+ }
+
+ // 2.2 构建载荷
+ byte[] payload = deviceMessageService.encodeDeviceMessage(message, deviceInfo.getProductKey(), deviceInfo.getDeviceName());
+
+ // 2.3 发布消息
+ protocol.publishMessage(topic, new String(payload));
+ }
+
+ /**
+ * 根据消息方法和回复状态构建主题
+ *
+ * @param message 设备消息
+ * @param productKey 产品标识
+ * @param deviceName 设备名称
+ * @return 构建的主题,如果方法不支持返回 null
+ */
+ private String buildTopicByMethod(IotDeviceMessage message, String productKey, String deviceName) {
+ // 1. 解析消息方法
+ IotDeviceMessageMethodEnum methodEnum = IotDeviceMessageMethodEnum.of(message.getMethod());
+ if (methodEnum == null) {
+ log.warn("[buildTopicByMethod][未知的消息方法: {}]", message.getMethod());
+ return null;
+ }
+
+ // 2. 判断是否回复消息
+ boolean isReply = IotDeviceMessageUtils.isReplyMessage(message);
+
+ // 3. 根据消息方法和回复状态,构建主题
+ if (methodEnum == IotDeviceMessageMethodEnum.PROPERTY_POST && isReply) {
+ return IotMqttTopicUtils.buildPropertyPostReplyTopic(productKey, deviceName);
+ }
+ if (methodEnum == IotDeviceMessageMethodEnum.PROPERTY_SET && !isReply) {
+ return IotMqttTopicUtils.buildPropertySetTopic(productKey, deviceName);
+ }
+
+ log.warn("[buildTopicByMethod][暂时不支持的下行消息: method={}, isReply={}]",
+ message.getMethod(), isReply);
+ return null;
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxUpstreamHandler.java
new file mode 100644
index 0000000..81d8cbb
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxUpstreamHandler.java
@@ -0,0 +1,60 @@
+package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.extra.spring.SpringUtil;
+import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
+import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol;
+import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
+import io.vertx.mqtt.messages.MqttPublishMessage;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * IoT 网关 EMQX 上行消息处理器
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public class IotEmqxUpstreamHandler {
+
+ private final IotDeviceMessageService deviceMessageService;
+
+ private final String serverId;
+
+ public IotEmqxUpstreamHandler(IotEmqxUpstreamProtocol protocol) {
+ this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
+ this.serverId = protocol.getServerId();
+ }
+
+ /**
+ * 处理 MQTT 发布消息
+ */
+ public void handle(MqttPublishMessage mqttMessage) {
+ log.info("[handle][收到 MQTT 消息, topic: {}, payload: {}]", mqttMessage.topicName(), mqttMessage.payload());
+ String topic = mqttMessage.topicName();
+ byte[] payload = mqttMessage.payload().getBytes();
+ try {
+ // 1. 解析主题,一次性获取所有信息
+ String[] topicParts = topic.split("/");
+ if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) {
+ log.warn("[handle][topic({}) 格式不正确,无法解析有效的 productKey 和 deviceName]", topic);
+ return;
+ }
+
+ String productKey = topicParts[2];
+ String deviceName = topicParts[3];
+
+ // 3. 解码消息
+ IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName);
+ if (message == null) {
+ log.warn("[handle][topic({}) payload({}) 消息解码失败]", topic, new String(payload));
+ return;
+ }
+
+ // 4. 发送消息到队列
+ deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId);
+ } catch (Exception e) {
+ log.error("[handle][topic({}) payload({}) 处理异常]", topic, new String(payload), e);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttDownstreamSubscriber.java
deleted file mode 100644
index 861c3a5..0000000
--- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttDownstreamSubscriber.java
+++ /dev/null
@@ -1,66 +0,0 @@
-package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;
-
-import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
-import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;
-import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
-import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
-import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler;
-import jakarta.annotation.PostConstruct;
-import lombok.extern.slf4j.Slf4j;
-
-/**
- * IoT 网关 MQTT 订阅者:接收下行给设备的消息
- *
- * @author 芋道源码
- */
-@Slf4j
-public class IotMqttDownstreamSubscriber implements IotMessageSubscriber
- * 1. MQTT 客户端:连接 EMQX,消费处理设备上行和下行消息
- * 2. HTTP 认证服务:为 EMQX 提供设备认证、连接、断开接口
- *
- * @author 芋道源码
- */
-@Slf4j
-public class IotMqttUpstreamProtocol {
-
- private final IotGatewayProperties.EmqxProperties emqxProperties;
-
- /**
- * 服务运行状态标志
- */
- private volatile boolean isRunning = false;
-
- private Vertx vertx;
-
- @Getter
- private final String serverId;
-
- // MQTT 客户端相关
- private MqttClient mqttClient;
- private IotMqttUpstreamHandler upstreamHandler;
-
- // HTTP 认证服务相关
- private HttpServer httpAuthServer;
-
- public IotMqttUpstreamProtocol(IotGatewayProperties.EmqxProperties emqxProperties) {
- this.emqxProperties = emqxProperties;
- this.serverId = IotDeviceMessageUtils.generateServerId(emqxProperties.getMqttPort());
- }
-
- @PostConstruct
- public void start() {
- if (isRunning) {
- log.warn("[start][MQTT 协议服务已经在运行中,请勿重复启动]");
- return;
- }
- log.info("[start][启动 MQTT 协议服务]");
-
- try {
- this.vertx = Vertx.vertx();
-
- // 1. 启动 HTTP 认证服务
- startHttpAuthServer();
-
- // 2. 启动 MQTT 客户端
- startMqttClient();
-
- isRunning = true;
- log.info("[start][MQTT 协议服务启动完成]");
- } catch (Exception e) {
- log.error("[start][MQTT 协议服务启动失败]", e);
- // 启动失败时清理资源
- stop();
- throw e;
- }
- }
-
- @PreDestroy
- public void stop() {
- if (!isRunning) {
- log.warn("[stop][MQTT 协议服务已经停止,无需再次停止]");
- return;
- }
- log.info("[stop][停止 MQTT 协议服务]");
-
- // 1. 停止 MQTT 客户端
- stopMqttClient();
-
- // 2. 停止 HTTP 认证服务
- stopHttpAuthServer();
-
- // 3. 关闭 Vertx 实例
- if (vertx != null) {
- try {
- vertx.close();
- log.debug("[stop][Vertx 实例已关闭]");
- } catch (Exception e) {
- log.warn("[stop][关闭 Vertx 实例失败]", e);
- }
- }
-
- isRunning = false;
- log.info("[stop][MQTT 协议服务已停止]");
- }
-
- /**
- * 启动 HTTP 认证服务
- */
- private void startHttpAuthServer() {
- log.info("[startHttpAuthServer][启动 HTTP 认证服务]");
-
- // 1.1 创建路由
- Router router = Router.router(vertx);
- router.route().handler(BodyHandler.create());
- // 1.2 创建认证处理器
- IotMqttHttpAuthHandler authHandler = new IotMqttHttpAuthHandler(this);
- router.post(IotMqttTopicUtils.MQTT_AUTH_PATH).handler(authHandler::handleAuth);
- router.post(IotMqttTopicUtils.MQTT_EVENT_PATH).handler(authHandler::handleEvent);
-
- // 2. 启动 HTTP 服务器
- int authPort = emqxProperties.getHttpPort();
- try {
- httpAuthServer = vertx.createHttpServer()
- .requestHandler(router)
- .listen(authPort)
- .result();
- log.info("[startHttpAuthServer][HTTP 认证服务启动成功, 端口: {}]", authPort);
- } catch (Exception e) {
- log.error("[startHttpAuthServer][HTTP 认证服务启动失败]", e);
- throw e;
- }
- }
-
- /**
- * 停止 HTTP 认证服务
- */
- private void stopHttpAuthServer() {
- if (httpAuthServer == null) {
- return;
- }
- try {
- httpAuthServer.close().result();
- log.info("[stopHttpAuthServer][HTTP 认证服务已停止]");
- } catch (Exception e) {
- log.error("[stopHttpAuthServer][HTTP 认证服务停止失败]", e);
- }
- }
-
- /**
- * 启动 MQTT 客户端
- */
- private void startMqttClient() {
- log.info("[startMqttClient][启动 MQTT 客户端]");
-
- try {
- // 1. 初始化消息处理器
- this.upstreamHandler = new IotMqttUpstreamHandler(this);
-
- // 2. 创建 MQTT 客户端
- log.info("[startMqttClient][使用 MQTT 客户端 ID: {}]", emqxProperties.getMqttClientId());
- createMqttClient();
-
- // 3. 连接 MQTT Broker(同步等待首次连接结果)
- boolean connected = connectMqttSync();
- if (!connected) {
- throw new RuntimeException("首次连接 MQTT Broker 失败");
- }
-
- log.info("[startMqttClient][MQTT 客户端启动完成]");
- } catch (Exception e) {
- log.error("[startMqttClient][MQTT 客户端启动失败]", e);
- throw new RuntimeException("MQTT 客户端启动失败", e);
- }
- }
-
- /**
- * 同步连接 MQTT Broker
- *
- * @return 是否连接成功
- */
- private boolean connectMqttSync() {
- String host = emqxProperties.getMqttHost();
- Integer port = emqxProperties.getMqttPort();
- log.info("[connectMqttSync][开始连接 MQTT Broker, host: {}, port: {}]", host, port);
-
- // 使用计数器实现同步等待
- java.util.concurrent.CountDownLatch latch = new java.util.concurrent.CountDownLatch(1);
- java.util.concurrent.atomic.AtomicBoolean success = new java.util.concurrent.atomic.AtomicBoolean(false);
-
- mqttClient.connect(port, host, connectResult -> {
- if (connectResult.succeeded()) {
- log.info("[connectMqttSync][MQTT 客户端连接成功, host: {}, port: {}]", host, port);
- // 设置处理器
- setupMqttHandlers();
- // 订阅主题
- subscribeToTopics();
- success.set(true);
- } else {
- log.error("[connectMqttSync][连接 MQTT Broker 失败, host: {}, port: {}]",
- host, port, connectResult.cause());
- // 首次连接失败,启动重连机制
- reconnectWithDelay();
- }
- latch.countDown();
- });
-
- try {
- // 等待连接结果,最多等待10秒
- latch.await(10, java.util.concurrent.TimeUnit.SECONDS);
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- log.error("[connectMqttSync][等待连接结果被中断]", e);
- }
-
- return success.get();
- }
-
- /**
- * 停止 MQTT 客户端
- */
- private void stopMqttClient() {
- // 1. 取消 MQTT 主题订阅
- if (mqttClient != null && mqttClient.isConnected()) {
- List
- * 从消息总线接收到下行消息,然后发布到 MQTT Broker,从而被设备所接收
- *
- * @author 芋道源码
- */
-@Slf4j
-public class IotMqttDownstreamHandler {
-
- private final IotMqttUpstreamProtocol protocol;
-
- private final IotDeviceService deviceService;
-
- private final IotDeviceMessageService deviceMessageService;
-
- public IotMqttDownstreamHandler(IotMqttUpstreamProtocol protocol) {
- this.protocol = protocol;
- this.deviceService = SpringUtil.getBean(IotDeviceService.class);
- this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
- }
-
- /**
- * 处理下行消息
- *
- * @param message 设备消息
- */
- public void handle(IotDeviceMessage message) {
- // 1. 获取设备信息(使用缓存)
- IotDeviceRespDTO deviceInfo = deviceService.getDeviceFromCache(message.getDeviceId());
- if (deviceInfo == null) {
- log.error("[handle][设备信息({})不存在]", message.getDeviceId());
- return;
- }
-
- // 2.1 根据方法构建主题
- String topic = buildTopicByMethod(message, deviceInfo.getProductKey(), deviceInfo.getDeviceName());
- if (StrUtil.isBlank(topic)) {
- log.warn("[handle][未知的消息方法: {}]", message.getMethod());
- return;
- }
-
- // 2.2 构建载荷
- byte[] payload = deviceMessageService.encodeDeviceMessage(message, deviceInfo.getProductKey(), deviceInfo.getDeviceName());
-
- // 2.3 发布消息
- protocol.publishMessage(topic, new String(payload));
- }
-
- /**
- * 根据消息方法和回复状态构建主题
- *
- * @param message 设备消息
- * @param productKey 产品标识
- * @param deviceName 设备名称
- * @return 构建的主题,如果方法不支持返回 null
- */
- private String buildTopicByMethod(IotDeviceMessage message, String productKey, String deviceName) {
- // 1. 解析消息方法
- IotDeviceMessageMethodEnum methodEnum = IotDeviceMessageMethodEnum.of(message.getMethod());
- if (methodEnum == null) {
- log.warn("[buildTopicByMethod][未知的消息方法: {}]", message.getMethod());
- return null;
- }
-
- // 2. 判断是否回复消息
- boolean isReply = IotDeviceMessageUtils.isReplyMessage(message);
-
- // 3. 根据消息方法和回复状态,构建主题
- if (methodEnum == IotDeviceMessageMethodEnum.PROPERTY_POST && isReply) {
- return IotMqttTopicUtils.buildPropertyPostReplyTopic(productKey, deviceName);
- }
- if (methodEnum == IotDeviceMessageMethodEnum.PROPERTY_SET && !isReply) {
- return IotMqttTopicUtils.buildPropertySetTopic(productKey, deviceName);
- }
-
- log.warn("[buildTopicByMethod][暂时不支持的下行消息: method={}, isReply={}]",
- message.getMethod(), isReply);
- return null;
- }
-
-}
\ No newline at end of file
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttHttpAuthHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttHttpAuthHandler.java
deleted file mode 100644
index 3410fb3..0000000
--- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttHttpAuthHandler.java
+++ /dev/null
@@ -1,250 +0,0 @@
-package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router;
-
-import cn.hutool.core.util.BooleanUtil;
-import cn.hutool.core.util.StrUtil;
-import cn.hutool.extra.spring.SpringUtil;
-import cn.iocoder.yudao.framework.common.pojo.CommonResult;
-import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
-import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
-import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
-import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
-import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol;
-import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
-import io.vertx.core.json.JsonObject;
-import io.vertx.ext.web.RoutingContext;
-import lombok.extern.slf4j.Slf4j;
-
-/**
- * IoT 网关 MQTT HTTP 认证处理器
- *
- * 处理 EMQX 的认证请求和事件钩子,提供统一的错误处理和参数校验
- *
- * @author 芋道源码
- */
-@Slf4j
-public class IotMqttHttpAuthHandler {
-
- /**
- * HTTP 成功状态码(EMQX 要求固定使用 200)
- */
- private static final int SUCCESS_STATUS_CODE = 200;
-
- /**
- * 认证允许结果
- */
- private static final String RESULT_ALLOW = "allow";
- /**
- * 认证拒绝结果
- */
- private static final String RESULT_DENY = "deny";
- /**
- * 认证忽略结果
- */
- private static final String RESULT_IGNORE = "ignore";
-
- /**
- * EMQX 事件类型常量
- */
- private static final String EVENT_CLIENT_CONNECTED = "client.connected";
- private static final String EVENT_CLIENT_DISCONNECTED = "client.disconnected";
-
- private final IotMqttUpstreamProtocol protocol;
-
- private final IotDeviceMessageService deviceMessageService;
-
- private final IotDeviceCommonApi deviceApi;
-
- public IotMqttHttpAuthHandler(IotMqttUpstreamProtocol protocol) {
- this.protocol = protocol;
- this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
- this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
- }
-
- /**
- * EMQX 认证接口
- */
- public void handleAuth(RoutingContext context) {
- try {
- // 1. 参数校验
- JsonObject body = parseRequestBody(context);
- if (body == null) {
- return;
- }
- String clientId = body.getString("clientid");
- String username = body.getString("username");
- String password = body.getString("password");
- log.debug("[handleAuth][设备认证请求: clientId={}, username={}]", clientId, username);
- if (StrUtil.hasEmpty(clientId, username, password)) {
- log.info("[handleAuth][认证参数不完整: clientId={}, username={}]", clientId, username);
- sendAuthResponse(context, RESULT_DENY);
- return;
- }
-
- // 2. 执行认证
- boolean authResult = performDeviceAuth(clientId, username, password);
- log.info("[handleAuth][设备认证结果: {} -> {}]", username, authResult);
- if (authResult) {
- sendAuthResponse(context, RESULT_ALLOW);
- } else {
- sendAuthResponse(context, RESULT_DENY);
- }
- } catch (Exception e) {
- log.error("[handleAuth][设备认证异常]", e);
- sendAuthResponse(context, RESULT_IGNORE);
- }
- }
-
- /**
- * EMQX 统一事件处理接口
- * 根据 EMQX 官方 Webhook 设计,统一处理所有客户端事件
- * 支持的事件类型:client.connected、client.disconnected 等
- */
- public void handleEvent(RoutingContext context) {
- JsonObject body = null;
- try {
- // 1. 解析请求体
- body = parseRequestBody(context);
- if (body == null) {
- return;
- }
- String event = body.getString("event");
- String username = body.getString("username");
- log.debug("[handleEvent][收到事件: {} - {}]", event, username);
-
- // 2. 根据事件类型进行分发处理
- switch (event) {
- case EVENT_CLIENT_CONNECTED:
- handleClientConnected(body);
- break;
- case EVENT_CLIENT_DISCONNECTED:
- handleClientDisconnected(body);
- break;
- default:
- break;
- }
-
- // EMQX Webhook 只需要 200 状态码,无需响应体
- context.response().setStatusCode(SUCCESS_STATUS_CODE).end();
- } catch (Exception e) {
- log.error("[handleEvent][事件处理失败][body={}]", body != null ? body.encode() : "null", e);
- // 即使处理失败,也返回 200 避免EMQX重试
- context.response().setStatusCode(SUCCESS_STATUS_CODE).end();
- }
- }
-
- /**
- * 处理客户端连接事件
- */
- private void handleClientConnected(JsonObject body) {
- String username = body.getString("username");
- log.info("[handleClientConnected][设备上线: {}]", username);
- handleDeviceStateChange(username, true);
- }
-
- /**
- * 处理客户端断开连接事件
- */
- private void handleClientDisconnected(JsonObject body) {
- String username = body.getString("username");
- String reason = body.getString("reason");
- log.info("[handleClientDisconnected][设备下线: {} ({})]", username, reason);
- handleDeviceStateChange(username, false);
- }
-
- /**
- * 解析请求体
- *
- * @param context 路由上下文
- * @return 请求体JSON对象,解析失败时返回null
- */
- private JsonObject parseRequestBody(RoutingContext context) {
- String rawBody = null;
- try {
- rawBody = context.body().asString();
- JsonObject body = context.body().asJsonObject();
- if (body == null) {
- log.info("[parseRequestBody][请求体为空][rawBody={}]", rawBody);
- sendAuthResponse(context, RESULT_IGNORE);
- return null;
- }
- return body;
- } catch (Exception e) {
- log.error("[parseRequestBody][解析请求体失败][rawBody={}]", rawBody, e);
- sendAuthResponse(context, RESULT_IGNORE);
- return null;
- }
- }
-
- /**
- * 执行设备认证
- *
- * @param clientId 客户端ID
- * @param username 用户名
- * @param password 密码
- * @return 认证是否成功
- */
- private boolean performDeviceAuth(String clientId, String username, String password) {
- try {
- CommonResult