diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceController.java index c7f4fe4b40..109f4b2787 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceController.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceController.java @@ -51,6 +51,7 @@ public class IotDeviceController { return success(deviceService.createDevice(createReqVO)); } + @PutMapping("/update") @Operation(summary = "更新设备") @PreAuthorize("@ss.hasPermission('iot:device:update')") @@ -59,7 +60,72 @@ public class IotDeviceController { return success(true); } - // TODO @芋艿:参考阿里云:1)绑定网关;2)解绑网关 + @PutMapping("/bind-gateway") + @Operation(summary = "绑定子设备到网关") + @PreAuthorize("@ss.hasPermission('iot:device:update')") + public CommonResult bindDeviceGateway(@Valid @RequestBody IotDeviceBindGatewayReqVO reqVO) { + deviceService.bindDeviceGateway(reqVO.getIds(), reqVO.getGatewayId()); + return success(true); + } + + @PutMapping("/unbind-gateway") + @Operation(summary = "解绑子设备与网关") + @PreAuthorize("@ss.hasPermission('iot:device:update')") + public CommonResult unbindDeviceGateway(@Valid @RequestBody IotDeviceUnbindGatewayReqVO reqVO) { + deviceService.unbindDeviceGateway(reqVO.getIds()); + return success(true); + } + + @GetMapping("/sub-device-list") + @Operation(summary = "获取网关的子设备列表") + @Parameter(name = "gatewayId", description = "网关设备编号", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('iot:device:query')") + public CommonResult> getSubDeviceList(@RequestParam("gatewayId") Long gatewayId) { + List list = deviceService.getDeviceListByGatewayId(gatewayId); + if (CollUtil.isEmpty(list)) { + return success(Collections.emptyList()); + } + + // 补充产品名称 + Map productMap = convertMap(productService.getProductList(), IotProductDO::getId); + return success(convertList(list, device -> { + IotDeviceRespVO respVO = BeanUtils.toBean(device, IotDeviceRespVO.class); + MapUtils.findAndThen(productMap, device.getProductId(), + product -> respVO.setProductName(product.getName())); + return respVO; + })); + } + + // TODO @AI:希望改成“未绑定的”。需要剔除已经绑定,包括自己的; + // TODO @AI:不需要传递 gatewayId; + // TODO @AI:需要分页; + @GetMapping("/bindable-sub-device-list") + @Operation(summary = "获取可绑定到网关的子设备列表") + @Parameter(name = "gatewayId", description = "网关设备编号(可选)", example = "1") + @PreAuthorize("@ss.hasPermission('iot:device:query')") + public CommonResult> getBindableSubDeviceList( + @RequestParam(value = "gatewayId", required = false) Long gatewayId) { + List list = deviceService.getBindableSubDeviceList(gatewayId); + if (CollUtil.isEmpty(list)) { + return success(Collections.emptyList()); + } + + // 补充产品名称 + Map productMap = convertMap(productService.getProductList(), IotProductDO::getId); + return success(convertList(list, device -> { + // TODO @AI:可以 beanutils 转换么? + IotDeviceRespVO respVO = new IotDeviceRespVO() + .setId(device.getId()) + .setDeviceName(device.getDeviceName()) + .setNickname(device.getNickname()) + .setProductId(device.getProductId()) + .setState(device.getState()) + .setGatewayId(device.getGatewayId()); + MapUtils.findAndThen(productMap, device.getProductId(), + product -> respVO.setProductName(product.getName())); + return respVO; + })); + } @PutMapping("/update-group") @Operation(summary = "更新设备分组") diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceBindGatewayReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceBindGatewayReqVO.java new file mode 100644 index 0000000000..be122d8730 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceBindGatewayReqVO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.Set; + +@Schema(description = "管理后台 - IoT 设备绑定网关 Request VO") +@Data +public class IotDeviceBindGatewayReqVO { + + @Schema(description = "子设备编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3") + @NotEmpty(message = "子设备编号列表不能为空") + private Set ids; + + @Schema(description = "网关设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @NotNull(message = "网关设备编号不能为空") + private Long gatewayId; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceUnbindGatewayReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceUnbindGatewayReqVO.java new file mode 100644 index 0000000000..64215f3f6b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceUnbindGatewayReqVO.java @@ -0,0 +1,17 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.util.Set; + +@Schema(description = "管理后台 - IoT 设备解绑网关 Request VO") +@Data +public class IotDeviceUnbindGatewayReqVO { + + @Schema(description = "子设备编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3") + @NotEmpty(message = "子设备编号列表不能为空") + private Set ids; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceMapper.java index bc76afe1f0..da1f5dd9eb 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceMapper.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceMapper.java @@ -129,4 +129,33 @@ public interface IotDeviceMapper extends BaseMapperX { .isNotNull(IotDeviceDO::getLongitude)); } + // ========== 网关-子设备绑定相关 ========== + + /** + * 根据网关编号查询子设备列表 + * + * @param gatewayId 网关设备编号 + * @return 子设备列表 + */ + default List selectListByGatewayId(Long gatewayId) { + return selectList(IotDeviceDO::getGatewayId, gatewayId); + } + + /** + * 查询可绑定到网关的子设备列表 + *

