Browse Source

feat:修改视频上传为分片上传

lalala 1 year ago
parent
commit
26400922e2

+ 2 - 2
application.yaml

@@ -80,8 +80,8 @@ config:
 
 doubao:
   ark:
-    api-key: a60ca069-fdd2-4060-a6a4-1a63d7659480
-    endpoint-id: ep-20250210154808-gj4ln
+    api-key: c87a7b2f-33d1-45d5-9a08-d36d2204f05c
+    endpoint-id: ep-20250719220931-ncfwh
 xunfei:
   ark:
     app-id: fdf4a5d8

+ 121 - 1
src/main/java/com/njuzr/eaibackend/config/OssConfig.java

@@ -2,10 +2,17 @@ package com.njuzr.eaibackend.config;
 
 import com.aliyun.oss.OSS;
 import com.aliyun.oss.OSSClientBuilder;
+import com.aliyun.oss.model.*;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+
 /**
  * @author: Leonezhurui
  * @Date: 2024/3/13 - 09:15
@@ -23,8 +30,121 @@ public class OssConfig {
     @Value("${aliyun.oss.accessKeySecret}")
     private String accessKeySecret;
 
+    @Value("${aliyun.oss.bucketName}")
+    private String bucketName;
+
+    private OSS ossClient;
+
+
+    // 存储uploadId的映射,key为fileMd5
+    private final ConcurrentHashMap<String, String> uploadIdMap = new ConcurrentHashMap<>();
+    // 存储已上传分片的映射,key为fileMd5
+    private final ConcurrentHashMap<String, List<PartETag>> partETagsMap = new ConcurrentHashMap<>();
     @Bean
     public OSS ossClient() {
-        return new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
+        if(null == this.ossClient){
+            this.ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
+        }
+         return this.ossClient;
+    }
+
+    /**
+     * 初始化分片上传
+     */
+    public String initMultipartUpload(String fileMd5, String fileName) {
+        String objectName = "uploads/" + fileMd5+"-" + fileName;
+        InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(bucketName, objectName);
+        InitiateMultipartUploadResult result = ossClient.initiateMultipartUpload(request);
+        String uploadId = result.getUploadId();
+        System.out.println("init"+ uploadId);
+        uploadIdMap.put(fileMd5, uploadId);
+        partETagsMap.put(fileMd5, new ArrayList<>());
+        return uploadId;
+    }
+
+    public boolean isUploadInitialized(String fileMd5) {
+        return uploadIdMap.containsKey(fileMd5);
+    }
+
+    /**
+     * 上传分片
+     */
+    public boolean uploadPart(String fileName,String fileMd5, int partNumber, InputStream inputStream, long partSize) throws IOException {
+        String uploadId = uploadIdMap.get(fileMd5);
+        if (uploadId == null) {
+            throw new RuntimeException("未找到对应的uploadId,请先初始化分片上传");
+        }
+        String objectName = "uploads/" + fileMd5 + "-" + fileName;
+        UploadPartRequest uploadPartRequest = new UploadPartRequest();
+        uploadPartRequest.setBucketName(bucketName);
+        uploadPartRequest.setKey(objectName);
+        uploadPartRequest.setUploadId(uploadId);
+        uploadPartRequest.setInputStream(inputStream);
+        uploadPartRequest.setPartSize(partSize);
+        uploadPartRequest.setPartNumber(partNumber);
+
+        UploadPartResult uploadPartResult = ossClient.uploadPart(uploadPartRequest);
+        partETagsMap.get(fileMd5).add(uploadPartResult.getPartETag());
+        return true;
+    }
+
+    /**
+     * 完成分片上传
+     */
+    public String completeMultipartUpload(String fileMd5, String fileName) {
+        String uploadId = uploadIdMap.get(fileMd5);
+        if (uploadId == null) {
+            throw new RuntimeException("未找到对应的uploadId");
+        }
+        String objectName = "uploads/" + fileMd5 + "-" + fileName;
+        CompleteMultipartUploadRequest completeRequest = new CompleteMultipartUploadRequest(
+                bucketName,
+                objectName,
+                uploadId,
+                partETagsMap.get(fileMd5));
+
+        CompleteMultipartUploadResult result = ossClient.completeMultipartUpload(completeRequest);
+        // 清理资源
+        uploadIdMap.remove(fileMd5);
+        partETagsMap.remove(fileMd5);
+        // 生成文件访问URL
+        return generateFileUrl(objectName);
+    }
+
+    private String generateFileUrl(String objectName) {
+        return "https://" + bucketName + "." + endpoint.replace("https://", "") + "/" + objectName;
+    }
+
+    /**
+     * 取消分片上传
+     */
+    public void abortMultipartUpload(String fileMd5, String filName) {
+        String uploadId = uploadIdMap.get(fileMd5);
+        if (uploadId != null) {
+            String objectName = "uploads/" + fileMd5 + "-" + filName;
+            AbortMultipartUploadRequest abortRequest =
+                    new AbortMultipartUploadRequest(bucketName, objectName, uploadId);
+            ossClient.abortMultipartUpload(abortRequest);
+
+            uploadIdMap.remove(fileMd5);
+            partETagsMap.remove(fileMd5);
+        }
+    }
+
+    /**
+     * 获取已上传的分片数量
+     */
+    public int getChunkCount(String fileMd5) {
+        List<PartETag> partETags = partETagsMap.get(fileMd5);
+        return partETags != null ? partETags.size() : 0;
+    }
+
+    /**
+     * 关闭OSS客户端
+     */
+    public void shutdown() {
+        if (ossClient != null) {
+            ossClient.shutdown();
+        }
     }
 }

