liusuyi
2025-12-19 e0a6ad18bb69cdba48064327a439efca5673dcf1
增加无人机在线离线事件监听
增加飞行轨迹业务处理
7 files added
8 files modified
390 ■■■■■ changed files
ard-admin/src/main/java/com/ard/web/controller/work/UavBaseController.java 25 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/config/RouteStateMachineListener.java 1 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/domain/cache/UavTelemetryDataCache.java 13 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/domain/dto/telemetry/TelemetryData.java 4 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/domain/entity/ArdFlightTaskPart.java 16 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/domain/event/DeviceStatusEvent.java 17 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/listener/DeviceStatusEventListener.java 38 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/mapper/ArdFlightTaskMapper.java 1 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/mapper/ArdFlightTaskPartMapper.java 26 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/service/IArdFlightTaskPartService.java 26 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/service/impl/ArdDroneInfoServiceImpl.java 60 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/service/impl/ArdFlightTaskPartServiceImpl.java 128 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/service/impl/ArdFlightTaskServiceServiceImpl.java 1 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/service/impl/TyServiceImpl.java 7 ●●●●● patch | view | raw | blame | history
ard-work/src/main/resources/mapper/ArdFlightTaskPartMapper.xml 27 ●●●●● patch | view | raw | blame | history
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);
                } else {
                    redisCache.deleteObject(redisKey);
                }
                // 获取上次状态
                boolean lastOnline = redisCache.getCacheObject(redisKey) != null;
                // 数据库同步
                ArdDroneInfo local = localMap.get(boxSn);
                if (local == null) continue;
                if (updateChangedAttributes(local, remote)) {
                    updateArdDrone(local); // 只有真的有变化才更新数据库
                // 如果状态变化才更新
                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) {
                        local.setOnLine(currentOnline ? 1 : 0);
                        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 &gt;= #{params.beginTime}
        </if>
        <if test="params.endTime != null"><!-- 结束时间检索 -->
            AND u.create_time &lt;= #{params.endTime}
        </if>
    </select>
</mapper>