Files
----/前端源码/uni-app/utils/wechat-payment.ts

458 lines
13 KiB
TypeScript
Raw Permalink 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 { 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<PendingPaymentState, 'timestamp'>): 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<PaymentResult> => {
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<PaymentResult> => {
// 支持平铺和嵌套两种格式
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<PaymentResult> => {
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<boolean> => {
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<PaymentResult> => {
return initiateWechatPayment(params, {
enablePolling: false // 不启用轮询,支付成功后立即返回
});
};
/**
* 安全支付(完整验证版本,适用于重要交易)
* @param params 订单参数
* @returns Promise<支付结果>
*/
export const secureWechatPay = async (params: CreateOrderParams): Promise<PaymentResult> => {
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;
}
}
};