Commit 0072482af8faf113b5ed663d15eaae90b5d15dd5

Authored by YunaiV
1 parent 610ae6d5

feat(iot): 完善 WebSocket 协议实现,优化代码质量

1. 配置属性校验完善
   - CoAP 配置添加 @NotNull 校验注解,替换 TODO 注释

2. WebSocket 协议核心优化
   - ConnectionInfo 新增 codecType 字段,支持动态编解码类型
   - 上行/下行处理器根据连接的 codecType 进行消息编解码
   - 使用 StrUtil.utf8Str/utf8Bytes 替换 StandardCharsets 硬编码

3. 包注释完善
   - http/tcp package-info.java 添加规范的包级注释

4. 单元测试重构
   - 使用 WebSocketClient.connect() 替换废弃的 HttpClient.webSocket()
   - 提取公共方法,简化测试代码结构
Showing 10 changed files with 680 additions and 610 deletions
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java
... ... @@ -568,25 +568,25 @@ public class IotGatewayProperties {
568 568 /**
569 569 * 服务端口(CoAP 默认端口 5683)
570 570 */
571   - // TODO @AI:默认不为空
  571 + @NotNull(message = "服务端口不能为空")
572 572 private Integer port = 5683;
573 573  
574 574 /**
575 575 * 最大消息大小(字节)
576 576 */
577   - // TODO @AI:默认不为空
  577 + @NotNull(message = "最大消息大小不能为空")
578 578 private Integer maxMessageSize = 1024;
579 579  
580 580 /**
581 581 * ACK 超时时间(毫秒)
582 582 */
583   - // TODO @AI:默认不为空
  583 + @NotNull(message = "ACK 超时时间不能为空")
584 584 private Integer ackTimeout = 2000;
585 585  
586 586 /**
587 587 * 最大重传次数
588 588 */
589   - // TODO @AI:默认不为空
  589 + @NotNull(message = "最大重传次数不能为空")
590 590 private Integer maxRetransmit = 4;
591 591  
592 592 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/package-info.java
1   -// TODO @AI:参考 /Users/yunai/Java/ruoyi-vue-pro-jdk25/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/package-info.java 完善注释;
2   -package cn.iocoder.yudao.module.iot.gateway.protocol.http;
3 1 \ No newline at end of file
  2 +/**
  3 + * HTTP 协议实现包
  4 + * <p>
  5 + * 提供基于 Vert.x HTTP Server 的 IoT 设备连接和消息处理功能
  6 + */
  7 +package cn.iocoder.yudao.module.iot.gateway.protocol.http;
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/package-info.java
1   -// TODO @AI:参考 /Users/yunai/Java/ruoyi-vue-pro-jdk25/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/package-info.java
2   -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp;
3 1 \ No newline at end of file
  2 +/**
  3 + * TCP 协议实现包
  4 + * <p>
  5 + * 提供基于 Vert.x TCP Server 的 IoT 设备连接和消息处理功能
  6 + */
  7 +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp;
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketUpstreamProtocol.java
... ... @@ -53,6 +53,7 @@ public class IotWebSocketUpstreamProtocol {
53 53 }
54 54  
55 55 @PostConstruct
  56 + @SuppressWarnings("deprecation")
56 57 public void start() {
57 58 // 1.1 创建服务器选项
58 59 HttpServerOptions options = new HttpServerOptions()
... ... @@ -73,15 +74,14 @@ public class IotWebSocketUpstreamProtocol {
73 74 httpServer.webSocketHandler(socket -> {
74 75 // 验证路径
75 76 if (ObjUtil.notEqual(wsProperties.getPath(), socket.path())) {
76   - log.warn("[webSocketHandler][WebSocket 路径不匹配,拒绝连接,路径: {},期望: {}]", socket.path(), wsProperties.getPath());
77   - // TODO @AI:已经被废弃,看看换什么其他方法;
  77 + log.warn("[webSocketHandler][WebSocket 路径不匹配,拒绝连接,路径: {},期望: {}]",
  78 + socket.path(), wsProperties.getPath());
78 79 socket.reject();
79 80 return;
80 81 }
81   -
82 82 // 创建上行处理器
83   - IotWebSocketUpstreamHandler handler = new IotWebSocketUpstreamHandler(
84   - this, messageService, deviceService, connectionManager);
  83 + IotWebSocketUpstreamHandler handler = new IotWebSocketUpstreamHandler(this,
  84 + messageService, deviceService, connectionManager);
85 85 handler.handle(socket);
86 86 });
87 87  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/manager/IotWebSocketConnectionManager.java
... ... @@ -139,8 +139,10 @@ public class IotWebSocketConnectionManager {
139 139 * 客户端 ID
140 140 */
141 141 private String clientId;
142   -
143   - // TODO @AI:增加有个 codecType 字段;后续可以使用,参考 tcp、udp;然后下行的时候,也基于这个 codeType 去获取;
  142 + /**
  143 + * 消息编解码类型(认证后确定)
  144 + */
  145 + private String codecType;
144 146  
145 147 }
146 148  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/router/IotWebSocketDownstreamHandler.java
1 1 package cn.iocoder.yudao.module.iot.gateway.protocol.websocket.router;
2 2  
  3 +import cn.hutool.core.util.StrUtil;
3 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
4   -import cn.iocoder.yudao.module.iot.gateway.codec.websocket.IotWebSocketJsonDeviceMessageCodec;
5 5 import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager;
6 6 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
7 7 import lombok.RequiredArgsConstructor;
8 8 import lombok.extern.slf4j.Slf4j;
9 9  
10   -import java.nio.charset.StandardCharsets;
11   -
12 10 /**
13 11 * IoT 网关 WebSocket 下行消息处理器
14 12 *
... ... @@ -18,9 +16,6 @@ import java.nio.charset.StandardCharsets;
18 16 @RequiredArgsConstructor
19 17 public class IotWebSocketDownstreamHandler {
20 18  
21   - // TODO @芋艿:codeType 的处理;
22   - private static final String CODEC_TYPE = IotWebSocketJsonDeviceMessageCodec.TYPE;
23   -
24 19 private final IotDeviceMessageService deviceMessageService;
25 20  
26 21 private final IotWebSocketConnectionManager connectionManager;
... ... @@ -42,8 +37,8 @@ public class IotWebSocketDownstreamHandler {
42 37 }
43 38  
44 39 // 2. 编码消息并发送到设备
45   - byte[] bytes = deviceMessageService.encodeDeviceMessage(message, CODEC_TYPE);
46   - String jsonMessage = new String(bytes, StandardCharsets.UTF_8);
  40 + byte[] bytes = deviceMessageService.encodeDeviceMessage(message, connectionInfo.getCodecType());
  41 + String jsonMessage = StrUtil.utf8Str(bytes);
47 42 boolean success = connectionManager.sendToDevice(message.getDeviceId(), jsonMessage);
48 43 if (success) {
49 44 log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]",
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/router/IotWebSocketUpstreamHandler.java
... ... @@ -25,7 +25,6 @@ import io.vertx.core.Handler;
25 25 import io.vertx.core.http.ServerWebSocket;
26 26 import lombok.extern.slf4j.Slf4j;
27 27  
28   -import java.nio.charset.StandardCharsets;
29 28  
30 29 /**
31 30 * WebSocket 上行消息处理器
... ... @@ -35,7 +34,9 @@ import java.nio.charset.StandardCharsets;
35 34 @Slf4j
36 35 public class IotWebSocketUpstreamHandler implements Handler<ServerWebSocket> {
37 36  
38   - // TODO @芋艿:codeType 的处理;
  37 + /**
  38 + * 默认消息编解码类型
  39 + */
39 40 private static final String CODEC_TYPE = IotWebSocketJsonDeviceMessageCodec.TYPE;
40 41  
41 42 private static final String AUTH_METHOD = "auth";
... ... @@ -63,13 +64,10 @@ public class IotWebSocketUpstreamHandler implements Handler&lt;ServerWebSocket&gt; {
63 64  
64 65 @Override
65 66 public void handle(ServerWebSocket socket) {
66   - // 1. 接受 WebSocket 连接
67 67 String clientId = IdUtil.simpleUUID();
68 68 log.debug("[handle][设备连接,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress());
69   - // TODO @AI:这个方法已经废弃,看看有没其他替换的
70   - socket.accept();
71 69  
72   - // 2.1 设置异常和关闭处理器
  70 + // 1. 设置异常和关闭处理器
73 71 socket.exceptionHandler(ex -> {
74 72 log.warn("[handle][连接异常,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress());
75 73 cleanupConnection(socket);
... ... @@ -79,7 +77,7 @@ public class IotWebSocketUpstreamHandler implements Handler&lt;ServerWebSocket&gt; {
79 77 cleanupConnection(socket);
80 78 });
81 79  
82   - // 2.2 设置文本消息处理器
  80 + // 2. 设置文本消息处理器
83 81 socket.textMessageHandler(message -> {
84 82 try {
85 83 processMessage(clientId, message, socket);
... ... @@ -105,12 +103,13 @@ public class IotWebSocketUpstreamHandler implements Handler&lt;ServerWebSocket&gt; {
105 103 if (StrUtil.isBlank(message)) {
106 104 return;
107 105 }
108   - // 1.2 解码消息
109   - // TODO @AI:应该只有初始使用 CODEC_TYPE 解析,后续基于
  106 + // 1.2 解码消息(已认证连接使用其 codecType,未认证连接使用默认 CODEC_TYPE)
  107 + IotWebSocketConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket);
  108 + String codecType = connectionInfo != null ? connectionInfo.getCodecType() : CODEC_TYPE;
110 109 IotDeviceMessage deviceMessage;
111 110 try {
112 111 deviceMessage = deviceMessageService.decodeDeviceMessage(
113   - message.getBytes(StandardCharsets.UTF_8), CODEC_TYPE);
  112 + StrUtil.utf8Bytes(message), codecType);
114 113 if (deviceMessage == null) {
115 114 throw new Exception("解码后消息为空");
116 115 }
... ... @@ -269,7 +268,8 @@ public class IotWebSocketUpstreamHandler implements Handler&lt;ServerWebSocket&gt; {
269 268 .setDeviceId(device.getId())
270 269 .setProductKey(device.getProductKey())
271 270 .setDeviceName(device.getDeviceName())
272   - .setClientId(clientId);
  271 + .setClientId(clientId)
  272 + .setCodecType(CODEC_TYPE);
273 273 // 注册连接
274 274 connectionManager.registerConnection(socket, device.getId(), connectionInfo);
275 275 }
... ... @@ -330,7 +330,7 @@ public class IotWebSocketUpstreamHandler implements Handler&lt;ServerWebSocket&gt; {
330 330 IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, AUTH_METHOD, responseData, code, message);
331 331  
332 332 byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, CODEC_TYPE);
333   - socket.writeTextMessage(new String(encodedData, StandardCharsets.UTF_8));
  333 + socket.writeTextMessage(StrUtil.utf8Str(encodedData));
334 334 } catch (Exception e) {
335 335 log.error("[sendResponse][发送响应失败,requestId: {}]", requestId, e);
336 336 }
... ... @@ -472,7 +472,7 @@ public class IotWebSocketUpstreamHandler implements Handler&lt;ServerWebSocket&gt; {
472 472 IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerResp, 0, null);
473 473 // 2. 发送响应
474 474 byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, CODEC_TYPE);
475   - socket.writeTextMessage(new String(encodedData, StandardCharsets.UTF_8));
  475 + socket.writeTextMessage(StrUtil.utf8Str(encodedData));
476 476 } catch (Exception e) {
477 477 log.error("[sendRegisterSuccessResponse][发送注册成功响应失败,requestId: {}]", requestId, e);
478 478 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotDirectDeviceWebSocketProtocolIntegrationTest.java
... ... @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.websocket;
2 2  
3 3 import cn.hutool.core.map.MapUtil;
4 4 import cn.hutool.core.util.IdUtil;
  5 +import cn.hutool.core.util.StrUtil;
5 6 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
6 7 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
7 8 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
... ... @@ -12,15 +13,14 @@ import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
12 13 import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
13 14 import cn.iocoder.yudao.module.iot.gateway.codec.websocket.IotWebSocketJsonDeviceMessageCodec;
14 15 import io.vertx.core.Vertx;
15   -import io.vertx.core.http.HttpClient;
16 16 import io.vertx.core.http.WebSocket;
  17 +import io.vertx.core.http.WebSocketClient;
17 18 import io.vertx.core.http.WebSocketConnectOptions;
18 19 import lombok.extern.slf4j.Slf4j;
19 20 import org.junit.jupiter.api.AfterAll;
20 21 import org.junit.jupiter.api.BeforeAll;
21 22 import org.junit.jupiter.api.Test;
22 23  
23   -import java.nio.charset.StandardCharsets;
24 24 import java.util.concurrent.CountDownLatch;
25 25 import java.util.concurrent.TimeUnit;
26 26 import java.util.concurrent.atomic.AtomicReference;
... ... @@ -81,119 +81,83 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest {
81 81  
82 82 // ===================== 认证测试 =====================
83 83  
84   - // TODO @AI:参考 /Users/yunai/Java/ruoyi-vue-pro-jdk25/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotDirectDeviceTcpProtocolIntegrationTest.java 或 /Users/yunai/Java/ruoyi-vue-pro-jdk25/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java 类,优化代码结构
85   -
86 84 /**
87 85 * 认证测试:获取设备 Token
88 86 */
89 87 @Test
90 88 public void testAuth() throws Exception {
91   - CountDownLatch latch = new CountDownLatch(1);
92   - AtomicReference<String> responseRef = new AtomicReference<>();
93   -
94   - // 1. 创建 WebSocket 连接
95   - HttpClient client = vertx.createHttpClient();
96   - WebSocketConnectOptions options = new WebSocketConnectOptions()
97   - .setHost(SERVER_HOST)
98   - .setPort(SERVER_PORT)
99   - .setURI(WS_PATH);
100   -
101   - // TODO @AI:这里有告警;Deprecate /instead use WebSocketClient.connect(WebSocketConnectOptions)
102   - client.webSocket(options).onComplete(ar -> {
103   - if (ar.succeeded()) {
104   - WebSocket ws = ar.result();
105   - log.info("[testAuth][WebSocket 连接成功]");
106   -
107   - // 设置消息处理器
108   - ws.textMessageHandler(message -> {
109   - log.info("[testAuth][收到响应: {}]", message);
110   - responseRef.set(message);
111   - ws.close();
112   - latch.countDown();
113   - });
114   -
115   - // 2. 构建认证消息
116   - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
117   - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
118   - .setClientId(authInfo.getClientId())
119   - .setUsername(authInfo.getUsername())
120   - .setPassword(authInfo.getPassword());
121   - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null);
122   -
123   - // 3. 编码并发送
124   - byte[] payload = CODEC.encode(request);
125   - String jsonMessage = new String(payload, StandardCharsets.UTF_8);
126   - log.info("[testAuth][发送认证请求: {}]", jsonMessage);
127   - ws.writeTextMessage(jsonMessage);
128   - } else {
129   - log.error("[testAuth][WebSocket 连接失败]", ar.cause());
130   - latch.countDown();
131   - }
132   - });
133   -
134   - // 4. 等待响应
135   - boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS);
136   - if (completed && responseRef.get() != null) {
137   - IotDeviceMessage response = CODEC.decode(responseRef.get().getBytes(StandardCharsets.UTF_8));
138   - log.info("[testAuth][解码响应: {}]", response);
  89 + // 1.1 构建认证消息
  90 + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
  91 + IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
  92 + .setClientId(authInfo.getClientId())
  93 + .setUsername(authInfo.getUsername())
  94 + .setPassword(authInfo.getPassword());
  95 + IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null);
  96 + // 1.2 编码
  97 + byte[] payload = CODEC.encode(request);
  98 + String jsonMessage = StrUtil.utf8Str(payload);
  99 + log.info("[testAuth][Codec: {}, 请求消息: {}]", CODEC.type(), request);
  100 +
  101 + // 2.1 创建 WebSocket 连接(同步)
  102 + WebSocket ws = createWebSocketConnection();
  103 + log.info("[testAuth][WebSocket 连接成功]");
  104 +
  105 + // 2.2 发送并等待响应
  106 + String response = sendAndReceive(ws, jsonMessage);
  107 +
  108 + // 3. 解码响应
  109 + if (response != null) {
  110 + IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response));
  111 + log.info("[testAuth][响应消息: {}]", responseMessage);
139 112 } else {
140   - log.warn("[testAuth][测试超时或未收到响应]");
  113 + log.warn("[testAuth][未收到响应]");
141 114 }
  115 +
  116 + // 4. 关闭连接
  117 + ws.close();
142 118 }
143 119  
144 120 // ===================== 动态注册测试 =====================
145 121  
146 122 /**
147 123 * 直连设备动态注册测试(一型一密)
  124 + * <p>
  125 + * 使用产品密钥(productSecret)验证身份,成功后返回设备密钥(deviceSecret)
  126 + * <p>
  127 + * 注意:此接口不需要认证
148 128 */
149 129 @Test
150 130 public void testDeviceRegister() throws Exception {
151   - CountDownLatch latch = new CountDownLatch(1);
152   - AtomicReference<String> responseRef = new AtomicReference<>();
153   -
154   - HttpClient client = vertx.createHttpClient();
155   - WebSocketConnectOptions options = new WebSocketConnectOptions()
156   - .setHost(SERVER_HOST)
157   - .setPort(SERVER_PORT)
158   - .setURI(WS_PATH);
159   -
160   - client.webSocket(options).onComplete(ar -> {
161   - if (ar.succeeded()) {
162   - WebSocket ws = ar.result();
163   - log.info("[testDeviceRegister][WebSocket 连接成功]");
164   -
165   - ws.textMessageHandler(message -> {
166   - log.info("[testDeviceRegister][收到响应: {}]", message);
167   - responseRef.set(message);
168   - ws.close();
169   - latch.countDown();
170   - });
171   -
172   - // 构建注册消息
173   - IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO();
174   - registerReqDTO.setProductKey(PRODUCT_KEY);
175   - registerReqDTO.setDeviceName("test-ws-" + System.currentTimeMillis());
176   - registerReqDTO.setProductSecret("test-product-secret");
177   - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(),
178   - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO, null, null, null);
179   -
180   - byte[] payload = CODEC.encode(request);
181   - String jsonMessage = new String(payload, StandardCharsets.UTF_8);
182   - log.info("[testDeviceRegister][发送注册请求: {}]", jsonMessage);
183   - ws.writeTextMessage(jsonMessage);
184   - } else {
185   - log.error("[testDeviceRegister][WebSocket 连接失败]", ar.cause());
186   - latch.countDown();
187   - }
188   - });
189   -
190   - boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS);
191   - if (completed && responseRef.get() != null) {
192   - IotDeviceMessage response = CODEC.decode(responseRef.get().getBytes(StandardCharsets.UTF_8));
193   - log.info("[testDeviceRegister][解码响应: {}]", response);
  131 + // 1.1 构建注册消息
  132 + IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO();
  133 + registerReqDTO.setProductKey(PRODUCT_KEY);
  134 + registerReqDTO.setDeviceName("test-ws-" + System.currentTimeMillis());
  135 + registerReqDTO.setProductSecret("test-product-secret");
  136 + IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(),
  137 + IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO, null, null, null);
  138 + // 1.2 编码
  139 + byte[] payload = CODEC.encode(request);
  140 + String jsonMessage = StrUtil.utf8Str(payload);
  141 + log.info("[testDeviceRegister][Codec: {}, 请求消息: {}]", CODEC.type(), request);
  142 +
  143 + // 2.1 创建 WebSocket 连接(同步)
  144 + WebSocket ws = createWebSocketConnection();
  145 + log.info("[testDeviceRegister][WebSocket 连接成功]");
  146 +
  147 + // 2.2 发送并等待响应
  148 + String response = sendAndReceive(ws, jsonMessage);
  149 +
  150 + // 3. 解码响应
  151 + if (response != null) {
  152 + IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response));
  153 + log.info("[testDeviceRegister][响应消息: {}]", responseMessage);
  154 + log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]");
194 155 } else {
195   - log.warn("[testDeviceRegister][测试超时或未收到响应]");
  156 + log.warn("[testDeviceRegister][未收到响应]");
196 157 }
  158 +
  159 + // 4. 关闭连接
  160 + ws.close();
197 161 }
198 162  
199 163 // ===================== 直连设备属性上报测试 =====================
... ... @@ -203,82 +167,40 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest {
203 167 */
204 168 @Test
205 169 public void testPropertyPost() throws Exception {
206   - CountDownLatch latch = new CountDownLatch(2); // 认证 + 属性上报
207   - AtomicReference<String> authResponseRef = new AtomicReference<>();
208   - AtomicReference<String> propertyResponseRef = new AtomicReference<>();
209   -
210   - HttpClient client = vertx.createHttpClient();
211   - WebSocketConnectOptions options = new WebSocketConnectOptions()
212   - .setHost(SERVER_HOST)
213   - .setPort(SERVER_PORT)
214   - .setURI(WS_PATH);
215   -
216   - client.webSocket(options).onComplete(ar -> {
217   - if (ar.succeeded()) {
218   - WebSocket ws = ar.result();
219   - log.info("[testPropertyPost][WebSocket 连接成功]");
220   -
221   - final boolean[] authenticated = {false};
222   -
223   - ws.textMessageHandler(message -> {
224   - log.info("[testPropertyPost][收到响应: {}]", message);
225   - if (!authenticated[0]) {
226   - authResponseRef.set(message);
227   - authenticated[0] = true;
228   - latch.countDown();
229   -
230   - // 认证成功后发送属性上报
231   - IotDeviceMessage propertyRequest = IotDeviceMessage.of(
232   - IdUtil.fastSimpleUUID(),
233   - IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(),
234   - IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
235   - .put("width", 1)
236   - .put("height", "2")
237   - .build()),
238   - null, null, null);
239   - byte[] payload = CODEC.encode(propertyRequest);
240   - String jsonMessage = new String(payload, StandardCharsets.UTF_8);
241   - log.info("[testPropertyPost][发送属性上报请求: {}]", jsonMessage);
242   - ws.writeTextMessage(jsonMessage);
243   - } else {
244   - propertyResponseRef.set(message);
245   - ws.close();
246   - latch.countDown();
247   - }
248   - });
249   -
250   - // 先发送认证请求
251   - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
252   - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
253   - .setClientId(authInfo.getClientId())
254   - .setUsername(authInfo.getUsername())
255   - .setPassword(authInfo.getPassword());
256   - IotDeviceMessage authRequest = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null);
257   -
258   - byte[] payload = CODEC.encode(authRequest);
259   - String jsonMessage = new String(payload, StandardCharsets.UTF_8);
260   - log.info("[testPropertyPost][发送认证请求: {}]", jsonMessage);
261   - ws.writeTextMessage(jsonMessage);
262   - } else {
263   - log.error("[testPropertyPost][WebSocket 连接失败]", ar.cause());
264   - latch.countDown();
265   - latch.countDown();
266   - }
267   - });
268   -
269   - boolean completed = latch.await(TIMEOUT_SECONDS * 2, TimeUnit.SECONDS);
270   - if (completed) {
271   - if (authResponseRef.get() != null) {
272   - IotDeviceMessage authResponse = CODEC.decode(authResponseRef.get().getBytes(StandardCharsets.UTF_8));
273   - log.info("[testPropertyPost][认证响应: {}]", authResponse);
274   - }
275   - if (propertyResponseRef.get() != null) {
276   - IotDeviceMessage propertyResponse = CODEC.decode(propertyResponseRef.get().getBytes(StandardCharsets.UTF_8));
277   - log.info("[testPropertyPost][属性上报响应: {}]", propertyResponse);
278   - }
  170 + // 1.1 创建 WebSocket 连接(同步)
  171 + WebSocket ws = createWebSocketConnection();
  172 + log.info("[testPropertyPost][WebSocket 连接成功]");
  173 +
  174 + // 1.2 先进行认证
  175 + IotDeviceMessage authResponse = authenticate(ws);
  176 + log.info("[testPropertyPost][认证响应: {}]", authResponse);
  177 +
  178 + // 2.1 构建属性上报消息
  179 + IotDeviceMessage request = IotDeviceMessage.of(
  180 + IdUtil.fastSimpleUUID(),
  181 + IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(),
  182 + IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
  183 + .put("width", 1)
  184 + .put("height", "2")
  185 + .build()),
  186 + null, null, null);
  187 + // 2.2 编码
  188 + byte[] payload = CODEC.encode(request);
  189 + String jsonMessage = StrUtil.utf8Str(payload);
  190 + log.info("[testPropertyPost][Codec: {}, 请求消息: {}]", CODEC.type(), request);
  191 +
  192 + // 3.1 发送并等待响应
  193 + String response = sendAndReceive(ws, jsonMessage);
  194 + // 3.2 解码响应
  195 + if (response != null) {
  196 + IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response));
  197 + log.info("[testPropertyPost][响应消息: {}]", responseMessage);
