/** * 微信支付集成工具类 * 专门处理微信支付相关功能 */ import { paymentApi } from '@/api'; import type { CreateOrderParams } from '@/api/types'; import { showToast } from './uni-compat'; declare const uni: any; const PENDING_PAYMENT_KEY = 'wx_pending_payment'; export interface PendingPaymentState { activeTab?: string; reportId?: number; unlockType?: string; price?: number; timestamp: number; } export const savePendingPayment = (state: Omit): void => { try { uni.setStorageSync(PENDING_PAYMENT_KEY, JSON.stringify({ ...state, timestamp: Date.now() })); } catch {} }; export const getPendingPayment = (): PendingPaymentState | null => { try { const raw = uni.getStorageSync(PENDING_PAYMENT_KEY); if (!raw) return null; const state = JSON.parse(raw) as PendingPaymentState; if (Date.now() - state.timestamp > 5 * 60 * 1000) { uni.removeStorageSync(PENDING_PAYMENT_KEY); return null; } return state; } catch { return null; } }; export const clearPendingPayment = (): void => { try { uni.removeStorageSync(PENDING_PAYMENT_KEY); } catch {} }; export const cleanH5WechatCodeFromUrl = (): void => { try { if (typeof window === 'undefined') return; const url = new URL(window.location.href); if (url.searchParams.has('code')) { url.searchParams.delete('code'); url.searchParams.delete('state'); window.history.replaceState(null, '', url.toString()); } if (url.hash && url.hash.includes('code=')) { const [hashPath, hashQuery] = url.hash.split('?'); if (hashQuery) { const hp = new URLSearchParams(hashQuery); hp.delete('code'); hp.delete('state'); const newHash = hp.toString() ? `${hashPath}?${hp.toString()}` : hashPath; window.history.replaceState(null, '', `${url.origin}${url.pathname}${url.search}${newHash}`); } } } catch {} }; export const isH5WechatBrowser = (): boolean => { try { if (typeof window === 'undefined') return false; const ua = window.navigator?.userAgent || ''; return /MicroMessenger/i.test(ua); } catch { return false; } }; const getH5UrlParam = (name: string): string | undefined => { try { if (typeof window === 'undefined') return undefined; let search = window.location.search || ''; if (!search && window.location.hash && window.location.hash.includes('?')) { const hashQuery = window.location.hash.split('?')[1] || ''; search = hashQuery ? `?${hashQuery}` : ''; } if (!search) return undefined; const params = new URLSearchParams(search.startsWith('?') ? search.slice(1) : search); const value = params.get(name) || ''; return value || undefined; } catch { return undefined; } }; export const getH5WechatCode = (): string | undefined => { try { if (!isH5WechatBrowser()) return undefined; return getH5UrlParam('code'); } catch { return undefined; } }; export const redirectToH5WechatOAuthForCode = (): void => { if (typeof window === 'undefined') return; if (!isH5WechatBrowser()) return; const existingCode = getH5WechatCode(); if (existingCode) return; const host = window.location.hostname; if (host === 'localhost' || host === '127.0.0.1' || /^192\.168\./.test(host) || /^10\./.test(host) || /^172\.(1[6-9]|2\d|3[01])\./.test(host)) { uni.showToast({ title: '本地环境无法微信授权,请使用线上域名', icon: 'none' }); return; } const appId = 'wx1ca1ac7ad12123ac'; if (!appId) { uni.showToast({ title: '缺少VITE_WECHAT_H5_APPID', icon: 'none' }); return; } const href = window.location.href; const cleanUrl = new URL(href); cleanUrl.searchParams.delete('code'); cleanUrl.searchParams.delete('state'); const redirectUri = encodeURIComponent(cleanUrl.toString()); const scope = 'snsapi_base'; const url = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appId}&redirect_uri=${redirectUri}&response_type=code&scope=${scope}&state=STATE#wechat_redirect`; window.location.replace(url); }; /** * 支付状态枚举 */ export enum PaymentStatus { PENDING = 'pending', PAID = 'paid', CANCELLED = 'cancelled', FAILED = 'failed', REFUNDED = 'refunded' } /** * 支付结果接口 */ export interface PaymentResult { success: boolean; outTradeNo?: string; msg?: string; status?: PaymentStatus; } /** * 微信支付配置 */ export interface WechatPayConfig { enablePolling?: boolean; // 是否启用轮询查询 maxPollAttempts?: number; // 最大轮询次数 pollInterval?: number; // 轮询间隔(毫秒) } /** * 发起微信支付(完整流程) * @param params 订单参数 * @param config 支付配置 * @returns Promise<支付结果> */ export const initiateWechatPayment = async ( params: CreateOrderParams, config: WechatPayConfig = {} ): Promise => { const { enablePolling = true, maxPollAttempts = 10, pollInterval = 2000 } = config; try { // 1. 创建支付订单 const orderResult = await createPaymentOrder(params); if (!orderResult.success) { return orderResult; } // 2. 调起微信支付 const paymentResult = await callWechatPay(orderResult.orderData!); // 3. 如果支付成功且启用轮询,则验证支付状态 if (paymentResult.success && enablePolling && paymentResult.outTradeNo) { const verified = await verifyPaymentStatus( paymentResult.outTradeNo, maxPollAttempts, pollInterval ); if (!verified) { return { success: false, outTradeNo: paymentResult.outTradeNo, msg: '支付状态验证失败,请联系客服', status: PaymentStatus.FAILED }; } } return paymentResult; } catch (error: any) { console.error('微信支付流程失败:', error); return { success: false, msg: error.msg || error.message || '支付失败,请重试', status: PaymentStatus.FAILED }; } }; /** * 创建支付订单 * @param params 订单参数 * @returns Promise<订单创建结果> */ const createPaymentOrder = async (params: CreateOrderParams): Promise<{ success: boolean; orderData?: any; msg?: string; }> => { try { uni.showLoading({ title: '创建订单中...' }); // 确保支付参数完整 const h5Code = params.code || getH5WechatCode(); if (isH5WechatBrowser() && !h5Code) { uni.hideLoading(); redirectToH5WechatOAuthForCode(); return { success: false, msg: '微信授权中', }; } const paymentParams: CreateOrderParams = { ...params, pay_type: 'jsapi', code: h5Code, }; const orderRes = await paymentApi.createOrder(paymentParams); uni.hideLoading(); cleanH5WechatCodeFromUrl(); if (!orderRes.appId && !orderRes.payment_params) { throw new Error('获取支付参数失败'); } return { success: true, orderData: orderRes }; } catch (error: any) { uni.hideLoading(); console.error('创建订单失败:', error); // invalid code 时清理状态,避免无限循环 const errorMsg = error.msg || error.message || '创建订单失败'; if (errorMsg.includes('invalid code') || errorMsg.includes('code')) { cleanH5WechatCodeFromUrl(); clearPendingPayment(); } showToast({ title: errorMsg, icon: 'none' }); return { success: false, msg: errorMsg }; } }; /** * 调起微信支付(H5使用WeixinJSBridge,小程序使用uni.requestPayment) * @param orderData 订单数据 * @returns Promise<支付结果> */ const callWechatPay = async (orderData: any): Promise => { // 支持平铺和嵌套两种格式 const pp = orderData.payment_params || orderData; if (!pp?.paySign) { return { success: false, msg: '缺少支付参数', status: PaymentStatus.FAILED }; } // H5微信浏览器使用 WeixinJSBridge if (isH5WechatBrowser() && typeof window !== 'undefined') { return callWechatPayH5(orderData); } // 小程序环境使用 uni.requestPayment return new Promise((resolve) => { uni.requestPayment({ provider: 'wxpay', timeStamp: pp.timeStamp, nonceStr: pp.nonceStr, package: pp.package, signType: pp.signType || 'MD5', paySign: pp.paySign, success: () => { showToast({ title: '支付成功', icon: 'success' }); resolve({ success: true, outTradeNo: orderData.out_trade_no, msg: '支付成功', status: PaymentStatus.PAID }); }, fail: (err: any) => { let msg = '支付失败'; let status = PaymentStatus.FAILED; if (err.errMsg?.includes('cancel')) { msg = '用户取消支付'; status = PaymentStatus.CANCELLED; } if (status !== PaymentStatus.CANCELLED) showToast({ title: msg, icon: 'none' }); resolve({ success: false, outTradeNo: orderData.out_trade_no, msg, status }); } }); }); }; /** * H5微信浏览器内调起JSAPI支付 */ const callWechatPayH5 = (orderData: any): Promise => { const pp = orderData.payment_params || orderData; return new Promise((resolve) => { const invoke = () => { (window as any).WeixinJSBridge.invoke( 'getBrandWCPayRequest', { appId: pp.appId, timeStamp: pp.timeStamp, nonceStr: pp.nonceStr, package: pp.package, signType: pp.signType || 'RSA', paySign: pp.paySign, }, (res: any) => { if (res.err_msg === 'get_brand_wcpay_request:ok') { showToast({ title: '支付成功', icon: 'success' }); resolve({ success: true, outTradeNo: orderData.out_trade_no, msg: '支付成功', status: PaymentStatus.PAID }); } else if (res.err_msg === 'get_brand_wcpay_request:cancel') { resolve({ success: false, outTradeNo: orderData.out_trade_no, msg: '用户取消支付', status: PaymentStatus.CANCELLED }); } else { showToast({ title: '支付失败', icon: 'none' }); resolve({ success: false, outTradeNo: orderData.out_trade_no, msg: res.err_msg || '支付失败', status: PaymentStatus.FAILED }); } } ); }; if (typeof (window as any).WeixinJSBridge === 'undefined') { document.addEventListener('WeixinJSBridgeReady', invoke, false); } else { invoke(); } }); }; /** * 验证支付状态(轮询查询) * @param outTradeNo 商户订单号 * @param maxAttempts 最大尝试次数 * @param interval 查询间隔(毫秒) * @returns Promise<是否支付成功> */ const verifyPaymentStatus = async ( outTradeNo: string, maxAttempts: number = 10, interval: number = 2000 ): Promise => { for (let i = 0; i < maxAttempts; i++) { try { const res = await paymentApi.queryOrder(outTradeNo); if (res.status === 'paid') { return true; } else if (res.status === 'cancelled' || res.status === 'refunded') { return false; } // 等待后继续查询 await new Promise(resolve => setTimeout(resolve, interval)); } catch (error) { console.error(`第${i + 1}次查询订单状态失败:`, error); if (i === maxAttempts - 1) { throw error; } await new Promise(resolve => setTimeout(resolve, interval)); } } return false; }; /** * 快速支付(简化版本,适用于大多数场景) * @param params 订单参数 * @returns Promise<支付结果> */ export const quickWechatPay = async (params: CreateOrderParams): Promise => { return initiateWechatPayment(params, { enablePolling: false // 不启用轮询,支付成功后立即返回 }); }; /** * 安全支付(完整验证版本,适用于重要交易) * @param params 订单参数 * @returns Promise<支付结果> */ export const secureWechatPay = async (params: CreateOrderParams): Promise => { return initiateWechatPayment(params, { enablePolling: true, maxPollAttempts: 15, pollInterval: 1500 }); }; /** * 处理支付结果的通用方法 * @param result 支付结果 * @param onSuccess 成功回调 * @param onFail 失败回调 * @param onCancel 取消回调 */ export const handlePaymentResult = ( result: PaymentResult, callbacks: { onSuccess?: (result: PaymentResult) => void; onFail?: (result: PaymentResult) => void; onCancel?: (result: PaymentResult) => void; } ) => { const { onSuccess, onFail, onCancel } = callbacks; if (result.success) { onSuccess?.(result); } else { switch (result.status) { case PaymentStatus.CANCELLED: onCancel?.(result); break; case PaymentStatus.FAILED: default: onFail?.(result); break; } } };