Commit 1309be39c3da7a4a28fcb0e6260816c49af0d497

Authored by YunaiV
1 parent 679b696a

feat(iot):【网关设备:80%】整体流程跑通,并增加 IotGatewayDeviceHttpProtocolIntegrationTest、IotGa…

…tewaySubDeviceHttpProtocolIntegrationTest 单测
Showing 11 changed files with 507 additions and 35 deletions
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceController.java
@@ -64,7 +64,7 @@ public class IotDeviceController { @@ -64,7 +64,7 @@ public class IotDeviceController {
64 @Operation(summary = "绑定子设备到网关") 64 @Operation(summary = "绑定子设备到网关")
65 @PreAuthorize("@ss.hasPermission('iot:device:update')") 65 @PreAuthorize("@ss.hasPermission('iot:device:update')")
66 public CommonResult<Boolean> bindDeviceGateway(@Valid @RequestBody IotDeviceBindGatewayReqVO reqVO) { 66 public CommonResult<Boolean> bindDeviceGateway(@Valid @RequestBody IotDeviceBindGatewayReqVO reqVO) {
67 - deviceService.bindDeviceGateway(reqVO.getIds(), reqVO.getGatewayId()); 67 + deviceService.bindDeviceGateway(reqVO.getSubIds(), reqVO.getGatewayId());
68 return success(true); 68 return success(true);
69 } 69 }
70 70
@@ -72,7 +72,7 @@ public class IotDeviceController { @@ -72,7 +72,7 @@ public class IotDeviceController {
72 @Operation(summary = "解绑子设备与网关") 72 @Operation(summary = "解绑子设备与网关")
73 @PreAuthorize("@ss.hasPermission('iot:device:update')") 73 @PreAuthorize("@ss.hasPermission('iot:device:update')")
74 public CommonResult<Boolean> unbindDeviceGateway(@Valid @RequestBody IotDeviceUnbindGatewayReqVO reqVO) { 74 public CommonResult<Boolean> unbindDeviceGateway(@Valid @RequestBody IotDeviceUnbindGatewayReqVO reqVO) {
75 - deviceService.unbindDeviceGateway(reqVO.getIds(), reqVO.getGatewayId()); 75 + deviceService.unbindDeviceGateway(reqVO.getSubIds(), reqVO.getGatewayId());
76 return success(true); 76 return success(true);
77 } 77 }
78 78
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceBindGatewayReqVO.java
@@ -13,7 +13,7 @@ public class IotDeviceBindGatewayReqVO { @@ -13,7 +13,7 @@ public class IotDeviceBindGatewayReqVO {
13 13
14 @Schema(description = "子设备编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3") 14 @Schema(description = "子设备编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3")
15 @NotEmpty(message = "子设备编号列表不能为空") 15 @NotEmpty(message = "子设备编号列表不能为空")
16 - private Set<Long> ids; 16 + private Set<Long> subIds;
17 17
18 @Schema(description = "网关设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") 18 @Schema(description = "网关设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
19 @NotNull(message = "网关设备编号不能为空") 19 @NotNull(message = "网关设备编号不能为空")
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceUnbindGatewayReqVO.java
@@ -13,7 +13,7 @@ public class IotDeviceUnbindGatewayReqVO { @@ -13,7 +13,7 @@ public class IotDeviceUnbindGatewayReqVO {
13 13
14 @Schema(description = "子设备编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3") 14 @Schema(description = "子设备编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3")
15 @NotEmpty(message = "子设备编号列表不能为空") 15 @NotEmpty(message = "子设备编号列表不能为空")
16 - private Set<Long> ids; 16 + private Set<Long> subIds;
17 17
18 @Schema(description = "网关设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") 18 @Schema(description = "网关设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
19 @NotNull(message = "网关设备编号不能为空") 19 @NotNull(message = "网关设备编号不能为空")
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java
@@ -297,18 +297,18 @@ public interface IotDeviceService { @@ -297,18 +297,18 @@ public interface IotDeviceService {
297 /** 297 /**
298 * 绑定子设备到网关 298 * 绑定子设备到网关
299 * 299 *
300 - * @param ids 子设备编号列表 300 + * @param subIds 子设备编号列表
301 * @param gatewayId 网关设备编号 301 * @param gatewayId 网关设备编号
302 */ 302 */
303 - void bindDeviceGateway(Collection<Long> ids, Long gatewayId); 303 + void bindDeviceGateway(Collection<Long> subIds, Long gatewayId);
304 304
305 /** 305 /**
306 * 解绑子设备与网关 306 * 解绑子设备与网关
307 * 307 *
308 - * @param ids 子设备编号列表 308 + * @param subIds 子设备编号列表
309 * @param gatewayId 网关设备编号 309 * @param gatewayId 网关设备编号
310 */ 310 */
311 - void unbindDeviceGateway(Collection<Long> ids, Long gatewayId); 311 + void unbindDeviceGateway(Collection<Long> subIds, Long gatewayId);
312 312
313 /** 313 /**
314 * 获取未绑定网关的子设备分页 314 * 获取未绑定网关的子设备分页
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java
@@ -518,13 +518,6 @@ public class IotDeviceServiceImpl implements IotDeviceService { @@ -518,13 +518,6 @@ public class IotDeviceServiceImpl implements IotDeviceService {
518 log.error("[authDevice][设备({}/{}) 密码不正确]", productKey, deviceName); 518 log.error("[authDevice][设备({}/{}) 密码不正确]", productKey, deviceName);
519 return false; 519 return false;
520 } 520 }
521 -  
522 - // 3. 校验子设备拓扑关系:子设备必须先绑定到某网关才能认证上线  
523 - if (IotProductDeviceTypeEnum.isGatewaySub(device.getDeviceType())  
524 - && device.getGatewayId() == null) {  
525 - log.warn("[authDevice][子设备({}/{}) 未绑定到任何网关,认证失败]", productKey, deviceName);  
526 - return false;  
527 - }  
528 return true; 521 return true;
529 } 522 }
530 523
@@ -577,14 +570,14 @@ public class IotDeviceServiceImpl implements IotDeviceService { @@ -577,14 +570,14 @@ public class IotDeviceServiceImpl implements IotDeviceService {
577 570
578 @Override 571 @Override
579 @Transactional(rollbackFor = Exception.class) 572 @Transactional(rollbackFor = Exception.class)
580 - public void bindDeviceGateway(Collection<Long> ids, Long gatewayId) {  
581 - if (CollUtil.isEmpty(ids)) { 573 + public void bindDeviceGateway(Collection<Long> subIds, Long gatewayId) {
  574 + if (CollUtil.isEmpty(subIds)) {
582 return; 575 return;
583 } 576 }
584 // 1.1 校验网关设备存在且类型正确 577 // 1.1 校验网关设备存在且类型正确
585 validateGatewayDeviceExists(gatewayId); 578 validateGatewayDeviceExists(gatewayId);
586 // 1.2 校验每个设备是否可绑定 579 // 1.2 校验每个设备是否可绑定
587 - List<IotDeviceDO> devices = deviceMapper.selectByIds(ids); 580 + List<IotDeviceDO> devices = deviceMapper.selectByIds(subIds);
588 for (IotDeviceDO device : devices) { 581 for (IotDeviceDO device : devices) {
589 checkSubDeviceCanBind(device, gatewayId); 582 checkSubDeviceCanBind(device, gatewayId);
590 } 583 }
@@ -613,13 +606,13 @@ public class IotDeviceServiceImpl implements IotDeviceService { @@ -613,13 +606,13 @@ public class IotDeviceServiceImpl implements IotDeviceService {
613 606
614 @Override 607 @Override
615 @Transactional(rollbackFor = Exception.class) 608 @Transactional(rollbackFor = Exception.class)
616 - public void unbindDeviceGateway(Collection<Long> ids, Long gatewayId) { 609 + public void unbindDeviceGateway(Collection<Long> subIds, Long gatewayId) {
617 // 1. 校验设备存在 610 // 1. 校验设备存在
618 - if (CollUtil.isEmpty(ids)) { 611 + if (CollUtil.isEmpty(subIds)) {
619 return; 612 return;
620 } 613 }
621 - List<IotDeviceDO> devices = deviceMapper.selectByIds(ids);  
622 - devices.removeIf(device -> device.getGatewayId() == null); 614 + List<IotDeviceDO> devices = deviceMapper.selectByIds(subIds);
  615 + devices.removeIf(device -> ObjUtil.notEqual(device.getGatewayId(), gatewayId));
623 if (CollUtil.isEmpty(devices)) { 616 if (CollUtil.isEmpty(devices)) {
624 return; 617 return;
625 } 618 }
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageService.java
@@ -7,7 +7,6 @@ import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsD @@ -7,7 +7,6 @@ import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsD
7 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 7 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
8 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; 8 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
9 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceMessageDO; 9 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceMessageDO;
10 -import jakarta.validation.constraints.NotEmpty;  
11 import jakarta.validation.constraints.NotNull; 10 import jakarta.validation.constraints.NotNull;
12 11
13 import javax.annotation.Nullable; 12 import javax.annotation.Nullable;
@@ -75,7 +74,7 @@ public interface IotDeviceMessageService { @@ -75,7 +74,7 @@ public interface IotDeviceMessageService {
75 */ 74 */
76 List<IotDeviceMessageDO> getDeviceMessageListByRequestIdsAndReply( 75 List<IotDeviceMessageDO> getDeviceMessageListByRequestIdsAndReply(
77 @NotNull(message = "设备编号不能为空") Long deviceId, 76 @NotNull(message = "设备编号不能为空") Long deviceId,
78 - @NotEmpty(message = "请求编号不能为空") List<String> requestIds, 77 + List<String> requestIds,
79 Boolean reply); 78 Boolean reply);
80 79
81 /** 80 /**
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageServiceImpl.java
1 package cn.iocoder.yudao.module.iot.service.device.message; 1 package cn.iocoder.yudao.module.iot.service.device.message;
2 2
3 import cn.hutool.core.collection.CollUtil; 3 import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.collection.ListUtil;
4 import cn.hutool.core.date.LocalDateTimeUtil; 5 import cn.hutool.core.date.LocalDateTimeUtil;
5 import cn.hutool.core.lang.Assert; 6 import cn.hutool.core.lang.Assert;
6 import cn.hutool.core.map.MapUtil; 7 import cn.hutool.core.map.MapUtil;
@@ -103,7 +104,6 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { @@ -103,7 +104,6 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
103 return sendDeviceMessage(message, device); 104 return sendDeviceMessage(message, device);
104 } 105 }
105 106
106 - // TODO @芋艿:针对连接网关的设备,是不是 productKey、deviceName 需要调整下;  
107 @Override 107 @Override
108 public IotDeviceMessage sendDeviceMessage(IotDeviceMessage message, IotDeviceDO device) { 108 public IotDeviceMessage sendDeviceMessage(IotDeviceMessage message, IotDeviceDO device) {
109 return sendDeviceMessage(message, device, null); 109 return sendDeviceMessage(message, device, null);
@@ -273,7 +273,8 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { @@ -273,7 +273,8 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
273 log.warn("[handlePackMessage][子设备({}/{}) 不存在]", identity.getProductKey(), identity.getDeviceName()); 273 log.warn("[handlePackMessage][子设备({}/{}) 不存在]", identity.getProductKey(), identity.getDeviceName());
274 continue; 274 continue;
275 } 275 }
276 - sendDevicePackData(subDevice, packMessage.getServerId(), subDeviceData.getProperties(), subDeviceData.getEvents()); 276 + // 特殊:子设备不需要指定 serverId,因为子设备实际可能连接在不同的 gateway-server 上,导致 serverId 不同
  277 + sendDevicePackData(subDevice, null, subDeviceData.getProperties(), subDeviceData.getEvents());
277 } catch (Exception ex) { 278 } catch (Exception ex) {
278 log.error("[handlePackMessage][子设备({}/{}) 数据处理失败]", subDeviceData.getIdentity().getProductKey(), 279 log.error("[handlePackMessage][子设备({}/{}) 数据处理失败]", subDeviceData.getIdentity().getProductKey(),
279 subDeviceData.getIdentity().getDeviceName(), ex); 280 subDeviceData.getIdentity().getDeviceName(), ex);
@@ -335,9 +336,10 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { @@ -335,9 +336,10 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
335 } 336 }
336 337
337 @Override 338 @Override
338 - public List<IotDeviceMessageDO> getDeviceMessageListByRequestIdsAndReply(Long deviceId,  
339 - List<String> requestIds,  
340 - Boolean reply) { 339 + public List<IotDeviceMessageDO> getDeviceMessageListByRequestIdsAndReply(Long deviceId, List<String> requestIds, Boolean reply) {
  340 + if (CollUtil.isEmpty(requestIds)) {
  341 + return ListUtil.of();
  342 + }
341 return deviceMessageMapper.selectListByRequestIdsAndReply(deviceId, requestIds, reply); 343 return deviceMessageMapper.selectListByRequestIdsAndReply(deviceId, requestIds, reply);
342 } 344 }
343 345
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceAuthUtils.java
@@ -72,7 +72,7 @@ public class IotDeviceAuthUtils { @@ -72,7 +72,7 @@ public class IotDeviceAuthUtils {
72 .digestHex(content); 72 .digestHex(content);
73 } 73 }
74 74
75 - public static String buildContent(String clientId, String productKey, String deviceName, String deviceSecret) { 75 + private static String buildContent(String clientId, String productKey, String deviceName, String deviceSecret) {
76 return "clientId" + clientId + 76 return "clientId" + clientId +
77 "deviceName" + deviceName + 77 "deviceName" + deviceName +
78 "deviceSecret" + deviceSecret + 78 "deviceSecret" + deviceSecret +
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotDirectDeviceHttpProtocolIntegrationTest.java
@@ -21,9 +21,14 @@ import org.junit.jupiter.api.Test; @@ -21,9 +21,14 @@ import org.junit.jupiter.api.Test;
21 * 21 *
22 * <p>使用步骤: 22 * <p>使用步骤:
23 * <ol> 23 * <ol>
24 - * <li>启动 yudao-module-iot-gateway 服务(HTTP 端口 8082)</li>  
25 - * <li>运行 {@link #testAuth()} 获取 token,将返回的 token 粘贴到 {@link #TOKEN} 常量</li>  
26 - * <li>运行 {@link #testPropertyPost()} 测试属性上报,或运行 {@link #testEventPost()} 测试事件上报</li> 24 + * <li>启动 yudao-module-iot-gateway 服务(HTTP 端口 8092)</li>
  25 + * <li>运行 {@link #testAuth()} 获取设备 token,将返回的 token 粘贴到 {@link #TOKEN} 常量</li>
  26 + * <li>运行以下测试方法:
  27 + * <ul>
  28 + * <li>{@link #testPropertyPost()} - 设备属性上报</li>
  29 + * <li>{@link #testEventPost()} - 设备事件上报</li>
  30 + * </ul>
  31 + * </li>
27 * </ol> 32 * </ol>
28 * 33 *
29 * @author 芋道源码 34 * @author 芋道源码
@@ -35,16 +40,18 @@ public class IotDirectDeviceHttpProtocolIntegrationTest { @@ -35,16 +40,18 @@ public class IotDirectDeviceHttpProtocolIntegrationTest {
35 private static final String SERVER_HOST = "127.0.0.1"; 40 private static final String SERVER_HOST = "127.0.0.1";
36 private static final int SERVER_PORT = 8092; 41 private static final int SERVER_PORT = 8092;
37 42
38 - // 设备信息(根据实际情况修改 PRODUCT_KEY、DEVICE_NAME、DEVICE_SECRET,从 iot_device 表查询) 43 + // ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
39 private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT"; 44 private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT";
40 private static final String DEVICE_NAME = "small"; 45 private static final String DEVICE_NAME = "small";
41 private static final String DEVICE_SECRET = "0baa4c2ecc104ae1a26b4070c218bdf3"; 46 private static final String DEVICE_SECRET = "0baa4c2ecc104ae1a26b4070c218bdf3";
42 47
43 /** 48 /**
44 - * 设备 Token:从 {@link #testAuth()} 方法获取后,粘贴到这里 49 + * 直连设备 Token:从 {@link #testAuth()} 方法获取后,粘贴到这里
45 */ 50 */
46 private static final String TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoiNGF5bVpnT1RPT0NyREtSVCIsImV4cCI6MTc2OTMwNTA1NSwiZGV2aWNlTmFtZSI6InNtYWxsIn0.mf3MEATCn5bp6cXgULunZjs8d00RGUxj96JEz0hMS7k"; 51 private static final String TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoiNGF5bVpnT1RPT0NyREtSVCIsImV4cCI6MTc2OTMwNTA1NSwiZGV2aWNlTmFtZSI6InNtYWxsIn0.mf3MEATCn5bp6cXgULunZjs8d00RGUxj96JEz0hMS7k";
47 52
  53 + // ===================== 认证测试 =====================
  54 +
48 /** 55 /**
49 * 认证测试:获取设备 Token 56 * 认证测试:获取设备 Token
50 */ 57 */
@@ -69,6 +76,8 @@ public class IotDirectDeviceHttpProtocolIntegrationTest { @@ -69,6 +76,8 @@ public class IotDirectDeviceHttpProtocolIntegrationTest {
69 log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]"); 76 log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]");
70 } 77 }
71 78
  79 + // ===================== 直连设备属性上报测试 =====================
  80 +
72 /** 81 /**
73 * 属性上报测试 82 * 属性上报测试
74 */ 83 */
@@ -101,6 +110,8 @@ public class IotDirectDeviceHttpProtocolIntegrationTest { @@ -101,6 +110,8 @@ public class IotDirectDeviceHttpProtocolIntegrationTest {
101 } 110 }
102 } 111 }
103 112
  113 + // ===================== 直连设备事件上报测试 =====================
  114 +
104 /** 115 /**
105 * 事件上报测试 116 * 事件上报测试
106 */ 117 */
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotGatewayDeviceHttpProtocolIntegrationTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.http;
  2 +
  3 +import cn.hutool.core.map.MapUtil;
  4 +import cn.hutool.core.util.IdUtil;
  5 +import cn.hutool.http.HttpResponse;
  6 +import cn.hutool.http.HttpUtil;
  7 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  8 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
  9 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
  10 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
  11 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO;
  12 +import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPackPostReqDTO;
  13 +import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO;
  14 +import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO;
  15 +import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetReqDTO;
  16 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  17 +import lombok.extern.slf4j.Slf4j;
  18 +import org.junit.jupiter.api.Test;
  19 +
  20 +import java.util.Collections;
  21 +import java.util.List;
  22 +import java.util.Map;
  23 +
  24 +
  25 +/**
  26 + * IoT 网关设备 HTTP 协议集成测试(手动测试)
  27 + *
  28 + * <p>测试场景:网关设备(IotProductDeviceTypeEnum 的 GATEWAY 类型)通过 HTTP 协议管理子设备拓扑关系
  29 + *
  30 + * <p>使用步骤:
  31 + * <ol>
  32 + * <li>启动 yudao-module-iot-gateway 服务(HTTP 端口 8092)</li>
  33 + * <li>运行 {@link #testAuth()} 获取网关设备 token,将返回的 token 粘贴到 {@link #GATEWAY_TOKEN} 常量</li>
  34 + * <li>运行以下测试方法:
  35 + * <ul>
  36 + * <li>{@link #testTopoAdd()} - 添加子设备拓扑关系</li>
  37 + * <li>{@link #testTopoDelete()} - 删除子设备拓扑关系</li>
  38 + * <li>{@link #testTopoGet()} - 获取子设备拓扑关系</li>
  39 + * <li>{@link #testSubDeviceRegister()} - 子设备动态注册</li>
  40 + * <li>{@link #testPropertyPackPost()} - 批量上报属性(网关 + 子设备)</li>
  41 + * </ul>
  42 + * </li>
  43 + * </ol>
  44 + *
  45 + * @author 芋道源码
  46 + */
  47 +@Slf4j
  48 +@SuppressWarnings("HttpUrlsUsage")
  49 +public class IotGatewayDeviceHttpProtocolIntegrationTest {
  50 +
  51 + private static final String SERVER_HOST = "127.0.0.1";
  52 + private static final int SERVER_PORT = 8092;
  53 +
  54 + // ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) =====================
  55 + private static final String GATEWAY_PRODUCT_KEY = "m6XcS1ZJ3TW8eC0v";
  56 + private static final String GATEWAY_DEVICE_NAME = "sub-ddd";
  57 + private static final String GATEWAY_DEVICE_SECRET = "b3d62c70f8a4495487ed1d35d61ac2b3";
  58 +
  59 + /**
  60 + * 网关设备 Token:从 {@link #testAuth()} 方法获取后,粘贴到这里
  61 + */
  62 + private static final String GATEWAY_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoibTZYY1MxWkozVFc4ZUMwdiIsImV4cCI6MTc2OTg2NjY3OCwiZGV2aWNlTmFtZSI6InN1Yi1kZGQifQ.nCLSAfHEjXLtTDRXARjOoFqpuo5WfArjFWweUAzrjKU";
  63 +
  64 + // ===================== 子设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
  65 + private static final String SUB_DEVICE_PRODUCT_KEY = "jAufEMTF1W6wnPhn";
  66 + private static final String SUB_DEVICE_NAME = "chazuo-it";
  67 + private static final String SUB_DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af";
  68 +
  69 + // ===================== 认证测试 =====================
  70 +
  71 + /**
  72 + * 网关设备认证测试:获取网关设备 Token
  73 + */
  74 + @Test
  75 + public void testAuth() {
  76 + // 1.1 构建请求
  77 + String url = String.format("http://%s:%d/auth", SERVER_HOST, SERVER_PORT);
  78 + IotDeviceAuthUtils.AuthInfo authInfo = IotDeviceAuthUtils.getAuthInfo(
  79 + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET);
  80 + IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
  81 + .setClientId(authInfo.getClientId())
  82 + .setUsername(authInfo.getUsername())
  83 + .setPassword(authInfo.getPassword());
  84 + String payload = JsonUtils.toJsonString(authReqDTO);
  85 + // 1.2 输出请求
  86 + log.info("[testAuth][请求 URL: {}]", url);
  87 + log.info("[testAuth][请求体: {}]", payload);
  88 +
  89 + // 2.1 发送请求
  90 + String response = HttpUtil.post(url, payload);
  91 + // 2.2 输出结果
  92 + log.info("[testAuth][响应体: {}]", response);
  93 + log.info("[testAuth][请将返回的 token 复制到 GATEWAY_TOKEN 常量中]");
  94 + }
  95 +
  96 + // ===================== 拓扑管理测试 =====================
  97 +
  98 + // TODO @芋艿:待测试
  99 +
  100 + /**
  101 + * 添加子设备拓扑关系测试
  102 + * <p>
  103 + * 网关设备向平台上报需要绑定的子设备信息
  104 + */
  105 + @Test
  106 + public void testTopoAdd() {
  107 + // 1.1 构建请求
  108 + String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/topo/add",
  109 + SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
  110 + // 1.2 构建子设备认证信息
  111 + IotDeviceAuthUtils.AuthInfo subAuthInfo = IotDeviceAuthUtils.getAuthInfo(
  112 + SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET);
  113 + IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO()
  114 + .setClientId(subAuthInfo.getClientId())
  115 + .setUsername(subAuthInfo.getUsername())
  116 + .setPassword(subAuthInfo.getPassword());
  117 + // 1.3 构建请求参数
  118 + IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO();
  119 + params.setSubDevices(Collections.singletonList(subDeviceAuth));
  120 + String payload = JsonUtils.toJsonString(MapUtil.builder()
  121 + .put("id", IdUtil.fastSimpleUUID())
  122 + .put("method", IotDeviceMessageMethodEnum.TOPO_ADD.getMethod())
  123 + .put("version", "1.0")
  124 + .put("params", params)
  125 + .build());
  126 + // 1.4 输出请求
  127 + log.info("[testTopoAdd][请求 URL: {}]", url);
  128 + log.info("[testTopoAdd][请求体: {}]", payload);
  129 +
  130 + // 2.1 发送请求
  131 + try (HttpResponse httpResponse = HttpUtil.createPost(url)
  132 + .header("Authorization", GATEWAY_TOKEN)
  133 + .body(payload)
  134 + .execute()) {
  135 + // 2.2 输出结果
  136 + log.info("[testTopoAdd][响应体: {}]", httpResponse.body());
  137 + }
  138 + }
  139 +
  140 + /**
  141 + * 删除子设备拓扑关系测试
  142 + * <p>
  143 + * 网关设备向平台上报需要解绑的子设备信息
  144 + */
  145 + @Test
  146 + public void testTopoDelete() {
  147 + // 1.1 构建请求
  148 + String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/topo/delete",
  149 + SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
  150 + // 1.2 构建请求参数
  151 + IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO();
  152 + params.setSubDevices(Collections.singletonList(
  153 + new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)));
  154 + String payload = JsonUtils.toJsonString(MapUtil.builder()
  155 + .put("id", IdUtil.fastSimpleUUID())
  156 + .put("method", IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod())
  157 + .put("version", "1.0")
  158 + .put("params", params)
  159 + .build());
  160 + // 1.3 输出请求
  161 + log.info("[testTopoDelete][请求 URL: {}]", url);
  162 + log.info("[testTopoDelete][请求体: {}]", payload);
  163 +
  164 + // 2.1 发送请求
  165 + try (HttpResponse httpResponse = HttpUtil.createPost(url)
  166 + .header("Authorization", GATEWAY_TOKEN)
  167 + .body(payload)
  168 + .execute()) {
  169 + // 2.2 输出结果
  170 + log.info("[testTopoDelete][响应体: {}]", httpResponse.body());
  171 + }
  172 + }
  173 +
  174 + /**
  175 + * 获取子设备拓扑关系测试
  176 + * <p>
  177 + * 网关设备向平台查询已绑定的子设备列表
  178 + */
  179 + @Test
  180 + public void testTopoGet() {
  181 + // 1.1 构建请求
  182 + String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/topo/get",
  183 + SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
  184 + // 1.2 构建请求参数(目前为空,预留扩展)
  185 + IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO();
  186 + String payload = JsonUtils.toJsonString(MapUtil.builder()
  187 + .put("id", IdUtil.fastSimpleUUID())
  188 + .put("method", IotDeviceMessageMethodEnum.TOPO_GET.getMethod())
  189 + .put("version", "1.0")
  190 + .put("params", params)
  191 + .build());
  192 + // 1.3 输出请求
  193 + log.info("[testTopoGet][请求 URL: {}]", url);
  194 + log.info("[testTopoGet][请求体: {}]", payload);
  195 +
  196 + // 2.1 发送请求
  197 + try (HttpResponse httpResponse = HttpUtil.createPost(url)
  198 + .header("Authorization", GATEWAY_TOKEN)
  199 + .body(payload)
  200 + .execute()) {
  201 + // 2.2 输出结果
  202 + log.info("[testTopoGet][响应体: {}]", httpResponse.body());
  203 + }
  204 + }
  205 +
  206 + // ===================== 子设备注册测试 =====================
  207 +
  208 + // TODO @芋艿:待测试
  209 +
  210 + /**
  211 + * 子设备动态注册测试
  212 + * <p>
  213 + * 网关设备代理子设备进行动态注册,平台返回子设备的 deviceSecret
  214 + */
  215 + @Test
  216 + public void testSubDeviceRegister() {
  217 + // 1.1 构建请求
  218 + String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/sub/register",
  219 + SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
  220 + // 1.2 构建请求参数
  221 + IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO();
  222 + subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY);
  223 + subDevice.setDeviceName(SUB_DEVICE_NAME);
  224 + String payload = JsonUtils.toJsonString(MapUtil.builder()
  225 + .put("id", IdUtil.fastSimpleUUID())
  226 + .put("method", IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod())
  227 + .put("version", "1.0")
  228 + .put("params", Collections.singletonList(subDevice))
  229 + .build());
  230 + // 1.3 输出请求
  231 + log.info("[testSubDeviceRegister][请求 URL: {}]", url);
  232 + log.info("[testSubDeviceRegister][请求体: {}]", payload);
  233 +
  234 + // 2.1 发送请求
  235 + try (HttpResponse httpResponse = HttpUtil.createPost(url)
  236 + .header("Authorization", GATEWAY_TOKEN)
  237 + .body(payload)
  238 + .execute()) {
  239 + // 2.2 输出结果
  240 + log.info("[testSubDeviceRegister][响应体: {}]", httpResponse.body());
  241 + }
  242 + }
  243 +
  244 + // ===================== 批量上报测试 =====================
  245 +
  246 + /**
  247 + * 批量上报属性测试(网关 + 子设备)
  248 + * <p>
  249 + * 网关设备批量上报自身属性、事件,以及子设备的属性、事件
  250 + */
  251 + @Test
  252 + public void testPropertyPackPost() {
  253 + // 1.1 构建请求
  254 + String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/event/property/pack/post",
  255 + SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
  256 + // 1.2 构建【网关设备】自身属性
  257 + Map<String, Object> gatewayProperties = MapUtil.<String, Object>builder()
  258 + .put("temperature", 25.5)
  259 + .build();
  260 + // 1.3 构建【网关设备】自身事件
  261 + IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue();
  262 + gatewayEvent.setValue(MapUtil.builder().put("message", "gateway started").build());
  263 + gatewayEvent.setTime(System.currentTimeMillis());
  264 + Map<String, IotDevicePropertyPackPostReqDTO.EventValue> gatewayEvents = MapUtil.<String, IotDevicePropertyPackPostReqDTO.EventValue>builder()
  265 + .put("statusReport", gatewayEvent)
  266 + .build();
  267 + // 1.4 构建【网关子设备】属性
  268 + Map<String, Object> subDeviceProperties = MapUtil.<String, Object>builder()
  269 + .put("power", 100)
  270 + .build();
  271 + // 1.5 构建【网关子设备】事件
  272 + IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue();
  273 + subDeviceEvent.setValue(MapUtil.builder().put("errorCode", 0).build());
  274 + subDeviceEvent.setTime(System.currentTimeMillis());
  275 + Map<String, IotDevicePropertyPackPostReqDTO.EventValue> subDeviceEvents = MapUtil.<String, IotDevicePropertyPackPostReqDTO.EventValue>builder()
  276 + .put("healthCheck", subDeviceEvent)
  277 + .build();
  278 + // 1.6 构建子设备数据
  279 + IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData();
  280 + subDeviceData.setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME));
  281 + subDeviceData.setProperties(subDeviceProperties);
  282 + subDeviceData.setEvents(subDeviceEvents);
  283 + // 1.7 构建请求参数
  284 + IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO();
  285 + params.setProperties(gatewayProperties);
  286 + params.setEvents(gatewayEvents);
  287 + params.setSubDevices(List.of(subDeviceData));
  288 + String payload = JsonUtils.toJsonString(MapUtil.builder()
  289 + .put("id", IdUtil.fastSimpleUUID())
  290 + .put("method", IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod())
  291 + .put("version", "1.0")
  292 + .put("params", params)
  293 + .build());
  294 + // 1.8 输出请求
  295 + log.info("[testPropertyPackPost][请求 URL: {}]", url);
  296 + log.info("[testPropertyPackPost][请求体: {}]", payload);
  297 +
  298 + // 2.1 发送请求
  299 + try (HttpResponse httpResponse = HttpUtil.createPost(url)
  300 + .header("Authorization", GATEWAY_TOKEN)
  301 + .body(payload)
  302 + .execute()) {
  303 + // 2.2 输出结果
  304 + log.info("[testPropertyPackPost][响应体: {}]", httpResponse.body());
  305 + }
  306 + }
  307 +
  308 +}
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotGatewaySubDeviceHttpProtocolIntegrationTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.http;
  2 +
  3 +import cn.hutool.core.map.MapUtil;
  4 +import cn.hutool.core.util.IdUtil;
  5 +import cn.hutool.http.HttpResponse;
  6 +import cn.hutool.http.HttpUtil;
  7 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  8 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
  9 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
  10 +import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
  11 +import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
  12 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  13 +import lombok.extern.slf4j.Slf4j;
  14 +import org.junit.jupiter.api.Test;
  15 +
  16 +
  17 +/**
  18 + * IoT 网关子设备 HTTP 协议集成测试(手动测试)
  19 + *
  20 + * <p>测试场景:子设备(IotProductDeviceTypeEnum 的 SUB 类型)通过网关设备代理上报数据
  21 + *
  22 + * <p><b>重要说明:子设备无法直接连接平台,所有请求均由网关设备(Gateway)代为转发。</b>
  23 + * <p>网关设备转发子设备请求时,URL 和 Token 都使用子设备自己的信息。
  24 + *
  25 + * <p>使用步骤:
  26 + * <ol>
  27 + * <li>启动 yudao-module-iot-gateway 服务(HTTP 端口 8092)</li>
  28 + * <li>确保子设备已通过 {@link IotGatewayDeviceHttpProtocolIntegrationTest#testTopoAdd()} 绑定到网关</li>
  29 + * <li>运行 {@link #testAuth()} 获取子设备 token,将返回的 token 粘贴到 {@link #TOKEN} 常量</li>
  30 + * <li>运行以下测试方法:
  31 + * <ul>
  32 + * <li>{@link #testPropertyPost()} - 子设备属性上报(由网关代理转发)</li>
  33 + * <li>{@link #testEventPost()} - 子设备事件上报(由网关代理转发)</li>
  34 + * </ul>
  35 + * </li>
  36 + * </ol>
  37 + *
  38 + * @author 芋道源码
  39 + */
  40 +@Slf4j
  41 +@SuppressWarnings("HttpUrlsUsage")
  42 +public class IotGatewaySubDeviceHttpProtocolIntegrationTest {
  43 +
  44 + private static final String SERVER_HOST = "127.0.0.1";
  45 + private static final int SERVER_PORT = 8092;
  46 +
  47 + // ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
  48 + private static final String PRODUCT_KEY = "jAufEMTF1W6wnPhn";
  49 + private static final String DEVICE_NAME = "chazuo-it";
  50 + private static final String DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af";
  51 +
  52 + /**
  53 + * 网关子设备 Token:从 {@link #testAuth()} 方法获取后,粘贴到这里
  54 + */
  55 + private static final String TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoiakF1ZkVNVEYxVzZ3blBobiIsImV4cCI6MTc2OTg3MTI3NCwiZGV2aWNlTmFtZSI6ImNoYXp1by1pdCJ9.99sAlRalzMU3CqRlGStDzCwWSBJq6u3PJw48JQ3NpzQ";
  56 +
  57 + // ===================== 认证测试 =====================
  58 +
  59 + /**
  60 + * 子设备认证测试:获取子设备 Token
  61 + */
  62 + @Test
  63 + public void testAuth() {
  64 + // 1.1 构建请求
  65 + String url = String.format("http://%s:%d/auth", SERVER_HOST, SERVER_PORT);
  66 + IotDeviceAuthUtils.AuthInfo authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
  67 + IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
  68 + .setClientId(authInfo.getClientId())
  69 + .setUsername(authInfo.getUsername())
  70 + .setPassword(authInfo.getPassword());
  71 + String payload = JsonUtils.toJsonString(authReqDTO);
  72 + // 1.2 输出请求
  73 + log.info("[testAuth][请求 URL: {}]", url);
  74 + log.info("[testAuth][请求体: {}]", payload);
  75 +
  76 + // 2.1 发送请求
  77 + String response = HttpUtil.post(url, payload);
  78 + // 2.2 输出结果
  79 + log.info("[testAuth][响应体: {}]", response);
  80 + log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]");
  81 + }
  82 +
  83 + // ===================== 子设备属性上报测试 =====================
  84 +
  85 + /**
  86 + * 子设备属性上报测试
  87 + */
  88 + @Test
  89 + public void testPropertyPost() {
  90 + // 1.1 构建请求
  91 + String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/property/post",
  92 + SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME);
  93 + String payload = JsonUtils.toJsonString(MapUtil.builder()
  94 + .put("id", IdUtil.fastSimpleUUID())
  95 + .put("method", IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod())
  96 + .put("version", "1.0")
  97 + .put("params", IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
  98 + .put("power", 100)
  99 + .put("status", "online")
  100 + .put("temperature", 36.5)
  101 + .build())
  102 + )
  103 + .build());
  104 + // 1.2 输出请求
  105 + log.info("[testPropertyPost][子设备属性上报 - 请求实际由 Gateway 代为转发]");
  106 + log.info("[testPropertyPost][请求 URL: {}]", url);
  107 + log.info("[testPropertyPost][请求体: {}]", payload);
  108 +
  109 + // 2.1 发送请求
  110 + try (HttpResponse httpResponse = HttpUtil.createPost(url)
  111 + .header("Authorization", TOKEN)
  112 + .body(payload)
  113 + .execute()) {
  114 + // 2.2 输出结果
  115 + log.info("[testPropertyPost][响应体: {}]", httpResponse.body());
  116 + }
  117 + }
  118 +
  119 + // ===================== 子设备事件上报测试 =====================
  120 +
  121 + /**
  122 + * 子设备事件上报测试
  123 + */
  124 + @Test
  125 + public void testEventPost() {
  126 + // 1.1 构建请求
  127 + String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/event/post",
  128 + SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME);
  129 + String payload = JsonUtils.toJsonString(MapUtil.builder()
  130 + .put("id", IdUtil.fastSimpleUUID())
  131 + .put("method", IotDeviceMessageMethodEnum.EVENT_POST.getMethod())
  132 + .put("version", "1.0")
  133 + .put("params", IotDeviceEventPostReqDTO.of(
  134 + "alarm",
  135 + MapUtil.<String, Object>builder()
  136 + .put("level", "warning")
  137 + .put("message", "temperature too high")
  138 + .put("threshold", 40)
  139 + .put("current", 42)
  140 + .build(),
  141 + System.currentTimeMillis())
  142 + )
  143 + .build());
  144 + // 1.2 输出请求
  145 + log.info("[testEventPost][子设备事件上报 - 请求实际由 Gateway 代为转发]");
  146 + log.info("[testEventPost][请求 URL: {}]", url);
  147 + log.info("[testEventPost][请求体: {}]", payload);
  148 +
  149 + // 2.1 发送请求
  150 + try (HttpResponse httpResponse = HttpUtil.createPost(url)
  151 + .header("Authorization", TOKEN)
  152 + .body(payload)
  153 + .execute()) {
  154 + // 2.2 输出结果
  155 + log.info("[testEventPost][响应体: {}]", httpResponse.body());
  156 + }
  157 + }
  158 +
  159 +}