279 198 } else {
280   - log.warn("[testPropertyPost][测试超时]");
  199 + log.warn("[testPropertyPost][未收到响应]");
281 200 }
  201 +
  202 + // 4. 关闭连接
  203 + ws.close();
282 204 }
283 205  
284 206 // ===================== 直连设备事件上报测试 =====================
... ... @@ -288,82 +210,111 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest {
288 210 */
289 211 @Test
290 212 public void testEventPost() throws Exception {
291   - CountDownLatch latch = new CountDownLatch(2); // 认证 + 事件上报
292   - AtomicReference<String> authResponseRef = new AtomicReference<>();
293   - AtomicReference<String> eventResponseRef = new AtomicReference<>();
  213 + // 1.1 创建 WebSocket 连接(同步)
  214 + WebSocket ws = createWebSocketConnection();
  215 + log.info("[testEventPost][WebSocket 连接成功]");
  216 +
  217 + // 1.2 先进行认证
  218 + IotDeviceMessage authResponse = authenticate(ws);
  219 + log.info("[testEventPost][认证响应: {}]", authResponse);
  220 +
  221 + // 2.1 构建事件上报消息
  222 + IotDeviceMessage request = IotDeviceMessage.of(
  223 + IdUtil.fastSimpleUUID(),
  224 + IotDeviceMessageMethodEnum.EVENT_POST.getMethod(),
  225 + IotDeviceEventPostReqDTO.of(
  226 + "eat",
  227 + MapUtil.<String, Object>builder().put("rice", 3).build(),
  228 + System.currentTimeMillis()),
  229 + null, null, null);
  230 + // 2.2 编码
  231 + byte[] payload = CODEC.encode(request);
  232 + String jsonMessage = StrUtil.utf8Str(payload);
  233 + log.info("[testEventPost][Codec: {}, 请求消息: {}]", CODEC.type(), request);
  234 +
  235 + // 3.1 发送并等待响应
  236 + String response = sendAndReceive(ws, jsonMessage);
  237 + // 3.2 解码响应
  238 + if (response != null) {
  239 + IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response));
  240 + log.info("[testEventPost][响应消息: {}]", responseMessage);
  241 + } else {
  242 + log.warn("[testEventPost][未收到响应]");
  243 + }
  244 +
  245 + // 4. 关闭连接
  246 + ws.close();
  247 + }
  248 +
  249 + // ===================== 辅助方法 =====================
