Commit 4b1dfad063fa8fafa9e8866f4f90f78f2fce69f4

Authored by YunaiV
1 parent 45638b35

feat(iot):【网关设备:20%】增加网关设备绑定能力(未完成),基于 breezy-doodling-starlight.md 规划

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&lt;IotDeviceDO&gt; { @@ -129,4 +129,33 @@ public interface IotDeviceMapper extends BaseMapperX&lt;IotDeviceDO&gt; {
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 }