在航空航天领域,三维模型文件通常体积庞大(4GB以上),传统上传方式面临诸多挑战。我们为某政府客户开发的文件传输系统需要满足以下核心需求:
| 方案 | 优点 | 缺点 | 信创适配性 |
|---|---|---|---|
| WebUploader | 成熟稳定 | 依赖Flash,国产浏览器兼容差 | ❌ |
| Plupload | 多引擎支持 | 代码臃肿,性能一般 | ❌ |
| Resumable.js | 纯HTML5实现 | 功能简单,缺少进度管理 | ✔️ |
| 原生File API+分片 | 完全可控,性能优异 | 开发成本高 | ✔️ |
前端方案:
后端方案:
javascript复制// 分片大小动态计算(单位:字节)
const calculateChunkSize = (fileSize) => {
const baseSize = 5 * 1024 * 1024 // 5MB基础分片
const maxChunks = 1000 // 最大分片数限制
// 根据文件大小动态调整分片尺寸
return Math.min(
baseSize,
Math.ceil(fileSize / maxChunks)
)
}
设计考量:
javascript复制// hash-worker.js
self.importScripts('spark-md5.min.js')
self.onmessage = async (e) => {
const file = e.data.file
const chunkSize = e.data.chunkSize
const spark = new SparkMD5.ArrayBuffer()
// 增量计算哈希
for (let offset = 0; offset < file.size; offset += chunkSize) {
const chunk = file.slice(offset, offset + chunkSize)
const buffer = await chunk.arrayBuffer()
spark.append(buffer)
// 进度反馈
self.postMessage({
type: 'progress',
percent: Math.min(offset / file.size * 100, 99)
})
}
// 最终哈希
self.postMessage({
type: 'complete',
hash: spark.end()
})
}
优化点:
php复制// 上传状态记录结构
class UploadSession {
public $uploadId; // 唯一上传ID
public $fileName; // 原始文件名
public $fileHash; // 文件整体哈希
public $chunkSize; // 分片大小
public $totalChunks; // 总分片数
public $uploadedChunks = []; // 已上传分片索引
public function save() {
file_put_contents(
"{$this->getTempDir()}/session.json",
json_encode($this)
);
}
public function isCompleted() {
return count($this->uploadedChunks) >= $this->totalChunks;
}
}
| 浏览器类型 | 检测方式 | 降级方案 |
|---|---|---|
| 现代浏览器 | 支持File API和Blob.slice | 完整功能 |
| 360安全浏览器 | 检测UA包含"QihooBrowser" | 提示升级到极速模式 |
| 红芯浏览器 | 检测UA包含"Redcore" | 提供独立安装包 |
| 环境组合 | 上传速度 | 稳定性 | 备注 |
|---|---|---|---|
| 麒麟V10 + 飞腾FT-2000 | 28MB/s | ★★★★☆ | 需关闭安全沙箱 |
| UOS20 + 鲲鹏920 | 32MB/s | ★★★★★ | 性能最优 |
| 中标麒麟 + 龙芯3A4000 | 15MB/s | ★★★☆☆ | 建议降低分片大小 |
javascript复制// 并发上传控制器
class UploadQueue {
constructor(maxConcurrent = 3) {
this.queue = []
this.activeCount = 0
this.maxConcurrent = maxConcurrent
}
add(task) {
this.queue.push(task)
this.run()
}
async run() {
while (this.activeCount < this.maxConcurrent && this.queue.length) {
const task = this.queue.shift()
this.activeCount++
try {
await task()
} finally {
this.activeCount--
this.run()
}
}
}
}
调优建议:
javascript复制// 非响应式文件引用
const file = markRaw(originalFile)
php复制// 分片校验中间件
class ChunkValidator {
public function handle($request, $next) {
$expectedSize = $request->input('chunkSize');
$actualSize = $request->file('chunk')->getSize();
if (abs($expectedSize - $actualSize) > 1024) {
abort(422, '分片大小异常');
}
return $next($request);
}
}
PHP.ini关键参数:
ini复制upload_max_filesize = 1024M
post_max_size = 1024M
max_execution_time = 3600
memory_limit = 512M
Nginx优化:
nginx复制client_max_body_size 1024M;
client_body_timeout 3600s;
proxy_read_timeout 3600s;
| 存储类型 | 适用场景 | 配置示例 |
|---|---|---|
| 本地存储 | 小规模部署 | storage/app/uploads |
| FastDFS | 高可用集群 | tracker_server=192.168.1.100:22122 |
| 对象存储 | 云环境 | OSS/S3兼容接口 |
| 分片大小 | 上传时间 | CPU占用 | 内存峰值 |
|---|---|---|---|
| 1MB | 28分12秒 | 65% | 1.2GB |
| 5MB | 19分45秒 | 42% | 850MB |
| 10MB | 17分33秒 | 38% | 780MB |
| 20MB | 16分08秒 | 35% | 720MB |
| 网络中断位置 | 续传成功率 | 平均耗时 |
|---|---|---|
| 10% | 100% | 1.2秒 |
| 50% | 100% | 1.5秒 |
| 90% | 99.8% | 2.1秒 |
| 错误码 | 含义 | 解决方案 |
|---|---|---|
| 4001 | 分片大小不符 | 检查前端分片计算逻辑 |
| 4002 | 哈希校验失败 | 重新生成文件哈希 |
| 5001 | 临时目录不可写 | 检查storage目录权限 |
| 5002 | 分片索引越界 | 验证totalChunks参数 |
bash复制# 监控上传进程
tail -f storage/logs/upload-$(date +%Y-%m-%d).log | grep 'chunk_upload'
javascript复制// 递归处理文件夹
async processDirectoryEntry(directory, path = '') {
const entries = await readDirectory(directory)
for (const entry of entries) {
const fullPath = `${path}/${entry.name}`
if (entry.isDirectory) {
await this.processDirectoryEntry(entry, fullPath)
} else {
await this.processFileEntry(entry, fullPath)
}
}
}
javascript复制// 令牌桶限速算法
class RateLimiter {
constructor(rate) {
this.tokens = 0
this.lastTime = Date.now()
this.rate = rate // bytes/ms
}
async consume(bytes) {
const now = Date.now()
const elapsed = now - this.lastTime
this.lastTime = now
this.tokens = Math.min(
this.tokens + elapsed * this.rate,
this.rate * 1000 // 1秒桶容量
)
if (this.tokens < bytes) {
const waitTime = (bytes - this.tokens) / this.rate
await new Promise(r => setTimeout(r, waitTime))
this.tokens = 0
} else {
this.tokens -= bytes
}
}
}
javascript复制// FileUploader.vue
export default {
props: {
chunkSize: {
type: Number,
default: 5 * 1024 * 1024 // 5MB
},
maxConcurrent: {
type: Number,
default: 3
}
},
methods: {
async startUpload() {
// 1. 计算文件哈希
this.fileHash = await this.calculateHash()
// 2. 初始化上传会话
const { uploadId } = await api.initUpload({
fileName: this.file.name,
fileSize: this.file.size,
fileHash: this.fileHash,
chunkSize: this.chunkSize
})
// 3. 创建上传队列
this.uploadQueue = new UploadQueue(this.maxConcurrent)
// 4. 添加所有分片任务
for (let i = 0; i < this.totalChunks; i++) {
this.uploadQueue.add(() => this.uploadChunk(i))
}
// 5. 等待完成
await this.uploadQueue.completed()
// 6. 合并文件
await api.mergeChunks({ uploadId, fileHash: this.fileHash })
}
}
}
vue复制<template>
<div class="progress-container">
<div class="progress-bar">
<div
class="progress-fill"
:style="{ width: `${progress}%` }"
></div>
</div>
<div class="progress-info">
<span>哈希计算: {{ hashProgress }}%</span>
<span>分片上传: {{ uploadProgress }}%</span>
<span>速度: {{ uploadSpeed }}</span>
</div>
</div>
</template>
<script>
export default {
computed: {
uploadSpeed() {
const bytes = this.uploadedSize - this.lastMeasuredSize
const kb = (bytes / 1024).toFixed(1)
return `${kb} KB/s`
}
}
}
</script>
php复制// 使用流式合并避免内存溢出
public function mergeChunks($chunkPaths, $outputPath) {
$output = fopen($outputPath, 'wb');
foreach ($chunkPaths as $chunkPath) {
$chunk = fopen($chunkPath, 'rb');
stream_copy_to_stream($chunk, $output);
fclose($chunk);
unlink($chunkPath);
}
fclose($output);
}
nginx复制# 负载均衡配置
upstream upload_servers {
server 192.168.1.101:9000;
server 192.168.1.102:9000;
server 192.168.1.103:9000;
}
location /api/upload {
proxy_pass http://upload_servers;
proxy_set_header X-Real-IP $remote_addr;
proxy_next_upstream error timeout invalid_header;
}
php复制// 验证STL模型文件头
public function isValidStl($filePath) {
$header = file_get_contents($filePath, false, null, 0, 80);
return preg_match('/^solid\s|^\x00{80}/', $header);
}
javascript复制// 确保分片不切割三角面片数据
function alignChunkBoundary(file, start, chunkSize) {
const stlHeaderSize = 80
const facetSize = 50 // 每个三角面片50字节
if (start < stlHeaderSize) return stlHeaderSize
const remainder = (start - stlHeaderSize) % facetSize
if (remainder === 0) return start
return start + (facetSize - remainder)
}
javascript复制// 模拟大文件上传测试
function createTestFile(size) {
const buffer = new ArrayBuffer(size)
const view = new Uint8Array(buffer)
// 填充随机数据
for (let i = 0; i < size; i++) {
view[i] = Math.floor(Math.random() * 256)
}
return new Blob([buffer], { type: 'application/octet-stream' })
}
// 10个并发上传任务
for (let i = 0; i < 10; i++) {
const file = createTestFile(1024 * 1024 * 500) // 500MB
uploadTest(file)
}
bash复制# 编译SparkMD5的WASM版本
emcc -O3 -s WASM=1 -s EXPORTED_FUNCTIONS="['_malloc']" \
spark-md5.c -o spark-md5.wasm
javascript复制// 检测CPU架构切换算法
if (navigator.userAgent.includes('LoongArch')) {
import('./hash-loongarch.js').then(module => {
this.hasher = new module.LoongArchHasher()
})
} else {
this.hasher = new SparkMD5()
}
javascript复制// 使用wx.uploadFile分片上传
const uploadChunk = (chunk, index) => {
return new Promise((resolve, reject) => {
const task = wx.uploadFile({
url: 'https://api.example.com/upload',
filePath: chunk.path,
name: 'chunk',
formData: {
chunkIndex: index,
fileHash: this.fileHash
},
success: resolve,
fail: reject
})
task.onProgressUpdate((res) => {
this.updateProgress(index, res.progress)
})
})
}
php复制// 安全日志记录
$logger->info('File upload', [
'ip' => $request->ip(),
'user' => auth()->id(),
'action' => 'chunk_upload',
'file_hash' => $fileHash,
'chunk_index' => $chunkIndex,
'risk_score' => $this->calculateRiskScore($request)
]);
yaml复制# metrics配置
- name: upload_chunks_total
help: Total uploaded chunks
type: counter
labels: [status]
- name: upload_duration_seconds
help: Upload duration histogram
type: histogram
buckets: [0.1, 0.5, 1, 5, 10]
yaml复制rules:
- alert: HighUploadFailureRate
expr: rate(upload_chunks_total{status="failed"}[5m]) / rate(upload_chunks_total[5m]) > 0.05
for: 10m
labels:
severity: warning
annotations:
summary: "High upload failure rate ({{ $value }})"
php复制// 每天清理超过24小时的临时文件
$files = Storage::disk('temp')->files();
foreach ($files as $file) {
if (time() - Storage::lastModified($file) > 86400) {
Storage::delete($file);
}
}
javascript复制// 检查本地是否有相同哈希文件
const cachedFile = await caches.match(`file:${fileHash}`)
if (cachedFile) {
this.showToast('使用缓存文件跳过上传')
return cachedFile
}