Commit 8a2c38a02691567b77725bb8724ffc54fc83d081

Authored by 芋道源码
Committed by Gitee
2 parents e765099a e013b1ad

!1504 feat(iot):【网关设备】

Merge pull request !1504 from 芋道源码/feature/iot-sub
Showing 52 changed files with 2126 additions and 163 deletions
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java
@@ -229,4 +229,53 @@ public class JsonUtils { @@ -229,4 +229,53 @@ public class JsonUtils {
229 return JSONUtil.isTypeJSONObject(str); 229 return JSONUtil.isTypeJSONObject(str);
230 } 230 }
231 231
  232 + /**
  233 + * 将 Object 转换为目标类型
  234 + * <p>
  235 + * 避免先转 jsonString 再 parseObject 的性能损耗
  236 + *
  237 + * @param obj 源对象(可以是 Map、POJO 等)
  238 + * @param clazz 目标类型
  239 + * @return 转换后的对象
  240 + */
  241 + public static <T> T convertObject(Object obj, Class<T> clazz) {
  242 + if (obj == null) {
  243 + return null;
  244 + }
  245 + if (clazz.isInstance(obj)) {
  246 + return clazz.cast(obj);
  247 + }
  248 + return objectMapper.convertValue(obj, clazz);
  249 + }
  250 +
  251 + /**
  252 + * 将 Object 转换为目标类型(支持泛型)
  253 + *
  254 + * @param obj 源对象
  255 + * @param typeReference 目标类型引用
  256 + * @return 转换后的对象
  257 + */
  258 + public static <T> T convertObject(Object obj, TypeReference<T> typeReference) {
  259 + if (obj == null) {
  260 + return null;
  261 + }
  262 + return objectMapper.convertValue(obj, typeReference);
  263 + }
  264 +
  265 + /**
  266 + * 将 Object 转换为 List 类型
  267 + * <p>
  268 + * 避免先转 jsonString 再 parseArray 的性能损耗
  269 + *
  270 + * @param obj 源对象(可以是 List、数组等)
  271 + * @param clazz 目标元素类型
  272 + * @return 转换后的 List
  273 + */
  274 + public static <T> List<T> convertList(Object obj, Class<T> clazz) {
  275 + if (obj == null) {
  276 + return new ArrayList<>();
  277 + }
  278 + return objectMapper.convertValue(obj, objectMapper.getTypeFactory().constructCollectionType(List.class, clazz));
  279 + }
  280 +
232 } 281 }
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java
@@ -7,6 +7,10 @@ import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; @@ -7,6 +7,10 @@ import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
7 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; 7 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
8 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; 8 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO;
9 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; 9 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
  10 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
  11 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
  12 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
  13 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO;
10 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; 14 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
11 import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; 15 import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
12 import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; 16 import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
@@ -19,6 +23,8 @@ import org.springframework.web.bind.annotation.PostMapping; @@ -19,6 +23,8 @@ import org.springframework.web.bind.annotation.PostMapping;
19 import org.springframework.web.bind.annotation.RequestBody; 23 import org.springframework.web.bind.annotation.RequestBody;
20 import org.springframework.web.bind.annotation.RestController; 24 import org.springframework.web.bind.annotation.RestController;
21 25
  26 +import java.util.List;
  27 +
22 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; 28 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
23 29
24 /** 30 /**
@@ -57,4 +63,18 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi { @@ -57,4 +63,18 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi {
57 })); 63 }));
58 } 64 }
59 65
  66 + @Override
  67 + @PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/device/register")
  68 + @PermitAll
  69 + public CommonResult<IotDeviceRegisterRespDTO> registerDevice(@RequestBody IotDeviceRegisterReqDTO reqDTO) {
  70 + return success(deviceService.registerDevice(reqDTO));
  71 + }
  72 +
  73 + @Override
  74 + @PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/device/register-sub")
  75 + @PermitAll
  76 + public CommonResult<List<IotSubDeviceRegisterRespDTO>> registerSubDevices(@RequestBody IotSubDeviceRegisterFullReqDTO reqDTO) {
  77 + return success(deviceService.registerSubDevices(reqDTO));
  78 + }
  79 +
60 } 80 }
61 \ No newline at end of file 81 \ No newline at end of file
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()); 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/IotDeviceRespVO.java
@@ -4,7 +4,6 @@ import cn.idev.excel.annotation.ExcelIgnoreUnannotated; @@ -4,7 +4,6 @@ import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4 import cn.idev.excel.annotation.ExcelProperty; 4 import cn.idev.excel.annotation.ExcelProperty;
5 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; 5 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
6 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; 6 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
7 -import cn.iocoder.yudao.module.iot.enums.DictTypeConstants;  
8 import io.swagger.v3.oas.annotations.media.Schema; 7 import io.swagger.v3.oas.annotations.media.Schema;
9 import lombok.Data; 8 import lombok.Data;
10 9
@@ -80,10 +79,6 @@ public class IotDeviceRespVO { @@ -80,10 +79,6 @@ public class IotDeviceRespVO {
80 @ExcelProperty("设备密钥") 79 @ExcelProperty("设备密钥")
81 private String deviceSecret; 80 private String deviceSecret;
82 81
83 - @Schema(description = "认证类型(如一机一密、动态注册)", example = "2")  
84 - @ExcelProperty("认证类型(如一机一密、动态注册)")  
85 - private String authType;  
86 -  
87 @Schema(description = "设备配置", example = "{\"abc\": \"efg\"}") 82 @Schema(description = "设备配置", example = "{\"abc\": \"efg\"}")
88 private String config; 83 private String config;
89 84
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceUnbindGatewayReqVO.java
@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device; @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device;
2 2
3 import io.swagger.v3.oas.annotations.media.Schema; 3 import io.swagger.v3.oas.annotations.media.Schema;
4 import jakarta.validation.constraints.NotEmpty; 4 import jakarta.validation.constraints.NotEmpty;
  5 +import jakarta.validation.constraints.NotNull;
5 import lombok.Data; 6 import lombok.Data;
6 7
7 import java.util.Set; 8 import java.util.Set;
@@ -12,6 +13,10 @@ public class IotDeviceUnbindGatewayReqVO { @@ -12,6 +13,10 @@ public class IotDeviceUnbindGatewayReqVO {
12 13
13 @Schema(description = "子设备编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3") 14 @Schema(description = "子设备编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3")
14 @NotEmpty(message = "子设备编号列表不能为空") 15 @NotEmpty(message = "子设备编号列表不能为空")
15 - private Set<Long> ids; 16 + private Set<Long> subIds;
  17 +
  18 + @Schema(description = "网关设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  19 + @NotNull(message = "网关设备编号不能为空")
  20 + private Long gatewayId;
16 21
17 } 22 }
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java
@@ -27,6 +27,12 @@ public class IotProductRespVO { @@ -27,6 +27,12 @@ public class IotProductRespVO {
27 @ExcelProperty("产品标识") 27 @ExcelProperty("产品标识")
28 private String productKey; 28 private String productKey;
29 29
  30 + @Schema(description = "产品密钥", requiredMode = Schema.RequiredMode.REQUIRED)
  31 + private String productSecret;
  32 +
  33 + @Schema(description = "是否开启动态注册", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
  34 + private Boolean registerEnabled;
  35 +
30 @Schema(description = "产品分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") 36 @Schema(description = "产品分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
31 private Long categoryId; 37 private Long categoryId;
32 38
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductSaveReqVO.java
@@ -48,4 +48,8 @@ public class IotProductSaveReqVO { @@ -48,4 +48,8 @@ public class IotProductSaveReqVO {
48 @NotEmpty(message = "数据格式不能为空") 48 @NotEmpty(message = "数据格式不能为空")
49 private String codecType; 49 private String codecType;
50 50
  51 + @Schema(description = "是否开启动态注册", example = "false")
  52 + @NotNull(message = "是否开启动态注册不能为空")
  53 + private Boolean registerEnabled;
  54 +
51 } 55 }
52 \ No newline at end of file 56 \ No newline at end of file
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java
@@ -123,11 +123,6 @@ public class IotDeviceDO extends TenantBaseDO { @@ -123,11 +123,6 @@ public class IotDeviceDO extends TenantBaseDO {
123 * 设备密钥,用于设备认证 123 * 设备密钥,用于设备认证
124 */ 124 */
125 private String deviceSecret; 125 private String deviceSecret;
126 - /**  
127 - * 认证类型(如一机一密、动态注册)  
128 - */  
129 - // TODO @haohao:是不是要枚举哈  
130 - private String authType;  
131 126
132 /** 127 /**
133 * 设备位置的纬度 128 * 设备位置的纬度
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductDO.java
@@ -33,6 +33,14 @@ public class IotProductDO extends TenantBaseDO { @@ -33,6 +33,14 @@ public class IotProductDO extends TenantBaseDO {
33 */ 33 */
34 private String productKey; 34 private String productKey;
35 /** 35 /**
  36 + * 产品密钥,用于一型一密动态注册
  37 + */
  38 + private String productSecret;
  39 + /**
  40 + * 是否开启动态注册
  41 + */
  42 + private Boolean registerEnabled;
  43 + /**
36 * 产品分类编号 44 * 产品分类编号
37 * <p> 45 * <p>
38 * 关联 {@link IotProductCategoryDO#getId()} 46 * 关联 {@link IotProductCategoryDO#getId()}
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceMapper.java
@@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.IotDevicePa @@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.IotDevicePa
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.enums.product.IotProductDeviceTypeEnum; 9 import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum;
10 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; 10 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
  11 +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
11 import jakarta.annotation.Nullable; 12 import jakarta.annotation.Nullable;
12 import org.apache.ibatis.annotations.Mapper; 13 import org.apache.ibatis.annotations.Mapper;
13 14
@@ -159,4 +160,16 @@ public interface IotDeviceMapper extends BaseMapperX&lt;IotDeviceDO&gt; { @@ -159,4 +160,16 @@ public interface IotDeviceMapper extends BaseMapperX&lt;IotDeviceDO&gt; {
159 .orderByDesc(IotDeviceDO::getId)); 160 .orderByDesc(IotDeviceDO::getId));
160 } 161 }
161 162
  163 + /**
  164 + * 批量更新设备的网关编号
  165 + *
  166 + * @param ids 设备编号列表
  167 + * @param gatewayId 网关设备编号(可以为 null,表示解绑)
  168 + */
  169 + default void updateGatewayIdBatch(Collection<Long> ids, Long gatewayId) {
  170 + update(null, new LambdaUpdateWrapper<IotDeviceDO>()
  171 + .set(IotDeviceDO::getGatewayId, gatewayId)
  172 + .in(IotDeviceDO::getId, ids));
  173 + }
  174 +
