feat: 订单轮询 + 竞态防护 + 性能优化 (OrdersPage 完善)
- startPolling: 基于 Map<orderId, timer> 的多订单并发轮询
幂等启动(重复点击防重)+ 局部 state 更新 + 终态全量刷新
- fetchOrders: AbortController 防竞态,切换筛选/分页时取消旧请求
AbortError 静默吞掉,不污染 error state
- columns: useMemo 稳定引用,依赖 [token.colorPrimary, handleContinuePay]
- 卸载清理: abort + clearTimeout 双重防护
- 文档同步: CHANGELOG / TODO 更新至 0.0.21
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
|
||||
|
||||
import { useState, useCallback, useEffect, useRef } from 'react';
|
||||
import { useState, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Table,
|
||||
@@ -30,7 +30,6 @@ const { Text } = Typography;
|
||||
// ---------- 常量 ----------
|
||||
|
||||
/** 订单状态轮询间隔(毫秒) */
|
||||
// startPolling 实现后将使用此常量
|
||||
const POLL_INTERVAL_MS = 3000;
|
||||
|
||||
/** 状态筛选项 */
|
||||
@@ -78,10 +77,17 @@ export function OrdersPage() {
|
||||
const pollingRef = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map());
|
||||
// 组件是否已挂载
|
||||
const mountedRef = useRef(true);
|
||||
// fetchOrders 的 AbortController(切换筛选/分页时取消旧请求)
|
||||
const abortRef = useRef<AbortController | null>(null);
|
||||
|
||||
// ======== 数据获取 ========
|
||||
|
||||
const fetchOrders = useCallback(async () => {
|
||||
// 取消上一次未完成的请求(防竞态)
|
||||
abortRef.current?.abort();
|
||||
const controller = new AbortController();
|
||||
abortRef.current = controller;
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
@@ -92,11 +98,13 @@ export function OrdersPage() {
|
||||
if (statusFilter !== 'all') {
|
||||
params.status = statusFilter as PaymentStatus;
|
||||
}
|
||||
const data = await PaymentAPI.getOrdersListInfo(params);
|
||||
const data = await PaymentAPI.getOrdersListInfo(params, controller.signal);
|
||||
if (mountedRef.current) {
|
||||
setOrders(data);
|
||||
}
|
||||
} catch (err) {
|
||||
// AbortError 是预期行为(用户切换筛选导致),不视为错误
|
||||
if ((err as Error)?.name === 'AbortError') return;
|
||||
if (mountedRef.current) {
|
||||
setError((err as Error)?.message || '加载订单列表失败');
|
||||
}
|
||||
@@ -116,29 +124,79 @@ export function OrdersPage() {
|
||||
useEffect(() => {
|
||||
mountedRef.current = true;
|
||||
const polling = pollingRef.current;
|
||||
const abort = abortRef.current;
|
||||
return () => {
|
||||
mountedRef.current = false;
|
||||
// 清理所有轮询定时器
|
||||
polling.forEach((timer) => clearTimeout(timer));
|
||||
polling.clear();
|
||||
// 取消进行中的列表请求
|
||||
abort?.abort();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// ======== 单订单轮询 ========
|
||||
|
||||
// TODO(human): 实现 startPolling 函数
|
||||
// 该函数接收 orderId,对该订单启动轮询(每 POLL_INTERVAL_MS 查一次)
|
||||
// 轮询到终态时停止,并刷新列表;网络异常时延长间隔重试
|
||||
// 需要考虑:
|
||||
// 1. 取消之前同一订单的轮询(避免重复)
|
||||
// 2. 组件卸载时清理定时器(通过 mountedRef)
|
||||
// 3. 轮询结果更新到 orders state 中(局部更新后,终态时调用 fetchOrders 保证一致性)
|
||||
const startPolling = useCallback((_orderId: string) => {
|
||||
// TODO(human): 实现此函数(使用 POLL_INTERVAL_MS、message、fetchOrders 等闭包变量)
|
||||
void POLL_INTERVAL_MS;
|
||||
void message;
|
||||
void fetchOrders;
|
||||
void ({} as OrderInfoResponseBody);
|
||||
/**
|
||||
* 单订单轮询:对指定 orderId 启动轮询,每 POLL_INTERVAL_MS 查一次
|
||||
* - 幂等启动:取消同一订单的旧轮询再创建新的
|
||||
* - 局部更新 orders state,终态时调 fetchOrders 保证一致性
|
||||
* - 网络异常时延长间隔(2×)重试
|
||||
* - 通过 mountedRef 防止卸载后 setState
|
||||
*/
|
||||
const startPolling = useCallback((orderId: string) => {
|
||||
// ① 取消同一订单的旧轮询(幂等启动,避免重复轮询)
|
||||
const existing = pollingRef.current.get(orderId);
|
||||
if (existing) {
|
||||
clearTimeout(existing);
|
||||
pollingRef.current.delete(orderId);
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
const poll = async () => {
|
||||
if (cancelled || !mountedRef.current) return;
|
||||
|
||||
try {
|
||||
const order: OrderInfoResponseBody = await PaymentAPI.getOrderInfo({ order_id: orderId });
|
||||
if (cancelled || !mountedRef.current) return;
|
||||
|
||||
const newStatus = order.status;
|
||||
|
||||
// 局部更新 orders state(merge 最新订单信息)
|
||||
setOrders((prev) =>
|
||||
prev.map((o) => (o.id === orderId ? { ...o, ...order } : o)),
|
||||
);
|
||||
|
||||
if (newStatus === PAYMENT_STATUS.Pending) {
|
||||
// 仍待支付 → 继续轮询
|
||||
const timer = setTimeout(poll, POLL_INTERVAL_MS);
|
||||
pollingRef.current.set(orderId, timer);
|
||||
} else {
|
||||
// 终态 → 停止轮询,刷新列表保证整体一致性
|
||||
pollingRef.current.delete(orderId);
|
||||
fetchOrders();
|
||||
|
||||
if (newStatus === PAYMENT_STATUS.Paid) {
|
||||
message.success('支付成功!');
|
||||
} else if (newStatus === PAYMENT_STATUS.Failed) {
|
||||
message.error('支付失败,请重试');
|
||||
} else if (newStatus === PAYMENT_STATUS.Expired) {
|
||||
message.warning('订单已过期');
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// 网络异常 → 延长间隔后重试
|
||||
if (!cancelled && mountedRef.current) {
|
||||
const timer = setTimeout(poll, POLL_INTERVAL_MS * 2);
|
||||
pollingRef.current.set(orderId, timer);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 启动首次轮询
|
||||
const timer = setTimeout(poll, POLL_INTERVAL_MS);
|
||||
pollingRef.current.set(orderId, timer);
|
||||
}, [message, fetchOrders]);
|
||||
|
||||
// ======== 操作回调 ========
|
||||
@@ -155,7 +213,7 @@ export function OrdersPage() {
|
||||
|
||||
// ======== 表格列定义 ========
|
||||
|
||||
const columns = [
|
||||
const columns = useMemo(() => [
|
||||
{
|
||||
title: '订单编号',
|
||||
dataIndex: 'id',
|
||||
@@ -237,7 +295,7 @@ export function OrdersPage() {
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
], [token.colorPrimary, handleContinuePay]);
|
||||
|
||||
// ============================================================
|
||||
// 渲染:错误态(无数据时)
|
||||
|
||||
Reference in New Issue
Block a user