Quellcode durchsuchen

另外一种方案,暂时注释掉截取第一针,第二次提交

liudongzhi vor 1 Monat
Ursprung
Commit
6119791815

+ 43 - 0
alien-store/src/main/java/shop/alien/store/controller/StoreFileUploadController.java

@@ -13,6 +13,7 @@ import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;
 import org.springframework.web.multipart.MultipartRequest;
 import shop.alien.entity.result.R;
+import shop.alien.store.entity.VideoUploadResult;
 import shop.alien.store.util.FileUploadUtil;
 import shop.alien.store.util.ImageToPdfUploadUtil;
 
@@ -237,4 +238,46 @@ public class StoreFileUploadController {
         }
     }
 
+    @ApiOperation("高性能视频上传(快速上传,截图异步处理)")
+    @ApiOperationSupport(order = 10)
+    @ApiImplicitParams({
+            @ApiImplicitParam(
+                    name = "file",
+                    value = "视频文件(支持mp4、avi、flv、mkv、rmvb、wmv、3gp、mov格式)",
+                    dataType = "file",
+                    paramType = "form",
+                    required = true
+            ),
+            @ApiImplicitParam(
+                    name = "needScreenshot",
+                    value = "是否需要截图(true/false,默认false,截图会异步处理)",
+                    dataType = "boolean",
+                    paramType = "form",
+                    required = false
+            )
+    })
+    @PostMapping("/uploadVideoFast")
+    public R<VideoUploadResult> uploadVideoFast(@RequestParam("file") MultipartFile file,
+                                                 @RequestParam(value = "needScreenshot", required = false, defaultValue = "false") Boolean needScreenshot) {
+        log.info("StoreFileUploadController.uploadVideoFast fileName:{}, needScreenshot:{}", file.getOriginalFilename(), needScreenshot);
+        try {
+            // 参数验证
+            if (file == null || file.isEmpty()) {
+                return R.fail("请选择要上传的视频文件");
+            }
+            
+            VideoUploadResult result = fileUpload.uploadVideoFast(file, needScreenshot);
+            if (result == null) {
+                return R.fail("视频上传失败");
+            }
+            return R.data(result);
+        } catch (IllegalArgumentException e) {
+            log.warn("视频上传参数错误: {}", e.getMessage());
+            return R.fail("参数错误: " + e.getMessage());
+        } catch (Exception e) {
+            log.error("视频上传失败: {}", e.getMessage(), e);
+            return R.fail("视频上传失败: " + e.getMessage());
+        }
+    }
+
 }

+ 51 - 0
alien-store/src/main/java/shop/alien/store/entity/VideoUploadResult.java

@@ -0,0 +1,51 @@
+package shop.alien.store.entity;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 视频上传结果
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@ApiModel("视频上传结果")
+public class VideoUploadResult implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty("视频URL")
+    private String videoUrl;
+
+    @ApiModelProperty("截图URL(如果异步处理中,可能暂时为空)")
+    private String screenshotUrl;
+
+    @ApiModelProperty("是否已生成截图")
+    private Boolean screenshotReady;
+
+    @ApiModelProperty("视频文件名")
+    private String fileName;
+
+    @ApiModelProperty("文件大小(字节)")
+    private Long fileSize;
+
+    @ApiModelProperty("上传耗时(毫秒)")
+    private Long uploadTime;
+
+    public VideoUploadResult(String videoUrl, String fileName, Long fileSize, Long uploadTime) {
+        this.videoUrl = videoUrl;
+        this.fileName = fileName;
+        this.fileSize = fileSize;
+        this.uploadTime = uploadTime;
+        this.screenshotUrl = null;
+        this.screenshotReady = false;
+    }
+}

+ 288 - 72
alien-store/src/main/java/shop/alien/store/util/FileUploadUtil.java

@@ -9,6 +9,7 @@ import org.springframework.stereotype.Component;
 import org.springframework.web.multipart.MultipartFile;
 import org.springframework.web.multipart.MultipartRequest;
 import shop.alien.entity.store.vo.StoreImgVo;