294 250  
295   - HttpClient client = vertx.createHttpClient();
  251 + /**
  252 + * 创建 WebSocket 连接(同步)
  253 + *
  254 + * @return WebSocket 连接
  255 + */
  256 + private WebSocket createWebSocketConnection() throws Exception {
  257 + WebSocketClient wsClient = vertx.createWebSocketClient();
296 258 WebSocketConnectOptions options = new WebSocketConnectOptions()
297 259 .setHost(SERVER_HOST)
298 260 .setPort(SERVER_PORT)
299 261 .setURI(WS_PATH);
  262 + return wsClient.connect(options).toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  263 + }
300 264  
301   - client.webSocket(options).onComplete(ar -> {
302   - if (ar.succeeded()) {
303   - WebSocket ws = ar.result();
304   - log.info("[testEventPost][WebSocket 连接成功]");
305   -
306   - final boolean[] authenticated = {false};
307   -
308   - ws.textMessageHandler(message -> {
309   - log.info("[testEventPost][收到响应: {}]", message);
310   - if (!authenticated[0]) {
311   - authResponseRef.set(message);
312   - authenticated[0] = true;
313   - latch.countDown();
314   -
315   - // 认证成功后发送事件上报
316   - IotDeviceMessage eventRequest = IotDeviceMessage.of(
317   - IdUtil.fastSimpleUUID(),
318   - IotDeviceMessageMethodEnum.EVENT_POST.getMethod(),
319   - IotDeviceEventPostReqDTO.of(
320   - "eat",
321   - MapUtil.<String, Object>builder().put("rice", 3).build(),
322   - System.currentTimeMillis()),
323   - null, null, null);
324   - byte[] payload = CODEC.encode(eventRequest);
325   - String jsonMessage = new String(payload, StandardCharsets.UTF_8);
326   - log.info("[testEventPost][发送事件上报请求: {}]", jsonMessage);
327   - ws.writeTextMessage(jsonMessage);
328   - } else {
329   - eventResponseRef.set(message);
330   - ws.close();
331   - latch.countDown();
332   - }
333   - });
334   -
335   - // 先发送认证请求
336   - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
337   - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
338   - .setClientId(authInfo.getClientId())
339   - .setUsername(authInfo.getUsername())
340   - .setPassword(authInfo.getPassword());
341   - IotDeviceMessage authRequest = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null);
342   -
343   - byte[] payload = CODEC.encode(authRequest);
344   - String jsonMessage = new String(payload, StandardCharsets.UTF_8);
345   - log.info("[testEventPost][发送认证请求: {}]", jsonMessage);
346   - ws.writeTextMessage(jsonMessage);
347   - } else {
348   - log.error("[testEventPost][WebSocket 连接失败]", ar.cause());
349   - latch.countDown();
350   - latch.countDown();
351   - }
  265 + /**
  266 + * 发送消息并等待响应(同步)
  267 + *
  268 + * @param ws WebSocket 连接
  269 + * @param message 请求消息
  270 + * @return 响应消息
  271 + */
  272 + public static String sendAndReceive(WebSocket ws, String message) throws Exception {
  273 + CountDownLatch latch = new CountDownLatch(1);
  274 + AtomicReference<String> responseRef = new AtomicReference<>();
  275 +
  276 + // 设置消息处理器
  277 + ws.textMessageHandler(response -> {
  278 + log.info("[sendAndReceive][收到响应: {}]", response);
  279 + responseRef.set(response);
  280 + latch.countDown();
352 281 });
353 282  
354   - boolean completed = latch.await(TIMEOUT_SECONDS * 2, TimeUnit.SECONDS);
355   - if (completed) {
356   - if (authResponseRef.get() != null) {
357   - IotDeviceMessage authResponse = CODEC.decode(authResponseRef.get().getBytes(StandardCharsets.UTF_8));
358   - log.info("[testEventPost][认证响应: {}]", authResponse);
359   - }
360   - if (eventResponseRef.get() != null) {
361   - IotDeviceMessage eventResponse = CODEC.decode(eventResponseRef.get().getBytes(StandardCharsets.UTF_8));
362   - log.info("[testEventPost][事件上报响应: {}]", eventResponse);
363   - }
364   - } else {
365   - log.warn("[testEventPost][测试超时]");
  283 + // 发送请求
  284 + log.info("[sendAndReceive][发送请求: {}]", message);
  285 + ws.writeTextMessage(message);
  286 +
  287 + // 等待响应
  288 + boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  289 + if (!completed) {
  290 + log.warn("[sendAndReceive][等待响应超时]");
  291 + }
  292 + return responseRef.get();
  293 + }
  294 +
  295 + /**
  296 + * 执行设备认证(同步)
  297 + *
  298 + * @param ws WebSocket 连接
  299 + * @return 认证响应消息
  300 + */
  301 + private IotDeviceMessage authenticate(WebSocket ws) throws Exception {
  302 + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
  303 + IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
  304 + .setClientId(authInfo.getClientId())
  305 + .setUsername(authInfo.getUsername())
  306 + .setPassword(authInfo.getPassword());
  307 + IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null);
  308 +
  309 + byte[] payload = CODEC.encode(request);
  310 + String jsonMessage = StrUtil.utf8Str(payload);
  311 + log.info("[authenticate][发送认证请求: {}]", jsonMessage);
  312 +
  313 + String response = sendAndReceive(ws, jsonMessage);
  314 + if (response != null) {
  315 + return CODEC.decode(StrUtil.utf8Bytes(response));
366 316 }
  317 + return null;
367 318 }
368 319  
369 320 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotGatewayDeviceWebSocketProtocolIntegrationTest.java
... ... @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.websocket;
2 2  
3 3 import cn.hutool.core.map.MapUtil;
4 4 import cn.hutool.core.util.IdUtil;
  5 +import cn.hutool.core.util.StrUtil;
5 6 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
6 7 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
7 8 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
... ... @@ -15,15 +16,14 @@ import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
15 16 import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
16 17 import cn.iocoder.yudao.module.iot.gateway.codec.websocket.IotWebSocketJsonDeviceMessageCodec;
17 18 import io.vertx.core.Vertx;
18   -import io.vertx.core.http.HttpClient;
19 19 import io.vertx.core.http.WebSocket;
  20 +import io.vertx.core.http.WebSocketClient;
20 21 import io.vertx.core.http.WebSocketConnectOptions;
21 22 import lombok.extern.slf4j.Slf4j;
22 23 import org.junit.jupiter.api.AfterAll;
23 24 import org.junit.jupiter.api.BeforeAll;
24 25 import org.junit.jupiter.api.Test;
25 26  
26   -import java.nio.charset.StandardCharsets;
27 27 import java.util.Collections;
28 28 import java.util.List;
29 29 import java.util.Map;
... ... @@ -100,53 +100,36 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest {
100 100 */
101 101 @Test
102 102 public void testAuth() throws Exception {
103   - CountDownLatch latch = new CountDownLatch(1);
104   - AtomicReference<String> responseRef = new AtomicReference<>();
105   -
106   - HttpClient client = vertx.createHttpClient();
107   - WebSocketConnectOptions options = new WebSocketConnectOptions()
108   - .setHost(SERVER_HOST)
109   - .setPort(SERVER_PORT)
110   - .setURI(WS_PATH);
111   -
112   - client.webSocket(options).onComplete(ar -> {
113   - if (ar.succeeded()) {
114   - WebSocket ws = ar.result();
115   - log.info("[testAuth][WebSocket 连接成功]");
116   -
117   - ws.textMessageHandler(message -> {
118   - log.info("[testAuth][收到响应: {}]", message);
119   - responseRef.set(message);
120   - ws.close();
121   - latch.countDown();
122   - });
123   -
124   - // 构建认证消息
125   - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(
126   - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET);
127   - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
128   - .setClientId(authInfo.getClientId())
129   - .setUsername(authInfo.getUsername())
130   - .setPassword(authInfo.getPassword());
131   - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null);
132   -
133   - byte[] payload = CODEC.encode(request);
134   - String jsonMessage = new String(payload, StandardCharsets.UTF_8);
135   - log.info("[testAuth][发送认证请求: {}]", jsonMessage);
136   - ws.writeTextMessage(jsonMessage);
137   - } else {
138   - log.error("[testAuth][WebSocket 连接失败]", ar.cause());
139   - latch.countDown();
140   - }
141   - });
142   -
143   - boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS);
144   - if (completed && responseRef.get() != null) {
145   - IotDeviceMessage response = CODEC.decode(responseRef.get().getBytes(StandardCharsets.UTF_8));
146   - log.info("[testAuth][解码响应: {}]", response);
  103 + // 1.1 构建认证消息
  104 + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(
  105 + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET);
  106 + IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
  107 + .setClientId(authInfo.getClientId())
  108 + .setUsername(authInfo.getUsername())
  109 + .setPassword(authInfo.getPassword());
  110 + IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null);
  111 + // 1.2 编码
  112 + byte[] payload = CODEC.encode(request);
  113 + String jsonMessage = StrUtil.utf8Str(payload);
  114 + log.info("[testAuth][Codec: {}, 请求消息: {}]", CODEC.type(), request);
  115 +
  116 + // 2.1 创建 WebSocket 连接(同步)
  117 + WebSocket ws = createWebSocketConnection();
  118 + log.info("[testAuth][WebSocket 连接成功]");
  119 +
  120 + // 2.2 发送并等待响应
  121 + String response = sendAndReceive(ws, jsonMessage);
  122 +
  123 + // 3. 解码响应
  124 + if (response != null) {
  125 + IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response));
  126 + log.info("[testAuth][响应消息: {}]", responseMessage);
