liusuyi
2025-12-26 6bdda41435c30affe62af2bea8861da916c0c56d
优化架次和媒体
7 files added
4 files modified
3 files deleted
1610 ■■■■■ changed files
ard-admin/src/main/java/com/ard/web/controller/work/ArdFlightTaskController.java 78 ●●●●● patch | view | raw | blame | history
ard-admin/src/main/java/com/ard/web/controller/work/UavBaseController.java 30 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/domain/entity/ArdMedia.java 30 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/domain/entity/ArdMediaPic.java 26 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/domain/entity/ArdMediaVideo.java 28 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/mapper/ArdMediaMapper.java 40 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/mapper/ArdMediaPicMapper.java 41 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/mapper/ArdMediaVideoMapper.java 36 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/service/IArdMediaService.java 20 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/service/impl/ArdFlightTaskServiceImpl.java 397 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/service/impl/ArdMediaServiceImpl.java 480 ●●●●● patch | view | raw | blame | history
ard-work/src/main/resources/mapper/ArdMediaMapper.xml 202 ●●●●● patch | view | raw | blame | history
ard-work/src/main/resources/mapper/ArdMediaPicMapper.xml 84 ●●●●● patch | view | raw | blame | history
ard-work/src/main/resources/mapper/ArdMediaVideoMapper.xml 118 ●●●●● patch | view | raw | blame | history
ard-admin/src/main/java/com/ard/web/controller/work/ArdFlightTaskController.java
New file
@@ -0,0 +1,78 @@
package com.ard.web.controller.work;
import com.ard.common.core.controller.BaseController;
import com.ard.common.core.page.TableDataInfo;
import com.ard.work.domain.dto.*;
import com.ard.work.domain.entity.ArdFlightTask;
import com.ard.work.domain.entity.ArdFlightTaskPart;
import com.ard.work.domain.entity.ArdMediaPic;
import com.ard.work.domain.entity.ArdMediaVideo;
import com.ard.work.service.IArdFlightTaskPartService;
import com.ard.work.service.IArdFlightTaskService;
import com.ard.work.service.IArdMediaService;
import com.ard.work.service.ITyService;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import com.github.xiaoymin.knife4j.annotations.ApiSort;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
/**
 * 飞行架次管理接口
 * ArdFlightController
 *
 * @author liusuyi
 * @date 2024年10月21日13:52
 **/
@Api(tags = "飞行架次管理接口")
@ApiSort(value = 2)
@RequestMapping("/uav/base")
@RestController
public class ArdFlightTaskController extends BaseController {
    @Resource
    private IArdFlightTaskService ardFlightTaskService;
    @Resource
    private IArdFlightTaskPartService ardFlightTaskPartService;
    @Resource
    private IArdMediaService ardMediaService;
    @ApiOperation("获取飞行架次")
    @ApiOperationSupport(order = 1)
    @GetMapping("/getFlightTask")
    public TableDataInfo getFlightTask(ArdFlightTask ardFlightTask) {
        startPage();
        List<ArdFlightTask> list = ardFlightTaskService.selectArdFlightTask(ardFlightTask);
        return getDataTable(list);
    }
    @ApiOperation("获取架次飞行轨迹")
    @ApiOperationSupport(order = 2)
    @GetMapping("/getCompleteFlightData")
    public TableDataInfo getCompleteFlightData(ArdFlightTaskPart ardFlightTaskPart) {
        List<UavFlightStateDTO> completeFlightData = ardFlightTaskPartService.getCompleteFlightData(ardFlightTaskPart);
        return getDataTable(completeFlightData);
    }
    @ApiOperation("获取架次图片数据")
    @ApiOperationSupport(order = 3)
    @GetMapping("/getMediaPicList")
    public TableDataInfo getMediaPicList(ArdMediaPic ardMedia) {
        startPage();
        List<ArdMediaPic> list = ardMediaService.selectArdMediaPicList(ardMedia);
        return getDataTable(list);
    }
    @ApiOperation("获取架次录像数据")
    @ApiOperationSupport(order = 4)
    @GetMapping("/getMediaVideoList")
    public TableDataInfo getMediaVideoList(ArdMediaVideo ardMedia) {
        startPage();
        List<ArdMediaVideo> list = ardMediaService.selectArdMediaVideoList(ardMedia);
        return getDataTable(list);
    }
}
ard-admin/src/main/java/com/ard/web/controller/work/UavBaseController.java
@@ -5,9 +5,7 @@
import com.ard.common.core.page.TableDataInfo;
import com.ard.work.domain.dto.*;
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.entity.*;
import com.ard.work.domain.receiver.*;
import com.ard.work.service.IArdFlightTaskPartService;
import com.ard.work.service.IArdFlightTaskService;
@@ -70,32 +68,6 @@
    public AjaxResult getBoxHistory(@RequestBody BoxHistoryDTO boxHistoryDTO) {
        List<BoxHistoryReceiver> boxHistory = tyService.getBoxHistory(boxHistoryDTO);
        return AjaxResult.success(boxHistory);
    }
    @ApiOperation("获取飞行架次")
    @ApiOperationSupport(order = 4)
    @GetMapping("/getFlightTask")
    public TableDataInfo getFlightTask(ArdFlightTask ardFlightTask) {
        startPage();
        List<ArdFlightTask> list = ardFlightTaskService.selectArdFlightTask(ardFlightTask);
        return getDataTable(list);
    }
    @ApiOperation("获取架次飞行轨迹")
    @ApiOperationSupport(order = 4)
    @GetMapping("/getCompleteFlightData")
    public TableDataInfo getCompleteFlightData(ArdFlightTaskPart ardFlightTaskPart) {
        List<UavFlightStateDTO> completeFlightData = ardFlightTaskPartService.getCompleteFlightData(ardFlightTaskPart);
        return getDataTable(completeFlightData);
    }
    @ApiOperation("获取媒体数据")
    @ApiOperationSupport(order = 4)
    @GetMapping("/getMediaList")
    public TableDataInfo getMediaList(ArdMedia ardMedia) {
        startPage();
        List<ArdMedia> list = ardMediaService.selectArdMediaList(ardMedia);
        return getDataTable(list);
    }
    @ApiOperation("获取任务照片")