+ * 条件:设备类型为 GATEWAY_SUB 且未绑定任何网关,或已绑定到指定网关 + * + * @param gatewayId 网关设备编号(可选,用于包含已绑定到该网关的设备) + * @return 子设备列表 + */ + default List selectBindableSubDeviceList(@Nullable Long gatewayId) { + return selectList(new LambdaQueryWrapperX() + .eq(IotDeviceDO::getDeviceType, cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum.GATEWAY_SUB.getType()) + .and(wrapper -> wrapper + .isNull(IotDeviceDO::getGatewayId))); +// .or() +// .eqIfPresent(IotDeviceDO::getGatewayId, gatewayId))) + } + } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java index 025d61390e..f94ce9af0d 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java @@ -33,6 +33,12 @@ public interface ErrorCodeConstants { ErrorCode DEVICE_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_050_003_006, "导入设备数据不能为空!"); ErrorCode DEVICE_DOWNSTREAM_FAILED_SERVER_ID_NULL = new ErrorCode(1_050_003_007, "下行设备消息失败,原因:设备未连接网关"); ErrorCode DEVICE_SERIAL_NUMBER_EXISTS = new ErrorCode(1_050_003_008, "设备序列号已存在,序列号必须全局唯一"); + // TODO @AI:1_050_003_009 需要提示具体的哪个设备。产品/设备,标识下 + ErrorCode DEVICE_NOT_GATEWAY_SUB = new ErrorCode(1_050_003_009, "设备不是网关子设备类型,无法绑定到网关"); + // TODO @AI:1_050_003_009 需要提示具体的哪个设备。产品/设备,标识下 + ErrorCode DEVICE_GATEWAY_BINDTO_EXISTS = new ErrorCode(1_050_003_010, "设备已绑定到其他网关,请先解绑"); + // TODO @AI:是不是可以删除,DEVICE_GATEWAY_BINDTO_NOT_EXISTS + ErrorCode DEVICE_GATEWAY_BINDTO_NOT_EXISTS = new ErrorCode(1_050_003_011, "设备未绑定到任何网关"); // ========== 产品分类 1-050-004-000 ========== ErrorCode PRODUCT_CATEGORY_NOT_EXISTS = new ErrorCode(1_050_004_000, "产品分类不存在"); diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java index 3664e96e16..84bdf8e09e 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java @@ -73,6 +73,7 @@ public interface IotDeviceService { */ void updateDeviceGroup(@Valid IotDeviceUpdateGroupReqVO updateReqVO); + // TODO @AI:网关设备被删除时,需要查看是否有子设备绑定。如果有,则不允许删除。 /** * 删除单个设备 * @@ -288,4 +289,48 @@ public interface IotDeviceService { */ List getDeviceListByHasLocation(); + // ========== 网关-子设备绑定相关 ========== + + /** + * 绑定子设备到网关 + * + * @param ids 子设备编号列表 + * @param gatewayId 网关设备编号 + */ + void bindDeviceGateway(Collection ids, Long gatewayId); + + /** + * 解绑子设备与网关 + * + * @param ids 子设备编号列表 + */ + void unbindDeviceGateway(Collection ids); + + /** + * 获取可绑定到网关的子设备列表 + *

+ * 条件:设备类型为 GATEWAY_SUB 且未绑定任何网关 + * + * @param gatewayId 网关设备编号(可选,用于包含已绑定到该网关的设备) + * @return 子设备列表 + */ + List getBindableSubDeviceList(@Nullable Long gatewayId); + + /** + * 根据网关编号获取子设备列表 + * + * @param gatewayId 网关设备编号 + * @return 子设备列表 + */ + List getDeviceListByGatewayId(Long gatewayId); + + // TODO @AI:暂时用不到,可以删除。 + /** + * 根据网关编号获取子设备数量 + * + * @param gatewayId 网关设备编号 + * @return 子设备数量 + */ + Long getDeviceCountByGatewayId(Long gatewayId); + } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java index 532b254a1b..f5b0784806 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java @@ -514,6 +514,85 @@ public class IotDeviceServiceImpl implements IotDeviceService { return deviceMapper.selectListByHasLocation(); } + // ========== 网关-子设备绑定相关 ========== + + @Override + @Transactional(rollbackFor = Exception.class) + public void bindDeviceGateway(Collection ids, Long gatewayId) { + if (CollUtil.isEmpty(ids)) { + return; + } + // TODO @AI:校验应该是 1.1、1.2 统一风格; + // 1. 校验网关设备存在且类型正确 + validateGatewayDeviceExists(gatewayId); + + // 2. 校验并绑定每个子设备 + List devices = deviceMapper.selectByIds(ids); + if (devices.size() != ids.size()) { + throw exception(DEVICE_NOT_EXISTS); + } + + List updateList = new ArrayList<>(); + for (IotDeviceDO device : devices) { + // 2.1 校验是否为子设备类型 + if (!IotProductDeviceTypeEnum.isGatewaySub(device.getDeviceType())) { + throw exception(DEVICE_NOT_GATEWAY_SUB); + } + // 2.2 校验是否已绑定其他网关 + if (device.getGatewayId() != null && !device.getGatewayId().equals(gatewayId)) { + throw exception(DEVICE_GATEWAY_BINDTO_EXISTS); + } + updateList.add(new IotDeviceDO().setId(device.getId()).setGatewayId(gatewayId)); + } + + // 3. 批量更新数据库 + // TODO @AI:List updateList 直接 convertList,不用上面 for 里面搞;校验是校验,插入是插入; + deviceMapper.updateBatch(updateList); + + // 4. 清空对应缓存 + deleteDeviceCache(devices); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void unbindDeviceGateway(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return; + } + // 1. 校验设备存在 + List devices = deviceMapper.selectByIds(ids); + if (devices.size() != ids.size()) { + throw exception(DEVICE_NOT_EXISTS); + } + + // 2. 批量更新数据库(将 gatewayId 设置为 null) + List updateList = devices.stream() + .filter(device -> device.getGatewayId() != null) + .map(device -> new IotDeviceDO().setId(device.getId()).setGatewayId(null)) + .toList(); + if (CollUtil.isNotEmpty(updateList)) { + deviceMapper.updateBatch(updateList); + } + + // 3. 清空对应缓存 + deleteDeviceCache(devices); + } + + @Override + public List getBindableSubDeviceList(@Nullable Long gatewayId) { + return deviceMapper.selectBindableSubDeviceList(gatewayId); + } + + @Override + public List getDeviceListByGatewayId(Long gatewayId) { + return deviceMapper.selectListByGatewayId(gatewayId); + } + + @Override + public Long getDeviceCountByGatewayId(Long gatewayId) { + return deviceMapper.selectCountByGatewayId(gatewayId); + } + private IotDeviceServiceImpl getSelf() { return SpringUtil.getBean(getClass()); }