diff --git a/app/api/vip/check-by-user/route.ts b/app/api/vip/check-by-user/route.ts index 4ab51e1..cd4b359 100644 --- a/app/api/vip/check-by-user/route.ts +++ b/app/api/vip/check-by-user/route.ts @@ -16,6 +16,30 @@ type PayjsVipResult = { }; const inflightUserChecks = new Map>(); +const inflightUserRequests = new Map< + string, + Promise<{ + ok: true; + vip: boolean; + alreadyLinked: boolean; + email?: string; + linkedUserId?: string; + }> +>(); +const userCheckResultCache = new Map< + string, + { + expiresAt: number; + payload: { + ok: true; + vip: boolean; + alreadyLinked: boolean; + email?: string; + linkedUserId?: string; + }; + } +>(); +const USER_CHECK_CACHE_TTL_MS = Number(process.env.CHECK_BY_USER_CACHE_TTL_MS) || 10_000; async function fetchPayjsVipWithRetry( url: string, @@ -126,139 +150,167 @@ export async function POST(request: NextRequest) { ); } - let payjsResult: PayjsVipResult | null = null; - let vip = false; - let email: string | null = null; - let linkedUserId: string | undefined; - let vipFromSite = false; - let vipFromPayments = false; - let vipSource = vip ? "payjsapi" : "none"; + const cached = userCheckResultCache.get(userId); + if (cached && cached.expiresAt > Date.now()) { + return NextResponse.json(cached.payload); + } + if (cached) { + userCheckResultCache.delete(userId); + } - try { - const adminToken = await getAdminToken().catch((error) => { - console.error("[check-by-user] getAdminToken failed:", error); - return null; - }); - if (!adminToken) { - payjsResult = await checkVipFromPayjsapi(request, userId); - vip = !!payjsResult?.vip; - email = payjsResult?.email ?? null; - console.error("[check-by-user] missing PocketBase admin token, fallback to payjs result"); - return NextResponse.json({ + const inflight = inflightUserRequests.get(userId); + if (inflight) { + const payload = await inflight; + return NextResponse.json(payload); + } + + const task = (async () => { + let payjsResult: PayjsVipResult | null = null; + let vip = false; + let email: string | null = null; + let linkedUserId: string | undefined; + let vipFromSite = false; + let vipFromPayments = false; + let vipSource = vip ? "payjsapi" : "none"; + + try { + const adminToken = await getAdminToken().catch((error) => { + console.error("[check-by-user] getAdminToken failed:", error); + return null; + }); + if (!adminToken) { + payjsResult = await checkVipFromPayjsapi(request, userId); + vip = !!payjsResult?.vip; + email = payjsResult?.email ?? null; + console.error("[check-by-user] missing PocketBase admin token, fallback to payjs result"); + return { + ok: true, + vip, + alreadyLinked: !!email, + email: email ?? undefined, + }; + } + + if (!vip) { + vipFromSite = await checkSiteVipByUserId(adminToken, userId); + vip = vipFromSite; + if (!vip) { + vipFromPayments = await checkPaymentsByUserId(adminToken, userId); + vip = vipFromPayments; + } + if (vipFromSite) { + vipSource = "pocketbase:site_vip"; + } else if (vipFromPayments) { + vipSource = "pocketbase:payments"; + } + console.log( + "[check-by-user] pb fallback user_id=%s site_vip=%s payments=%s", + userId, + vipFromSite, + vipFromPayments + ); + } + + // PB 未命中再请求 payjsapi,避免每次都先走远端网络 + if (!vip) { + payjsResult = await checkVipFromPayjsapi(request, userId); + if (payjsResult?.vip) { + vip = true; + email = payjsResult.email ?? null; + vipSource = "payjsapi"; + } + } + + if (isAnonymousUserId(userId)) { + const linkRecord = await findVipEmailLinkByAnonymousUserId(adminToken, userId); + const linkEmail = + typeof linkRecord?.email === "string" + ? linkRecord.email.trim().toLowerCase() + : ""; + const linkUserId = + typeof linkRecord?.user_id === "string" ? linkRecord.user_id.trim() : ""; + + // 匿名 user_id 本身可能已迁移到真实用户,需补查映射用户的 VIP + if (!vip && linkUserId && linkUserId !== userId) { + const linkedVip = await checkSiteVipByUserId(adminToken, linkUserId); + const linkedPaymentVip = linkedVip + ? true + : await checkPaymentsByUserId(adminToken, linkUserId); + if (linkedVip || linkedPaymentVip) { + vip = true; + linkedUserId = linkUserId; + vipSource = linkedVip ? "vip_email_link:site_vip" : "vip_email_link:payments"; + } + } + + if (vip && linkEmail) { + email = linkEmail; + if (linkUserId && !isAnonymousUserId(linkUserId)) { + linkedUserId = linkUserId; + } + if (!vipSource || vipSource === "none") { + vipSource = "vip_email_link"; + } + console.log( + "[check-by-user] linked by vip_email_link user_id=%s linkedUserId=%s email=%s", + userId, + linkedUserId || "(none)", + email + ); + } else if (vip) { + const linkedMember = await findLinkedMemberFromOrder(adminToken, userId); + if (linkedMember) { + email = linkedMember.email; + linkedUserId = linkedMember.userId; + await upsertVipEmailLink( + adminToken, + linkedMember.email, + linkedMember.userId, + userId + ); + vipSource = "linked_from_order"; + console.log( + "[check-by-user] linked by order user_id=%s linkedUserId=%s email=%s", + userId, + linkedUserId, + email + ); + } + } + } + + console.log( + "[check-by-user] result user_id=%s vip=%s source=%s alreadyLinked=%s linkedUserId=%s", + userId, + vip, + vipSource, + !!email, + linkedUserId || "(none)" + ); + + return { ok: true, vip, alreadyLinked: !!email, email: email ?? undefined, - }); + linkedUserId, + }; + } catch (error) { + console.error("[check-by-user] failed:", error); + // 接口容错:即使 PB 回退失败,也不抛 500 给前端 + return { ok: true, vip: false, alreadyLinked: false }; } + })(); - if (!vip) { - vipFromSite = await checkSiteVipByUserId(adminToken, userId); - vip = vipFromSite; - if (!vip) { - vipFromPayments = await checkPaymentsByUserId(adminToken, userId); - vip = vipFromPayments; - } - if (vipFromSite) { - vipSource = "pocketbase:site_vip"; - } else if (vipFromPayments) { - vipSource = "pocketbase:payments"; - } - console.log( - "[check-by-user] pb fallback user_id=%s site_vip=%s payments=%s", - userId, - vipFromSite, - vipFromPayments - ); - } - - // PB 未命中再请求 payjsapi,避免每次都先走远端网络 - if (!vip) { - payjsResult = await checkVipFromPayjsapi(request, userId); - if (payjsResult?.vip) { - vip = true; - email = payjsResult.email ?? null; - vipSource = "payjsapi"; - } - } - - if (isAnonymousUserId(userId)) { - const linkRecord = await findVipEmailLinkByAnonymousUserId(adminToken, userId); - const linkEmail = - typeof linkRecord?.email === "string" - ? linkRecord.email.trim().toLowerCase() - : ""; - const linkUserId = - typeof linkRecord?.user_id === "string" ? linkRecord.user_id.trim() : ""; - - // 匿名 user_id 本身可能已迁移到真实用户,需补查映射用户的 VIP - if (!vip && linkUserId && linkUserId !== userId) { - const linkedVip = await checkSiteVipByUserId(adminToken, linkUserId); - const linkedPaymentVip = linkedVip - ? true - : await checkPaymentsByUserId(adminToken, linkUserId); - if (linkedVip || linkedPaymentVip) { - vip = true; - linkedUserId = linkUserId; - vipSource = linkedVip ? "vip_email_link:site_vip" : "vip_email_link:payments"; - } - } - - if (vip && linkEmail) { - email = linkEmail; - if (linkUserId && !isAnonymousUserId(linkUserId)) { - linkedUserId = linkUserId; - } - if (!vipSource || vipSource === "none") { - vipSource = "vip_email_link"; - } - console.log( - "[check-by-user] linked by vip_email_link user_id=%s linkedUserId=%s email=%s", - userId, - linkedUserId || "(none)", - email - ); - } else if (vip) { - const linkedMember = await findLinkedMemberFromOrder(adminToken, userId); - if (linkedMember) { - email = linkedMember.email; - linkedUserId = linkedMember.userId; - await upsertVipEmailLink( - adminToken, - linkedMember.email, - linkedMember.userId, - userId - ); - vipSource = "linked_from_order"; - console.log( - "[check-by-user] linked by order user_id=%s linkedUserId=%s email=%s", - userId, - linkedUserId, - email - ); - } - } - } - - console.log( - "[check-by-user] result user_id=%s vip=%s source=%s alreadyLinked=%s linkedUserId=%s", - userId, - vip, - vipSource, - !!email, - linkedUserId || "(none)" - ); - - return NextResponse.json({ - ok: true, - vip, - alreadyLinked: !!email, - email: email ?? undefined, - linkedUserId, + inflightUserRequests.set(userId, task); + try { + const payload = await task; + userCheckResultCache.set(userId, { + payload, + expiresAt: Date.now() + USER_CHECK_CACHE_TTL_MS, }); - } catch (error) { - console.error("[check-by-user] failed:", error); - // 接口容错:即使 PB 回退失败,也不抛 500 给前端 - return NextResponse.json({ ok: true, vip: false, error: "check_failed_degraded" }); + return NextResponse.json(payload); + } finally { + inflightUserRequests.delete(userId); } }