ard-work/src/main/java/com/ard/work/domain/entity/ArdMedia.java
File was deleted
ard-work/src/main/java/com/ard/work/domain/entity/ArdMediaPic.java
New file
@@ -0,0 +1,26 @@
package com.ard.work.domain.entity;
import com.ard.common.core.domain.BaseEntity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.*;
import java.util.Date;
@EqualsAndHashCode(callSuper = true)
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ArdMediaPic extends BaseEntity {
    private Long id;
    private Integer taskId;
    private Long fileSize;
    private String fileMd5;
    private String mediaUrl;
    private String sourceUrl;
    private Double longitude;
    private Double latitude;
    private Double height;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date captureTime;
}
ard-work/src/main/java/com/ard/work/domain/entity/ArdMediaVideo.java
New file
@@ -0,0 +1,28 @@
package com.ard.work.domain.entity;
import com.ard.common.core.domain.BaseEntity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.*;
import java.util.Date;
@EqualsAndHashCode(callSuper = true)
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ArdMediaVideo extends BaseEntity {
    private Long id;
    private String name;
    private Long fileSize;
    private String mediaUrl;
    private String sourceUrl;
    private String thumbnailUrl;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date startTime;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date endTime;
    private Integer spanSeconds;
    private Integer taskId;
}
ard-work/src/main/java/com/ard/work/mapper/ArdMediaMapper.java
File was deleted
ard-work/src/main/java/com/ard/work/mapper/ArdMediaPicMapper.java
New file
@@ -0,0 +1,41 @@
package com.ard.work.mapper;
import com.ard.work.domain.entity.ArdMediaPic;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface ArdMediaPicMapper {
    /**
     * 查询图片列表(条件查询)
     */
    List<ArdMediaPic> selectPicList(ArdMediaPic query);
    /**
     * 根据任务ID查询图片
     */
    List<ArdMediaPic> selectPicByTaskId(@Param("taskId") Integer taskId);
    /**
     * 新增图片
     */
    int insertPic(ArdMediaPic pic);
    /**
     * 批量新增图片
     */
    int insertPicList(@Param("list") List<ArdMediaPic> list);
    /**
     * 根据ID删除图片
     */
    int deletePicById(@Param("id") String id);
    /**
     * 根据任务ID删除图片
     */
    int deletePicByTaskId(@Param("taskId") Integer taskId);
}
ard-work/src/main/java/com/ard/work/mapper/ArdMediaVideoMapper.java
New file
@@ -0,0 +1,36 @@
package com.ard.work.mapper;
import com.ard.work.domain.entity.ArdMediaVideo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface ArdMediaVideoMapper {
    /**
     * 查询视频列表(条件查询)
     */
    List<ArdMediaVideo> selectArdMediaVideoList(ArdMediaVideo query);
    /**
     * 根据任务ID查询视频
     */
    List<ArdMediaVideo> selectArdMediaVideoByTaskId(@Param("taskId") Integer taskId);
    /**
     * 新增视频
     */
    int insertArdMediaVideo(ArdMediaVideo video);
    /**
     * 根据ID删除视频
     */
    int deleteArdMediaVideoById(@Param("id") String id);
    /**
     * 根据任务ID删除视频
     */
    int deleteArdMediaVideoByTaskId(@Param("taskId") Integer taskId);
}
ard-work/src/main/java/com/ard/work/service/IArdMediaService.java
@@ -1,7 +1,8 @@
package com.ard.work.service;
import com.ard.work.domain.entity.ArdMedia;
import com.ard.work.domain.entity.ArdFlightTask;
import com.ard.work.domain.entity.ArdMediaPic;
import com.ard.work.domain.entity.ArdMediaVideo;
import java.util.List;
/**
@@ -10,11 +11,12 @@
 * Date: 2025年11月18日13:15
 **/
