全项目扫描修复: Docker数据卷修复+安全requireAdmin+12页SEO+API白名单+脚本超时+常量提取+假数据删除
This commit is contained in:
@@ -3,6 +3,18 @@ import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@/lib/auth";
|
||||
import { NextResponse } from "next/server";
|
||||
import { rewardPoints } from "@/lib/points-reward";
|
||||
import { triggerAchievementCheck } from "@/lib/achievements";
|
||||
|
||||
const MENTION_REGEX = /@([\w一-龥]{1,30})/g;
|
||||
|
||||
function extractMentionNames(content: string): string[] {
|
||||
const matches = Array.from(content.matchAll(MENTION_REGEX));
|
||||
const set = new Set<string>();
|
||||
for (const m of matches) {
|
||||
if (m[1]) set.add(m[1]);
|
||||
}
|
||||
return Array.from(set);
|
||||
}
|
||||
|
||||
export async function GET(req: Request, { params }: { params: { id: string } }) {
|
||||
const topicId = parseInt(params.id);
|
||||
@@ -15,6 +27,13 @@ export async function GET(req: Request, { params }: { params: { id: string } })
|
||||
where: { topicId },
|
||||
include: {
|
||||
user: { select: { id: true, name: true, email: true, avatarUrl: true, level: true, points: true } },
|
||||
replyTo: {
|
||||
select: {
|
||||
id: true,
|
||||
content: true,
|
||||
user: { select: { id: true, name: true, email: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: "asc" },
|
||||
skip: (page - 1) * pageSize,
|
||||
@@ -27,6 +46,15 @@ export async function GET(req: Request, { params }: { params: { id: string } })
|
||||
id: p.id,
|
||||
content: p.content,
|
||||
isAnswer: p.isAnswer,
|
||||
replyToId: p.replyToId,
|
||||
replyToSnapshot: p.replyToSnapshot,
|
||||
replyTo: p.replyTo
|
||||
? {
|
||||
id: p.replyTo.id,
|
||||
content: p.replyTo.content.slice(0, 200),
|
||||
userName: p.replyTo.user.name || p.replyTo.user.email.split("@")[0],
|
||||
}
|
||||
: null,
|
||||
createdAt: p.createdAt,
|
||||
updatedAt: p.updatedAt,
|
||||
user: {
|
||||
@@ -52,7 +80,7 @@ export async function POST(req: Request, { params }: { params: { id: string } })
|
||||
const topicId = parseInt(params.id);
|
||||
const userId = parseInt(session.user.id);
|
||||
const body = await req.json();
|
||||
const { content } = body;
|
||||
const { content, replyToId } = body as { content?: string; replyToId?: number | null };
|
||||
|
||||
if (!content || !content.trim()) {
|
||||
return NextResponse.json({ error: "回复内容不能为空" }, { status: 400 });
|
||||
@@ -61,7 +89,10 @@ export async function POST(req: Request, { params }: { params: { id: string } })
|
||||
return NextResponse.json({ error: "回复不能超过5000个字符" }, { status: 400 });
|
||||
}
|
||||
|
||||
const topic = await prisma.forumTopic.findUnique({ where: { id: topicId } });
|
||||
const topic = await prisma.forumTopic.findUnique({
|
||||
where: { id: topicId },
|
||||
select: { id: true, title: true, userId: true, isLocked: true },
|
||||
});
|
||||
if (!topic) {
|
||||
return NextResponse.json({ error: "话题不存在" }, { status: 404 });
|
||||
}
|
||||
@@ -69,15 +100,51 @@ export async function POST(req: Request, { params }: { params: { id: string } })
|
||||
return NextResponse.json({ error: "话题已锁定" }, { status: 400 });
|
||||
}
|
||||
|
||||
// 处理 replyToId:必须属于同一话题
|
||||
let replyToSnapshot: string | null = null;
|
||||
if (replyToId) {
|
||||
const target = await prisma.forumPost.findUnique({
|
||||
where: { id: replyToId },
|
||||
include: { user: { select: { id: true, name: true, email: true } } },
|
||||
});
|
||||
if (!target || target.topicId !== topicId) {
|
||||
return NextResponse.json({ error: "引用目标不存在" }, { status: 400 });
|
||||
}
|
||||
const targetName = target.user.name || target.user.email.split("@")[0];
|
||||
const snippet = target.content.slice(0, 200).replace(/\s+/g, " ").trim();
|
||||
replyToSnapshot = `@${targetName}: ${snippet}`;
|
||||
}
|
||||
|
||||
// 解析 @mention 用户
|
||||
const mentionNames = extractMentionNames(content);
|
||||
const mentionedUsers = mentionNames.length
|
||||
? await prisma.user.findMany({
|
||||
where: { name: { in: mentionNames } },
|
||||
select: { id: true, name: true, email: true },
|
||||
take: 10,
|
||||
})
|
||||
: [];
|
||||
|
||||
const actorName = session.user.name || session.user.email?.split("@")[0] || "用户";
|
||||
|
||||
const post = await prisma.$transaction(async (tx) => {
|
||||
const newPost = await tx.forumPost.create({
|
||||
data: {
|
||||
topicId,
|
||||
userId,
|
||||
content: content.trim(),
|
||||
replyToId: replyToId || null,
|
||||
replyToSnapshot,
|
||||
},
|
||||
include: {
|
||||
user: { select: { id: true, name: true, email: true, avatarUrl: true, level: true, points: true } },
|
||||
replyTo: {
|
||||
select: {
|
||||
id: true,
|
||||
content: true,
|
||||
user: { select: { id: true, name: true, email: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -91,18 +158,84 @@ export async function POST(req: Request, { params }: { params: { id: string } })
|
||||
},
|
||||
});
|
||||
|
||||
// 收集需要通知的目标
|
||||
const notifs: Array<{
|
||||
userId: number;
|
||||
type: "TOPIC_REPLY" | "MENTION";
|
||||
title: string;
|
||||
content?: string;
|
||||
link?: string;
|
||||
}> = [];
|
||||
|
||||
// 1) 通知楼主(自己回复自己就不通知)
|
||||
if (topic.userId !== userId) {
|
||||
await tx.notification.create({
|
||||
data: {
|
||||
userId: topic.userId,
|
||||
type: "TOPIC_REPLY",
|
||||
title: `${session.user.name || "用户"} 回复了你的话题`,
|
||||
content: content.trim().slice(0, 100),
|
||||
link: `/community/topic/${topicId}`,
|
||||
},
|
||||
notifs.push({
|
||||
userId: topic.userId,
|
||||
type: "TOPIC_REPLY",
|
||||
title: `${actorName} 回复了你的话题《${topic.title.slice(0, 30)}》`,
|
||||
content: content.trim().slice(0, 100),
|
||||
link: `/community/topic/${topicId}#post-${newPost.id}`,
|
||||
});
|
||||
}
|
||||
|
||||
// 2) 通知被引用的人(如果不是自己)
|
||||
if (replyToId) {
|
||||
const target = await tx.forumPost.findUnique({
|
||||
where: { id: replyToId },
|
||||
select: { userId: true },
|
||||
});
|
||||
if (target && target.userId !== userId && target.userId !== topic.userId) {
|
||||
notifs.push({
|
||||
userId: target.userId,
|
||||
type: "MENTION",
|
||||
title: `${actorName} 回复了你的回复`,
|
||||
content: content.trim().slice(0, 100),
|
||||
link: `/community/topic/${topicId}#post-${newPost.id}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 3) 通知 @mention 的用户(不是自己、不重复)
|
||||
const notifiedSet = new Set(notifs.map((n) => n.userId));
|
||||
for (const u of mentionedUsers) {
|
||||
if (u.id === userId) continue;
|
||||
if (notifiedSet.has(u.id)) continue;
|
||||
notifs.push({
|
||||
userId: u.id,
|
||||
type: "MENTION",
|
||||
title: `${actorName} 在话题《${topic.title.slice(0, 30)}》中提到了你`,
|
||||
content: content.trim().slice(0, 100),
|
||||
link: `/community/topic/${topicId}#post-${newPost.id}`,
|
||||
});
|
||||
notifiedSet.add(u.id);
|
||||
}
|
||||
|
||||
// 4) 通知订阅者(不是作者本人、不是楼主、不是已通知的人)
|
||||
const subscribers = await tx.forumTopicSubscription.findMany({
|
||||
where: {
|
||||
topicId,
|
||||
notifyReply: true,
|
||||
userId: { not: userId },
|
||||
},
|
||||
select: { userId: true },
|
||||
});
|
||||
for (const s of subscribers) {
|
||||
if (s.userId === topic.userId) continue;
|
||||
if (notifiedSet.has(s.userId)) continue;
|
||||
notifs.push({
|
||||
userId: s.userId,
|
||||
type: "TOPIC_REPLY",
|
||||
title: `你订阅的话题《${topic.title.slice(0, 30)}》有新回复`,
|
||||
content: content.trim().slice(0, 100),
|
||||
link: `/community/topic/${topicId}#post-${newPost.id}`,
|
||||
});
|
||||
notifiedSet.add(s.userId);
|
||||
}
|
||||
|
||||
if (notifs.length > 0) {
|
||||
await tx.notification.createMany({ data: notifs });
|
||||
}
|
||||
|
||||
return newPost;
|
||||
});
|
||||
|
||||
@@ -113,6 +246,15 @@ export async function POST(req: Request, { params }: { params: { id: string } })
|
||||
id: post.id,
|
||||
content: post.content,
|
||||
isAnswer: post.isAnswer,
|
||||
replyToId: post.replyToId,
|
||||
replyToSnapshot: post.replyToSnapshot,
|
||||
replyTo: post.replyTo
|
||||
? {
|
||||
id: post.replyTo.id,
|
||||
content: post.replyTo.content.slice(0, 200),
|
||||
userName: post.replyTo.user.name || post.replyTo.user.email.split("@")[0],
|
||||
}
|
||||
: null,
|
||||
createdAt: post.createdAt,
|
||||
updatedAt: post.updatedAt,
|
||||
user: {
|
||||
|
||||
Reference in New Issue
Block a user