147 127 } else {
148   - log.warn("[testAuth][测试超时或未收到响应]");
  128 + log.warn("[testAuth][未收到响应]");
149 129 }
  130 +
  131 + // 4. 关闭连接
  132 + ws.close();
150 133 }
151 134  
152 135 // ===================== 拓扑管理测试 =====================
... ... @@ -156,23 +139,46 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest {
156 139 */
157 140 @Test
158 141 public void testTopoAdd() throws Exception {
159   - executeAuthenticatedRequest("testTopoAdd", ws -> {
160   - // 构建子设备认证信息
161   - IotDeviceAuthReqDTO subAuthInfo = IotDeviceAuthUtils.getAuthInfo(
162   - SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET);
163   - IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO()
164   - .setClientId(subAuthInfo.getClientId())
165   - .setUsername(subAuthInfo.getUsername())
166   - .setPassword(subAuthInfo.getPassword());
167   - // 构建请求参数
168   - IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO();
169   - params.setSubDevices(Collections.singletonList(subDeviceAuth));
170   - return IotDeviceMessage.of(
171   - IdUtil.fastSimpleUUID(),
172   - IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(),
173   - params,
174   - null, null, null);
175   - });
  142 + // 1.1 创建 WebSocket 连接(同步)
  143 + WebSocket ws = createWebSocketConnection();
  144 + log.info("[testTopoAdd][WebSocket 连接成功]");
  145 +
  146 + // 1.2 先进行认证
  147 + IotDeviceMessage authResponse = authenticate(ws);
  148 + log.info("[testTopoAdd][认证响应: {}]", authResponse);
  149 +
  150 + // 2.1 构建子设备认证信息
  151 + IotDeviceAuthReqDTO subAuthInfo = IotDeviceAuthUtils.getAuthInfo(
  152 + SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET);
  153 + IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO()
  154 + .setClientId(subAuthInfo.getClientId())
  155 + .setUsername(subAuthInfo.getUsername())
  156 + .setPassword(subAuthInfo.getPassword());
  157 + // 2.2 构建请求参数
  158 + IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO();
  159 + params.setSubDevices(Collections.singletonList(subDeviceAuth));
  160 + IotDeviceMessage request = IotDeviceMessage.of(
  161 + IdUtil.fastSimpleUUID(),
  162 + IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(),
  163 + params,
  164 + null, null, null);
  165 + // 2.3 编码
  166 + byte[] payload = CODEC.encode(request);
  167 + String jsonMessage = StrUtil.utf8Str(payload);
  168 + log.info("[testTopoAdd][Codec: {}, 请求消息: {}]", CODEC.type(), request);
  169 +
  170 + // 3.1 发送并等待响应
  171 + String response = sendAndReceive(ws, jsonMessage);
  172 + // 3.2 解码响应
  173 + if (response != null) {
  174 + IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response));
  175 + log.info("[testTopoAdd][响应消息: {}]", responseMessage);
  176 + } else {
  177 + log.warn("[testTopoAdd][未收到响应]");
  178 + }
  179 +
  180 + // 4. 关闭连接
  181 + ws.close();
