Commit 4b67fc2d65e6c4af3661bc5774c51751434830ad

Authored by YunaiV
1 parent eb5fa9fd

feat(iot):tcp 协议完善 IotDirectDeviceUdpProtocolIntegrationTest 单测,并支持动态注册

yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java
@@ -518,9 +518,9 @@ public class IotGatewayProperties { @@ -518,9 +518,9 @@ public class IotGatewayProperties {
518 private Boolean enabled; 518 private Boolean enabled;
519 519
520 /** 520 /**
521 - * 服务端口(默认 8092) 521 + * 服务端口(默认 8093)
522 */ 522 */
523 - private Integer port = 8092; 523 + private Integer port = 8093;
524 524
525 /** 525 /**
526 * 接收缓冲区大小(默认 64KB) 526 * 接收缓冲区大小(默认 64KB)
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/router/IotUdpUpstreamHandler.java
@@ -9,7 +9,11 @@ import cn.iocoder.yudao.framework.common.util.json.JsonUtils; @@ -9,7 +9,11 @@ import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
9 import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; 9 import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
10 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; 10 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
11 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; 11 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
  12 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
12 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 13 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  14 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
  15 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
  16 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
13 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; 17 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
14 import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec; 18 import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec;
15 import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec; 19 import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec;
@@ -129,6 +133,9 @@ public class IotUdpUpstreamHandler { @@ -129,6 +133,9 @@ public class IotUdpUpstreamHandler {
129 if (AUTH_METHOD.equals(message.getMethod())) { 133 if (AUTH_METHOD.equals(message.getMethod())) {
130 // 认证请求 134 // 认证请求
131 handleAuthenticationRequest(message, codecType, senderAddress, socket); 135 handleAuthenticationRequest(message, codecType, senderAddress, socket);
  136 + } else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(message.getMethod())) {
  137 + // 设备动态注册请求
  138 + handleRegisterRequest(message, codecType, senderAddress, socket);
132 } else { 139 } else {
133 // 业务消息 140 // 业务消息
134 handleBusinessRequest(message, codecType, senderAddress, socket); 141 handleBusinessRequest(message, codecType, senderAddress, socket);
@@ -168,7 +175,7 @@ public class IotUdpUpstreamHandler { @@ -168,7 +175,7 @@ public class IotUdpUpstreamHandler {
168 } 175 }
169 176
170 // 2.1 解析设备信息 177 // 2.1 解析设备信息
171 - IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername()); 178 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername());
172 if (deviceInfo == null) { 179 if (deviceInfo == null) {
173 sendErrorResponse(socket, senderAddress, message.getRequestId(), "解析设备信息失败", codecType); 180 sendErrorResponse(socket, senderAddress, message.getRequestId(), "解析设备信息失败", codecType);
174 return; 181 return;
@@ -201,6 +208,45 @@ public class IotUdpUpstreamHandler { @@ -201,6 +208,45 @@ public class IotUdpUpstreamHandler {
201 } 208 }
202 209
203 /** 210 /**
  211 + * 处理设备动态注册请求(一型一密,不需要 Token)
  212 + *
  213 + * @param message 消息信息
  214 + * @param codecType 消息编解码类型
  215 + * @param senderAddress 发送者地址
  216 + * @param socket UDP Socket
  217 + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
  218 + */
  219 + private void handleRegisterRequest(IotDeviceMessage message, String codecType,
  220 + InetSocketAddress senderAddress, DatagramSocket socket) {
  221 + String addressKey = sessionManager.buildAddressKey(senderAddress);
  222 + try {
  223 + // 1. 解析注册参数
  224 + IotDeviceRegisterReqDTO registerParams = parseRegisterParams(message.getParams());
  225 + if (registerParams == null) {
  226 + log.warn("[handleRegisterRequest][注册参数解析失败,来源: {}]", addressKey);
  227 + sendErrorResponse(socket, senderAddress, message.getRequestId(), "注册参数不完整", codecType);
  228 + return;
  229 + }
  230 +
  231 + // 2. 调用动态注册
  232 + CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(registerParams);
  233 + if (result.isError()) {
  234 + log.warn("[handleRegisterRequest][注册失败,来源: {},错误: {}]", addressKey, result.getMsg());
  235 + sendErrorResponse(socket, senderAddress, message.getRequestId(), result.getMsg(), codecType);
  236 + return;
  237 + }
  238 +
  239 + // 3. 发送成功响应(包含 deviceSecret)
  240 + sendRegisterSuccessResponse(socket, senderAddress, message.getRequestId(), result.getData(), codecType);
  241 + log.info("[handleRegisterRequest][注册成功,设备名: {},来源: {}]",
  242 + registerParams.getDeviceName(), addressKey);
  243 + } catch (Exception e) {
  244 + log.error("[handleRegisterRequest][注册处理异常,来源: {}]", addressKey, e);
  245 + sendErrorResponse(socket, senderAddress, message.getRequestId(), "注册处理异常", codecType);
  246 + }
  247 + }
  248 +
  249 + /**
204 * 处理业务请求 250 * 处理业务请求
205 * 251 *
206 * @param message 消息信息 252 * @param message 消息信息
@@ -225,7 +271,7 @@ public class IotUdpUpstreamHandler { @@ -225,7 +271,7 @@ public class IotUdpUpstreamHandler {
225 return; 271 return;
226 } 272 }
227 // 1.2 验证 token,获取设备信息 273 // 1.2 验证 token,获取设备信息
228 - IotDeviceAuthUtils.DeviceInfo deviceInfo = deviceTokenService.verifyToken(token); 274 + IotDeviceIdentity deviceInfo = deviceTokenService.verifyToken(token);
229 if (deviceInfo == null) { 275 if (deviceInfo == null) {
230 log.warn("[handleBusinessRequest][token 无效或已过期,来源: {}]", addressKey); 276 log.warn("[handleBusinessRequest][token 无效或已过期,来源: {}]", addressKey);
231 sendErrorResponse(socket, senderAddress, message.getRequestId(), "token 无效或已过期", codecType); 277 sendErrorResponse(socket, senderAddress, message.getRequestId(), "token 无效或已过期", codecType);
@@ -317,14 +363,15 @@ public class IotUdpUpstreamHandler { @@ -317,14 +363,15 @@ public class IotUdpUpstreamHandler {
317 private void sendAuthSuccessResponse(DatagramSocket socket, InetSocketAddress address, 363 private void sendAuthSuccessResponse(DatagramSocket socket, InetSocketAddress address,
318 String requestId, String token, String codecType) { 364 String requestId, String token, String codecType) {
319 try { 365 try {
  366 + // 构建响应数据
320 Object responseData = MapUtil.builder() 367 Object responseData = MapUtil.builder()
321 .put("success", true) 368 .put("success", true)
322 .put("token", token) 369 .put("token", token)
323 .put("message", "认证成功") 370 .put("message", "认证成功")
324 .build(); 371 .build();
325 -  
326 IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, AUTH_METHOD, responseData, 0, "认证成功"); 372 IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, AUTH_METHOD, responseData, 0, "认证成功");
327 byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType); 373 byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType);
  374 + // 发送响应
328 socket.send(Buffer.buffer(encodedData), address.getPort(), address.getHostString(), result -> { 375 socket.send(Buffer.buffer(encodedData), address.getPort(), address.getHostString(), result -> {
329 if (result.failed()) { 376 if (result.failed()) {
330 log.error("[sendAuthSuccessResponse][发送认证成功响应失败,地址: {}]", 377 log.error("[sendAuthSuccessResponse][发送认证成功响应失败,地址: {}]",
@@ -338,6 +385,41 @@ public class IotUdpUpstreamHandler { @@ -338,6 +385,41 @@ public class IotUdpUpstreamHandler {
338 } 385 }
339 386
340 /** 387 /**
  388 + * 发送注册成功响应(包含 deviceSecret)
  389 + *
  390 + * @param socket UDP Socket
  391 + * @param address 目标地址
  392 + * @param requestId 请求 ID
  393 + * @param registerResp 注册响应
  394 + * @param codecType 消息编解码类型
  395 + */
  396 + private void sendRegisterSuccessResponse(DatagramSocket socket, InetSocketAddress address,
  397 + String requestId, IotDeviceRegisterRespDTO registerResp,
  398 + String codecType) {
  399 + try {
  400 + // 构建响应数据
  401 + Object responseData = MapUtil.builder()
  402 + .put("success", true)
  403 + .put("deviceSecret", registerResp.getDeviceSecret())
  404 + .put("message", "注册成功")
  405 + .build();
  406 + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId,
  407 + IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), responseData, 0, "注册成功");
  408 + // 发送响应
  409 + byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType);
  410 + socket.send(Buffer.buffer(encodedData), address.getPort(), address.getHostString(), result -> {
  411 + if (result.failed()) {
  412 + log.error("[sendRegisterSuccessResponse][发送注册成功响应失败,地址: {}]",
  413 + sessionManager.buildAddressKey(address), result.cause());
  414 + }
  415 + });
  416 + } catch (Exception e) {
  417 + log.error("[sendRegisterSuccessResponse][发送注册成功响应异常,地址: {}]",
  418 + sessionManager.buildAddressKey(address), e);
  419 + }
  420 + }
  421 +
  422 + /**
341 * 发送成功响应 423 * 发送成功响应
342 * 424 *
343 * @param socket UDP Socket 425 * @param socket UDP Socket
@@ -437,4 +519,46 @@ public class IotUdpUpstreamHandler { @@ -437,4 +519,46 @@ public class IotUdpUpstreamHandler {
437 } 519 }
438 } 520 }
439 521
  522 + /**
  523 + * 解析注册参数
  524 + *
  525 + * @param params 参数对象(通常为 Map 类型)
  526 + * @return 注册参数 DTO,解析失败时返回 null
  527 + */
  528 + @SuppressWarnings("unchecked")
  529 + private IotDeviceRegisterReqDTO parseRegisterParams(Object params) {
  530 + if (params == null) {
  531 + return null;
  532 + }
  533 +
  534 + try {
  535 + // 参数默认为 Map 类型,直接转换
  536 + if (params instanceof Map) {
  537 + Map<String, Object> paramMap = (Map<String, Object>) params;
  538 + String productKey = MapUtil.getStr(paramMap, "productKey");
  539 + String deviceName = MapUtil.getStr(paramMap, "deviceName");
  540 + String productSecret = MapUtil.getStr(paramMap, "productSecret");
  541 + if (StrUtil.hasBlank(productKey, deviceName, productSecret)) {
  542 + return null;
  543 + }
  544 + return new IotDeviceRegisterReqDTO()
  545 + .setProductKey(productKey)
  546 + .setDeviceName(deviceName)
  547 + .setProductSecret(productSecret);
  548 + }
  549 +
  550 + // 如果已经是目标类型,直接返回
  551 + if (params instanceof IotDeviceRegisterReqDTO) {
  552 + return (IotDeviceRegisterReqDTO) params;
  553 + }
  554 +
  555 + // 其他情况尝试 JSON 转换
  556 + String jsonStr = JsonUtils.toJsonString(params);
  557 + return JsonUtils.parseObject(jsonStr, IotDeviceRegisterReqDTO.class);
  558 + } catch (Exception e) {
  559 + log.error("[parseRegisterParams][解析注册参数({})失败]", params, e);
  560 + return null;
  561 + }
  562 + }
  563 +
