当前位置:首页 > 技术 > 正文内容

基于Spring Boot与Vue的分片上传与秒传实现

访客 技术 2026年7月16日 1

在处理大文件上传时,采用分片上传机制可有效提升上传成功率并支持断点续传。以下为基于 Spring Boot 后端与 Vue 前端结合的完整实现方案,包含文件分片、合并及极速秒传逻辑。

后端核心工具类:分片上传与合并


/**
 * 大文件分片上传处理
 * @param fileName 原始文件名
 * @param file 待上传文件
 * @param fileKey 唯一标识键
 * @param chunkIndex 当前分片索引(从1开始)
 * @param totalChunks 总分片数
 */
public static void uploadChunk(String fileName, MultipartFile file, String fileKey, int chunkIndex, int totalChunks) throws IOException {
    String basePath = getDefaultBaseDir() + "/" + DateUtils.datePath() + "/" + fileKey;
    File directory = new File(basePath);
    if (!directory.exists()) {
        directory.mkdirs();
    }

    // 构造分片文件路径
    String chunkPath = basePath + "/" + fileKey + "." + chunkIndex;
    File chunkFile = new File(chunkPath);
    file.transferTo(chunkFile);

    // 最后一个分片上传完成,触发合并
    if (chunkIndex == totalChunks) {
        mergeFiles(fileName, totalChunks, fileKey);
    }
}

/**
 * 合并所有分片文件
 */
private static void mergeFiles(String originalName, int totalChunks, String fileKey) throws IOException {
    String destPath = getDefaultBaseDir() + "/" + DateUtils.datePath() + "/" + fileKey + "/" + originalName;
    try (FileOutputStream fos = new FileOutputStream(destPath, true);
         FileInputStream fis = null) {

        byte[] buffer = new byte[10 * 1024 * 1024]; // 10MB 缓冲区
        for (int i = 1; i <= totalChunks; i++) {
            String chunkPath = getDefaultBaseDir() + "/" + DateUtils.datePath() + "/" + fileKey + "/" + fileKey + "." + i;
            try (FileInputStream fisTemp = new FileInputStream(chunkPath)) {
                int bytesRead;
                while ((bytesRead = fisTemp.read(buffer)) != -1) {
                    fos.write(buffer, 0, bytesRead);
                }
            }
        }
    } catch (Exception e) {
        throw new IOException("合并文件失败", e);
    } finally {
        System.gc(); // 建议手动触发垃圾回收
    }
}
  

控制器接口设计


// 检查已上传分片状态
@GetMapping("/check")
public AjaxResult checkStatus(@RequestParam String key) {
    PanoramicFileTb record = panoramicFileTbService.getLastUploadedChunk(key);
    log.info("查询分片进度: {}", key);
    return AjaxResult.success(record);
}

// 接收分片上传请求
@PostMapping("/upload")
@PreAuthorize("@ss.hasPermi('system:file:add')")
@Log(title = "文件上传", businessType = BusinessType.INSERT)
public AjaxResult handleChunkUpload(@RequestParam("file") MultipartFile file,
                                   FileUploadRequest request) throws Exception {
    uploadChunk(request.getFileName(), file, request.getKey(), request.getChunkIndex(), request.getTotalChunks());

    // 更新上传记录
    PanoramicFileTb info = PanoramicFileTb.builder()
        .fKey(request.getKey())
        .fIndex(request.getChunkIndex())
        .fTotal(request.getTotalChunks())
        .fName(request.getFileName())
        .build();

    if (panoramicFileTbService.exists(request.getKey())) {
        panoramicFileTbService.update(info);
    } else {
        panoramicFileTbService.save(info);
    }

    return AjaxResult.success();
}
  

数据传输对象定义


public class FileUploadRequest {
    private String key;
    private String fileName;
    private int chunkIndex;
    private int totalChunks;
    private long size;
    private String suffix;

    // getter/setter 略...
}
  

实体类:文件上传状态记录


@Builder
public class PanoramicFileTb extends BaseEntity {
    private Integer id;
    private String fKey;       // 文件唯一标识
    private Integer fIndex;    // 当前已上传分片索引
    private Integer fTotal;    // 总分片数量
    private String fName;      // 原始文件名

    // getter/setter 略...
}
  

服务层逻辑


