From 4f94a29e2722e2185a81355a396ab2a33936d8ff Mon Sep 17 00:00:00 2001
From: liusuyi <1951119284@qq.com>
Date: Sun, 28 Dec 2025 15:19:48 +0800
Subject: [PATCH] 优化架次视频和图片同步任务

---
 ard-quartz/pom.xml                                                                 |    4 
 ard-work/src/main/java/com/ard/work/domain/entity/ArdMediaPic.java                 |    2 
 ard-work/src/main/java/com/ard/work/mapper/ArdMediaPicMapper.java                  |   18 
 ard-work/src/main/java/com/ard/work/mapper/ArdMediaVideoMapper.java                |   18 +
 ard-work/src/main/resources/mapper/ArdMediaVideoMapper.xml                         |   94 ++----
 ard-work/src/main/java/com/ard/work/domain/entity/ArdMediaVideo.java               |    2 
 ard-work/src/main/resources/mapper/ArdMediaPicMapper.xml                           |   69 +---
 ard-framework/src/main/java/com/ard/framework/config/ThreadPoolConfig.java         |   28 +
 ard-work/src/main/java/com/ard/work/service/impl/ArdFlightTaskPartServiceImpl.java |    2 
 ard-quartz/src/main/java/com/ard/quartz/task/MediaTask.java                        |  317 +++++++++++++++++++++
 ard-work/src/main/java/com/ard/work/service/impl/ArdMediaServiceImpl.java          |  293 ++++---------------
 11 files changed, 506 insertions(+), 341 deletions(-)

diff --git a/ard-framework/src/main/java/com/ard/framework/config/ThreadPoolConfig.java b/ard-framework/src/main/java/com/ard/framework/config/ThreadPoolConfig.java
index 44329d0..76c3a93 100644
--- a/ard-framework/src/main/java/com/ard/framework/config/ThreadPoolConfig.java
+++ b/ard-framework/src/main/java/com/ard/framework/config/ThreadPoolConfig.java
@@ -62,4 +62,32 @@
             }
         };
     }
+    @Bean("mediaVideoExecutor")
+    public ThreadPoolTaskExecutor mediaVideoExecutor() {
+        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+        executor.setCorePoolSize(10);    // 核心线程数:同时处理视频的核心线程数
+        executor.setMaxPoolSize(10);     // 最大线程数:线程池不会扩展超过这个数量
+        executor.setQueueCapacity(50);   // 队列容量:等待执行的任务数量,之前20太小可能会拒绝任务
+        executor.setThreadNamePrefix("media-video-");
+
+        // 当线程池和队列都满时的拒绝策略
+        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+
+        executor.initialize();
+        return executor;
+    }
+    @Bean("mediaPicExecutor")
+    public ThreadPoolTaskExecutor mediaPicExecutor() {
+        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+        executor.setCorePoolSize(10);    // 核心线程数:同时处理视频的核心线程数
+        executor.setMaxPoolSize(10);     // 最大线程数:线程池不会扩展超过这个数量
+        executor.setQueueCapacity(50);   // 队列容量:等待执行的任务数量,之前20太小可能会拒绝任务
+        executor.setThreadNamePrefix("media-pic-");
+
+        // 当线程池和队列都满时的拒绝策略
+        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+
+        executor.initialize();
+        return executor;
+    }
 }
diff --git a/ard-quartz/pom.xml b/ard-quartz/pom.xml
index cb73cbd..6c743db 100644
--- a/ard-quartz/pom.xml
+++ b/ard-quartz/pom.xml
@@ -34,6 +34,10 @@
             <groupId>com.ard</groupId>
             <artifactId>ard-common</artifactId>
         </dependency>
+        <dependency>
+            <groupId>com.ard</groupId>
+            <artifactId>ard-work</artifactId>
+        </dependency>
 
     </dependencies>
 
