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'; import { getWxOpenId, getUserInfo } from '@/api/layout'; /** * 支付类型枚举 */ export enum PaymentType { BALANCE = 0, // 余额支付 WECHAT = 1, // 微信支付 ALIPAY = 3, // 支付宝支付 } /** * 支付结果回调 */ export interface PaymentCallback { // Return `false` to skip default "支付成功" toast + redirect. onSuccess?: () => void | boolean | Promise; onError?: (error: string) => void; onComplete?: () => void; } /** * 统一支付处理类 */ export class PaymentHandler { // 简单缓存,避免频繁请求(小程序单次运行生命周期内有效) private static storeRidersCache = new Map(); private static warehousesCache: ShopStoreWarehouse[] | null = null; /** * 【关键修复】支付前确保当前微信用户的 openid 已正确绑定到后端 * * 问题场景: * 1. 用户通过手机号注册 → openid 未绑定 → 后端用空 openid 创建预支付单 → 支付时报"账号不一致" * 2. 用户切换了微信账号 → 本地缓存的用户信息是旧账号的 → openid 与当前支付人不匹配 * 3. 自动登录(loginByOpenId)未触发 getWxOpenId 绑定流程 * * 解决方案:每次微信支付前,重新获取 wx.login code 并调用 getWxOpenId 绑定, * 确保后端记录的 openid = 当前实际支付人的 openid */ private static async ensureOpenIdBeforePay(): Promise { try { // 非微信环境跳过(如 H5 开发调试) let isWeapp = false; try { isWeapp = Taro.getEnv() === Taro.ENV_TYPE.WEAPP; } catch (_e) { isWeapp = process.env.TARO_ENV === 'weapp'; } if (!isWeapp) return; // 获取当前登录用户的最新信息(检查是否已有 openid) let currentUser = null; try { currentUser = await getUserInfo(); } catch (_e) { // getUserInfo 失败时不阻塞支付(可能是 token 过期等),让后续接口自行报错 console.warn('[ensureOpenId] 获取用户信息失败,跳过 openid 校验'); return; } // 如果用户已有 openid,仍然需要刷新:因为用户可能切换了微信账号 // 每次都重新获取 code + 绑定,确保 openid 是当前微信会话的 const code = await new Promise((resolve, reject) => { Taro.login({ success: (res) => resolve(res.code as string), fail: (e) => reject(e), timeout: 5000, // 5秒超时 }); }); if (!code) { console.warn('[ensureOpenId] wx.login 未返回 code'); return; } // 调用后端绑定/更新 openid await getWxOpenId({ code }); console.log('[ensureOpenId] openid 刷新/绑定成功'); // 同步本地 User 缓存,确保后续逻辑能读到最新 openid try { const freshUser = await getUserInfo(); if (freshUser) { Taro.setStorageSync('User', freshUser); } } catch (_e) { // ignore: 服务端已更新 openid,本地缓存不同步也不影响本次支付 } } catch (error) { // openid 刷新失败不阻塞支付流程(可能网络波动), // 但记录日志方便排查;若确实不一致,微信支付侧会报错 console.warn('[ensureOpenId] openid 刷新失败(非阻塞):', error); } } /** * 执行支付 * @param orderData 订单数据 * @param paymentType 支付类型 * @param callback 回调函数 */ static async pay( orderData: OrderCreateRequest, paymentType: PaymentType, callback?: PaymentCallback ): Promise { 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; // 【关键修复】微信支付前,强制刷新/绑定当前微信用户的 openid // 防止"下单账号与支付账号不一致"错误 if (paymentType === PaymentType.WECHAT) { await this.ensureOpenIdBeforePay(); } 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 { const cached = this.storeRidersCache.get(storeId); if (cached) return cached; // 后端字段可能叫 dealerId 或 storeId,这里都带上,服务端忽略未知字段即可。 // 这里做一次路径兼容(camel vs kebab),避免接口路径不一致导致整单失败。 const list = await this.listByCompatEndpoint( ['/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 { 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 { if (this.warehousesCache) return this.warehousesCache; const list = await this.listByCompatEndpoint( ['/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 { 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( urls: string[], params: Record ): Promise { 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 { 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 { 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 { // 支付宝支付逻辑,根据实际情况实现 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; deliveryMethod?: string; deliveryFloor?: number; } ): 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, deliveryMethod: options?.deliveryMethod, deliveryFloor: options?.deliveryFloor }; } /** * 构建购物车订单数据 */ 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 }; }