's'
This commit is contained in:
260
app/page.tsx
260
app/page.tsx
@@ -1,150 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { triggerPay } from "@/app/lib/triggerPay";
|
||||
import { getPayEnv, getDeviceFromEnv } from "@/app/lib/payEnv";
|
||||
import { getPaymentConfig } from "@/config/services.config";
|
||||
import { usePayStatusPoll } from "@/app/lib/usePayStatusPoll";
|
||||
import { VipLayout } from "@/app/vip/components/VipLayout";
|
||||
import { LandingPage } from "@/app/vip/components/LandingPage";
|
||||
import { DashboardPage } from "@/app/vip/components/DashboardPage";
|
||||
import styles from "@/app/vip/components/vip.module.css";
|
||||
|
||||
/** 微信内支付渠道:zpay | xorpay */
|
||||
const WECHAT_PAY_PROVIDER: "zpay" | "xorpay" = "xorpay";
|
||||
// 渠道规则:pc/手机浏览器 -> zpay,仅微信内 -> xorpay
|
||||
const DEFAULT_EMAIL_PASSWORD = "12345678";
|
||||
const ANONYMOUS_USER_COOKIE = "nomadvip_anon_uid";
|
||||
const ANONYMOUS_USER_COOKIE_MAX_AGE = 60 * 60 * 24 * 30;
|
||||
const PAY_ORDER_COOKIE = "nomadvip_pay_order";
|
||||
const PAY_ORDER_MAX_AGE = 60 * 15;
|
||||
|
||||
function redirectToPayZpay(params: {
|
||||
user_id: string;
|
||||
return_url: string;
|
||||
total_fee: number;
|
||||
type: string;
|
||||
channel: "alipay" | "wxpay";
|
||||
device: "pc" | "h5" | "wechat";
|
||||
}) {
|
||||
const form = document.createElement("form");
|
||||
form.method = "POST";
|
||||
form.action = "/api/pay";
|
||||
form.target = "_self";
|
||||
form.style.display = "none";
|
||||
const fields: [string, string][] = [
|
||||
["user_id", params.user_id],
|
||||
["return_url", params.return_url],
|
||||
["total_fee", String(params.total_fee)],
|
||||
["type", params.type],
|
||||
["channel", params.channel],
|
||||
["device", params.device],
|
||||
["provider", "zpay"],
|
||||
["html", "1"],
|
||||
];
|
||||
fields.forEach(([k, v]) => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "hidden";
|
||||
input.name = k;
|
||||
input.value = v;
|
||||
form.appendChild(input);
|
||||
});
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
|
||||
async function redirectToPayZpayH5(params: {
|
||||
user_id: string;
|
||||
return_url: string;
|
||||
total_fee: number;
|
||||
type: string;
|
||||
channel: "alipay" | "wxpay";
|
||||
}, onPendingOrder?: (orderId: string) => void): Promise<string | null> {
|
||||
const res = await fetch("/api/pay", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
...params,
|
||||
device: "h5",
|
||||
provider: "zpay",
|
||||
html: "0",
|
||||
}),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!data?.ok || !data?.params || !data?.payUrl) {
|
||||
alert(data?.error || "支付发起失败");
|
||||
return null;
|
||||
}
|
||||
const orderId = data?.order_id?.trim() || null;
|
||||
if (orderId) {
|
||||
savePendingOrder(orderId);
|
||||
onPendingOrder?.(orderId);
|
||||
}
|
||||
if (data?.redirect_url) {
|
||||
window.location.href = data.redirect_url;
|
||||
return orderId;
|
||||
}
|
||||
const form = document.createElement("form");
|
||||
form.method = "POST";
|
||||
form.action = data.payUrl;
|
||||
form.target = "_self";
|
||||
form.style.display = "none";
|
||||
for (const [k, v] of Object.entries(data.params as Record<string, string>)) {
|
||||
if (v == null) continue;
|
||||
const input = document.createElement("input");
|
||||
input.type = "hidden";
|
||||
input.name = k;
|
||||
input.value = String(v);
|
||||
form.appendChild(input);
|
||||
}
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
document.body.removeChild(form);
|
||||
return orderId;
|
||||
}
|
||||
|
||||
async function payWechatXorpay(params: {
|
||||
user_id: string;
|
||||
return_url: string;
|
||||
total_fee: number;
|
||||
type: string;
|
||||
channel: "alipay" | "wxpay";
|
||||
}) {
|
||||
const res = await fetch("/api/pay", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
...params,
|
||||
total_fee: params.total_fee,
|
||||
device: "wechat",
|
||||
provider: "xorpay",
|
||||
html: "0",
|
||||
}),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!data?.ok || !data?.params || !data?.payUrl) {
|
||||
alert(data?.error || "支付发起失败");
|
||||
return;
|
||||
}
|
||||
const form = document.createElement("form");
|
||||
form.method = "POST";
|
||||
form.action = data.payUrl;
|
||||
const orderId = data?.order_id?.trim();
|
||||
if (orderId) {
|
||||
savePendingOrder(orderId);
|
||||
}
|
||||
form.target = "_self";
|
||||
form.style.display = "none";
|
||||
for (const [k, v] of Object.entries(data.params as Record<string, string>)) {
|
||||
if (v == null) continue;
|
||||
const input = document.createElement("input");
|
||||
input.type = "hidden";
|
||||
input.name = k;
|
||||
input.value = String(v);
|
||||
form.appendChild(input);
|
||||
}
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
|
||||
function getStorage(key: string): string | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
return localStorage.getItem(key);
|
||||
@@ -213,11 +84,11 @@ function getPendingOrderId(): string | null {
|
||||
return orderId || null;
|
||||
}
|
||||
|
||||
/** 首页永远不显示 loading,所有接口在后台静默执行 */
|
||||
export default function Home() {
|
||||
const [paidType, setPaidType] = useState<string | null>(null);
|
||||
const [userName, setUserName] = useState<string | null>(null);
|
||||
const [userEmail, setUserEmail] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [payPendingOrderId, setPayPendingOrderId] = useState<string | null>(null);
|
||||
|
||||
const savePaidType = useCallback((type: string | null) => {
|
||||
@@ -490,7 +361,6 @@ export default function Home() {
|
||||
clearPendingOrder();
|
||||
setPaidType(null);
|
||||
setUserName(null);
|
||||
setLoading(false);
|
||||
const path = window.location.pathname || "/";
|
||||
window.location.replace(path);
|
||||
} catch (error) {
|
||||
@@ -498,37 +368,9 @@ export default function Home() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const doPay = useCallback(async () => {
|
||||
const userId = await getOrCreateUserId();
|
||||
if (/^user\d+$/.test(userId)) {
|
||||
setStorage("memosAccount", userId);
|
||||
setStorage("userId", userId);
|
||||
setAnonymousUserCookie(userId);
|
||||
}
|
||||
const returnUrl = `${window.location.origin}/paid`;
|
||||
const env = getPayEnv();
|
||||
const device = getDeviceFromEnv(env);
|
||||
const totalFee = getPaymentConfig().defaultAmount ?? 8800;
|
||||
const base = {
|
||||
user_id: userId,
|
||||
return_url: returnUrl,
|
||||
total_fee: totalFee,
|
||||
type: "vip",
|
||||
channel: "wxpay" as const,
|
||||
};
|
||||
if (device === "wechat" && WECHAT_PAY_PROVIDER === "xorpay") {
|
||||
payWechatXorpay(base);
|
||||
} else if (device === "h5") {
|
||||
const orderId = await redirectToPayZpayH5(base, setPayPendingOrderId);
|
||||
if (orderId) setPayPendingOrderId(orderId);
|
||||
} else {
|
||||
redirectToPayZpay({ ...base, device });
|
||||
}
|
||||
}, [getOrCreateUserId]);
|
||||
|
||||
const handlePay = useCallback(() => {
|
||||
doPay();
|
||||
}, [doPay]);
|
||||
triggerPay(setPayPendingOrderId);
|
||||
}, []);
|
||||
|
||||
const handleVerifySuccess = useCallback(() => {
|
||||
window.location.reload();
|
||||
@@ -593,46 +435,56 @@ export default function Home() {
|
||||
const pendingOrderId = getPendingOrderId();
|
||||
if (pendingOrderId) {
|
||||
setPayPendingOrderId(pendingOrderId);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 本地已有 VIP 标识时优先使用,不因跨站登录态覆盖
|
||||
// 本地已有 VIP 标识时优先使用,立即展示;后台悄悄校验,若 user 被删或 VIP 被撤则更新本地状态
|
||||
const localType = getStorage("paidType");
|
||||
const localUserName = getStorage("userName");
|
||||
const localUserEmail = getStorage("userEmail");
|
||||
const localUserId = getStorage("userId");
|
||||
if (localType === "vip" && (localUserName || localUserEmail) && localUserId) {
|
||||
const isAnonymous = /^user\d+$/.test(localUserId);
|
||||
let verified = false;
|
||||
if (isAnonymous) {
|
||||
verified = await checkPaymentFromPB(localUserId);
|
||||
} else {
|
||||
const vipRes = await fetch("/api/vip/check", { credentials: "include", cache: "no-store" });
|
||||
const vipData = await vipRes.json();
|
||||
verified = !!vipData?.vip;
|
||||
// 立即展示 VIP 状态,不阻塞首屏
|
||||
setPaidType(localType);
|
||||
setUserName(localUserEmail || localUserName || "");
|
||||
setUserEmail(localUserEmail || (localUserName?.includes("@") ? localUserName : null));
|
||||
if (!localUserEmail && !localUserName?.includes("@")) {
|
||||
try {
|
||||
const meRes = await fetch("/api/auth/me", { credentials: "include", cache: "no-store" });
|
||||
const meData = await meRes.json();
|
||||
if (meData?.user?.email) setUserEmail(meData.user.email);
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
if (verified) {
|
||||
setPaidType(localType);
|
||||
setUserName(localUserEmail || localUserName || "");
|
||||
setUserEmail(localUserEmail || (localUserName?.includes("@") ? localUserName : null));
|
||||
if (!localUserEmail && !localUserName?.includes("@")) {
|
||||
try {
|
||||
const meRes = await fetch("/api/auth/me", { credentials: "include", cache: "no-store" });
|
||||
const meData = await meRes.json();
|
||||
if (meData?.user?.email) setUserEmail(meData.user.email);
|
||||
} catch { /* ignore */ }
|
||||
|
||||
// 后台悄悄校验:user 被删或 VIP 被撤则清除本地存储并更新状态
|
||||
const verifyInBackground = async () => {
|
||||
const isAnonymous = /^user\d+$/.test(localUserId);
|
||||
let verified = 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();
|
||||
verified = !!vipData?.vip;
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
if (!verified) {
|
||||
removeStorage("paidType");
|
||||
removeStorage("userName");
|
||||
removeStorage("userEmail");
|
||||
savePaidType(null);
|
||||
setUserName(null);
|
||||
setUserEmail(null);
|
||||
}
|
||||
setLoading(false);
|
||||
return;
|
||||
} else {
|
||||
removeStorage("paidType");
|
||||
removeStorage("userName");
|
||||
removeStorage("userEmail");
|
||||
}
|
||||
};
|
||||
void verifyInBackground();
|
||||
return;
|
||||
}
|
||||
|
||||
// 校验逻辑:有本地存储(登录资料+VIP)时优先使用并校验;无身份/VIP 时点击按钮直接进入支付,不校验
|
||||
const anonymousCandidate = getStoredAnonymousUserId();
|
||||
const hadStoredUserId = !!anonymousCandidate;
|
||||
if (anonymousCandidate) {
|
||||
const recovered = await tryRecoverByAnonymousUserId(anonymousCandidate);
|
||||
if (recovered) {
|
||||
@@ -642,7 +494,6 @@ export default function Home() {
|
||||
const name = getStorage("userEmail") || getStorage("userName") || anonymousCandidate;
|
||||
setUserName(name);
|
||||
setUserEmail(name.includes("@") ? name : getStorage("userEmail"));
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -656,7 +507,6 @@ export default function Home() {
|
||||
setPaidType(afterLocalType);
|
||||
setUserName(afterLocalUserEmail || afterLocalUserName || "");
|
||||
setUserEmail(afterLocalUserEmail || (afterLocalUserName?.includes("@") ? afterLocalUserName : null));
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -688,7 +538,6 @@ export default function Home() {
|
||||
}
|
||||
savePaidType("vip");
|
||||
saveUserName(meData.user.email || meData.user.id);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -696,15 +545,17 @@ export default function Home() {
|
||||
/* ignore */
|
||||
}
|
||||
|
||||
const userIdForCheck = getStoredAnonymousUserId() || getStorage("userId");
|
||||
if (userIdForCheck) {
|
||||
const found = await checkPaymentFromPB(userIdForCheck);
|
||||
if (found) {
|
||||
const name = getStorage("userEmail") || getStorage("userName");
|
||||
if (name) setUserName(name);
|
||||
// 仅当有本地存储的 userId 时才校验(无身份/VIP 时跳过,点击按钮直接进入支付流程)
|
||||
if (hadStoredUserId) {
|
||||
const userIdForCheck = getStoredAnonymousUserId() || getStorage("userId");
|
||||
if (userIdForCheck) {
|
||||
const found = await checkPaymentFromPB(userIdForCheck);
|
||||
if (found) {
|
||||
const name = getStorage("userEmail") || getStorage("userName");
|
||||
if (name) setUserName(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
init();
|
||||
@@ -778,15 +629,6 @@ export default function Home() {
|
||||
|
||||
const isLoggedIn = paidType === "vip";
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className={styles.loading}>
|
||||
<div className={styles.loadingSpinner} />
|
||||
<p>加载中...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<VipLayout isLoggedIn={isLoggedIn} userName={userName} userEmail={userEmail} onLogout={handleLogout}>
|
||||
|
||||
Reference in New Issue
Block a user