forked from gxwebsoft/mp-10550
feat(order): 添加订单重新发起支付功能并优化支付流程
- 新增 prepayShopOrder 接口用于对未支付订单生成新预支付参数 - 实现多路径兼容探测机制,支持不同后端版本的支付接口 - 优化订单支付逻辑,优先使用服务端最新状态避免重复支付 - 添加 fallback 机制,在重新支付失败时降级为重新创建订单 - 实现支付成功后自动取消旧待支付订单,避免列表堆积 - 修复订单列表中key值重复的问题 - 在商品列表中添加数量标识符x提升UI显示效果
This commit is contained in:
@@ -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('发起支付失败'));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建订单
|
* 创建订单
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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>
|
||||||
))
|
))
|
||||||
|
|||||||
Reference in New Issue
Block a user