做视频上传,尤其是动辄几百MB、几个GB的大文件,第一反应就是:直接传肯定会炸。PHP环境有upload_max_filesize和post_max_size管着,Nginx又有client_max_body_size拦在路上,就算全调大,用户那边网络一抖,传了十分钟的文件说没就没,重来一遍体验极差。分片上传就是这个场景下的常规解法:把一个大文件切成很多个小块,每一块独立上传,最后在服务端拼接回来。WebUploader是我用过比较顺手的一套开源上传组件,它把分片、并发、进度、队列这些都封装好了,配合PHP后端做一个完整的视频分片上传链路,并没有想象中那么复杂。
1. 为什么视频上传必须走分片这条路
1.1 先看清传统上传的两个致命瓶颈
传统表单上传最大问题不是代码写得不对,而是服务器和网络都扛不住大请求。先说服务器侧:PHP默认upload_max_filesize只有2M,post_max_size也只有8M,视频随便一个都超标。就算你把这两个参数调到512M,Nginx那边默认client_max_body_size是1M,一个超过1M的POST请求直接返回413。你会想“那我全调大不就行了”?可以,但这只是把门槛抬高了,并没有解决另外一个更现实的问题:一次HTTP请求要承载几百MB的数据,任何一环出问题都会前功尽弃。
网络侧同样麻烦。上行带宽不足时,一个大文件可能要传十几分钟,期间手机切了个网络、笔记本休眠了一下、代理超时断连,浏览器这边请求失败,用户看到的是失败提示,服务端收到的是一个写到一半的文件,而且大概率已经被PHP当成临时文件清掉了。就算服务端做得好,能保留半成品,前端重新上传时怎么跳过已传的部分,又是一堆额外工作。传统上传方式在视频这个量级面前,真的不太够用。
1.2 分片上传的本质:一次大传输拆成多次小请求
分片上传的思路很直白:前端先把视频文件按固定大小切成N个分片,每个分片单独发起一次HTTP POST请求,服务端收到后先把分片暂存起来,等所有分片到齐,再按顺序合并成完整文件。这样做有几个直接好处:
- 单片请求体小,轻松绕开
upload_max_filesize这类单请求限制; - 某个分片失败只需要重传那一片,不用整个文件重来;
- 可以通过控制并发分片数来平衡速度和服务端压力;
- 每个分片都带文件唯一标识和序号,服务端能识别已上传的分片,断点续传才有了基础。
这个思路放在生活里特别容易理解:搬家时一个大衣柜不好搬,拆成几块板运过去,到了新家重新组装。组装顺序不能乱,所以每块板上都得标好编号。分片上传里的编号,就是WebUploader在请求里携带的chunk参数。
1.3 WebUploader在分片上传里的角色
WebUploader做的事情,就是帮我把“切分”、“编号”、“并发上传”、“进度统计”、“失败重试”这些前端工作全部封装好。我只需要告诉它:分片大小是多少、并发几个、上传地址在哪、文件字段叫什么。剩余的工作,比如用HTML5 File API的Blob.slice()切割文件、维护队列、计算上传进度,它都内部处理了。
这也是为什么很多老项目至今还在用它:API稳定,文档齐全,网上踩坑案例多。对于不打算从零手写XHR分片逻辑的团队来说,先用它把业务跑通,是最划算的选择。本文后半部分的PHP服务端逻辑,即使你换了别的上传组件,甚至纯手写前端,核心思路也完全通用——服务端根本不关心前端是谁,只看你POST过来什么参数。
2. WebUploader前端配置:先搞定这五个参数
2.1 核心配置项逐项拆解
用WebUploader做视频分片上传,WebUploader.create()里的这几个配置项我是建议优先理解的:
server:分片上传请求发往后端的地址,没有它整个上传流程根本跑不起来。chunked:是否启用分片,做视频上传必须设为true。chunkSize:每个分片的大小,单位是字节。比如5 * 1024 * 1024就是5MB。这个值不是越大越好,也不是越小越好,我后面单独说。threads:同时上传的分片数。注意它不是同时上传的文件数,而是同一个文件内部并发分片的数量。fileVal:文件字段名,默认是file,PHP端就用$_FILES['file']接收。formData:附加在每次分片请求里的自定义表单数据,比如用于标识文件的guid字段。
还有一个容易忽略但实际很关键的参数是duplicate。默认情况下WebUploader会过滤掉重复选择同一文件的操作,如果断点续传时需要反复选择同一个文件来触发继续上传,就得把duplicate设为true,否则第二个请求直接进不了队列。
2.2 用代码把前端骨架搭起来
下面这段是我常用的配置模板,直接抄到一个空白页面里,配合后端的upload.php即可跑通基础流程:
var uploader = WebUploader.create({ swf: 'Uploader.swf', server: 'upload.php', pick: '#picker', auto: true, accept: { title: '视频文件', extensions: 'mp4,mov,avi,mkv', mimeTypes: 'video/*' }, chunked: true, chunkSize: 5 * 1024 * 1024, threads: 3, fileNumLimit: 1, fileVal: 'file', duplicate: true });这里swf配置项现在基本只是兼容老代码用的,现代浏览器走的是HTML5通道,不会加载Flash。accept里的extensions是给用户的选择器做过滤,属于用户体验层面的限制,不能当作服务端的安全校验,这一点我在第4章还会重点强调。
chunkSize选多大,取决于目标文件大小和服务端承受能力。我自己的习惯是:视频普遍在几百MB级别,用5MB分片,请求量大概几十到一两百个,压力可控;如果是GB级视频,我会把分片调到10MB,减少请求数量,降低网络往返开销。分片太小比如1MB,一个500MB的视频就要500个请求,服务器日志刷得飞起,网络握手开销占比也大;分片太比如50MB,单片传输失败时的重传代价又高了。
2.3 文件指纹与断点续传的前端准备
WebUploader本身只负责把文件切成片传出去,不做判断“这个文件之前传过没有”。要做断点续传,通常是在前端生成一个文件标识,比如文件名的哈希、文件大小和最后修改时间的组合,每次选择文件时算出一个guid,通过beforeUpload事件写入formData:
uploader.on('beforeUpload', function(file) { uploader.options.formData = { guid: getFileGuid(file) }; });getFileGuid的计算方式我常用文件名、文件大小和最后修改时间的组合,再做一个简单的哈希处理,保证同一个文件每次选择都得到同一个guid。这样刷新页面后重新选择同一个视频,服务端才能通过guid找到之前没传完的临时分片目录。
要做到更严格的文件级秒传,需要在选择文件后先异步请求服务端,询问该guid是否已经有完整文件,如果存在,直接跳过上传。这个逻辑WebUploader也支持,在beforeUpload里返回一个Deferred对象,查询期间暂停当前文件的上传过程。
3. PHP服务端的接收与合并:这才是大头
3.1 分片请求带了哪些参数
当WebUploader以分片方式发起上传时,后端收到的POST请求,除了$_FILES里的一份分片数据之外,还会带上几个关键参数。默认情况下:
$_POST['chunk']:当前分片的序号,从0开始;$_POST['chunks']:总分片数;$_POST['guid']:我自己在formData里加的文件标识;$_FILES['file']:当前这个分片的临时文件,字段名对应前端配置的fileVal。
判断一个请求是不是某个大文件的最后一片,不需要在前端做特殊通知。服务端只需检查“这个guid对应的临时目录里,0到chunks-1号分片是否都存在”,如果都存在,说明全部到达,可以触发合并。所以接收分片的逻辑本质上就是:保存分片、逐个检查序号、齐了就合并。
3.2 服务端接收分片的完整流程
下面这段PHP代码是我在实际项目里沉淀下来的基础版本。它把分片保存到以guid命名的临时目录里,分片文件名直接用序号加.part后缀:
<?php error_reporting(E_ALL); ini_set('display_errors', '0'); header('Content-Type: application/json; charset=utf-8'); set_time_limit(0); define('UPLOAD_ROOT', __DIR__ . '/upload/'); define('TMP_DIR', UPLOAD_ROOT . 'tmp/'); define('VIDEO_DIR', UPLOAD_ROOT . 'video/'); if (!is_dir(TMP_DIR)) mkdir(TMP_DIR, 0755, true); if (!is_dir(VIDEO_DIR)) mkdir(VIDEO_DIR, 0755, true); if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['status' => 0, 'msg' => '仅支持POST请求']); exit; } $guid = trim($_POST['guid'] ?? ''); $chunkIndex = intval($_POST['chunk'] ?? 0); $totalChunks = max(1, intval($_POST['chunks'] ?? 1)); // guid严格白名单校验,防止路径穿越 if ($guid === '' || !preg_match('/^[a-zA-Z0-9_-]{6,64}$/', $guid)) { echo json_encode(['status' => 0, 'msg' => '非法文件标识']); exit; } if (!isset($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) { echo json_encode(['status' => 0, 'msg' => '分片数据接收失败']); exit; } $tmpDir = TMP_DIR . $guid . '/'; if (!is_dir($tmpDir)) mkdir($tmpDir, 0755, true); $partFile = $tmpDir . $chunkIndex . '.part'; if (!move_uploaded_file($_FILES['file']['tmp_name'], $partFile)) { echo json_encode(['status' => 0, 'msg' => '分片写入失败']); exit; } // 逐个检查分片是否齐全 $allPresent = true; for ($i = 0; $i < $totalChunks; $i++) { if (!is_file($tmpDir . $i . '.part')) { $allPresent = false; break; } } if (!$allPresent) { echo json_encode(['status' => 0, 'msg' => '分片已接收', 'chunk' => $chunkIndex]); exit; } // 分片齐全,开始合并 $targetFile = VIDEO_DIR . $guid . '.mp4'; $out = fopen($targetFile, 'wb'); if (!$out) { echo json_encode(['status' => 0, 'msg' => '目标文件创建失败']); exit; } for ($i = 0; $i < $totalChunks; $i++) { $partPath = $tmpDir . $i . '.part'; $in = fopen($partPath, 'rb'); if (!$in) { fclose($out); @unlink($targetFile); echo json_encode(['status' => 0, 'msg' => "分片{$i}读取失败"]); exit; } stream_copy_to_stream($in, $out); fclose($in); @unlink($partPath); } fclose($out); @rmdir($tmpDir); echo json_encode([ 'status' => 1, 'msg' => '上传成功', 'path' => 'upload/video/' . $guid . '.mp4', 'size' => filesize($targetFile) ]);注意几个细节。第一,move_uploaded_file是唯一正确处理PHP上传临时文件的函数,它会把文件从PHP的临时目录移动到指定位置;直接rename或copy某些场景下也能用,但move_uploaded_file会额外校验文件确实是通过HTTP POST上传的,更安全。第二,分片目录用guid做名字而不是原始文件名,是为了避免不同用户上传同名文件互相干扰,也避免中文文件名在部分服务器上产生编码问题。第三,判断“分片是否齐全”必须循环检查每个序号,不能只看文件数量,否则遇到某个分片被重传覆盖、另一个分片缺失的情况,就会被误判为全部到齐。
3.3 分片合并的正确姿势与内存控制
合并部分,我见过不少人在循环里用file_get_contents($partFile)配合file_put_contents($targetFile, $content, FILE_APPEND),小文件没问题,但遇到大视频或者超大分片时,内存占用会直线上升。file_get_contents会把整个分片读入内存,一个10MB的分片还能接受,但如果有人把chunkSize设成50MB,PHP进程瞬间吃掉几十MB,多并发几路,内存直接告急。
更稳妥的做法是用fopen打开目标文件,以二进制追加方式写,通过stream_copy_to_stream把一个分片流式拷贝进去。这样内存占用只取决于缓冲区大小,和分片大小没有线性关系。上面的合并代码已经用了这个方式,合并过程中每拷贝完一个分片就删除对应.part文件,省磁盘空间,也方便异常清理。
合并完成后最好检查一下最终文件大小,至少能发现“前端口口声声说传完了,但实际分片不完整”的异常。更严格的做法是服务端在分片上传过程中累计每个分片的大小,合并后对比总大小,不过WebUploader自带的请求参数里没有提供这个信息,需要前端额外传一个totalSize字段,实现也不复杂,但务必要记住这个大小来自客户端,只能作为参考值,不能当作安全凭证。
3.4 合并后的文件清理与目录结构设计
临时分片目录要记得清理。一个实际的问题:用户传了一半不传了,刷新页面关了浏览器,临时目录里的.part文件就成了垃圾。我习惯在服务器上配置一个定时任务,删除超过24小时未被修改的临时分片目录。这一步看起来不起眼,但对长期运行的服务器来说非常关键,否则磁盘会被碎片文件塞满。
目录结构方面,我是按upload/tmp/和upload/video/分开设计的。video目录放合并后的最终文件,如果需要通过URL直接播放,就让它保持Web可读;tmp目录只对服务端开放,绝对不允许用户通过浏览器直接访问。你可以在Nginx里给tmp目录单独配置拒绝规则,也可以把整个upload目录放到Web根目录之外,靠PHP脚本读写,后一种安全等级更高,但播放视频时需要另外写一个输出文件流的接口,麻烦一些。小型项目用前一种就够了。
4. 别急着上线:校验、安全与性能调优
4.1 文件类型与完整性校验
先说一个新手容易踩的认知误区:前端accept里的extensions只是选择框过滤,攻击者可以随便改请求,后端不能信这个。那后端靠什么?扩展名白名单是最基础的一层,还有一个很容易忽略的事实:分片阶段不适合用文件头去判断视频类型。
为什么?因为分片只是完整文件的一部分,大多数视频格式的文件头只在文件开头几个KB里。序号较大的分片,内容是视频的中段或尾部,没有完整的结构头,用finfo或读取魔数去判断都会误报。正确的做法是:合并完成之后,再对完整文件做一次MIME或文件头校验,确认它真的是视频文件。基于这个原则,上面的Demo代码里没有在接收分片时做严格的MIME判断,合并完成后可以这样补充:
$finfo = new finfo(FILEINFO_MIME_TYPE); $mime = $finfo->file($targetFile); $allowedMimes = ['video/mp4', 'video/quicktime', 'video/x-msvideo', 'video/x-matroska']; if (!in_array($mime, $allowedMimes)) { @unlink($targetFile); echo json_encode(['status' => 0, 'msg' => '文件类型校验失败']); exit; }服务器上如果没装fileinfo扩展,可以退而求其次读文件头几个字节判断。比如MP4文件偏移4字节处通常是ftyp标识,很多视频文件的头部都有类似魔法数字。但这个方法对某些编码的MOV、MKV不一定准确,所以生产环境最好还是开fileinfo。另外,合并后的filesize要做一个下限校验,比如目标文件小于某个阈值(比如100KB),基本可以断定不是正常视频,直接拒绝。
4.2 上传目录安全配置
上传功能最怕两件事:一是用户传上来一个PHP脚本伪装成视频,还能被服务器当成PHP执行;二是路径拼接没处理好,用户通过文件名穿越目录。第一件事靠目录执行权限控制,第二件事靠服务端对文件名严格校验。
我在上面的代码里对guid做了正则白名单校验,只允许字母、数字、下划线和连字符,长度为6到64位。这样就彻底堵死了../../这类路径穿越的可能。不要拿前端file.name直接拼路径,不管它经过怎么样的过滤,都不要,这是铁律。
Web目录下的upload/tmp要禁止外部访问。Nginx里可以这样配:
location ~* ^/upload/tmp/ { deny all; } location ~* ^/upload/.*\.(php|php5)$ { deny all; }第一个规则拒绝任何人直接访问临时目录里的分片文件,第二个规则禁止upload目录下任何PHP脚本被Web执行。这里特别提醒一点:即使上传的文件扩展名被强制改成.mp4,也不能放松警惕。某些老版本Web服务器存在解析漏洞,比如/upload/video/xxx.mp4/.php这种路径会被错误解析为PHP,所以上面第二条规则能加就加上。
4.3 服务器与PHP配置的联动修改
走到这里,服务端逻辑没问题了,但能不能跑起来,还取决于环境配置。分片上传虽然绕过了upload_max_filesize对单文件大小的限制,但每一个分片请求仍然是一次POST,所以post_max_size仍然会限制请求体大小,而且Nginx的client_max_body_size管的也不是“整文件”,而是“单次请求体”。
我做视频上传时,环境配置至少会改成这样:
upload_max_filesize = 20M post_max_size = 32M max_execution_time = 120 memory_limit = 256Mupload_max_filesize管的是单文件大小,前端chunkSize如果是5MB,这个值设20M给足余量,避免其他需要一次性上传的场景受限。post_max_size必须大于upload_max_filesize,因为POST请求体里除了文件还有各种表单字段和HTTP头,我习惯留出十几MB的余量。max_execution_time在合并大文件时主要靠脚本里的set_time_limit(0)兜底,但其他PHP脚本还是需要一个合理上限。
Nginx这边,对应修改的是client_max_body_size和超时时间:
client_max_body_size 32m; client_body_timeout 120s;如果你用的是Apache,注意LimitRequestBody和TimeOut也是类似的思路。这些配置改完要重启Web服务器才生效,很多同学改完php.ini发现没变化,就是忘了重启PHP-FPM。
5. 实战中踩过的坑:问题排查速查表
5.1 最大概率踩到的几个坑
下面这张表是我自己遇到过的、以及帮别人排查过的最高频问题,按出现概率排序:
| 现象 | 直接原因 | 解决方向 |
|---|---|---|
| 上传返回413 | Nginxclient_max_body_size小于单片大小 | 调大该参数并重启Nginx |
| 上传提示POST大小超限 | PHPpost_max_size小于请求体 | 调大post_max_size,同时检查upload_max_filesize |
| 前端提示上传失败,但后端日志显示收到了文件 | 服务端返回的业务状态不是前端期望的status:1 | 保持前后端约定一致的响应结构;固定Content-Type: application/json |
| 合并后视频文件打不开 | 合并顺序错误或分片缺失 | 确认合并循环严格按分片序号递增,使用wb二进制模式写入 |
| 上传进度卡在99%后失败 | 最后一片请求处理时间过长,触发超时 | 合并不放在最后一片的请求里,或set_time_limit(0) |
关于最后一片触发合并导致超时的问题,很多项目早期都会在这里栽跟头:一个大视频全部分片到达,PHP在最后一个请求里要读取几十个分片并合并,耗时可能超过默认的超时时间。前面我用了set_time_limit(0),在高并发下也可以把合并操作丢给异步任务队列处理,先向前端返回“分片已全部接收”,后台慢慢合并,完成后回调通知。具体取舍看你的服务器规模和业务要求。
还有一个隐藏很深的坑:PHP的session_start()会锁定session文件。如果你在上传接口里同时开启了session,并发的那几个分片请求会变成排队执行——因为每个请求都要等session锁释放。前端看起来就是明明设置了3个并发,实际请求却一个接一个。解决方法很简单:上传接口里如果不需要读session,就别开;必须开的话,尽早session_start()然后立即session_write_close()释放锁。
5.2 断点续传的隐藏陷阱
断点续传看起来高大上,无非就是服务端检查已有分片、前端跳过已传分片。实际做起来有几个细节很容易被忽略。
第一,guid的生成规则必须稳定。如果每次刷新页面都生成一个完全不同的随机字符串,那服务端就永远找不到旧分片,每次都是全新上传。用“文件名+大小+最后修改时间”这些文件本身的信息来生成标识,连续两次选择同一个文件才能得到同一个guid。
第二,检查“已有分片”时,不能只检查分片文件是否存在,还要检查大小是否大于0。我曾经遇到某客户端上传分片时偶发写入空文件,服务端误以为分片已存在,直接跳过,最后合并出来的视频播放到中间就断。后续的检查逻辑里加了一条“分片文件大小必须大于0KB才认为有效”。
第三,秒传和断点续传是两个不同级别的功能。断点续传是分片级别的,秒传是文件级别的:服务端查一下这个guid是否已经合并过完整文件,如果有,前端直接跳过整个文件。这两个接口可以单独做,合并接口和查询接口职责各不相同,不要混在一起写。
5.3 并发合并时的竞态问题
分片上传天然是并发的,服务端合并逻辑里有个典型的竞态:所有分片最后一片到达时,恰好有两个分片请求同时进入“检查分片齐全”这段代码,两个进程都认为分片齐了,于是同时开始合并,写入同一个目标文件,轻则数据错乱,重则互相覆盖导致文件损坏。
解决思路也简单:在合并动作之前加一把锁,或者用一个“合并中”的标记文件。比如在临时目录里创建一个.merging文件,fopen它时用排他锁,或者直接判断这个标记文件是否存在,存在就说明另一个进程正在合并,当前请求只返回成功状态即可。合并完成后删除标记文件,再清理临时目录。我实际项目里更倾向于用一个独立的合并队列,分片到达后只负责落盘,由后台任务统一扫描、合并,从根上避免了竞态问题,代价是多写一个常驻脚本,适合文件量比较大的场景。
6. 一个可直接复用的Demo归纳
6.1 前端完整代码
把前面的核心配置拼起来,一个最小的可运行页面大概是这样的。页面里有一个选择按钮,一个显示结果的容器,静态资源webuploader.js、webuploader.css和jquery要自己放到对应目录。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>视频分片上传Demo</title> <link rel="stylesheet" href="webuploader.css"> </head> <body> <div id="picker">选择视频</div> <div id="result"></div> <script src="jquery.min.js"></script> <script src="webuploader.js"></script> <script> var uploader = WebUploader.create({ swf: 'Uploader.swf', server: 'upload.php', pick: '#picker', auto: true, accept: { title: '视频文件', extensions: 'mp4,mov,avi,mkv', mimeTypes: 'video/*' }, chunked: true, chunkSize: 5 * 1024 * 1024, threads: 3, fileNumLimit: 1, fileVal: 'file', duplicate: true }); function getFileGuid(file) { var info = file.name + '_' + file.size + '_' + (file.lastModifiedDate ? file.lastModifiedDate.getTime() : ''); var hash = 0; for (var i = 0; i < info.length; i++) { hash = ((hash << 5) - hash) + info.charCodeAt(i); hash |= 0; } return 'vid_' + Math.abs(hash).toString(36) + '_' + Date.now().toString(36); } uploader.on('beforeUpload', function(file) { uploader.options.formData = { guid: getFileGuid(file) }; }); uploader.on('uploadProgress', function(file, percentage) { console.log('进度:' + percentage); }); uploader.on('uploadSuccess', function(file, response) { var data = typeof response === 'string' ? JSON.parse(response) : response; if (data.status === 1) { $('#result').html('上传完成,文件地址:' + data.path); } }); uploader.on('uploadError', function(file, reason) { alert('上传失败:' + reason); }); </script> </body> </html>这里有一个值得注意的细节:uploadSuccess回调里的response可能是字符串,也可能是对象。服务端如果设置了Content-Type: application/json,WebUploader会自动解析成对象;如果服务端返回的是普通文本或没有正确设置响应头,response就是一串JSON字符串。所以我在回调里做了typeof response === 'string'的判断,这个小习惯能避免很多莫名其妙的问题。
6.2 服务端完整代码
服务端完整代码在第3章的Demo基础上稍作调整即可。如果你把上传目录放在Web根目录外,记得在uploadSuccess返回给前端的path字段里拼接一个可访问的映射地址,而不是真实的服务器绝对路径。
还要补一个“查询是否已存在完整文件”的接口,用于秒传场景。代码思路很简单:guid对应的目标文件如果存在,返回exists: true;否则检查临时目录,返回已有分片数。前端拿到结果后可以决定是否调用uploader.skipFile(file)。
6.3 我的几点经验总结
做了几个实际项目之后,我对WebUploader分片上传有这么几点体会。
分片大小不要死脑筋用默认值。默认配置不一定适合你的业务场景,需要根据平均文件大小、服务器带宽、后端并发能力去调。5MB是一个比较通用的起点,但如果你们平台视频普遍在2GB以上,可以适当调大到10MB甚至20MB,同时记得同步调大各个服务端的限制参数。
并发数控制在3到4最稳妥。有人觉得并发越大传得越快,实际上对于普通带宽的服务器,并发高了反而导致分片到达顺序错乱、磁盘写入争抢,上传进度条还会因为乱序而频繁跳动。真正常见的情况是:单条TCP连接的吞吐已经接近用户上行带宽上限,开那么多并发并没有额外收益。
服务端临时目录一定要有清理机制。我接手过一个跑了半年的项目,临时目录里堆了几十GB的半截分片,磁盘告警才知道是用户传一半放弃留下的。后来安排了一个每天凌晨清理超过24小时分片的任务,问题再没出现过。
最后,关于响应结构,前后端尽早约定好。status、msg、path、size这些字段名的意思要写清楚,最好有个简单的接口文档。我在排查别人代码时见过最乱的场景是:前端判断status === 0表示成功,后端却用status === 1表示成功,两边都觉得自己没错,最后浪费一个下午对代码。约定好之后,再配合上面这套完整的接收、合并、校验逻辑,一套稳定可用的视频分片上传服务就算落地了。