feat(order): 添加订单自动取消过期功能

- 在OrderList组件中新增autoCancelExpired和paymentTimeoutHours属性
- 实现支付过期订单的自动检测和取消逻辑
- 添加parseTime和isOrderPaymentExpiredSafe辅助函数
- 使用useRef管理自动取消状态避免重复执行
- 支持单次最多处理20笔过期订单避免接口风暴
- 区分resetPage和loadMore场景下的列表状态同步
- 更新useEffect依赖数组包含新的属性参数
This commit is contained in:
2026-03-08 13:08:35 +08:00
parent 86f7506422
commit 81c63e0e65
2 changed files with 98 additions and 13 deletions

View File

@@ -104,6 +104,10 @@ interface OrderListProps {
baseParams?: ShopOrderParam;
// 只读模式:隐藏“支付/取消/确认收货/退款”等用户操作按钮
readOnly?: boolean;
// 是否自动取消“支付已过期”的待支付订单(仅 user 模式生效)
autoCancelExpired?: boolean;
// 支付超时时间(小时),默认 24 小时
paymentTimeoutHours?: number;
}
function OrderList(props: OrderListProps) {
@@ -111,6 +115,8 @@ function OrderList(props: OrderListProps) {
const pageRef = useRef(1)
const [hasMore, setHasMore] = useState(true)
const [payingOrderId, setPayingOrderId] = useState<number | null>(null)
const autoCanceledOrderIdsRef = useRef<Set<number>>(new Set())
const autoCancelRunningRef = useRef(false)
// 根据传入的statusFilter设置初始tab索引
const getInitialTabIndex = () => {
if (props.searchParams?.statusFilter !== undefined) {
@@ -138,6 +144,26 @@ function OrderList(props: OrderListProps) {
return Number.isFinite(n) ? n : undefined;
};
const parseTime = (raw: any): dayjs.Dayjs | null => {
const text = String(raw ?? '').trim();
if (!text) return null;
const t = /^\d+$/.test(text)
? dayjs(Number(text) < 1e12 ? Number(text) * 1000 : Number(text))
: dayjs(text);
return t.isValid() ? t : null;
};
const isOrderPaymentExpiredSafe = (order: ShopOrder, timeoutHours: number) => {
if (order.payStatus) return false;
if (toNum(order.orderStatus) === 2) return false;
const expiration = parseTime(order.expirationTime);
if (expiration) return dayjs().isAfter(expiration);
if (order.createTime) return isPaymentExpired(order.createTime, timeoutHours);
return false;
};
// “已完成”应以订单状态为准不要用商品ID等字段推断完成态否则会造成 Tab(待发货/待收货) 与状态文案不同步
const isOrderCompleted = (order: ShopOrder) => toNum(order.orderStatus) === 1;
@@ -248,24 +274,82 @@ function OrderList(props: OrderListProps) {
finalStatusFilter: searchConditions.statusFilter
});
try {
const res = await pageShopOrder(searchConditions);
try {
const timeoutHours = typeof props.paymentTimeoutHours === 'number' ? props.paymentTimeoutHours : 24;
const canAutoCancelExpired =
!!props.autoCancelExpired &&
(!props.mode || props.mode === 'user') &&
!props.readOnly;
const isPendingPayList = statusParams.statusFilter === 0;
if (res?.list && res?.list.length > 0) {
const fetchOrders = async () => pageShopOrder(searchConditions);
let res = await fetchOrders();
let incoming = (res?.list || []) as ShopOrder[];
let rawIncomingLength = incoming.length;
// 自动取消“支付已过期”的待支付订单(避免用户看到一堆不可支付的过期单)
if (canAutoCancelExpired && incoming.length && !autoCancelRunningRef.current) {
const expiredToCancel = incoming
.filter(o => !!o?.orderId)
.filter(o => !autoCanceledOrderIdsRef.current.has(o.orderId as number))
.filter(o => isOrderPaymentExpiredSafe(o, timeoutHours));
if (expiredToCancel.length) {
autoCancelRunningRef.current = true;
const justCanceled = new Set<number>();
try {
// 单次最多处理 20 笔,避免接口风暴
for (const order of expiredToCancel.slice(0, 20)) {
try {
await updateShopOrder({ orderId: order.orderId, orderStatus: 2 });
autoCanceledOrderIdsRef.current.add(order.orderId as number);
justCanceled.add(order.orderId as number);
} catch (e) {
console.warn('自动取消过期订单失败:', order?.orderId, e);
}
}
} finally {
autoCancelRunningRef.current = false;
}
if (justCanceled.size > 0) {
if (resetPage) {
// resetPage 时重新拉取一次,确保列表状态与服务端一致
res = await fetchOrders();
incoming = (res?.list || []) as ShopOrder[];
rawIncomingLength = incoming.length;
Taro.showToast({ title: '已自动取消过期订单', icon: 'none' });
} else {
// loadMore 时不重新拉取,避免破坏滚动;仅在本地列表中做最小同步
if (isPendingPayList) {
incoming = incoming.filter(o => !justCanceled.has(o.orderId as number));
} else {
incoming = incoming.map(o => (
justCanceled.has(o.orderId as number) ? { ...o, orderStatus: 2 } : o
));
}
}
}
}
}
if (rawIncomingLength > 0) {
// 订单分页接口已返回 orderGoods列表直接使用该字段
const incoming = res.list as ShopOrder[];
// 使用函数式更新避免依赖 list
setList(prevList => {
const newList = resetPage ? incoming : (prevList || []).concat(incoming);
return newList;
});
if (incoming.length > 0) {
setList(prevList => (resetPage ? incoming : (prevList || []).concat(incoming)));
} else {
// 本页数据全部被自动取消过滤掉:不清空历史列表,仅保持现状
setList(prevList => (resetPage ? [] : prevList));
}
// 正确判断是否还有更多数据
const hasMoreData = incoming.length >= 10; // 假设每页10条数据
// 正确判断是否还有更多数据(以服务端返回条数为准)
const hasMoreData = rawIncomingLength >= 10; // 假设每页10条数据
setHasMore(hasMoreData);
} else {
setList(prevList => resetPage ? [] : prevList);
// 服务端已无更多数据
setList(prevList => (resetPage ? [] : prevList));
setHasMore(false);
}
@@ -281,7 +365,7 @@ function OrderList(props: OrderListProps) {
icon: 'none'
});
}
}, [tapIndex, props.searchParams]); // 移除 list/page 依赖避免useEffect触发循环
}, [tapIndex, props.searchParams, props.baseParams, props.mode, props.readOnly, props.autoCancelExpired, props.paymentTimeoutHours]); // 移除 list/page 依赖避免useEffect触发循环
const reloadMore = useCallback(async () => {
if (loading || !hasMore) return; // 防止重复加载

View File

@@ -164,6 +164,7 @@ function Order() {
onReload={() => reload(searchParams)}
searchParams={searchParams}
showSearch={showSearch}
autoCancelExpired
onSearchParamsChange={(newParams) => {
console.log('父组件接收到searchParams变化:', newParams);
setSearchParams(newParams);