1031 lines
27 KiB
JavaScript
1031 lines
27 KiB
JavaScript
/**
|
||
* 支付服务工具
|
||
* 统一管理支付相关功能
|
||
*/
|
||
|
||
import { API_BASE } from '@/config/api.js';
|
||
|
||
/**
|
||
* 模拟支付模式配置
|
||
* 设置为 true 时,所有支付都会使用模拟数据,不调用真实支付接口
|
||
*/
|
||
export const MOCK_PAYMENT_MODE = false;
|
||
|
||
export function mapOfficialVoiceDisplay(list) {
|
||
const regionPrefixMap = {
|
||
'湾区大叔': '台湾',
|
||
'台湾小何': '台湾',
|
||
'双节棍小哥': '台湾',
|
||
'广州德哥': '广州',
|
||
'浩宇小哥': '大陆',
|
||
'湾湾小何': '台湾',
|
||
'北京小爷': '北京',
|
||
'京腔侃爷/Harmony': '北京',
|
||
'林北浩儿': '台湾',
|
||
'粤语小灿': '广东',
|
||
'泉州子轩': '福建',
|
||
'豫州子轩': '河南',
|
||
'呆萌川妹': '四川',
|
||
'广西远舟': '广西',
|
||
'妹坨洁儿': '湖南'
|
||
};
|
||
return (list || []).map(v => {
|
||
if (!v) return v;
|
||
const vt = v.voice_type || 'CLONE';
|
||
if (vt !== 'OFFICIAL') return v;
|
||
const rawName = v.voice_name ? String(v.voice_name) : '';
|
||
const prefix = rawName && regionPrefixMap[rawName] ? regionPrefixMap[rawName] : '';
|
||
if (!prefix || !rawName) return v;
|
||
if (rawName.indexOf(prefix + '-') === 0) return v;
|
||
return { ...v, voice_name: `${prefix}-${rawName}` };
|
||
});
|
||
}
|
||
|
||
|
||
/**
|
||
* 服务类型映射(后端类型 -> 前端配置)
|
||
*/
|
||
const SERVICE_TYPE_MAPPING = {
|
||
'CREATE_VOICE': 'voice_clone',
|
||
'PHOTO_REVIVAL': 'photo_revival',
|
||
'VOLCENGINE_VIDEO': 'volcengine_video',
|
||
'SYNTHESIZE': 'tts_synthesis',
|
||
'VIDEO_CALL': 'video_call',
|
||
'AI_CALL': 'conversation'
|
||
};
|
||
|
||
/**
|
||
* 服务类型配置(默认值,会被后端数据覆盖)
|
||
*/
|
||
export const SERVICE_TYPES = {
|
||
VOICE_CLONE: {
|
||
type: 'voice_clone',
|
||
backendType: 'CREATE_VOICE',
|
||
name: '声音克隆',
|
||
desc: '克隆您的声音,让AI学会您的音色',
|
||
price: 0.00,
|
||
freeTrialCount: 0
|
||
},
|
||
PHOTO_REVIVAL: {
|
||
type: 'photo_revival',
|
||
backendType: 'PHOTO_REVIVAL',
|
||
name: '照片复活',
|
||
desc: '让照片中的人开口说话',
|
||
price: 0.00,
|
||
freeTrialCount: 0
|
||
},
|
||
VOLCENGINE_VIDEO: {
|
||
type: 'volcengine_video',
|
||
backendType: 'VOLCENGINE_VIDEO',
|
||
name: '火山视频复活',
|
||
desc: '火山视频复活',
|
||
price: 0.00,
|
||
freeTrialCount: 0
|
||
},
|
||
TTS_SYNTHESIS: {
|
||
type: 'tts_synthesis',
|
||
backendType: 'SYNTHESIZE',
|
||
name: '语音合成',
|
||
desc: '使用已有音色合成语音',
|
||
price: 0.00,
|
||
freeTrialCount: 0
|
||
},
|
||
VIDEO_CALL: {
|
||
type: 'video_call',
|
||
backendType: 'VIDEO_CALL',
|
||
name: 'AI视频通话',
|
||
desc: 'AI视频通话',
|
||
price: 0.00,
|
||
freeTrialCount: 0
|
||
},
|
||
CONVERSATION: {
|
||
type: 'conversation',
|
||
backendType: 'AI_CALL',
|
||
name: '实时对话',
|
||
desc: '与AI进行实时语音对话',
|
||
price: 0.00,
|
||
freeTrialCount: 0
|
||
}
|
||
};
|
||
|
||
// 缓存价格数据
|
||
let priceCache = null;
|
||
let priceCacheTime = 0;
|
||
const CACHE_DURATION = 5 * 60 * 1000; // 5分钟缓存
|
||
|
||
/**
|
||
* 从后端获取服务价格列表
|
||
* @returns {Promise<Array>}
|
||
*/
|
||
export async function fetchServicePrices() {
|
||
// 检查缓存
|
||
const now = Date.now();
|
||
if (priceCache && (now - priceCacheTime) < CACHE_DURATION) {
|
||
console.log('[Payment] 使用缓存价格数据');
|
||
return priceCache;
|
||
}
|
||
|
||
console.log('[Payment] 开始获取服务价格,API地址:', `${API_BASE}/api/pay/prices`);
|
||
|
||
return new Promise((resolve, reject) => {
|
||
uni.request({
|
||
url: `${API_BASE}/api/pay/prices`,
|
||
method: 'GET',
|
||
success: (res) => {
|
||
console.log('[Payment] 价格API响应:', res);
|
||
|
||
if (res.statusCode === 200 && res.data) {
|
||
priceCache = res.data;
|
||
priceCacheTime = now;
|
||
|
||
console.log('[Payment] 后端返回价格数据:', res.data);
|
||
|
||
// 更新SERVICE_TYPES中的价格
|
||
let updateCount = 0;
|
||
res.data.forEach(priceItem => {
|
||
console.log('[Payment] 处理价格项:', priceItem);
|
||
const frontendType = SERVICE_TYPE_MAPPING[priceItem.serviceType];
|
||
console.log('[Payment] 映射类型:', priceItem.serviceType, '->', frontendType);
|
||
|
||
if (frontendType) {
|
||
const serviceKey = Object.keys(SERVICE_TYPES).find(
|
||
key => SERVICE_TYPES[key].type === frontendType
|
||
);
|
||
if (serviceKey && SERVICE_TYPES[serviceKey]) {
|
||
console.log('[Payment] 更新服务:', serviceKey, '价格:', priceItem.price, '免费次数:', priceItem.freeTrialCount);
|
||
SERVICE_TYPES[serviceKey].price = priceItem.price;
|
||
SERVICE_TYPES[serviceKey].freeTrialCount = priceItem.freeTrialCount || 0;
|
||
SERVICE_TYPES[serviceKey].name = priceItem.serviceName;
|
||
SERVICE_TYPES[serviceKey].desc = priceItem.description || SERVICE_TYPES[serviceKey].desc;
|
||
updateCount++;
|
||
}
|
||
} else {
|
||
console.warn('[Payment] 未找到映射的前端类型:', priceItem.serviceType);
|
||
}
|
||
});
|
||
|
||
console.log('[Payment] 价格更新完成,共更新', updateCount, '个服务');
|
||
console.log('[Payment] 最终SERVICE_TYPES:', JSON.stringify(SERVICE_TYPES, null, 2));
|
||
resolve(res.data);
|
||
} else {
|
||
console.error('[Payment] 获取价格失败,状态码:', res.statusCode);
|
||
if (priceCache) {
|
||
resolve(priceCache);
|
||
return;
|
||
}
|
||
reject(new Error('获取价格失败'));
|
||
}
|
||
},
|
||
fail: (err) => {
|
||
console.error('[Payment] 获取价格请求失败:', err);
|
||
if (priceCache) {
|
||
resolve(priceCache);
|
||
return;
|
||
}
|
||
reject(err);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 生成订单号
|
||
*/
|
||
function generateOrderNo() {
|
||
const timestamp = Date.now();
|
||
const random = Math.floor(Math.random() * 10000).toString().padStart(4, '0');
|
||
return `ORD${timestamp}${random}`;
|
||
}
|
||
|
||
/**
|
||
* 获取用户信息
|
||
*/
|
||
function getUserInfo() {
|
||
return {
|
||
userId: uni.getStorageSync('userId') || '',
|
||
token: uni.getStorageSync('token') || ''
|
||
};
|
||
}
|
||
|
||
async function fetchUsagePreview(frontendServiceType) {
|
||
try {
|
||
const { userId, token } = getUserInfo();
|
||
if (!userId) return null;
|
||
const service = Object.values(SERVICE_TYPES).find(s => s.type === frontendServiceType);
|
||
const backendServiceType = service ? service.backendType : frontendServiceType;
|
||
return await new Promise((resolve) => {
|
||
uni.request({
|
||
url: `${API_BASE}/api/pay/usage-preview?serviceType=${backendServiceType}`,
|
||
method: 'GET',
|
||
header: {
|
||
'X-User-Id': userId,
|
||
'Authorization': token ? `Bearer ${token}` : ''
|
||
},
|
||
success: (res) => {
|
||
if (res.statusCode === 200 && res.data && res.data.success) {
|
||
resolve(res.data);
|
||
return;
|
||
}
|
||
resolve(null);
|
||
},
|
||
fail: () => resolve(null)
|
||
});
|
||
});
|
||
} catch (e) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 后端资格校验:免费次数/补发次数/已支付 都视为可用。
|
||
* @param {string} frontendServiceType - 前端 service.type
|
||
* @returns {Promise<boolean>} - true 表示无需支付可直接使用
|
||
*/
|
||
async function checkBackendEligibility(frontendServiceType) {
|
||
try {
|
||
const { userId, token } = getUserInfo();
|
||
if (!userId) return false;
|
||
const service = Object.values(SERVICE_TYPES).find(s => s.type === frontendServiceType);
|
||
const backendServiceType = service ? service.backendType : frontendServiceType;
|
||
return await new Promise((resolve) => {
|
||
uni.request({
|
||
url: `${API_BASE}/api/pay/check?userId=${userId}&serviceType=${backendServiceType}`,
|
||
method: 'GET',
|
||
header: {
|
||
'X-User-Id': userId,
|
||
'Authorization': token ? `Bearer ${token}` : ''
|
||
},
|
||
success: (res) => {
|
||
// 按次付费场景:只有“明确剩余次数>0”才视为可用。
|
||
// 兼容后端可能返回的字段:
|
||
// - paid: boolean
|
||
// - remainingTotal / remaining / remainingCount / remainingTimes
|
||
// - freeTrialRemaining / freeTrialLeft
|
||
if (res.statusCode === 200 && res.data && typeof res.data.paid === 'boolean') {
|
||
const data = res.data || {};
|
||
console.log('[Payment] /api/pay/check 响应:', backendServiceType, data);
|
||
|
||
if (data.paid === false) {
|
||
resolve(false);
|
||
return;
|
||
}
|
||
|
||
const remainingTotal = (typeof data.remainingTotal === 'number') ? data.remainingTotal
|
||
: (typeof data.remaining === 'number') ? data.remaining
|
||
: (typeof data.remainingCount === 'number') ? data.remainingCount
|
||
: (typeof data.remainingTimes === 'number') ? data.remainingTimes
|
||
: null;
|
||
|
||
const freeTrialRemaining = (typeof data.freeTrialRemaining === 'number') ? data.freeTrialRemaining
|
||
: (typeof data.freeTrialLeft === 'number') ? data.freeTrialLeft
|
||
: null;
|
||
|
||
// 有任何一种剩余次数>0,则允许跳过支付
|
||
if ((typeof remainingTotal === 'number' && remainingTotal > 0) || (typeof freeTrialRemaining === 'number' && freeTrialRemaining > 0)) {
|
||
resolve(true);
|
||
return;
|
||
}
|
||
|
||
// paid=true 但没有明确剩余次数(或<=0),视为需要付费
|
||
resolve(false);
|
||
return;
|
||
}
|
||
resolve(false);
|
||
},
|
||
fail: () => resolve(false)
|
||
});
|
||
});
|
||
} catch (e) {
|
||
console.error('[Payment] 后端资格校验异常:', e);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 创建订单
|
||
* @param {string} serviceType - 服务类型
|
||
* @param {object} extraData - 额外数据
|
||
* @returns {Promise}
|
||
*/
|
||
export async function createOrder(serviceType, extraData = {}) {
|
||
try {
|
||
// 先获取最新价格
|
||
await fetchServicePrices();
|
||
|
||
// 获取服务配置
|
||
const service = Object.values(SERVICE_TYPES).find(s => s.type === serviceType);
|
||
if (!service) {
|
||
throw new Error('无效的服务类型');
|
||
}
|
||
|
||
const orderNo = generateOrderNo();
|
||
|
||
console.log('[Payment] 创建订单:', {
|
||
orderNo,
|
||
serviceType,
|
||
serviceName: service.name,
|
||
amount: service.price
|
||
});
|
||
|
||
return {
|
||
orderNo,
|
||
serviceType,
|
||
serviceName: service.name,
|
||
amount: service.price,
|
||
service,
|
||
...extraData
|
||
};
|
||
} catch (error) {
|
||
console.error('[Payment] 创建订单失败:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取支付渠道
|
||
* @returns {string} 'wechat' | 'alipay'
|
||
*/
|
||
function getPaymentChannel() {
|
||
// #ifdef MP-WEIXIN
|
||
return 'wechat';
|
||
// #endif
|
||
// #ifdef MP-ALIPAY
|
||
return 'alipay';
|
||
// #endif
|
||
// #ifdef APP-PLUS
|
||
// App环境:可以根据用户选择或默认使用微信
|
||
// 这里默认使用微信,也可以让用户选择
|
||
return uni.getStorageSync('preferred_payment') || 'wechat';
|
||
// #endif
|
||
// 默认返回微信
|
||
return 'wechat';
|
||
}
|
||
|
||
/**
|
||
* 创建后端支付订单
|
||
* @param {string} serviceType - 服务类型(前端类型)
|
||
* @param {number} userId - 用户ID
|
||
* @returns {Promise}
|
||
*/
|
||
export async function createBackendOrder(serviceType, userId) {
|
||
// 模拟支付模式:直接返回模拟订单数据
|
||
if (MOCK_PAYMENT_MODE) {
|
||
console.log('[Payment] 模拟支付模式:创建模拟订单');
|
||
const service = Object.values(SERVICE_TYPES).find(s => s.type === serviceType);
|
||
const mockOrderNo = 'MOCK_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||
|
||
return new Promise((resolve) => {
|
||
setTimeout(() => {
|
||
resolve({
|
||
orderNo: mockOrderNo,
|
||
userId: userId,
|
||
serviceType: service ? service.backendType : serviceType,
|
||
serviceName: service ? service.name : '未知服务',
|
||
amount: service ? service.price : 0,
|
||
status: 'PENDING',
|
||
paymentChannel: 'mock',
|
||
qrCodeUrl: 'MOCK_QR_CODE_' + mockOrderNo,
|
||
createdAt: new Date().toISOString()
|
||
});
|
||
}, 500); // 模拟网络延迟
|
||
});
|
||
}
|
||
|
||
// 真实支付模式
|
||
const channel = getPaymentChannel();
|
||
|
||
// 将前端类型转换为后端类型
|
||
const service = Object.values(SERVICE_TYPES).find(s => s.type === serviceType);
|
||
const backendServiceType = service ? service.backendType : serviceType;
|
||
|
||
console.log('[Payment] 服务类型转换:', serviceType, '->', backendServiceType);
|
||
|
||
// 获取用户标识用于小程序支付
|
||
let bizId = null;
|
||
if (channel === 'wechat') {
|
||
// #ifdef MP-WEIXIN
|
||
const openid = uni.getStorageSync('wx_openid');
|
||
console.log('[Payment] wx_openid:', openid);
|
||
if (openid) {
|
||
bizId = 'wx_openid_' + openid;
|
||
} else {
|
||
console.error('[Payment] 缺少wx_openid,无法创建微信小程序支付订单');
|
||
return Promise.reject(new Error('缺少openid,请先退出并重新微信登录'));
|
||
}
|
||
// #endif
|
||
// #ifdef APP-PLUS
|
||
// App微信支付不需要openid
|
||
bizId = 'wx_app';
|
||
// #endif
|
||
} else if (channel === 'alipay') {
|
||
const alipayUserId = uni.getStorageSync('alipay_user_id');
|
||
if (alipayUserId) {
|
||
bizId = 'alipay_user_' + alipayUserId;
|
||
}
|
||
}
|
||
console.log('[Payment] 下单参数:', { userId, serviceType: backendServiceType, paymentChannel: channel, bizId });
|
||
|
||
return new Promise((resolve, reject) => {
|
||
uni.request({
|
||
url: `${API_BASE}/api/pay/orders`,
|
||
method: 'POST',
|
||
header: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${uni.getStorageSync('token') || ''}`
|
||
},
|
||
data: {
|
||
userId: userId,
|
||
serviceType: backendServiceType,
|
||
paymentChannel: channel,
|
||
bizId: bizId
|
||
},
|
||
success: (res) => {
|
||
if (res.statusCode === 200 && res.data) {
|
||
console.log('[Payment] 后端订单创建成功:', res.data);
|
||
resolve(res.data);
|
||
} else {
|
||
console.error('[Payment] 后端订单创建失败:', {
|
||
statusCode: res.statusCode,
|
||
data: res.data,
|
||
header: res.header
|
||
});
|
||
if (res.data && res.data.message) {
|
||
console.error('[Payment] 后端错误信息:', res.data.message);
|
||
}
|
||
reject(new Error(res.data?.message || '创建订单失败'));
|
||
}
|
||
},
|
||
fail: (err) => {
|
||
console.error('[Payment] 创建订单请求失败:', err);
|
||
reject(err);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 执行微信支付(支持小程序和App)
|
||
* @param {object} orderData - 订单数据
|
||
* @returns {Promise}
|
||
*/
|
||
function executeWechatPayment(orderData) {
|
||
return new Promise((resolve, reject) => {
|
||
// 模拟支付模式:直接返回成功
|
||
if (MOCK_PAYMENT_MODE) {
|
||
console.log('[Payment] 模拟支付模式:模拟微信支付成功');
|
||
setTimeout(() => {
|
||
resolve({
|
||
success: true,
|
||
orderNo: orderData.orderNo,
|
||
payTime: new Date().toISOString(),
|
||
message: '支付成功(模拟)'
|
||
});
|
||
}, 1000); // 模拟1秒支付延迟
|
||
return;
|
||
}
|
||
|
||
// #ifdef MP-WEIXIN
|
||
// 微信小程序支付
|
||
uni.requestPayment({
|
||
timeStamp: orderData.timeStamp || String(Date.now()),
|
||
nonceStr: orderData.nonceStr || '',
|
||
package: orderData.package || '',
|
||
signType: orderData.signType || 'RSA',
|
||
paySign: orderData.paySign || '',
|
||
success: (res) => {
|
||
console.log('[Payment] 微信支付成功:', res);
|
||
uni.request({
|
||
url: `${API_BASE}/api/pay/orders/${orderData.orderNo}/sync/wechat`,
|
||
method: 'POST',
|
||
header: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${uni.getStorageSync('token') || ''}`
|
||
},
|
||
success: (syncRes) => {
|
||
if (syncRes.statusCode === 200) {
|
||
console.log('[Payment] 订单同步成功:', syncRes.data);
|
||
resolve({
|
||
success: true,
|
||
orderNo: orderData.orderNo,
|
||
payTime: new Date().toISOString(),
|
||
message: '支付成功'
|
||
});
|
||
} else {
|
||
console.error('[Payment] 订单同步失败:', syncRes);
|
||
reject(new Error(syncRes.data?.message || '支付成功但订单同步失败'));
|
||
}
|
||
},
|
||
fail: (err) => {
|
||
console.error('[Payment] 订单同步请求失败:', err);
|
||
reject(new Error('支付成功但订单同步请求失败'));
|
||
}
|
||
});
|
||
},
|
||
fail: (err) => {
|
||
console.error('[Payment] 微信支付失败:', err);
|
||
if (err.errMsg && err.errMsg.includes('cancel')) {
|
||
reject(new Error('用户取消支付'));
|
||
} else {
|
||
reject(new Error(err.errMsg || '支付失败'));
|
||
}
|
||
}
|
||
});
|
||
// #endif
|
||
|
||
// #ifdef APP-PLUS
|
||
uni.requestPayment({
|
||
provider: 'wxpay',
|
||
orderInfo: orderData.orderInfo || '',
|
||
success: (res) => {
|
||
console.log('[Payment] App微信支付成功:', res);
|
||
resolve({
|
||
success: true,
|
||
orderNo: orderData.orderNo,
|
||
payTime: new Date().toISOString(),
|
||
message: '支付成功'
|
||
});
|
||
},
|
||
fail: (err) => {
|
||
console.error('[Payment] App微信支付失败:', err);
|
||
if (err.errMsg && err.errMsg.includes('cancel')) {
|
||
reject(new Error('用户取消支付'));
|
||
} else {
|
||
reject(new Error(err.errMsg || '支付失败'));
|
||
}
|
||
}
|
||
});
|
||
// #endif
|
||
|
||
// #if !defined(MP-WEIXIN) && !defined(APP-PLUS)
|
||
reject(new Error('当前环境不支持微信支付'));
|
||
// #endif
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 执行支付宝支付(支持小程序和App)
|
||
* @param {object} orderData - 订单数据
|
||
* @returns {Promise}
|
||
*/
|
||
function executeAlipayPayment(orderData) {
|
||
return new Promise((resolve, reject) => {
|
||
// 模拟支付模式:直接返回成功
|
||
if (MOCK_PAYMENT_MODE) {
|
||
console.log('[Payment] 模拟支付模式:模拟支付宝支付成功');
|
||
setTimeout(() => {
|
||
resolve({
|
||
success: true,
|
||
orderNo: orderData.orderNo,
|
||
payTime: new Date().toISOString(),
|
||
message: '支付成功(模拟)'
|
||
});
|
||
}, 1000); // 模拟1秒支付延迟
|
||
return;
|
||
}
|
||
|
||
// #ifdef MP-ALIPAY
|
||
// 支付宝小程序支付
|
||
my.tradePay({
|
||
tradeNO: orderData.tradeNo || '',
|
||
success: (res) => {
|
||
console.log('[Payment] 支付宝支付成功:', res);
|
||
resolve({
|
||
success: true,
|
||
orderNo: orderData.orderNo,
|
||
payTime: new Date().toISOString(),
|
||
message: '支付成功'
|
||
});
|
||
},
|
||
fail: (err) => {
|
||
console.error('[Payment] 支付宝支付失败:', err);
|
||
if (err.resultCode === '6001') {
|
||
reject(new Error('用户取消支付'));
|
||
} else {
|
||
reject(new Error(err.memo || '支付失败'));
|
||
}
|
||
}
|
||
});
|
||
// #endif
|
||
|
||
// #ifdef APP-PLUS
|
||
// App环境:暂时使用模拟支付(开发测试用)
|
||
console.log('[Payment] App环境 - 使用模拟支付');
|
||
setTimeout(() => {
|
||
console.log('[Payment] 模拟支付宝支付成功');
|
||
resolve({
|
||
success: true,
|
||
orderNo: orderData.orderNo,
|
||
payTime: new Date().toISOString(),
|
||
message: '支付成功(模拟)'
|
||
});
|
||
}, 1000); // 模拟1秒支付延迟
|
||
|
||
// 真实支付代码(暂时注释)
|
||
// uni.requestPayment({
|
||
// provider: 'alipay',
|
||
// orderInfo: orderData.orderInfo || orderData.tradeNo || '',
|
||
// success: (res) => {
|
||
// console.log('[Payment] App支付宝支付成功:', res);
|
||
// resolve({
|
||
// success: true,
|
||
// orderNo: orderData.orderNo,
|
||
// payTime: new Date().toISOString(),
|
||
// message: '支付成功'
|
||
// });
|
||
// },
|
||
// fail: (err) => {
|
||
// console.error('[Payment] App支付宝支付失败:', err);
|
||
// if (err.errMsg && err.errMsg.includes('cancel')) {
|
||
// reject(new Error('用户取消支付'));
|
||
// } else {
|
||
// reject(new Error(err.errMsg || '支付失败'));
|
||
// }
|
||
// }
|
||
// });
|
||
// #endif
|
||
|
||
// #if !defined(MP-ALIPAY) && !defined(APP-PLUS)
|
||
reject(new Error('当前环境不支持支付宝支付'));
|
||
// #endif
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 执行支付(真实版本)
|
||
* @param {string} serviceType - 服务类型
|
||
* @param {number} userId - 用户ID
|
||
* @returns {Promise}
|
||
*/
|
||
export async function executePayment(serviceType, userId) {
|
||
// 优先校验后端是否已具备使用资格(免费次数/补发/已支付)
|
||
try {
|
||
const eligible = await checkBackendEligibility(serviceType);
|
||
if (eligible) {
|
||
console.log('[Payment] 后端校验通过,跳过支付:', serviceType);
|
||
return {
|
||
success: true,
|
||
message: '已具备使用资格,无需支付',
|
||
skipPayment: true
|
||
};
|
||
}
|
||
} catch (e) {
|
||
console.error('[Payment] 后端资格校验失败(忽略继续走支付):', e);
|
||
}
|
||
|
||
// 先获取服务价格
|
||
await fetchServicePrices();
|
||
const service = Object.values(SERVICE_TYPES).find(s => s.type === serviceType);
|
||
|
||
// 如果价格为0,直接返回成功,跳过支付
|
||
if (service && service.price === 0) {
|
||
console.log('[Payment] 服务价格为0,跳过支付流程');
|
||
const orderNo = generateOrderNo();
|
||
const result = {
|
||
success: true,
|
||
orderNo: orderNo,
|
||
payTime: new Date().toISOString(),
|
||
message: '免费服务,无需支付',
|
||
free: true
|
||
};
|
||
|
||
uni.showToast({
|
||
title: '免费使用',
|
||
icon: 'success',
|
||
duration: 1500
|
||
});
|
||
|
||
console.log('[Payment] 免费服务,直接返回成功:', result);
|
||
return result;
|
||
}
|
||
|
||
// #ifdef APP-PLUS
|
||
// APP环境:使用真实支付流程
|
||
try {
|
||
uni.showLoading({
|
||
title: '创建订单中...',
|
||
mask: true
|
||
});
|
||
|
||
const orderData = await createBackendOrder(serviceType, userId);
|
||
|
||
uni.showLoading({
|
||
title: '正在调起支付...',
|
||
mask: true
|
||
});
|
||
|
||
const channel = getPaymentChannel();
|
||
let paymentResult;
|
||
if (channel === 'wechat') {
|
||
paymentResult = await executeWechatPayment(orderData);
|
||
} else if (channel === 'alipay') {
|
||
paymentResult = await executeAlipayPayment(orderData);
|
||
} else {
|
||
throw new Error('不支持的支付渠道');
|
||
}
|
||
|
||
uni.hideLoading();
|
||
return paymentResult;
|
||
} catch (error) {
|
||
uni.hideLoading();
|
||
throw error;
|
||
}
|
||
// #endif
|
||
|
||
// #ifndef APP-PLUS
|
||
// 小程序环境:使用真实支付流程
|
||
try {
|
||
uni.showLoading({
|
||
title: '创建订单中...',
|
||
mask: true
|
||
});
|
||
|
||
// 1. 创建后端订单
|
||
const orderData = await createBackendOrder(serviceType, userId);
|
||
|
||
uni.showLoading({
|
||
title: '正在调起支付...',
|
||
mask: true
|
||
});
|
||
|
||
// 2. 根据渠道执行支付
|
||
const channel = getPaymentChannel();
|
||
let paymentResult;
|
||
|
||
if (channel === 'wechat') {
|
||
paymentResult = await executeWechatPayment(orderData);
|
||
} else if (channel === 'alipay') {
|
||
paymentResult = await executeAlipayPayment(orderData);
|
||
} else {
|
||
throw new Error('不支持的支付渠道');
|
||
}
|
||
|
||
uni.hideLoading();
|
||
return paymentResult;
|
||
} catch (error) {
|
||
uni.hideLoading();
|
||
throw error;
|
||
}
|
||
// #endif
|
||
}
|
||
|
||
/**
|
||
* 检查订单支付状态(真实版本)
|
||
* @param {string} orderNo - 订单号
|
||
* @returns {Promise}
|
||
*/
|
||
export function checkPaymentStatus(orderNo) {
|
||
return new Promise((resolve, reject) => {
|
||
uni.request({
|
||
url: `${API_BASE}/api/pay/orders/${orderNo}`,
|
||
method: 'GET',
|
||
header: {
|
||
'Authorization': `Bearer ${uni.getStorageSync('token') || ''}`
|
||
},
|
||
success: (res) => {
|
||
if (res.statusCode === 200 && res.data) {
|
||
console.log('[Payment] 查询支付状态成功:', res.data);
|
||
resolve({
|
||
success: true,
|
||
orderNo: res.data.orderNo,
|
||
status: res.data.status,
|
||
payTime: res.data.paidAt
|
||
});
|
||
} else {
|
||
reject(new Error('查询订单状态失败'));
|
||
}
|
||
},
|
||
fail: (err) => {
|
||
console.error('[Payment] 查询订单状态失败:', err);
|
||
reject(err);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 完整的支付流程
|
||
* @param {string} serviceType - 服务类型
|
||
* @param {object} options - 配置选项
|
||
* @returns {Promise}
|
||
*/
|
||
export async function processPayment(serviceType, options = {}) {
|
||
const {
|
||
extraData = {},
|
||
onOrderCreated = null,
|
||
onPaymentSuccess = null,
|
||
onPaymentFailed = null
|
||
} = options;
|
||
|
||
try {
|
||
// 1. 创建订单
|
||
console.log('[Payment] 创建订单:', serviceType);
|
||
const orderData = await createOrder(serviceType, extraData);
|
||
|
||
if (onOrderCreated) {
|
||
onOrderCreated(orderData);
|
||
}
|
||
|
||
// 2. 执行支付
|
||
console.log('[Payment] 执行支付:', serviceType);
|
||
const userId = getUserInfo().userId;
|
||
const paymentResult = await executePayment(serviceType, userId);
|
||
|
||
// 3. 支付成功
|
||
console.log('[Payment] 支付成功:', paymentResult);
|
||
|
||
if (onPaymentSuccess) {
|
||
onPaymentSuccess(paymentResult);
|
||
}
|
||
|
||
return {
|
||
success: true,
|
||
orderNo: orderData.orderNo,
|
||
...paymentResult
|
||
};
|
||
|
||
} catch (error) {
|
||
console.error('[Payment] 支付流程失败:', error);
|
||
|
||
if (onPaymentFailed) {
|
||
onPaymentFailed(error);
|
||
}
|
||
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 显示支付弹窗并处理支付
|
||
* 这是一个高级封装,配合PaymentModal组件使用
|
||
* @param {object} vm - Vue实例
|
||
* @param {string} serviceType - 服务类型
|
||
* @param {function} onSuccess - 支付成功回调
|
||
* @param {function} onFailed - 支付失败回调
|
||
*/
|
||
export async function showPaymentModal(vm, serviceType, onSuccess, onFailed) {
|
||
// 先获取最新价格
|
||
try {
|
||
await fetchServicePrices();
|
||
} catch (e) {
|
||
// ignore
|
||
}
|
||
|
||
// 优先校验后端是否已具备使用资格(免费次数/补发/已支付),避免弹支付窗
|
||
try {
|
||
const eligible = await checkBackendEligibility(serviceType);
|
||
if (eligible) {
|
||
console.log('[Payment] 后端校验通过,跳过支付弹窗:', serviceType);
|
||
try {
|
||
const preview = await fetchUsagePreview(serviceType);
|
||
if (preview && preview.consumeType === 'FREE_TRIAL') {
|
||
uni.showToast({
|
||
title: '本次使用免费次数',
|
||
icon: 'none',
|
||
duration: 1500
|
||
});
|
||
}
|
||
} catch (e) {
|
||
// ignore
|
||
}
|
||
if (onSuccess) {
|
||
onSuccess({ success: true, skipPayment: true, message: '已具备使用资格,无需支付' });
|
||
}
|
||
return;
|
||
}
|
||
} catch (e) {
|
||
console.error('[Payment] 后端资格校验失败(忽略继续弹窗):', e);
|
||
}
|
||
|
||
// 获取服务配置
|
||
const service = Object.values(SERVICE_TYPES).find(s => s.type === serviceType);
|
||
if (!service) {
|
||
const error = new Error('无效的服务类型');
|
||
console.error('[Payment] 显示支付弹窗失败:', error);
|
||
uni.showToast({
|
||
title: '无效的服务类型',
|
||
icon: 'none'
|
||
});
|
||
if (onFailed) {
|
||
onFailed(error);
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 如果价格为0,直接执行支付(跳过弹窗)
|
||
if (service.price === 0) {
|
||
console.log('[Payment] 服务价格为0,跳过支付弹窗,直接执行');
|
||
try {
|
||
const userId = getUserInfo().userId;
|
||
const result = await executePayment(serviceType, userId);
|
||
|
||
// 调用成功回调
|
||
if (onSuccess) {
|
||
onSuccess(result);
|
||
}
|
||
} catch (error) {
|
||
console.error('[Payment] 免费服务执行失败:', error);
|
||
if (onFailed) {
|
||
onFailed(error);
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 价格不为0,显示支付弹窗
|
||
createOrder(serviceType)
|
||
.then(orderData => {
|
||
// 设置弹窗数据
|
||
vm.paymentModalData = {
|
||
show: true,
|
||
serviceType: service.type,
|
||
serviceName: service.name,
|
||
serviceDesc: service.desc,
|
||
price: service.price,
|
||
orderNo: orderData.orderNo
|
||
};
|
||
|
||
// 保存回调
|
||
vm._paymentOnSuccess = onSuccess;
|
||
vm._paymentOnFailed = onFailed;
|
||
})
|
||
.catch(error => {
|
||
console.error('[Payment] 创建订单失败:', error);
|
||
uni.showToast({
|
||
title: error.message || '创建订单失败',
|
||
icon: 'none'
|
||
});
|
||
if (onFailed) {
|
||
onFailed(error);
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 处理支付确认(在PaymentModal的confirm事件中调用)
|
||
* @param {object} vm - Vue实例
|
||
* @param {object} paymentData - 支付数据
|
||
*/
|
||
export async function handlePaymentConfirm(vm, paymentData) {
|
||
try {
|
||
// 执行支付
|
||
const userId = getUserInfo().userId;
|
||
const result = await executePayment(paymentData.serviceType, userId);
|
||
|
||
// 通知PaymentModal支付成功
|
||
if (vm.$refs.paymentModal) {
|
||
vm.$refs.paymentModal.paymentSuccess();
|
||
}
|
||
|
||
// 关闭弹窗
|
||
vm.paymentModalData.show = false;
|
||
|
||
// 显示成功提示
|
||
uni.showToast({
|
||
title: '支付成功',
|
||
icon: 'success'
|
||
});
|
||
|
||
// 调用成功回调
|
||
if (vm._paymentOnSuccess) {
|
||
vm._paymentOnSuccess(result);
|
||
}
|
||
|
||
if (vm._paymentResolve) {
|
||
vm._paymentResolve(result);
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('[Payment] 支付失败:', error);
|
||
|
||
// 通知PaymentModal支付失败
|
||
if (vm.$refs.paymentModal) {
|
||
vm.$refs.paymentModal.paymentFailed(error);
|
||
}
|
||
|
||
// 显示错误提示
|
||
uni.showToast({
|
||
title: error.message || '支付失败',
|
||
icon: 'none'
|
||
});
|
||
|
||
// 调用失败回调
|
||
if (vm._paymentOnFailed) {
|
||
vm._paymentOnFailed(error);
|
||
}
|
||
|
||
if (vm._paymentReject) {
|
||
vm._paymentReject(error);
|
||
}
|
||
}
|
||
}
|
||
|
||
export default {
|
||
SERVICE_TYPES,
|
||
createOrder,
|
||
executePayment,
|
||
checkPaymentStatus,
|
||
processPayment,
|
||
showPaymentModal,
|
||
handlePaymentConfirm
|
||
};
|