ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Spring Boot分片上传与断点续传:从原理到避坑指南

Spring Boot分片上传与断点续传:从原理到避坑指南 简介这是一份面向Spring Boot开发者的文件上传进阶资料聚焦断点续传与分片上传两种大文件传输方案。资源结合MultipartFile、分片合并、状态管理等核心知识点并附带前后端配套实现可帮助有一定Java基础的学习者理解上传机制并直接参考代码改造到实际项目中。资源包共114个文件压缩包仅112KB其中主要包含51个Java源码、45个编译后class文件、11个XML配置、2个SQL脚本、2个YAML配置以及JS、FTL模板等辅助资源能清晰对应后端逻辑、接口配置、数据库表与页面交互。已有1717人学习下载属于轻量而完整的小型开源参考实现。通过该资源读者可以掌握断点续传的偏移量处理思路、分片上传的分片接收与合并流程同时获得上传状态管理与异常恢复的代码参考适合用于毕设或企业项目中大文件上传模块的快速落地。1. 为什么你的大文件上传总在最后一秒翻车Spring Boot 断点续传与分片上传项目里总有那么一个需求上传 2GB 的安装包、视频素材、离线文档。前端 file 选择框一点进度条走到 98%Wi-Fi 抖了一下请求失败只能从头再来。这不是网络不好而是你把整个文件放进了一个 HTTP 请求里。springboot断点续传或分片上传就是把这个大请求拆成若干个小请求每传成功一片就记录一片失败后下次只补传没完成的片。这套方案在后端主要落在 Spring Boot 里前端可以用 vue-simple-uploader也可以自己用 File.slice 拼一个做法不复杂但参数、状态记录和并发处理的坑不少。适合正在做文件上传功能、或者被 springboot 面试题里这道题卡住的人。下面按后面能直接落地的顺序讲。2. 分片上传与断点续传的原理和选型动手前先定这 4 件事2.1 分片上传不是把文件切小那么简单从「一次大请求」到「可重试小请求」先说清分片上传的优势在哪。常规上传是把整个文件负载放到请求体里对服务器、网关和浏览器都有压力Nginx、Tomcat 或者云负载均衡器通常会设置请求体上限和超时时间文件一大任何一个中间节点断连整个请求就失败。把文件切成若干分片后每个分片是独立的小请求几百 KB 到几 MB 的请求很快就能完成单个请求失败只需要重传那一片。断点续传是建立在分片上的「记忆」后端记录每个 fileId 下哪些分片已经写成功前端再次上传时先问一遍后端跳过已传的。有一个容易混淆的点浏览器下载文件有 Range 头可以实现下载续传上传时却很少有客户端和服务器原生支持。所以文件上传的断点续传必须依赖业务层的状态记录而不是 HTTP 协议本身。这也是面试官喜欢追问「为什么分片上传要后端配合」的原因——因为后端不记录状态前端切多少次片都没有续传能力。这里要约定几个核心参数前端和后端必须一致参数含义推荐取值fileId一次上传任务的唯一标识一般由前端生成uuid 或 file.md5 时间戳chunkSize每个分片的大小字节2MB~10MB弱网场景可调小index分片序号从 0 开始0 到 totalChunks-1totalChunks总分片数Math.ceil(file.size / chunkSize)fileId 是整个断点续传的钥匙。后端用它创建临时目录前端用它查询已上传的分片。如果前端每次进入页面都重新生成 fileId断点续传就永远续不上。总分片数也不能太夸张一个 5GB 文件如果切 1MB 片会有 5120 个请求虽然可行但合并时排序和校验的负担变重。常见选择是 2MB~5MB兼顾重传代价和请求数量。2.2 后端接口拆几块三个接口的职责边界后端最少需要三个接口分片上传接口接收单个分片按 fileId 和 index 写入临时存储。校验分片接口根据 fileId 返回已上传分片列表让前端跳过已传分片。合并接口当所有分片都上传后把临时分片合并成完整文件并清理临时数据。校验接口是断点续传的关键。vue-simple-uploader 这类组件默认会先发探测请求如果后端没有实现校验接口前端就会认为所有分片都没传过每次都从零开始上传。这也就是为什么网上不少项目号称做了断点续传实际上只是做了分片上传。另有一个可选接口是秒传前端先把文件的 MD5 发给后端后端查库发现相同文件已经存在就直接返回成功跳过整个上传过程这个功能适合重复上传多的场景但它不能替代分片上传本身。三个接口里分片上传接口对时序要求最高。前端很可能并发发多个分片请求后端如果对同一个 fileId 的写入不加控制分片数据可能互相覆盖合并时才发现文件不对。2.3 上传组件选型手写 axios 队列还是 vue-simple-uploader前端常用两条路。一是手写。用 File 对象的 slice 方法切分文件把每个分片组成 FormData用 axios 发请求。好处是协议自己定和任何后端都能对接调试时能看到每个请求的状态码坏处是要自己实现进度汇总、失败重试和并发控制代码量不小。二是用 vue-simple-uploader。它在浏览器端封装了分片、并发、重试、进度和校验逻辑开箱即用但它的默认请求方式和校验约定是固定的后端必须按它的约定返回探测结果。如果项目本身就是 Vue并且不想重复造轮子组件方案更省事如果上传功能只是某个后台页面的一小块或者后端接口还有自己的加密、签名逻辑手写反而更好控制。组件方案还有一个隐性成本默认的并发数、重试策略和校验策略不一定符合你的约束。有些组件要求后端返回特定状态码前后端要一起调。手写方案虽然代码多但每个请求的 url、header、body 都是自己控制的后续接权限、签名、限流都好处理。我一般会建议临时内部工具用 vue-simple-uploader 快速出效果对外提供的文件服务或长期维护的产品功能手写分片上传更稳妥因为出了问题你能看到每一个请求。2.4 分片状态记录在哪儿临时目录、Redis 还是数据库分片状态记录有三种典型位置。临时目录扫描是最朴素的做法后端用 fileId 建目录每收到一个分片就写一个状态文件校验接口直接读目录。这个方案不依赖额外组件缺点是在分布式部署时多个实例各自有本地磁盘分片状态不共享需要做会话保持或者共享存储。Redis 适合做高并发下的状态缓存用 hash 结构记录某个 fileId 已收到的分片索引读写都很快。用 Redis 时建议把 fileId 作为 key用 set 记录分片索引例如 SADD upload:{fileId} 0 1 2校验接口直接返回 SMEMBERS 结果。缺点是 Redis 有清理策略任务中断几小时记录可能过期所以更适合给数据库做缓冲或者用于合并时的锁。Redis 的持久化配置也要注意appendonly 关闭时重启会丢数据断点续传状态也就丢了。MySQL 是最终要落地的记录文件表记录 fileId、文件名、大小、MD5、状态分片表记录每个分片是否上传完成。校验接口查分片表合并时更新文件表状态。这个方案在分布式环境下最可靠代价是每次上传分片都要写一次数据库。对绝大多数单体 Spring Boot 项目建议用「临时目录 一个状态文件」起步等真的需要多实例横向扩展了再把状态迁移到 Redis 或数据库。第 3 章的代码就是按这个思路写的。3. 用 Spring Boot 写断点续传后端分片接收、校验与合并的完整代码3.1 定义目录规范和存储方案后端先把目录约定好。常见做法是在应用目录下建一个 upload_tmp按 fileId 再建子目录upload_tmp/ fileId/ fileId.part # 所有分片按偏移量写入的同一个文件 fileId.done # 记录已收到的分片索引 merged/ 原始文件名为什么不把每个分片单独存成一个文件因为分片一多文件数量就非常大合并前还要做一次「按 index 排序」的 IO 操作把所有分片按偏移量写进同一个 .part合并时直接把文件改名或用流复制到最终位置就行。这个方案的代价是并发写同一个文件时必须加锁不能两个线程同时 seek 和 write。3.2 分片上传接口MultipartFile 接收后按偏移量写入先给出分片上传接口的完整代码RestController RequestMapping(/upload) public class ChunkUploadController { private static final int CHUNK_SIZE 2 * 1024 * 1024; // 与前端约定一致 private final String UPLOAD_ROOT System.getProperty(user.dir) /upload_tmp/; PostMapping(/chunk) public ResponseEntityMapString, Object uploadChunk( RequestParam(file) MultipartFile file, RequestParam(fileId) String fileId, RequestParam(index) int index, RequestParam(totalChunks) int totalChunks) throws IOException { File dir new File(UPLOAD_ROOT fileId); if (!dir.exists()) { dir.mkdirs(); } File partFile new File(dir, fileId .part); synchronized (fileId.intern()) { try (RandomAccessFile raf new RandomAccessFile(partFile, rw)) { raf.seek((long) index * CHUNK_SIZE); raf.write(file.getBytes()); } } // 追加记录这个分片已经收到 File doneFile new File(dir, fileId .done); Files.write(doneFile.toPath(), (index ,).getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE, StandardOpenOption.APPEND); return ResponseEntity.ok(Map.of(code, 0, msg, ok)); } }代码逻辑并不复杂每次上传把文件指针 seek 到 index * CHUNK_SIZE 的位置再写入本次的分片字节。这样即使分片乱序到达最后 .part 文件里每个分片也都在正确的位置。synchronized (fileId.intern()) 保证同一个 fileId 的写入是串行的避免两个并发请求同时写导致错位。有几个参数值得注意。CHUNK_SIZE 必须和前端约定的分片大小完全一致否则 seek 位置会算错这里用固定 2MB实际项目建议做成配置项。MultipartFile 的 getBytes() 会把整个分片读进内存分片超过 10MB 时要注意内存占用更稳的写法是先转成 InputStream 再写入。totalChunks 在这个接口里没有用到但建议保留合并前可以校验实际收到的分片数与它是否一致。另外 Map.of 要求 Java 9如果你的 Spring Boot 2.x 项目还在 Java 8换成 HashMap 即可。3.3 校验接口返回已上传分片列表分片上传完成后前端需要知道哪些分片已经存在。对应接口如下GetMapping(/check) public ResponseEntityMapString, Object checkChunks( RequestParam(fileId) String fileId) throws IOException { File dir new File(UPLOAD_ROOT fileId); ListInteger uploaded new ArrayList(); File doneFile new File(dir, fileId .done); if (doneFile.exists()) { String content new String(Files.readAllBytes(doneFile.toPath()), StandardCharsets.UTF_8); for (String part : content.split(,)) { if (!part.isEmpty()) { uploaded.add(Integer.parseInt(part)); } } } return ResponseEntity.ok(Map.of(code, 0, uploaded, uploaded)); }这个接口要返回完整的分片索引列表而不是只返回「是否全部完成」。前端拿到列表后会跳过其中已存在的 index。如果后端只给一个布尔值前端必须自己根据列表长度判断哪些片缺失逻辑就容易出错。实际项目中我会在这个接口里顺手做两件事一是把已上传分片数与文件总大小一起返回前端可以对比二是如果校验发现 .part 文件已经存在且长度等于文件总大小返回一个 completed 标志前端可以直接调用合并接口省去最后一轮重复上传。3.4 合并接口校验大小、落盘并清理临时文件当所有分片都完成后前端调用合并接口PostMapping(/merge) public ResponseEntityMapString, Object merge( RequestParam(fileId) String fileId, RequestParam(fileName) String fileName, RequestParam(totalSize) long totalSize) throws IOException { File dir new File(UPLOAD_ROOT fileId); File partFile new File(dir, fileId .part); if (!partFile.exists()) { return ResponseEntity.badRequest().body(Map.of(code, 400, msg, 分片文件不存在)); } if (partFile.length() ! totalSize) { return ResponseEntity.badRequest().body(Map.of(code, 400, msg, 已上传大小不一致: partFile.length() vs totalSize)); } File mergedDir new File(UPLOAD_ROOT merged); if (!mergedDir.exists()) { mergedDir.mkdirs(); } File dest new File(mergedDir, fileName); // .part 文件本身已经是按偏移量排好的完整文件直接搬过来 Files.move(partFile.toPath(), dest.toPath(), StandardCopyOption.REPLACE_EXISTING); deleteDir(dir); return ResponseEntity.ok(Map.of(code, 0, path, dest.getAbsolutePath())); }合并接口最大的价值不是拼接而是校验。partFile.length() 与前端传的 totalSize 不一致说明有分片漏传或者多传直接返回错误不要往下走。移动文件比边读边写更省 IO这里用 Files.move 把 .part 直接改成最终文件名。deleteDir 就是递归删除这个 fileId 的临时目录避免磁盘堆积。要注意的是确保临时目录和合并目录在同一个磁盘分区否则 Files.move 会退化成复制大文件会非常慢。如果用了数据库或 Redis合并接口还需要把文件状态改成 already_merged并把真实路径存下来后续下载接口根据这个状态返回文件。3.5 秒传接口MD5 校验什么时候值得做秒传是加分项不是必选项。前端在开始上传前算好整个文件的 MD5传给后端PostMapping(/exists) public ResponseEntityMapString, Object exists( RequestParam(md5) String md5, RequestParam(size) long size) { boolean exists fileMetaService.existsByMd5AndSize(md5, size); return ResponseEntity.ok(Map.of(code, 0, exists, exists)); }逻辑很简单查文件表相同 MD5 和大小已经存在前端就跳过整个上传流程直接显示成功。要注意两个坑一是大文件在前端计算 MD5 很慢2GB 文件可能要几十秒用户会以为页面卡了通常要做进度提示或者放到 Web Worker 里算二是 MD5 会碰撞所以生产中最好把大小作为条件再加一层文件名或目录归属校验不完全依赖哈希。4. 前端与后端打通断点续传手写 File.slice 与 vue-simple-uploader 两条路4.1 手写前端切分、探测、并发上传和失败重试后端三个接口就位后前端核心逻辑可以写得很薄。用原生 File API 切分用 axios 上传分片async function uploadWholeFile(file, fileId, chunkSize 2 * 1024 * 1024) { const totalChunks Math.ceil(file.size / chunkSize); // 1. 先查询已上传分片 const uploaded await fetch(/upload/check?fileId${fileId}) .then(r r.json()) .then(d d.uploaded || []); // 2. 并发上传缺失分片并限制并发数 const queue []; for (let index 0; index totalChunks; index) { if (uploaded.includes(index)) continue; const blob file.slice(index * chunkSize, Math.min(file.size, (index 1) * chunkSize)); const form new FormData(); form.append(file, blob); form.append(fileId, fileId); form.append(index, index); form.append(totalChunks, totalChunks); const task axios.post(/upload/chunk, form) .catch(() retryLater(form)); // 失败重试 queue.push(task); if (queue.length 3) { await Promise.all(queue); queue.length 0; } } await Promise.all(queue); // 3. 全部完成后通知后端合并 await axios.post(/upload/merge, null, { params: { fileId, fileName: file.name, totalSize: file.size } }); }这个函数完成了三件事先探测已传分片再把缺失的分片并发传上去最后触发合并。文件切片通过 file.slice 完成注意最后一个分片的结束位置是文件总大小不能用 (index 1) * chunkSize 直接算。这里的并发控制是每攒满 3 个请求就等待一次避免一下发出几千个请求把浏览器和 Tomcat 打爆。uploaded.includes(index) 这一步就是断点续传的落点页面刷新后只要 fileId 不变后端能查到之前已经写好的分片索引前端就会跳过。这也是 fileId 需要按固定规则生成的原因——用文件 MD5 加时间戳生成同一个文件再次上传时可以复用之前的记录。断点续传能不能生效很多时候不是后端代码的问题而是前端把 fileId 随手 new 了一个。4.2 并发数和分片大小怎么定两个影响体验的参数明白代码之后实际影响体验的是 chunkSize 和并发数两个量。分片大小影响「失败重传的粒度」和「请求数量」之间的平衡。分片太小比如 256KB重传粒度小了但总请求数多合并时文件句柄和状态记录的开销大分片太大比如 50MB又退化成接近整文件上传断了以后重传代价大。常见选择网络环境分片大小并发数内网/带宽充足5MB~10MB3~5公网普通宽带2MB~4MB2~3弱网/移动网络1MB~2MB1~2并发数不是越大越好。浏览器对同一域名的连接数有限制TCP 连接过多会相互争抢带宽后端每接收一个分片就要解析一次 multipart 请求并发太高时线程池和内存都吃紧。一般 3 个并发足够。提示chunkSize 前端和后端必须一致。上线后如果调整要把旧任务先处理完否则历史临时文件的偏移量会对不上。4.3 用 vue-simple-uploader 对接 Spring Boot校验与响应格式如果选用 vue-simple-uploader核心配置里和断点续传相关的部分如下new Uploader({ target: /upload/chunk, chunkSize: 2 * 1024 * 1024, testChunks: true, successStatuses: [200], checkChunkUploadedByResponse: (chunk, message) { // Spring Boot 校验接口返回的 JSON含 uploaded 列表 const data JSON.parse(message); // 组件序号一般从 1 开始转成后端从 0 开始的 index const index chunk.chunkNumber - 1; return data.uploaded data.uploaded.includes(index); } })vue-simple-uploader 开启 testChunks 后每个分片在真正上传前会先发探测请求探测结果由 checkChunkUploadedByResponse 回调决定。其中的 chunk 对象在不同封装版本里字段略有差异但通常能拿到 chunkNumber 或起始字节偏移换算成 index 再拿去和后端返回的 uploaded 列表比对。关键是要保证后端探测接口返回的 JSON 能被这个函数解析两边字段对不上就会变成「永远在重新上传」。我一般会在后端校验接口把 uploaded 列表做成数组返回前端回调里做 includes 判断这是最容易理解和排错的方式。这里有个常见误解有人以为 testChunks 只是把探测请求发出去后端返回 200 就算已存在这是不对的。后端必须返回能区分具体分片的响应体前端才能精确跳过。4.4 合并的时机最后一个分片回调再触发不论手写还是组件合并接口都必须在最后一个分片完成之后调用不能在「所有分片发出」之后调用。上面手写版是在 Promise.all 之后调用 merge组件版要放在 fileSuccess 或所有 chunk 的回调里。一个很容易犯的错前端在进度显示 100% 时立刻跳转页面或关闭窗口而合并请求还没执行完。合并是大文件上传中最后一步 IO 操作可能比单个分片上传慢得多要在合并请求的 Promise resolve 之后再做后续跳转。否则前端显示传完了后端临时目录里只有一堆分片文件永远合不出来。组件方案里还有一个选择让组件自己合并还是通知后端合并。我建议通知后端合并理由很简单后端才知道所有分片是否真正落盘。组件并发上传完成后后端再做一次文件长度校验和完整性校验把重量操作留在服务端前端只需要等待结果。把「合并」放在前端做等于把权限和风险都交出去了生产环境不推荐。5. 避坑清单断点续传最容易翻车的 5 个场景5.1 分片全部传完但合并乱序后到的分片被追加到文件尾现象两个分片请求几乎同时到达后端都打开同一个 .part 文件写入合并后文件段序颠倒视频花屏或压缩包损坏。原因如果后端用 append 模式打开文件比如 FileWriter 或 new FileOutputStream(file, true)后到的分片会被追加到文件末尾而不是它应该所在的偏移量。并发越高段序越乱。还有些实现把每个分片存成独立文件但合并时直接按文件名排序导致 2、10、20 这样的序号排序错误10 排到了 2 前面。解决写入时显式 seek 到 index * chunkSize用 RandomAccessFile 而不是 append分片独立存放时合并前按 index 数值排序。多实例并发写同一个文件时用 fileId 粒度的锁把写入串行化。5.2 Spring Boot multipart 默认 1MB 限制分片一传就 413现象前端明明切了 2MB 的分片请求发出去就报 MaxUploadSizeExceededException或者返回 413。原因Spring Boot 的 multipart 默认单文件大小上限是 1MB总请求大小是 10MB分片超过限制直接被拒。这个默认值太保守经常被忽视。解决在 application.yml 里调大spring: servlet: multipart: max-file-size: 20MB max-request-size: 20MB如果是 Spring Boot 1.x配置前缀是 spring.http.multipart新版换成了 spring.servlet.multipart网上老博客里的配置直接复制到新版本不会生效这也是「springboot 版本太高」最常见的一个坑。假如前面还有 NginxNginx 的 client_max_body_size 也要同步调大两个限制同时存在时以小的为准。本地调试时在 IDEA 的 Run Configuration 里可以设置程序启动参数覆盖端口或配置文件但 multipart 上限这种配置还是写进 yml 更稳妥。5.3 校验接口说「分片已存在」前端却从头再传现象断点续传完全没生效页面刷新后进度从 0% 开始后端日志里还能看到重复上传同一个分片。原因常见有两类。一是前端每次进页面重新生成 fileIdfileId 不同后端自然查不到上次的目录二是校验接口的响应格式和前端组件的解析逻辑不一致组件认为该分片不存在。解决fileId 要按固定规则生成同一个文件的 fileId 在有效期内保持不变校验接口返回的 uploaded 数组要和前端约定的字段名完全一致。调试时可以打开组件的 testChunks 开关观察每个分片先发出的探测请求看返回 JSON 是否符合前端解析逻辑。这是断点续传里最典型的黑匣子问题把探测请求和响应打印出来原因会立刻清楚。5.4 合并后文件大小对但内容损坏视频打开到一半花屏现象合并后的视频或压缩包能打开但播放到某段画质异常或者解压时报 CRC 错误文件的大小和原始文件一模一样。原因分片写入位置算错。最常见的是 seek 用了 index * chunkSize 却把 index 从 1 开始或者没有考虑最后一个分片不足 chunkSize 的情况还有一个原因是前端和后端约定的 chunkSize 不一致比如前端按 2MB 切后端按 1MB 的偏移量写数据全部错位。解决后端 seek 写法固定为 (long) index * chunkSizeindex 从 0 开始合并前校验 part 文件总长度等于 totalSize。如果还出错把 .part 文件保留下来用二进制比较工具对比原始文件对应 offset 的内容能快速定位哪一段错位。合并成功后建议对最终文件再算一次 MD5和前端上传前算的 MD5 比对这比人工抽查可靠得多。5.5 临时目录只写不收磁盘被打满现象系统监控里磁盘占用持续上涨上传目录里有大量 .part 和 .done 文件再过一阵所有分片上传都报「磁盘空间不足」。原因合并接口只在成功时清理临时目录上传到一半放弃、浏览器直接关闭、或者上传失败没调合并接口的应用临时目录里残留着文件。日积月累数量非常可观。解决合并成功后递归删除 fileId 目录另外写一个 Spring Scheduled 定时任务扫描 upload_tmp 下修改时间超过 24 小时的目录直接删除。删除前判断目录里是否还有活跃上传可以用状态文件里的最近修改时间做依据。这个回收任务不能省生产环境里很多「上传功能用一段时间后突然失灵」的问题都是磁盘被临时文件悄悄占满了。6. 进阶验证与生产化把断点续传从「能跑」变成「能扛」6.1 用 curl 模拟断点续传自测后端接口写完先用脚本自测一遍不要急着接前端。curl 可以模拟「传一半断掉再传另一半」的过程# 先只传 0 号分片 curl -F filepart_0 -F fileIdtest-001 -F index0 -F totalChunks10 \ http://localhost:8080/upload/chunk # 查询校验接口看是否只返回了 0 curl http://localhost:8080/upload/check?fileIdtest-001 # 再传剩下的分片最后合并 curl -X POST http://localhost:8080/upload/merge?fileIdtest-001fileNametest.bintotalSize20000000这个流程验证的是后端状态记录是否可靠中途没有传其他分片check 接口应该能正确返回已传列表merge 接口也应该能拒绝不完整的文件。如果 check 接口返回空说明状态写入或目录结构有问题先修后端再折腾前端。6.2 并发合并与幂等用 Redis 锁保证只有一个合并请求成功多个客户端同时点击合并或者后端重试机制重复触发合并会导致重复改名、状态错乱。生产中对合并动作加一把锁更稳Boolean locked redisTemplate.opsForValue() .setIfAbsent(upload:merge: fileId, 1, Duration.ofMinutes(5)); if (!Boolean.TRUE.equals(locked)) { return ResponseEntity.ok(Map.of(code, 409, msg, 合并已在处理中)); }锁住后后到的合并请求直接返回「处理中」前端可以轮询稍后再问。注意锁的过期时间要大于合并操作的真实耗时否则大文件合并超过 5 分钟锁自动过期第二个请求又进来了。Redis 锁不是唯一方案单机部署用 synchronized 也够但这个 lock key 的设计思路能直接迁移到多实例。6.3 我最后会做的三件事上传功能上线前我会把分片大小、并发数、临时目录回收时间三个参数全部提成配置项不写死在代码里会在校验接口的响应里同时返回 uploaded 列表和 completed 标志给前端省一次多余的合并请求会保留每次上传任务的完整日志包括 fileId、分片总数、实际写入总数和耗时。断点续传最怕的就是出了问题时只能看到「上传失败」四个字连哪个分片出了问题都不知道。这也是我做这个功能最大的教训一开始图省事只写了分片接口和合并接口没写校验接口后来前端每次刷新都从头传才补上 check 接口。断点续传不是一个文件接口而是一整套「状态记录 查询 合并」的闭环少一环都不叫续传。希望这个完整流程和避坑清单能帮到你。本文还有配套的精品资源点击获取
返回列表