/** * 支付状态轮询:点击支付后轮询,支付成功刷新,超时重置 * 优先使用 sessionStorage(pay_pending_order),其次 cookie */ import { useEffect, useRef, useState } from "react"; const COOKIE_NAME = "nomadvip_pay_order"; const SESSION_KEY_ORDER = "pay_pending_order"; const SESSION_KEY_TS = "pay_pending_ts"; const POLL_INTERVAL = 2000; const MAX_POLLS = 160; // ~5 分钟 const MAX_AGE_MS = 15 * 60 * 1000; function getCookie(name: string): string | null { if (typeof document === "undefined") return null; const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`)); return match ? decodeURIComponent(match[1]) : null; } function clearCookie(name: string): void { if (typeof document === "undefined") return; document.cookie = `${name}=; path=/; max-age=0`; } function getSessionOrder(): [string | null, boolean] { if (typeof window === "undefined") return [null, false]; const orderId = sessionStorage.getItem(SESSION_KEY_ORDER)?.trim() || null; const ts = sessionStorage.getItem(SESSION_KEY_TS); const timestamp = ts ? parseInt(ts, 10) : NaN; if (!orderId) return [null, false]; if (Number.isNaN(timestamp)) return [orderId, true]; if (Date.now() - timestamp > MAX_AGE_MS) return [orderId, false]; return [orderId, true]; } function setSessionOrder(orderId: string): void { if (typeof window === "undefined") return; sessionStorage.setItem(SESSION_KEY_ORDER, orderId); sessionStorage.setItem(SESSION_KEY_TS, String(Date.now())); } function clearSessionOrder(): void { if (typeof window === "undefined") return; sessionStorage.removeItem(SESSION_KEY_ORDER); sessionStorage.removeItem(SESSION_KEY_TS); } function parsePayOrderCookie(raw: string | null): [string | null, boolean] { if (!raw?.trim()) return [null, false]; const parts = raw.split("|"); const orderId = parts[0]?.trim() || raw.trim(); const ts = parts[1] ? parseInt(parts[1], 10) : NaN; if (!orderId) return [null, false]; if (Number.isNaN(ts)) return [orderId, false]; if (Date.now() - ts > MAX_AGE_MS) return [orderId, false]; return [orderId, true]; } function getOrderIdToPoll(): [string | null, "session" | "cookie" | null] { const [sessionOrder, sessionValid] = getSessionOrder(); if (sessionOrder && sessionValid) return [sessionOrder, "session"]; const raw = getCookie(COOKIE_NAME); const [cookieOrder, cookieValid] = parsePayOrderCookie(raw); if (cookieOrder && cookieValid) return [cookieOrder, "cookie"]; return [null, null]; } export function setPayPendingOrder(orderId: string): void { setSessionOrder(orderId); } export function usePayStatusPoll(onPaid: (orderId?: string) => void, onTimeout?: () => void): boolean { const [polling, setPolling] = useState(false); const onPaidRef = useRef(onPaid); const onTimeoutRef = useRef(onTimeout); onPaidRef.current = onPaid; onTimeoutRef.current = onTimeout; useEffect(() => { const [orderId] = getOrderIdToPoll(); if (!orderId) { const raw = getCookie(COOKIE_NAME); if (raw) clearCookie(COOKIE_NAME); return; } setPolling(true); let count = 0; const timer = setInterval(async () => { count++; if (count > MAX_POLLS) { clearInterval(timer); clearSessionOrder(); clearCookie(COOKIE_NAME); setPolling(false); onTimeoutRef.current?.(); return; } try { const res = await fetch(`/api/pay/status?order_id=${encodeURIComponent(orderId)}`); const data = await res.json(); if (data?.ok && data?.paid) { clearInterval(timer); clearSessionOrder(); clearCookie(COOKIE_NAME); setPolling(false); onPaidRef.current(orderId); } } catch { // 继续轮询 } }, POLL_INTERVAL); return () => clearInterval(timer); }, []); return polling; }