package cn.iocoder.yudao.module.iot.service.device; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.BooleanUtil; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; 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.biz.dto.IotSubDeviceRegisterFullReqDTO; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.enums.device.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.IotDeviceRegisterReqDTO; import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; 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; import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceGroupDO; 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; import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Caching; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import javax.annotation.Nullable; import java.math.BigDecimal; import java.time.LocalDateTime; 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 实现类 * * @author 芋道源码 */ @Service @Validated @Slf4j public class IotDeviceServiceImpl implements IotDeviceService { @Resource private IotDeviceMapper deviceMapper; @Resource @Lazy // 延迟加载,解决循环依赖 private IotProductService productService; @Resource @Lazy // 延迟加载,解决循环依赖 private IotDeviceGroupService deviceGroupService; @Resource @Lazy // 延迟加载,解决循环依赖 private IotDeviceMessageService deviceMessageService; private IotDeviceServiceImpl getSelf() { return SpringUtil.getBean(getClass()); } @Override public Long createDevice(IotDeviceSaveReqVO createReqVO) { return createDevice0(createReqVO).getId(); } private IotDeviceDO createDevice0(IotDeviceSaveReqVO createReqVO) { // 1.1 校验产品是否存在 IotProductDO product = productService.getProduct(createReqVO.getProductId()); if (product == null) { throw exception(PRODUCT_NOT_EXISTS); } // 1.2 统一校验 validateCreateDeviceParam(product.getProductKey(), createReqVO.getDeviceName(), createReqVO.getGatewayId(), product); // 1.3 校验分组存在 deviceGroupService.validateDeviceGroupExists(createReqVO.getGroupIds()); // 1.4 校验设备序列号全局唯一 validateSerialNumberUnique(createReqVO.getSerialNumber(), null); // 2. 插入到数据库 IotDeviceDO device = BeanUtils.toBean(createReqVO, IotDeviceDO.class); initDevice(device, product); deviceMapper.insert(device); return device; } private void validateCreateDeviceParam(String productKey, String deviceName, Long gatewayId, IotProductDO product) { // 校验设备名称在同一产品下是否唯一 TenantUtils.executeIgnore(() -> { if (deviceMapper.selectByProductKeyAndDeviceName(productKey, deviceName) != null) { throw exception(DEVICE_NAME_EXISTS); } }); // 校验父设备是否为合法网关 if (IotProductDeviceTypeEnum.isGatewaySub(product.getDeviceType()) && gatewayId != null) { validateGatewayDeviceExists(gatewayId); } } /** * 校验设备序列号全局唯一性 * * @param serialNumber 设备序列号 * @param excludeId 排除的设备编号(用于更新时排除自身) */ private void validateSerialNumberUnique(String serialNumber, Long excludeId) { if (StrUtil.isBlank(serialNumber)) { return; } IotDeviceDO existDevice = deviceMapper.selectBySerialNumber(serialNumber); if (existDevice != null && ObjUtil.notEqual(existDevice.getId(), excludeId)) { throw exception(DEVICE_SERIAL_NUMBER_EXISTS); } } private void initDevice(IotDeviceDO device, IotProductDO product) { device.setProductId(product.getId()).setProductKey(product.getProductKey()) .setDeviceType(product.getDeviceType()) .setDeviceSecret(generateDeviceSecret()) // 生成密钥 .setState(IotDeviceStateEnum.INACTIVE.getState()); // 默认未激活 } private String generateDeviceSecret() { return IdUtil.fastSimpleUUID(); } @Override public void updateDevice(IotDeviceSaveReqVO updateReqVO) { updateReqVO.setDeviceName(null).setProductId(null); // 不允许更新 // 1.1 校验存在 IotDeviceDO device = validateDeviceExists(updateReqVO.getId()); // 1.2 校验父设备是否为合法网关 if (IotProductDeviceTypeEnum.isGatewaySub(device.getDeviceType()) && updateReqVO.getGatewayId() != null) { validateGatewayDeviceExists(updateReqVO.getGatewayId()); } // 1.3 校验分组存在 deviceGroupService.validateDeviceGroupExists(updateReqVO.getGroupIds()); // 1.4 校验设备序列号全局唯一 validateSerialNumberUnique(updateReqVO.getSerialNumber(), updateReqVO.getId()); // 2. 更新到数据库 IotDeviceDO updateObj = BeanUtils.toBean(updateReqVO, IotDeviceDO.class); deviceMapper.updateById(updateObj); // 3. 清空对应缓存 deleteDeviceCache(device); } @Override @Transactional(rollbackFor = Exception.class) public void updateDeviceGroup(IotDeviceUpdateGroupReqVO updateReqVO) { // 1.1 校验设备存在 List devices = deviceMapper.selectByIds(updateReqVO.getIds()); if (CollUtil.isEmpty(devices)) { return; } // 1.2 校验分组存在 deviceGroupService.validateDeviceGroupExists(updateReqVO.getGroupIds()); // 3. 更新设备分组 deviceMapper.updateBatch(convertList(devices, device -> new IotDeviceDO() .setId(device.getId()).setGroupIds(updateReqVO.getGroupIds()))); // 4. 清空对应缓存 deleteDeviceCache(devices); } @Override public void deleteDevice(Long id) { // 1.1 校验存在 IotDeviceDO device = validateDeviceExists(id); // 1.2 如果是网关设备,检查是否有子设备绑定 if (IotProductDeviceTypeEnum.isGateway(device.getDeviceType()) && deviceMapper.selectCountByGatewayId(id) > 0) { throw exception(DEVICE_GATEWAY_HAS_SUB); } // 2. 删除设备 deviceMapper.deleteById(id); // 3. 清空对应缓存 deleteDeviceCache(device); } @Override @Transactional(rollbackFor = Exception.class) public void deleteDeviceList(Collection ids) { // 1.1 校验存在 if (CollUtil.isEmpty(ids)) { return; } List devices = deviceMapper.selectByIds(ids); if (CollUtil.isEmpty(devices)) { return; } // 1.2 如果是网关设备,检查是否有子设备绑定 for (IotDeviceDO device : devices) { if (IotProductDeviceTypeEnum.isGateway(device.getDeviceType()) && deviceMapper.selectCountByGatewayId(device.getId()) > 0) { throw exception(DEVICE_GATEWAY_HAS_SUB); } } // 2. 删除设备 deviceMapper.deleteByIds(ids); // 3. 清空对应缓存 deleteDeviceCache(devices); } @Override public IotDeviceDO validateDeviceExists(Long id) { IotDeviceDO device = deviceMapper.selectById(id); if (device == null) { throw exception(DEVICE_NOT_EXISTS); } return device; } @Override public IotDeviceDO validateDeviceExistsFromCache(Long id) { IotDeviceDO device = getSelf().getDeviceFromCache(id); if (device == null) { throw exception(DEVICE_NOT_EXISTS); } return device; } /** * 校验网关设备是否存在 * * @param id 设备 ID */ private void validateGatewayDeviceExists(Long id) { IotDeviceDO device = deviceMapper.selectById(id); if (device == null) { throw exception(DEVICE_GATEWAY_NOT_EXISTS); } if (!IotProductDeviceTypeEnum.isGateway(device.getDeviceType())) { throw exception(DEVICE_NOT_GATEWAY); } } @Override public IotDeviceDO getDevice(Long id) { return deviceMapper.selectById(id); } @Override @Cacheable(value = RedisKeyConstants.DEVICE, key = "#id", unless = "#result == null") @TenantIgnore // 忽略租户信息 public IotDeviceDO getDeviceFromCache(Long id) { return deviceMapper.selectById(id); } @Override @Cacheable(value = RedisKeyConstants.DEVICE, key = "#productKey + '_' + #deviceName", unless = "#result == null") @TenantIgnore // 忽略租户信息,跨租户 productKey + deviceName 是唯一的 public IotDeviceDO getDeviceFromCache(String productKey, String deviceName) { return deviceMapper.selectByProductKeyAndDeviceName(productKey, deviceName); } @Override public PageResult getDevicePage(IotDevicePageReqVO pageReqVO) { return deviceMapper.selectPage(pageReqVO); } @Override public List getDeviceListByCondition(@Nullable Integer deviceType, @Nullable Long productId) { return deviceMapper.selectListByCondition(deviceType, productId); } @Override public List getDeviceListByState(Integer state) { return deviceMapper.selectListByState(state); } @Override public List getDeviceListByProductId(Long productId) { return deviceMapper.selectListByProductId(productId); } @Override public void updateDeviceState(IotDeviceDO device, Integer state) { // 1. 更新状态和时间 IotDeviceDO updateObj = new IotDeviceDO().setId(device.getId()).setState(state); if (device.getOnlineTime() == null && Objects.equals(state, IotDeviceStateEnum.ONLINE.getState())) { updateObj.setActiveTime(LocalDateTime.now()); } if (Objects.equals(state, IotDeviceStateEnum.ONLINE.getState())) { updateObj.setOnlineTime(LocalDateTime.now()); } else if (Objects.equals(state, IotDeviceStateEnum.OFFLINE.getState())) { updateObj.setOfflineTime(LocalDateTime.now()); } deviceMapper.updateById(updateObj); // 2. 清空对应缓存 deleteDeviceCache(device); // 3. 网关设备下线时,联动所有子设备下线 if (Objects.equals(state, IotDeviceStateEnum.OFFLINE.getState()) && IotProductDeviceTypeEnum.isGateway(device.getDeviceType())) { handleGatewayOffline(device); } } /** * 处理网关下线,联动所有子设备下线 * * @param gatewayDevice 网关设备 */ private void handleGatewayOffline(IotDeviceDO gatewayDevice) { List subDevices = deviceMapper.selectListByGatewayId(gatewayDevice.getId()); if (CollUtil.isEmpty(subDevices)) { return; } for (IotDeviceDO subDevice : subDevices) { if (Objects.equals(subDevice.getState(), IotDeviceStateEnum.ONLINE.getState())) { try { updateDeviceState(subDevice, IotDeviceStateEnum.OFFLINE.getState()); log.info("[handleGatewayOffline][网关({}/{}) 下线,子设备({}/{}) 联动下线]", gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(), subDevice.getProductKey(), subDevice.getDeviceName()); } catch (Exception ex) { log.error("[handleGatewayOffline][子设备({}/{}) 下线失败]", subDevice.getProductKey(), subDevice.getDeviceName(), ex); } } } } @Override public void updateDeviceState(Long id, Integer state) { // 校验存在 IotDeviceDO device = validateDeviceExists(id); // 执行更新 updateDeviceState(device, state); } @Override public Long getDeviceCountByProductId(Long productId) { return deviceMapper.selectCountByProductId(productId); } @Override public Long getDeviceCountByGroupId(Long groupId) { return deviceMapper.selectCountByGroupId(groupId); } @Override @Transactional(rollbackFor = Exception.class) // 添加事务,异常则回滚所有导入 public IotDeviceImportRespVO importDevice(List importDevices, boolean updateSupport) { // 1. 参数校验 if (CollUtil.isEmpty(importDevices)) { throw exception(DEVICE_IMPORT_LIST_IS_EMPTY); } // 2. 遍历,逐个创建 or 更新 IotDeviceImportRespVO respVO = IotDeviceImportRespVO.builder().createDeviceNames(new ArrayList<>()) .updateDeviceNames(new ArrayList<>()).failureDeviceNames(new LinkedHashMap<>()).build(); importDevices.forEach(importDevice -> { try { // 2.1.1 校验字段是否符合要求 try { ValidationUtils.validate(importDevice); } catch (ConstraintViolationException ex) { respVO.getFailureDeviceNames().put(importDevice.getDeviceName(), ex.getMessage()); return; } // 2.1.2 校验产品是否存在 IotProductDO product = productService.validateProductExists(importDevice.getProductKey()); // 2.1.3 校验父设备是否存在 Long gatewayId = null; if (StrUtil.isNotEmpty(importDevice.getParentDeviceName())) { IotDeviceDO gatewayDevice = deviceMapper.selectByDeviceName(importDevice.getParentDeviceName()); if (gatewayDevice == null) { throw exception(DEVICE_GATEWAY_NOT_EXISTS); } if (!IotProductDeviceTypeEnum.isGateway(gatewayDevice.getDeviceType())) { throw exception(DEVICE_NOT_GATEWAY); } gatewayId = gatewayDevice.getId(); } // 2.1.4 校验设备分组是否存在 Set groupIds = new HashSet<>(); if (StrUtil.isNotEmpty(importDevice.getGroupNames())) { String[] groupNames = importDevice.getGroupNames().split(","); for (String groupName : groupNames) { IotDeviceGroupDO group = deviceGroupService.getDeviceGroupByName(groupName); if (group == null) { throw exception(DEVICE_GROUP_NOT_EXISTS); } groupIds.add(group.getId()); } } // 2.2.1 判断如果不存在,在进行插入 IotDeviceDO existDevice = deviceMapper.selectByDeviceName(importDevice.getDeviceName()); if (existDevice == null) { createDevice(new IotDeviceSaveReqVO() .setDeviceName(importDevice.getDeviceName()) .setProductId(product.getId()).setGatewayId(gatewayId).setGroupIds(groupIds)); respVO.getCreateDeviceNames().add(importDevice.getDeviceName()); return; } // 2.2.2 如果存在,判断是否允许更新 if (!updateSupport) { throw exception(DEVICE_KEY_EXISTS); } updateDevice(new IotDeviceSaveReqVO().setId(existDevice.getId()) .setGatewayId(gatewayId).setGroupIds(groupIds)); respVO.getUpdateDeviceNames().add(importDevice.getDeviceName()); } catch (ServiceException ex) { respVO.getFailureDeviceNames().put(importDevice.getDeviceName(), ex.getMessage()); } }); return respVO; } @Override public IotDeviceAuthInfoRespVO getDeviceAuthInfo(Long id) { IotDeviceDO device = validateDeviceExists(id); // 使用 IotDeviceAuthUtils 生成认证信息 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( device.getProductKey(), device.getDeviceName(), device.getDeviceSecret()); return BeanUtils.toBean(authInfo, IotDeviceAuthInfoRespVO.class); } private void deleteDeviceCache(IotDeviceDO device) { // 保证 Spring AOP 触发 getSelf().deleteDeviceCache0(device); } private void deleteDeviceCache(List devices) { devices.forEach(this::deleteDeviceCache); } @SuppressWarnings("unused") @Caching(evict = { @CacheEvict(value = RedisKeyConstants.DEVICE, key = "#device.id"), @CacheEvict(value = RedisKeyConstants.DEVICE, key = "#device.productKey + '_' + #device.deviceName") }) public void deleteDeviceCache0(IotDeviceDO device) { } @Override public Long getDeviceCount(LocalDateTime createTime) { return deviceMapper.selectCountByCreateTime(createTime); } @Override public Map getDeviceCountMapByProductId() { return deviceMapper.selectDeviceCountMapByProductId(); } @Override public Map getDeviceCountMapByState() { return deviceMapper.selectDeviceCountGroupByState(); } @Override public List getDeviceListByProductKeyAndNames(String productKey, List deviceNames) { if (StrUtil.isBlank(productKey) || CollUtil.isEmpty(deviceNames)) { return Collections.emptyList(); } return deviceMapper.selectByProductKeyAndDeviceNames(productKey, deviceNames); } @Override public boolean authDevice(IotDeviceAuthReqDTO authReqDTO) { // 1. 校验设备是否存在 IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authReqDTO.getUsername()); if (deviceInfo == null) { log.error("[authDevice][认证失败,username({}) 格式不正确]", authReqDTO.getUsername()); return false; } String deviceName = deviceInfo.getDeviceName(); String productKey = deviceInfo.getProductKey(); IotDeviceDO device = getSelf().getDeviceFromCache(productKey, deviceName); if (device == null) { log.warn("[authDevice][设备({}/{}) 不存在]", productKey, deviceName); return false; } // 2. 校验密码 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(productKey, deviceName, device.getDeviceSecret()); if (ObjUtil.notEqual(authInfo.getPassword(), authReqDTO.getPassword())) { log.error("[authDevice][设备({}/{}) 密码不正确]", productKey, deviceName); return false; } return true; } @Override public List validateDeviceListExists(Collection ids) { List devices = getDeviceList(ids); if (devices.size() != ids.size()) { throw exception(DEVICE_NOT_EXISTS); } return devices; } @Override public List getDeviceList(Collection ids) { if (CollUtil.isEmpty(ids)) { return Collections.emptyList(); } return deviceMapper.selectByIds(ids); } @Override public void updateDeviceFirmware(Long deviceId, Long firmwareId) { // 1. 校验设备是否存在 IotDeviceDO device = validateDeviceExists(deviceId); // 2. 更新设备固件版本 IotDeviceDO updateObj = new IotDeviceDO().setId(deviceId).setFirmwareId(firmwareId); deviceMapper.updateById(updateObj); // 3. 清空对应缓存 deleteDeviceCache(device); } @Override public void updateDeviceLocation(IotDeviceDO device, BigDecimal longitude, BigDecimal latitude) { // 1. 更新定位信息 deviceMapper.updateById(new IotDeviceDO().setId(device.getId()) .setLongitude(longitude).setLatitude(latitude)); // 2. 清空对应缓存 deleteDeviceCache(device); } @Override public List getDeviceListByHasLocation() { return deviceMapper.selectListByHasLocation(); } // ========== 网关-拓扑管理(后台操作) ========== @Override @Transactional(rollbackFor = Exception.class) public void bindDeviceGateway(Collection subIds, Long gatewayId) { if (CollUtil.isEmpty(subIds)) { return; } // 1.1 校验网关设备存在且类型正确 validateGatewayDeviceExists(gatewayId); // 1.2 校验每个设备是否可绑定 List devices = deviceMapper.selectByIds(subIds); for (IotDeviceDO device : devices) { checkSubDeviceCanBind(device, gatewayId); } // 2. 批量更新数据库 List updateList = convertList(devices, device -> new IotDeviceDO().setId(device.getId()).setGatewayId(gatewayId)); deviceMapper.updateBatch(updateList); // 3. 清空对应缓存 deleteDeviceCache(devices); // 4. 下发网关设备拓扑变更通知(增加) sendTopoChangeNotify(gatewayId, IotDeviceTopoChangeReqDTO.STATUS_CREATE, devices); } private void checkSubDeviceCanBind(IotDeviceDO device, Long gatewayId) { if (!IotProductDeviceTypeEnum.isGatewaySub(device.getDeviceType())) { throw exception(DEVICE_NOT_GATEWAY_SUB, device.getProductKey(), device.getDeviceName()); } // 已绑定到其他网关,拒绝绑定(需先解绑) if (device.getGatewayId() != null && ObjUtil.notEqual(device.getGatewayId(), gatewayId)) { throw exception(DEVICE_GATEWAY_BINDTO_EXISTS, device.getProductKey(), device.getDeviceName()); } } @Override @Transactional(rollbackFor = Exception.class) public void unbindDeviceGateway(Collection subIds, Long gatewayId) { // 1. 校验设备存在 if (CollUtil.isEmpty(subIds)) { return; } List devices = deviceMapper.selectByIds(subIds); devices.removeIf(device -> ObjUtil.notEqual(device.getGatewayId(), gatewayId)); if (CollUtil.isEmpty(devices)) { return; } // 2. 批量更新数据库(将 gatewayId 设置为 null) deviceMapper.updateGatewayIdBatch(convertList(devices, IotDeviceDO::getId), null); // 3. 清空对应缓存 deleteDeviceCache(devices); // 4. 下发网关设备拓扑变更通知(删除) sendTopoChangeNotify(gatewayId, IotDeviceTopoChangeReqDTO.STATUS_DELETE, devices); } @Override public PageResult getUnboundSubDevicePage(IotDevicePageReqVO pageReqVO) { return deviceMapper.selectUnboundSubDevicePage(pageReqVO); } @Override public List getDeviceListByGatewayId(Long gatewayId) { return deviceMapper.selectListByGatewayId(gatewayId); } // ========== 网关-拓扑管理(设备上报) ========== @Override public List handleTopoAddMessage(IotDeviceMessage message, IotDeviceDO gatewayDevice) { // 1.1 校验网关设备类型 if (!IotProductDeviceTypeEnum.isGateway(gatewayDevice.getDeviceType())) { throw exception(DEVICE_NOT_GATEWAY); } // 1.2 解析参数 IotDeviceTopoAddReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceTopoAddReqDTO.class); if (params == null || CollUtil.isEmpty(params.getSubDevices())) { throw exception(DEVICE_TOPO_PARAMS_INVALID); } // 2. 遍历处理每个子设备 List addedSubDevices = new ArrayList<>(); for (IotDeviceAuthReqDTO subDeviceAuth : params.getSubDevices()) { try { IotDeviceDO subDevice = addDeviceTopo(gatewayDevice, subDeviceAuth); addedSubDevices.add(new IotDeviceIdentity(subDevice.getProductKey(), subDevice.getDeviceName())); } catch (Exception ex) { log.warn("[handleTopoAddMessage][网关({}/{}) 添加子设备失败,message={}]", gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(), message, ex); } } // 3. 返回响应数据(包含成功添加的子设备列表) return addedSubDevices; } private IotDeviceDO addDeviceTopo(IotDeviceDO gatewayDevice, IotDeviceAuthReqDTO subDeviceAuth) { // 1.1 解析子设备信息 IotDeviceIdentity subDeviceInfo = IotDeviceAuthUtils.parseUsername(subDeviceAuth.getUsername()); if (subDeviceInfo == null) { throw exception(DEVICE_TOPO_SUB_DEVICE_USERNAME_INVALID); } // 1.2 校验子设备认证信息 if (!authDevice(subDeviceAuth)) { throw exception(DEVICE_TOPO_SUB_DEVICE_AUTH_FAILED); } // 1.3 获取子设备 IotDeviceDO subDevice = getSelf().getDeviceFromCache(subDeviceInfo.getProductKey(), subDeviceInfo.getDeviceName()); if (subDevice == null) { throw exception(DEVICE_NOT_EXISTS); } // 1.4 校验子设备类型 checkSubDeviceCanBind(subDevice, gatewayDevice.getId()); // 2. 更新数据库 deviceMapper.updateById(new IotDeviceDO().setId(subDevice.getId()).setGatewayId(gatewayDevice.getId())); log.info("[addDeviceTopo][网关({}/{}) 绑定子设备({}/{})]", gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(), subDevice.getProductKey(), subDevice.getDeviceName()); // 3. 清空对应缓存 deleteDeviceCache(subDevice); return subDevice; } @Override public List handleTopoDeleteMessage(IotDeviceMessage message, IotDeviceDO gatewayDevice) { // 1.1 校验网关设备类型 if (!IotProductDeviceTypeEnum.isGateway(gatewayDevice.getDeviceType())) { throw exception(DEVICE_NOT_GATEWAY); } // 1.2 解析参数 IotDeviceTopoDeleteReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceTopoDeleteReqDTO.class); if (params == null || CollUtil.isEmpty(params.getSubDevices())) { throw exception(DEVICE_TOPO_PARAMS_INVALID); } // 2. 遍历处理每个子设备 List deletedSubDevices = new ArrayList<>(); for (IotDeviceIdentity subDeviceIdentity : params.getSubDevices()) { try { deleteDeviceTopo(gatewayDevice, subDeviceIdentity); deletedSubDevices.add(subDeviceIdentity); } catch (Exception ex) { log.warn("[handleTopoDeleteMessage][网关({}/{}) 删除子设备失败,productKey={}, deviceName={}]", gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(), subDeviceIdentity.getProductKey(), subDeviceIdentity.getDeviceName(), ex); } } // 3. 返回响应数据(包含成功删除的子设备列表) return deletedSubDevices; } private void deleteDeviceTopo(IotDeviceDO gatewayDevice, IotDeviceIdentity subDeviceIdentity) { // 1.1 获取子设备 IotDeviceDO subDevice = getSelf().getDeviceFromCache(subDeviceIdentity.getProductKey(), subDeviceIdentity.getDeviceName()); if (subDevice == null) { throw exception(DEVICE_NOT_EXISTS); } // 1.2 校验子设备是否绑定到该网关 if (ObjUtil.notEqual(subDevice.getGatewayId(), gatewayDevice.getId())) { throw exception(DEVICE_TOPO_SUB_NOT_BINDTO_GATEWAY, subDeviceIdentity.getProductKey(), subDeviceIdentity.getDeviceName()); } // 2. 更新数据库(将 gatewayId 设置为 null) deviceMapper.updateGatewayIdBatch(singletonList(subDevice.getId()), null); log.info("[deleteDeviceTopo][网关({}/{}) 解绑子设备({}/{})]", gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(), subDevice.getProductKey(), subDevice.getDeviceName()); // 3. 清空对应缓存 deleteDeviceCache(subDevice); // 4. 子设备下线 if (Objects.equals(subDevice.getState(), IotDeviceStateEnum.ONLINE.getState())) { updateDeviceState(subDevice, IotDeviceStateEnum.OFFLINE.getState()); } } @Override public IotDeviceTopoGetRespDTO handleTopoGetMessage(IotDeviceDO gatewayDevice) { // 1. 校验网关设备类型 if (!IotProductDeviceTypeEnum.isGateway(gatewayDevice.getDeviceType())) { throw exception(DEVICE_NOT_GATEWAY); } // 2. 获取子设备列表并转换 List subDevices = deviceMapper.selectListByGatewayId(gatewayDevice.getId()); List subDeviceIdentities = convertList(subDevices, subDevice -> new IotDeviceIdentity(subDevice.getProductKey(), subDevice.getDeviceName())); return new IotDeviceTopoGetRespDTO().setSubDevices(subDeviceIdentities); } /** * 发送拓扑变更通知给网关设备 * * @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); } } // ========== 设备动态注册 ========== @Override public IotDeviceRegisterRespDTO registerDevice(IotDeviceRegisterReqDTO reqDTO) { // 1.1 校验产品 IotProductDO product = TenantUtils.executeIgnore(() -> productService.getProductByProductKey(reqDTO.getProductKey())); if (product == null) { throw exception(PRODUCT_NOT_EXISTS); } // 1.2 校验产品是否开启动态注册 if (BooleanUtil.isFalse(product.getRegisterEnabled())) { throw exception(DEVICE_REGISTER_DISABLED); } // 1.3 【重要!!!】验证签名 if (!IotProductAuthUtils.verifySign(reqDTO.getProductKey(), reqDTO.getDeviceName(), product.getProductSecret(), reqDTO.getSign())) { throw exception(DEVICE_REGISTER_SECRET_INVALID); } return TenantUtils.execute(product.getTenantId(), () -> { // 1.4 校验设备是否已存在(已存在则不允许重复注册) IotDeviceDO device = getSelf().getDeviceFromCache(reqDTO.getProductKey(), reqDTO.getDeviceName()); if (device != null) { throw exception(DEVICE_REGISTER_ALREADY_EXISTS); } // 2.1 自动创建设备 IotDeviceSaveReqVO createReqVO = new IotDeviceSaveReqVO() .setDeviceName(reqDTO.getDeviceName()) .setProductId(product.getId()); device = createDevice0(createReqVO); log.info("[registerDevice][产品({}) 自动创建设备({})]", reqDTO.getProductKey(), reqDTO.getDeviceName()); // 2.2 返回设备密钥 return new IotDeviceRegisterRespDTO(device.getProductKey(), device.getDeviceName(), device.getDeviceSecret()); }); } @Override public List registerSubDevices(IotSubDeviceRegisterFullReqDTO reqDTO) { // 1. 校验网关设备 IotDeviceDO gatewayDevice = getSelf().getDeviceFromCache(reqDTO.getGatewayProductKey(), reqDTO.getGatewayDeviceName()); // 2. 遍历注册每个子设备 return TenantUtils.execute(gatewayDevice.getTenantId(), () -> registerSubDevices0(gatewayDevice, reqDTO.getSubDevices())); } @Override public List handleSubDeviceRegisterMessage(IotDeviceMessage message, IotDeviceDO gatewayDevice) { // 1. 解析参数 if (!(message.getParams() instanceof List)) { throw exception(DEVICE_SUB_REGISTER_PARAMS_INVALID); } List subDevices = JsonUtils.convertList(message.getParams(), IotSubDeviceRegisterReqDTO.class); // 2. 遍历注册每个子设备 return registerSubDevices0(gatewayDevice, subDevices); } private List registerSubDevices0(IotDeviceDO gatewayDevice, List subDevices) { // 1.1 校验网关设备 if (gatewayDevice == null) { throw exception(DEVICE_NOT_EXISTS); } if (!IotProductDeviceTypeEnum.isGateway(gatewayDevice.getDeviceType())) { throw exception(DEVICE_NOT_GATEWAY); } // 1.2 注册设备不能为空 if (CollUtil.isEmpty(subDevices)) { throw exception(DEVICE_SUB_REGISTER_PARAMS_INVALID); } // 2. 遍历注册每个子设备 List results = new ArrayList<>(subDevices.size()); for (IotSubDeviceRegisterReqDTO subDevice : subDevices) { try { IotDeviceDO device = registerSubDevice0(gatewayDevice, subDevice); results.add(new IotSubDeviceRegisterRespDTO( subDevice.getProductKey(), subDevice.getDeviceName(), device.getDeviceSecret())); } catch (Exception ex) { log.error("[registerSubDevices0][子设备({}/{}) 注册失败]", subDevice.getProductKey(), subDevice.getDeviceName(), ex); } } return results; } private IotDeviceDO registerSubDevice0(IotDeviceDO gatewayDevice, IotSubDeviceRegisterReqDTO params) { // 1.1 校验产品 IotProductDO product = productService.getProductByProductKey(params.getProductKey()); if (product == null) { throw exception(PRODUCT_NOT_EXISTS); } // 1.2 校验产品是否为网关子设备类型 if (!IotProductDeviceTypeEnum.isGatewaySub(product.getDeviceType())) { throw exception(DEVICE_SUB_REGISTER_PRODUCT_NOT_GATEWAY_SUB, params.getProductKey()); } // 1.3 校验设备是否已存在(子设备动态注册:设备必须已预注册) IotDeviceDO existDevice = getSelf().getDeviceFromCache(params.getProductKey(), params.getDeviceName()); if (existDevice == null) { throw exception(DEVICE_NOT_EXISTS); } // 1.4 校验是否绑定到其他网关 if (existDevice.getGatewayId() != null && ObjUtil.notEqual(existDevice.getGatewayId(), gatewayDevice.getId())) { throw exception(DEVICE_GATEWAY_BINDTO_EXISTS, existDevice.getProductKey(), existDevice.getDeviceName()); } // 2. 绑定到网关(如果尚未绑定) if (existDevice.getGatewayId() == null) { // 2.1 更新数据库 deviceMapper.updateById(new IotDeviceDO().setId(existDevice.getId()).setGatewayId(gatewayDevice.getId())); // 2.2 清空对应缓存 deleteDeviceCache(existDevice); log.info("[registerSubDevice][网关({}/{}) 绑定子设备({}/{})]", gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(), existDevice.getProductKey(), existDevice.getDeviceName()); } return existDevice; } }