☰
简历附件 500MB?分片上传 + 断点续传 + 秒传,前端后端一次配齐
2026/10/8 6:56:14 网站建设 项目流程

导读:招聘平台要传附件,视频简历、作品集动不动几百 MB,普通表单上传两个问题:大文件传一半断了全完蛋、Nginx 默认client_max_body_size 1m直接 413。我后来把上传改成分片:前端把文件切块、后端合并,顺带做了秒传和断点续传。这篇把前端 uniapp 和后端 Spring Boot 的完整链路讲清楚。

分片上传的完整链路

整个流程分三步:先初始化拿 uploadId → 分片逐个上传 → 全部传完触发合并。秒传则是在初始化前先按文件 MD5 查一下,服务端有就直接返回文件地址,一秒钟完事。

前端 File.slice 分片 → 后端接收分片存临时目录 ↓ 全部上传完成 后端校验分片完整性 → 合并成完整文件 → 返回最终 URL

后端:分片表 + 三个接口

先建表,分片记录按 uploadId 关联:

CREATETABLEqkl_file_upload(idBIGINTAUTO_INCREMENTPRIMARYKEY,upload_idVARCHAR(64)NOTNULLCOMMENT'上传任务ID',file_md5VARCHAR(32)NOTNULLCOMMENT'文件MD5,用于秒传',file_nameVARCHAR(255)NOTNULL,file_sizeBIGINTNOTNULL,chunk_sizeINTNOTNULLCOMMENT'分片大小,字节',total_chunksINTNOTNULL,uploaded_chunksINTNOTNULLDEFAULT0COMMENT'已上传分片数',statusTINYINTNOTNULLDEFAULT0COMMENT'0处理中 1已完成',final_pathVARCHAR(500)DEFAULTNULL,create_timeDATETIMEDEFAULTCURRENT_TIMESTAMP,UNIQUEKEYuk_upload_id(upload_id))ENGINE=InnoDBDEFAULTCHARSET=utf8mb4COMMENT='大文件分片上传记录';

三个接口,职责单一:

// 1. 初始化:查秒传,没有就建任务@PostMapping("/init")publicResultinit(@RequestBodyUploadInitDTOdto){// 先查秒传FileUploadrecord=fileUploadMapper.selectOne(newLambdaQueryWrapper().eq(FileUpload::getFileMd5,dto.getFileMd5()).eq(FileUpload::getStatus,1).last("LIMIT 1"));if(record!=null){returnResult.ok(UploadInitVO.ofExist(record.getFinalPath()));}StringuploadId=UUID.randomUUID().toString().replace("-","");FileUploadupload=newFileUpload();upload.setUploadId(uploadId);upload.setFileMd5(dto.getFileMd5());upload.setFileName(dto.getFileName());upload.setFileSize(dto.getFileSize());upload.setChunkSize(dto.getChunkSize());upload.setTotalChunks((int)Math.ceil((double)dto.getFileSize()/dto.getChunkSize()));fileUploadMapper.insert(upload);returnResult.ok(UploadInitVO.ofNew(uploadId,upload.getTotalChunks()));}
// 2. 上传分片:chunkIndex 从 0 开始,落到临时目录@PostMapping("/chunk")publicResultuploadChunk(@RequestParam("uploadId")StringuploadId,@RequestParam("chunkIndex")IntegerchunkIndex,@RequestParam("file")MultipartFilefile){Stringdir=uploadDir+"/"+uploadId;Filetarget=newFile(dir,chunkIndex+".part");file.transferTo(target);// 幂等:已存在的分片覆盖写,保证重传不重复计数intdone=fileUploadMapper.addUploadedChunk(uploadId);returnResult.ok();}

addUploadedChunk是个乐观计数:UPDATE qkl_file_upload SET uploaded_chunks = uploaded_chunks + 1 WHERE upload_id = ? AND uploaded_chunks < total_chunks,重传同一分片会覆盖文件但不重复累加计数,这是断点续传的关键。

