1. 汽车制造行业设计图纸传输的痛点与需求分析
在汽车制造行业,设计图纸的传输一直是个老大难问题。我去年接手某车企的PDM系统升级项目时,设计部门负责人直接甩给我三个硬性指标:第一,单个CAD图纸集经常超过10GB;第二,跨国团队协作必须支持断点续传;第三,必须兼容老旧的Windows XP系统。这三点要求直接排除了市面上90%的现成解决方案。
1.1 行业特殊需求解析
汽车设计图纸有几个鲜明的特点:
- 文件体积巨大:一个整车的外覆盖件模具图纸集通常包含3000+个DWG/STP文件,压缩后仍有15-20GB
- 目录结构复杂:严格按照BOM层级组织,如
/车身/左前门/外板/冲压模具体系/... - 传输稳定性要求高:跨国传输时常遇到网络抖动,必须支持从断点继续
- 安全保密性强:图纸外泄可能导致重大商业损失
1.2 技术选型对比
我们对比了三种主流方案:
- 传统FTP方案:
- 优点:兼容性好
- 缺点:无断点续传、目录结构易丢失、传输无加密
- 商业网盘方案:
- 优点:开箱即用
- 缺点:年费高昂(某Drive企业版每年$15/用户)、数据出境风险
- 自研Web方案:
- 优点:完全可控、成本低
- 缺点:开发周期长
最终选择基于Vue3+WebUploader的自研方案,核心考虑点:
- 利用WebUploader的分片机制处理大文件
- 通过Vue3的响应式特性实现实时进度展示
- 自定义目录结构保持逻辑
2. 核心架构设计
2.1 整体技术栈
graph TD A[前端Vue3] --> B[WebUploader封装] B --> C[分片上传] B --> D[断点续传] B --> E[目录保持] A --> F[加密模块] G[后端SpringBoot] --> H[分片存储] G --> I[文件合并] G --> J[SM4解密]2.2 关键技术实现
2.2.1 分片策略优化
针对汽车图纸特点,我们采用动态分片策略:
// 根据文件类型动态调整分片大小 function getChunkSize(file) { const ext = file.name.split('.').pop().toLowerCase() const typeMap = { 'dwg': 10 * 1024 * 1024, // CAD文件用10MB分片 'stp': 5 * 1024 * 1024, 'default': 2 * 1024 * 1024 } return typeMap[ext] || typeMap['default'] }2.2.2 断点续传实现
通过localStorage+服务端校验实现可靠续传:
// 断点信息存储结构 { fileId: 'design_12345', chunks: [1,0,1,0,1], // 1表示已上传 lastModified: 1625097600000, path: '/车身/左前门' }2.2.3 目录结构保持
改造WebUploader的fileAdded事件:
uploader.on('fileAdded', (file) => { // 保留webkitRelativePath或模拟路径 file.relativePath = file.webkitRelativePath || simulatePath(file, rootFolder) // 添加到上传队列 this.uploadList.push({ id: generateId(), name: file.name, path: file.relativePath, size: file.size }) })3. 完整实现代码
3.1 Vue3组件封装
<template> <div class="upload-container"> <div v-if="!isIE" class="folder-upload"> <button @click="triggerFolder">选择设计图纸文件夹</button> <input type="file" ref="folderInput" webkitdirectory @change="handleFolderChange" hidden> </div> <div class="file-list"> <div v-for="item in queue" :key="item.id" class="file-item"> <div class="path">{{ item.path }}</div> <div class="progress"> <progress :value="item.loaded" :max="item.total"></progress> <span>{{ item.percent }}%</span> </div> </div> </div> </div> </template> <script> import WebUploader from 'webuploader' import { reactive, ref, onMounted } from 'vue' export default { setup() { const queue = reactive([]) const isIE = /trident/i.test(navigator.userAgent) const folderInput = ref(null) const triggerFolder = () => { folderInput.value.click() } const handleFolderChange = (e) => { const files = Array.from(e.target.files) initUploader(files) } const initUploader = (files) => { const uploader = WebUploader.create({ server: '/api/design/upload', chunkSize: getDynamicChunkSize(files), chunkRetry: 3, threads: 3, formData: { projectId: 'car_design_2023' } }) files.forEach(file => { uploader.addFile(file) }) uploader.on('uploadProgress', (file, percentage) => { const item = queue.find(q => q.id === file.id) if (item) { item.percent = Math.round(percentage * 100) item.loaded = file.loaded } }) } return { queue, isIE, folderInput, triggerFolder, handleFolderChange } } } </script>3.2 服务端关键接口
@RestController @RequestMapping("/api/design") public class DesignUploadController { @PostMapping("/upload") public ResponseEntity<?> uploadChunk( @RequestParam String fileId, @RequestParam int chunk, @RequestParam int chunks, @RequestParam String path, @RequestPart MultipartFile chunkFile) { // 校验分片 if (chunkFile.isEmpty()) { return ResponseEntity.badRequest().build(); } // 存储分片 String chunkName = String.format("%s_%d.tmp", fileId, chunk); Path chunkPath = Paths.get("/tmp/upload", chunkName); try { Files.write(chunkPath, chunkFile.getBytes()); // 记录上传进度 RedisTemplate.opsForHash().put( "upload:" + fileId, String.valueOf(chunk), "1"); return ResponseEntity.ok().build(); } catch (IOException e) { return ResponseEntity.status(500).build(); } } @PostMapping("/merge") public ResponseEntity<?> mergeChunks( @RequestBody MergeRequest request) { // 验证所有分片是否完整 Long uploaded = RedisTemplate.opsForHash().size("upload:" + request.fileId()); if (uploaded != request.totalChunks()) { return ResponseEntity.badRequest().body("分片不完整"); } // 合并文件 try { mergeService.merge( request.fileId(), request.totalChunks(), request.targetPath()); return ResponseEntity.ok().build(); } catch (Exception e) { return ResponseEntity.status(500).build(); } } }4. 生产环境优化策略
4.1 传输稳定性保障
我们通过以下措施确保99.9%的传输成功率:
- 分片MD5校验:每个分片上传后立即校验
function calculateChunkMD5(chunk) { return new Promise(resolve => { const reader = new FileReader() reader.onload = e => { const md5 = CryptoJS.MD5(CryptoJS.lib.WordArray.create(e.target.result)) resolve(md5.toString()) } reader.readAsArrayBuffer(chunk) }) } - 自动重试机制:失败分片自动重试3次
- 网络切换检测:监听navigator.connection变化
4.2 老旧浏览器兼容方案
对于必须支持IE9的特殊场景:
// IE9兼容模式 if (document.documentMode) { // 使用Flash版WebUploader const uploader = WebUploader.create({ swf: '/static/webuploader/flash/webuploader.swf', // 其他配置... }) // 模拟目录结构 function simulateFolder(files) { const basePath = prompt('请输入文件夹名称') files.forEach(file => { file.relativePath = basePath + '/' + file.name }) return files } }5. 实际应用效果
在某德系车企项目中,该方案实现了:
- 传输效率:20GB图纸集上传时间从原来的6小时降至1.5小时
- 成功率:跨国传输成功率从78%提升至99.3%
- 兼容性:完美支持从WinXP到macOS的所有设计端环境
典型的图纸上传监控界面:
[2023-07-15 14:30:45] 开始上传 /车身/左前门/外板.dwg [2023-07-15 14:31:22] 分片3/128上传成功 (MD5校验通过) [2023-07-15 14:35:18] 网络波动,自动重试分片67 [2023-07-15 15:42:11] 所有分片上传完成,开始合并 [2023-07-15 15:45:33] 文件合并成功,总计耗时1h14m6. 扩展应用场景
该方案稍作改造即可用于:
- 模具加工G代码传输:同样具有大文件、目录结构要求
- 整车测试数据回收:路试产生的TB级数据回传
- 供应商协同平台:与零部件供应商间的图纸交换
我在实际项目中总结出一个经验:当遇到网络异常中断时,不要立即清理临时文件。建议保留7天,因为汽车设计师经常会在出差时使用不稳定的酒店网络。