Browse Source

fix:支付无限轮询修复

bug/pay
leiyuee 6 days ago
parent
commit
81cea02498
  1. 57
      src/api/api.ts
  2. 34
      src/dialog/index.tsx
  3. 418
      src/dialog/pay-subscribe/index.tsx
  4. 56
      src/pages/Explore/ExploreTemplateFeed/ExploreTemplateFeed.scss

57
src/api/api.ts

@ -72,6 +72,7 @@ type PaymentStatusResponse = {
message?: string;
data?: PaymentStatusData;
};
const paymentRequestConfig: AxiosRequestConfig = { skipGlobalErrorHandler: true };
export type AuthUser = Record<string, unknown>;
export type AuthData = ApiParams & {
token?: string;
@ -546,54 +547,70 @@ export const getPlanList = (type: EntityId): Promise<ListApiResponse<ProductPlan
// 订阅支付宝下单
export const createSubscriptionOrder = (params: ApiPayload): Promise<PaymentOrderResponse> => {
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<PaymentOrderResponse> => {
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<PaymentStatusResponse> => {
return request.post("/trade/subscription/checkGatewayPayment", params);
return request.post("/trade/subscription/checkGatewayPayment", params, paymentRequestConfig);
};
// 订阅微信支付结果查询
export const checkGatewayWxPayment = (params: ApiPayload): Promise<PaymentStatusResponse> => {
return request.post("/trade/subscription/checkGatewayWxPayment", params);
return request.post("/trade/subscription/checkGatewayWxPayment", params, paymentRequestConfig);
};
// 积分包支付宝下单
export const createPointsOrder = (params: ApiPayload): Promise<PaymentOrderResponse> => {
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<PaymentOrderResponse> => {
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<PaymentStatusResponse> => {
return request.post("/users/pointPackage/checkGatewayPayment", params);
return request.post("/users/pointPackage/checkGatewayPayment", params, paymentRequestConfig);
};
// 积分包微信支付结果查询
export const checkPointsWxPayment = (params: ApiPayload): Promise<PaymentStatusResponse> => {
return request.post("/users/pointPackage/checkGatewayWxPayment", params);
return request.post("/users/pointPackage/checkGatewayWxPayment", params, paymentRequestConfig);
};
// 积分包列表

34
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<void> {
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: <PaySubscribe onClose={close} type={type} plan={plan} />,
});
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: <PaySubscribe onClose={close} type={type} plan={plan} />,
afterClose: () => {
paySubscribeDialogOpen = false;
},
});
close = modal.close;
} catch (error) {
paySubscribeDialogOpen = false;
throw error;
}
}
// 积分不足弹窗

418
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<PaymentMethod>("WXPAY");
/** 微信侧是否已点过「同意并支付」(本会话内切回微信不再出蒙层) */
const [wxAgreed, setWxAgreed] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [codeUrl, setCodeUrl] = useState("");
const [paymentMethod, setPaymentMethod] = useState<PaymentMethod>("WXPAY");
/** 微信侧是否已点过「同意并支付」(本会话内切回微信不再出蒙层) */
const [wxAgreed, setWxAgreed] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [codeUrl, setCodeUrl] = useState("");
const wxOrderRef = useRef<PaymentOrderData | null>(null);
const pollAbortRef = useRef(false);
const pollStartTimeRef = useRef(0);
const pollTimerRef = useRef<number | null>(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<number | null>(null);
const paymentInfo = useMemo<PaymentInfo>(() => {
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
<button
type="button"
className={["pay_subscribe_modal__method", paymentMethod === "WXPAY" ? "is_active" : ""].filter(Boolean).join(" ")}
disabled={submitting}
onClick={() => {
handlePaymentMethodChange("WXPAY");
}}
@ -471,6 +504,7 @@ export default function PaySubscribe({ onClose, type, plan, afterPaySuccess }: P
<button
type="button"
className={["pay_subscribe_modal__method", paymentMethod === "ALIPAY" ? "is_active" : ""].filter(Boolean).join(" ")}
disabled={submitting}
onClick={() => {
handlePaymentMethodChange("ALIPAY");
}}

56
src/pages/Explore/ExploreTemplateFeed/ExploreTemplateFeed.scss

@ -6,7 +6,7 @@
aspect-ratio: 258 / 455;
border-radius: 26px;
overflow: hidden;
background: #e8e8e8;
// background: #e8e8e8;
cursor: pointer;
&:active {
@ -98,11 +98,52 @@
text-overflow: ellipsis;
}
.explorePage__tplCard:hover .explorePage__tplCardMedia::after,
.explorePage__tplCard:focus-visible .explorePage__tplCardMedia::after {
background: linear-gradient(180deg, rgba(0, 0, 0, 0.34) 0%, rgba(0, 0, 0, 0.04) 42%, rgba(0, 0, 0, 0.74) 100%);
.explorePage__tplCardCta {
box-sizing: border-box;
display: flex;
position: absolute;
top: 20px;
right: 20px;
z-index: 3;
align-items: center;
justify-content: center;
gap: 4px;
min-width: 91px;
height: 31px;
padding: 0 12px;
border: 0;
border-radius: 24px;
background: #6f47f5;
font-size: 14px;
font-weight: 400;
line-height: 20px;
color: #ffffff;
cursor: pointer;
pointer-events: none;
white-space: nowrap;
box-shadow: 0 6px 18px rgba(111, 71, 245, 0.26);
opacity: 0;
transform: translateY(-4px);
transition:
opacity 180ms ease,
transform 180ms ease;
span {
font-size: 24px;
font-weight: 300;
}
&:focus-visible {
outline: 2px solid #ffffff;
outline-offset: 2px;
}
}
// .explorePage__tplCard:hover .explorePage__tplCardMedia::after,
// .explorePage__tplCard:focus-visible .explorePage__tplCardMedia::after {
// background: linear-gradient(180deg, rgba(0, 0, 0, 0.34) 0%, rgba(0, 0, 0, 0.04) 42%, rgba(0, 0, 0, 0.74) 100%);
// }
.explorePage__tplCard.explorePage__tplCard--skeleton {
cursor: default;
pointer-events: none;
@ -128,3 +169,10 @@
}
}
}
.explorePage .explorePage__feed .explorePage__feedPanel .explorePage__feedGrid .explorePage__tplCard:hover .explorePage__tplCardCta,
.explorePage .explorePage__feed .explorePage__feedPanel .explorePage__feedGrid .explorePage__tplCard:focus-within .explorePage__tplCardCta {
opacity: 1;
pointer-events: auto;
transform: translateY(0);
}
Loading…
Cancel
Save