// 3. 合并:校验分片数量,逐个拼接后删除临时目录@PostMapping("/merge")publicResultmerge(@RequestBodyMergeDTOdto){FileUploadupload=fileUploadMapper.selectByUploadId(dto.getUploadId());if(upload==null||upload.getUploadedChunks()<upload.getTotalChunks()){thrownewQklBizException("分片未传完,当前 "+upload.getUploadedChunks()+"/"+upload.getTotalChunks());}Filedir=newFile(uploadDir,dto.getUploadId());Filemerged=newFile(uploadDir,upload.getFileName());try(FileOutputStreamfos=newFileOutputStream(merged)){for(inti=0;i<upload.getTotalChunks();i++){Filepart=newFile(dir,i+".part");if(!part.exists()){thrownewQklBizException("分片缺失 index="+i+",请重传该分片");}Files.copy(part.toPath(),fos);}}deleteQuietly(dir);upload.setStatus(1);upload.setFinalPath(merged.getAbsolutePath());fileUploadMapper.updateById(upload);returnResult.ok(upload.getFinalPath());}

合并前必须校验分片数量,缺一片都不能合并,缺了就让前端重传缺失分片(断点续传的"续"就是从这里来的)。

前端:uniapp 分片 + 并发上传

前端核心逻辑,File.slice分片后用 Promise 控制 3 个并发:

constCHUNK_SIZE=5*1024*1024;// 5MB 一片asyncfunctionuploadBigFile(filePath,fileName,fileSize){constfileMd5=awaitcomputeFileMd5(filePath);// 计算整个文件 MD5constinitRes=awaituploadApi('/api/file/init',{fileMd5,fileName,fileSize,chunkSize:CHUNK_SIZE});if(initRes.exist){returninitRes.finalPath;// 秒传:服务端已有}consttotalChunks=initRes.totalChunks;constuploadId=initRes.uploadId;letoffset=0;consttasks=[];for(leti=0;i<totalChunks;i++){tasks.push(uploadChunk(uploadId,i,filePath,offset,CHUNK_SIZE));offset+=CHUNK_SIZE;}// 3 个并发跑,失败的分片单独重试awaitrunWithConcurrency(tasks,3);returnawaituploadApi('/api/file/merge',{uploadId});}

注意uploadChunk内部要包一层"失败重试 3 次",因为大文件场景网络抖动是常态,单分片重试比整个文件重传成本低得多。

踩坑:Nginx 413 Request Entity Too Large,前端传一半直接被断

问题现象:本地联调好好的,上测试环境一传大文件就报413 Request Entity Too Large,前端拿到的错误是"请求被服务器拒绝"。

排查过程:先看后端日志,根本没收到请求——请求在 Nginx 层就被拦了。查 Nginx 配置,发现client_max_body_size没配,默认1m,超过就 413。前端报错文案又是笼统的"上传失败",一度以为是跨域问题。

定位思路:抓请求看状态码,413 很明确是 body 大小限制。分片虽然单片只有 5MB,但没配client_max_body_size 10m一样过不去。

最终解决:

http { client_max_body_size 10m; # 必须大于分片大小,留余量 }

顺带把/api/的请求头超时也调了:proxy_read_timeout 300s;——分片上传是慢请求,默认 60s 超时会在传大分片时被 Nginx 掐断。

可直接复用的清单

  • 三接口模型:init(含秒传)→chunk(幂等覆盖写)→merge(校验后合并);
  • 分片表记录uploaded_chunks,乐观计数防重复累加;
  • 前端按5MB/片、并发 3,单分片失败重试 3 次;
  • Nginx 必须配client_max_body_size(大于单片大小)和proxy_read_timeout;
  • 秒传靠文件 MD5 前置查询,别把大文件传上来再判重。

完整实现可以直接参考 qkl-boot 的文件模块。项目源码:https://gitee.com/gzqkl/qkl-boot

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询