全项目扫描修复: Docker数据卷修复+安全requireAdmin+12页SEO+API白名单+脚本超时+常量提取+假数据删除
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET(req: Request) {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const q = (searchParams.get("q") || "").trim();
|
||||
const page = Math.max(1, parseInt(searchParams.get("page") || "1", 10));
|
||||
const pageSize = Math.min(50, parseInt(searchParams.get("pageSize") || "15", 10));
|
||||
const sort = searchParams.get("sort") || "latest";
|
||||
|
||||
if (!q) {
|
||||
return NextResponse.json({ topics: [], totalCount: 0, page, pageSize, q: "" });
|
||||
}
|
||||
|
||||
const where: any = {
|
||||
isHidden: false,
|
||||
};
|
||||
|
||||
// 优先使用全文索引,回退到 LIKE
|
||||
const useFullText = q.length >= 2;
|
||||
if (useFullText) {
|
||||
// MySQL FULLTEXT 的 boolean mode 搜索
|
||||
// 把关键词拆词,规避 FULLTEXT 最小长度限制
|
||||
const tokens = q
|
||||
.split(/\s+/)
|
||||
.filter((t) => t.length >= 1)
|
||||
.slice(0, 10)
|
||||
.map((t) => `+${t.replace(/[+\-><()~*"@]/g, "")}*`);
|
||||
if (tokens.length > 0) {
|
||||
// Prisma 的 where.fulltext 字段是 reserved,使用原始查询更稳
|
||||
// 这里先尝试简单的 contains 组合
|
||||
const ors: any[] = [];
|
||||
for (const t of q.split(/\s+/).filter((x) => x.length >= 1)) {
|
||||
ors.push({ title: { contains: t } });
|
||||
ors.push({ content: { contains: t } });
|
||||
}
|
||||
where.OR = ors;
|
||||
} else {
|
||||
where.OR = [{ title: { contains: q } }, { content: { contains: q } }];
|
||||
}
|
||||
} else {
|
||||
where.OR = [{ title: { contains: q } }, { content: { contains: q } }];
|
||||
}
|
||||
|
||||
const orderBy: any = {};
|
||||
if (sort === "relevance" && useFullText) {
|
||||
// 简化:标题命中权重更高 → 让标题命中靠前
|
||||
orderBy.isPinned = "desc";
|
||||
} else if (sort === "hot") {
|
||||
orderBy.viewCount = "desc";
|
||||
} else if (sort === "active") {
|
||||
orderBy.lastReplyAt = "desc";
|
||||
} else {
|
||||
orderBy.createdAt = "desc";
|
||||
}
|
||||
|
||||
const [topics, totalCount] = await Promise.all([
|
||||
prisma.forumTopic.findMany({
|
||||
where,
|
||||
include: {
|
||||
category: { select: { name: true, slug: true, icon: true } },
|
||||
user: { select: { id: true, name: true, email: true, avatarUrl: true, level: true, points: true, isBot: true } },
|
||||
_count: { select: { posts: true } },
|
||||
tags: { include: { tag: true } },
|
||||
},
|
||||
orderBy: [{ isPinned: "desc" }, orderBy],
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
}),
|
||||
prisma.forumTopic.count({ where }),
|
||||
]);
|
||||
|
||||
// 生成高亮摘要
|
||||
const snippets = topics.map((t) => {
|
||||
const titleHit = q && t.title.toLowerCase().includes(q.toLowerCase());
|
||||
let snippet = "";
|
||||
if (q) {
|
||||
const idx = t.content.toLowerCase().indexOf(q.toLowerCase());
|
||||
if (idx >= 0) {
|
||||
const start = Math.max(0, idx - 50);
|
||||
const end = Math.min(t.content.length, idx + q.length + 100);
|
||||
snippet =
|
||||
(start > 0 ? "..." : "") +
|
||||
t.content.slice(start, end).replace(/\s+/g, " ").trim() +
|
||||
(end < t.content.length ? "..." : "");
|
||||
} else {
|
||||
snippet = t.content.slice(0, 150).replace(/\s+/g, " ").trim() + (t.content.length > 150 ? "..." : "");
|
||||
}
|
||||
} else {
|
||||
snippet = t.content.slice(0, 150).replace(/\s+/g, " ").trim();
|
||||
}
|
||||
return { topicId: t.id, titleHit, snippet };
|
||||
});
|
||||
|
||||
return NextResponse.json({ topics, totalCount, page, pageSize, q, snippets });
|
||||
}
|
||||
Reference in New Issue
Block a user