车间里老师傅拍了一段10分钟的机床换模操作视频1080p手机直出一看文件大小1.2GB。想传到OA里给全车间做SOP培训结果拖进网页传了一个小时最后在99%的地方弹了个上传失败。重来一遍又得面对同样的问题。这不是网速的锅也不是OA平台故意为难人而是传统表单上传这种模式在设计上就没有为“超大文件”做过准备。机械制造企业里设备操作视频的场景越来越多从标准作业指导书SOP录制、新员工培训到设备验收、售后故障排查动辄就是几百MB甚至上GB的文件偏偏OA系统最常见的技术栈又是HTMLPHP默认的上传限制往往只有2MB到8MB。这篇文章我把自己在制造企业OA里做设备操作视频超大文件分片续传的完整思路、前后端代码、踩坑记录和服务器参数配置整理出来给同样被大文件上传折磨过的运维和实施同学做个参考。适合在推进企业数字化转型、OA二次开发或者设备管理信息化建设中需要处理大附件的读者。1. 先捋清业务场景设备操作视频为什么“大到必须分片”1.1 三类必须录视频并传到OA的真实需求我在制造企业里调研过一圈设备操作视频的需求主要集中在三个方向每个方向都对文件大小有非常直接的影响。第一类是设备SOP标准化这是最典型的需求。每台设备的换型、换刀、对刀、装夹、调参都有严格的步骤顺序。过去这些内容写在纸面上老师傅离职了新人根本看不懂纸上的抽象描述。现在普遍的做法是录成视频多角度拍摄配上休息点提示一个完整的换模SOP视频很容易超过500MB。第二类是质检与验收留痕。设备出厂前的运行测试、客户现场安装后的带料试切、供应商设备到场后的开箱验收都要录视频作为证据保存。这类视频涉及责任划分画质不能压缩太狠原片归档存档动不动就是1GB起步。第三类是故障排查与维修记录。设备停机后维修人员到现场先把故障现象拍下来再录拆解排障过程。这类视频往往是一次性素材不抓紧录就没了所以现场直接用手机拍原始画质全部保留文件体积完全不受控。这三类视频有一个共同特点拍摄是不可逆的丢了就再也补不回来。所以上传方案的可靠性比上传速度更重要。1.2 算一笔账10分钟视频到底有多大手机录视频的体积问题我直接用实际数据说明。以一台主流手机录制1080p、30帧的视频为例实时码率大约在12Mbps到20Mbps之间。按15Mbps的中间值估算15Mbps等于每秒约1.875MB录制10分钟就是约1125MB。也就是说一部10分钟的手机视频轻松超过1GB。如果设备是4K、60帧录制10分钟文件冲到3GB到5GB都很正常。而PHP默认的upload_max_filesize是2MB很多OA系统实施时即使调整过通常也只放到几十MB。一个1GB的视频和默认上传限制之间差着500倍。这意味着如果方案没有处理好这个视频在厂区普通千兆内网里想通过网页表单直接传上去就是永远的失败现场。1.3 传统单请求上传在面对大文件时的四个硬伤在动手写代码之前我先把自己在传统上传方式上踩过的坑总结一下方便理解为什么非要分片。第一是超时。浏览器和服务器之间的请求有超时时间一个几百MB的文件传输耗时可能超过10分钟一旦超过网关、中间件或者PHP执行时间限制连接就被切断。在文件末尾弹出“上传失败”就是最常见的结果。第二是断线全废。传统表单上传是一次性POST请求文件作为一个整体发送。网络只要闪断一下已经传出去的所有字节全部作废只能从头再来。第三是无进度。浏览器对原生表单上传的进度感知几乎没有用户看到的是一个不确定的等待状态。在工厂车间这种多设备同时运转、内网偶发波动的环境下用户等得心慌就会反复刷新重传把服务器拖得更慢。第四是服务器内存压力。PHP接收大文件时如果配置不当会把整个请求体读入内存。一个1GB的视频几路并发上传服务器直接内存吃满连OA主站都跟着卡死。分片上传的思路本质上是把一个不可中断的大请求拆成很多个可以独立失败、独立重试的小请求。单次请求只要满足服务器限制即可断点续传则把“传了一半”的成本从“归零重来”降为“补齐缺口”。2. 技术选型为什么在OA里坚持用HTMLPHP自己做分片2.1 为什么不直接把upload_max_filesize改到1GB很多人的第一反应是既然文件大把服务器上传限制调大不就完了。这个思路在几十MB的文件上有效在GB级视频上会撞上三堵墙。第一堵墙是Nginx的client_max_body_size默认只有1MB不改的话任何超过1MB的请求直接返回413。它不像PHP参数改完就能生效还需要同时处理反向代理的超时参数proxy_read_timeout、proxy_send_timeout否则长请求会在代理层被掐断。第二堵墙是PHP的执行时间和内存。上传1GB文件时如果PHP把整个请求体缓存到临时文件内存压力还相对可控但max_execution_time如果不够超时就被杀了。更重要的是如果代码里有任何file_get_contents(php://input)之类的操作1GB文件直接读进内存再多的memory_limit都白搭。第三堵墙是用户体验。就算全部参数调通单个1GB请求仍然意味着任何一次网络抖动都会导致全盘重来。我在现场见过太多老师傅看着进度条走到90%然后断线表情从期待变成绝望从此不再信任信息系统。2.2 为什么不依赖OA平台自带的附件组件国内制造业常用的泛微、致远、蓝凌这类OA平台都自带附件上传能力。但这些能力在设计时主要面向文档和图片对超大视频的支持普遍不理想。有的平台附件大小限制是100MB有的大附件上传需要单独购买模块还有的虽然宣称支持大文件实际上走的是商业闭源控件遇到问题只能提工单等售后。更关键的一点是设备管理模块里往往还需要把视频和具体的设备台账、维修工单关联。直接往OA附件上传文件是传到了但业务关联、字段回填、权限控制这些还是得写代码。与其绕一圈走OA的封闭能力不如自己在前端做HTML5分片在后端用PHP收分片、合并文件最后把生成好的文件信息回填给OA表单。这个方案对OA版本完全无侵入迁移成本低逻辑透明可控。2.3 HTML5 File API PHP这套组合的适用边界在2020年以后的浏览器环境里HTML5的File API、Blob.slice()、FormData、XMLHttpRequest这组能力是所有前端方案的基础。不需要安装任何插件不需要ActiveX控件苹果手机Safari和Windows上的Chrome、Edge都能直接跑。后端用PHP是因为OA系统本身的运行环境就是PHP用同一个语言栈部署、维护、排错都不需要引入额外的运行时。分片上传对后端的要求是“能收文件、能写磁盘、能合并”PHP在这些操作上并不吃力。只要做事的粒度合理单分片控制在几MBPHP完全可以轻松承受。这套方案的核心优势是它把超大文件问题的解决逻辑从前端到后端都掌握在自己手里。业务需要断点续传就加一个查询接口需要限制每个人上传总量就在合并时加个配额判断需要把视频转成网页可播放的MP4就在合并完成后调用ffmpeg处理。所有环节都是可插拔的。3. 核心实现前端把视频“切块”传上去的完整过程3.1 基础切片与上传逻辑前端切片的底层能力是Blob.slice()它可以直接从File对象上按字节范围切出一个小Blob。每一片都独立封装成一个FormData请求发送到后端。后端把每个分片当作独立的小文件保存下来最后再把所有分片按序号合并。这里给出一个最简可运行的前端切片逻辑上传部分用XMLHttpRequest是为了能拿到上传进度。const input document.getElementById(videoFile); const file input.files[0]; // 每片大小5MB这个值后面再讲为什么 const CHUNK_SIZE 5 * 1024 * 1024; const totalChunks Math.ceil(file.size / CHUNK_SIZE); let currentChunk 0; function uploadChunk(chunkIndex) { const start chunkIndex * CHUNK_SIZE; const end Math.min(start CHUNK_SIZE, file.size); const blob file.slice(start, end); const formData new FormData(); formData.append(chunk, blob); formData.append(fileName, file.name); formData.append(chunkIndex, chunkIndex); formData.append(chunkTotal, totalChunks); formData.append(fileSize, file.size); const xhr new XMLHttpRequest(); xhr.open(POST, /oa_upload/receive_chunk.php, true); // 监听当前分片的字节级进度 xhr.upload.addEventListener(progress, (e) { if (e.lengthComputable) { const loaded chunkIndex * CHUNK_SIZE e.loaded; const percent Math.round((loaded / file.size) * 100); document.getElementById(progressBar).textContent percent %; } }); xhr.onload function() { if (xhr.status 200) { currentChunk; if (currentChunk totalChunks) { uploadChunk(currentChunk); } else { mergeChunks(file.name, totalChunks); } } else { // 失败则原地重试这里只做一次重试后面会讲改良版 uploadChunk(chunkIndex); } }; xhr.send(formData); } // 全部切片传完之后调合并接口 async function mergeChunks(fileName, chunkTotal) { const formData new FormData(); formData.append(fileName, fileName); formData.append(chunkTotal, chunkTotal); formData.append(fileSize, file.size); await fetch(/oa_upload/merge_chunks.php, { method: POST, body: formData }); }这段代码里最重要的一点是进度计算方式。单次上传的progress事件只针对当前分片如果直接用e.loaded进度条会在每个分片之间反复回跳。整个文件的进度必须用“已上传分片序号 × 分片大小 当前分片内已上传字节”来算这样进度条才是稳定递增的。3.2 断点续传前端如何知道“上次传到哪里了”真正实用的版本必须有断点续传能力。核心问题是刷新页面或者断网之后怎么知道哪些分片已经传过了。我的做法是后端提供一个查询接口。前端在开始上传之前先拿文件名和文件大小去问一下服务器这个文件当前传了哪些分片。然后从缺失的第一个分片开始继续传。后端查询接口的返回数据如下{ code: 0, data: { uploaded: [0, 1, 2, 3], fileExists: false, totalChunks: 12 } }front-end收到这个结果后把currentChunk直接从4开始。前面4个分片不再重复传输后端也省去了重复接收和校验的IO开销。前端的续传判断逻辑async function initUpload(file) { const formData new FormData(); formData.append(fileName, file.name); formData.append(fileSize, file.size); formData.append(action, status); const resp await fetch(/oa_upload/receive_chunk.php, { method: POST, body: formData }); const result await resp.json(); let uploadedSet new Set(result.data.uploaded); if (result.data.fileExists) { // 服务端已经存在完整文件直接跳过全部上传 showMessage(秒传成功); return; } // 从第一个缺失的分片开始传 const startIndex findFirstMissing(uploadedSet, totalChunks); uploadChunk(startIndex); }这里有个细节值得说一说“秒传”。如果后端发现某个文件已经传完且大小一致就直接返回fileExists:true前端弹一句“秒传成功”用户压根不用重复上传。我在实际项目里发现同一台设备同一个SOP视频经常被多个车间重复上传秒传功能能把服务器压力降一大截。3.3 并发控制与失败重试策略上面的基础版是逐片顺序上传逻辑简单但速度偏慢尤其在内网延迟低的场景下等响应的时间白白浪费了。更实际的方案是控制并发数比如同时传3个分片。let concurrency 3; let activeCount 0; let uploadQueue []; function startUploads() { for (let i 0; i concurrency; i) { if (uploadQueue.length 0) { const chunkIndex uploadQueue.shift(); activeCount; uploadChunk(chunkIndex).finally(() { activeCount--; startUploads(); }); } } } function enqueueRemaining() { for (let i 0; i totalChunks; i) { if (!uploadedSet.has(i)) { uploadQueue.push(i); } } startUploads(); }并发数不建议开得太大3到5是合理区间。并发越大服务器同时打开的临时文件越多磁盘IO和内存压力都会上升。制造企业的OA服务器往往还要跑流程审批、报表统计没必要为一个上传任务把整个服务拖垮。失败重试的策略也很重要。我采用“每片最多重试3次失败后记录错误分片全部结束后统一补传”的方式。这样即使某个分片因为网络抖动连续失败也不会影响其他分片的正常传输。4. 后端PHP接收分片、可靠落盘、最终合成完整视频4.1 分片接收接口的完整实现后端接口的第一要务是识别是“查询状态”还是“接收分片”我这里用action参数区分行为。接收分片时先把文件名做安全过滤再按用户维度建立独立的临时目录避免不同员工上传的相同文件名互相干扰。?php // receive_chunk.php // 必须登录才能上传OA的Session在这里可以直接用 session_start(); if (!$_SESSION[user_id]) { http_response_code(401); exit(json_encode([code 401, msg 请先登录])); } $action $_POST[action] ?? upload; // 存放根目录按用户隔离 $userDir /data/oa_upload_tmp/ . md5($_SESSION[user_id] . _ . $_SESSION[user_name]); // 先处理查询请求 if ($action status) { $fileName basename((string)($_POST[fileName] ?? )); $fileSize intval($_POST[fileSize] ?? 0); $totalChunks intval($_POST[chunkTotal] ?? 0); $uploaded []; $fileKey md5($fileName . _ . $fileSize); $fileTmpDir $userDir . / . $fileKey; if (is_dir($fileTmpDir)) { foreach (glob($fileTmpDir . /*.part) as $partFile) { $uploaded[] intval(basename($partFile, .part)); } } // 检查完整文件是否已存在秒传判断 $finalDir /data/oa_videos/ . date(Ymd) . /; $finalFile $finalDir . $fileName; $fileExists file_exists($finalFile) filesize($finalFile) $fileSize; echo json_encode([ code 0, data [ uploaded $uploaded, fileExists $fileExists, totalChunks $totalChunks ] ]); exit; } // 处理分片接收 if ($action upload) { $fileName basename((string)($_POST[fileName] ?? )); $chunkIndex intval($_POST[chunkIndex] ?? -1); $chunkTotal intval($_POST[chunkTotal] ?? 0); $fileSize intval($_POST[fileSize] ?? 0); if ($fileName || $chunkIndex 0 || $chunkTotal 0) { http_response_code(400); exit(json_encode([code 400, msg 参数不合法])); } // 分片大小必须符合预期拒绝超限数据 if (!isset($_FILES[chunk]) || $_FILES[chunk][error] ! UPLOAD_ERR_OK) { http_response_code(400); exit(json_encode([code 400, msg 分片未正常接收])); } // 校验扩展名白名单 $ext strtolower(pathinfo($fileName, PATHINFO_EXTENSION)); $allowExt [mp4, mov, avi, mkv, wmv, flv, webm]; if (!in_array($ext, $allowExt)) { http_response_code(403); exit(json_encode([code 403, msg 文件类型不允许])); } // 落盘到用户隔离目录下的文件专属目录 $fileKey md5($fileName . _ . $fileSize); $fileTmpDir $userDir . / . $fileKey; if (!is_dir($fileTmpDir)) { mkdir($fileTmpDir, 0750, true); } $partFile $fileTmpDir . / . $chunkIndex . .part; // 如果这个分片已经传过直接返回成功保证幂等性 if (file_exists($partFile)) { echo json_encode([code 0, msg 分片已存在]); exit; } if (!move_uploaded_file($_FILES[chunk][tmp_name], $partFile)) { http_response_code(500); exit(json_encode([code 500, msg 分片写入失败])); } echo json_encode([code 0, msg ok]); exit; }这段代码里有几个细节需要解释一下。幂等处理是分片上传的重中之重。网络超时可能导致前端重发同一分片如果后端对重复分片不做处理合并时同一分片出现两次文件就毁了。所以在接收分片前先判断文件是否存在存在就直接返回成功。临时目录按用户和文件双重隔离是为了解决两个实际问题一是多人上传同名文件时互相覆盖二是如果某个用户的临时文件写权限失控其他用户的目录不被波及。目录层级是“用户名MD5/文件MD5/序号.part”虽然目录层级多了一点但对于GB级视频的可靠性保障是有价值的。4.2 分片合并接口的实现与优化当所有分片都传完之后前端调用合并接口。合并的过程是按顺序读取所有.part文件追加写入最终视频文件。?php // merge_chunks.php session_start(); if (!$_SESSION[user_id]) { http_response_code(401); exit(json_encode([code 401, msg 请先登录])); } $fileName basename((string)($_POST[fileName] ?? )); $fileSize intval($_POST[fileSize] ?? 0); $chunkTotal intval($_POST[chunkTotal] ?? 0); if ($fileName || $chunkTotal 0) { http_response_code(400); exit(json_encode([code 400, msg 参数不合法])); } $userDir /data/oa_upload_tmp/ . md5($_SESSION[user_id] . _ . $_SESSION[user_name]); $fileKey md5($fileName . _ . $fileSize); $fileTmpDir $userDir . / . $fileKey; if (!is_dir($fileTmpDir)) { http_response_code(400); exit(json_encode([code 400, msg 没有找到对应的临时分片目录])); } $finalDir /data/oa_videos/ . date(Ymd) . /; if (!is_dir($finalDir)) { mkdir($finalDir, 0750, true); } $finalFile $finalDir . $fileName; // 用追加二进制模式写入避免一次性读入大文件 $fp fopen($finalFile, ab); if (!$fp) { http_response_code(500); exit(json_encode([code 500, msg 无法创建最终文件])); } for ($i 0; $i $chunkTotal; $i) { $partFile $fileTmpDir . / . $i . .part; if (!file_exists($partFile)) { fclose($fp); unlink($finalFile); http_response_code(400); exit(json_encode([code 400, msg 缺少分片 . $i])); } // stream_copy_to_stream比file_get_contents更省内存 $src fopen($partFile, rb); if (!$src) { fclose($fp); unlink($finalFile); http_response_code(500); exit(json_encode([code 500, msg 分片读取失败 . $i])); } stream_copy_to_stream($src, $fp); fclose($src); } fclose($fp); // 校验最终文件大小 if (filesize($finalFile) ! $fileSize) { unlink($finalFile); http_response_code(500); exit(json_encode([code 500, msg 文件大小校验失败])); } // 清理临时目录 $parts glob($fileTmpDir . /*.part); if ($parts) { foreach ($parts as $p) { unlink($p); } } rmdir($fileTmpDir); echo json_encode([code 0, msg 合并成功, filePath $finalFile]);合并这段有两个容易翻车的地方。第一是内存。有些教程会让用file_get_contents读完整分片再fwrite5MB分片时问题不大但如果有人把分片大小调到50MB甚至更高并发的内存占用就会失控。稳妥的做法就是用fopen加stream_copy_to_stream它走的是流式拷贝不会把整个分片一次性拉进内存。第二是中途失败时的清理。合并过程中如果发现缺少分片必须把已经写了一半的最终文件删掉否则留一个半个视频在正式目录里前端播放器一打开就是坏的用户还以为是系统做的视频有问题。4.3 需要调整的PHP与服务器参数清单下面这份参数配置是这个方案里最容易踩坑的部分实测下来这样设置最稳妥。配置项建议值说明upload_max_filesize10M单个分片不超过5MB10M留出余量post_max_size15M必须大于upload_max_filesize且要考虑FormData里其他参数max_execution_time300合并大文件时给足时间但也不要无限大max_input_time300接收分片请求时的输入时间限制memory_limit256M正常情况下够用主要是防止恶意大请求client_max_body_sizeNginx15m必须与post_max_size匹配否则直接413proxy_read_timeout / proxy_send_timeoutNginx300如果PHP在代理后面必须调大否则代理层掐断连接很多人在这一步犯的错是把upload_max_filesize直接调到2G。分片方案的核心思想就是单请求只需要承载一个分片完全不需要给PHP开这么大的单请求上传上限。开太大反而增加内存和超时风险得不偿失。5. 实操中容易踩的坑分片大小、文件名、并发与OA集成5.1 分片大小到底选多大比较合适分片大小是个需要平衡的决策。我之前试过1MB和20MB两种极端值各有各的问题。1MB分片优点是单请求轻量失败重传成本低服务器内存压力小。缺点是要传输的分片数量太多一个1GB文件要切1024片每片都要建立TCP连接、发起POST请求、等待响应总耗时明显偏长而且分片元数据的管理开销会上升。20MB分片优点是请求次数少传输效率高。缺点是单请求已经触碰Nginx和PHP的配置边界而且一旦某个分片传了一半断线重传20MB的成本相当高。在千兆内网的车间环境里我最后选了5MB。这个值单请求不会撞上Nginx默认的1MB限制所以要调也不需要把PHP参数改得很夸张同时切出来的分片数量适中一个300MB的视频只需要60片10分钟级别的大视频也就200多片请求量完全可接受。如果你所在的场景是公网传输带宽有限建议把分片降到2MB。公网丢包率更高小分片的重传成本更低整体成功率会明显改善。5.2 中文文件名、空格与特殊字符的处理设备操作视频的文件名通常长这样“龙门加工中心换刀操作流程2025-03-01.mp4”。中文、括号、空格、横线全都齐了。这种文件名直接传给后端会出现两个问题。第一是路径问题。PHP的basename函数能过滤掉目录穿越字符但不同系统的文件系统对特殊字符的支持不同Windows后端和Linux后端的处理结果可能有差异。我在Linux服务器上就遇到过文件名末尾带空格导致后续合并时找不到文件的情况。我的处理流程是前端在上传时额外传一个加密后的文件标识字段后端在临时目录和最终目录都使用这个标识作为存储文件名只有最终写入OA附件字段时才还原原始文件名。// 前端生成文件标识 const fileId md5(file.name file.size Date.now()); formData.append(fileId, fileId);后端落盘全部用fileId彻底避开原始文件名的干扰。展示给用户看的时候从数据库里读原始文件名显示两套逻辑各管各的互不干扰。5.3 并发上传时的服务器IO压力控制有一段真实的现场经历。车间里同时有十几位老师傅上传视频每个人并发3个分片瞬间就有几十个写入请求打到服务器磁盘。机械硬盘在这种压力下随机写入性能会直线下降上传速度反而比顺序传还慢。后来我做了两个改进。第一是前端把并发数从3降为2显著缓解了IO压力。第二是后端做了分片目录的批量写入优化先用array_multisort按文件名排序再处理减少目录跳转。虽然单个分片5MB不算大但高并发时磁盘寻道时间累计起来的影响还是很明显的。如果企业预算允许把上传临时目录放到SSD上是最直接有效的方案。OA服务器哪怕只分一个100GB的SSD分区给临时上传目录体验也会好很多。5.4 上传完成之后怎么和OA业务流程打通分片上传完成只是前半段后半段是把视频挂到OA的业务单据上。制造企业常见的需求是把视频关联到设备台账、维修工单或培训计划里。我的做法是在OA的审批表单里放一个自定义按钮调用自己开发的附件上传页面。视频传完、合并完成后PHP把文件路径写入OA的数据库表或者通过OA提供的接口把附件记录关联到当前单据的单据号、流程ID。这样文件本体存在独立存储目录业务数据在OA里照常流转。这个方案的优点是文件存储独立不会因为OA的升级、迁移而丢失视频文件。缺点是OA管理员需要有一定的数据库操作能力毕竟往业务表里插入数据要根据具体OA的数据结构来调整。但相比把视频塞进OA的附件插件这种解耦方式在后续扩容时明显更灵活。6. 常见问题与排查技巧实录6.1 问题速查表现象直接原因排查与解决上传请求返回413Nginx的client_max_body_size过小检查Nginx配置确保大于单分片大小重启Nginx上传请求报500日志显示POST Content-Length超过限制PHP的post_max_size过小调整post_max_size必须大于upload_max_filesize页面白屏或请求超时PHP的max_execution_time过短合并操作加长执行时间建议300秒断网刷新后进度条归零前端没有查询已传分片列表实现status查询接口跳过已传分片合并后文件播放一半就断分片顺序错乱或分片缺失检查合并逻辑是否按序号严格顺序写入检查临时目录是否被清理重复上传秒传不生效后端fileExists判断的目录不对确认finalDir和finalFile路径一致比较filesize是否等于fileSize中文文件名乱码前端UTF-8和后端编码不一致统一全链路UTF-8建议显式header设置上传进度条走不到100%并发下进度计算错误用已传分片数加当前分片内字节计算总进度临时目录堆积大量.part文件上传中断后没有清理机制加cron定时任务清理超过24小时的临时目录6.2 最棘手的一次故障合并后视频文件损坏项目上线后遇到一次诡异的故障用户反馈上传的视频文件大小完全正确但播放到第4分钟左右画面变黑、声音消失拖动进度条还会卡死。刚开始怀疑是分片合并顺序错乱检查代码后发现循环变量没有问题。最后定位到问题出在分片接收时的并发写入。两个并发请求可能同时处理同一个分片序号导致.part文件被覆盖写入了一半新内容一半旧内容文件大小没变但内部数据损坏了。这次故障让我完善了两处设计一是分片接收接口加文件锁同一分片同一时间只能有一个写入操作二是在合并完成后追加了一遍完整性校验——不仅是文件总大小还要抽样比对几个分片的MD5值。虽然不能100%保证视频可播但至少能拦截明显的数据错乱。6.3 上传中断后的“灵异事件”另一个常见现象是用户传完所有分片之后调用合并接口时报错“缺少分片”。排查发现临时目录里确实少了一个分片但前端明明显示所有分片都传完了。原因出在断网前最后一个分片的HTTP响应没有到达前端。前端认为上传失败重试时又因为网络瞬断导致重试请求也没发出去但用户在界面上看到的是进度条已经走满。所以严格来说前端不能以“所有分片都调用过上传”作为完成标准必须以“后端status接口返回的已传分片数等于总分片数”为准。我把这个逻辑加进了前端所有分片上传完成后不直接调merge先查一次status确认uploaded数组的长度等于totalChunks再发起合并请求。从此再没有出现过这个灵异问题。6.4 商业OA平台偶发连接失败错误码的处理思路用商业OA平台的企业还会遇到一类问题附件模块在服务端压力大或磁盘读写慢时返回类似“访问失败”的错误码文件传一半被强制断开。这类问题的本质往往不是OA代码有bug而是超长的大文件请求占用了平台内置组件的缓冲区或执行线程。我的处理思路是把大视频和OA常规附件分离。视频走独立的HTMLPHP分片上传通道上传完成后只在OA表单里保留文件链接和元数据。OA常规附件继续使用平台自带能力。这样绕开了商业平台对单请求大小的限制也避免了长连接占用平台进程资源。7. 安全与合规提示文件上传功能最容易忽视的边界7.1 不能只信前端传的MIME和扩展名视频上传功能的本质是一个文件写入接口如果不做安全校验恶意用户可以用它上传PHP脚本、WebShell或者超大垃圾文件。我在接收分片代码里做了三层校验登录态校验、扩展名白名单校验、分片大小校验。但扩展名白名单只是第一道防线。攻击者完全可以把一个PHP脚本改名为.mp4上传。所以最终视频存储目录必须禁止执行脚本最简单的做法是在Nginx配置里对该目录禁用PHP解析location /data/oa_videos/ { location ~ \.php$ { return 403; } }配合PHP侧的finfo_file函数读取真实文件类型如果返回的不是video/*开头的类型拒绝合并。这样即使上传了伪装文件也无法在服务器上造成破坏。7.2 临时目录的定期清理与权限收敛分片上传中断是常态每个中断的上传任务都会在临时目录里留下几十个.part文件。如果不管磁盘迟早被填满。我做了两个机制。第一是文件权限收敛。上传临时目录和最终视频目录都设置为独立的系统用户只给750权限不授予PHP进程所在组的写权限之外的其他权限。这样即使某个目录被攻击者利用影响范围也被限制在单一目录内。第二是cron定时清理。每天凌晨3点扫描上传临时目录删除修改时间超过24小时的目录0 3 * * * find /data/oa_upload_tmp -type f -name *.part -mtime 1 -delete find /data/oa_upload_tmp -type d -empty -delete这个脚本保证临时垃圾不过夜同时不会误删还在进行中的上传任务。上传时间超过24小时的任务本身也该放弃重来了。7.3 关于元数据记录方式的一个安全建议在设计分片清单和元数据记录时我坚持一个原则不引入XML格式。业界不只一次出现过OA周边组件在处理用户上传的XML文件时因实体展开问题导致的安全风险。分片上传这类功能本身就涉及大量用户可控的文件名、参数、请求体攻击面已经不小没必要再平添一个解析入口。我的元数据记录方式非常简单分片序号用目录文件名体现文件大小、完整状态用数据库行记录前端续传状态用localStorage加后端查询接口配合。不解析任何用户可控的标记语言把攻击面降到最低。做文件上传功能越朴素越安全。最后再分享一个我个人的建议。分片续传方案上线后一定要做一轮真实弱网和断网测试。找一台机器故意用网络限速软件制造丢包或者直接拔网线模拟断连反复测试十几个大文件上传。分片上传大部分代码路径在正常网络下是永远走不到的只有异常情况下才会触发重试、恢复、合并失败这些分支。把这些分支跑通系统才算真正能交付到车间老师傅手里。