440 } 564 }
yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml
@@ -100,7 +100,7 @@ yudao: @@ -100,7 +100,7 @@ yudao:
100 # ==================================== 100 # ====================================
101 udp: 101 udp:
102 enabled: false # 是否启用 UDP 102 enabled: false # 是否启用 UDP
103 - port: 8092 # UDP 服务端口 103 + port: 8093 # UDP 服务端口
104 receive-buffer-size: 65536 # 接收缓冲区大小(字节,默认 64KB) 104 receive-buffer-size: 65536 # 接收缓冲区大小(字节,默认 64KB)
105 send-buffer-size: 65536 # 发送缓冲区大小(字节,默认 64KB) 105 send-buffer-size: 65536 # 发送缓冲区大小(字节,默认 64KB)
106 session-timeout-ms: 60000 # 会话超时时间(毫秒,默认 60 秒) 106 session-timeout-ms: 60000 # 会话超时时间(毫秒,默认 60 秒)
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpProtocolIntegrationTest.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotDirectDeviceUdpProtocolIntegrationTest.java
@@ -3,7 +3,12 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.udp; @@ -3,7 +3,12 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.udp;
3 import cn.hutool.core.map.MapUtil; 3 import cn.hutool.core.map.MapUtil;
4 import cn.hutool.core.util.IdUtil; 4 import cn.hutool.core.util.IdUtil;
5 import cn.iocoder.yudao.framework.common.util.json.JsonUtils; 5 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  6 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
6 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; 7 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
  8 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
  9 +import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
  10 +import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
  11 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
