添加基本数据同步功能

This commit is contained in:
孙小云 2026-01-17 13:57:14 +08:00
parent f728d1f0c1
commit 296d535b25
19 changed files with 485 additions and 23 deletions

View File

@ -27,6 +27,14 @@ public interface IAircraftDomain
*/
Aircraft selectAircraftByAircraftId(Long aircraftId);
/**
* 根据设备主键查询无人机
*
* @param deviceId 设备主键
* @return 无人机
*/
Aircraft selectAircraftByDeviceId(Long deviceId);
/**
* 新增无人机
*

View File

@ -27,6 +27,14 @@ public interface IDeviceDomain
*/
Device selectDeviceByDeviceId(Long deviceId);
/**
* 根据IOT设备ID查询设备
*
* @param iotDeviceId IOT设备ID
* @return 设备
*/
Device selectDeviceByIotDeviceId(String iotDeviceId);
/**
* 新增设备
*

View File

@ -27,6 +27,14 @@ public interface IDockDomain
*/
Dock selectDockByDockId(Long dockId);
/**
* 根据设备主键查询机场
*
* @param deviceId 设备主键
* @return 机场
*/
Dock selectDockByDeviceId(Long deviceId);
/**
* 新增机场
*

View File

@ -55,4 +55,13 @@ public interface IThingsBoardDomain {
* @return 类型安全的遥测数据映射只包含预定义的遥测数据
*/
TelemetryMap getPredefinedDeviceTelemetry(String deviceId);
/**
* 获取设备所属的网关设备ID
* 通过 ThingsBoard EntityRelation 查询设备与网关的 "Contains" 关系
*
* @param deviceId 设备ID
* @return 网关设备ID如果设备不属于任何网关则返回 null
*/
String getDeviceGatewayId(String deviceId);
}

View File

@ -40,6 +40,13 @@ public class AircraftDomainImpl implements IAircraftDomain
return AircraftDomainConvert.toModel(entity);
}
@Override
public Aircraft selectAircraftByDeviceId(Long deviceId)
{
AircraftEntity entity = aircraftMapper.selectAircraftByDeviceId(deviceId);
return AircraftDomainConvert.toModel(entity);
}
@Override
public int insertAircraft(Aircraft aircraft)
{

View File

@ -40,6 +40,13 @@ public class DeviceDomainImpl implements IDeviceDomain
return DeviceDomainConvert.toModel(entity);
}
@Override
public Device selectDeviceByIotDeviceId(String iotDeviceId)
{
DeviceEntity entity = deviceMapper.selectDeviceByIotDeviceId(iotDeviceId);
return DeviceDomainConvert.toModel(entity);
}
@Override
public int insertDevice(Device device)
{

View File

@ -40,6 +40,13 @@ public class DockDomainImpl implements IDockDomain
return DockDomainConvert.toModel(entity);
}
@Override
public Dock selectDockByDeviceId(Long deviceId)
{
DockEntity entity = dockMapper.selectDockByDeviceId(deviceId);
return DockDomainConvert.toModel(entity);
}
@Override
public int insertDock(Dock dock)
{

View File

@ -12,8 +12,11 @@ import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.thingsboard.rest.client.RestClient;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import java.util.List;
import java.util.Optional;
@ -173,6 +176,36 @@ public class ThingsBoardDomainImpl implements IThingsBoardDomain {
return predefinedTelemetry;
}
@Override
public String getDeviceGatewayId(String deviceId) {
try {
DeviceId id = new DeviceId(UUID.fromString(deviceId));
// 查询指向该设备的 "Contains" 关系网关 -> 设备
List<EntityRelation> relations = client.findByTo(
id,
EntityRelation.CONTAINS_TYPE,
RelationTypeGroup.COMMON
);
if (relations == null || relations.isEmpty()) {
log.debug("设备 {} 不属于任何网关", deviceId);
return null;
}
// 获取第一个关系的 from 实体网关设备
EntityId gatewayEntityId = relations.get(0).getFrom();
String gatewayId = gatewayEntityId.getId().toString();
log.debug("设备 {} 属于网关 {}", deviceId, gatewayId);
return gatewayId;
} catch (Exception e) {
log.error("获取设备网关关系失败: deviceId={}, error={}", deviceId, e.getMessage(), e);
return null;
}
}
/**
* 解析属性并添加到AttributeMap
* 使用延迟注册机制自动处理所有属性

View File

@ -1,5 +1,6 @@
package com.ruoyi.device.domain.model.thingsboard;
import com.fasterxml.jackson.databind.JsonNode;
import org.thingsboard.server.common.data.id.DeviceId;
/**
@ -10,12 +11,14 @@ public class DeviceInfo {
private final String id;
private final String type;
private final DeviceId deviceId;
private final JsonNode additionalInfo;
public DeviceInfo(String name, String id, String type, DeviceId deviceId) {
public DeviceInfo(String name, String id, String type, DeviceId deviceId, JsonNode additionalInfo) {
this.name = name;
this.id = id;
this.type = type;
this.deviceId = deviceId;
this.additionalInfo = additionalInfo;
}
public String getName() {
@ -34,12 +37,30 @@ public class DeviceInfo {
return deviceId;
}
public JsonNode getAdditionalInfo() {
return additionalInfo;
}
/**
* 判断设备是否为网关
* 根据 ThingsBoard 标准检查 additionalInfo 中的 "gateway" 字段
*
* @return true 如果是网关设备否则返回 false
*/
public boolean isGateway() {
if (additionalInfo == null) {
return false;
}
return additionalInfo.has("gateway") && additionalInfo.get("gateway").asBoolean();
}
@Override
public String toString() {
return "DeviceInfo{" +
"name='" + name + '\'' +
", id='" + id + '\'' +
", type='" + type + '\'' +
", isGateway=" + isGateway() +
'}';
}
}

