diff --git a/components/UploadModal.tsx b/components/UploadModal.tsx index b6165ae..696b4c3 100644 --- a/components/UploadModal.tsx +++ b/components/UploadModal.tsx @@ -921,7 +921,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) { }) }, skipComplete: true, - maxRetries: Infinity, + maxRetries: 5, }) totalUploadedBytes += item.file.size diff --git a/components/upload/batch-upload-handlers.ts b/components/upload/batch-upload-handlers.ts index 4a8b3ba..7656db1 100644 --- a/components/upload/batch-upload-handlers.ts +++ b/components/upload/batch-upload-handlers.ts @@ -65,7 +65,7 @@ export async function uploadBatchFile( onProgress, onRequest, skipComplete: true, - maxRetries: Infinity, + maxRetries: 5, }) onStatusUpdate({ status: 'processing', progress: 100, uploadId: result?.uploadId }) diff --git a/components/upload/special-batch-uploaders.ts b/components/upload/special-batch-uploaders.ts index 4d6ba78..5c6c373 100644 --- a/components/upload/special-batch-uploaders.ts +++ b/components/upload/special-batch-uploaders.ts @@ -215,7 +215,7 @@ export async function uploadBatchMusic( onProgress(totalProgress) }, skipComplete: true, - maxRetries: Infinity, + maxRetries: 5, }) // 等待completeBatchUpload完成,确保素材记录被创建 @@ -289,7 +289,7 @@ export async function uploadBatchVideo( onProgress(totalProgress) }, skipComplete: true, - maxRetries: Infinity, + maxRetries: 5, }) // 等待completeBatchUpload完成,确保素材记录被创建 @@ -368,7 +368,7 @@ export async function uploadBatch3D( onProgress(totalProgress) }, skipComplete: true, - maxRetries: Infinity, + maxRetries: 5, }) if (result?.uploadId) { @@ -615,7 +615,7 @@ export async function uploadBatchBrandVI( onProgress(totalProgress) }, skipComplete: true, - maxRetries: Infinity, + maxRetries: 5, }) // 等待completeBatchUpload完成,确保素材记录被创建 diff --git a/docs/NETWORK_TOPOLOGY.md b/docs/NETWORK_TOPOLOGY.md index b040142..ac0c0ba 100644 --- a/docs/NETWORK_TOPOLOGY.md +++ b/docs/NETWORK_TOPOLOGY.md @@ -32,6 +32,7 @@ - 首页自动切到 `192.168.58.20:4320` 的逻辑已经下线。 - `180.97.221.140:2320` 不再作为正式入口链路的一部分。 - `sc.jiansun.vip` 的 nginx 必须放开大文件上传:`client_max_body_size 2048m`,并设置较长 `proxy_read_timeout` / `proxy_send_timeout`。2026-05-07 曾因该配置缺失导致 `/api/materials/upload/chunk` 50MB 分片被 nginx 直接返回 `413`,表现为素材上传一直卡住。 +- 当前前端分片策略是内网 20MB / 2 并发,公网 10MB / 1 并发;公网不要再默认使用 50MB 大分片,否则会放大 nginx + FRP 链路抖动下的 `499` / `502` 和上传 100% 后长时间等待问题。 ### 2.2 资源 / S3 diff --git a/hooks/useBatchFilesUpload.ts b/hooks/useBatchFilesUpload.ts index 02205d1..68ad58a 100644 --- a/hooks/useBatchFilesUpload.ts +++ b/hooks/useBatchFilesUpload.ts @@ -246,7 +246,7 @@ export function useBatchFilesUpload( }) }, skipComplete: true, - maxRetries: Infinity, + maxRetries: 5, }) totalUploadedBytes += item.file.size diff --git a/lib/chunk-upload-config.ts b/lib/chunk-upload-config.ts index 31862d4..5caa048 100644 --- a/lib/chunk-upload-config.ts +++ b/lib/chunk-upload-config.ts @@ -1,44 +1,50 @@ /** * 分片上传配置 - * 统一使用50MB大分片,提升上传效率 + * 按网络环境选择分片大小和并发数。 * * 策略: - * - 统一使用50MB分片(内网公网一致) - * - 请求数减少80%(1GB文件从100个减少到20个) - * - TCP利用率更高(单个分片传输时间更长) - * - * 优势: - * - 减少网络开销(请求数少) - * - 提升传输效率(TCP慢启动影响更小) - * - 降低服务器压力(并发处理更少) + * - 内网:20MB 分片,2 并发 + * - 公网:10MB 分片,1 并发 + * - 公网链路经过 nginx + FRP,过大的单请求会放大连接抖动和 499/502 风险 */ import { getNetworkStatus, initializeNetwork } from './network' /** - * 获取最优分片大小(统一50MB) + * 获取最优分片大小 * @returns 分片大小(字节) */ export async function getOptimalChunkSize(): Promise { - // 统一使用50MB分片(内网公网一致) - // 优势: - // 1. 减少请求数(1GB文件从100个减少到20个) - // 2. TCP利用率更高(单个分片传输时间更长) - // 3. 降低网络开销 - const chunkSize = 50 * 1024 * 1024 // 50MB + let network = getNetworkStatus() - console.log(`[ChunkConfig] 使用 50MB 分片(统一配置)`) + if (!network.lastChecked) { + try { + network = await initializeNetwork() + } catch (error) { + console.warn('[ChunkConfig] 网络检测失败,使用公网保守分片(10MB):', error) + return 10 * 1024 * 1024 + } + } + + const chunkSize = network.isInternal + ? 20 * 1024 * 1024 + : 10 * 1024 * 1024 + + console.log(`[ChunkConfig] 检测到${network.isInternal ? '内网' : '公网'}访问,使用 ${formatChunkSize(chunkSize)} 分片`) return chunkSize } /** - * 同步获取分片大小(统一50MB) + * 同步获取分片大小 * @returns 分片大小(字节) */ export function getChunkSizeSync(): number { - const chunkSize = 50 * 1024 * 1024 // 50MB - console.log(`[ChunkConfig] 使用 50MB 分片(同步)`) + const network = getNetworkStatus() + const chunkSize = network.isInternal + ? 20 * 1024 * 1024 + : 10 * 1024 * 1024 + console.log(`[ChunkConfig] 使用 ${formatChunkSize(chunkSize)} 分片(同步)`) return chunkSize } @@ -55,7 +61,7 @@ export function getChunkTimeout(chunkSize: number): number { // 加上100%缓冲,确保不会轻易超时 const timeout = Math.ceil(baseTimeout * 2) - // 最小5分钟,最大10分钟(50MB分片在300KB/s下需要约5.5分钟) + // 最小5分钟,最大10分钟 return Math.max(300000, Math.min(600000, timeout)) } @@ -71,12 +77,12 @@ export function formatChunkSize(chunkSize: number): string { /** * 获取最优并发数(根据网络环境和系统负载动态调整) * - * 动态调整策略(1~3并发): - * - 内网用户:2-3并发 - * - 公网用户:2并发 - * - 原因:现在已配置 proxyClientMaxBodySize = 100MB,可以处理 50MB 分片 + * 动态调整策略: + * - 内网用户:2 并发 + * - 公网用户:1 并发 + * - 公网链路经过 FRP,优先减少大请求并发,避免上传到 100% 后等待/重试过久 * - * @returns 并发数(1-3) + * @returns 并发数(1-2) */ export async function getOptimalConcurrency(): Promise { // 获取当前网络状态 @@ -96,17 +102,17 @@ export async function getOptimalConcurrency(): Promise { let concurrency: number if (network.isInternal) { - // 内网用户:3并发 - concurrency = 3 - } else { - // 公网用户:2并发 + // 内网用户:2并发 concurrency = 2 + } else { + // 公网用户:1并发 + concurrency = 1 } - // 确保在1-3范围内 - concurrency = Math.max(1, Math.min(3, concurrency)) + // 确保在1-2范围内 + concurrency = Math.max(1, Math.min(2, concurrency)) - console.log(`[ChunkConfig] 检测到${network.isInternal ? '内网' : '公网'}访问,使用 ${concurrency} 个并发(1~3范围)`) + console.log(`[ChunkConfig] 检测到${network.isInternal ? '内网' : '公网'}访问,使用 ${concurrency} 个并发(1~2范围)`) return concurrency } @@ -119,23 +125,23 @@ export async function getOptimalConcurrency(): Promise { export function getConcurrencySync(): number { const network = getNetworkStatus() - // 如果还没检测过,返回保守值(2并发) + // 如果还没检测过,返回公网保守值(1并发) if (!network.lastChecked) { - console.warn('[ChunkConfig] 网络状态未检测,使用默认并发数(2)') - return 2 + console.warn('[ChunkConfig] 网络状态未检测,使用默认并发数(1)') + return 1 } // 同步版本:使用固定策略(不依赖异步检测) let concurrency: number if (network.isInternal) { - concurrency = 3 // 内网:3并发 + concurrency = 2 // 内网:2并发 } else { - concurrency = 2 // 公网:2并发 + concurrency = 1 // 公网:1并发 } - // 确保在1-3范围内 - concurrency = Math.max(1, Math.min(3, concurrency)) + // 确保在1-2范围内 + concurrency = Math.max(1, Math.min(2, concurrency)) return concurrency } diff --git a/lib/chunk-upload.ts b/lib/chunk-upload.ts index 8ea3253..8a3adb4 100644 --- a/lib/chunk-upload.ts +++ b/lib/chunk-upload.ts @@ -7,7 +7,7 @@ interface ChunkUploadOptions { chunkSize?: number // 每块大小(字节),默认根据网络环境自动选择(内网20MB,公网10MB) onProgress?: (progress: number, speed?: number) => void // progress: 0-100, speed: 字节/秒 onError?: (error: Error) => void - maxRetries?: number // 最大重试次数,默认无限重试 + maxRetries?: number // 最大重试次数,默认5次 retryDelay?: number // 重试延迟(毫秒),默认1000ms onStageChange?: ( stage: 'uploading' | 'processing', @@ -383,7 +383,7 @@ export async function chunkUpload( const { chunkSize = defaultChunkSize, // 根据网络环境自动选择(内网20MB,公网10MB) onProgress, - maxRetries = Infinity, // 默认无限重试 + maxRetries = 5, retryDelay = 1000, onStageChange, onRequest,