7 import lombok.extern.slf4j.Slf4j; 12 import lombok.extern.slf4j.Slf4j;
8 import org.junit.jupiter.api.Test; 13 import org.junit.jupiter.api.Test;
9 14
@@ -11,123 +16,197 @@ import java.net.DatagramPacket; @@ -11,123 +16,197 @@ import java.net.DatagramPacket;
11 import java.net.DatagramSocket; 16 import java.net.DatagramSocket;
12 import java.net.InetAddress; 17 import java.net.InetAddress;
13 import java.nio.charset.StandardCharsets; 18 import java.nio.charset.StandardCharsets;
  19 +import java.util.HashMap;
  20 +import java.util.Map;
14 21
15 /** 22 /**
16 - * IoT 网关 UDP 协议集成测试(手动测试) 23 + * IoT 直连设备 UDP 协议集成测试(手动测试)
  24 + *
  25 + * <p>测试场景:直连设备(IotProductDeviceTypeEnum 的 DIRECT 类型)通过 UDP 协议直接连接平台
17 * 26 *
18 * <p>使用步骤: 27 * <p>使用步骤:
19 * <ol> 28 * <ol>
20 * <li>启动 yudao-module-iot-gateway 服务(UDP 端口 8092)</li> 29 * <li>启动 yudao-module-iot-gateway 服务(UDP 端口 8092)</li>
21 - * <li>运行 {@link #testAuth()} 获取 token,将返回的 token 粘贴到 {@link #TOKEN} 常量</li>  
22 - * <li>运行 {@link #testPropertyPost()} 测试属性上报,或运行 {@link #testEventPost()} 测试事件上报</li> 30 + * <li>运行 {@link #testDeviceRegister()} 测试直连设备动态注册(一型一密)</li>
  31 + * <li>运行 {@link #testAuth()} 获取设备 token,将返回的 token 粘贴到 {@link #TOKEN} 常量</li>
  32 + * <li>运行以下测试方法:
  33 + * <ul>
  34 + * <li>{@link #testPropertyPost()} - 设备属性上报</li>
  35 + * <li>{@link #testEventPost()} - 设备事件上报</li>
  36 + * </ul>
  37 + * </li>
23 * </ol> 38 * </ol>
24 * 39 *
  40 + * <p>注意:UDP 协议是无状态的,每次请求需要在 params 中携带 token(与 HTTP 通过 Header 传递不同)
  41 + *
25 * @author 芋道源码 42 * @author 芋道源码
26 */ 43 */
27 @Slf4j 44 @Slf4j
28 -public class IotUdpProtocolIntegrationTest { 45 +public class IotDirectDeviceUdpProtocolIntegrationTest {
29 46
30 private static final String SERVER_HOST = "127.0.0.1"; 47 private static final String SERVER_HOST = "127.0.0.1";
31 - private static final int SERVER_PORT = 8092; 48 + private static final int SERVER_PORT = 8093;
32 private static final int TIMEOUT_MS = 5000; 49 private static final int TIMEOUT_MS = 5000;
33 50
34 - // 设备信息(根据实际情况修改 PRODUCT_KEY、DEVICE_NAME、PASSWORD) 51 + // ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
35 private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT"; 52 private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT";
36 private static final String DEVICE_NAME = "small"; 53 private static final String DEVICE_NAME = "small";
37 - private static final String PASSWORD = "509e2b08f7598eb139d276388c600435913ba4c94cd0d50aebc5c0d1855bcb75";  
38 -  
39 - // TODO @芋艿:1、IotDeviceAuthUtils 调整下拼接;2、password 的生成;3、后续给 http 也整个单测;4、后续给 tcp 也整个单测;5、后续给 mqtt 也整个单测;6、后续给 emqp 也整个单测  
40 - private static final String CLIENT_ID = PRODUCT_KEY + "." + DEVICE_NAME;  
41 - private static final String USERNAME = DEVICE_NAME + "&" + PRODUCT_KEY; 54 + private static final String DEVICE_SECRET = "0baa4c2ecc104ae1a26b4070c218bdf3";
42 55
43 /** 56 /**
44 - * 设备 Token:从 {@link #testAuth()} 方法获取后,粘贴到这里 57 + * 直连设备 Token:从 {@link #testAuth()} 方法获取后,粘贴到这里
45 */ 58 */
46 - private static final String TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoiNGF5bVpnT1RPT0NyREtSVCIsImV4cCI6MTc2OTMxMTY0NiwiZGV2aWNlTmFtZSI6InNtYWxsIn0.re6LCaRfKiE9VQTP3w0Brh2ScVIgrvN3H96z_snndoM"; 59 + private static final String TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoiNGF5bVpnT1RPT0NyREtSVCIsImV4cCI6MTc2OTk0ODYzOCwiZGV2aWNlTmFtZSI6InNtYWxsIn0.TrOJisXhloZ3quLBOAIyowmpq6Syp9PHiEpfj-nQ9xo";
  60 +
  61 + // ===================== 认证测试 =====================
47 62
48 /** 63 /**
49 * 认证测试:获取设备 Token 64 * 认证测试:获取设备 Token
50 */ 65 */
51 @Test 66 @Test
52 public void testAuth() throws Exception { 67 public void testAuth() throws Exception {
  68 + // 1.1 构建请求
  69 + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
  70 + IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
  71 + .setClientId(authInfo.getClientId())
  72 + .setUsername(authInfo.getUsername())
  73 + .setPassword(authInfo.getPassword());
53 String payload = JsonUtils.toJsonString(MapUtil.builder() 74 String payload = JsonUtils.toJsonString(MapUtil.builder()
54 .put("id", IdUtil.fastSimpleUUID()) 75 .put("id", IdUtil.fastSimpleUUID())
55 .put("method", "auth") 76 .put("method", "auth")
56 - .put("params", MapUtil.builder()  
57 - .put("clientId", CLIENT_ID)  
58 - .put("username", USERNAME)  
59 - .put("password", PASSWORD)  
60 - .build()) 77 + .put("params", authReqDTO)
61 .build()); 78 .build());
  79 + // 1.2 输出请求
  80 + log.info("[testAuth][请求体: {}]", payload);
62 81
  82 + // 2.1 发送请求
63 try (DatagramSocket socket = new DatagramSocket()) { 83 try (DatagramSocket socket = new DatagramSocket()) {
64 socket.setSoTimeout(TIMEOUT_MS); 84 socket.setSoTimeout(TIMEOUT_MS);
65 -  
66 - log.info("[testAuth][请求体: {}]", payload);  
67 -  
68 String response = sendAndReceive(socket, payload); 85 String response = sendAndReceive(socket, payload);
69 - 86 + // 2.2 输出结果
70 log.info("[testAuth][响应体: {}]", response); 87 log.info("[testAuth][响应体: {}]", response);
71 log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]"); 88 log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]");
72 } 89 }
73 } 90 }
74 91
  92 + // ===================== 直连设备属性上报测试 =====================
  93 +
