Commit 7ac6c02e76842eec7c1ceeb9acc24b7fd0b2b1f9

Authored by YunaiV
1 parent 9be7d6fa

feat:【iot】UDP 协议:1)兼容 TOKEN 在 list 的情况,基于 token、body 拆分;2)新增网关设备的单测;3)去掉 udp 默认响应,避免响应多次。

yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/router/IotUdpUpstreamHandler.java
@@ -52,6 +52,10 @@ public class IotUdpUpstreamHandler { @@ -52,6 +52,10 @@ public class IotUdpUpstreamHandler {
52 * Token 参数 Key 52 * Token 参数 Key
53 */ 53 */
54 private static final String PARAM_KEY_TOKEN = "token"; 54 private static final String PARAM_KEY_TOKEN = "token";
  55 + /**
  56 + * Body 参数 Key(实际请求内容)
  57 + */
  58 + private static final String PARAM_KEY_BODY = "body";
55 59
56 private final IotDeviceMessageService deviceMessageService; 60 private final IotDeviceMessageService deviceMessageService;
57 61
@@ -248,6 +252,10 @@ public class IotUdpUpstreamHandler { @@ -248,6 +252,10 @@ public class IotUdpUpstreamHandler {
248 252
249 /** 253 /**
250 * 处理业务请求 254 * 处理业务请求
  255 + * <p>
  256 + * 请求参数格式:
  257 + * - token:JWT 令牌
  258 + * - body:实际请求内容(可以是 Map、List 或其他类型)
251 * 259 *
252 * @param message 消息信息 260 * @param message 消息信息
253 * @param codecType 消息编解码类型 261 * @param codecType 消息编解码类型
@@ -259,11 +267,13 @@ public class IotUdpUpstreamHandler { @@ -259,11 +267,13 @@ public class IotUdpUpstreamHandler {
259 InetSocketAddress senderAddress, DatagramSocket socket) { 267 InetSocketAddress senderAddress, DatagramSocket socket) {
260 String addressKey = sessionManager.buildAddressKey(senderAddress); 268 String addressKey = sessionManager.buildAddressKey(senderAddress);
261 try { 269 try {
262 - // 1.1 从消息中提取 token(无状态:消息体携带 token) 270 + // 1.1 从消息中提取 token 和 body(格式:{token: "xxx", body: {...}} 或 {token: "xxx", body: [...]})
263 String token = null; 271 String token = null;
  272 + Object body = null;
264 if (message.getParams() instanceof Map) { 273 if (message.getParams() instanceof Map) {
265 Map<String, Object> paramsMap = (Map<String, Object>) message.getParams(); 274 Map<String, Object> paramsMap = (Map<String, Object>) message.getParams();
266 - token = (String) paramsMap.remove(PARAM_KEY_TOKEN); 275 + token = (String) paramsMap.get(PARAM_KEY_TOKEN);
  276 + body = paramsMap.get(PARAM_KEY_BODY);
267 } 277 }
268 if (StrUtil.isBlank(token)) { 278 if (StrUtil.isBlank(token)) {
269 log.warn("[handleBusinessRequest][缺少 token,来源: {}]", addressKey); 279 log.warn("[handleBusinessRequest][缺少 token,来源: {}]", addressKey);
@@ -291,12 +301,10 @@ public class IotUdpUpstreamHandler { @@ -291,12 +301,10 @@ public class IotUdpUpstreamHandler {
291 // 3. 更新设备地址映射(保持最新) 301 // 3. 更新设备地址映射(保持最新)
292 sessionManager.updateDeviceAddress(device.getId(), senderAddress); 302 sessionManager.updateDeviceAddress(device.getId(), senderAddress);
293 303
294 - // 4. 发送消息到消息总线 304 + // 4. 将 body 设置为实际的 params,发送消息到消息总线
  305 + message.setParams(body);
295 deviceMessageService.sendDeviceMessage(message, device.getProductKey(), 306 deviceMessageService.sendDeviceMessage(message, device.getProductKey(),
296 device.getDeviceName(), serverId); 307 device.getDeviceName(), serverId);
297 -  
298 - // 5. 发送成功响应  
299 - sendSuccessResponse(socket, senderAddress, message.getRequestId(), "处理成功", codecType);  
300 log.debug("[handleBusinessRequest][业务消息处理成功,设备 ID: {},方法: {},来源: {}]", 308 log.debug("[handleBusinessRequest][业务消息处理成功,设备 ID: {},方法: {},来源: {}]",
301 device.getId(), message.getMethod(), addressKey); 309 device.getId(), message.getMethod(), addressKey);
302 } catch (Exception e) { 310 } catch (Exception e) {
@@ -420,21 +428,6 @@ public class IotUdpUpstreamHandler { @@ -420,21 +428,6 @@ public class IotUdpUpstreamHandler {
420 } 428 }
421 429
422 /** 430 /**
423 - * 发送成功响应  
424 - *  
425 - * @param socket UDP Socket  
426 - * @param address 目标地址  
427 - * @param requestId 请求 ID  
428 - * @param message 消息  
429 - * @param codecType 消息编解码类型  
430 - */  
431 - @SuppressWarnings("SameParameterValue")  
432 - private void sendSuccessResponse(DatagramSocket socket, InetSocketAddress address,  
433 - String requestId, String message, String codecType) {  
434 - sendResponse(socket, address, true, message, requestId, codecType);  
435 - }  
436 -  
437 - /**  
438 * 发送错误响应 431 * 发送错误响应
439 * 432 *
440 * @param socket UDP Socket 433 * @param socket UDP Socket
@@ -458,18 +451,20 @@ public class IotUdpUpstreamHandler { @@ -458,18 +451,20 @@ public class IotUdpUpstreamHandler {
458 * @param requestId 请求 ID 451 * @param requestId 请求 ID
459 * @param codecType 消息编解码类型 452 * @param codecType 消息编解码类型
460 */ 453 */
  454 + @SuppressWarnings("SameParameterValue")
461 private void sendResponse(DatagramSocket socket, InetSocketAddress address, boolean success, 455 private void sendResponse(DatagramSocket socket, InetSocketAddress address, boolean success,
462 String message, String requestId, String codecType) { 456 String message, String requestId, String codecType) {
463 try { 457 try {
  458 + // 构建响应数据
464 Object responseData = MapUtil.builder() 459 Object responseData = MapUtil.builder()
465 .put("success", success) 460 .put("success", success)
466 .put("message", message) 461 .put("message", message)
467 .build(); 462 .build();
468 -  
469 int code = success ? 0 : 401; 463 int code = success ? 0 : 401;
470 - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, "response", responseData,  
471 - code, message); 464 + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId,
  465 + "response", responseData, code, message);
472 466
  467 + // 发送响应
473 byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType); 468 byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType);
474 socket.send(Buffer.buffer(encodedData), address.getPort(), address.getHostString(), ar -> { 469 socket.send(Buffer.buffer(encodedData), address.getPort(), address.getHostString(), ar -> {
475 if (ar.failed()) { 470 if (ar.failed()) {
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotDirectDeviceUdpProtocolIntegrationTest.java
@@ -109,7 +109,7 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { @@ -109,7 +109,7 @@ public class IotDirectDeviceUdpProtocolIntegrationTest {
109 // 1.2 输出请求 109 // 1.2 输出请求
110 log.info("[testPropertyPost][请求体: {}]", payload); 110 log.info("[testPropertyPost][请求体: {}]", payload);
111 111
112 - // 2.1 发送请求 112 + // 2. 发送请求
113 try (DatagramSocket socket = new DatagramSocket()) { 113 try (DatagramSocket socket = new DatagramSocket()) {
114 socket.setSoTimeout(TIMEOUT_MS); 114 socket.setSoTimeout(TIMEOUT_MS);
115 String response = sendAndReceive(socket, payload); 115 String response = sendAndReceive(socket, payload);
@@ -185,30 +185,23 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { @@ -185,30 +185,23 @@ public class IotDirectDeviceUdpProtocolIntegrationTest {
185 // ===================== 辅助方法 ===================== 185 // ===================== 辅助方法 =====================
186 186
187 /** 187 /**
188 - * 将 token 添加到 params 中 188 + * 构建带 token 的 params
189 * <p> 189 * <p>
190 - * 支持 Map 或普通对象,通过 JSON 转换统一处理 190 + * 返回格式:{token: "xxx", body: params}
  191 + * - token:JWT 令牌
  192 + * - body:实际请求内容(可以是 Map、List 或其他类型)
191 * 193 *
192 - * @param params 原始参数(Map 或对象)  
193 - * @return 添加了 token 的 Map 194 + * @param params 原始参数(Map、List 或对象)
  195 + * @return 包含 token 和 body 的 Map
194 */ 196 */
195 - @SuppressWarnings("unchecked")  
196 private Map<String, Object> withToken(Object params) { 197 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);  
208 - }  
209 - return map; 198 + Map<String, Object> result = new HashMap<>();
  199 + result.put("token", TOKEN);
  200 + result.put("body", params);
  201 + return result;
210 } 202 }
211 203
  204 +
212 /** 205 /**
213 * 发送 UDP 请求并接收响应 206 * 发送 UDP 请求并接收响应
214 * 207 *
@@ -216,7 +209,7 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { @@ -216,7 +209,7 @@ public class IotDirectDeviceUdpProtocolIntegrationTest {
216 * @param payload 请求体 209 * @param payload 请求体
217 * @return 响应内容 210 * @return 响应内容
218 */ 211 */
219 - private String sendAndReceive(DatagramSocket socket, String payload) throws Exception { 212 + public static String sendAndReceive(DatagramSocket socket, String payload) throws Exception {
220 byte[] sendData = payload.getBytes(StandardCharsets.UTF_8); 213 byte[] sendData = payload.getBytes(StandardCharsets.UTF_8);
221 InetAddress address = InetAddress.getByName(SERVER_HOST); 214 InetAddress address = InetAddress.getByName(SERVER_HOST);
222 215
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotGatewayDeviceUdpProtocolIntegrationTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.udp;
  2 +
  3 +import cn.hutool.core.map.MapUtil;
  4 +import cn.hutool.core.util.IdUtil;
  5 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  6 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
  7 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
  8 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
  9 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO;
  10 +import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPackPostReqDTO;
  11 +import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO;
  12 +import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO;
  13 +import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetReqDTO;
  14 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  15 +import lombok.extern.slf4j.Slf4j;
  16 +import org.junit.jupiter.api.Test;
  17 +
  18 +import java.net.DatagramSocket;
  19 +import java.util.Collections;
  20 +import java.util.HashMap;
  21 +import java.util.List;
  22 +import java.util.Map;
  23 +
  24 +import static cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotDirectDeviceUdpProtocolIntegrationTest.sendAndReceive;
  25 +
  26 +/**
  27 + * IoT 网关设备 UDP 协议集成测试(手动测试)
  28 + *
  29 + * <p>测试场景:网关设备(IotProductDeviceTypeEnum 的 GATEWAY 类型)通过 UDP 协议管理子设备拓扑关系
  30 + *
  31 + * <p>使用步骤:
  32 + * <ol>
  33 + * <li>启动 yudao-module-iot-gateway 服务(UDP 端口 8093)</li>
  34 + * <li>运行 {@link #testAuth()} 获取网关设备 token,将返回的 token 粘贴到 {@link #GATEWAY_TOKEN} 常量</li>
  35 + * <li>运行以下测试方法:
  36 + * <ul>
  37 + * <li>{@link #testTopoAdd()} - 添加子设备拓扑关系</li>
  38 + * <li>{@link #testTopoDelete()} - 删除子设备拓扑关系</li>
  39 + * <li>{@link #testTopoGet()} - 获取子设备拓扑关系</li>
  40 + * <li>{@link #testSubDeviceRegister()} - 子设备动态注册</li>
  41 + * <li>{@link #testPropertyPackPost()} - 批量上报属性(网关 + 子设备)</li>
  42 + * </ul>
  43 + * </li>
  44 + * </ol>
  45 + *
  46 + * <p>注意:UDP 协议是无状态的,每次请求需要在 params 中携带 token(与 HTTP 通过 Header 传递不同)
  47 + *
  48 + * @author 芋道源码
  49 + */
  50 +@Slf4j
  51 +public class IotGatewayDeviceUdpProtocolIntegrationTest {
  52 +
  53 + private static final String SERVER_HOST = "127.0.0.1";
  54 + private static final int SERVER_PORT = 8093;
  55 + private static final int TIMEOUT_MS = 5000;
  56 +
  57 + // ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) =====================
  58 + private static final String GATEWAY_PRODUCT_KEY = "m6XcS1ZJ3TW8eC0v";
  59 + private static final String GATEWAY_DEVICE_NAME = "sub-ddd";
  60 + private static final String GATEWAY_DEVICE_SECRET = "b3d62c70f8a4495487ed1d35d61ac2b3";
  61 +
  62 + /**
  63 + * 网关设备 Token:从 {@link #testAuth()} 方法获取后,粘贴到这里
  64 + */
  65 + private static final String GATEWAY_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoibTZYY1MxWkozVFc4ZUMwdiIsImV4cCI6MTc2OTk1NDcxNSwiZGV2aWNlTmFtZSI6InN1Yi1kZGQifQ.Vg5iateNrpg0FVQI2eJomggxrYXGpwug8wsz9BsVr5w";
  66 +
  67 + // ===================== 子设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
  68 + private static final String SUB_DEVICE_PRODUCT_KEY = "jAufEMTF1W6wnPhn";
  69 + private static final String SUB_DEVICE_NAME = "chazuo-it";
  70 + private static final String SUB_DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af";
  71 +
  72 + // ===================== 认证测试 =====================
  73 +
  74 + /**
  75 + * 网关设备认证测试:获取网关设备 Token
  76 + */
  77 + @Test
  78 + public void testAuth() throws Exception {
  79 + // 1.1 构建请求
  80 + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(
  81 + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET);
  82 + IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
  83 + .setClientId(authInfo.getClientId())
  84 + .setUsername(authInfo.getUsername())
  85 + .setPassword(authInfo.getPassword());
  86 + String payload = JsonUtils.toJsonString(MapUtil.builder()
  87 + .put("id", IdUtil.fastSimpleUUID())
  88 + .put("method", "auth")
  89 + .put("params", authReqDTO)
  90 + .build());
  91 + // 1.2 输出请求
  92 + log.info("[testAuth][请求体: {}]", payload);
  93 +
  94 + // 2.1 发送请求
  95 + try (DatagramSocket socket = new DatagramSocket()) {
  96 + socket.setSoTimeout(TIMEOUT_MS);
  97 + String response = sendAndReceive(socket, payload);
  98 + // 2.2 输出结果
  99 + log.info("[testAuth][响应体: {}]", response);
  100 + log.info("[testAuth][请将返回的 token 复制到 GATEWAY_TOKEN 常量中]");
  101 + }
  102 + }
  103 +
  104 + // ===================== 拓扑管理测试 =====================
  105 +
  106 + /**
  107 + * 添加子设备拓扑关系测试
  108 + * <p>
  109 + * 网关设备向平台上报需要绑定的子设备信息
  110 + */
  111 + @Test
  112 + public void testTopoAdd() throws Exception {
  113 + // 1.1 构建子设备认证信息
  114 + IotDeviceAuthReqDTO subAuthInfo = IotDeviceAuthUtils.getAuthInfo(
  115 + SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET);
  116 + IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO()
  117 + .setClientId(subAuthInfo.getClientId())
  118 + .setUsername(subAuthInfo.getUsername())
  119 + .setPassword(subAuthInfo.getPassword());
  120 + // 1.2 构建请求参数
  121 + IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO();
  122 + params.setSubDevices(Collections.singletonList(subDeviceAuth));
  123 + String payload = JsonUtils.toJsonString(MapUtil.builder()
  124 + .put("id", IdUtil.fastSimpleUUID())
  125 + .put("method", IotDeviceMessageMethodEnum.TOPO_ADD.getMethod())
  126 + .put("version", "1.0")
  127 + .put("params", withToken(params))
  128 + .build());
  129 + // 1.3 输出请求
  130 + log.info("[testTopoAdd][请求体: {}]", payload);
  131 +
  132 + // 2.1 发送请求
  133 + try (DatagramSocket socket = new DatagramSocket()) {
  134 + socket.setSoTimeout(TIMEOUT_MS);
  135 + String response = sendAndReceive(socket, payload);
  136 + // 2.2 输出结果
  137 + log.info("[testTopoAdd][响应体: {}]", response);
  138 + }
  139 + }
  140 +
  141 + /**
  142 + * 删除子设备拓扑关系测试
  143 + * <p>
  144 + * 网关设备向平台上报需要解绑的子设备信息
  145 + */
  146 + @Test
  147 + public void testTopoDelete() throws Exception {
  148 + // 1.1 构建请求参数
  149 + IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO();
  150 + params.setSubDevices(Collections.singletonList(
  151 + new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)));
  152 + String payload = JsonUtils.toJsonString(MapUtil.builder()
  153 + .put("id", IdUtil.fastSimpleUUID())
  154 + .put("method", IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod())
  155 + .put("version", "1.0")
  156 + .put("params", withToken(params))
  157 + .build());
  158 + // 1.2 输出请求
  159 + log.info("[testTopoDelete][请求体: {}]", payload);
  160 +
  161 + // 2.1 发送请求
  162 + try (DatagramSocket socket = new DatagramSocket()) {
  163 + socket.setSoTimeout(TIMEOUT_MS);
  164 + String response = sendAndReceive(socket, payload);
  165 + // 2.2 输出结果
  166 + log.info("[testTopoDelete][响应体: {}]", response);
  167 + }
  168 + }
  169 +
  170 + /**
  171 + * 获取子设备拓扑关系测试
  172 + * <p>
  173 + * 网关设备向平台查询已绑定的子设备列表
  174 + */
  175 + @Test
  176 + public void testTopoGet() throws Exception {
  177 + // 1.1 构建请求参数(目前为空,预留扩展)
  178 + IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO();
  179 + String payload = JsonUtils.toJsonString(MapUtil.builder()
  180 + .put("id", IdUtil.fastSimpleUUID())
  181 + .put("method", IotDeviceMessageMethodEnum.TOPO_GET.getMethod())
  182 + .put("version", "1.0")
  183 + .put("params", withToken(params))
  184 + .build());
  185 + // 1.2 输出请求
  186 + log.info("[testTopoGet][请求体: {}]", payload);
  187 +
  188 + // 2.1 发送请求
  189 + try (DatagramSocket socket = new DatagramSocket()) {
  190 + socket.setSoTimeout(TIMEOUT_MS);
  191 + String response = sendAndReceive(socket, payload);
  192 + // 2.2 输出结果
  193 + log.info("[testTopoGet][响应体: {}]", response);
  194 + }
  195 + }
  196 +
  197 + // ===================== 子设备注册测试 =====================
  198 +
  199 + /**
  200 + * 子设备动态注册测试
  201 + * <p>
  202 + * 网关设备代理子设备进行动态注册,平台返回子设备的 deviceSecret
  203 + * <p>
  204 + * 注意:此接口需要网关 Token 认证
  205 + */
  206 + @Test
  207 + public void testSubDeviceRegister() throws Exception {
  208 + // 1.1 构建请求参数
  209 + IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO();
  210 + subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY);
  211 + subDevice.setDeviceName("mougezishebei");
  212 + String payload = JsonUtils.toJsonString(MapUtil.builder()
  213 + .put("id", IdUtil.fastSimpleUUID())
  214 + .put("method", IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod())
  215 + .put("version", "1.0")
  216 + .put("params", withToken(Collections.singletonList(subDevice)))
  217 + .build());
  218 + // 1.2 输出请求
  219 + log.info("[testSubDeviceRegister][请求体: {}]", payload);
  220 +
  221 + // 2.1 发送请求
  222 + try (DatagramSocket socket = new DatagramSocket()) {
  223 + socket.setSoTimeout(TIMEOUT_MS);
  224 + String response = sendAndReceive(socket, payload);
  225 + // 2.2 输出结果
  226 + log.info("[testSubDeviceRegister][响应体: {}]", response);
  227 + }
  228 + }
  229 +
  230 + // ===================== 批量上报测试 =====================
  231 +
  232 + /**
  233 + * 批量上报属性测试(网关 + 子设备)
  234 + * <p>
  235 + * 网关设备批量上报自身属性、事件,以及子设备的属性、事件
  236 + */
  237 + @Test
  238 + public void testPropertyPackPost() throws Exception {
  239 + // 1.1 构建【网关设备】自身属性
  240 + Map<String, Object> gatewayProperties = MapUtil.<String, Object>builder()
  241 + .put("temperature", 25.5)
  242 + .build();
  243 + // 1.2 构建【网关设备】自身事件
  244 + IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue();
  245 + gatewayEvent.setValue(MapUtil.builder().put("message", "gateway started").build());
  246 + gatewayEvent.setTime(System.currentTimeMillis());
  247 + Map<String, IotDevicePropertyPackPostReqDTO.EventValue> gatewayEvents = MapUtil.<String, IotDevicePropertyPackPostReqDTO.EventValue>builder()
  248 + .put("statusReport", gatewayEvent)
  249 + .build();
  250 + // 1.3 构建【网关子设备】属性
  251 + Map<String, Object> subDeviceProperties = MapUtil.<String, Object>builder()
  252 + .put("power", 100)
  253 + .build();
  254 + // 1.4 构建【网关子设备】事件
  255 + IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue();
  256 + subDeviceEvent.setValue(MapUtil.builder().put("errorCode", 0).build());
  257 + subDeviceEvent.setTime(System.currentTimeMillis());
  258 + Map<String, IotDevicePropertyPackPostReqDTO.EventValue> subDeviceEvents = MapUtil.<String, IotDevicePropertyPackPostReqDTO.EventValue>builder()
  259 + .put("healthCheck", subDeviceEvent)
  260 + .build();
  261 + // 1.5 构建子设备数据
  262 + IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData();
  263 + subDeviceData.setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME));
  264 + subDeviceData.setProperties(subDeviceProperties);
  265 + subDeviceData.setEvents(subDeviceEvents);
  266 + // 1.6 构建请求参数
  267 + IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO();
  268 + params.setProperties(gatewayProperties);
  269 + params.setEvents(gatewayEvents);
  270 + params.setSubDevices(List.of(subDeviceData));
  271 + String payload = JsonUtils.toJsonString(MapUtil.builder()
  272 + .put("id", IdUtil.fastSimpleUUID())
  273 + .put("method", IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod())
  274 + .put("version", "1.0")
  275 + .put("params", withToken(params))
  276 + .build());
  277 + // 1.7 输出请求
  278 + log.info("[testPropertyPackPost][请求体: {}]", payload);
  279 +
  280 + // 2.1 发送请求
  281 + try (DatagramSocket socket = new DatagramSocket()) {
  282 + socket.setSoTimeout(TIMEOUT_MS);
  283 + String response = sendAndReceive(socket, payload);
  284 + // 2.2 输出结果
  285 + log.info("[testPropertyPackPost][响应体: {}]", response);
  286 + }
  287 + }
  288 +
  289 + // ===================== 辅助方法 =====================
  290 +
  291 + /**
  292 + * 构建带 token 的 params
  293 + * <p>
  294 + * 返回格式:{token: "xxx", body: params}
  295 + * - token:JWT 令牌
  296 + * - body:实际请求内容(可以是 Map、List 或其他类型)
  297 + *
  298 + * @param params 原始参数(Map、List 或对象)
  299 + * @return 包含 token 和 body 的 Map
  300 + */
  301 + private Map<String, Object> withToken(Object params) {
  302 + Map<String, Object> result = new HashMap<>();
  303 + result.put("token", GATEWAY_TOKEN);
  304 + result.put("body", params);
  305 + return result;
  306 + }
  307 +
  308 +}
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotGatewaySubDeviceUdpProtocolIntegrationTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.udp;
  2 +
  3 +import cn.hutool.core.map.MapUtil;
  4 +import cn.hutool.core.util.IdUtil;
  5 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  6 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
  7 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
  8 +import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
  9 +import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
  10 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  11 +import lombok.extern.slf4j.Slf4j;
  12 +import org.junit.jupiter.api.Test;
  13 +
  14 +import java.net.DatagramSocket;
  15 +import java.util.HashMap;
  16 +import java.util.Map;
  17 +
  18 +import static cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotDirectDeviceUdpProtocolIntegrationTest.sendAndReceive;
  19 +
  20 +/**
  21 + * IoT 网关子设备 UDP 协议集成测试(手动测试)
  22 + *
  23 + * <p>测试场景:子设备(IotProductDeviceTypeEnum 的 SUB 类型)通过网关设备代理上报数据
  24 + *
  25 + * <p><b>重要说明:子设备无法直接连接平台,所有请求均由网关设备(Gateway)代为转发。</b>
  26 + * <p>网关设备转发子设备请求时,Token 使用子设备自己的信息。
  27 + *
  28 + * <p>使用步骤:
  29 + * <ol>
  30 + * <li>启动 yudao-module-iot-gateway 服务(UDP 端口 8093)</li>
  31 + * <li>确保子设备已通过 {@link IotGatewayDeviceUdpProtocolIntegrationTest#testTopoAdd()} 绑定到网关</li>
  32 + * <li>运行 {@link #testAuth()} 获取子设备 token,将返回的 token 粘贴到 {@link #TOKEN} 常量</li>
  33 + * <li>运行以下测试方法:
  34 + * <ul>
  35 + * <li>{@link #testPropertyPost()} - 子设备属性上报(由网关代理转发)</li>
  36 + * <li>{@link #testEventPost()} - 子设备事件上报(由网关代理转发)</li>
  37 + * </ul>
  38 + * </li>
  39 + * </ol>
  40 + *
  41 + * <p>注意:UDP 协议是无状态的,每次请求需要在 params 中携带 token(与 HTTP 通过 Header 传递不同)
  42 + *
  43 + * @author 芋道源码
  44 + */
  45 +@Slf4j
  46 +public class IotGatewaySubDeviceUdpProtocolIntegrationTest {
  47 +
  48 + private static final String SERVER_HOST = "127.0.0.1";
  49 + private static final int SERVER_PORT = 8093;
  50 + private static final int TIMEOUT_MS = 5000;
  51 +
  52 + // ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
  53 + private static final String PRODUCT_KEY = "jAufEMTF1W6wnPhn";
  54 + private static final String DEVICE_NAME = "chazuo-it";
  55 + private static final String DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af";
  56 +
  57 + /**
  58 + * 网关子设备 Token:从 {@link #testAuth()} 方法获取后,粘贴到这里
  59 + */
  60 + private static final String TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoiakF1ZkVNVEYxVzZ3blBobiIsImV4cCI6MTc2OTk1NDY3OSwiZGV2aWNlTmFtZSI6ImNoYXp1by1pdCJ9.jfbUAoU0xkJl4UvO-NUvcJ6yITPRgUjQ4MKATPuwneg";
  61 +
  62 + // ===================== 认证测试 =====================
  63 +
  64 + /**
  65 + * 子设备认证测试:获取子设备 Token
  66 + */
  67 + @Test
  68 + public void testAuth() throws Exception {
  69 + // 1.1 构建请求
  70 + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
  71 + IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
  72 + .setClientId(authInfo.getClientId())
  73 + .setUsername(authInfo.getUsername())
  74 + .setPassword(authInfo.getPassword());
  75 + String payload = JsonUtils.toJsonString(MapUtil.builder()
  76 + .put("id", IdUtil.fastSimpleUUID())
  77 + .put("method", "auth")
  78 + .put("params", authReqDTO)
  79 + .build());
  80 + // 1.2 输出请求
  81 + log.info("[testAuth][请求体: {}]", payload);
  82 +
  83 + // 2.1 发送请求
  84 + try (DatagramSocket socket = new DatagramSocket()) {
  85 + socket.setSoTimeout(TIMEOUT_MS);
  86 + String response = sendAndReceive(socket, payload);
  87 + // 2.2 输出结果
  88 + log.info("[testAuth][响应体: {}]", response);
  89 + log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]");
  90 + }
  91 + }
  92 +
  93 + // ===================== 子设备属性上报测试 =====================
  94 +
  95 + /**
  96 + * 子设备属性上报测试
  97 + */
  98 + @Test
  99 + public void testPropertyPost() throws Exception {
  100 + // 1.1 构建请求(UDP 协议:token 放在 params 中)
  101 + String payload = JsonUtils.toJsonString(MapUtil.builder()
  102 + .put("id", IdUtil.fastSimpleUUID())
  103 + .put("method", IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod())
  104 + .put("version", "1.0")
  105 + .put("params", withToken(IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
  106 + .put("power", 100)
  107 + .put("status", "online")
  108 + .put("temperature", 36.5)
  109 + .build())))
  110 + .build());
  111 + // 1.2 输出请求
  112 + log.info("[testPropertyPost][子设备属性上报 - 请求实际由 Gateway 代为转发]");
  113 + log.info("[testPropertyPost][请求体: {}]", payload);
  114 +
  115 + // 2.1 发送请求
  116 + try (DatagramSocket socket = new DatagramSocket()) {
  117 + socket.setSoTimeout(TIMEOUT_MS);
  118 + String response = sendAndReceive(socket, payload);
  119 + // 2.2 输出结果
  120 + log.info("[testPropertyPost][响应体: {}]", response);
  121 + }
  122 + }
  123 +
  124 + // ===================== 子设备事件上报测试 =====================
  125 +
  126 + /**
  127 + * 子设备事件上报测试
  128 + */
  129 + @Test
  130 + public void testEventPost() throws Exception {
  131 + // 1.1 构建请求(UDP 协议:token 放在 params 中)
  132 + String payload = JsonUtils.toJsonString(MapUtil.builder()
  133 + .put("id", IdUtil.fastSimpleUUID())
  134 + .put("method", IotDeviceMessageMethodEnum.EVENT_POST.getMethod())
  135 + .put("version", "1.0")
  136 + .put("params", withToken(IotDeviceEventPostReqDTO.of(
  137 + "alarm",
  138 + MapUtil.<String, Object>builder()
  139 + .put("level", "warning")
  140 + .put("message", "temperature too high")
  141 + .put("threshold", 40)
  142 + .put("current", 42)
  143 + .build(),
  144 + System.currentTimeMillis())))
  145 + .build());
  146 + // 1.2 输出请求
  147 + log.info("[testEventPost][子设备事件上报 - 请求实际由 Gateway 代为转发]");
  148 + log.info("[testEventPost][请求体: {}]", payload);
  149 +
  150 + // 2.1 发送请求
  151 + try (DatagramSocket socket = new DatagramSocket()) {
  152 + socket.setSoTimeout(TIMEOUT_MS);
  153 + String response = sendAndReceive(socket, payload);
  154 + // 2.2 输出结果
  155 + log.info("[testEventPost][响应体: {}]", response);
  156 + }
  157 + }
  158 +
  159 + // ===================== 辅助方法 =====================
  160 +
  161 + /**
  162 + * 构建带 token 的 params
  163 + * <p>
  164 + * 返回格式:{token: "xxx", body: params}
  165 + * - token:JWT 令牌
  166 + * - body:实际请求内容(可以是 Map、List 或其他类型)
  167 + *
  168 + * @param params 原始参数(Map、List 或对象)
  169 + * @return 包含 token 和 body 的 Map
  170 + */
  171 + private Map<String, Object> withToken(Object params) {
  172 + Map<String, Object> result = new HashMap<>();
  173 + result.put("token", TOKEN);
  174 + result.put("body", params);
  175 + return result;
  176 + }
  177 +
  178 +}