public interface IArdMediaService {
    /**
     * 查询媒体数据列表
     *
     * @param ardMedia 媒体数据
     * @return 媒体数据集合
     */
    public List<ArdMedia> selectArdMediaList(ArdMedia ardMedia);
    List<ArdMediaPic> selectArdMediaPicList(ArdMediaPic ardMediaPic);
    List<ArdMediaVideo> selectArdMediaVideoList(ArdMediaVideo ardMediaVideo);
    void insertPhotoToDatabase(ArdFlightTask flightTask, String serverId, String boxSn);
    void insertVideoToDatabase(ArdFlightTask flightTask, String serverId, String boxSn);
    void updatePhotoToDatabase(ArdFlightTask flightTask, String serverId, String boxSn);
    void updateVideoToDatabase(ArdFlightTask flightTask, String serverId, String boxSn);
}
ard-work/src/main/java/com/ard/work/service/impl/ArdFlightTaskServiceImpl.java
@@ -4,49 +4,23 @@
import com.ard.common.core.domain.entity.SysUser;
import com.ard.common.core.redis.RedisCache;
import com.ard.common.utils.DateUtils;
import com.ard.common.utils.file.MimeTypeUtils;
import com.ard.common.utils.uuid.IdUtils;
import com.ard.oss.service.impl.OssServiceContext;
import com.ard.work.component.forest.TyClient;
import com.ard.work.domain.dto.BoxHistoryDTO;
import com.ard.work.domain.dto.BoxPhotosDTO;
import com.ard.work.domain.dto.BoxVideosDTO;
import com.ard.work.domain.entity.ArdFlightTask;
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.BoxVideosReceiver;
import com.ard.work.domain.receiver.TyHttpCommonReceiver;
import com.ard.work.mapper.ArdFlightTaskMapper;
import com.ard.work.mapper.ArdMediaMapper;
import com.ard.work.service.IArdFlightTaskService;
import com.ard.work.service.IArdMediaService;
import com.ard.work.service.IArdServerInfoService;
import com.ard.work.service.control.UavControlService;
import com.ard.work.util.osSelect;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
@Slf4j
@Service
@@ -55,19 +29,16 @@
    @Resource
    private ArdFlightTaskMapper ardFlightTaskMapper;
    @Resource
    private ArdMediaMapper ardMediaMapper;
    @Resource
    private IArdServerInfoService ardServerInfoService;
    @Resource
    private TyClient tyClient;
    @Resource
    private OssServiceContext ossServiceContext;
    @Resource
    private ThreadPoolTaskExecutor threadPoolTaskExecutor;  // 使用线程池执行异步任务
    @Resource
    private UavControlService uavControlService;
    @Autowired
    @Resource
    private IArdMediaService ardMediaService;
    @Resource
    private RedisCache redisCache;
    @Resource
    private TyClient tyClient;
    /**
     * 查询飞行架次列表
     *
@@ -97,6 +68,12 @@
    private Date getLastSyncTimeForServer(String serverId) {
        Date maxStartTime = ardFlightTaskMapper.findMaxStartTimeByServerId(serverId);
        return maxStartTime != null ? maxStartTime : new Date(0);  // 如果没有数据,返回 Unix 纪元时间作为起点
    }
    // 调用接口获取飞行历史数据
    private List<BoxHistoryReceiver> getBoxHistory(String serverId, BoxHistoryDTO boxHistoryDTO) {
        TyHttpCommonReceiver<List<BoxHistoryReceiver>> boxHistory = tyClient.getBoxHistory(serverId, boxHistoryDTO);
        return boxHistory.getContent();
    }
    // 根据上次同步时间同步飞行历史
@@ -158,24 +135,23 @@
            // 插入新记录
            int rows = ardFlightTaskMapper.insertArdFlightTask(flightTask);
            if (rows > 0) {
                // 删除旧的媒体记录,防止重复插入
                ardMediaMapper.deleteArdMediaByTaskId(flightTask.getTaskId());
                // 插入视频
                insertVideoToDatabase(flightTask, serverId, boxHistoryReceiver.getBoxSn());
                ardMediaService.insertVideoToDatabase(flightTask, serverId, boxHistoryReceiver.getBoxSn());
                // 插入图片
                insertPhotoToDatabase(flightTask, serverId, boxHistoryReceiver.getBoxSn());
                ardMediaService.insertPhotoToDatabase(flightTask, serverId, boxHistoryReceiver.getBoxSn());
            }
        } else {
            // 如果存在,则更新记录
            log.debug("Updating task {}", boxHistoryReceiver.getTaskId());
            updateFlightTask(ardFlightTask, boxHistoryReceiver);
            // 增量更新视频
            updateVideoToDatabase(ardFlightTask, serverId, boxHistoryReceiver.getBoxSn());
            ardMediaService.updateVideoToDatabase(ardFlightTask, serverId, boxHistoryReceiver.getBoxSn());
            // 增量更新图片
            updatePhotoToDatabase(ardFlightTask, serverId, boxHistoryReceiver.getBoxSn());
            ardMediaService.updatePhotoToDatabase(ardFlightTask, serverId, boxHistoryReceiver.getBoxSn());
        }
    }
    private void updateFlightTask(ArdFlightTask local, BoxHistoryReceiver remote) {
        boolean needUpdate = false;
@@ -208,342 +184,5 @@
        }
    }
    // 调用接口获取飞行历史数据
    private List<BoxHistoryReceiver> getBoxHistory(String serverId, BoxHistoryDTO boxHistoryDTO) {
        TyHttpCommonReceiver<List<BoxHistoryReceiver>> boxHistory = tyClient.getBoxHistory(serverId, boxHistoryDTO);
        return boxHistory.getContent();
    }
    // 调用接口获取架次图片
    private List<BoxPhotosReceiver> getBoxPhotos(String serverId, BoxPhotosDTO boxPhotosDTO) {
        TyHttpCommonReceiver<List<BoxPhotosReceiver>> boxPhotos = tyClient.getBoxPhotos(serverId, boxPhotosDTO);
        return boxPhotos.getContent();
    }
    // 调用接口获取架次视频
    private List<BoxVideosReceiver> getBoxVideos(String serverId, BoxVideosDTO boxVideosDTO) {
        TyHttpCommonReceiver<List<BoxVideosReceiver>> boxVideos = tyClient.getBoxVideos(serverId, boxVideosDTO);
        return boxVideos.getContent();
    }
    private void insertPhotoToDatabase(ArdFlightTask flightTask, String serverId, String boxSn) {
        // 查询图片
        BoxPhotosDTO boxPhotosDTO = new BoxPhotosDTO();
        boxPhotosDTO.setTaskId(flightTask.getTaskId());
        boxPhotosDTO.setBoxSn(boxSn);
        boxPhotosDTO.setPageIndex(1);
        boxPhotosDTO.setPageSize(100);
        List<BoxPhotosReceiver> photoList = getBoxPhotos(serverId, boxPhotosDTO);
        if (photoList != null && !photoList.isEmpty()) {
            addPhotos(flightTask, photoList);
        }
    }
    private void insertVideoToDatabase(ArdFlightTask flightTask, String serverId, String boxSn) {
        BoxVideosDTO boxVideosDTO = new BoxVideosDTO();
        boxVideosDTO.setTaskId(flightTask.getTaskId());
        boxVideosDTO.setBoxSn(boxSn);
        boxVideosDTO.setPageIndex(1);
        boxVideosDTO.setPageSize(100);
        List<BoxVideosReceiver> videoList = getBoxVideos(serverId, boxVideosDTO);
        if (videoList != null && !videoList.isEmpty()) {
            addVideos(flightTask, videoList);
        }
    }
    private void updatePhotoToDatabase(ArdFlightTask flightTask, String serverId, String boxSn) {
        // 获取本地数据库中已存在的图片记录(类型为 0 表示图片)
        List<ArdMedia> existingPhotos = ardMediaMapper.selectArdMediaByTaskIdAndType(flightTask.getTaskId(), 0);
        int count = existingPhotos.size(); //获取本地库图片数量
        // 查询图片
        BoxPhotosDTO boxPhotosDTO = BoxPhotosDTO.builder()
                .taskId(flightTask.getTaskId())
                .boxSn(boxSn)
                .pageIndex(1)
                .pageSize(500)
                .build();
        TyHttpCommonReceiver<List<BoxPhotosReceiver>> boxPhotos = tyClient.getBoxPhotos(serverId, boxPhotosDTO);
        List<BoxPhotosReceiver> photoList = boxPhotos.getContent();
        int size = photoList.size();
        if (size > count) {
            log.info("任务【{}】图片数量不一致,需要更新数据库", flightTask.getTaskId());
            // 需要插入新的视频
            // 提取现有的图片路径(根据 imgUrl 来判断是否重复)
            List<String> existingPhotoUrls = existingPhotos.stream()
                    .map(ArdMedia::getSourceUrl) // 提取 ArdMedia 中的图片 URL
                    .collect(Collectors.toList());
            // 筛选出 photoList 中没有出现在 existingPhotos 中的新图片
            List<BoxPhotosReceiver> newPhotos = photoList.stream()
                    .filter(photo -> !existingPhotoUrls.contains(photo.getImgUrl())) // 过滤出新图片
                    .collect(Collectors.toList());
            if (!newPhotos.isEmpty()) {
                // 插入新的视频记录
                addPhotos(flightTask, newPhotos);
                // 更新任务的视频数量
                flightTask.setPhotoCount(flightTask.getPhotoCount() + newPhotos.size());
                ardFlightTaskMapper.updateArdFlightTask(flightTask);
            }
        }
    }
    private void updateVideoToDatabase(ArdFlightTask flightTask, String serverId, String boxSn) {
        // 获取本地数据库中已存在的图片记录(类型为 0 表示图片)
        List<ArdMedia> existingVideos = ardMediaMapper.selectArdMediaByTaskIdAndType(flightTask.getTaskId(), 1);
        int count = existingVideos.size(); //获取本地库视频数量
        // 查询图片
        BoxVideosDTO boxVideosDTO = BoxVideosDTO.builder()
                .taskId(flightTask.getTaskId())
                .boxSn(boxSn)
                .pageIndex(1)
                .pageSize(500)
                .build();
        TyHttpCommonReceiver<List<BoxVideosReceiver>> boxVideos = tyClient.getBoxVideos(serverId, boxVideosDTO);
        List<BoxVideosReceiver> videoList = boxVideos.getContent();
        int size = videoList.size();
        if (size > count) {
            log.info("任务【{}】视频数量不一致,需要更新数据库", flightTask.getTaskId());
            // 需要插入新的视频
            // 提取现有的图片路径(根据 imgUrl 来判断是否重复)
            List<String> existingVideoUrls = existingVideos.stream()
                    .map(ArdMedia::getSourceUrl) // 提取 ArdMedia 中的图片 URL
                    .collect(Collectors.toList());
            // 筛选出 photoList 中没有出现在 existingPhotos 中的新图片
            List<BoxVideosReceiver> newVideos = videoList.stream()
                    .filter(photo -> !existingVideoUrls.contains(photo.getImgUrl())) // 过滤出新视频
                    .collect(Collectors.toList());
            if (!newVideos.isEmpty()) {
                // 插入新的视频记录
                addVideos(flightTask, newVideos);
                // 更新任务的视频数量
                flightTask.setVideoCount(flightTask.getPhotoCount() + newVideos.size());
                ardFlightTaskMapper.updateArdFlightTask(flightTask);
            }
        }
    }
    private void addPhotos(ArdFlightTask flightTask, List<BoxPhotosReceiver> photoList) {
        String boxSn = flightTask.getBoxSn();
        for (BoxPhotosReceiver photo : photoList) {
            ArdMedia ardMedia = ArdMedia.builder()
                    .id(IdUtils.simpleUUID())
                    .taskId(flightTask.getTaskId())
                    .mediaType(0) // 0表示图片
                    .fileSize(photo.getFileSize())
                    .fileMd5(photo.getFileMd5())
                    .sourceUrl(photo.getImgUrl())
                    .photoId(photo.getPhotoId())
                    .longitude(photo.getLng())
                    .latitude(photo.getLat())
                    .height(photo.getHeight())
                    .captureTime(DateUtils.dateTime(DateUtils.YYYY_MM_DD_HH_MM_SS, photo.getTimeStamp()))
                    .build();
            // 异步上传文件到OSS,并在上传完成后插入数据库
            CompletableFuture.supplyAsync(() -> {
                        try (InputStream inputStream = openUrlInputStream(photo.getImgUrl(), 5000, 10000)) {
                            if (inputStream != null) {
                                return ossServiceContext.upload(
                                        "pic",
                                        boxSn + "/" + ardMedia.getId() + ".jpg",
                                        inputStream,
                                        MimeTypeUtils.IMAGE_JPG
                                );
                            }
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                        return null;
                    }, threadPoolTaskExecutor)
                    .thenAccept(url -> {
                        ardMedia.setMediaUrl(url);
                        ardMedia.setCreateTime(new Date());
                        ardMediaMapper.insertArdMedia(ardMedia);
                    });
        }
    }
    private void addVideos(ArdFlightTask flightTask, List<BoxVideosReceiver> videoList) {
        String boxSn = flightTask.getBoxSn();
        for (BoxVideosReceiver video : videoList) {
            ArdMedia ardMedia = ArdMedia.builder()
                    .id(IdUtils.simpleUUID())
                    .taskId(flightTask.getTaskId())
                    .mediaType(1) // 1表示视频
                    .fileSize(getFileSizeByUrl(video.getImgUrl()))
                    .sourceUrl(video.getImgUrl())
                    .captureTime(DateUtils.parseDate(video.getStartTime()))
                    .build();
            // 异步上传文件到OSS,并在上传完成后插入数据库
            // ---- 1. 视频上传(失败不抛异常,返回 null) ----
            CompletableFuture<String> videoFuture = CompletableFuture.supplyAsync(() -> {
                try (InputStream inputStream = openUrlInputStream(ardMedia.getSourceUrl(), 5000, 10000)) {
                    if (inputStream != null) {
                        return ossServiceContext.upload(
                                "record",
                                boxSn + "/" + ardMedia.getId() + ".mp4",
                                inputStream,
                                MimeTypeUtils.VIDEO_MP4
                        );
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return null;
            }, threadPoolTaskExecutor);
            // ---- 2. 缩略图上传(失败不抛异常,返回 null) ----
            CompletableFuture<String> thumbFuture = CompletableFuture.supplyAsync(() -> {
                try {
                    try (ByteArrayInputStream thumbStream = generateThumbnail(ardMedia.getSourceUrl(), 1, 200)) {
                        if (thumbStream != null) {
                            return ossServiceContext.upload(
                                    "record",
                                    boxSn + "/" + ardMedia.getId() + ".jpg",
                                    thumbStream,
                                    MimeTypeUtils.IMAGE_JPEG
                            );
                        }
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return null;
            }, threadPoolTaskExecutor);
            // 两个完成后写数据库(任意失败不阻断流程)
            videoFuture.thenCombine(thumbFuture, (vUrl, tUrl) -> {
                ardMedia.setMediaUrl(vUrl);
                ardMedia.setThumbnailUrl(tUrl);
                ardMedia.setCreateTime(new Date());
                return ardMedia;
            }).thenAcceptAsync(ardMediaMapper::insertArdMedia, threadPoolTaskExecutor);
        }
    }
    // 获取文件大小
    private long getFileSizeByUrl(String fileUrl) {
        try {
            URL url = new URL(fileUrl);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod("HEAD");
            conn.connect();
            return conn.getContentLengthLong(); // 返回字节大小
        } catch (Exception e) {
            return -1;
        }
    }
    // 生成视频缩略图
    public ByteArrayInputStream generateThumbnail(String videoFile, int skipSeconds, int targetWidth) {
        String ffmpegPath = "";
        if (osSelect.isWindows()) {
            ffmpegPath = "ffmpeg";
        } else if (osSelect.isLinux()) {
            ffmpegPath = "/usr/local/ffmpeg/bin/ffmpeg";
        }
        try {
            List<String> command = new ArrayList<>();
            command.add(ffmpegPath);
            command.add("-timeout");
            command.add("3000000");
            command.add("-rw_timeout");
            command.add("3000000");
            command.add("-ss");
            command.add(String.format("00:00:%02d", skipSeconds));
            command.add("-i");
            command.add(videoFile);
            command.add("-vframes");
            command.add("1");
            // 等比缩放
            command.add("-vf");
            command.add("scale=" + targetWidth + ":-1");
            // **确保输出 JPEG**
            command.add("-f");
            command.add("mjpeg");      // 关键点:输出格式固定为 JPEG
            command.add("-vcodec");
            command.add("mjpeg");
            // 输出到管道
            command.add("pipe:1");
            // ⭐ 打印最终命令(可复制)
            log.info("ffmpeg command: {}", String.join(" ", command));
            ProcessBuilder pb = new ProcessBuilder(command);
            pb.redirectErrorStream(false);
            Process process = pb.start();
            // 读取 FFmpeg 输出(JPEG 数据)到内存
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            try (InputStream is = process.getInputStream()) {
                byte[] buffer = new byte[8192];
                int len;
                while ((len = is.read(buffer)) != -1) {
                    baos.write(buffer, 0, len);
                }
            }
            int exitCode = process.waitFor();
            if (exitCode != 0) {
                throw new RuntimeException("FFmpeg 执行失败,退出码: " + exitCode);
            }
            return new ByteArrayInputStream(baos.toByteArray());
        } catch (IOException | InterruptedException e) {
            throw new RuntimeException("FFmpeg 生成缩略图失败", e);
        }
    }
    /**
     * 将 URL 转成 InputStream
     *
     * @param fileUrl              文件 URL
     * @param connectTimeoutMillis 连接超时(毫秒)
     * @param readTimeoutMillis    读取超时(毫秒)
     * @return InputStream,如果失败返回 null
     */
    public InputStream openUrlInputStream(String fileUrl, int connectTimeoutMillis, int readTimeoutMillis) {
        try {
            URL url = new URL(fileUrl);
            // 支持 HTTPS 信任所有证书(如果需要访问自签名证书)
            if ("https".equalsIgnoreCase(url.getProtocol())) {
                TrustManager[] trustAllCerts = new TrustManager[]{
                        new X509TrustManager() {
                            @Override
                            public void checkClientTrusted(X509Certificate[] chain, String authType) {
                            }
                            @Override
                            public void checkServerTrusted(X509Certificate[] chain, String authType) {
                            }
                            @Override
                            public X509Certificate[] getAcceptedIssuers() {
                                return null;
                            }
                        }
                };
                SSLContext sc = SSLContext.getInstance("SSL");
                sc.init(null, trustAllCerts, new java.security.SecureRandom());
                HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
                HttpsURLConnection.setDefaultHostnameVerifier((hostname, session) -> true);
            }
            URLConnection connection = url.openConnection();
            connection.setConnectTimeout(connectTimeoutMillis);
            connection.setReadTimeout(readTimeoutMillis);
            return connection.getInputStream();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}
ard-work/src/main/java/com/ard/work/service/impl/ArdMediaServiceImpl.java
@@ -1,31 +1,493 @@
package com.ard.work.service.impl;
import com.ard.work.domain.entity.ArdMedia;
import com.ard.work.mapper.ArdMediaMapper;
import com.ard.common.utils.DateUtils;
import com.ard.common.utils.file.MimeTypeUtils;
import com.ard.oss.service.impl.OssServiceContext;
import com.ard.work.component.forest.TyClient;
import com.ard.work.domain.dto.BoxPhotosDTO;
import com.ard.work.domain.dto.BoxVideosDTO;
import com.ard.work.domain.entity.ArdFlightTask;
import com.ard.work.domain.entity.ArdMediaPic;
import com.ard.work.domain.entity.ArdMediaVideo;
import com.ard.work.domain.receiver.BoxPhotosReceiver;
import com.ard.work.domain.receiver.BoxVideosReceiver;
import com.ard.work.domain.receiver.TyHttpCommonReceiver;
import com.ard.work.mapper.ArdFlightTaskMapper;
import com.ard.work.mapper.ArdMediaPicMapper;
import com.ard.work.mapper.ArdMediaVideoMapper;
import com.ard.work.service.IArdMediaService;
import com.ard.work.util.osSelect;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
/**
 * Description:
 * Author: lsy
 * Date: 2025年11月18日13:17
 **/
@Slf4j
@Service
public class ArdMediaServiceImpl implements IArdMediaService {
    @Resource
    private ArdMediaMapper ardMediaMapper;
    private TyClient tyClient;
    @Resource
    private ArdMediaPicMapper ardMediaPicMapper;
    @Resource
    private ArdMediaVideoMapper ardMediaVideoMapper;
    @Resource
    private OssServiceContext ossServiceContext;
    @Resource
    private ArdFlightTaskMapper ardFlightTaskMapper;
    @Resource
    private ThreadPoolTaskExecutor threadPoolTaskExecutor;
    // 调用接口获取架次图片
    private List<BoxPhotosReceiver> getBoxPhotos(String serverId, BoxPhotosDTO boxPhotosDTO) {
        TyHttpCommonReceiver<List<BoxPhotosReceiver>> boxPhotos = tyClient.getBoxPhotos(serverId, boxPhotosDTO);
        return boxPhotos.getContent();
    }
    // 调用接口获取架次视频
    private List<BoxVideosReceiver> getBoxVideos(String serverId, BoxVideosDTO boxVideosDTO) {
        TyHttpCommonReceiver<List<BoxVideosReceiver>> boxVideos = tyClient.getBoxVideos(serverId, boxVideosDTO);
        return boxVideos.getContent();
    }
    @Override
    public List<ArdMediaPic> selectArdMediaPicList(ArdMediaPic ardMediaPic) {
        return ardMediaPicMapper.selectPicList(ardMediaPic);
    }
    @Override
    public List<ArdMediaVideo> selectArdMediaVideoList(ArdMediaVideo ardMediaVideo) {
        return ardMediaVideoMapper.selectArdMediaVideoList(ardMediaVideo);
    }
    @Override
    public void insertPhotoToDatabase(ArdFlightTask flightTask, String serverId, String boxSn) {
        log.warn("开始获取图片信息");
        // 查询图片
        BoxPhotosDTO boxPhotosDTO = new BoxPhotosDTO();
        boxPhotosDTO.setTaskId(flightTask.getTaskId());
        boxPhotosDTO.setBoxSn(boxSn);
        boxPhotosDTO.setPageIndex(1);
        boxPhotosDTO.setPageSize(100);
        List<BoxPhotosReceiver> photoList = getBoxPhotos(serverId, boxPhotosDTO);
        if (photoList != null && !photoList.isEmpty()) {
            addPhotos(flightTask, photoList);
        }
    }
    @Override
    public void insertVideoToDatabase(ArdFlightTask flightTask, String serverId, String boxSn) {
        log.warn("开始获取视频信息");
        BoxVideosDTO boxVideosDTO = new BoxVideosDTO();
        boxVideosDTO.setTaskId(flightTask.getTaskId());
        boxVideosDTO.setBoxSn(boxSn);
        boxVideosDTO.setPageIndex(1);
        boxVideosDTO.setPageSize(100);
        List<BoxVideosReceiver> videoList = getBoxVideos(serverId, boxVideosDTO);
        if (videoList != null && !videoList.isEmpty()) {
            addVideos(flightTask, videoList);
        }
    }
    @Override
    public void updatePhotoToDatabase(ArdFlightTask flightTask, String serverId, String boxSn) {
        // 获取本地数据库中已存在的图片记录(类型为 0 表示图片)
        List<ArdMediaPic> existingPhotos = ardMediaPicMapper.selectPicByTaskId(flightTask.getTaskId());
        int count = existingPhotos.size(); //获取本地库图片数量
        // 查询图片
        BoxPhotosDTO boxPhotosDTO = BoxPhotosDTO.builder()
                .taskId(flightTask.getTaskId())
                .boxSn(boxSn)
                .pageIndex(1)
                .pageSize(500)
                .build();
        TyHttpCommonReceiver<List<BoxPhotosReceiver>> boxPhotos = tyClient.getBoxPhotos(serverId, boxPhotosDTO);
        List<BoxPhotosReceiver> photoList = boxPhotos.getContent();
        int size = photoList.size();
        if (size > count) {
            log.info("任务【{}】图片数量不一致,需要更新数据库", flightTask.getTaskId());
            // 需要插入新的视频
            // 提取现有的图片路径(根据 imgUrl 来判断是否重复)
            List<String> existingPhotoUrls = existingPhotos.stream()
                    .map(ArdMediaPic::getSourceUrl) // 提取 ArdMediaPic 中的图片 URL
                    .collect(Collectors.toList());
            // 筛选出 photoList 中没有出现在 existingPhotos 中的新图片
            List<BoxPhotosReceiver> newPhotos = photoList.stream()
                    .filter(photo -> !existingPhotoUrls.contains(photo.getImgUrl())) // 过滤出新图片
                    .collect(Collectors.toList());
            if (!newPhotos.isEmpty()) {
                // 插入新的视频记录
                addPhotos(flightTask, newPhotos);
                // 更新任务的视频数量
                flightTask.setPhotoCount(flightTask.getPhotoCount() + newPhotos.size());
                ardFlightTaskMapper.updateArdFlightTask(flightTask);
            }
        }
    }
    @Override
    public void updateVideoToDatabase(ArdFlightTask flightTask, String serverId, String boxSn) {
        // 获取本地数据库中已存在的视频记录
        List<ArdMediaVideo> existingVideos = ardMediaVideoMapper.selectArdMediaVideoByTaskId(flightTask.getTaskId());
        int count = existingVideos.size(); //获取本地库视频数量
        // 查询图片
        BoxVideosDTO boxVideosDTO = BoxVideosDTO.builder()
                .taskId(flightTask.getTaskId())
                .boxSn(boxSn)
                .pageIndex(1)
                .pageSize(500)
                .build();
        TyHttpCommonReceiver<List<BoxVideosReceiver>> boxVideos = tyClient.getBoxVideos(serverId, boxVideosDTO);
        List<BoxVideosReceiver> videoList = boxVideos.getContent();
        int size = videoList.size();
        if (size > count) {
            log.info("任务【{}】视频数量不一致,需要更新数据库", flightTask.getTaskId());
            // 需要插入新的视频
            // 提取现有的图片路径(根据 imgUrl 来判断是否重复)
            List<String> existingVideoUrls = existingVideos.stream()
                    .map(ArdMediaVideo::getSourceUrl) // 提取 ArdMedia 中的视频 URL
                    .collect(Collectors.toList());
            // 筛选出 photoList 中没有出现在 existingPhotos 中的新图片
            List<BoxVideosReceiver> newVideos = videoList.stream()
                    .filter(photo -> !existingVideoUrls.contains(photo.getImgUrl())) // 过滤出新视频
                    .collect(Collectors.toList());
            if (!newVideos.isEmpty()) {
                // 插入新的视频记录
                addVideos(flightTask, newVideos);
                // 更新任务的视频数量
                flightTask.setVideoCount(flightTask.getVideoCount() + newVideos.size());
                ardFlightTaskMapper.updateArdFlightTask(flightTask);
            }
        }
    }
    private void addPhotos1(ArdFlightTask flightTask, List<BoxPhotosReceiver> photoList) {
        log.warn("开始添加图片");
        String boxSn = flightTask.getBoxSn();
        for (BoxPhotosReceiver photo : photoList) {
            ArdMediaPic ardMedia = ArdMediaPic.builder()
                    .id(photo.getPhotoId())
                    .taskId(flightTask.getTaskId())
                    .fileSize(photo.getFileSize())
                    .fileMd5(photo.getFileMd5())
                    .sourceUrl(photo.getImgUrl())
                    .longitude(photo.getLng())
                    .latitude(photo.getLat())
                    .height(photo.getHeight())
                    .captureTime(DateUtils.parseDate(photo.getTimeStamp()))
                    .build();
            String url = "";
            try {
                InputStream inputStream = openUrlInputStream(photo.getImgUrl(), 5000, 10000);
                if (inputStream != null) {
                    url = ossServiceContext.upload(
                            "pic",
                            boxSn + "/" + ardMedia.getId() + ".jpg",
                            inputStream,
                            MimeTypeUtils.IMAGE_JPG
                    );
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            ardMedia.setMediaUrl(url);
            ardMedia.setCreateTime(new Date());
            ardMediaPicMapper.insertPic(ardMedia);
        }
    }
    private void addPhotos(ArdFlightTask flightTask, List<BoxPhotosReceiver> photoList) {
        log.warn("开始添加图片");
        String boxSn = flightTask.getBoxSn();
        for (BoxPhotosReceiver photo : photoList) {
            CompletableFuture.runAsync(() -> {
                ArdMediaPic ardMedia = ArdMediaPic.builder()
                        .id(photo.getPhotoId())
                        .taskId(flightTask.getTaskId())
                        .fileSize(photo.getFileSize())
                        .fileMd5(photo.getFileMd5())
                        .sourceUrl(photo.getImgUrl())
                        .longitude(photo.getLng())
                        .latitude(photo.getLat())
                        .height(photo.getHeight())
                        .captureTime(DateUtils.parseDate(photo.getTimeStamp()))
                        .build();
                String url = "";
                try (InputStream inputStream = openUrlInputStream(photo.getImgUrl(), 5000, 10000)) {
                    if (inputStream != null) {
                        url = ossServiceContext.upload(
                                "pic",
                                boxSn + "/" + ardMedia.getId() + ".jpg",
                                inputStream,
                                MimeTypeUtils.IMAGE_JPG
                        );
                    }
                } catch (Exception e) {
                    log.error("图片上传失败 taskId={}, photoId={}", flightTask.getTaskId(), photo.getPhotoId(), e);
                }
                ardMedia.setMediaUrl(url);
                ardMedia.setCreateTime(new Date());
                ardMediaPicMapper.insertPic(ardMedia);
            }, threadPoolTaskExecutor); // 使用自定义线程池
        }
    }
    private void addVideos1(ArdFlightTask flightTask, List<BoxVideosReceiver> videoList) {
        log.warn("开始添加视频");
        String boxSn = flightTask.getBoxSn();
        for (BoxVideosReceiver video : videoList) {
            ArdMediaVideo ardMedia = ArdMediaVideo.builder()
                    .id(video.getId())
                    .name(video.getName())
                    .taskId(flightTask.getTaskId())
                    .fileSize(getFileSizeByUrl(video.getImgUrl()))
                    .sourceUrl(video.getImgUrl())
                    .startTime(DateUtils.parseDate(video.getStartTime()))
                    .endTime(DateUtils.parseDate(video.getEndTime()))
                    .spanSeconds(Integer.parseInt(video.getSpanSeconds()))
                    .build();
            // 视频上传
            String videoUrl = null;
            try (InputStream videoStream = openUrlInputStream(ardMedia.getSourceUrl(), 5000, 10000)) {
                if (videoStream != null) {
                    videoUrl = ossServiceContext.upload("record", boxSn + "/" + ardMedia.getId() + ".mp4", videoStream, MimeTypeUtils.VIDEO_MP4);
                }
            } catch (Exception e) {
                log.error("视频上传失败 taskId={}, videoId={}", flightTask.getTaskId(), video.getId(), e);
            }
            // 缩略图上传(失败不影响视频入库)
            String thumbUrl = null;
            try (ByteArrayInputStream thumbStream = generateThumbnail(ardMedia.getSourceUrl(), 1, 200)) {
                if (thumbStream != null) {
                    thumbUrl = ossServiceContext.upload("record", boxSn + "/" + ardMedia.getId() + ".jpg", thumbStream, MimeTypeUtils.IMAGE_JPEG);
                }
            } catch (Exception e) {
                log.warn("缩略图生成或上传失败 taskId={}, videoId={}", flightTask.getTaskId(), video.getId(), e);
            }
            // 入库(即使缩略图为空也入库)
            ardMedia.setMediaUrl(videoUrl);
            ardMedia.setThumbnailUrl(thumbUrl);
            ardMedia.setCreateTime(new Date());
            ardMediaVideoMapper.insertArdMediaVideo(ardMedia);
        }
    }
    private void addVideos(ArdFlightTask flightTask, List<BoxVideosReceiver> videoList) {
        String boxSn = flightTask.getBoxSn();
        for (BoxVideosReceiver video : videoList) {
            CompletableFuture.runAsync(() -> {
                ArdMediaVideo ardMedia = ArdMediaVideo.builder()
                        .id(video.getId())
                        .name(video.getName())
                        .taskId(flightTask.getTaskId())
                        .fileSize(getFileSizeByUrl(video.getImgUrl()))
                        .sourceUrl(video.getImgUrl())
                        .startTime(DateUtils.parseDate(video.getStartTime()))
                        .endTime(DateUtils.parseDate(video.getEndTime()))
                        .spanSeconds(Integer.parseInt(video.getSpanSeconds()))
                        .build();
                // 视频上传
                String videoUrl = null;
                try (InputStream videoStream = openUrlInputStream(ardMedia.getSourceUrl(), 5000, 10000)) {
                    if (videoStream != null) {
                        videoUrl = ossServiceContext.upload("record",
                                boxSn + "/" + ardMedia.getId() + ".mp4",
                                videoStream,
                                MimeTypeUtils.VIDEO_MP4);
                    }
                } catch (Exception e) {
                    log.error("视频上传失败 taskId={}, videoId={}", flightTask.getTaskId(), video.getId(), e);
                }
                // 缩略图上传(失败不影响视频入库)
                String thumbUrl = null;
                try (ByteArrayInputStream thumbStream = generateThumbnail(ardMedia.getSourceUrl(), 1, 200)) {
                    if (thumbStream != null) {
                        thumbUrl = ossServiceContext.upload("record",
                                boxSn + "/" + ardMedia.getId() + ".jpg",
                                thumbStream,
                                MimeTypeUtils.IMAGE_JPEG);
                    }
                } catch (Exception e) {
                    log.warn("缩略图生成或上传失败 taskId={}, videoId={}", flightTask.getTaskId(), video.getId(), e);
                }
                // 入库(即使缩略图为空也入库)
                ardMedia.setMediaUrl(videoUrl);
                ardMedia.setThumbnailUrl(thumbUrl);
                ardMedia.setCreateTime(new Date());
                ardMediaVideoMapper.insertArdMediaVideo(ardMedia);
            }, threadPoolTaskExecutor);
        }
    }
    // 获取文件大小
    private long getFileSizeByUrl(String fileUrl) {
        try {
            URL url = new URL(fileUrl);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod("HEAD");
            conn.connect();
            return conn.getContentLengthLong(); // 返回字节大小
        } catch (Exception e) {
            return -1;
        }
    }
    // 生成视频缩略图
    public ByteArrayInputStream generateThumbnail(String videoFile, int skipSeconds, int targetWidth) {
        String ffmpegPath = "";
        if (osSelect.isWindows()) {
            ffmpegPath = "ffmpeg";
        } else if (osSelect.isLinux()) {
            ffmpegPath = "/usr/local/ffmpeg/bin/ffmpeg";
        }
        try {
            List<String> command = new ArrayList<>();
            command.add(ffmpegPath);
            command.add("-timeout");
            command.add("3000000");
            command.add("-rw_timeout");
            command.add("3000000");
            command.add("-ss");
            command.add(String.format("00:00:%02d", skipSeconds));
            command.add("-i");
            command.add(videoFile);
            command.add("-vframes");
            command.add("1");
            // 等比缩放
            command.add("-vf");
            command.add("scale=" + targetWidth + ":-1");
            // **确保输出 JPEG**
            command.add("-f");
            command.add("mjpeg");      // 关键点:输出格式固定为 JPEG
            command.add("-vcodec");
            command.add("mjpeg");
            // 输出到管道
            command.add("pipe:1");
            // ⭐ 打印最终命令(可复制)
            log.info("ffmpeg command: {}", String.join(" ", command));
            ProcessBuilder pb = new ProcessBuilder(command);
            pb.redirectErrorStream(false);
            Process process = pb.start();
            // 读取 FFmpeg 输出(JPEG 数据)到内存
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            try (InputStream is = process.getInputStream()) {
                byte[] buffer = new byte[8192];
                int len;
                while ((len = is.read(buffer)) != -1) {
                    baos.write(buffer, 0, len);
                }
            }
            int exitCode = process.waitFor();
            if (exitCode != 0) {
                throw new RuntimeException("FFmpeg 执行失败,退出码: " + exitCode);
            }
            return new ByteArrayInputStream(baos.toByteArray());
        } catch (IOException | InterruptedException e) {
            throw new RuntimeException("FFmpeg 生成缩略图失败", e);
        }
    }
    /**
     * 查询媒体数据列表
     * 将 URL 转成 InputStream
     *
     * @param ardMedia 媒体数据
     * @return 媒体数据集合
     * @param fileUrl              文件 URL
     * @param connectTimeoutMillis 连接超时(毫秒)
     * @param readTimeoutMillis    读取超时(毫秒)
     * @return InputStream,如果失败返回 null
     */
    @Override
    public List<ArdMedia> selectArdMediaList(ArdMedia ardMedia) {
        return ardMediaMapper.selectArdMedia(ardMedia);
    public InputStream openUrlInputStream(String fileUrl, int connectTimeoutMillis, int readTimeoutMillis) {
        try {
            URL url = new URL(fileUrl);
            // 支持 HTTPS 信任所有证书(如果需要访问自签名证书)
            if ("https".equalsIgnoreCase(url.getProtocol())) {
                TrustManager[] trustAllCerts = new TrustManager[]{
                        new X509TrustManager() {
                            @Override
                            public void checkClientTrusted(X509Certificate[] chain, String authType) {
                            }
                            @Override
                            public void checkServerTrusted(X509Certificate[] chain, String authType) {
                            }
                            @Override
                            public X509Certificate[] getAcceptedIssuers() {
                                return null;
                            }
                        }
                };
                SSLContext sc = SSLContext.getInstance("SSL");
                sc.init(null, trustAllCerts, new java.security.SecureRandom());
                HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
                HttpsURLConnection.setDefaultHostnameVerifier((hostname, session) -> true);
            }
            URLConnection connection = url.openConnection();
            connection.setConnectTimeout(connectTimeoutMillis);
            connection.setReadTimeout(readTimeoutMillis);
            return connection.getInputStream();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}
ard-work/src/main/resources/mapper/ArdMediaMapper.xml
File was deleted
ard-work/src/main/resources/mapper/ArdMediaPicMapper.xml
New file
@@ -0,0 +1,84 @@
<?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.ArdMediaPicMapper">
    <resultMap id="ArdMediaPicResult" type="ArdMediaPic">
        <result property="id" column="id"/>
        <result property="fileSize" column="file_size"/>
        <result property="fileMd5" column="file_md5"/>
        <result property="mediaUrl" column="media_url"/>
        <result property="sourceUrl" column="source_url"/>
        <result property="longitude" column="longitude"/>
        <result property="latitude" column="latitude"/>
        <result property="height" column="height"/>
        <result property="taskId" column="task_id"/>
        <result property="captureTime" column="capture_time"/>
        <result property="createTime" column="create_time"/>
        <result property="updateTime" column="update_time"/>
    </resultMap>
    <!-- 查询图片列表 -->
    <select id="selectPicList" resultMap="ArdMediaPicResult">
        SELECT
        id, task_id, file_size, file_md5, media_url, source_url,
        longitude, latitude, height, capture_time, create_time, update_time
        FROM ard_media_pic
        <where>
            <if test="taskId != null">AND task_id = #{taskId}</if>
            <if test="fileMd5 != null">AND file_md5 = #{fileMd5}</if>
        </where>
        ORDER BY capture_time DESC
    </select>
    <!-- 根据任务ID查询图片 -->
    <select id="selectPicByTaskId" resultMap="ArdMediaPicResult">
        SELECT * FROM ard_media_pic WHERE task_id = #{taskId}
        ORDER BY capture_time DESC
    </select>
    <!-- 插入图片 -->
    <insert id="insertPic" parameterType="com.ard.work.domain.entity.ArdMediaPic">
        INSERT INTO ard_media_pic (
            id, task_id, file_size, file_md5,
            media_url, source_url,
            longitude, latitude, height,
            capture_time, create_time
        ) VALUES (
                     #{id}, #{taskId}, #{fileSize}, #{fileMd5},
                     #{mediaUrl}, #{sourceUrl},
                     #{longitude}, #{latitude}, #{height},
                     #{captureTime}, #{createTime}
                 )
    </insert>
    <!-- 批量插入图片 -->
    <insert id="insertPicList">
        INSERT INTO ard_media_pic (
        id, task_id, file_size, file_md5,
        media_url, source_url,
        longitude, latitude, height,
        capture_time, create_time
        )
        VALUES
        <foreach collection="list" item="item" separator=",">
            (
            #{item.id}, #{item.taskId}, #{item.fileSize}, #{item.fileMd5},
            #{item.mediaUrl}, #{item.sourceUrl},
            #{item.longitude}, #{item.latitude}, #{item.height},
            #{item.captureTime}, #{item.createTime}
            )
        </foreach>
    </insert>
    <delete id="deletePicById">
        DELETE FROM ard_media_pic WHERE id = #{id}
    </delete>
    <delete id="deletePicByTaskId">
        DELETE FROM ard_media_pic WHERE task_id = #{taskId}
    </delete>
</mapper>
ard-work/src/main/resources/mapper/ArdMediaVideoMapper.xml
New file
@@ -0,0 +1,118 @@
<?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.ArdMediaVideoMapper">
    <!-- 结果映射 -->
    <resultMap id="ArdMediaVideoResult" type="com.ard.work.domain.entity.ArdMediaVideo">
        <id property="id" column="id"/>
        <result property="name" column="name"/>
        <result property="fileSize" column="file_size"/>
        <result property="mediaUrl" column="media_url"/>
        <result property="sourceUrl" column="source_url"/>
        <result property="thumbnailUrl" column="thumbnail_url"/>
        <result property="startTime" column="start_time"/>
        <result property="endTime" column="end_time"/>
        <result property="spanSeconds" column="span_seconds"/>
        <result property="taskId" column="task_id"/>
        <result property="createTime" column="create_time"/>
        <result property="updateTime" column="update_time"/>
    </resultMap>
    <!-- 查询视频列表 -->
    <select id="selectArdMediaVideoList" resultMap="ArdMediaVideoResult">
        SELECT
        id, name, file_size, media_url, source_url, thumbnail_url,
        start_time, end_time, span_seconds, task_id,
        create_time, update_time
        FROM ard_media_video
        <where>
            <if test="taskId != null">AND task_id = #{taskId}</if>
            <if test="name != null and name != ''">AND name LIKE CONCAT('%', #{name}, '%')</if>
            <if test="startTime != null">AND start_time &gt;= #{startTime}</if>
            <if test="endTime != null">AND end_time &lt;= #{endTime}</if>
        </where>
        ORDER BY start_time DESC
    </select>
    <!-- 根据ID查询 -->
    <select id="selectArdMediaVideoById" resultMap="ArdMediaVideoResult">
        SELECT
            id, name, file_size, media_url, source_url, thumbnail_url,
            start_time, end_time, span_seconds, task_id,
            create_time, update_time
        FROM ard_media_video
        WHERE id = #{id}
    </select>
    <!-- 根据任务ID查询 -->
    <select id="selectArdMediaVideoByTaskId" resultMap="ArdMediaVideoResult">
        SELECT
            id, name, file_size, media_url, source_url, thumbnail_url,
            start_time, end_time, span_seconds, task_id,
            create_time, update_time
        FROM ard_media_video
        WHERE task_id = #{taskId}
        ORDER BY start_time DESC
    </select>
    <!-- 新增视频 -->
    <insert id="insertArdMediaVideo" parameterType="com.ard.work.domain.entity.ArdMediaVideo">
        INSERT INTO ard_media_video (
            id, name, file_size, media_url, source_url, thumbnail_url,
            start_time, end_time, span_seconds, task_id,
            create_time, update_time
        ) VALUES (
                     #{id}, #{name}, #{fileSize}, #{mediaUrl}, #{sourceUrl}, #{thumbnailUrl},
                     #{startTime}, #{endTime}, #{spanSeconds}, #{taskId},
                     #{createTime}, #{updateTime}
                 )
    </insert>
    <!-- 批量新增 -->
    <insert id="insertArdMediaVideoList">
        INSERT INTO ard_media_video (
        id, name, file_size, media_url, source_url, thumbnail_url,
        start_time, end_time, span_seconds, task_id,
        create_time, update_time
        )
        VALUES
        <foreach collection="list" item="item" separator=",">
            (
            #{item.id}, #{item.name}, #{item.fileSize},
            #{item.mediaUrl}, #{item.sourceUrl}, #{item.thumbnailUrl},
            #{item.startTime}, #{item.endTime}, #{item.spanSeconds},
            #{item.taskId}, #{item.createTime}, #{item.updateTime}
            )
        </foreach>
    </insert>
    <!-- 更新视频 -->
    <update id="updateArdMediaVideo">
        UPDATE ard_media_video
        <set>
            <if test="name != null">name = #{name},</if>
            <if test="fileSize != null">file_size = #{fileSize},</if>
            <if test="mediaUrl != null">media_url = #{mediaUrl},</if>
            <if test="sourceUrl != null">source_url = #{sourceUrl},</if>
            <if test="thumbnailUrl != null">thumbnail_url = #{thumbnailUrl},</if>
            <if test="startTime != null">start_time = #{startTime},</if>
            <if test="endTime != null">end_time = #{endTime},</if>
            <if test="spanSeconds != null">span_seconds = #{spanSeconds},</if>
            <if test="updateTime != null">update_time = #{updateTime},</if>
        </set>
        WHERE id = #{id}
    </update>
    <!-- 删除 -->
    <delete id="deleteArdMediaVideoById">
        DELETE FROM ard_media_video WHERE id = #{id}
    </delete>
    <delete id="deleteArdMediaVideoByTaskId">
        DELETE FROM ard_media_video WHERE task_id = #{taskId}
    </delete>
</mapper>