'fix'
This commit is contained in:
@@ -85,6 +85,57 @@ function getPendingOrderId(): string | null {
|
|||||||
return orderId || null;
|
return orderId || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type VipProbeResult = "yes" | "no" | "unknown";
|
||||||
|
|
||||||
|
/** 仅当明确 yes/no;弱网/5xx/服务端 uncertain 一律 unknown,禁止用来清空本地 VIP */
|
||||||
|
function probeFromCheckByUserBody(
|
||||||
|
data: Record<string, unknown> | 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<VipProbeResult> {
|
||||||
|
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<string, unknown> | null;
|
||||||
|
return probeFromCheckByUserBody(data, res.ok);
|
||||||
|
} catch {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function probeSessionVip(): Promise<VipProbeResult> {
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/vip/check", {
|
||||||
|
credentials: "include",
|
||||||
|
cache: "no-store",
|
||||||
|
});
|
||||||
|
const data = (await res.json().catch(() => null)) as Record<string, unknown> | 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) */
|
/** 仅在本组件内使用(由 page.tsx dynamic ssr:false 仅客户端挂载,首帧可读 localStorage) */
|
||||||
function readVipBootstrapState(): {
|
function readVipBootstrapState(): {
|
||||||
paidType: string | null;
|
paidType: string | null;
|
||||||
@@ -305,36 +356,45 @@ export default function HomeClient() {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ user_id: normalizedUserId }),
|
body: JSON.stringify({ user_id: normalizedUserId }),
|
||||||
|
credentials: "same-origin",
|
||||||
|
cache: "no-store",
|
||||||
});
|
});
|
||||||
const data = await res.json().catch(() => ({}));
|
const data = (await res.json().catch(() => null)) as Record<string, unknown> | null;
|
||||||
if (data?.vip) {
|
const probe = probeFromCheckByUserBody(data, res.ok);
|
||||||
setStorage("userId", normalizedUserId);
|
if (probe === "unknown") {
|
||||||
if (/^user\d+$/.test(normalizedUserId)) {
|
return false;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
savePaidType(null);
|
if (probe === "no") {
|
||||||
return false;
|
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) {
|
} catch (err) {
|
||||||
console.error("验证失败:", err);
|
console.error("验证失败:", err);
|
||||||
return false;
|
return false;
|
||||||
@@ -501,32 +561,21 @@ export default function HomeClient() {
|
|||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 后台悄悄校验:user 被删或 VIP 被撤则清除本地存储并更新状态
|
// 后台悄悄校验:仅当服务端明确返回「非 VIP」才清本地;弱网/异常/uncertain 一律保留(避免微信内误踢)
|
||||||
const verifyInBackground = async () => {
|
const verifyInBackground = async () => {
|
||||||
const isAnonymous = /^user\d+$/.test(localUserId);
|
const isAnonymous = /^user\d+$/.test(localUserId);
|
||||||
let verified = false;
|
const probe = isAnonymous
|
||||||
let skipClear = false;
|
? await probeCheckByUserId(localUserId)
|
||||||
try {
|
: await probeSessionVip();
|
||||||
if (isAnonymous) {
|
if (probe !== "no") {
|
||||||
verified = await checkPaymentFromPB(localUserId);
|
return;
|
||||||
} 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);
|
|
||||||
}
|
}
|
||||||
|
removeStorage("paidType");
|
||||||
|
removeStorage("userName");
|
||||||
|
removeStorage("userEmail");
|
||||||
|
savePaidType(null);
|
||||||
|
setUserName(null);
|
||||||
|
setUserEmail(null);
|
||||||
};
|
};
|
||||||
void verifyInBackground();
|
void verifyInBackground();
|
||||||
return;
|
return;
|
||||||
@@ -704,7 +753,7 @@ export default function HomeClient() {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="min-h-[50vh]" aria-hidden />
|
<div className="min-h-screen bg-[var(--background)]" aria-hidden />
|
||||||
)}
|
)}
|
||||||
</VipLayout>
|
</VipLayout>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import dynamic from "next/dynamic";
|
|||||||
|
|
||||||
const HomeClient = dynamic(() => import("./HomeClient"), {
|
const HomeClient = dynamic(() => import("./HomeClient"), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
loading: () => <div className="min-h-screen bg-[var(--background)]" aria-hidden />,
|
loading: () => (
|
||||||
|
<div className="min-h-screen bg-[var(--background)] text-[var(--foreground)]" aria-hidden />
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function HomeClientLoader() {
|
export function HomeClientLoader() {
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ type UserCheckPayload = {
|
|||||||
ok: boolean;
|
ok: boolean;
|
||||||
vip: boolean;
|
vip: boolean;
|
||||||
alreadyLinked: boolean;
|
alreadyLinked: boolean;
|
||||||
|
/** 服务端异常/超时时为 true,前端勿据此清空本地已登录 VIP */
|
||||||
|
uncertain?: boolean;
|
||||||
email?: string;
|
email?: string;
|
||||||
linkedUserId?: string;
|
linkedUserId?: string;
|
||||||
};
|
};
|
||||||
@@ -292,18 +294,25 @@ export async function POST(request: NextRequest) {
|
|||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[check-by-user] failed:", error);
|
console.error("[check-by-user] failed:", error);
|
||||||
// 接口容错:即使 PB 回退失败,也不抛 500 给前端
|
// 勿伪装成「确定非 VIP」,否则微信/H5 弱网会误清本地态
|
||||||
return { ok: true, vip: false, alreadyLinked: false };
|
return {
|
||||||
|
ok: false,
|
||||||
|
vip: false,
|
||||||
|
alreadyLinked: false,
|
||||||
|
uncertain: true,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
inflightUserRequests.set(userId, task);
|
inflightUserRequests.set(userId, task);
|
||||||
try {
|
try {
|
||||||
const payload = await task;
|
const payload = await task;
|
||||||
userCheckResultCache.set(userId, {
|
if (!payload.uncertain) {
|
||||||
payload,
|
userCheckResultCache.set(userId, {
|
||||||
expiresAt: Date.now() + USER_CHECK_CACHE_TTL_MS,
|
payload,
|
||||||
});
|
expiresAt: Date.now() + USER_CHECK_CACHE_TTL_MS,
|
||||||
|
});
|
||||||
|
}
|
||||||
return NextResponse.json(payload);
|
return NextResponse.json(payload);
|
||||||
} finally {
|
} finally {
|
||||||
inflightUserRequests.delete(userId);
|
inflightUserRequests.delete(userId);
|
||||||
|
|||||||
@@ -86,6 +86,10 @@ export async function GET(request: NextRequest) {
|
|||||||
|
|
||||||
return NextResponse.json({ ok: true, vip: false });
|
return NextResponse.json({ ok: true, vip: false });
|
||||||
} catch {
|
} catch {
|
||||||
return NextResponse.json({ ok: true, vip: false });
|
return NextResponse.json({
|
||||||
|
ok: true,
|
||||||
|
vip: false,
|
||||||
|
uncertain: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user