From e1ed5a56dc71a0ffa57ab89fc9beb75213436c9d Mon Sep 17 00:00:00 2001 From: eric Date: Fri, 27 Mar 2026 19:22:37 -0500 Subject: [PATCH] 'fix' --- app/HomeClient.tsx | 153 +++++++++++++++++++---------- app/HomeClientLoader.tsx | 4 +- app/api/vip/check-by-user/route.ts | 21 ++-- app/api/vip/check/route.ts | 6 +- 4 files changed, 124 insertions(+), 60 deletions(-) diff --git a/app/HomeClient.tsx b/app/HomeClient.tsx index c0df242..e845f42 100644 --- a/app/HomeClient.tsx +++ b/app/HomeClient.tsx @@ -85,6 +85,57 @@ function getPendingOrderId(): string | null { return orderId || null; } +type VipProbeResult = "yes" | "no" | "unknown"; + +/** 仅当明确 yes/no;弱网/5xx/服务端 uncertain 一律 unknown,禁止用来清空本地 VIP */ +function probeFromCheckByUserBody( + data: Record | null, + resOk: boolean +): VipProbeResult { + if (!resOk || !data || typeof data !== "object") return "unknown"; + if (data.uncertain === true) return "unknown"; + if (data.vip === true) return "yes"; + if (data.ok === true && data.vip === false) return "no"; + if (data.vip === false && data.ok !== false) return "no"; + return "unknown"; +} + +async function probeCheckByUserId(userId: string): Promise { + const trimmed = userId.trim(); + if (!trimmed) return "unknown"; + try { + const res = await fetch("/api/vip/check-by-user", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ user_id: trimmed }), + credentials: "same-origin", + cache: "no-store", + }); + const data = (await res.json().catch(() => null)) as Record | null; + return probeFromCheckByUserBody(data, res.ok); + } catch { + return "unknown"; + } +} + +async function probeSessionVip(): Promise { + try { + const res = await fetch("/api/vip/check", { + credentials: "include", + cache: "no-store", + }); + const data = (await res.json().catch(() => null)) as Record | null; + if (!res.ok) return "unknown"; + if (!data || typeof data !== "object") return "unknown"; + if (data.adminUnavailable === true) return "unknown"; + if (data.uncertain === true) return "unknown"; + if (data.vip === true) return "yes"; + return "no"; + } catch { + return "unknown"; + } +} + /** 仅在本组件内使用(由 page.tsx dynamic ssr:false 仅客户端挂载,首帧可读 localStorage) */ function readVipBootstrapState(): { paidType: string | null; @@ -305,36 +356,45 @@ export default function HomeClient() { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ user_id: normalizedUserId }), + credentials: "same-origin", + cache: "no-store", }); - const data = await res.json().catch(() => ({})); - if (data?.vip) { - setStorage("userId", normalizedUserId); - if (/^user\d+$/.test(normalizedUserId)) { - setStorage("memosAccount", normalizedUserId); - setAnonymousUserCookie(normalizedUserId); - } - savePaidType("vip"); - if (data?.alreadyLinked && data?.email) { - const linkedEmail = String(data.email).trim().toLowerCase(); - setStorage("emailLinked", "1"); - setStorage("userEmail", linkedEmail); - setStorage("userName", linkedEmail); - setUserEmail(linkedEmail); - saveUserName(linkedEmail); - await recoverLinkedVipSession( - linkedEmail, - DEFAULT_EMAIL_PASSWORD, - normalizedUserId - ).catch(() => {}); - } else { - const linked = getStorage("emailLinked") === "1"; - const hasEmail = getStorage("userEmail") || getStorage("userName")?.includes("@"); - if (!linked && !hasEmail) saveUserName(normalizedUserId); - } - return true; + const data = (await res.json().catch(() => null)) as Record | null; + const probe = probeFromCheckByUserBody(data, res.ok); + if (probe === "unknown") { + return false; } - savePaidType(null); - return false; + if (probe === "no") { + savePaidType(null); + return false; + } + if (!data?.vip) { + return false; + } + setStorage("userId", normalizedUserId); + if (/^user\d+$/.test(normalizedUserId)) { + setStorage("memosAccount", normalizedUserId); + setAnonymousUserCookie(normalizedUserId); + } + savePaidType("vip"); + if (data?.alreadyLinked && data?.email) { + const linkedEmail = String(data.email).trim().toLowerCase(); + setStorage("emailLinked", "1"); + setStorage("userEmail", linkedEmail); + setStorage("userName", linkedEmail); + setUserEmail(linkedEmail); + saveUserName(linkedEmail); + await recoverLinkedVipSession( + linkedEmail, + DEFAULT_EMAIL_PASSWORD, + normalizedUserId + ).catch(() => {}); + } else { + const linked = getStorage("emailLinked") === "1"; + const hasEmail = getStorage("userEmail") || getStorage("userName")?.includes("@"); + if (!linked && !hasEmail) saveUserName(normalizedUserId); + } + return true; } catch (err) { console.error("验证失败:", err); return false; @@ -501,32 +561,21 @@ export default function HomeClient() { } catch { /* ignore */ } } - // 后台悄悄校验:user 被删或 VIP 被撤则清除本地存储并更新状态 + // 后台悄悄校验:仅当服务端明确返回「非 VIP」才清本地;弱网/异常/uncertain 一律保留(避免微信内误踢) const verifyInBackground = async () => { const isAnonymous = /^user\d+$/.test(localUserId); - let verified = false; - let skipClear = false; - try { - if (isAnonymous) { - verified = await checkPaymentFromPB(localUserId); - } else { - const vipRes = await fetch("/api/vip/check", { credentials: "include", cache: "no-store" }); - const vipData = await vipRes.json(); - if (vipData?.adminUnavailable) { - skipClear = true; - } else { - verified = !!vipData?.vip; - } - } - } catch { /* ignore */ } - if (!verified && !skipClear) { - removeStorage("paidType"); - removeStorage("userName"); - removeStorage("userEmail"); - savePaidType(null); - setUserName(null); - setUserEmail(null); + const probe = isAnonymous + ? await probeCheckByUserId(localUserId) + : await probeSessionVip(); + if (probe !== "no") { + return; } + removeStorage("paidType"); + removeStorage("userName"); + removeStorage("userEmail"); + savePaidType(null); + setUserName(null); + setUserEmail(null); }; void verifyInBackground(); return; @@ -704,7 +753,7 @@ export default function HomeClient() { /> ) : ( -
+
)} diff --git a/app/HomeClientLoader.tsx b/app/HomeClientLoader.tsx index 07188f1..408e1a2 100644 --- a/app/HomeClientLoader.tsx +++ b/app/HomeClientLoader.tsx @@ -4,7 +4,9 @@ import dynamic from "next/dynamic"; const HomeClient = dynamic(() => import("./HomeClient"), { ssr: false, - loading: () =>
, + loading: () => ( +
+ ), }); export function HomeClientLoader() { diff --git a/app/api/vip/check-by-user/route.ts b/app/api/vip/check-by-user/route.ts index 6c32549..510bd34 100644 --- a/app/api/vip/check-by-user/route.ts +++ b/app/api/vip/check-by-user/route.ts @@ -20,6 +20,8 @@ type UserCheckPayload = { ok: boolean; vip: boolean; alreadyLinked: boolean; + /** 服务端异常/超时时为 true,前端勿据此清空本地已登录 VIP */ + uncertain?: boolean; email?: string; linkedUserId?: string; }; @@ -292,18 +294,25 @@ export async function POST(request: NextRequest) { }; } catch (error) { console.error("[check-by-user] failed:", error); - // 接口容错:即使 PB 回退失败,也不抛 500 给前端 - return { ok: true, vip: false, alreadyLinked: false }; + // 勿伪装成「确定非 VIP」,否则微信/H5 弱网会误清本地态 + return { + ok: false, + vip: false, + alreadyLinked: false, + uncertain: true, + }; } })(); inflightUserRequests.set(userId, task); try { const payload = await task; - userCheckResultCache.set(userId, { - payload, - expiresAt: Date.now() + USER_CHECK_CACHE_TTL_MS, - }); + if (!payload.uncertain) { + userCheckResultCache.set(userId, { + payload, + expiresAt: Date.now() + USER_CHECK_CACHE_TTL_MS, + }); + } return NextResponse.json(payload); } finally { inflightUserRequests.delete(userId); diff --git a/app/api/vip/check/route.ts b/app/api/vip/check/route.ts index 248a7e6..507010c 100644 --- a/app/api/vip/check/route.ts +++ b/app/api/vip/check/route.ts @@ -86,6 +86,10 @@ export async function GET(request: NextRequest) { return NextResponse.json({ ok: true, vip: false }); } catch { - return NextResponse.json({ ok: true, vip: false }); + return NextResponse.json({ + ok: true, + vip: false, + uncertain: true, + }); } }