Commit 05eb745f2a7e84d2cbb0d51991b97ea064e4be04

Authored by YunaiV
1 parent bfbc352a

feat(iot):【网关设备:67%】整体初步实现(优化部分代码)

yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java
@@ -262,4 +262,20 @@ public class JsonUtils { @@ -262,4 +262,20 @@ public class JsonUtils {
262 return objectMapper.convertValue(obj, typeReference); 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,7 +72,7 @@ public class IotDeviceController {
72 @Operation(summary = "解绑子设备与网关") 72 @Operation(summary = "解绑子设备与网关")
73 @PreAuthorize("@ss.hasPermission('iot:device:update')") 73 @PreAuthorize("@ss.hasPermission('iot:device:update')")
74 public CommonResult<Boolean> unbindDeviceGateway(@Valid @RequestBody IotDeviceUnbindGatewayReqVO reqVO) { 74 public CommonResult<Boolean> unbindDeviceGateway(@Valid @RequestBody IotDeviceUnbindGatewayReqVO reqVO) {
75 - deviceService.unbindDeviceGateway(reqVO.getIds()); 75 + deviceService.unbindDeviceGateway(reqVO.getIds(), reqVO.getGatewayId());
76 return success(true); 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,6 +2,7 @@ package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device;
2 2
3 import io.swagger.v3.oas.annotations.media.Schema; 3 import io.swagger.v3.oas.annotations.media.Schema;
4 import jakarta.validation.constraints.NotEmpty; 4 import jakarta.validation.constraints.NotEmpty;
  5 +import jakarta.validation.constraints.NotNull;
5 import lombok.Data; 6 import lombok.Data;
6 7
7 import java.util.Set; 8 import java.util.Set;
@@ -14,4 +15,8 @@ public class IotDeviceUnbindGatewayReqVO { @@ -14,4 +15,8 @@ public class IotDeviceUnbindGatewayReqVO {
14 @NotEmpty(message = "子设备编号列表不能为空") 15 @NotEmpty(message = "子设备编号列表不能为空")
15 private Set<Long> ids; 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,6 +8,7 @@ import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.IotDevicePa
8 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; 8 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
9 import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum; 9 import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum;
10 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; 10 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
  11 +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
11 import jakarta.annotation.Nullable; 12 import jakarta.annotation.Nullable;
12 import org.apache.ibatis.annotations.Mapper; 13 import org.apache.ibatis.annotations.Mapper;
13 14
@@ -159,4 +160,16 @@ public interface IotDeviceMapper extends BaseMapperX&lt;IotDeviceDO&gt; { @@ -159,4 +160,16 @@ public interface IotDeviceMapper extends BaseMapperX&lt;IotDeviceDO&gt; {
159 .orderByDesc(IotDeviceDO::getId)); 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,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,12 +14,14 @@ import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
14 import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; 14 import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
15 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*; 15 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*;
16 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; 16 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
  17 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
17 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; 18 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
18 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 19 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
19 import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; 20 import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
20 import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO; 21 import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO;
21 import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO; 22 import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO;
22 import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO; 23 import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO;
  24 +import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoChangeReqDTO;
23 import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO; 25 import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO;
24 import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetRespDTO; 26 import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetRespDTO;
25 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; 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,6 +31,7 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
29 import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceMapper; 31 import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceMapper;
30 import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants; 32 import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants;
31 import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum; 33 import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum;
  34 +import cn.iocoder.yudao.module.iot.service.device.message.IotDeviceMessageService;
32 import cn.iocoder.yudao.module.iot.service.product.IotProductService; 35 import cn.iocoder.yudao.module.iot.service.product.IotProductService;
33 import jakarta.annotation.Resource; 36 import jakarta.annotation.Resource;
34 import jakarta.validation.ConstraintViolationException; 37 import jakarta.validation.ConstraintViolationException;
@@ -49,6 +52,7 @@ import java.util.*; @@ -49,6 +52,7 @@ import java.util.*;
49 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 52 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
50 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; 53 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
51 import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; 54 import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*;
  55 +import static java.util.Collections.singletonList;
52 56
53 /** 57 /**
54 * IoT 设备 Service 实现类 58 * IoT 设备 Service 实现类
@@ -69,6 +73,9 @@ public class IotDeviceServiceImpl implements IotDeviceService { @@ -69,6 +73,9 @@ public class IotDeviceServiceImpl implements IotDeviceService {
69 @Resource 73 @Resource
70 @Lazy // 延迟加载,解决循环依赖 74 @Lazy // 延迟加载,解决循环依赖
71 private IotDeviceGroupService deviceGroupService; 75 private IotDeviceGroupService deviceGroupService;
  76 + @Resource
  77 + @Lazy // 延迟加载,解决循环依赖
  78 + private IotDeviceMessageService deviceMessageService;
72 79
73 @Override 80 @Override
74 public Long createDevice(IotDeviceSaveReqVO createReqVO) { 81 public Long createDevice(IotDeviceSaveReqVO createReqVO) {
@@ -590,7 +597,8 @@ public class IotDeviceServiceImpl implements IotDeviceService { @@ -590,7 +597,8 @@ public class IotDeviceServiceImpl implements IotDeviceService {
590 // 3. 清空对应缓存 597 // 3. 清空对应缓存
591 deleteDeviceCache(devices); 598 deleteDeviceCache(devices);
592 599
593 - // TODO @AI:需要下发网关设备,让其建立拓扑关系吗?(增加) 600 + // 4. 下发网关设备拓扑变更通知(增加)
  601 + sendTopoChangeNotify(gatewayId, IotDeviceTopoChangeReqDTO.STATUS_CREATE, devices);
594 } 602 }
595 603
596 private void checkSubDeviceCanBind(IotDeviceDO device, Long gatewayId) { 604 private void checkSubDeviceCanBind(IotDeviceDO device, Long gatewayId) {
@@ -604,30 +612,25 @@ public class IotDeviceServiceImpl implements IotDeviceService { @@ -604,30 +612,25 @@ public class IotDeviceServiceImpl implements IotDeviceService {
604 612
605 @Override 613 @Override
606 @Transactional(rollbackFor = Exception.class) 614 @Transactional(rollbackFor = Exception.class)
607 - public void unbindDeviceGateway(Collection<Long> ids) { 615 + public void unbindDeviceGateway(Collection<Long> ids, Long gatewayId) {
608 // 1. 校验设备存在 616 // 1. 校验设备存在
609 if (CollUtil.isEmpty(ids)) { 617 if (CollUtil.isEmpty(ids)) {
610 return; 618 return;
611 } 619 }
612 List<IotDeviceDO> devices = deviceMapper.selectByIds(ids); 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 return; 623 return;
615 } 624 }
616 625
617 // 2. 批量更新数据库(将 gatewayId 设置为 null) 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 // 3. 清空对应缓存 629 // 3. 清空对应缓存
628 deleteDeviceCache(devices); 630 deleteDeviceCache(devices);
629 631
630 - // TODO @AI:需要下发网关设备,让其建立拓扑关系吗?(减少) 632 + // 4. 下发网关设备拓扑变更通知(删除)
  633 + sendTopoChangeNotify(gatewayId, IotDeviceTopoChangeReqDTO.STATUS_DELETE, devices);
631 } 634 }
632 635
633 @Override 636 @Override
@@ -742,9 +745,8 @@ public class IotDeviceServiceImpl implements IotDeviceService { @@ -742,9 +745,8 @@ public class IotDeviceServiceImpl implements IotDeviceService {
742 subDeviceIdentity.getProductKey(), subDeviceIdentity.getDeviceName()); 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 log.info("[deleteDeviceTopo][网关({}/{}) 解绑子设备({}/{})]", 750 log.info("[deleteDeviceTopo][网关({}/{}) 解绑子设备({}/{})]",
749 gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(), 751 gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(),
750 subDevice.getProductKey(), subDevice.getDeviceName()); 752 subDevice.getProductKey(), subDevice.getDeviceName());
@@ -782,8 +784,7 @@ public class IotDeviceServiceImpl implements IotDeviceService { @@ -782,8 +784,7 @@ public class IotDeviceServiceImpl implements IotDeviceService {
782 if (!(message.getParams() instanceof List)) { 784 if (!(message.getParams() instanceof List)) {
783 throw exception(DEVICE_SUB_REGISTER_PARAMS_INVALID); 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 IotSubDeviceRegisterReqDTO.class); 788 IotSubDeviceRegisterReqDTO.class);
788 if (CollUtil.isEmpty(paramsList)) { 789 if (CollUtil.isEmpty(paramsList)) {
789 throw exception(DEVICE_SUB_REGISTER_PARAMS_INVALID); 790 throw exception(DEVICE_SUB_REGISTER_PARAMS_INVALID);
@@ -817,9 +818,13 @@ public class IotDeviceServiceImpl implements IotDeviceService { @@ -817,9 +818,13 @@ public class IotDeviceServiceImpl implements IotDeviceService {
817 throw exception(DEVICE_SUB_REGISTER_PRODUCT_NOT_GATEWAY_SUB, params.getProductKey()); 818 throw exception(DEVICE_SUB_REGISTER_PRODUCT_NOT_GATEWAY_SUB, params.getProductKey());
818 } 819 }
819 // 1.3 查找设备是否已存在 820 // 1.3 查找设备是否已存在
820 - // TODO @AI:存在的时候,必须父设备是自己,才返回,否则抛出业务异常;  
821 IotDeviceDO existDevice = getSelf().getDeviceFromCache(params.getProductKey(), params.getDeviceName()); 821 IotDeviceDO existDevice = getSelf().getDeviceFromCache(params.getProductKey(), params.getDeviceName());
822 if (existDevice != null) { 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 return existDevice; 829 return existDevice;
825 } 830 }
@@ -840,4 +845,42 @@ public class IotDeviceServiceImpl implements IotDeviceService { @@ -840,4 +845,42 @@ public class IotDeviceServiceImpl implements IotDeviceService {
840 return SpringUtil.getBean(getClass()); 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 }