+import shop.alien.store.entity.VideoUploadResult;
 import shop.alien.store.service.StoreImgService;
 import shop.alien.util.ali.AliOSSUtil;
 import shop.alien.util.common.RandomCreateUtil;
@@ -18,12 +19,14 @@ import shop.alien.util.file.FileUtil;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
+import java.io.InputStream;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.*;
 import java.util.concurrent.CompletableFuture;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import shop.alien.store.config.BaseRedisService;
 import shop.alien.util.md5.FileMd5Util;
 import shop.alien.store.util.ai.AiContentModerationUtil;
@@ -133,89 +136,302 @@ public class FileUploadUtil {
     }
 
     /**
-     * 上传多个文件
+     * 高性能视频上传(快速上传,截图异步处理)
+     * 
+     * 性能优化点:
+     * 1. 【关键优化】不需要截图时:直接使用 InputStream 流式上传,避免先保存到本地(减少一次IO操作)
+     * 2. 需要截图时:先保存文件,然后从文件上传(因为需要文件用于截图)
+     * 3. 截图异步处理,不阻塞主流程
+     * 4. 立即返回视频URL,提升响应速度
+     * 
+     * @param multipartFile 视频文件
+     * @param needScreenshot 是否需要截图(默认false,截图会异步处理)
+     * @return 视频上传结果
+     */
+    public VideoUploadResult uploadVideoFast(MultipartFile multipartFile, Boolean needScreenshot) {
+        long startTime = System.currentTimeMillis();
+        File tempVideoFile = null;
+        
+        try {
+            // 参数验证
+            if (multipartFile == null || multipartFile.isEmpty()) {
+                throw new IllegalArgumentException("视频文件不能为空");
+            }
+            
+            Map<String, String> fileNameAndType = FileUtil.getFileNameAndType(multipartFile);
+            String fileType = fileNameAndType.get("type").toLowerCase();
+            
+            // 验证是否为视频文件
+            if (!videoFileType.contains(fileType)) {
+                throw new IllegalArgumentException("文件不是视频格式,支持的格式: " + videoFileType);
+            }
+            
+            String prefix = "video/";
+            String videoFileName = fileNameAndType.get("name").replaceAll(",", "") + RandomCreateUtil.getRandomNum(6);
+            String ossFilePath = prefix + videoFileName + "." + fileNameAndType.get("type");
+            
+            log.info("开始高性能视频上传: {}, 文件大小: {} bytes, 需要截图: {}", 
+                    multipartFile.getOriginalFilename(), multipartFile.getSize(), needScreenshot);
+            
+            String uploadDir = this.uploadDir.replace("file:///", "").replace("\\", "/") + "/video/";
+            String videoUrl;
+            
+            // 性能优化:根据是否需要截图选择不同的上传策略
+            if (Boolean.TRUE.equals(needScreenshot)) {
+                // 需要截图:先保存文件,然后从文件上传(因为截图需要文件)
+                String tempPath = copyFile(uploadDir, multipartFile);
+                tempVideoFile = new File(tempPath);
+                if (!tempVideoFile.exists() || tempVideoFile.length() == 0) {
+                    throw new RuntimeException("临时文件保存失败: " + tempPath);
+                }
+                // 从文件上传
+                videoUrl = aliOSSUtil.uploadFile(tempVideoFile, ossFilePath);
+            } else {
+                // 不需要截图:直接流式上传(最快路径,避免磁盘IO)
+                // 关键优化:直接使用 InputStream,不经过本地文件系统
+                try (InputStream inputStream = multipartFile.getInputStream()) {
+                    videoUrl = aliOSSUtil.uploadFile(inputStream, ossFilePath);
+                }
+            }
+            
+            if (videoUrl == null || videoUrl.isEmpty()) {
+                throw new RuntimeException("视频上传到OSS失败");
+            }
+            
+            long uploadTime = System.currentTimeMillis() - startTime;
+            log.info("视频上传成功: {}, 耗时: {} ms", videoUrl, uploadTime);
+            
+            // 创建返回结果
+            VideoUploadResult result = new VideoUploadResult(
+                    videoUrl,
+                    multipartFile.getOriginalFilename(),
+                    multipartFile.getSize(),
+                    uploadTime
+            );
+            
+            // 如果需要截图,异步处理(不阻塞返回)
+            if (Boolean.TRUE.equals(needScreenshot) && tempVideoFile != null && tempVideoFile.exists()) {
+                processVideoScreenshotAsync(tempVideoFile, videoFileName, prefix, uploadDir);
+            }
+            
+            return result;
+            
+        } catch (Exception e) {
+            log.error("高性能视频上传失败: {}", e.getMessage(), e);
+            // 清理临时文件
+            if (tempVideoFile != null && tempVideoFile.exists()) {
+                tempVideoFile.delete();
+            }
+            throw new RuntimeException("视频上传失败: " + e.getMessage(), e);
+        }
+    }
+    
+    /**
+     * 异步处理视频截图
+     * 
+     * @param tempVideoFile 临时视频文件
+     * @param videoFileName 视频文件名(不含扩展名)
+     * @param prefix OSS路径前缀
+     * @param uploadDir 上传目录
+     */
+    @Async
+    protected void processVideoScreenshotAsync(File tempVideoFile, String videoFileName, String prefix, String uploadDir) {
+        try {
+            log.info("开始异步处理视频截图: {}", tempVideoFile.getName());
+            
+            // 获取视频截图
+            String screenshotPath = videoUtils.getImg(uploadDir + tempVideoFile.getName());
+            
+            if (screenshotPath != null && !screenshotPath.isEmpty()) {
+                File screenshotFile = new File(screenshotPath);
+                if (screenshotFile.exists()) {
+                    try {
+                        // 上传截图到OSS
+                        Map<String, String> screenshotInfo = FileUtil.getFileNameAndType(screenshotFile);
+                        String screenshotUrl = aliOSSUtil.uploadFile(screenshotFile, prefix + videoFileName + "." + screenshotInfo.get("type"));
+                        
+                        log.info("视频截图上传成功: {} -> {}", tempVideoFile.getName(), screenshotUrl);
+                    } finally {
+                        // 清理临时截图文件
+                        screenshotFile.delete();
+                    }
+                } else {
+                    log.warn("视频截图文件不存在: {}", screenshotPath);
+                }
+            } else {
+                log.warn("视频截图失败: {}", tempVideoFile.getName());
+            }
+            
+        } catch (Exception e) {
+            log.error("异步处理视频截图失败: {}", tempVideoFile.getName(), e);
+        } finally {
+            // 清理临时视频文件
+            if (tempVideoFile != null && tempVideoFile.exists()) {
+                tempVideoFile.delete();
+            }
+        }
+    }
+
+    /**
+     * 上传多个文件(高性能优化版)
+     * 
+     * 性能优化点:
+     * 1. 并行处理多个文件,充分利用多核CPU
+     * 2. 直接使用 InputStream 流式上传,避免先保存到本地(减少一次IO操作)
+     * 3. 视频截图异步处理,不阻塞主流程
+     * 4. 立即返回文件URL列表,提升响应速度
      *
      * @param multipartRequest 多文件
      * @return List<String>
      */
     public List<String> uploadMoreFile(MultipartRequest multipartRequest) {
+        long startTime = System.currentTimeMillis();
         try {
             log.info("FileUpload.uploadMoreFile multipartRequest={}", multipartRequest.getFileNames());
-            Set<String> fileNameSet = multipartRequest.getMultiFileMap().keySet();
-            List<String> filePathList = new ArrayList<>();
-            for (String s : fileNameSet) {
-                MultipartFile multipartFile = multipartRequest.getFileMap().get(s);
-                log.info("FileUpload.uploadMoreFile fileName={}", multipartFile.getOriginalFilename());
-                String uploadDir = this.uploadDir.replace("file:///", "").replace("\\", "/");
-                String prefix;
-                Map<String, String> fileNameAndType = FileUtil.getFileNameAndType(multipartFile);
-                //区分文件类型
-                if (imageFileType.contains(fileNameAndType.get("type").toLowerCase())) {
-                    uploadDir += "/image";
-                    prefix = "image/";
-                    log.info("FileUpload.uploadMoreFile 获取到图片文件准备复制 {} {} {}", uploadDir, prefix, multipartFile.getOriginalFilename());
-                    // 去除文件名中的逗号,避免URL拼接时被错误分割
-                    String imageFileName = fileNameAndType.get("name").replaceAll(",", "");
-                    filePathList.add(aliOSSUtil.uploadFile(multipartFile, prefix + imageFileName + RandomCreateUtil.getRandomNum(6) + "." + fileNameAndType.get("type")));
-                    ;
-                } else if (videoFileType.contains(fileNameAndType.get("type").toLowerCase())) {
-                    uploadDir += "/video/";
-                    prefix = "video/";
-                    //上传视频文件
-                    log.info("FileUpload.uploadMoreFile 获取到视频文件准备复制 {} {} {}", uploadDir, prefix, multipartFile.getOriginalFilename());
-                    // 去除文件名中的逗号,避免URL拼接时被错误分割
-                    String videoFileName = fileNameAndType.get("name").replaceAll(",", "") + RandomCreateUtil.getRandomNum(6);
-                    String cacheVideoPath = copyFile(uploadDir, multipartFile);
-                    filePathList.add(aliOSSUtil.uploadFile(multipartFile, prefix + videoFileName + "." + fileNameAndType.get("type")));
-                    //缓存视频截图使用
-//                    File videoFile = new File(cacheVideoPath);
-//                    //获取视频某帧截图
-//                    log.info("FileUpload.uploadMoreFile 视频文件复制完毕, 获取第一秒图片 {}", videoFile.getName());
-//                    String videoPath = videoUtils.getImg(uploadDir + videoFile.getName());
-//                    log.info("FileUpload.uploadMoreFile 视频文件复制完毕, 图片位置 {}", videoPath);
-//                    if (!videoPath.isEmpty()) {
-//                        File videoImgFile = new File(videoPath);
-//                        Map<String, String> videoImg = FileUtil.getFileNameAndType(videoImgFile);
-//                        filePathList.add(aliOSSUtil.uploadFile(videoImgFile, prefix + videoFileName + "." + videoImg.get("type")));
-//                        videoImgFile.delete();
-//                        videoFile.delete();
-//                    } else {
-//                        throw new RuntimeException("视频截图失败");
-//                    }
-                } else if (voiceFileType.contains(fileNameAndType.get("type").toLowerCase())) {
-                    uploadDir += "/voice";
-                    prefix = "voice/";
-                    log.info("FileUpload.uploadMoreFile 获取到语音文件准备复制 {} {} {}", uploadDir, prefix, multipartFile.getOriginalFilename());
-                    // 去除文件名中的逗号,避免URL拼接时被错误分割
-                    String voiceFileName = fileNameAndType.get("name").replaceAll(",", "");
-                    filePathList.add(aliOSSUtil.uploadFile(multipartFile, prefix + voiceFileName + RandomCreateUtil.getRandomNum(6) + "." + fileNameAndType.get("type")));
-                } else if (privacyFileType.contains(fileNameAndType.get("type").toLowerCase())) {
-                    uploadDir += "/privacy/";
-                    prefix = "privacy/";
-                    log.info("FileUpload.uploadMoreFile 获取到隐私文件准备复制 {} {} {}", uploadDir, prefix, multipartFile.getOriginalFilename());
-                    // 去除文件名中的逗号,避免URL拼接时被错误分割
-                    String privacyFileName = fileNameAndType.get("name").replaceAll(",", "");
-                    filePathList.add(aliOSSUtil.uploadFile(multipartFile, prefix + privacyFileName + RandomCreateUtil.getRandomNum(6) + "." + fileNameAndType.get("type")));
-                } else if (pdfFileType.contains(fileNameAndType.get("type").toLowerCase())) {
-                    uploadDir += "/pdf";
-                    prefix = "pdf/";
-                    log.info("FileUpload.uploadMoreFile 获取到PDF文件准备复制 {} {} {}", uploadDir, prefix, multipartFile.getOriginalFilename());
-                    // 去除文件名中的逗号,避免URL拼接时被错误分割
-                    String pdfFileName = fileNameAndType.get("name").replaceAll(",", "");
-                    filePathList.add(aliOSSUtil.uploadFile(multipartFile, prefix + pdfFileName + RandomCreateUtil.getRandomNum(6) + "." + fileNameAndType.get("type")));
-                } else if (ohterFileType.contains(fileNameAndType.get("type").toLowerCase())) {
-                    uploadDir += "/other/";
-                    prefix = "other/";
-                    log.info("FileUpload.uploadMoreFile 获取到其他文件准备复制 {} {} {}", uploadDir, prefix, multipartFile.getOriginalFilename());
-                    // 去除文件名中的逗号,避免URL拼接时被错误分割
-                    String otherFileName = fileNameAndType.get("name").replaceAll(",", "");
-                    filePathList.add(aliOSSUtil.uploadFile(multipartFile, prefix + otherFileName + RandomCreateUtil.getRandomNum(6) + "." + fileNameAndType.get("type")));
+            Collection<MultipartFile> files = multipartRequest.getFileMap().values();
+            
+            if (files.isEmpty()) {
+                return Collections.emptyList();
+            }
+            
+            String baseUploadDir = this.uploadDir.replace("file:///", "").replace("\\", "/");
+            
+            // 如果只有一个文件,直接处理(避免线程池开销)
+            if (files.size() == 1) {
+                MultipartFile file = files.iterator().next();
+                return processSingleFileForUploadMore(file, baseUploadDir);
+            }
+            
+            // 多个文件使用并行流处理
+            List<String> filePathList = files.parallelStream()
+                    .flatMap(file -> {
+                        try {
+                            return processSingleFileForUploadMore(file, baseUploadDir).stream();
+                        } catch (Exception e) {
+                            log.error("处理文件失败: {}", file.getOriginalFilename(), e);
+                            return Stream.empty();
+                        }
+                    })
+                    .collect(Collectors.toList());
+            
+            long totalTime = System.currentTimeMillis() - startTime;
+            log.info("批量文件上传完成,文件数: {}, 总耗时: {} ms", files.size(), totalTime);
+            
+            return filePathList;
+        } catch (Exception e) {
+            log.error("FileUpload.uploadMoreFile ERROR Msg={}", e.getMessage(), e);
+            throw new RuntimeException("批量文件上传失败: " + e.getMessage(), e);
+        }
+    }
+    
+    /**
+     * 处理单个文件(用于 uploadMoreFile)
+     * 
+     * @param multipartFile 文件
+     * @param baseUploadDir 基础上传目录
+     * @return 文件URL列表(视频会包含视频URL和截图URL)
+     */
+    private List<String> processSingleFileForUploadMore(MultipartFile multipartFile, String baseUploadDir) {
+        List<String> filePathList = new ArrayList<>();
+        File tempVideoFile = null;
+        
+        try {
+            Map<String, String> fileNameAndType = FileUtil.getFileNameAndType(multipartFile);
+            String fileType = fileNameAndType.get("type").toLowerCase();
+            String prefix;
+            String uploadDir = baseUploadDir;
+            
+            // 区分文件类型并处理
+            if (imageFileType.contains(fileType)) {
+                prefix = "image/";
+                String imageFileName = fileNameAndType.get("name").replaceAll(",", "");
+                String ossFilePath = prefix + imageFileName + RandomCreateUtil.getRandomNum(6) + "." + fileNameAndType.get("type");
+                // 直接流式上传
+                try (InputStream inputStream = multipartFile.getInputStream()) {
+                    String url = aliOSSUtil.uploadFile(inputStream, ossFilePath);
+                    if (url != null) {
+                        filePathList.add(url);
+                    }
+                }
+                
+            } else if (videoFileType.contains(fileType)) {
+                prefix = "video/";
+                uploadDir += "/video/";
+                String videoFileName = fileNameAndType.get("name").replaceAll(",", "") + RandomCreateUtil.getRandomNum(6);
+                String ossFilePath = prefix + videoFileName + "." + fileNameAndType.get("type");
+                
+                // 视频需要截图,先保存文件
+                String tempPath = copyFile(uploadDir, multipartFile);
+                tempVideoFile = new File(tempPath);
+                if (!tempVideoFile.exists() || tempVideoFile.length() == 0) {
+                    throw new RuntimeException("视频文件保存失败: " + tempPath);
+                }
+                
+                // 从文件上传视频
+                String videoUrl = aliOSSUtil.uploadFile(tempVideoFile, ossFilePath);
+                if (videoUrl != null) {
+                    filePathList.add(videoUrl);
+                }
+                
+                // 异步处理视频截图
+                processVideoScreenshotAsync(tempVideoFile, videoFileName, prefix, uploadDir);
+                
+            } else if (voiceFileType.contains(fileType)) {
+                prefix = "voice/";
+                String voiceFileName = fileNameAndType.get("name").replaceAll(",", "");
+                String ossFilePath = prefix + voiceFileName + RandomCreateUtil.getRandomNum(6) + "." + fileNameAndType.get("type");
+                // 直接流式上传
+                try (InputStream inputStream = multipartFile.getInputStream()) {
+                    String url = aliOSSUtil.uploadFile(inputStream, ossFilePath);
+                    if (url != null) {
+                        filePathList.add(url);
+                    }
+                }
+                
+            } else if (privacyFileType.contains(fileType)) {
+                prefix = "privacy/";
+                String privacyFileName = fileNameAndType.get("name").replaceAll(",", "");
+                String ossFilePath = prefix + privacyFileName + RandomCreateUtil.getRandomNum(6) + "." + fileNameAndType.get("type");
+                // 直接流式上传
+                try (InputStream inputStream = multipartFile.getInputStream()) {
+                    String url = aliOSSUtil.uploadFile(inputStream, ossFilePath);
+                    if (url != null) {
+                        filePathList.add(url);
+                    }
+                }
+                
+            } else if (pdfFileType.contains(fileType)) {
+                prefix = "pdf/";
+                String pdfFileName = fileNameAndType.get("name").replaceAll(",", "");
+                String ossFilePath = prefix + pdfFileName + RandomCreateUtil.getRandomNum(6) + "." + fileNameAndType.get("type");
+                // 直接流式上传
+                try (InputStream inputStream = multipartFile.getInputStream()) {
+                    String url = aliOSSUtil.uploadFile(inputStream, ossFilePath);
+                    if (url != null) {
+                        filePathList.add(url);
+                    }
+                }
+                
+            } else if (ohterFileType.contains(fileType)) {
+                prefix = "other/";
+                String otherFileName = fileNameAndType.get("name").replaceAll(",", "");
+                String ossFilePath = prefix + otherFileName + RandomCreateUtil.getRandomNum(6) + "." + fileNameAndType.get("type");
+                // 直接流式上传
+                try (InputStream inputStream = multipartFile.getInputStream()) {
+                    String url = aliOSSUtil.uploadFile(inputStream, ossFilePath);
+                    if (url != null) {
+                        filePathList.add(url);
+                    }
                 }
             }
+            
             return filePathList;
+            
         } catch (Exception e) {
-            log.error("FileUpload.uploadMoreFile ERROR Msg={}", e.getMessage());
-            throw new RuntimeException(e);
+            log.error("处理文件失败: {}", multipartFile.getOriginalFilename(), e);
+            throw new RuntimeException("处理文件失败: " + multipartFile.getOriginalFilename(), e);
+        } finally {
+            // 注意:视频文件的临时文件在异步截图处理完成后才会删除,这里不删除
         }
     }