View File

@ -50,7 +50,8 @@ public class DeviceIterator implements Iterable<List<DeviceInfo>> {
device.getName(),
device.getId().getId().toString(),
device.getType(),
device.getId()
device.getId(),
device.getAdditionalInfo()
))
.toList();

View File

@ -84,6 +84,20 @@ public class DeviceAttributes {
}
);
// 机场SN号 - String
public static final AttributeKey<String> DOCK_SN = AttributeKey.of(
"dock_sn",
String.class,
value -> value != null ? value.toString() : null
);
// 子设备SN号 - String
public static final AttributeKey<String> SUB_DEVICE_SN = AttributeKey.of(
"sub_device.device_sn",
String.class,
value -> value != null ? value.toString() : null
);
private DeviceAttributes() {
// 工具类禁止实例化
}
@ -101,7 +115,9 @@ public class DeviceAttributes {
LAST_CONNECT_TIME,
ACTIVE,
LAST_ACTIVITY_TIME,
LAST_DISCONNECT_TIME
LAST_DISCONNECT_TIME,
DOCK_SN,
SUB_DEVICE_SN
);
}

View File

@ -19,6 +19,14 @@ public interface AircraftMapper
*/
AircraftEntity selectAircraftByAircraftId(Long aircraftId);
/**
* 根据设备主键查询无人机
*
* @param deviceId 设备主键
* @return 无人机信息
*/
AircraftEntity selectAircraftByDeviceId(Long deviceId);
/**
* 根据设备主键查询无人机列表
*

View File

@ -19,6 +19,14 @@ public interface DeviceMapper
*/
DeviceEntity selectDeviceByDeviceId(Long deviceId);
/**
* 根据IOT设备ID查询设备
*
* @param iotDeviceId IOT设备ID
* @return 设备信息
*/
DeviceEntity selectDeviceByIotDeviceId(String iotDeviceId);
/**
* 查询设备列表
*

View File

@ -19,6 +19,14 @@ public interface DockMapper
*/
DockEntity selectDockByDockId(Long dockId);
/**
* 根据设备主键查询机场
*
* @param deviceId 设备主键
* @return 机场信息
*/
DockEntity selectDockByDeviceId(Long deviceId);
/**
* 根据设备主键查询机场列表
*

View File

@ -0,0 +1,47 @@
package com.ruoyi.device.service.enums;
/**
* 设备类型枚举
* Service层使用
*
* @author ruoyi
* @date 2026-01-17
*/
public enum DeviceType {
/**
* 无人机
*/
AIRCRAFT("AIRCRAFT", "无人机"),
/**
* 机场
*/
DOCK("DOCK", "机场"),
/**
* 网关
*/
GATEWAY("GATEWAY", "网关");
private final String code;
private final String description;
DeviceType(String code, String description) {
this.code = code;
this.description = description;
}
public String getCode() {
return code;
}
public String getDescription() {
return description;
}
@Override
public String toString() {
return code;
}
}

