主要改动: 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>
177 lines
5.4 KiB
TypeScript
177 lines
5.4 KiB
TypeScript
// 弹幕API - 支持SSE实时推送和普通查询
|
||
import { NextRequest, NextResponse } from 'next/server';
|
||
import { getServerSession } from 'next-auth';
|
||
import { authOptions } from '@/lib/auth';
|
||
import { prisma } from '@/lib/prisma';
|
||
|
||
// GET - 获取弹幕列表(支持SSE实时推送)
|
||
export async function GET(request: NextRequest) {
|
||
try {
|
||
const { searchParams } = new URL(request.url);
|
||
const isSSE = searchParams.get('sse') === 'true';
|
||
|
||
// SSE模式 - 实时推送弹幕
|
||
if (isSSE) {
|
||
const encoder = new TextEncoder();
|
||
|
||
const stream = new ReadableStream({
|
||
async start(controller) {
|
||
let interval: NodeJS.Timeout | null = null;
|
||
let lastActivity = Date.now();
|
||
const maxIdleTime = 5 * 60 * 1000; // 5分钟无活动自动关闭
|
||
const maxConnectionTime = 30 * 60 * 1000; // 最长连接30分钟
|
||
const connectionStartTime = Date.now();
|
||
|
||
// 清理函数
|
||
const cleanup = () => {
|
||
if (interval) {
|
||
clearInterval(interval);
|
||
interval = null;
|
||
}
|
||
try {
|
||
controller.close();
|
||
} catch (err) {
|
||
// 忽略关闭错误(可能已经关闭)
|
||
}
|
||
console.log('[Danmaku SSE] Connection closed and cleaned up');
|
||
};
|
||
|
||
try {
|
||
// 首次发送最近50条弹幕
|
||
const recentDanmaku = await prisma.danmaku.findMany({
|
||
orderBy: { createdAt: 'desc' },
|
||
take: 50,
|
||
});
|
||
|
||
controller.enqueue(
|
||
encoder.encode(`data: ${JSON.stringify(recentDanmaku)}\n\n`)
|
||
);
|
||
|
||
// 每秒检查新弹幕
|
||
interval = setInterval(async () => {
|
||
try {
|
||
// 检查是否超时(无活动或连接时间过长)
|
||
const now = Date.now();
|
||
if (now - lastActivity > maxIdleTime) {
|
||
console.log('[Danmaku SSE] Connection idle timeout, closing');
|
||
cleanup();
|
||
return;
|
||
}
|
||
|
||
if (now - connectionStartTime > maxConnectionTime) {
|
||
console.log('[Danmaku SSE] Connection time limit reached, closing');
|
||
cleanup();
|
||
return;
|
||
}
|
||
|
||
const newDanmaku = await prisma.danmaku.findMany({
|
||
orderBy: { createdAt: 'desc' },
|
||
take: 1,
|
||
});
|
||
|
||
if (newDanmaku.length > 0) {
|
||
lastActivity = now;
|
||
controller.enqueue(
|
||
encoder.encode(`data: ${JSON.stringify(newDanmaku[0])}\n\n`)
|
||
);
|
||
}
|
||
} catch (error) {
|
||
console.error('[Danmaku SSE] Error in interval:', error);
|
||
cleanup();
|
||
}
|
||
}, 1000);
|
||
|
||
// 客户端断开连接时清理
|
||
request.signal.addEventListener('abort', () => {
|
||
console.log('[Danmaku SSE] Client aborted connection');
|
||
cleanup();
|
||
});
|
||
} catch (error) {
|
||
console.error('[Danmaku SSE] Error in start:', error);
|
||
cleanup();
|
||
}
|
||
},
|
||
});
|
||
|
||
return new NextResponse(stream, {
|
||
headers: {
|
||
'Content-Type': 'text/event-stream',
|
||
'Cache-Control': 'no-cache',
|
||
'Connection': 'keep-alive',
|
||
},
|
||
});
|
||
}
|
||
|
||
// 普通模式 - 返回弹幕列表
|
||
const limit = parseInt(searchParams.get('limit') || '50');
|
||
const danmaku = await prisma.danmaku.findMany({
|
||
orderBy: { createdAt: 'desc' },
|
||
take: limit,
|
||
});
|
||
|
||
return NextResponse.json(danmaku);
|
||
} catch (error) {
|
||
console.error('Get danmaku error:', error);
|
||
return NextResponse.json(
|
||
{ error: '获取弹幕失败' },
|
||
{ status: 500 }
|
||
);
|
||
}
|
||
}
|
||
|
||
// POST - 发送弹幕
|
||
export async function POST(request: NextRequest) {
|
||
try {
|
||
const session = await getServerSession(authOptions);
|
||
if (!session) {
|
||
return NextResponse.json(
|
||
{ error: '请先登录' },
|
||
{ status: 401 }
|
||
);
|
||
}
|
||
|
||
const { content } = await request.json();
|
||
|
||
// 验证弹幕内容
|
||
if (!content || content.trim().length === 0) {
|
||
return NextResponse.json(
|
||
{ error: '弹幕内容不能为空' },
|
||
{ status: 400 }
|
||
);
|
||
}
|
||
|
||
if (content.length > 100) {
|
||
return NextResponse.json(
|
||
{ error: '弹幕内容不能超过100字' },
|
||
{ status: 400 }
|
||
);
|
||
}
|
||
|
||
// 随机匿名昵称
|
||
const anonymousNames = [
|
||
'神秘访客', '路过的设计师', '热心网友', '匿名用户',
|
||
'悄悄路过', '神秘嘉宾', '隐身大佬', '低调看客',
|
||
'默默围观', '潜水员', '匿名网友', '神秘人士'
|
||
];
|
||
const randomName = anonymousNames[Math.floor(Math.random() * anonymousNames.length)];
|
||
|
||
// 创建弹幕
|
||
const userId = (session.user as any).id;
|
||
const danmaku = await prisma.danmaku.create({
|
||
data: {
|
||
content: content.trim(),
|
||
username: randomName,
|
||
userId: userId,
|
||
},
|
||
});
|
||
|
||
return NextResponse.json(danmaku);
|
||
} catch (error) {
|
||
console.error('Send danmaku error:', error);
|
||
return NextResponse.json(
|
||
{ error: '发送弹幕失败' },
|
||
{ status: 500 }
|
||
);
|
||
}
|
||
}
|