From eaa92ddf3609981120907904f271af7c0fce386a Mon Sep 17 00:00:00 2001 From: eric Date: Thu, 26 Mar 2026 10:57:26 -0500 Subject: [PATCH] 's' --- app/api/vip/check-by-email/route.ts | 114 +++++++++++++++++++---- app/api/vip/check-by-user/route.ts | 77 +++++++++++---- app/vip/components/landing/LoginForm.tsx | 16 +++- 3 files changed, 171 insertions(+), 36 deletions(-) diff --git a/app/api/vip/check-by-email/route.ts b/app/api/vip/check-by-email/route.ts index bcfc9ca..adf9d3c 100644 --- a/app/api/vip/check-by-email/route.ts +++ b/app/api/vip/check-by-email/route.ts @@ -21,8 +21,11 @@ type PBAuthRecord = { type PayjsVipEmailResult = { vip: boolean; + error?: string; effectiveUserId?: string; anonymousUserId?: string; + token?: string; + record?: PBAuthRecord; }; type RecoveryResult = { @@ -69,7 +72,13 @@ async function checkVipByEmailFromPayjsapi( const data = await res.json().catch(() => ({})); if (!data?.vip) { - return null; + return { + vip: false, + error: + typeof data?.error === "string" && data.error.trim() + ? data.error.trim() + : undefined, + }; } return { @@ -82,6 +91,8 @@ async function checkVipByEmailFromPayjsapi( typeof data?.anonymousUserId === "string" ? data.anonymousUserId.trim() : undefined, + token: typeof data?.token === "string" ? data.token : undefined, + record: data?.record?.id ? data.record : undefined, }; } catch (error) { console.log("[check-by-email] payjsapi request failed:", error); @@ -246,18 +257,74 @@ export async function POST(request: NextRequest) { } try { - const [authData, adminToken] = await Promise.all([ - loginPocketBaseUser(email, password), - getAdminToken(), - ]); - if (!adminToken) { + let authData: { token: string; record: PBAuthRecord } | null = null; + let loginError: string | null = null; + try { + authData = await loginPocketBaseUser(email, password); + } catch (error) { + loginError = error instanceof Error ? error.message : "登录失败"; + } + + let payjsResult: PayjsVipEmailResult | null = await checkVipByEmailFromPayjsapi( + request, + email, + password, + requestedAnonymousUserId || undefined + ); + + // PB 登录失败时,尝试使用 payjsapi 返回的 token/record 兜底恢复 + if (!authData && payjsResult?.vip && payjsResult.token && payjsResult.record?.id) { + console.log("[check-by-email] fallback by payjsapi token email=%s", email); + return NextResponse.json({ + ok: true, + vip: true, + token: payjsResult.token, + record: payjsResult.record, + effectiveUserId: payjsResult.record.id, + anonymousUserId: payjsResult.anonymousUserId ?? null, + memosAccount: payjsResult.anonymousUserId ?? null, + source: "payjsapi_token_fallback", + }); + } + + if (!authData) { + const msg = loginError || "登录失败"; return NextResponse.json( - { ok: false, vip: false, error: "PocketBase 管理员认证失败" }, - { status: 500 } + { ok: false, vip: false, error: msg }, + { status: msg.includes("密码") || msg.includes("邮箱") ? 401 : 503 } + ); + } + + const adminToken = await getAdminToken().catch((error) => { + console.error("[check-by-email] getAdminToken failed:", error); + return null; + }); + if (!adminToken) { + // 管理员 token 获取失败时,降级到 payjsapi 结果,避免直接 500 + if (payjsResult?.vip) { + return NextResponse.json({ + ok: true, + vip: true, + token: authData.token, + record: authData.record, + effectiveUserId: authData.record.id, + anonymousUserId: payjsResult.anonymousUserId ?? null, + memosAccount: payjsResult.anonymousUserId ?? null, + source: "payjsapi_degraded", + }); + } + return NextResponse.json( + { + ok: false, + vip: false, + error: + payjsResult?.error || + "服务繁忙,请稍后重试", + }, + { status: 503 } ); } - let payjsResult: PayjsVipEmailResult | null = null; let recovery = await resolveRecovery( adminToken, email, @@ -267,12 +334,14 @@ export async function POST(request: NextRequest) { ); if (!recovery.vip) { - payjsResult = await checkVipByEmailFromPayjsapi( - request, - email, - password, - requestedAnonymousUserId || undefined - ); + if (!payjsResult) { + payjsResult = await checkVipByEmailFromPayjsapi( + request, + email, + password, + requestedAnonymousUserId || undefined + ); + } recovery = await resolveRecovery( adminToken, email, @@ -291,18 +360,25 @@ export async function POST(request: NextRequest) { return NextResponse.json({ ok: false, vip: false, - error: + error: payjsResult?.error || "未找到关联的会员记录,请确认已支付并绑定邮箱;如为历史匿名订单,可填写支付时的 user_id 再登录", }); } - const fallbackAnonymousUserId = isAnonymousUserId(requestedAnonymousUserId) - ? requestedAnonymousUserId - : (await findAnonymousUserIdForUser(adminToken, authData.record.id)) || + let fallbackAnonymousUserId: string | undefined; + if ( + isAnonymousUserId(requestedAnonymousUserId) && + (await hasVipByUserId(adminToken, requestedAnonymousUserId)) + ) { + fallbackAnonymousUserId = requestedAnonymousUserId; + } else { + fallbackAnonymousUserId = + (await findAnonymousUserIdForUser(adminToken, authData.record.id)) || (typeof payjsResult?.anonymousUserId === "string" && isAnonymousUserId(payjsResult.anonymousUserId.trim()) ? payjsResult.anonymousUserId.trim() : undefined); + } const anonymousUserId = recovery.anonymousUserId || fallbackAnonymousUserId; diff --git a/app/api/vip/check-by-user/route.ts b/app/api/vip/check-by-user/route.ts index 78743c0..a4ebd30 100644 --- a/app/api/vip/check-by-user/route.ts +++ b/app/api/vip/check-by-user/route.ts @@ -15,6 +15,29 @@ type PayjsVipResult = { email?: string; }; +async function fetchPayjsVipWithRetry( + url: string, + timeoutMs: number, + retries: number +): Promise { + let lastError: unknown = null; + for (let i = 0; i <= retries; i += 1) { + try { + const res = await fetch(url, { + cache: "no-store", + signal: AbortSignal.timeout(timeoutMs), + }); + return res; + } catch (error) { + lastError = error; + if (i < retries) { + await new Promise((resolve) => setTimeout(resolve, 250 * (i + 1))); + } + } + } + throw lastError instanceof Error ? lastError : new Error("payjsapi fetch failed"); +} + function resolvePayApiUrl(request: NextRequest): string { const payConfig = getPaymentConfig(); const hostHeader = request.headers.get("host") || request.headers.get("x-forwarded-host"); @@ -35,10 +58,11 @@ async function checkVipFromPayjsapi( )}`; console.log("[check-by-user] payjsapi request user_id=%s url=%s", userId, url); const checkTimeout = Number(process.env.PAYJSAPI_CHECK_TIMEOUT) || 15000; - const res = await fetch( - url, - { cache: "no-store", signal: AbortSignal.timeout(checkTimeout) } - ); + const retries = Math.max(0, Number(process.env.PAYJSAPI_CHECK_RETRIES) || 1); + const res = await fetchPayjsVipWithRetry(url, checkTimeout, retries); + if (!res) { + return null; + } if (!res.ok) { console.log( "[check-by-user] payjsapi non-ok user_id=%s status=%s", @@ -109,10 +133,18 @@ export async function POST(request: NextRequest) { }); } - const adminToken = await getAdminToken(); + const adminToken = await getAdminToken().catch((error) => { + console.error("[check-by-user] getAdminToken failed:", error); + return null; + }); if (!adminToken) { - console.error("[check-by-user] missing PocketBase admin token"); - return NextResponse.json({ ok: false, vip: false }, { status: 500 }); + console.error("[check-by-user] missing PocketBase admin token, fallback to payjs result"); + return NextResponse.json({ + ok: true, + vip, + alreadyLinked: !!email, + email: email ?? undefined, + }); } if (!vip) { @@ -135,11 +167,8 @@ export async function POST(request: NextRequest) { ); } - if (vip && !email && isAnonymousUserId(userId)) { - const linkRecord = await findVipEmailLinkByAnonymousUserId( - adminToken, - userId - ); + if (isAnonymousUserId(userId)) { + const linkRecord = await findVipEmailLinkByAnonymousUserId(adminToken, userId); const linkEmail = typeof linkRecord?.email === "string" ? linkRecord.email.trim().toLowerCase() @@ -147,19 +176,34 @@ export async function POST(request: NextRequest) { const linkUserId = typeof linkRecord?.user_id === "string" ? linkRecord.user_id.trim() : ""; - if (linkEmail) { + // 匿名 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; } - vipSource = "vip_email_link"; + 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 { + } else if (vip) { const linkedMember = await findLinkedMemberFromOrder(adminToken, userId); if (linkedMember) { email = linkedMember.email; @@ -199,6 +243,7 @@ export async function POST(request: NextRequest) { }); } catch (error) { console.error("[check-by-user] failed:", error); - return NextResponse.json({ ok: false, vip: false }, { status: 500 }); + // 接口容错:即使 PB 回退失败,也不抛 500 给前端 + return NextResponse.json({ ok: true, vip: false, error: "check_failed_degraded" }); } } diff --git a/app/vip/components/landing/LoginForm.tsx b/app/vip/components/landing/LoginForm.tsx index 279290f..5ab06c1 100644 --- a/app/vip/components/landing/LoginForm.tsx +++ b/app/vip/components/landing/LoginForm.tsx @@ -28,6 +28,15 @@ export function LoginForm({ const [error, setError] = useState(null); const inputRef = useRef(null); + const getCandidateUserId = (): string => { + if (typeof window === "undefined") return ""; + const fromMemos = localStorage.getItem("memosAccount")?.trim() || ""; + if (/^user\d+$/.test(fromMemos)) return fromMemos; + const fromUserId = localStorage.getItem("userId")?.trim() || ""; + if (/^user\d+$/.test(fromUserId)) return fromUserId; + return ""; + }; + useEffect(() => { if (autoFocus) inputRef.current?.focus(); }, [autoFocus, mode]); @@ -71,7 +80,12 @@ export function LoginForm({ setError(null); setLoading(true); try { - const result = await onVerifyByEmail(trimmedEmail, trimmedPassword); + const candidateUserId = getCandidateUserId(); + const result = await onVerifyByEmail( + trimmedEmail, + trimmedPassword, + candidateUserId || undefined + ); const ok = typeof result === "object" ? result.ok : result; const errMsg = typeof result === "object" ? result.error : undefined; if (ok) onSuccess();