Files
gitlab-instance-0a899031_salon/app/api/join/stats/route.ts
2026-03-18 02:01:48 -05:00

111 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string, { count: number; joiners: { nickname: string; avatar?: string; xiaohongshu_url?: string }[] }> };
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<string, { count: number; joiners: { nickname: string; avatar?: string; xiaohongshu_url?: string }[] }> = {};
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中国公历或 createdUTC 兼容旧记录),两路查询合并
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<string>();
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<string, { count: number; joiners: { nickname: string; avatar?: string; xiaohongshu_url?: string }[] }> = {};
for (const s of SESSIONS) {
stats[s.id] = { count: 0, joiners: [] };
}
return stats;
}