feat(order): 添加订单重新发起支付功能并优化支付流程

- 新增 prepayShopOrder 接口用于对未支付订单生成新预支付参数
- 实现多路径兼容探测机制,支持不同后端版本的支付接口
- 优化订单支付逻辑,优先使用服务端最新状态避免重复支付
- 添加 fallback 机制,在重新支付失败时降级为重新创建订单
- 实现支付成功后自动取消旧待支付订单,避免列表堆积
- 修复订单列表中key值重复的问题
- 在商品列表中添加数量标识符x提升UI显示效果
This commit is contained in:
2026-02-04 15:32:27 +08:00
parent afe8f93c32
commit 5e36f243ef
2 changed files with 111 additions and 38 deletions

View File

@@ -1,4 +1,4 @@
import request from '@/utils/request'; import request, { ErrorType, RequestError } from '@/utils/request';
import type { ApiResult, PageResult } from '@/api'; import type { ApiResult, PageResult } from '@/api';
import type { ShopOrder, ShopOrderParam, OrderCreateRequest } from './model'; import type { ShopOrder, ShopOrderParam, OrderCreateRequest } from './model';
@@ -113,6 +113,44 @@ export interface WxPayResult {
paySign: string; paySign: string;
} }
/**
* 订单重新发起支付(对“已创建但未支付”的订单生成新的预支付参数,不应重复创建订单)
*
* 说明:不同后端版本可能暴露不同路径,这里做兼容探测;若全部失败,调用方可自行降级处理。
*/
export interface OrderPrepayRequest {
orderId: number;
payType: number;
}
export async function prepayShopOrder(data: OrderPrepayRequest) {
const urls = [
'/shop/shop-order/pay',
'/shop/shop-order/prepay',
'/shop/shop-order/repay'
];
let lastError: unknown;
let businessError: unknown;
for (const url of urls) {
try {
const res = await request.post<ApiResult<WxPayResult>>(url, data, { showError: false });
// request.ts 在 code!=0 时会直接 throw走到这里通常都是 code===0
if (res.code === 0) return res.data;
} catch (e) {
// 若已命中“业务错误”(例如订单已取消/已支付),优先保留该错误用于向上提示;
// 不要被后续的 404/网络错误覆盖掉,避免调用方误判为“不支持该接口”而降级走创建订单。
if (!businessError && e instanceof RequestError && e.type === ErrorType.BUSINESS_ERROR) {
businessError = e;
} else {
lastError = e;
}
}
}
return Promise.reject(businessError || lastError || new Error('发起支付失败'));
}
/** /**
* 创建订单 * 创建订单
*/ */

View File

