diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java index fe55d9bf51..7711ae0d88 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java @@ -262,4 +262,20 @@ public class JsonUtils { return objectMapper.convertValue(obj, typeReference); } + /** + * 将 Object 转换为 List 类型 + *

+ * 避免先转 jsonString 再 parseArray 的性能损耗 + * + * @param obj 源对象(可以是 List、数组等) + * @param clazz 目标元素类型 + * @return 转换后的 List + */ + public static List convertList(Object obj, Class clazz) { + if (obj == null) { + return new ArrayList<>(); + } + return objectMapper.convertValue(obj, objectMapper.getTypeFactory().constructCollectionType(List.class, clazz)); + } + } 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 cdc25d803c..deb8cf38b0 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 @@ -72,7 +72,7 @@ public class IotDeviceController { @Operation(summary = "解绑子设备与网关") @PreAuthorize("@ss.hasPermission('iot:device:update')") public CommonResult unbindDeviceGateway(@Valid @RequestBody IotDeviceUnbindGatewayReqVO reqVO) { - deviceService.unbindDeviceGateway(reqVO.getIds()); + deviceService.unbindDeviceGateway(reqVO.getIds(), reqVO.getGatewayId()); return success(true); } 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 index 64215f3f6b..e198193722 100644 --- 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 @@ -2,6 +2,7 @@ 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; @@ -14,4 +15,8 @@ public class IotDeviceUnbindGatewayReqVO { @NotEmpty(message = "子设备编号列表不能为空") private Set ids; + @Schema(description = "网关设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "网关设备编号不能为空") + private Long gatewayId; + } 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 c61acf960c..1e3fb2e576 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 @@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.IotDevicePa import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import jakarta.annotation.Nullable; import org.apache.ibatis.annotations.Mapper; @@ -159,4 +160,16 @@ public interface IotDeviceMapper extends BaseMapperX { .orderByDesc(IotDeviceDO::getId)); } + /** + * 批量更新设备的网关编号 + * + * @param ids 设备编号列表 + * @param gatewayId 网关设备编号(可以为 null,表示解绑) + */ + default void updateGatewayIdBatch(Collection ids, Long gatewayId) { + update(null, new LambdaUpdateWrapper() + .set(IotDeviceDO::getGatewayId, gatewayId) + .in(IotDeviceDO::getId, ids)); + } + } 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 d69195a98c..e69ebc2da7 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 @@ -305,9 +305,10 @@ public interface IotDeviceService { /** * 解绑子设备与网关 * - * @param ids 子设备编号列表 + * @param ids 子设备编号列表 + * @param gatewayId 网关设备编号 */ - void unbindDeviceGateway(Collection ids); + void unbindDeviceGateway(Collection ids, 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 4104a30617..3b5efb16ff 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 @@ -14,12 +14,14 @@ import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO; import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO; import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO; +import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoChangeReqDTO; import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO; import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetRespDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; @@ -29,6 +31,7 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceMapper; import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants; import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum; +import cn.iocoder.yudao.module.iot.service.device.message.IotDeviceMessageService; import cn.iocoder.yudao.module.iot.service.product.IotProductService; import jakarta.annotation.Resource; import jakarta.validation.ConstraintViolationException; @@ -49,6 +52,7 @@ import java.util.*; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; +import static java.util.Collections.singletonList; /** * IoT 设备 Service 实现类 @@ -69,6 +73,9 @@ public class IotDeviceServiceImpl implements IotDeviceService { @Resource @Lazy // 延迟加载,解决循环依赖 private IotDeviceGroupService deviceGroupService; + @Resource + @Lazy // 延迟加载,解决循环依赖 + private IotDeviceMessageService deviceMessageService; @Override public Long createDevice(IotDeviceSaveReqVO createReqVO) { @@ -590,7 +597,8 @@ public class IotDeviceServiceImpl implements IotDeviceService { // 3. 清空对应缓存 deleteDeviceCache(devices); - // TODO @AI:需要下发网关设备,让其建立拓扑关系吗?(增加) + // 4. 下发网关设备拓扑变更通知(增加) + sendTopoChangeNotify(gatewayId, IotDeviceTopoChangeReqDTO.STATUS_CREATE, devices); } private void checkSubDeviceCanBind(IotDeviceDO device, Long gatewayId) { @@ -604,30 +612,25 @@ public class IotDeviceServiceImpl implements IotDeviceService { @Override @Transactional(rollbackFor = Exception.class) - public void unbindDeviceGateway(Collection ids) { + public void unbindDeviceGateway(Collection ids, Long gatewayId) { // 1. 校验设备存在 if (CollUtil.isEmpty(ids)) { return; } List devices = deviceMapper.selectByIds(ids); - if (CollUtil.isNotEmpty(devices)) { + devices.removeIf(device -> device.getGatewayId() == null); + if (CollUtil.isEmpty(devices)) { return; } // 2. 批量更新数据库(将 gatewayId 设置为 null) - // TODO @AI:需要搞个方法,专门批量更新某个字段为 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); - } + deviceMapper.updateGatewayIdBatch(convertList(devices, IotDeviceDO::getId), null); // 3. 清空对应缓存 deleteDeviceCache(devices); - // TODO @AI:需要下发网关设备,让其建立拓扑关系吗?(减少) + // 4. 下发网关设备拓扑变更通知(删除) + sendTopoChangeNotify(gatewayId, IotDeviceTopoChangeReqDTO.STATUS_DELETE, devices); } @Override @@ -742,9 +745,8 @@ public class IotDeviceServiceImpl implements IotDeviceService { subDeviceIdentity.getProductKey(), subDeviceIdentity.getDeviceName()); } - // 2. 更新数据库 - // TODO @AI:直接调用更新方法; -// unbindDeviceGateway(Collections.singletonList(subDevice.getId())); + // 2. 更新数据库(将 gatewayId 设置为 null) + deviceMapper.updateGatewayIdBatch(singletonList(subDevice.getId()), null); log.info("[deleteDeviceTopo][网关({}/{}) 解绑子设备({}/{})]", gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(), subDevice.getProductKey(), subDevice.getDeviceName()); @@ -782,8 +784,7 @@ public class IotDeviceServiceImpl implements IotDeviceService { if (!(message.getParams() instanceof List)) { throw exception(DEVICE_SUB_REGISTER_PARAMS_INVALID); } - // TODO @AI:这个要不也弄到 JsonUtils 里面去?感觉类似 convertObject 呀。 - List paramsList = JsonUtils.parseArray(JsonUtils.toJsonString(message.getParams()), + List paramsList = JsonUtils.convertList(message.getParams(), IotSubDeviceRegisterReqDTO.class); if (CollUtil.isEmpty(paramsList)) { throw exception(DEVICE_SUB_REGISTER_PARAMS_INVALID); @@ -817,9 +818,13 @@ public class IotDeviceServiceImpl implements IotDeviceService { throw exception(DEVICE_SUB_REGISTER_PRODUCT_NOT_GATEWAY_SUB, params.getProductKey()); } // 1.3 查找设备是否已存在 - // TODO @AI:存在的时候,必须父设备是自己,才返回,否则抛出业务异常; IotDeviceDO existDevice = getSelf().getDeviceFromCache(params.getProductKey(), params.getDeviceName()); if (existDevice != null) { + // 校验是否绑定到当前网关 + if (ObjUtil.notEqual(existDevice.getGatewayId(), gatewayDevice.getId())) { + throw exception(DEVICE_GATEWAY_BINDTO_EXISTS, + existDevice.getProductKey(), existDevice.getDeviceName()); + } // 已存在则返回设备信息 return existDevice; } @@ -840,4 +845,42 @@ public class IotDeviceServiceImpl implements IotDeviceService { return SpringUtil.getBean(getClass()); } + /** + * 发送拓扑变更通知给网关设备 + * + * @param gatewayId 网关设备编号 + * @param status 变更状态(0-创建, 1-删除) + * @param subDevices 子设备列表 + * @see 阿里云 - 通知网关拓扑关系变化 + */ + private void sendTopoChangeNotify(Long gatewayId, Integer status, List subDevices) { + if (CollUtil.isEmpty(subDevices)) { + return; + } + // 1. 获取网关设备 + IotDeviceDO gatewayDevice = deviceMapper.selectById(gatewayId); + if (gatewayDevice == null) { + log.warn("[sendTopoChangeNotify][网关设备({}) 不存在,无法发送拓扑变更通知]", gatewayId); + return; + } + + try { + // 2.1 构建拓扑变更通知消息 + List subList = convertList(subDevices, subDevice -> + new IotDeviceIdentity(subDevice.getProductKey(), subDevice.getDeviceName())); + IotDeviceTopoChangeReqDTO params = new IotDeviceTopoChangeReqDTO(status, subList); + IotDeviceMessage notifyMessage = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.TOPO_CHANGE.getMethod(), params); + + // 2.2 发送消息 + deviceMessageService.sendDeviceMessage(notifyMessage, gatewayDevice); + log.info("[sendTopoChangeNotify][网关({}/{}) 发送拓扑变更通知成功,status={}, subDevices={}]", + gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(), + status, subList); + } catch (Exception ex) { + log.error("[sendTopoChangeNotify][网关({}/{}) 发送拓扑变更通知失败,status={}]", + gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(), status, ex); + } + } + }