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 51af9bd3ce..257ff96ad0 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 @@ -6,6 +6,10 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscr 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 cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; @@ -49,7 +53,7 @@ public class IotGatewayConfiguration { @Configuration @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.emqx", name = "enabled", havingValue = "true") @Slf4j - public static class MqttProtocolConfiguration { + public static class EmqxProtocolConfiguration { @Bean(destroyMethod = "close") public Vertx emqxVertx() { @@ -110,4 +114,42 @@ public class IotGatewayConfiguration { } + /** + * IoT 网关 MQTT 协议配置类 + */ + @Configuration + @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.mqtt", name = "enabled", havingValue = "true") + @Slf4j + public static class MqttProtocolConfiguration { + + @Bean(destroyMethod = "close") + public Vertx mqttVertx() { + return Vertx.vertx(); + } + + @Bean + public IotMqttUpstreamProtocol iotMqttUpstreamProtocol(IotGatewayProperties gatewayProperties, + IotDeviceService deviceService, + IotDeviceMessageService messageService, + IotMqttConnectionManager connectionManager, + Vertx mqttVertx) { + return new IotMqttUpstreamProtocol(gatewayProperties.getProtocol().getMqtt(), + deviceService, messageService, connectionManager, mqttVertx); + } + + @Bean + public IotMqttDownstreamHandler iotMqttDownstreamHandler(IotDeviceMessageService messageService, + IotMqttConnectionManager connectionManager) { + return new IotMqttDownstreamHandler(messageService, connectionManager); + } + + @Bean + public IotMqttDownstreamSubscriber iotMqttDownstreamSubscriber(IotMqttUpstreamProtocol mqttUpstreamProtocol, + IotMqttDownstreamHandler downstreamHandler, + IotMessageBus messageBus) { + return new IotMqttDownstreamSubscriber(mqttUpstreamProtocol, downstreamHandler, messageBus); + } + + } + } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java index e4886df07a..7684972d29 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java @@ -83,6 +83,11 @@ public class IotGatewayProperties { */ private TcpProperties tcp; + /** + * MQTT 组件配置 + */ + private MqttProperties mqtt; + } @Data @@ -325,4 +330,83 @@ public class IotGatewayProperties { } + @Data + public static class MqttProperties { + + /** + * 是否开启 + */ + @NotNull(message = "是否开启不能为空") + private Boolean enabled; + + /** + * 服务器端口 + */ + private Integer port = 1883; + + /** + * 最大消息大小(字节) + */ + private Integer maxMessageSize = 8192; + + /** + * 连接超时时间(秒) + */ + private Integer connectTimeoutSeconds = 60; + + /** + * 保持连接超时时间(秒) + */ + private Integer keepAliveTimeoutSeconds = 300; + + /** + * 是否启用 SSL + */ + private Boolean sslEnabled = false; + + /** + * SSL 配置 + */ + private SslOptions sslOptions = new SslOptions(); + + /** + * SSL 配置选项 + */ + @Data + public static class SslOptions { + + /** + * 密钥证书选项 + */ + private io.vertx.core.net.KeyCertOptions keyCertOptions; + + /** + * 信任选项 + */ + private io.vertx.core.net.TrustOptions trustOptions; + + /** + * SSL 证书路径 + */ + private String certPath; + + /** + * SSL 私钥路径 + */ + private String keyPath; + + /** + * 信任存储路径 + */ + private String trustStorePath; + + /** + * 信任存储密码 + */ + private String trustStorePassword; + + } + + } + } 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 new file mode 100644 index 0000000000..3b62368fd9 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttDownstreamSubscriber.java @@ -0,0 +1,79 @@ +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
+ * 统一管理 MQTT 连接的认证状态、设备会话和消息发送功能:
+ * 1. 管理 MQTT 连接的认证状态
+ * 2. 管理设备会话和在线状态
+ * 3. 管理消息发送到设备
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+@Component
+public class IotMqttConnectionManager {
+
+ /**
+ * 连接信息映射:MqttEndpoint -> 连接信息
+ */
+ private final Map
+ * 提供基于 Vert.x MQTT Server 的 IoT 设备连接和消息处理功能
+ */
+package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttDownstreamHandler.java
new file mode 100644
index 0000000000..6714c64115
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttDownstreamHandler.java
@@ -0,0 +1,133 @@
+package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router;
+
+import cn.hutool.core.util.StrUtil;
+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.manager.IotMqttConnectionManager;
+import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
+import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
+import io.netty.handler.codec.mqtt.MqttQoS;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * IoT 网关 MQTT 协议:下行消息处理器
+ *
+ * 专门处理下行消息的业务逻辑,包括:
+ * 1. 消息编码
+ * 2. 主题构建
+ * 3. 消息发送
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public class IotMqttDownstreamHandler {
+
+ private final IotDeviceMessageService deviceMessageService;
+
+ private final IotMqttConnectionManager connectionManager;
+
+ public IotMqttDownstreamHandler(IotDeviceMessageService deviceMessageService,
+ IotMqttConnectionManager connectionManager) {
+ this.deviceMessageService = deviceMessageService;
+ this.connectionManager = connectionManager;
+ }
+
+ /**
+ * 处理下行消息
+ *
+ * @param message 设备消息
+ * @return 是否处理成功
+ */
+ public boolean handleDownstreamMessage(IotDeviceMessage message) {
+ try {
+ // 1. 基础校验
+ if (message == null || message.getDeviceId() == null) {
+ log.warn("[handleDownstreamMessage][消息或设备 ID 为空,忽略处理]");
+ return false;
+ }
+
+ // 2. 检查设备是否在线
+ if (connectionManager.isDeviceOffline(message.getDeviceId())) {
+ log.warn("[handleDownstreamMessage][设备离线,无法发送消息,设备 ID:{}]", message.getDeviceId());
+ return false;
+ }
+
+ // 3. 获取连接信息
+ IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfoByDeviceId(message.getDeviceId());
+ if (connectionInfo == null) {
+ log.warn("[handleDownstreamMessage][连接信息不存在,设备 ID:{}]", message.getDeviceId());
+ return false;
+ }
+
+ // 4. 编码消息
+ byte[] payload = deviceMessageService.encodeDeviceMessage(message, connectionInfo.getProductKey(),
+ connectionInfo.getDeviceName());
+ if (payload == null || payload.length == 0) {
+ log.warn("[handleDownstreamMessage][消息编码失败,设备 ID:{}]", message.getDeviceId());
+ return false;
+ }
+
+ // 5. 发送消息到设备
+ return sendMessageToDevice(message, connectionInfo, payload);
+
+ } catch (Exception e) {
+ if (message != null) {
+ log.error("[handleDownstreamMessage][处理下行消息异常,设备 ID:{},错误:{}]",
+ message.getDeviceId(), e.getMessage(), e);
+ }
+ return false;
+ }
+ }
+
+ /**
+ * 发送消息到设备
+ *
+ * @param message 设备消息
+ * @param connectionInfo 连接信息
+ * @param payload 消息负载
+ * @return 是否发送成功
+ */
+ private boolean sendMessageToDevice(IotDeviceMessage message,
+ IotMqttConnectionManager.ConnectionInfo connectionInfo,
+ byte[] payload) {
+ // 1. 构建主题
+ String topic = buildDownstreamTopic(message, connectionInfo);
+ if (StrUtil.isBlank(topic)) {
+ log.warn("[sendMessageToDevice][主题构建失败,设备 ID:{},方法:{}]",
+ message.getDeviceId(), message.getMethod());
+ return false;
+ }
+
+ // 2. 发送消息
+ boolean success = connectionManager.sendToDevice(message.getDeviceId(), topic, payload, MqttQoS.AT_LEAST_ONCE.value(), false);
+ if (success) {
+ log.debug("[sendMessageToDevice][消息发送成功,设备 ID:{},主题:{},方法:{}]",
+ message.getDeviceId(), topic, message.getMethod());
+ } else {
+ log.warn("[sendMessageToDevice][消息发送失败,设备 ID:{},主题:{},方法:{}]",
+ message.getDeviceId(), topic, message.getMethod());
+ }
+ return success;
+ }
+
+ /**
+ * 构建下行消息主题
+ *
+ * @param message 设备消息
+ * @param connectionInfo 连接信息
+ * @return 主题
+ */
+ private String buildDownstreamTopic(IotDeviceMessage message,
+ IotMqttConnectionManager.ConnectionInfo connectionInfo) {
+ String method = message.getMethod();
+ if (StrUtil.isBlank(method)) {
+ return null;
+ }
+
+ // 使用工具类构建主题,支持回复消息处理
+ boolean isReply = IotDeviceMessageUtils.isReplyMessage(message);
+ return IotMqttTopicUtils.buildTopicByMethod(method, connectionInfo.getProductKey(),
+ connectionInfo.getDeviceName(), isReply);
+ }
+
+}
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttUpstreamHandler.java
new file mode 100644
index 0000000000..5ce691293c
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttUpstreamHandler.java
@@ -0,0 +1,298 @@
+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.biz.dto.IotDeviceGetReqDTO;
+import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
+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.protocol.mqtt.manager.IotMqttConnectionManager;
+import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
+import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
+import io.netty.handler.codec.mqtt.MqttConnectReturnCode;
+import io.netty.handler.codec.mqtt.MqttQoS;
+import io.vertx.mqtt.MqttEndpoint;
+import io.vertx.mqtt.MqttTopicSubscription;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.List;
+
+/**
+ * MQTT 上行消息处理器
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public class IotMqttUpstreamHandler {
+
+ private final IotDeviceMessageService deviceMessageService;
+
+ private final IotMqttConnectionManager connectionManager;
+
+ private final IotDeviceCommonApi deviceApi;
+
+ private final String serverId;
+
+ public IotMqttUpstreamHandler(IotMqttUpstreamProtocol protocol,
+ IotDeviceMessageService deviceMessageService,
+ IotDeviceService deviceService,
+ IotMqttConnectionManager connectionManager) {
+ this.deviceMessageService = deviceMessageService;
+ this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
+ this.connectionManager = connectionManager;
+ this.serverId = protocol.getServerId();
+ }
+
+ /**
+ * 处理 MQTT 连接
+ *
+ * @param endpoint MQTT 连接端点
+ */
+ public void handle(MqttEndpoint endpoint) {
+ String clientId = endpoint.clientIdentifier();
+ String username = endpoint.auth() != null ? endpoint.auth().getUsername() : null;
+ String password = endpoint.auth() != null ? endpoint.auth().getPassword() : null;
+
+ log.debug("[handle][设备连接请求,客户端 ID: {},用户名: {},地址: {}]",
+ clientId, username, getEndpointAddress(endpoint));
+
+ // 1. 先进行认证
+ if (!authenticateDevice(clientId, username, password, endpoint)) {
+ log.warn("[handle][设备认证失败,拒绝连接,客户端 ID: {},用户名: {}]", clientId, username);
+ endpoint.reject(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD);
+ return;
+ }
+
+ log.info("[handle][设备认证成功,建立连接,客户端 ID: {},用户名: {}]", clientId, username);
+
+ // 设置异常和关闭处理器
+ endpoint.exceptionHandler(ex -> {
+ log.warn("[handle][连接异常,客户端 ID: {},地址: {}]", clientId, getEndpointAddress(endpoint));
+ cleanupConnection(endpoint);
+ });
+ endpoint.closeHandler(v -> {
+ log.debug("[handle][连接关闭,客户端 ID: {},地址: {}]", clientId, getEndpointAddress(endpoint));
+ cleanupConnection(endpoint);
+ });
+
+ // 设置消息处理器
+ endpoint.publishHandler(message -> {
+ try {
+ processMessage(clientId, message.topicName(), message.payload().getBytes(), endpoint);
+ } catch (Exception e) {
+ log.error("[handle][消息解码失败,断开连接,客户端 ID: {},地址: {},错误: {}]",
+ clientId, getEndpointAddress(endpoint), e.getMessage());
+ cleanupConnection(endpoint);
+ endpoint.close();
+ }
+ });
+
+ // 设置订阅处理器
+ endpoint.subscribeHandler(subscribe -> {
+ log.debug("[handle][设备订阅,客户端 ID: {},主题: {}]", clientId, subscribe.topicSubscriptions());
+ // 提取 QoS 列表
+ List