springboot3+vue3+elementPlus+minio8.2 大文件分片上传

  1. 后端上传分片方法和合并分片方法

    复制代码
     /**
         * 分片上传文件
         * @param file
         * @param chunkIndex -分片序号
         * @param totalChunks - 总分片数
         * @param fileName
         * @return
         */
        @PostMapping("/chunk")
        public R uploadChunk(@RequestParam("file") MultipartFile file, @RequestParam("chunkIndex") int chunkIndex, @RequestParam("totalChunks") int totalChunks,
                             @RequestParam(value = "fileName") String fileName,
                             @RequestParam("objectName") String objectName,
                             @RequestParam("dir") String dir) {
            try {
                minioPartUploadService.uploadChunk(file,chunkIndex,totalChunks,fileName,objectName,dir);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return R.ok();
        }
    
        /**
         * 合并分片上传文件
         * @param fileName
         * @param totalChunks 总分片数
         * @return
         */
        @PostMapping("/merge")
        public R mergeChunks(@RequestParam("fileName") String fileName,@RequestParam("objectName") String objectName,@RequestParam("dir") String dir,@RequestParam("totalChunks") int totalChunks) {
            return minioPartUploadService.mergeChunks(fileName,objectName,dir,totalChunks);
        }
    
    
    package com.shpl.scp.admin.service.impl;
    
    import com.shpl.scp.admin.config.MinioConfiguration;
    import com.shpl.scp.admin.service.MinioPartUploadService;
    import com.shpl.scp.admin.service.SysFileService;
    import com.shpl.scp.common.core.util.R;
    import io.minio.*;
    import lombok.RequiredArgsConstructor;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.poi.util.StringUtil;
    import org.springframework.stereotype.Service;
    import org.springframework.web.multipart.MultipartFile;
    
    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    @Slf4j
    @Service
    @RequiredArgsConstructor
    public class MinioPartUploadServiceImpl implements MinioPartUploadService {
    
    
        private final MinioClient minioClient;
    
    
        private final MinioConfiguration configuration;
    
        private final SysFileService sysFileService;
        @Override
        public void uploadChunk(MultipartFile file,int chunkIndex,int totalChunks,String fileName,String objectName,String dir) {
            try {
                if (StringUtil.isNotBlank(dir)) {
                    // 确保 dir 以 '/' 结尾,避免路径拼接错误
                    dir = !dir.endsWith("/") ? dir + "/" : dir;
                    objectName = dir + objectName;
                }
                String chunkFileName = objectName + "_" + chunkIndex;
                minioClient.putObject(PutObjectArgs.builder()
                        .bucket(configuration.getBucketName())
                        .object(chunkFileName)
                        .stream(file.getInputStream(), file.getSize(), -1)
                        .contentType(file.getContentType())
                        .build());
            } catch (Exception e) {
                log.error("上传分片异常 Error uploading chunk: " + e.getMessage());
                e.printStackTrace();
            }
        }
    
        @Override
        public R mergeChunks(String fileName, String objectName, String dir, int totalChunks) {
            Map<String, String> resultMap = new HashMap<>(4);
            try {
               
                resultMap.put("bucketName", configuration.getBucketName());
                resultMap.put("fileName", objectName);
                resultMap.put("url", String.format("/admin/sys-file/oss/file?fileName=%s", objectName));
                //保存上传文件记录
                sysFileService.savefileLog(fileName,objectName,dir,null);
                if (StringUtil.isNotBlank(dir)) {
                    // 确保 dir 以 '/' 结尾,避免路径拼接错误
                    dir = !dir.endsWith("/") ? dir + "/" : dir;
                    objectName = dir + objectName;
                }
                String mergedFileName = objectName;//objectName + "_merged";
                // 收集所有分片作为合并源
                List<ComposeSource> sourceObjects = new ArrayList<>();
                for (int i = 0; i < totalChunks; i++) {
                    String chunkFileName = objectName + "_" + i;
                    // 添加分片到源列表
                    sourceObjects.add(ComposeSource.builder()
                            .bucket(configuration.getBucketName())
                            .object(chunkFileName)
                            .build());
                }
                // 使用 composeObject 在服务端合并所有分片
                minioClient.composeObject(
                        ComposeObjectArgs.builder()
                                .bucket(configuration.getBucketName())
                                .object(mergedFileName)
                                .sources(sourceObjects)
                                .build()
                );
                // 删除分片文件
                for (int i = 0; i < totalChunks; i++) {
                    String chunkFileName = objectName + "_" + i;
                    minioClient.removeObject(RemoveObjectArgs.builder()
                            .bucket(configuration.getBucketName())
                            .object(chunkFileName)
                            .build());
                }
            } catch (Exception e) {
                log.error("合并分片异常 Error mergeChunks : " + e.getMessage());
            }
            return R.ok(resultMap);
        }
    }
  2. vue3+elementPlus前端分片,每片5M并发上传

    复制代码
     <el-col :span="12" class="mb20">
              <el-form-item label="附件" prop="attribute2">
                <el-upload
                    class="upload-demo"
                    multiple
                    :limit="100"
                    :http-request="handleCustomUpload"
                    :on-progress="handleCustomUploadProgress"
                    :on-success="handleCustomUploadSuccess"
                    :on-error="handleCustomUploadError">
                  <el-button type="primary">点击上传</el-button>
                  <template #tip>
                    <div class="el-upload__tip">
                      支持大文件分片上传,每个分片 5MB,自动重试失败分片
                    </div>
                  </template>
                </el-upload>
              </el-form-item>
            </el-col>
    
    
    // 上传进度处理
    const handleCustomUploadProgress = (event: any, file: any, fileList: any[]) => {
      console.log(`上传进度:${file.name} - ${event.percent}%`);
    };
    
    // 上传成功处理
    const handleCustomUploadSuccess = (response: any, file: any, fileList: any[]) => {
      console.log(`${file.name} 上传成功`, response);
      // 可以在这里保存文件 URL 到表单
      // form.attribute2 = response.data?.url;
    };
    
    // 上传失败处理
    const handleCustomUploadError = (error: any, file: any, fileList: any[]) => {
      console.error(`${file.name} 上传失败`, error);
    };
    
    /**
     * 分片上传文件
     * @param file - 要上传的文件对象
     * @param onProgress - 进度回调函数 (可选)
     * @returns 上传结果
     */
    async function uploadPartFile(file: File, onProgress?: (percent: number) => Promise<void> | void) {
      const chunkSize = 5 * 1024 * 1024; // 5MB per chunk
      const totalChunks = Math.ceil(file.size / chunkSize);
      const uploadedChunks = new Set<number>();
      const maxRetries = 3;
      const concurrentLimit = 3; // 最多同时上传 3 个分片
    
      // 上传单个分片
      const uploadChunk = async (chunkIndex: number, retryCount = 0): Promise<void> => {
        try {
          console.log(`上传分片 ${chunkIndex + 1}/${totalChunks}`);
          const start = chunkIndex * chunkSize;
          const end = Math.min((chunkIndex + 1) * chunkSize, file.size);
          const chunk = file.slice(start, end);
    
          const formData = new FormData();
          formData.append('file', chunk);
          formData.append('chunkIndex', chunkIndex.toString());
          formData.append('totalChunks', totalChunks.toString());
          formData.append('fileName', file.name);
    
          // 使用项目的 request 工具
          await request({
            url: '/admin/sys-file/chunk',
            method: 'post',
            data: formData,
            headers: {
              'Content-Type': 'multipart/form-data',
            },
          });
    
          uploadedChunks.add(chunkIndex);
    
          // 更新进度
          if (onProgress) {
            const percent = Math.round((uploadedChunks.size / totalChunks) * 100);
            await onProgress(percent);
          }
        } catch (error) {
          // 失败重试
          if (retryCount < maxRetries) {
            console.warn(`分片 ${chunkIndex} 上传失败,第 ${retryCount + 1} 次重试...`);
            await uploadChunk(chunkIndex, retryCount + 1);
          } else {
            throw new Error(`分片 ${chunkIndex} 上传失败,已达到最大重试次数`);
          }
        }
      };
    
      // 并发控制上传
      const uploadWithConcurrency = async () => {
        const queue: number[] = [];
        for (let i = 0; i < totalChunks; i++) {
          queue.push(i);
        }
    
        const workers: Promise<void>[] = [];
    
        while (queue.length > 0 || workers.length > 0) {
          // 填充 worker 到并发限制
          while (workers.length < concurrentLimit && queue.length > 0) {
            const chunkIndex = queue.shift()!;
            const worker = uploadChunk(chunkIndex).finally(() => {
              const index = workers.indexOf(worker);
              if (index > -1) workers.splice(index, 1);
            });
            workers.push(worker);
          }
    
          // 等待至少一个 worker 完成
          if (workers.length > 0) {
            await Promise.race(workers);
          }
        }
    
        await Promise.all(workers);
      };
    
      try {
        // 开始上传所有分片
        await uploadWithConcurrency();
        console.log(`合并分片`+ file.name);
        // 合并分片
        await request({
          url: '/admin/sys-file/merge',
          method: 'post',
          data: {
            fileName: file.name,
            totalChunks: totalChunks,
          },
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
          },
        });
    
        console.log(`文件 ${file.name} 上传成功,共 ${totalChunks} 个分片`);
        return { success: true, fileName: file.name };
      } catch (error: any) {
        console.error('文件上传失败:', error);
        throw new Error(`文件上传失败:${error.message}`);
      }
    }
    
    /**
     * el-upload 组件的自定义上传方法
     * @param options - 上传选项
     */
    const handleCustomUpload = async (options: any) => {
      const { file, onSuccess, onError, onProgress } = options;
    
      try {
        await uploadPartFile(file, (percent) => {
          if (onProgress) {
            onProgress({ percent });
          }
        });
    
        if (onSuccess) {
          onSuccess({ code: 0, message: '上传成功' });
        }
        useMessage().success('文件上传成功');
      } catch (error: any) {
        console.error('上传失败:', error);
        if (onError) {
          onError(error);
        }
        useMessage().error(`文件上传失败:${error.message}`);
      }
    };

    或者如下js方法:

    复制代码
    async function uploadFile(file) {
       
        const chunkSize = 5 * 1024 * 1024; // 5MB
        const totalChunks = Math.ceil(file.size / chunkSize);
    
        for (let i = 0; i < totalChunks; i++) {
       
            const chunk = file.slice(i * chunkSize, (i + 1) * chunkSize);
            const formData = new FormData();
            formData.append("file", chunk);
            formData.append("chunkIndex", i);
            formData.append("totalChunks", totalChunks);
            formData.append("fileName", file.name);
    
            await fetch("/upload/chunk", {
       
                method: "POST",
                body: formData
            });
        }
    
        await fetch("/upload/merge", {
       
            method: "POST",
            body: JSON.stringify({
       
                fileName: file.name,
                totalChunks: totalChunks
            }),
            headers: {
       
                "Content-Type": "application/json"
            }
        });
    }
相关推荐
phltxy2 小时前
前缀和算法:从一维到二维,解锁高效区间求和
java·开发语言·算法
码上淘金2 小时前
Prometheus 瘦身指南:小白也能看懂的指标过滤与标签优化
java·算法·prometheus
一轮弯弯的明月2 小时前
竞赛刷题-建造最大岛屿-Java版
java·算法·深度优先·图搜索算法·学习心得
weixin199701080162 小时前
开山网商品详情页前端性能优化实战
java·前端·python
Memory_荒年2 小时前
AQS:Java并发包里的“包租公”,管理着你的锁和通行证!
java·后端
小钻风33662 小时前
Java 8 流式编程
java·开发语言·windows
肯戳加勾2 小时前
JAVA最常见的装箱/拆箱坑
java·后端
Memory_荒年2 小时前
ReentrantLock:AQS家的“锁二代”,但比 synchronized 更会“来事儿”
java·后端
巫山老妖2 小时前
OpenClaw 心跳机制实战:让 AI Agent 24 小时不停自主运行
java·前端