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

97 lines
3.3 KiB
TypeScript

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 });
}