1. 为什么直接传大文件总是失败:超时、内存与断线三重坑
做Web开发这么多年,我印象里最让人头疼的需求之一就是“用户要上传一个2GB的视频文件”。常规做法是搞一个Upload按钮,把整个文件塞进multipart/form-data请求里,交给后端处理。小文件这么干没问题,但文件一过几百兆,问题就全冒出来了。
先说超时。浏览器发起一个大文件上传请求,整个文件流要持续很久。如果nginx、IIS或者其他网关层设置的client_max_body_size和请求超时时间不够大,几十分钟的请求会被网关直接掐断,用户看到的就是“网络错误”“请求超时”或者“502 Bad Gateway”。即使后端把超时调到很大,用户的网络环境稍微抖动一下,TCP连接一断,整个上传就前功尽弃。
再说内存。很多后端框架在接收上传时会先把整个请求体缓冲到内存或者临时文件里。如果你的服务器内存是4GB,用户同时传两个2GB文件,应用进程直接内存飙高甚至OOM崩溃,这在生产环境我遇到过不止一次。ASP.NET Core默认的请求体缓冲策略虽然在Kestrel下表现尚可,但一旦开了代理,背后还是逃不过内存压力。
最后是断线。移动网络下的用户上传文件,信号稍微不稳定,之前的进度全部归零。上传15分钟后断线,又要从头再来,大部分用户会直接放弃这个产品。我在一个网盘项目里做过统计:超过500MB的文件,一次成功上传率不到40%,其余全是各种中断重试。
所以大文件上传必须换思路。核心解决方式就是分块——把一个大文件切成多个小块,逐个上传,后端接收后按顺序合并。配合“秒传”逻辑,服务端发现相同文件已经存在时,直接跳过上传,用户感觉“刚点上传就完成了”,这就是大文件分块秒传解决的核心问题。这篇文章我直接用C#(ASP.NET Core)加上前端HTML5 File API,从零实现一套可运行的方案,适合做网盘、视频平台、协同办公系统的开发者参考。
1.1 分块上传的本质是什么
分块上传的本质就四个字:化整为零。把大文件切成固定大小的小块,比如2MB一片,用JavaScript读取文件后逐块发到后端,后端每一块单独落盘,等所有块都传完后,再按顺序把分散的块拼回完整文件。
这个思路有两个明显的好处。第一,单次请求的体量变小,服务器内存占用被压到可控范围,每块2MB的话,即使100个并发上传,内存开销也远小于直接接收一个2GB大文件。第二,天然支持断点续传,某一块失败只需要重传那一块,而不是整个文件。
1.2 秒传是怎么“秒”起来的
秒传的本质不是传输,而是跳过传输。用户选择文件之后,前端先计算文件内容的哈希值(通常是MD5或SHA-1),把哈希值发给后端。后端查一下存储目录里有没有相同哈希的文件,如果有,直接返回“文件已存在”,前端立刻显示上传完成100%。这一步没有任何实际文件数据在网络里传输,所以它快得离谱,看起来就像“秒传”。
要注意,哈希值必须是文件内容的哈希,而不是文件名。文件名重复不代表内容相同。两个用户上传同一个电影,文件名可能完全不同,但内容哈希一致,所以后端能识别出是同一个文件。
1.3 分块、秒传、断点续传三者的协同关系
这三者不是并列关系,而是叠加关系。秒传是“上传前的判决”,分块是“上传中的策略”,断点续传是“上传失败后的补救”。实际运行时,流程大概是:
- 用户选中文件,前端计算文件内容哈希。
- 前端先请求后端“这个文件已存在吗?”如果存在,直接完成,秒传生效。
- 如果不存在,前端把文件切成块,逐块上传,后端每块单独保存。
- 如果某一块上传失败,前端记录失败位置,下次只重传失败的块。
- 全部块上传完成后,前端通知后端合并分块,还原完整文件。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 前端分块与后端接收的选型思路:先定协议再写代码
实现分块上传最忌讳的事情是“代码写一半才发现前后端协议对不上”。我自己在第一个版本里就吃过这个亏,前端用了一个开源插件,后端自己写接口,字段名对不上,调试了一整天。所以动手之前,先把方案定下来。
2.1 前端分块的具体手段
前端分块依赖HTML5 File API。File对象继承自Blob,提供了slice(start, end)方法,可以直接截取文件的某一段。用requestAnimationFrame或者setTimeout控制节奏,就能用循环把文件切成一堆Blob块,再用XMLHttpRequest或fetch逐块提交。
下面是一个简单的分块循环逻辑,每次切2MB一片:
javascript复制const CHUNK_SIZE = 2 * 1024 * 1024;
async function uploadFile(file) {
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
for (let i = 0; i < totalChunks; i++) {
const start = i * CHUNK_SIZE;
const end = Math.min(start + CHUNK_SIZE, file.size);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append('file', chunk);
formData.append('fileName', file.name);
formData.append('totalChunks', totalChunks);
formData.append('chunkIndex', i);
await fetch('/api/upload/chunk', {
method: 'POST',
body: formData
});
// 更新进度条
updateProgress((i + 1) / totalChunks);
}
// 通知后端合并
await fetch('/api/upload/merge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
fileName: file.name,
totalChunks: totalChunks
})
});
}
2.2 后端接口协议的设计
我建议把后端拆成两个接口,一个是分块上传,一个是合并文件。再加上一个秒传预检接口。协议字段必须明确,格式要稳定。我用的约定如下:
| 接口 | 方法 | 参数 | 作用 |
|---|---|---|---|
/api/upload/check |
POST | fileHash, fileName |
秒传预检,判断文件是否已存在 |
/api/upload/chunk |
POST | file(二进制), fileName, chunkIndex, totalChunks |
接收单块数据 |
/api/upload/merge |
POST | fileName, totalChunks, fileHash |
合并所有分块 |
这个协议是前后端共同遵守的契约。无论前端用什么框架,只要按这套字段发,后端就能处理。
2.3 为什么我选择用原生API而不引入重型插件
网上有不少成熟的上传插件,比如WebUploader和Plupload。坦白说,插件确实能快速做出一个可用的上传页面。但到了生产环境,插件的弊端很明显:很多插件是为特定后端框架设计的,字段名、签名机制都是写死的,想要改成你自己的业务逻辑,得去翻源码,改起来很难受。另外,插件支持的功能多,体积大,其中大量功能其实用不上。
我更推荐自己写一个轻量上传器。原生File API加fetch,代码量不大,逻辑完全自己掌控,出问题能快速定位,而且不会有兼容性黑盒。如果你的团队没有精力维护前端上传逻辑,用WebUploader也行,但我个人在实际项目中踩过插件的坑之后,还是倾向自己写,尤其是需要和后端深度集成做秒传和断点续传时,原生方案最灵活。
3. C#后端分块接收接口的实现细节
后端我用的是ASP.NET Core 6 Web API。这套实现的核心逻辑不复杂,但有一些细节容易被忽略,比如临时文件的命名规范、并发冲突的处理、请求体大小限制的调整等,我逐一展开。
3.1 预检接口:秒传的判定入口
秒传判定必须在分块上传之前做。前端计算出文件内容的MD5后,调用预检接口。后端收到哈希值后,去文件存储目录里查询是否存在同名同哈希的最终文件,如果已存在,直接返回exists: true。
需要说明的是,更严密的做法是后端维护一张文件索引表,记录每个上传过的文件的哈希值、存储路径、文件大小、上传时间。这个可以放数据库也可以放内存缓存。简单方案下,直接遍历存储目录计算文件名也是一种办法,但文件量大的时候性能会差。我建议为了演示方便,先用一个静态字典来存储已上传文件的信息,生产环境换成数据库即可。
下面是我写的预检接口:
csharp复制[HttpPost("check")]
public async Task<IActionResult> CheckFile([FromBody] CheckRequest request)
{
var hashKey = request.FileHash;
if (_fileIndex.TryGetValue(hashKey, out var existing))
{
return Ok(new { exists = true, message = "文件已存在,秒传完成", path = existing });
}
return Ok(new { exists = false });
}
public class CheckRequest
{
public string FileHash { get; set; }
public string FileName { get; set; }
}
使用ConcurrentDictionary存已经完成上传的文件索引,就是为了防止在并发场景下出现读写异常。这里有一个非常关键的点:预检逻辑必须是“内容哈希”而不是“文件名+大小”,因为大文件场景下,两个文件内容相同但文件名不同的情况太常见了。
3.2 分块上传接口:直接落盘与命名策略
分块接收接口的核心工作就两件:接收二进制流,把它写到磁盘上的临时分块文件里。注意,这里不建议把分块存到内存再统一处理,分块的意义就是控制内存占用,一定要直接写文件。
分块文件的命名我采用“文件标识符_块序号.tmp”的格式。文件标识符可以用GUID生成,或者用文件哈希加文件名组合,但哈希加文件名在极端情况下会有特殊字符问题,所以我直接用GUID作为本次上传任务的标识。
csharp复制[HttpPost("chunk")]
public async Task<IActionResult> UploadChunk([FromForm] ChunkUploadRequest request)
{
var file = request.File;
if (file == null || file.Length == 0)
return BadRequest("分块内容为空");
var uploadId = request.UploadId; // 前端生成的一次上传会话ID
var chunkDir = Path.Combine(_tempRoot, uploadId);
if (!Directory.Exists(chunkDir))
Directory.CreateDirectory(chunkDir);
var chunkPath = Path.Combine(chunkDir, $"{request.ChunkIndex:D6}.tmp");
await using (var stream = new FileStream(chunkPath, FileMode.Create))
{
await file.CopyToAsync(stream);
}
return Ok(new { received = request.ChunkIndex });
}
public class ChunkUploadRequest
{
public IFormFile File { get; set; }
public string UploadId { get; set; }
public int ChunkIndex { get; set; }
public int TotalChunks { get; set; }
}
这里我加了一个UploadId字段,由前端在开始上传时生成一个GUID,整个上传过程都用它来标识“这是同一个文件的那几块”。这样做比单纯用文件名安全,因为两个用户同时上传同名文件时,如果都用“文件名+序号”命名分块文件,会出现互相覆盖的情况。
另外注意ChunkIndex:D6的格式化,这保证了合并时块的排序是按字典序正确的。如果不用补零格式化,第10块会排在第2块前面,合并时会出大问题。这个细节看着不起眼,实际操作中真的会坑人。
3.3 合并接口:顺序校验与写入优化
合并接口是最后一步,前端把所有块传完后调用它。后端要做的第一件事是校验块的数量是否完整,不完整直接报错,防止前端漏发导致文件损坏。
合并时我用的是流式写入,而不是把所有块一次性读进内存。逐块读取、逐块写入,每块打开一个FileStream,读完关闭,这样内存占用始终是稳定的。写入的缓冲区设置大一点,比如1MB,能明显降低磁盘IO次数。
csharp复制[HttpPost("merge")]
public async Task<IActionResult> MergeChunks([FromBody] MergeRequest request)
{
var chunkDir = Path.Combine(_tempRoot, request.UploadId);
if (!Directory.Exists(chunkDir))
return BadRequest("上传会话不存在");
// 校验分块数量
var chunkFiles = Directory.GetFiles(chunkDir, "*.tmp");
if (chunkFiles.Length != request.TotalChunks)
return BadRequest($"分块不完整,已收到{chunkFiles.Length}块,应该为{request.TotalChunks}块");
var finalDir = Path.Combine(_storeRoot);
if (!Directory.Exists(finalDir))
Directory.CreateDirectory(finalDir);
var finalPath = Path.Combine(finalDir, $"{request.FileHash}_{request.FileName}");
await using var finalStream = new FileStream(finalPath, FileMode.Create);
for (int i = 0; i < request.TotalChunks; i++)
{
var chunkPath = Path.Combine(chunkDir, $"{i:D6}.tmp");
await using var chunkStream = new FileStream(chunkPath, FileMode.Open, FileAccess.Read);
await chunkStream.CopyToAsync(finalStream);
}
// 清理临时分块目录
Directory.Delete(chunkDir, true);
// 登记到索引中,下次秒传直接命中
_fileIndex[request.FileHash] = finalPath;
return Ok(new { message = "合并完成", path = finalPath });
}
public class MergeRequest
{
public string UploadId { get; set; }
public string FileName { get; set; }
public string FileHash { get; set; }
public int TotalChunks { get; set; }
}
合并完成之后,临时分块目录一定要清理。很多线上事故就是临时文件堆积过多导致磁盘满,我见过一台服务器因为断了十几单上传任务,几百个小临时文件占了几个GB的磁盘空间。
4. 秒传校验与文件指纹计算:MD5之外还有增量哈希
秒传听起来简单,但要想做得真正好用,“文件指纹怎么算”是个需要认真对待的问题。最大的难点是前端计算大文件哈希时,如果一次性读入整个文件,浏览器内存直接爆炸。所以要分步读、分步算,而且要注意MD5本身在并行哈希场景下的限制。
4.1 前端如何计算大文件的MD5
MD5的算法是串行的,它不能把一个文件拆开并行算然后再拼接,否则结果不对。如果文件有2GB,前端一次性FileReader.readAsArrayBuffer读完整个文件再算MD5,内存峰值会到2GB,在大多数用户设备上会直接卡死。
正确的方式是“分块读取、逐步更新哈希”。每次读取一段固定大小的数据(比如2MB),把它喂给MD5库,更新哈希状态,读完整个文件后再取出最终的哈希值。这样内存占用始终是固定的2MB左右,无论文件多大都能算完。
我推荐用spark-md5这个库,它提供了incremental模式,正是为这种场景设计的:
javascript复制import SparkMD5 from 'spark-md5';
function computeFileHash(file) {
return new Promise((resolve, reject) => {
const chunkSize = 2 * 1024 * 1024;
const chunks = Math.ceil(file.size / chunkSize);
const spark = new SparkMD5.ArrayBuffer();
let currentChunk = 0;
const fileReader = new FileReader();
fileReader.onload = function(e) {
spark.append(e.target.result);
currentChunk++;
if (currentChunk < chunks) {
loadNext();
} else {
const hash = spark.end();
resolve(hash);
}
};
fileReader.onerror = reject;
function loadNext() {
const start = currentChunk * chunkSize;
const end = Math.min(start + chunkSize, file.size);
fileReader.readAsArrayBuffer(file.slice(start, end));
}
loadNext();
});
}
这段代码先把文件切片逐个读成ArrayBuffer,然后追加到SparkMD5实例里。注意不能在每片结束时调用spark.end(),这个方法调用后哈希状态会被重置,必须在所有数据追加完成后调用一次。
4.2 增量哈希:一种更快的替代方案
如果你想进一步优化秒传的体验,还有一个思路叫增量哈希(Incremental Hash),比如用BLAKE2或者xxHash这类算法,配合流式读取。它的好处是速度比MD5快一个量级,2GB的文件MD5可能需要十几秒,xxHash可能只需要一两秒。
但增量哈希方案有一个前提:前后端必须使用同一套哈希算法,并且算法库在目标平台上可用。你自己写分块上传和秒传逻辑时用增量哈希没任何问题,但如果你要兼容WebUploader等插件的自带MD5逻辑,就得老老实实用MD5,否则预检接口查不到匹配记录。
从实际经验看,除非文件特别大(几十GB级别),否则MD5的速度已经够了。计算2GB文件哈希大约需要8-15秒,这个时间对用户来说是可接受的,而且计算哈希的同时可以开始传第一块,不一定非要等到全部哈希算完。
4.3 预检与上传交叉进行的优化策略
这里分享一个我实测下来体验更好的做法:不等待整个文件哈希计算完,而是先算前几MB的“头部哈希”做快速预检,如果头部哈希匹配,再继续算全文件哈希做二次确认。这样对于重复文件,可以做到真正“秒”判,而不需要任何上传动作。
不过这个策略也有风险,如果文件内容相同但头部恰好相同、中间不同,快速预检会误判。所以加二次确认是必须的。实战中我一般这样做:
- 计算文件前2MB哈希,发出快速预检请求。
- 如果快速预检通过(说明可能已存在),继续计算全文件哈希,再做一次完整预检。
- 如果完整预检也通过,直接返回秒传成功。
- 如果完整预检不通过,继续分块上传。
这套流程虽然多了一次预检请求,但换来的是“99%的重复文件在2MB粒度即可直接跳过”的体验。实际效果非常理想,很多用户反馈“刚到上传页面还没等进度条出来就完成了”。
5. 合并之后的事:完整性校验、并发控制与临时文件治理
很多教程写到合并接口就结束了。但生产环境中,合并完成只是开始。这一节聊聊我实际部署后总结的三个关键处理点。
5.1 合并后的完整性校验:别让磁盘坏道毁掉用户文件
文件合并完成后,我强烈建议做一次完整性校验。校验方式很简单:合并后的文件大小是否等于前端上报的文件大小,再算一次文件哈希看是否和前端预检时的哈希一致。
这一步很耗资源,但一定不能省。原因有二:第一,分块上传过程中可能存在网络传输错误,比如某一块虽然收到了但内容被篡改或损坏,合并后的文件就是坏的;第二,磁盘IO可能存在偶发错误,合并写入时数据不完整。两次哈希一致,才能100%确认文件没问题。
csharp复制var finalSize = new FileInfo(finalPath).Length;
if (finalSize != request.FileSize)
{
File.Delete(finalPath);
return BadRequest("文件大小校验不一致,请重新上传");
}
我把FileSize也加到了合并请求参数里,前端在上传前就知道文件总大小。如果合并后的大小和预期不一致,直接删除并请求重传。
5.2 并发上传场景下的写入冲突与锁
分块上传的接口天然支持并发——多个块可以并行提交,这样可以充分利用带宽。但要小心一个坑:合并的时候,其他分块还在写入中。前端必须保证所有块都返回成功后,才能发起合并请求。这个时序问题必须在前端把控,后端无法判断“某一块是否还在传输中”。
后端的并发风险主要在两个地方:一个是同一个上传会话的多个块同时写目录,Directory.CreateDirectory是线程安全的,可以放心调用;但如果你自己在代码里做了“先检查目录是否存在再创建”的逻辑,就要加锁,否则两个请求同时检查,发现不存在,同时创建,会抛异常。另一个是最终文件的索引写入,用ConcurrentDictionary就足够了,不必用手动锁。
5.3 临时文件的过期清理机制
前面我提到临时分块目录要清理,但生产环境不能只靠合并接口触发清理。用户上传到一半放弃、断网、浏览器崩溃,这些情况都会留下大量孤儿临时目录。
我建议写一个后台清理任务,定期扫描临时目录,删除超过24小时没有更新的分块文件。ASP.NET Core里可以用BackgroundService实现一个简单的定时清理服务。
csharp复制public class TempFileCleaner : BackgroundService
{
private readonly string _tempRoot;
private readonly TimeSpan _maxAge = TimeSpan.FromHours(24);
public TempFileCleaner(string tempRoot)
{
_tempRoot = tempRoot;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
foreach (var dir in Directory.GetDirectories(_tempRoot))
{
var lastWrite = Directory.GetLastWriteTime(dir);
if (DateTime.Now - lastWrite > _maxAge)
{
Directory.Delete(dir, true);
}
}
}
catch (Exception ex)
{
// 记录日志,但不要因为清理任务影响主流程
}
await Task.Delay(TimeSpan.FromHours(1), stoppingToken);
}
}
}
服务启动时注册:builder.Services.AddHostedService(sp => new TempFileCleaner(tempRoot));
磁盘清理听着好像可有可无,但真的不能等到磁盘满了再处理。我在一个项目里遇到过用户批量上传十几GB素材包,中途断网扔了几十个分块目录,两天后磁盘满了,整个服务不可用,最后是人肉登录服务器清理的。
6. 实测中踩过的配置坑:请求大小限制、超时与服务器环境调优
代码写得再好,如果服务器配置没跟上,一样跑不动。大文件分块上传在生产环境极其依赖服务端框架和网关的配置调整。这些配置问题如果不提前处理,用户上传到一半就会掉链子。
6.1 ASP.NET Core的请求体大小限制
ASP.NET Core的Kestrel默认请求体最大是30MB,所以分块大小必须小于这个值,否则接口直接返回413 Payload Too Large。我建议在Program.cs里显式调大限制:
csharp复制builder.Services.Configure<KestrelServerOptions>(options =>
{
options.Limits.MaxRequestBodySize = 50 * 1024 * 1024; // 50MB
});
如果你用的是IIS作为反向代理,还要修改web.config里的maxAllowedContentLength:
xml复制<system.webServer>
<security>
<requestFiltering>
<requestLimits maxAllowedContentLength="52428800" />
</requestFiltering>
</security>
</system.webServer>
这里有个容易混淆的地方:Kestrel的限制是以字节为单位,IIS的maxAllowedContentLength也是字节,但很多网上资料写成KB,照抄的话会把限制缩小1000倍,导致大一点的分块全部被拒。我最初就踩过这个坑,想了半天为什么分块一到2MB就被拒。
6.2 前端分块大小的选择与网络适配
分块大小不是固定不变的,要根据目标用户的网络情况调整。手机4G/5G环境下,分块太大会导致单次请求失败概率升高,太小又会让请求数膨胀,增加网络往返开销。
根据我的经验,普通Web应用推荐2MB到5MB作为默认分块大小。这个范围在大多数网络环境下成功率都不错。如果是面向企业内网用户,带宽稳定且带宽大,可以调到10MB甚至20MB,减少请求次数,降低服务端开销。
另外要注意:分块大小直接决定了并发策略。如果你设置了10个并发上传分块,每个分块5MB,那么瞬时带宽占用是50MB的传输量,这个服务器吞吐压力要提前评估。
6.3 几项生产环境参数的推荐值
我把几个关键配置项的推荐值整理成表格,方便直接抄。
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| Kestrel MaxRequestBodySize | 50MB | 略大于最大分块大小,留出余量 |
| 分块大小 | 2MB-5MB | 根据目标网络环境调整 |
| 前端并发上传数 | 3-5个 | 并发太多会让服务器磁盘IO饱和 |
| 临时文件清理周期 | 1小时 | 配合24小时过期时间 |
| MD5计算分片大小 | 2MB | 与分块大小保持一致 |
| 合并写入缓冲区 | 1MB | 减少磁盘写入次数 |
并发上传数的选择很值得说一句。很多人以为并发越大上传越快,实际上到了服务器端,所有分块都在争抢磁盘IO。如果服务器用的是普通机械硬盘,并发过大反而会让写入速度下降。我实测下来,机械硬盘环境下并发3个是最优的,SSD上可以放宽到5个。
7. 我在实战中反复调整的几个细节
最后一节不写代码,写点实用经验。这些细节分别来自不同项目中的真实教训,也是我反复调整后总结下来的。
7.1 前端进度条要用心跳策略而不是逐块更新
分块上传的进度条如果每个分块完成才更新一次,用户会看到进度条一顿一顿地跳,体验不好。我建议把进度分成两部分:计算哈希阶段显示“分析文件中”,上传阶段用一个动态心跳模拟平滑进度,比如每200毫秒在区间内平滑推进,实际进度到了再校正。这样用户感觉到的上传过程是流畅的,而不是卡顿的。
7.2 不要让用户自己决定“重传”
网络断开后,如果前端能自动续传,就不要弹窗让用户手动选择“继续上传”还是“重新上传”。实现断点续传的方式很简单:前端启动上传前,先调用合并检查或分块状态查询接口,问一下后端“这些块哪些已经传过了”,已经存在的块跳过,直接传缺失的块。实际操作中我是在后端加了一个chunk/status接口,返回已接收的分块序号列表,前端根据这个列表决定从哪个块开始继续传。
7.3 文件哈希的计算结果可以缓存
同一个文件,用户可能在不同时间多次上传。每次重新计算一个2GB文件的MD5要十几秒,很浪费时间。我建议前端把计算结果缓存在浏览器IndexedDB里,键是文件的大小、文件名后缀、最后修改时间这几个基础元数据。发生了真正的内容变更,这些元数据也会变化,哈希缓存自然失效。这个优化能大幅提升重复上传场景下的体验。
7.4 安全与合规的提醒
最后提醒一下,上传功能涉及恶意文件风险。生产环境一定要加两个校验:第一,文件扩展名白名单,不能放行.exe、.js这类可执行文件;第二,文件内容扫描,必要时接病毒扫描API。内部项目可以把这些校验放在合并之后执行,但一定要有,不要等出事了再补。
这套方案我从第一版到现在已经跑了一年多,稳定支撑过单文件最大32GB的素材上传,分块加秒传加断点续传这条路确实比原生的整体上传可靠太多。如果你在集成过程中遇到问题,建议先抓一下协议对接的日志,多数情况下问题都出在字段名或者请求体大小限制上,那是最容易排查也最容易忽略的两个地方。