162 } 175 }
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java
@@ -35,6 +35,17 @@ public interface ErrorCodeConstants { @@ -35,6 +35,17 @@ public interface ErrorCodeConstants {
35 ErrorCode DEVICE_SERIAL_NUMBER_EXISTS = new ErrorCode(1_050_003_008, "设备序列号已存在,序列号必须全局唯一"); 35 ErrorCode DEVICE_SERIAL_NUMBER_EXISTS = new ErrorCode(1_050_003_008, "设备序列号已存在,序列号必须全局唯一");
36 ErrorCode DEVICE_NOT_GATEWAY_SUB = new ErrorCode(1_050_003_009, "设备【{}/{}】不是网关子设备类型,无法绑定到网关"); 36 ErrorCode DEVICE_NOT_GATEWAY_SUB = new ErrorCode(1_050_003_009, "设备【{}/{}】不是网关子设备类型,无法绑定到网关");
37 ErrorCode DEVICE_GATEWAY_BINDTO_EXISTS = new ErrorCode(1_050_003_010, "设备【{}/{}】已绑定到其他网关,请先解绑"); 37 ErrorCode DEVICE_GATEWAY_BINDTO_EXISTS = new ErrorCode(1_050_003_010, "设备【{}/{}】已绑定到其他网关,请先解绑");
  38 + // 拓扑管理相关错误码 1-050-003-100
  39 + ErrorCode DEVICE_TOPO_PARAMS_INVALID = new ErrorCode(1_050_003_100, "拓扑管理参数无效");
  40 + ErrorCode DEVICE_TOPO_SUB_DEVICE_USERNAME_INVALID = new ErrorCode(1_050_003_101, "子设备用户名格式无效");
  41 + ErrorCode DEVICE_TOPO_SUB_DEVICE_AUTH_FAILED = new ErrorCode(1_050_003_102, "子设备认证失败");
  42 + ErrorCode DEVICE_TOPO_SUB_NOT_BINDTO_GATEWAY = new ErrorCode(1_050_003_103, "子设备【{}/{}】未绑定到该网关");
  43 + // 设备注册相关错误码 1-050-003-200
  44 + ErrorCode DEVICE_SUB_REGISTER_PARAMS_INVALID = new ErrorCode(1_050_003_200, "子设备注册参数无效");
  45 + ErrorCode DEVICE_SUB_REGISTER_PRODUCT_NOT_GATEWAY_SUB = new ErrorCode(1_050_003_201, "产品【{}】不是网关子设备类型");
  46 + ErrorCode DEVICE_REGISTER_DISABLED = new ErrorCode(1_050_003_210, "该产品未开启动态注册功能");
  47 + ErrorCode DEVICE_REGISTER_SECRET_INVALID = new ErrorCode(1_050_003_211, "产品密钥验证失败");
  48 + ErrorCode DEVICE_REGISTER_ALREADY_EXISTS = new ErrorCode(1_050_003_212, "设备已存在,不允许重复注册");
38 49
39 // ========== 产品分类 1-050-004-000 ========== 50 // ========== 产品分类 1-050-004-000 ==========
40 ErrorCode PRODUCT_CATEGORY_NOT_EXISTS = new ErrorCode(1_050_004_000, "产品分类不存在"); 51 ErrorCode PRODUCT_CATEGORY_NOT_EXISTS = new ErrorCode(1_050_004_000, "产品分类不存在");
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java
@@ -3,7 +3,14 @@ package cn.iocoder.yudao.module.iot.service.device; @@ -3,7 +3,14 @@ package cn.iocoder.yudao.module.iot.service.device;
3 import cn.iocoder.yudao.framework.common.pojo.PageResult; 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*; 4 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*;
5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; 5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
  6 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
6 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; 7 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
  8 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  9 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
  10 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
  11 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
  12 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO;
  13 +import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetRespDTO;
7 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; 14 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
8 import jakarta.validation.Valid; 15 import jakarta.validation.Valid;
9 16
@@ -38,18 +45,6 @@ public interface IotDeviceService { @@ -38,18 +45,6 @@ public interface IotDeviceService {
38 */ 45 */
39 void updateDevice(@Valid IotDeviceSaveReqVO updateReqVO); 46 void updateDevice(@Valid IotDeviceSaveReqVO updateReqVO);
40 47
41 - // TODO @芋艿:先这么实现。未来看情况,要不要自己实现  
42 -  
43 - /**  
44 - * 更新设备的所属网关  
45 - *  
46 - * @param id 编号  
47 - * @param gatewayId 网关设备 ID  
48 - */  
49 - default void updateDeviceGateway(Long id, Long gatewayId) {  
50 - updateDevice(new IotDeviceSaveReqVO().setId(id).setGatewayId(gatewayId));  
51 - }  
52 -  
53 /** 48 /**
54 * 更新设备状态 49 * 更新设备状态
55 * 50 *
@@ -288,22 +283,23 @@ public interface IotDeviceService { @@ -288,22 +283,23 @@ public interface IotDeviceService {
288 */ 283 */
289 List<IotDeviceDO> getDeviceListByHasLocation(); 284 List<IotDeviceDO> getDeviceListByHasLocation();
290 285
291 - // ========== 网关-子设备绑定相关 ========== 286 + // ========== 网关-拓扑管理(后台操作) ==========
292 287
293 /** 288 /**
294 * 绑定子设备到网关 289 * 绑定子设备到网关
295 * 290 *
296 - * @param ids 子设备编号列表 291 + * @param subIds 子设备编号列表
297 * @param gatewayId 网关设备编号 292 * @param gatewayId 网关设备编号
298 */ 293 */
299 - void bindDeviceGateway(Collection<Long> ids, Long gatewayId); 294 + void bindDeviceGateway(Collection<Long> subIds, Long gatewayId);
300 295
301 /** 296 /**
302 * 解绑子设备与网关 297 * 解绑子设备与网关
303 * 298 *
304 - * @param ids 子设备编号列表 299 + * @param subIds 子设备编号列表
  300 + * @param gatewayId 网关设备编号
305 */ 301 */
306 - void unbindDeviceGateway(Collection<Long> ids); 302 + void unbindDeviceGateway(Collection<Long> subIds, Long gatewayId);
307 303
308 /** 304 /**
309 * 获取未绑定网关的子设备分页 305 * 获取未绑定网关的子设备分页
@@ -321,4 +317,62 @@ public interface IotDeviceService { @@ -321,4 +317,62 @@ public interface IotDeviceService {
321 */ 317 */
322 List<IotDeviceDO> getDeviceListByGatewayId(Long gatewayId); 318 List<IotDeviceDO> getDeviceListByGatewayId(Long gatewayId);
323 319
  320 + // ========== 网关-拓扑管理(设备上报) ==========
  321 +
  322 + /**
  323 + * 处理添加拓扑关系消息(网关设备上报)
  324 + *
  325 + * @param message 消息
  326 + * @param gatewayDevice 网关设备
  327 + * @return 成功添加的子设备列表
  328 + */
  329 + List<IotDeviceIdentity> handleTopoAddMessage(IotDeviceMessage message, IotDeviceDO gatewayDevice);
  330 +
  331 + /**
  332 + * 处理删除拓扑关系消息(网关设备上报)
  333 + *
  334 + * @param message 消息
  335 + * @param gatewayDevice 网关设备
  336 + * @return 成功删除的子设备列表
  337 + */
  338 + List<IotDeviceIdentity> handleTopoDeleteMessage(IotDeviceMessage message, IotDeviceDO gatewayDevice);
  339 +
  340 + /**
  341 + * 处理获取拓扑关系消息(网关设备上报)
  342 + *
  343 + * @param gatewayDevice 网关设备
  344 + * @return 拓扑关系响应
  345 + */
  346 + IotDeviceTopoGetRespDTO handleTopoGetMessage(IotDeviceDO gatewayDevice);
  347 +
  348 + // ========== 设备动态注册 ==========
  349 +
  350 + /**
  351 + * 直连/网关设备动态注册
  352 + *
  353 + * @param reqDTO 动态注册请求
  354 + * @return 注册结果(包含 DeviceSecret)
  355 + */
  356 + IotDeviceRegisterRespDTO registerDevice(@Valid IotDeviceRegisterReqDTO reqDTO);
  357 +
  358 + /**
  359 + * 网关子设备动态注册
  360 + * <p>
  361 + * 与 {@link #handleSubDeviceRegisterMessage} 方法的区别:
  362 + * 该方法网关设备信息通过 reqDTO 参数传入,而 {@link #handleSubDeviceRegisterMessage} 方法通过 gatewayDevice 参数传入
  363 + *
  364 + * @param reqDTO 子设备注册请求(包含网关设备信息)
  365 + * @return 注册结果列表
  366 + */
  367 + List<IotSubDeviceRegisterRespDTO> registerSubDevices(@Valid IotSubDeviceRegisterFullReqDTO reqDTO);
  368 +
  369 + /**
  370 + * 处理子设备动态注册消息(网关设备上报)
  371 + *
  372 + * @param message 消息
  373 + * @param gatewayDevice 网关设备
  374 + * @return 注册结果列表
  375 + */
  376 + List<IotSubDeviceRegisterRespDTO> handleSubDeviceRegisterMessage(IotDeviceMessage message, IotDeviceDO gatewayDevice);
  377 +
324 } 378 }
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java
1 package cn.iocoder.yudao.module.iot.service.device; 1 package cn.iocoder.yudao.module.iot.service.device;
2 2
3 import cn.hutool.core.collection.CollUtil; 3 import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.util.BooleanUtil;
4 import cn.hutool.core.util.IdUtil; 5 import cn.hutool.core.util.IdUtil;
5 import cn.hutool.core.util.ObjUtil; 6 import cn.hutool.core.util.ObjUtil;
6 import cn.hutool.core.util.StrUtil; 7 import cn.hutool.core.util.StrUtil;
7 import cn.hutool.extra.spring.SpringUtil; 8 import cn.hutool.extra.spring.SpringUtil;
8 import cn.iocoder.yudao.framework.common.exception.ServiceException; 9 import cn.iocoder.yudao.framework.common.exception.ServiceException;
9 import cn.iocoder.yudao.framework.common.pojo.PageResult; 10 import cn.iocoder.yudao.framework.common.pojo.PageResult;
  11 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
10 import cn.iocoder.yudao.framework.common.util.object.BeanUtils; 12 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
11 import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; 13 import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
12 import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; 14 import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
13 import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; 15 import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
14 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*; 16 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*;
15 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; 17 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
  18 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
  19 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
16 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; 20 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
  21 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  22 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
  23 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
  24 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
  25 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO;
  26 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO;
  27 +import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO;
  28 +import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoChangeReqDTO;
  29 +import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO;
  30 +import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetRespDTO;
17 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; 31 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
18 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; 32 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
19 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceGroupDO; 33 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceGroupDO;
@@ -21,6 +35,7 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; @@ -21,6 +35,7 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
21 import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceMapper; 35 import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceMapper;
22 import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants; 36 import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants;
23 import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum; 37 import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum;
  38 +import cn.iocoder.yudao.module.iot.service.device.message.IotDeviceMessageService;
24 import cn.iocoder.yudao.module.iot.service.product.IotProductService; 39 import cn.iocoder.yudao.module.iot.service.product.IotProductService;
25 import jakarta.annotation.Resource; 40 import jakarta.annotation.Resource;
26 import jakarta.validation.ConstraintViolationException; 41 import jakarta.validation.ConstraintViolationException;
@@ -41,6 +56,7 @@ import java.util.*; @@ -41,6 +56,7 @@ import java.util.*;
41 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 56 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
42 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; 57 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
43 import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; 58 import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*;
  59 +import static java.util.Collections.singletonList;
44 60
45 /** 61 /**
46 * IoT 设备 Service 实现类 62 * IoT 设备 Service 实现类
@@ -61,9 +77,20 @@ public class IotDeviceServiceImpl implements IotDeviceService { @@ -61,9 +77,20 @@ public class IotDeviceServiceImpl implements IotDeviceService {
61 @Resource 77 @Resource
62 @Lazy // 延迟加载,解决循环依赖 78 @Lazy // 延迟加载,解决循环依赖
63 private IotDeviceGroupService deviceGroupService; 79 private IotDeviceGroupService deviceGroupService;
  80 + @Resource
  81 + @Lazy // 延迟加载,解决循环依赖
  82 + private IotDeviceMessageService deviceMessageService;
  83 +
  84 + private IotDeviceServiceImpl getSelf() {
  85 + return SpringUtil.getBean(getClass());
  86 + }
64 87
65 @Override 88 @Override
66 public Long createDevice(IotDeviceSaveReqVO createReqVO) { 89 public Long createDevice(IotDeviceSaveReqVO createReqVO) {
  90 + return createDevice0(createReqVO).getId();
  91 + }
  92 +
  93 + private IotDeviceDO createDevice0(IotDeviceSaveReqVO createReqVO) {
67 // 1.1 校验产品是否存在 94 // 1.1 校验产品是否存在
68 IotProductDO product = productService.getProduct(createReqVO.getProductId()); 95 IotProductDO product = productService.getProduct(createReqVO.getProductId());
69 if (product == null) { 96 if (product == null) {
@@ -81,7 +108,7 @@ public class IotDeviceServiceImpl implements IotDeviceService { @@ -81,7 +108,7 @@ public class IotDeviceServiceImpl implements IotDeviceService {
81 IotDeviceDO device = BeanUtils.toBean(createReqVO, IotDeviceDO.class); 108 IotDeviceDO device = BeanUtils.toBean(createReqVO, IotDeviceDO.class);
82 initDevice(device, product); 109 initDevice(device, product);
83 deviceMapper.insert(device); 110 deviceMapper.insert(device);
84 - return device.getId(); 111 + return device;
85 } 112 }
86 113
87 private void validateCreateDeviceParam(String productKey, String deviceName, 114 private void validateCreateDeviceParam(String productKey, String deviceName,
@@ -117,11 +144,13 @@ public class IotDeviceServiceImpl implements IotDeviceService { @@ -117,11 +144,13 @@ public class IotDeviceServiceImpl implements IotDeviceService {
117 144
118 private void initDevice(IotDeviceDO device, IotProductDO product) { 145 private void initDevice(IotDeviceDO device, IotProductDO product) {
119 device.setProductId(product.getId()).setProductKey(product.getProductKey()) 146 device.setProductId(product.getId()).setProductKey(product.getProductKey())
120 - .setDeviceType(product.getDeviceType());  
121 - // 生成密钥  
122 - device.setDeviceSecret(generateDeviceSecret());  
123 - // 设置设备状态为未激活  
124 - device.setState(IotDeviceStateEnum.INACTIVE.getState()); 147 + .setDeviceType(product.getDeviceType())
  148 + .setDeviceSecret(generateDeviceSecret()) // 生成密钥
  149 + .setState(IotDeviceStateEnum.INACTIVE.getState()); // 默认未激活
  150 + }
  151 +
  152 + private String generateDeviceSecret() {
  153 + return IdUtil.fastSimpleUUID();
125 } 154 }
126 155
127 @Override 156 @Override
@@ -298,6 +327,37 @@ public class IotDeviceServiceImpl implements IotDeviceService { @@ -298,6 +327,37 @@ public class IotDeviceServiceImpl implements IotDeviceService {
298 327
299 // 2. 清空对应缓存 328 // 2. 清空对应缓存
300 deleteDeviceCache(device); 329 deleteDeviceCache(device);
  330 +
  331 + // 3. 网关设备下线时,联动所有子设备下线
  332 + if (Objects.equals(state, IotDeviceStateEnum.OFFLINE.getState())
  333 + && IotProductDeviceTypeEnum.isGateway(device.getDeviceType())) {
  334 + handleGatewayOffline(device);
  335 + }
  336 + }
  337 +
  338 + /**
  339 + * 处理网关下线,联动所有子设备下线
  340 + *
  341 + * @param gatewayDevice 网关设备
  342 + */
  343 + private void handleGatewayOffline(IotDeviceDO gatewayDevice) {
  344 + List<IotDeviceDO> subDevices = deviceMapper.selectListByGatewayId(gatewayDevice.getId());
  345 + if (CollUtil.isEmpty(subDevices)) {
  346 + return;
  347 + }
  348 + for (IotDeviceDO subDevice : subDevices) {
  349 + if (Objects.equals(subDevice.getState(), IotDeviceStateEnum.ONLINE.getState())) {
  350 + try {
  351 + updateDeviceState(subDevice, IotDeviceStateEnum.OFFLINE.getState());
  352 + log.info("[handleGatewayOffline][网关({}/{}) 下线,子设备({}/{}) 联动下线]",
  353 + gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(),
  354 + subDevice.getProductKey(), subDevice.getDeviceName());
  355 + } catch (Exception ex) {
  356 + log.error("[handleGatewayOffline][子设备({}/{}) 下线失败]",
  357 + subDevice.getProductKey(), subDevice.getDeviceName(), ex);
  358 + }
  359 + }
  360 + }
301 } 361 }
302 362
303 @Override 363 @Override
@@ -318,15 +378,6 @@ public class IotDeviceServiceImpl implements IotDeviceService { @@ -318,15 +378,6 @@ public class IotDeviceServiceImpl implements IotDeviceService {
318 return deviceMapper.selectCountByGroupId(groupId); 378 return deviceMapper.selectCountByGroupId(groupId);
319 } 379 }
320 380
321 - /**  
322 - * 生成 deviceSecret  
323 - *  
324 - * @return 生成的 deviceSecret  
325 - */  
326 - private String generateDeviceSecret() {  
327 - return IdUtil.fastSimpleUUID();  
328 - }  
329 -  
330 @Override 381 @Override
331 @Transactional(rollbackFor = Exception.class) // 添加事务,异常则回滚所有导入 382 @Transactional(rollbackFor = Exception.class) // 添加事务,异常则回滚所有导入
332 public IotDeviceImportRespVO importDevice(List<IotDeviceImportExcelVO> importDevices, boolean updateSupport) { 383 public IotDeviceImportRespVO importDevice(List<IotDeviceImportExcelVO> importDevices, boolean updateSupport) {
@@ -401,7 +452,7 @@ public class IotDeviceServiceImpl implements IotDeviceService { @@ -401,7 +452,7 @@ public class IotDeviceServiceImpl implements IotDeviceService {
401 public IotDeviceAuthInfoRespVO getDeviceAuthInfo(Long id) { 452 public IotDeviceAuthInfoRespVO getDeviceAuthInfo(Long id) {
402 IotDeviceDO device = validateDeviceExists(id); 453 IotDeviceDO device = validateDeviceExists(id);
403 // 使用 IotDeviceAuthUtils 生成认证信息 454 // 使用 IotDeviceAuthUtils 生成认证信息
404 - IotDeviceAuthUtils.AuthInfo authInfo = IotDeviceAuthUtils.getAuthInfo( 455 + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(
405 device.getProductKey(), device.getDeviceName(), device.getDeviceSecret()); 456 device.getProductKey(), device.getDeviceName(), device.getDeviceSecret());
406 return BeanUtils.toBean(authInfo, IotDeviceAuthInfoRespVO.class); 457 return BeanUtils.toBean(authInfo, IotDeviceAuthInfoRespVO.class);
407 } 458 }
@@ -449,7 +500,7 @@ public class IotDeviceServiceImpl implements IotDeviceService { @@ -449,7 +500,7 @@ public class IotDeviceServiceImpl implements IotDeviceService {
449 @Override 500 @Override
450 public boolean authDevice(IotDeviceAuthReqDTO authReqDTO) { 501 public boolean authDevice(IotDeviceAuthReqDTO authReqDTO) {
451 // 1. 校验设备是否存在 502 // 1. 校验设备是否存在
452 - IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(authReqDTO.getUsername()); 503 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authReqDTO.getUsername());
453 if (deviceInfo == null) { 504 if (deviceInfo == null) {
454 log.error("[authDevice][认证失败,username({}) 格式不正确]", authReqDTO.getUsername()); 505 log.error("[authDevice][认证失败,username({}) 格式不正确]", authReqDTO.getUsername());
455 return false; 506 return false;
@@ -463,7 +514,7 @@ public class IotDeviceServiceImpl implements IotDeviceService { @@ -463,7 +514,7 @@ public class IotDeviceServiceImpl implements IotDeviceService {
463 } 514 }
464 515
465 // 2. 校验密码 516 // 2. 校验密码
466 - IotDeviceAuthUtils.AuthInfo authInfo = IotDeviceAuthUtils.getAuthInfo(productKey, deviceName, device.getDeviceSecret()); 517 + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(productKey, deviceName, device.getDeviceSecret());
467 if (ObjUtil.notEqual(authInfo.getPassword(), authReqDTO.getPassword())) { 518 if (ObjUtil.notEqual(authInfo.getPassword(), authReqDTO.getPassword())) {
468 log.error("[authDevice][设备({}/{}) 密码不正确]", productKey, deviceName); 519 log.error("[authDevice][设备({}/{}) 密码不正确]", productKey, deviceName);
469 return false; 520 return false;
@@ -516,29 +567,20 @@ public class IotDeviceServiceImpl implements IotDeviceService { @@ -516,29 +567,20 @@ public class IotDeviceServiceImpl implements IotDeviceService {
516 return deviceMapper.selectListByHasLocation(); 567 return deviceMapper.selectListByHasLocation();
517 } 568 }
518 569
519 - // ========== 网关-子设备绑定相关 ========== 570 + // ========== 网关-拓扑管理(后台操作) ==========
520 571
521 @Override 572 @Override
522 @Transactional(rollbackFor = Exception.class) 573 @Transactional(rollbackFor = Exception.class)
523 - public void bindDeviceGateway(Collection<Long> ids, Long gatewayId) {  
524 - if (CollUtil.isEmpty(ids)) { 574 + public void bindDeviceGateway(Collection<Long> subIds, Long gatewayId) {
  575 + if (CollUtil.isEmpty(subIds)) {
525 return; 576 return;
526 } 577 }
527 // 1.1 校验网关设备存在且类型正确 578 // 1.1 校验网关设备存在且类型正确
528 validateGatewayDeviceExists(gatewayId); 579 validateGatewayDeviceExists(gatewayId);
529 - // 1.2 校验子设备存在  
530 - List<IotDeviceDO> devices = deviceMapper.selectByIds(ids);  
531 - if (devices.size() != ids.size()) {  
532 - throw exception(DEVICE_NOT_EXISTS);  
533 - }  
534 - // 1.3 校验每个设备是否可绑定 580 + // 1.2 校验每个设备是否可绑定
  581 + List<IotDeviceDO> devices = deviceMapper.selectByIds(subIds);
535 for (IotDeviceDO device : devices) { 582 for (IotDeviceDO device : devices) {
536 - if (!IotProductDeviceTypeEnum.isGatewaySub(device.getDeviceType())) {  
537 - throw exception(DEVICE_NOT_GATEWAY_SUB, device.getProductKey(), device.getDeviceName());  
538 - }  
539 - if (device.getGatewayId() != null && !device.getGatewayId().equals(gatewayId)) {  
540 - throw exception(DEVICE_GATEWAY_BINDTO_EXISTS, device.getProductKey(), device.getDeviceName());  
541 - } 583 + checkSubDeviceCanBind(device, gatewayId);
542 } 584 }
543 585
544 // 2. 批量更新数据库 586 // 2. 批量更新数据库
@@ -548,31 +590,42 @@ public class IotDeviceServiceImpl implements IotDeviceService { @@ -548,31 +590,42 @@ public class IotDeviceServiceImpl implements IotDeviceService {
548 590
549 // 3. 清空对应缓存 591 // 3. 清空对应缓存
550 deleteDeviceCache(devices); 592 deleteDeviceCache(devices);
  593 +
  594 + // 4. 下发网关设备拓扑变更通知(增加)
  595 + sendTopoChangeNotify(gatewayId, IotDeviceTopoChangeReqDTO.STATUS_CREATE, devices);
  596 + }
  597 +
  598 + private void checkSubDeviceCanBind(IotDeviceDO device, Long gatewayId) {
  599 + if (!IotProductDeviceTypeEnum.isGatewaySub(device.getDeviceType())) {
  600 + throw exception(DEVICE_NOT_GATEWAY_SUB, device.getProductKey(), device.getDeviceName());
  601 + }
  602 + // 已绑定到其他网关,拒绝绑定(需先解绑)
  603 + if (device.getGatewayId() != null && ObjUtil.notEqual(device.getGatewayId(), gatewayId)) {
  604 + throw exception(DEVICE_GATEWAY_BINDTO_EXISTS, device.getProductKey(), device.getDeviceName());
  605 + }
551 } 606 }
552 607
553 @Override 608 @Override
554 @Transactional(rollbackFor = Exception.class) 609 @Transactional(rollbackFor = Exception.class)
555 - public void unbindDeviceGateway(Collection<Long> ids) {  
556 - if (CollUtil.isEmpty(ids)) { 610 + public void unbindDeviceGateway(Collection<Long> subIds, Long gatewayId) {
  611 + // 1. 校验设备存在
  612 + if (CollUtil.isEmpty(subIds)) {
557 return; 613 return;
558 } 614 }
559 - // 1. 校验设备存在  
560 - List<IotDeviceDO> devices = deviceMapper.selectByIds(ids);  
561 - if (devices.size() != ids.size()) {  
562 - throw exception(DEVICE_NOT_EXISTS); 615 + List<IotDeviceDO> devices = deviceMapper.selectByIds(subIds);
  616 + devices.removeIf(device -> ObjUtil.notEqual(device.getGatewayId(), gatewayId));
  617 + if (CollUtil.isEmpty(devices)) {
  618 + return;
563 } 619 }
564 620
565 // 2. 批量更新数据库(将 gatewayId 设置为 null) 621 // 2. 批量更新数据库(将 gatewayId 设置为 null)
566 - List<IotDeviceDO> updateList = devices.stream()  
567 - .filter(device -> device.getGatewayId() != null)  
568 - .map(device -> new IotDeviceDO().setId(device.getId()).setGatewayId(null))  
569 - .toList();  
570 - if (CollUtil.isNotEmpty(updateList)) {  
571 - deviceMapper.updateBatch(updateList);  
572 - } 622 + deviceMapper.updateGatewayIdBatch(convertList(devices, IotDeviceDO::getId), null);
573 623
574 // 3. 清空对应缓存 624 // 3. 清空对应缓存
575 deleteDeviceCache(devices); 625 deleteDeviceCache(devices);
  626 +
  627 + // 4. 下发网关设备拓扑变更通知(删除)
  628 + sendTopoChangeNotify(gatewayId, IotDeviceTopoChangeReqDTO.STATUS_DELETE, devices);
576 } 629 }
577 630
578 @Override 631 @Override
@@ -585,8 +638,293 @@ public class IotDeviceServiceImpl implements IotDeviceService { @@ -585,8 +638,293 @@ public class IotDeviceServiceImpl implements IotDeviceService {
585 return deviceMapper.selectListByGatewayId(gatewayId); 638 return deviceMapper.selectListByGatewayId(gatewayId);
586 } 639 }
587 640
588 - private IotDeviceServiceImpl getSelf() {  
589 - return SpringUtil.getBean(getClass()); 641 + // ========== 网关-拓扑管理(设备上报) ==========
  642 +
  643 + @Override
  644 + public List<IotDeviceIdentity> handleTopoAddMessage(IotDeviceMessage message, IotDeviceDO gatewayDevice) {
  645 + // 1.1 校验网关设备类型
  646 + if (!IotProductDeviceTypeEnum.isGateway(gatewayDevice.getDeviceType())) {
  647 + throw exception(DEVICE_NOT_GATEWAY);
  648 + }
  649 + // 1.2 解析参数
  650 + IotDeviceTopoAddReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceTopoAddReqDTO.class);
  651 + if (params == null || CollUtil.isEmpty(params.getSubDevices())) {
  652 + throw exception(DEVICE_TOPO_PARAMS_INVALID);
  653 + }
  654 +
  655 + // 2. 遍历处理每个子设备
  656 + List<IotDeviceIdentity> addedSubDevices = new ArrayList<>();
  657 + for (IotDeviceAuthReqDTO subDeviceAuth : params.getSubDevices()) {
  658 + try {
  659 + IotDeviceDO subDevice = addDeviceTopo(gatewayDevice, subDeviceAuth);
  660 + addedSubDevices.add(new IotDeviceIdentity(subDevice.getProductKey(), subDevice.getDeviceName()));
  661 + } catch (Exception ex) {
  662 + log.warn("[handleTopoAddMessage][网关({}/{}) 添加子设备失败,message={}]",
  663 + gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(), message, ex);
  664 + }
  665 + }
  666 +
  667 + // 3. 返回响应数据(包含成功添加的子设备列表)
  668 + return addedSubDevices;
  669 + }
  670 +
  671 + private IotDeviceDO addDeviceTopo(IotDeviceDO gatewayDevice, IotDeviceAuthReqDTO subDeviceAuth) {
  672 + // 1.1 解析子设备信息
  673 + IotDeviceIdentity subDeviceInfo = IotDeviceAuthUtils.parseUsername(subDeviceAuth.getUsername());
  674 + if (subDeviceInfo == null) {
  675 + throw exception(DEVICE_TOPO_SUB_DEVICE_USERNAME_INVALID);
  676 + }
  677 + // 1.2 校验子设备认证信息
  678 + if (!authDevice(subDeviceAuth)) {
  679 + throw exception(DEVICE_TOPO_SUB_DEVICE_AUTH_FAILED);
  680 + }
  681 + // 1.3 获取子设备
  682 + IotDeviceDO subDevice = getSelf().getDeviceFromCache(subDeviceInfo.getProductKey(), subDeviceInfo.getDeviceName());
  683 + if (subDevice == null) {
  684 + throw exception(DEVICE_NOT_EXISTS);
  685 + }
  686 + // 1.4 校验子设备类型
  687 + checkSubDeviceCanBind(subDevice, gatewayDevice.getId());
  688 +
  689 + // 2. 更新数据库
  690 + deviceMapper.updateById(new IotDeviceDO().setId(subDevice.getId()).setGatewayId(gatewayDevice.getId()));
  691 + log.info("[addDeviceTopo][网关({}/{}) 绑定子设备({}/{})]",
  692 + gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(),
  693 + subDevice.getProductKey(), subDevice.getDeviceName());
  694 +
  695 + // 3. 清空对应缓存
  696 + deleteDeviceCache(subDevice);
  697 + return subDevice;
  698 + }
  699 +
  700 + @Override
  701 + public List<IotDeviceIdentity> handleTopoDeleteMessage(IotDeviceMessage message, IotDeviceDO gatewayDevice) {
  702 + // 1.1 校验网关设备类型
  703 + if (!IotProductDeviceTypeEnum.isGateway(gatewayDevice.getDeviceType())) {
  704 + throw exception(DEVICE_NOT_GATEWAY);
  705 + }
  706 + // 1.2 解析参数
  707 + IotDeviceTopoDeleteReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceTopoDeleteReqDTO.class);
  708 + if (params == null || CollUtil.isEmpty(params.getSubDevices())) {
  709 + throw exception(DEVICE_TOPO_PARAMS_INVALID);
  710 + }
  711 +
  712 + // 2. 遍历处理每个子设备
  713 + List<IotDeviceIdentity> deletedSubDevices = new ArrayList<>();
  714 + for (IotDeviceIdentity subDeviceIdentity : params.getSubDevices()) {
  715 + try {
  716 + deleteDeviceTopo(gatewayDevice, subDeviceIdentity);
  717 + deletedSubDevices.add(subDeviceIdentity);
  718 + } catch (Exception ex) {
  719 + log.warn("[handleTopoDeleteMessage][网关({}/{}) 删除子设备失败,productKey={}, deviceName={}]",
  720 + gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(),
  721 + subDeviceIdentity.getProductKey(), subDeviceIdentity.getDeviceName(), ex);
  722 + }
  723 + }
  724 +
  725 + // 3. 返回响应数据(包含成功删除的子设备列表)
  726 + return deletedSubDevices;
  727 + }
  728 +
  729 + private void deleteDeviceTopo(IotDeviceDO gatewayDevice, IotDeviceIdentity subDeviceIdentity) {
  730 + // 1.1 获取子设备
  731 + IotDeviceDO subDevice = getSelf().getDeviceFromCache(subDeviceIdentity.getProductKey(), subDeviceIdentity.getDeviceName());
  732 + if (subDevice == null) {
  733 + throw exception(DEVICE_NOT_EXISTS);
  734 + }
  735 + // 1.2 校验子设备是否绑定到该网关
  736 + if (ObjUtil.notEqual(subDevice.getGatewayId(), gatewayDevice.getId())) {
  737 + throw exception(DEVICE_TOPO_SUB_NOT_BINDTO_GATEWAY,
  738 + subDeviceIdentity.getProductKey(), subDeviceIdentity.getDeviceName());
  739 + }
  740 +
  741 + // 2. 更新数据库(将 gatewayId 设置为 null)
  742 + deviceMapper.updateGatewayIdBatch(singletonList(subDevice.getId()), null);
  743 + log.info("[deleteDeviceTopo][网关({}/{}) 解绑子设备({}/{})]",
  744 + gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(),
  745 + subDevice.getProductKey(), subDevice.getDeviceName());
  746 +
  747 + // 3. 清空对应缓存
  748 + deleteDeviceCache(subDevice);
  749 +
  750 + // 4. 子设备下线
  751 + if (Objects.equals(subDevice.getState(), IotDeviceStateEnum.ONLINE.getState())) {
  752 + updateDeviceState(subDevice, IotDeviceStateEnum.OFFLINE.getState());
  753 + }
  754 + }
  755 +
  756 + @Override
  757 + public IotDeviceTopoGetRespDTO handleTopoGetMessage(IotDeviceDO gatewayDevice) {
  758 + // 1. 校验网关设备类型
  759 + if (!IotProductDeviceTypeEnum.isGateway(gatewayDevice.getDeviceType())) {
  760 + throw exception(DEVICE_NOT_GATEWAY);
  761 + }
  762 +
  763 + // 2. 获取子设备列表并转换
  764 + List<IotDeviceDO> subDevices = deviceMapper.selectListByGatewayId(gatewayDevice.getId());
  765 + List<IotDeviceIdentity> subDeviceIdentities = convertList(subDevices, subDevice ->
  766 + new IotDeviceIdentity(subDevice.getProductKey(), subDevice.getDeviceName()));
  767 + return new IotDeviceTopoGetRespDTO().setSubDevices(subDeviceIdentities);
  768 + }
  769 +
  770 + /**
  771 + * 发送拓扑变更通知给网关设备
  772 + *
  773 + * @param gatewayId 网关设备编号
  774 + * @param status 变更状态(0-创建, 1-删除)
  775 + * @param subDevices 子设备列表
  776 + * @see <a href="https://help.aliyun.com/zh/marketplace/notify-gateway-topology-changes">阿里云 - 通知网关拓扑关系变化</a>
  777 + */
  778 + private void sendTopoChangeNotify(Long gatewayId, Integer status, List<IotDeviceDO> subDevices) {
  779 + if (CollUtil.isEmpty(subDevices)) {
  780 + return;
  781 + }
  782 + // 1. 获取网关设备
  783 + IotDeviceDO gatewayDevice = deviceMapper.selectById(gatewayId);
  784 + if (gatewayDevice == null) {
  785 + log.warn("[sendTopoChangeNotify][网关设备({}) 不存在,无法发送拓扑变更通知]", gatewayId);
  786 + return;
  787 + }
  788 +
  789 + try {
  790 + // 2.1 构建拓扑变更通知消息
  791 + List<IotDeviceIdentity> subList = convertList(subDevices, subDevice ->
  792 + new IotDeviceIdentity(subDevice.getProductKey(), subDevice.getDeviceName()));
  793 + IotDeviceTopoChangeReqDTO params = new IotDeviceTopoChangeReqDTO(status, subList);
  794 + IotDeviceMessage notifyMessage = IotDeviceMessage.requestOf(
  795 + IotDeviceMessageMethodEnum.TOPO_CHANGE.getMethod(), params);
  796 +
  797 + // 2.2 发送消息
  798 + deviceMessageService.sendDeviceMessage(notifyMessage, gatewayDevice);
  799 + log.info("[sendTopoChangeNotify][网关({}/{}) 发送拓扑变更通知成功,status={}, subDevices={}]",
  800 + gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(),
  801 + status, subList);
  802 + } catch (Exception ex) {
  803 + log.error("[sendTopoChangeNotify][网关({}/{}) 发送拓扑变更通知失败,status={}]",
  804 + gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(), status, ex);
  805 + }
  806 + }
  807 +
  808 + // ========== 设备动态注册 ==========
  809 +
  810 + @Override
  811 + public IotDeviceRegisterRespDTO registerDevice(IotDeviceRegisterReqDTO reqDTO) {
  812 + // 1.1 校验产品
  813 + IotProductDO product = TenantUtils.executeIgnore(() ->
  814 + productService.getProductByProductKey(reqDTO.getProductKey()));
  815 + if (product == null) {
  816 + throw exception(PRODUCT_NOT_EXISTS);
  817 + }
  818 + // 1.2 校验产品是否开启动态注册
  819 + if (BooleanUtil.isFalse(product.getRegisterEnabled())) {
  820 + throw exception(DEVICE_REGISTER_DISABLED);
  821 + }
  822 + // 1.3 验证 productSecret
  823 + if (ObjUtil.notEqual(product.getProductSecret(), reqDTO.getProductSecret())) {
  824 + throw exception(DEVICE_REGISTER_SECRET_INVALID);
  825 + }
  826 + return TenantUtils.execute(product.getTenantId(), () -> {
  827 + // 1.4 校验设备是否已存在(已存在则不允许重复注册)
  828 + IotDeviceDO device = getSelf().getDeviceFromCache(reqDTO.getProductKey(), reqDTO.getDeviceName());
  829 + if (device != null) {
  830 + throw exception(DEVICE_REGISTER_ALREADY_EXISTS);
  831 + }
  832 +
  833 + // 2.1 自动创建设备
  834 + IotDeviceSaveReqVO createReqVO = new IotDeviceSaveReqVO()
  835 + .setDeviceName(reqDTO.getDeviceName())
  836 + .setProductId(product.getId());
  837 + device = createDevice0(createReqVO);
  838 + log.info("[registerDevice][产品({}) 自动创建设备({})]",
  839 + reqDTO.getProductKey(), reqDTO.getDeviceName());
  840 + // 2.2 返回设备密钥
  841 + return new IotDeviceRegisterRespDTO(device.getProductKey(), device.getDeviceName(), device.getDeviceSecret());
  842 + });
  843 + }
  844 +
  845 + @Override
  846 + public List<IotSubDeviceRegisterRespDTO> registerSubDevices(IotSubDeviceRegisterFullReqDTO reqDTO) {
  847 + // 1. 校验网关设备
  848 + IotDeviceDO gatewayDevice = getSelf().getDeviceFromCache(reqDTO.getGatewayProductKey(), reqDTO.getGatewayDeviceName());
  849 +
  850 + // 2. 遍历注册每个子设备
  851 + return TenantUtils.execute(gatewayDevice.getTenantId(), () ->
  852 + registerSubDevices0(gatewayDevice, reqDTO.getSubDevices()));
  853 + }
  854 +
  855 + @Override
  856 + public List<IotSubDeviceRegisterRespDTO> handleSubDeviceRegisterMessage(IotDeviceMessage message, IotDeviceDO gatewayDevice) {
  857 + // 1. 解析参数
  858 + if (!(message.getParams() instanceof List)) {
  859 + throw exception(DEVICE_SUB_REGISTER_PARAMS_INVALID);
  860 + }
  861 + List<IotSubDeviceRegisterReqDTO> subDevices = JsonUtils.convertList(message.getParams(), IotSubDeviceRegisterReqDTO.class);
  862 +
  863 + // 2. 遍历注册每个子设备
  864 + return registerSubDevices0(gatewayDevice, subDevices);
  865 + }
  866 +
  867 + private List<IotSubDeviceRegisterRespDTO> registerSubDevices0(IotDeviceDO gatewayDevice,
  868 + List<IotSubDeviceRegisterReqDTO> subDevices) {
  869 + // 1.1 校验网关设备
  870 + if (gatewayDevice == null) {
  871 + throw exception(DEVICE_NOT_EXISTS);
  872 + }
  873 + if (!IotProductDeviceTypeEnum.isGateway(gatewayDevice.getDeviceType())) {
  874 + throw exception(DEVICE_NOT_GATEWAY);
  875 + }
  876 + // 1.2 注册设备不能为空
  877 + if (CollUtil.isEmpty(subDevices)) {
  878 + throw exception(DEVICE_SUB_REGISTER_PARAMS_INVALID);
  879 + }
  880 +
  881 + // 2. 遍历注册每个子设备
  882 + List<IotSubDeviceRegisterRespDTO> results = new ArrayList<>(subDevices.size());
  883 + for (IotSubDeviceRegisterReqDTO subDevice : subDevices) {
  884 + try {
  885 + IotDeviceDO device = registerSubDevice0(gatewayDevice, subDevice);
  886 + results.add(new IotSubDeviceRegisterRespDTO(
  887 + subDevice.getProductKey(), subDevice.getDeviceName(), device.getDeviceSecret()));
  888 + } catch (Exception ex) {
  889 + log.error("[registerSubDevices0][子设备({}/{}) 注册失败]",
  890 + subDevice.getProductKey(), subDevice.getDeviceName(), ex);
  891 + }
  892 + }
  893 + return results;
  894 + }
  895 +
  896 + private IotDeviceDO registerSubDevice0(IotDeviceDO gatewayDevice, IotSubDeviceRegisterReqDTO params) {
  897 + // 1.1 校验产品
  898 + IotProductDO product = productService.getProductByProductKey(params.getProductKey());
  899 + if (product == null) {
  900 + throw exception(PRODUCT_NOT_EXISTS);
  901 + }
  902 + // 1.2 校验产品是否为网关子设备类型
  903 + if (!IotProductDeviceTypeEnum.isGatewaySub(product.getDeviceType())) {
  904 + throw exception(DEVICE_SUB_REGISTER_PRODUCT_NOT_GATEWAY_SUB, params.getProductKey());
  905 + }
  906 + // 1.3 校验设备是否已存在(子设备动态注册:设备必须已预注册)
  907 + IotDeviceDO existDevice = getSelf().getDeviceFromCache(params.getProductKey(), params.getDeviceName());
  908 + if (existDevice == null) {
  909 + throw exception(DEVICE_NOT_EXISTS);
  910 + }
  911 + // 1.4 校验是否绑定到其他网关
  912 + if (existDevice.getGatewayId() != null && ObjUtil.notEqual(existDevice.getGatewayId(), gatewayDevice.getId())) {
  913 + throw exception(DEVICE_GATEWAY_BINDTO_EXISTS,
  914 + existDevice.getProductKey(), existDevice.getDeviceName());
  915 + }
  916 +
  917 + // 2. 绑定到网关(如果尚未绑定)
  918 + if (existDevice.getGatewayId() == null) {
  919 + // 2.1 更新数据库
  920 + deviceMapper.updateById(new IotDeviceDO().setId(existDevice.getId()).setGatewayId(gatewayDevice.getId()));
  921 + // 2.2 清空对应缓存
  922 + deleteDeviceCache(existDevice);
  923 + log.info("[registerSubDevice][网关({}/{}) 绑定子设备({}/{})]",
  924 + gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(),
  925 + existDevice.getProductKey(), existDevice.getDeviceName());
  926 + }
  927 + return existDevice;
590 } 928 }
591 929
592 } 930 }
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;
  4 +import cn.hutool.core.collection.ListUtil;
3 import cn.hutool.core.date.LocalDateTimeUtil; 5 import cn.hutool.core.date.LocalDateTimeUtil;
4 import cn.hutool.core.lang.Assert; 6 import cn.hutool.core.lang.Assert;
5 import cn.hutool.core.map.MapUtil; 7 import cn.hutool.core.map.MapUtil;
@@ -16,6 +18,10 @@ import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsD @@ -16,6 +18,10 @@ import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsD
16 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; 18 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
17 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 19 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
18 import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer; 20 import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer;
  21 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
  22 +import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
  23 +import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPackPostReqDTO;
  24 +import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
19 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; 25 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
20 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; 26 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
21 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceMessageDO; 27 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceMessageDO;
@@ -98,7 +104,6 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { @@ -98,7 +104,6 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
98 return sendDeviceMessage(message, device); 104 return sendDeviceMessage(message, device);
99 } 105 }
100 106
101 - // TODO @芋艿:针对连接网关的设备,是不是 productKey、deviceName 需要调整下;  
102 @Override 107 @Override
103 public IotDeviceMessage sendDeviceMessage(IotDeviceMessage message, IotDeviceDO device) { 108 public IotDeviceMessage sendDeviceMessage(IotDeviceMessage message, IotDeviceDO device) {
104 return sendDeviceMessage(message, device, null); 109 return sendDeviceMessage(message, device, null);
@@ -168,7 +173,7 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { @@ -168,7 +173,7 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
168 // 2. 记录消息 173 // 2. 记录消息
169 getSelf().createDeviceLogAsync(message); 174 getSelf().createDeviceLogAsync(message);
170 175
171 - // 3. 回复消息。前提:非 _reply 消息,并且非禁用回复的消息 176 + // 3. 回复消息。前提:非 _reply 消息、非禁用回复的消息
172 if (IotDeviceMessageUtils.isReplyMessage(message) 177 if (IotDeviceMessageUtils.isReplyMessage(message)
173 || IotDeviceMessageMethodEnum.isReplyDisabled(message.getMethod()) 178 || IotDeviceMessageMethodEnum.isReplyDisabled(message.getMethod())
174 || StrUtil.isEmpty(message.getServerId())) { 179 || StrUtil.isEmpty(message.getServerId())) {
@@ -185,15 +190,14 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { @@ -185,15 +190,14 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
185 } 190 }
186 191
187 // TODO @芋艿:可优化:未来逻辑复杂后,可以独立拆除 Processor 处理器 192 // TODO @芋艿:可优化:未来逻辑复杂后,可以独立拆除 Processor 处理器
188 - @SuppressWarnings("SameReturnValue")  
189 private Object handleUpstreamDeviceMessage0(IotDeviceMessage message, IotDeviceDO device) { 193 private Object handleUpstreamDeviceMessage0(IotDeviceMessage message, IotDeviceDO device) {
190 // 设备上下线 194 // 设备上下线
191 if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod())) { 195 if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod())) {
192 String stateStr = IotDeviceMessageUtils.getIdentifier(message); 196 String stateStr = IotDeviceMessageUtils.getIdentifier(message);
193 assert stateStr != null; 197 assert stateStr != null;
194 Assert.notEmpty(stateStr, "设备状态不能为空"); 198 Assert.notEmpty(stateStr, "设备状态不能为空");
195 - deviceService.updateDeviceState(device, Integer.valueOf(stateStr));  
196 - // TODO 芋艿:子设备的关联 199 + Integer state = Integer.valueOf(stateStr);
  200 + deviceService.updateDeviceState(device, state);
197 return null; 201 return null;
198 } 202 }
199 203
@@ -202,6 +206,11 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { @@ -202,6 +206,11 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
202 devicePropertyService.saveDeviceProperty(device, message); 206 devicePropertyService.saveDeviceProperty(device, message);
203 return null; 207 return null;
204 } 208 }
  209 + // 批量上报(属性+事件+子设备)
  210 + if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod())) {
  211 + handlePackMessage(message, device);
  212 + return null;
  213 + }
205 214
206 // OTA 上报升级进度 215 // OTA 上报升级进度
207 if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.OTA_PROGRESS.getMethod())) { 216 if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.OTA_PROGRESS.getMethod())) {
@@ -209,10 +218,109 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { @@ -209,10 +218,109 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
209 return null; 218 return null;
210 } 219 }
211 220
212 - // TODO @芋艿:这里可以按需,添加别的逻辑; 221 + // 添加拓扑关系
  222 + if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.TOPO_ADD.getMethod())) {
  223 + return deviceService.handleTopoAddMessage(message, device);
  224 + }
  225 + // 删除拓扑关系
  226 + if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod())) {
  227 + return deviceService.handleTopoDeleteMessage(message, device);
  228 + }
  229 + // 获取拓扑关系
  230 + if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.TOPO_GET.getMethod())) {
  231 + return deviceService.handleTopoGetMessage(device);
  232 + }
  233 +
  234 + // 子设备动态注册
  235 + if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod())) {
  236 + return deviceService.handleSubDeviceRegisterMessage(message, device);
  237 + }
  238 +
