gongxiangp2/components/upload/batch-upload-handlers.ts
chenpei cd6e40d9d2 优化:网络传输性能和稳定性 - TCP缓冲区+并发控制优化
主要改动:
1. TCP缓冲区优化(16MB)- 解决公网上传速度慢问题
2. 并发数降低到1-3 - 避免4并发导致ERR_CONNECTION_RESET
3. 动态并发控制 - 根据网络环境自动调整
4. Nginx超时增加到120s - 支持大分片上传
5. 上传进度修复 - 修复剩余时间计算
6. 新增任务-页面上传器和详情组件
7. 统一部署文档为deploy-remote-build.ps1

问题修复:
- 公网用户上传从200-900KB/s提升到预期3-5MB/s
- 内网用户上传更稳定,无连接重置错误
- UploadProgress组件剩余时间计算修正

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-27 09:33:09 +08:00

156 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 批量上传处理函数
import { chunkUpload } from '@/lib/chunk-upload'
import type { BatchFileItem, BatchUploadStatus } from './upload-types'
import { PREVIEW_OPTIONAL_TYPES, CHUNK_THRESHOLD } from './upload-constants'
import { completeBatchUpload, fileToBase64String } from './special-batch-uploaders'
import { compressPreviewImage } from './image-processor'
import { shouldPassRenderer, requiresRenderer, shouldAutoFillGeneral } from '@/lib/material-validation'
import { parseApiError } from '@/lib/error-handler'
// 批量上传单个文件的处理函数
export async function uploadBatchFile(
item: BatchFileItem,
fileIndex: number,
materialType: string,
categoryName: string,
sceneName: string,
platformName: string,
productCategoryName: string,
renderer: string,
onProgress: (progress: number, speed?: number) => void,
onStatusUpdate: (status: Partial<BatchUploadStatus['fileStatuses'][0]>) => void,
onRequest: (xhr: XMLHttpRequest | null) => void,
isCancelled: () => boolean,
startTime: number
): Promise<{ success: boolean; uploadId?: string; error?: string }> {
try {
// 构建元数据
const metadata: any = {
materialType,
productModel: item.productModel,
categoryName: shouldAutoFillGeneral(materialType)
? '通用'
: item.categoryName,
sceneName: shouldAutoFillGeneral(materialType)
? '通用'
: item.sceneName,
platformName: item.platformName || platformName || '',
productCategoryName: item.productCategoryName || productCategoryName || '',
}
// 添加渲染器(3D场景)
if (requiresRenderer(materialType) && item.renderer) {
metadata.renderer = item.renderer
}
// 处理预览图(如果需要)- 先压缩再转换为base64
// 详情模板:预览图可选,如果上传了则使用
const needsPreview = !PREVIEW_OPTIONAL_TYPES.includes(materialType)
let compressedPreview: File | undefined
if (needsPreview && item.previewImage) {
compressedPreview = await compressPreviewImage(item.previewImage)
metadata.previewImage = await fileToBase64String(compressedPreview)
} else if (materialType === '详情模板' && item.previewImage) {
// 详情模板:如果用户上传了预览图,也处理
compressedPreview = await compressPreviewImage(item.previewImage)
metadata.previewImage = await fileToBase64String(compressedPreview)
}
// 判断是否需要分片上传
const useChunk = item.file.size >= CHUNK_THRESHOLD
if (useChunk) {
// 分片上传(chunkSize使用默认配置:内网20MB,公网10MB)
const result = await chunkUpload(item.file, metadata, {
onProgress,
onRequest,
skipComplete: true,
maxRetries: Infinity,
})
onStatusUpdate({ status: 'processing', progress: 100, uploadId: result?.uploadId })
if (result?.uploadId) {
// 后台异步处理
completeBatchUpload(result.uploadId, metadata).catch((err) => {
console.error(`[Upload] 文件 ${item.file.name} 后台处理失败:`, err)
onStatusUpdate({ status: 'failed', error: '后台处理失败' })
})
}
return { success: true, uploadId: result?.uploadId }
} else {
// 小文件直接上传(使用XMLHttpRequest以便取消和显示进度)
const formData = new FormData()
formData.append('zipFile', item.file)
formData.append('productModel', item.productModel)
formData.append('categoryName', metadata.categoryName)
formData.append('sceneName', metadata.sceneName)
formData.append('platformName', metadata.platformName)
formData.append('productCategoryName', metadata.productCategoryName)
formData.append('materialType', materialType)
// 统一处理renderer字段:根据素材类型判断是否需要传递
const rendererInfo = shouldPassRenderer(materialType, metadata.renderer)
if (rendererInfo.shouldPass) {
formData.append('renderer', rendererInfo.value)
}
// 小文件上传也使用压缩后的预览图
if (compressedPreview) {
formData.append('previewImage', compressedPreview)
} else if (item.previewImage) {
// 如果没有压缩过(不需要预览图的类型),使用原图
formData.append('previewImage', item.previewImage)
}
const xhr = new XMLHttpRequest()
onRequest(xhr)
await new Promise<void>((resolve, reject) => {
xhr.upload.onprogress = (e) => {
if (e.lengthComputable && !isCancelled()) {
const progress = Math.round((e.loaded / e.total) * 100)
const elapsed = (Date.now() - startTime) / 1000
const speed = e.loaded / elapsed
onProgress(progress, speed)
}
}
xhr.onload = () => {
onRequest(null)
if (xhr.status >= 200 && xhr.status < 300) {
resolve()
} else {
reject(new Error(`上传失败: ${xhr.statusText}`))
}
}
xhr.onerror = () => {
onRequest(null)
reject(new Error('网络错误'))
}
xhr.onabort = () => {
onRequest(null)
reject(new Error('上传已取消'))
}
xhr.open('POST', '/api/materials/upload')
xhr.send(formData)
})
onStatusUpdate({ status: 'processing', progress: 100 })
return { success: true }
}
} catch (error: any) {
console.error(`[Upload] 文件 ${item.file.name} 上传失败:`, error)
const { message } = parseApiError(error)
onStatusUpdate({
status: 'failed',
error: message
})
return { success: false, error: message }
}
}