全项目扫描修复: Docker数据卷修复+安全requireAdmin+12页SEO+API白名单+脚本超时+常量提取+假数据删除

This commit is contained in:
ZhuiGuangAI Dev
2026-06-12 17:27:47 +08:00
parent 464924aea0
commit 4c325c8699
454 changed files with 33647 additions and 980 deletions
+152 -10
View File
@@ -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: {