Commit e41d66ff9221b1dc1a96d2f5e5f5147bb057e640

Authored by YunaiV
1 parent d357105d

feat(iot):【协议改造】emqx 初步改造(20%):调整包目录

Showing 9 changed files with 462 additions and 13 deletions
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java
@@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.iot.gateway.config; @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.iot.gateway.config;
3 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; 3 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
4 import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolManager; 4 import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolManager;
5 import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxAuthEventProtocol; 5 import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxAuthEventProtocol;
6 -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscriber; 6 +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.downstream.IotEmqxDownstreamSubscriber;
7 import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; 7 import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol;
8 import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; 8 import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager;
9 import io.vertx.core.Vertx; 9 import io.vertx.core.Vertx;
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxAuthEventProtocol.java
@@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.emqx; @@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.emqx;
2 2
3 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; 3 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
4 import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; 4 import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
5 -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router.IotEmqxAuthEventHandler; 5 +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream.IotEmqxAuthEventHandler;
6 import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; 6 import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
7 import io.vertx.core.Vertx; 7 import io.vertx.core.Vertx;
8 import io.vertx.core.http.HttpServer; 8 import io.vertx.core.http.HttpServer;
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxUpstreamProtocol.java
@@ -6,7 +6,7 @@ import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; @@ -6,7 +6,7 @@ import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
6 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; 6 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
7 import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; 7 import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
8 import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; 8 import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
9 -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router.IotEmqxUpstreamHandler; 9 +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream.IotEmqxUpstreamHandler;
10 import io.netty.handler.codec.mqtt.MqttQoS; 10 import io.netty.handler.codec.mqtt.MqttQoS;
11 import io.vertx.core.Vertx; 11 import io.vertx.core.Vertx;
12 import io.vertx.core.buffer.Buffer; 12 import io.vertx.core.buffer.Buffer;
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxDownstreamHandler.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamHandler.java
1 -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router; 1 +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.downstream;
2 2
3 import cn.hutool.core.util.StrUtil; 3 import cn.hutool.core.util.StrUtil;
4 import cn.hutool.extra.spring.SpringUtil; 4 import cn.hutool.extra.spring.SpringUtil;
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxDownstreamSubscriber.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamSubscriber.java
1 -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx; 1 +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.downstream;
2 2
3 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; 3 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
5 -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;  
6 import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; 5 import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber;
7 -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router.IotEmqxDownstreamHandler; 6 +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol;
  7 +import jakarta.annotation.PostConstruct;
  8 +import jakarta.annotation.PreDestroy;
