做医疗信息化这些年,最容易被业务部门反复投诉的往往不是HIS系统开单慢,反倒是影像科、病理科上传大文件这个看似不起眼的功能。上个月还有位主任跟我诉苦:1.2GB的病理切片扫描文件,断断续续传了一下午,最后一跳网络抖动直接超时,整个文件作废,患者家属那边催得又紧,场面一度很难看。这种场景在医疗系统里太典型了——PACS影像、病理切片、电子病历附件,随便一个都是几百MB甚至上GB的量级,而内网环境虽然带宽不错,却扛不住传输中断后从头再来的代价。今天专门聊聊我们当时怎么用WebUploader把"局域网大文件断点续传"这件事做扎实的,包括分片策略、断点续传原理、服务端合并方案,以及那些文档里不会写但实战中一定会踩的坑。希望这篇文章能给正在做医疗系统、或者任何内网大文件上传场景的同行一些可落地的参考。无论是刚接手这类需求的新人,还是已经被"大文件上传"折磨过几轮的开发,都能从这里找到对应的解决思路和可直接抄的配置。
1. 项目背景与核心需求拆解
1.1 医疗系统为什么对大文件上传如此头疼
医疗行业的大文件场景比普通OA系统要极端得多。普通办公系统传个几十MB的附件已经算大,但在医院里,一张MR(核磁共振)序列动辄几百MB,一台高端病理切片扫描仪单张切片图可以到2GB以上,内镜中心的高清视频录播更是轻松突破5GB。这些文件不仅体积大,而且具有强时效性——会诊医生等着看片子、病理报告等着审核签字,传输链路一旦断了,影响的不是一个人的工作,而是一条完整的诊疗链路。
更要命的是医疗系统的部署环境普遍保守。很多三甲医院的内网虽然已经上了万兆骨干,但终端到接入层的链路往往还是千兆甚至百兆,加上部分楼层交换机老旧、双绞线老化、终端网卡驱动版本混乱,实际传输速率远达不到理论值。大文件在这种链路里传输,时间窗口拉长,被交换机缓冲、终端休眠、网络策略扫描等各类因素打断的概率就指数级上升。传统的单次HTTP上传在这种场景下几乎没有生存空间,你必须从架构层面把人家的"全量传输"改造成"分片+续传"模式。
1.2 选型WebUploader而不是自研轮子的原因
这个决定我们内部确实吵过一轮。有同事建议直接用原生XMLHttpRequest封装分片逻辑,也有同事提出上OSS SDK直传,但讨论后还是定了WebUploader。原因有几个:第一,WebUploader的浏览器兼容性覆盖Windows 7 + IE11这类医疗终端标配组合,这是原生XMLHttpRequest Level 2没法完全保证的;第二,它自带分片、并发、进度回调、文件MD5计算等一整套成熟机制,我们不需要从零手写一个大文件上传内核,把精力集中在服务端和业务适配层;第三,它是百度开源的项目,社区用得多,踩坑资料相对好找,后续接手维护的人也容易上手。
并不是说它完美,WebUploader的代码已经不怎么更新了,维护状态基本属于"能用但别指望官方迭代"。但体检做下来,发现它因为简单、稳定、可控,反而比那些"全家桶式"的上传组件更适合医院这种追求稳定压倒一切的环境。我们选择的是"WebUploader做前端交互和分片控制,后端用Spring Boot接收分片并自行完成合并和幂等校验"的组合方式,这样既有现成轮子的效率,关键逻辑又全部掌握在自己手里。如果你所在的项目对前端包体积不敏感、对浏览器兼容性有硬性要求、并且不想引入一套重型的云存储SDK,这个思路值得参考。
1.3 局域网场景的隐性约束条件
很多人做局域网大文件上传时,默认网络是可靠的,这是最大的误区。局域网不是一根网线直连,中间有交换机、防火墙、行为管理网关,有些医院还串联了网闸和审计设备。这些设备会做流量整形、连接数限制、超时清理,TCP长连接在空闲后被中间设备切断是家常便饭。断点续传当然能守住"分片级"的进度,但如果你并发开得太大、分片太小、请求频率过高,同样会触发交换机的端口限速,反而比单线程更慢。
所以做局域网方案,除了功能层面要解决"断了能续",还必须回答"怎么传输才稳定"。这就涉及到分片大小、并发数、重试策略等一连串参数的配合,后面我会专门展开讲。另外医疗系统还有个特殊点:HIS/LIS/PACS各子系统之间经常共享同一台应用服务器,上传高峰恰好也是业务高峰,如果你不做并发控制,大文件上传分分钟把应用的线程池打满,影响正常业务,这是临床科室绝对不能接受的。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. WebUploader断点续传的工作原理
2.1 分片上传的核心机制拆解
WebUploader的断点续传在实现上并不神秘,核心就是把大文件切成若干个独立的分片,每个分片作为独立的HTTP请求上传到服务端。前端一个很关键的配置是这样的:
javascript复制var uploader = WebUploader.create({
swf: 'js/Uploader.swf',
server: '/api/upload/chunk',
pick: '#picker',
accept: {
title: '影像文件',
extensions: 'dcm,jpg,png,pdf',
mimeTypes: '*.dcm,*.jpg,*.png,*.pdf'
},
auto: false,
chunked: true,
chunkSize: 2 * 1024 * 1024, // 每个分片 2MB
threads: 3, // 同时上传 3 个分片
fileVal: 'file',
formData: {
token: getToken()
}
});
这里chunked: true开启分片模式,chunkSize决定每个分片的大小,threads控制并发数。文件被切割后,每个分片会带上该分片的序号以及文件的总分片数,服务端收到后将分片暂存在临时目录,等全部分片到齐后再触发合并。这里有个容易被忽视的逻辑:一旦一个文件被分片上传,服务端必须按序保存分片的元信息(文件唯一标识、分片序号、总分片数、文件原始名称),否则合并阶段无法还原文件。
WebUploader用于标识文件唯一性的默认策略是基于文件的名称、大小、最后修改时间计算出一个MD5值,也可以在fileQueued事件里用md5方法计算整个文件的MD5,但要注意大文件全量MD5计算会消耗较长时间,并且会阻塞浏览器内存。我们实际采用的是"文件大小+文件名+修改时间"生成唯一key,再对每个分片内容做MD5校验,这样能在可靠性和性能之间取得平衡。如果你的业务对数据完整性有严格要求,比如医疗影像必须保证逐字节一致,那全文件MD5还是不能省的,但可以放到上传完成后的后台校验异步去算,不要阻塞上传流程。
2.2 "断点续传"到底续的是什么
断点续传的核心逻辑一句话就能说明白:上传中断后再次选择相同文件时,前端要知道哪些分片已经上传成功,只上传剩余的分片。WebUploader通过两种方式记住状态:一是浏览器的localStorage,二是服务端的分片状态查询接口。
localStorage方案下,WebUploader会把已上传分片的序号列表持久化到本地,下次选择同一文件时,它在fileQueued阶段通过uploader.md5File或自定义key比对后,自动跳过已完成分片。但这里有个坑:localStorage是按浏览器隔离的,如果用户换了台电脑或者清了浏览器缓存,这个状态就丢了。所以在医院内网环境下,更稳妥的方式是服务端接管状态——文件分片是否齐了不是前端说了算,而是后端通过查询临时目录或数据库记录来判断。前端在文件入队后调用一个/api/upload/status接口,传入唯一key,后端返回已存在分片的序号数组,前端据此过滤出真正需要上传的分片。
我建议两种方式结合使用。localStorage负责前端快速跳过,服务端接口负责最终一致性兜底。因为即便前端本地缓存说"所有分片都传完了",假如服务端某个分片写入时恰好断电损坏,前端不复查就会漏传,合并时自然报错。让服务端作为权威数据源,前端只是建议方,这个思维在整个文件上传系统中非常重要,别把状态的信任全押在浏览器端。
2.3 秒传机制与MD5校验的取舍
说到秒传,很多医疗项目的需求都描述得很模糊:"相同的文件能不能不要重复传?"实际上秒传的本质是服务端已经存在同一内容的文件,此时不需要真正上传数据,服务端直接返回一个"已存在"的标识,前端展示100%完成即可。实现的关键在于服务端要对文件内容生成唯一指纹,最常用的就是MD5。
WebUploader前端可以用uploader.md5File(file, 0, 10 * 1024 * 1024)做部分文件的MD5快速计算,也可以全文件计算。我们医疗影像场景对准确性要求极高,所以最终选的是全文件MD5,但配合了"先查询、后上传"的流程。具体做法是:文件入队后先调/api/upload/check接口,携带文件MD5,后端在文件索引表里查;如果命中了相同MD5且文件在存储中存在,就直接标记秒传完成,不再走分片上传流程。这个机制处理影像科反复上传同一批患者的CT/MRI文件时效果立竿见影,节省的带宽和存储空间相当可观。
不过MD5计算对大文件来说非常耗时,一个1GB文件的MD5计算在本机上也要数秒到十几秒。前端在算MD5期间,会让用户觉得"卡在选择文件后的等待期",体验不好。我们的优化方案是:上传前先算MD5,同时把计算过程做成异步任务,界面上提示"正在校验文件信息,请稍候",而不是让用户干瞪眼。另外MD5只在"文件完全一致"这个粒度上有效,两个文件MD5相同但内容不同的概率极低,医疗场景下如果还不放心,可以在合并完成后另行做一次分片级CRC校验,这样基本能做到万无一失。
2.4 为什么需要"断点续传"而不是"失败重传"
这个问题听起来像是废话,但真有人来问过我——既然分片粒度已经这么细了,中断后重新上传整个过程不也就几十个分片的事?为什么还要费劲搞续传和状态记录?答案在于医疗内网的实际网速并不理想,很多终端到服务器的传输速度只有2MB/s到5MB/s。如果传输过程在98%的位置断了,剩下2%虽然只有几十MB,但重新上传整个文件意味着把之前已经烧掉的十几分钟全部作废。更麻烦的是,如果终端设备是低配工控机,反复全量上传会导致磁盘IO和内存持续高负载,影响现场其他业务系统的运行。
有了断点续传之后,即使中断,恢复后只需要补传剩余分片,成本被压缩到最低。这在操作层面的意义是:医生不需要在中断后重新选择文件、重新等待校验,甚至不需要知道刚才发生过一次网络闪断,系统自动把断点续上了。对于医院的用户体验来说,这种"无感容错"比任何人工操作指导都有效。我们要做的就是在前端把重试逻辑做完善,例如分片失败自动重试3次,间隔递增,避免一次网络抖动直接中断整个上传任务。
3. 局域网环境下的性能调优实践
3.1 分片大小与并发数的黄金组合
很多开发一上来就照着网上教程抄,chunkSize: 2 * 1024 * 1024、threads: 3,但到了局域网场景,这个配比不一定最优。分片大小和并发数之间是跷跷板关系:分片太大,单次上传耗时长,中途失败重传的代价高;分片太小,HTTP请求次数暴涨,服务端要处理的请求头、TCP建连、MD5校验等开销反而拖累整体效率。我们最终是通过实测定的参数:在千兆内网环境下,2MB分片配合3并发是性价比比较稳的组合;在百兆或老旧交换机环境下,反而建议把分片调到4MB到8MB,并发降到2,因为链路是瓶颈,请求频率太高容易触发交换机的连接数限制。
具体来说有几点参考。第一,局域网延迟通常在1ms到5ms之间,网络不再是大头,服务端磁盘IO和Tomcat线程池才是瓶颈,并发数太高会导致Tomcat线程被上传请求占满,拖垮其他业务接口。第二,分片大小会影响服务端临时文件的写入方式,4MB以上的分片在机械硬盘RAID阵列上连续写入性能更好,如果是SSD则2MB反而更稳。第三,前端用threads: 3时,浏览器的并发连接数会多占用3个TCP连接,对普通应用服务器影响不大,但如果同一时间有多台终端同时上传,服务端就需要加连接池限制和队列控制,避免瞬间压垮I/O。
3.2 局域网丢包和延迟对上传的影响
局域网虽然不像公网那样有严重丢包,但医院网络环境远比你想象得复杂。我们排查过一次"上传大文件必断流"的故障,最后定位到是一台楼层交换机开启了广播风暴抑制,把持续的大流量包误伤丢弃了一部分。TCP虽然会自动重传,但如果重传超时设置不当、应用层没有反馈,HTTP连接就会在长时间无响应的假象中挂断。
针对这类问题,除了分片机制兜底,还有两个实用技巧。一是WebUploader的上传请求里加上超时和重试机制,例如timeout: 30 * 1000,超过30秒无响应就重发当前分片。二是服务端必须要做"幂等接收",同一个分片被客户端重试多次提交时,服务端不能重复写文件或报错,而是直接返回成功。我们是在接口里先判断分片是否已存在且大小一致,存在就直接返回成功,这样即使客户端疯狂重试也不会把服务端搞出脏数据。
3.3 内存与磁盘开销的边界控制
大型文件分片上传时,浏览器的内存占用和服务器临时目录的磁盘占用是两块隐形压力源。前端方面,WebUploader默认会把文件对象读入内存进行操作,如果分片文件是2MB,3个并发就是6MB内存,看起来不多,但加上页面其他模块和MD5计算时的ArrayBuffer,一个标签页的峰值内存可能达到几百MB甚至更多。在医疗终端普遍只有4GB到8GB内存的存量设备上,这个数值必须被控制住。
我们后来做了两处优化:一是在分片上传前释放不再使用的分片Raw Data,二是关闭multiple: true,让医生一次只能选一个文件,避免多文件同时上传把浏览器拖死。服务端的临时分片目录同样需要做容量预警,比如限定单用户临时空间上限为10GB,超过则拒绝新的分片写入。清理策略用定时任务扫描超过48小时未完成的分片直接删除,不然每天几GB的临时分片累积下来,服务器的磁盘空间很快会报警。
3.4 存储介质与目录规划的讲究
医疗系统里服务器后端存储通常不是普通磁盘,而是RAID阵列或SAN存储,大文件上传的写入路径会影响阵列的I/O性能。我们在实际项目中为分片临时目录和合并后的正式目录做了隔离:临时目录放在独立的快速存储卷上,正式目录放在归档存储卷上。因为分片写入是高频的随机写,而正式文件是低频的顺序写,两种负载特性不同,混在一起会让RAID缓存策略无所适从。
目录规划也很重要,别把所有患者都塞进一个目录。我们按照"检查日期/检查号/MD5前两位"建立三级目录,例如/data/archive/20240512/CS12345678/ab/xxx.dcm,这样既方便管理,又能避免单个目录下文件数量过多导致文件系统索引变慢。如果后续要接入对象存储(很多同行问我MinIO能不能断点续传,MinIO本身支持分片上传,但需要前端配合做Presigned URL分片直传,和WebUploader这套方案是两种路线),目录设计也要为将来搬迁留好接口,至少在文件元数据里保留原始路径信息。
4. 服务端设计与实现的关键细节
4.1 分片接收接口的幂等设计
WebUploader前端发出的每个分片请求都会带上参数,包括文件名name、分片序号chunk、总分片数chunks、文件唯一标识guid等。我们定义的分片接收接口如下:
java复制@PostMapping("/api/upload/chunk")
public Result uploadChunk(@RequestParam("file") MultipartFile file,
@RequestParam("chunk") Integer chunk,
@RequestParam("chunks") Integer chunks,
@RequestParam("name") String name,
@RequestParam("guid") String guid) {
// 1. 参数校验
if (file.isEmpty() || chunk == null || chunks == null) {
return Result.error("参数不合法");
}
// 2. 构造分片临时目录
String chunkDir = getChunkDir(guid);
File dir = new File(chunkDir);
if (!dir.exists()) {
dir.mkdirs();
}
// 3. 保存分片文件,分片序号作为文件名
File chunkFile = new File(chunkDir, chunk + ".part");
if (chunkFile.exists() && chunkFile.length() == file.getSize()) {
return Result.success("分片已存在");
}
file.transferTo(chunkFile);
return Result.success("分片上传成功");
}
注意到代码里有一个"分片已存在"的判断:如果目标分片文件已经存在且大小一致,直接返回成功。这是实现断点续传和重试机制的基础。网络闪断后浏览器自动重发当前分片时,服务端就不会因为写入了同一个分片而报错,也不会因为重复写入造成文件错乱。这里有个细节:file.transferTo在Spring Boot里默认会先将MultipartFile转存到临时目录再复制,大内存配置下也可能走内存,因此在application.yml里要合理配置spring.servlet.multipart.max-file-size和max-request-size,否则一旦分片大小超过默认1MB,请求会直接414或500,初学者经常在这个环节摔跟头。
4.2 全部分片到达后如何安全合并
分片合并的核心是"按序将临时分片文件写入最终文件",而不是单纯把分片目录下的文件通过拼接工具合在一起。我们常用的实现方式有RandomAccessFile和顺序流式写入两种。RandomAccessFile支持随机写入,理论上可以并发写,但并发写对分片顺序和文件指针管理要求很高,一旦出现分片乱序、指针重叠,文件就废了。所以我们选择更稳妥的"按序号从小到大顺序流式写入",代码如下:
java复制@PostMapping("/api/upload/merge")
public Result merge(@RequestParam("guid") String guid,
@RequestParam("name") String name,
@RequestParam("md5") String md5) {
String chunkDir = getChunkDir(guid);
File dest = new File(getFinalDir(guid, name));
try (FileOutputStream fos = new FileOutputStream(dest);
BufferedOutputStream bos = new BufferedOutputStream(fos)) {
File dir = new File(chunkDir);
File[] partFiles = dir.listFiles((d, fileName) -> fileName.endsWith(".part"));
// 按分片序号排序
Arrays.sort(partFiles, (f1, f2) -> {
int i1 = Integer.parseInt(f1.getName().split("\\.")[0]);
int i2 = Integer.parseInt(f2.getName().split("\\.")[0]);
return Integer.compare(i1, i2);
});
for (File part : partFiles) {
try (FileInputStream fis = new FileInputStream(part);
BufferedInputStream bis = new BufferedInputStream(fis)) {
byte[] buffer = new byte[8192];
int len;
while ((len = bis.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
}
}
}
// 合并完成后清理临时分片目录
deleteDir(new File(chunkDir));
return Result.success("文件合并成功");
}
这段代码在局域网场景下有几个注意点。一是排序要一致,前端按0开始计数,后端排序也按0开始,一旦起始序号不一致,合并出来的文件会整体错位一个分片,这种错误很隐蔽,可能需要拿到终端上打开文件才能发现。二是合并过程要加事务性控制,合并前先写一个.merging标记文件,合并完成后删除,整个文件处于"合并中"状态时如果服务重启,启动清理任务时要优先处理残留的.merging文件,避免留下半截文件被业务系统误用。三是合并过程中磁盘IO会突然增加,尤其是多个大文件同时合并时,建议用信号量控制合并操作的并发数,例如限制同时最多合并2个文件。
4.3 秒传、去重与MD5索引表
前面提到秒传需要后端有MD5索引表。我们在数据库里建了file_repository表,字段包括id、md5、file_name、file_size、storage_path、upload_time、hospital_id等。当用户选择文件后,前端先调用/api/upload/check?md5=xxx接口,后端先查索引表,如果MD5存在,直接返回"秒传成功";如果不存在,才进入正常的分片上传流程。合并完成后,再插入一条新的file_repository记录。
但秒传有个前置条件:MD5需要前端先计算出来。WebUploader自带的MD5计算模块在计算时是同步的,文件稍微大一点就会卡界面。我们采用的方案是把MD5计算放到一个Web Worker线程里执行,计算完成后再把结果回传主线程。在IE11时代,Web Worker的支持程度有限,所以我们做了一个降级策略:如果浏览器不支持Worker,就改用分片抽样MD5(取文件头部1MB、中间1MB、尾部1MB拼接后计算MD5),虽然不能保证100%精确,但对临床场景来说,误判概率已经低到可以接受,并且上传后会做最终的全文件MD5校验兜底。
这里要特别说一句:秒传不能只依赖前端计算的MD5。恶意用户完全可以改掉前端参数把任意文件的MD5传上来,让后端以为文件已存在。我们的后端在“秒传成功”之前还会对存储路径里的文件做一次大小和头部字节抽查,如果连大小都对不上,直接拒绝秒传并提示重新上传。医疗数据不允许有任何侥幸。
4.4 异常清理与临时目录的生命周期管理
服务端临时目录是最容易被忽视的生命周期对象。分片上传期间,每个文件都会有一个独立的临时目录,一旦上传中断、用户关闭浏览器、或者网络长时间断开,服务端的临时目录和分片文件会一直留在磁盘上。如果不做清理,日积月累会有几十GB的垃圾文件,甚至拖垮服务器的inode资源。
我们的定时清理策略分两级。一是"超时未完成清理":每个上传任务在创建临时目录时会在Redis或数据库里记录一条upload_task记录,包含guid、createTime、lastUpdateTime。定时任务每10分钟扫描一次,发现lastUpdateTime超过24小时未更新的任务,直接删除整个临时目录。二是"合并成功清理":合并完成后立即删除临时目录,不等到定时任务去处理。这两级配合,基本不会出现磁盘被垃圾分片塞满的情况。另外,服务端重启时要处理残留的临时目录,别让重启后的新文件和旧文件的guid撞上,所以guid的生成一定要加上UUID,而不是单纯用文件名加时间戳拼。
5. 踩坑实录与高频问题排查
5.1 医疗环境下最常见的六个问题
这部分内容很多是从医院现场运维总结出来的,每一个都对应过真实的科室投诉。
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 选择文件后一直处于"等待"状态,进度条不动 | 文件MD5计算阻塞了主线程,或check接口超时 |
异步计算MD5,给check接口设置独立超时时间 |
| 上传到一半提示"分片上传失败"且自动重试无效 | 服务器最大上传大小没配置,或分片大小超过Tomcat限制 | 调整spring.servlet.multipart.max-file-size |
| 断点续传失效,刷新页面后必须从头传 | localStorage被清理或浏览器兼容模式问题 | 前端状态只做辅助,后端以/status接口为准 |
| 百兆内网下多终端同时上传,全院网络卡顿 | 并发数和分片大小设置不合理 | 分片调大至4MB,并发调低至1~2 |
| IE11下WebUploader的Flash上传方式报错 | 浏览器安全设置阻挡了Flash | 设置受信任站点,启用ActiveX和Flash权限 |
| 合并完成后的文件损坏,无法打开 | 分片排序错位或漏掉某个分片 | 检查排序起始序号,合并前校验分片数量和大小 |
| 上传间隙服务器磁盘突然爆满 | 临时分片目录未及时清理 | 开启定时任务,合并成功后立即删除临时目录 |
以上问题里面,IE11下的Flash问题是医疗行业特有的老大难。医院内网为了兼容各种老的业务系统,浏览器版本常年固定在IE,而WebUploader在IE下依赖Flash来支持分片上传。但很多医院终端安全策略默认禁用了Flash,或者没有把WebUploader的swf路径加入受信任站点。现场工程师通常的做法是:在IE的Internet选项里把医院OA和PACS域名加入受信任站点,然后在"自定义级别"里把"运行ActiveX控件和插件"设置为启用。注意这里要按终端批次做批量策略下发,别一台一台手动配置。遇到实在改不了安全策略的,就得考虑用WebUploader的HTML5模式,但IE11对HTML5分片上传的支持也有限,需要做好降级预案。
5.2 特殊流程:大文件上传时浏览器"假死"
很多人遇到并发上传时页面卡死,第一反应是WebUploader问题,实际上背景原因大多是选择了大文件后,浏览器执行了文件全量MD5计算,导致主线程长期处于阻塞状态。这个卡顿过程可能持续十几秒甚至更久,医生会误以为是系统崩溃,直接强杀浏览器,然后重来一遍。
我们解决的思路是调整MD5计算策略。文件小于200MB时采用全量MD5计算,文件大于200MB时改成"采样分段MD5",抽取文件头部1MB、中间1MB、尾部1MB,拼接后计算MD5。这样既保证了指纹的稳定性,又把等待时间控制在可接受的范围。当然,采样MD5不能用于最终数据的完整性校验,所以我们会在合并完成后触发一次后台全文件校验,校验结果写入日志并关联到file_repository记录。如果发现校验不一致,系统自动标记该文件为"异常",并通知管理员介入处理。这套机制在影像数据归档场景里运行了小半年,效果稳定。
5.3 断点续传状态的最终一致性保障
WebUploader断点续传有个容易踩的坑:如果前端localStorage里的记录和服务端实际存储的分片数据不一致,续传就会出问题。最常见的场景是,用户A上传了一半的临时文件,管理员手动清理了临时目录,但用户A的浏览器localStorage里仍保留着"已上传分片"的记录。随后用户A重新续传时,前端认为那些分片已经上传成功,实际服务端却没有,最终合并时缺失分片,文件损坏。
为了避免这个问题,前端在合并前必须调一次服务端的完整性校验接口/api/upload/complete,携带chunks总分片数,服务端检查临时目录中实际存在的分片数量是否等于chunks,且每个分片大小是否一致。如果缺了,就把缺失分片的序号数组返回给前端,前端针对缺失分片重新上传。只有服务端确认所有分片都齐了,才允许执行合并操作。这套“前端发起合并建议,后端做完整性确认”的流程,比单纯依靠前端状态判断可靠得多。我在几个项目里都坚持这样的设计,虽然没有让代码看起来更炫酷,但稳定压倒一切。
5.4 与后端存储方案(MinIO等)的衔接思考
很多团队后续会把文件存储迁到MinIO或云对象存储,遇到一个共性问题:MinIO支持断点续传吗?答案是支持,而且MinIO的API原生支持分片上传,它的S3兼容接口里就有CreateMultipartUpload、UploadPart、CompleteMultipartUpload这套流程,本质上和WebUploader的分片上传思想一致。但要注意,WebUploader是前端分好后用HTTP上传到你的应用服务,再由你的应用服务转发或直传到MinIO;而MinIO的分片直传通常需要前端生成Presigned URL,然后让浏览器直接把分片POST到MinIO,这样应用服务器不经过二进制流,压力小很多。
我们当时考虑到医院内网环境对防火墙策略比较敏感,没有做前端直传MinIO,而是保持"浏览器 -> 应用服务器 -> MinIO"的链路,因为这样更利于审计和权限控制。应用服务器在接收完分片、合并成完整文件后,再将文件PUT到MinIO,同时把MinIO的object key记录到业务数据库。这个方案的缺点是多一跳传输,千兆内网下速度影响不大,但在百兆链路下会明显拉长上传时间。如果你的网络环境足够好、也不需要应用层干预二进制数据,未来完全可以改成前端直传MinIO,WebUploader只需要生成分片清单并交给后端去触发MinIO的分片上传逻辑即可。
写在最后的实操体会
我在实际项目里体会到,大文件断点续传并不是一个靠前端组件“开箱即用”的功能,而是一套前后端紧密配合的完整链路。WebUploader帮我们省去了分片控制和UI交互的重复劳动,但真正决定稳定性的,是服务端的分片校验、合并逻辑、临时目录生命周期管理,以及前端的失败重试、状态一致性设计。遇到医疗系统这种对稳定性和合规性要求极高的场景,宁可多写几个校验接口,也不要在数据完整性上偷懒。
最后再分享一个小技巧:上线后一定要建立上传监控大屏,实时统计分片上传成功率、失败重试率、平均上传耗时、临时目录使用量等指标。我们第一次上线时没有做监控,直到影像科反馈某个目录上传总是报错,才发现是某台服务器磁盘满导致的。后来加了Prometheus监控和日志采集,再遇到问题都是先看监控面板再动手排查,效率提升明显。
这套方案我们在两家三甲医院落地过,运行大半年没有出现一例大文件损坏或上传中断后无法恢复的投诉。如果你正在做类似的内网大文件上传功能,欢迎按我上面的思路去完善自己的实现,也希望能少踩几个我们踩过的坑。
