ard-admin/src/main/java/com/ard/web/controller/work/UavBaseController.java
@@ -6,13 +6,15 @@ import com.ard.work.domain.dto.BoxHistoryDTO; import com.ard.work.domain.dto.BoxPhotosDTO; import com.ard.work.domain.dto.BoxSettingsDTO; import com.ard.work.domain.entity.ArdDroneInfo; import com.ard.work.domain.dto.telemetry.TelemetryData; import com.ard.work.domain.entity.ArdFlightTask; import com.ard.work.domain.entity.ArdFlightTaskPart; import com.ard.work.domain.entity.ArdMedia; import com.ard.work.domain.receiver.BoxHistoryReceiver; import com.ard.work.domain.receiver.BoxPhotosReceiver; import com.ard.work.domain.receiver.BoxInfoReceiver; import com.ard.work.domain.receiver.BoxPhotosReceiver; import com.ard.work.domain.receiver.BoxStreamUrlReceiver; import com.ard.work.service.IArdFlightTaskPartService; import com.ard.work.service.IArdFlightTaskService; import com.ard.work.service.IArdMediaService; import com.ard.work.service.ITyService; @@ -20,11 +22,13 @@ import com.github.xiaoymin.knife4j.annotations.ApiSort; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import java.util.List; import static com.ard.common.utils.PageUtils.startPage; import javax.annotation.Resource; import java.time.LocalDateTime; import java.util.Date; import java.util.List; /** * UavBaseController @@ -42,6 +46,8 @@ private ITyService tyService; @Resource private IArdFlightTaskService ardFlightTaskService; @Resource private IArdFlightTaskPartService ardFlightTaskPartService; @Resource private IArdMediaService ardMediaService; @@ -79,6 +85,15 @@ return getDataTable(list); } @ApiOperation("获取架次飞行轨迹") @ApiOperationSupport(order = 4) @GetMapping("/getCompleteFlightData") public TableDataInfo getCompleteFlightData(ArdFlightTaskPart ardFlightTaskPart){ startPage(); List<TelemetryData> completeFlightData = ardFlightTaskPartService.getCompleteFlightData(ardFlightTaskPart); return getDataTable(completeFlightData); } @ApiOperation("获取媒体数据") @ApiOperationSupport(order = 4) @GetMapping("/getMediaList") ard-work/src/main/java/com/ard/work/config/RouteStateMachineListener.java
@@ -31,5 +31,4 @@ log.info("boxSn={} 状态变更为 {}", boxSn, target); } } ard-work/src/main/java/com/ard/work/domain/cache/UavTelemetryDataCache.java
@@ -3,7 +3,9 @@ import com.ard.work.domain.dto.state.UavState; import com.ard.work.domain.dto.telemetry.TelemetryData; import java.util.Collection; import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiConsumer; /** * 无人机遥测数据缓存 @@ -31,12 +33,9 @@ cache.remove(boxSn); } // 定时任务使用:判断状遥测数据是否变化,但不更新缓存 public static boolean hasChanged(String boxSn, TelemetryData telemetryData) { TelemetryData oldTelemetryData= cache.get(boxSn); if (oldTelemetryData == null) { return true; // 没有缓存,算作变化 } return !oldTelemetryData.equals(telemetryData); // true = 有变化 // 遍历并处理每条 TelemetryData public static void forEach(BiConsumer<String, TelemetryData> consumer) { cache.forEach(consumer); } } ard-work/src/main/java/com/ard/work/domain/dto/telemetry/TelemetryData.java
@@ -2,12 +2,14 @@ import lombok.*; import java.io.Serializable; @Data @Builder @AllArgsConstructor @NoArgsConstructor @EqualsAndHashCode public class TelemetryData { public class TelemetryData implements Serializable { private double lng; // 经度 private double lat; // 纬度 ard-work/src/main/java/com/ard/work/domain/entity/ArdFlightTaskPart.java
New file @@ -0,0 +1,16 @@ package com.ard.work.domain.entity; import com.ard.common.core.domain.BaseEntity; import lombok.*; @EqualsAndHashCode(callSuper = true) @Data @Builder @NoArgsConstructor @AllArgsConstructor public class ArdFlightTaskPart extends BaseEntity { private Long id; private String boxSn; private Integer count; private byte[] dataPart; } ard-work/src/main/java/com/ard/work/domain/event/DeviceStatusEvent.java
New file @@ -0,0 +1,17 @@ package com.ard.work.domain.event; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @Data @Builder @NoArgsConstructor @AllArgsConstructor public class DeviceStatusEvent { private String boxSn; // 设备唯一标识符 @Builder.Default private boolean isOnline = false; // true表示在线,false表示离线 } ard-work/src/main/java/com/ard/work/listener/DeviceStatusEventListener.java
New file @@ -0,0 +1,38 @@ package com.ard.work.listener; import com.ard.work.domain.event.DeviceStatusEvent; import com.ard.work.service.IArdFlightTaskPartService; import lombok.extern.java.Log; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; /** * 无人机状态事件监听 * * @author lsy * @date 2025/12/19 */ @Component @Slf4j public class DeviceStatusEventListener { @Autowired private IArdFlightTaskPartService ardFlightTaskPartService; @EventListener public void handleDeviceStatusEvent(DeviceStatusEvent event) { String boxSn = event.getBoxSn(); boolean isOnline = event.isOnline(); if (isOnline) { log.info("设备 {} 已上线", boxSn); } else { log.info("设备 {} 已离线", boxSn); //将剩余轨迹分片数据入库 ardFlightTaskPartService.flushRemaining(boxSn); } } } ard-work/src/main/java/com/ard/work/mapper/ArdFlightTaskMapper.java
@@ -22,7 +22,6 @@ // 获取某个时间戳后的任务 List<ArdFlightTask> getTasksAfter(@Param("lastSyncTime") String lastSyncTime); // 获取最大 startTime Date findMaxStartTimeByServerId(String serverId); } ard-work/src/main/java/com/ard/work/mapper/ArdFlightTaskPartMapper.java
New file @@ -0,0 +1,26 @@ package com.ard.work.mapper; import com.ard.work.domain.entity.ArdFlightTaskPart; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import java.time.LocalDateTime; import java.util.Date; import java.util.List; @Mapper public interface ArdFlightTaskPartMapper { /** * 根据 taskId 查询分片 */ List<ArdFlightTaskPart> selectByTaskId(int taskId); /** * 根据 boxSn 和航线开始结束时间查询分片 * @return 对应时间段的分片列表 */ List<ArdFlightTaskPart> selectByTaskStartTime(ArdFlightTaskPart ardFlightTaskPart); int insert(ArdFlightTaskPart entity); } ard-work/src/main/java/com/ard/work/service/IArdFlightTaskPartService.java
New file @@ -0,0 +1,26 @@ package com.ard.work.service; import com.ard.work.domain.dto.telemetry.TelemetryData; import com.ard.work.domain.entity.ArdFlightTaskPart; import java.util.List; /** * 飞行任务分片服务接口 */ public interface IArdFlightTaskPartService { /** * 添加一条遥测数据到缓存 */ void addTelemetryData(TelemetryData telemetryData); /** * 将剩余缓存分片保存 */ void flushRemaining(String boxSn); /** * 获取完整航线轨迹(合并所有分片) */ List<TelemetryData> getCompleteFlightData(ArdFlightTaskPart ardFlightTaskPart); } ard-work/src/main/java/com/ard/work/service/impl/ArdDroneInfoServiceImpl.java
@@ -11,6 +11,7 @@ import com.ard.work.domain.dto.BoxSettingsDTO; import com.ard.work.domain.entity.ArdDroneInfo; import com.ard.work.domain.entity.ArdServerInfo; import com.ard.work.domain.event.DeviceStatusEvent; import com.ard.work.domain.receiver.BoxInfoReceiver; import com.ard.work.domain.receiver.BoxStreamUrlReceiver; import com.ard.work.mapper.ArdDroneInfoMapper; @@ -19,6 +20,7 @@ import com.ard.work.service.ITyService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; @@ -51,6 +53,10 @@ @Autowired private IArdServerInfoService ardServerInfoService; // 自动注入 ApplicationEventPublisher @Autowired private ApplicationEventPublisher eventPublisher; /** * 查询无人机盒子 @@ -282,44 +288,63 @@ /** * 同步盒子属性 */ @Scheduled(fixedRate = 1000 * 5) @Scheduled(fixedRate = 5000) public void syncBoxInfo() { ArdServerInfo ardServerInfo = new ArdServerInfo(); ardServerInfo.setUseDataScope(false); List<ArdServerInfo> servers = ardServerInfoService.selectArdServerInfoList(ardServerInfo); for (ArdServerInfo server : servers) { ArdDroneInfo droneInfo = new ArdDroneInfo(); droneInfo.setServerId(server.getId()); droneInfo.setUseDataScope(false); List<ArdDroneInfo> localList = selectArdDroneList(droneInfo); Map<String, ArdDroneInfo> localMap = localList.stream() .collect(Collectors.toMap(ArdDroneInfo::getBoxSn, drone -> drone)); Map<String, ArdDroneInfo> localMap = localList.stream().collect(Collectors.toMap(ArdDroneInfo::getBoxSn, d -> d)); List<BoxInfoReceiver> boxList = tyService.getBoxList(server.getId()); if (boxList == null) { updateArdDrone(ArdDroneInfo.builder().serverId(server.getId()).onLine(0).build()); // server 没有返回设备列表 → 全部离线 if (boxList == null || boxList.isEmpty()) { for (ArdDroneInfo local : localList) { if (local.getOnLine() != 0) { // 仅当状态变化才更新 local.setOnLine(0); ArdDroneInfoMapper.updateArdDrone(local); eventPublisher.publishEvent(new DeviceStatusEvent(local.getBoxSn(), false)); redisCache.deleteObject(CacheConstants.UAV_ONLINE_KEY + local.getBoxSn()); } } continue; } // server 返回设备列表 → 通过缓存判断状态 for (BoxInfoReceiver remote : boxList) { String boxSn = remote.getBoxSn(); String redisKey = CacheConstants.UAV_ONLINE_KEY + boxSn; boolean currentOnline = remote.getOnLine() == 1; // 只处理在线的 Redis if (remote.getOnLine() == 1) { redisCache.setCacheObject(redisKey, remote, 30, TimeUnit.SECONDS); // 获取上次状态 boolean lastOnline = redisCache.getCacheObject(redisKey) != null; // 如果状态变化才更新 if (lastOnline != currentOnline) { // 更新 Redis if (currentOnline) { redisCache.setCacheObject(redisKey, remote); } else { redisCache.deleteObject(redisKey); } // 数据库同步 // 发布事件 eventPublisher.publishEvent(new DeviceStatusEvent(boxSn, currentOnline)); // 更新数据库 ArdDroneInfo local = localMap.get(boxSn); if (local == null) continue; if (local != null) { local.setOnLine(currentOnline ? 1 : 0); updateArdDrone(local); } } } } } if (updateChangedAttributes(local, remote)) { updateArdDrone(local); // 只有真的有变化才更新数据库 } } } } /** * 更新 localBoxInfo 的字段,如果有变化返回 true,否则返回 false */ @@ -347,4 +372,5 @@ return changed; } } ard-work/src/main/java/com/ard/work/service/impl/ArdFlightTaskPartServiceImpl.java
New file @@ -0,0 +1,128 @@ package com.ard.work.service.impl; import cn.hutool.core.util.SerializeUtil; import com.ard.work.domain.cache.UavTelemetryDataCache; import com.ard.work.domain.dto.telemetry.TelemetryData; import com.ard.work.domain.entity.ArdFlightTaskPart; import com.ard.work.mapper.ArdFlightTaskPartMapper; import com.ard.work.service.IArdFlightTaskPartService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.concurrent.ConcurrentHashMap; /** * 轨迹分片处理业务 */ @Slf4j @Service @RequiredArgsConstructor public class ArdFlightTaskPartServiceImpl implements IArdFlightTaskPartService { private final ArdFlightTaskPartMapper ardflightTaskPartMapper; /** * 缓存:boxSn -> 当前分片数据 */ private final ConcurrentHashMap<String, List<TelemetryData>> cacheMap = new ConcurrentHashMap<>(); /** * 分片保存阈值条数 */ private static final int CHUNK_SIZE = 300; /** * 定时任务:每秒读取一次设备状态数据 * 注意:这里模拟读取状态,实际应该调用设备接口或缓存 */ @Scheduled(fixedRate = 1000) public void readTelemetryTask() { UavTelemetryDataCache.forEach((boxSn, telemetryData) -> { addTelemetryData(telemetryData); }); } /** * 添加一条遥测数据到缓存 */ @Override public void addTelemetryData(TelemetryData telemetryData) { String boxSn = telemetryData.getBoxSn(); List<TelemetryData> list = cacheMap.computeIfAbsent(boxSn, k -> new ArrayList<>()); list.add(telemetryData); // 当缓存条数达到阈值 或 时间间隔超过 CHUNK_INTERVAL 时,分片保存 if (list.size() >= CHUNK_SIZE) { saveChunk(boxSn); // taskId 可根据你的业务传入 } } /** * 分片保存缓存数据到数据库 */ @Async public void saveChunk(String boxSn) { List<TelemetryData> dataList = cacheMap.getOrDefault(boxSn, new ArrayList<>()); if (dataList.isEmpty()) { return; } try { byte[] blobData = SerializeUtil.serialize(dataList); ArdFlightTaskPart part = ArdFlightTaskPart.builder() .boxSn(boxSn) .count(dataList.size()) .dataPart(blobData) .build(); part.setCreateTime(new Date()); ardflightTaskPartMapper.insert(part); log.info("设备 {} 分片保存 {} 条飞行数据到数据库成功", boxSn, dataList.size()); // 清空缓存,更新上次保存时间 cacheMap.put(boxSn, new ArrayList<>()); } catch (Exception e) { log.error("设备 {} 分片保存飞行数据失败", boxSn, e); } } /** * 将剩余缓存分片保存 */ @Override public void flushRemaining(String boxSn) { List<TelemetryData> dataList = cacheMap.getOrDefault(boxSn, new ArrayList<>()); if (!dataList.isEmpty()) { saveChunk(boxSn); } } /** * 获取完整航线轨迹(合并所有分片) */ @Override public List<TelemetryData> getCompleteFlightData(ArdFlightTaskPart ardFlightTaskPart) { List<TelemetryData> allData = new ArrayList<>(); String boxSn = ardFlightTaskPart.getBoxSn(); List<ArdFlightTaskPart> parts = ardflightTaskPartMapper.selectByTaskStartTime(ardFlightTaskPart); for (ArdFlightTaskPart part : parts) { try { List<TelemetryData> partData = SerializeUtil.deserialize(part.getDataPart()); allData.addAll(partData); } catch (Exception e) { log.error("反序列化 boxSn={} 分片数据失败", boxSn, e); } } return allData; } } ard-work/src/main/java/com/ard/work/service/impl/ArdFlightTaskServiceServiceImpl.java
@@ -452,7 +452,6 @@ } } // 获取文件大小 private long getFileSizeByUrl(String fileUrl) { try { ard-work/src/main/java/com/ard/work/service/impl/TyServiceImpl.java
@@ -54,6 +54,7 @@ import com.ard.work.domain.message.UavControlType; import com.ard.work.domain.receiver.*; import com.ard.work.service.IArdDroneInfoService; import com.ard.work.service.IArdFlightTaskPartService; import com.ard.work.service.IArdServerInfoService; import com.ard.work.service.ITyService; import lombok.extern.slf4j.Slf4j; @@ -62,6 +63,7 @@ import javax.annotation.Resource; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -82,6 +84,8 @@ private TyClient tyClient; @Autowired private RouteStateMachineService stateMachineService; @Autowired private IArdFlightTaskPartService ardFlightTaskPartService; /** * 获取边缘服务器token信息 @@ -952,7 +956,8 @@ case (byte) 0xA9: // 处理无人机遥测消息 UavTelemetryDataCmd uavTelemetryDataCmd = (UavTelemetryDataCmd) tyCommand; UavTelemetryDataCache.put(uavTelemetryDataCmd.getTelemetryData()); TelemetryData telemetryData = uavTelemetryDataCmd.getTelemetryData(); UavTelemetryDataCache.put(telemetryData); //logPrint("无人机遥测", uavTelemetryDataCmd.getTelemetryData().getBoxSn()); //WebSocketUtil.sendMessageAll(uavTelemetryDataCmd.getTelemetryData()); break; ard-work/src/main/resources/mapper/ArdFlightTaskPartMapper.xml
New file @@ -0,0 +1,27 @@ <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.ard.work.mapper.ArdFlightTaskPartMapper"> <!-- 插入一条分片数据 --> <insert id="insert" parameterType="ArdFlightTaskPart" useGeneratedKeys="true" keyProperty="id"> INSERT INTO ard_flight_task_part (box_sn, count, data_part, create_time) VALUES (#{boxSn}, #{count}, #{dataPart}, #{createTime}) </insert> <select id="selectByTaskStartTime" resultType="ArdFlightTaskPart"> SELECT * FROM ard_flight_task_part u WHERE u.box_sn = #{boxSn} <if test="params.beginTime != null"><!-- 开始时间检索 --> AND u.create_time >= #{params.beginTime} </if> <if test="params.endTime != null"><!-- 结束时间检索 --> AND u.create_time <= #{params.endTime} </if> </select> </mapper>