C#实现大文件分段上传与秒传:前端切片到后端合并全解析
做后台系统的这些年我接手过不少“文件上传”相关的需求其中最让人头疼的就是大附件。你想想一个 2GB 的压缩包让用户挂在网页上传传了半小时断了又得重来服务端那边也好不到哪去传统的一体化上传要么直接把内存撑爆要么被网关拦截要么请求超时。这还只是“传得过去”的问题如果文件之前在系统里已经有一份一模一样的用户还得傻乎乎再传一遍既浪费带宽又浪费等待时间——这其实就是“分段上传分片上传”和“秒传”这两个技术要解决的核心痛点。这篇文章要聊的就是怎么用 C# 作为后端语言在网页端实现大附件的分段上传与秒传。前端用 JavaScript 做文件的切片和哈希计算后端用 ASP.NET Core Web API 接收分片、校验秒传、合并文件。内容适合正在做网盘、文件管理系统、内部办公系统或者是对接第三方上传场景的开发者参考哪怕你是第一次接触这块按文章里的思路和代码把工程建起来也能一步步跑通整个流程——这是我在真实项目中验证过的方案不是理论推演。1. 从需求出发为什么大附件上传必须分段1.1 浏览器和服务器在大文件面前的双重困境先捋一下传统的“一次上传”方式到底卡在哪儿。浏览器端如果你直接用一个input typefile加上 AJAX把整个文件塞进请求体文件稍微大点内存占用立刻飙升因为前端要把整个文件读进 Blob 才能发送而且一旦网络抖动整个请求失败你没有任何办法“接着传”只能从头再来。服务端那边ASP.NET 的请求体默认有大小限制比如经典的 Kestrel 默认请求体最大 30MBIIS 环境下也有 maxAllowedContentLength 的限制一个 2GB 的文件直接就被拦在外面了就算你把限制全部放开服务端把整个文件一次性接收并写入磁盘中间任何一步出错都会导致进程内存暴涨甚至崩溃。这就是分段上传的意义把一个大文件切成若干个小块分片每个分片独立上传、独立失败重试、独立记录进度。服务器收到所有分片后再按顺序把分片合并成完整文件。这样一来内存占用变成恒定的小值比如只处理一个 10MB 的分片而不是整个 2GB遇到断网重新传的时候只需上传没完成的那几个分片而不是整块文件。用一句话概括用分片换稳定用记录换断点续传。1.2 分段上传和秒传到底解决什么问题分段上传解决的是“传得上去、传得稳定”的问题。而秒传解决的是“能不能不传”的问题——它的思路是在真正上传之前前端先算出一个文件唯一特征一般是计算文件的哈希值比如 MD5把这个哈希值连同文件名、大小发给服务端。服务端在数据库里查一下如果存在一个完全相同哈希和相同大小的文件就说明这个文件已经上传过了不需要再传一遍直接给前端返回“秒传成功”。从用户视角看几百兆的文件“嗖”一下就上传完成了像是用了什么黑科技。这两个机制在企业系统场景里搭配使用效果尤其好。比如一个团队经常互相传递几十上百 MB 的设计稿、安装包用秒传就能省下大量重复上传的流量。再比如做离线数据备份的场景单个文件经常超过 1GB没分段上传这个功能系统基本就废了。这两个功能一个是“效率”层面的一个是“稳定”层面的一次做进去整个上传模块就完整了。1.3 整体方案选型C# Web API 前端切片 特征值校验技术选型方面后端用 ASP.NET Core Web API.NET 6 或 .NET 8 都可以下面的代码基于 .NET 6 以上风格原因很实际跨平台、内置依赖注入、IFormFile 接收文件流非常方便、Stream 操作天然适合分片写入。前端不需要引入重量级框架原生 JavaScript 就足够完成切片的操作我习惯配合 SparkMD5 这个轻量库来做文件哈希计算比纯手写 MD5 高效得多也支持流式增量计算不会因为文件大而卡死页面。这里多说一句你可能在网上看到过一些老方案比如用 WebUploader 这种库直接对接它内部确实封装了分片上传但问题也不少库本身已经很多年没有维护了分片参数的命名方式是chunk、chunks这种泛化名称对接 .NET Core 的 Controller 时经常要写一堆自定义适配代码反而比原生实现更麻烦。所以我的建议是如果项目要求长期维护、逻辑要完全可控直接基于 File API 的 slice 方法 C# 接口自己实现踩坑之后你才知道边界在哪里。2. 接口设计与数据模型先把架构搭明白2.1 四个核心接口串起整个上传流程先别急着写代码把接口设计梳理清楚后面就不会乱。一个完整的“分段上传 秒传”流程最少需要四个接口接口方法作用关键参数/api/upload/checkPOST上传前预检判断是否可秒传、是否已有记录fileHash文件总体 MD5、fileName、fileSize/api/upload/chunkPOST接收单个分片文件并保存到临时目录identifier上传任务ID、chunkIndex、totalChunks文件本身用 FormData 传输/api/upload/mergePOST所有分片传完后服务端合并分片为完整文件identifier、fileName、fileHash用于合并后校验/api/upload/cancelPOST取消上传清理临时分片identifier整个调用顺序是一个“预检 → 分段传输 → 合并 → 完成”的状态链路。前端拿到文件后第一步调check做秒传判断如果服务端说need_upload前端开始切片按顺序或并发调chunk接口全部成功后调merge合并成功后再返回一个最终文件的访问路径。中间任何一步失败前端都能根据已上传的分片列表决定从哪里续传。后面第三节和第四节会展开这些接口的代码实现这里先把数据模型说清楚。2.2 数据表设计文件表、分片表、上传记录表分段上传涉及的数据量和状态光靠临时文件目录是不够的必须用数据库记录“到底传到了什么程度”。我在项目里通常建四张表核心结构如下。第一张叫UploadFileInfo存文件的最终记录CREATE TABLE UploadFileInfo ( Id BIGINT IDENTITY(1,1) PRIMARY KEY, FileHash NVARCHAR(64) NOT NULL, -- 文件整体 MD5 FileName NVARCHAR(255) NOT NULL, FileSize BIGINT NOT NULL, FilePath NVARCHAR(500) NOT NULL, -- 合并后的物理路径 UploadId NVARCHAR(50) NOT NULL, -- 秒传/合并时使用的任务标识 UploadTime DATETIME DEFAULT GETDATE(), IsComplete BIT DEFAULT 0 );第二张是ChunkRecord记录某个上传任务里每个分片是否已经到达CREATE TABLE ChunkRecord ( Id BIGINT IDENTITY(1,1) PRIMARY KEY, UploadId NVARCHAR(50) NOT NULL, ChunkIndex INT NOT NULL, -- 分片序号从 0 开始 ChunkSize BIGINT NOT NULL, IsUploaded BIT DEFAULT 0, UpdateTime DATETIME DEFAULT GETDATE() );在设计时需要注意一个点UploadFileInfo.FileHash要建唯一索引。这是秒传的基石——只有唯一索引才能保证同一个哈希不会重复存成多份文件。如果你们有“同一个内容但不同文件名”的需求可以把文件名逻辑拆到另一张表但物理文件唯一性必须由哈希保证。2.3 分片大小和并发数的选型逻辑分片大小是这一步的“定海神针”。我见过有人把分片设成 1MB结果 1GB 文件传一千个请求每个请求都有网络握手开销性能一塌糊涂也有人设成 100MB结果一个分片在公网上要传十几分钟随便一个波动就得重传体验非常差。我的经验值是5MB 到 20MB之间办公室内网可以用 20MB公网环境建议 5MB 或 10MB。分片大小的选择逻辑其实是“请求次数”和“单次请求耗时”之间的权衡网络差 / 公网环境分片小5MB单请求时间短失败重试成本低内网 / 带宽充足分片大20MB请求数量少减少握手开销用户几乎总是上传大文件分片稍大不要造成几千个分片导致服务端文件句柄暴增另一个关键参数是并发上传数。前端同时发多少个chunk请求我建议控制在 3 个左右。并发太高的好处是速度快但坏处是浏览器对同一域名的 TCP 连接数是受限的HTTP/1.1 一般是 6 个发再多也是排队而且服务端在高峰期同时写几十个临时文件磁盘 IO 会成瓶颈。稳定起见3-5 个并发是实测性价比最高的区间如果服务端和客户端都在同一内网且带宽足够大可以开到 5。3. 后端 C# 代码实现接受分片、校验秒传、合并文件3.1 秒传校验接口MD5 比对背后的原理秒传的关键是前端提供一个可靠的文件哈希值。服务端要做的事情很简单拿着哈希值去UploadFileInfo表里查有记录就返回“该文件已经存在直接成功”没有就返回“可以开始上传”。我在check接口里同时比对了几项信息避免仅仅依赖哈希值带来的误判风险[HttpPost(api/upload/check)] public async TaskIActionResult CheckFile([FromBody] CheckFileRequest request) { if (string.IsNullOrEmpty(request.FileHash) || request.FileSize 0) { return BadRequest(new { code 1, msg 参数不完整 }); } var existed await _db.UploadFileInfos .FirstOrDefaultAsync(f f.FileHash request.FileHash f.FileSize request.FileSize); if (existed ! null System.IO.File.Exists(existed.FilePath)) { // 命中文件已存在返回秒传成功 return Ok(new UploadCheckResponse { Status instant_success, FilePath existed.FilePath, Message 秒传成功 }); } // 未命中创建上传任务前端开始分段上传 var newUploadId Guid.NewGuid().ToString(N); return Ok(new UploadCheckResponse { Status need_upload, UploadId newUploadId }); }为什么要同时比对FileSize因为虽然 MD5 碰撞的概率极低但运维上的“误判”更多来自于前端计算错误或者网络传输阶段被截断——如果前端算出 MD5 是abc文件大小是 0后端也查不到对应记录当然没问题但如果出现某种异常情况导致哈希恰好相同但文件内容不同加上大小校验可以大大降低这种风险。生产环境我还会记录一下文件扩展名做二次提示但不作为唯一判定依据。3.2 分片上传接口临时文件的写入策略分片上传接口是核心中的核心。这里最需要注意的不是接收文件本身而是临时目录的组织方式和分片文件的命名方式。我用的策略是以UploadId为文件夹名每个上传任务一个独立的临时目录比如/uploads/temp/abc123.../临时目录下存放分片文件文件名直接用分片序号比如0.part、1.part、2.part这样做的原因有两个。第一UploadId是唯一的多个用户同时上传时不会出现分片相互覆盖第二用分片序号做文件名合并时直接按文件名排序就行了不要用Guid重命名——我见过有人为了防止重名把分片命名成guid.part结果合并的时候还要把真实序号存在数据库里再查出来排序纯属自找麻烦。[HttpPost(api/upload/chunk)] public async TaskIActionResult UploadChunk( [FromForm] IFormFile file, [FromForm] string uploadId, [FromForm] int chunkIndex, [FromForm] int totalChunks, [FromForm] string fileName) { if (string.IsNullOrEmpty(uploadId) || file null || file.Length 0) { return BadRequest(new { code 1, msg 分片参数缺失 }); } var tempRoot Path.Combine(_config.StoragePath, temp); var uploadTempDir Path.Combine(tempRoot, uploadId); Directory.CreateDirectory(uploadTempDir); // 关键点文件名就是分片序号后续合并直接按序号排序 var chunkFile Path.Combine(uploadTempDir, ${chunkIndex}.part); await using (var stream new FileStream(chunkFile, FileMode.Create, FileAccess.Write)) { await file.CopyToAsync(stream); } // 同步分片记录表已上传的分片数为 1 var record await _db.ChunkRecords .FirstOrDefaultAsync(c c.UploadId uploadId c.ChunkIndex chunkIndex); if (record null) { _db.ChunkRecords.Add(new ChunkRecord { UploadId uploadId, ChunkIndex chunkIndex, ChunkSize file.Length, IsUploaded true, UpdateTime DateTime.Now }); } else { record.IsUploaded true; record.ChunkSize file.Length; record.UpdateTime DateTime.Now; } await _db.SaveChangesAsync(); return Ok(new { code 0, msg $分片 {chunkIndex} 上传完成 }); }这里有两个特别容易被忽略的点值得单独强调。第一FileMode.Create而不是FileMode.OpenOrCreate。如果断点续传场景下前端重试了某个分片Create模式会把旧分片直接覆盖掉保证分片内容始终是最新上传的那一份不会出现同一个序号里混入新旧两份数据的脏情况。第二IFormFile.CopyToAsync内部走的是流式复制服务端内存占用和分片大小相当而不是与整个文件大小相当。这就是分片架构能扛住超大附件的根本原因——服务端每一刻的内存开销都是 O(分片大小)而不是 O(整个文件大小)。3.3 合并分片流式合并与完整性校验合并分片接口要做的三件事是检查所有分片是否齐了按序号合并成一个文件合并完成后删除临时目录。合并过程本身不复杂但有一个性能细节值得说不要天真地用小文件ReadAllBytes再拼接那会把临时目录下所有分片一次性读入内存内存占用直接回到“传统上传”的老路上去。正确做法是用Stream.CopyTo分块复制[HttpPost(api/upload/merge)] public async TaskIActionResult MergeChunks([FromBody] MergeRequest request) { var tempRoot Path.Combine(_config.StoragePath, temp); var uploadTempDir Path.Combine(tempRoot, request.UploadId); if (!Directory.Exists(uploadTempDir)) { return BadRequest(new { code 1, msg 临时目录不存在请重新上传 }); } var chunkFiles Directory.GetFiles(uploadTempDir, *.part) .Select(f new FileInfo(f)) .OrderBy(f int.Parse(f.Name.Replace(.part, ))) .ToList(); var expectedCount chunkFiles.Count; // 可选与前端提交的 totalChunks 比对防止分片缺失 if (request.TotalChunks 0 expectedCount ! request.TotalChunks) { return BadRequest(new { code 1, msg $分片数量不足当前 {expectedCount}期望 {request.TotalChunks} }); } var finalDir Path.Combine(_config.StoragePath, files); Directory.CreateDirectory(finalDir); // 文件名避免路径穿越简单做一层清洗 var safeName Path.GetFileName(request.FileName); var finalPath Path.Combine(finalDir, safeName); // 流式合并核心逻辑 await using (var output new FileStream(finalPath, FileMode.Create, FileAccess.Write)) { foreach (var chunk in chunkFiles) { await using var input chunk.OpenRead(); await input.CopyToAsync(output); } } // 合并后校验重新计算整体 MD5与前端提交的 fileHash 比对 var mergedHash await ComputeMd5Async(finalPath); if (!string.Equals(mergedHash, request.FileHash, StringComparison.OrdinalIgnoreCase)) { // 哈希不一致合并出的文件是损坏的建议删除并让用户重新上传 System.IO.File.Delete(finalPath); return BadRequest(new { code 1, msg 文件完整性校验失败请重新上传 }); } // 写入文件信息表 _db.UploadFileInfos.Add(new UploadFileInfo { FileHash request.FileHash, FileName safeName, FileSize new FileInfo(finalPath).Length, FilePath finalPath, UploadId request.UploadId, UploadTime DateTime.Now, IsComplete true }); await _db.SaveChangesAsync(); // 清理临时分片 Directory.Delete(uploadTempDir, true); return Ok(new { code 0, msg 合并完成, filePath finalPath }); }这段代码里的ComputeMd5Async是实现哈希计算的一个辅助方法它同样用流式方式读取不会把整个文件加载进内存private static async Taskstring ComputeMd5Async(string filePath) { using var md5 System.Security.Cryptography.MD5.Create(); await using var stream new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, 81920); var hashBytes await md5.ComputeHashAsync(stream); return Convert.ToHexString(hashBytes).ToLower(); }这里我要郑重提醒合并后校验这一步绝不能省。实际项目中分片上传偶尔会出现前台显示“100%”但合并出来的文件打不开的现象原因是某个分片在传输过程中静默损坏、或者totalChunks记录有偏差。合并后重新算一次 MD5和前端提交的哈希做比对是拦住这类问题的最后一道防线。宁可合并失败让用户重传一次也不能把损坏文件存进系统里否则后续打开文件时出现的问题更难排查。4. 前端实现切片、传分片、进度与断点续传4.1 前端切片与 MD5 计算SparkMD5 还是 Web Worker前端这边的第一件事是把文件切成若干 Blob同时算出文件的整体 MD5。切片用的是File.slice(start, end)这个 API 在 Chrome、Firefox、Edge 里都支持得很好不需要额外兼容。以 10MB 分片为例const CHUNK_SIZE 10 * 1024 * 1024; // 10MB function createChunks(file) { const chunks []; let start 0; let index 0; while (start file.size) { const end Math.min(start CHUNK_SIZE, file.size); chunks.push({ index, blob: file.slice(start, end), size: end - start }); start end; index; } return chunks; }MD5 的计算要特别小心。最直接的做法是读取整个文件再算哈希但一个 1GB 的文件直接把整个内容读进浏览器内存页面会卡成幻灯片。正确做法是用 SparkMD5 的增量模式边读文件边喂数据import SparkMD5 from spark-md5; function calculateFileHash(file) { return new Promise((resolve, reject) { const fileReader new FileReader(); const spark new SparkMD5.ArrayBuffer(); // 按 2MB 为一批分批读取 const partSize 2 * 1024 * 1024; let currentPos 0; const readNext () { const end Math.min(currentPos partSize, file.size); const blob file.slice(currentPos, end); fileReader.onload (e) { spark.append(e.target.result); currentPos end; if (currentPos file.size) { readNext(); } else { resolve(spark.end()); } }; fileReader.onerror reject; fileReader.readAsArrayBuffer(blob); }; readNext(); }); }如果文件特别大比如超过 500MB这个哈希计算过程本身也可能给用户带来几秒甚至十几秒的等待。一个体验上的优化方式是把哈希计算放到 Web Worker 里执行避免阻塞 UI 线程。我自己的习惯是500MB 以下直接增量计算超过 500MB 就用一个worker.js处理前端主线程只接收计算结果。Web Worker 的写法这里不展开核心思路就是把上面这段calculateFileHash整体挪到 worker 里通过postMessage返回哈希值。4.2 并发上传控制用一个简单队列搞定分片准备好、MD5 也算完了接下来就是上传。如果循环一个接一个地传1GB 文件切 100 个分片传 100 次效率太低如果一次性全发几十个并发请求同时打过来服务端磁盘 IO 扛不住。所以需要一个简单的并发控制队列。下面这个实现里uploadQueue维护一个并发上限的队列activeCount记录当前正在传的数量const CONCURRENT_LIMIT 3; let activeCount 0; const pendingQueue []; let uploadedChunkIndexes []; // 已上传分片用于断点续传 async function uploadChunk(uploadId, chunk) { const formData new FormData(); formData.append(uploadId, uploadId); formData.append(chunkIndex, chunk.index); formData.append(totalChunks, chunkList.length); formData.append(fileName, file.name); formData.append(file, chunk.blob, chunk-${chunk.index}); const resp await fetch(/api/upload/chunk, { method: POST, body: formData }); return resp.json(); } // 并发控制启动一个任务时计数加一结束减一并继续拉取下个任务 async function runTask(uploadId, chunk) { await uploadChunk(uploadId, chunk); uploadedChunkIndexes.push(chunk.index); progressRefresher(); } function enqueue(uploadId, chunk) { pendingQueue.push(() runTask(uploadId, chunk)); pump(); } function pump() { while (activeCount CONCURRENT_LIMIT pendingQueue.length 0) { const task pendingQueue.shift(); activeCount; task().finally(() { activeCount--; pump(); }); } }这段代码的业务逻辑太简单了实际项目里还要补几个细节失败重试、断点续传、上传进度计算。失败重试的通用做法是给每个分片记录一个重试次数比如最多重试 3 次超过 3 次就把这个分片标记为失败最终在上传列表里把失败的分片提示给用户由用户决定是否续传。断点续传的做法是每次上传一个分片成功时先把uploadedChunkIndexes存到localStorage键名用文件 MD5 标记重新上传时先调check接口如果返回need_upload再对比本地记录跳过已经传过的分片。这里有一个坑localStorage只适合存几百 KB 以内的数据分片索引列表通常很小完全够用但如果你的分片特别多建议把已传分片列表放到后端由后端在check接口里返回已上传的分片范围前端直接过滤——这是更可靠的方案因为用户清掉浏览器缓存后本地记录就没了而后端记录还在。进度计算就简单了已上传分片数除以总分片数。合并完再统一变成 100%。4.3 秒传触发逻辑与 UI 交互衔接整个上传流程在前端如何串联我用下面的伪代码展示一下方便你把各环节黏合起来async function handleUpload(file) { const fileHash await calculateFileHash(file); // 1. 预检后端判断是否秒传 const checkResp await fetch(/api/upload/check, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ fileName: file.name, fileSize: file.size, fileHash }) }); const checkData await checkResp.json(); if (checkData.status instant_success) { // 2. 秒传命中 showMessage(秒传成功, checkData.filePath); return; } // 3. 开始切片上传 const uploadId checkData.uploadId; const chunks createChunks(file); // 断点续传从后端查询已上传分片过滤掉 const uploaded await fetchUploadedChunks(uploadId); // 返回已传分片序号数组 chunks.filter(c !uploaded.includes(c.index)) .forEach(c enqueue(uploadId, c)); // 4. 等待队列清空 await waitForQueueIdle(); // 5. 合并分片 const mergeResp await fetch(/api/upload/merge, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ uploadId, fileName: file.name, totalChunks: chunks.length, fileHash }) }); const mergeData await mergeResp.json(); if (mergeData.code 0) { showMessage(上传成功, mergeData.filePath); } else { showMessage(合并失败, mergeData.msg); } }前端这块还有一个细节经常被忽略选择文件之后先不要立刻清空input的值。如果用户撤回了上传操作比如点击取消你可以回去拿到原选中的文件而一旦清空用户想继续就得重新找文件。我踩过这个坑后来改成始终在当前作用域里持有file变量直到上传流程结束。5. 常见问题排查与生产环境避坑指南5.1 高频问题速查表分段上传一旦到了生产环境容易出现的问题往往不在功能逻辑本身而在于各种环境限制和隐式约束。我把踩过的坑整理成一张速查表方便你直接对应排查。现象直接原因解决办法请求还没到 Controller 就被返回 404/413Kestrel 或 IIS 的请求体大小限制调大MaxRequestBodySizeIIS 下同时调整maxAllowedContentLength上传速度很慢总是排队前端并发数设得太低比如只有 1将并发控制在 3-5并开启 HTTP/2 或避免跨域代理合并后文件打不开分片顺序错乱或分片数据损坏检查合并时的排序规则合并后做 MD5 校验临时文件夹里残留大量.part文件用户中途取消、浏览器崩溃在后端加定时任务定期清理多天前的临时文件前端提示上传成功后端没有完整文件未调合并接口或合并接口报错检查前端是否在队列完成后正确触发 merge查看后端日志换了一台电脑原来传的文件无法续传本地存储记录不可用改用后端返回“已传分片列表”的机制5.2 生产环境必须处理的三个细节第一个细节请求体大小限制要改对地方。ASP.NET Core 默认的 Kestrel 请求体限制是 30MB你的分片如果恰好是 10MB 就没事但如果你把分片调到 50MB就必须在Program.cs里显式配置builder.WebHost.ConfigureKestrel(options { options.Limits.MaxRequestBodySize 100 * 1024 * 1024; // 100MB });如果部署在 IIS 后面还需要改web.config里面的maxAllowedContentLength单位是字节。两层限制各自独立只改一层都没用——这两个地方我都踩过。第二个细节临时文件必须过期清理。无论用户是主动取消、浏览器崩溃、还是直接关掉页面断点续传机制都会留下临时分片。后端一定要有一个定期任务比如每天凌晨跑一次把超过 48 小时的临时目录删掉。否则上传量一大磁盘就会被.part文件塞满。第三个细节文件名必须清洗。如果你直接用前端传来的fileName拼路径等于把路径穿越漏洞敞开大门。Path.GetFileName()只取路径最后一段但还不够建议再加一层白名单校验只允许字母、数字、中划线、下划线和点号其余一律替换成_。5.3 性能优化我从真实项目里拿到的数据我在一个真实项目中做过一次对比测试单个文件 1.2GB分片 10MB并发 3局域网内上传整个过程耗时约 1 分 50 秒服务端内存稳定在 80MB 以内未出现请求超时或内存溢出。作为对比传统一体化上传方案在同一环境下请求直接被网关拦截根本无法完成。换成公网环境受制于上行带宽1.2GB 文件可能要传近半小时这时候断点续传的价值就体现出来了用户断网重连后只需要补传最后几个分片而不是从头再来。我建议你在前端把“已传百分比”做得足够显眼配合断点续传用户的焦虑感会大幅降低。如果未来文件量再上一个量级比如动辄 10GB 以上可以再考虑引入类似 Tus 协议的开源分片上传标准或者把存储层切到对象存储如 MinIO、阿里云 OSS 的分片上传接口。但基础原理和这篇文章讲的是相通的前端切片、后端接收、断点续传、秒传校验核心架构不变只是把“本地磁盘存储”替换成“对象存储”。回到我们最初的问题。分段上传和秒传不是两个孤立功能它们是一套完整的大文件上传方案中的“稳定层”和“效率层”。我在实际项目里最大的感受是分段上传的代码并不难写难的是把所有边界场景都考虑到——分片缺失、分片覆盖、请求体限制、临时文件清理、并发控制、断点续传——每个细节在生产环境里都可能变成一个“线上事故”。所以这篇文章里我把数据和模型设计放在前面代码实现放在后面因为架构想清楚之后代码只是翻译而已。最后再分享一个小技巧给这个上传模块做前端反馈时不要只显示“x%”可以同时显示“已传 x/y 个分片”。原因很简单大文件的分片上传往往要持续很久一个百分比数字看多了会让人觉得进度卡住而“37/100 个分片”这种基于分片的颗粒度反而让人明确知道程序还在干活。这个小改动体验提升非常明显强烈建议你试一下。