@@ -4,13 +4,14 @@ import {View, Text} from '@tarojs/components'
import Taro from '@tarojs/taro'; import Taro from '@tarojs/taro';
import {InfiniteLoading} from '@nutui/nutui-react-taro' import {InfiniteLoading} from '@nutui/nutui-react-taro'
import dayjs from "dayjs"; import dayjs from "dayjs";
import {pageShopOrder, updateShopOrder, createOrder} from "@/api/shop/shopOrder"; import {pageShopOrder, updateShopOrder, createOrder, getShopOrder, prepayShopOrder} from "@/api/shop/shopOrder";
import {ShopOrder, ShopOrderParam} from "@/api/shop/shopOrder/model"; import {OrderCreateRequest, ShopOrder, ShopOrderParam} from "@/api/shop/shopOrder/model";
import {listShopOrderGoods} from "@/api/shop/shopOrderGoods"; import {listShopOrderGoods} from "@/api/shop/shopOrderGoods";
import {copyText} from "@/utils/common"; import {copyText} from "@/utils/common";
import PaymentCountdown from "@/components/PaymentCountdown"; import PaymentCountdown from "@/components/PaymentCountdown";
import {PaymentType} from "@/utils/payment"; import {PaymentType} from "@/utils/payment";
import {goTo} from "@/utils/navigation"; import {goTo} from "@/utils/navigation";
import {ErrorType, RequestError} from "@/utils/request";
// 判断订单是否支付已过期 // 判断订单是否支付已过期
const isPaymentExpired = (createTime: string, timeoutHours: number = 24): boolean => { const isPaymentExpired = (createTime: string, timeoutHours: number = 24): boolean => {
@@ -406,7 +407,7 @@ function OrderList(props: OrderListProps) {
// 立即支付 // 立即支付
const payOrder = async (order: ShopOrder) => { const payOrder = async (order: ShopOrder) => {
try { try {
if (!order.orderId || !order.orderNo) { if (!order.orderId) {
Taro.showToast({ Taro.showToast({
title: '订单信息错误', title: '订单信息错误',
icon: 'error' icon: 'error'
@@ -419,27 +420,37 @@ function OrderList(props: OrderListProps) {
} }
setPayingOrderId(order.orderId); setPayingOrderId(order.orderId);
// 检查订单是否已过期 // 尽量以服务端最新状态为准,避免“已取消/已支付”但列表未刷新导致误发起支付
if (order.createTime && isPaymentExpired(order.createTime)) { let latestOrder: ShopOrder | null = null;
Taro.showToast({ try {
title: '订单已过期,无法支付', latestOrder = await getShopOrder(order.orderId);
icon: 'error' } catch (_e) {
}); // 忽略:网络波动时继续使用列表数据兜底
return;
} }
const effectiveOrder = latestOrder ? { ...order, ...latestOrder } : order;
// 检查订单状态 if (effectiveOrder.payStatus) {
if (order.payStatus) {
Taro.showToast({ Taro.showToast({
title: '订单已支付', title: '订单已支付',
icon: 'none' icon: 'none'
}); });
// 同步刷新一次,避免列表显示旧状态
void reload(true);
return;
}
if (effectiveOrder.orderStatus === 2) {
Taro.showToast({
title: '订单已取消,无法支付',
icon: 'error'
});
void reload(true);
return; return;
} }
if (order.orderStatus === 2) { // 检查订单是否已过期(以最新 createTime 为准)
if (effectiveOrder.createTime && isPaymentExpired(effectiveOrder.createTime)) {
Taro.showToast({ Taro.showToast({
title: '订单已取消,无法支付', title: '订单已过期,无法支付',
icon: 'error' icon: 'error'
}); });
return; return;
@@ -448,10 +459,10 @@ function OrderList(props: OrderListProps) {
Taro.showLoading({title: '发起支付...'}); Taro.showLoading({title: '发起支付...'});
// 构建商品数据:优先使用订单分页接口返回的 orderGoods缺失时再补拉一次避免goodsItems为空导致后端拒绝/再次支付失败 // 构建商品数据:优先使用订单分页接口返回的 orderGoods缺失时再补拉一次避免goodsItems为空导致后端拒绝/再次支付失败
let orderGoods = order.orderGoods || []; let orderGoods = effectiveOrder.orderGoods || [];
if (!orderGoods.length) { if (!orderGoods.length) {
try { try {
orderGoods = (await listShopOrderGoods({orderId: order.orderId})) || []; orderGoods = (await listShopOrderGoods({orderId: effectiveOrder.orderId})) || [];
} catch (e) { } catch (e) {
// 继续走下面的校验提示 // 继续走下面的校验提示
console.error('补拉订单商品失败:', e); console.error('补拉订单商品失败:', e);
@@ -476,26 +487,37 @@ function OrderList(props: OrderListProps) {
return; return;
} }
// 对于已存在的订单,我们需要重新发起支付 // 优先:对“已创建但未支付”的订单走“重新发起支付”接口(不应重复创建订单)
// 构建支付请求数据,包含完整的商品信息 // 若后端未提供该接口,则降级为重新创建订单(此时不传 orderNo避免出现“相同订单号重复订单”
const paymentData = { let result: any;
orderId: order.orderId, let usedFallbackCreate = false;
orderNo: order.orderNo, try {
goodsItems: goodsItems, result = await prepayShopOrder({
addressId: order.addressId, orderId: effectiveOrder.orderId!,
payType: PaymentType.WECHAT, payType: PaymentType.WECHAT
// 尽量携带原订单信息,避免后端重新计算/校验不一致(如使用了优惠券/自提等) });
couponId: order.couponId, } catch (e) {
deliveryType: order.deliveryType, // 订单状态等业务错误:直接提示,不要降级“重新创建订单”导致产生多笔订单
selfTakeMerchantId: order.selfTakeMerchantId, if (e instanceof RequestError && e.type === ErrorType.BUSINESS_ERROR) {
comments: order.comments, throw e;
title: order.title }
}; usedFallbackCreate = true;
const orderData: OrderCreateRequest = {
console.log('重新支付数据:', paymentData); goodsItems,
addressId: effectiveOrder.addressId,
// 直接调用createOrder API进行重新支付 payType: PaymentType.WECHAT,
const result = await createOrder(paymentData as any); couponId: effectiveOrder.couponId,
deliveryType: effectiveOrder.deliveryType,
selfTakeMerchantId: effectiveOrder.selfTakeMerchantId,
comments: effectiveOrder.comments,
title: effectiveOrder.title,
storeId: effectiveOrder.storeId,
storeName: effectiveOrder.storeName,
riderId: effectiveOrder.riderId,
warehouseId: effectiveOrder.warehouseId
};
result = await createOrder(orderData);
}
if (!result) { if (!result) {
throw new Error('支付发起失败'); throw new Error('支付发起失败');
@@ -534,6 +556,18 @@ function OrderList(props: OrderListProps) {
icon: 'success' icon: 'success'
}); });
// 若因后端不支持“重新发起支付”而降级“重新创建订单”,则原订单会遗留为待支付,支付成功后自动将其标记为已取消,避免列表堆积
if (usedFallbackCreate && effectiveOrder.orderId && !effectiveOrder.payStatus && effectiveOrder.orderStatus !== 2) {
try {
await updateShopOrder({
orderId: effectiveOrder.orderId,
orderStatus: 2
});
} catch (e) {
console.warn('自动取消旧待支付订单失败:', e);
}
}
// 重新加载订单列表 // 重新加载订单列表
void reload(true); void reload(true);
props.onReload?.(); props.onReload?.();
@@ -689,7 +723,7 @@ function OrderList(props: OrderListProps) {
}) })
?.map((item, index) => { ?.map((item, index) => {
return ( return (
<Cell key={index} style={{padding: '16px'}} <Cell key={item.orderId ?? item.orderNo ?? index} style={{padding: '16px'}}
onClick={() => Taro.navigateTo({url: `/shop/orderDetail/index?orderId=${item.orderId}`})}> onClick={() => Taro.navigateTo({url: `/shop/orderDetail/index?orderId=${item.orderId}`})}>
<Space direction={'vertical'} className={'w-full flex flex-col'}> <Space direction={'vertical'} className={'w-full flex flex-col'}>
<View className={'order-no flex justify-between'}> <View className={'order-no flex justify-between'}>
@@ -739,6 +773,7 @@ function OrderList(props: OrderListProps) {
)} )}
<Text className={'text-gray-500 text-xs'}>{(goods as any).quantity ?? goods.totalNum}</Text> <Text className={'text-gray-500 text-xs'}>{(goods as any).quantity ?? goods.totalNum}</Text>
</View> </View>
<Text className={'text-gray-400 text-xs'}>x</Text>
<Text className={'text-sm'}>{goods.price || (goods as any).payPrice}</Text> <Text className={'text-sm'}>{goods.price || (goods as any).payPrice}</Text>
</View> </View>
)) ))