feat(order): 添加订单自动取消过期功能
- 在OrderList组件中新增autoCancelExpired和paymentTimeoutHours属性 - 实现支付过期订单的自动检测和取消逻辑 - 添加parseTime和isOrderPaymentExpiredSafe辅助函数 - 使用useRef管理自动取消状态避免重复执行 - 支持单次最多处理20笔过期订单避免接口风暴 - 区分resetPage和loadMore场景下的列表状态同步 - 更新useEffect依赖数组包含新的属性参数
This commit is contained in:
@@ -104,6 +104,10 @@ interface OrderListProps {
|
|||||||
baseParams?: ShopOrderParam;
|
baseParams?: ShopOrderParam;
|
||||||
// 只读模式:隐藏“支付/取消/确认收货/退款”等用户操作按钮
|
// 只读模式:隐藏“支付/取消/确认收货/退款”等用户操作按钮
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
|
// 是否自动取消“支付已过期”的待支付订单(仅 user 模式生效)
|
||||||
|
autoCancelExpired?: boolean;
|
||||||
|
// 支付超时时间(小时),默认 24 小时
|
||||||
|
paymentTimeoutHours?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function OrderList(props: OrderListProps) {
|
function OrderList(props: OrderListProps) {
|
||||||
@@ -111,6 +115,8 @@ function OrderList(props: OrderListProps) {
|
|||||||
const pageRef = useRef(1)
|
const pageRef = useRef(1)
|
||||||
const [hasMore, setHasMore] = useState(true)
|
const [hasMore, setHasMore] = useState(true)
|
||||||
const [payingOrderId, setPayingOrderId] = useState<number | null>(null)
|
const [payingOrderId, setPayingOrderId] = useState<number | null>(null)
|
||||||
|
const autoCanceledOrderIdsRef = useRef<Set<number>>(new Set())
|
||||||
|
const autoCancelRunningRef = useRef(false)
|
||||||
// 根据传入的statusFilter设置初始tab索引
|
// 根据传入的statusFilter设置初始tab索引
|
||||||
const getInitialTabIndex = () => {
|
const getInitialTabIndex = () => {
|
||||||
if (props.searchParams?.statusFilter !== undefined) {
|
if (props.searchParams?.statusFilter !== undefined) {
|
||||||
@@ -138,6 +144,26 @@ function OrderList(props: OrderListProps) {
|
|||||||
return Number.isFinite(n) ? n : undefined;
|
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(待发货/待收货) 与状态文案不同步
|
// “已完成”应以订单状态为准;不要用商品ID等字段推断完成态,否则会造成 Tab(待发货/待收货) 与状态文案不同步
|
||||||
const isOrderCompleted = (order: ShopOrder) => toNum(order.orderStatus) === 1;
|
const isOrderCompleted = (order: ShopOrder) => toNum(order.orderStatus) === 1;
|
||||||
|
|
||||||
@@ -249,23 +275,81 @@ function OrderList(props: OrderListProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await pageShopOrder(searchConditions);
|
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:列表直接使用该字段
|
// 订单分页接口已返回 orderGoods:列表直接使用该字段
|
||||||
const incoming = res.list as ShopOrder[];
|
|
||||||
|
|
||||||
// 使用函数式更新避免依赖 list
|
// 使用函数式更新避免依赖 list
|
||||||
setList(prevList => {
|
if (incoming.length > 0) {
|
||||||
const newList = resetPage ? incoming : (prevList || []).concat(incoming);
|
setList(prevList => (resetPage ? incoming : (prevList || []).concat(incoming)));
|
||||||
return newList;
|
} else {
|
||||||
});
|
// 本页数据全部被自动取消过滤掉:不清空历史列表,仅保持现状
|
||||||
|
setList(prevList => (resetPage ? [] : prevList));
|
||||||
|
}
|
||||||
|
|
||||||
// 正确判断是否还有更多数据
|
// 正确判断是否还有更多数据(以服务端返回条数为准)
|
||||||
const hasMoreData = incoming.length >= 10; // 假设每页10条数据
|
const hasMoreData = rawIncomingLength >= 10; // 假设每页10条数据
|
||||||
setHasMore(hasMoreData);
|
setHasMore(hasMoreData);
|
||||||
} else {
|
} else {
|
||||||
setList(prevList => resetPage ? [] : prevList);
|
// 服务端已无更多数据
|
||||||
|
setList(prevList => (resetPage ? [] : prevList));
|
||||||
setHasMore(false);
|
setHasMore(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,7 +365,7 @@ function OrderList(props: OrderListProps) {
|
|||||||
icon: 'none'
|
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 () => {
|
const reloadMore = useCallback(async () => {
|
||||||
if (loading || !hasMore) return; // 防止重复加载
|
if (loading || !hasMore) return; // 防止重复加载
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ function Order() {
|
|||||||
onReload={() => reload(searchParams)}
|
onReload={() => reload(searchParams)}
|
||||||
searchParams={searchParams}
|
searchParams={searchParams}
|
||||||
showSearch={showSearch}
|
showSearch={showSearch}
|
||||||
|
autoCancelExpired
|
||||||
onSearchParamsChange={(newParams) => {
|
onSearchParamsChange={(newParams) => {
|
||||||
console.log('父组件接收到searchParams变化:', newParams);
|
console.log('父组件接收到searchParams变化:', newParams);
|
||||||
setSearchParams(newParams);
|
setSearchParams(newParams);
|
||||||
|
|||||||
Reference in New Issue
Block a user