fix: 消除 buildOrderStatusUrl 重复定义,修复轮询回调引用稳定性
- 将 buildOrderStatusUrl 提取到 status-url.ts(客户端安全模块), 删除 OrderStatus/PaymentQRCode/result 三处重复定义 - OrderStatus.tsx 轮询 effect 使用 useRef 保存 onStateChange, 避免非 memoized 回调导致定时器不断重建 - result/page.tsx 增加 accessToken 最小长度校验, 避免无效参数触发无意义的 API 请求
This commit is contained in:
@@ -5,6 +5,7 @@ import { useSearchParams } from 'next/navigation';
|
|||||||
import { useEffect, useState, Suspense } from 'react';
|
import { useEffect, useState, Suspense } from 'react';
|
||||||
import { applyLocaleToSearchParams, pickLocaleText, resolveLocale, type Locale } from '@/lib/locale';
|
import { applyLocaleToSearchParams, pickLocaleText, resolveLocale, type Locale } from '@/lib/locale';
|
||||||
import type { PublicOrderStatusSnapshot } from '@/lib/order/status';
|
import type { PublicOrderStatusSnapshot } from '@/lib/order/status';
|
||||||
|
import { buildOrderStatusUrl } from '@/lib/order/status-url';
|
||||||
|
|
||||||
type WindowWithAlipayBridge = Window & {
|
type WindowWithAlipayBridge = Window & {
|
||||||
AlipayJSBridge?: {
|
AlipayJSBridge?: {
|
||||||
@@ -54,15 +55,6 @@ function closeCurrentWindow() {
|
|||||||
}, 250);
|
}, 250);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildOrderStatusUrl(orderId: string, accessToken?: string | null): string {
|
|
||||||
const query = new URLSearchParams();
|
|
||||||
if (accessToken) {
|
|
||||||
query.set('access_token', accessToken);
|
|
||||||
}
|
|
||||||
const suffix = query.toString();
|
|
||||||
return suffix ? `/api/orders/${orderId}?${suffix}` : `/api/orders/${orderId}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStatusConfig(order: PublicOrderStatusSnapshot | null, locale: Locale, hasAccessToken: boolean) {
|
function getStatusConfig(order: PublicOrderStatusSnapshot | null, locale: Locale, hasAccessToken: boolean) {
|
||||||
if (!order) {
|
if (!order) {
|
||||||
return locale === 'en'
|
return locale === 'en'
|
||||||
@@ -142,7 +134,7 @@ function ResultContent() {
|
|||||||
}, [isPopup]);
|
}, [isPopup]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!outTradeNo || !accessToken) {
|
if (!outTradeNo || !accessToken || accessToken.length < 10) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import type { Locale } from '@/lib/locale';
|
import type { Locale } from '@/lib/locale';
|
||||||
import type { PublicOrderStatusSnapshot } from '@/lib/order/status';
|
import type { PublicOrderStatusSnapshot } from '@/lib/order/status';
|
||||||
|
import { buildOrderStatusUrl } from '@/lib/order/status-url';
|
||||||
|
|
||||||
interface OrderStatusProps {
|
interface OrderStatusProps {
|
||||||
orderId: string;
|
orderId: string;
|
||||||
@@ -15,15 +16,6 @@ interface OrderStatusProps {
|
|||||||
locale?: Locale;
|
locale?: Locale;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildOrderStatusUrl(orderId: string, statusAccessToken?: string): string {
|
|
||||||
const query = new URLSearchParams();
|
|
||||||
if (statusAccessToken) {
|
|
||||||
query.set('access_token', statusAccessToken);
|
|
||||||
}
|
|
||||||
const suffix = query.toString();
|
|
||||||
return suffix ? `/api/orders/${orderId}?${suffix}` : `/api/orders/${orderId}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStatusConfig(order: PublicOrderStatusSnapshot, locale: Locale) {
|
function getStatusConfig(order: PublicOrderStatusSnapshot, locale: Locale) {
|
||||||
if (order.rechargeSuccess) {
|
if (order.rechargeSuccess) {
|
||||||
return locale === 'en'
|
return locale === 'en'
|
||||||
@@ -84,6 +76,8 @@ export default function OrderStatus({
|
|||||||
locale = 'zh',
|
locale = 'zh',
|
||||||
}: OrderStatusProps) {
|
}: OrderStatusProps) {
|
||||||
const [currentOrder, setCurrentOrder] = useState(order);
|
const [currentOrder, setCurrentOrder] = useState(order);
|
||||||
|
const onStateChangeRef = useRef(onStateChange);
|
||||||
|
onStateChangeRef.current = onStateChange;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentOrder(order);
|
setCurrentOrder(order);
|
||||||
@@ -103,7 +97,7 @@ export default function OrderStatus({
|
|||||||
const nextOrder = (await response.json()) as PublicOrderStatusSnapshot;
|
const nextOrder = (await response.json()) as PublicOrderStatusSnapshot;
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
setCurrentOrder(nextOrder);
|
setCurrentOrder(nextOrder);
|
||||||
onStateChange?.(nextOrder);
|
onStateChangeRef.current?.(nextOrder);
|
||||||
} catch {
|
} catch {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -117,7 +111,7 @@ export default function OrderStatus({
|
|||||||
clearInterval(timer);
|
clearInterval(timer);
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
};
|
};
|
||||||
}, [orderId, currentOrder.paymentSuccess, currentOrder.rechargeSuccess, onStateChange, statusAccessToken]);
|
}, [orderId, currentOrder.paymentSuccess, currentOrder.rechargeSuccess, statusAccessToken]);
|
||||||
|
|
||||||
const config = getStatusConfig(currentOrder, locale);
|
const config = getStatusConfig(currentOrder, locale);
|
||||||
const doneLabel = locale === 'en' ? 'Done' : '完成';
|
const doneLabel = locale === 'en' ? 'Done' : '完成';
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
getPaymentIconSrc,
|
getPaymentIconSrc,
|
||||||
getPaymentChannelLabel,
|
getPaymentChannelLabel,
|
||||||
} from '@/lib/pay-utils';
|
} from '@/lib/pay-utils';
|
||||||
|
import { buildOrderStatusUrl } from '@/lib/order/status-url';
|
||||||
import { TERMINAL_STATUSES } from '@/lib/constants';
|
import { TERMINAL_STATUSES } from '@/lib/constants';
|
||||||
|
|
||||||
interface PaymentQRCodeProps {
|
interface PaymentQRCodeProps {
|
||||||
@@ -36,15 +37,6 @@ function isVisibleOrderOutcome(data: PublicOrderStatusSnapshot): boolean {
|
|||||||
return data.paymentSuccess || TERMINAL_STATUSES.has(data.status);
|
return data.paymentSuccess || TERMINAL_STATUSES.has(data.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildOrderStatusUrl(orderId: string, statusAccessToken?: string): string {
|
|
||||||
const query = new URLSearchParams();
|
|
||||||
if (statusAccessToken) {
|
|
||||||
query.set('access_token', statusAccessToken);
|
|
||||||
}
|
|
||||||
const suffix = query.toString();
|
|
||||||
return suffix ? `/api/orders/${orderId}?${suffix}` : `/api/orders/${orderId}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function PaymentQRCode({
|
export default function PaymentQRCode({
|
||||||
orderId,
|
orderId,
|
||||||
token,
|
token,
|
||||||
|
|||||||
15
src/lib/order/status-url.ts
Normal file
15
src/lib/order/status-url.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Client-safe utility for building order status API URLs.
|
||||||
|
* This module must NOT import any server-only modules (config, fs, crypto, etc.).
|
||||||
|
*/
|
||||||
|
|
||||||
|
const ACCESS_TOKEN_KEY = 'access_token';
|
||||||
|
|
||||||
|
export function buildOrderStatusUrl(orderId: string, accessToken?: string | null): string {
|
||||||
|
const query = new URLSearchParams();
|
||||||
|
if (accessToken) {
|
||||||
|
query.set(ACCESS_TOKEN_KEY, accessToken);
|
||||||
|
}
|
||||||
|
const suffix = query.toString();
|
||||||
|
return suffix ? `/api/orders/${orderId}?${suffix}` : `/api/orders/${orderId}`;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user