View File

@ -1,15 +1,27 @@
package com.ruoyi.device.service.impl;
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;
import com.ruoyi.device.domain.api.IThingsBoardDomain;
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;
import com.ruoyi.device.domain.model.thingsboard.AttributeMap;
import com.ruoyi.device.domain.model.thingsboard.DeviceInfo;
import com.ruoyi.device.domain.model.thingsboard.constants.DeviceAttributes;
import com.ruoyi.device.service.enums.DeviceType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@Service
@ -19,6 +31,18 @@ public class SynService {
private final IThingsBoardDomain iThingsBoardDomain;
@Autowired
private IDeviceDomain deviceDomain;
@Autowired
private IDockDomain dockDomain;
@Autowired
private IAircraftDomain aircraftDomain;
@Autowired
private IDockAircraftDomain dockAircraftDomain;
public SynService(IThingsBoardDomain iThingsBoardDomain) {
this.iThingsBoardDomain = iThingsBoardDomain;
}
@ -39,32 +63,257 @@ public class SynService {
int totalCount = 0;
for (List<DeviceInfo> deviceBatch : allDevices) {
for (DeviceInfo device : deviceBatch) {
// 获取设备属性以获取活跃状态
Boolean activeStatus = false;
for (DeviceInfo deviceInfo : deviceBatch) {
try {
AttributeMap attributes = iThingsBoardDomain.getDeviceAttributes(device.getId());
// 尝试从 AttributeMap 中获取 active 属性
Optional<Boolean> active = attributes.get(DeviceAttributes.ACTIVE);
if (active.isPresent()) {
activeStatus = active.get();
}
} catch (Exception e) {
log.debug("获取设备 {} 的活跃状态失败: {}", device.getId(), e.getMessage());
}
// 获取设备属性
AttributeMap attributes = iThingsBoardDomain.getDeviceAttributes(deviceInfo.getId());
log.info("Device Name: {}, Device ID: {}, Device Type: {}, Active: {}",
device.getName(),
device.getId(),
device.getType(),
activeStatus);
totalCount++;
// 判断设备类型
DeviceType deviceType = determineDeviceType(deviceInfo, attributes);
// 同步设备表
Long deviceId = syncDevice(deviceInfo, attributes, deviceType);
// 根据设备类型进行不同的处理
if (deviceType == DeviceType.DOCK) {
// 机场同步机场表
syncDock(deviceId, deviceInfo.getName());
// 获取机场挂载的无人机SN号
Optional<String> subDeviceSnOpt = attributes.get(DeviceAttributes.SUB_DEVICE_SN);
if (subDeviceSnOpt.isPresent() && StringUtils.hasText(subDeviceSnOpt.get())) {
String aircraftSn = subDeviceSnOpt.get();
// 通过SN号查找无人机设备
Device aircraftDevice = findDeviceBySn(aircraftSn);
if (aircraftDevice != null) {
// 同步机场无人机关联
syncDockAircraft(deviceId, aircraftDevice.getDeviceId());
}
}
} else if (deviceType == DeviceType.AIRCRAFT) {
// 无人机同步无人机表
syncAircraft(deviceId, deviceInfo.getName());
}
// 网关类型不需要额外处理
totalCount++;
} catch (Exception e) {
log.error("同步设备失败: deviceId={}, deviceName={}, error={}",
deviceInfo.getId(), deviceInfo.getName(), e.getMessage(), e);
}
}
}
log.info("========== 定时任务执行完成,共打印 {} 个设备 ==========", totalCount);
log.info("========== 数据同步任务完成,共同步 {} 个设备 ==========", totalCount);
} catch (Exception e) {
log.error("定时任务执行失败: {}", e.getMessage(), e);
log.error("数据同步任务执行失败: {}", e.getMessage(), e);
}
}
/**
* 判断设备类型
* 优化后的判断逻辑
* 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 attributes 设备属性
* @param deviceType 设备类型
* @return 设备主键ID
*/
private Long syncDevice(DeviceInfo deviceInfo, AttributeMap attributes, DeviceType deviceType) {
String iotDeviceId = deviceInfo.getId();
String deviceName = deviceInfo.getName();
// 使用 ThingsBoard 标准方式获取网关设备ID通过 EntityRelation
String gateway = iThingsBoardDomain.getDeviceGatewayId(iotDeviceId);
String deviceSn = deviceName; // 使用设备名称作为SN号,网关其实是没有SN号的
// 查询设备是否已存在
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);
newDevice.setGateway(gateway);
newDevice.setCreateBy("system");
deviceDomain.insertDevice(newDevice);
log.info("插入新设备: iotDeviceId={}, deviceName={}, deviceType={}", iotDeviceId, deviceName, deviceType);
return newDevice.getDeviceId();
} 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;
}
if (!Objects.equals(existingDevice.getGateway(), gateway)) {
existingDevice.setGateway(gateway);
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) {
// 查询机场是否已存在
Dock existingDock = dockDomain.selectDockByDeviceId(deviceId);
if (existingDock == null) {
// 机场不存在插入新机场
Dock newDock = new Dock();
newDock.setDockName(deviceName);
newDock.setDeviceId(deviceId);
newDock.setCreateBy("system");
dockDomain.insertDock(newDock);
log.info("插入新机场: deviceId={}, dockName={}", deviceId, deviceName);
}
// 机场已存在无需更新
}
/**
* 同步无人机数据
*
* @param deviceId 设备主键ID
* @param deviceName 设备名称
* @return 无人机主键ID
*/
private Long syncAircraft(Long deviceId, String deviceName) {
// 查询无人机是否已存在
Aircraft existingAircraft = aircraftDomain.selectAircraftByDeviceId(deviceId);
if (existingAircraft == null) {
// 无人机不存在插入新无人机
Aircraft newAircraft = new Aircraft();
newAircraft.setAircraftName(deviceName);
newAircraft.setDeviceId(deviceId);
newAircraft.setCreateBy("system");
aircraftDomain.insertAircraft(newAircraft);
log.info("插入新无人机: deviceId={}, aircraftName={}", deviceId, deviceName);
return newAircraft.getAircraftId();
}
// 无人机已存在无需更新
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);
}
}
}
}