75 /** 94 /**
76 * 属性上报测试 95 * 属性上报测试
77 */ 96 */
78 @Test 97 @Test
79 public void testPropertyPost() throws Exception { 98 public void testPropertyPost() throws Exception {
  99 + // 1.1 构建请求(UDP 协议:token 放在 params 中)
80 String payload = JsonUtils.toJsonString(MapUtil.builder() 100 String payload = JsonUtils.toJsonString(MapUtil.builder()
81 .put("id", IdUtil.fastSimpleUUID()) 101 .put("id", IdUtil.fastSimpleUUID())
82 .put("method", IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod()) 102 .put("method", IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod())
83 .put("version", "1.0") 103 .put("version", "1.0")
84 - .put("params", MapUtil.builder()  
85 - .put("token", TOKEN) 104 + .put("params", withToken(IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
86 .put("width", 1) 105 .put("width", 1)
87 .put("height", "2") 106 .put("height", "2")
88 - .build()) 107 + .build())))
89 .build()); 108 .build());
  109 + // 1.2 输出请求
  110 + log.info("[testPropertyPost][请求体: {}]", payload);
90 111
  112 + // 2.1 发送请求
91 try (DatagramSocket socket = new DatagramSocket()) { 113 try (DatagramSocket socket = new DatagramSocket()) {
92 socket.setSoTimeout(TIMEOUT_MS); 114 socket.setSoTimeout(TIMEOUT_MS);
93 -  
94 - log.info("[testPropertyPost][请求体: {}]", payload);  
95 -  
96 String response = sendAndReceive(socket, payload); 115 String response = sendAndReceive(socket, payload);
97 - 116 + // 2.2 输出结果
98 log.info("[testPropertyPost][响应体: {}]", response); 117 log.info("[testPropertyPost][响应体: {}]", response);
99 } 118 }
100 } 119 }
101 120
  121 + // ===================== 直连设备事件上报测试 =====================
  122 +
