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

124 lines
4.5 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 { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import path from 'path'
import fs from 'fs/promises'
import { createWriteStream, createReadStream } from 'fs'
import { pipeline } from 'stream/promises'
import { getUploadSession, deleteUploadSession } from '@/lib/upload-sessions'
import { sanitizeFileName } from '@/lib/image-processor'
export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'
export const maxDuration = 10800 // 最长3小时
export async function POST(req: NextRequest) {
try {
const session = await getServerSession(authOptions)
if (!session) {
return NextResponse.json({ error: '请先登录' }, { status: 401 })
}
const { uploadId, originalFileName } = await req.json()
if (!uploadId) {
return NextResponse.json({ error: '缺少 uploadId' }, { status: 400 })
}
const uploadDir = process.env.UPLOAD_DIR || './uploads'
const tempDir = path.join(uploadDir, 'temp', uploadId)
// 读取会话
const uploadSession = await getUploadSession(uploadId)
if (!uploadSession) {
return NextResponse.json({ error: '上传会话不存在或已过期' }, { status: 404 })
}
// 会话归属校验
if (uploadSession.userId !== (session.user as any).id) {
return NextResponse.json({ error: '无权访问此上传会话' }, { status: 403 })
}
const totalChunks = uploadSession.totalChunks
const fileSize = uploadSession.fileSize
// 收集分片文件
const chunkFiles = []
for (let i = 0; i < totalChunks; i++) {
const chunkPath = path.join(tempDir, `chunk-${i}`)
chunkFiles.push(chunkPath)
}
// 校验分片是否齐全
for (const chunkPath of chunkFiles) {
try {
await fs.access(chunkPath)
} catch {
return NextResponse.json({ error: '分片不完整,请重新上传' }, { status: 400 })
}
}
// 合并文件
await fs.mkdir(tempDir, { recursive: true })
// 处理文件名:提取纯文件名(去除路径),并清理特殊字符,避免中文路径或特殊字符导致的问题
const rawFileName = originalFileName || uploadSession.fileName || `upload-${uploadId}`
const baseFileName = path.basename(rawFileName) // 提取文件名部分,去除路径
const mergedFileName = sanitizeFileName(baseFileName) || `upload-${uploadId}` // 清理特殊字符
const mergedPath = path.join(tempDir, `merged-${mergedFileName}`)
const mergedStream = createWriteStream(mergedPath)
for (const chunkPath of chunkFiles) {
const chunkStream = createReadStream(chunkPath)
await pipeline(chunkStream, mergedStream, { end: false })
}
// 手动结束写入并等待关闭
mergedStream.end()
await new Promise<void>((resolve, reject) => {
mergedStream.on('close', () => resolve())
mergedStream.on('error', (err) => reject(err))
})
// 校验大小
const stat = await fs.stat(mergedPath)
if (stat.size !== fileSize) {
return NextResponse.json(
{ error: `合并后的文件大小不匹配,期望 ${fileSize} 实际 ${stat.size}` },
{ status: 400 }
)
}
// 清理分片,保留合并后的文件供后续正式上传使用
// 使用统一的文件删除工具
const { deleteFiles } = await import('@/lib/file-cleanup')
const result = await deleteFiles(chunkFiles)
if (result.failedCount > 0) {
console.warn('[UploadComplete] 清理分片失败:', result.errors)
}
// 上传完成后,立即删除会话(不再占用Redis内存)
try {
const { deleteUploadSession } = await import('@/lib/upload-sessions-redis')
await deleteUploadSession(uploadId)
console.log('[UploadComplete] 已删除会话:', uploadId)
} catch (deleteError) {
console.warn('[UploadComplete] 删除会话失败(忽略):', deleteError)
}
// 返回合并结果
return NextResponse.json({
message: '分片合并完成',
existingFilePath: mergedPath,
fileName: mergedFileName,
fileSize: stat.size,
uploadId,
})
} catch (error) {
console.error('[UploadComplete] Error:', error)
return NextResponse.json({ error: '合并分片失败' }, { status: 500 })
}
}