a-tuoheng-device/src/main/java/com/ruoyi/device/service/impl/SynService.java

505 lines
21 KiB
Java
Raw Normal View History

2026-01-17 13:05:43 +08:00
package com.ruoyi.device.service.impl;
2026-01-17 13:57:14 +08:00
import com.ruoyi.device.domain.api.IAircraftDomain;
import com.ruoyi.device.domain.api.IDeviceDomain;
import com.ruoyi.device.domain.api.IDockAircraftDomain;
import com.ruoyi.device.domain.api.IDockDomain;
2026-01-17 13:05:43 +08:00
import com.ruoyi.device.domain.api.IThingsBoardDomain;
2026-01-17 13:57:14 +08:00
import com.ruoyi.device.domain.model.Aircraft;
import com.ruoyi.device.domain.model.Device;
import com.ruoyi.device.domain.model.Dock;
import com.ruoyi.device.domain.model.DockAircraft;
2026-01-17 13:05:43 +08:00
import com.ruoyi.device.domain.model.thingsboard.AttributeMap;
import com.ruoyi.device.domain.model.thingsboard.DeviceInfo;
2026-01-19 09:12:27 +08:00
import com.ruoyi.device.domain.model.thingsboard.TelemetryMap;
import com.ruoyi.device.domain.model.thingsboard.TelemetryValue;
import com.ruoyi.device.domain.model.thingsboard.attributes.psdk.PsdkDevice;
2026-01-17 13:05:43 +08:00
import com.ruoyi.device.domain.model.thingsboard.constants.DeviceAttributes;
2026-01-19 09:12:27 +08:00
import com.ruoyi.device.domain.model.thingsboard.constants.DeviceTelemetry;
2026-01-17 13:57:14 +08:00
import com.ruoyi.device.service.enums.DeviceType;
2026-01-17 13:05:43 +08:00
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
2026-01-17 13:57:14 +08:00
import org.springframework.beans.factory.annotation.Autowired;
2026-01-17 14:08:11 +08:00
import org.springframework.beans.factory.annotation.Value;
2026-01-17 13:05:43 +08:00
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
2026-01-19 09:12:27 +08:00
import org.springframework.util.CollectionUtils;
2026-01-17 13:57:14 +08:00
import org.springframework.util.StringUtils;
2026-01-17 13:05:43 +08:00
2026-01-19 09:12:27 +08:00
import java.util.*;
2026-01-17 14:08:11 +08:00
import java.util.regex.Pattern;
2026-01-17 13:05:43 +08:00
@Service
public class SynService {
private static final Logger log = LoggerFactory.getLogger(SynService.class);
private final IThingsBoardDomain iThingsBoardDomain;
2026-01-17 14:08:11 +08:00
/**
* 设备名称过滤正则表达式列表从配置文件读取
* 匹配这些正则表达式的设备将被跳过不进行同步
*/
@Value("${device.sync.exclude-patterns:}")
private String excludePatterns;
/**
* 编译后的正则表达式模式列表延迟初始化
*/
private List<Pattern> excludePatternList;
2026-01-17 13:57:14 +08:00
@Autowired
private IDeviceDomain deviceDomain;
@Autowired
private IDockDomain dockDomain;
@Autowired
private IAircraftDomain aircraftDomain;
@Autowired
private IDockAircraftDomain dockAircraftDomain;
2026-01-17 13:05:43 +08:00
public SynService(IThingsBoardDomain iThingsBoardDomain) {
this.iThingsBoardDomain = iThingsBoardDomain;
}
2026-01-17 14:08:11 +08:00
/**
* 初始化设备名称过滤正则表达式列表
* 延迟初始化在第一次使用时编译正则表达式
*/
private void initExcludePatterns() {
if (excludePatternList == null) {
excludePatternList = new ArrayList<>();
if (StringUtils.hasText(excludePatterns)) {
String[] patterns = excludePatterns.split(",");
for (String pattern : patterns) {
String trimmedPattern = pattern.trim();
if (StringUtils.hasText(trimmedPattern)) {
try {
excludePatternList.add(Pattern.compile(trimmedPattern));
log.info("加载设备名称过滤规则: {}", trimmedPattern);
} catch (Exception e) {
log.error("无效的正则表达式: {}, error={}", trimmedPattern, e.getMessage());
}
}
}
}
if (excludePatternList.isEmpty()) {
log.info("未配置设备名称过滤规则");
}
}
}
/**
* 判断设备名称是否应该被过滤跳过同步
*
* @param deviceName 设备名称
* @return true 表示应该被过滤false 表示不过滤
*/
private boolean shouldExcludeDevice(String deviceName) {
// 延迟初始化
initExcludePatterns();
// 如果没有配置过滤规则,不过滤任何设备
if (excludePatternList.isEmpty()) {
return false;
}
// 检查设备名称是否匹配任何过滤规则
for (Pattern pattern : excludePatternList) {
if (pattern.matcher(deviceName).matches()) {
log.debug("设备 {} 匹配过滤规则 {},跳过同步", deviceName, pattern.pattern());
return true;
}
}
return false;
}
2026-01-17 15:22:02 +08:00
2026-01-17 13:05:43 +08:00
/**
* 定时任务同步基础表数据
2026-01-17 16:57:03 +08:00
* 执行时间启动后1分钟开始每10分钟执行一次可通过配置文件修改
2026-01-17 13:05:43 +08:00
* 配置项device.schedule.print-devices.initial-delay 初始延迟时间毫秒
* device.schedule.print-devices.fixed-delay 执行间隔时间毫秒
2026-01-17 15:22:02 +08:00
*
* 优化策略
* 1. 先识别所有网关设备
* 2. 遍历每个网关同步网关及其子设备
* 3. 同步非网关的独立设备机场和无人机
2026-01-17 13:05:43 +08:00
*/
@Scheduled(initialDelayString = "${device.schedule.update-devices.initial-delay:60000}",
2026-01-17 16:57:03 +08:00
fixedDelayString = "${device.schedule.update-devices.fixed-delay:600000}")
2026-01-17 13:05:43 +08:00
public void updateDevicesScheduled() {
try {
log.info("========== 开始执行定时任务:同步基础表数据 ==========");
int totalCount = 0;
2026-01-17 15:22:02 +08:00
int skippedCount = 0;
2026-01-17 13:05:43 +08:00
2026-01-17 15:22:02 +08:00
// 第一步:直接获取所有网关设备(服务端过滤)
Iterable<List<DeviceInfo>> gatewayDevices = iThingsBoardDomain.getAllGatewayDevices();
2026-01-17 14:08:11 +08:00
2026-01-17 15:22:02 +08:00
// 第二步:遍历每个网关,同步网关及其子设备
for (List<DeviceInfo> gatewayBatch : gatewayDevices) {
for (DeviceInfo gatewayInfo : gatewayBatch) {
2026-01-17 13:05:43 +08:00
try {
2026-01-17 15:22:02 +08:00
// 按照名字过滤网关设备
if (shouldExcludeDevice(gatewayInfo.getName())) {
log.info("网关 {} 匹配过滤规则,跳过同步", gatewayInfo.getName());
skippedCount++;
continue;
2026-01-17 13:05:43 +08:00
}
2026-01-17 15:22:02 +08:00
// 同步网关设备本身
log.info("开始同步网关: {}", gatewayInfo.getName());
syncDevice(gatewayInfo, DeviceType.GATEWAY, null);
2026-01-17 13:57:14 +08:00
totalCount++;
2026-01-17 15:22:02 +08:00
// 获取该网关的所有子设备ID
List<String> childDeviceIds = iThingsBoardDomain.getGatewayChildDevices(gatewayInfo.getId());
log.info("网关 {} 有 {} 个子设备", gatewayInfo.getName(), childDeviceIds.size());
// 遍历该网关的子设备
for (String childDeviceId : childDeviceIds) {
// 通过API获取子设备信息
DeviceInfo childDeviceInfo = iThingsBoardDomain.getDeviceInfo(childDeviceId);
if (childDeviceInfo == null) {
log.warn("子设备 {} 不存在,跳过", childDeviceId);
skippedCount++;
continue;
}
// 按照名字过滤子设备
if (shouldExcludeDevice(childDeviceInfo.getName())) {
log.info("子设备 {} 匹配过滤规则,跳过同步", childDeviceInfo.getName());
skippedCount++;
continue;
}
try {
// 获取子设备属性
2026-01-17 16:08:58 +08:00
AttributeMap attributes = iThingsBoardDomain.getPredefinedDeviceAttributes(childDeviceId);
2026-01-17 15:22:02 +08:00
// 判断设备类型
DeviceType deviceType = determineDeviceType(childDeviceInfo, attributes);
// 同步子设备传入网关ID
Long deviceId = syncDevice(childDeviceInfo, deviceType, gatewayInfo.getId());
// 根据设备类型进行不同的处理
syncDeviceByType(deviceId, childDeviceInfo.getName(), deviceType, attributes);
totalCount++;
} catch (Exception e) {
log.error("同步网关子设备失败: gatewayName={}, childDeviceId={}, error={}",
gatewayInfo.getName(), childDeviceId, e.getMessage(), e);
}
}
2026-01-17 13:05:43 +08:00
} catch (Exception e) {
2026-01-17 15:22:02 +08:00
log.error("同步网关失败: gatewayName={}, error={}",
gatewayInfo.getName(), e.getMessage(), e);
2026-01-17 13:05:43 +08:00
}
}
}
2026-01-17 15:22:02 +08:00
log.info("========== 数据同步任务完成,共同步 {} 个设备,跳过 {} 个设备 ==========", totalCount, skippedCount);
2026-01-17 13:05:43 +08:00
} catch (Exception e) {
2026-01-17 13:57:14 +08:00
log.error("数据同步任务执行失败: {}", e.getMessage(), e);
}
}
2026-01-17 15:22:02 +08:00
/**
* 根据设备类型进行不同的同步处理
*
* @param deviceId 设备主键ID
* @param deviceName 设备名称
* @param deviceType 设备类型
* @param attributes 设备属性
*/
private void syncDeviceByType(Long deviceId, String deviceName, DeviceType deviceType, AttributeMap attributes) {
if (deviceType == DeviceType.DOCK) {
// 机场:同步机场表
syncDock(deviceId, deviceName);
// 获取机场挂载的无人机SN号
Optional<String> subDeviceSnOpt = attributes.get(DeviceAttributes.SUB_DEVICE_SN);
if (subDeviceSnOpt.isPresent() && StringUtils.hasText(subDeviceSnOpt.get())) {
String aircraftSn = subDeviceSnOpt.get();
Device aircraftDevice = findDeviceBySn(aircraftSn);
if (aircraftDevice != null) {
syncDockAircraft(deviceId, aircraftDevice.getDeviceId());
}
}
} else if (deviceType == DeviceType.AIRCRAFT) {
// 无人机:同步无人机表
syncAircraft(deviceId, deviceName);
}
// 网关类型不需要额外处理
}
2026-01-17 13:57:14 +08:00
/**
* 判断设备类型
* 优化后的判断逻辑
* 1. 优先使用 ThingsBoard 标准的 additionalInfo.gateway 字段判断网关
* 2. 对于非网关设备通过 dock_sn 属性区分机场和无人机
*
* @param deviceInfo ThingsBoard设备信息
* @param attributes 设备属性
* @return 设备类型
*/
private DeviceType determineDeviceType(DeviceInfo deviceInfo, AttributeMap attributes) {
String deviceName = deviceInfo.getName();
// 1. 使用 ThingsBoard 标准方式判断网关:检查 additionalInfo 中的 gateway 字段
if (deviceInfo.isGateway()) {
return DeviceType.GATEWAY;
}
// 2. 非网关设备:通过 dock_sn 属性区分机场和无人机
Optional<String> dockSnOpt = attributes.get(DeviceAttributes.DOCK_SN);
if (dockSnOpt.isPresent() && StringUtils.hasText(dockSnOpt.get())) {
String dockSn = dockSnOpt.get();
// dock_sn 等于设备名称 -> 机场
// dock_sn 不等于设备名称 -> 无人机(挂载在该机场下)
return deviceName.equals(dockSn) ? DeviceType.DOCK : DeviceType.AIRCRAFT;
}
// dock_sn 属性不存在或为空,无法判断设备类型
throw new IllegalStateException("无法确定设备类型:设备 " + deviceName + " 缺少 dock_sn 属性");
}
/**
* 同步设备数据
*
* @param deviceInfo ThingsBoard设备信息
* @param deviceType 设备类型
2026-01-17 15:22:02 +08:00
* @param gatewayId 网关设备ID从映射中获取避免重复API调用
2026-01-17 13:57:14 +08:00
* @return 设备主键ID
*/
2026-01-17 15:22:02 +08:00
private Long syncDevice(DeviceInfo deviceInfo, DeviceType deviceType, String gatewayId) {
2026-01-17 13:57:14 +08:00
String iotDeviceId = deviceInfo.getId();
String deviceName = deviceInfo.getName();
String deviceSn = deviceName; // 使用设备名称作为SN号,网关其实是没有SN号的
2026-01-17 16:39:18 +08:00
log.info("开始同步设备: iotDeviceId={}, deviceName={}, deviceType={}, gatewayId={}",
iotDeviceId, deviceName, deviceType, gatewayId);
2026-01-17 13:57:14 +08:00
// 查询设备是否已存在
Device existingDevice = deviceDomain.selectDeviceByIotDeviceId(iotDeviceId);
if (existingDevice == null) {
// 设备不存在,插入新设备
Device newDevice = new Device();
newDevice.setDeviceName(deviceName);
newDevice.setIotDeviceId(iotDeviceId);
newDevice.setDeviceType(deviceType.getCode());
newDevice.setDeviceSn(deviceSn);
2026-01-17 15:22:02 +08:00
newDevice.setGateway(gatewayId);
2026-01-17 13:57:14 +08:00
newDevice.setCreateBy("system");
2026-01-17 16:39:18 +08:00
log.info("准备插入新设备: deviceName={}, deviceType={}, deviceSn={}",
deviceName, deviceType, deviceSn);
2026-01-17 13:57:14 +08:00
deviceDomain.insertDevice(newDevice);
2026-01-17 16:39:18 +08:00
Long deviceId = newDevice.getDeviceId();
log.info("插入新设备成功: iotDeviceId={}, deviceName={}, deviceType={}, 返回deviceId={}",
iotDeviceId, deviceName, deviceType, deviceId);
return deviceId;
2026-01-17 13:57:14 +08:00
} else {
// 设备已存在,检查是否需要更新
boolean needUpdate = false;
if (!Objects.equals(existingDevice.getDeviceName(), deviceName)) {
existingDevice.setDeviceName(deviceName);
needUpdate = true;
}
if (!Objects.equals(existingDevice.getDeviceType(), deviceType.getCode())) {
existingDevice.setDeviceType(deviceType.getCode());
needUpdate = true;
}
if (!Objects.equals(existingDevice.getDeviceSn(), deviceSn)) {
existingDevice.setDeviceSn(deviceSn);
needUpdate = true;
}
2026-01-17 15:22:02 +08:00
if (!Objects.equals(existingDevice.getGateway(), gatewayId)) {
existingDevice.setGateway(gatewayId);
2026-01-17 13:57:14 +08:00
needUpdate = true;
}
if (needUpdate) {
existingDevice.setUpdateBy("system");
deviceDomain.updateDevice(existingDevice);
log.info("更新设备: iotDeviceId={}, deviceName={}, deviceType={}", iotDeviceId, deviceName, deviceType);
}
return existingDevice.getDeviceId();
}
}
/**
* 同步机场数据
*
* @param deviceId 设备主键ID
* @param deviceName 设备名称
*/
private void syncDock(Long deviceId, String deviceName) {
2026-01-17 16:41:22 +08:00
log.info("开始同步机场: deviceId={}, deviceName={}", deviceId, deviceName);
2026-01-17 13:57:14 +08:00
// 查询机场是否已存在
Dock existingDock = dockDomain.selectDockByDeviceId(deviceId);
if (existingDock == null) {
// 机场不存在,插入新机场
Dock newDock = new Dock();
newDock.setDockName(deviceName);
newDock.setDeviceId(deviceId);
newDock.setCreateBy("system");
2026-01-17 16:41:22 +08:00
log.info("准备插入新机场: deviceId={}, dockName={}", deviceId, deviceName);
2026-01-17 13:57:14 +08:00
dockDomain.insertDock(newDock);
2026-01-17 16:41:22 +08:00
log.info("插入新机场成功: deviceId={}, dockName={}, dockId={}",
deviceId, deviceName, newDock.getDockId());
} else {
log.info("机场已存在,跳过插入: deviceId={}, dockName={}", deviceId, deviceName);
2026-01-17 13:57:14 +08:00
}
// 机场已存在,无需更新
}
/**
* 同步无人机数据
*
* @param deviceId 设备主键ID
* @param deviceName 设备名称
* @return 无人机主键ID
*/
private Long syncAircraft(Long deviceId, String deviceName) {
2026-01-17 16:41:22 +08:00
log.info("开始同步无人机: deviceId={}, deviceName={}", deviceId, deviceName);
2026-01-17 13:57:14 +08:00
// 查询无人机是否已存在
Aircraft existingAircraft = aircraftDomain.selectAircraftByDeviceId(deviceId);
if (existingAircraft == null) {
// 无人机不存在,插入新无人机
Aircraft newAircraft = new Aircraft();
newAircraft.setAircraftName(deviceName);
newAircraft.setDeviceId(deviceId);
newAircraft.setCreateBy("system");
2026-01-17 16:41:22 +08:00
log.info("准备插入新无人机: deviceId={}, aircraftName={}", deviceId, deviceName);
2026-01-17 13:57:14 +08:00
aircraftDomain.insertAircraft(newAircraft);
2026-01-17 16:41:22 +08:00
Long aircraftId = newAircraft.getAircraftId();
log.info("插入新无人机成功: deviceId={}, aircraftName={}, aircraftId={}",
deviceId, deviceName, aircraftId);
return aircraftId;
} else {
log.info("无人机已存在,跳过插入: deviceId={}, aircraftName={}", deviceId, deviceName);
2026-01-17 13:57:14 +08:00
}
2026-01-19 09:12:27 +08:00
TelemetryMap telemetryMap = iThingsBoardDomain.getPredefinedDeviceTelemetry(deviceName);
Optional<TelemetryValue<List<PsdkDevice>>>
psdkDevicesOption = telemetryMap.get(DeviceTelemetry.PSDK_WIDGET_VALUES);
if(psdkDevicesOption.isPresent()) {
List<PsdkDevice> psdkDevices = psdkDevicesOption.get().getValue();
if(!CollectionUtils.isEmpty(psdkDevices)) {
for (PsdkDevice psdkDevice : psdkDevices) {
if(Objects.nonNull(psdkDevice.getSpeaker()) &&
Objects.nonNull(psdkDevice.getPsdk_sn()) && !psdkDevice.getPsdk_sn().isEmpty()) {
// 第一步 插入喊话器设备 (没有则新增)
// 判断 device_payload 中 挂载SN号 中如果不存在为Psdk_sn的数据则插入
// 插入 device_payload 挂载类为喊话器(需要定义一个挂载的枚举类型)
// 挂载显示名称为喊话器 挂载SN号string为 Psdk_snIOT中的设备ID 为空
// 更新 无人机挂载表 表
// 更新 device_aircraft_payload 表
// 判断改表中是否存在 payload_id 为 device_payload 中主键的数据
// 存在的话,判断 aircraft_id 字段是否等于 deviceName , 不等于则更新 aircraft_id
// 等于则什么都不做;
// 表中不否存在 payload_id 为 device_payload 中主键的数据,则插入数据
// dock_id为 无人机表关联的基础表的主键
}
}
}
}
2026-01-17 13:57:14 +08:00
// 无人机已存在,无需更新
return existingAircraft.getAircraftId();
}
/**
* 根据设备SN号查找设备
*
* @param deviceSn 设备SN号
* @return 设备信息
*/
private Device findDeviceBySn(String deviceSn) {
Device queryDevice = new Device();
queryDevice.setDeviceSn(deviceSn);
List<Device> devices = deviceDomain.selectDeviceList(queryDevice);
return (devices != null && !devices.isEmpty()) ? devices.get(0) : null;
}
/**
* 同步机场无人机关联数据
* 按照一个机场只会挂载一台无人机的逻辑进行处理
*
* @param dockDeviceId 机场设备主键ID
* @param aircraftDeviceId 无人机设备主键ID
*/
private void syncDockAircraft(Long dockDeviceId, Long aircraftDeviceId) {
// 获取机场主键
Dock dock = dockDomain.selectDockByDeviceId(dockDeviceId);
if (dock == null) {
log.warn("机场不存在,无法同步机场无人机关联: dockDeviceId={}", dockDeviceId);
return;
}
// 获取无人机主键
Aircraft aircraft = aircraftDomain.selectAircraftByDeviceId(aircraftDeviceId);
if (aircraft == null) {
log.warn("无人机不存在,无法同步机场无人机关联: aircraftDeviceId={}", aircraftDeviceId);
return;
}
Long dockId = dock.getDockId();
Long aircraftId = aircraft.getAircraftId();
// 查询该机场是否已有关联
List<DockAircraft> existingRelations = dockAircraftDomain.selectDockAircraftByDockId(dockId);
if (existingRelations == null || existingRelations.isEmpty()) {
// 机场没有关联,插入新关联
DockAircraft newRelation = new DockAircraft();
newRelation.setDockId(dockId);
newRelation.setAircraftId(aircraftId);
newRelation.setCreateBy("system");
dockAircraftDomain.insertDockAircraft(newRelation);
log.info("插入机场无人机关联: dockId={}, aircraftId={}", dockId, aircraftId);
} else {
// 机场已有关联,检查是否需要更新
DockAircraft existingRelation = existingRelations.get(0);
if (!Objects.equals(existingRelation.getAircraftId(), aircraftId)) {
// 无人机发生变化,更新关联
existingRelation.setAircraftId(aircraftId);
existingRelation.setUpdateBy("system");
dockAircraftDomain.updateDockAircraft(existingRelation);
log.info("更新机场无人机关联: dockId={}, aircraftId={}", dockId, aircraftId);
}
2026-01-17 13:05:43 +08:00
}
}
}