diff --git a/ard-quartz/src/main/java/com/ard/quartz/task/MediaTask.java b/ard-quartz/src/main/java/com/ard/quartz/task/MediaTask.java
new file mode 100644
index 0000000..deef01b
--- /dev/null
+++ b/ard-quartz/src/main/java/com/ard/quartz/task/MediaTask.java
@@ -0,0 +1,317 @@
+package com.ard.quartz.task;
+
+import com.ard.common.utils.StringUtils;
+import com.ard.common.utils.file.MimeTypeUtils;
+import com.ard.oss.service.impl.OssServiceContext;
+import com.ard.work.domain.entity.ArdMediaPic;
+import com.ard.work.domain.entity.ArdMediaVideo;
+import com.ard.work.mapper.ArdMediaPicMapper;
+import com.ard.work.mapper.ArdMediaVideoMapper;
+import com.ard.work.util.osSelect;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+import org.springframework.stereotype.Component;
+
+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.*;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLConnection;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * 定时同步媒体任务
+ *
+ * @author ard
+ */
+@Component("MediaTask")
+@Slf4j
+public class MediaTask {
+    @Resource
+    private ArdMediaPicMapper ardMediaPicMapper;
+    @Resource
+    private ArdMediaVideoMapper ardMediaVideoMapper;
+    @Resource
+    private OssServiceContext ossServiceContext;
+    @Resource(name = "mediaVideoExecutor")
+    private ThreadPoolTaskExecutor mediaVideoExecutor;
+    @Resource(name = "mediaPicExecutor")
+    private ThreadPoolTaskExecutor mediaPicExecutor;
+
+    private final AtomicBoolean runningVideo = new AtomicBoolean(false);
+    private final AtomicBoolean runningPic = new AtomicBoolean(false);
+
+    public void syncVideoMediaUrl() {
+        log.warn("开始同步视频媒体");
+        if (!runningVideo.compareAndSet(false, true)) {
+            log.warn("上一次同步还没完成,跳过本次调度");
+            return;
+        }
+        try {
+            while (true) {
+                List<ArdMediaVideo> videos = ardMediaVideoMapper.selectPendingVideos(10);
+                if (videos.isEmpty()) {
+                    break; // 没有待处理视频,退出循环
+                }
+                for (ArdMediaVideo video : videos) {
+                    int updated = ardMediaVideoMapper.markProcessing(video.getId());
+                    if (updated != 1) continue;
+                    mediaVideoExecutor.execute(() -> processOneVideo(video));
+                }
+                Thread.sleep(200);
+            }
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+        } finally {
+            runningVideo.set(false);
+        }
+    }
+
+    public void syncPicMediaUrl() {
+        log.warn("开始同步图片媒体");
+        if (!runningPic.compareAndSet(false, true)) {
+            log.warn("上一次同步还没完成,跳过本次调度");
+            return;
+        }
+        try {
+            while (true) {
+                List<ArdMediaPic> pics = ardMediaPicMapper.selectPendingPics(10); // 查询待处理的图片
+                if (pics.isEmpty()) {
+                    break; // 没有待处理图片,退出循环
+                }
+                for (ArdMediaPic pic : pics) {
+                    // 🔒 原子抢占:0 → 1
+                    int updated = ardMediaPicMapper.markProcessing(pic.getId());
+                    if (updated != 1) {
+                        continue; // 已被别的任务抢占
+                    }
+                    mediaPicExecutor.execute(() -> processOnePic(pic));
+                }
+                // 短暂 sleep,避免无限循环CPU空转
+                Thread.sleep(200);
+            }
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+        } finally {
+            runningPic.set(false);
+        }
+    }
+
+    private void processOneVideo(ArdMediaVideo video) {
+        try {
+            try (InputStream is = openUrlInputStream(video.getSourceUrl())) {
+                if (is != null) {
+                    String mediaUrl = ossServiceContext.upload(
+                            "record",
+                            video.getTaskId() + "/" + video.getId() + ".mp4",
+                            is,
+                            MimeTypeUtils.VIDEO_MP4
+                    );
+                    video.setMediaUrl(mediaUrl);
+                }
+            }
+
+            processVideoThumbnail(video);
+            video.setSyncStatus(2);
+        } catch (Exception e) {
+            video.setSyncStatus(3);
+            log.error("视频处理失败 videoId={}", video.getId(), e);
+        } finally {
+            video.setUpdateTime(new Date());
+            try {
+                ardMediaVideoMapper.updateArdMediaVideo(video);
+            } catch (Exception e) {
+                log.error("更新视频状态失败 videoId={}", video.getId(), e);
+            }
+        }
+    }
+
+    private void processOnePic(ArdMediaPic pic) {
+        try {
+            try (InputStream inputStream = openUrlInputStream(pic.getSourceUrl())) {
+                if (inputStream != null) {
+                    String url = ossServiceContext.upload(
+                            "pic",
+                            pic.getTaskId() + "/" + pic.getId() + ".jpg",
+                            inputStream,
+                            MimeTypeUtils.IMAGE_JPG
+                    );
+                    pic.setMediaUrl(url);
+                    pic.setSyncStatus(2); // 成功
+                } else {
+                    pic.setSyncStatus(3); // 下载失败
+                }
+            }
+        } catch (Exception e) {
+            log.error("图片同步失败 taskId={}, photoId={}", pic.getTaskId(), pic.getId(), e);
+            pic.setSyncStatus(3); // 异常
+        } finally {
+            pic.setUpdateTime(new Date());
+            try {
+                ardMediaPicMapper.updatePic(pic);
+            } catch (Exception e) {
+                log.error("更新图片状态失败 photoId={}", pic.getId(), e);
+            }
+        }
+    }
+
+    /**
+     * 处理视频缩略图
+     */
+    private void processVideoThumbnail(ArdMediaVideo video) {
+        // 生成缩略图
+        try (ByteArrayInputStream in = generateThumbnail(video.getSourceUrl())) {
+            String thumbUrl = ossServiceContext.upload(
+                    "record",
+                    video.getTaskId() + "/" + video.getId() + ".jpg",
+                    in,
+                    MimeTypeUtils.IMAGE_JPEG
+            );
+            video.setThumbnailUrl(thumbUrl);
+        } catch (Exception e) {
+            log.warn("视频缩略图生成或上传失败 taskId={}, videoId={}", video.getTaskId(), video.getId(), e);
+        }
+        // 获取文件大小
+        try {
+            long videoSize = getFileSizeFromUrl(video.getSourceUrl());
+            video.setFileSize(videoSize);
+        } catch (Exception e) {
+            log.warn("获取视频大小失败 url={}", video.getSourceUrl(), e);
+        }
+    }
+
+    private long getFileSizeFromUrl(String fileUrl) throws IOException {
+        URL url = new URL(fileUrl);
+        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+        conn.setRequestMethod("HEAD");
+        conn.setConnectTimeout(5000);
+        conn.setReadTimeout(5000);
+        conn.connect();
+        long size = conn.getContentLengthLong(); // 返回字节数
+        conn.disconnect();
+        return size;
+    }
+
+    private ByteArrayInputStream generateThumbnail(String videoFile) {
+
+        String ffmpeg = osSelect.isWindows()
+                ? "ffmpeg"
+                : "/usr/local/ffmpeg/bin/ffmpeg";
+
+        Path tempImg = null;
+        try {
+            tempImg = Files.createTempFile("thumb_", ".jpg");
+
+            List<String> cmd = new ArrayList<>();
+            cmd.add(ffmpeg);
+            cmd.add("-y");
+            cmd.add("-loglevel");
+            cmd.add("error");
+            cmd.add("-i");
+            cmd.add(videoFile);
+            cmd.add("-ss");
+            cmd.add(String.valueOf(1));
+            cmd.add("-vframes");
+            cmd.add("1");
+            cmd.add("-vf");
+            cmd.add("scale=" + 200 + ":-1");
+            cmd.add(tempImg.toAbsolutePath().toString());
+
+            log.info("ffmpeg cmd: {}", String.join(" ", cmd));
+
+            Process process = new ProcessBuilder(cmd).start();
+
+            drain(process.getErrorStream());
+
+            boolean finished = process.waitFor(15, TimeUnit.SECONDS); // 超时保护
+            if (!finished) {
+                process.destroyForcibly();
+                throw new RuntimeException("ffmpeg timeout");
+            }
+
+            if (process.exitValue() != 0) {
+                throw new RuntimeException("ffmpeg exit=" + process.exitValue());
+            }
+
+            byte[] data = Files.readAllBytes(tempImg);
+            return new ByteArrayInputStream(data);
+
+        } catch (Exception e) {
+            throw new RuntimeException("生成缩略图失败", e);
+        } finally {
+            if (tempImg != null) {
+                try {
+                    Files.deleteIfExists(tempImg);
+                } catch (IOException ignore) {
+                }
+            }
+        }
+    }
+
+    private static void drain(InputStream is) {
+        new Thread(() -> {
+            try (BufferedReader br = new BufferedReader(new InputStreamReader(is))) {
+                while (br.readLine() != null) {
+                }
+            } catch (IOException ignore) {
+            }
+        }, "ffmpeg-stderr").start();
+    }
+
+    /**
+     * 将 URL 转成 InputStream
+     *
+     * @param fileUrl 文件 URL
+     * @return InputStream,如果失败返回 null
+     */
+    private InputStream openUrlInputStream(String fileUrl) {
+        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(5000);
+            connection.setReadTimeout(10000);
+            return connection.getInputStream();
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+}
diff --git a/ard-work/src/main/java/com/ard/work/domain/entity/ArdMediaPic.java b/ard-work/src/main/java/com/ard/work/domain/entity/ArdMediaPic.java
index d5f4b34..d415301 100644
--- a/ard-work/src/main/java/com/ard/work/domain/entity/ArdMediaPic.java
+++ b/ard-work/src/main/java/com/ard/work/domain/entity/ArdMediaPic.java
@@ -23,4 +23,6 @@
     private Double height;
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private Date captureTime;
+    //同步状态 0待处理 1处理中 2成功 3失败
+    private Integer syncStatus;
 }
