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:
2026-06-15 19:58:25 +08:00
parent 06e6486aa3
commit 72a0290693
3 changed files with 94 additions and 19 deletions

View File

@@ -32,6 +32,20 @@
- uploadValidation 内联到 ImageInputcontrols/ 不再混工具函数) - uploadValidation 内联到 ImageInputcontrols/ 不再混工具函数)
- 删除空占位 Test3.tsx - 删除空占位 Test3.tsx
### 支付系统 & 订单明细
- **RechargePage antd 6.x 适配**`InputNumber.addonAfter``Space.Compact``Space direction``orientation`,消除弃用警告
- **OrdersPage 订单明细页面**`/orders`,方案 B — 独立订单管理页):
- 状态筛选 Segmented全部/待支付/已支付/已过期/支付失败)+ 分页 Table
- "继续支付"操作:打开支付 URL + `startPolling` 单订单轮询
- `pollingRef: Map<orderId, timer>` 管理多订单并发轮询,幂等启动(重复点击防重)
- 局部 state 更新 + 终态全量刷新(`fetchOrders`
- `AbortController` 防竞态:切换筛选/分页时取消旧请求
- `columns` 使用 `useMemo` 稳定引用
- 错误态 Result + 重试按钮
- **PageDispatcher**:注册 `/orders` 路由width: 820, height: 640新增 `CLOSE_PAGE` / `PAGE_LOCK` 事件监听
- **payments.ts**:支付 API 模块类型体系(`CreateOrderResponseBody``OrderListResponseBody` / `OrderInfoResponseBody` 名义类型),`PaymentAPI.getOrdersListInfo` 支持 `AbortSignal`
--- ---
## 0.0.202026-06-12 ## 0.0.202026-06-12

View File

@@ -15,9 +15,12 @@
### 路由与页面 ### 路由与页面
- [x] 账户信息页面(`/account``AccountInfo.tsx`AuthAPI.getUserInfo() 驱动 - [x] 账户信息页面(`/account``AccountInfo.tsx`AuthAPI.getUserInfo() 驱动
- [x] 充值页面(`/recharge``RechargePage.tsx`,创建订单 → 支付状态跟踪
- [x] 订单明细页面(`/orders``OrdersPage.tsx`,状态筛选 + 分页 + 继续支付 + 轮询
- [ ] 合规素材库页面(`/compliance-assets` - [ ] 合规素材库页面(`/compliance-assets`
- [ ] 项目管理页面(`/projects`,企业版) - [ ] 项目管理页面(`/projects`,企业版)
- [ ] 会员/充值/消费记录/订单明细页面 - [ ] 会员页面`/vip`
- [ ] 消费记录页面
### 导航栏 ### 导航栏

View File

@@ -1,6 +1,6 @@
import { useState, useCallback, useEffect, useRef } from 'react'; import { useState, useCallback, useEffect, useMemo, useRef } from 'react';
import { import {
Card, Card,
Table, Table,
@@ -30,7 +30,6 @@ const { Text } = Typography;
// ---------- 常量 ---------- // ---------- 常量 ----------
/** 订单状态轮询间隔(毫秒) */ /** 订单状态轮询间隔(毫秒) */
// startPolling 实现后将使用此常量
const POLL_INTERVAL_MS = 3000; const POLL_INTERVAL_MS = 3000;
/** 状态筛选项 */ /** 状态筛选项 */
@@ -78,10 +77,17 @@ export function OrdersPage() {
const pollingRef = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map()); const pollingRef = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map());
// 组件是否已挂载 // 组件是否已挂载
const mountedRef = useRef(true); const mountedRef = useRef(true);
// fetchOrders 的 AbortController切换筛选/分页时取消旧请求)
const abortRef = useRef<AbortController | null>(null);
// ======== 数据获取 ======== // ======== 数据获取 ========
const fetchOrders = useCallback(async () => { const fetchOrders = useCallback(async () => {
// 取消上一次未完成的请求(防竞态)
abortRef.current?.abort();
const controller = new AbortController();
abortRef.current = controller;
setLoading(true); setLoading(true);
setError(null); setError(null);
try { try {
@@ -92,11 +98,13 @@ export function OrdersPage() {
if (statusFilter !== 'all') { if (statusFilter !== 'all') {
params.status = statusFilter as PaymentStatus; params.status = statusFilter as PaymentStatus;
} }
const data = await PaymentAPI.getOrdersListInfo(params); const data = await PaymentAPI.getOrdersListInfo(params, controller.signal);
if (mountedRef.current) { if (mountedRef.current) {
setOrders(data); setOrders(data);
} }
} catch (err) { } catch (err) {
// AbortError 是预期行为(用户切换筛选导致),不视为错误
if ((err as Error)?.name === 'AbortError') return;
if (mountedRef.current) { if (mountedRef.current) {
setError((err as Error)?.message || '加载订单列表失败'); setError((err as Error)?.message || '加载订单列表失败');
} }
@@ -116,29 +124,79 @@ export function OrdersPage() {
useEffect(() => { useEffect(() => {
mountedRef.current = true; mountedRef.current = true;
const polling = pollingRef.current; const polling = pollingRef.current;
const abort = abortRef.current;
return () => { return () => {
mountedRef.current = false; mountedRef.current = false;
// 清理所有轮询定时器 // 清理所有轮询定时器
polling.forEach((timer) => clearTimeout(timer)); polling.forEach((timer) => clearTimeout(timer));
polling.clear(); polling.clear();
// 取消进行中的列表请求
abort?.abort();
}; };
}, []); }, []);
// ======== 单订单轮询 ======== // ======== 单订单轮询 ========
// TODO(human): 实现 startPolling 函数 /**
// 该函数接收 orderId,对该订单启动轮询每 POLL_INTERVAL_MS 查一次 * 单订单轮询:对指定 orderId 启动轮询每 POLL_INTERVAL_MS 查一次
// 轮询到终态时停止,并刷新列表;网络异常时延长间隔重试 * - 幂等启动:取消同一订单的旧轮询再创建新的
// 需要考虑: * - 局部更新 orders state终态时调 fetchOrders 保证一致性
// 1. 取消之前同一订单的轮询(避免重复) * - 网络异常时延长间隔2×重试
// 2. 组件卸载时清理定时器(通过 mountedRef * - 通过 mountedRef 防止卸载后 setState
// 3. 轮询结果更新到 orders state 中(局部更新后,终态时调用 fetchOrders 保证一致性) */
const startPolling = useCallback((_orderId: string) => { const startPolling = useCallback((orderId: string) => {
// TODO(human): 实现此函数(使用 POLL_INTERVAL_MS、message、fetchOrders 等闭包变量 // ① 取消同一订单的旧轮询(幂等启动,避免重复轮询
void POLL_INTERVAL_MS; const existing = pollingRef.current.get(orderId);
void message; if (existing) {
void fetchOrders; clearTimeout(existing);
void ({} as OrderInfoResponseBody); 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 statemerge 最新订单信息)
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]); }, [message, fetchOrders]);
// ======== 操作回调 ======== // ======== 操作回调 ========
@@ -155,7 +213,7 @@ export function OrdersPage() {
// ======== 表格列定义 ======== // ======== 表格列定义 ========
const columns = [ const columns = useMemo(() => [
{ {
title: '订单编号', title: '订单编号',
dataIndex: 'id', dataIndex: 'id',
@@ -237,7 +295,7 @@ export function OrdersPage() {
); );
}, },
}, },
]; ], [token.colorPrimary, handleContinuePay]);
// ============================================================ // ============================================================
// 渲染:错误态(无数据时) // 渲染:错误态(无数据时)