| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995 |
- 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<string> {
- 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<string, string>): 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<any> {
- 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<string | null> {
- 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<string | null> {
- 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<string | null> {
- 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();
|