176 182 }
177 183  
178 184 /**
... ... @@ -180,16 +186,40 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest {
180 186 */
181 187 @Test
182 188 public void testTopoDelete() throws Exception {
183   - executeAuthenticatedRequest("testTopoDelete", ws -> {
184   - IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO();
185   - params.setSubDevices(Collections.singletonList(
186   - new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)));
187   - return IotDeviceMessage.of(
188   - IdUtil.fastSimpleUUID(),
189   - IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(),
190   - params,
191   - null, null, null);
192   - });
  189 + // 1.1 创建 WebSocket 连接(同步)
  190 + WebSocket ws = createWebSocketConnection();
  191 + log.info("[testTopoDelete][WebSocket 连接成功]");
  192 +
  193 + // 1.2 先进行认证
  194 + IotDeviceMessage authResponse = authenticate(ws);
  195 + log.info("[testTopoDelete][认证响应: {}]", authResponse);
  196 +
  197 + // 2.1 构建请求参数
  198 + IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO();
  199 + params.setSubDevices(Collections.singletonList(
  200 + new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)));
  201 + IotDeviceMessage request = IotDeviceMessage.of(
  202 + IdUtil.fastSimpleUUID(),
  203 + IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(),
  204 + params,
  205 + null, null, null);
  206 + // 2.2 编码
  207 + byte[] payload = CODEC.encode(request);
  208 + String jsonMessage = StrUtil.utf8Str(payload);
  209 + log.info("[testTopoDelete][Codec: {}, 请求消息: {}]", CODEC.type(), request);
  210 +
  211 + // 3.1 发送并等待响应
  212 + String response = sendAndReceive(ws, jsonMessage);
  213 + // 3.2 解码响应
  214 + if (response != null) {
  215 + IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response));
  216 + log.info("[testTopoDelete][响应消息: {}]", responseMessage);
  217 + } else {
  218 + log.warn("[testTopoDelete][未收到响应]");
  219 + }
  220 +
  221 + // 4. 关闭连接
  222 + ws.close();
193 223 }
194 224  
195 225 /**
... ... @@ -197,14 +227,38 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest {
197 227 */
198 228 @Test
199 229 public void testTopoGet() throws Exception {
200   - executeAuthenticatedRequest("testTopoGet", ws -> {
201   - IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO();
202   - return IotDeviceMessage.of(
203   - IdUtil.fastSimpleUUID(),
204   - IotDeviceMessageMethodEnum.TOPO_GET.getMethod(),
205   - params,
206   - null, null, null);
207   - });
  230 + // 1.1 创建 WebSocket 连接(同步)
  231 + WebSocket ws = createWebSocketConnection();
  232 + log.info("[testTopoGet][WebSocket 连接成功]");
  233 +
  234 + // 1.2 先进行认证
  235 + IotDeviceMessage authResponse = authenticate(ws);
  236 + log.info("[testTopoGet][认证响应: {}]", authResponse);
  237 +
  238 + // 2.1 构建请求参数
  239 + IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO();
  240 + IotDeviceMessage request = IotDeviceMessage.of(
  241 + IdUtil.fastSimpleUUID(),
  242 + IotDeviceMessageMethodEnum.TOPO_GET.getMethod(),
  243 + params,
  244 + null, null, null);
  245 + // 2.2 编码
  246 + byte[] payload = CODEC.encode(request);
  247 + String jsonMessage = StrUtil.utf8Str(payload);
  248 + log.info("[testTopoGet][Codec: {}, 请求消息: {}]", CODEC.type(), request);
  249 +
  250 + // 3.1 发送并等待响应
  251 + String response = sendAndReceive(ws, jsonMessage);
  252 + // 3.2 解码响应
  253 + if (response != null) {
  254 + IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response));
  255 + log.info("[testTopoGet][响应消息: {}]", responseMessage);
  256 + } else {
  257 + log.warn("[testTopoGet][未收到响应]");
  258 + }
  259 +
  260 + // 4. 关闭连接
  261 + ws.close();
208 262 }
209 263  
210 264 // ===================== 子设备注册测试 =====================
... ... @@ -214,16 +268,40 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest {
214 268 */
215 269 @Test
216 270 public void testSubDeviceRegister() throws Exception {
217   - executeAuthenticatedRequest("testSubDeviceRegister", ws -> {
218   - IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO();
219   - subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY);
220   - subDevice.setDeviceName("mougezishebei-ws");
221   - return IotDeviceMessage.of(
222   - IdUtil.fastSimpleUUID(),
223   - IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod(),
224   - Collections.singletonList(subDevice),
225   - null, null, null);
226   - });
  271 + // 1.1 创建 WebSocket 连接(同步)
  272 + WebSocket ws = createWebSocketConnection();
  273 + log.info("[testSubDeviceRegister][WebSocket 连接成功]");
  274 +
  275 + // 1.2 先进行认证
  276 + IotDeviceMessage authResponse = authenticate(ws);
  277 + log.info("[testSubDeviceRegister][认证响应: {}]", authResponse);
  278 +
  279 + // 2.1 构建请求参数
  280 + IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO();
  281 + subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY);
  282 + subDevice.setDeviceName("mougezishebei-ws");
  283 + IotDeviceMessage request = IotDeviceMessage.of(
  284 + IdUtil.fastSimpleUUID(),
  285 + IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod(),
  286 + Collections.singletonList(subDevice),
  287 + null, null, null);
  288 + // 2.2 编码
  289 + byte[] payload = CODEC.encode(request);
  290 + String jsonMessage = StrUtil.utf8Str(payload);
  291 + log.info("[testSubDeviceRegister][Codec: {}, 请求消息: {}]", CODEC.type(), request);
  292 +
  293 + // 3.1 发送并等待响应
  294 + String response = sendAndReceive(ws, jsonMessage);
  295 + // 3.2 解码响应
  296 + if (response != null) {
  297 + IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response));
  298 + log.info("[testSubDeviceRegister][响应消息: {}]", responseMessage);
  299 + } else {
  300 + log.warn("[testSubDeviceRegister][未收到响应]");
  301 + }
  302 +
  303 + // 4. 关闭连接
  304 + ws.close();