diff --git a/ard-work/src/main/java/com/ard/work/domain/entity/ArdMediaVideo.java b/ard-work/src/main/java/com/ard/work/domain/entity/ArdMediaVideo.java
index 9611eb5..f6d7220 100644
--- a/ard-work/src/main/java/com/ard/work/domain/entity/ArdMediaVideo.java
+++ b/ard-work/src/main/java/com/ard/work/domain/entity/ArdMediaVideo.java
@@ -25,4 +25,6 @@
     private Date endTime;
     private Integer spanSeconds;
     private Integer taskId;
+    //同步状态 0待处理 1处理中 2成功 3失败
+    private Integer syncStatus;
 }
diff --git a/ard-work/src/main/java/com/ard/work/mapper/ArdMediaPicMapper.java b/ard-work/src/main/java/com/ard/work/mapper/ArdMediaPicMapper.java
index a6f8d6a..161b5bb 100644
--- a/ard-work/src/main/java/com/ard/work/mapper/ArdMediaPicMapper.java
+++ b/ard-work/src/main/java/com/ard/work/mapper/ArdMediaPicMapper.java
@@ -14,10 +14,11 @@
      * 查询图片列表(条件查询)
      */
     List<ArdMediaPic> selectPicList(ArdMediaPic query);
+
     /**
-     * 查询图片列表中本图为null的数据
+     * 查询待处理图片(sync_status = 0)
      */