213 return null; 239 return null;
214 } 240 }
215 241
  242 + // ========== 批量上报处理方法 ==========
  243 +
  244 + /**
  245 + * 处理批量上报消息
  246 + * <p>
  247 + * 将 pack 消息拆分成多条标准消息,发送到 MQ 让规则引擎处理
  248 + *
  249 + * @param packMessage 批量消息
  250 + * @param gatewayDevice 网关设备
  251 + */
  252 + private void handlePackMessage(IotDeviceMessage packMessage, IotDeviceDO gatewayDevice) {
  253 + // 1. 解析参数
  254 + IotDevicePropertyPackPostReqDTO params = JsonUtils.convertObject(
  255 + packMessage.getParams(), IotDevicePropertyPackPostReqDTO.class);
  256 + if (params == null) {
  257 + log.warn("[handlePackMessage][消息({}) 参数解析失败]", packMessage);
  258 + return;
  259 + }
  260 +
  261 + // 2. 处理网关设备(自身)的数据
  262 + sendDevicePackData(gatewayDevice, packMessage.getServerId(), params.getProperties(), params.getEvents());
  263 +
  264 + // 3. 处理子设备的数据
  265 + if (CollUtil.isEmpty(params.getSubDevices())) {
  266 + return;
  267 + }
  268 + for (IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData : params.getSubDevices()) {
  269 + try {
  270 + IotDeviceIdentity identity = subDeviceData.getIdentity();
  271 + IotDeviceDO subDevice = deviceService.getDeviceFromCache(identity.getProductKey(), identity.getDeviceName());
  272 + if (subDevice == null) {
  273 + log.warn("[handlePackMessage][子设备({}/{}) 不存在]", identity.getProductKey(), identity.getDeviceName());
  274 + continue;
  275 + }
  276 + // 特殊:子设备不需要指定 serverId,因为子设备实际可能连接在不同的 gateway-server 上,导致 serverId 不同
  277 + sendDevicePackData(subDevice, null, subDeviceData.getProperties(), subDeviceData.getEvents());
  278 + } catch (Exception ex) {
  279 + log.error("[handlePackMessage][子设备({}/{}) 数据处理失败]", subDeviceData.getIdentity().getProductKey(),
  280 + subDeviceData.getIdentity().getDeviceName(), ex);
  281 + }
  282 + }
  283 + }
  284 +
  285 + /**
  286 + * 发送设备 pack 数据到 MQ(属性 + 事件)
  287 + *
  288 + * @param device 设备
  289 + * @param serverId 服务标识
  290 + * @param properties 属性数据
  291 + * @param events 事件数据
  292 + */
  293 + private void sendDevicePackData(IotDeviceDO device, String serverId,
  294 + Map<String, Object> properties,
  295 + Map<String, IotDevicePropertyPackPostReqDTO.EventValue> events) {
  296 + // 1. 发送属性消息
  297 + if (MapUtil.isNotEmpty(properties)) {
  298 + IotDeviceMessage propertyMsg = IotDeviceMessage.requestOf(
  299 + device.getId(), device.getTenantId(), serverId,
  300 + IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(),
  301 + IotDevicePropertyPostReqDTO.of(properties));
  302 + deviceMessageProducer.sendDeviceMessage(propertyMsg);
  303 + }
  304 +
  305 + // 2. 发送事件消息
  306 + if (MapUtil.isNotEmpty(events)) {
  307 + for (Map.Entry<String, IotDevicePropertyPackPostReqDTO.EventValue> eventEntry : events.entrySet()) {
  308 + String eventId = eventEntry.getKey();
  309 + IotDevicePropertyPackPostReqDTO.EventValue eventValue = eventEntry.getValue();
  310 + if (eventValue == null) {
  311 + continue;
  312 + }
  313 + IotDeviceMessage eventMsg = IotDeviceMessage.requestOf(
  314 + device.getId(), device.getTenantId(), serverId,
  315 + IotDeviceMessageMethodEnum.EVENT_POST.getMethod(),
  316 + IotDeviceEventPostReqDTO.of(eventId, eventValue.getValue(), eventValue.getTime()));
  317 + deviceMessageProducer.sendDeviceMessage(eventMsg);
  318 + }
  319 + }
  320 + }
  321 +
  322 + // ========= 设备消息查询 ==========
  323 +