相关新闻

FreqFiles:JetBrains插件以访问频率为核心打造自动更新的高频文件导航面板

FreqFiles:JetBrains插件以访问频率为核心打造自动更新的高频文件导航面板

平时做开发最烦的是什么?不是需求改得乱七八糟,也不是线上Bug复现不了,而是明明代码就在那个文件里,你却要在文件树里一层一层展开,或者点开CtrlE发现最近文件列表全是一堆配置和日志文件,真正的核心文件被…

2026/10/9 3:24:08 阅读更多 →
内网私有化部署实战:用Sealos离线交付K8s云平台

内网私有化部署实战:用Sealos离线交付K8s云平台

上个月接了一个内网交付,客户要求把研发团队的 PaaS 环境整体搬到隔离网络里,公网不碰,半天之内要能跑业务。以前遇到这种需求,我第一反应是 OpenStack,或者老老实实手动拉 K8s 集群。但这次我直接用 Sealos 做私有化部…

2026/10/9 3:24:08 阅读更多 →
MetaGPT生产化实战:Coding Agent架构设计与商业落地

MetaGPT生产化实战:Coding Agent架构设计与商业落地

先说一个让我印象很深的案例。有一次我在开发者社区里看到一款基于MetaGPT思路改造的Coding Agent产品,这个项目没有花钱投放过任何广告,上线之后完全靠开发者社区的口碑转发,在几个月内做到了让我一度怀疑数据的收入体量。虽然这样的成绩不是…

