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 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&lt;IotDeviceDO&gt; {
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 }
... ...