216 @Override 324 @Override
217 public PageResult<IotDeviceMessageDO> getDeviceMessagePage(IotDeviceMessagePageReqVO pageReqVO) { 325 public PageResult<IotDeviceMessageDO> getDeviceMessagePage(IotDeviceMessagePageReqVO pageReqVO) {
218 try { 326 try {
@@ -228,9 +336,10 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { @@ -228,9 +336,10 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
228 } 336 }
229 337
230 @Override 338 @Override
231 - public List<IotDeviceMessageDO> getDeviceMessageListByRequestIdsAndReply(Long deviceId,  
232 - List<String> requestIds,  
233 - Boolean reply) { 339 + public List<IotDeviceMessageDO> getDeviceMessageListByRequestIdsAndReply(Long deviceId, List<String> requestIds, Boolean reply) {
  340 + if (CollUtil.isEmpty(requestIds)) {
  341 + return ListUtil.of();
  342 + }
234 return deviceMessageMapper.selectListByRequestIdsAndReply(deviceId, requestIds, reply); 343 return deviceMessageMapper.selectListByRequestIdsAndReply(deviceId, requestIds, reply);
235 } 344 }
236 345
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java
@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.iot.service.product; @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.iot.service.product;
3 import cn.hutool.core.collection.CollUtil; 3 import cn.hutool.core.collection.CollUtil;
4 import cn.iocoder.yudao.framework.common.pojo.PageResult; 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
5 import cn.iocoder.yudao.framework.common.util.object.BeanUtils; 5 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  6 +import cn.hutool.core.util.IdUtil;
6 import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; 7 import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
7 import cn.iocoder.yudao.module.iot.controller.admin.product.vo.product.IotProductPageReqVO; 8 import cn.iocoder.yudao.module.iot.controller.admin.product.vo.product.IotProductPageReqVO;
8 import cn.iocoder.yudao.module.iot.controller.admin.product.vo.product.IotProductSaveReqVO; 9 import cn.iocoder.yudao.module.iot.controller.admin.product.vo.product.IotProductSaveReqVO;
@@ -53,19 +54,22 @@ public class IotProductServiceImpl implements IotProductService { @@ -53,19 +54,22 @@ public class IotProductServiceImpl implements IotProductService {
53 54
54 // 2. 插入 55 // 2. 插入
55 IotProductDO product = BeanUtils.toBean(createReqVO, IotProductDO.class) 56 IotProductDO product = BeanUtils.toBean(createReqVO, IotProductDO.class)
56 - .setStatus(IotProductStatusEnum.UNPUBLISHED.getStatus()); 57 + .setStatus(IotProductStatusEnum.UNPUBLISHED.getStatus())
  58 + .setProductSecret(generateProductSecret());
57 productMapper.insert(product); 59 productMapper.insert(product);
58 return product.getId(); 60 return product.getId();
59 } 61 }
60 62
  63 + private String generateProductSecret() {
  64 + return IdUtil.fastSimpleUUID();
  65 + }
  66 +
61 @Override 67 @Override
62 @CacheEvict(value = RedisKeyConstants.PRODUCT, key = "#updateReqVO.id") 68 @CacheEvict(value = RedisKeyConstants.PRODUCT, key = "#updateReqVO.id")
63 public void updateProduct(IotProductSaveReqVO updateReqVO) { 69 public void updateProduct(IotProductSaveReqVO updateReqVO) {
64 updateReqVO.setProductKey(null); // 不更新产品标识 70 updateReqVO.setProductKey(null); // 不更新产品标识
65 - // 1.1 校验存在  
66 - IotProductDO iotProductDO = validateProductExists(updateReqVO.getId());  
67 - // 1.2 发布状态不可更新  
68 - validateProductStatus(iotProductDO); 71 + // 1. 校验存在
  72 + validateProductExists(updateReqVO.getId());
69 73
70 // 2. 更新 74 // 2. 更新
71 IotProductDO updateObj = BeanUtils.toBean(updateReqVO, IotProductDO.class); 75 IotProductDO updateObj = BeanUtils.toBean(updateReqVO, IotProductDO.class);
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/IotDeviceCommonApi.java
@@ -4,6 +4,12 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult; @@ -4,6 +4,12 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
4 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; 4 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; 5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO;
6 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; 6 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
  7 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
  8 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
  9 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
  10 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO;
  11 +
  12 +import java.util.List;
7 13
8 /** 14 /**
9 * IoT 设备通用 API 15 * IoT 设备通用 API
@@ -28,4 +34,20 @@ public interface IotDeviceCommonApi { @@ -28,4 +34,20 @@ public interface IotDeviceCommonApi {
28 */ 34 */
29 CommonResult<IotDeviceRespDTO> getDevice(IotDeviceGetReqDTO infoReqDTO); 35 CommonResult<IotDeviceRespDTO> getDevice(IotDeviceGetReqDTO infoReqDTO);
30 36
  37 + /**
  38 + * 直连/网关设备动态注册(一型一密)
  39 + *
  40 + * @param reqDTO 动态注册请求
  41 + * @return 注册结果(包含 DeviceSecret)
  42 + */
  43 + CommonResult<IotDeviceRegisterRespDTO> registerDevice(IotDeviceRegisterReqDTO reqDTO);
  44 +
  45 + /**
  46 + * 网关子设备动态注册(网关代理转发)
  47 + *
  48 + * @param reqDTO 子设备注册请求(包含网关标识和子设备列表)
  49 + * @return 注册结果列表
  50 + */
  51 + CommonResult<List<IotSubDeviceRegisterRespDTO>> registerSubDevices(IotSubDeviceRegisterFullReqDTO reqDTO);
  52 +
31 } 53 }
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotDeviceAuthReqDTO.java
1 package cn.iocoder.yudao.module.iot.core.biz.dto; 1 package cn.iocoder.yudao.module.iot.core.biz.dto;
2 2
3 import jakarta.validation.constraints.NotEmpty; 3 import jakarta.validation.constraints.NotEmpty;
  4 +import lombok.AllArgsConstructor;
4 import lombok.Data; 5 import lombok.Data;
  6 +import lombok.NoArgsConstructor;
5 7
6 /** 8 /**
7 * IoT 设备认证 Request DTO 9 * IoT 设备认证 Request DTO
@@ -9,6 +11,8 @@ import lombok.Data; @@ -9,6 +11,8 @@ import lombok.Data;
9 * @author 芋道源码 11 * @author 芋道源码
10 */ 12 */
11 @Data 13 @Data
  14 +@NoArgsConstructor
  15 +@AllArgsConstructor
12 public class IotDeviceAuthReqDTO { 16 public class IotDeviceAuthReqDTO {
13 17
14 /** 18 /**
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotSubDeviceRegisterFullReqDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.biz.dto;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO;
  4 +import jakarta.validation.constraints.NotEmpty;
  5 +import jakarta.validation.constraints.NotNull;
  6 +import lombok.Data;
  7 +
  8 +import java.util.List;
  9 +
  10 +/**
  11 + * IoT 子设备动态注册 Request DTO
  12 + * <p>
  13 + * 额外包含了网关设备的标识信息
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +@Data
  18 +public class IotSubDeviceRegisterFullReqDTO {
  19 +
  20 + /**
  21 + * 网关设备 ProductKey
  22 + */
  23 + @NotEmpty(message = "网关产品标识不能为空")
  24 + private String gatewayProductKey;
  25 +
  26 + /**
  27 + * 网关设备 DeviceName
  28 + */
  29 + @NotEmpty(message = "网关设备名称不能为空")
  30 + private String gatewayDeviceName;
  31 +
  32 + /**
  33 + * 子设备注册列表
  34 + */
  35 + @NotNull(message = "子设备注册列表不能为空")
  36 + private List<IotSubDeviceRegisterReqDTO> subDevices;
  37 +
  38 +}
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceMessageMethodEnum.java
@@ -24,12 +24,28 @@ public enum IotDeviceMessageMethodEnum implements ArrayValuable&lt;String&gt; { @@ -24,12 +24,28 @@ public enum IotDeviceMessageMethodEnum implements ArrayValuable&lt;String&gt; {
24 24
25 // TODO 芋艿:要不要加个 ping 消息; 25 // TODO 芋艿:要不要加个 ping 消息;
26 26
  27 + // ========== 拓扑管理 ==========
  28 + // 可参考:https://help.aliyun.com/zh/iot/user-guide/manage-topological-relationships
  29 +
  30 + TOPO_ADD("thing.topo.add", "添加拓扑关系", true),
  31 + TOPO_DELETE("thing.topo.delete", "删除拓扑关系", true),
  32 + TOPO_GET("thing.topo.get", "获取拓扑关系", true),
  33 + TOPO_CHANGE("thing.topo.change", "拓扑关系变更通知", false),
  34 +
  35 + // ========== 设备注册 ==========
  36 + // 可参考:https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification
  37 +
  38 + DEVICE_REGISTER("thing.auth.register", "设备动态注册", true),
  39 + SUB_DEVICE_REGISTER("thing.auth.register.sub", "子设备动态注册", true),
  40 +
27 // ========== 设备属性 ========== 41 // ========== 设备属性 ==========
28 // 可参考:https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services 42 // 可参考:https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services
29 43
30 PROPERTY_POST("thing.property.post", "属性上报", true), 44 PROPERTY_POST("thing.property.post", "属性上报", true),
31 PROPERTY_SET("thing.property.set", "属性设置", false), 45 PROPERTY_SET("thing.property.set", "属性设置", false),
32 46
  47 + PROPERTY_PACK_POST("thing.event.property.pack.post", "批量上报(属性 + 事件 + 子设备)", true), // 网关独有
  48 +
33 // ========== 设备事件 ========== 49 // ========== 设备事件 ==========
34 // 可参考:https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services 50 // 可参考:https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services
35 51
@@ -50,6 +66,7 @@ public enum IotDeviceMessageMethodEnum implements ArrayValuable&lt;String&gt; { @@ -50,6 +66,7 @@ public enum IotDeviceMessageMethodEnum implements ArrayValuable&lt;String&gt; {
50 66
51 OTA_UPGRADE("thing.ota.upgrade", "OTA 固定信息推送", false), 67 OTA_UPGRADE("thing.ota.upgrade", "OTA 固定信息推送", false),
52 OTA_PROGRESS("thing.ota.progress", "OTA 升级进度上报", true), 68 OTA_PROGRESS("thing.ota.progress", "OTA 升级进度上报", true),
  69 +
53 ; 70 ;
54 71
55 public static final String[] ARRAYS = Arrays.stream(values()).map(IotDeviceMessageMethodEnum::getMethod) 72 public static final String[] ARRAYS = Arrays.stream(values()).map(IotDeviceMessageMethodEnum::getMethod)
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/mq/message/IotDeviceMessage.java
@@ -108,6 +108,23 @@ public class IotDeviceMessage { @@ -108,6 +108,23 @@ public class IotDeviceMessage {
108 return of(requestId, method, params, null, null, null); 108 return of(requestId, method, params, null, null, null);
109 } 109 }
110 110
  111 + /**
  112 + * 创建设备请求消息(包含设备信息)
  113 + *
  114 + * @param deviceId 设备编号
  115 + * @param tenantId 租户编号
  116 + * @param serverId 服务标识
  117 + * @param method 消息方法
  118 + * @param params 消息参数
  119 + * @return 消息对象
  120 + */
  121 + public static IotDeviceMessage requestOf(Long deviceId, Long tenantId, String serverId,
  122 + String method, Object params) {
  123 + IotDeviceMessage message = of(null, method, params, null, null, null);
  124 + return message.setId(IotDeviceMessageUtils.generateMessageId())
  125 + .setDeviceId(deviceId).setTenantId(tenantId).setServerId(serverId);
  126 + }
  127 +
111 public static IotDeviceMessage replyOf(String requestId, String method, 128 public static IotDeviceMessage replyOf(String requestId, String method,
112 Object data, Integer code, String msg) { 129 Object data, Integer code, String msg) {
113 if (code == null) { 130 if (code == null) {
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/IotDeviceIdentity.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.topic;
  2 +
  3 +import jakarta.validation.constraints.NotEmpty;
  4 +import lombok.AllArgsConstructor;
  5 +import lombok.Data;
  6 +import lombok.NoArgsConstructor;
  7 +
  8 +/**
  9 + * IoT 设备标识
  10 + *
  11 + * 用于标识一个设备的基本信息(productKey + deviceName)
  12 + *
  13 + * @author 芋道源码
  14 + */
  15 +@Data
  16 +@NoArgsConstructor
  17 +@AllArgsConstructor
  18 +public class IotDeviceIdentity {
  19 +
  20 + /**
  21 + * 产品标识
  22 + */
  23 + @NotEmpty(message = "产品标识不能为空")
  24 + private String productKey;
  25 +
  26 + /**
  27 + * 设备名称
  28 + */
  29 + @NotEmpty(message = "设备名称不能为空")
  30 + private String deviceName;
  31 +
  32 +}
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterReqDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.topic.auth;
  2 +
  3 +import jakarta.validation.constraints.NotEmpty;
  4 +import lombok.Data;
  5 +
  6 +/**
  7 + * IoT 设备动态注册 Request DTO
  8 + * <p>
  9 + * 用于直连设备/网关的一型一密动态注册:使用 productSecret 验证,返回 deviceSecret
  10 + *
  11 + * @author 芋道源码
  12 + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
  13 + */
  14 +@Data
  15 +public class IotDeviceRegisterReqDTO {
  16 +
  17 + /**
  18 + * 产品标识
  19 + */
  20 + @NotEmpty(message = "产品标识不能为空")
  21 + private String productKey;
  22 +
  23 + /**
  24 + * 设备名称
  25 + */
  26 + @NotEmpty(message = "设备名称不能为空")
  27 + private String deviceName;
  28 +
  29 + /**
  30 + * 产品密钥
  31 + */
  32 + @NotEmpty(message = "产品密钥不能为空")
  33 + private String productSecret;
  34 +
  35 +}
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterRespDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.topic.auth;
  2 +
  3 +import lombok.AllArgsConstructor;
  4 +import lombok.Data;
  5 +import lombok.NoArgsConstructor;
  6 +
  7 +/**
  8 + * IoT 设备动态注册 Response DTO
  9 + * <p>
  10 + * 用于直连设备/网关的一型一密动态注册响应
  11 + *
  12 + * @author 芋道源码
  13 + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
  14 + */
  15 +@Data
  16 +@NoArgsConstructor
  17 +@AllArgsConstructor
  18 +public class IotDeviceRegisterRespDTO {
  19 +
  20 + /**
  21 + * 产品标识
  22 + */
  23 + private String productKey;
  24 +
  25 + /**
  26 + * 设备名称
  27 + */
  28 + private String deviceName;
  29 +
  30 + /**
  31 + * 设备密钥
  32 + */
  33 + private String deviceSecret;
  34 +
  35 +}
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterReqDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.topic.auth;
  2 +
  3 +import jakarta.validation.constraints.NotEmpty;
  4 +import lombok.Data;
  5 +
  6 +/**
  7 + * IoT 子设备动态注册 Request DTO
  8 + * <p>
  9 + * 用于 thing.auth.register.sub 消息的 params 数组元素
  10 + *
  11 + * 特殊:网关子设备的动态注册,必须已经创建好该网关子设备(不然哪来的 {@link #deviceName} 字段)。更多的好处,是设备不用提前烧录 deviceSecret 密钥。
  12 + *
  13 + * @author 芋道源码
  14 + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/register-devices">阿里云 - 动态注册子设备</a>
  15 + */
  16 +@Data
  17 +public class IotSubDeviceRegisterReqDTO {
  18 +
  19 + /**
  20 + * 子设备 ProductKey
  21 + */
  22 + @NotEmpty(message = "产品标识不能为空")
  23 + private String productKey;
  24 +
  25 + /**
  26 + * 子设备 DeviceName
  27 + */
  28 + @NotEmpty(message = "设备名称不能为空")
  29 + private String deviceName;
  30 +
  31 +}
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterRespDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.topic.auth;
  2 +
  3 +import lombok.AllArgsConstructor;
  4 +import lombok.Data;
  5 +import lombok.NoArgsConstructor;
  6 +
  7 +/**
  8 + * IoT 子设备动态注册 Response DTO
  9 + * <p>
  10 + * 用于 thing.auth.register.sub 响应的设备信息
  11 + *
  12 + * @author 芋道源码
  13 + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/register-devices">阿里云 - 动态注册子设备</a>
  14 + */
  15 +@Data
  16 +@NoArgsConstructor
  17 +@AllArgsConstructor
  18 +public class IotSubDeviceRegisterRespDTO {
  19 +
  20 + /**
  21 + * 子设备 ProductKey
  22 + */
  23 + private String productKey;
  24 +
  25 + /**
  26 + * 子设备 DeviceName
  27 + */
  28 + private String deviceName;
  29 +
  30 + /**
  31 + * 分配的 DeviceSecret
  32 + */
  33 + private String deviceSecret;
  34 +
  35 +}
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/event/IotDeviceEventPostReqDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.topic.event;
  2 +
  3 +import lombok.Data;
  4 +
  5 +/**
  6 + * IoT 设备事件上报 Request DTO
  7 + * <p>
  8 + * 用于 thing.event.post 消息的 params 参数
  9 + *
  10 + * @author 芋道源码
  11 + * @see <a href="http://help.aliyun.com/zh/marketplace/device-reporting-events">阿里云 - 设备上报事件</a>
  12 + */
  13 +@Data
  14 +public class IotDeviceEventPostReqDTO {
  15 +
  16 + /**
  17 + * 事件标识符
  18 + */
  19 + private String identifier;
  20 +
  21 + /**
  22 + * 事件输出参数
  23 + */
  24 + private Object value;
  25 +
  26 + /**
  27 + * 上报时间(毫秒时间戳,可选)
  28 + */
  29 + private Long time;
  30 +
  31 + /**
  32 + * 创建事件上报 DTO
  33 + *
  34 + * @param identifier 事件标识符
  35 + * @param value 事件值
  36 + * @return DTO 对象
  37 + */
  38 + public static IotDeviceEventPostReqDTO of(String identifier, Object value) {
  39 + return of(identifier, value, null);
  40 + }
  41 +
  42 + /**
  43 + * 创建事件上报 DTO(带时间)
  44 + *
  45 + * @param identifier 事件标识符
  46 + * @param value 事件值
  47 + * @param time 上报时间
  48 + * @return DTO 对象
  49 + */
  50 + public static IotDeviceEventPostReqDTO of(String identifier, Object value, Long time) {
  51 + return new IotDeviceEventPostReqDTO().setIdentifier(identifier).setValue(value).setTime(time);
  52 + }
  53 +
  54 +}
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/package-info.java 0 → 100644
  1 +/**
  2 + * IoT Topic 消息体 DTO 定义
  3 + * <p>
  4 + * 定义设备与平台通信的消息体结构,遵循(参考)阿里云 Alink 协议规范
  5 + *
  6 + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/alink-protocol-1">阿里云 Alink 协议</a>
  7 + */
  8 +package cn.iocoder.yudao.module.iot.core.topic;
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertyPackPostReqDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.topic.property;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
  4 +import lombok.Data;
  5 +
  6 +import java.util.List;
  7 +import java.util.Map;
  8 +
  9 +/**
  10 + * IoT 设备属性批量上报 Request DTO
  11 + * <p>
  12 + * 用于 thing.event.property.pack.post 消息的 params 参数
  13 + *
  14 + * @author 芋道源码
  15 + * @see <a href="http://help.aliyun.com/zh/marketplace/gateway-reports-data-in-batches">阿里云 - 网关批量上报数据</a>
  16 + */
  17 +@Data
  18 +public class IotDevicePropertyPackPostReqDTO {
  19 +
  20 + /**
  21 + * 网关自身属性
  22 + * <p>
  23 + * key: 属性标识符
  24 + * value: 属性值
  25 + */
  26 + private Map<String, Object> properties;
  27 +
  28 + /**
  29 + * 网关自身事件
  30 + * <p>
  31 + * key: 事件标识符
  32 + * value: 事件值对象(包含 value 和 time)
  33 + */
  34 + private Map<String, EventValue> events;
  35 +
  36 + /**
  37 + * 子设备数据列表
  38 + */
  39 + private List<SubDeviceData> subDevices;
  40 +
  41 + /**
  42 + * 事件值对象
  43 + */
  44 + @Data
  45 + public static class EventValue {
  46 +
  47 + /**
  48 + * 事件参数
  49 + */
  50 + private Object value;
  51 +
  52 + /**
  53 + * 上报时间(毫秒时间戳)
  54 + */
  55 + private Long time;
  56 +
  57 + }
  58 +
  59 + /**
  60 + * 子设备数据
  61 + */
  62 + @Data
  63 + public static class SubDeviceData {
  64 +
  65 + /**
  66 + * 子设备标识
  67 + */
  68 + private IotDeviceIdentity identity;
  69 +
  70 + /**
  71 + * 子设备属性
  72 + * <p>
  73 + * key: 属性标识符
  74 + * value: 属性值
  75 + */
  76 + private Map<String, Object> properties;
  77 +
  78 + /**
  79 + * 子设备事件
  80 + * <p>
  81 + * key: 事件标识符
  82 + * value: 事件值对象(包含 value 和 time)
  83 + */
  84 + private Map<String, EventValue> events;
  85 +
  86 + }
  87 +
  88 +}
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertyPostReqDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.topic.property;
  2 +
  3 +import java.util.HashMap;
  4 +import java.util.Map;
  5 +
  6 +/**
  7 + * IoT 设备属性上报 Request DTO
  8 + * <p>
  9 + * 用于 thing.property.post 消息的 params 参数
  10 + * <p>
  11 + * 本质是一个 Map,key 为属性标识符,value 为属性值
  12 + *
  13 + * @author 芋道源码
  14 + * @see <a href="http://help.aliyun.com/zh/marketplace/device-reporting-attributes">阿里云 - 设备上报属性</a>
  15 + */
  16 +public class IotDevicePropertyPostReqDTO extends HashMap<String, Object> {
  17 +
  18 + public IotDevicePropertyPostReqDTO() {
  19 + super();
  20 + }
  21 +
  22 + public IotDevicePropertyPostReqDTO(Map<String, Object> properties) {
  23 + super(properties);
  24 + }
  25 +
  26 + /**
  27 + * 创建属性上报 DTO
  28 + *
  29 + * @param properties 属性数据
  30 + * @return DTO 对象
  31 + */
  32 + public static IotDevicePropertyPostReqDTO of(Map<String, Object> properties) {
  33 + return new IotDevicePropertyPostReqDTO(properties);
  34 + }
  35 +
  36 +}
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoAddReqDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.topic.topo;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
  4 +import jakarta.validation.constraints.NotEmpty;
  5 +import lombok.Data;
  6 +
  7 +import java.util.List;
  8 +
  9 +/**
  10 + * IoT 设备拓扑添加 Request DTO
  11 + * <p>
  12 + * 用于 thing.topo.add 消息的 params 参数
  13 + *
  14 + * @author 芋道源码
  15 + * @see <a href="http://help.aliyun.com/zh/marketplace/add-topological-relationship">阿里云 - 添加拓扑关系</a>
  16 + */
  17 +@Data
  18 +public class IotDeviceTopoAddReqDTO {
  19 +
  20 + /**
  21 + * 子设备认证信息列表
  22 + * <p>
  23 + * 复用 {@link IotDeviceAuthReqDTO},包含 clientId、username、password
  24 + */
  25 + @NotEmpty(message = "子设备认证信息列表不能为空")
  26 + private List<IotDeviceAuthReqDTO> subDevices;
  27 +
  28 +}
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoChangeReqDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.topic.topo;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
  4 +import lombok.AllArgsConstructor;
  5 +import lombok.Data;
  6 +import lombok.NoArgsConstructor;
  7 +
  8 +import java.util.List;
  9 +
  10 +/**
  11 + * IoT 设备拓扑关系变更通知 Request DTO
  12 + * <p>
  13 + * 用于 thing.topo.change 下行消息的 params 参数
  14 + *
  15 + * @author 芋道源码
  16 + * @see <a href="https://help.aliyun.com/zh/marketplace/notify-gateway-topology-changes">阿里云 - 通知网关拓扑关系变化</a>
  17 + */
  18 +@Data
  19 +@NoArgsConstructor
  20 +@AllArgsConstructor
  21 +public class IotDeviceTopoChangeReqDTO {
  22 +
  23 + public static final Integer STATUS_CREATE = 0;
  24 + public static final Integer STATUS_DELETE = 1;
  25 +
  26 + /**
  27 + * 拓扑关系状态
  28 + */
  29 + private Integer status;
  30 +
  31 + /**
  32 + * 子设备列表
  33 + */
  34 + private List<IotDeviceIdentity> subList;
  35 +
  36 + public static IotDeviceTopoChangeReqDTO ofCreate(List<IotDeviceIdentity> subList) {
  37 + return new IotDeviceTopoChangeReqDTO(STATUS_CREATE, subList);
  38 + }
  39 +
  40 + public static IotDeviceTopoChangeReqDTO ofDelete(List<IotDeviceIdentity> subList) {
  41 + return new IotDeviceTopoChangeReqDTO(STATUS_DELETE, subList);
  42 + }
  43 +
  44 +}
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoDeleteReqDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.topic.topo;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
  4 +import jakarta.validation.Valid;
  5 +import jakarta.validation.constraints.NotEmpty;
  6 +import lombok.Data;
  7 +
  8 +import java.util.List;
  9 +
  10 +/**
  11 + * IoT 设备拓扑删除 Request DTO
  12 + * <p>
  13 + * 用于 thing.topo.delete 消息的 params 参数
  14 + *
  15 + * @author 芋道源码
  16 + * @see <a href="https://help.aliyun.com/zh/marketplace/delete-a-topological-relationship">阿里云 - 删除拓扑关系</a>
  17 + */
  18 +@Data
  19 +public class IotDeviceTopoDeleteReqDTO {
  20 +
  21 + /**
  22 + * 子设备标识列表
  23 + */
  24 + @Valid
  25 + @NotEmpty(message = "子设备标识列表不能为空")
  26 + private List<IotDeviceIdentity> subDevices;
  27 +
  28 +}
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoGetReqDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.topic.topo;
  2 +
  3 +import lombok.Data;
  4 +
  5 +/**
  6 + * IoT 设备拓扑关系获取 Request DTO
  7 + * <p>
  8 + * 用于 thing.topo.get 请求的 params 参数(目前为空,预留扩展)
  9 + *
  10 + * @author 芋道源码
  11 + * @see <a href="https://help.aliyun.com/zh/marketplace/obtain-topological-relationship">阿里云 - 获取拓扑关系</a>
  12 + */
  13 +@Data
  14 +public class IotDeviceTopoGetReqDTO {
  15 +
  16 +}
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoGetRespDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.topic.topo;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
  4 +import lombok.Data;
  5 +
  6 +import java.util.List;
  7 +
  8 +/**
  9 + * IoT 设备拓扑关系获取 Response DTO
  10 + * <p>
  11 + * 用于 thing.topo.get 响应
  12 + *
  13 + * @author 芋道源码
  14 + * @see <a href="https://help.aliyun.com/zh/marketplace/obtain-topological-relationship">阿里云 - 获取拓扑关系</a>
  15 + */
  16 +@Data
  17 +public class IotDeviceTopoGetRespDTO {
  18 +
  19 + /**
  20 + * 子设备列表
  21 + */
  22 + private List<IotDeviceIdentity> subDevices;
  23 +
  24 +}
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceAuthUtils.java
1 package cn.iocoder.yudao.module.iot.core.util; 1 package cn.iocoder.yudao.module.iot.core.util;
2 2
  3 +import cn.hutool.core.util.StrUtil;
3 import cn.hutool.crypto.digest.DigestUtil; 4 import cn.hutool.crypto.digest.DigestUtil;
4 import cn.hutool.crypto.digest.HmacAlgorithm; 5 import cn.hutool.crypto.digest.HmacAlgorithm;
5 -import lombok.AllArgsConstructor;  
6 -import lombok.Data;  
7 -import lombok.NoArgsConstructor; 6 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
  7 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
8 8
9 /** 9 /**
10 * IoT 设备【认证】的工具类,参考阿里云 10 * IoT 设备【认证】的工具类,参考阿里云
@@ -13,73 +13,40 @@ import lombok.NoArgsConstructor; @@ -13,73 +13,40 @@ import lombok.NoArgsConstructor;
13 */ 13 */
14 public class IotDeviceAuthUtils { 14 public class IotDeviceAuthUtils {
15 15
16 - /**  
17 - * 认证信息  
18 - */  
19 - @Data  
20 - @NoArgsConstructor  
21 - @AllArgsConstructor  
22 - public static class AuthInfo {  
23 -  
24 - /**  
25 - * 客户端 ID  
26 - */  
27 - private String clientId;  
28 -  
29 - /**  
30 - * 用户名  
31 - */  
32 - private String username;  
33 -  
34 - /**  
35 - * 密码  
36 - */  
37 - private String password;  
38 -  
39 - }  
40 -  
41 - /**  
42 - * 设备信息  
43 - */  
44 - @Data  
45 - public static class DeviceInfo {  
46 -  
47 - private String productKey;  
48 -  
49 - private String deviceName;  
50 -  
51 - }  
52 -  
53 - public static AuthInfo getAuthInfo(String productKey, String deviceName, String deviceSecret) { 16 + public static IotDeviceAuthReqDTO getAuthInfo(String productKey, String deviceName, String deviceSecret) {
54 String clientId = buildClientId(productKey, deviceName); 17 String clientId = buildClientId(productKey, deviceName);
55 String username = buildUsername(productKey, deviceName); 18 String username = buildUsername(productKey, deviceName);
56 - String content = "clientId" + clientId +  
57 - "deviceName" + deviceName +  
58 - "deviceSecret" + deviceSecret +  
59 - "productKey" + productKey;  
60 - String password = buildPassword(deviceSecret, content);  
61 - return new AuthInfo(clientId, username, password); 19 + String password = buildPassword(deviceSecret,
  20 + buildContent(clientId, productKey, deviceName, deviceSecret));
  21 + return new IotDeviceAuthReqDTO(clientId, username, password);
62 } 22 }
63 23
64 - private static String buildClientId(String productKey, String deviceName) { 24 + public static String buildClientId(String productKey, String deviceName) {
65 return String.format("%s.%s", productKey, deviceName); 25 return String.format("%s.%s", productKey, deviceName);
66 } 26 }
67 27
68 - private static String buildUsername(String productKey, String deviceName) { 28 + public static String buildUsername(String productKey, String deviceName) {
69 return String.format("%s&%s", deviceName, productKey); 29 return String.format("%s&%s", deviceName, productKey);
70 } 30 }
71 31
72 - private static String buildPassword(String deviceSecret, String content) {  
73 - return DigestUtil.hmac(HmacAlgorithm.HmacSHA256, deviceSecret.getBytes()) 32 + public static String buildPassword(String deviceSecret, String content) {
  33 + return DigestUtil.hmac(HmacAlgorithm.HmacSHA256, StrUtil.utf8Bytes(deviceSecret))
74 .digestHex(content); 34 .digestHex(content);
75 } 35 }
76 36
77 - public static DeviceInfo parseUsername(String username) { 37 + private static String buildContent(String clientId, String productKey, String deviceName, String deviceSecret) {
  38 + return "clientId" + clientId +
  39 + "deviceName" + deviceName +
  40 + "deviceSecret" + deviceSecret +
  41 + "productKey" + productKey;
  42 + }
  43 +
  44 + public static IotDeviceIdentity parseUsername(String username) {
78 String[] usernameParts = username.split("&"); 45 String[] usernameParts = username.split("&");
79 if (usernameParts.length != 2) { 46 if (usernameParts.length != 2) {
80 return null; 47 return null;
81 } 48 }
82 - return new DeviceInfo().setProductKey(usernameParts[1]).setDeviceName(usernameParts[0]); 49 + return new IotDeviceIdentity(usernameParts[1], usernameParts[0]);
83 } 50 }
84 51
85 } 52 }
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxAuthEventHandler.java
@@ -7,6 +7,7 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult; @@ -7,6 +7,7 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
7 import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; 7 import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
8 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; 8 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
9 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 9 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  10 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
10 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; 11 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
11 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; 12 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
12 import io.vertx.core.json.JsonObject; 13 import io.vertx.core.json.JsonObject;
@@ -201,7 +202,7 @@ public class IotEmqxAuthEventHandler { @@ -201,7 +202,7 @@ public class IotEmqxAuthEventHandler {
201 */ 202 */
202 private void handleDeviceStateChange(String username, boolean online) { 203 private void handleDeviceStateChange(String username, boolean online) {
203 // 1. 解析设备信息 204 // 1. 解析设备信息
204 - IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(username); 205 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
205 if (deviceInfo == null) { 206 if (deviceInfo == null) {
206 log.debug("[handleDeviceStateChange][跳过非设备({})连接]", username); 207 log.debug("[handleDeviceStateChange][跳过非设备({})连接]", username);
207 return; 208 return;
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpUpstreamProtocol.java
@@ -3,6 +3,8 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.http; @@ -3,6 +3,8 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.http;
3 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; 3 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
4 import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; 4 import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
5 import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAuthHandler; 5 import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAuthHandler;
  6 +import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpRegisterHandler;
  7 +import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpRegisterSubHandler;
6 import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpUpstreamHandler; 8 import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpUpstreamHandler;
7 import io.vertx.core.AbstractVerticle; 9 import io.vertx.core.AbstractVerticle;
8 import io.vertx.core.Vertx; 10 import io.vertx.core.Vertx;
@@ -47,6 +49,10 @@ public class IotHttpUpstreamProtocol extends AbstractVerticle { @@ -47,6 +49,10 @@ public class IotHttpUpstreamProtocol extends AbstractVerticle {
47 // 创建处理器,添加路由处理器 49 // 创建处理器,添加路由处理器
48 IotHttpAuthHandler authHandler = new IotHttpAuthHandler(this); 50 IotHttpAuthHandler authHandler = new IotHttpAuthHandler(this);
49 router.post(IotHttpAuthHandler.PATH).handler(authHandler); 51 router.post(IotHttpAuthHandler.PATH).handler(authHandler);
  52 + IotHttpRegisterHandler registerHandler = new IotHttpRegisterHandler();
  53 + router.post(IotHttpRegisterHandler.PATH).handler(registerHandler);
  54 + IotHttpRegisterSubHandler registerSubHandler = new IotHttpRegisterSubHandler();
  55 + router.post(IotHttpRegisterSubHandler.PATH).handler(registerSubHandler);
50 IotHttpUpstreamHandler upstreamHandler = new IotHttpUpstreamHandler(this); 56 IotHttpUpstreamHandler upstreamHandler = new IotHttpUpstreamHandler(this);
51 router.post(IotHttpUpstreamHandler.PATH).handler(upstreamHandler); 57 router.post(IotHttpUpstreamHandler.PATH).handler(upstreamHandler);
52 58
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAbstractHandler.java
@@ -7,7 +7,8 @@ import cn.hutool.extra.spring.SpringUtil; @@ -7,7 +7,8 @@ import cn.hutool.extra.spring.SpringUtil;
7 import cn.iocoder.yudao.framework.common.exception.ServiceException; 7 import cn.iocoder.yudao.framework.common.exception.ServiceException;
8 import cn.iocoder.yudao.framework.common.pojo.CommonResult; 8 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
9 import cn.iocoder.yudao.framework.common.util.json.JsonUtils; 9 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
10 -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; 10 +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
  11 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
11 import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; 12 import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
12 import io.vertx.core.Handler; 13 import io.vertx.core.Handler;
13 import io.vertx.core.http.HttpHeaders; 14 import io.vertx.core.http.HttpHeaders;
@@ -54,7 +55,7 @@ public abstract class IotHttpAbstractHandler implements Handler&lt;RoutingContext&gt; @@ -54,7 +55,7 @@ public abstract class IotHttpAbstractHandler implements Handler&lt;RoutingContext&gt;
54 private void beforeHandle(RoutingContext context) { 55 private void beforeHandle(RoutingContext context) {
55 // 如果不需要认证,则不走前置处理 56 // 如果不需要认证,则不走前置处理
56 String path = context.request().path(); 57 String path = context.request().path();
57 - if (ObjUtil.equal(path, IotHttpAuthHandler.PATH)) { 58 + if (ObjectUtils.equalsAny(path, IotHttpAuthHandler.PATH, IotHttpRegisterHandler.PATH)) {
58 return; 59 return;
59 } 60 }
60 61
@@ -73,7 +74,7 @@ public abstract class IotHttpAbstractHandler implements Handler&lt;RoutingContext&gt; @@ -73,7 +74,7 @@ public abstract class IotHttpAbstractHandler implements Handler&lt;RoutingContext&gt;
73 } 74 }
74 75
75 // 校验 token 76 // 校验 token
76 - IotDeviceAuthUtils.DeviceInfo deviceInfo = deviceTokenService.verifyToken(token); 77 + IotDeviceIdentity deviceInfo = deviceTokenService.verifyToken(token);
77 Assert.notNull(deviceInfo, "设备信息不能为空"); 78 Assert.notNull(deviceInfo, "设备信息不能为空");
78 // 校验设备信息是否匹配 79 // 校验设备信息是否匹配
79 if (ObjUtil.notEqual(productKey, deviceInfo.getProductKey()) 80 if (ObjUtil.notEqual(productKey, deviceInfo.getProductKey())
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAuthHandler.java
@@ -9,7 +9,7 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult; @@ -9,7 +9,7 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
9 import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; 9 import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
10 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; 10 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
11 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 11 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
12 -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; 12 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
13 import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol; 13 import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol;
14 import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; 14 import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
15 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; 15 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
@@ -72,7 +72,7 @@ public class IotHttpAuthHandler extends IotHttpAbstractHandler { @@ -72,7 +72,7 @@ public class IotHttpAuthHandler extends IotHttpAbstractHandler {
72 throw exception(DEVICE_AUTH_FAIL); 72 throw exception(DEVICE_AUTH_FAIL);
73 } 73 }
74 // 2.2 生成 Token 74 // 2.2 生成 Token
75 - IotDeviceAuthUtils.DeviceInfo deviceInfo = deviceTokenService.parseUsername(username); 75 + IotDeviceIdentity deviceInfo = deviceTokenService.parseUsername(username);
76 Assert.notNull(deviceInfo, "设备信息不能为空"); 76 Assert.notNull(deviceInfo, "设备信息不能为空");
77 String token = deviceTokenService.createToken(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); 77 String token = deviceTokenService.createToken(deviceInfo.getProductKey(), deviceInfo.getDeviceName());
78 Assert.notBlank(token, "生成 token 不能为空位"); 78 Assert.notBlank(token, "生成 token 不能为空位");
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.http.router;
  2 +
  3 +import cn.hutool.core.util.StrUtil;
  4 +import cn.hutool.extra.spring.SpringUtil;
  5 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  6 +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
  7 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
  8 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
  9 +import io.vertx.core.json.JsonObject;
  10 +import io.vertx.ext.web.RoutingContext;
  11 +
  12 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
  13 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  14 +
  15 +/**
  16 + * IoT 网关 HTTP 协议的【设备动态注册】处理器
  17 + * <p>
  18 + * 用于直连设备/网关的一型一密动态注册,不需要认证
  19 + *
  20 + * @author 芋道源码
  21 + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
  22 + */
  23 +public class IotHttpRegisterHandler extends IotHttpAbstractHandler {
  24 +
  25 + public static final String PATH = "/auth/register/device";
  26 +
  27 + private final IotDeviceCommonApi deviceApi;
  28 +
  29 + public IotHttpRegisterHandler() {
  30 + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
  31 + }
  32 +
  33 + @Override
  34 + public CommonResult<Object> handle0(RoutingContext context) {
  35 + // 1. 解析参数
  36 + JsonObject body = context.body().asJsonObject();
  37 + String productKey = body.getString("productKey");
  38 + if (StrUtil.isEmpty(productKey)) {
  39 + throw invalidParamException("productKey 不能为空");
  40 + }
  41 + String deviceName = body.getString("deviceName");
  42 + if (StrUtil.isEmpty(deviceName)) {
  43 + throw invalidParamException("deviceName 不能为空");
  44 + }
  45 + String productSecret = body.getString("productSecret");
  46 + if (StrUtil.isEmpty(productSecret)) {
  47 + throw invalidParamException("productSecret 不能为空");
  48 + }
  49 +
  50 + // 2. 调用动态注册
  51 + IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO()
  52 + .setProductKey(productKey).setDeviceName(deviceName).setProductSecret(productSecret);
  53 + CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(reqDTO);
  54 + result.checkError();
  55 +
  56 + // 3. 返回结果
  57 + return success(result.getData());
  58 + }
  59 +
  60 +}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterSubHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.http.router;
  2 +
  3 +import cn.hutool.extra.spring.SpringUtil;
  4 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  5 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  6 +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
  7 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
  8 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO;
  9 +import io.vertx.core.json.JsonObject;
  10 +import io.vertx.ext.web.RoutingContext;
  11 +
  12 +import java.util.List;
  13 +
  14 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  15 +
  16 +/**
  17 + * IoT 网关 HTTP 协议的【子设备动态注册】处理器
  18 + * <p>
  19 + * 用于子设备的动态注册,需要网关认证
  20 + *
  21 + * @author 芋道源码
  22 + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/register-devices">阿里云 - 动态注册子设备</a>
  23 + */
  24 +public class IotHttpRegisterSubHandler extends IotHttpAbstractHandler {
  25 +
  26 + /**
  27 + * 路径:/auth/register/sub-device/:productKey/:deviceName
  28 + * <p>
  29 + * productKey 和 deviceName 是网关设备的标识
  30 + */
  31 + public static final String PATH = "/auth/register/sub-device/:productKey/:deviceName";
  32 +
  33 + private final IotDeviceCommonApi deviceApi;
  34 +
  35 + public IotHttpRegisterSubHandler() {
  36 + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
  37 + }
  38 +
  39 + @Override
  40 + public CommonResult<Object> handle0(RoutingContext context) {
  41 + // 1. 解析通用参数
  42 + String productKey = context.pathParam("productKey");
  43 + String deviceName = context.pathParam("deviceName");
  44 +
  45 + // 2. 解析子设备列表
  46 + JsonObject body = context.body().asJsonObject();
  47 + List<cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO> subDevices = JsonUtils.parseArray(
  48 + body.getJsonArray("params").toString(), cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO.class);
  49 +
  50 + // 3. 调用子设备动态注册
  51 + IotSubDeviceRegisterFullReqDTO reqDTO = new IotSubDeviceRegisterFullReqDTO()
  52 + .setGatewayProductKey(productKey).setGatewayDeviceName(deviceName).setSubDevices(subDevices);
  53 + CommonResult<List<IotSubDeviceRegisterRespDTO>> result = deviceApi.registerSubDevices(reqDTO);
  54 + result.checkError();
  55 +
  56 + // 4. 返回结果
  57 + return success(result.getData());
  58 + }
  59 +
  60 +}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttUpstreamHandler.java
@@ -9,6 +9,7 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; @@ -9,6 +9,7 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
9 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; 9 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO;
10 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; 10 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
11 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 11 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  12 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
12 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; 13 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
13 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol; 14 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol;
14 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; 15 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
@@ -214,7 +215,7 @@ public class IotMqttUpstreamHandler { @@ -214,7 +215,7 @@ public class IotMqttUpstreamHandler {
214 } 215 }
215 216
216 // 4. 获取设备信息 217 // 4. 获取设备信息
217 - IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(username); 218 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
218 if (deviceInfo == null) { 219 if (deviceInfo == null) {
219 log.warn("[authenticateDevice][用户名格式不正确,客户端 ID: {},用户名: {}]", clientId, username); 220 log.warn("[authenticateDevice][用户名格式不正确,客户端 ID: {},用户名: {}]", clientId, username);
220 return false; 221 return false;
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqttws/router/IotMqttWsUpstreamHandler.java
@@ -10,6 +10,7 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; @@ -10,6 +10,7 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
10 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; 10 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO;
11 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; 11 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
12 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 12 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  13 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
13 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; 14 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
14 import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.IotMqttWsUpstreamProtocol; 15 import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.IotMqttWsUpstreamProtocol;
15 import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.manager.IotMqttWsConnectionManager; 16 import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.manager.IotMqttWsConnectionManager;
@@ -521,7 +522,7 @@ public class IotMqttWsUpstreamHandler { @@ -521,7 +522,7 @@ public class IotMqttWsUpstreamHandler {
521 } 522 }
522 523
523 // 3. 获取设备信息 524 // 3. 获取设备信息
524 - IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(username); 525 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
525 if (deviceInfo == null) { 526 if (deviceInfo == null) {
526 log.warn("[authenticateDevice][用户名格式不正确,username: {}]", username); 527 log.warn("[authenticateDevice][用户名格式不正确,username: {}]", username);
527 return null; 528 return null;
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java
@@ -11,6 +11,7 @@ import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; @@ -11,6 +11,7 @@ import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
11 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; 11 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
12 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; 12 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
13 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 13 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  14 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
14 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; 15 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
15 import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec; 16 import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec;
16 import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec; 17 import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec;
@@ -162,7 +163,7 @@ public class IotTcpUpstreamHandler implements Handler&lt;NetSocket&gt; { @@ -162,7 +163,7 @@ public class IotTcpUpstreamHandler implements Handler&lt;NetSocket&gt; {
162 } 163 }
163 164
164 // 2.1 解析设备信息 165 // 2.1 解析设备信息
165 - IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername()); 166 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername());
166 if (deviceInfo == null) { 167 if (deviceInfo == null) {
167 sendErrorResponse(socket, message.getRequestId(), "解析设备信息失败", codecType); 168 sendErrorResponse(socket, message.getRequestId(), "解析设备信息失败", codecType);
168 return; 169 return;
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/auth/IotDeviceTokenService.java
1 package cn.iocoder.yudao.module.iot.gateway.service.auth; 1 package cn.iocoder.yudao.module.iot.gateway.service.auth;
2 2
3 -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; 3 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
4 4
5 /** 5 /**
6 * IoT 设备 Token Service 接口 6 * IoT 设备 Token Service 接口
@@ -24,7 +24,7 @@ public interface IotDeviceTokenService { @@ -24,7 +24,7 @@ public interface IotDeviceTokenService {
24 * @param token 设备 Token 24 * @param token 设备 Token
25 * @return 设备信息 25 * @return 设备信息
26 */ 26 */
27 - IotDeviceAuthUtils.DeviceInfo verifyToken(String token); 27 + IotDeviceIdentity verifyToken(String token);
28 28
29 /** 29 /**
30 * 解析用户名 30 * 解析用户名
@@ -32,6 +32,6 @@ public interface IotDeviceTokenService { @@ -32,6 +32,6 @@ public interface IotDeviceTokenService {
32 * @param username 用户名 32 * @param username 用户名
33 * @return 设备信息 33 * @return 设备信息
34 */ 34 */
35 - IotDeviceAuthUtils.DeviceInfo parseUsername(String username); 35 + IotDeviceIdentity parseUsername(String username);
36 36
37 } 37 }
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/auth/IotDeviceTokenServiceImpl.java
@@ -5,6 +5,7 @@ import cn.hutool.json.JSONObject; @@ -5,6 +5,7 @@ import cn.hutool.json.JSONObject;
5 import cn.hutool.jwt.JWT; 5 import cn.hutool.jwt.JWT;
6 import cn.hutool.jwt.JWTUtil; 6 import cn.hutool.jwt.JWTUtil;
7 import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; 7 import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
  8 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
8 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; 9 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
9 import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; 10 import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
10 import jakarta.annotation.Resource; 11 import jakarta.annotation.Resource;
@@ -48,7 +49,7 @@ public class IotDeviceTokenServiceImpl implements IotDeviceTokenService { @@ -48,7 +49,7 @@ public class IotDeviceTokenServiceImpl implements IotDeviceTokenService {
48 } 49 }
49 50
50 @Override 51 @Override
51 - public IotDeviceAuthUtils.DeviceInfo verifyToken(String token) { 52 + public IotDeviceIdentity verifyToken(String token) {
52 Assert.notBlank(token, "token 不能为空"); 53 Assert.notBlank(token, "token 不能为空");
53 // 校验 JWT Token 54 // 校验 JWT Token
54 boolean verify = JWTUtil.verify(token, gatewayProperties.getToken().getSecret().getBytes()); 55 boolean verify = JWTUtil.verify(token, gatewayProperties.getToken().getSecret().getBytes());
@@ -68,11 +69,11 @@ public class IotDeviceTokenServiceImpl implements IotDeviceTokenService { @@ -68,11 +69,11 @@ public class IotDeviceTokenServiceImpl implements IotDeviceTokenService {
68 String deviceName = payload.getStr("deviceName"); 69 String deviceName = payload.getStr("deviceName");
69 Assert.notBlank(productKey, "productKey 不能为空"); 70 Assert.notBlank(productKey, "productKey 不能为空");
70 Assert.notBlank(deviceName, "deviceName 不能为空"); 71 Assert.notBlank(deviceName, "deviceName 不能为空");
71 - return new IotDeviceAuthUtils.DeviceInfo().setProductKey(productKey).setDeviceName(deviceName); 72 + return new IotDeviceIdentity(productKey, deviceName);
72 } 73 }
73 74
74 @Override 75 @Override
75 - public IotDeviceAuthUtils.DeviceInfo parseUsername(String username) { 76 + public IotDeviceIdentity parseUsername(String username) {
76 return IotDeviceAuthUtils.parseUsername(username); 77 return IotDeviceAuthUtils.parseUsername(username);
77 } 78 }
78 79
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/remote/IotDeviceApiImpl.java
@@ -6,7 +6,13 @@ import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; @@ -6,7 +6,13 @@ import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
6 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; 6 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
7 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; 7 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO;
8 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; 8 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
  9 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
  10 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
  11 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
  12 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO;
9 import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; 13 import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
  14 +
  15 +import java.util.List;
10 import jakarta.annotation.PostConstruct; 16 import jakarta.annotation.PostConstruct;
11 import jakarta.annotation.Resource; 17 import jakarta.annotation.Resource;
12 import lombok.extern.slf4j.Slf4j; 18 import lombok.extern.slf4j.Slf4j;
@@ -54,6 +60,16 @@ public class IotDeviceApiImpl implements IotDeviceCommonApi { @@ -54,6 +60,16 @@ public class IotDeviceApiImpl implements IotDeviceCommonApi {
54 return doPost("/get", getReqDTO, new ParameterizedTypeReference<>() { }); 60 return doPost("/get", getReqDTO, new ParameterizedTypeReference<>() { });
55 } 61 }
56 62
  63 + @Override
  64 + public CommonResult<IotDeviceRegisterRespDTO> registerDevice(IotDeviceRegisterReqDTO reqDTO) {
  65 + return doPost("/register", reqDTO, new ParameterizedTypeReference<>() { });
  66 + }
  67 +
  68 + @Override
  69 + public CommonResult<List<IotSubDeviceRegisterRespDTO>> registerSubDevices(IotSubDeviceRegisterFullReqDTO reqDTO) {
  70 + return doPost("/register-sub", reqDTO, new ParameterizedTypeReference<>() { });
  71 + }
  72 +
57 private <T, R> CommonResult<R> doPost(String url, T body, 73 private <T, R> CommonResult<R> doPost(String url, T body,
58 ParameterizedTypeReference<CommonResult<R>> responseType) { 74 ParameterizedTypeReference<CommonResult<R>> responseType) {
59 try { 75 try {
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotDirectDeviceHttpProtocolIntegrationTest.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.auth.IotDeviceRegisterReqDTO;
  11 +import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
  12 +import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
  13 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  14 +import lombok.extern.slf4j.Slf4j;
  15 +import org.junit.jupiter.api.Test;
  16 +
  17 +
  18 +/**
  19 + * IoT 直连设备 HTTP 协议集成测试(手动测试)
  20 + *
  21 + * <p>测试场景:直连设备(IotProductDeviceTypeEnum 的 DIRECT 类型)通过 HTTP 协议直接连接平台
  22 + *
  23 + * <p>使用步骤:
  24 + * <ol>
  25 + * <li>启动 yudao-module-iot-gateway 服务(HTTP 端口 8092)</li>
  26 + * <li>运行 {@link #testDeviceRegister()} 测试直连设备动态注册(一型一密)</li>
  27 + * <li>运行 {@link #testAuth()} 获取设备 token,将返回的 token 粘贴到 {@link #TOKEN} 常量</li>
  28 + * <li>运行以下测试方法:
  29 + * <ul>
  30 + * <li>{@link #testPropertyPost()} - 设备属性上报</li>
  31 + * <li>{@link #testEventPost()} - 设备事件上报</li>
  32 + * </ul>
  33 + * </li>
  34 + * </ol>
  35 + *
  36 + * @author 芋道源码
  37 + */
  38 +@Slf4j
  39 +@SuppressWarnings("HttpUrlsUsage")
  40 +public class IotDirectDeviceHttpProtocolIntegrationTest {
  41 +
  42 + private static final String SERVER_HOST = "127.0.0.1";
  43 + private static final int SERVER_PORT = 8092;
  44 +
  45 + // ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
  46 + private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT";
  47 + private static final String DEVICE_NAME = "small";
  48 + private static final String DEVICE_SECRET = "0baa4c2ecc104ae1a26b4070c218bdf3";
  49 +
  50 + /**
  51 + * 直连设备 Token:从 {@link #testAuth()} 方法获取后,粘贴到这里
  52 + */
  53 + private static final String TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoiNGF5bVpnT1RPT0NyREtSVCIsImV4cCI6MTc2OTMwNTA1NSwiZGV2aWNlTmFtZSI6InNtYWxsIn0.mf3MEATCn5bp6cXgULunZjs8d00RGUxj96JEz0hMS7k";
  54 +
  55 + // ===================== 认证测试 =====================
  56 +
  57 + /**
  58 + * 认证测试:获取设备 Token
  59 + */
  60 + @Test
  61 + public void testAuth() {
  62 + // 1.1 构建请求
  63 + String url = String.format("http://%s:%d/auth", SERVER_HOST, SERVER_PORT);
  64 + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
  65 + IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
  66 + .setClientId(authInfo.getClientId())
  67 + .setUsername(authInfo.getUsername())
  68 + .setPassword(authInfo.getPassword());
  69 + String payload = JsonUtils.toJsonString(authReqDTO);
  70 + // 1.2 输出请求
  71 + log.info("[testAuth][请求 URL: {}]", url);
  72 + log.info("[testAuth][请求体: {}]", payload);
  73 +
  74 + // 2.1 发送请求
  75 + String response = HttpUtil.post(url, payload);
  76 + // 2.2 输出结果
  77 + log.info("[testAuth][响应体: {}]", response);
  78 + log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]");
  79 + }
  80 +
  81 + // ===================== 直连设备属性上报测试 =====================
  82 +
  83 + /**
  84 + * 属性上报测试
  85 + */
  86 + @Test
  87 + public void testPropertyPost() {
  88 + // 1.1 构建请求
  89 + String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/property/post",
  90 + SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME);
  91 + String payload = JsonUtils.toJsonString(MapUtil.builder()
  92 + .put("id", IdUtil.fastSimpleUUID())
  93 + .put("method", IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod())
  94 + .put("version", "1.0")
  95 + .put("params", IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
  96 + .put("width", 1)
  97 + .put("height", "2")
  98 + .build())
  99 + )
  100 + .build());
  101 + // 1.2 输出请求
  102 + log.info("[testPropertyPost][请求 URL: {}]", url);
  103 + log.info("[testPropertyPost][请求体: {}]", payload);
  104 +
  105 + // 2.1 发送请求
  106 + try (HttpResponse httpResponse = HttpUtil.createPost(url)
  107 + .header("Authorization", TOKEN)
  108 + .body(payload)
  109 + .execute()) {
  110 + // 2.2 输出结果
  111 + log.info("[testPropertyPost][响应体: {}]", httpResponse.body());
  112 + }
  113 + }
  114 +
  115 + // ===================== 直连设备事件上报测试 =====================
  116 +
  117 + /**
  118 + * 事件上报测试
  119 + */
  120 + @Test
  121 + public void testEventPost() {
  122 + // 1.1 构建请求
  123 + String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/event/post",
  124 + SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME);
  125 + String payload = JsonUtils.toJsonString(MapUtil.builder()
  126 + .put("id", IdUtil.fastSimpleUUID())
  127 + .put("method", IotDeviceMessageMethodEnum.EVENT_POST.getMethod())
  128 + .put("version", "1.0")
  129 + .put("params", IotDeviceEventPostReqDTO.of(
  130 + "eat",
  131 + MapUtil.<String, Object>builder().put("rice", 3).build(),
  132 + System.currentTimeMillis())
  133 + )
  134 + .build());
  135 + // 1.2 输出请求
  136 + log.info("[testEventPost][请求 URL: {}]", url);
  137 + log.info("[testEventPost][请求体: {}]", payload);
  138 +
  139 + // 2.1 发送请求
  140 + try (HttpResponse httpResponse = HttpUtil.createPost(url)
  141 + .header("Authorization", TOKEN)
  142 + .body(payload)
  143 + .execute()) {
  144 + // 2.2 输出结果
  145 + log.info("[testEventPost][响应体: {}]", httpResponse.body());
  146 + }
  147 + }
  148 +
  149 + // ===================== 动态注册测试 =====================
  150 +
  151 + /**
  152 + * 直连设备动态注册测试(一型一密)
  153 + * <p>
  154 + * 使用产品密钥(productSecret)验证身份,成功后返回设备密钥(deviceSecret)
  155 + * <p>
  156 + * 注意:此接口不需要 Token 认证
  157 + */
  158 + @Test
  159 + public void testDeviceRegister() {
  160 + // 1.1 构建请求
  161 + String url = String.format("http://%s:%d/auth/register/device", SERVER_HOST, SERVER_PORT);
  162 + // 1.2 构建请求参数
  163 + IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO();
  164 + reqDTO.setProductKey(PRODUCT_KEY);
  165 + reqDTO.setDeviceName("test-" + System.currentTimeMillis());
  166 + reqDTO.setProductSecret("test-product-secret");
  167 + String payload = JsonUtils.toJsonString(reqDTO);
  168 + // 1.3 输出请求
  169 + log.info("[testDeviceRegister][请求 URL: {}]", url);
  170 + log.info("[testDeviceRegister][请求体: {}]", payload);
  171 +
  172 + // 2.1 发送请求
  173 + String response = HttpUtil.post(url, payload);
  174 + // 2.2 输出结果
  175 + log.info("[testDeviceRegister][响应体: {}]", response);
  176 + log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]");
  177 + }
  178 +
  179 +}
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 + IotDeviceAuthReqDTO 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 + /**
  99 + * 添加子设备拓扑关系测试
  100 + * <p>
  101 + * 网关设备向平台上报需要绑定的子设备信息
  102 + */
  103 + @Test
  104 + public void testTopoAdd() {
  105 + // 1.1 构建请求
  106 + String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/topo/add",
  107 + SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
  108 + // 1.2 构建子设备认证信息
  109 + IotDeviceAuthReqDTO subAuthInfo = IotDeviceAuthUtils.getAuthInfo(
  110 + SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET);
  111 + IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO()
  112 + .setClientId(subAuthInfo.getClientId())
  113 + .setUsername(subAuthInfo.getUsername())
  114 + .setPassword(subAuthInfo.getPassword());
  115 + // 1.3 构建请求参数
  116 + IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO();
  117 + params.setSubDevices(Collections.singletonList(subDeviceAuth));
  118 + String payload = JsonUtils.toJsonString(MapUtil.builder()
  119 + .put("id", IdUtil.fastSimpleUUID())
  120 + .put("method", IotDeviceMessageMethodEnum.TOPO_ADD.getMethod())
  121 + .put("version", "1.0")
  122 + .put("params", params)
  123 + .build());
  124 + // 1.4 输出请求
  125 + log.info("[testTopoAdd][请求 URL: {}]", url);
  126 + log.info("[testTopoAdd][请求体: {}]", payload);
  127 +
  128 + // 2.1 发送请求
  129 + try (HttpResponse httpResponse = HttpUtil.createPost(url)
  130 + .header("Authorization", GATEWAY_TOKEN)
  131 + .body(payload)
  132 + .execute()) {
  133 + // 2.2 输出结果
  134 + log.info("[testTopoAdd][响应体: {}]", httpResponse.body());
  135 + }
  136 + }
  137 +
  138 + /**
  139 + * 删除子设备拓扑关系测试
  140 + * <p>
  141 + * 网关设备向平台上报需要解绑的子设备信息
  142 + */
  143 + @Test
  144 + public void testTopoDelete() {
  145 + // 1.1 构建请求
  146 + String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/topo/delete",
  147 + SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
  148 + // 1.2 构建请求参数
  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", params)
  157 + .build());
  158 + // 1.3 输出请求
  159 + log.info("[testTopoDelete][请求 URL: {}]", url);
  160 + log.info("[testTopoDelete][请求体: {}]", payload);
  161 +
  162 + // 2.1 发送请求
  163 + try (HttpResponse httpResponse = HttpUtil.createPost(url)
  164 + .header("Authorization", GATEWAY_TOKEN)
  165 + .body(payload)
  166 + .execute()) {
  167 + // 2.2 输出结果
  168 + log.info("[testTopoDelete][响应体: {}]", httpResponse.body());
  169 + }
  170 + }
  171 +
  172 + /**
  173 + * 获取子设备拓扑关系测试
  174 + * <p>
  175 + * 网关设备向平台查询已绑定的子设备列表
  176 + */
  177 + @Test
  178 + public void testTopoGet() {
  179 + // 1.1 构建请求
  180 + String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/topo/get",
  181 + SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
  182 + // 1.2 构建请求参数(目前为空,预留扩展)
  183 + IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO();
  184 + String payload = JsonUtils.toJsonString(MapUtil.builder()
  185 + .put("id", IdUtil.fastSimpleUUID())
  186 + .put("method", IotDeviceMessageMethodEnum.TOPO_GET.getMethod())
  187 + .put("version", "1.0")
  188 + .put("params", params)
  189 + .build());
  190 + // 1.3 输出请求
  191 + log.info("[testTopoGet][请求 URL: {}]", url);
  192 + log.info("[testTopoGet][请求体: {}]", payload);
  193 +
  194 + // 2.1 发送请求
  195 + try (HttpResponse httpResponse = HttpUtil.createPost(url)
  196 + .header("Authorization", GATEWAY_TOKEN)
  197 + .body(payload)
  198 + .execute()) {
  199 + // 2.2 输出结果
  200 + log.info("[testTopoGet][响应体: {}]", httpResponse.body());
  201 + }
  202 + }
  203 +
  204 + // ===================== 子设备注册测试 =====================
  205 +
  206 + // TODO @芋艿:待测试
  207 +
  208 + /**
  209 + * 子设备动态注册测试
  210 + * <p>
  211 + * 网关设备代理子设备进行动态注册,平台返回子设备的 deviceSecret
  212 + * <p>
  213 + * 注意:此接口需要网关 Token 认证
  214 + */
  215 + @Test
  216 + public void testSubDeviceRegister() {
  217 + // 1.1 构建请求
  218 + String url = String.format("http://%s:%d/auth/register/sub-device/%s/%s",
  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("mougezishebei");
  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 + IotDeviceAuthReqDTO 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 +}