"use client"; import { useCallback, useEffect, useState } from "react"; import PocketBase from "pocketbase"; 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 { PayAuthModal } from "@/app/vip/components/landing/PayAuthModal"; import styles from "@/app/vip/components/vip.module.css"; const PB_URL = process.env.NEXT_PUBLIC_POCKETBASE_URL || "https://pocketbase.hackrobot.cn"; /** 微信内支付渠道:zpay | xorpay */ const WECHAT_PAY_PROVIDER: "zpay" | "xorpay" = "xorpay"; 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"; }): Promise { 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 (data?.redirect_url) { window.open(data.redirect_url, "_blank"); return orderId; } const form = document.createElement("form"); form.method = "POST"; form.action = data.payUrl; form.target = "_blank"; form.style.display = "none"; for (const [k, v] of Object.entries(data.params as Record)) { 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; form.target = "_self"; form.style.display = "none"; for (const [k, v] of Object.entries(data.params as Record)) { 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); } function setStorage(key: string, value: string): void { if (typeof window === "undefined") return; localStorage.setItem(key, value); } function removeStorage(key: string): void { if (typeof window === "undefined") return; localStorage.removeItem(key); } export default function Home() { const [paidType, setPaidType] = useState(null); const [userName, setUserName] = useState(null); const [loading, setLoading] = useState(true); const [payPendingOrderId, setPayPendingOrderId] = useState(null); const savePaidType = useCallback((type: string | null) => { if (type) setStorage("paidType", type); else removeStorage("paidType"); setPaidType(type); }, []); const saveUserName = useCallback((name: string) => { setStorage("userName", name); setUserName(name); }, []); const getOrCreateUserId = useCallback(async () => { // 优先使用 PocketBase 用户 ID(已登录时,含跨站 Cookie) try { const meRes = await fetch("/api/auth/me", { credentials: "include", cache: "no-store" }); const meData = await meRes.json(); if (meData?.user?.id) { const pbUserId = meData.user.id; setStorage("userId", pbUserId); return pbUserId; } } catch { /* ignore */ } let userId = getStorage("userId"); if (userId?.startsWith("{") && userId.includes("data")) { try { const parsed = JSON.parse(userId) as { data?: string }; if (parsed?.data) { userId = parsed.data; setStorage("userId", userId); } } catch { userId = null; } } if (!userId) { userId = `user${Date.now()}`; setStorage("userId", userId); } return userId; }, []); const checkPaymentFromPB = useCallback( async (userId: string): Promise => { try { const pb = new PocketBase(PB_URL); const records = await pb.collection("payments").getList(1, 1, { filter: `user_id = "${userId}"`, sort: "-created", }); if (records.items.length > 0) { const latestType = records.items[0].type as string; const dbUserId = records.items[0].user_id as string; setStorage("userId", dbUserId); savePaidType(latestType); saveUserName(dbUserId); return latestType === "vip"; } savePaidType(null); return false; } catch (err) { console.error("查询 PocketBase 失败:", err); return false; } }, [savePaidType, saveUserName] ); /** 会员账号恢复:通过邮箱+密码验证,关联 users 后检查 site_vip / payments */ const checkVerifyByEmail = useCallback( async (email: string, password: string): Promise => { try { const { pbLogin, pbSaveAuth } = await import("@/app/lib/pocketbase"); const result = await pbLogin(email, password); pbSaveAuth(result.token, result.record); await fetch("/api/auth/sync-session", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ token: result.token, record: result.record }), credentials: "include", }); const userId = result.record.id; const vipRes = await fetch("/api/vip/check", { credentials: "include", cache: "no-store" }); const vipData = await vipRes.json(); if (vipData?.vip) { setStorage("userId", userId); savePaidType("vip"); saveUserName(email); return true; } const found = await checkPaymentFromPB(userId); if (found) return true; return false; } catch { return false; } }, [checkPaymentFromPB, savePaidType, saveUserName] ); const clearAllStorage = useCallback(() => { try { if (typeof window === "undefined") return; const keys = ["userId", "paidType", "userName"]; keys.forEach((k) => { try { localStorage.removeItem(k); } catch { /* ignore */ } }); setPaidType(null); setUserName(null); setLoading(false); const path = window.location.pathname || "/"; window.location.replace(path); } catch (error) { console.error("清空存储失败:", error); } }, []); const [authForPayOpen, setAuthForPayOpen] = useState(false); const doPay = useCallback(async () => { const userId = await getOrCreateUserId(); 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); if (orderId) setPayPendingOrderId(orderId); } else { redirectToPayZpay({ ...base, device }); } }, [getOrCreateUserId]); const handlePay = useCallback(async () => { try { const meRes = await fetch("/api/auth/me", { credentials: "include", cache: "no-store" }); const meData = await meRes.json(); if (meData?.user?.id) { doPay(); return; } } catch { /* ignore */ } setAuthForPayOpen(true); }, [doPay]); const handleVerifySuccess = useCallback(() => { window.location.reload(); }, []); const env = getPayEnv(); const device = getDeviceFromEnv(env); const isH5 = device === "h5"; usePayStatusPoll( (orderId) => { let uid = getStorage("userId"); if (!uid && orderId?.includes("_order_")) { const prefix = orderId.split("_order_")[0]; const parts = prefix.split("_"); if (parts.length >= 2) uid = parts.slice(0, -1).join("_"); } if (uid) saveUserName(uid); savePaidType("vip"); if (isH5) setPayPendingOrderId(null); window.location.reload(); }, () => { if (isH5) setPayPendingOrderId(null); }, isH5 ? payPendingOrderId : undefined ); useEffect(() => { const urlParams = new URLSearchParams(window.location.search); if (urlParams.get("type")?.trim() === "clean") { clearAllStorage(); return; } if (urlParams.get("paid") === "1") { savePaidType("vip"); saveUserName(getStorage("userId") || ""); } const init = async () => { await getOrCreateUserId(); const localType = getStorage("paidType"); const localUserName = getStorage("userName"); if (localType && localUserName) { setPaidType(localType); setUserName(localUserName); setLoading(false); return; } // SSO: 优先检查根域 Cookie(跨站登录态,如从 digital 登录后跳转过来) try { const meRes = await fetch("/api/auth/me", { credentials: "include", cache: "no-store" }); const meData = await meRes.json(); if (meData?.user && meData?.token) { const { pbSaveAuth } = await import("@/app/lib/pocketbase"); pbSaveAuth(meData.token, { id: meData.user.id, email: meData.user.email }); const vipRes = await fetch("/api/vip/check", { credentials: "include", cache: "no-store" }); const vipData = await vipRes.json(); if (vipData?.vip) { setStorage("userId", meData.user.id); savePaidType("vip"); saveUserName(meData.user.email || meData.user.id); setLoading(false); return; } } } catch { /* ignore */ } const userId = getStorage("userId"); if (userId) { const found = await checkPaymentFromPB(userId); if (found) { setUserName(getStorage("userName")); } } setLoading(false); }; init(); }, [clearAllStorage, getOrCreateUserId, checkPaymentFromPB, savePaidType, saveUserName]); useEffect(() => { const syncFromStorage = () => { const localType = getStorage("paidType"); const localUserName = getStorage("userName"); if (localType && localUserName && !paidType) { setPaidType(localType); setUserName(localUserName); } }; const onVisible = () => syncFromStorage(); const onPageShow = (e: PageTransitionEvent) => { if (e.persisted) syncFromStorage(); }; if (typeof document !== "undefined" && document.addEventListener) { document.addEventListener("visibilitychange", onVisible); window.addEventListener("pageshow", onPageShow); return () => { document.removeEventListener("visibilitychange", onVisible); window.removeEventListener("pageshow", onPageShow); }; } }, [paidType]); const handleLogout = useCallback(async () => { const { pbLogout } = await import("@/app/lib/pocketbase"); await pbLogout(); clearAllStorage(); }, [clearAllStorage]); const isLoggedIn = paidType === "vip"; if (loading) { return (

加载中...

); } return ( <> {isLoggedIn ? ( ) : ( )} setAuthForPayOpen(false)} onSuccess={() => { setAuthForPayOpen(false); doPay(); }} /> ); }