102 /** 123 /**
103 * 事件上报测试 124 * 事件上报测试
104 */ 125 */
105 @Test 126 @Test
106 public void testEventPost() throws Exception { 127 public void testEventPost() throws Exception {
  128 + // 1.1 构建请求(UDP 协议:token 放在 params 中)
107 String payload = JsonUtils.toJsonString(MapUtil.builder() 129 String payload = JsonUtils.toJsonString(MapUtil.builder()
108 .put("id", IdUtil.fastSimpleUUID()) 130 .put("id", IdUtil.fastSimpleUUID())
109 .put("method", IotDeviceMessageMethodEnum.EVENT_POST.getMethod()) 131 .put("method", IotDeviceMessageMethodEnum.EVENT_POST.getMethod())
110 .put("version", "1.0") 132 .put("version", "1.0")
111 - .put("params", MapUtil.builder()  
112 - .put("identifier", "eat")  
113 - .put("value", MapUtil.builder()  
114 - .put("width", 1)  
115 - .put("height", "2")  
116 - .put("oneThree", "3")  
117 - .build())  
118 - .put("time", System.currentTimeMillis())  
119 - .build()) 133 + .put("params", withToken(IotDeviceEventPostReqDTO.of(
  134 + "eat",
  135 + MapUtil.<String, Object>builder().put("rice", 3).build(),
  136 + System.currentTimeMillis())))
120 .build()); 137 .build());
  138 + // 1.2 输出请求
  139 + log.info("[testEventPost][请求体: {}]", payload);
121 140
  141 + // 2.1 发送请求
122 try (DatagramSocket socket = new DatagramSocket()) { 142 try (DatagramSocket socket = new DatagramSocket()) {
123 socket.setSoTimeout(TIMEOUT_MS); 143 socket.setSoTimeout(TIMEOUT_MS);
  144 + String response = sendAndReceive(socket, payload);
  145 + // 2.2 输出结果
  146 + log.info("[testEventPost][响应体: {}]", response);
  147 + }
  148 + }
124 149
125 - log.info("[testEventPost][请求体: {}]", payload); 150 + // ===================== 动态注册测试 =====================
126 151
  152 + /**
  153 + * 直连设备动态注册测试(一型一密)
  154 + * <p>
  155 + * 使用产品密钥(productSecret)验证身份,成功后返回设备密钥(deviceSecret)
  156 + * <p>
  157 + * 注意:此接口不需要 Token 认证
  158 + */
  159 + @Test
  160 + public void testDeviceRegister() throws Exception {
  161 + // 1.1 构建请求参数
  162 + IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO();
  163 + reqDTO.setProductKey(PRODUCT_KEY);
  164 + reqDTO.setDeviceName("test-" + System.currentTimeMillis());
  165 + reqDTO.setProductSecret("test-product-secret");
  166 + // 1.2 构建请求
  167 + String payload = JsonUtils.toJsonString(MapUtil.builder()
  168 + .put("id", IdUtil.fastSimpleUUID())
  169 + .put("method", IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod())
  170 + .put("params", reqDTO)
  171 + .build());
  172 + // 1.3 输出请求
  173 + log.info("[testDeviceRegister][请求体: {}]", payload);
  174 +
  175 + // 2.1 发送请求
  176 + try (DatagramSocket socket = new DatagramSocket()) {
  177 + socket.setSoTimeout(TIMEOUT_MS);
127 String response = sendAndReceive(socket, payload); 178 String response = sendAndReceive(socket, payload);
  179 + // 2.2 输出结果
  180 + log.info("[testDeviceRegister][响应体: {}]", response);
  181 + log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]");
  182 + }
  183 + }
128 184
129 - log.info("[testEventPost][响应体: {}]", response); 185 + // ===================== 辅助方法 =====================
  186 +
  187 + /**
  188 + * 将 token 添加到 params 中
  189 + * <p>
  190 + * 支持 Map 或普通对象,通过 JSON 转换统一处理
  191 + *
  192 + * @param params 原始参数(Map 或对象)
  193 + * @return 添加了 token 的 Map
  194 + */
  195 + @SuppressWarnings("unchecked")
  196 + private Map<String, Object> withToken(Object params) {
  197 + // 1. 转成 Map
  198 + Map<String, Object> map;
  199 + if (params instanceof Map) {
  200 + map = new HashMap<>((Map<String, Object>) params);
  201 + } else {
  202 + // 对象转 Map(通过 JSON 序列化再反序列化)
  203 + map = JsonUtils.parseObject(JsonUtils.toJsonString(params), Map.class);
  204 + }
  205 + // 2. 添加 token
  206 + if (map != null) {
  207 + map.put("token", TOKEN);
130 } 208 }
  209 + return map;
131 } 210 }
132 211
133 /** 212 /**