-    List<ArdMediaPic> selectPicsWithNullMedia();
+    List<ArdMediaPic> selectPendingPics(@Param("limit") int limit);
     /**
      * 根据任务ID查询图片
      */
@@ -32,11 +33,18 @@
      * 更新图片
      */
     int updatePic(ArdMediaPic pic);
+    /**
+     * 原子抢占:0 → 1
+     */
+    int markProcessing(@Param("id") Long id);
 
     /**
-     * 批量新增图片
+     * 更新同步状态
+     * 0 = 待处理
+     * 1 = 处理中
+     * 2 = 已完成
      */
-    int insertPicList(@Param("list") List<ArdMediaPic> list);
+    int updateSyncStatus(@Param("id") Long id, @Param("status") Integer status);
 
     /**
      * 根据ID删除图片
@@ -47,4 +55,6 @@
      * 根据任务ID删除图片
      */
     int deletePicByTaskId(@Param("taskId") Integer taskId);
+
+
 }
diff --git a/ard-work/src/main/java/com/ard/work/mapper/ArdMediaVideoMapper.java b/ard-work/src/main/java/com/ard/work/mapper/ArdMediaVideoMapper.java
index cdc752b..bb465eb 100644
--- a/ard-work/src/main/java/com/ard/work/mapper/ArdMediaVideoMapper.java
+++ b/ard-work/src/main/java/com/ard/work/mapper/ArdMediaVideoMapper.java
@@ -15,11 +15,23 @@
     List<ArdMediaVideo> selectArdMediaVideoList(ArdMediaVideo query);
 
     /**
-     * 查询视频列表中本地视频和缩略图为null的数据
+     * 查询待处理视频(sync_status = 0)
      */
-    List<ArdMediaVideo> selectVideosWithNullMedia();
+    List<ArdMediaVideo> selectPendingVideos(@Param("limit") int limit);
 
-    List<ArdMediaVideo> selectVideosWithThumbnailNull();
+    /**
+     * 原子抢占:0 → 1
+     */
+    int markProcessing(@Param("id") Long id);
+
+    /**
+     * 更新同步状态
+     * 0 = 待处理
+     * 1 = 处理中
+     * 2 = 已完成
+     */
+    int updateSyncStatus(@Param("id") Long id, @Param("status") Integer status);
+
     /**
      * 根据任务ID查询视频
      */
diff --git a/ard-work/src/main/java/com/ard/work/service/impl/ArdFlightTaskPartServiceImpl.java b/ard-work/src/main/java/com/ard/work/service/impl/ArdFlightTaskPartServiceImpl.java
index eb9e7ae..ae769d2 100644
--- a/ard-work/src/main/java/com/ard/work/service/impl/ArdFlightTaskPartServiceImpl.java
+++ b/ard-work/src/main/java/com/ard/work/service/impl/ArdFlightTaskPartServiceImpl.java
@@ -73,7 +73,7 @@
                 flushRemaining(boxSn);
             }
             lastFlightStatusMap.put(boxSn, currentStatus);
