From 81cea024988e502eb241258a937ff4451109f85b Mon Sep 17 00:00:00 2001 From: leiyuee Date: Wed, 8 Jul 2026 18:29:07 +0800 Subject: [PATCH] =?UTF-8?q?fix=EF=BC=9A=E6=94=AF=E4=BB=98=E6=97=A0?= =?UTF-8?q?=E9=99=90=E8=BD=AE=E8=AF=A2=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/api.ts | 57 ++- src/dialog/index.tsx | 34 +- src/dialog/pay-subscribe/index.tsx | 418 ++++++++++-------- .../ExploreTemplateFeed.scss | 56 ++- 4 files changed, 338 insertions(+), 227 deletions(-) diff --git a/src/api/api.ts b/src/api/api.ts index a4f20a3..cd7f718 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -72,6 +72,7 @@ type PaymentStatusResponse = { message?: string; data?: PaymentStatusData; }; +const paymentRequestConfig: AxiosRequestConfig = { skipGlobalErrorHandler: true }; export type AuthUser = Record; export type AuthData = ApiParams & { token?: string; @@ -546,54 +547,70 @@ export const getPlanList = (type: EntityId): Promise => { - return request.post("/trade/subscription/payByGatewayAliPay", { - origin: "skillhub", - ...params, - }); + return request.post( + "/trade/subscription/payByGatewayAliPay", + { + origin: "skillhub", + ...params, + }, + paymentRequestConfig, + ); }; // 订阅微信下单 export const createSubscriptionOrderByWxPay = (params: ApiPayload): Promise => { - return request.post("/trade/subscription/payByGatewayWxPay", { - origin: "skillhub", - ...params, - }); + return request.post( + "/trade/subscription/payByGatewayWxPay", + { + origin: "skillhub", + ...params, + }, + paymentRequestConfig, + ); }; // 订阅支付宝支付结果查询 export const checkGatewayPayment = (params: ApiPayload): Promise => { - return request.post("/trade/subscription/checkGatewayPayment", params); + return request.post("/trade/subscription/checkGatewayPayment", params, paymentRequestConfig); }; // 订阅微信支付结果查询 export const checkGatewayWxPayment = (params: ApiPayload): Promise => { - return request.post("/trade/subscription/checkGatewayWxPayment", params); + return request.post("/trade/subscription/checkGatewayWxPayment", params, paymentRequestConfig); }; // 积分包支付宝下单 export const createPointsOrder = (params: ApiPayload): Promise => { - return request.post("/users/pointPackage/payByGatewayAliPay", { - origin: "skillhub", - ...params, - }); + return request.post( + "/users/pointPackage/payByGatewayAliPay", + { + origin: "skillhub", + ...params, + }, + paymentRequestConfig, + ); }; // 积分包微信下单 export const createPointsOrderByWxPay = (params: ApiPayload): Promise => { - return request.post("/users/pointPackage/payByGatewayWxPay", { - origin: "skillhub", - ...params, - }); + return request.post( + "/users/pointPackage/payByGatewayWxPay", + { + origin: "skillhub", + ...params, + }, + paymentRequestConfig, + ); }; // 积分包支付宝支付结果查询 export const checkPointsPayment = (params: ApiPayload): Promise => { - return request.post("/users/pointPackage/checkGatewayPayment", params); + return request.post("/users/pointPackage/checkGatewayPayment", params, paymentRequestConfig); }; // 积分包微信支付结果查询 export const checkPointsWxPayment = (params: ApiPayload): Promise => { - return request.post("/users/pointPackage/checkGatewayWxPayment", params); + return request.post("/users/pointPackage/checkGatewayWxPayment", params, paymentRequestConfig); }; // 积分包列表 diff --git a/src/dialog/index.tsx b/src/dialog/index.tsx index 1ab5e0d..286b837 100644 --- a/src/dialog/index.tsx +++ b/src/dialog/index.tsx @@ -22,6 +22,7 @@ let dialogHistorySyncing = false; let closeCharacterDetailModalImpl: (() => void) | null = null; let closeAssetDetailsModalImpl: (() => void) | null = null; let closeFoldersAssetDetailsModalImpl: (() => void) | null = null; +let paySubscribeDialogOpen = false; export function closeCharacterDetailModal(): void { closeCharacterDetailModalImpl?.(); @@ -186,17 +187,28 @@ interface OpenPaySubscribeDialogOptions { } export async function openPaySubscribeDialog({ type, plan }: OpenPaySubscribeDialogOptions): Promise { - const { default: PaySubscribe } = await import("@/dialog/pay-subscribe"); - let close = () => {}; - const modal = openDialogWithHistory({ - title: null, - icon: null, - closable: true, - footer: null, - style: { width: "fit-content", maxWidth: "calc(100vw - 32px)", borderRadius: "9px", padding: "0" }, - content: , - }); - close = modal.close; + if (paySubscribeDialogOpen) return; + paySubscribeDialogOpen = true; + + try { + const { default: PaySubscribe } = await import("@/dialog/pay-subscribe"); + let close = () => {}; + const modal = openDialogWithHistory({ + title: null, + icon: null, + closable: true, + footer: null, + style: { width: "fit-content", maxWidth: "calc(100vw - 32px)", borderRadius: "9px", padding: "0" }, + content: , + afterClose: () => { + paySubscribeDialogOpen = false; + }, + }); + close = modal.close; + } catch (error) { + paySubscribeDialogOpen = false; + throw error; + } } // 积分不足弹窗 diff --git a/src/dialog/pay-subscribe/index.tsx b/src/dialog/pay-subscribe/index.tsx index c6f7760..9519600 100644 --- a/src/dialog/pay-subscribe/index.tsx +++ b/src/dialog/pay-subscribe/index.tsx @@ -1,9 +1,9 @@ /** - * 订阅 / 积分支付弹窗 - * - * 微信:打开即预拉二维码;蒙层在首次同意前盖住二维码;同意一次后切回微信不再蒙层,并轮询。 - * 支付宝:无二维码,蒙层始终存在;每次点「同意并支付」才下单拿链接并跳转,再轮询。 - */ + * 订阅 / 积分支付弹窗 + * + * 微信:同意后下单拉二维码;拿到有效二维码后移除蒙层并轮询。 + * 支付宝:无二维码,蒙层始终存在;每次点「同意并支付」才下单拿链接并跳转,再轮询。 + */ import { Message, Modal } from "@arco-design/web-react"; import { useTranslation } from "react-i18next"; import { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react"; @@ -111,16 +111,20 @@ export default function PaySubscribe({ onClose, type, plan, afterPaySuccess }: P afterPaySuccessRef.current = afterPaySuccess; }, [afterPaySuccess]); - const [paymentMethod, setPaymentMethod] = useState("WXPAY"); - /** 微信侧是否已点过「同意并支付」(本会话内切回微信不再出蒙层) */ - const [wxAgreed, setWxAgreed] = useState(false); - const [submitting, setSubmitting] = useState(false); - const [codeUrl, setCodeUrl] = useState(""); - + const [paymentMethod, setPaymentMethod] = useState("WXPAY"); + /** 微信侧是否已点过「同意并支付」(本会话内切回微信不再出蒙层) */ + const [wxAgreed, setWxAgreed] = useState(false); + const [submitting, setSubmitting] = useState(false); + const [codeUrl, setCodeUrl] = useState(""); + const wxOrderRef = useRef(null); - const pollAbortRef = useRef(false); - const pollStartTimeRef = useRef(0); - const pollTimerRef = useRef(null); + const mountedRef = useRef(true); + const requestSeqRef = useRef(0); + const activePollSeqRef = useRef(0); + const submittingRef = useRef(false); + const pollAbortRef = useRef(false); + const pollStartTimeRef = useRef(0); + const pollTimerRef = useRef(null); const paymentInfo = useMemo(() => { if (payType === "subscribe") { @@ -153,23 +157,9 @@ export default function PaySubscribe({ onClose, type, plan, afterPaySuccess }: P /** 支付宝始终蒙层;微信仅首次同意前蒙层 */ const showAgreementOverlay = paymentMethod === "ALIPAY" || !wxAgreed; - useEffect(() => { - return () => { - pollAbortRef.current = true; - if (pollTimerRef.current != null) { - window.clearTimeout(pollTimerRef.current); - pollTimerRef.current = null; - } - }; - }, []); - - const safeClose = useCallback(() => { - onClose(); - }, [onClose]); - - const requireLogin = useCallback((): boolean => { - if (!getToken() || !user) { - Message.warning(t("auth.please_login_first")); + const requireLogin = useCallback((): boolean => { + if (!getToken() || !user) { + Message.warning(t("auth.please_login_first")); return false; } return true; @@ -177,12 +167,39 @@ export default function PaySubscribe({ onClose, type, plan, afterPaySuccess }: P const stopPolling = useCallback(() => { pollAbortRef.current = true; + activePollSeqRef.current += 1; if (pollTimerRef.current != null) { window.clearTimeout(pollTimerRef.current); pollTimerRef.current = null; } }, []); - const isPollingAborted = useCallback(() => pollAbortRef.current, []); + const isPollingActive = useCallback((pollSeq: number) => mountedRef.current && !pollAbortRef.current && activePollSeqRef.current === pollSeq, []); + const setSubmittingState = useCallback((nextSubmitting: boolean) => { + submittingRef.current = nextSubmitting; + if (mountedRef.current) { + setSubmitting(nextSubmitting); + } + }, []); + + useEffect(() => { + return () => { + mountedRef.current = false; + requestSeqRef.current += 1; + stopPolling(); + submittingRef.current = false; + }; + }, [stopPolling]); + + const safeClose = useCallback(() => { + onClose(); + }, [onClose]); + + const nextPaymentRequestSeq = useCallback(() => { + requestSeqRef.current += 1; + return requestSeqRef.current; + }, []); + + const isPaymentRequestActive = useCallback((requestSeq: number) => mountedRef.current && requestSeqRef.current === requestSeq, []); // 轮询支付状态 const startPaymentPoll = useCallback( @@ -190,18 +207,20 @@ export default function PaySubscribe({ onClose, type, plan, afterPaySuccess }: P const tradeNo = orderRes.trade_no ?? orderRes.tradeNo; if (!tradeNo) return; - stopPolling(); - pollAbortRef.current = false; - pollStartTimeRef.current = Date.now(); - - void (async () => { - const { paymentMethod: method, payType: ctxPayType } = ctx; - - for (;;) { - if (pollAbortRef.current) return; - - if (Date.now() - pollStartTimeRef.current >= PAYMENT_POLL_MAX_MS) { - openPayNoticeModal("warning", { + stopPolling(); + pollAbortRef.current = false; + pollStartTimeRef.current = Date.now(); + const pollSeq = activePollSeqRef.current + 1; + activePollSeqRef.current = pollSeq; + + void (async () => { + const { paymentMethod: method, payType: ctxPayType } = ctx; + + for (;;) { + if (!isPollingActive(pollSeq)) return; + + if (Date.now() - pollStartTimeRef.current >= PAYMENT_POLL_MAX_MS) { + openPayNoticeModal("warning", { title: t("subscribe.pay_notice_timeout_title"), content: t("subscribe.pay_result_timeout"), okText: t("button.okay"), @@ -212,11 +231,11 @@ export default function PaySubscribe({ onClose, type, plan, afterPaySuccess }: P try { const { data: statusRes } = await fetchPaymentStatus(tradeNo, method, ctxPayType); - if (isPollingAborted()) return; - - const tradeStatus = extractTradeStatus(statusRes, method); - if (!tradeStatus) { - await sleep(PAYMENT_POLL_INTERVAL_MS); + if (!isPollingActive(pollSeq)) return; + + const tradeStatus = extractTradeStatus(statusRes, method); + if (!tradeStatus) { + await sleep(PAYMENT_POLL_INTERVAL_MS); continue; } @@ -248,30 +267,31 @@ export default function PaySubscribe({ onClose, type, plan, afterPaySuccess }: P }); return; } - - await sleep(PAYMENT_POLL_INTERVAL_MS); - } catch { - await sleep(PAYMENT_POLL_INTERVAL_MS); - } - } - })(); - }, - [isPollingAborted, safeClose, setUser, stopPolling, t], - ); + + await sleep(PAYMENT_POLL_INTERVAL_MS); + } catch { + if (!isPollingActive(pollSeq)) return; + await sleep(PAYMENT_POLL_INTERVAL_MS); + } + } + })(); + }, + [isPollingActive, safeClose, setUser, stopPolling, t], + ); // 调度轮询 const schedulePoll = useCallback( (orderRes: PaymentOrderData, method: PaymentMethod) => { - if (pollTimerRef.current != null) { - window.clearTimeout(pollTimerRef.current); - } - const ctx: PollContext = { paymentMethod: method, payType }; - pollTimerRef.current = window.setTimeout(() => { - startPaymentPoll(orderRes, ctx); - }, PAYMENT_POLL_INTERVAL_MS); - }, - [payType, startPaymentPoll], - ); + if (!mountedRef.current) return; + stopPolling(); + const ctx: PollContext = { paymentMethod: method, payType }; + pollTimerRef.current = window.setTimeout(() => { + if (!mountedRef.current) return; + startPaymentPoll(orderRes, ctx); + }, PAYMENT_POLL_INTERVAL_MS); + }, + [payType, startPaymentPoll, stopPolling], + ); // 解析套餐ID const resolvePlanIds = useCallback(() => { @@ -296,31 +316,29 @@ export default function PaySubscribe({ onClose, type, plan, afterPaySuccess }: P return requireLogin(); }, [requireLogin, resolvePlanIds, safeClose, t]); - /** 微信预下单:仅拉 code_url,不跳转 */ - const fetchWxOrder = useCallback( - async (options?: { silent?: boolean }) => { - if (submitting) return null; - - const { id, subscriptionId, packageId } = resolvePlanIds(); - if (!id) { - if (!options?.silent) { - openPayNoticeModal("warning", { - title: t("subscribe.pay_notice_missing_plan_title"), - content: t("subscribe.missing_plan_info"), - okText: t("button.okay"), - onOk: safeClose, - }); - } - return null; - } - - if (options?.silent) { - if (!getToken() || !user) return null; - } else if (!requireLogin()) { - return null; - } - setSubmitting(true); - const returnUrl = buildPaymentReturnUrl(payType, "WXPAY"); + /** 微信下单:仅拉 code_url,不跳转 */ + const fetchWxOrder = useCallback( + async () => { + if (submittingRef.current) return null; + + const requestSeq = nextPaymentRequestSeq(); + stopPolling(); + const { id, subscriptionId, packageId } = resolvePlanIds(); + if (!id) { + openPayNoticeModal("warning", { + title: t("subscribe.pay_notice_missing_plan_title"), + content: t("subscribe.missing_plan_info"), + okText: t("button.okay"), + onOk: safeClose, + }); + return null; + } + + if (!requireLogin()) { + return null; + } + setSubmittingState(true); + const returnUrl = buildPaymentReturnUrl(payType, "WXPAY"); try { if (payType === "subscribe") { @@ -328,60 +346,93 @@ export default function PaySubscribe({ onClose, type, plan, afterPaySuccess }: P returnUrl, subscriptionId, }); + if (!isPaymentRequestActive(requestSeq)) return null; if (!res) return null; + if (!res.code_url) { + openPayNoticeModal("error", { + title: t("subscribe.pay_notice_start_failed_title"), + content: t("subscribe.pay_start_failed"), + okText: t("button.okay"), + }); + return null; + } wxOrderRef.current = res; - setCodeUrl(res.code_url ?? ""); + setCodeUrl(res.code_url); return res; } const { data: res } = await createPointsOrderByWxPay({ returnUrl, packageId }); + if (!isPaymentRequestActive(requestSeq)) return null; if (!res) return null; + if (!res.code_url) { + openPayNoticeModal("error", { + title: t("subscribe.pay_notice_start_failed_title"), + content: t("subscribe.pay_start_failed"), + okText: t("button.okay"), + }); + return null; + } wxOrderRef.current = res; - setCodeUrl(res.code_url ?? ""); + setCodeUrl(res.code_url); return res; - } catch { - openPayNoticeModal("error", { - title: t("subscribe.pay_notice_start_failed_title"), - content: t("subscribe.pay_start_failed"), - okText: t("button.okay"), - onOk: safeClose, - }); - return null; - } finally { - setSubmitting(false); - } - }, - [payType, requireLogin, resolvePlanIds, safeClose, submitting, t, user], - ); - - /** 支付宝下单:同意后才调用,拿链接并跳转 */ - const fetchAlipayAndRedirect = useCallback(async () => { - if (submitting) return null; - if (!assertPlanAndLogin()) return null; - - const { subscriptionId, packageId } = resolvePlanIds(); - setSubmitting(true); - const returnUrl = buildPaymentReturnUrl(payType, "ALIPAY"); - - try { - const res = payType === "subscribe" ? (await createSubscriptionOrder({ returnUrl, subscriptionId })).data : (await createPointsOrder({ returnUrl, packageId })).data; - - if (res?.url) { - schedulePoll(res, "ALIPAY"); - location.href = res.url; - } - return res; - } catch { - openPayNoticeModal("error", { - title: t("subscribe.pay_notice_start_failed_title"), - content: t("subscribe.pay_start_failed"), - okText: t("button.okay"), - onOk: safeClose, - }); - return null; - } finally { - setSubmitting(false); - } - }, [assertPlanAndLogin, payType, resolvePlanIds, safeClose, schedulePoll, submitting, t]); + } catch { + if (isPaymentRequestActive(requestSeq)) { + openPayNoticeModal("error", { + title: t("subscribe.pay_notice_start_failed_title"), + content: t("subscribe.pay_start_failed"), + okText: t("button.okay"), + }); + } + return null; + } finally { + if (isPaymentRequestActive(requestSeq)) { + setSubmittingState(false); + } + } + }, + [isPaymentRequestActive, nextPaymentRequestSeq, payType, requireLogin, resolvePlanIds, safeClose, setSubmittingState, stopPolling, t], + ); + + /** 支付宝下单:同意后才调用,拿链接并跳转 */ + const fetchAlipayAndRedirect = useCallback(async () => { + if (submittingRef.current) return null; + if (!assertPlanAndLogin()) return null; + + const requestSeq = nextPaymentRequestSeq(); + stopPolling(); + const { subscriptionId, packageId } = resolvePlanIds(); + setSubmittingState(true); + const returnUrl = buildPaymentReturnUrl(payType, "ALIPAY"); + + try { + const res = payType === "subscribe" ? (await createSubscriptionOrder({ returnUrl, subscriptionId })).data : (await createPointsOrder({ returnUrl, packageId })).data; + if (!isPaymentRequestActive(requestSeq)) return null; + + if (res?.url) { + schedulePoll(res, "ALIPAY"); + location.href = res.url; + return res; + } + openPayNoticeModal("error", { + title: t("subscribe.pay_notice_start_failed_title"), + content: t("subscribe.pay_start_failed"), + okText: t("button.okay"), + }); + return null; + } catch { + if (isPaymentRequestActive(requestSeq)) { + openPayNoticeModal("error", { + title: t("subscribe.pay_notice_start_failed_title"), + content: t("subscribe.pay_start_failed"), + okText: t("button.okay"), + }); + } + return null; + } finally { + if (isPaymentRequestActive(requestSeq)) { + setSubmittingState(false); + } + } + }, [assertPlanAndLogin, isPaymentRequestActive, nextPaymentRequestSeq, payType, resolvePlanIds, schedulePoll, setSubmittingState, stopPolling, t]); const resumeWxPolling = useCallback(() => { if (wxOrderRef.current) { @@ -389,65 +440,46 @@ export default function PaySubscribe({ onClose, type, plan, afterPaySuccess }: P } }, [schedulePoll]); - /** 换套餐:重置微信同意态,预拉微信码 */ - useEffect(() => { - if (!planId) return; - - const cancellation = { current: false }; + /** 换套餐:重置支付态,不自动创建订单 */ + useEffect(() => { + nextPaymentRequestSeq(); stopPolling(); + setSubmittingState(false); setWxAgreed(false); setPaymentMethod("WXPAY"); - setCodeUrl(""); wxOrderRef.current = null; + setCodeUrl(""); + }, [nextPaymentRequestSeq, planId, setSubmittingState, stopPolling]); - void (async () => { - if (!cancellation.current) { - await fetchWxOrder({ silent: true }); - } - })(); + const handlePaymentMethodChange = useCallback( + (method: PaymentMethod) => { + if (method === paymentMethod) return; + if (submittingRef.current) return; - return () => { - cancellation.current = true; - }; - }, [fetchWxOrder, planId, stopPolling]); - - const handlePaymentMethodChange = useCallback( - (method: PaymentMethod) => { - if (method === paymentMethod) return; - - stopPolling(); - setPaymentMethod(method); - - if (method === "WXPAY") { - if (!codeUrl && !wxOrderRef.current?.code_url) { - void fetchWxOrder(); - } else if (wxAgreed) { - resumeWxPolling(); - } - } - }, - [codeUrl, fetchWxOrder, paymentMethod, resumeWxPolling, stopPolling, wxAgreed], - ); - - const handleAgreeAndPay = useCallback(async () => { - if (submitting) return; - if (!assertPlanAndLogin()) return; - - if (paymentMethod === "ALIPAY") { - await fetchAlipayAndRedirect(); - return; - } - - setWxAgreed(true); - - let order: PaymentOrderData | null = wxOrderRef.current; - if (!order?.code_url && !codeUrl) { - order = await fetchWxOrder(); + nextPaymentRequestSeq(); + stopPolling(); + setPaymentMethod(method); + setWxAgreed(false); + setCodeUrl(""); + wxOrderRef.current = null; + }, + [nextPaymentRequestSeq, paymentMethod, stopPolling], + ); + + const handleAgreeAndPay = useCallback(async () => { + if (submittingRef.current) return; + + if (paymentMethod === "ALIPAY") { + await fetchAlipayAndRedirect(); + return; } - if (order) { - resumeWxPolling(); - } - }, [assertPlanAndLogin, codeUrl, fetchAlipayAndRedirect, fetchWxOrder, paymentMethod, resumeWxPolling, submitting]); + + const order = await fetchWxOrder(); + if (order?.code_url) { + setWxAgreed(true); + resumeWxPolling(); + } + }, [fetchAlipayAndRedirect, fetchWxOrder, paymentMethod, resumeWxPolling]); const wxLogo = new URL("@/assets/images/logo_wxpay.png", import.meta.url).href; const alipayLogo = new URL("@/assets/images/logo_alipay.png", import.meta.url).href; @@ -461,6 +493,7 @@ export default function PaySubscribe({ onClose, type, plan, afterPaySuccess }: P