227 305 }
228 306  
229 307 // ===================== 批量上报测试 =====================
... ... @@ -233,126 +311,140 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest {
233 311 */
234 312 @Test
235 313 public void testPropertyPackPost() throws Exception {
236   - executeAuthenticatedRequest("testPropertyPackPost", ws -> {
237   - // 构建【网关设备】自身属性
238   - Map<String, Object> gatewayProperties = MapUtil.<String, Object>builder()
239   - .put("temperature", 25.5)
240   - .build();
241   - // 构建【网关设备】自身事件
242   - IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue();
243   - gatewayEvent.setValue(MapUtil.builder().put("message", "gateway started").build());
244   - gatewayEvent.setTime(System.currentTimeMillis());
245   - Map<String, IotDevicePropertyPackPostReqDTO.EventValue> gatewayEvents = MapUtil.<String, IotDevicePropertyPackPostReqDTO.EventValue>builder()
246   - .put("statusReport", gatewayEvent)
247   - .build();
248   - // 构建【网关子设备】属性
249   - Map<String, Object> subDeviceProperties = MapUtil.<String, Object>builder()
250   - .put("power", 100)
251   - .build();
252   - // 构建【网关子设备】事件
253   - IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue();
254   - subDeviceEvent.setValue(MapUtil.builder().put("errorCode", 0).build());
255   - subDeviceEvent.setTime(System.currentTimeMillis());
256   - Map<String, IotDevicePropertyPackPostReqDTO.EventValue> subDeviceEvents = MapUtil.<String, IotDevicePropertyPackPostReqDTO.EventValue>builder()
257   - .put("healthCheck", subDeviceEvent)
258   - .build();
259   - // 构建子设备数据
260   - IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData();
261   - subDeviceData.setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME));
262   - subDeviceData.setProperties(subDeviceProperties);
263   - subDeviceData.setEvents(subDeviceEvents);
264   - // 构建请求参数
265   - IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO();
266   - params.setProperties(gatewayProperties);
267   - params.setEvents(gatewayEvents);
268   - params.setSubDevices(List.of(subDeviceData));
269   - return IotDeviceMessage.of(
270   - IdUtil.fastSimpleUUID(),
271   - IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(),
272   - params,
273   - null, null, null);
274   - });
  314 + // 1.1 创建 WebSocket 连接(同步)
  315 + WebSocket ws = createWebSocketConnection();
  316 + log.info("[testPropertyPackPost][WebSocket 连接成功]");
  317 +
  318 + // 1.2 先进行认证
  319 + IotDeviceMessage authResponse = authenticate(ws);
  320 + log.info("[testPropertyPackPost][认证响应: {}]", authResponse);
  321 +
  322 + // 2.1 构建【网关设备】自身属性
  323 + Map<String, Object> gatewayProperties = MapUtil.<String, Object>builder()
  324 + .put("temperature", 25.5)
  325 + .build();
  326 + // 2.2 构建【网关设备】自身事件
  327 + IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue();
  328 + gatewayEvent.setValue(MapUtil.builder().put("message", "gateway started").build());
  329 + gatewayEvent.setTime(System.currentTimeMillis());
  330 + Map<String, IotDevicePropertyPackPostReqDTO.EventValue> gatewayEvents = MapUtil.<String, IotDevicePropertyPackPostReqDTO.EventValue>builder()
  331 + .put("statusReport", gatewayEvent)
  332 + .build();
  333 + // 2.3 构建【网关子设备】属性
  334 + Map<String, Object> subDeviceProperties = MapUtil.<String, Object>builder()
  335 + .put("power", 100)
  336 + .build();
  337 + // 2.4 构建【网关子设备】事件
  338 + IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue();
  339 + subDeviceEvent.setValue(MapUtil.builder().put("errorCode", 0).build());
  340 + subDeviceEvent.setTime(System.currentTimeMillis());
  341 + Map<String, IotDevicePropertyPackPostReqDTO.EventValue> subDeviceEvents = MapUtil.<String, IotDevicePropertyPackPostReqDTO.EventValue>builder()
  342 + .put("healthCheck", subDeviceEvent)
  343 + .build();
  344 + // 2.5 构建子设备数据
  345 + IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData();
  346 + subDeviceData.setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME));
  347 + subDeviceData.setProperties(subDeviceProperties);
  348 + subDeviceData.setEvents(subDeviceEvents);
  349 + // 2.6 构建请求参数
  350 + IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO();
  351 + params.setProperties(gatewayProperties);
  352 + params.setEvents(gatewayEvents);
  353 + params.setSubDevices(List.of(subDeviceData));
  354 + IotDeviceMessage request = IotDeviceMessage.of(
  355 + IdUtil.fastSimpleUUID(),
  356 + IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(),
  357 + params,
  358 + null, null, null);
  359 + // 2.7 编码
  360 + byte[] payload = CODEC.encode(request);
  361 + String jsonMessage = StrUtil.utf8Str(payload);
  362 + log.info("[testPropertyPackPost][Codec: {}, 请求消息: {}]", CODEC.type(), request);
  363 +
  364 + // 3.1 发送并等待响应
  365 + String response = sendAndReceive(ws, jsonMessage);
  366 + // 3.2 解码响应
  367 + if (response != null) {
  368 + IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response));
  369 + log.info("[testPropertyPackPost][响应消息: {}]", responseMessage);
  370 + } else {
  371 + log.warn("[testPropertyPackPost][未收到响应]");
  372 + }
  373 +
  374 + // 4. 关闭连接
  375 + ws.close();