View File

@ -26,6 +26,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
where aircraft_id = #{aircraftId}
</select>
<select id="selectAircraftByDeviceId" parameterType="Long" resultMap="AircraftResult">
<include refid="selectAircraftVo"/>
where device_id = #{deviceId}
limit 1
</select>
<select id="selectAircraftListByDeviceId" parameterType="Long" resultMap="AircraftResult">
<include refid="selectAircraftVo"/>
where device_id = #{deviceId}

View File

@ -32,6 +32,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
where device_id = #{deviceId}
</select>
<select id="selectDeviceByIotDeviceId" parameterType="String" resultMap="DeviceResult">
<include refid="selectDeviceVo"/>
where iot_device_id = #{iotDeviceId}
</select>
<select id="selectDeviceList" parameterType="com.ruoyi.device.mapper.entity.DeviceEntity" resultMap="DeviceResult">
<include refid="selectDeviceVo"/>
<where>

View File

@ -27,6 +27,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
where dock_id = #{dockId}
</select>
<select id="selectDockByDeviceId" parameterType="Long" resultMap="DockResult">
<include refid="selectDockVo"/>
where device_id = #{deviceId}
limit 1
</select>
<select id="selectDockListByDeviceId" parameterType="Long" resultMap="DockResult">
<include refid="selectDockVo"/>
where device_id = #{deviceId}