Files
template-10584/src/utils/payment.ts
赵忠林 3248315f6e refactor(shop): 移除水票套票商品配送时间选择功能
- 删除了配送时间相关的状态管理和日期选择器组件
- 移除了配送时间验证和格式化逻辑
- 更新了订单提交流程,不再传递配送时间参数
- 修改支付回调处理,支持自定义成功行为和跳转逻辑
- 简化了水票商品的购买流程,移除配送时间相关校验
2026-03-09 12:17:29 +08:00

526 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Taro from '@tarojs/taro';
import { createOrder, WxPayResult } from '@/api/shop/shopOrder';
import { OrderCreateRequest } from '@/api/shop/shopOrder/model';
import { getSelectedStoreFromStorage, getSelectedStoreIdFromStorage } from '@/utils/storeSelection';
import type { ShopStoreRider } from '@/api/shop/shopStoreRider/model';
import type { ShopStoreWarehouse } from '@/api/shop/shopStoreWarehouse/model';
import request from '@/utils/request';
/**
* 支付类型枚举
*/
export enum PaymentType {
BALANCE = 0, // 余额支付
WECHAT = 1, // 微信支付
ALIPAY = 3, // 支付宝支付
}
/**
* 支付结果回调
*/
export interface PaymentCallback {
// Return `false` to skip default "支付成功" toast + redirect.
onSuccess?: () => void | boolean | Promise<void | boolean>;
onError?: (error: string) => void;
onComplete?: () => void;
}
/**
* 统一支付处理类
*/
export class PaymentHandler {
// 简单缓存,避免频繁请求(小程序单次运行生命周期内有效)
private static storeRidersCache = new Map<number, ShopStoreRider[]>();
private static warehousesCache: ShopStoreWarehouse[] | null = null;
/**
* 执行支付
* @param orderData 订单数据
* @param paymentType 支付类型
* @param callback 回调函数
*/
static async pay(
orderData: OrderCreateRequest,
paymentType: PaymentType,
callback?: PaymentCallback
): Promise<void> {
Taro.showLoading({ title: '支付中...' });
try {
// 若调用方未指定门店,则自动注入“已选门店”,用于订单门店归属/统计。
if (orderData.storeId === undefined || orderData.storeId === null) {
const storeId = getSelectedStoreIdFromStorage();
if (storeId) {
orderData.storeId = storeId;
}
}
if (!orderData.storeName) {
const store = getSelectedStoreFromStorage();
if (store?.name) {
orderData.storeName = store.name;
}
}
// 自动派单按门店骑手优先级dispatchPriority选择 riderId不覆盖手动指定
if ((orderData.riderId === undefined || orderData.riderId === null) && orderData.storeId) {
const riderUserId = await this.pickRiderUserIdForStore(orderData.storeId);
if (riderUserId) {
orderData.riderId = riderUserId;
}
}
// 仓库选择:若未指定 warehouseId则按“离门店最近”兜底选择一个不覆盖手动指定
if ((orderData.warehouseId === undefined || orderData.warehouseId === null) && orderData.storeId) {
const warehouseId = await this.pickWarehouseIdForStore(orderData.storeId);
if (warehouseId) {
orderData.warehouseId = warehouseId;
}
}
// 设置支付类型
orderData.payType = paymentType;
console.log('创建订单请求:', orderData);
// 创建订单
const result = await createOrder(orderData);
console.log('订单创建结果:', result);
if (!result) {
throw new Error('创建订单失败');
}
// 验证订单创建结果
if (!result.orderNo) {
throw new Error('订单号获取失败');
}
let paymentSuccess = false;
// 根据支付类型处理
switch (paymentType) {
case PaymentType.WECHAT:
await this.handleWechatPay(result);
paymentSuccess = true;
break;
case PaymentType.BALANCE:
paymentSuccess = await this.handleBalancePay(result);
break;
case PaymentType.ALIPAY:
await this.handleAlipay(result);
paymentSuccess = true;
break;
default:
throw new Error('不支持的支付方式');
}
// 只有确认支付成功才显示成功提示和跳转
if (paymentSuccess) {
console.log('支付成功,订单号:', result.orderNo);
// 先收起 loading避免遮挡 modal/toast
try {
Taro.hideLoading();
} catch (_e) {
// ignore
}
const onSuccessResult = await callback?.onSuccess?.();
const skipDefaultSuccessBehavior = onSuccessResult === false;
if (!skipDefaultSuccessBehavior) {
Taro.showToast({
title: '支付成功',
icon: 'success'
});
// 跳转到订单页面
setTimeout(() => {
Taro.navigateTo({ url: '/user/order/order' });
}, 2000);
}
} else {
throw new Error('支付未完成');
}
} catch (error: any) {
console.error('支付失败:', error);
// 获取详细错误信息
const errorMessage = this.getErrorMessage(error);
Taro.showToast({
title: errorMessage,
icon: 'error'
});
// 标记错误已处理,避免上层重复处理
error.handled = true;
callback?.onError?.(errorMessage);
// 重新抛出错误,让上层知道支付失败
throw error;
} finally {
Taro.hideLoading();
callback?.onComplete?.();
}
}
private static parseLngLat(raw: string | undefined): { lng: number; lat: number } | null {
const text = (raw || '').trim();
if (!text) return null;
const parts = text.split(/[,\s]+/).filter(Boolean);
if (parts.length < 2) return null;
const a = parseFloat(parts[0]);
const b = parseFloat(parts[1]);
if (Number.isNaN(a) || Number.isNaN(b)) return null;
const looksLikeLngLat = Math.abs(a) <= 180 && Math.abs(b) <= 90;
const looksLikeLatLng = Math.abs(a) <= 90 && Math.abs(b) <= 180;
if (looksLikeLngLat) return { lng: a, lat: b };
if (looksLikeLatLng) return { lng: b, lat: a };
return null;
}
private static distanceMeters(a: { lng: number; lat: number }, b: { lng: number; lat: number }) {
const toRad = (x: number) => (x * Math.PI) / 180;
const R = 6371000;
const dLat = toRad(b.lat - a.lat);
const dLng = toRad(b.lng - a.lng);
const lat1 = toRad(a.lat);
const lat2 = toRad(b.lat);
const sin1 = Math.sin(dLat / 2);
const sin2 = Math.sin(dLng / 2);
const h = sin1 * sin1 + Math.cos(lat1) * Math.cos(lat2) * sin2 * sin2;
return 2 * R * Math.asin(Math.min(1, Math.sqrt(h)));
}
private static async getRidersForStore(storeId: number): Promise<ShopStoreRider[]> {
const cached = this.storeRidersCache.get(storeId);
if (cached) return cached;
// 后端字段可能叫 dealerId 或 storeId这里都带上服务端忽略未知字段即可。
// 这里做一次路径兼容camel vs kebab避免接口路径不一致导致整单失败。
const list = await this.listByCompatEndpoint<ShopStoreRider>(
['/shop/shop-store-rider'],
{
storeId: storeId,
status: 1
}
);
const usable = (list || []).filter(r => r?.isDelete !== 1 && (r.status === undefined || r.status === 1));
this.storeRidersCache.set(storeId, usable);
return usable;
}
private static async pickRiderUserIdForStore(storeId: number): Promise<number | undefined> {
const riders = await this.getRidersForStore(storeId);
if (!riders.length) return undefined;
// 优先:启用 + 在线 + 自动派单,再按 dispatchPriority 由高到低
const score = (r: ShopStoreRider) => {
const enabled = (r.status === undefined || r.status === 1) ? 1 : 0;
const online = r.workStatus === 1 ? 1 : 0;
const auto = r.autoDispatchEnabled === 1 ? 1 : 0;
const p = typeof r.dispatchPriority === 'number' ? r.dispatchPriority : 0;
return enabled * 1000 + online * 100 + auto * 10 + p;
};
const sorted = [...riders].sort((a, b) => score(b) - score(a));
return sorted[0]?.userId;
}
private static async getWarehouses(): Promise<ShopStoreWarehouse[]> {
if (this.warehousesCache) return this.warehousesCache;
const list = await this.listByCompatEndpoint<ShopStoreWarehouse>(
['/shop/shop-store-warehouse'],
{}
);
const usable = (list || []).filter(w => w?.isDelete !== 1 && (w.status === undefined || w.status === 1));
this.warehousesCache = usable;
return usable;
}
private static async pickWarehouseIdForStore(storeId: number): Promise<number | undefined> {
const store = getSelectedStoreFromStorage();
if (!store?.id || store.id !== storeId) return undefined;
// 一门店一默认仓库:优先使用门店自带的 warehouseId
if (store.warehouseId) return store.warehouseId;
const storeCoords = this.parseLngLat(store.lngAndLat || store.location);
if (!storeCoords) return undefined;
const warehouses = await this.getWarehouses();
if (!warehouses.length) return undefined;
// 优先选择“门店仓”,否则选最近的任意仓库
const candidates = warehouses.filter(w => w.type?.includes('门店') || w.type?.includes('门店仓'));
const list = candidates.length ? candidates : warehouses;
const withDistance = list
.map(w => {
const coords = this.parseLngLat(w.lngAndLat);
if (!coords) return { w, d: Number.POSITIVE_INFINITY };
return { w, d: this.distanceMeters(storeCoords, coords) };
})
.sort((a, b) => a.d - b.d);
return withDistance[0]?.w?.id;
}
private static async listByCompatEndpoint<T>(
urls: string[],
params: Record<string, any>
): Promise<T[]> {
for (const url of urls) {
try {
const res: any = await (request as any).get(url, params, { showError: false });
if (res?.code === 0 && Array.isArray(res?.data)) {
return res.data as T[];
}
} catch (_e) {
// try next
}
}
return [];
}
/**
* 处理微信支付
*/
private static async handleWechatPay(result: WxPayResult): Promise<void> {
console.log('处理微信支付:', result);
if (!result) {
throw new Error('微信支付参数错误');
}
// 验证微信支付必要参数
if (!result.timeStamp || !result.nonceStr || !result.package || !result.paySign) {
throw new Error('微信支付参数不完整');
}
try {
await Taro.requestPayment({
timeStamp: result.timeStamp,
nonceStr: result.nonceStr,
package: result.package,
signType: result.signType as any, // 类型转换因为微信支付的signType是字符串
paySign: result.paySign,
});
console.log('微信支付成功');
} catch (payError: any) {
console.error('微信支付失败:', payError);
// 处理微信支付特定错误
if (payError.errMsg) {
if (payError.errMsg.includes('cancel')) {
throw new Error('用户取消支付');
} else if (payError.errMsg.includes('fail')) {
throw new Error('微信支付失败,请重试');
}
}
throw new Error('微信支付失败');
}
}
/**
* 处理余额支付
*/
private static async handleBalancePay(result: any): Promise<boolean> {
console.log('处理余额支付:', result);
if (!result || !result.orderNo) {
throw new Error('余额支付参数错误');
}
// 检查支付状态 - 根据后端返回的字段调整
if (result.payStatus === false || result.payStatus === 0 || result.payStatus === '0') {
throw new Error('余额不足或支付失败');
}
// 检查订单状态 - 1表示已付款
if (result.orderStatus !== undefined && result.orderStatus !== 1) {
throw new Error('订单状态异常,支付可能未成功');
}
// 验证实际扣款金额
if (result.payPrice !== undefined) {
const payPrice = parseFloat(result.payPrice);
if (payPrice <= 0) {
throw new Error('支付金额异常');
}
}
// 如果有错误信息字段,检查是否有错误
if (result.error || result.errorMsg) {
throw new Error(result.error || result.errorMsg);
}
console.log('余额支付验证通过');
return true;
}
/**
* 处理支付宝支付
*/
private static async handleAlipay(_result: any): Promise<void> {
// 支付宝支付逻辑,根据实际情况实现
throw new Error('支付宝支付暂未实现');
}
/**
* 获取详细错误信息
*/
private static getErrorMessage(error: any): string {
if (!error.message) {
return '支付失败,请重试';
}
const message = error.message;
// 配送范围/电子围栏相关错误(优先于“地址信息有误”的兜底)
if (
message.includes('不在配送范围') ||
message.includes('配送范围') ||
message.includes('电子围栏') ||
message.includes('围栏')
) {
// Toast 文案尽量短(小程序 showToast 标题长度有限),更详细的引导可在业务页面用 Modal 呈现。
return '暂不支持配送';
}
// 余额相关错误
if (message.includes('余额不足') || message.includes('balance')) {
return '账户余额不足,请充值后重试';
}
// 优惠券相关错误
if (message.includes('优惠券') || message.includes('coupon')) {
return '优惠券使用失败,请重新选择';
}
// 库存相关错误
if (message.includes('库存') || message.includes('stock')) {
return '商品库存不足,请减少购买数量';
}
// 地址相关错误
if (message.includes('地址') || message.includes('address')) {
return '收货地址信息有误,请重新选择';
}
// 订单相关错误
if (message.includes('订单') || message.includes('order')) {
return '订单创建失败,请重试';
}
// 网络相关错误
if (message.includes('网络') || message.includes('network') || message.includes('timeout')) {
return '网络连接异常,请检查网络后重试';
}
// 微信支付相关错误
if (message.includes('微信') || message.includes('wechat') || message.includes('wx')) {
return '微信支付失败,请重试';
}
// 返回原始错误信息
return message;
}
}
/**
* 快捷支付方法
*/
export const quickPay = {
/**
* 微信支付
*/
wechat: (orderData: OrderCreateRequest, callback?: PaymentCallback) => {
return PaymentHandler.pay(orderData, PaymentType.WECHAT, callback);
},
/**
* 余额支付
*/
balance: (orderData: OrderCreateRequest, callback?: PaymentCallback) => {
return PaymentHandler.pay(orderData, PaymentType.BALANCE, callback);
},
/**
* 支付宝支付
*/
alipay: (orderData: OrderCreateRequest, callback?: PaymentCallback) => {
return PaymentHandler.pay(orderData, PaymentType.ALIPAY, callback);
}
};
/**
* 构建单商品订单数据
*/
export function buildSingleGoodsOrder(
goodsId: number,
quantity: number = 1,
addressId?: number,
options?: {
comments?: string;
deliveryType?: number;
couponId?: any;
selfTakeMerchantId?: number;
skuId?: number;
specInfo?: string;
buyerRemarks?: string;
sendStartTime?: string;
}
): OrderCreateRequest {
return {
goodsItems: [
{
goodsId,
quantity,
skuId: options?.skuId,
specInfo: options?.specInfo
}
],
addressId,
payType: PaymentType.WECHAT, // 默认微信支付会被PaymentHandler覆盖
comments: options?.buyerRemarks || options?.comments || '',
sendStartTime: options?.sendStartTime,
deliveryType: options?.deliveryType || 0,
couponId: options?.couponId,
selfTakeMerchantId: options?.selfTakeMerchantId
};
}
/**
* 构建购物车订单数据
*/
export function buildCartOrder(
cartItems: Array<{ goodsId: number; quantity: number }>,
addressId?: number,
options?: {
comments?: string;
deliveryType?: number;
couponId?: number;
selfTakeMerchantId?: number;
}
): OrderCreateRequest {
return {
goodsItems: cartItems.map(item => ({
goodsId: item.goodsId,
quantity: item.quantity
})),
addressId,
payType: PaymentType.WECHAT, // 默认微信支付会被PaymentHandler覆盖
comments: options?.comments || '购物车下单',
deliveryType: options?.deliveryType || 0,
couponId: options?.couponId,
selfTakeMerchantId: options?.selfTakeMerchantId
};
}