207 lines
6.5 KiB
TypeScript
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 });
|
|
}
|