diff --git a/.env.example b/.env.example index b202c6c..0a45fb5 100644 --- a/.env.example +++ b/.env.example @@ -17,7 +17,7 @@ NEXT_PUBLIC_API_URL=http://127.0.0.1:8007 # PAYMENT_API_INTERNAL_URL=http://127.0.0.1:8007 # 内网/慢网络可适当提高超时(毫秒):check_vip 默认 15s,payh5/redirect 默认 60s # PAYJSAPI_CHECK_TIMEOUT=15000 -# PAYJSAPI_CHECK_BY_EMAIL_TIMEOUT=15000 +# PAYJSAPI_CHECK_BY_EMAIL_TIMEOUT=20000 # PAYJSAPI_CHECK_BY_EMAIL_RETRIES=1 # PAYJSAPI_PAYH5_TIMEOUT=60000 # PAYJSAPI_REDIRECT_TIMEOUT=60000 diff --git a/app/api/vip/_helpers.ts b/app/api/vip/_helpers.ts index a594544..d84e00f 100644 --- a/app/api/vip/_helpers.ts +++ b/app/api/vip/_helpers.ts @@ -1,6 +1,10 @@ import { SITE_ID, getPocketBaseConfig } from "@/config/services.config"; const VIP_EMAIL_LINK_COLLECTION = "vip_email_link"; +const PB_FETCH_TIMEOUT = Number(process.env.POCKETBASE_FETCH_TIMEOUT) || 10000; +const PB_ADMIN_TOKEN_TTL_MS = Number(process.env.POCKETBASE_ADMIN_TOKEN_TTL_MS) || 55_000; + +let adminTokenCache: { token: string; expiresAt: number } | null = null; type PBRecord = Record; @@ -22,6 +26,11 @@ function uniqueStrings(values: Array): string[] { ); } +async function pbFetch(input: string, init: RequestInit = {}): Promise { + const signal = init.signal ?? AbortSignal.timeout(PB_FETCH_TIMEOUT); + return fetch(input, { ...init, signal }); +} + async function listRecords( collection: string, filter: string, @@ -35,7 +44,7 @@ async function listRecords( url.searchParams.set("sort", options.sort); } - const res = await fetch(url.toString(), { + const res = await pbFetch(url.toString(), { headers: { Authorization: `Bearer ${adminToken}` }, cache: "no-store", }); @@ -52,7 +61,7 @@ async function createRecord( adminToken: string, body: Record ): Promise { - await fetch(`${getPBBaseUrl()}/api/collections/${collection}/records`, { + await pbFetch(`${getPBBaseUrl()}/api/collections/${collection}/records`, { method: "POST", headers: { "Content-Type": "application/json", @@ -69,7 +78,7 @@ async function patchRecord( adminToken: string, body: Record ): Promise { - await fetch( + await pbFetch( `${getPBBaseUrl()}/api/collections/${collection}/records/${recordId}`, { method: "PATCH", @@ -88,7 +97,7 @@ async function deleteRecord( recordId: string, adminToken: string ): Promise { - await fetch( + await pbFetch( `${getPBBaseUrl()}/api/collections/${collection}/records/${recordId}`, { method: "DELETE", @@ -103,7 +112,7 @@ async function getRecord( recordId: string, adminToken: string ): Promise { - const res = await fetch( + const res = await pbFetch( `${getPBBaseUrl()}/api/collections/${collection}/records/${recordId}`, { headers: { Authorization: `Bearer ${adminToken}` }, @@ -139,6 +148,10 @@ export async function getAdminToken(): Promise { return null; } + if (adminTokenCache && adminTokenCache.expiresAt > Date.now()) { + return adminTokenCache.token; + } + const baseUrl = getPBBaseUrl(); const paths = [ "/api/admins/auth-with-password", @@ -146,7 +159,7 @@ export async function getAdminToken(): Promise { ]; for (const path of paths) { - const res = await fetch(`${baseUrl}${path}`, { + const res = await pbFetch(`${baseUrl}${path}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ identity: email, password }), @@ -158,6 +171,10 @@ export async function getAdminToken(): Promise { const data = await res.json().catch(() => ({})); if (typeof data?.token === "string" && data.token) { + adminTokenCache = { + token: data.token, + expiresAt: Date.now() + PB_ADMIN_TOKEN_TTL_MS, + }; return data.token; } } diff --git a/app/api/vip/check-by-email/route.ts b/app/api/vip/check-by-email/route.ts index dda9566..92dfb15 100644 --- a/app/api/vip/check-by-email/route.ts +++ b/app/api/vip/check-by-email/route.ts @@ -36,7 +36,7 @@ type RecoveryResult = { }; const PAYJS_CHECK_BY_EMAIL_TIMEOUT = - Number(process.env.PAYJSAPI_CHECK_BY_EMAIL_TIMEOUT) || 5000; + Number(process.env.PAYJSAPI_CHECK_BY_EMAIL_TIMEOUT) || 20000; const PAYJS_CHECK_BY_EMAIL_RETRIES = Math.max(0, Number(process.env.PAYJSAPI_CHECK_BY_EMAIL_RETRIES) || 1); @@ -49,6 +49,13 @@ function resolvePayApiUrl(request: NextRequest): string { ).replace(/\/$/, ""); } +function getLocalPayApiUrl(): string { + return ( + process.env.PAYMENT_API_INTERNAL_URL?.trim() || + "http://127.0.0.1:8007" + ).replace(/\/$/, ""); +} + async function checkVipByEmailFromPayjsapi( request: NextRequest, email: string, @@ -57,8 +64,13 @@ async function checkVipByEmailFromPayjsapi( ): Promise { const payConfig = getPaymentConfig(); const primaryUrl = resolvePayApiUrl(request); + const localUrl = getLocalPayApiUrl(); const candidates = Array.from( - new Set([primaryUrl, payConfig.apiUrl].map((x) => (x || "").trim().replace(/\/$/, "")).filter(Boolean)) + new Set( + [localUrl, primaryUrl, payConfig.apiUrl] + .map((x) => (x || "").trim().replace(/\/$/, "")) + .filter(Boolean) + ) ); for (const apiUrl of candidates) { @@ -280,29 +292,29 @@ export async function POST(request: NextRequest) { 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", - }); - } + let payjsResult: PayjsVipEmailResult | null = null; if (!authData) { + payjsResult = await checkVipByEmailFromPayjsapi( + request, + email, + password, + requestedAnonymousUserId || undefined + ); + // PB 登录失败时,尝试使用 payjsapi 返回的 token/record 兜底恢复 + if (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", + }); + } const msg = loginError || "登录失败"; return NextResponse.json( { ok: false, vip: false, error: msg }, @@ -316,6 +328,14 @@ export async function POST(request: NextRequest) { }); if (!adminToken) { // 管理员 token 获取失败时,降级到 payjsapi 结果,避免直接 500 + if (!payjsResult) { + payjsResult = await checkVipByEmailFromPayjsapi( + request, + email, + password, + requestedAnonymousUserId || undefined + ); + } if (payjsResult?.vip) { return NextResponse.json({ ok: true, @@ -349,14 +369,12 @@ export async function POST(request: NextRequest) { ); if (!recovery.vip) { - if (!payjsResult) { - payjsResult = await checkVipByEmailFromPayjsapi( - request, - email, - password, - requestedAnonymousUserId || undefined - ); - } + payjsResult = await checkVipByEmailFromPayjsapi( + request, + email, + password, + requestedAnonymousUserId || undefined + ); recovery = await resolveRecovery( adminToken, email, diff --git a/app/api/vip/check-by-user/route.ts b/app/api/vip/check-by-user/route.ts index a4ebd30..a98a046 100644 --- a/app/api/vip/check-by-user/route.ts +++ b/app/api/vip/check-by-user/route.ts @@ -110,34 +110,23 @@ export async function POST(request: NextRequest) { ); } - const payjsResult = await checkVipFromPayjsapi(request, userId); - let vip = !!payjsResult?.vip; - let email = payjsResult?.email ?? null; + 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 { - if (vip) { - console.log( - "[check-by-user] hit payjsapi user_id=%s alreadyLinked=%s", - userId, - !!email - ); - return NextResponse.json({ - ok: true, - vip: true, - alreadyLinked: !!email, - email: email ?? undefined, - }); - } - 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({ ok: true, @@ -167,6 +156,16 @@ export async function POST(request: NextRequest) { ); } + // 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 =