tsurumure 8 месяцев назад
Родитель
Сommit
c75b139715

+ 4 - 3
db/sys_upload.sql

@@ -10,11 +10,12 @@ CREATE TABLE `sys_upload` (
     `id` BIGINT(10) AUTO_INCREMENT COMMENT 'ID',
     `category_id` BIGINT(10) COMMENT '分类ID',
     `request_id` VARCHAR(100) COMMENT '请求响应ID',
+    `upload_id` VARCHAR(100) COMMENT '分块上传ID',
     `user_id` BIGINT(10) COMMENT '上传用户ID',
-    `name` VARCHAR(255) NOT NULL COMMENT '文件名称',
-    `content_type` VARCHAR(255) NOT NULL COMMENT '文件类型',
+    `name` VARCHAR(255) COMMENT '文件名称',
+    `content_type` VARCHAR(255) COMMENT '文件类型',
     `url` VARCHAR(1000) COMMENT 'URL',
-    `object_key` VARCHAR(500) COMMENT 'ObjectKey',
+    `object_key` VARCHAR(500) NOT NULL COMMENT 'ObjectKey',
     `size` INT COMMENT '文件大小',
     `md5` VARCHAR(255) COMMENT '文件MD5',
     `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',

+ 32 - 0
src/main/java/com/backendsys/modules/common/utils/CommonUtil.java

@@ -1,4 +1,36 @@
 package com.backendsys.modules.common.utils;
 
+import cn.hutool.core.convert.Convert;
+import cn.hutool.core.lang.UUID;
+import org.springframework.web.multipart.MultipartFile;
+
 public class CommonUtil {
+
+    // [文件名] 获得文件后缀名
+    public static String getFilenameSuffix(MultipartFile multipartFile, boolean includeDot) {
+        String originalFileName = multipartFile.getOriginalFilename();
+        int dotIndex = originalFileName.lastIndexOf(".");
+        // 根据 includeDot 参数决定是否包含点
+        if (includeDot) {
+            return originalFileName.substring(dotIndex);
+        } else {
+            return originalFileName.substring(dotIndex + 1);
+        }
+    }
+
+    // [文件名] 生成新的文件名 (UUID + 后缀名)
+    public static String generateUUIDFilename(MultipartFile multipartFile) {
+        return Convert.toStr(UUID.randomUUID()) + getFilenameSuffix(multipartFile, true);
+    }
+
+    // [文件名] 插入 '-thumb' 文件名后缀
+    public static String insertThumbSuffix(String filePath) {
+        int dotIndex = filePath.lastIndexOf('.');
+        if (dotIndex != -1) {
+            return filePath.substring(0, dotIndex) + "-thumb" + filePath.substring(dotIndex);
+        } else {
+            return filePath;
+        }
+    }
+
 }

+ 0 - 41
src/main/java/com/backendsys/modules/common/utils/FilenameUtil.java

@@ -1,41 +0,0 @@
-package com.backendsys.modules.common.utils;
-
-import cn.hutool.core.convert.Convert;
-import cn.hutool.core.lang.UUID;
-import org.springframework.web.multipart.MultipartFile;
-
-public class FilenameUtil {
-
-    public static String getFilenameSuffix(MultipartFile multipartFile, boolean includeDot) {
-        String originalFileName = multipartFile.getOriginalFilename();
-        int dotIndex = originalFileName.lastIndexOf(".");
-        // 根据 includeDot 参数决定是否包含点
-        if (includeDot) {
-            return originalFileName.substring(dotIndex);
-        } else {
-            return originalFileName.substring(dotIndex + 1);
-        }
-    }
-
-    // [文件名] 生成新的文件名 (UUID + 后缀名)
-    public static String generateUUIDFilename(MultipartFile multipartFile) {
-        return Convert.toStr(UUID.randomUUID()) + getFilenameSuffix(multipartFile, true);
-    }
-
-    /**
-     * Inserts "_thumb" between the file name and extension.
-     * @param filePath The original file path.
-     * @return The modified file path with "_thumb" inserted.
-     */
-    public static String insertThumbSuffix(String filePath) {
-        int dotIndex = filePath.lastIndexOf('.');
-        if (dotIndex != -1) {
-            return filePath.substring(0, dotIndex) + "-thumb" + filePath.substring(dotIndex);
-        } else {
-            return filePath;
-        }
-    }
-
-
-
-}

+ 9 - 1
src/main/java/com/backendsys/modules/sdk/tencent/cos/service/TencentCosService.java

@@ -1,5 +1,7 @@
 package com.backendsys.modules.sdk.tencent.cos.service;
 
+import com.qcloud.cos.model.InitiateMultipartUploadResult;
+import com.qcloud.cos.model.PartListing;
 import com.qcloud.cos.model.UploadResult;
 import org.springframework.web.multipart.MultipartFile;
 
@@ -10,7 +12,7 @@ import java.util.Map;
 public interface TencentCosService {
 
     // [腾讯云COS] 上传对象
-    UploadResult putObject(MultipartFile file, String object_key) throws IOException;
+    UploadResult putObject(MultipartFile multipartFile, String object_key) throws IOException;
 
     // [腾讯云COS] 删除对象
     void deleteObject(String object_key);
@@ -20,4 +22,10 @@ public interface TencentCosService {
 
     // [腾讯云COS] 获得临时图片地址
     URL generateUrl(String object_key, Integer minutes);
+
+    // [腾讯云COS] 初始化分块上传
+    InitiateMultipartUploadResult initiateMultipartUpload(MultipartFile multipartFile);
+
+    // [腾讯云COS] 查询已上传的分块
+    PartListing listParts(String upload_id, String object_key);
 }

+ 66 - 9
src/main/java/com/backendsys/modules/sdk/tencent/cos/service/impl/TencentCosServiceImpl.java

@@ -4,7 +4,7 @@ import cn.hutool.core.convert.Convert;
 import cn.hutool.core.util.StrUtil;
 import com.backendsys.exception.CustException;
 import com.backendsys.modules.common.config.security.utils.HttpRequestUtil;
-import com.backendsys.modules.common.utils.FilenameUtil;
+import com.backendsys.modules.common.utils.CommonUtil;
 import com.backendsys.modules.sdk.tencent.cos.entity.Progress;
 import com.backendsys.modules.sdk.tencent.cos.entity.ProgressData;
 import com.backendsys.modules.sdk.tencent.cos.service.TencentCosService;
@@ -14,11 +14,10 @@ import com.qcloud.cos.COSClient;
 import com.qcloud.cos.ClientConfig;
 import com.qcloud.cos.auth.BasicCOSCredentials;
 import com.qcloud.cos.auth.COSCredentials;
+import com.qcloud.cos.exception.CosClientException;
+import com.qcloud.cos.exception.CosServiceException;
 import com.qcloud.cos.http.HttpMethodName;
-import com.qcloud.cos.model.GeneratePresignedUrlRequest;
-import com.qcloud.cos.model.ObjectMetadata;
-import com.qcloud.cos.model.PutObjectRequest;
-import com.qcloud.cos.model.UploadResult;
+import com.qcloud.cos.model.*;
 import com.qcloud.cos.region.Region;
 import com.qcloud.cos.transfer.Transfer;
 import com.qcloud.cos.transfer.TransferManager;
@@ -32,9 +31,8 @@ import org.springframework.web.multipart.MultipartFile;
 
 import java.io.*;
 import java.net.URL;
-import java.net.URLEncoder;
-import java.util.Arrays;
 import java.util.Date;
+import java.util.LinkedList;
 import java.util.List;
 
 // 图片处理概述
@@ -117,8 +115,8 @@ public class TencentCosServiceImpl implements TencentCosService {
         COSClient cosClient = getClient();
         try {
 
-            // 如果不传 object_key,则默认使用临时文件夹 (/temp) + MD5文件名
-            String filename = FilenameUtil.generateUUIDFilename(multipartFile);
+            // 如果不传 object_key,则默认使用临时文件夹 (/temp) + UUID文件名
+            String filename = CommonUtil.generateUUIDFilename(multipartFile);
             if (StrUtil.isEmpty(object_key)) object_key = "temp/" + filename;
             System.out.println("Upload object: " + object_key);
 
@@ -240,6 +238,7 @@ public class TencentCosServiceImpl implements TencentCosService {
         cosClient.shutdown();
     }
 
+    // [腾讯云COS] 获得临时图片地址
     @Override
     public URL generateUrl(String object_key, Integer minutes) {
         // 生成临时访问的URL (15分钟)
@@ -251,4 +250,62 @@ public class TencentCosServiceImpl implements TencentCosService {
         cosClient.shutdown();
         return url;
     }
+
+    // [腾讯云COS] 初始化分块上传
+    @Override
+    public InitiateMultipartUploadResult initiateMultipartUpload(MultipartFile multipartFile) {
+
+        if (multipartFile.isEmpty()) throw new CustException("file 文件不能为空");
+
+        COSClient cosClient = getClient();
+        try {
+
+            // 使用临时文件夹 (/temp) + UUID文件名
+            String filename = CommonUtil.generateUUIDFilename(multipartFile);
+            String object_key = "temp/" + filename;
+            System.out.println("initiateMultipartUpload object: " + object_key);
+
+            InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(BUCKET_NAME, object_key);
+            InitiateMultipartUploadResult initResult = cosClient.initiateMultipartUpload(request);
+            return initResult;
+
+        } catch (CosServiceException e) {
+            throw new CustException(e.getMessage());
+        } catch (CosClientException e) {
+            throw new CustException(e.getMessage());
+        } finally {
+            if (cosClient != null) cosClient.shutdown();
+        }
+    }
+
+
+    // [腾讯云COS] 查询已上传的分块
+    @Override
+    public PartListing listParts(String upload_id, String object_key) {
+
+        if (StrUtil.isEmpty(upload_id)) throw new CustException("upload_id 不能为空");
+        if (StrUtil.isEmpty(object_key)) throw new CustException("object_key 不能为空");
+
+        COSClient cosClient = getClient();
+        try {
+
+            // 用于保存已上传的分片信息
+            List<PartETag> partETags = new LinkedList<>();
+            PartListing partListing = null;
+            ListPartsRequest listPartsRequest = new ListPartsRequest(BUCKET_NAME, object_key, upload_id);
+
+            do {
+                partListing = cosClient.listParts(listPartsRequest);
+                for (PartSummary partSummary : partListing.getParts()) {
+                    partETags.add(new PartETag(partSummary.getPartNumber(), partSummary.getETag()));
+                    System.out.println("list multipart upload parts, partNum:" + partSummary.getPartNumber() + ", etag:" + partSummary.getETag());
+                }
+                return partListing;
+            } while (partListing.isTruncated());
+
+        } finally {
+            if (cosClient != null) cosClient.shutdown();
+        }
+    }
+
 }

+ 4 - 3
src/main/java/com/backendsys/modules/upload/controller/SysUploadController.java

@@ -3,6 +3,7 @@ package com.backendsys.modules.upload.controller;
 import cn.hutool.core.convert.Convert;
 import com.backendsys.entity.PageDTO;
 import com.backendsys.entity.System.SysFile.SysFileDTO;
+import com.backendsys.entity.Tencent.TencentCos.MultipartUploadDTO;
 import com.backendsys.exception.CustException;
 import com.backendsys.modules.common.config.security.utils.HttpRequestUtil;
 import com.backendsys.modules.common.utils.Result;
@@ -12,6 +13,7 @@ import com.backendsys.modules.sse.utils.SseEmitterUTF8;
 import com.backendsys.modules.sse.utils.SseUtil;
 import com.backendsys.modules.upload.entity.SysUpload;
 import com.backendsys.modules.upload.service.SysUploadService;
+import io.swagger.v3.oas.annotations.tags.Tag;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.validation.annotation.Validated;
@@ -23,10 +25,9 @@ import java.io.IOException;
 
 @Validated
 @RestController
+@Tag(name = "上传文件")
 public class SysUploadController {
 
-    @Autowired
-    private SseUtil sseUtil;
     @Autowired
     private HttpRequestUtil httpRequestUtil;
     @Autowired
@@ -42,7 +43,7 @@ public class SysUploadController {
     }
 
     /**
-     * 上传文件 (单文件上传不超过 20MB)
+     * 上传文件 (普通上传,单文件上传不超过 20MB)
      */
     @PreAuthorize("@ss.hasPermi(1.1)")
     @PostMapping("/api/upload/uploadSmall")

+ 59 - 0
src/main/java/com/backendsys/modules/upload/controller/SysUploadMultipartController.java

@@ -0,0 +1,59 @@
+package com.backendsys.modules.upload.controller;
+
+import com.backendsys.entity.Tencent.TencentCos.MultipartUploadDTO;
+import com.backendsys.modules.common.utils.Result;
+import com.backendsys.modules.upload.service.SysUploadMultipartService;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+
+@Validated
+@RestController
+@Tag(name = "上传文件 (分块上传)")
+public class SysUploadMultipartController {
+
+    @Autowired
+    private SysUploadMultipartService sysUploadMultipartService;
+
+    /*
+        分块上传的流程:
+        1.初始化分块上传(Initiate Multipart Upload),得到 UploadId。
+        2.使用 UploadId 上传分块(Upload Part)。
+        3.完成分块上传(Complete Multipart Upload)。
+     */
+
+    /**
+     * 初始化分块上传
+     */
+    @PreAuthorize("@ss.hasPermi(1.1)")
+    @PostMapping("/api/upload/initiateMultipartUpload")
+    public Result initiateMultipartUpload(@RequestParam("file") MultipartFile multipartFile, Long category_id) {
+        return Result.success().put("data", sysUploadMultipartService.initiateMultipartUpload(multipartFile, category_id));
+    }
+
+    /**
+     * 上传分块
+     */
+    @PreAuthorize("@ss.hasPermi(1.1)")
+    @PostMapping("/api/upload/uploadMultipart")
+    public Result uploadMultipart(@RequestParam("file") MultipartFile multipartFile, String upload_id) {
+        return Result.success().put("data", sysUploadMultipartService.uploadMultipart(multipartFile, upload_id));
+    }
+
+    /**
+     * 查询分块上传情况
+     */
+    @PreAuthorize("@ss.hasPermi(1.1)")
+    @GetMapping("/api/upload/getListParts")
+    public Result getListParts(String upload_id, String object_key) {
+        return Result.success().put("data", sysUploadMultipartService.listParts(upload_id, object_key));
+    }
+
+}

+ 1 - 0
src/main/java/com/backendsys/modules/upload/entity/SysUpload.java

@@ -17,6 +17,7 @@ public class SysUpload {
     private Long id;
     private Long category_id;
     private String request_id;
+    private String upload_id;
     private Long user_id;
     private String name;
     private String content_type;

+ 20 - 0
src/main/java/com/backendsys/modules/upload/service/SysUploadMultipartService.java

@@ -0,0 +1,20 @@
+package com.backendsys.modules.upload.service;
+
+import com.qcloud.cos.model.InitiateMultipartUploadResult;
+import com.qcloud.cos.model.PartListing;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.Map;
+
+public interface SysUploadMultipartService {
+
+    // 初始化分块上传
+    Map<String, Object> initiateMultipartUpload(MultipartFile multipartFile, Long category_id);
+
+    // 上传分块
+    Map<String, Object> uploadMultipart(MultipartFile multipartFile, String upload_id);
+
+    // 查询分块上传情况
+    PartListing listParts(String upload_id, String object_key);
+
+}

+ 84 - 0
src/main/java/com/backendsys/modules/upload/service/impl/SysUploadMultipartServiceImpl.java

@@ -0,0 +1,84 @@
+package com.backendsys.modules.upload.service.impl;
+
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.io.file.FileNameUtil;
+import cn.hutool.crypto.digest.DigestUtil;
+import com.backendsys.modules.common.config.security.utils.HttpRequestUtil;
+import com.backendsys.modules.sdk.tencent.cos.service.TencentCosService;
+import com.backendsys.modules.upload.dao.SysUploadDao;
+import com.backendsys.modules.upload.entity.SysUpload;
+import com.backendsys.modules.upload.service.SysUploadMultipartService;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.qcloud.cos.model.InitiateMultipartUploadResult;
+import com.qcloud.cos.model.PartListing;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+@Service
+public class SysUploadMultipartServiceImpl implements SysUploadMultipartService {
+
+    @Autowired
+    private HttpRequestUtil httpRequestUtil;
+    @Autowired
+    private TencentCosService tencentCosService;
+    @Autowired
+    private SysUploadDao sysUploadDao;
+
+    // 初始化分块上传 (获得 upload_id, object_key)
+    @Override
+    public Map<String, Object> initiateMultipartUpload(MultipartFile multipartFile, Long category_id) {
+
+        InitiateMultipartUploadResult uploadResult = tencentCosService.initiateMultipartUpload(multipartFile);
+        String upload_id = uploadResult.getUploadId();
+        String object_key = uploadResult.getKey();
+
+        try {
+            // 根据文件MD5 判断是否上传过文件 (如果上传过,则仅更新记录,不再上传文件)
+            String md5 = DigestUtil.md5Hex(multipartFile.getInputStream());
+            SysUpload sysUploadEntity = sysUploadDao.selectOne(new LambdaQueryWrapper<SysUpload>().eq(SysUpload::getMd5, md5));
+            if (sysUploadEntity == null) {
+                sysUploadEntity = new SysUpload();
+                sysUploadEntity.setCategory_id(category_id);
+                sysUploadEntity.setUser_id(httpRequestUtil.getUserId());
+                sysUploadEntity.setUpload_id(upload_id);
+                sysUploadEntity.setName(FileNameUtil.getName(object_key));
+                sysUploadEntity.setMd5(md5);
+                sysUploadEntity.setObject_key(object_key);
+                sysUploadDao.insert(sysUploadEntity);
+            } else {
+                // [更新] 上传文件记录
+                sysUploadEntity.setCategory_id(category_id);
+                sysUploadEntity.setUpload_id(upload_id);
+                sysUploadEntity.setName(FileNameUtil.getName(object_key));
+                sysUploadEntity.setObject_key(object_key);
+                sysUploadDao.updateById(sysUploadEntity);
+            }
+
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+
+        Map<String, Object> resp = new LinkedHashMap<>();
+        resp.put("upload_id", upload_id);
+        resp.put("object_key", object_key);
+        return resp;
+    }
+
+    // 上传分块
+    @Override
+    public Map<String, Object> uploadMultipart(MultipartFile multipartFile, String upload_id) {
+        return null;
+    }
+
+    // 查询分块上传情况
+    @Override
+    public PartListing listParts(String upload_id, String object_key) {
+        return tencentCosService.listParts(upload_id, object_key);
+    }
+
+}