import { prisma } from "@/lib/prisma"; import { Prisma } from "@prisma/client"; import { NextRequest, NextResponse } from "next/server"; export async function GET( request: NextRequest, { params }: { params: Promise<{ awemeId: string }> } ) { const awemeId = (await params).awemeId; const searchParams = request.nextUrl.searchParams; const skip = parseInt(searchParams.get("skip") || "0", 10); const take = parseInt(searchParams.get("take") || "20", 10); const mode = (searchParams.get("mode") || "").toLowerCase(); // ranked 模式参数(均为可选,提供合理默认值) const seed = searchParams.get("seed") || new Date().toISOString().slice(0, 10); // 默认按日期稳定 const snapshotIso = searchParams.get("snapshot"); const snapshot = snapshotIso ? new Date(snapshotIso) : new Date(); // 用于时间衰减的基准时间,确保单次会话稳定 const halfLifeHours = parseFloat(searchParams.get("halfLifeHours") || "24"); const wPop = parseFloat(searchParams.get("wPop") || "1"); // 热度权重 const wTime = parseFloat(searchParams.get("wTime") || "2"); // 时间衰减权重 const wJit = parseFloat(searchParams.get("wJit") || "10"); // 随机扰动权重(稳定随机) try { // 查找是视频还是图文 const [video, post] = await Promise.all([ prisma.video.findUnique({ where: { aweme_id: awemeId }, select: { aweme_id: true }, }), prisma.imagePost.findUnique({ where: { aweme_id: awemeId }, select: { aweme_id: true }, }), ]); if (!video && !post) { return NextResponse.json({ error: "作品不存在" }, { status: 404 }); } // 构建查询条件 const where = video ? { videoId: awemeId } : { imagePostId: awemeId }; // ranked 模式:按「热度 + 时间衰减 + 稳定随机扰动」打分排序;否则使用稳定的时间排序 if (mode === "ranked") { const total = await prisma.comment.count({ where }); // 计算稳定随机扰动:基于 (cid, seed) 的 md5 -> 取前4字节 -> 映射到 [0,1) const jitterExpr = Prisma.sql`( ( get_byte(decode(md5(c."cid" || ':' || ${seed}), 'hex'), 0)::double precision * 16777216.0 + get_byte(decode(md5(c."cid" || ':' || ${seed}), 'hex'), 1)::double precision * 65536.0 + get_byte(decode(md5(c."cid" || ':' || ${seed}), 'hex'), 2)::double precision * 256.0 + get_byte(decode(md5(c."cid" || ':' || ${seed}), 'hex'), 3)::double precision ) / 4294967295.0 )`; const snapshotTs = snapshot.toISOString(); // popularity: ln(1 + digg_count) // time decay: exp(- age_hours / halfLifeHours),使用固定 snapshot 基准保证会话内稳定 const scoreExpr = Prisma.sql`( ${wPop} * ln(1 + c."digg_count"::numeric) + ${wTime} * exp(- (extract(epoch from (${snapshotTs}::timestamptz - c."created_at")) / 3600.0) / ${halfLifeHours}) + ${wJit} * ${jitterExpr} )`; const whereField = (await video) ? Prisma.sql`c."videoId"` : Prisma.sql`c."imagePostId"`; const rows: Array<{ cid: string; text: string; created_at: Date; digg_count: bigint; nickname: string; avatar_url: string | null; }> = await prisma.$queryRaw( Prisma.sql` SELECT c."cid", c."text", c."created_at", c."digg_count", u."nickname", u."avatar_url" FROM "Comment" c JOIN "CommentUser" u ON u."id" = c."userId" WHERE ${whereField} = ${awemeId} ORDER BY ${scoreExpr} DESC, c."created_at" DESC, c."cid" ASC OFFSET ${skip} LIMIT ${take} ` ); // 批量查询每条评论的配图/贴纸 const cids = rows.map(r => r.cid); const images = cids.length ? await prisma.commentImage.findMany({ where: { commentId: { in: cids } }, orderBy: { order: 'asc' }, select: { commentId: true, url: true, width: true, height: true, order: true }, }) : []; const group = new Map(); for (const img of images) { const arr = group.get(img.commentId) || []; arr.push({ url: img.url, width: img.width, height: img.height, order: img.order }); group.set(img.commentId, arr); } const formattedComments = rows.map((c) => ({ cid: c.cid, text: c.text, created_at: c.created_at, digg_count: Number(c.digg_count), user: { nickname: c.nickname, avatar_url: c.avatar_url || undefined, }, images: (group.get(c.cid) || []).sort((a,b)=>a.order-b.order).map(i=>({ url: i.url, width: i.width ?? undefined, height: i.height ?? undefined })), })); return NextResponse.json({ comments: formattedComments, total, hasMore: skip + take < total, // 回传用于稳定排序的参数,前端可在后续请求中透传 seed, snapshot: snapshotTs, weights: { wPop, wTime, wJit, halfLifeHours }, mode: "ranked", }); } // 默认模式:获取评论总数和分页数据(使用稳定且不可变的排序,避免因 digg_count 变化导致翻页重复/遗漏) const [total, comments] = await Promise.all([ prisma.comment.count({ where }), prisma.comment.findMany({ where, orderBy: [ { created_at: "desc" }, { cid: "asc" }, ], include: { user: true, images: { orderBy: { order: 'asc' }, select: { url: true, width: true, height: true } } }, skip, take, }), ]); const formattedComments = comments.map((c) => ({ cid: c.cid, text: c.text, created_at: c.created_at, digg_count: Number(c.digg_count), user: { nickname: c.user.nickname, avatar_url: c.user.avatar_url, }, images: (c.images || []).map(i=>({ url: i.url, width: i.width ?? undefined, height: i.height ?? undefined })), })); return NextResponse.json({ comments: formattedComments, total, hasMore: skip + take < total, }); } catch (error) { console.error("获取评论失败:", error); return NextResponse.json( { error: "获取评论失败" }, { status: 500 } ); } }