-            log.warn("设备 {} 当前状态为:{}", boxSn, currentStatus);
+            log.debug("设备 {} 当前状态为:{}", boxSn, currentStatus);
             // 只有飞行中才添加缓存
             if (currentStatus == 2) {
                 log.warn("设备 {} 当前状态为飞行中,添加遥测数据到缓存", boxSn);
diff --git a/ard-work/src/main/java/com/ard/work/service/impl/ArdMediaServiceImpl.java b/ard-work/src/main/java/com/ard/work/service/impl/ArdMediaServiceImpl.java
index ec2dddc..80f4603 100644
--- a/ard-work/src/main/java/com/ard/work/service/impl/ArdMediaServiceImpl.java
+++ b/ard-work/src/main/java/com/ard/work/service/impl/ArdMediaServiceImpl.java
@@ -58,15 +58,10 @@
     private ArdMediaPicMapper ardMediaPicMapper;
     @Resource
     private ArdMediaVideoMapper ardMediaVideoMapper;
-    @Resource
-    private OssServiceContext ossServiceContext;
+
     @Resource
     private ArdFlightTaskMapper ardFlightTaskMapper;
-    @Resource
-    private ThreadPoolTaskExecutor threadPoolTaskExecutor;
 
-    private final Set<Long> uploadingVideos = ConcurrentHashMap.newKeySet();
-    private final Set<Long> uploadingPics = ConcurrentHashMap.newKeySet();
 
     // 调用接口获取架次图片
     private List<BoxPhotosReceiver> getBoxPhotos(String serverId, BoxPhotosDTO boxPhotosDTO) {
@@ -230,227 +225,73 @@
         }
     }
 