+ 0 - 4
src/main/java/com/njuzr/eaibackend/controller/DoubaoController.java

@@ -55,10 +55,6 @@ public class DoubaoController {
         return MyResponse.success(doubaoService.deleteChatHistory(userId, assignmentId, userQuestion, voiceCode));
     }
 
-    @PostMapping("/saveSigment")
-    public MyResponse saveSigment(@RequestParam Long userId, @RequestParam Long assignmentId, @RequestParam("file") MultipartFile file, @RequestParam Double duration) {
-        return MyResponse.success(doubaoService.saveSigment(userId, assignmentId, file, duration));
-    }
 
     @GetMapping("/getSigment")
     public MyResponse getSigment(@RequestParam Long userId, @RequestParam Long assignmentId) {

+ 60 - 2
src/main/java/com/njuzr/eaibackend/controller/FileController.java

@@ -1,11 +1,11 @@
 package com.njuzr.eaibackend.controller;
-
+import com.njuzr.eaibackend.config.OssConfig;
+import com.njuzr.eaibackend.service.DoubaoService;
 import com.njuzr.eaibackend.utils.OssUtil;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;
-
 import java.io.IOException;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.TimeUnit;
@@ -22,11 +22,17 @@ import java.util.concurrent.TimeUnit;
 public class FileController {
     private final OssUtil ossUtil;
 
+
     @Autowired
     public FileController(OssUtil ossUtil) {
         this.ossUtil = ossUtil;
     }
 
+    @Autowired
+    OssConfig ossConfig;
+
+    @Autowired
+    DoubaoService doubaoService;
 
     @PostMapping
     public CompletableFuture<MyResponse> upload(@RequestParam("file") MultipartFile file) {
@@ -57,4 +63,56 @@ public class FileController {
         return MyResponse.success("删除成功");
     }
 
+
+    @PostMapping("/large")
+    public MyResponse uploadLargeFile(
+            @RequestPart("file") MultipartFile file,
+            @RequestParam("fileName") String fileName,
+            @RequestParam("fileMd5") String fileMd5,
+            @RequestParam("chunkCount") Integer chunkCount,
+            @RequestParam("currentIndex") Integer currentIndex,
+            @RequestParam("userId") Long userId,
+            @RequestParam("assignmentId") Long assignmentId,
+            @RequestParam("duration") Double duration) {
+
+        try {
+            // 如果是第一个分片,初始化上传
+            if (!ossConfig.isUploadInitialized(fileMd5)) {
+                synchronized(this) {
+                    if (!ossConfig.isUploadInitialized(fileMd5)) {
+                        ossConfig.initMultipartUpload(fileMd5, fileName);
+                    }
+                }
+            }
+            // 上传当前分片
+            boolean uploadSuccess = ossConfig.uploadPart(
+                    fileName,
+                    fileMd5,
+                    currentIndex + 1,  // 分片号从1开始
+                    file.getInputStream(),
+                    file.getSize()
+            );
+            if (!uploadSuccess) {
+                return MyResponse.error(500, "分片上传失败");
+            }
+            // 检查是否所有分片都已上传
+            int uploadedChunks = ossConfig.getChunkCount(fileMd5);
+            if (uploadedChunks == chunkCount) {
+                // 所有分片已上传,完成上传
+                String fileUrl = ossConfig.completeMultipartUpload(fileMd5, fileName);
+                log.info("文件上传完成,ETag: {}", fileUrl);
+                doubaoService.saveSigment(userId,assignmentId, fileUrl, duration);
+                return MyResponse.success(1.0f);
+            } else {
+                float progress = (float) uploadedChunks / chunkCount;
+                log.info("文件上传进度: {}/{} ({}%)", uploadedChunks, chunkCount, progress * 100);
+                return MyResponse.success(progress);
+            }
+        } catch (IOException e) {
+            // 上传失败时取消上传
+            ossConfig.abortMultipartUpload(fileMd5, fileName);
+            return MyResponse.error(500, "文件上传失败: " + e.getMessage());
+        }
+    }
+
 }

+ 1 - 1
src/main/java/com/njuzr/eaibackend/service/DoubaoService.java

@@ -20,7 +20,7 @@ public interface DoubaoService {
     List<SpeakingAIDialogue.DialogueEntry> getChatHistory(Long userId, Long assignmentId);
     List<SpeakingAIDialogue.DialogueEntry> deleteChatHistory(Long userId, Long assignmentId, String userQuestion, Integer voiceCode);
 
-    String saveSigment(Long userId, Long assignmentId, MultipartFile file, Double duration);
+    void saveSigment(Long userId, Long assignmentId, String fileUrl, Double duration);
 
     List<String> getSigment(Long userId, Long assignmentId);
 

+ 0 - 1
src/main/java/com/njuzr/eaibackend/service/impl/CourseServiceImpl.java

@@ -317,7 +317,6 @@ public class CourseServiceImpl implements CourseService {
             throw MyException.create(HttpStatus.BAD_REQUEST, "课程不存在");
         if (!target.getEnrollCode().equals(enrollDTO.getEnrollCode()))
             throw MyException.create(HttpStatus.BAD_REQUEST, "选课码错误");
-
         Enrollment enrollment = ModelMapperUtil.map(enrollDTO, Enrollment.class);
         courseStudentMapper.insert(enrollment);
     }

+ 18 - 33
src/main/java/com/njuzr/eaibackend/service/impl/DoubaoServiceImpl.java

@@ -320,40 +320,19 @@ public class DoubaoServiceImpl implements DoubaoService {
         return new ArrayList<>();
     }
 
-    public String saveSigment(Long userId, Long assignmentId, MultipartFile file, Double duration) {
-        // 1. 立即返回响应
-        String immediateResponse = "请求已接收,系统正在处理您的文件";
-        // 2. 在后台异步执行上传和保存操作
-        CompletableFuture.runAsync(() -> {
-            try {
-                // 上传文件
-                String filePath = "uploads/" + file.getOriginalFilename();
-                String url = ossUtil.uploadFile(file.getInputStream(), filePath);
-                log.info("文件上传成功,URL: {}", url);
-
-                // 保存到数据库
-                Query query = new Query(Criteria.where("userId").is(userId)
-                        .and("assignmentId").is(assignmentId));
-                SpeakingAIDialogue dialogue = mongoTemplate.findOne(query, SpeakingAIDialogue.class);
-
-                if (dialogue == null) {
-                    log.error("不存在相应记录 - userId: {}, assignmentId: {}", userId, assignmentId);
-                    return;
-                }
-                Update update = new Update()
-                        .push("audioSigment", url)
-                        .push("audioDuration", duration);
-                mongoTemplate.updateFirst(query, update, SpeakingAIDialogue.class);
+    public void saveSigment(Long userId, Long assignmentId, String fileUrl, Double duration) {
+        // 保存到数据库
+        Query query = new Query(Criteria.where("userId").is(userId)
+                .and("assignmentId").is(assignmentId));
+        SpeakingAIDialogue dialogue = mongoTemplate.findOne(query, SpeakingAIDialogue.class);
 
-                log.info("后台处理完成 - userId: {}, assignmentId: {}, URL: {}", userId, assignmentId, url);
-            } catch (IOException e) {
-                log.error("后台处理失败 - userId: {}, assignmentId: {}", userId, assignmentId, e);
-            }
-        }).exceptionally(ex -> {
-            log.error("后台任务执行异常", ex);
-            return null;
-        });
-        return immediateResponse;
+        if (dialogue == null) {
+            throw new MyException(400, "不存在相应记录");
+        }
+        Update update = new Update()
+                .push("audioSigment", fileUrl)
+                .push("audioDuration", duration);
+        mongoTemplate.updateFirst(query, update, SpeakingAIDialogue.class);
     }
 
     @Override
@@ -362,6 +341,12 @@ public class DoubaoServiceImpl implements DoubaoService {
         Query query = Query.query(Criteria.where("userId").is(userId).and("assignmentId").is(assignmentId));
         // 执行删除操作,获取删除的结果
         SpeakingAIDialogue result = mongoTemplate.findAndRemove(query, SpeakingAIDialogue.class);
+        if(result != null && result.getAudioSigment() != null){
+            for (String s : result.getAudioSigment()) {
+                ossUtil.deleteFile(s);
+                System.out.println(s);
+            }
+        }
         // 如果删除操作返回的结果不为空,说明成功删除,返回 true;否则返回 false
         return startChat(userId, assignmentId, userQuestion, voiceCode);
     }