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