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 64 @Operation(summary = "绑定子设备到网关")
65 65 @PreAuthorize("@ss.hasPermission('iot:device:update')")
66 66 public CommonResult<Boolean> bindDeviceGateway(@Valid @RequestBody IotDeviceBindGatewayReqVO reqVO) {
67   - deviceService.bindDeviceGateway(reqVO.getIds(), reqVO.getGatewayId());
  67 + deviceService.bindDeviceGateway(reqVO.getSubIds(), reqVO.getGatewayId());
68 68 return success(true);
69 69 }
70 70  
... ... @@ -72,7 +72,7 @@ public class IotDeviceController {
72 72 @Operation(summary = "解绑子设备与网关")
73 73 @PreAuthorize("@ss.hasPermission('iot:device:update')")
74 74 public CommonResult<Boolean> unbindDeviceGateway(@Valid @RequestBody IotDeviceUnbindGatewayReqVO reqVO) {
75   - deviceService.unbindDeviceGateway(reqVO.getIds(), reqVO.getGatewayId());
  75 + deviceService.unbindDeviceGateway(reqVO.getSubIds(), reqVO.getGatewayId());
76 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 13  
14 14 @Schema(description = "子设备编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3")
15 15 @NotEmpty(message = "子设备编号列表不能为空")
16   - private Set<Long> ids;
  16 + private Set<Long> subIds;
17 17  
18 18 @Schema(description = "网关设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
19 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 13  
14 14 @Schema(description = "子设备编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3")
15 15 @NotEmpty(message = "子设备编号列表不能为空")
16   - private Set<Long> ids;
  16 + private Set<Long> subIds;
17 17  
18 18 @Schema(description = "网关设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
19 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 297 /**
298 298 * 绑定子设备到网关
299 299 *
300   - * @param ids 子设备编号列表
  300 + * @param subIds 子设备编号列表
301 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 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 518 log.error("[authDevice][设备({}/{}) 密码不正确]", productKey, deviceName);
519 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 521 return true;
529 522 }
530 523  
... ... @@ -577,14 +570,14 @@ public class IotDeviceServiceImpl implements IotDeviceService {
577 570  
578 571 @Override
579 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 575 return;
583 576 }
584 577 // 1.1 校验网关设备存在且类型正确
585 578 validateGatewayDeviceExists(gatewayId);
586 579 // 1.2 校验每个设备是否可绑定
587   - List<IotDeviceDO> devices = deviceMapper.selectByIds(ids);
  580 + List<IotDeviceDO> devices = deviceMapper.selectByIds(subIds);
588 581 for (IotDeviceDO device : devices) {
589 582 checkSubDeviceCanBind(device, gatewayId);
590 583 }
... ... @@ -613,13 +606,13 @@ public class IotDeviceServiceImpl implements IotDeviceService {
613 606  
614 607 @Override
615 608 @Transactional(rollbackFor = Exception.class)
616   - public void unbindDeviceGateway(Collection<Long> ids, Long gatewayId) {
  609 + public void unbindDeviceGateway(Collection<Long> subIds, Long gatewayId) {
617 610 // 1. 校验设备存在
618   - if (CollUtil.isEmpty(ids)) {
  611 + if (CollUtil.isEmpty(subIds)) {
619 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 616 if (CollUtil.isEmpty(devices)) {
624 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 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
8 8 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
9 9 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceMessageDO;
10   -import jakarta.validation.constraints.NotEmpty;
11 10 import jakarta.validation.constraints.NotNull;
12 11  
13 12 import javax.annotation.Nullable;
... ... @@ -75,7 +74,7 @@ public interface IotDeviceMessageService {
75 74 */
76 75 List<IotDeviceMessageDO> getDeviceMessageListByRequestIdsAndReply(
77 76 @NotNull(message = "设备编号不能为空") Long deviceId,
78   - @NotEmpty(message = "请求编号不能为空") List<String> requestIds,
  77 + List<String> requestIds,
79 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 1 package cn.iocoder.yudao.module.iot.service.device.message;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.collection.ListUtil;
4 5 import cn.hutool.core.date.LocalDateTimeUtil;
5 6 import cn.hutool.core.lang.Assert;
6 7 import cn.hutool.core.map.MapUtil;
... ... @@ -103,7 +104,6 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
103 104 return sendDeviceMessage(message, device);
104 105 }
105 106  
106   - // TODO @芋艿:针对连接网关的设备,是不是 productKey、deviceName 需要调整下;
107 107 @Override
108 108 public IotDeviceMessage sendDeviceMessage(IotDeviceMessage message, IotDeviceDO device) {
109 109 return sendDeviceMessage(message, device, null);
... ... @@ -273,7 +273,8 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
273 273 log.warn("[handlePackMessage][子设备({}/{}) 不存在]", identity.getProductKey(), identity.getDeviceName());
274 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 278 } catch (Exception ex) {
278 279 log.error("[handlePackMessage][子设备({}/{}) 数据处理失败]", subDeviceData.getIdentity().getProductKey(),
279 280 subDeviceData.getIdentity().getDeviceName(), ex);
... ... @@ -335,9 +336,10 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
335 336 }
336 337  
337 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 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 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 76 return "clientId" + clientId +
77 77 "deviceName" + deviceName +
78 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 21 *
22 22 * <p>使用步骤:
23 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 32 * </ol>
28 33 *
29 34 * @author 芋道源码
... ... @@ -35,16 +40,18 @@ public class IotDirectDeviceHttpProtocolIntegrationTest {
35 40 private static final String SERVER_HOST = "127.0.0.1";
36 41 private static final int SERVER_PORT = 8092;
37 42  
38   - // 设备信息(根据实际情况修改 PRODUCT_KEY、DEVICE_NAME、DEVICE_SECRET,从 iot_device 表查询)
  43 + // ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
39 44 private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT";
40 45 private static final String DEVICE_NAME = "small";
41 46 private static final String DEVICE_SECRET = "0baa4c2ecc104ae1a26b4070c218bdf3";
42 47  
43 48 /**
44   - * 设备 Token:从 {@link #testAuth()} 方法获取后,粘贴到这里
  49 + * 直连设备 Token:从 {@link #testAuth()} 方法获取后,粘贴到这里
45 50 */
46 51 private static final String TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoiNGF5bVpnT1RPT0NyREtSVCIsImV4cCI6MTc2OTMwNTA1NSwiZGV2aWNlTmFtZSI6InNtYWxsIn0.mf3MEATCn5bp6cXgULunZjs8d00RGUxj96JEz0hMS7k";
47 52  
  53 + // ===================== 认证测试 =====================
  54 +
48 55 /**
49 56 * 认证测试:获取设备 Token
50 57 */
... ... @@ -69,6 +76,8 @@ public class IotDirectDeviceHttpProtocolIntegrationTest {
69 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 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 +}
... ...