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 handle0(RoutingContext context) { + // 1. 解析参数 + JsonObject body = context.body().asJsonObject(); + String productKey = body.getString("productKey"); + if (StrUtil.isEmpty(productKey)) { + throw invalidParamException("productKey 不能为空"); + } + String deviceName = body.getString("deviceName"); + if (StrUtil.isEmpty(deviceName)) { + throw invalidParamException("deviceName 不能为空"); + } + String productSecret = body.getString("productSecret"); + if (StrUtil.isEmpty(productSecret)) { + throw invalidParamException("productSecret 不能为空"); + } + + // 2. 调用动态注册 + IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO() + .setProductKey(productKey).setDeviceName(deviceName).setProductSecret(productSecret); + CommonResult result = deviceApi.registerDevice(reqDTO); + result.checkError(); + + // 3. 返回结果 + return success(result.getData()); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterSubHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterSubHandler.java new file mode 100644 index 0000000000..04aad65128 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterSubHandler.java @@ -0,0 +1,60 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.http.router; + +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO; +import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.RoutingContext; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +/** + * IoT 网关 HTTP 协议的【子设备动态注册】处理器 + *

+ * 用于子设备的动态注册,需要网关认证 + * + * @author 芋道源码 + * @see 阿里云 - 动态注册子设备 + */ +public class IotHttpRegisterSubHandler extends IotHttpAbstractHandler { + + /** + * 路径:/auth/register/sub-device/:productKey/:deviceName + *

+ * productKey 和 deviceName 是网关设备的标识 + */ + public static final String PATH = "/auth/register/sub-device/:productKey/:deviceName"; + + private final IotDeviceCommonApi deviceApi; + + public IotHttpRegisterSubHandler() { + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); + } + + @Override + public CommonResult handle0(RoutingContext context) { + // 1. 解析通用参数 + String productKey = context.pathParam("productKey"); + String deviceName = context.pathParam("deviceName"); + + // 2. 解析子设备列表 + JsonObject body = context.body().asJsonObject(); + List subDevices = JsonUtils.parseArray( + body.getJsonArray("params").toString(), cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO.class); + + // 3. 调用子设备动态注册 + IotSubDeviceRegisterFullReqDTO reqDTO = new IotSubDeviceRegisterFullReqDTO() + .setGatewayProductKey(productKey).setGatewayDeviceName(deviceName).setSubDevices(subDevices); + CommonResult> result = deviceApi.registerSubDevices(reqDTO); + result.checkError(); + + // 4. 返回结果 + return success(result.getData()); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttUpstreamHandler.java index 4c0eb6e612..7c3d1a627a 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttUpstreamHandler.java +++ b/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; 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.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.protocol.mqtt.IotMqttUpstreamProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; @@ -214,7 +215,7 @@ public class IotMqttUpstreamHandler { } // 4. 获取设备信息 - IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(username); + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); if (deviceInfo == null) { log.warn("[authenticateDevice][用户名格式不正确,客户端 ID: {},用户名: {}]", clientId, username); return false; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqttws/router/IotMqttWsUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqttws/router/IotMqttWsUpstreamHandler.java index d11d109502..26833fb46f 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqttws/router/IotMqttWsUpstreamHandler.java +++ b/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; 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.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.protocol.mqttws.IotMqttWsUpstreamProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.manager.IotMqttWsConnectionManager; @@ -521,7 +522,7 @@ public class IotMqttWsUpstreamHandler { } // 3. 获取设备信息 - IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(username); + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); if (deviceInfo == null) { log.warn("[authenticateDevice][用户名格式不正确,username: {}]", username); return null; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java index 0aff8f72f2..554a384cd7 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java +++ b/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; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; 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.codec.tcp.IotTcpBinaryDeviceMessageCodec; import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec; @@ -162,7 +163,7 @@ public class IotTcpUpstreamHandler implements Handler { } // 2.1 解析设备信息 - IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername()); + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername()); if (deviceInfo == null) { sendErrorResponse(socket, message.getRequestId(), "解析设备信息失败", codecType); return; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/auth/IotDeviceTokenService.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/auth/IotDeviceTokenService.java index 9aab67236b..6864c8de73 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/auth/IotDeviceTokenService.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/auth/IotDeviceTokenService.java @@ -1,6 +1,6 @@ package cn.iocoder.yudao.module.iot.gateway.service.auth; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; /** * IoT 设备 Token Service 接口 @@ -24,7 +24,7 @@ public interface IotDeviceTokenService { * @param token 设备 Token * @return 设备信息 */ - IotDeviceAuthUtils.DeviceInfo verifyToken(String token); + IotDeviceIdentity verifyToken(String token); /** * 解析用户名 @@ -32,6 +32,6 @@ public interface IotDeviceTokenService { * @param username 用户名 * @return 设备信息 */ - IotDeviceAuthUtils.DeviceInfo parseUsername(String username); + IotDeviceIdentity parseUsername(String username); } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/auth/IotDeviceTokenServiceImpl.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/auth/IotDeviceTokenServiceImpl.java index 79ba4e77e7..cc6e3fd37b 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/auth/IotDeviceTokenServiceImpl.java +++ b/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; import cn.hutool.jwt.JWT; import cn.hutool.jwt.JWTUtil; import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; +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.config.IotGatewayProperties; import jakarta.annotation.Resource; @@ -48,7 +49,7 @@ public class IotDeviceTokenServiceImpl implements IotDeviceTokenService { } @Override - public IotDeviceAuthUtils.DeviceInfo verifyToken(String token) { + public IotDeviceIdentity verifyToken(String token) { Assert.notBlank(token, "token 不能为空"); // 校验 JWT Token boolean verify = JWTUtil.verify(token, gatewayProperties.getToken().getSecret().getBytes()); @@ -68,11 +69,11 @@ public class IotDeviceTokenServiceImpl implements IotDeviceTokenService { String deviceName = payload.getStr("deviceName"); Assert.notBlank(productKey, "productKey 不能为空"); Assert.notBlank(deviceName, "deviceName 不能为空"); - return new IotDeviceAuthUtils.DeviceInfo().setProductKey(productKey).setDeviceName(deviceName); + return new IotDeviceIdentity(productKey, deviceName); } @Override - public IotDeviceAuthUtils.DeviceInfo parseUsername(String username) { + public IotDeviceIdentity parseUsername(String username) { return IotDeviceAuthUtils.parseUsername(username); } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/remote/IotDeviceApiImpl.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/remote/IotDeviceApiImpl.java index b325103743..97312559b9 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/remote/IotDeviceApiImpl.java +++ b/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; 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.gateway.config.IotGatewayProperties; + +import java.util.List; import jakarta.annotation.PostConstruct; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; @@ -54,6 +60,16 @@ public class IotDeviceApiImpl implements IotDeviceCommonApi { return doPost("/get", getReqDTO, new ParameterizedTypeReference<>() { }); } + @Override + public CommonResult registerDevice(IotDeviceRegisterReqDTO reqDTO) { + return doPost("/register", reqDTO, new ParameterizedTypeReference<>() { }); + } + + @Override + public CommonResult> registerSubDevices(IotSubDeviceRegisterFullReqDTO reqDTO) { + return doPost("/register-sub", reqDTO, new ParameterizedTypeReference<>() { }); + } + private CommonResult doPost(String url, T body, ParameterizedTypeReference> responseType) { try { diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotDirectDeviceHttpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotDirectDeviceHttpProtocolIntegrationTest.java new file mode 100644 index 0000000000..f4869873c8 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotDirectDeviceHttpProtocolIntegrationTest.java @@ -0,0 +1,179 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.http; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.http.HttpResponse; +import cn.hutool.http.HttpUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; +import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; +import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; + + +/** + * IoT 直连设备 HTTP 协议集成测试(手动测试) + * + *

测试场景:直连设备(IotProductDeviceTypeEnum 的 DIRECT 类型)通过 HTTP 协议直接连接平台 + * + *

使用步骤: + *

    + *
  1. 启动 yudao-module-iot-gateway 服务(HTTP 端口 8092)
  2. + *
  3. 运行 {@link #testDeviceRegister()} 测试直连设备动态注册(一型一密)
  4. + *
  5. 运行 {@link #testAuth()} 获取设备 token,将返回的 token 粘贴到 {@link #TOKEN} 常量
  6. + *
  7. 运行以下测试方法: + *
      + *
    • {@link #testPropertyPost()} - 设备属性上报
    • + *
    • {@link #testEventPost()} - 设备事件上报
    • + *
    + *
  8. + *
+ * + * @author 芋道源码 + */ +@Slf4j +@SuppressWarnings("HttpUrlsUsage") +public class IotDirectDeviceHttpProtocolIntegrationTest { + + private static final String SERVER_HOST = "127.0.0.1"; + private static final int SERVER_PORT = 8092; + + // ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询子设备) ===================== + private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT"; + private static final String DEVICE_NAME = "small"; + private static final String DEVICE_SECRET = "0baa4c2ecc104ae1a26b4070c218bdf3"; + + /** + * 直连设备 Token:从 {@link #testAuth()} 方法获取后,粘贴到这里 + */ + private static final String TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoiNGF5bVpnT1RPT0NyREtSVCIsImV4cCI6MTc2OTMwNTA1NSwiZGV2aWNlTmFtZSI6InNtYWxsIn0.mf3MEATCn5bp6cXgULunZjs8d00RGUxj96JEz0hMS7k"; + + // ===================== 认证测试 ===================== + + /** + * 认证测试:获取设备 Token + */ + @Test + public void testAuth() { + // 1.1 构建请求 + String url = String.format("http://%s:%d/auth", SERVER_HOST, SERVER_PORT); + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); + IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() + .setClientId(authInfo.getClientId()) + .setUsername(authInfo.getUsername()) + .setPassword(authInfo.getPassword()); + String payload = JsonUtils.toJsonString(authReqDTO); + // 1.2 输出请求 + log.info("[testAuth][请求 URL: {}]", url); + log.info("[testAuth][请求体: {}]", payload); + + // 2.1 发送请求 + String response = HttpUtil.post(url, payload); + // 2.2 输出结果 + log.info("[testAuth][响应体: {}]", response); + log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]"); + } + + // ===================== 直连设备属性上报测试 ===================== + + /** + * 属性上报测试 + */ + @Test + public void testPropertyPost() { + // 1.1 构建请求 + String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/property/post", + SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME); + String payload = JsonUtils.toJsonString(MapUtil.builder() + .put("id", IdUtil.fastSimpleUUID()) + .put("method", IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod()) + .put("version", "1.0") + .put("params", IotDevicePropertyPostReqDTO.of(MapUtil.builder() + .put("width", 1) + .put("height", "2") + .build()) + ) + .build()); + // 1.2 输出请求 + log.info("[testPropertyPost][请求 URL: {}]", url); + log.info("[testPropertyPost][请求体: {}]", payload); + + // 2.1 发送请求 + try (HttpResponse httpResponse = HttpUtil.createPost(url) + .header("Authorization", TOKEN) + .body(payload) + .execute()) { + // 2.2 输出结果 + log.info("[testPropertyPost][响应体: {}]", httpResponse.body()); + } + } + + // ===================== 直连设备事件上报测试 ===================== + + /** + * 事件上报测试 + */ + @Test + public void testEventPost() { + // 1.1 构建请求 + String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/event/post", + SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME); + String payload = JsonUtils.toJsonString(MapUtil.builder() + .put("id", IdUtil.fastSimpleUUID()) + .put("method", IotDeviceMessageMethodEnum.EVENT_POST.getMethod()) + .put("version", "1.0") + .put("params", IotDeviceEventPostReqDTO.of( + "eat", + MapUtil.builder().put("rice", 3).build(), + System.currentTimeMillis()) + ) + .build()); + // 1.2 输出请求 + log.info("[testEventPost][请求 URL: {}]", url); + log.info("[testEventPost][请求体: {}]", payload); + + // 2.1 发送请求 + try (HttpResponse httpResponse = HttpUtil.createPost(url) + .header("Authorization", TOKEN) + .body(payload) + .execute()) { + // 2.2 输出结果 + log.info("[testEventPost][响应体: {}]", httpResponse.body()); + } + } + + // ===================== 动态注册测试 ===================== + + /** + * 直连设备动态注册测试(一型一密) + *

+ * 使用产品密钥(productSecret)验证身份,成功后返回设备密钥(deviceSecret) + *

+ * 注意:此接口不需要 Token 认证 + */ + @Test + public void testDeviceRegister() { + // 1.1 构建请求 + String url = String.format("http://%s:%d/auth/register/device", SERVER_HOST, SERVER_PORT); + // 1.2 构建请求参数 + IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO(); + reqDTO.setProductKey(PRODUCT_KEY); + reqDTO.setDeviceName("test-" + System.currentTimeMillis()); + reqDTO.setProductSecret("test-product-secret"); + String payload = JsonUtils.toJsonString(reqDTO); + // 1.3 输出请求 + log.info("[testDeviceRegister][请求 URL: {}]", url); + log.info("[testDeviceRegister][请求体: {}]", payload); + + // 2.1 发送请求 + String response = HttpUtil.post(url, payload); + // 2.2 输出结果 + log.info("[testDeviceRegister][响应体: {}]", response); + log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]"); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotGatewayDeviceHttpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotGatewayDeviceHttpProtocolIntegrationTest.java new file mode 100644 index 0000000000..948a5efa3a --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotGatewayDeviceHttpProtocolIntegrationTest.java @@ -0,0 +1,308 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.http; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.http.HttpResponse; +import cn.hutool.http.HttpUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; +import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO; +import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPackPostReqDTO; +import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO; +import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO; +import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetReqDTO; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + + +/** + * IoT 网关设备 HTTP 协议集成测试(手动测试) + * + *

测试场景:网关设备(IotProductDeviceTypeEnum 的 GATEWAY 类型)通过 HTTP 协议管理子设备拓扑关系 + * + *

使用步骤: + *

    + *
  1. 启动 yudao-module-iot-gateway 服务(HTTP 端口 8092)
  2. + *
  3. 运行 {@link #testAuth()} 获取网关设备 token,将返回的 token 粘贴到 {@link #GATEWAY_TOKEN} 常量
  4. + *
  5. 运行以下测试方法: + *
      + *
    • {@link #testTopoAdd()} - 添加子设备拓扑关系
    • + *
    • {@link #testTopoDelete()} - 删除子设备拓扑关系
    • + *
    • {@link #testTopoGet()} - 获取子设备拓扑关系
    • + *
    • {@link #testSubDeviceRegister()} - 子设备动态注册
    • + *
    • {@link #testPropertyPackPost()} - 批量上报属性(网关 + 子设备)
    • + *
    + *
  6. + *
+ * + * @author 芋道源码 + */ +@Slf4j +@SuppressWarnings("HttpUrlsUsage") +public class IotGatewayDeviceHttpProtocolIntegrationTest { + + private static final String SERVER_HOST = "127.0.0.1"; + private static final int SERVER_PORT = 8092; + + // ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) ===================== + private static final String GATEWAY_PRODUCT_KEY = "m6XcS1ZJ3TW8eC0v"; + private static final String GATEWAY_DEVICE_NAME = "sub-ddd"; + private static final String GATEWAY_DEVICE_SECRET = "b3d62c70f8a4495487ed1d35d61ac2b3"; + + /** + * 网关设备 Token:从 {@link #testAuth()} 方法获取后,粘贴到这里 + */ + private static final String GATEWAY_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoibTZYY1MxWkozVFc4ZUMwdiIsImV4cCI6MTc2OTg2NjY3OCwiZGV2aWNlTmFtZSI6InN1Yi1kZGQifQ.nCLSAfHEjXLtTDRXARjOoFqpuo5WfArjFWweUAzrjKU"; + + // ===================== 子设备信息(根据实际情况修改,从 iot_device 表查询子设备) ===================== + private static final String SUB_DEVICE_PRODUCT_KEY = "jAufEMTF1W6wnPhn"; + private static final String SUB_DEVICE_NAME = "chazuo-it"; + private static final String SUB_DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af"; + + // ===================== 认证测试 ===================== + + /** + * 网关设备认证测试:获取网关设备 Token + */ + @Test + public void testAuth() { + // 1.1 构建请求 + String url = String.format("http://%s:%d/auth", SERVER_HOST, SERVER_PORT); + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET); + IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() + .setClientId(authInfo.getClientId()) + .setUsername(authInfo.getUsername()) + .setPassword(authInfo.getPassword()); + String payload = JsonUtils.toJsonString(authReqDTO); + // 1.2 输出请求 + log.info("[testAuth][请求 URL: {}]", url); + log.info("[testAuth][请求体: {}]", payload); + + // 2.1 发送请求 + String response = HttpUtil.post(url, payload); + // 2.2 输出结果 + log.info("[testAuth][响应体: {}]", response); + log.info("[testAuth][请将返回的 token 复制到 GATEWAY_TOKEN 常量中]"); + } + + // ===================== 拓扑管理测试 ===================== + + /** + * 添加子设备拓扑关系测试 + *

+ * 网关设备向平台上报需要绑定的子设备信息 + */ + @Test + public void testTopoAdd() { + // 1.1 构建请求 + String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/topo/add", + SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); + // 1.2 构建子设备认证信息 + IotDeviceAuthReqDTO subAuthInfo = IotDeviceAuthUtils.getAuthInfo( + SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET); + IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO() + .setClientId(subAuthInfo.getClientId()) + .setUsername(subAuthInfo.getUsername()) + .setPassword(subAuthInfo.getPassword()); + // 1.3 构建请求参数 + IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO(); + params.setSubDevices(Collections.singletonList(subDeviceAuth)); + String payload = JsonUtils.toJsonString(MapUtil.builder() + .put("id", IdUtil.fastSimpleUUID()) + .put("method", IotDeviceMessageMethodEnum.TOPO_ADD.getMethod()) + .put("version", "1.0") + .put("params", params) + .build()); + // 1.4 输出请求 + log.info("[testTopoAdd][请求 URL: {}]", url); + log.info("[testTopoAdd][请求体: {}]", payload); + + // 2.1 发送请求 + try (HttpResponse httpResponse = HttpUtil.createPost(url) + .header("Authorization", GATEWAY_TOKEN) + .body(payload) + .execute()) { + // 2.2 输出结果 + log.info("[testTopoAdd][响应体: {}]", httpResponse.body()); + } + } + + /** + * 删除子设备拓扑关系测试 + *

+ * 网关设备向平台上报需要解绑的子设备信息 + */ + @Test + public void testTopoDelete() { + // 1.1 构建请求 + String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/topo/delete", + SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); + // 1.2 构建请求参数 + IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO(); + params.setSubDevices(Collections.singletonList( + new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME))); + String payload = JsonUtils.toJsonString(MapUtil.builder() + .put("id", IdUtil.fastSimpleUUID()) + .put("method", IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod()) + .put("version", "1.0") + .put("params", params) + .build()); + // 1.3 输出请求 + log.info("[testTopoDelete][请求 URL: {}]", url); + log.info("[testTopoDelete][请求体: {}]", payload); + + // 2.1 发送请求 + try (HttpResponse httpResponse = HttpUtil.createPost(url) + .header("Authorization", GATEWAY_TOKEN) + .body(payload) + .execute()) { + // 2.2 输出结果 + log.info("[testTopoDelete][响应体: {}]", httpResponse.body()); + } + } + + /** + * 获取子设备拓扑关系测试 + *

+ * 网关设备向平台查询已绑定的子设备列表 + */ + @Test + public void testTopoGet() { + // 1.1 构建请求 + String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/topo/get", + SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); + // 1.2 构建请求参数(目前为空,预留扩展) + IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO(); + String payload = JsonUtils.toJsonString(MapUtil.builder() + .put("id", IdUtil.fastSimpleUUID()) + .put("method", IotDeviceMessageMethodEnum.TOPO_GET.getMethod()) + .put("version", "1.0") + .put("params", params) + .build()); + // 1.3 输出请求 + log.info("[testTopoGet][请求 URL: {}]", url); + log.info("[testTopoGet][请求体: {}]", payload); + + // 2.1 发送请求 + try (HttpResponse httpResponse = HttpUtil.createPost(url) + .header("Authorization", GATEWAY_TOKEN) + .body(payload) + .execute()) { + // 2.2 输出结果 + log.info("[testTopoGet][响应体: {}]", httpResponse.body()); + } + } + + // ===================== 子设备注册测试 ===================== + + // TODO @芋艿:待测试 + + /** + * 子设备动态注册测试 + *

+ * 网关设备代理子设备进行动态注册,平台返回子设备的 deviceSecret + *

+ * 注意:此接口需要网关 Token 认证 + */ + @Test + public void testSubDeviceRegister() { + // 1.1 构建请求 + String url = String.format("http://%s:%d/auth/register/sub-device/%s/%s", + SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); + // 1.2 构建请求参数 + IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO(); + subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY); + subDevice.setDeviceName("mougezishebei"); + String payload = JsonUtils.toJsonString(MapUtil.builder() + .put("id", IdUtil.fastSimpleUUID()) + .put("method", IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod()) + .put("version", "1.0") + .put("params", Collections.singletonList(subDevice)) + .build()); + // 1.3 输出请求 + log.info("[testSubDeviceRegister][请求 URL: {}]", url); + log.info("[testSubDeviceRegister][请求体: {}]", payload); + + // 2.1 发送请求 + try (HttpResponse httpResponse = HttpUtil.createPost(url) + .header("Authorization", GATEWAY_TOKEN) + .body(payload) + .execute()) { + // 2.2 输出结果 + log.info("[testSubDeviceRegister][响应体: {}]", httpResponse.body()); + } + } + + // ===================== 批量上报测试 ===================== + + /** + * 批量上报属性测试(网关 + 子设备) + *

+ * 网关设备批量上报自身属性、事件,以及子设备的属性、事件 + */ + @Test + public void testPropertyPackPost() { + // 1.1 构建请求 + String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/event/property/pack/post", + SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); + // 1.2 构建【网关设备】自身属性 + Map gatewayProperties = MapUtil.builder() + .put("temperature", 25.5) + .build(); + // 1.3 构建【网关设备】自身事件 + IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue(); + gatewayEvent.setValue(MapUtil.builder().put("message", "gateway started").build()); + gatewayEvent.setTime(System.currentTimeMillis()); + Map gatewayEvents = MapUtil.builder() + .put("statusReport", gatewayEvent) + .build(); + // 1.4 构建【网关子设备】属性 + Map subDeviceProperties = MapUtil.builder() + .put("power", 100) + .build(); + // 1.5 构建【网关子设备】事件 + IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue(); + subDeviceEvent.setValue(MapUtil.builder().put("errorCode", 0).build()); + subDeviceEvent.setTime(System.currentTimeMillis()); + Map subDeviceEvents = MapUtil.builder() + .put("healthCheck", subDeviceEvent) + .build(); + // 1.6 构建子设备数据 + IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData(); + subDeviceData.setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)); + subDeviceData.setProperties(subDeviceProperties); + subDeviceData.setEvents(subDeviceEvents); + // 1.7 构建请求参数 + IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO(); + params.setProperties(gatewayProperties); + params.setEvents(gatewayEvents); + params.setSubDevices(List.of(subDeviceData)); + String payload = JsonUtils.toJsonString(MapUtil.builder() + .put("id", IdUtil.fastSimpleUUID()) + .put("method", IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod()) + .put("version", "1.0") + .put("params", params) + .build()); + // 1.8 输出请求 + log.info("[testPropertyPackPost][请求 URL: {}]", url); + log.info("[testPropertyPackPost][请求体: {}]", payload); + + // 2.1 发送请求 + try (HttpResponse httpResponse = HttpUtil.createPost(url) + .header("Authorization", GATEWAY_TOKEN) + .body(payload) + .execute()) { + // 2.2 输出结果 + log.info("[testPropertyPackPost][响应体: {}]", httpResponse.body()); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotGatewaySubDeviceHttpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotGatewaySubDeviceHttpProtocolIntegrationTest.java new file mode 100644 index 0000000000..7bb83a52b9 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotGatewaySubDeviceHttpProtocolIntegrationTest.java @@ -0,0 +1,159 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.http; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.http.HttpResponse; +import cn.hutool.http.HttpUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; +import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; + + +/** + * IoT 网关子设备 HTTP 协议集成测试(手动测试) + * + *

测试场景:子设备(IotProductDeviceTypeEnum 的 SUB 类型)通过网关设备代理上报数据 + * + *

重要说明:子设备无法直接连接平台,所有请求均由网关设备(Gateway)代为转发。 + *

网关设备转发子设备请求时,URL 和 Token 都使用子设备自己的信息。 + * + *

使用步骤: + *

    + *
  1. 启动 yudao-module-iot-gateway 服务(HTTP 端口 8092)
  2. + *
  3. 确保子设备已通过 {@link IotGatewayDeviceHttpProtocolIntegrationTest#testTopoAdd()} 绑定到网关
  4. + *
  5. 运行 {@link #testAuth()} 获取子设备 token,将返回的 token 粘贴到 {@link #TOKEN} 常量
  6. + *
  7. 运行以下测试方法: + *
      + *
    • {@link #testPropertyPost()} - 子设备属性上报(由网关代理转发)
    • + *
    • {@link #testEventPost()} - 子设备事件上报(由网关代理转发)
    • + *
    + *
  8. + *
+ * + * @author 芋道源码 + */ +@Slf4j +@SuppressWarnings("HttpUrlsUsage") +public class IotGatewaySubDeviceHttpProtocolIntegrationTest { + + private static final String SERVER_HOST = "127.0.0.1"; + private static final int SERVER_PORT = 8092; + + // ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) ===================== + private static final String PRODUCT_KEY = "jAufEMTF1W6wnPhn"; + private static final String DEVICE_NAME = "chazuo-it"; + private static final String DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af"; + + /** + * 网关子设备 Token:从 {@link #testAuth()} 方法获取后,粘贴到这里 + */ + private static final String TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoiakF1ZkVNVEYxVzZ3blBobiIsImV4cCI6MTc2OTg3MTI3NCwiZGV2aWNlTmFtZSI6ImNoYXp1by1pdCJ9.99sAlRalzMU3CqRlGStDzCwWSBJq6u3PJw48JQ3NpzQ"; + + // ===================== 认证测试 ===================== + + /** + * 子设备认证测试:获取子设备 Token + */ + @Test + public void testAuth() { + // 1.1 构建请求 + String url = String.format("http://%s:%d/auth", SERVER_HOST, SERVER_PORT); + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); + IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() + .setClientId(authInfo.getClientId()) + .setUsername(authInfo.getUsername()) + .setPassword(authInfo.getPassword()); + String payload = JsonUtils.toJsonString(authReqDTO); + // 1.2 输出请求 + log.info("[testAuth][请求 URL: {}]", url); + log.info("[testAuth][请求体: {}]", payload); + + // 2.1 发送请求 + String response = HttpUtil.post(url, payload); + // 2.2 输出结果 + log.info("[testAuth][响应体: {}]", response); + log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]"); + } + + // ===================== 子设备属性上报测试 ===================== + + /** + * 子设备属性上报测试 + */ + @Test + public void testPropertyPost() { + // 1.1 构建请求 + String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/property/post", + SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME); + String payload = JsonUtils.toJsonString(MapUtil.builder() + .put("id", IdUtil.fastSimpleUUID()) + .put("method", IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod()) + .put("version", "1.0") + .put("params", IotDevicePropertyPostReqDTO.of(MapUtil.builder() + .put("power", 100) + .put("status", "online") + .put("temperature", 36.5) + .build()) + ) + .build()); + // 1.2 输出请求 + log.info("[testPropertyPost][子设备属性上报 - 请求实际由 Gateway 代为转发]"); + log.info("[testPropertyPost][请求 URL: {}]", url); + log.info("[testPropertyPost][请求体: {}]", payload); + + // 2.1 发送请求 + try (HttpResponse httpResponse = HttpUtil.createPost(url) + .header("Authorization", TOKEN) + .body(payload) + .execute()) { + // 2.2 输出结果 + log.info("[testPropertyPost][响应体: {}]", httpResponse.body()); + } + } + + // ===================== 子设备事件上报测试 ===================== + + /** + * 子设备事件上报测试 + */ + @Test + public void testEventPost() { + // 1.1 构建请求 + String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/event/post", + SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME); + String payload = JsonUtils.toJsonString(MapUtil.builder() + .put("id", IdUtil.fastSimpleUUID()) + .put("method", IotDeviceMessageMethodEnum.EVENT_POST.getMethod()) + .put("version", "1.0") + .put("params", IotDeviceEventPostReqDTO.of( + "alarm", + MapUtil.builder() + .put("level", "warning") + .put("message", "temperature too high") + .put("threshold", 40) + .put("current", 42) + .build(), + System.currentTimeMillis()) + ) + .build()); + // 1.2 输出请求 + log.info("[testEventPost][子设备事件上报 - 请求实际由 Gateway 代为转发]"); + log.info("[testEventPost][请求 URL: {}]", url); + log.info("[testEventPost][请求体: {}]", payload); + + // 2.1 发送请求 + try (HttpResponse httpResponse = HttpUtil.createPost(url) + .header("Authorization", TOKEN) + .body(payload) + .execute()) { + // 2.2 输出结果 + log.info("[testEventPost][响应体: {}]", httpResponse.body()); + } + } + +}