@Override
public void save(PanoramicFileTb record) {
    panoramicFileTbMapper.insert(record);
}

@Override
public void update(PanoramicFileTb record) {
    panoramicFileTbMapper.update(record);
}

@Override
public boolean exists(String key) {
    return panoramicFileTbMapper.countByKey(key) > 0;
}

@Override
public PanoramicFileTb getLastUploadedChunk(String key) {
    PanoramicFileTb result = panoramicFileTbMapper.selectByFileKey(key);
    return result != null ? result : PanoramicFileTb.builder()
        .fKey(key)
        .fIndex(-1)
        .fTotal(0)
        .fName("")
        .build();
}
  

前端实现:分片上传与秒传

需安装 js-md5 用于生成文件指纹:


npm install js-md5 --save
  

<template>
  <div class="upload-container">
    <el-upload
      drag
      ref="uploader"
      :limit="1"
      :action="''"
      :auto-upload="false"
      :on-exceed="handleExceed"
      :http-request="handleUpload"
    >
      <i class="el-icon-upload"></i>
      <div class="el-upload__text">拖拽文件或点击上传<em>上传视频</em></div>
    </el-upload>
    <el-button type="primary" @click="submitUpload">开始上传</el-button>

    <el-card class="preview-card">
      <video :src="videoSrc" controls autoplay width="100%"></video>
    </el-card>
  </div>
</template>

<script>
import md5 from 'js-md5';

export default {
  data() {
    return {
      actionUrl: 'http://localhost:8098/upload',
      chunkSize: 10 * 1024 * 1024,
      videoSrc: ''
    };
  },
  methods: {
    handleExceed() {
      this.$message.warning('仅支持单个文件上传');
    },
    submitUpload() {
      this.$refs.uploader.submit();
    },
    async checkProgress(fileKey) {
      const res = await this.$http.get('/check', { params: { key: fileKey } });
      return res.data?.data || {};
    },
    async uploadNextChunk(params, file) {
      const { key, chunkIndex, totalChunks, chunkSize, fileName, suffix, size } = params;
      const start = (chunkIndex - 1) * chunkSize;
      const end = Math.min(size, start + chunkSize);
      const chunkBlob = file.slice(start, end);

      const formData = new FormData();
      formData.append('file', chunkBlob);
      formData.append('key', key);
      formData.append('chunkIndex', chunkIndex);
      formData.append('totalChunks', totalChunks);
      formData.append('size', size);
      formData.append('fileName', fileName);
      formData.append('suffix', suffix);

      const response = await this.$http.post('/upload', formData, {
        headers: { 'Content-Type': 'multipart/form-data' }
      });

      if (response.data.status) {
        this.$notify({
          title: '成功',
          message: `第 ${chunkIndex} 片上传完成`,
          type: 'success'
        });

        if (chunkIndex < totalChunks) {
          params.chunkIndex += 1;
          this.uploadNextChunk(params, file);
        } else {
          this.videoSrc = response.data.data;
          this.$notify({ title: '完成', message: '全部上传成功', type: 'success' });
        }
      }
    },
    async handleUpload(req) {
      const file = req.file;
      const name = file.name;
      const ext = name.split('.').pop().toLowerCase();
      if (ext !== 'mp4') {
        this.$message.error('仅支持 MP4 格式视频');
        return;
      }

      const fileSize = file.size;
      const totalChunks = Math.ceil(fileSize / this.chunkSize);
      const fileKey = md5(name + fileSize + file.type);

      const uploadParams = {
        key: fileKey,
        fileName: name,
        chunkIndex: 1,
        totalChunks,
        size: fileSize,
        suffix: ext,
        shardSize: this.chunkSize
      };

      const status = await this.checkProgress(fileKey);
      const lastUploaded = status.fIndex || -1;

      if (lastUploaded === -1) {
        this.uploadNextChunk(uploadParams, file);
      } else if (lastUploaded < totalChunks) {
        uploadParams.chunkIndex = lastUploaded + 1;
        this.uploadNextChunk(uploadParams, file);
      } else {
        this.videoSrc = status.fName;
        this.$message.success('秒传成功,文件已存在');
      }
    }
  }
};
</script>

<style scoped>
.upload-container {
  padding: 20px;
}
.preview-card {
  margin-top: 20px;
  text-align: center;
}
</style>
  
标签: Spring Boot

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。