2026/10/9 3:24:08 阅读更多 →

最新新闻

ASP.NET C# ERP源码二次开发:从部署到改造全流程实战

ASP.NET C# ERP源码二次开发:从部署到改造全流程实战

简介:这是一份面向.NET开发团队的ASP.NET C#大型综合管理系统源码包,定位于大型ERP与全能后台管理系统的项目样板,适合具备一定C#基础、希望直接参考完整工程结构或进行二次开发的中高级开发者。压缩包约52.88MB,以zip格式提供&am…

2026/10/9 4:01:29 阅读更多 →
t3code 整合 Claude Code 与 Codex:Electron 多引擎 AI 编程工具架构解析

t3code 整合 Claude Code 与 Codex:Electron 多引擎 AI 编程工具架构解析

1. 从 t3code 这个标题说起:它到底想解决什么问题第一次看到 “t3code” 这个标题,我脑子里蹦出来的第一个念头是:这大概率又是一个围绕 AI 编程工具做整合或增强的项目。为什么这么判断?因为把标题和它周围那一圈热搜词放在一起看…

2026/10/9 4:01:29 阅读更多 →
Agent-Reach:多Agent协作的触达与编排实战指南

Agent-Reach:多Agent协作的触达与编排实战指南

去年下半年我接手了一个多Agent协作项目,前期单体Agent玩得很溜,结果一上多Agent就翻车——20多个Agent挂在一起互相调用,上午还跑得好好的,下午某几个Agent就开始失联,任务直接在中间环节卡死。折腾了两周&#xff0c…

