很多做工程信息化的人都有一个共同痛点:BIM模型传不上去。我在多个项目的运维现场见过同样的场景——一个综合模型动辄几十GB,Revit中心模型加一堆链接文件能上百GB,网络明明是千兆内网,带宽根本不缺,但Web端上传愣是能传一晚上,第二天一看提示中断,整个人都麻了。BIM模型大文件的目录结构分片传输这个需求,不是靠加大带宽能解决的。
问题出在哪?表面上是文件大,本质上是因为BIM模型根本不是一个文件,而是一整棵“目录树”——几十万个小文件按楼层、专业、版本分门别类地躺着,里面还混杂着几个2GB、3GB的大块头单体文件。传统的Web上传组件遇到这种场景基本是白给。所以我当时决定用JS在前端重写一套专门针对“目录结构”的分片传输方案,前后折腾了大半个月,踩了无数坑,今天把整个设计思路、关键代码和实际教训都整理出来。
1. 内网里BIM模型传输的真正难点:它不是单个文件,是“目录树”
1.1 看清BIM模型落地后的真实形态
做工程的人都知道,BIM平台里最终归档的往往不是某一个“.rvt”或“.dwg”文件,而是总包交底、分包协作产生的一整套过程产物。
举个例子,一个地下室机电综合项目的交付包解压出来通常长这样:
- 根目录按标段分成若干个一级目录;
- 每个标段里再按“建筑、结构、机电、暖通、给排水”分专业;
- 专业目录下是按楼栋、按楼层组织的工作集、链接文件、材质贴图;
- 文件夹里还会混着大量PDF交底文件、NWC轻量化缓存、明细表导出、IFC交换文件。
这套目录不是拍脑袋建的,它承载了项目管理的业务语义。对业主方和施工方来说,“哪个目录对应哪个专业、哪个版本能追溯”比“快速把文件传上去”更重要。所以纯粹把几万个文件平铺上传、再用数据库存一个平铺文件列表,对工程类项目基本没法用——后续做模型校审、版本对比的时候,人脑根本找不着北。
这也是标题里强调“目录结构”的原因。我们做的分片传输,核心诉求是:前端按原目录结构分片上传,服务端按原目录结构重组还原,最后落在服务器上的结果和源目录一模一样。
1.2 传统上传方案为什么在国企局域网内失灵
我在项目里先试过几类常规办法,结果都被现实教育了。
第一种是网页表单直接选整个文件夹提交。浏览器拿到几十万个文件后,卡死算是轻的;更致命的是传统表单上传是一次性POST,一个2GB的模型文件刚传了500MB,中间交换机或服务端网关超时,整个请求就报废了。而且文件是排队传的,前面卡住后面全卡住,没有任何可视化进度。
第二种是挂一个共享盘让用户自己拷。网络共享虽然快,但安全审计过不去:每个分包商都要开账号、开目录权限,文件拷进去之后又没有操作留痕,后续出了版本纠纷根本说不清是谁什么时候覆盖了谁。在网络安全要求比较严的单位里,这类“野蛮接入”很快就叫停了。
第三种是用第三方FTP或商业传输软件。这东西在局域网里跑起来确实不错,但问题是要给合作单位装客户端。合作单位分布在五湖四海,不少人机子还是老配置、老浏览器,一个客户端装三天,运维同事直接崩溃。而且商业软件往往封闭,很难和自建的BIM协同平台做版本状态联动。
所以最后结论很明确:要靠浏览器本身的能力,用纯JS加少量文件读写接口,在前端实现目录扫描、文件分片、并发传输、断点续传,服务端再负责校验重组。整个过程不需要装任何插件,安全审计上也说得过去。
1.3 项目立项时的边界和目标
动手之前我先给自己划了几条技术边界,避免后面没完没了地改方案:
- 兼容性上不追求老式浏览器,只保证Chrome和Edge的较新版本能用;
- 容量目标先按单次传输50GB设计,文件数量按10万以内考虑;
- 一期只做内网传输,不把外网弱网场景纳入设计;
- 必须支持断点续传和失败重试,不能一断就从头再来;
- 服务端按原相对路径还原目录结构,而不是平铺落库;
- 整个过程要在Web页面里展示总进度、目录进度、每个文件的状态。
这些边界看起来很基本,但等你真正做起来就会发现,每一个都会衍生出一堆技术细节。
2. 把目录结构翻译成分片协议:先设计接口再写代码
2.1 用manifest清单保住目录树
很多网上的“大文件分片上传”教程只讲单个文件怎么切、怎么传、怎么合并,一遇到目录树就翻车。因为它们缺少最关键的一步:先提交一份描述目录结构的清单。
我的做法是前端在选完目录后,生成一份manifest清单。清单内容不复杂,本质就是一张文件明细表,每一条记录包含:
- 相对路径:这是核心,服务端要靠它重建目录树;
- 文件大小:用于规划分片数量和计算总进度;
- 最后修改时间:用于判断这个文件在两次任务之间是否发生过变更;
- 文件类型:BIM场景下用来做轻量化预检。
举个例子,清单里一条记录看起来是:
{ "relPath": "标段二/机电专业/3号塔楼/MEP_3F_给排水_202406.rvt", "size": 2550136832, "lastModified": 1718024400000, "ext": ".rvt" }几十GB的模型、几万个文件,这个清单本身并不大,一般也就几MB。但它的作用是决定性的:服务端拿到清单后先建好目录骨架和文件登记表,分片数据随后按“相对路径”归属到具体文件。没有这一层设计,分片传完了之后你根本不知道某一片属于目录里的哪个位置。
需要注意一个细节:浏览器里通过目录选择拿到的文件,路径里会带上用户选中的那个根目录名。比如用户在文件框里选中“交付包”这个文件夹,那么拿到的路径可能是“交付包/标段二/...”。传清单时要统一把根目录名剥掉,服务端只使用“标段二/...”这些真正的相对路径,否则服务器上会多套一层名字不一致的目录,最后还原出来跟源目录对不上。
2.2 三个核心接口:初始化、上分片、合并
接口数量不需要多,我最终只定了三个,把业务逻辑尽量放在前端和服务端的文件系统处理上。
初始化接口
前端把manifest整体POST到服务端,服务端做几件事:生成任务ID、检查每个文件哪些分片已经传过、建立目录结构登记。响应里会带两个关键信息:
- taskId:后续所有分片请求都要带上它;
- existedChunks:已经传过的分片集合,前端据此跳过,实现断点续传和秒传。
分片上传接口
这是最核心的接口。前端把一个分片的二进制数据POST上来,参数包含任务ID、文件相对路径、分片序号、分片偏移量、分片大小、分片哈希。服务端把分片落盘后返回成功标记。这个接口必须是幂等的:同一个分片重复上传,结果不能是报错,而是直接返回已存在。
合并接口
所有分片上传完成后,前端调合并接口。服务端读取该任务下所有分片文件,按偏移量排序拼接成完整文件,再按相对路径落位。合并完成后服务端做一次总文件大小和哈希校验,返回“成功”或“校验失败”状态。
接口层面的设计看起来很简单,但注意一个原则:前端不要自己拼好整个文件再传,也不要把所有文件信息一次性塞进分片请求里。分片请求只携带最少的定位信息,数据越少,失败重试的熵越小。
2.3 分片大小和小文件合并策略
分片大小不是拍脑袋定的。BIM原始模型单文件经常几个GB,但整个目录里有大量几十KB的PDF、DWG小文件。如果所有文件都按同一套分片逻辑处理,几万个小文件会生成几万个分片,每个分片都要走一次HTTP,请求数量会直接拖垮服务端。
我采用了两个阈值策略。
一是分片大小按单个文件动态计算。大于20MB的文件才真正分片,分片默认8MB;小文件直接整个文件作为一个“分片”上传。这么做的原因是:8MB分片既控制了单请求的数据量,又不至于把请求次数放大到可怕的程度。1GB的单体文件在8MB分片下是128个请求,数量完全可控。
二是小文件合并打包传输。针对几十KB的小文件,我后来直接在客户端做了一层“逻辑打包”:把同一个目录下的一批小文件按固定大小拼成一个逻辑包,在包内部记录每个小文件的相对路径、长度、偏移,服务端收到后拆包落盘。这一层优化能把几万个请求压缩到几百个,对局域网Web服务器是决定性的差别。
下表是我在千兆内网环境下实测的分片大小参考,不同项目机器配置有差异,但趋势是一致的:
| 分片大小 | 1GB文件的请求数 | 单分片平均传输耗时 | 问题 |
|---|---|---|---|
| 1MB | 1024 | 约10ms | 请求数太多,服务端日志和IO频繁,整体偏慢 |
| 4MB | 256 | 约45ms | 可选,但重传成本还是偏低 |
| 8MB | 128 | 约95ms | 推荐,请求和重传成本相对均衡 |
| 16MB | 64 | 约190ms | 大文件可以,小文件服务端内存峰值高 |
另外补充一句:不要把分片大小设成2MB以下,我不止一次看到有人的分片上传组件卡在“分片太碎导致TCP建连次数过多”上。在局域网里,每次HTTP请求都有建连和断开的开销,分片越碎开销占比越高。
3. 前端JS实现:目录读取、分片封装与哈希计算的细节
3.1 用webkitdirectory读取目录,注意不要一上来走DOM
浏览器原生目录选择接口是input[webkitdirectory],虽然带webkit前缀,但Chrome和Edge一直支持,这就是我决定不兼容老式浏览器的底气。
选完目录后,通过e.target.files能拿到一个文件列表,每个文件对象上有webkitRelativePath属性。这个属性就是该文件相对于所选根目录的完整路径。前端构建manifest的代码大致是:
const input = document.getElementById('dirPicker'); input.webkitdirectory = true; input.addEventListener('change', async (e) => { const fileList = Array.from(e.target.files); const manifest = { schemaVersion: 1, files: [] }; for (const file of fileList) { // webkitRelativePath 形如 "交付包/标段二/xxx.rvt" const relative = file.webkitRelativePath.replace(/^[^/]+/, ''); manifest.files.push({ relPath: relative.replace(/^\/+/, ''), size: file.size, lastModified: file.lastModified, ext: (file.name.match(/\.[^.]*$/) || [''])[0].toLowerCase() }); } // 将 manifest POST 到初始化接口 });这里有三点容易踩坑:
第一,Array.from(e.target.files)看起来轻松,但如果目录里有十万个文件,这一下会占用不少内存,而且如果你紧接着把这十万个对象塞进Vue或React的响应式数组里,页面直接卡到无响应。我的做法是只保留manifest需要的信息,文件对象本身不放进响应式系统里,后续按需从原始的e.target.files里取。
第二,manifest构建如果文件量很大,不要用同步循环一跑到底。可以用requestIdleCallback分批处理,或者干脆放到Web Worker里生成。我最初的版本是在主线程上直接跑,三万多个文件跑完,界面已经白屏了好几次。
第三,目录选择是用户一次性触发的,没有办法做到“选完根目录后增量追加”。如果交付包里有新增的文件,用户只能重新选一遍根目录,此时就需要通过manifest里的lastModified和大小判断哪些文件没有变化,直接走秒传逻辑。
3.2 文件分片与内存控制:正确姿势是“切片后读取”,不是“读完后切片”
前面提到大于20MB的文件才分片。分片的第一步是计算分片总数,然后按偏移量切片。核心API是Blob.prototype.slice。
很多人习惯先把整个文件通过FileReader.readAsArrayBuffer读进内存,再手动切ArrayBuffer。对大文件来说这是灾难:一个2GB的RVT文件读进来,浏览器内存直接多2GB,低配办公机当场死给你看。
正确做法是先用file.slice切出一个Blob分片,再把这个分片读成ArrayBuffer。Blob切片在底层只是原文件的一段逻辑引用,不需要把整个文件同时读入内存。关键代码如下:
const CHUNK_SIZE = 8 * 1024 * 1024; async function* iterChunks(file, chunkSize = CHUNK_SIZE) { const total = file.size; let index = 0; for (let offset = 0; offset < total; offset += chunkSize) { const size = Math.min(chunkSize, total - offset); const blob = file.slice(offset, offset + size); const buffer = await blob.arrayBuffer(); yield { index, offset, size, buffer }; index += 1; } }注意一点:blob.arrayBuffer()会把这一个分片读完,所以每次循环只存在一个分片大小的内存。8MB对现代浏览器完全无压力。我的传输队列最多并发3个分片,算上请求体和接收缓冲区,内存峰值控制在100MB以内。
还有一个容易被忽略的点:file.slice的偏移量是字节单位,BIM文件名里偶尔会有中文,中文在UTF-8下占3个字节,但这些都不影响分片逻辑,因为切片只是按绝对字节偏移切,不关心文本编码。
3.3 为什么哈希计算必须交给Web Worker
分片传输一定要有完整性校验,否则一个分片在网络里坏了、服务端却不知道,合并之后的模型文件就是坏的,轻量化转模的时候报错,锅还得技术来背。
哈希方案上,我一开始想在主线程直接对每个分片算MD5。结果发现主线程算哈希是同步操作,2GB文件算完全量哈希,整个页面卡住,进度条不动,用户以为程序崩了。
解决方案很明确:用Web Worker跑增量哈希。简单说一下spark-md5的增量用法,核心是边读取分片边喂入,不把整个文件加载到内存:
// worker 内部伪代码 importScripts('spark-md5.min.js'); self.onmessage = async (e) => { const file = e.data.file; const spark = new SparkMD5.ArrayBuffer(); let offset = 0; while (offset < file.size) { const chunk = file.slice(offset, offset + CHUNK_SIZE); const buffer = await chunk.arrayBuffer(); spark.append(buffer); offset += CHUNK_SIZE; self.postMessage({ type: 'progress', offset }); } self.postMessage({ type: 'done', hash: spark.end() }); };分片哈希也是一样的思路:每个分片单独算一个MD5,分片上传时带上,服务端校验后落盘。全文件哈希不是必须的,因为合并后的完整文件可以由每个分片哈希组合推理保证。但如果有“秒传”需求,全文件哈希就很有用了——服务端用文件大小加哈希做索引,遇到同样的文件直接跳过。
Web Worker还有一个隐藏优势:目录扫描和manifest构建也能塞进去。扫描几万条路径、拼接路径字符串、排序,这些在Worker里跑不会卡界面。代价是Worker里不能直接访问DOM和input的文件数组,需要先把file对象列表通过消息通道传给Worker,这个传递本身开销不大,因为File对象是引用。
4. 传输引擎实测:并发控制、断点续传、失败重试的工程取舍
4.1 并发数量不是越大越好:浏览器连接数和服务端IO才是天花板
第一次写传输引擎的时候,我最关心速度,于是把并发数设成了8。结果千兆内网跑起来,前几分钟看着飞快,之后就陆续出现分片超时和服务器拒绝连接。查下来是两个原因叠加:
一是浏览器对同一域名的HTTP/1.1连接数限制一般是6个,并发8意味着有请求一直在排队,排着排着就超时了。二是服务端同时接到多个8MB分片,写盘和校验的瞬时CPU冲高,旧接口本来还带同步日志,一下子全堵住了。
后来我把并发数降到了3,传输速度并没有肉眼可见的下降,稳定性反而上来了。原因是内网带宽虽然高,但单个服务器处理请求的瓶颈往往在磁盘IO上,不在网速。前端用一个简单的异步队列控制并发即可:
async function runTaskQueue(queue, limit) { const workers = []; for (let i = 0; i < limit; i++) { workers.push((async () => { while (queue.length) { const task = queue.shift(); await uploadChunkWithRetry(task); } })()); } await Promise.all(workers); }这个队列虽然简单,但要注意一个细节:不要用数组的forEach去发起请求然后Promise.all,那等于一次性把所有请求打出去,不是并发控制而是同时爆发。
4.2 断点续传与秒传:服务端状态机要配合好
断点续传的实现,前端只是“跳过已传分片”,真正干活的在服务端的初始化接口。
约定是这样的:前端每次新任务都生成一个fileKey,这个key是文件相对路径、文件大小、最后修改时间拼接哈希得到的。如果这个文件内容没变,fileKey就不会变。初始化时服务端拿着taskId和fileKey到分片记录表里查,返回“哪些分片已经传过”。前端再对比本地分片序号,把这些分片从队列里剔除即可。
秒传本质上就是“所有分片都已存在”,前端可以跳过整个文件的所有上传操作。BIM项目里经常出现“同一个交付包在不同标段重复使用”的情况,比如公共图纸在多个专业目录里各放了一份,秒传能节省很多时间。
这里要特别注意一个数据一致性问题:分片“已存在”的判断不能只看分片序号,还要看分片大小和分片哈希。真实场景里遇到过:用户在传输中断后,把源文件的一个版本更新了一部分,文件大小恰好没变,但是内容变了。如果不校验分片哈希,服务端就会用旧分片拼出新文件,最后模型损坏。我的做法是初始化接口返回的“已传分片列表”里带上每个分片的哈希,前端如果发现相同序号的分片哈希不匹配,就重新上传该分片。
4.3 内网“不稳”时的失败重试策略
很多人觉得内网很可靠,实际上局域网里的网络抖动一点不少。我在项目里遇到过的场景包括:老旧交换机在高峰期丢包、防火墙对连续大流量请求做限流、服务器磁盘临时IO饱和导致接口响应变慢等等。
所以分片上传必须带重试机制。我的策略是每片最多重试5次,连续失败后对该分片做指数退避,退避间隔从500毫秒开始,每次翻倍,最长到8秒。如果5次之后还是失败,就把这个分片放进失败清单,但不中断整个任务,等所有分片都遍历完后,允许用户一键重传失败清单里的分片。
伪代码如下:
async function uploadChunkWithRetry(chunk, retries = 5) { for (let attempt = 0; attempt <= retries; attempt++) { try { await uploadChunk(chunk); return; } catch (err) { if (attempt === retries) { failures.push(chunk); return; } await sleep(Math.min(500 * Math.pow(2, attempt), 8000)); } } }这里我特别想说的一点是:不要把整个文件的所有分片塞进一个Promise.all里去重试,一旦某个分片反复失败,它占用的资源会阻塞其他分片的重试队列。用上面的队列模型,每个失败分片退避时让出工作线程,其他分片照常推进。
5. 服务端重组:从一堆分片还原成完整目录树
5.1 分片落盘:既要性能也要防路径穿越
服务端我这里用Node.js举例,但思路可以换成Java或Python。分片接收接口要做的事很简单:把前端传来的分片二进制数据写进一个临时目录,文件名或路径包含taskId、文件相对路径的编码、分片序号。为了减少临时目录下的文件数量,我按taskId建一个总目录,再按文件相对路径建子目录存分片,这样合并的时候好找。
但这里有个必须处理的安全问题:前端传来的relPath是用户可控的。如果不加校验,一个恶意请求可以传一个“../../../../etc/passwd”之类路径,服务端落盘时就可能写到任意位置。这在单位内网也是绝对不允许的。
校验方式很基础但有效:
function safeResolve(baseDir, relPath) { const fullBase = path.resolve(baseDir); const target = path.resolve(fullBase, relPath); if (target.slice(0, fullBase.length) !== fullBase) { throw new Error('非法路径'); } return target; }另外,Windows服务器上的路径分隔符和Linux不一样,前端路径一律是正斜杠“/”,服务端落盘时要用path模块转换。很多坑就是在这里出的:开发时在Windows上没问题,部署到Linux后路径拼接错了,目录树多了一层空目录。
分片落盘的性能也要注意。我第一次写的时候用了fs.writeFileSync逐片写,结果并发3个分片同时写,磁盘排队严重。后来改成用可写流或fs.promises.writeFile,并在分片校验完成后再落盘;不要校验和落盘混在一起做,否则一个分片哈希不对,你都不知道该不该清理写了一半的文件。
5.2 合并与校验:按偏移拼回原文件
所有分片上传完成后,前端调合并接口。服务端遍历该文件的所有分片,按offset排序,然后逐片读取并写入最终文件。
这里有一个性能关键点:不要用fs.readFileSync把分片读成字符串再拼接。分片是二进制数据,字符串拼接会破坏内容。正确做法是把分片作为Buffer流式写入,或者按偏移量用文件句柄直接write。
合并后立刻做三件事:
- 比对文件大小:文件大小不一致,说明分片有缺失或重复;
- 分批抽样校验分片哈希:不必把整个大文件重新读一遍,但至少对头部、中间、尾部几个分片做哈希比对;
- 清理临时分片目录:合并成功后才清理,失败则保留分片,方便前端不再重传。
完成一个文件后,服务端还要把该文件的元数据落到数据库里,状态标记为“已归档”。这一步是给业务系统用的,BIM平台后续要先知道有哪些文件、在什么路径、多大,才能去触发轻量化转换或模型浏览服务。
5.3 与BIM协同平台的对接:还原目录不是终点
到这里,服务端已经能从一堆分片还原出完整的目录树和文件。但如果只是把文件还原到服务器磁盘上,那这个传输系统还只是一半。BIM协同平台需要的是“可检索的文件上下文”。
我在实践中的做法是,在任务初始化时,让前端把manifest额外提交一个根级参数,例如“项目ID”“单位工程ID”。服务端归档的路径设置为“存储根目录/项目ID/原始交付目录”,同时把每个文件的关键信息入库,形成一张“文件归档表”,字段包括:
- 项目ID(用于业务隔离)
- 相对路径(用于还原目录树)
- 文件大小、文件哈希(用于版本对比和秒传)
- 归档时间、传输任务ID(用于追溯)
- 轻量化状态(初始为待转换)
BIM平台或者模型浏览服务接到“文件已归档”的消息后,可以按相对路径找到RVT、IFC、NWC等格式文件,开始二次处理。这一步做好之后,整个传输工具就不再是孤立的“上传插件”,而是平台的数据进场通道。内部验收的时候,业务方在乎的其实不是分片算法多精巧,而是“我交底的文件在哪、能不能按专业找到、版本不要错”,这些都要靠对接层回答。
6. 实测数据和踩坑记录:从“能用”到“稳定”的差距
6.1 一次完整传输的实测数据
我在一个中型项目现场做了完整压测,模型交付包的实际组成非常典型:大量小文件加少量大文件。原始包约42GB,文件总数约3.8万个,单文件最大是2.6GB的RVT模型。客户端是普通办公Windows机器、Chrome浏览器,服务端是2核4G的虚拟机加一块机械盘,网络是千兆内网。
采用了8MB分片、并发3、小文件逻辑打包后,完整传输耗时约14分钟。其中纯带宽理论上只需要几十秒,但瓶颈完全在“请求数量”和“服务端磁盘IO”上。下面是其中几个有代表性的阶段耗时:
| 阶段 | 数据量 | 耗时 | 说明 |
|---|---|---|---|
| 目录扫描与manifest生成 | 3.8万个文件 | 约20秒 | 放Web Worker后不卡界面 |
| 小文件打包传输 | 约25GB | 约8分钟 | 请求数约1200个 |
| 大文件分片传输 | 约17GB | 约5分钟 | 单个2.6GB文件约5分钟 |
| 服务端重组校验 | 42GB | 约40秒 | 主要是读分片、合并、清理 |
对比没用小文件打包策略的版本,同样的包耗时超过40分钟,因为3.8万个文件至少会产生3.8万个请求,交换机和服务端日志系统完全被请求风暴淹没。
6.2 坑1:低配客户端把目录扫描跑成了浏览器白屏
项目第一次现场演示时,准备阶段选完目录,页面白屏了将近一分钟,领导的脸当场就黑了。原因就是我前面说的:几万个文件对象被一次性丢进前端响应式框架里,主线程直接被同步扫描卡死。
这个坑的修复分两步:
第一步把manifest构建挪到Web Worker里,扫描过程主线程不再阻塞; 第二步是别把原始File对象数组塞进UI状态,只暴露一个“已扫描到xxxx个文件”的计数器和manifest摘要。界面展示按目录聚合后的汇总信息,而不是把每个文件都渲染成DOM节点。这个经验对任何超过几千个文件的目录场景都适用。
6.3 坑2:并发调到6台服务器直接“假死”
还有一个印象深刻的坑:并发数从3调到6之后,服务器连续几次出现“端口能ping通、接口却全部超时”的状态。排查下来是服务端同时收到多个分片,每个分片8MB,内存被临时Buffer占满,加上机械盘IO饱和,Node.js事件循环被阻塞到“假死”。
把并发降回3、服务端分片接收改成流式写入后,问题消失。这让我意识到一个道理:局域网环境下,前端不要想当然地认为“带宽大就可以把并发拉满”。中间设备的连接数限制、服务端磁盘写入能力、内存峰值,任何一个都是软瓶颈。稳定性永远优先于峰值速率。
还有一个容易被忽视的细节:传输期间如果用户切换了网络(比如从网口切到WiFi),所有正在上传的请求都会断掉。我后来加了一个全局网络状态监听,在断网时暂停队列,恢复网络后自动续传,这个功能在真实办公环境里非常实用。
6.4 给后来者的一点个人建议
做了这一整套下来,我的体会是:BIM模型大文件的传输,难的不是写代码,而是把“目录完整还原、断点续传、失败重试、服务端校验”这些看似基础的能力真正做扎实。很多项目输就输在只能传单个文件,或者传完一堆文件却没有目录结构,后面业务系统根本没法用。
如果让我给后来者建议,第一句话一定是:先把“不丢、不坏、可续传”做到,再谈速度。在这个基础上,小文件打包、分片大小调整、并发优化才是顺水推舟的事。希望这篇记录能帮正在做同类需求的人少走几个弯。