162 lines
4.7 KiB
TypeScript
162 lines
4.7 KiB
TypeScript
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 = request.nextUrl.searchParams
|
||
const isSSE = searchParams.get('sse') === 'true'
|
||
|
||
if (isSSE) {
|
||
// Server-Sent Events 实时推送
|
||
const encoder = new TextEncoder()
|
||
|
||
const stream = new ReadableStream({
|
||
async start(controller) {
|
||
// 发送初始弹幕(限制数量,减少数据传输)
|
||
const initialDanmakus = await prisma.danmaku.findMany({
|
||
orderBy: { createdAt: 'desc' },
|
||
take: 30, // 减少初始数据量
|
||
select: {
|
||
id: true,
|
||
content: true,
|
||
username: true,
|
||
createdAt: true,
|
||
}
|
||
})
|
||
|
||
controller.enqueue(
|
||
encoder.encode(`data: ${JSON.stringify(initialDanmakus)}\n\n`)
|
||
)
|
||
|
||
// 记录最后一条弹幕的ID,避免重复推送
|
||
let lastDanmakuId = initialDanmakus[0]?.id || null
|
||
|
||
// 轮询新弹幕(优化:只查询新弹幕)
|
||
const interval = setInterval(async () => {
|
||
try {
|
||
const latestDanmaku = await prisma.danmaku.findFirst({
|
||
where: lastDanmakuId ? {
|
||
id: { not: lastDanmakuId }
|
||
} : undefined,
|
||
orderBy: { createdAt: 'desc' },
|
||
select: {
|
||
id: true,
|
||
content: true,
|
||
username: true,
|
||
createdAt: true,
|
||
}
|
||
})
|
||
|
||
if (latestDanmaku) {
|
||
lastDanmakuId = latestDanmaku.id
|
||
controller.enqueue(
|
||
encoder.encode(`data: ${JSON.stringify(latestDanmaku)}\n\n`)
|
||
)
|
||
}
|
||
} catch (error) {
|
||
console.error('[Danmaku] SSE error:', error)
|
||
// 出错时关闭连接,避免资源泄漏
|
||
clearInterval(interval)
|
||
controller.close()
|
||
}
|
||
}, 2000) // 增加轮询间隔到2秒,减少数据库压力
|
||
|
||
// 清理
|
||
request.signal.addEventListener('abort', () => {
|
||
clearInterval(interval)
|
||
controller.close()
|
||
})
|
||
}
|
||
})
|
||
|
||
return new NextResponse(stream, {
|
||
headers: {
|
||
'Content-Type': 'text/event-stream',
|
||
'Cache-Control': 'no-cache',
|
||
'Connection': 'keep-alive',
|
||
},
|
||
})
|
||
}
|
||
|
||
// 普通GET请求(支持分页)
|
||
const limit = Math.min(parseInt(searchParams.get('limit') || '50', 10), 100) // 最大100条
|
||
const danmakus = await prisma.danmaku.findMany({
|
||
orderBy: { createdAt: 'desc' },
|
||
take: limit,
|
||
select: {
|
||
id: true,
|
||
content: true,
|
||
username: true,
|
||
createdAt: true,
|
||
// 不返回userId,保护隐私
|
||
}
|
||
})
|
||
|
||
return NextResponse.json(danmakus)
|
||
} 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 randomNames = [
|
||
'神秘访客', '路过的设计师', '热心网友', '匿名用户',
|
||
'悄悄路过', '神秘嘉宾', '隐身大佬', '低调看客',
|
||
'默默围观', '潜水员', '匿名网友', '神秘人士'
|
||
]
|
||
const anonymousName = randomNames[Math.floor(Math.random() * randomNames.length)]
|
||
|
||
const danmaku = await prisma.danmaku.create({
|
||
data: {
|
||
content: content.trim(),
|
||
username: anonymousName, // 使用匿名名称
|
||
userId: (session.user as any).id
|
||
}
|
||
})
|
||
|
||
return NextResponse.json(danmaku)
|
||
} catch (error) {
|
||
console.error('Send danmaku error:', error)
|
||
return NextResponse.json(
|
||
{ error: '发送弹幕失败' },
|
||
{ status: 500 }
|
||
)
|
||
}
|
||
}
|
||
|