2026/10/9 4:01:29 阅读更多 →
基于Hadoop和Spark的信贷风控系统架构与落地实践

基于Hadoop和Spark的信贷风控系统架构与落地实践

简介:面向大数据金融信贷风控领域学习者和毕业设计开发者的完整项目源码包,基于Hadoop与Spark技术栈实现信贷风险控制系统,覆盖数据接入、流式处理、风控逻辑及可视化等环节,适合课程设计、毕设或项目初期演示。压缩包内共69个文件…

2026/10/9 4:01:29 阅读更多 →
pstack-claude:本地化进程栈+AI诊断的轻量级系统调试方案

pstack-claude:本地化进程栈+AI诊断的轻量级系统调试方案

1. 项目概述:pstack-claude 是什么,它解决的是哪类开发者的实际痛点?pstack-claude 这个名字乍看像一个工具组合词,但拆解后立刻能抓住核心——它不是某个官方发布的软件包,而是开发者社区中自发形成的一套轻量级本地化…

2026/10/9 4:01:29 阅读更多 →
JavaWeb在线问卷调查系统课程设计:结构部署与核心代码解析

JavaWeb在线问卷调查系统课程设计:结构部署与核心代码解析

简介:基于JavaWeb的在线问卷调查系统课程设计源码包,面向需要完成Java课设、毕设或学习Servlet/JSP与Spring Boot整合开发的学生和开发者。系统覆盖用户注册登录、问卷创建与填写、管理员统一管理、多题型支持(单选、多选、文本题&#xff09…

2026/10/9 4:00:29 阅读更多 →

日新闻

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API这个话题,隔三差五就会在群里被翻出来讨论一次。上周还有个同事线上处理一个订单超时问题,排查到最后发现是ZonedDateTime序列化后时区丢了,用户在下单当天晚上看到的时间整整差了8个小时。这类问题几乎每个做Java开发的人都遇到过…

2026/10/9 0:00:49 阅读更多 →
EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

前几个月我手头有好几台机器需要互相访问:办公室台式机、家里 NAS、还有一台云主机。如果只是偶尔传个文件倒还好,问题是工作场景经常要在几处环境之间来回切换,每次都先登录跳板机再层层代理,实在折腾。我先后试过端口映射、自建…

2026/10/9 0:00:49 阅读更多 →
AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent 这个词在过去一年里被反复提及,但真正动手搭过一套能跑起来的 Agent 系统的人都知道,从"知道它是什么"到"让它稳定干活"之间隔着一整套工程决策。我前后参与过几个 Agent 项目的落地,从最初用现成框架拼装&…

2026/10/9 0:01:50 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:32 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:40 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 10:10:36 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 21:13:17 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 13:34:55 阅读更多 →