import { NextRequest, NextResponse } from "next/server"; import { getAdminToken } from "@/app/lib/pocketbase/admin"; import { getPocketBaseConfig } from "@/config/services.config"; import { SESSIONS } from "@/config/home.config"; import { getThisWeekRangeChinaStr, getThisWeekRangeISO } from "@/app/lib/dates"; import { getSalonApi } from "@/app/lib/salonApi"; export const dynamic = "force-dynamic"; /** 按场次返回报名人数和最近报名者,仅限中国日历本周(周一到周日)报名的记录。优先 salonapi,失败时回退直连 PocketBase */ export async function GET(request: NextRequest) { try { const { status, data } = await getSalonApi(request, "/api/salon/join/stats", { timeoutMs: 8000, }); if (status === 200 && data && typeof data === "object") { const payload = data as { ok?: boolean; stats?: Record }; if (payload?.stats && typeof payload.stats === "object") { return NextResponse.json({ ok: true, stats: payload.stats }); } } } catch { /* salonapi 不可用,回退直连 PocketBase */ } const token = await getAdminToken(); if (!token) { return NextResponse.json( { ok: true, stats: getEmptyStats() }, { status: 200 } ); } const pb = getPocketBaseConfig(); const base = pb.url.replace(/\/$/, ""); const stats: Record = {}; const { start: weekStartCn, end: weekEndCn } = getThisWeekRangeChinaStr(); const { start: weekStartUtc, end: weekEndUtc } = getThisWeekRangeISO(); const results = await Promise.all( SESSIONS.map(async (s) => { try { // session-1 周六 / session-2 周日;meetupdate 含关键词或为空(兼容旧记录) const dayKeyword = s.id === "session-1" ? "周六" : "周日"; const meetupdatePart = s.id === "session-1" ? `(meetupdate ~ "${dayKeyword}" || meetupdate = "")` : `meetupdate ~ "${dayKeyword}"`; // 本周:submitted_at_cn(中国公历)或 created(UTC 兼容旧记录),两路查询合并 const filterSubmitted = encodeURIComponent( `${meetupdatePart} && submitted_at_cn >= "${weekStartCn}" && submitted_at_cn <= "${weekEndCn}"` ); const filterCreated = encodeURIComponent( `${meetupdatePart} && created >= "${weekStartUtc}" && created <= "${weekEndUtc}"` ); const [res1, res2] = await Promise.all([ fetch(`${base}/api/collections/${pb.joinCollection}/records?filter=${filterSubmitted}&sort=-created&perPage=6`, { headers: { Authorization: `Bearer ${token}` }, cache: "no-store", }), fetch(`${base}/api/collections/${pb.joinCollection}/records?filter=${filterCreated}&sort=-created&perPage=6`, { headers: { Authorization: `Bearer ${token}` }, cache: "no-store", }), ]); const data1 = res1.ok ? await res1.json() : { items: [], totalItems: 0 }; const data2 = res2.ok ? await res2.json() : { items: [], totalItems: 0 }; const seen = new Set(); const merged: typeof data1.items = []; for (const r of [...(data1.items || []), ...(data2.items || [])]) { if (r?.id && !seen.has(r.id)) { seen.add(r.id); merged.push(r); } } merged.sort((a: { created?: string }, b: { created?: string }) => (b?.created || "").localeCompare(a?.created || "")); const items = merged.slice(0, 6); const total = Math.max(data1.totalItems || 0, data2.totalItems || 0, merged.length); return { id: s.id, stat: { count: total, joiners: items.map((r: { nickname?: string; xiaohongshu_nickname?: string; xiaohongshu_avatar?: string; xiaohongshu_url?: string; media?: string }) => ({ nickname: String(r?.xiaohongshu_nickname || r?.nickname || "").trim() || "匿名", avatar: String(r?.xiaohongshu_avatar || r?.media || "").trim() || undefined, xiaohongshu_url: String(r?.xiaohongshu_url || "").trim() || undefined, })), }, }; } catch { return { id: s.id, stat: { count: 0, joiners: [] } }; } }) ); for (const { id, stat } of results) { stats[id] = stat; } return NextResponse.json({ ok: true, stats }); } function getEmptyStats() { const stats: Record = {}; for (const s of SESSIONS) { stats[s.id] = { count: 0, joiners: [] }; } return stats; }