upload project source code
This commit is contained in:
457
前端源码/uni-app/utils/wechat-payment.ts
Normal file
457
前端源码/uni-app/utils/wechat-payment.ts
Normal file
@@ -0,0 +1,457 @@
|
||||
/**
|
||||
* 微信支付集成工具类
|
||||
* 专门处理微信支付相关功能
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user