Commit 4b1dfad063fa8fafa9e8866f4f90f78f2fce69f4
1 parent
45638b35
feat(iot):【网关设备:20%】增加网关设备绑定能力(未完成),基于 breezy-doodling-starlight.md 规划
Showing
7 changed files
with
265 additions
and
1 deletions
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceController.java
| @@ -51,6 +51,7 @@ public class IotDeviceController { | @@ -51,6 +51,7 @@ public class IotDeviceController { | ||
| 51 | return success(deviceService.createDevice(createReqVO)); | 51 | return success(deviceService.createDevice(createReqVO)); |
| 52 | } | 52 | } |
| 53 | 53 | ||
| 54 | + | ||
| 54 | @PutMapping("/update") | 55 | @PutMapping("/update") |
| 55 | @Operation(summary = "更新设备") | 56 | @Operation(summary = "更新设备") |
| 56 | @PreAuthorize("@ss.hasPermission('iot:device:update')") | 57 | @PreAuthorize("@ss.hasPermission('iot:device:update')") |
| @@ -59,7 +60,72 @@ public class IotDeviceController { | @@ -59,7 +60,72 @@ public class IotDeviceController { | ||
| 59 | return success(true); | 60 | return success(true); |
| 60 | } | 61 | } |
| 61 | 62 | ||
| 62 | - // TODO @芋艿:参考阿里云:1)绑定网关;2)解绑网关 | 63 | + @PutMapping("/bind-gateway") |
| 64 | + @Operation(summary = "绑定子设备到网关") | ||
| 65 | + @PreAuthorize("@ss.hasPermission('iot:device:update')") | ||
| 66 | + public CommonResult<Boolean> bindDeviceGateway(@Valid @RequestBody IotDeviceBindGatewayReqVO reqVO) { | ||
| 67 | + deviceService.bindDeviceGateway(reqVO.getIds(), reqVO.getGatewayId()); | ||
| 68 | + return success(true); | ||
| 69 | + } | ||
| 70 | + | ||
| 71 | + @PutMapping("/unbind-gateway") | ||
| 72 | + @Operation(summary = "解绑子设备与网关") | ||
| 73 | + @PreAuthorize("@ss.hasPermission('iot:device:update')") | ||
| 74 | + public CommonResult<Boolean> unbindDeviceGateway(@Valid @RequestBody IotDeviceUnbindGatewayReqVO reqVO) { | ||
| 75 | + deviceService.unbindDeviceGateway(reqVO.getIds()); | ||
| 76 | + return success(true); | ||
| 77 | + } | ||
| 78 | + | ||
| 79 | + @GetMapping("/sub-device-list") | ||
| 80 | + @Operation(summary = "获取网关的子设备列表") | ||
| 81 | + @Parameter(name = "gatewayId", description = "网关设备编号", required = true, example = "1") | ||
| 82 | + @PreAuthorize("@ss.hasPermission('iot:device:query')") | ||
| 83 | + public CommonResult<List<IotDeviceRespVO>> getSubDeviceList(@RequestParam("gatewayId") Long gatewayId) { | ||
| 84 | + List<IotDeviceDO> list = deviceService.getDeviceListByGatewayId(gatewayId); | ||
| 85 | + if (CollUtil.isEmpty(list)) { | ||
| 86 | + return success(Collections.emptyList()); | ||
| 87 | + } | ||
| 88 | + | ||
| 89 | + // 补充产品名称 | ||
| 90 | + Map<Long, IotProductDO> productMap = convertMap(productService.getProductList(), IotProductDO::getId); | ||
| 91 | + return success(convertList(list, device -> { | ||
| 92 | + IotDeviceRespVO respVO = BeanUtils.toBean(device, IotDeviceRespVO.class); | ||
| 93 | + MapUtils.findAndThen(productMap, device.getProductId(), | ||
| 94 | + product -> respVO.setProductName(product.getName())); | ||
| 95 | + return respVO; | ||
| 96 | + })); | ||
| 97 | + } | ||
| 98 | + | ||
| 99 | + // TODO @AI:希望改成“未绑定的”。需要剔除已经绑定,包括自己的; | ||
| 100 | + // TODO @AI:不需要传递 gatewayId; | ||
| 101 | + // TODO @AI:需要分页; | ||
| 102 | + @GetMapping("/bindable-sub-device-list") | ||
| 103 | + @Operation(summary = "获取可绑定到网关的子设备列表") | ||
| 104 | + @Parameter(name = "gatewayId", description = "网关设备编号(可选)", example = "1") | ||
| 105 | + @PreAuthorize("@ss.hasPermission('iot:device:query')") | ||
| 106 | + public CommonResult<List<IotDeviceRespVO>> getBindableSubDeviceList( | ||
| 107 | + @RequestParam(value = "gatewayId", required = false) Long gatewayId) { | ||
| 108 | + List<IotDeviceDO> list = deviceService.getBindableSubDeviceList(gatewayId); | ||
| 109 | + if (CollUtil.isEmpty(list)) { | ||
| 110 | + return success(Collections.emptyList()); | ||
| 111 | + } | ||
| 112 | + | ||
| 113 | + // 补充产品名称 | ||
| 114 | + Map<Long, IotProductDO> productMap = convertMap(productService.getProductList(), IotProductDO::getId); | ||
| 115 | + return success(convertList(list, device -> { | ||
| 116 | + // TODO @AI:可以 beanutils 转换么? | ||
| 117 | + IotDeviceRespVO respVO = new IotDeviceRespVO() | ||
| 118 | + .setId(device.getId()) | ||
| 119 | + .setDeviceName(device.getDeviceName()) | ||
| 120 | + .setNickname(device.getNickname()) | ||
| 121 | + .setProductId(device.getProductId()) | ||
| 122 | + .setState(device.getState()) | ||
| 123 | + .setGatewayId(device.getGatewayId()); | ||
| 124 | + MapUtils.findAndThen(productMap, device.getProductId(), | ||
| 125 | + product -> respVO.setProductName(product.getName())); | ||
| 126 | + return respVO; | ||
| 127 | + })); | ||
| 128 | + } | ||
| 63 | 129 | ||
| 64 | @PutMapping("/update-group") | 130 | @PutMapping("/update-group") |
| 65 | @Operation(summary = "更新设备分组") | 131 | @Operation(summary = "更新设备分组") |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceBindGatewayReqVO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device; | ||
| 2 | + | ||
| 3 | +import io.swagger.v3.oas.annotations.media.Schema; | ||
| 4 | +import jakarta.validation.constraints.NotEmpty; | ||
| 5 | +import jakarta.validation.constraints.NotNull; | ||
| 6 | +import lombok.Data; | ||
| 7 | + | ||
| 8 | +import java.util.Set; | ||
| 9 | + | ||
| 10 | +@Schema(description = "管理后台 - IoT 设备绑定网关 Request VO") | ||
| 11 | +@Data | ||
| 12 | +public class IotDeviceBindGatewayReqVO { | ||
| 13 | + | ||
| 14 | + @Schema(description = "子设备编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3") | ||
| 15 | + @NotEmpty(message = "子设备编号列表不能为空") | ||
| 16 | + private Set<Long> ids; | ||
| 17 | + | ||
| 18 | + @Schema(description = "网关设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") | ||
| 19 | + @NotNull(message = "网关设备编号不能为空") | ||
| 20 | + private Long gatewayId; | ||
| 21 | + | ||
| 22 | +} |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceUnbindGatewayReqVO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device; | ||
| 2 | + | ||
| 3 | +import io.swagger.v3.oas.annotations.media.Schema; | ||
| 4 | +import jakarta.validation.constraints.NotEmpty; | ||
| 5 | +import lombok.Data; | ||
| 6 | + | ||
| 7 | +import java.util.Set; | ||
| 8 | + | ||
| 9 | +@Schema(description = "管理后台 - IoT 设备解绑网关 Request VO") | ||
| 10 | +@Data | ||
| 11 | +public class IotDeviceUnbindGatewayReqVO { | ||
| 12 | + | ||
| 13 | + @Schema(description = "子设备编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3") | ||
| 14 | + @NotEmpty(message = "子设备编号列表不能为空") | ||
| 15 | + private Set<Long> ids; | ||
| 16 | + | ||
| 17 | +} |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceMapper.java
| @@ -129,4 +129,33 @@ public interface IotDeviceMapper extends BaseMapperX<IotDeviceDO> { | @@ -129,4 +129,33 @@ public interface IotDeviceMapper extends BaseMapperX<IotDeviceDO> { | ||
| 129 | .isNotNull(IotDeviceDO::getLongitude)); | 129 | .isNotNull(IotDeviceDO::getLongitude)); |
| 130 | } | 130 | } |
| 131 | 131 | ||
| 132 | + // ========== 网关-子设备绑定相关 ========== | ||
| 133 | + | ||
| 134 | + /** | ||
| 135 | + * 根据网关编号查询子设备列表 | ||
| 136 | + * | ||
| 137 | + * @param gatewayId 网关设备编号 | ||
| 138 | + * @return 子设备列表 | ||
| 139 | + */ | ||
| 140 | + default List<IotDeviceDO> selectListByGatewayId(Long gatewayId) { | ||
| 141 | + return selectList(IotDeviceDO::getGatewayId, gatewayId); | ||
| 142 | + } | ||
| 143 | + | ||
| 144 | + /** | ||
| 145 | + * 查询可绑定到网关的子设备列表 | ||
| 146 | + * <p> | ||
| 147 | + * 条件:设备类型为 GATEWAY_SUB 且未绑定任何网关,或已绑定到指定网关 | ||
| 148 | + * | ||
| 149 | + * @param gatewayId 网关设备编号(可选,用于包含已绑定到该网关的设备) | ||
| 150 | + * @return 子设备列表 | ||
| 151 | + */ | ||
| 152 | + default List<IotDeviceDO> selectBindableSubDeviceList(@Nullable Long gatewayId) { | ||
| 153 | + return selectList(new LambdaQueryWrapperX<IotDeviceDO>() | ||
| 154 | + .eq(IotDeviceDO::getDeviceType, cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum.GATEWAY_SUB.getType()) | ||
| 155 | + .and(wrapper -> wrapper | ||
| 156 | + .isNull(IotDeviceDO::getGatewayId))); | ||
| 157 | +// .or() | ||
| 158 | +// .eqIfPresent(IotDeviceDO::getGatewayId, gatewayId))) | ||
| 159 | + } | ||
| 160 | + | ||
| 132 | } | 161 | } |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java
| @@ -33,6 +33,12 @@ public interface ErrorCodeConstants { | @@ -33,6 +33,12 @@ public interface ErrorCodeConstants { | ||
| 33 | ErrorCode DEVICE_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_050_003_006, "导入设备数据不能为空!"); | 33 | ErrorCode DEVICE_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_050_003_006, "导入设备数据不能为空!"); |
| 34 | ErrorCode DEVICE_DOWNSTREAM_FAILED_SERVER_ID_NULL = new ErrorCode(1_050_003_007, "下行设备消息失败,原因:设备未连接网关"); | 34 | ErrorCode DEVICE_DOWNSTREAM_FAILED_SERVER_ID_NULL = new ErrorCode(1_050_003_007, "下行设备消息失败,原因:设备未连接网关"); |
| 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 | + // TODO @AI:1_050_003_009 需要提示具体的哪个设备。产品/设备,标识下 | ||
| 37 | + ErrorCode DEVICE_NOT_GATEWAY_SUB = new ErrorCode(1_050_003_009, "设备不是网关子设备类型,无法绑定到网关"); | ||
| 38 | + // TODO @AI:1_050_003_009 需要提示具体的哪个设备。产品/设备,标识下 | ||
| 39 | + ErrorCode DEVICE_GATEWAY_BINDTO_EXISTS = new ErrorCode(1_050_003_010, "设备已绑定到其他网关,请先解绑"); | ||
| 40 | + // TODO @AI:是不是可以删除,DEVICE_GATEWAY_BINDTO_NOT_EXISTS | ||
| 41 | + ErrorCode DEVICE_GATEWAY_BINDTO_NOT_EXISTS = new ErrorCode(1_050_003_011, "设备未绑定到任何网关"); | ||
| 36 | 42 | ||
| 37 | // ========== 产品分类 1-050-004-000 ========== | 43 | // ========== 产品分类 1-050-004-000 ========== |
| 38 | ErrorCode PRODUCT_CATEGORY_NOT_EXISTS = new ErrorCode(1_050_004_000, "产品分类不存在"); | 44 | 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
| @@ -73,6 +73,7 @@ public interface IotDeviceService { | @@ -73,6 +73,7 @@ public interface IotDeviceService { | ||
| 73 | */ | 73 | */ |
| 74 | void updateDeviceGroup(@Valid IotDeviceUpdateGroupReqVO updateReqVO); | 74 | void updateDeviceGroup(@Valid IotDeviceUpdateGroupReqVO updateReqVO); |
| 75 | 75 | ||
| 76 | + // TODO @AI:网关设备被删除时,需要查看是否有子设备绑定。如果有,则不允许删除。 | ||
| 76 | /** | 77 | /** |
| 77 | * 删除单个设备 | 78 | * 删除单个设备 |
| 78 | * | 79 | * |
| @@ -288,4 +289,48 @@ public interface IotDeviceService { | @@ -288,4 +289,48 @@ public interface IotDeviceService { | ||
| 288 | */ | 289 | */ |
| 289 | List<IotDeviceDO> getDeviceListByHasLocation(); | 290 | List<IotDeviceDO> getDeviceListByHasLocation(); |
| 290 | 291 | ||
| 292 | + // ========== 网关-子设备绑定相关 ========== | ||
| 293 | + | ||
| 294 | + /** | ||
| 295 | + * 绑定子设备到网关 | ||
| 296 | + * | ||
| 297 | + * @param ids 子设备编号列表 | ||
| 298 | + * @param gatewayId 网关设备编号 | ||
| 299 | + */ | ||
| 300 | + void bindDeviceGateway(Collection<Long> ids, Long gatewayId); | ||
| 301 | + | ||
| 302 | + /** | ||
| 303 | + * 解绑子设备与网关 | ||
| 304 | + * | ||
| 305 | + * @param ids 子设备编号列表 | ||
| 306 | + */ | ||
| 307 | + void unbindDeviceGateway(Collection<Long> ids); | ||
| 308 | + | ||
| 309 | + /** | ||
| 310 | + * 获取可绑定到网关的子设备列表 | ||
| 311 | + * <p> | ||
| 312 | + * 条件:设备类型为 GATEWAY_SUB 且未绑定任何网关 | ||
| 313 | + * | ||
| 314 | + * @param gatewayId 网关设备编号(可选,用于包含已绑定到该网关的设备) | ||
| 315 | + * @return 子设备列表 | ||
| 316 | + */ | ||
| 317 | + List<IotDeviceDO> getBindableSubDeviceList(@Nullable Long gatewayId); | ||
| 318 | + | ||
| 319 | + /** | ||
| 320 | + * 根据网关编号获取子设备列表 | ||
| 321 | + * | ||
| 322 | + * @param gatewayId 网关设备编号 | ||
| 323 | + * @return 子设备列表 | ||
| 324 | + */ | ||
| 325 | + List<IotDeviceDO> getDeviceListByGatewayId(Long gatewayId); | ||
| 326 | + | ||
| 327 | + // TODO @AI:暂时用不到,可以删除。 | ||
| 328 | + /** | ||
| 329 | + * 根据网关编号获取子设备数量 | ||
| 330 | + * | ||
| 331 | + * @param gatewayId 网关设备编号 | ||
| 332 | + * @return 子设备数量 | ||
| 333 | + */ | ||
| 334 | + Long getDeviceCountByGatewayId(Long gatewayId); | ||
| 335 | + | ||
| 291 | } | 336 | } |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java
| @@ -514,6 +514,85 @@ public class IotDeviceServiceImpl implements IotDeviceService { | @@ -514,6 +514,85 @@ public class IotDeviceServiceImpl implements IotDeviceService { | ||
| 514 | return deviceMapper.selectListByHasLocation(); | 514 | return deviceMapper.selectListByHasLocation(); |
| 515 | } | 515 | } |
| 516 | 516 | ||
| 517 | + // ========== 网关-子设备绑定相关 ========== | ||
| 518 | + | ||
| 519 | + @Override | ||
| 520 | + @Transactional(rollbackFor = Exception.class) | ||
| 521 | + public void bindDeviceGateway(Collection<Long> ids, Long gatewayId) { | ||
| 522 | + if (CollUtil.isEmpty(ids)) { | ||
| 523 | + return; | ||
| 524 | + } | ||
| 525 | + // TODO @AI:校验应该是 1.1、1.2 统一风格; | ||
| 526 | + // 1. 校验网关设备存在且类型正确 | ||
| 527 | + validateGatewayDeviceExists(gatewayId); | ||
| 528 | + | ||
| 529 | + // 2. 校验并绑定每个子设备 | ||
| 530 | + List<IotDeviceDO> devices = deviceMapper.selectByIds(ids); | ||
| 531 | + if (devices.size() != ids.size()) { | ||
| 532 | + throw exception(DEVICE_NOT_EXISTS); | ||
| 533 | + } | ||
| 534 | + | ||
| 535 | + List<IotDeviceDO> updateList = new ArrayList<>(); | ||
| 536 | + for (IotDeviceDO device : devices) { | ||
| 537 | + // 2.1 校验是否为子设备类型 | ||
| 538 | + if (!IotProductDeviceTypeEnum.isGatewaySub(device.getDeviceType())) { | ||
| 539 | + throw exception(DEVICE_NOT_GATEWAY_SUB); | ||
| 540 | + } | ||
| 541 | + // 2.2 校验是否已绑定其他网关 | ||
| 542 | + if (device.getGatewayId() != null && !device.getGatewayId().equals(gatewayId)) { | ||
| 543 | + throw exception(DEVICE_GATEWAY_BINDTO_EXISTS); | ||
| 544 | + } | ||
| 545 | + updateList.add(new IotDeviceDO().setId(device.getId()).setGatewayId(gatewayId)); | ||
| 546 | + } | ||
| 547 | + | ||
| 548 | + // 3. 批量更新数据库 | ||
| 549 | + // TODO @AI:List<IotDeviceDO> updateList 直接 convertList,不用上面 for 里面搞;校验是校验,插入是插入; | ||
| 550 | + deviceMapper.updateBatch(updateList); | ||
| 551 | + | ||
| 552 | + // 4. 清空对应缓存 | ||
| 553 | + deleteDeviceCache(devices); | ||
| 554 | + } | ||
| 555 | + | ||
| 556 | + @Override | ||
| 557 | + @Transactional(rollbackFor = Exception.class) | ||
| 558 | + public void unbindDeviceGateway(Collection<Long> ids) { | ||
| 559 | + if (CollUtil.isEmpty(ids)) { | ||
| 560 | + return; | ||
| 561 | + } | ||
| 562 | + // 1. 校验设备存在 | ||
| 563 | + List<IotDeviceDO> devices = deviceMapper.selectByIds(ids); | ||
| 564 | + if (devices.size() != ids.size()) { | ||
| 565 | + throw exception(DEVICE_NOT_EXISTS); | ||
| 566 | + } | ||
| 567 | + | ||
| 568 | + // 2. 批量更新数据库(将 gatewayId 设置为 null) | ||
| 569 | + List<IotDeviceDO> updateList = devices.stream() | ||
| 570 | + .filter(device -> device.getGatewayId() != null) | ||
| 571 | + .map(device -> new IotDeviceDO().setId(device.getId()).setGatewayId(null)) | ||
| 572 | + .toList(); | ||
| 573 | + if (CollUtil.isNotEmpty(updateList)) { | ||
| 574 | + deviceMapper.updateBatch(updateList); | ||
| 575 | + } | ||
| 576 | + | ||
| 577 | + // 3. 清空对应缓存 | ||
| 578 | + deleteDeviceCache(devices); | ||
| 579 | + } | ||
| 580 | + | ||
| 581 | + @Override | ||
| 582 | + public List<IotDeviceDO> getBindableSubDeviceList(@Nullable Long gatewayId) { | ||
| 583 | + return deviceMapper.selectBindableSubDeviceList(gatewayId); | ||
| 584 | + } | ||
| 585 | + | ||
| 586 | + @Override | ||
| 587 | + public List<IotDeviceDO> getDeviceListByGatewayId(Long gatewayId) { | ||
| 588 | + return deviceMapper.selectListByGatewayId(gatewayId); | ||
| 589 | + } | ||
| 590 | + | ||
| 591 | + @Override | ||
| 592 | + public Long getDeviceCountByGatewayId(Long gatewayId) { | ||
| 593 | + return deviceMapper.selectCountByGatewayId(gatewayId); | ||
| 594 | + } | ||
| 595 | + | ||
| 517 | private IotDeviceServiceImpl getSelf() { | 596 | private IotDeviceServiceImpl getSelf() { |
| 518 | return SpringUtil.getBean(getClass()); | 597 | return SpringUtil.getBean(getClass()); |
| 519 | } | 598 | } |