upload project source code

This commit is contained in:
2026-04-30 18:49:43 +08:00
commit 9b394ba682
2277 changed files with 660945 additions and 0 deletions

View 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;
}
}
};