做海报素材网站推荐唐山网站建设策划
- 作者: 五速梦信息网
- 时间: 2026年04月18日 10:03
当前位置: 首页 > news >正文
做海报素材网站推荐,唐山网站建设策划,微信如何上传wordpress,重庆建筑信息工程官网SpringBoot 大文件基于md5实现分片上传、断点续传、秒传 SpringBoot 大文件基于md5实现分片上传、断点续传、秒传前言1. 基本概念1.1 分片上传1.2 断点续传1.3 秒传1.4 分片上传的实现 2. 分片上传前端实现2.1 什么是WebUploader#xff1f;功能特点接口说明事件APIHook 机制 … SpringBoot 大文件基于md5实现分片上传、断点续传、秒传 SpringBoot 大文件基于md5实现分片上传、断点续传、秒传前言1. 基本概念1.1 分片上传1.2 断点续传1.3 秒传1.4 分片上传的实现 2. 分片上传前端实现2.1 什么是WebUploader功能特点接口说明事件APIHook 机制 2.2 前端代码实现2.2.1 模块引入2.2.2 核心代码核心分片组件WebUpload.vue引用组件App.vue 2.2.3 项目结构和运行效果 3 .分片上传后端实现3.1 项目结构和技术介绍3.2 核心代码控制类FileUploadController.java核心实现方法FileZoneRecordServiceImpl.java 4. 项目运行测试4.1 测试效果4.2 数据库记录4.3 上传目录文件4.4 网络访问上传的文件 5. 项目源码6.参考链接 SpringBoot 大文件基于md5实现分片上传、断点续传、秒传 阅读说明 本文适用于有初级后端开发基础或者初级前端开发者的人群如果不想看相关技术介绍可以直接跳转到第23章节可运行项目的前后端源码在文末后端地址: git clone https://gitee.com/zhouquanstudy/springboot-file-chunk-md5.git前端地址: git clone https://gitee.com/zhouquanstudy/file-chunk-upload-md5.git 如有疑问或者错误之处敬请指正 前言 在项目开发中需要上传非常大的文件时单次上传整个文件往往会遇到网络不稳定、带宽限制、上传失败等问题。为了解决这些问题文件分片上传也称为断点续传应运而生。本文将介绍大文件上传的基本概念及其在 SpringBoot 中的实现方法包括分片上传、断点续传和秒传技术。效果图如下 1. 基本概念 1.1 分片上传 分片上传的核心思想是将一个大文件分成若干份大小相等的多个小块数据块称为 Part。所有小块文件上传成功后再将其合并成完整的原始文件。 分片上传的优点 断点续传在网络中断或其他错误导致上传失败时只需重新上传失败的部分而不必从头开始上传整个文件从而提高上传的可靠性和效率。降低网络压力分片上传可以控制每个片段的大小避免一次性传输大量数据导致的网络拥堵提高网络资源的利用率。并行上传多个分片可以同时上传加快整体上传速度。灵活处理服务器可以更灵活地处理和存储文件分片减少内存和带宽的占用。 1.2 断点续传 断点续传是在下载或上传时将下载或上传任务一个文件或一个压缩包人为划分为几个部分每个部分采用一个线程进行上传或下载。如果遇到网络故障可以从已经上传或下载的部分开始继续上传或者下载未完成的部分而无需从头开始。 断点续传的实现过程 前端将文件按百分比进行计算每次上传文件的百分之一文件分片给文件分片编号。后端将前端每次上传的文件放入缓存目录。前端全部文件上传完毕后发送合并请求。后端使用 RandomAccessFile 进行多线程读取所有分片文件一个线程一个分片。后端每个线程按序号将分片文件写入目标文件中。上传过程中发生断网或手动暂停下次上传时发送续传请求后端删除最后一个分片。前端重新发送上次的文件分片。 1.3 秒传 文件上传中的“秒传”是一种优化文件上传过程的技术。其主要原理是通过文件的特征值通常是文件的哈希值如 MD5、SHA-1 或 SHA-256 等来判断文件是否已经存在于服务器上从而避免重复上传相同的文件。 秒传的具体流程 计算文件哈希值客户端在开始上传文件之前计算文件的哈希值。发送哈希值客户端将计算得到的哈希值发送给服务器。服务器校验服务器根据收到的哈希值查询数据库或文件存储系统判断是否已存在相同哈希值的文件。 如果文件已存在服务器直接返回文件已存在的信息客户端即可认为上传完成不需实际上传文件数据。如果文件不存在服务器通知客户端继续上传文件数据。 上传文件数据如果服务器通知文件不存在客户端实际上传文件数据服务器接收后存储并更新相应哈希值记录。 秒传的优点 节省带宽避免重复上传相同的文件特别是在大文件上传场景中效果显著。加快上传速度用户体验更好对于已存在的文件可以实现“秒传”。减轻服务器负担减少不必要的数据传输和存储压力。 秒传技术广泛应用于网盘、云存储、文件共享平台等场景中。 1.4 分片上传的实现 在 SpringBoot 中可以通过以下步骤实现分片上传 2.1 前端实现 前端使用 WebUploader 等库实现分片上传。具体步骤如下 使用 WebUploader 初始化上传组件设置分片大小及其他参数。在文件分片上传前计算每个分片的哈希值并发送到服务器。服务器验证分片的哈希值返回是否需要上传该分片。前端根据服务器返回结果决定是否上传分片。 2.2 后端实现 后端可以使用 SpringBoot 提供的文件上传接口来处理分片上传请求。具体步骤如下 接收并验证前端发送的分片文件及其哈希值。将分片文件保存到临时目录。保存分片文件信息如序号、哈希值等到数据库。在接收到所有分片后合并分片文件为完整文件。
分片上传前端实现 技术栈或技术点vue、webuploader、elmentui 2.1 什么是WebUploader WebUploader 是由百度公司开发的一个现代文件上传组件主要基于 HTML5同时辅以 Flash 技术。它支持大文件的分片上传提高了上传效率并且兼容主流浏览器。 官网地址 Web Uploader - Web Uploader (fex-team.github.io) 功能特点 分片、并发上传 WebUploader 支持将大文件分割成小片段并行上传极大地提高了上传效率。预览、压缩 支持常用图片格式如 jpg、jpeg、gif、bmp、png的预览和压缩节省了网络传输数据量。多途径添加文件 支持文件多选、类型过滤、拖拽文件和文件夹以及图片粘贴功能。HTML5 FLASH 兼容所有主流浏览器接口一致不需要担心内部实现细节。MD5 秒传 通过 MD5 值验证避免重复上传相同文件。易扩展、可拆分 采用模块化设计各功能独立成小组件可自由组合搭配。 接口说明 WebUploader 提供了丰富的接口和钩子函数以下是几个关键的接口 before-send-file 在文件发送之前执行。before-file 在文件分片后、上传之前执行。after-send-file 在所有文件分片上传完毕且无错误时执行。 WebUploader 的所有代码都在一个闭包中对外只暴露了一个变量 WebUploader避免与其他框架冲突。所有内部类和功能都通过 WebUploader 命名空间进行访问。 事件API Uploader 实例拥有类似 Backbone 的事件 API可以通过 on、off、once 和 trigger 进行事件绑定和触发。 uploader.on(fileQueued, function(file) {// 处理文件加入队列的事件 });this.uploader.on(uploadSuccess, (file, response) {// 上传成功事件 });除了通过 on 绑定事件外还可以直接在 Uploader 实例上添加事件处理函数 uploader.onFileQueued function(file) {// 处理文件加入队列的事件 };Hook 机制 关于hook机制的个人理解Hook机制就像是在程序中的特定事件或时刻比如做地锅鸡的时候设定一些“钩子”。当这些事件发生时程序会去“钩子”上找有没有要执行的额外功能然后把这些功能执行一下。这就好比在做地锅鸡的过程中你可以在某个步骤比如炖鸡的时候加上自己的调料或额外的配菜来调整和丰富最终的味道而不需要改动整体的食谱。 Uploader 内部功能被拆分成多个小组件通过命令机制进行通信。例如当用户选择文件后filepicker 组件会发送一个添加文件的请求负责队列的组件会根据配置项处理文件并决定是否加入队列。 webUploader.Uploader.register({before-send-file: beforeSendFile,before-send: beforeSend,after-send-file: afterSendFile},{// 时间点1所有分块进行上传之前调用此函数beforeSendFile: function(file) {// 利用 md5File() 方法计算文件的唯一标记符// 创建一个 deferred 对象var deferred webUploader.Deferred();// 计算文件的唯一标记用于断点续传和秒传// 请求后台检查文件是否已存在实现秒传功能return deferred.promise();},// 时间点2如果有分块上传则每个分块上传之前调用此函数beforeSend: function(block) {// 向后台发送当前文件的唯一标记// 请求后台检查当前分块是否已存在实现断点续传功能var deferred webUploader.Deferred();return deferred.promise();},// 时间点3所有分块上传成功之后调用此函数afterSendFile: function(file) {// 前台通知后台合并文件// 请求后台合并所有分块文件}} );2.2 前端代码实现 2.2.1 模块引入 在已有项目或者新的空vue项目中先执行下列命令
引入分片需要
npm install webuploader npm install jquery1.12.42.2.2 核心代码 核心分片组件WebUpload.vue templatediv classcenter-containerdiv classcontainerdiv classhandle-boxel-button typeprimary idextend-upload-chooseFile iconel-icon-upload2选择文件/el-buttondiv classshowMsg支持上传的文件后缀span stylecolor: #f10808; font-size: 18px{{options.fileType}}/span/div/divel-table :datafileList stylewidth: 100%el-table-column propfileName label文件名称 aligncenter width180/el-table-columnel-table-column propfileSize aligncenter label文件大小 width180/el-table-columnel-table-column label进度 aligncenter width300template slot-scopescopediv classprogress-containerel-progress :text-insidetrue :stroke-width15 :percentagescope.row.percentage/el-progress/div/template/el-table-columnel-table-column label上传速度 aligncenter width150template slot-scopescopediv{{ scope.row.speed }}/div/template/el-table-columnel-table-column label操作 aligncenter fixedrighttemplate slot-scopescopeel-button typetext iconel-icon-close classred clickremoveRow(scope.\(index, scope.row)移除/el-button/template/el-table-column/el-table/div/div /templatescript import \) from jquery import webUploader from webuploaderexport default {name: WebUpload,props: {headers: {type: String,default: },fileNumLimit: {type: Number,default: 100},fileSize: {type: Number,default: 100 * 1024 * 1024 * 1024},chunkSize: {type: Number,default: 1 * 1024 * 1024},uploadSuffixUrl: {type: String,default: http://localhost:8810},options: {default: function () {return {fileType: doc,docx,pdf,xls,xlsx,ppt,pptx,gif,jpg,jpeg,bmp,png,rar,zip,mp4,avi,fileUploadUrl: /v1/upload/zone/zoneUpload, //上传地址fileCheckUrl: /v1/upload/zone/md5Check, //检测文件是否存在urlcheckChunkUrl: /v1/upload/zone/md5Check, //检测分片urlmergeChunksUrl: /v1/upload/zone/merge, //合并文件请求地址 提交测试headers: {}}}},fileListData: {type: Array,default: function () {return []}}},data() {return {fileList: [], // 存储等待上传文件列表的数组percentage: 0, // 上传进度初始化为0uploader: {}, // WebUploader实例对象uploadStatus: el-icon-upload, // 上传状态图标默认为上传图标uploadStartTime: null, // 文件上传开始时间uploadedFiles: [] // 存储上传成功文件信息的数组}},mounted() {this.register()this.initUploader()this.initEvents()// 监视 fileListData 变化并将其赋值给 fileListthis.\(watch(fileListData, (newVal) {this.fileList [...newVal];});},methods: {initUploader() {var fileType this.options.fileTypethis.uploader webUploader.create({// 不压缩imageresize: false,// swf文件路径swf: ../../../assets/Uploader.swf, // swf文件路径 兼容ie的可以不设置// 默认文件接收服务端。server: this.uploadSuffixUrl this.options.fileUploadUrl,pick: {id: #extend-upload-chooseFile, //指定选择文件的按钮容器multiple: false //开启文件多选,},accept: [{title: file,extensions: fileType,mimeTypes: this.buildFileType(fileType)}],compressSize: 0,fileNumLimit: this.fileNumLimit,fileSizeLimit: 2 * 1024 * 1024 * 1024 * 1024,fileSingleSizeLimit: this.fileSize,chunked: true,threads: 10,chunkSize: this.chunkSize,prepareNextFile: false,})},register() {const that this;const options this.options;const uploadSuffixUrl this.uploadSuffixUrl;const fileCheckUrl uploadSuffixUrl options.fileCheckUrl;const checkChunkUrl uploadSuffixUrl options.checkChunkUrl;const mergeChunksUrl uploadSuffixUrl options.mergeChunksUrl;webUploader.Uploader.register({before-send-file: beforeSendFile,before-send: beforeSend,after-send-file: afterSendFile},{beforeSendFile: function (file) {const deferred webUploader.Deferred();new webUploader.Uploader().md5File(file, 0, 10 * 1024 * 1024).progress(function () {}).then(function (val) {file.fileMd5 val\).ajax({type: POST,url: fileCheckUrl,data: {checkType: FILE_EXISTS,contentType: file.type,zoneTotalMd5: val},dataType: json,success: function (response) {if (response.success) {that.uploader.skipFile(file)// 更新进度条that.percentage 1that.\(notify.success({showClose: true,message: [ \){file.name} ]文件秒传})that.uploadedFiles.push(response.data)deferred.reject()} else {if (response.code 30001) {const m response.message 文件后缀 file.ext;that.uploader.skipFile(file)that.setTableBtn(file.id, m)that.uploadedFiles.push(response.data)deferred.reject()} else {deferred.resolve()}}}})})return deferred.promise()},beforeSend: function (block) {const deferred webUploader.Deferred();new webUploader.Uploader().md5File(block.file, block.start, block.end).progress(function () {}).then(function (val) {block.zoneMd5 val\(.ajax({type: POST,url: checkChunkUrl,data: {checkType: ZONE_EXISTS,zoneTotalMd5: block.file.fileMd5,zoneMd5: block.zoneMd5},dataType: json,success: function (response) {if (response.success) {deferred.reject()} else {deferred.resolve()}}})})return deferred.promise()},afterSendFile: function (file) {\).ajax({type: POST,url: mergeChunksUrl ?totalMd5 file.fileMd5,dataType: JSON,success: function (res) {if (res.success) {const data res.data.fileInfo;that.uploader.skipFile(file)// 更新进度条that.percentage 1that.uploadedFiles.push(data)}}})}})},initEvents() {const that this;const uploader this.uploader;uploader.on(fileQueued, function (file) {// 清空现有文件列表实现只上传单个文件if (!this.multiple) {this.fileList []this.uploadedFiles []}const fileSize that.formatFileSize(file.size);const row {fileId: file.id,fileName: file.name,fileSize: fileSize,validateMd5: 0%,progress: 等待上传,percentage: 0,speed: 0KB/s,state: 就绪};that.fileList.push(row)that.uploadToServer()})this.uploader.on(uploadProgress, (file, percentage) {// 找到对应文件并更新进度和速度let targetFile this.fileList.find(item item.fileId file.id)if (targetFile) {// 计算上传速度const currentTime new Date().getTime()const elapsedTime (currentTime - (targetFile.startTime || currentTime)) / 1000 // 秒const uploadedSize percentage * file.sizeconst speed this.formatFileSize(uploadedSize / elapsedTime) /s// 更新文件信息targetFile.percentage parseFloat((percentage * 100).toFixed(2))targetFile.speed speedtargetFile.startTime targetFile.startTime || currentTime}})this.uploader.on(uploadSuccess, (file, response) {this.uploadedFiles []if (response.code 10000) {response.data.fileName response.data.originalNameresponse.data.percentage this.fileList[0].percentageresponse.data.fileSize this.fileList[0].fileSizeresponse.data.speed this.fileList[0].speedthis.uploadedFiles.push(response.data)// this.\(message.success(上传完成)} else {this.\)message.error(上传失败: response.message)}})/上传之前/uploader.on(uploadBeforeSend, function (block, data, headers) {data.fileMd5 block.file.fileMd5data.contentType block.file.typedata.chunks block.file.chunksdata.zoneTotalMd5 block.file.fileMd5data.zoneMd5 block.zoneMd5data.zoneTotalCount block.chunksdata.zoneNowIndex block.chunkdata.zoneTotalSize block.totaldata.zoneStartSize block.startdata.zoneEndSize block.endheaders.Authorization that.options.headers.Authorization})uploader.on(uploadFinished, function () {that.percentage 1that.uploadStaus el-icon-uploadthat.\(message.success({showClose: true,message: 文件上传完毕})})},setTableBtn(fileId, showmsg, sid) {var fileList this.fileListfor (var i 0; i fileList.length; i) {if (fileList[i].fileId fileId) {this.fileList[i].progress showmsgthis.fileList[i].sid sid || }}},removeRow(index, row) {this.fileList.splice(index, 1)this.removeFileFromUploaderQueue(row.fileId)this.\)emit(removeRow, index, row)},removeFileFromUploaderQueue(fileId) {const files this.uploader.getFiles()for (let i 0; i files.length; i) {if (files[i].id fileId) {this.uploader.removeFile(files[i], true)break}}},uploadToServer() {this.uploadStatus el-icon-loadingthis.uploadStartTime new Date()this.uploader.upload()},clearFiles() {const that thisthat.uploadStaus el-icon-uploadthat.uploader.reset()this.$emit(clearFiles, [])},buildFileType(fileType) {var ts fileType.split(,)var ty for (var i 0; i ts.length; i) {ty ty . ts[i] ,}return ty.substring(0, ty.length - 1)},strIsNull(str) {if (typeof str undefined || str null || str ) {return true} else {return false}},formatFileSize(size) {var fileSize 0if (size / 1024 1024) {var len size / 1024 / 1024fileSize len.toFixed(2) MB} else if (size / 1024 / 1024 1024) {len size / 1024 / 1024fileSize len.toFixed(2) GB} else {len size / 1024fileSize len.toFixed(2) KB}return fileSize}} } /script style .center-container {transform: scale(1.1); /* 缩放整个容器 /margin-left: 300px;justify-content: center;align-items: center;height: 100vh; / 让容器占满整个视口高度 / }.container {padding: 30px;border: 1px solid #312828;border-radius: 5px; }.handle-box {margin-bottom: 20px; }#picker div:nth-child(2) {width: 100% !important;height: 100% !important; }.webuploader-element-invisible {position: absolute !important;clip: rect(1px 1px 1px 1px); / IE6, IE7 /clip: rect(1px, 1px, 1px, 1px); }.webuploader-pick-hover {background: #409eff; }/ 统一设置 label 的字体大小 / .el-table-column label {font-size: 30px; }.showMsg {margin: 5px;font-size: 16px; } /style 引用组件App.vue templatediv idappmainel-form :span20el-col :span20el-form-item!– 分片上传组件 –WebUpload/WebUpload/el-form-item/el-col/el-form/main/div /templatescript import WebUpload from ./components/WebUpload.vueexport default {name: App,components: {WebUpload} } /scriptstyle #app {font-family: Avenir, Helvetica, Arial, sans-serif;-webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: grayscale;text-align: center;color: #2c3e50;margin-top: 60px; } /style 同时使用了样式因此需要引入element-ui npm install element-ui -S# main.js中内容 import Vue from vue; import ElementUI from element-ui; import element-ui/lib/theme-chalk/index.css; import App from ./App.vue;Vue.use(ElementUI);new Vue({el: #app,render: h h(App) }); 2.2.3 项目结构和运行效果 执行npm run sever运行后页面效果和最终项目代码结构 3 .分片上传后端实现 3.1 项目结构和技术介绍 本项目的后端采用Spring Boot框架结合MyBatis-Plus以提高数据库操作的效率。数据库使用MySQL提供高性能和可靠性。这些技术的组合确保了系统的稳定性和高效性并简化了开发和维护过程 3.2 核心代码 控制类FileUploadController.java FileUploadController类负责处理文件上传相关的操作。其主要功能包括 大文件分片上传处理前端分片上传的大文件请求接收并记录文件片段信息。MD5校验校验文件或分片的MD5值检查文件或分片是否已经存在以避免重复上传。文件合并在所有分片上传完成后将所有分片合并成一个完整的文件。 package com.example.zhou.controller;import com.example.zhou.common.Result; import com.example.zhou.common.ResultCode; import com.example.zhou.entity.ArchiveZoneRecord; import com.example.zhou.entity.enums.CheckType; import com.example.zhou.param.FileUploadResultBO; import com.example.zhou.param.ZoneUploadResultBO; import com.example.zhou.service.IFileZoneRecordService; import lombok.extern.slf4j.Slf4j; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.; import org.springframework.web.multipart.MultipartFile;import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import javax.validation.constraints.NotNull; import java.util.Date;/*** author ZhouQuan* desciption 文件上传操作录控制类* date 2024/5/4 17:09/ Validated Slf4j RestController RequestMapping(/v1/upload/zone) public class FileUploadController {Resourceprivate IFileZoneRecordService iFileZoneRecordService;/** 大文件分片上传** param multipartFile 文件二进制数据* param id 文件ID* param name 文件名称* param type 文件类型* param lastModifiedDate 最后修改日期* param fileMd5 文件MD5* param zoneTotalMd5 总分片MD5* param zoneMd5 当前分片MD5* param zoneTotalCount 总分片数量* param zoneNowIndex 当前分片序号* param zoneTotalSize 文件总大小* param zoneStartSize 文件开始位置* param zoneEndSize 文件结束位置* param request HttpServletRequest 对象* return 返回上传结果/PostMapping(/zoneUpload)public Result zoneUpload(RequestParam(file) NotNull(message 文件不能为空) MultipartFile multipartFile,RequestParam(id) String id,RequestParam(name) String name,RequestParam(type) String type,RequestParam(lastModifiedDate) Date lastModifiedDate,RequestParam(fileMd5) String fileMd5,RequestParam(zoneTotalMd5) String zoneTotalMd5,RequestParam(zoneMd5) String zoneMd5,RequestParam(zoneTotalCount) int zoneTotalCount,RequestParam(zoneNowIndex) int zoneNowIndex,RequestParam(zoneTotalSize) long zoneTotalSize,RequestParam(zoneStartSize) long zoneStartSize,RequestParam(zoneEndSize) long zoneEndSize,HttpServletRequest request) {long startTime System.currentTimeMillis();// 使用构造函数初始化 ArchiveZoneRecord 对象ArchiveZoneRecord archiveZoneRecord new ArchiveZoneRecord(id, name, type, lastModifiedDate, fileMd5, zoneTotalMd5, zoneMd5,zoneTotalCount, zoneNowIndex, zoneTotalSize, zoneStartSize, zoneEndSize);// 调用服务方法进行上传ZoneUploadResultBO resultBo iFileZoneRecordService.zoneUpload(request, archiveZoneRecord, multipartFile);long endTime System.currentTimeMillis();log.info(zoneUpload 上传耗时{} ms, (endTime - startTime));return new Result(ResultCode.SUCCESS, resultBo);}/** 校验文件或者分片的md5值** param ArchiveZoneRecord 文件或者分片信息* param checkType FILE_EXISTS:校验文件是否存在,ZONE_EXISTS:校验分片是否存在* param request* return/PostMapping(/md5Check)public Result md5Check(ArchiveZoneRecord ArchiveZoneRecord, CheckType checkType, HttpServletRequest request) {long l System.currentTimeMillis();Result result iFileZoneRecordService.md5Check(ArchiveZoneRecord, checkType, request);log.info(md5Check校验耗时{}, System.currentTimeMillis() - l);return result;}/** 合并文件* 前端所有分片上传完成时发起请求将所有的文件合并成一个完整的文件** param totalMd5 总文件的MD5值* param request* return/PostMapping(/merge)public Result mergeZoneFile(RequestParam(totalMd5) String totalMd5, HttpServletRequest request) {long l System.currentTimeMillis();FileUploadResultBO result iFileZoneRecordService.mergeZoneFile(totalMd5, request);log.info(merge合并校验耗时{}, System.currentTimeMillis() - l);return new Result(ResultCode.SUCCESS, result);}} 核心实现方法FileZoneRecordServiceImpl.java package com.example.zhou.service.impl;import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.example.zhou.common.Result; import com.example.zhou.common.ResultCode; import com.example.zhou.config.FileUploadConfig; import com.example.zhou.entity.Archive; import com.example.zhou.entity.ArchiveZoneRecord; import com.example.zhou.entity.enums.CheckType; import com.example.zhou.mapper.ArchiveMapper; import com.example.zhou.mapper.ArchiveRecordMapper; import com.example.zhou.param.FileUploadResultBO; import com.example.zhou.param.ZoneUploadResultBO; import com.example.zhou.service.IFileRecordService; import com.example.zhou.service.IFileZoneRecordService; import com.example.zhou.utils.FileHandleUtil; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import org.springframework.util.DigestUtils; import org.springframework.web.multipart.MultipartFile;import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Date; import java.util.List; import java.util.UUID;Slf4j Service public class FileZoneRecordServiceImpl extends ServiceImplArchiveRecordMapper, ArchiveZoneRecord implements IFileZoneRecordService {Resourceprivate ArchiveMapper archiveMapper;Resourceprivate FileUploadConfig fileUploadConfig;Resourceprivate IFileRecordService fileRecordService;Resourceprivate ArchiveRecordMapper archiveRecordMapper;Overridepublic ZoneUploadResultBO zoneUpload(HttpServletRequest request, ArchiveZoneRecord archiveZoneRecord,MultipartFile multipartFile) {if (multipartFile.isEmpty()) {// 如果文件为空返回错误信息throw new RuntimeException(请选择文件);}try {// 根据UUID生成同步锁避免多线程竞争确保线程安全// 根据MD5和zoneTotalMd5查询分片记录ArchiveZoneRecord zoneRecord archiveRecordMapper.selectOne(Wrappers.ArchiveZoneRecordlambdaQuery().eq(ArchiveZoneRecord::getZoneMd5, archiveZoneRecord.getZoneMd5()).eq(ArchiveZoneRecord::getZoneTotalMd5, archiveZoneRecord.getZoneTotalMd5()).last(limit 1));// 如果分片记录存在返回已存在的分片记录信息if (zoneRecord ! null) {ZoneUploadResultBO resultBo new ZoneUploadResultBO(zoneRecord, true,zoneRecord.getZoneNowIndex());return resultBo;}Archive archive null;// 根据MD5和上传类型查询文件记录archive archiveMapper.selectOne(Wrappers.ArchivelambdaQuery().eq(Archive::getMd5Value, archiveZoneRecord.getZoneTotalMd5()).last(limit 1));// (文件秒传)如果文件记录已存在且已经上传完毕则返回文件已上传的错误信息if (archive ! null archive.isZoneFlag() archive.isMergeFlag()) {throw new RuntimeException(文件已经上传);}// 获取文件md5String filemd5 archiveZoneRecord.getZoneMd5();// 如果分片记录的md5为空则生成md5if (StringUtils.isBlank(filemd5)) {filemd5 DigestUtils.md5DigestAsHex(multipartFile.getInputStream());archiveZoneRecord.setZoneMd5(filemd5);}// 获取文件后缀String fileSuffix . FilenameUtils.getExtension(multipartFile.getOriginalFilename());// 获取保存路径String saveFilePath ;String fileRecordId ;// 如果数据库中不存在对应的文件记录则创建新记录if (archive null) {// 保存分片的路径saveFilePath Paths.get(fileUploadConfig.getUploadFolder(), chunks,archiveZoneRecord.getZoneTotalMd5()).toString();// 保存文件记录fileRecordId saveFileRecord(request, archiveZoneRecord, multipartFile.getOriginalFilename(),saveFilePath);} else {// 如果文件记录已存在则获取文件记录idfileRecordId archive.getSid();saveFilePath archive.getPath();}// 生成临时文件文件名String serverFileName filemd5 fileSuffix .chunks;// 上传文件FileHandleUtil.upload(multipartFile.getInputStream(), saveFilePath, serverFileName);// 保存分片记录saveFileZoneRecord(archiveZoneRecord, filemd5, fileRecordId, serverFileName, saveFilePath,fileSuffix);// 返回结果信息ZoneUploadResultBO resultBo new ZoneUploadResultBO(archiveZoneRecord, false,archiveZoneRecord.getZoneNowIndex());return resultBo;} catch (Exception e) {e.printStackTrace();log.error(文件上传错误错误消息 e.getMessage());throw new RuntimeException(文件上传错误错误消息 e.getMessage());}}/** 保存分片记录** param archiveZoneRecord* param fileMd5* param fileRecordId* param serverFileName* param localPath* param fileSuffix/private void saveFileZoneRecord(ArchiveZoneRecord archiveZoneRecord, String fileMd5, String fileRecordId,String serverFileName, String localPath, String fileSuffix) {archiveZoneRecord.setSid(UUID.randomUUID() );archiveZoneRecord.setZoneMd5(fileMd5);archiveZoneRecord.setArchiveSid(fileRecordId);archiveZoneRecord.setName(serverFileName);archiveZoneRecord.setZonePath(localPath);archiveZoneRecord.setZoneCheckDate(new Date());archiveZoneRecord.setZoneSuffix(fileSuffix);super.saveOrUpdate(archiveZoneRecord);}private String saveFileRecord(HttpServletRequest request, ArchiveZoneRecord ArchiveZoneRecord,String originalFilename, String localPath) {Archive archive new Archive();archive.setSize(ArchiveZoneRecord.getZoneTotalSize());archive.setFileType(FilenameUtils.getExtension(originalFilename));archive.setMd5Value(ArchiveZoneRecord.getZoneTotalMd5());archive.setOriginalName(originalFilename);archive.setPath(localPath);archive.setZoneFlag(true);archive.setMergeFlag(false);archive.setZoneTotal(ArchiveZoneRecord.getZoneTotalCount());archive.setZoneDate(LocalDateTime.now());fileRecordService.saveOrUpdate(archive);return archive.getSid();}Overridepublic Result md5Check(ArchiveZoneRecord archiveZoneRecord, CheckType checkType, HttpServletRequest request) {if (checkType CheckType.FILE_EXISTS) {Archive archive archiveMapper.selectOne(Wrappers.ArchivelambdaQuery().eq(Archive::getMd5Value, archiveZoneRecord.getZoneTotalMd5()).last(limit 1));return archive ! null archive.isMergeFlag() ?new Result(ResultCode.FILEUPLOADED, archive) :new Result(ResultCode.SERVER_ERROR, 请选择文件上传);} else {ArchiveZoneRecord ArchiveZoneRecordDB archiveRecordMapper.selectOne(Wrappers.ArchiveZoneRecordlambdaQuery().eq(ArchiveZoneRecord::getZoneMd5, archiveZoneRecord.getZoneMd5()).eq(ArchiveZoneRecord::getZoneTotalMd5, archiveZoneRecord.getZoneTotalMd5()).last(limit 1));return ArchiveZoneRecordDB ! null ?new Result(ResultCode.SUCCESS, ArchiveZoneRecordDB) :new Result(ResultCode.SERVER_ERROR, 分片文件不存在继续上传);}}/** 合并分片文件并保存到服务器** param totalMd5 分片文件的总MD5值* param request HttpServletRequest对象* return 返回合并结果/Overridepublic FileUploadResultBO mergeZoneFile(String totalMd5, HttpServletRequest request) {FileUploadResultBO resultBO new FileUploadResultBO();if (totalMd5 null || totalMd5.trim().length() 0) {throw new RuntimeException(总MD5值不能为空);}// 查询总MD5值对应的文件信息Archive archive archiveMapper.selectOne(Wrappers.ArchivelambdaQuery().eq(Archive::getMd5Value, totalMd5).last(limit 1));if (archive null) {throw new RuntimeException(文件MD5: totalMd5 对应的文件不存在);}if (archive.isZoneFlag() archive.isMergeFlag()) {// 如果文件已上传并合并完成则返回文件信息resultBO.setFileId(archive.getSid());resultBO.setFileInfo(archive);Path netPath Paths.get(fileUploadConfig.getStaticAccessPath(), archive.getFileType(),archive.getPath());resultBO.setNetworkPath(netPath.toString());return resultBO;}String fileType archive.getFileType();// 查询分片记录ListArchiveZoneRecord archiveZoneRecords super.list(Wrappers.ArchiveZoneRecordlambdaQuery().eq(ArchiveZoneRecord::getZoneTotalMd5, totalMd5).orderByAsc(ArchiveZoneRecord::getZoneNowIndex));if (CollectionUtils.isEmpty(archiveZoneRecords)) {throw new RuntimeException(文件MD5: totalMd5 对应的分片记录不存在);}// 获取当前日期和时间用于生成文件路径String pathDate LocalDateTime.now().format(DateTimeFormatter.ofPattern(yyyy/MMdd/HH));// 获取文件上传路径不包含文件名 示例D:/upload/file/2023/03/08/String localPath Paths.get(fileUploadConfig.getUploadFolder(), fileType, pathDate).toString();// 生成唯一文件名String saveFileName UUID.randomUUID() . archive.getFileType();// 设置文件信息的路径和全路径archive.setFullPath(localPath saveFileName);archive.setPath(Paths.get(pathDate, saveFileName).toString());archive.setFileName(saveFileName);// 合并分片文件并写入文件mergeAndWriteFile(localPath, saveFileName, archiveZoneRecords, pathDate, archive);// 保存或更新文件信息fileRecordService.saveOrUpdate(archive);// 获取网络访问路径Path netPath Paths.get(fileUploadConfig.getUploadUrl(), fileUploadConfig.getStaticAccessPath(),fileType, pathDate, saveFileName);resultBO.setNetworkPath(netPath.toString());resultBO.setFileInfo(archive);resultBO.setFileId(archive.getSid());return resultBO;}/** 合并分片文件并写入文件** param localPath 存储文件的本地路径* param saveFileName 保存的文件名* param archiveZoneRecords 分片文件的记录列表* param pathDate 文件路径日期部分* param archive 文件档案对象*/private void mergeAndWriteFile(String localPath, String saveFileName, ListArchiveZoneRecord archiveZoneRecords,String pathDate, Archive archive) {String allPath Paths.get(localPath, saveFileName).toString();File targetFile new File(allPath);FileOutputStream fileOutputStream null;try {if (!targetFile.exists()) {// 创建目录如果不存在FileHandleUtil.createDirIfNotExists(localPath);// 创建目标临时文件如果不存在则创建targetFile.getParentFile().mkdirs();targetFile.createNewFile();}fileOutputStream new FileOutputStream(targetFile, true); // 使用追加模式// 合并分片文件for (ArchiveZoneRecord archiveZoneRecord : archiveZoneRecords) {File partFile new File(archiveZoneRecord.getZonePath(), archiveZoneRecord.getName());try (FileInputStream fis new FileInputStream(partFile)) {byte[] buffer new byte[1024];int len;while ((len fis.read(buffer)) ! -1) {fileOutputStream.write(buffer, 0, len);}}}// 更新文件信息archive.setZoneMergeDate(LocalDateTime.now());archive.setMergeFlag(true);fileRecordService.saveOrUpdate(archive);// 删除由于并发导致文件archive多条重复记录todo 这里在上传方法中使用乐观锁锁来避免fileRecordService.remove(Wrappers.ArchivelambdaQuery().eq(Archive::getMd5Value, archive.getMd5Value()).isNotNull(Archive::isMergeFlag));} catch (Exception e) {e.printStackTrace();throw new RuntimeException(文件合并失败原因 e.getMessage());} finally {if (fileOutputStream ! null) {try {fileOutputStream.close();} catch (IOException e) {throw new RuntimeException(e);}}}} }4. 项目运行测试 4.1 测试效果 4.2 数据库记录 如下图所示文件表中存储已经上传到服务器中当前文件的上传信息文件分片表则记录了当前文件分片所有的分片信息 4.3 上传目录文件 如下图所示上传目录中存在chunks(分片文件夹)和mp4(合并后的文件) 4.4 网络访问上传的文件 访问效果如下 5. 项目源码 gitee项目地址
后端地址
git clone https://gitee.com/zhouquanstudy/springboot-file-chunk-md5.git
前端地址
git clone https://gitee.com/zhouquanstudy/file-chunk-upload-md5.git项目压缩包 https://zhouquanquan.lanzouh.com/b00g2d7sdg 密码:bpyg6.参考链接 官方地址 https://github.com/fex-team/webuploader基于SpringBoot和WebUploader实现大文件分块上传.断点续传.秒传-阿里云开发者社区 (aliyun.com)在Vue项目中使用WebUploader实现文件上传_vue webuploader-CSDN博客vue中大文件上传webuploader前端用法_vue webuploader 大文件上传-CSDN博客
- 上一篇: 做海报哪个网站好互联网时代 网站建设
- 下一篇: 做海报有哪些网站外贸网站
相关文章
-
做海报哪个网站好互联网时代 网站建设
做海报哪个网站好互联网时代 网站建设
- 技术栈
- 2026年04月18日
-
做海报的网站有哪些内容无障碍插件wordpress
做海报的网站有哪些内容无障碍插件wordpress
- 技术栈
- 2026年04月18日
-
做国外网站调查挣取零花钱佛山网页网站设计
做国外网站调查挣取零花钱佛山网页网站设计
- 技术栈
- 2026年04月18日
-
做海报有哪些网站外贸网站
做海报有哪些网站外贸网站
- 技术栈
- 2026年04月18日
-
做海外贸易网站为什么别的电脑能打开的网站我的电脑打不开
做海外贸易网站为什么别的电脑能打开的网站我的电脑打不开
- 技术栈
- 2026年04月18日
-
做海外网站的公司本地dede网站怎么上线
做海外网站的公司本地dede网站怎么上线
- 技术栈
- 2026年04月18日
