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