Commit 05eb745f2a7e84d2cbb0d51991b97ea064e4be04
1 parent
bfbc352a
feat(iot):【网关设备:67%】整体初步实现(优化部分代码)
Showing
6 changed files
with
99 additions
and
21 deletions
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java
| ... | ... | @@ -262,4 +262,20 @@ public class JsonUtils { |
| 262 | 262 | return objectMapper.convertValue(obj, typeReference); |
| 263 | 263 | } |
| 264 | 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 | + | |
| 265 | 281 | } | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceController.java
| ... | ... | @@ -72,7 +72,7 @@ public class IotDeviceController { |
| 72 | 72 | @Operation(summary = "解绑子设备与网关") |
| 73 | 73 | @PreAuthorize("@ss.hasPermission('iot:device:update')") |
| 74 | 74 | public CommonResult<Boolean> unbindDeviceGateway(@Valid @RequestBody IotDeviceUnbindGatewayReqVO reqVO) { |
| 75 | - deviceService.unbindDeviceGateway(reqVO.getIds()); | |
| 75 | + deviceService.unbindDeviceGateway(reqVO.getIds(), reqVO.getGatewayId()); | |
| 76 | 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/IotDeviceUnbindGatewayReqVO.java
| ... | ... | @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device; |
| 2 | 2 | |
| 3 | 3 | import io.swagger.v3.oas.annotations.media.Schema; |
| 4 | 4 | import jakarta.validation.constraints.NotEmpty; |
| 5 | +import jakarta.validation.constraints.NotNull; | |
| 5 | 6 | import lombok.Data; |
| 6 | 7 | |
| 7 | 8 | import java.util.Set; |
| ... | ... | @@ -14,4 +15,8 @@ public class IotDeviceUnbindGatewayReqVO { |
| 14 | 15 | @NotEmpty(message = "子设备编号列表不能为空") |
| 15 | 16 | private Set<Long> ids; |
| 16 | 17 | |
| 18 | + @Schema(description = "网关设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") | |
| 19 | + @NotNull(message = "网关设备编号不能为空") | |
| 20 | + private Long gatewayId; | |
| 21 | + | |
| 17 | 22 | } | ... | ... |
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 | 8 | import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; |
| 9 | 9 | import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum; |
| 10 | 10 | import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; |
| 11 | +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; | |
| 11 | 12 | import jakarta.annotation.Nullable; |
| 12 | 13 | import org.apache.ibatis.annotations.Mapper; |
| 13 | 14 | |
| ... | ... | @@ -159,4 +160,16 @@ public interface IotDeviceMapper extends BaseMapperX<IotDeviceDO> { |
| 159 | 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/service/device/IotDeviceService.java
| ... | ... | @@ -305,9 +305,10 @@ public interface IotDeviceService { |
| 305 | 305 | /** |
| 306 | 306 | * 解绑子设备与网关 |
| 307 | 307 | * |
| 308 | - * @param ids 子设备编号列表 | |
| 308 | + * @param ids 子设备编号列表 | |
| 309 | + * @param gatewayId 网关设备编号 | |
| 309 | 310 | */ |
| 310 | - void unbindDeviceGateway(Collection<Long> ids); | |
| 311 | + void unbindDeviceGateway(Collection<Long> ids, Long gatewayId); | |
| 311 | 312 | |
| 312 | 313 | /** |
| 313 | 314 | * 获取未绑定网关的子设备分页 | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java
| ... | ... | @@ -14,12 +14,14 @@ import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; |
| 14 | 14 | import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; |
| 15 | 15 | import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*; |
| 16 | 16 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; |
| 17 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | |
| 17 | 18 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; |
| 18 | 19 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 19 | 20 | import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; |
| 20 | 21 | import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO; |
| 21 | 22 | import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO; |
| 22 | 23 | import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO; |
| 24 | +import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoChangeReqDTO; | |
| 23 | 25 | import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO; |
| 24 | 26 | import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetRespDTO; |
| 25 | 27 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; |
| ... | ... | @@ -29,6 +31,7 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; |
| 29 | 31 | import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceMapper; |
| 30 | 32 | import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants; |
| 31 | 33 | import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum; |
| 34 | +import cn.iocoder.yudao.module.iot.service.device.message.IotDeviceMessageService; | |
| 32 | 35 | import cn.iocoder.yudao.module.iot.service.product.IotProductService; |
| 33 | 36 | import jakarta.annotation.Resource; |
| 34 | 37 | import jakarta.validation.ConstraintViolationException; |
| ... | ... | @@ -49,6 +52,7 @@ import java.util.*; |
| 49 | 52 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| 50 | 53 | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; |
| 51 | 54 | import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; |
| 55 | +import static java.util.Collections.singletonList; | |
| 52 | 56 | |
| 53 | 57 | /** |
| 54 | 58 | * IoT 设备 Service 实现类 |
| ... | ... | @@ -69,6 +73,9 @@ public class IotDeviceServiceImpl implements IotDeviceService { |
| 69 | 73 | @Resource |
| 70 | 74 | @Lazy // 延迟加载,解决循环依赖 |
| 71 | 75 | private IotDeviceGroupService deviceGroupService; |
| 76 | + @Resource | |
| 77 | + @Lazy // 延迟加载,解决循环依赖 | |
| 78 | + private IotDeviceMessageService deviceMessageService; | |
| 72 | 79 | |
| 73 | 80 | @Override |
| 74 | 81 | public Long createDevice(IotDeviceSaveReqVO createReqVO) { |
| ... | ... | @@ -590,7 +597,8 @@ public class IotDeviceServiceImpl implements IotDeviceService { |
| 590 | 597 | // 3. 清空对应缓存 |
| 591 | 598 | deleteDeviceCache(devices); |
| 592 | 599 | |
| 593 | - // TODO @AI:需要下发网关设备,让其建立拓扑关系吗?(增加) | |
| 600 | + // 4. 下发网关设备拓扑变更通知(增加) | |
| 601 | + sendTopoChangeNotify(gatewayId, IotDeviceTopoChangeReqDTO.STATUS_CREATE, devices); | |
| 594 | 602 | } |
| 595 | 603 | |
| 596 | 604 | private void checkSubDeviceCanBind(IotDeviceDO device, Long gatewayId) { |
| ... | ... | @@ -604,30 +612,25 @@ public class IotDeviceServiceImpl implements IotDeviceService { |
| 604 | 612 | |
| 605 | 613 | @Override |
| 606 | 614 | @Transactional(rollbackFor = Exception.class) |
| 607 | - public void unbindDeviceGateway(Collection<Long> ids) { | |
| 615 | + public void unbindDeviceGateway(Collection<Long> ids, Long gatewayId) { | |
| 608 | 616 | // 1. 校验设备存在 |
| 609 | 617 | if (CollUtil.isEmpty(ids)) { |
| 610 | 618 | return; |
| 611 | 619 | } |
| 612 | 620 | List<IotDeviceDO> devices = deviceMapper.selectByIds(ids); |
| 613 | - if (CollUtil.isNotEmpty(devices)) { | |
| 621 | + devices.removeIf(device -> device.getGatewayId() == null); | |
| 622 | + if (CollUtil.isEmpty(devices)) { | |
| 614 | 623 | return; |
| 615 | 624 | } |
| 616 | 625 | |
| 617 | 626 | // 2. 批量更新数据库(将 gatewayId 设置为 null) |
| 618 | - // TODO @AI:需要搞个方法,专门批量更新某个字段为 null。 | |
| 619 | - List<IotDeviceDO> updateList = devices.stream() | |
| 620 | - .filter(device -> device.getGatewayId() != null) | |
| 621 | - .map(device -> new IotDeviceDO().setId(device.getId()).setGatewayId(null)) | |
| 622 | - .toList(); | |
| 623 | - if (CollUtil.isNotEmpty(updateList)) { | |
| 624 | - deviceMapper.updateBatch(updateList); | |
| 625 | - } | |
| 627 | + deviceMapper.updateGatewayIdBatch(convertList(devices, IotDeviceDO::getId), null); | |
| 626 | 628 | |
| 627 | 629 | // 3. 清空对应缓存 |
| 628 | 630 | deleteDeviceCache(devices); |
| 629 | 631 | |
| 630 | - // TODO @AI:需要下发网关设备,让其建立拓扑关系吗?(减少) | |
| 632 | + // 4. 下发网关设备拓扑变更通知(删除) | |
| 633 | + sendTopoChangeNotify(gatewayId, IotDeviceTopoChangeReqDTO.STATUS_DELETE, devices); | |
| 631 | 634 | } |
| 632 | 635 | |
| 633 | 636 | @Override |
| ... | ... | @@ -742,9 +745,8 @@ public class IotDeviceServiceImpl implements IotDeviceService { |
| 742 | 745 | subDeviceIdentity.getProductKey(), subDeviceIdentity.getDeviceName()); |
| 743 | 746 | } |
| 744 | 747 | |
| 745 | - // 2. 更新数据库 | |
| 746 | - // TODO @AI:直接调用更新方法; | |
| 747 | -// unbindDeviceGateway(Collections.singletonList(subDevice.getId())); | |
| 748 | + // 2. 更新数据库(将 gatewayId 设置为 null) | |
| 749 | + deviceMapper.updateGatewayIdBatch(singletonList(subDevice.getId()), null); | |
| 748 | 750 | log.info("[deleteDeviceTopo][网关({}/{}) 解绑子设备({}/{})]", |
| 749 | 751 | gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(), |
| 750 | 752 | subDevice.getProductKey(), subDevice.getDeviceName()); |
| ... | ... | @@ -782,8 +784,7 @@ public class IotDeviceServiceImpl implements IotDeviceService { |
| 782 | 784 | if (!(message.getParams() instanceof List)) { |
| 783 | 785 | throw exception(DEVICE_SUB_REGISTER_PARAMS_INVALID); |
| 784 | 786 | } |
| 785 | - // TODO @AI:这个要不也弄到 JsonUtils 里面去?感觉类似 convertObject 呀。 | |
| 786 | - List<IotSubDeviceRegisterReqDTO> paramsList = JsonUtils.parseArray(JsonUtils.toJsonString(message.getParams()), | |
| 787 | + List<IotSubDeviceRegisterReqDTO> paramsList = JsonUtils.convertList(message.getParams(), | |
| 787 | 788 | IotSubDeviceRegisterReqDTO.class); |
| 788 | 789 | if (CollUtil.isEmpty(paramsList)) { |
| 789 | 790 | throw exception(DEVICE_SUB_REGISTER_PARAMS_INVALID); |
| ... | ... | @@ -817,9 +818,13 @@ public class IotDeviceServiceImpl implements IotDeviceService { |
| 817 | 818 | throw exception(DEVICE_SUB_REGISTER_PRODUCT_NOT_GATEWAY_SUB, params.getProductKey()); |
| 818 | 819 | } |
| 819 | 820 | // 1.3 查找设备是否已存在 |
| 820 | - // TODO @AI:存在的时候,必须父设备是自己,才返回,否则抛出业务异常; | |
| 821 | 821 | IotDeviceDO existDevice = getSelf().getDeviceFromCache(params.getProductKey(), params.getDeviceName()); |
| 822 | 822 | if (existDevice != null) { |
| 823 | + // 校验是否绑定到当前网关 | |
| 824 | + if (ObjUtil.notEqual(existDevice.getGatewayId(), gatewayDevice.getId())) { | |
| 825 | + throw exception(DEVICE_GATEWAY_BINDTO_EXISTS, | |
| 826 | + existDevice.getProductKey(), existDevice.getDeviceName()); | |
| 827 | + } | |
| 823 | 828 | // 已存在则返回设备信息 |
| 824 | 829 | return existDevice; |
| 825 | 830 | } |
| ... | ... | @@ -840,4 +845,42 @@ public class IotDeviceServiceImpl implements IotDeviceService { |
| 840 | 845 | return SpringUtil.getBean(getClass()); |
| 841 | 846 | } |
| 842 | 847 | |
| 848 | + /** | |
| 849 | + * 发送拓扑变更通知给网关设备 | |
| 850 | + * | |
| 851 | + * @param gatewayId 网关设备编号 | |
| 852 | + * @param status 变更状态(0-创建, 1-删除) | |
| 853 | + * @param subDevices 子设备列表 | |
| 854 | + * @see <a href="https://help.aliyun.com/zh/marketplace/notify-gateway-topology-changes">阿里云 - 通知网关拓扑关系变化</a> | |
| 855 | + */ | |
| 856 | + private void sendTopoChangeNotify(Long gatewayId, Integer status, List<IotDeviceDO> subDevices) { | |
| 857 | + if (CollUtil.isEmpty(subDevices)) { | |
| 858 | + return; | |
| 859 | + } | |
| 860 | + // 1. 获取网关设备 | |
| 861 | + IotDeviceDO gatewayDevice = deviceMapper.selectById(gatewayId); | |
| 862 | + if (gatewayDevice == null) { | |
| 863 | + log.warn("[sendTopoChangeNotify][网关设备({}) 不存在,无法发送拓扑变更通知]", gatewayId); | |
| 864 | + return; | |
| 865 | + } | |
| 866 | + | |
| 867 | + try { | |
| 868 | + // 2.1 构建拓扑变更通知消息 | |
| 869 | + List<IotDeviceIdentity> subList = convertList(subDevices, subDevice -> | |
| 870 | + new IotDeviceIdentity(subDevice.getProductKey(), subDevice.getDeviceName())); | |
| 871 | + IotDeviceTopoChangeReqDTO params = new IotDeviceTopoChangeReqDTO(status, subList); | |
| 872 | + IotDeviceMessage notifyMessage = IotDeviceMessage.requestOf( | |
| 873 | + IotDeviceMessageMethodEnum.TOPO_CHANGE.getMethod(), params); | |
| 874 | + | |
| 875 | + // 2.2 发送消息 | |
| 876 | + deviceMessageService.sendDeviceMessage(notifyMessage, gatewayDevice); | |
| 877 | + log.info("[sendTopoChangeNotify][网关({}/{}) 发送拓扑变更通知成功,status={}, subDevices={}]", | |
| 878 | + gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(), | |
| 879 | + status, subList); | |
| 880 | + } catch (Exception ex) { | |
| 881 | + log.error("[sendTopoChangeNotify][网关({}/{}) 发送拓扑变更通知失败,status={}]", | |
| 882 | + gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(), status, ex); | |
| 883 | + } | |
| 884 | + } | |
| 885 | + | |
| 843 | 886 | } | ... | ... |