275 376 }
276 377  
277 378 // ===================== 辅助方法 =====================
278 379  
279 380 /**
280   - * 执行需要认证的请求
  381 + * 创建 WebSocket 连接(同步)
281 382 *
282   - * @param testName 测试名称
283   - * @param requestSupplier 请求消息提供者
  383 + * @return WebSocket 连接
284 384 */
285   - private void executeAuthenticatedRequest(String testName, java.util.function.Function<WebSocket, IotDeviceMessage> requestSupplier) throws Exception {
286   - CountDownLatch latch = new CountDownLatch(2);
287   - AtomicReference<String> authResponseRef = new AtomicReference<>();
288   - AtomicReference<String> businessResponseRef = new AtomicReference<>();
289   -
290   - HttpClient client = vertx.createHttpClient();
  385 + private WebSocket createWebSocketConnection() throws Exception {
  386 + WebSocketClient wsClient = vertx.createWebSocketClient();
291 387 WebSocketConnectOptions options = new WebSocketConnectOptions()
292 388 .setHost(SERVER_HOST)
293 389 .setPort(SERVER_PORT)
294 390 .setURI(WS_PATH);
  391 + return wsClient.connect(options).toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  392 + }
  393 +
  394 + /**
  395 + * 发送消息并等待响应(同步)
  396 + *
  397 + * @param ws WebSocket 连接
  398 + * @param message 请求消息
  399 + * @return 响应消息
  400 + */
  401 + private String sendAndReceive(WebSocket ws, String message) throws Exception {
  402 + CountDownLatch latch = new CountDownLatch(1);
  403 + AtomicReference<String> responseRef = new AtomicReference<>();
295 404  
296   - client.webSocket(options).onComplete(ar -> {
297   - if (ar.succeeded()) {
298   - WebSocket ws = ar.result();
299   - log.info("[{}][WebSocket 连接成功]", testName);
300   -
301   - final boolean[] authenticated = {false};
302   -
303   - ws.textMessageHandler(message -> {
304   - log.info("[{}][收到响应: {}]", testName, message);
305   - if (!authenticated[0]) {
306   - authResponseRef.set(message);
307   - authenticated[0] = true;
308   - latch.countDown();
309   -
310   - // 认证成功后发送业务请求
311   - IotDeviceMessage businessRequest = requestSupplier.apply(ws);
312   - byte[] payload = CODEC.encode(businessRequest);
313   - String jsonMessage = new String(payload, StandardCharsets.UTF_8);
314   - log.info("[{}][发送业务请求: {}]", testName, jsonMessage);
315   - ws.writeTextMessage(jsonMessage);
316   - } else {
317   - businessResponseRef.set(message);
318   - ws.close();
319   - latch.countDown();
320   - }
321   - });
322   -
323   - // 先发送认证请求
324   - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(
325   - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET);
326   - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
327   - .setClientId(authInfo.getClientId())
328   - .setUsername(authInfo.getUsername())
329   - .setPassword(authInfo.getPassword());
330   - IotDeviceMessage authRequest = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null);
331   -
332   - byte[] payload = CODEC.encode(authRequest);
333   - String jsonMessage = new String(payload, StandardCharsets.UTF_8);
334   - log.info("[{}][发送认证请求: {}]", testName, jsonMessage);
335   - ws.writeTextMessage(jsonMessage);
336   - } else {
337   - log.error("[{}][WebSocket 连接失败]", testName, ar.cause());
338   - latch.countDown();
339   - latch.countDown();
340   - }
  405 + // 设置消息处理器
  406 + ws.textMessageHandler(response -> {
  407 + log.info("[sendAndReceive][收到响应: {}]", response);
  408 + responseRef.set(response);
  409 + latch.countDown();
341 410 });
342 411  
343   - boolean completed = latch.await(TIMEOUT_SECONDS * 2, TimeUnit.SECONDS);
344   - if (completed) {
345   - if (authResponseRef.get() != null) {
346   - IotDeviceMessage authResponse = CODEC.decode(authResponseRef.get().getBytes(StandardCharsets.UTF_8));
347   - log.info("[{}][认证响应: {}]", testName, authResponse);
348   - }
349   - if (businessResponseRef.get() != null) {
350   - IotDeviceMessage businessResponse = CODEC.decode(businessResponseRef.get().getBytes(StandardCharsets.UTF_8));
351   - log.info("[{}][业务响应: {}]", testName, businessResponse);
352   - }
353   - } else {
354   - log.warn("[{}][测试超时]", testName);
  412 + // 发送请求
  413 + log.info("[sendAndReceive][发送请求: {}]", message);
  414 + ws.writeTextMessage(message);
  415 +
  416 + // 等待响应
  417 + boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  418 + if (!completed) {
  419 + log.warn("[sendAndReceive][等待响应超时]");
  420 + }
  421 + return responseRef.get();
  422 + }
  423 +
  424 + /**
  425 + * 执行网关设备认证(同步)
  426 + *
  427 + * @param ws WebSocket 连接
  428 + * @return 认证响应消息
  429 + */
  430 + private IotDeviceMessage authenticate(WebSocket ws) throws Exception {
  431 + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(
  432 + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET);
  433 + IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
  434 + .setClientId(authInfo.getClientId())
  435 + .setUsername(authInfo.getUsername())
  436 + .setPassword(authInfo.getPassword());
  437 + IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null);
  438 +
  439 + byte[] payload = CODEC.encode(request);
  440 + String jsonMessage = StrUtil.utf8Str(payload);
  441 + log.info("[authenticate][发送认证请求: {}]", jsonMessage);
  442 +
  443 + String response = sendAndReceive(ws, jsonMessage);
  444 + if (response != null) {
  445 + return CODEC.decode(StrUtil.utf8Bytes(response));
355 446 }
  447 + return null;
356 448 }
357 449  
358 450 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotGatewaySubDeviceWebSocketProtocolIntegrationTest.java
... ... @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.websocket;
2 2  
3 3 import cn.hutool.core.map.MapUtil;
4 4 import cn.hutool.core.util.IdUtil;
  5 +import cn.hutool.core.util.StrUtil;
5 6 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
6 7 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
7 8 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
... ... @@ -11,15 +12,14 @@ import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
11 12 import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
12 13 import cn.iocoder.yudao.module.iot.gateway.codec.websocket.IotWebSocketJsonDeviceMessageCodec;
13 14 import io.vertx.core.Vertx;
14   -import io.vertx.core.http.HttpClient;
15 15 import io.vertx.core.http.WebSocket;
  16 +import io.vertx.core.http.WebSocketClient;
16 17 import io.vertx.core.http.WebSocketConnectOptions;
17 18 import lombok.extern.slf4j.Slf4j;
18 19 import org.junit.jupiter.api.AfterAll;
19 20 import org.junit.jupiter.api.BeforeAll;
20 21 import org.junit.jupiter.api.Test;
21 22  
22   -import java.nio.charset.StandardCharsets;
23 23 import java.util.concurrent.CountDownLatch;
24 24 import java.util.concurrent.TimeUnit;
25 25 import java.util.concurrent.atomic.AtomicReference;
... ... @@ -87,52 +87,35 @@ public class IotGatewaySubDeviceWebSocketProtocolIntegrationTest {
87 87 */
88 88 @Test
89 89 public void testAuth() throws Exception {
90   - CountDownLatch latch = new CountDownLatch(1);
91   - AtomicReference<String> responseRef = new AtomicReference<>();
92   -
93   - HttpClient client = vertx.createHttpClient();
94   - WebSocketConnectOptions options = new WebSocketConnectOptions()
95   - .setHost(SERVER_HOST)
96   - .setPort(SERVER_PORT)
97   - .setURI(WS_PATH);
98   -
99   - client.webSocket(options).onComplete(ar -> {
100   - if (ar.succeeded()) {
101   - WebSocket ws = ar.result();
102   - log.info("[testAuth][WebSocket 连接成功]");
103   -
104   - ws.textMessageHandler(message -> {
105   - log.info("[testAuth][收到响应: {}]", message);
106   - responseRef.set(message);
107   - ws.close();
108   - latch.countDown();
109   - });
110   -
111   - // 构建认证消息
112   - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
113   - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
114   - .setClientId(authInfo.getClientId())
115   - .setUsername(authInfo.getUsername())
116   - .setPassword(authInfo.getPassword());
117   - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null);
118   -
119   - byte[] payload = CODEC.encode(request);
120   - String jsonMessage = new String(payload, StandardCharsets.UTF_8);
121   - log.info("[testAuth][发送认证请求: {}]", jsonMessage);
122   - ws.writeTextMessage(jsonMessage);
123   - } else {
124   - log.error("[testAuth][WebSocket 连接失败]", ar.cause());
125   - latch.countDown();
126   - }
127   - });
128   -
129   - boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS);
130   - if (completed && responseRef.get() != null) {
131   - IotDeviceMessage response = CODEC.decode(responseRef.get().getBytes(StandardCharsets.UTF_8));
132   - log.info("[testAuth][解码响应: {}]", response);
  90 + // 1.1 构建认证消息
  91 + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
  92 + IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
  93 + .setClientId(authInfo.getClientId())
  94 + .setUsername(authInfo.getUsername())
  95 + .setPassword(authInfo.getPassword());
  96 + IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null);
  97 + // 1.2 编码
  98 + byte[] payload = CODEC.encode(request);
  99 + String jsonMessage = StrUtil.utf8Str(payload);
  100 + log.info("[testAuth][Codec: {}, 请求消息: {}]", CODEC.type(), request);
  101 +
  102 + // 2.1 创建 WebSocket 连接(同步)
  103 + WebSocket ws = createWebSocketConnection();
  104 + log.info("[testAuth][WebSocket 连接成功]");
  105 +
  106 + // 2.2 发送并等待响应
  107 + String response = sendAndReceive(ws, jsonMessage);
  108 +
  109 + // 3. 解码响应
  110 + if (response != null) {
  111 + IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response));
  112 + log.info("[testAuth][响应消息: {}]", responseMessage);
133 113 } else {
134   - log.warn("[testAuth][测试超时或未收到响应]");
  114 + log.warn("[testAuth][未收到响应]");
135 115 }
  116 +
  117 + // 4. 关闭连接
  118 + ws.close();
136 119 }
137 120  
138 121 // ===================== 子设备属性上报测试 =====================
... ... @@ -142,18 +125,42 @@ public class IotGatewaySubDeviceWebSocketProtocolIntegrationTest {
142 125 */
143 126 @Test
144 127 public void testPropertyPost() throws Exception {
145   - executeAuthenticatedRequest("testPropertyPost", ws -> {
146   - log.info("[testPropertyPost][子设备属性上报 - 请求实际由 Gateway 代为转发]");
147   - return IotDeviceMessage.of(
148   - IdUtil.fastSimpleUUID(),
149   - IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(),
150   - IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
151   - .put("power", 100)
152   - .put("status", "online")
153   - .put("temperature", 36.5)
154   - .build()),
155   - null, null, null);
156   - });
  128 + // 1.1 创建 WebSocket 连接(同步)
  129 + WebSocket ws = createWebSocketConnection();
  130 + log.info("[testPropertyPost][WebSocket 连接成功]");
  131 +
  132 + // 1.2 先进行认证
  133 + IotDeviceMessage authResponse = authenticate(ws);
  134 + log.info("[testPropertyPost][认证响应: {}]", authResponse);
  135 + log.info("[testPropertyPost][子设备属性上报 - 请求实际由 Gateway 代为转发]");
  136 +
  137 + // 2.1 构建属性上报消息
  138 + IotDeviceMessage request = IotDeviceMessage.of(
  139 + IdUtil.fastSimpleUUID(),
  140 + IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(),
  141 + IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
  142 + .put("power", 100)
  143 + .put("status", "online")
  144 + .put("temperature", 36.5)
  145 + .build()),
  146 + null, null, null);
  147 + // 2.2 编码
  148 + byte[] payload = CODEC.encode(request);
  149 + String jsonMessage = StrUtil.utf8Str(payload);
  150 + log.info("[testPropertyPost][Codec: {}, 请求消息: {}]", CODEC.type(), request);
  151 +
  152 + // 3.1 发送并等待响应
  153 + String response = sendAndReceive(ws, jsonMessage);
  154 + // 3.2 解码响应
  155 + if (response != null) {
  156 + IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response));
  157 + log.info("[testPropertyPost][响应消息: {}]", responseMessage);
  158 + } else {
  159 + log.warn("[testPropertyPost][未收到响应]");
  160 + }
  161 +
  162 + // 4. 关闭连接
  163 + ws.close();
