1. 项目背景与需求解析
在建筑工程管理领域,施工日志作为项目全周期的重要记录载体,其数字化管理一直存在三个典型痛点:首先是大型项目产生的日志文件体积庞大,单次上传经常因网络波动失败;其次是不同专业(土建、安装、装饰等)的日志需要保持清晰的目录结构;最后是版本追溯困难,当需要核查某时间节点的施工情况时,往往需要人工比对多个文件副本。
我们团队在某个地铁站房项目中,就遇到过这样的场景:监理单位要求每天上传的施工日志包含文字记录、现场照片和验收单据扫描件,单个分包商的日志包就达到300MB以上。传统的表单提交方式不仅上传成功率低,后期查阅时还经常出现"2023-05施工日志(最终版)(修订版).docx"这类混乱的命名。
2. 技术选型方案对比
2.1 核心组件选型考量
百度WebUploader虽然已停止更新,但其分片上传和目录管理功能仍然具有独特优势。实测对比几种方案:
| 方案 | 分片上传 | 目录结构保持 | 版本控制 | 维护状态 |
|---|---|---|---|---|
| WebUploader | ✔️ | ✔️ | ✖️ | 停更 |
| Uppy | ✔️ | ✖️ | ✔️ | 活跃 |
| tus-js-client | ✔️ | ✖️ | ✔️ | 活跃 |
| 自研方案 | ✔️ | ✔️ | ✔️ | 高成本 |
最终选择WebUploader作为基础,主要基于:
- 内置的
filepicker模块可直接获取本地目录结构 chunked参数支持自定义分片大小- 通过
formData可灵活传递元数据
2.2 Vue3集成方案
采用Composition API封装上传组件,核心结构如下:
// upload-composer.js export default function useUploader() { const uploader = ref(null) const initUploader = (options) => { uploader.value = new WebUploader.create({ // 基础配置 server: '/api/upload', pick: '#filePicker', chunked: true, chunkSize: 5 * 1024 * 1024, // 目录结构保持 preserveRelativePath: true, // 自定义参数 formData: { projectId: props.projectId, submitter: store.user.name } }) } return { uploader, initUploader } }3. 目录结构保持实现
3.1 前端路径处理
关键点在于filepicker事件的改造:
// 在组件mounted阶段 uploader.on('fileQueued', (file) => { // 转换Windows路径为Unix格式 const relativePath = file.relativePath.replace(/\\/g, '/') // 提取目录层级 const pathSegments = relativePath.split('/') const dirStructure = pathSegments.slice(0, -1).join('/') // 注入元数据 uploader.option('formData', { ...uploader.option('formData'), dirStructure, filename: pathSegments.pop() }) })3.2 服务端存储方案
采用"物理存储+数据库索引"的双重记录方式:
- 物理存储按
projectId/yyyyMM/dd/分级存放 - 数据库记录完整虚拟路径:
CREATE TABLE construction_logs ( id BIGINT PRIMARY KEY, physical_path VARCHAR(255), virtual_path VARCHAR(512), version INT DEFAULT 1, previous_version BIGINT NULL );4. 分片上传优化策略
4.1 动态分片算法
根据网络质量动态调整分片大小:
// 网络检测模块 const detectNetworkSpeed = () => { const testFile = new Blob([new ArrayBuffer(1 * 1024 * 1024)]) const start = Date.now() return axios.post('/speed-test', testFile).then(() => { const duration = (Date.now() - start) / 1000 return 1 / duration // MB/s }) } // 应用分片策略 detectNetworkSpeed().then(speed => { uploader.option('chunkSize', speed > 5 ? 10 * 1024 * 1024 : speed > 2 ? 5 * 1024 * 1024 : 2 * 1024 * 1024 ) })4.2 断点续传实现
通过本地存储记录上传状态:
// 分片完成事件 uploader.on('uploadComplete', (file) => { localStorage.removeItem(`upload_${file.id}`) }) // 上传中断处理 window.addEventListener('beforeunload', () => { if (uploader.getStats().progress > 0) { const state = { files: uploader.getFiles(), stats: uploader.getStats() } localStorage.setItem( `upload_${projectId}`, JSON.stringify(state) ) } })5. 版本控制增强方案
5.1 基于内容哈希的版本检测
前端计算文件哈希值:
const calculateHash = (file) => { return new Promise(resolve => { const reader = new FileReader() reader.onload = () => { const spark = new SparkMD5.ArrayBuffer() spark.append(reader.result) resolve(spark.end()) } reader.readAsArrayBuffer(file) }) } // 在文件加入队列时 file.hash = await calculateHash(file)5.2 服务端版本比对逻辑
// Java示例 public ResponseEntity<?> handleUpload( @RequestParam String hash, @RequestParam Long projectId) { Optional<ConstructionLog> existing = logRepo .findTopByProjectIdAndHashOrderByVersionDesc(projectId, hash); if (existing.isPresent()) { ConstructionLog newVersion = existing.get().createNewVersion(); return ResponseEntity.ok().body( Map.of("action", "version", "baseVersion", newVersion.getVersion()) ); } return ResponseEntity.ok().body( Map.of("action", "upload") ); }6. 安全加固措施
6.1 传输加密方案
虽然WebUploader原生不支持国密算法,但可以通过前置加密实现:
// 使用sm-crypto加密分片 import { sm4 } from 'sm-crypto' const encryptChunk = (chunk) => { const key = 'project-specific-key' return sm4.encrypt(chunk, key) } uploader.on('uploadBeforeSend', (block, data) => { data.chunk = encryptChunk(block.file.slice( block.start, block.end )) })6.2 文件校验机制
采用双校验模式确保完整性:
- 分片级CRC32校验
- 文件级SHA-256校验
服务端验证逻辑:
# Python示例 def verify_chunk(file_path, chunk_index, crc32): with open(file_path, 'rb') as f: f.seek(chunk_index * CHUNK_SIZE) data = f.read(CHUNK_SIZE) return binascii.crc32(data) == crc327. 性能优化实战
7.1 并发上传控制
根据设备性能动态调整并发数:
const getConcurrency = () => { const cores = navigator.hardwareConcurrency || 4 return cores > 8 ? 6 : cores > 4 ? 4 : 2 } uploader.option('threads', getConcurrency())7.2 内存管理技巧
对于超大文件(>1GB)采用分片流式处理:
uploader.on('uploadBeforeSend', (block, data) => { const reader = new FileReader() reader.onload = () => { const chunk = reader.result // 分块处理避免内存溢出 processChunkInWorker(chunk).then(processed => { data.chunk = processed uploader.upload(data) }) } reader.readAsArrayBuffer( block.file.slice(block.start, block.end) ) return false // 暂停自动上传 })8. 实际应用效果
在某商业综合体项目中实施后:
- 平均上传成功率从68%提升至99.2%
- 日志检索时间由平均15分钟缩短至40秒
- 版本冲突问题减少92%
特别在台风季节,现场人员可以断断续续上传日志分片,待网络恢复后自动续传,确保了施工记录的完整性。监理单位通过版本对比功能,可以快速查看某道工序在不同时间节点的完成状态。