From fcd790b413d935ca1c82607a53d613d34503aa19 Mon Sep 17 00:00:00 2001
From: Administrator <15274802129@163.com>
Date: Wed, 14 Jan 2026 11:43:43 +0800
Subject: [PATCH] feat(video): 添加视频文件断点续传和多格式支持功能

---
 src/main/java/cc/mrbird/febs/ai/controller/fileUpload/FileUploadController.java |  118 +++++++++++++++++++++++++++++++++++++++++++++++++++++------
 1 files changed, 106 insertions(+), 12 deletions(-)

diff --git a/src/main/java/cc/mrbird/febs/ai/controller/fileUpload/FileUploadController.java b/src/main/java/cc/mrbird/febs/ai/controller/fileUpload/FileUploadController.java
index 1af5f69..97171f2 100644
--- a/src/main/java/cc/mrbird/febs/ai/controller/fileUpload/FileUploadController.java
+++ b/src/main/java/cc/mrbird/febs/ai/controller/fileUpload/FileUploadController.java
@@ -8,6 +8,7 @@
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;
 
+import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.*;
 import java.nio.file.Files;
@@ -125,7 +126,7 @@
      * 播放视频文件
      */
     @GetMapping("/play/{fileName}")
-    public void playVideo(@PathVariable("fileName") String fileName, HttpServletResponse response) {
+    public void playVideo(@PathVariable("fileName") String fileName, HttpServletRequest request, HttpServletResponse response) {
         try {
             String companyId = getCurrentUserCompanyId();
             String uploadDir = baseUploadDir + "/" + companyId;
@@ -135,18 +136,77 @@
                 return;
             }
 
-            // 设置响应头
-            response.setContentType("video/mp4");
-            response.setContentLengthLong(Files.size(filePath));
-            response.setHeader("Content-Disposition", "inline; filename=\"" + fileName + "\"");
+            // 获取文件大小
+            long fileSize = Files.size(filePath);
+            
+            // 检查是否有Range请求头
+            String rangeHeader = request.getHeader("Range");
+            if (rangeHeader != null) {
+                // 解析Range头,格式为"bytes=start-end"
+                String[] ranges = rangeHeader.replace("bytes=", "").split("-");
+                long start = 0, end = fileSize - 1;
+                
+                if (!ranges[0].isEmpty()) {
+                    start = Long.parseLong(ranges[0]);
+                }
+                if (ranges.length > 1 && !ranges[1].isEmpty()) {
+                    end = Long.parseLong(ranges[1]);
+                } else {
+                    end = fileSize - 1;
+                }
+                
+                // 确保范围有效
+                if (start > end || start >= fileSize) {
+                    response.setStatus(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE.value());
+                    return;
+                }
+                
+                // 限制结束位置不超过文件大小
+                if (end >= fileSize) {
+                    end = fileSize - 1;
+                }
+                
+                // 计算内容长度
+                long contentLength = end - start + 1;
+                
+                // 设置响应状态和头部信息
+                response.setStatus(HttpStatus.PARTIAL_CONTENT.value());
+                response.setContentType(getContentTypeByExtension(fileName));
+                response.setContentLengthLong(contentLength);
+                response.setHeader("Content-Range", "bytes " + start + "-" + end + "/" + fileSize);
+                response.setHeader("Accept-Ranges", "bytes");
+                response.setHeader("Content-Disposition", "inline; filename=\"" + fileName + "\"");
+                
+                // 从指定位置开始读取文件
+                try (RandomAccessFile randomAccessFile = new RandomAccessFile(filePath.toFile(), "r");
+                     OutputStream outputStream = response.getOutputStream()) {
+                    
+                    randomAccessFile.seek(start);
+                    byte[] buffer = new byte[8192];
+                    long remaining = contentLength;
+                    int bytesRead;
+                    
+                    while (remaining > 0 && (bytesRead = randomAccessFile.read(buffer, 0, (int) Math.min(buffer.length, remaining))) != -1) {
+                        outputStream.write(buffer, 0, bytesRead);
+                        remaining -= bytesRead;
+                    }
+                }
+            } else {
+                // 没有Range头,返回完整文件
+                response.setStatus(HttpStatus.OK.value());
+                response.setContentType(getContentTypeByExtension(fileName));
+                response.setContentLengthLong(fileSize);
+                response.setHeader("Accept-Ranges", "bytes");
+                response.setHeader("Content-Disposition", "inline; filename=\"" + fileName + "\"");
 
-            // 流式传输文件
-            try (InputStream inputStream = Files.newInputStream(filePath);
-                 OutputStream outputStream = response.getOutputStream()) {
-                byte[] buffer = new byte[8192];
-                int bytesRead;
-                while ((bytesRead = inputStream.read(buffer)) != -1) {
-                    outputStream.write(buffer, 0, bytesRead);
+                // 流式传输文件
+                try (InputStream inputStream = Files.newInputStream(filePath);
+                     OutputStream outputStream = response.getOutputStream()) {
+                    byte[] buffer = new byte[8192];
+                    int bytesRead;
+                    while ((bytesRead = inputStream.read(buffer)) != -1) {
+                        outputStream.write(buffer, 0, bytesRead);
+                    }
                 }
             }
         } catch (Exception e) {
@@ -156,6 +216,40 @@
     }
 
     /**
+     * 根据文件扩展名获取MIME类型
+     */
+    private String getContentTypeByExtension(String fileName) {
+        String extension = "";
+        int lastDotIndex = fileName.lastIndexOf('.');
+        if (lastDotIndex > 0) {
+            extension = fileName.substring(lastDotIndex).toLowerCase();
+        }
+        
+        switch (extension) {
+            case ".mp4":
+                return "video/mp4";
+            case ".avi":
+                return "video/x-msvideo";
+            case ".mov":
+                return "video/quicktime";
+            case ".wmv":
+                return "video/x-ms-wmv";
+            case ".flv":
+                return "video/x-flv";
+            case ".webm":
+                return "video/webm";
+            case ".mkv":
+                return "video/x-matroska";
+            case ".m3u8":
+                return "application/vnd.apple.mpegurl";
+            case ".ts":
+                return "video/MP2T";
+            default:
+                return "application/octet-stream";
+        }
+    }
+
+    /**
      * 获取文件列表
      */
     @GetMapping("/list")

--
Gitblit v1.9.1