-    /**
-     * 处理视频缩略图
-     */
-    private void processVideoThumbnail(ArdMediaVideo video) {
-        // 生成缩略图
-        try (ByteArrayInputStream in = generateThumbnail(video.getSourceUrl())) {
-            String thumbUrl = ossServiceContext.upload(
-                    "record",
-                    video.getTaskId() + "/" + video.getId() + ".jpg",
-                    in,
-                    MimeTypeUtils.IMAGE_JPEG
-            );
-            video.setThumbnailUrl(thumbUrl);
-        } catch (Exception e) {
-            log.warn("视频缩略图生成或上传失败 taskId={}, videoId={}", video.getTaskId(), video.getId(), e);
-        }
-        // 获取文件大小
-        try {
-            long videoSize = getFileSizeFromUrl(video.getSourceUrl());
-            video.setFileSize(videoSize);
-        } catch (Exception e) {
-            log.warn("获取视频大小失败 url={}", video.getSourceUrl(), e);
-        }
-        // 更新数据库
-        video.setUpdateTime(new Date());
-        ardMediaVideoMapper.updateArdMediaVideo(video);
-    }
-
-    public long getFileSizeFromUrl(String fileUrl) throws IOException {
-        URL url = new URL(fileUrl);
-        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
-        conn.setRequestMethod("HEAD");
-        conn.setConnectTimeout(5000);
-        conn.setReadTimeout(5000);
-        conn.connect();
-        long size = conn.getContentLengthLong(); // 返回字节数
-        conn.disconnect();
-        return size;
-    }
 
 
-    private ByteArrayInputStream generateThumbnail(String videoFile) {
-
-        String ffmpeg = osSelect.isWindows()
-                ? "ffmpeg"
-                : "/usr/local/ffmpeg/bin/ffmpeg";
-
-        Path tempImg = null;
-        try {
-            tempImg = Files.createTempFile("thumb_", ".jpg");
-
-            List<String> cmd = new ArrayList<>();
-            cmd.add(ffmpeg);
-            cmd.add("-y");
-            cmd.add("-loglevel");
-            cmd.add("error");
-            cmd.add("-i");
-            cmd.add(videoFile);
-            cmd.add("-ss");
-            cmd.add(String.valueOf(1));
-            cmd.add("-vframes");
-            cmd.add("1");
-            cmd.add("-vf");
-            cmd.add("scale=" + 200 + ":-1");
-            cmd.add(tempImg.toAbsolutePath().toString());
-
-            log.info("ffmpeg cmd: {}", String.join(" ", cmd));
-
-            Process process = new ProcessBuilder(cmd).start();
-
-            drain(process.getErrorStream());
-
-            boolean finished = process.waitFor(15, TimeUnit.SECONDS); // 超时保护
-            if (!finished) {
-                process.destroyForcibly();
-                throw new RuntimeException("ffmpeg timeout");
-            }
-
-            if (process.exitValue() != 0) {
-                throw new RuntimeException("ffmpeg exit=" + process.exitValue());
-            }
-
-            byte[] data = Files.readAllBytes(tempImg);
-            return new ByteArrayInputStream(data);
-
-        } catch (Exception e) {
-            throw new RuntimeException("生成缩略图失败", e);
-        } finally {
-            if (tempImg != null) {
-                try {
-                    Files.deleteIfExists(tempImg);
-                } catch (IOException ignore) {
-                }
-            }
-        }
-    }
-
-    private static void drain(InputStream is) {
-        new Thread(() -> {
-            try (BufferedReader br = new BufferedReader(new InputStreamReader(is))) {
-                while (br.readLine() != null) {
-                }
-            } catch (IOException ignore) {
-            }
-        }, "ffmpeg-stderr").start();
-    }
-
-
-    /**
-     * 将 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;
-        }
-    }
-
-    @Scheduled(fixedDelay = 10000) // 每10秒执行一次
-    public void syncVideoMediaUrl() {
-        List<ArdMediaVideo> videos = ardMediaVideoMapper.selectVideosWithNullMedia();
-        for (ArdMediaVideo video : videos) {
-            if (!uploadingVideos.add(video.getId())) {
-                continue;
-            }
-            CompletableFuture.runAsync(() -> {
-                try {
-                    // 上传视频到 OSS
-                    try (InputStream is = openUrlInputStream(video.getSourceUrl(), 5000, 10000)) {
-                        if (is != null) {
-                            String mediaUrl = ossServiceContext.upload(
-                                    "record",
-                                    video.getTaskId() + "/" + video.getId() + ".mp4",
-                                    is,
-                                    MimeTypeUtils.VIDEO_MP4
-                            );
-                            log.info("upload mediaUrl={}", mediaUrl);
-                            video.setMediaUrl(mediaUrl);
-                        }
-                    }
-                    // 生成缩略图
-                    processVideoThumbnail(video);
-
-                    // 更新数据库
-                    video.setUpdateTime(new Date());
-                    ardMediaVideoMapper.updateArdMediaVideo(video);
-                } catch (Exception e) {
-                    log.error("生成 mediaUrl/缩略图失败 videoId={}", video.getId(), e);
-                } finally {
-                    uploadingVideos.remove(video.getId());
-                }
-            }, threadPoolTaskExecutor);
-        }
-    }
-
-    @Scheduled(fixedDelay = 10000) // 每10秒执行一次
-    public void syncPicMediaUrl() {
-        List<ArdMediaPic> pics = ardMediaPicMapper.selectPicsWithNullMedia();
-        for (ArdMediaPic pic : pics) {
-            if (!uploadingPics.add(pic.getId())) {
-                continue;
-            }
-            CompletableFuture.runAsync(() -> {
-                try (InputStream inputStream = openUrlInputStream(pic.getSourceUrl(), 5000, 10000)) {
-                    if (inputStream != null) {
-                        String url = ossServiceContext.upload(
-                                "pic",
-                                pic.getTaskId() + "/" + pic.getId() + ".jpg",
-                                inputStream,
-                                MimeTypeUtils.IMAGE_JPG
-                        );
-                        pic.setMediaUrl(url);
-                        pic.setUpdateTime(new Date());
-                        ardMediaPicMapper.updatePic(pic);
-                    }
-                } catch (Exception e) {
-                    log.error("图片上传失败 taskId={}, photoId={}", pic.getTaskId(), pic.getId(), e);
-                } finally {
-                    uploadingPics.remove(pic.getId());
-                }
-
-            }, threadPoolTaskExecutor); // 使用自定义线程池
-        }
-    }
+//    @Scheduled(fixedDelay = 10000) // 每10秒执行一次
+//    public void syncVideoMediaUrl() {
+//        List<ArdMediaVideo> videos = ardMediaVideoMapper.selectVideosWithNullMedia();
+//        for (ArdMediaVideo video : videos) {
+//            if (!uploadingVideos.add(video.getId())) {
+//                continue;
+//            }
+//            CompletableFuture.runAsync(() -> {
+//                try {
+//                    // 上传视频到 OSS
+//                    try (InputStream is = openUrlInputStream(video.getSourceUrl(), 5000, 10000)) {
+//                        if (is != null) {
+//                            String mediaUrl = ossServiceContext.upload(
+//                                    "record",
+//                                    video.getTaskId() + "/" + video.getId() + ".mp4",
+//                                    is,
+//                                    MimeTypeUtils.VIDEO_MP4
+//                            );
+//                            log.info("upload mediaUrl={}", mediaUrl);
+//                            video.setMediaUrl(mediaUrl);
+//                        }
+//                    }
+//                    // 生成缩略图
+//                    processVideoThumbnail(video);
+//
+//                    // 更新数据库
+//                    video.setUpdateTime(new Date());
+//                    ardMediaVideoMapper.updateArdMediaVideo(video);
+//                } catch (Exception e) {
+//                    log.error("生成 mediaUrl/缩略图失败 videoId={}", video.getId(), e);
+//                } finally {
+//                    uploadingVideos.remove(video.getId());
+//                }
+//            }, threadPoolTaskExecutor);
+//        }
+//    }
+//
+//    @Scheduled(fixedDelay = 10000) // 每10秒执行一次
+//    public void syncPicMediaUrl() {
+//        List<ArdMediaPic> pics = ardMediaPicMapper.selectPicsWithNullMedia();
+//        for (ArdMediaPic pic : pics) {
+//            if (!uploadingPics.add(pic.getId())) {
+//                continue;
+//            }
+//            CompletableFuture.runAsync(() -> {
+//                try (InputStream inputStream = openUrlInputStream(pic.getSourceUrl(), 5000, 10000)) {
+//                    if (inputStream != null) {
+//                        String url = ossServiceContext.upload(
+//                                "pic",
+//                                pic.getTaskId() + "/" + pic.getId() + ".jpg",
+//                                inputStream,
+//                                MimeTypeUtils.IMAGE_JPG
+//                        );
+//                        pic.setMediaUrl(url);
+//                        pic.setUpdateTime(new Date());
+//                        ardMediaPicMapper.updatePic(pic);
+//                    }
+//                } catch (Exception e) {
+//                    log.error("图片上传失败 taskId={}, photoId={}", pic.getTaskId(), pic.getId(), e);
+//                } finally {
+//                    uploadingPics.remove(pic.getId());
+//                }
+//
+//            }, threadPoolTaskExecutor); // 使用自定义线程池
+//        }
+//    }
 
 }
diff --git a/ard-work/src/main/resources/mapper/ArdMediaPicMapper.xml b/ard-work/src/main/resources/mapper/ArdMediaPicMapper.xml
index cba6580..dc7b7f2 100644
--- a/ard-work/src/main/resources/mapper/ArdMediaPicMapper.xml
+++ b/ard-work/src/main/resources/mapper/ArdMediaPicMapper.xml
@@ -18,42 +18,27 @@
         <result property="captureTime" column="capture_time"/>
         <result property="createTime" column="create_time"/>
         <result property="updateTime" column="update_time"/>
+        <result property="syncStatus" column="sync_status"/>
     </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
+        SELECT *
         FROM ard_media_pic
         <where>
             <if test="taskId != null">AND task_id = #{taskId}</if>
-            <if test="fileMd5 != null">AND file_md5 = #{fileMd5}</if>
+            <if test="syncStatus != null">AND sync_status = #{syncStatus}</if>
         </where>
         ORDER BY capture_time DESC
     </select>
-    <!-- 查询 media_url为 NULL 的视频 -->
-    <select id="selectPicsWithNullMedia" 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
-            media_url IS NULL
-        ORDER BY
-            capture_time ASC
+    <!-- 查询待处理的图片 -->
+    <select id="selectPendingPics" resultMap="ArdMediaPicResult">
+        SELECT *
+        FROM ard_media_pic
+        WHERE sync_status = 0
+        ORDER BY id LIMIT #{limit}
     </select>
+
     <!-- 根据任务ID查询图片 -->
     <select id="selectPicByTaskId" resultMap="ArdMediaPicResult">
         SELECT * FROM ard_media_pic WHERE task_id = #{taskId}
@@ -66,12 +51,12 @@
             id, task_id, file_size, file_md5,
             media_url, source_url,
             longitude, latitude, height,
-            capture_time, create_time
+            capture_time, create_time,sync_status
         ) VALUES (
                      #{id}, #{taskId}, #{fileSize}, #{fileMd5},
                      #{mediaUrl}, #{sourceUrl},
                      #{longitude}, #{latitude}, #{height},
-                     #{captureTime}, #{createTime}
+                     #{captureTime}, #{createTime},0
                  )
     </insert>
     <!-- 更新图片 -->
@@ -79,28 +64,20 @@
         UPDATE ard_media_pic
         SET
             media_url   = #{mediaUrl},
+            sync_status = #{syncStatus},
             update_time = #{updateTime}
         WHERE id = #{id} AND media_url IS NULL
     </update>
-
-    <!-- 批量插入图片 -->
-    <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>
+    <!-- 标记为处理中 -->
+    <update id="markProcessing">
+        UPDATE ard_media_pic SET sync_status = 1 WHERE id = #{id} AND sync_status = 0
+    </update>
+    <!-- 更新同步状态 -->
+    <update id="updateSyncStatus">
+        UPDATE ard_media_pic
+        SET sync_status = #{status},update_time = NOW()
+        WHERE id = #{id}
+    </update>
 
     <delete id="deletePicById">
         DELETE FROM ard_media_pic WHERE id = #{id}
diff --git a/ard-work/src/main/resources/mapper/ArdMediaVideoMapper.xml b/ard-work/src/main/resources/mapper/ArdMediaVideoMapper.xml
index a87800d..a19d361 100644
--- a/ard-work/src/main/resources/mapper/ArdMediaVideoMapper.xml
+++ b/ard-work/src/main/resources/mapper/ArdMediaVideoMapper.xml
@@ -6,7 +6,7 @@
 <mapper namespace="com.ard.work.mapper.ArdMediaVideoMapper">
 
     <!-- 结果映射 -->
-    <resultMap id="ArdMediaVideoResult" type="com.ard.work.domain.entity.ArdMediaVideo">
+    <resultMap id="ArdMediaVideoResult" type="ArdMediaVideo">
         <id property="id" column="id"/>
         <result property="name" column="name"/>
         <result property="fileSize" column="file_size"/>
@@ -19,6 +19,7 @@
         <result property="taskId" column="task_id"/>
         <result property="createTime" column="create_time"/>
         <result property="updateTime" column="update_time"/>
+        <result property="syncStatus" column="sync_status"/>
     </resultMap>
 
     <!-- 查询视频列表 -->
@@ -26,51 +27,29 @@
         SELECT
         id, name, file_size, media_url, source_url, thumbnail_url,
         start_time, end_time, span_seconds, task_id,
-        create_time, update_time
+        create_time, update_time,sync_status
         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>
+            <if test="syncStatus != null">AND sync_status = #{syncStatus}</if>
         </where>
         ORDER BY start_time DESC
     </select>
 
-    <!-- 查询 media_url 或 thumbnail_url 为 NULL 的视频 -->
-    <select id="selectVideosWithNullMedia" 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 media_url IS NULL
-        ORDER BY start_time ASC
-    </select>
-    <select id="selectVideosWithThumbnailNull" 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 thumbnail_url IS NULL
-        ORDER BY start_time ASC
-    </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
+    <!-- 查询待处理的视频 -->
+    <select id="selectPendingVideos" resultMap="ArdMediaVideoResult">
+        SELECT *
         FROM ard_media_video
-        WHERE id = #{id}
+        WHERE sync_status = 0
+        ORDER BY id LIMIT #{limit}
     </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
+        SELECT *
         FROM ard_media_video
         WHERE task_id = #{taskId}
         ORDER BY start_time DESC
@@ -78,33 +57,12 @@
 
     <!-- 新增视频 -->
     <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 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, sync_status)
+        VALUES (#{id}, #{name}, #{fileSize}, #{mediaUrl}, #{sourceUrl}, #{thumbnailUrl},
+                #{startTime}, #{endTime}, #{spanSeconds}, #{taskId},
+                #{createTime}, #{updateTime}, 0)
     </insert>
 
     <!-- 更新视频 -->
@@ -120,17 +78,31 @@
             <if test="endTime != null">end_time = #{endTime},</if>
             <if test="spanSeconds != null">span_seconds = #{spanSeconds},</if>
             <if test="updateTime != null">update_time = #{updateTime},</if>
+            <if test="syncStatus != null">sync_status = #{syncStatus},</if>
         </set>
         WHERE id = #{id}
     </update>
-
+    <!-- 标记为处理中 -->
+    <update id="markProcessing">
+        UPDATE ard_media_video SET sync_status = 1 WHERE id = #{id} AND sync_status = 0
+    </update>
+    <!-- 更新同步状态 -->
+    <update id="updateSyncStatus">
+        UPDATE ard_media_video
+        SET sync_status = #{status},update_time = NOW()
+        WHERE id = #{id}
+    </update>
     <!-- 删除 -->
     <delete id="deleteArdMediaVideoById">
-        DELETE FROM ard_media_video WHERE id = #{id}
+        DELETE
+        FROM ard_media_video
+        WHERE id = #{id}
     </delete>
 
     <delete id="deleteArdMediaVideoByTaskId">
-        DELETE FROM ard_media_video WHERE task_id = #{taskId}
+        DELETE
+        FROM ard_media_video
+        WHERE task_id = #{taskId}
     </delete>
 
 </mapper>

--
Gitblit v1.9.3