157 164 }
158 165  
159 166 // ===================== 子设备事件上报测试 =====================
... ... @@ -163,102 +170,117 @@ public class IotGatewaySubDeviceWebSocketProtocolIntegrationTest {
163 170 */
164 171 @Test
165 172 public void testEventPost() throws Exception {
166   - executeAuthenticatedRequest("testEventPost", ws -> {
167   - log.info("[testEventPost][子设备事件上报 - 请求实际由 Gateway 代为转发]");
168   - return IotDeviceMessage.of(
169   - IdUtil.fastSimpleUUID(),
170   - IotDeviceMessageMethodEnum.EVENT_POST.getMethod(),
171   - IotDeviceEventPostReqDTO.of(
172   - "alarm",
173   - MapUtil.<String, Object>builder()
174   - .put("level", "warning")
175   - .put("message", "temperature too high")
176   - .put("threshold", 40)
177   - .put("current", 42)
178   - .build(),
179   - System.currentTimeMillis()),
180   - null, null, null);
181   - });
  173 + // 1.1 创建 WebSocket 连接(同步)
  174 + WebSocket ws = createWebSocketConnection();
  175 + log.info("[testEventPost][WebSocket 连接成功]");
  176 +
  177 + // 1.2 先进行认证
  178 + IotDeviceMessage authResponse = authenticate(ws);
  179 + log.info("[testEventPost][认证响应: {}]", authResponse);
  180 + log.info("[testEventPost][子设备事件上报 - 请求实际由 Gateway 代为转发]");
  181 +
  182 + // 2.1 构建事件上报消息
  183 + IotDeviceMessage request = IotDeviceMessage.of(
  184 + IdUtil.fastSimpleUUID(),
  185 + IotDeviceMessageMethodEnum.EVENT_POST.getMethod(),
  186 + IotDeviceEventPostReqDTO.of(
  187 + "alarm",
  188 + MapUtil.<String, Object>builder()
  189 + .put("level", "warning")
  190 + .put("message", "temperature too high")
  191 + .put("threshold", 40)
  192 + .put("current", 42)
  193 + .build(),
  194 + System.currentTimeMillis()),
  195 + null, null, null);
  196 + // 2.2 编码
  197 + byte[] payload = CODEC.encode(request);
  198 + String jsonMessage = StrUtil.utf8Str(payload);
  199 + log.info("[testEventPost][Codec: {}, 请求消息: {}]", CODEC.type(), request);
  200 +
  201 + // 3.1 发送并等待响应
  202 + String response = sendAndReceive(ws, jsonMessage);
  203 + // 3.2 解码响应
  204 + if (response != null) {
  205 + IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response));
  206 + log.info("[testEventPost][响应消息: {}]", responseMessage);
  207 + } else {
  208 + log.warn("[testEventPost][未收到响应]");
  209 + }
  210 +
  211 + // 4. 关闭连接
  212 + ws.close();
182 213 }
183 214  
184 215 // ===================== 辅助方法 =====================
185 216  
186 217 /**
187   - * 执行需要认证的请求
  218 + * 创建 WebSocket 连接(同步)
188 219 *
189   - * @param testName 测试名称
190   - * @param requestSupplier 请求消息提供者
  220 + * @return WebSocket 连接
191 221 */
192   - private void executeAuthenticatedRequest(String testName, java.util.function.Function<WebSocket, IotDeviceMessage> requestSupplier) throws Exception {
193   - CountDownLatch latch = new CountDownLatch(2);
194   - AtomicReference<String> authResponseRef = new AtomicReference<>();
195   - AtomicReference<String> businessResponseRef = new AtomicReference<>();
196   -
197   - HttpClient client = vertx.createHttpClient();
  222 + private WebSocket createWebSocketConnection() throws Exception {
  223 + WebSocketClient wsClient = vertx.createWebSocketClient();
198 224 WebSocketConnectOptions options = new WebSocketConnectOptions()
199 225 .setHost(SERVER_HOST)
200 226 .setPort(SERVER_PORT)
201 227 .setURI(WS_PATH);
  228 + return wsClient.connect(options).toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  229 + }
  230 +
  231 + /**
  232 + * 发送消息并等待响应(同步)
  233 + *
  234 + * @param ws WebSocket 连接
  235 + * @param message 请求消息
  236 + * @return 响应消息
  237 + */
  238 + private String sendAndReceive(WebSocket ws, String message) throws Exception {
  239 + CountDownLatch latch = new CountDownLatch(1);
  240 + AtomicReference<String> responseRef = new AtomicReference<>();
202 241  
203   - client.webSocket(options).onComplete(ar -> {
204   - if (ar.succeeded()) {
205   - WebSocket ws = ar.result();
206   - log.info("[{}][WebSocket 连接成功]", testName);
207   -
208   - final boolean[] authenticated = {false};
209   -
210   - ws.textMessageHandler(message -> {
211   - log.info("[{}][收到响应: {}]", testName, message);
212   - if (!authenticated[0]) {
213   - authResponseRef.set(message);
214   - authenticated[0] = true;
215   - latch.countDown();
216   -
217   - // 认证成功后发送业务请求
218   - IotDeviceMessage businessRequest = requestSupplier.apply(ws);
219   - byte[] payload = CODEC.encode(businessRequest);
220   - String jsonMessage = new String(payload, StandardCharsets.UTF_8);
221   - log.info("[{}][发送业务请求: {}]", testName, jsonMessage);
222   - ws.writeTextMessage(jsonMessage);
223   - } else {
224   - businessResponseRef.set(message);
225   - ws.close();
226   - latch.countDown();
227   - }
228   - });
229   -
230   - // 先发送认证请求
231   - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
232   - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
233   - .setClientId(authInfo.getClientId())
234   - .setUsername(authInfo.getUsername())
235   - .setPassword(authInfo.getPassword());
236   - IotDeviceMessage authRequest = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null);
237   -
238   - byte[] payload = CODEC.encode(authRequest);
239   - String jsonMessage = new String(payload, StandardCharsets.UTF_8);
240   - log.info("[{}][发送认证请求: {}]", testName, jsonMessage);
241   - ws.writeTextMessage(jsonMessage);
242   - } else {
243   - log.error("[{}][WebSocket 连接失败]", testName, ar.cause());
244   - latch.countDown();
245   - latch.countDown();
246   - }
  242 + // 设置消息处理器
  243 + ws.textMessageHandler(response -> {
  244 + log.info("[sendAndReceive][收到响应: {}]", response);
  245 + responseRef.set(response);
  246 + latch.countDown();
247 247 });
248 248  
249   - boolean completed = latch.await(TIMEOUT_SECONDS * 2, TimeUnit.SECONDS);
250   - if (completed) {
251   - if (authResponseRef.get() != null) {
252   - IotDeviceMessage authResponse = CODEC.decode(authResponseRef.get().getBytes(StandardCharsets.UTF_8));
253   - log.info("[{}][认证响应: {}]", testName, authResponse);
254   - }
255   - if (businessResponseRef.get() != null) {
256   - IotDeviceMessage businessResponse = CODEC.decode(businessResponseRef.get().getBytes(StandardCharsets.UTF_8));
257   - log.info("[{}][业务响应: {}]", testName, businessResponse);
258   - }
259   - } else {
260   - log.warn("[{}][测试超时]", testName);
  249 + // 发送请求
  250 + log.info("[sendAndReceive][发送请求: {}]", message);
  251 + ws.writeTextMessage(message);
  252 +
  253 + // 等待响应
  254 + boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  255 + if (!completed) {
  256 + log.warn("[sendAndReceive][等待响应超时]");
  257 + }
  258 + return responseRef.get();
  259 + }
  260 +
  261 + /**
  262 + * 执行子设备认证(同步)
  263 + *
  264 + * @param ws WebSocket 连接
  265 + * @return 认证响应消息
  266 + */
  267 + private IotDeviceMessage authenticate(WebSocket ws) throws Exception {
  268 + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
  269 + IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
  270 + .setClientId(authInfo.getClientId())
  271 + .setUsername(authInfo.getUsername())
  272 + .setPassword(authInfo.getPassword());
  273 + IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null);
  274 +
  275 + byte[] payload = CODEC.encode(request);
  276 + String jsonMessage = StrUtil.utf8Str(payload);
  277 + log.info("[authenticate][发送认证请求: {}]", jsonMessage);
  278 +
  279 + String response = sendAndReceive(ws, jsonMessage);
  280 + if (response != null) {
  281 + return CODEC.decode(StrUtil.utf8Bytes(response));
261 282 }
  283 + return null;
262 284 }
263 285  
264 286 }
... ...