import { prisma } from '../../models'; import crypto from 'crypto'; import https from 'https'; // 懒加载支付 SDK(避免 Node.js ESM 兼容性问题) let AlipaySdk: any = null; let WeChatPay: any = null; let sdkInitialized = false; // ==================== 微信 JSAPI 支付相关缓存 ==================== let cachedAccessToken: string | null = null; let accessTokenExpireAt: number = 0; let cachedJsapiTicket: string | null = null; let jsapiTicketExpireAt: number = 0; // 初始化支付 SDK async function initPaymentSdks() { if (sdkInitialized) return; sdkInitialized = true; try { const sdk = await import('alipay-sdk'); AlipaySdk = sdk.AlipaySdk; console.log('[Payment] 支付宝 SDK 加载成功'); } catch (e) { console.warn('[Payment] 支付宝 SDK 加载失败:', e); } try { // 直接 require,因为 wechatpay-node-v3 是 CommonJS 模块 WeChatPay = require('wechatpay-node-v3'); console.log('[Payment] 微信支付 SDK 加载成功'); } catch (e) { console.warn('[Payment] 微信支付 SDK 加载失败:', e); } } // 生成订单号 export function generateOrderNo(): string { const now = new Date(); const dateStr = now.toISOString().slice(0, 10).replace(/-/g, ''); const random = Math.random().toString(36).substring(2, 8).toUpperCase(); return `PAY${dateStr}${random}`; } // Token 包配置 const TOKEN_PACK = { minutesPerPack: 100, packUnitPrice: 0.048, get packPrice() { return Math.round(this.minutesPerPack * this.packUnitPrice * 100) / 100; }, get packQuotaValue() { return Math.round(this.minutesPerPack * 0.032 * 100) / 100; }, }; // 创建 Token 包支付订单 export async function createTokenPackOrder( userId: number, quantity: number, paymentMethod: 'alipay' | 'wechat' | 'mock', returnUrl?: string ): Promise<{ orderNo: string; amount: number; planName: string; paymentUrl?: string; qrcode?: string; }> { await initPaymentSdks(); if (quantity < 1) { throw new Error('购买数量至少为1'); } const amount = Math.round(TOKEN_PACK.packPrice * quantity * 100) / 100; const orderNo = generateOrderNo(); const subject = `${quantity}包积分包(共${TOKEN_PACK.minutesPerPack * quantity}分钟)`; // 创建订单记录(productType='token-pack',无 planId) await prisma.order.create({ data: { userId, orderNo, planId: null, productType: 'token-pack', amount, status: 'pending', paymentMethod, } }); // 生成支付信息 let paymentUrl: string | undefined; let qrcode: string | undefined; try { if (paymentMethod === 'alipay') { const alipayResult = await generateAlipayPayment(orderNo, amount, subject, returnUrl); paymentUrl = alipayResult.paymentUrl; qrcode = alipayResult.qrcode; } else if (paymentMethod === 'wechat') { const wechatResult = await generateWechatPayment(orderNo, amount, subject); qrcode = wechatResult.qrcode; paymentUrl = wechatResult.h5Url; } } catch (error) { console.error('[Payment] 支付链接生成失败:', error); throw error; } return { orderNo, amount, planName: subject, paymentUrl, qrcode }; } // 初始化支付宝 SDK let alipaySdk: any | null = null; function getAlipaySdk(): any | null { if (!AlipaySdk) { console.warn('[Alipay] 支付宝 SDK 未加载'); return null; } const appId = process.env.ALIPAY_APP_ID || ''; const privateKey = process.env.ALIPAY_PRIVATE_KEY || ''; const alipayPublicKey = process.env.ALIPAY_PUBLIC_KEY || ''; if (!appId || !privateKey || !alipayPublicKey) { console.warn('[Alipay] 未配置支付宝参数', { appId: !!appId, hasPrivateKey: !!privateKey, hasPublicKey: !!alipayPublicKey }); return null; } if (!alipaySdk) { // keyType 必须指定为 PKCS8,因为 ALIPAY_PRIVATE_KEY 是 PKCS#8 格式(以 MIIEvQIBADAN 开头) // 如果不指定 keyType,SDK 默认按 PKCS1 处理,会把 PEM 头写为 RSA PRIVATE KEY // 导致 Node.js crypto 模块解析 PKCS8 数据时出错,从而签名失败 alipaySdk = new AlipaySdk({ appId, privateKey, alipayPublicKey, gateway: 'https://openapi.alipay.com/gateway.do', signType: 'RSA2', keyType: 'PKCS8', }); } return alipaySdk; } // 初始化微信支付 SDK let wechatPay: any | null = null; function getWechatPay(): any | null { if (!WeChatPay) { console.warn('[WeChatPay] 微信支付 SDK 未加载'); return null; } const appId = process.env.WECHAT_APP_ID; const mchId = process.env.WECHAT_MCH_ID; const serialNo = process.env.WECHAT_SERIAL_NO; const apiv3Key = process.env.WECHAT_APIV3_KEY; // 读取私钥和公钥文件(dotenv 无法正确解析多行 PEM 密钥) const fs = require('fs'); const path = require('path'); const certDir = path.join(__dirname, '../../../cert/wx'); const privateKeyPath = path.join(certDir, 'apiclient_key.pem'); const publicKeyPath = path.join(certDir, 'pub_key.pem'); let privateKey = ''; let publicKey = ''; try { privateKey = fs.readFileSync(privateKeyPath, 'utf8'); publicKey = fs.readFileSync(publicKeyPath, 'utf8'); } catch (e) { console.warn('[WeChatPay] 未找到证书文件:', e.message); } if (!appId || !mchId || !privateKey || !serialNo || !apiv3Key) { console.warn('[WeChatPay] 未配置微信支付参数'); return null; } if (!wechatPay) { // 从私钥导出公钥(dotenv无法正确解析多行PEM,且提供的pub_key.pem与私钥不匹配) const privKeyObj = crypto.createPrivateKey(privateKey); const pubKeyFromPriv = crypto.createPublicKey(privKeyObj); const publicKeyDer = pubKeyFromPriv.export({ type: 'spki', format: 'der' }); wechatPay = new WeChatPay({ appid: appId, mchid: mchId, serial_no: serialNo, publicKey: publicKeyDer, privateKey: Buffer.from(privateKey), key: apiv3Key, }); } return wechatPay; } // 创建支付订单 export async function createPaymentOrder( userId: number, planId: number, paymentMethod: 'alipay' | 'wechat' | 'mock', period: 'monthly' | 'yearly' = 'monthly', returnUrl?: string ): Promise<{ orderNo: string; amount: number; planName: string; paymentUrl?: string; qrcode?: string; }> { // 确保 SDK 已加载 await initPaymentSdks(); // 获取套餐信息 const plan = await prisma.subscriptionPlan.findUnique({ where: { id: planId } }); if (!plan) { throw new Error('套餐不存在'); } // 生成订单号 const orderNo = generateOrderNo(); // 根据周期选择价格 const amount = period === 'yearly' ? Number(plan.priceYearly) : Number(plan.priceMonthly); // 创建订单记录 await prisma.order.create({ data: { userId, orderNo, planId, productType: period, amount, status: 'pending', paymentMethod, } }); // 生成支付信息 let paymentUrl: string | undefined; let qrcode: string | undefined; try { if (paymentMethod === 'alipay') { const alipayResult = await generateAlipayPayment(orderNo, amount, plan.name, returnUrl); paymentUrl = alipayResult.paymentUrl; qrcode = alipayResult.qrcode; } else if (paymentMethod === 'wechat') { const wechatResult = await generateWechatPayment(orderNo, amount, plan.name); qrcode = wechatResult.qrcode; paymentUrl = wechatResult.h5Url; } } catch (error) { console.error('[Payment] 支付链接生成失败:', error); throw error; } return { orderNo, amount, planName: plan.name, paymentUrl, qrcode }; } // 生成支付宝支付链接 async function generateAlipayPayment( orderNo: string, amount: number, subject: string, returnUrl?: string ): Promise<{ paymentUrl?: string; qrcode?: string }> { const sdk = getAlipaySdk(); // 默认回调地址 const defaultReturnUrl = process.env.ALIPAY_RETURN_URL || `${process.env.BASE_URL || 'https://your-domain.com'}/#/pages/payment-result/index`; const finalReturnUrl = returnUrl || defaultReturnUrl; if (!sdk) { console.warn('[Alipay] 未配置支付宝,返回模拟支付链接'); return { paymentUrl: `https://openapi.alipay.com/gateway.do?out_trade_no=${orderNo}&total_amount=${amount}&subject=${encodeURIComponent(subject)}` }; } // 使用手机网站支付 API(WAP支付) // sdkExecute 返回完整查询字符串(含 biz_content + sign),拼上网关地址即可 // pageExec 生成的 POST 表单把 biz_content 放在隐藏 input 里,提取 action URL 会丢失它 try { const notifyUrl = process.env.ALIPAY_NOTIFY_URL || ''; const queryString = sdk.sdkExec('alipay.trade.wap.pay', { bizContent: { out_trade_no: orderNo, total_amount: String(amount), subject: subject, product_code: 'QUICK_WAP_WAY', }, returnUrl: finalReturnUrl, notifyUrl, }); const paymentUrl = `https://openapi.alipay.com/gateway.do?${queryString}`; return { paymentUrl, }; } catch (error) { console.error('[Alipay] 支付链接生成失败:', error); throw new Error('支付宝支付链接生成失败'); } } // 生成微信支付二维码链接 async function generateWechatPayment( orderNo: string, amount: number, subject: string ): Promise<{ qrcode?: string; h5Url?: string }> { const wechat = getWechatPay(); if (!wechat) { console.warn('[WeChatPay] 未配置微信支付,返回模拟支付链接'); return { qrcode: `https://api.mch.weixin.qq.com/qrcode/${orderNo}`, h5Url: `weixin://wxpay/bizpayurl?pr=${encodeURIComponent(orderNo)}` }; } try { console.log('[WeChatPay] 开始创建 NATIVE 支付订单:', { orderNo, amount, subject }); // 使用 NATIVE 支付(扫码支付) const result: any = await wechat.transactions_native({ description: subject, out_trade_no: orderNo, amount: { total: Math.round(amount * 100), // 转换为分 currency: 'CNY', }, notify_url: process.env.WECHAT_NOTIFY_URL || `${process.env.BASE_URL || 'https://your-domain.com'}/api/payment/wechat/notify`, }); console.log('[WeChatPay] 支付接口返回, result:', JSON.stringify(result)); // 检查返回状态(SDK 不会reject错误响应,而是返回包含error的对象) if (result.status !== 200 && result.status !== '200') { const errorMsg = JSON.stringify(result); console.error('[WeChatPay] 支付失败:', errorMsg); throw new Error(`微信支付接口返回错误: ${errorMsg}`); } console.log('[WeChatPay] 支付二维码生成成功, code_url:', result.data?.code_url || result.code_url); // 返回二维码链接(前端自行生成二维码图片) return { qrcode: result.data?.code_url || result.code_url, }; } catch (error: any) { const detail = error.message || JSON.stringify(error); console.error('[WeChatPay] 支付二维码生成失败:', detail); throw new Error(`微信支付暂不可用(${detail}),请稍后重试或使用其他支付方式`); } } // 生成支付宝扫码支付二维码 export async function generateAlipayQrcode( orderNo: string, amount: number, subject: string ): Promise { const sdk = getAlipaySdk(); if (!sdk) { console.warn('[Alipay] 未配置支付宝,返回模拟二维码链接'); return `https://qr.alipay.com/${orderNo}`; } try { // 使用当面付预下单 API(v3 curl 替代已废弃的 exec) const result = await sdk.curl('POST', '/v3/alipay/trade/precreate', { body: { out_trade_no: orderNo, total_amount: String(amount), subject: subject, }, timeout: 10000, }); // 解析返回的二维码链接 const response = result as any; if (response.qr_code) { return response.qr_code; } return result as string; } catch (error) { console.error('[Alipay] 扫码支付二维码生成失败:', error); throw new Error('支付宝扫码支付生成失败'); } } // 验证支付宝回调签名 export function verifyAlipaySign(params: Record): boolean { const sdk = getAlipaySdk(); if (!sdk) { console.warn('[Alipay] 未配置支付宝,跳过签名验证'); return true; } try { // checkNotifySignV2 不会对参数值做 decode,避免特殊字符导致验签失败 const signVerified = sdk.checkNotifySignV2(params); console.log('[Alipay] 签名验证结果:', signVerified); return signVerified; } catch (error) { console.error('[Alipay] 签名验证失败:', error); return false; } } // 处理支付回调 export async function handlePaymentCallback( orderNo: string, paymentId: string, status: 'success' | 'failed' ) { const order = await prisma.order.findFirst({ where: { orderNo } }); if (!order) { throw new Error('订单不存在'); } if (order.status === 'paid') { // 订单已经支付成功,直接返回 return { success: true, message: '订单已支付' }; } if (status === 'failed') { await prisma.order.update({ where: { id: order.id }, data: { status: 'failed', paymentId, updatedAt: new Date() } }); return { success: false, message: '支付失败' }; } // 支付成功 await prisma.order.update({ where: { id: order.id }, data: { status: 'paid', paymentId, paidAt: new Date(), updatedAt: new Date() } }); // 根据产品类型处理 if (order.productType === 'token-pack') { // 积分包:增加用户 costLimit await processTokenPackOrder(order); } else if (order.planId) { // 套餐订阅:激活订阅 await activateSubscription(order.userId, order.planId, order.productType); } return { success: true, message: '支付成功' }; } // 处理积分包订单(支付成功后给用户加额度) async function processTokenPackOrder(order: any) { const quantity = Math.round(Number(order.amount) / TOKEN_PACK.packPrice); const addedQuota = Math.round(TOKEN_PACK.packQuotaValue * quantity * 100) / 100; const addedTokens = TOKEN_PACK.minutesPerPack * quantity * 150; // 150 tokens/分钟 // 原子更新 costLimit await prisma.user.update({ where: { id: order.userId }, data: { costLimit: { increment: addedQuota } }, }); // 同步增加 TokenBalance(积分包部分) await prisma.tokenBalance.upsert({ where: { userId: order.userId }, create: { userId: order.userId, totalTokens: addedTokens, packTokens: addedTokens }, update: { totalTokens: { increment: addedTokens }, packTokens: { increment: addedTokens } }, }); // 记录购买历史 await prisma.tokenPackPurchase.create({ data: { userId: order.userId, packMinutes: TOKEN_PACK.minutesPerPack, quantity, unitPrice: TOKEN_PACK.packUnitPrice, totalAmount: Number(order.amount), addedQuota, }, }); console.log(`[TokenPack] 用户#${order.userId} 支付成功,购买 ${quantity} 包,增加额度 ¥${addedQuota},增加积分 ${addedTokens}`); } // 激活订阅 export async function activateSubscription(userId: number, planId: number, productType: string = 'monthly') { const plan = await prisma.subscriptionPlan.findUnique({ where: { id: planId } }); if (!plan) { throw new Error('套餐不存在'); } const now = new Date(); // 根据订单的产品类型确定有效天数 const days = productType === 'yearly' ? 365 : 30; const endDate = new Date(now.getTime() + days * 24 * 60 * 60 * 1000); const existingSubscription = await prisma.subscription.findFirst({ where: { userId, status: 'active', endDate: { gt: now } } }); let subscriptionEndDate = endDate; if (existingSubscription) { const isSamePlan = existingSubscription.planId === planId; if (isSamePlan) { // 续费:同套餐叠加天数 subscriptionEndDate = new Date(existingSubscription.endDate.getTime() + days * 24 * 60 * 60 * 1000); await prisma.subscription.update({ where: { id: existingSubscription.id }, data: { endDate: subscriptionEndDate, updatedAt: now } }); } else { // 升级/降级:结束旧订阅,新订阅从今天起算 await prisma.subscription.update({ where: { id: existingSubscription.id }, data: { status: 'upgraded', updatedAt: now } }); await prisma.subscription.create({ data: { userId, planId, startDate: now, endDate, status: 'active', autoRenew: false } }); } } else { await prisma.subscription.create({ data: { userId, planId, startDate: now, endDate: subscriptionEndDate, status: 'active', autoRenew: false } }); } // 更新用户会员等级 await prisma.user.update({ where: { id: userId }, data: { memberLevel: plan.level, memberExpireAt: subscriptionEndDate } }); // 更新Token余额 const monthlyTokens = plan.monthlyTokens; const currentBalance = await prisma.tokenBalance.findUnique({ where: { userId } }); if (monthlyTokens !== -1) { // 订阅激活:保留积分包数据,重置订阅部分 const existingPackTokens = currentBalance?.packTokens || 0; const existingUsedPackTokens = currentBalance?.usedPackTokens || 0; if (currentBalance) { await prisma.tokenBalance.update({ where: { userId }, data: { totalTokens: monthlyTokens + existingPackTokens, usedTokens: existingUsedPackTokens, // 只保留积分包已用 packTokens: existingPackTokens, usedPackTokens: existingUsedPackTokens, resetDate: new Date(now.getFullYear(), now.getMonth() + 1, 1) } }); } else { await prisma.tokenBalance.create({ data: { userId, totalTokens: monthlyTokens, usedTokens: 0, resetDate: new Date(now.getFullYear(), now.getMonth() + 1, 1) } }); } } else { // VIP无限额度:保留积分包数据 const existingPackTokens = currentBalance?.packTokens || 0; const existingUsedPackTokens = currentBalance?.usedPackTokens || 0; if (currentBalance) { await prisma.tokenBalance.update({ where: { userId }, data: { totalTokens: -1, usedTokens: 0, packTokens: existingPackTokens, usedPackTokens: existingUsedPackTokens, resetDate: null } }); } else { await prisma.tokenBalance.create({ data: { userId, totalTokens: -1, usedTokens: 0 } }); } } } // 模拟支付成功 export async function mockPaymentSuccess(orderNo: string, userId: number) { return handlePaymentCallback(orderNo, 'MOCK_' + Date.now(), 'success'); } // 获取订单列表 export async function getOrderList(userId: number, page: number = 1, pageSize: number = 20) { const where = { userId }; const total = await prisma.order.count({ where }); const list = await prisma.order.findMany({ where, include: { plan: true }, orderBy: { createdAt: 'desc' }, skip: (page - 1) * pageSize, take: pageSize }); return { list: list.map(order => ({ ...order, amount: Number(order.amount), planName: order.plan?.name || '' })), total, page, pageSize, totalPages: Math.ceil(total / pageSize) }; } // 获取订单详情 export async function getOrderDetail(orderNo: string, userId: number) { const order = await prisma.order.findFirst({ where: { orderNo, userId }, include: { plan: true } }); if (!order) { throw new Error('订单不存在'); } return { ...order, amount: Number(order.amount), planName: order.plan?.name || '' }; } // 查询微信支付订单状态 export async function queryWechatOrder(outTradeNo: string) { const wechat = getWechatPay(); if (!wechat) { return null; } try { const result = await wechat.query({ out_trade_no: outTradeNo }); return result; } catch (error) { console.error('[WeChatPay] 查询订单失败:', error); return null; } } // 解密 WeChat V3 回调通知 export function decryptWechatNotify(body: any): any | null { const wechat = getWechatPay(); if (!wechat) return null; const { resource } = body || {}; if (!resource || !resource.ciphertext) { console.error('[WeChatPay] 回调格式无效,缺少 resource.ciphertext'); return null; } try { const decrypted = wechat.decipher_gcm( resource.ciphertext, resource.associated_data || '', resource.nonce, wechat.key // APIv3 key ); console.log('[WeChatPay] 回调解密成功:', JSON.stringify(decrypted)); return decrypted; } catch (e: any) { console.error('[WeChatPay] 回调解密失败:', e.message); return null; } } // 查询订单并同步微信支付状态(主动查询 + 自动更新) export async function getOrderDetailWithSync(orderNo: string, userId: number) { const order = await prisma.order.findFirst({ where: { orderNo, userId }, include: { plan: true } }); if (!order) { throw new Error('订单不存在'); } // 如果订单是 pending 状态且为微信支付,主动查询微信确认状态 if (order.status === 'pending' && order.paymentMethod === 'wechat') { console.log('[Payment] 订单 pending,主动查询微信支付状态:', orderNo); const wechatResult = await queryWechatOrder(orderNo); if (wechatResult?.data?.trade_state === 'SUCCESS') { console.log('[Payment] 微信支付已成功,自动更新订单状态:', orderNo); const transactionId = wechatResult.data.transaction_id || ''; await handlePaymentCallback(orderNo, transactionId, 'success'); // 重新查询更新后的订单 const updatedOrder = await prisma.order.findFirst({ where: { orderNo, userId }, include: { plan: true } }); if (updatedOrder) { return { ...updatedOrder, amount: Number(updatedOrder.amount), planName: updatedOrder.plan?.name || '' }; } } } return { ...order, amount: Number(order.amount), planName: order.plan?.name || '' }; } // ==================== 微信 JSAPI 支付(公众号支付) ==================== // Node.js https 简单 GET 请求 function httpsGet(url: string): Promise { return new Promise((resolve, reject) => { https.get(url, (res) => { let data = ''; res.on('data', (chunk) => data += chunk); res.on('end', () => { try { resolve(JSON.parse(data)); } catch (e) { resolve(data); } }); }).on('error', reject); }); } // 获取微信公众号 access_token(带缓存) export async function getWechatAccessToken(): Promise { const now = Date.now(); if (cachedAccessToken && now < accessTokenExpireAt) { return cachedAccessToken; } const appId = process.env.WECHAT_APP_ID; const appSecret = process.env.WECHAT_APP_SECRET; if (!appId || !appSecret) { console.warn('[WeChat JSAPI] 未配置 WECHAT_APP_ID 或 WECHAT_APP_SECRET'); return null; } try { const data = await httpsGet( `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appId}&secret=${appSecret}` ); if (data.errcode) { console.error('[WeChat JSAPI] 获取 access_token 失败:', data); return null; } cachedAccessToken = data.access_token; // 提前 5 分钟过期 accessTokenExpireAt = now + (data.expires_in - 300) * 1000; console.log('[WeChat JSAPI] access_token 刷新成功, 过期时间:', new Date(accessTokenExpireAt).toISOString()); return cachedAccessToken; } catch (e) { console.error('[WeChat JSAPI] 获取 access_token 异常:', e); return null; } } // 获取微信 JS-SDK ticket(带缓存) export async function getWechatJsapiTicket(): Promise { const now = Date.now(); if (cachedJsapiTicket && now < jsapiTicketExpireAt) { return cachedJsapiTicket; } const accessToken = await getWechatAccessToken(); if (!accessToken) return null; try { const data = await httpsGet( `https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=${accessToken}&type=jsapi` ); if (data.errcode !== 0) { console.error('[WeChat JSAPI] 获取 jsapi_ticket 失败:', data); return null; } cachedJsapiTicket = data.ticket; // 提前 5 分钟过期 jsapiTicketExpireAt = now + (data.expires_in - 300) * 1000; console.log('[WeChat JSAPI] jsapi_ticket 刷新成功'); return cachedJsapiTicket; } catch (e) { console.error('[WeChat JSAPI] 获取 jsapi_ticket 异常:', e); return null; } } // 生成微信 JS-SDK 签名配置 export async function getWechatJsapiConfig(url: string): Promise<{ appId: string; timestamp: string; nonceStr: string; signature: string; } | null> { const ticket = await getWechatJsapiTicket(); if (!ticket) return null; const nonceStr = crypto.randomBytes(16).toString('hex'); const timestamp = Math.floor(Date.now() / 1000).toString(); const signStr = `jsapi_ticket=${ticket}&noncestr=${nonceStr}×tamp=${timestamp}&url=${url}`; const signature = crypto.createHash('sha1').update(signStr).digest('hex'); return { appId: process.env.WECHAT_APP_ID || '', timestamp, nonceStr, signature, }; } // 通过 OAuth code 获取用户 openid export async function getWechatOpenid(code: string): Promise { const appId = process.env.WECHAT_APP_ID; const appSecret = process.env.WECHAT_APP_SECRET; if (!appId || !appSecret) { console.warn('[WeChat OAuth] 未配置 WECHAT_APP_ID 或 WECHAT_APP_SECRET'); return null; } try { const data = await httpsGet( `https://api.weixin.qq.com/sns/oauth2/access_token?appid=${appId}&secret=${appSecret}&code=${code}&grant_type=authorization_code` ); if (data.errcode || !data.openid) { console.error('[WeChat OAuth] 获取 openid 失败:', data); return null; } console.log('[WeChat OAuth] 获取 openid 成功'); return data.openid; } catch (e) { console.error('[WeChat OAuth] 获取 openid 异常:', e); return null; } } // 生成微信 JSAPI 支付(公众号内支付) // 注意:SDK的transactions_jsapi已自动处理签名,result.data直接可用于wx.chooseWXPay export async function generateWechatJsapiPayment( orderNo: string, amount: number, subject: string, openid: string ): Promise<{ appId: string; timeStamp: string; nonceStr: string; package: string; signType: string; paySign: string; } | null> { const wechat = getWechatPay(); if (!wechat) { console.warn('[WeChat JSAPI] 未配置微信支付'); return null; } try { console.log('[WeChat JSAPI] 开始创建 JSAPI 支付订单:', { orderNo, amount, subject, openid }); // SDK 的 transactions_jsapi 会自动: // 1. 添加 appid 和 mchid // 2. 调用 /v3/pay/transactions/jsapi // 3. 对 prepay_id 进行 RSA 签名 // 4. 返回可直接传给 wx.chooseWXPay 的参数 const result: any = await wechat.transactions_jsapi({ description: subject, out_trade_no: orderNo, amount: { total: Math.round(amount * 100), // 转换为分 currency: 'CNY', }, payer: { openid, }, notify_url: process.env.WECHAT_NOTIFY_URL || `${process.env.BASE_URL || 'https://your-domain.com'}/api/payment/wechat/notify`, }); console.log('[WeChat JSAPI] 支付接口返回 status:', result.status); // 检查返回状态 if (result.status !== 200 && result.status !== '200') { const errorMsg = JSON.stringify(result); console.error('[WeChat JSAPI] 支付失败:', errorMsg); throw new Error(`微信支付接口返回错误: ${errorMsg}`); } // result.data 已经包含完整的 wx.chooseWXPay 参数: // { appId, timeStamp, nonceStr, package, signType, paySign } if (!result.data || !result.data.package) { console.error('[WeChat JSAPI] 返回数据异常:', JSON.stringify(result)); throw new Error('微信支付返回数据异常,未获取到支付参数'); } console.log('[WeChat JSAPI] 调起支付参数生成成功'); return result.data; } catch (error: any) { const detail = error.message || JSON.stringify(error); console.error('[WeChat JSAPI] JSAPI支付创建失败:', detail); throw new Error(`微信支付暂不可用(${detail}),请稍后重试或使用其他支付方式`); } } // 初始化 SDK initPaymentSdks();