From 1ef2c612a9157cd9c74fae0fcf3e7b322b204ea1 Mon Sep 17 00:00:00 2001 From: eric Date: Tue, 10 Mar 2026 08:58:50 -0500 Subject: [PATCH] ;'s' --- app/lib/usePayStatusPoll.ts | 66 +++++++++++-------------------------- app/page.tsx | 62 +++++++++++++++++++--------------- 2 files changed, 55 insertions(+), 73 deletions(-) diff --git a/app/lib/usePayStatusPoll.ts b/app/lib/usePayStatusPoll.ts index a2f259e..8aabc68 100644 --- a/app/lib/usePayStatusPoll.ts +++ b/app/lib/usePayStatusPoll.ts @@ -1,12 +1,11 @@ /** - * 支付状态轮询:点击支付后轮询,支付成功刷新,超时重置 - * 优先使用 sessionStorage(pay_pending_order),其次 cookie + * 支付状态轮询 + * - 手机 H5:传入 orderId(点击支付后由 state 传入),直接轮询,不依赖 sessionStorage/cookie + * - PC/微信:不传 orderId,使用 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; @@ -22,29 +21,6 @@ function clearCookie(name: string): void { 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("|"); @@ -56,20 +32,12 @@ function parsePayOrderCookie(raw: string | null): [string | null, boolean] { 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 { +export function usePayStatusPoll( + onPaid: (orderId?: string) => void, + onTimeout?: () => void, + /** 手机 H5:点击支付后传入的 orderId,直接轮询,不依赖 sessionStorage/cookie */ + orderIdFromState?: string | null +): boolean { const [polling, setPolling] = useState(false); const onPaidRef = useRef(onPaid); const onTimeoutRef = useRef(onTimeout); @@ -77,10 +45,16 @@ export function usePayStatusPoll(onPaid: (orderId?: string) => void, onTimeout?: onTimeoutRef.current = onTimeout; useEffect(() => { - const [orderId] = getOrderIdToPoll(); + const orderId = + orderIdFromState !== undefined + ? (orderIdFromState?.trim() || null) + : (() => { + const raw = getCookie(COOKIE_NAME); + const [oid, valid] = parsePayOrderCookie(raw); + return valid ? oid : null; + })(); if (!orderId) { - const raw = getCookie(COOKIE_NAME); - if (raw) clearCookie(COOKIE_NAME); + if (orderIdFromState === undefined && getCookie(COOKIE_NAME)) clearCookie(COOKIE_NAME); return; } @@ -90,7 +64,6 @@ export function usePayStatusPoll(onPaid: (orderId?: string) => void, onTimeout?: count++; if (count > MAX_POLLS) { clearInterval(timer); - clearSessionOrder(); clearCookie(COOKIE_NAME); setPolling(false); onTimeoutRef.current?.(); @@ -101,7 +74,6 @@ export function usePayStatusPoll(onPaid: (orderId?: string) => void, onTimeout?: const data = await res.json(); if (data?.ok && data?.paid) { clearInterval(timer); - clearSessionOrder(); clearCookie(COOKIE_NAME); setPolling(false); onPaidRef.current(orderId); @@ -112,7 +84,7 @@ export function usePayStatusPoll(onPaid: (orderId?: string) => void, onTimeout?: }, POLL_INTERVAL); return () => clearInterval(timer); - }, []); + }, [orderIdFromState]); return polling; } diff --git a/app/page.tsx b/app/page.tsx index ba0911f..53ab719 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -8,7 +8,7 @@ import { indexMd } from "./index-content"; import styles from "./index.module.css"; import { getPayEnv, getDeviceFromEnv } from "@/app/lib/payEnv"; import { getPaymentConfig } from "@/config/services.config"; -import { usePayStatusPoll, setPayPendingOrder } from "@/app/lib/usePayStatusPoll"; +import { usePayStatusPoll } from "@/app/lib/usePayStatusPoll"; const PB_URL = process.env.NEXT_PUBLIC_POCKETBASE_URL || "https://pocketbase.hackrobot.cn"; @@ -51,14 +51,14 @@ function redirectToPayZpay(params: { form.submit(); } -/** H5 手机:fetch 获取 order_id + 支付参数,写入 sessionStorage 后提交表单,支持轮询 */ +/** H5 手机:fetch 获取 order_id,新标签页打开支付,当前页保持可轮询 */ 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" }, @@ -72,18 +72,17 @@ async function redirectToPayZpayH5(params: { const data = await res.json().catch(() => ({})); if (!data?.ok || !data?.params || !data?.payUrl) { alert(data?.error || "支付发起失败"); - return; + return null; } - const orderId = data?.order_id?.trim(); - if (orderId) setPayPendingOrder(orderId); + const orderId = data?.order_id?.trim() || null; if (data?.redirect_url) { - window.location.href = data.redirect_url; - return; + window.open(data.redirect_url, "_blank"); + return orderId; } const form = document.createElement("form"); form.method = "POST"; form.action = data.payUrl; - form.target = "_self"; + form.target = "_blank"; form.style.display = "none"; for (const [k, v] of Object.entries(data.params as Record)) { if (v == null) continue; @@ -95,9 +94,11 @@ async function redirectToPayZpayH5(params: { } document.body.appendChild(form); form.submit(); + document.body.removeChild(form); + return orderId; } -/** 微信内:fetch 获取 xorpay 参数,写入 order_id 后提交表单到收银台,无中转页 */ +/** 微信内:fetch 获取 xorpay 参数,直接提交表单到收银台,无中转页 */ async function payWechatXorpay(params: { user_id: string; return_url: string; @@ -121,8 +122,6 @@ async function payWechatXorpay(params: { alert(data?.error || "支付发起失败"); return; } - const orderId = data?.order_id?.trim(); - if (orderId) setPayPendingOrder(orderId); const form = document.createElement("form"); form.method = "POST"; form.action = data.payUrl; @@ -159,6 +158,7 @@ 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 contentRef = useRef(null); const savePaidType = useCallback((type: string | null) => { @@ -250,7 +250,7 @@ export default function Home() { } }, []); - const handlePay = useCallback(() => { + const handlePay = useCallback(async () => { const userId = getOrCreateUserId(); const returnUrl = `${window.location.origin}/paid`; const env = getPayEnv(); @@ -260,7 +260,8 @@ export default function Home() { if (device === "wechat" && WECHAT_PAY_PROVIDER === "xorpay") { payWechatXorpay(base); } else if (device === "h5") { - redirectToPayZpayH5(base); + const orderId = await redirectToPayZpayH5(base); + if (orderId) setPayPendingOrderId(orderId); } else { redirectToPayZpay({ ...base, device }); } @@ -274,18 +275,27 @@ export default function Home() { } }, [userName]); - // 微信/手机 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"); - 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);