Files
zhuiguang-ai/src/app/api/comments/route.ts
T

207 lines
6.5 KiB
TypeScript

import { prisma } from "@/lib/prisma";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { NextResponse } from "next/server";
import { rewardPoints } from "@/lib/points-reward";
export async function GET(req: Request) {
const { searchParams } = new URL(req.url);
const toolId = searchParams.get("toolId");
const skillId = searchParams.get("skillId");
const parentId = searchParams.get("parentId");
const page = parseInt(searchParams.get("page") || "1", 10);
const pageSize = parseInt(searchParams.get("pageSize") || "20", 10);
if (!toolId && !skillId) {
return NextResponse.json({ error: "必须提供 toolId 或 skillId" }, { status: 400 });
}
const where: any = { status: "ACTIVE" };
if (toolId) where.toolId = parseInt(toolId);
if (skillId) where.skillId = parseInt(skillId);
if (parentId) where.parentId = parseInt(parentId);
else where.parentId = null;
const [comments, totalCount] = await Promise.all([
prisma.comment.findMany({
where,
include: {
user: { select: { id: true, name: true, email: true, avatarUrl: true, level: true, points: true } },
likes: { select: { userId: true } },
replies: {
where: { status: "ACTIVE" },
include: {
user: { select: { id: true, name: true, email: true, avatarUrl: true, level: true, points: true } },
likes: { select: { userId: true } },
},
orderBy: { createdAt: "asc" },
take: 5,
},
_count: { select: { replies: true } },
},
orderBy: [
{ isPinned: "desc" },
{ createdAt: "desc" },
],
skip: (page - 1) * pageSize,
take: pageSize,
}),
prisma.comment.count({ where }),
]);
const session = await getServerSession(authOptions);
const currentUserId = session?.user?.id ? parseInt(session.user.id) : null;
const formatted = comments.map((c: any) => ({
id: c.id,
content: c.content,
rating: c.rating,
likeCount: c.likeCount,
isPinned: c.isPinned,
createdAt: c.createdAt,
updatedAt: c.updatedAt,
replyCount: c._count.replies,
liked: currentUserId ? c.likes.some((l: any) => l.userId === currentUserId) : false,
user: {
id: c.user.id,
name: c.user.name || c.user.email.split("@")[0],
avatarUrl: c.user.avatarUrl,
level: c.user.level,
points: c.user.points,
},
replies: c.replies.map((r: any) => ({
id: r.id,
content: r.content,
likeCount: r.likeCount,
createdAt: r.createdAt,
liked: currentUserId ? r.likes.some((l: any) => l.userId === currentUserId) : false,
user: {
id: r.user.id,
name: r.user.name || r.user.email.split("@")[0],
avatarUrl: r.user.avatarUrl,
level: r.user.level,
points: r.user.points,
},
})),
}));
return NextResponse.json({ comments: formatted, totalCount, page, pageSize });
}
export async function POST(req: Request) {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json({ error: "请先登录" }, { status: 401 });
}
const body = await req.json();
const { toolSlug, skillSlug, content, rating, parentId } = body;
const userId = parseInt(session.user.id);
if (!content || !content.trim()) {
return NextResponse.json({ error: "评论内容不能为空" }, { status: 400 });
}
if (content.trim().length > 2000) {
return NextResponse.json({ error: "评论内容不能超过2000个字符" }, { status: 400 });
}
let toolId: number | undefined;
let skillId: number | undefined;
if (toolSlug) {
const tool = await prisma.tool.findUnique({ where: { slug: toolSlug } });
if (!tool) return NextResponse.json({ error: "工具不存在" }, { status: 404 });
toolId = tool.id;
} else if (skillSlug) {
const skill = await prisma.skill.findUnique({ where: { slug: skillSlug } });
if (!skill) return NextResponse.json({ error: "技能不存在" }, { status: 404 });
skillId = skill.id;
} else {
return NextResponse.json({ error: "必须提供 toolSlug 或 skillSlug" }, { status: 400 });
}
if (!parentId && rating && (rating < 1 || rating > 5)) {
return NextResponse.json({ error: "评分必须为1-5" }, { status: 400 });
}
const comment = await prisma.$transaction(async (tx) => {
const newComment = await tx.comment.create({
data: {
userId,
toolId,
skillId,
parentId: parentId || null,
content: content.trim(),
rating: parentId ? null : rating || null,
},
include: {
user: { select: { id: true, name: true, email: true, avatarUrl: true, level: true, points: true } },
},
});
if (!parentId) {
if (toolId) {
await tx.tool.update({
where: { id: toolId },
data: { commentCount: { increment: 1 } },
});
}
if (skillId) {
await tx.skill.update({
where: { id: skillId },
data: { commentCount: { increment: 1 } },
});
}
}
const parentComment = parentId
? await tx.comment.findUnique({ where: { id: parentId }, select: { userId: true } })
: null;
if (parentComment && parentComment.userId !== userId) {
const replyTo = await tx.user.findUnique({
where: { id: parentId ? (await tx.comment.findUnique({ where: { id: parentId } }))?.userId : undefined },
select: { id: true, name: true },
});
if (replyTo) {
await tx.notification.create({
data: {
userId: replyTo.id,
type: "COMMENT_REPLY",
title: `${session.user.name || "用户"} 回复了你的评论`,
content: content.trim().slice(0, 100),
link: toolSlug ? `/tools/${toolSlug}` : `/skills/${skillSlug}`,
},
});
}
}
return newComment;
});
if (!parentId) {
await rewardPoints(userId, "comment", `评论${toolSlug || skillSlug}`, toolSlug ? "tool" : "skill", toolId || skillId);
}
const formatted = {
id: comment.id,
content: comment.content,
rating: comment.rating,
likeCount: 0,
isPinned: comment.isPinned,
createdAt: comment.createdAt,
updatedAt: comment.updatedAt,
replyCount: 0,
liked: false,
user: {
id: comment.user.id,
name: comment.user.name || comment.user.email.split("@")[0],
avatarUrl: comment.user.avatarUrl,
level: comment.user.level,
points: comment.user.points,
},
};
return NextResponse.json(formatted, { status: 201 });
}