diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java
index e35cd9b437..7711ae0d88 100644
--- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java
+++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java
@@ -229,4 +229,53 @@ public class JsonUtils {
return JSONUtil.isTypeJSONObject(str);
}
+ /**
+ * 将 Object 转换为目标类型
+ *
+ * 避免先转 jsonString 再 parseObject 的性能损耗
+ *
+ * @param obj 源对象(可以是 Map、POJO 等)
+ * @param clazz 目标类型
+ * @return 转换后的对象
+ */
+ public static T convertObject(Object obj, Class clazz) {
+ if (obj == null) {
+ return null;
+ }
+ if (clazz.isInstance(obj)) {
+ return clazz.cast(obj);
+ }
+ return objectMapper.convertValue(obj, clazz);
+ }
+
+ /**
+ * 将 Object 转换为目标类型(支持泛型)
+ *
+ * @param obj 源对象
+ * @param typeReference 目标类型引用
+ * @return 转换后的对象
+ */
+ public static T convertObject(Object obj, TypeReference typeReference) {
+ if (obj == null) {
+ return null;
+ }
+ return objectMapper.convertValue(obj, typeReference);
+ }
+
+ /**
+ * 将 Object 转换为 List 类型
+ *
+ * 避免先转 jsonString 再 parseArray 的性能损耗
+ *
+ * @param obj 源对象(可以是 List、数组等)
+ * @param clazz 目标元素类型
+ * @return 转换后的 List
+ */
+ public static List convertList(Object obj, Class clazz) {
+ if (obj == null) {
+ return new ArrayList<>();
+ }
+ return objectMapper.convertValue(obj, objectMapper.getTypeFactory().constructCollectionType(List.class, clazz));
+ }
+
}
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java
index eb55b1852a..db0a862d0e 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java
+++ b/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;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
+import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
+import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
+import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
+import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO;
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
@@ -19,6 +23,8 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
+import java.util.List;
+
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
@@ -57,4 +63,18 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi {
}));
}
+ @Override
+ @PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/device/register")
+ @PermitAll
+ public CommonResult registerDevice(@RequestBody IotDeviceRegisterReqDTO reqDTO) {
+ return success(deviceService.registerDevice(reqDTO));
+ }
+
+ @Override
+ @PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/device/register-sub")
+ @PermitAll
+ public CommonResult> registerSubDevices(@RequestBody IotSubDeviceRegisterFullReqDTO reqDTO) {
+ return success(deviceService.registerSubDevices(reqDTO));
+ }
+
}
\ No newline at end of file
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceController.java
index cdc25d803c..18553a7359 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceController.java
+++ b/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 {
@Operation(summary = "绑定子设备到网关")
@PreAuthorize("@ss.hasPermission('iot:device:update')")
public CommonResult bindDeviceGateway(@Valid @RequestBody IotDeviceBindGatewayReqVO reqVO) {
- deviceService.bindDeviceGateway(reqVO.getIds(), reqVO.getGatewayId());
+ deviceService.bindDeviceGateway(reqVO.getSubIds(), reqVO.getGatewayId());
return success(true);
}
@@ -72,7 +72,7 @@ public class IotDeviceController {
@Operation(summary = "解绑子设备与网关")
@PreAuthorize("@ss.hasPermission('iot:device:update')")
public CommonResult unbindDeviceGateway(@Valid @RequestBody IotDeviceUnbindGatewayReqVO reqVO) {
- deviceService.unbindDeviceGateway(reqVO.getIds());
+ deviceService.unbindDeviceGateway(reqVO.getSubIds(), reqVO.getGatewayId());
return success(true);
}
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceBindGatewayReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceBindGatewayReqVO.java
index be122d8730..dbfa523b9c 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceBindGatewayReqVO.java
+++ b/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 {
@Schema(description = "子设备编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3")
@NotEmpty(message = "子设备编号列表不能为空")
- private Set ids;
+ private Set subIds;
@Schema(description = "网关设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
@NotNull(message = "网关设备编号不能为空")
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceRespVO.java
index 648f1405da..0d4a9d8b5b 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceRespVO.java
+++ b/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;
import cn.idev.excel.annotation.ExcelProperty;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
-import cn.iocoder.yudao.module.iot.enums.DictTypeConstants;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -80,10 +79,6 @@ public class IotDeviceRespVO {
@ExcelProperty("设备密钥")
private String deviceSecret;
- @Schema(description = "认证类型(如一机一密、动态注册)", example = "2")
- @ExcelProperty("认证类型(如一机一密、动态注册)")
- private String authType;
-
@Schema(description = "设备配置", example = "{\"abc\": \"efg\"}")
private String config;
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceUnbindGatewayReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceUnbindGatewayReqVO.java
index 64215f3f6b..f51d6599ea 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceUnbindGatewayReqVO.java
+++ b/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;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.Set;
@@ -12,6 +13,10 @@ public class IotDeviceUnbindGatewayReqVO {
@Schema(description = "子设备编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3")
@NotEmpty(message = "子设备编号列表不能为空")
- private Set ids;
+ private Set subIds;
+
+ @Schema(description = "网关设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotNull(message = "网关设备编号不能为空")
+ private Long gatewayId;
}
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java
index ab581d25ba..ffc92a2132 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java
+++ b/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 {
@ExcelProperty("产品标识")
private String productKey;
+ @Schema(description = "产品密钥", requiredMode = Schema.RequiredMode.REQUIRED)
+ private String productSecret;
+
+ @Schema(description = "是否开启动态注册", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+ private Boolean registerEnabled;
+
@Schema(description = "产品分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long categoryId;
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductSaveReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductSaveReqVO.java
index 38f2d24ac8..08c636f7f2 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductSaveReqVO.java
+++ b/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 {
@NotEmpty(message = "数据格式不能为空")
private String codecType;
+ @Schema(description = "是否开启动态注册", example = "false")
+ @NotNull(message = "是否开启动态注册不能为空")
+ private Boolean registerEnabled;
+
}
\ No newline at end of file
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java
index efb232b963..7b7d021c3b 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java
+++ b/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 {
* 设备密钥,用于设备认证
*/
private String deviceSecret;
- /**
- * 认证类型(如一机一密、动态注册)
- */
- // TODO @haohao:是不是要枚举哈
- private String authType;
/**
* 设备位置的纬度
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductDO.java
index 376360e889..e296b35017 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductDO.java
+++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductDO.java
@@ -32,6 +32,14 @@ public class IotProductDO extends TenantBaseDO {
* 产品标识
*/
private String productKey;
+ /**
+ * 产品密钥,用于一型一密动态注册
+ */
+ private String productSecret;
+ /**
+ * 是否开启动态注册
+ */
+ private Boolean registerEnabled;
/**
* 产品分类编号
*
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceMapper.java
index c61acf960c..1e3fb2e576 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceMapper.java
+++ b/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
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import jakarta.annotation.Nullable;
import org.apache.ibatis.annotations.Mapper;
@@ -159,4 +160,16 @@ public interface IotDeviceMapper extends BaseMapperX {
.orderByDesc(IotDeviceDO::getId));
}
+ /**
+ * 批量更新设备的网关编号
+ *
+ * @param ids 设备编号列表
+ * @param gatewayId 网关设备编号(可以为 null,表示解绑)
+ */
+ default void updateGatewayIdBatch(Collection ids, Long gatewayId) {
+ update(null, new LambdaUpdateWrapper()
+ .set(IotDeviceDO::getGatewayId, gatewayId)
+ .in(IotDeviceDO::getId, ids));
+ }
+
}
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java
index 8c5345de1e..3679dbf1ce 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java
+++ b/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 {
ErrorCode DEVICE_SERIAL_NUMBER_EXISTS = new ErrorCode(1_050_003_008, "设备序列号已存在,序列号必须全局唯一");
ErrorCode DEVICE_NOT_GATEWAY_SUB = new ErrorCode(1_050_003_009, "设备【{}/{}】不是网关子设备类型,无法绑定到网关");
ErrorCode DEVICE_GATEWAY_BINDTO_EXISTS = new ErrorCode(1_050_003_010, "设备【{}/{}】已绑定到其他网关,请先解绑");
+ // 拓扑管理相关错误码 1-050-003-100
+ ErrorCode DEVICE_TOPO_PARAMS_INVALID = new ErrorCode(1_050_003_100, "拓扑管理参数无效");
+ ErrorCode DEVICE_TOPO_SUB_DEVICE_USERNAME_INVALID = new ErrorCode(1_050_003_101, "子设备用户名格式无效");
+ ErrorCode DEVICE_TOPO_SUB_DEVICE_AUTH_FAILED = new ErrorCode(1_050_003_102, "子设备认证失败");
+ ErrorCode DEVICE_TOPO_SUB_NOT_BINDTO_GATEWAY = new ErrorCode(1_050_003_103, "子设备【{}/{}】未绑定到该网关");
+ // 设备注册相关错误码 1-050-003-200
+ ErrorCode DEVICE_SUB_REGISTER_PARAMS_INVALID = new ErrorCode(1_050_003_200, "子设备注册参数无效");
+ ErrorCode DEVICE_SUB_REGISTER_PRODUCT_NOT_GATEWAY_SUB = new ErrorCode(1_050_003_201, "产品【{}】不是网关子设备类型");
+ ErrorCode DEVICE_REGISTER_DISABLED = new ErrorCode(1_050_003_210, "该产品未开启动态注册功能");
+ ErrorCode DEVICE_REGISTER_SECRET_INVALID = new ErrorCode(1_050_003_211, "产品密钥验证失败");
+ ErrorCode DEVICE_REGISTER_ALREADY_EXISTS = new ErrorCode(1_050_003_212, "设备已存在,不允许重复注册");
// ========== 产品分类 1-050-004-000 ==========
ErrorCode PRODUCT_CATEGORY_NOT_EXISTS = new ErrorCode(1_050_004_000, "产品分类不存在");
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java
index 5ddc973667..5a622e5654 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java
+++ b/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;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
+import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
+import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
+import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
+import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
+import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
+import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO;
+import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetRespDTO;
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
import jakarta.validation.Valid;
@@ -38,18 +45,6 @@ public interface IotDeviceService {
*/
void updateDevice(@Valid IotDeviceSaveReqVO updateReqVO);
- // TODO @芋艿:先这么实现。未来看情况,要不要自己实现
-
- /**
- * 更新设备的所属网关
- *
- * @param id 编号
- * @param gatewayId 网关设备 ID
- */
- default void updateDeviceGateway(Long id, Long gatewayId) {
- updateDevice(new IotDeviceSaveReqVO().setId(id).setGatewayId(gatewayId));
- }
-
/**
* 更新设备状态
*
@@ -288,22 +283,23 @@ public interface IotDeviceService {
*/
List getDeviceListByHasLocation();
- // ========== 网关-子设备绑定相关 ==========
+ // ========== 网关-拓扑管理(后台操作) ==========
/**
* 绑定子设备到网关
*
- * @param ids 子设备编号列表
+ * @param subIds 子设备编号列表
* @param gatewayId 网关设备编号
*/
- void bindDeviceGateway(Collection ids, Long gatewayId);
+ void bindDeviceGateway(Collection subIds, Long gatewayId);
/**
* 解绑子设备与网关
*
- * @param ids 子设备编号列表
+ * @param subIds 子设备编号列表
+ * @param gatewayId 网关设备编号
*/
- void unbindDeviceGateway(Collection ids);
+ void unbindDeviceGateway(Collection subIds, Long gatewayId);
/**
* 获取未绑定网关的子设备分页
@@ -321,4 +317,62 @@ public interface IotDeviceService {
*/
List getDeviceListByGatewayId(Long gatewayId);
+ // ========== 网关-拓扑管理(设备上报) ==========
+
+ /**
+ * 处理添加拓扑关系消息(网关设备上报)
+ *
+ * @param message 消息
+ * @param gatewayDevice 网关设备
+ * @return 成功添加的子设备列表
+ */
+ List handleTopoAddMessage(IotDeviceMessage message, IotDeviceDO gatewayDevice);
+
+ /**
+ * 处理删除拓扑关系消息(网关设备上报)
+ *
+ * @param message 消息
+ * @param gatewayDevice 网关设备
+ * @return 成功删除的子设备列表
+ */
+ List handleTopoDeleteMessage(IotDeviceMessage message, IotDeviceDO gatewayDevice);
+
+ /**
+ * 处理获取拓扑关系消息(网关设备上报)
+ *
+ * @param gatewayDevice 网关设备
+ * @return 拓扑关系响应
+ */
+ IotDeviceTopoGetRespDTO handleTopoGetMessage(IotDeviceDO gatewayDevice);
+
+ // ========== 设备动态注册 ==========
+
+ /**
+ * 直连/网关设备动态注册
+ *
+ * @param reqDTO 动态注册请求
+ * @return 注册结果(包含 DeviceSecret)
+ */
+ IotDeviceRegisterRespDTO registerDevice(@Valid IotDeviceRegisterReqDTO reqDTO);
+
+ /**
+ * 网关子设备动态注册
+ *
+ * 与 {@link #handleSubDeviceRegisterMessage} 方法的区别:
+ * 该方法网关设备信息通过 reqDTO 参数传入,而 {@link #handleSubDeviceRegisterMessage} 方法通过 gatewayDevice 参数传入
+ *
+ * @param reqDTO 子设备注册请求(包含网关设备信息)
+ * @return 注册结果列表
+ */
+ List registerSubDevices(@Valid IotSubDeviceRegisterFullReqDTO reqDTO);
+
+ /**
+ * 处理子设备动态注册消息(网关设备上报)
+ *
+ * @param message 消息
+ * @param gatewayDevice 网关设备
+ * @return 注册结果列表
+ */
+ List handleSubDeviceRegisterMessage(IotDeviceMessage message, IotDeviceDO gatewayDevice);
+
}
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java
index b7ca5070c2..4ec70e08fb 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java
+++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java
@@ -1,19 +1,33 @@
package cn.iocoder.yudao.module.iot.service.device;
import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
+import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
+import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
+import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
+import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
+import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
+import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
+import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO;
+import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO;
+import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO;
+import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoChangeReqDTO;
+import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO;
+import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetRespDTO;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceGroupDO;
@@ -21,6 +35,7 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceMapper;
import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants;
import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum;
+import cn.iocoder.yudao.module.iot.service.device.message.IotDeviceMessageService;
import cn.iocoder.yudao.module.iot.service.product.IotProductService;
import jakarta.annotation.Resource;
import jakarta.validation.ConstraintViolationException;
@@ -41,6 +56,7 @@ import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*;
+import static java.util.Collections.singletonList;
/**
* IoT 设备 Service 实现类
@@ -61,9 +77,20 @@ public class IotDeviceServiceImpl implements IotDeviceService {
@Resource
@Lazy // 延迟加载,解决循环依赖
private IotDeviceGroupService deviceGroupService;
+ @Resource
+ @Lazy // 延迟加载,解决循环依赖
+ private IotDeviceMessageService deviceMessageService;
+
+ private IotDeviceServiceImpl getSelf() {
+ return SpringUtil.getBean(getClass());
+ }
@Override
public Long createDevice(IotDeviceSaveReqVO createReqVO) {
+ return createDevice0(createReqVO).getId();
+ }
+
+ private IotDeviceDO createDevice0(IotDeviceSaveReqVO createReqVO) {
// 1.1 校验产品是否存在
IotProductDO product = productService.getProduct(createReqVO.getProductId());
if (product == null) {
@@ -81,7 +108,7 @@ public class IotDeviceServiceImpl implements IotDeviceService {
IotDeviceDO device = BeanUtils.toBean(createReqVO, IotDeviceDO.class);
initDevice(device, product);
deviceMapper.insert(device);
- return device.getId();
+ return device;
}
private void validateCreateDeviceParam(String productKey, String deviceName,
@@ -117,11 +144,13 @@ public class IotDeviceServiceImpl implements IotDeviceService {
private void initDevice(IotDeviceDO device, IotProductDO product) {
device.setProductId(product.getId()).setProductKey(product.getProductKey())
- .setDeviceType(product.getDeviceType());
- // 生成密钥
- device.setDeviceSecret(generateDeviceSecret());
- // 设置设备状态为未激活
- device.setState(IotDeviceStateEnum.INACTIVE.getState());
+ .setDeviceType(product.getDeviceType())
+ .setDeviceSecret(generateDeviceSecret()) // 生成密钥
+ .setState(IotDeviceStateEnum.INACTIVE.getState()); // 默认未激活
+ }
+
+ private String generateDeviceSecret() {
+ return IdUtil.fastSimpleUUID();
}
@Override
@@ -298,6 +327,37 @@ public class IotDeviceServiceImpl implements IotDeviceService {
// 2. 清空对应缓存
deleteDeviceCache(device);
+
+ // 3. 网关设备下线时,联动所有子设备下线
+ if (Objects.equals(state, IotDeviceStateEnum.OFFLINE.getState())
+ && IotProductDeviceTypeEnum.isGateway(device.getDeviceType())) {
+ handleGatewayOffline(device);
+ }
+ }
+
+ /**
+ * 处理网关下线,联动所有子设备下线
+ *
+ * @param gatewayDevice 网关设备
+ */
+ private void handleGatewayOffline(IotDeviceDO gatewayDevice) {
+ List subDevices = deviceMapper.selectListByGatewayId(gatewayDevice.getId());
+ if (CollUtil.isEmpty(subDevices)) {
+ return;
+ }
+ for (IotDeviceDO subDevice : subDevices) {
+ if (Objects.equals(subDevice.getState(), IotDeviceStateEnum.ONLINE.getState())) {
+ try {
+ updateDeviceState(subDevice, IotDeviceStateEnum.OFFLINE.getState());
+ log.info("[handleGatewayOffline][网关({}/{}) 下线,子设备({}/{}) 联动下线]",
+ gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(),
+ subDevice.getProductKey(), subDevice.getDeviceName());
+ } catch (Exception ex) {
+ log.error("[handleGatewayOffline][子设备({}/{}) 下线失败]",
+ subDevice.getProductKey(), subDevice.getDeviceName(), ex);
+ }
+ }
+ }
}
@Override
@@ -318,15 +378,6 @@ public class IotDeviceServiceImpl implements IotDeviceService {
return deviceMapper.selectCountByGroupId(groupId);
}
- /**
- * 生成 deviceSecret
- *
- * @return 生成的 deviceSecret
- */
- private String generateDeviceSecret() {
- return IdUtil.fastSimpleUUID();
- }
-
@Override
@Transactional(rollbackFor = Exception.class) // 添加事务,异常则回滚所有导入
public IotDeviceImportRespVO importDevice(List importDevices, boolean updateSupport) {
@@ -401,7 +452,7 @@ public class IotDeviceServiceImpl implements IotDeviceService {
public IotDeviceAuthInfoRespVO getDeviceAuthInfo(Long id) {
IotDeviceDO device = validateDeviceExists(id);
// 使用 IotDeviceAuthUtils 生成认证信息
- IotDeviceAuthUtils.AuthInfo authInfo = IotDeviceAuthUtils.getAuthInfo(
+ IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(
device.getProductKey(), device.getDeviceName(), device.getDeviceSecret());
return BeanUtils.toBean(authInfo, IotDeviceAuthInfoRespVO.class);
}
@@ -449,7 +500,7 @@ public class IotDeviceServiceImpl implements IotDeviceService {
@Override
public boolean authDevice(IotDeviceAuthReqDTO authReqDTO) {
// 1. 校验设备是否存在
- IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(authReqDTO.getUsername());
+ IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authReqDTO.getUsername());
if (deviceInfo == null) {
log.error("[authDevice][认证失败,username({}) 格式不正确]", authReqDTO.getUsername());
return false;
@@ -463,7 +514,7 @@ public class IotDeviceServiceImpl implements IotDeviceService {
}
// 2. 校验密码
- IotDeviceAuthUtils.AuthInfo authInfo = IotDeviceAuthUtils.getAuthInfo(productKey, deviceName, device.getDeviceSecret());
+ IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(productKey, deviceName, device.getDeviceSecret());
if (ObjUtil.notEqual(authInfo.getPassword(), authReqDTO.getPassword())) {
log.error("[authDevice][设备({}/{}) 密码不正确]", productKey, deviceName);
return false;
@@ -516,29 +567,20 @@ public class IotDeviceServiceImpl implements IotDeviceService {
return deviceMapper.selectListByHasLocation();
}
- // ========== 网关-子设备绑定相关 ==========
+ // ========== 网关-拓扑管理(后台操作) ==========
@Override
@Transactional(rollbackFor = Exception.class)
- public void bindDeviceGateway(Collection ids, Long gatewayId) {
- if (CollUtil.isEmpty(ids)) {
+ public void bindDeviceGateway(Collection subIds, Long gatewayId) {
+ if (CollUtil.isEmpty(subIds)) {
return;
}
// 1.1 校验网关设备存在且类型正确
validateGatewayDeviceExists(gatewayId);
- // 1.2 校验子设备存在
- List devices = deviceMapper.selectByIds(ids);
- if (devices.size() != ids.size()) {
- throw exception(DEVICE_NOT_EXISTS);
- }
- // 1.3 校验每个设备是否可绑定
+ // 1.2 校验每个设备是否可绑定
+ List devices = deviceMapper.selectByIds(subIds);
for (IotDeviceDO device : devices) {
- if (!IotProductDeviceTypeEnum.isGatewaySub(device.getDeviceType())) {
- throw exception(DEVICE_NOT_GATEWAY_SUB, device.getProductKey(), device.getDeviceName());
- }
- if (device.getGatewayId() != null && !device.getGatewayId().equals(gatewayId)) {
- throw exception(DEVICE_GATEWAY_BINDTO_EXISTS, device.getProductKey(), device.getDeviceName());
- }
+ checkSubDeviceCanBind(device, gatewayId);
}
// 2. 批量更新数据库
@@ -548,31 +590,42 @@ public class IotDeviceServiceImpl implements IotDeviceService {
// 3. 清空对应缓存
deleteDeviceCache(devices);
+
+ // 4. 下发网关设备拓扑变更通知(增加)
+ sendTopoChangeNotify(gatewayId, IotDeviceTopoChangeReqDTO.STATUS_CREATE, devices);
+ }
+
+ private void checkSubDeviceCanBind(IotDeviceDO device, Long gatewayId) {
+ if (!IotProductDeviceTypeEnum.isGatewaySub(device.getDeviceType())) {
+ throw exception(DEVICE_NOT_GATEWAY_SUB, device.getProductKey(), device.getDeviceName());
+ }
+ // 已绑定到其他网关,拒绝绑定(需先解绑)
+ if (device.getGatewayId() != null && ObjUtil.notEqual(device.getGatewayId(), gatewayId)) {
+ throw exception(DEVICE_GATEWAY_BINDTO_EXISTS, device.getProductKey(), device.getDeviceName());
+ }
}
@Override
@Transactional(rollbackFor = Exception.class)
- public void unbindDeviceGateway(Collection ids) {
- if (CollUtil.isEmpty(ids)) {
+ public void unbindDeviceGateway(Collection subIds, Long gatewayId) {
+ // 1. 校验设备存在
+ if (CollUtil.isEmpty(subIds)) {
return;
}
- // 1. 校验设备存在
- List devices = deviceMapper.selectByIds(ids);
- if (devices.size() != ids.size()) {
- throw exception(DEVICE_NOT_EXISTS);
+ List devices = deviceMapper.selectByIds(subIds);
+ devices.removeIf(device -> ObjUtil.notEqual(device.getGatewayId(), gatewayId));
+ if (CollUtil.isEmpty(devices)) {
+ return;
}
// 2. 批量更新数据库(将 gatewayId 设置为 null)
- List updateList = devices.stream()
- .filter(device -> device.getGatewayId() != null)
- .map(device -> new IotDeviceDO().setId(device.getId()).setGatewayId(null))
- .toList();
- if (CollUtil.isNotEmpty(updateList)) {
- deviceMapper.updateBatch(updateList);
- }
+ deviceMapper.updateGatewayIdBatch(convertList(devices, IotDeviceDO::getId), null);
// 3. 清空对应缓存
deleteDeviceCache(devices);
+
+ // 4. 下发网关设备拓扑变更通知(删除)
+ sendTopoChangeNotify(gatewayId, IotDeviceTopoChangeReqDTO.STATUS_DELETE, devices);
}
@Override
@@ -585,8 +638,293 @@ public class IotDeviceServiceImpl implements IotDeviceService {
return deviceMapper.selectListByGatewayId(gatewayId);
}
- private IotDeviceServiceImpl getSelf() {
- return SpringUtil.getBean(getClass());
+ // ========== 网关-拓扑管理(设备上报) ==========
+
+ @Override
+ public List handleTopoAddMessage(IotDeviceMessage message, IotDeviceDO gatewayDevice) {
+ // 1.1 校验网关设备类型
+ if (!IotProductDeviceTypeEnum.isGateway(gatewayDevice.getDeviceType())) {
+ throw exception(DEVICE_NOT_GATEWAY);
+ }
+ // 1.2 解析参数
+ IotDeviceTopoAddReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceTopoAddReqDTO.class);
+ if (params == null || CollUtil.isEmpty(params.getSubDevices())) {
+ throw exception(DEVICE_TOPO_PARAMS_INVALID);
+ }
+
+ // 2. 遍历处理每个子设备
+ List addedSubDevices = new ArrayList<>();
+ for (IotDeviceAuthReqDTO subDeviceAuth : params.getSubDevices()) {
+ try {
+ IotDeviceDO subDevice = addDeviceTopo(gatewayDevice, subDeviceAuth);
+ addedSubDevices.add(new IotDeviceIdentity(subDevice.getProductKey(), subDevice.getDeviceName()));
+ } catch (Exception ex) {
+ log.warn("[handleTopoAddMessage][网关({}/{}) 添加子设备失败,message={}]",
+ gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(), message, ex);
+ }
+ }
+
+ // 3. 返回响应数据(包含成功添加的子设备列表)
+ return addedSubDevices;
+ }
+
+ private IotDeviceDO addDeviceTopo(IotDeviceDO gatewayDevice, IotDeviceAuthReqDTO subDeviceAuth) {
+ // 1.1 解析子设备信息
+ IotDeviceIdentity subDeviceInfo = IotDeviceAuthUtils.parseUsername(subDeviceAuth.getUsername());
+ if (subDeviceInfo == null) {
+ throw exception(DEVICE_TOPO_SUB_DEVICE_USERNAME_INVALID);
+ }
+ // 1.2 校验子设备认证信息
+ if (!authDevice(subDeviceAuth)) {
+ throw exception(DEVICE_TOPO_SUB_DEVICE_AUTH_FAILED);
+ }
+ // 1.3 获取子设备
+ IotDeviceDO subDevice = getSelf().getDeviceFromCache(subDeviceInfo.getProductKey(), subDeviceInfo.getDeviceName());
+ if (subDevice == null) {
+ throw exception(DEVICE_NOT_EXISTS);
+ }
+ // 1.4 校验子设备类型
+ checkSubDeviceCanBind(subDevice, gatewayDevice.getId());
+
+ // 2. 更新数据库
+ deviceMapper.updateById(new IotDeviceDO().setId(subDevice.getId()).setGatewayId(gatewayDevice.getId()));
+ log.info("[addDeviceTopo][网关({}/{}) 绑定子设备({}/{})]",
+ gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(),
+ subDevice.getProductKey(), subDevice.getDeviceName());
+
+ // 3. 清空对应缓存
+ deleteDeviceCache(subDevice);
+ return subDevice;
+ }
+
+ @Override
+ public List handleTopoDeleteMessage(IotDeviceMessage message, IotDeviceDO gatewayDevice) {
+ // 1.1 校验网关设备类型
+ if (!IotProductDeviceTypeEnum.isGateway(gatewayDevice.getDeviceType())) {
+ throw exception(DEVICE_NOT_GATEWAY);
+ }
+ // 1.2 解析参数
+ IotDeviceTopoDeleteReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceTopoDeleteReqDTO.class);
+ if (params == null || CollUtil.isEmpty(params.getSubDevices())) {
+ throw exception(DEVICE_TOPO_PARAMS_INVALID);
+ }
+
+ // 2. 遍历处理每个子设备
+ List deletedSubDevices = new ArrayList<>();
+ for (IotDeviceIdentity subDeviceIdentity : params.getSubDevices()) {
+ try {
+ deleteDeviceTopo(gatewayDevice, subDeviceIdentity);
+ deletedSubDevices.add(subDeviceIdentity);
+ } catch (Exception ex) {
+ log.warn("[handleTopoDeleteMessage][网关({}/{}) 删除子设备失败,productKey={}, deviceName={}]",
+ gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(),
+ subDeviceIdentity.getProductKey(), subDeviceIdentity.getDeviceName(), ex);
+ }
+ }
+
+ // 3. 返回响应数据(包含成功删除的子设备列表)
+ return deletedSubDevices;
+ }
+
+ private void deleteDeviceTopo(IotDeviceDO gatewayDevice, IotDeviceIdentity subDeviceIdentity) {
+ // 1.1 获取子设备
+ IotDeviceDO subDevice = getSelf().getDeviceFromCache(subDeviceIdentity.getProductKey(), subDeviceIdentity.getDeviceName());
+ if (subDevice == null) {
+ throw exception(DEVICE_NOT_EXISTS);
+ }
+ // 1.2 校验子设备是否绑定到该网关
+ if (ObjUtil.notEqual(subDevice.getGatewayId(), gatewayDevice.getId())) {
+ throw exception(DEVICE_TOPO_SUB_NOT_BINDTO_GATEWAY,
+ subDeviceIdentity.getProductKey(), subDeviceIdentity.getDeviceName());
+ }
+
+ // 2. 更新数据库(将 gatewayId 设置为 null)
+ deviceMapper.updateGatewayIdBatch(singletonList(subDevice.getId()), null);
+ log.info("[deleteDeviceTopo][网关({}/{}) 解绑子设备({}/{})]",
+ gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(),
+ subDevice.getProductKey(), subDevice.getDeviceName());
+
+ // 3. 清空对应缓存
+ deleteDeviceCache(subDevice);
+
+ // 4. 子设备下线
+ if (Objects.equals(subDevice.getState(), IotDeviceStateEnum.ONLINE.getState())) {
+ updateDeviceState(subDevice, IotDeviceStateEnum.OFFLINE.getState());
+ }
+ }
+
+ @Override
+ public IotDeviceTopoGetRespDTO handleTopoGetMessage(IotDeviceDO gatewayDevice) {
+ // 1. 校验网关设备类型
+ if (!IotProductDeviceTypeEnum.isGateway(gatewayDevice.getDeviceType())) {
+ throw exception(DEVICE_NOT_GATEWAY);
+ }
+
+ // 2. 获取子设备列表并转换
+ List subDevices = deviceMapper.selectListByGatewayId(gatewayDevice.getId());
+ List subDeviceIdentities = convertList(subDevices, subDevice ->
+ new IotDeviceIdentity(subDevice.getProductKey(), subDevice.getDeviceName()));
+ return new IotDeviceTopoGetRespDTO().setSubDevices(subDeviceIdentities);
+ }
+
+ /**
+ * 发送拓扑变更通知给网关设备
+ *
+ * @param gatewayId 网关设备编号
+ * @param status 变更状态(0-创建, 1-删除)
+ * @param subDevices 子设备列表
+ * @see 阿里云 - 通知网关拓扑关系变化
+ */
+ private void sendTopoChangeNotify(Long gatewayId, Integer status, List subDevices) {
+ if (CollUtil.isEmpty(subDevices)) {
+ return;
+ }
+ // 1. 获取网关设备
+ IotDeviceDO gatewayDevice = deviceMapper.selectById(gatewayId);
+ if (gatewayDevice == null) {
+ log.warn("[sendTopoChangeNotify][网关设备({}) 不存在,无法发送拓扑变更通知]", gatewayId);
+ return;
+ }
+
+ try {
+ // 2.1 构建拓扑变更通知消息
+ List subList = convertList(subDevices, subDevice ->
+ new IotDeviceIdentity(subDevice.getProductKey(), subDevice.getDeviceName()));
+ IotDeviceTopoChangeReqDTO params = new IotDeviceTopoChangeReqDTO(status, subList);
+ IotDeviceMessage notifyMessage = IotDeviceMessage.requestOf(
+ IotDeviceMessageMethodEnum.TOPO_CHANGE.getMethod(), params);
+
+ // 2.2 发送消息
+ deviceMessageService.sendDeviceMessage(notifyMessage, gatewayDevice);
+ log.info("[sendTopoChangeNotify][网关({}/{}) 发送拓扑变更通知成功,status={}, subDevices={}]",
+ gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(),
+ status, subList);
+ } catch (Exception ex) {
+ log.error("[sendTopoChangeNotify][网关({}/{}) 发送拓扑变更通知失败,status={}]",
+ gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(), status, ex);
+ }
+ }
+
+ // ========== 设备动态注册 ==========
+
+ @Override
+ public IotDeviceRegisterRespDTO registerDevice(IotDeviceRegisterReqDTO reqDTO) {
+ // 1.1 校验产品
+ IotProductDO product = TenantUtils.executeIgnore(() ->
+ productService.getProductByProductKey(reqDTO.getProductKey()));
+ if (product == null) {
+ throw exception(PRODUCT_NOT_EXISTS);
+ }
+ // 1.2 校验产品是否开启动态注册
+ if (BooleanUtil.isFalse(product.getRegisterEnabled())) {
+ throw exception(DEVICE_REGISTER_DISABLED);
+ }
+ // 1.3 验证 productSecret
+ if (ObjUtil.notEqual(product.getProductSecret(), reqDTO.getProductSecret())) {
+ throw exception(DEVICE_REGISTER_SECRET_INVALID);
+ }
+ return TenantUtils.execute(product.getTenantId(), () -> {
+ // 1.4 校验设备是否已存在(已存在则不允许重复注册)
+ IotDeviceDO device = getSelf().getDeviceFromCache(reqDTO.getProductKey(), reqDTO.getDeviceName());
+ if (device != null) {
+ throw exception(DEVICE_REGISTER_ALREADY_EXISTS);
+ }
+
+ // 2.1 自动创建设备
+ IotDeviceSaveReqVO createReqVO = new IotDeviceSaveReqVO()
+ .setDeviceName(reqDTO.getDeviceName())
+ .setProductId(product.getId());
+ device = createDevice0(createReqVO);
+ log.info("[registerDevice][产品({}) 自动创建设备({})]",
+ reqDTO.getProductKey(), reqDTO.getDeviceName());
+ // 2.2 返回设备密钥
+ return new IotDeviceRegisterRespDTO(device.getProductKey(), device.getDeviceName(), device.getDeviceSecret());
+ });
+ }
+
+ @Override
+ public List registerSubDevices(IotSubDeviceRegisterFullReqDTO reqDTO) {
+ // 1. 校验网关设备
+ IotDeviceDO gatewayDevice = getSelf().getDeviceFromCache(reqDTO.getGatewayProductKey(), reqDTO.getGatewayDeviceName());
+
+ // 2. 遍历注册每个子设备
+ return TenantUtils.execute(gatewayDevice.getTenantId(), () ->
+ registerSubDevices0(gatewayDevice, reqDTO.getSubDevices()));
+ }
+
+ @Override
+ public List handleSubDeviceRegisterMessage(IotDeviceMessage message, IotDeviceDO gatewayDevice) {
+ // 1. 解析参数
+ if (!(message.getParams() instanceof List)) {
+ throw exception(DEVICE_SUB_REGISTER_PARAMS_INVALID);
+ }
+ List subDevices = JsonUtils.convertList(message.getParams(), IotSubDeviceRegisterReqDTO.class);
+
+ // 2. 遍历注册每个子设备
+ return registerSubDevices0(gatewayDevice, subDevices);
+ }
+
+ private List registerSubDevices0(IotDeviceDO gatewayDevice,
+ List subDevices) {
+ // 1.1 校验网关设备
+ if (gatewayDevice == null) {
+ throw exception(DEVICE_NOT_EXISTS);
+ }
+ if (!IotProductDeviceTypeEnum.isGateway(gatewayDevice.getDeviceType())) {
+ throw exception(DEVICE_NOT_GATEWAY);
+ }
+ // 1.2 注册设备不能为空
+ if (CollUtil.isEmpty(subDevices)) {
+ throw exception(DEVICE_SUB_REGISTER_PARAMS_INVALID);
+ }
+
+ // 2. 遍历注册每个子设备
+ List results = new ArrayList<>(subDevices.size());
+ for (IotSubDeviceRegisterReqDTO subDevice : subDevices) {
+ try {
+ IotDeviceDO device = registerSubDevice0(gatewayDevice, subDevice);
+ results.add(new IotSubDeviceRegisterRespDTO(
+ subDevice.getProductKey(), subDevice.getDeviceName(), device.getDeviceSecret()));
+ } catch (Exception ex) {
+ log.error("[registerSubDevices0][子设备({}/{}) 注册失败]",
+ subDevice.getProductKey(), subDevice.getDeviceName(), ex);
+ }
+ }
+ return results;
+ }
+
+ private IotDeviceDO registerSubDevice0(IotDeviceDO gatewayDevice, IotSubDeviceRegisterReqDTO params) {
+ // 1.1 校验产品
+ IotProductDO product = productService.getProductByProductKey(params.getProductKey());
+ if (product == null) {
+ throw exception(PRODUCT_NOT_EXISTS);
+ }
+ // 1.2 校验产品是否为网关子设备类型
+ if (!IotProductDeviceTypeEnum.isGatewaySub(product.getDeviceType())) {
+ throw exception(DEVICE_SUB_REGISTER_PRODUCT_NOT_GATEWAY_SUB, params.getProductKey());
+ }
+ // 1.3 校验设备是否已存在(子设备动态注册:设备必须已预注册)
+ IotDeviceDO existDevice = getSelf().getDeviceFromCache(params.getProductKey(), params.getDeviceName());
+ if (existDevice == null) {
+ throw exception(DEVICE_NOT_EXISTS);
+ }
+ // 1.4 校验是否绑定到其他网关
+ if (existDevice.getGatewayId() != null && ObjUtil.notEqual(existDevice.getGatewayId(), gatewayDevice.getId())) {
+ throw exception(DEVICE_GATEWAY_BINDTO_EXISTS,
+ existDevice.getProductKey(), existDevice.getDeviceName());
+ }
+
+ // 2. 绑定到网关(如果尚未绑定)
+ if (existDevice.getGatewayId() == null) {
+ // 2.1 更新数据库
+ deviceMapper.updateById(new IotDeviceDO().setId(existDevice.getId()).setGatewayId(gatewayDevice.getId()));
+ // 2.2 清空对应缓存
+ deleteDeviceCache(existDevice);
+ log.info("[registerSubDevice][网关({}/{}) 绑定子设备({}/{})]",
+ gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(),
+ existDevice.getProductKey(), existDevice.getDeviceName());
+ }
+ return existDevice;
}
}
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageService.java
index 4a300dfc30..e28f489997 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageService.java
+++ b/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
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceMessageDO;
-import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import javax.annotation.Nullable;
@@ -75,7 +74,7 @@ public interface IotDeviceMessageService {
*/
List getDeviceMessageListByRequestIdsAndReply(
@NotNull(message = "设备编号不能为空") Long deviceId,
- @NotEmpty(message = "请求编号不能为空") List requestIds,
+ List requestIds,
Boolean reply);
/**
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageServiceImpl.java
index 01d1c45eee..24a5bb91b7 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageServiceImpl.java
+++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageServiceImpl.java
@@ -1,5 +1,7 @@
package cn.iocoder.yudao.module.iot.service.device.message;
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
@@ -16,6 +18,10 @@ import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsD
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer;
+import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
+import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
+import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPackPostReqDTO;
+import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceMessageDO;
@@ -98,7 +104,6 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
return sendDeviceMessage(message, device);
}
- // TODO @芋艿:针对连接网关的设备,是不是 productKey、deviceName 需要调整下;
@Override
public IotDeviceMessage sendDeviceMessage(IotDeviceMessage message, IotDeviceDO device) {
return sendDeviceMessage(message, device, null);
@@ -168,7 +173,7 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
// 2. 记录消息
getSelf().createDeviceLogAsync(message);
- // 3. 回复消息。前提:非 _reply 消息,并且非禁用回复的消息
+ // 3. 回复消息。前提:非 _reply 消息、非禁用回复的消息
if (IotDeviceMessageUtils.isReplyMessage(message)
|| IotDeviceMessageMethodEnum.isReplyDisabled(message.getMethod())
|| StrUtil.isEmpty(message.getServerId())) {
@@ -185,15 +190,14 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
}
// TODO @芋艿:可优化:未来逻辑复杂后,可以独立拆除 Processor 处理器
- @SuppressWarnings("SameReturnValue")
private Object handleUpstreamDeviceMessage0(IotDeviceMessage message, IotDeviceDO device) {
// 设备上下线
if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod())) {
String stateStr = IotDeviceMessageUtils.getIdentifier(message);
assert stateStr != null;
Assert.notEmpty(stateStr, "设备状态不能为空");
- deviceService.updateDeviceState(device, Integer.valueOf(stateStr));
- // TODO 芋艿:子设备的关联
+ Integer state = Integer.valueOf(stateStr);
+ deviceService.updateDeviceState(device, state);
return null;
}
@@ -202,6 +206,11 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
devicePropertyService.saveDeviceProperty(device, message);
return null;
}
+ // 批量上报(属性+事件+子设备)
+ if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod())) {
+ handlePackMessage(message, device);
+ return null;
+ }
// OTA 上报升级进度
if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.OTA_PROGRESS.getMethod())) {
@@ -209,10 +218,109 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
return null;
}
- // TODO @芋艿:这里可以按需,添加别的逻辑;
+ // 添加拓扑关系
+ if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.TOPO_ADD.getMethod())) {
+ return deviceService.handleTopoAddMessage(message, device);
+ }
+ // 删除拓扑关系
+ if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod())) {
+ return deviceService.handleTopoDeleteMessage(message, device);
+ }
+ // 获取拓扑关系
+ if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.TOPO_GET.getMethod())) {
+ return deviceService.handleTopoGetMessage(device);
+ }
+
+ // 子设备动态注册
+ if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod())) {
+ return deviceService.handleSubDeviceRegisterMessage(message, device);
+ }
+
return null;
}
+ // ========== 批量上报处理方法 ==========
+
+ /**
+ * 处理批量上报消息
+ *
+ * 将 pack 消息拆分成多条标准消息,发送到 MQ 让规则引擎处理
+ *
+ * @param packMessage 批量消息
+ * @param gatewayDevice 网关设备
+ */
+ private void handlePackMessage(IotDeviceMessage packMessage, IotDeviceDO gatewayDevice) {
+ // 1. 解析参数
+ IotDevicePropertyPackPostReqDTO params = JsonUtils.convertObject(
+ packMessage.getParams(), IotDevicePropertyPackPostReqDTO.class);
+ if (params == null) {
+ log.warn("[handlePackMessage][消息({}) 参数解析失败]", packMessage);
+ return;
+ }
+
+ // 2. 处理网关设备(自身)的数据
+ sendDevicePackData(gatewayDevice, packMessage.getServerId(), params.getProperties(), params.getEvents());
+
+ // 3. 处理子设备的数据
+ if (CollUtil.isEmpty(params.getSubDevices())) {
+ return;
+ }
+ for (IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData : params.getSubDevices()) {
+ try {
+ IotDeviceIdentity identity = subDeviceData.getIdentity();
+ IotDeviceDO subDevice = deviceService.getDeviceFromCache(identity.getProductKey(), identity.getDeviceName());
+ if (subDevice == null) {
+ log.warn("[handlePackMessage][子设备({}/{}) 不存在]", identity.getProductKey(), identity.getDeviceName());
+ continue;
+ }
+ // 特殊:子设备不需要指定 serverId,因为子设备实际可能连接在不同的 gateway-server 上,导致 serverId 不同
+ sendDevicePackData(subDevice, null, subDeviceData.getProperties(), subDeviceData.getEvents());
+ } catch (Exception ex) {
+ log.error("[handlePackMessage][子设备({}/{}) 数据处理失败]", subDeviceData.getIdentity().getProductKey(),
+ subDeviceData.getIdentity().getDeviceName(), ex);
+ }
+ }
+ }
+
+ /**
+ * 发送设备 pack 数据到 MQ(属性 + 事件)
+ *
+ * @param device 设备
+ * @param serverId 服务标识
+ * @param properties 属性数据
+ * @param events 事件数据
+ */
+ private void sendDevicePackData(IotDeviceDO device, String serverId,
+ Map properties,
+ Map events) {
+ // 1. 发送属性消息
+ if (MapUtil.isNotEmpty(properties)) {
+ IotDeviceMessage propertyMsg = IotDeviceMessage.requestOf(
+ device.getId(), device.getTenantId(), serverId,
+ IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(),
+ IotDevicePropertyPostReqDTO.of(properties));
+ deviceMessageProducer.sendDeviceMessage(propertyMsg);
+ }
+
+ // 2. 发送事件消息
+ if (MapUtil.isNotEmpty(events)) {
+ for (Map.Entry eventEntry : events.entrySet()) {
+ String eventId = eventEntry.getKey();
+ IotDevicePropertyPackPostReqDTO.EventValue eventValue = eventEntry.getValue();
+ if (eventValue == null) {
+ continue;
+ }
+ IotDeviceMessage eventMsg = IotDeviceMessage.requestOf(
+ device.getId(), device.getTenantId(), serverId,
+ IotDeviceMessageMethodEnum.EVENT_POST.getMethod(),
+ IotDeviceEventPostReqDTO.of(eventId, eventValue.getValue(), eventValue.getTime()));
+ deviceMessageProducer.sendDeviceMessage(eventMsg);
+ }
+ }
+ }
+
+ // ========= 设备消息查询 ==========
+
@Override
public PageResult getDeviceMessagePage(IotDeviceMessagePageReqVO pageReqVO) {
try {
@@ -228,9 +336,10 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
}
@Override
- public List getDeviceMessageListByRequestIdsAndReply(Long deviceId,
- List requestIds,
- Boolean reply) {
+ public List getDeviceMessageListByRequestIdsAndReply(Long deviceId, List requestIds, Boolean reply) {
+ if (CollUtil.isEmpty(requestIds)) {
+ return ListUtil.of();
+ }
return deviceMessageMapper.selectListByRequestIdsAndReply(deviceId, requestIds, reply);
}
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java
index a07d027909..e001f46a2b 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java
+++ b/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;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.hutool.core.util.IdUtil;
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
import cn.iocoder.yudao.module.iot.controller.admin.product.vo.product.IotProductPageReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.product.vo.product.IotProductSaveReqVO;
@@ -53,19 +54,22 @@ public class IotProductServiceImpl implements IotProductService {
// 2. 插入
IotProductDO product = BeanUtils.toBean(createReqVO, IotProductDO.class)
- .setStatus(IotProductStatusEnum.UNPUBLISHED.getStatus());
+ .setStatus(IotProductStatusEnum.UNPUBLISHED.getStatus())
+ .setProductSecret(generateProductSecret());
productMapper.insert(product);
return product.getId();
}
+ private String generateProductSecret() {
+ return IdUtil.fastSimpleUUID();
+ }
+
@Override
@CacheEvict(value = RedisKeyConstants.PRODUCT, key = "#updateReqVO.id")
public void updateProduct(IotProductSaveReqVO updateReqVO) {
updateReqVO.setProductKey(null); // 不更新产品标识
- // 1.1 校验存在
- IotProductDO iotProductDO = validateProductExists(updateReqVO.getId());
- // 1.2 发布状态不可更新
- validateProductStatus(iotProductDO);
+ // 1. 校验存在
+ validateProductExists(updateReqVO.getId());
// 2. 更新
IotProductDO updateObj = BeanUtils.toBean(updateReqVO, IotProductDO.class);
diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/IotDeviceCommonApi.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/IotDeviceCommonApi.java
index 29d540e73e..cc0cb071a1 100644
--- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/IotDeviceCommonApi.java
+++ b/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;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
+import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
+import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
+import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
+import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO;
+
+import java.util.List;
/**
* IoT 设备通用 API
@@ -28,4 +34,20 @@ public interface IotDeviceCommonApi {
*/
CommonResult getDevice(IotDeviceGetReqDTO infoReqDTO);
+ /**
+ * 直连/网关设备动态注册(一型一密)
+ *
+ * @param reqDTO 动态注册请求
+ * @return 注册结果(包含 DeviceSecret)
+ */
+ CommonResult registerDevice(IotDeviceRegisterReqDTO reqDTO);
+
+ /**
+ * 网关子设备动态注册(网关代理转发)
+ *
+ * @param reqDTO 子设备注册请求(包含网关标识和子设备列表)
+ * @return 注册结果列表
+ */
+ CommonResult> registerSubDevices(IotSubDeviceRegisterFullReqDTO reqDTO);
+
}
diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotDeviceAuthReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotDeviceAuthReqDTO.java
index 9e62a2fc0c..2f25fb4964 100644
--- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotDeviceAuthReqDTO.java
+++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotDeviceAuthReqDTO.java
@@ -1,7 +1,9 @@
package cn.iocoder.yudao.module.iot.core.biz.dto;
import jakarta.validation.constraints.NotEmpty;
+import lombok.AllArgsConstructor;
import lombok.Data;
+import lombok.NoArgsConstructor;
/**
* IoT 设备认证 Request DTO
@@ -9,6 +11,8 @@ import lombok.Data;
* @author 芋道源码
*/
@Data
+@NoArgsConstructor
+@AllArgsConstructor
public class IotDeviceAuthReqDTO {
/**
diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotSubDeviceRegisterFullReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotSubDeviceRegisterFullReqDTO.java
new file mode 100644
index 0000000000..76bf5ffb3f
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotSubDeviceRegisterFullReqDTO.java
@@ -0,0 +1,38 @@
+package cn.iocoder.yudao.module.iot.core.biz.dto;
+
+import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * IoT 子设备动态注册 Request DTO
+ *
+ * 额外包含了网关设备的标识信息
+ *
+ * @author 芋道源码
+ */
+@Data
+public class IotSubDeviceRegisterFullReqDTO {
+
+ /**
+ * 网关设备 ProductKey
+ */
+ @NotEmpty(message = "网关产品标识不能为空")
+ private String gatewayProductKey;
+
+ /**
+ * 网关设备 DeviceName
+ */
+ @NotEmpty(message = "网关设备名称不能为空")
+ private String gatewayDeviceName;
+
+ /**
+ * 子设备注册列表
+ */
+ @NotNull(message = "子设备注册列表不能为空")
+ private List subDevices;
+
+}
diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceMessageMethodEnum.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceMessageMethodEnum.java
index e62b78e245..d980032842 100644
--- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceMessageMethodEnum.java
+++ b/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 {
// TODO 芋艿:要不要加个 ping 消息;
+ // ========== 拓扑管理 ==========
+ // 可参考:https://help.aliyun.com/zh/iot/user-guide/manage-topological-relationships
+
+ TOPO_ADD("thing.topo.add", "添加拓扑关系", true),
+ TOPO_DELETE("thing.topo.delete", "删除拓扑关系", true),
+ TOPO_GET("thing.topo.get", "获取拓扑关系", true),
+ TOPO_CHANGE("thing.topo.change", "拓扑关系变更通知", false),
+
+ // ========== 设备注册 ==========
+ // 可参考:https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification
+
+ DEVICE_REGISTER("thing.auth.register", "设备动态注册", true),
+ SUB_DEVICE_REGISTER("thing.auth.register.sub", "子设备动态注册", true),
+
// ========== 设备属性 ==========
// 可参考:https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services
PROPERTY_POST("thing.property.post", "属性上报", true),
PROPERTY_SET("thing.property.set", "属性设置", false),
+ PROPERTY_PACK_POST("thing.event.property.pack.post", "批量上报(属性 + 事件 + 子设备)", true), // 网关独有
+
// ========== 设备事件 ==========
// 可参考:https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services
@@ -50,6 +66,7 @@ public enum IotDeviceMessageMethodEnum implements ArrayValuable {
OTA_UPGRADE("thing.ota.upgrade", "OTA 固定信息推送", false),
OTA_PROGRESS("thing.ota.progress", "OTA 升级进度上报", true),
+
;
public static final String[] ARRAYS = Arrays.stream(values()).map(IotDeviceMessageMethodEnum::getMethod)
diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/mq/message/IotDeviceMessage.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/mq/message/IotDeviceMessage.java
index 6821c0d160..feed3eb2a2 100644
--- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/mq/message/IotDeviceMessage.java
+++ b/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 {
return of(requestId, method, params, null, null, null);
}
+ /**
+ * 创建设备请求消息(包含设备信息)
+ *
+ * @param deviceId 设备编号
+ * @param tenantId 租户编号
+ * @param serverId 服务标识
+ * @param method 消息方法
+ * @param params 消息参数
+ * @return 消息对象
+ */
+ public static IotDeviceMessage requestOf(Long deviceId, Long tenantId, String serverId,
+ String method, Object params) {
+ IotDeviceMessage message = of(null, method, params, null, null, null);
+ return message.setId(IotDeviceMessageUtils.generateMessageId())
+ .setDeviceId(deviceId).setTenantId(tenantId).setServerId(serverId);
+ }
+
public static IotDeviceMessage replyOf(String requestId, String method,
Object data, Integer code, String msg) {
if (code == null) {
diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/IotDeviceIdentity.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/IotDeviceIdentity.java
new file mode 100644
index 0000000000..1987026718
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/IotDeviceIdentity.java
@@ -0,0 +1,32 @@
+package cn.iocoder.yudao.module.iot.core.topic;
+
+import jakarta.validation.constraints.NotEmpty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * IoT 设备标识
+ *
+ * 用于标识一个设备的基本信息(productKey + deviceName)
+ *
+ * @author 芋道源码
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class IotDeviceIdentity {
+
+ /**
+ * 产品标识
+ */
+ @NotEmpty(message = "产品标识不能为空")
+ private String productKey;
+
+ /**
+ * 设备名称
+ */
+ @NotEmpty(message = "设备名称不能为空")
+ private String deviceName;
+
+}
diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterReqDTO.java
new file mode 100644
index 0000000000..b8db15f188
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterReqDTO.java
@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.module.iot.core.topic.auth;
+
+import jakarta.validation.constraints.NotEmpty;
+import lombok.Data;
+
+/**
+ * IoT 设备动态注册 Request DTO
+ *
+ * 用于直连设备/网关的一型一密动态注册:使用 productSecret 验证,返回 deviceSecret
+ *
+ * @author 芋道源码
+ * @see 阿里云 - 一型一密
+ */
+@Data
+public class IotDeviceRegisterReqDTO {
+
+ /**
+ * 产品标识
+ */
+ @NotEmpty(message = "产品标识不能为空")
+ private String productKey;
+
+ /**
+ * 设备名称
+ */
+ @NotEmpty(message = "设备名称不能为空")
+ private String deviceName;
+
+ /**
+ * 产品密钥
+ */
+ @NotEmpty(message = "产品密钥不能为空")
+ private String productSecret;
+
+}
diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterRespDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterRespDTO.java
new file mode 100644
index 0000000000..707f79890b
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterRespDTO.java
@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.module.iot.core.topic.auth;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * IoT 设备动态注册 Response DTO
+ *
+ * 用于直连设备/网关的一型一密动态注册响应
+ *
+ * @author 芋道源码
+ * @see 阿里云 - 一型一密
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class IotDeviceRegisterRespDTO {
+
+ /**
+ * 产品标识
+ */
+ private String productKey;
+
+ /**
+ * 设备名称
+ */
+ private String deviceName;
+
+ /**
+ * 设备密钥
+ */
+ private String deviceSecret;
+
+}
diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterReqDTO.java
new file mode 100644
index 0000000000..cf34a1db2b
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterReqDTO.java
@@ -0,0 +1,31 @@
+package cn.iocoder.yudao.module.iot.core.topic.auth;
+
+import jakarta.validation.constraints.NotEmpty;
+import lombok.Data;
+
+/**
+ * IoT 子设备动态注册 Request DTO
+ *
+ * 用于 thing.auth.register.sub 消息的 params 数组元素
+ *
+ * 特殊:网关子设备的动态注册,必须已经创建好该网关子设备(不然哪来的 {@link #deviceName} 字段)。更多的好处,是设备不用提前烧录 deviceSecret 密钥。
+ *
+ * @author 芋道源码
+ * @see 阿里云 - 动态注册子设备
+ */
+@Data
+public class IotSubDeviceRegisterReqDTO {
+
+ /**
+ * 子设备 ProductKey
+ */
+ @NotEmpty(message = "产品标识不能为空")
+ private String productKey;
+
+ /**
+ * 子设备 DeviceName
+ */
+ @NotEmpty(message = "设备名称不能为空")
+ private String deviceName;
+
+}
diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterRespDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterRespDTO.java
new file mode 100644
index 0000000000..a45f14defe
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterRespDTO.java
@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.module.iot.core.topic.auth;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * IoT 子设备动态注册 Response DTO
+ *
+ * 用于 thing.auth.register.sub 响应的设备信息
+ *
+ * @author 芋道源码
+ * @see 阿里云 - 动态注册子设备
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class IotSubDeviceRegisterRespDTO {
+
+ /**
+ * 子设备 ProductKey
+ */
+ private String productKey;
+
+ /**
+ * 子设备 DeviceName
+ */
+ private String deviceName;
+
+ /**
+ * 分配的 DeviceSecret
+ */
+ private String deviceSecret;
+
+}
diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/event/IotDeviceEventPostReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/event/IotDeviceEventPostReqDTO.java
new file mode 100644
index 0000000000..3b6a7a7d4c
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/event/IotDeviceEventPostReqDTO.java
@@ -0,0 +1,54 @@
+package cn.iocoder.yudao.module.iot.core.topic.event;
+
+import lombok.Data;
+
+/**
+ * IoT 设备事件上报 Request DTO
+ *
+ * 用于 thing.event.post 消息的 params 参数
+ *
+ * @author 芋道源码
+ * @see 阿里云 - 设备上报事件
+ */
+@Data
+public class IotDeviceEventPostReqDTO {
+
+ /**
+ * 事件标识符
+ */
+ private String identifier;
+
+ /**
+ * 事件输出参数
+ */
+ private Object value;
+
+ /**
+ * 上报时间(毫秒时间戳,可选)
+ */
+ private Long time;
+
+ /**
+ * 创建事件上报 DTO
+ *
+ * @param identifier 事件标识符
+ * @param value 事件值
+ * @return DTO 对象
+ */
+ public static IotDeviceEventPostReqDTO of(String identifier, Object value) {
+ return of(identifier, value, null);
+ }
+
+ /**
+ * 创建事件上报 DTO(带时间)
+ *
+ * @param identifier 事件标识符
+ * @param value 事件值
+ * @param time 上报时间
+ * @return DTO 对象
+ */
+ public static IotDeviceEventPostReqDTO of(String identifier, Object value, Long time) {
+ return new IotDeviceEventPostReqDTO().setIdentifier(identifier).setValue(value).setTime(time);
+ }
+
+}
diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/package-info.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/package-info.java
new file mode 100644
index 0000000000..bc97dd944a
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/package-info.java
@@ -0,0 +1,8 @@
+/**
+ * IoT Topic 消息体 DTO 定义
+ *
+ * 定义设备与平台通信的消息体结构,遵循(参考)阿里云 Alink 协议规范
+ *
+ * @see 阿里云 Alink 协议
+ */
+package cn.iocoder.yudao.module.iot.core.topic;
diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertyPackPostReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertyPackPostReqDTO.java
new file mode 100644
index 0000000000..24494984eb
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertyPackPostReqDTO.java
@@ -0,0 +1,88 @@
+package cn.iocoder.yudao.module.iot.core.topic.property;
+
+import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
+import lombok.Data;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * IoT 设备属性批量上报 Request DTO
+ *
+ * 用于 thing.event.property.pack.post 消息的 params 参数
+ *
+ * @author 芋道源码
+ * @see 阿里云 - 网关批量上报数据
+ */
+@Data
+public class IotDevicePropertyPackPostReqDTO {
+
+ /**
+ * 网关自身属性
+ *
+ * key: 属性标识符
+ * value: 属性值
+ */
+ private Map properties;
+
+ /**
+ * 网关自身事件
+ *
+ * key: 事件标识符
+ * value: 事件值对象(包含 value 和 time)
+ */
+ private Map events;
+
+ /**
+ * 子设备数据列表
+ */
+ private List subDevices;
+
+ /**
+ * 事件值对象
+ */
+ @Data
+ public static class EventValue {
+
+ /**
+ * 事件参数
+ */
+ private Object value;
+
+ /**
+ * 上报时间(毫秒时间戳)
+ */
+ private Long time;
+
+ }
+
+ /**
+ * 子设备数据
+ */
+ @Data
+ public static class SubDeviceData {
+
+ /**
+ * 子设备标识
+ */
+ private IotDeviceIdentity identity;
+
+ /**
+ * 子设备属性
+ *
+ * key: 属性标识符
+ * value: 属性值
+ */
+ private Map properties;
+
+ /**
+ * 子设备事件
+ *
+ * key: 事件标识符
+ * value: 事件值对象(包含 value 和 time)
+ */
+ private Map events;
+
+ }
+
+}
diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertyPostReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertyPostReqDTO.java
new file mode 100644
index 0000000000..2e537442d7
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertyPostReqDTO.java
@@ -0,0 +1,36 @@
+package cn.iocoder.yudao.module.iot.core.topic.property;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * IoT 设备属性上报 Request DTO
+ *
+ * 用于 thing.property.post 消息的 params 参数
+ *
+ * 本质是一个 Map,key 为属性标识符,value 为属性值
+ *
+ * @author 芋道源码
+ * @see 阿里云 - 设备上报属性
+ */
+public class IotDevicePropertyPostReqDTO extends HashMap {
+
+ public IotDevicePropertyPostReqDTO() {
+ super();
+ }
+
+ public IotDevicePropertyPostReqDTO(Map properties) {
+ super(properties);
+ }
+
+ /**
+ * 创建属性上报 DTO
+ *
+ * @param properties 属性数据
+ * @return DTO 对象
+ */
+ public static IotDevicePropertyPostReqDTO of(Map properties) {
+ return new IotDevicePropertyPostReqDTO(properties);
+ }
+
+}
diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoAddReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoAddReqDTO.java
new file mode 100644
index 0000000000..97ec33200a
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoAddReqDTO.java
@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.module.iot.core.topic.topo;
+
+import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
+import jakarta.validation.constraints.NotEmpty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * IoT 设备拓扑添加 Request DTO
+ *
+ * 用于 thing.topo.add 消息的 params 参数
+ *
+ * @author 芋道源码
+ * @see 阿里云 - 添加拓扑关系
+ */
+@Data
+public class IotDeviceTopoAddReqDTO {
+
+ /**
+ * 子设备认证信息列表
+ *
+ * 复用 {@link IotDeviceAuthReqDTO},包含 clientId、username、password
+ */
+ @NotEmpty(message = "子设备认证信息列表不能为空")
+ private List subDevices;
+
+}
diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoChangeReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoChangeReqDTO.java
new file mode 100644
index 0000000000..0198206fe3
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoChangeReqDTO.java
@@ -0,0 +1,44 @@
+package cn.iocoder.yudao.module.iot.core.topic.topo;
+
+import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+/**
+ * IoT 设备拓扑关系变更通知 Request DTO
+ *
+ * 用于 thing.topo.change 下行消息的 params 参数
+ *
+ * @author 芋道源码
+ * @see 阿里云 - 通知网关拓扑关系变化
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class IotDeviceTopoChangeReqDTO {
+
+ public static final Integer STATUS_CREATE = 0;
+ public static final Integer STATUS_DELETE = 1;
+
+ /**
+ * 拓扑关系状态
+ */
+ private Integer status;
+
+ /**
+ * 子设备列表
+ */
+ private List subList;
+
+ public static IotDeviceTopoChangeReqDTO ofCreate(List subList) {
+ return new IotDeviceTopoChangeReqDTO(STATUS_CREATE, subList);
+ }
+
+ public static IotDeviceTopoChangeReqDTO ofDelete(List subList) {
+ return new IotDeviceTopoChangeReqDTO(STATUS_DELETE, subList);
+ }
+
+}
diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoDeleteReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoDeleteReqDTO.java
new file mode 100644
index 0000000000..71ee2bb8b2
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoDeleteReqDTO.java
@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.module.iot.core.topic.topo;
+
+import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotEmpty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * IoT 设备拓扑删除 Request DTO
+ *
+ * 用于 thing.topo.delete 消息的 params 参数
+ *
+ * @author 芋道源码
+ * @see 阿里云 - 删除拓扑关系
+ */
+@Data
+public class IotDeviceTopoDeleteReqDTO {
+
+ /**
+ * 子设备标识列表
+ */
+ @Valid
+ @NotEmpty(message = "子设备标识列表不能为空")
+ private List subDevices;
+
+}
diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoGetReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoGetReqDTO.java
new file mode 100644
index 0000000000..7a61af0a58
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoGetReqDTO.java
@@ -0,0 +1,16 @@
+package cn.iocoder.yudao.module.iot.core.topic.topo;
+
+import lombok.Data;
+
+/**
+ * IoT 设备拓扑关系获取 Request DTO
+ *
+ * 用于 thing.topo.get 请求的 params 参数(目前为空,预留扩展)
+ *
+ * @author 芋道源码
+ * @see 阿里云 - 获取拓扑关系
+ */
+@Data
+public class IotDeviceTopoGetReqDTO {
+
+}
diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoGetRespDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoGetRespDTO.java
new file mode 100644
index 0000000000..69c9b1555e
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoGetRespDTO.java
@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.module.iot.core.topic.topo;
+
+import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * IoT 设备拓扑关系获取 Response DTO
+ *
+ * 用于 thing.topo.get 响应
+ *
+ * @author 芋道源码
+ * @see 阿里云 - 获取拓扑关系
+ */
+@Data
+public class IotDeviceTopoGetRespDTO {
+
+ /**
+ * 子设备列表
+ */
+ private List subDevices;
+
+}
diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceAuthUtils.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceAuthUtils.java
index 2bc4880070..609d0a60ae 100644
--- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceAuthUtils.java
+++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceAuthUtils.java
@@ -1,10 +1,10 @@
package cn.iocoder.yudao.module.iot.core.util;
+import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.crypto.digest.HmacAlgorithm;
-import lombok.AllArgsConstructor;
-import lombok.Data;
-import lombok.NoArgsConstructor;
+import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
+import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
/**
* IoT 设备【认证】的工具类,参考阿里云
@@ -13,73 +13,40 @@ import lombok.NoArgsConstructor;
*/
public class IotDeviceAuthUtils {
- /**
- * 认证信息
- */
- @Data
- @NoArgsConstructor
- @AllArgsConstructor
- public static class AuthInfo {
-
- /**
- * 客户端 ID
- */
- private String clientId;
-
- /**
- * 用户名
- */
- private String username;
-
- /**
- * 密码
- */
- private String password;
-
- }
-
- /**
- * 设备信息
- */
- @Data
- public static class DeviceInfo {
-
- private String productKey;
-
- private String deviceName;
-
- }
-
- public static AuthInfo getAuthInfo(String productKey, String deviceName, String deviceSecret) {
+ public static IotDeviceAuthReqDTO getAuthInfo(String productKey, String deviceName, String deviceSecret) {
String clientId = buildClientId(productKey, deviceName);
String username = buildUsername(productKey, deviceName);
- String content = "clientId" + clientId +
- "deviceName" + deviceName +
- "deviceSecret" + deviceSecret +
- "productKey" + productKey;
- String password = buildPassword(deviceSecret, content);
- return new AuthInfo(clientId, username, password);
+ String password = buildPassword(deviceSecret,
+ buildContent(clientId, productKey, deviceName, deviceSecret));
+ return new IotDeviceAuthReqDTO(clientId, username, password);
}
- private static String buildClientId(String productKey, String deviceName) {
+ public static String buildClientId(String productKey, String deviceName) {
return String.format("%s.%s", productKey, deviceName);
}
- private static String buildUsername(String productKey, String deviceName) {
+ public static String buildUsername(String productKey, String deviceName) {
return String.format("%s&%s", deviceName, productKey);
}
- private static String buildPassword(String deviceSecret, String content) {
- return DigestUtil.hmac(HmacAlgorithm.HmacSHA256, deviceSecret.getBytes())
+ public static String buildPassword(String deviceSecret, String content) {
+ return DigestUtil.hmac(HmacAlgorithm.HmacSHA256, StrUtil.utf8Bytes(deviceSecret))
.digestHex(content);
}
- public static DeviceInfo parseUsername(String username) {
+ private static String buildContent(String clientId, String productKey, String deviceName, String deviceSecret) {
+ return "clientId" + clientId +
+ "deviceName" + deviceName +
+ "deviceSecret" + deviceSecret +
+ "productKey" + productKey;
+ }
+
+ public static IotDeviceIdentity parseUsername(String username) {
String[] usernameParts = username.split("&");
if (usernameParts.length != 2) {
return null;
}
- return new DeviceInfo().setProductKey(usernameParts[1]).setDeviceName(usernameParts[0]);
+ return new IotDeviceIdentity(usernameParts[1], usernameParts[0]);
}
}
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxAuthEventHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxAuthEventHandler.java
index d6957bd52f..3395d5c8ae 100644
--- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxAuthEventHandler.java
+++ b/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;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
+import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import io.vertx.core.json.JsonObject;
@@ -201,7 +202,7 @@ public class IotEmqxAuthEventHandler {
*/
private void handleDeviceStateChange(String username, boolean online) {
// 1. 解析设备信息
- IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(username);
+ IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
if (deviceInfo == null) {
log.debug("[handleDeviceStateChange][跳过非设备({})连接]", username);
return;
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpUpstreamProtocol.java
index eda59d13ff..a9ba930f1d 100644
--- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpUpstreamProtocol.java
+++ b/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;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAuthHandler;
+import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpRegisterHandler;
+import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpRegisterSubHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpUpstreamHandler;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.Vertx;
@@ -47,6 +49,10 @@ public class IotHttpUpstreamProtocol extends AbstractVerticle {
// 创建处理器,添加路由处理器
IotHttpAuthHandler authHandler = new IotHttpAuthHandler(this);
router.post(IotHttpAuthHandler.PATH).handler(authHandler);
+ IotHttpRegisterHandler registerHandler = new IotHttpRegisterHandler();
+ router.post(IotHttpRegisterHandler.PATH).handler(registerHandler);
+ IotHttpRegisterSubHandler registerSubHandler = new IotHttpRegisterSubHandler();
+ router.post(IotHttpRegisterSubHandler.PATH).handler(registerSubHandler);
IotHttpUpstreamHandler upstreamHandler = new IotHttpUpstreamHandler(this);
router.post(IotHttpUpstreamHandler.PATH).handler(upstreamHandler);
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAbstractHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAbstractHandler.java
index f5461c2c51..850fde1878 100644
--- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAbstractHandler.java
+++ b/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;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
-import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
+import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
+import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
import io.vertx.core.Handler;
import io.vertx.core.http.HttpHeaders;
@@ -54,7 +55,7 @@ public abstract class IotHttpAbstractHandler implements Handler
private void beforeHandle(RoutingContext context) {
// 如果不需要认证,则不走前置处理
String path = context.request().path();
- if (ObjUtil.equal(path, IotHttpAuthHandler.PATH)) {
+ if (ObjectUtils.equalsAny(path, IotHttpAuthHandler.PATH, IotHttpRegisterHandler.PATH)) {
return;
}
@@ -73,7 +74,7 @@ public abstract class IotHttpAbstractHandler implements Handler
}
// 校验 token
- IotDeviceAuthUtils.DeviceInfo deviceInfo = deviceTokenService.verifyToken(token);
+ IotDeviceIdentity deviceInfo = deviceTokenService.verifyToken(token);
Assert.notNull(deviceInfo, "设备信息不能为空");
// 校验设备信息是否匹配
if (ObjUtil.notEqual(productKey, deviceInfo.getProductKey())
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAuthHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAuthHandler.java
index e6a52cdf0f..c6a9331ab6 100644
--- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAuthHandler.java
+++ b/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;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
-import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
+import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
@@ -72,7 +72,7 @@ public class IotHttpAuthHandler extends IotHttpAbstractHandler {
throw exception(DEVICE_AUTH_FAIL);
}
// 2.2 生成 Token
- IotDeviceAuthUtils.DeviceInfo deviceInfo = deviceTokenService.parseUsername(username);
+ IotDeviceIdentity deviceInfo = deviceTokenService.parseUsername(username);
Assert.notNull(deviceInfo, "设备信息不能为空");
String token = deviceTokenService.createToken(deviceInfo.getProductKey(), deviceInfo.getDeviceName());
Assert.notBlank(token, "生成 token 不能为空位");
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterHandler.java
new file mode 100644
index 0000000000..525bd8487e
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterHandler.java
@@ -0,0 +1,60 @@
+package cn.iocoder.yudao.module.iot.gateway.protocol.http.router;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.extra.spring.SpringUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
+import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
+import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.web.RoutingContext;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+/**
+ * IoT 网关 HTTP 协议的【设备动态注册】处理器
+ *
+ * 用于直连设备/网关的一型一密动态注册,不需要认证
+ *
+ * @author 芋道源码
+ * @see 阿里云 - 一型一密
+ */
+public class IotHttpRegisterHandler extends IotHttpAbstractHandler {
+
+ public static final String PATH = "/auth/register/device";
+
+ private final IotDeviceCommonApi deviceApi;
+
+ public IotHttpRegisterHandler() {
+ this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
+ }
+
+ @Override
+ public CommonResult