8 import lombok.extern.slf4j.Slf4j; 9 import lombok.extern.slf4j.Slf4j;
9 10
10 /** 11 /**
@@ -22,6 +23,17 @@ public class IotEmqxDownstreamSubscriber extends IotProtocolDownstreamSubscriber @@ -22,6 +23,17 @@ public class IotEmqxDownstreamSubscriber extends IotProtocolDownstreamSubscriber
22 this.downstreamHandler = new IotEmqxDownstreamHandler(protocol); 23 this.downstreamHandler = new IotEmqxDownstreamHandler(protocol);
23 } 24 }
24 25
  26 + @PostConstruct
  27 + public void startSubscriber() {
  28 + // EMQX 模式下,由 Spring 管理 Bean 生命周期;需要显式启动订阅者,才能从消息总线消费下行消息并发布到 Broker
  29 + start();
  30 + }
  31 +
  32 + @PreDestroy
  33 + public void stopSubscriber() {
  34 + stop();
  35 + }
  36 +
25 @Override 37 @Override
26 protected void handleMessage(IotDeviceMessage message) { 38 protected void handleMessage(IotDeviceMessage message) {
27 downstreamHandler.handle(message); 39 downstreamHandler.handle(message);
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxAuthEventHandler.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxAuthEventHandler.java
1 -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router; 1 +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream;
2 2
3 import cn.hutool.core.util.BooleanUtil; 3 import cn.hutool.core.util.BooleanUtil;
4 import cn.hutool.core.util.StrUtil; 4 import cn.hutool.core.util.StrUtil;
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxUpstreamHandler.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxUpstreamHandler.java
1 -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router; 1 +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream;
2 2
3 import cn.hutool.core.util.StrUtil; 3 import cn.hutool.core.util.StrUtil;
4 import cn.hutool.extra.spring.SpringUtil; 4 import cn.hutool.extra.spring.SpringUtil;
yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml
@@ -92,7 +92,7 @@ yudao: @@ -92,7 +92,7 @@ yudao:
92 # 针对引入的 WebSocket 组件的配置 92 # 针对引入的 WebSocket 组件的配置
93 # ==================================== 93 # ====================================
94 - id: websocket-json 94 - id: websocket-json
95 - enabled: true 95 + enabled: false
96 protocol: websocket 96 protocol: websocket
97 port: 8094 97 port: 8094
98 serialize: json 98 serialize: json
@@ -117,9 +117,9 @@ yudao: @@ -117,9 +117,9 @@ yudao:
117 # 针对引入的 MQTT 组件的配置 117 # 针对引入的 MQTT 组件的配置
118 # ==================================== 118 # ====================================
119 - id: mqtt-json 119 - id: mqtt-json
120 - enabled: true 120 + enabled: false
121 protocol: mqtt 121 protocol: mqtt
122 - port: 1883 122 + port: 1884
123 serialize: json 123 serialize: json
124 mqtt: 124 mqtt:
125 max-message-size: 8192 # 最大消息大小(字节) 125 max-message-size: 8192 # 最大消息大小(字节)
@@ -132,7 +132,7 @@ yudao: @@ -132,7 +132,7 @@ yudao:
132 # 针对引入的 EMQX 组件的配置 132 # 针对引入的 EMQX 组件的配置
133 # ==================================== 133 # ====================================
134 emqx: 134 emqx:
135 - enabled: false 135 + enabled: true
136 http-port: 8090 # MQTT HTTP 服务端口 136 http-port: 8090 # MQTT HTTP 服务端口
137 mqtt-host: 127.0.0.1 # MQTT Broker 地址 137 mqtt-host: 127.0.0.1 # MQTT Broker 地址
138 mqtt-port: 1883 # MQTT Broker 端口 138 mqtt-port: 1883 # MQTT Broker 端口
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotDirectDeviceEmqxProtocolIntegrationTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx;
  2 +
  3 +import cn.hutool.core.map.MapUtil;
  4 +import cn.hutool.http.HttpResponse;
  5 +import cn.hutool.http.HttpUtil;
  6 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  7 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
  8 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
  9 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  10 +import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
  11 +import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
  12 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  13 +import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
  14 +import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec;
  15 +import io.netty.handler.codec.mqtt.MqttQoS;
  16 +import io.vertx.core.Vertx;
  17 +import io.vertx.core.buffer.Buffer;
  18 +import io.vertx.mqtt.MqttClient;
  19 +import io.vertx.mqtt.MqttClientOptions;
  20 +import lombok.extern.slf4j.Slf4j;
  21 +import org.junit.jupiter.api.AfterAll;
  22 +import org.junit.jupiter.api.BeforeAll;
  23 +import org.junit.jupiter.api.Disabled;
  24 +import org.junit.jupiter.api.Test;
  25 +
  26 +import java.util.concurrent.TimeUnit;
  27 +
  28 +/**
  29 + * IoT 直连设备 EMQX 协议集成测试(手动测试)
  30 + *
  31 + * <p>测试场景:直连设备(IotProductDeviceTypeEnum 的 DIRECT 类型)通过 EMQX Broker 连接平台
  32 + *
  33 + * <p>EMQX 协议架构:
  34 + * <pre>
  35 + * +--------+ MQTT +-------------+ HTTP Hook +---------+
  36 + * | 设备 | ----------------> | EMQX Broker | --------------------> | 网关 |
  37 + * +--------+ +-------------+ +---------+
  38 + * | | |
  39 + * | 1. 连接认证 | 2. 调用 /mqtt/auth |
  40 + * | 3. 发布消息 | 4. 调用 /mqtt/event (上线/下线) |
  41 + * | | 5. 网关订阅 EMQX 消息 |
  42 + * | | |
  43 + * </pre>
  44 + *
  45 + * <p>测试分类:
  46 + * <ul>
  47 + * <li>第一部分:模拟设备连接 EMQX Broker,发送 MQTT 消息</li>
  48 + * <li>第二部分:模拟 EMQX Server 调用网关 HTTP Hook 接口(认证、事件)</li>
  49 + * </ul>
  50 + *
  51 + * <p>使用步骤:
  52 + * <ol>
  53 + * <li>启动 EMQX Broker(MQTT 端口 1883)</li>
  54 + * <li>启动 yudao-module-iot-gateway 服务(HTTP 端口 18083)</li>
  55 + * <li>配置 EMQX HTTP 认证插件指向网关的 /mqtt/auth 接口</li>
  56 + * <li>配置 EMQX Webhook 插件指向网关的 /mqtt/event 接口</li>
  57 + * <li>运行测试方法</li>
  58 + * </ol>
  59 + *
  60 + * @author 芋道源码
  61 + */
  62 +@Slf4j
  63 +@Disabled
  64 +@SuppressWarnings("HttpUrlsUsage")
  65 +public class IotDirectDeviceEmqxProtocolIntegrationTest {
  66 +
  67 + private static final String SERVER_HOST = "127.0.0.1";
  68 + /**
  69 + * EMQX 认证事件 HTTP 接口端口(网关提供给 EMQX Server 调用)
  70 + */
  71 + private static final int HTTP_PORT = 18083;
  72 + /**
  73 + * EMQX Broker MQTT 端口(设备连接 EMQX)
  74 + */
  75 + private static final int MQTT_PORT = 1883;
  76 + private static final int TIMEOUT_SECONDS = 10;
  77 +
  78 + private static Vertx vertx;
  79 +
  80 + // ===================== 编解码器(EMQX 使用 Alink 协议) =====================
  81 +
  82 + private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec();
  83 +
  84 + // ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询) =====================
  85 +
  86 + private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT";
  87 + private static final String DEVICE_NAME = "small";
  88 + private static final String DEVICE_SECRET = "0baa4c2ecc104ae1a26b4070c218bdf3";
  89 +
  90 + @BeforeAll
  91 + public static void setUp() {
  92 + vertx = Vertx.vertx();
  93 + }
  94 +
  95 + @AfterAll
  96 + public static void tearDown() {
  97 + if (vertx != null) {
  98 + vertx.close();
  99 + }
  100 + }
  101 +
  102 + // ==================================================================================
  103 + // 第一部分:模拟设备连接 EMQX Broker
  104 + // ==================================================================================
  105 +
  106 + /**
  107 + * 设备连接测试:模拟设备连接 EMQX Broker
  108 + * <p>
  109 + * 当设备连接 EMQX 时,EMQX 会自动调用网关的 /mqtt/auth 接口进行认证
  110 + */
  111 + @Test
  112 + public void testDeviceConnect() throws Exception {
  113 + // 1. 构建认证信息
  114 + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
  115 + log.info("[testDeviceConnect][认证信息: clientId={}, username={}, password={}]",
  116 + authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword());
  117 +
  118 + // 2. 创建客户端并连接 EMQX Broker
  119 + MqttClient client = createClient(authInfo);
  120 + try {
  121 + client.connect(MQTT_PORT, SERVER_HOST)
  122 + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  123 + log.info("[testDeviceConnect][连接 EMQX Broker 成功,客户端 ID: {}]", client.clientId());
  124 + log.info("[testDeviceConnect][EMQX 会自动调用网关的 /mqtt/auth 接口进行认证]");
  125 + log.info("[testDeviceConnect][EMQX 会自动调用网关的 /mqtt/event 接口通知设备上线]");
  126 + } finally {
  127 + disconnect(client);
  128 + log.info("[testDeviceConnect][EMQX 会自动调用网关的 /mqtt/event 接口通知设备下线]");
  129 + }
  130 + }
  131 +
  132 + /**
  133 + * 属性上报测试:设备通过 EMQX Broker 发布属性消息
  134 + * <p>
  135 + * 消息流程:设备 -> EMQX Broker -> 网关(订阅 EMQX 消息)
  136 + */
  137 + @Test
  138 + public void testPropertyPost() throws Exception {
  139 + // 1. 连接 EMQX Broker
  140 + MqttClient client = connectToEmqx();
  141 + log.info("[testPropertyPost][连接 EMQX Broker 成功]");
  142 +
  143 + try {
  144 + // 2.1 构建属性上报消息
  145 + IotDeviceMessage request = IotDeviceMessage.requestOf(
  146 + IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(),
  147 + IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
  148 + .put("width", 1)
  149 + .put("height", "2")
  150 + .build()));
  151 +
  152 + // 2.2 发布消息到 EMQX Broker
  153 + String topic = String.format("/sys/%s/%s/thing/property/post", PRODUCT_KEY, DEVICE_NAME);
  154 + publish(client, topic, request);
  155 + log.info("[testPropertyPost][属性上报消息已发送到 EMQX Broker]");
  156 + log.info("[testPropertyPost][网关会通过订阅 EMQX 接收此消息]");
  157 +
  158 + // 2.3 等待消息处理
  159 + Thread.sleep(2000);
  160 + log.info("[testPropertyPost][请检查网关日志确认消息是否被正确处理]");
  161 + } finally {
  162 + disconnect(client);
  163 + }
  164 + }
  165 +
  166 + /**
  167 + * 事件上报测试:设备通过 EMQX Broker 发布事件消息
  168 + * <p>
  169 + * 消息流程:设备 -> EMQX Broker -> 网关(订阅 EMQX 消息)
  170 + */
  171 + @Test
  172 + public void testEventPost() throws Exception {
  173 + // 1. 连接 EMQX Broker
  174 + MqttClient client = connectToEmqx();
  175 + log.info("[testEventPost][连接 EMQX Broker 成功]");
  176 +
  177 + try {
  178 + // 2.1 构建事件上报消息
  179 + IotDeviceMessage request = IotDeviceMessage.requestOf(
  180 + IotDeviceMessageMethodEnum.EVENT_POST.getMethod(),
  181 + IotDeviceEventPostReqDTO.of(
  182 + "eat",
  183 + MapUtil.<String, Object>builder().put("rice", 3).build(),
  184 + System.currentTimeMillis()));
  185 +
  186 + // 2.2 发布消息到 EMQX Broker
  187 + String topic = String.format("/sys/%s/%s/thing/event/post", PRODUCT_KEY, DEVICE_NAME);
  188 + publish(client, topic, request);
  189 + log.info("[testEventPost][事件上报消息已发送到 EMQX Broker]");
  190 + log.info("[testEventPost][网关会通过订阅 EMQX 接收此消息]");
  191 +
  192 + // 2.3 等待消息处理
  193 + Thread.sleep(2000);
  194 + log.info("[testEventPost][请检查网关日志确认消息是否被正确处理]");
  195 + } finally {
  196 + disconnect(client);
  197 + }
  198 + }
  199 +
  200 + /**
  201 + * 订阅下行消息测试:设备订阅服务端下发的消息
  202 + * <p>
  203 + * 消息流程:网关 -> EMQX Broker -> 设备
  204 + */
  205 + @Test
  206 + public void testSubscribe() throws Exception {
  207 + // 1. 连接 EMQX Broker
  208 + MqttClient client = connectToEmqx();
  209 + log.info("[testSubscribe][连接 EMQX Broker 成功]");
  210 +
  211 + try {
  212 + // 2. 设置消息处理器
  213 + client.publishHandler(message -> log.info("[testSubscribe][收到下行消息: topic={}, payload={}]",
  214 + message.topicName(), message.payload().toString()));
  215 +
  216 + // 3. 订阅下行主题
  217 + String topic = String.format("/sys/%s/%s/thing/service/#", PRODUCT_KEY, DEVICE_NAME);
  218 + log.info("[testSubscribe][订阅主题: {}]", topic);
  219 + subscribe(client, topic);
  220 + log.info("[testSubscribe][订阅成功,等待下行消息... (30秒后自动断开)]");
  221 + log.info("[testSubscribe][网关下发的消息会通过 EMQX Broker 转发给设备]");
  222 +
  223 + // 4. 保持连接 30 秒等待消息
  224 + Thread.sleep(30000);
  225 + } finally {
  226 + disconnect(client);
  227 + }
  228 + }
  229 +
  230 + // ==================================================================================
  231 + // 第二部分:模拟 EMQX Server 调用网关 HTTP Hook 接口
  232 + // 说明:这些接口是 EMQX Server 自动调用的,这里只是用于单独测试接口功能
  233 + // ==================================================================================
  234 +
  235 + /**
  236 + * 认证接口测试:模拟 EMQX Server 调用 /mqtt/auth 接口
  237 + * <p>
  238 + * 注意:正常情况下此接口由 EMQX HTTP 认证插件自动调用,这里只是测试接口本身
  239 + */
  240 + @Test
  241 + public void testEmqxAuthHook() {
  242 + // 1.1 构建请求
  243 + String url = String.format("http://%s:%d/mqtt/auth", SERVER_HOST, HTTP_PORT);
  244 + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
  245 + // 1.2 EMQX 认证请求格式
  246 + String payload = JsonUtils.toJsonString(MapUtil.builder()
  247 + .put("clientid", authInfo.getClientId())
  248 + .put("username", authInfo.getUsername())
  249 + .put("password", authInfo.getPassword())
  250 + .build());
  251 + // 1.3 输出请求
  252 + log.info("[testEmqxAuthHook][模拟 EMQX Server 调用认证接口]");
  253 + log.info("[testEmqxAuthHook][请求 URL: {}]", url);
  254 + log.info("[testEmqxAuthHook][请求体: {}]", payload);
  255 +
  256 + // 2.1 发送请求
  257 + try (HttpResponse httpResponse = HttpUtil.createPost(url)
  258 + .header("Content-Type", "application/json")
  259 + .body(payload)
  260 + .execute()) {
  261 + // 2.2 输出结果
  262 + log.info("[testEmqxAuthHook][响应状态码: {}]", httpResponse.getStatus());
  263 + log.info("[testEmqxAuthHook][响应体: {}]", httpResponse.body());
  264 + log.info("[testEmqxAuthHook][认证结果: result=allow 表示认证成功, result=deny 表示认证失败]");
  265 + }
  266 + }
  267 +
  268 + /**
  269 + * 认证失败测试:模拟 EMQX Server 调用 /mqtt/auth 接口(错误密码)
  270 + */
  271 + @Test
  272 + public void testEmqxAuthHookFailed() {
  273 + // 1.1 构建请求
  274 + String url = String.format("http://%s:%d/mqtt/auth", SERVER_HOST, HTTP_PORT);
  275 + // 1.2 使用错误的密码
  276 + String payload = JsonUtils.toJsonString(MapUtil.builder()
  277 + .put("clientid", PRODUCT_KEY + "." + DEVICE_NAME)
  278 + .put("username", DEVICE_NAME + "&" + PRODUCT_KEY)
  279 + .put("password", "wrong_password")
  280 + .build());
  281 + // 1.3 输出请求
  282 + log.info("[testEmqxAuthHookFailed][模拟 EMQX Server 调用认证接口(错误密码)]");
  283 + log.info("[testEmqxAuthHookFailed][请求 URL: {}]", url);
  284 + log.info("[testEmqxAuthHookFailed][请求体: {}]", payload);
  285 +
  286 + // 2.1 发送请求
  287 + try (HttpResponse httpResponse = HttpUtil.createPost(url)
  288 + .header("Content-Type", "application/json")
  289 + .body(payload)
  290 + .execute()) {
  291 + // 2.2 输出结果
  292 + log.info("[testEmqxAuthHookFailed][响应状态码: {}]", httpResponse.getStatus());
  293 + log.info("[testEmqxAuthHookFailed][响应体: {}]", httpResponse.body());
  294 + log.info("[testEmqxAuthHookFailed][预期结果: result=deny]");
  295 + }
  296 + }
  297 +
  298 + /**
  299 + * 设备上线事件测试:模拟 EMQX Server Webhook 调用 /mqtt/event 接口
  300 + * <p>
  301 + * 注意:正常情况下此接口由 EMQX Webhook 插件自动调用,这里只是测试接口本身
  302 + */
  303 + @Test
  304 + public void testEmqxClientConnectedHook() {
  305 + // 1.1 构建请求
  306 + String url = String.format("http://%s:%d/mqtt/event", SERVER_HOST, HTTP_PORT);
  307 + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
  308 + // 1.2 EMQX Webhook client.connected 事件格式
  309 + String payload = JsonUtils.toJsonString(MapUtil.builder()
  310 + .put("event", "client.connected")
  311 + .put("clientid", authInfo.getClientId())
  312 + .put("username", authInfo.getUsername())
  313 + .put("peername", "127.0.0.1:12345")
  314 + .put("connected_at", System.currentTimeMillis())
  315 + .build());
  316 + // 1.3 输出请求
  317 + log.info("[testEmqxClientConnectedHook][模拟 EMQX Server Webhook 调用设备上线事件]");
  318 + log.info("[testEmqxClientConnectedHook][请求 URL: {}]", url);
  319 + log.info("[testEmqxClientConnectedHook][请求体: {}]", payload);
  320 +
  321 + // 2.1 发送请求
  322 + try (HttpResponse httpResponse = HttpUtil.createPost(url)
  323 + .header("Content-Type", "application/json")
  324 + .body(payload)
  325 + .execute()) {
  326 + // 2.2 输出结果
  327 + log.info("[testEmqxClientConnectedHook][响应状态码: {}]", httpResponse.getStatus());
  328 + log.info("[testEmqxClientConnectedHook][响应体: {}]", httpResponse.body());
  329 + log.info("[testEmqxClientConnectedHook][预期结果: 状态码 200,设备状态更新为在线]");
  330 + }
  331 + }
  332 +
  333 + /**
  334 + * 设备下线事件测试:模拟 EMQX Server Webhook 调用 /mqtt/event 接口
  335 + * <p>
  336 + * 注意:正常情况下此接口由 EMQX Webhook 插件自动调用,这里只是测试接口本身
  337 + */
  338 + @Test
  339 + public void testEmqxClientDisconnectedHook() {
  340 + // 1.1 构建请求
  341 + String url = String.format("http://%s:%d/mqtt/event", SERVER_HOST, HTTP_PORT);
  342 + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
  343 + // 1.2 EMQX Webhook client.disconnected 事件格式
  344 + String payload = JsonUtils.toJsonString(MapUtil.builder()
  345 + .put("event", "client.disconnected")
  346 + .put("clientid", authInfo.getClientId())
  347 + .put("username", authInfo.getUsername())
  348 + .put("reason", "normal")
  349 + .put("disconnected_at", System.currentTimeMillis())
  350 + .build());
  351 + // 1.3 输出请求
  352 + log.info("[testEmqxClientDisconnectedHook][模拟 EMQX Server Webhook 调用设备下线事件]");
  353 + log.info("[testEmqxClientDisconnectedHook][请求 URL: {}]", url);
  354 + log.info("[testEmqxClientDisconnectedHook][请求体: {}]", payload);
  355 +
  356 + // 2.1 发送请求
  357 + try (HttpResponse httpResponse = HttpUtil.createPost(url)
  358 + .header("Content-Type", "application/json")
  359 + .body(payload)
  360 + .execute()) {
  361 + // 2.2 输出结果
  362 + log.info("[testEmqxClientDisconnectedHook][响应状态码: {}]", httpResponse.getStatus());
  363 + log.info("[testEmqxClientDisconnectedHook][响应体: {}]", httpResponse.body());
  364 + log.info("[testEmqxClientDisconnectedHook][预期结果: 状态码 200,设备状态更新为离线]");
  365 + }
  366 + }
  367 +
  368 + // ===================== 辅助方法 =====================
  369 +
  370 + /**
  371 + * 创建 MQTT 客户端
  372 + *
  373 + * @param authInfo 认证信息
  374 + * @return MQTT 客户端
  375 + */
  376 + private MqttClient createClient(IotDeviceAuthReqDTO authInfo) {
  377 + MqttClientOptions options = new MqttClientOptions()
  378 + .setClientId(authInfo.getClientId())
  379 + .setUsername(authInfo.getUsername())
  380 + .setPassword(authInfo.getPassword())
  381 + .setCleanSession(true)
  382 + .setKeepAliveInterval(60);
  383 + return MqttClient.create(vertx, options);
  384 + }
  385 +
  386 + /**
  387 + * 连接 EMQX Broker 并认证设备
  388 + *
  389 + * @return 已认证的 MQTT 客户端
  390 + */
  391 + private MqttClient connectToEmqx() throws Exception {
  392 + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
  393 + MqttClient client = createClient(authInfo);
  394 + client.connect(MQTT_PORT, SERVER_HOST)
  395 + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  396 + return client;
  397 + }
  398 +
  399 + /**
  400 + * 订阅主题
  401 + *
  402 + * @param client MQTT 客户端
  403 + * @param topic 主题
  404 + */
  405 + private void subscribe(MqttClient client, String topic) throws Exception {
  406 + client.subscribe(topic, MqttQoS.AT_LEAST_ONCE.value())
  407 + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  408 + log.info("[subscribe][订阅主题成功: {}]", topic);
  409 + }
  410 +
  411 + /**
  412 + * 发布消息
  413 + *
  414 + * @param client MQTT 客户端
  415 + * @param topic 发布主题
  416 + * @param request 请求消息
  417 + */
  418 + private void publish(MqttClient client, String topic, IotDeviceMessage request) throws Exception {
  419 + byte[] payload = CODEC.encode(request);
  420 + log.info("[publish][发送消息: topic={}, payload={}]", topic, new String(payload));
  421 + client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false)
  422 + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  423 + log.info("[publish][消息发布成功]");
  424 + }
  425 +
  426 + /**
  427 + * 断开连接
  428 + *
  429 + * @param client MQTT 客户端
  430 + */
  431 + private void disconnect(MqttClient client) throws Exception {
  432 + client.disconnect()
  433 + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  434 + log.info("[disconnect][断开连接成功]");
  435 + }
  436 +
  437 +}