/** * 小程序支付API * 对接真实微信支付接口 * * 配置来源: 用户规则 * - 小程序AppID: wxb8bbb2b10dec74aa * - 商户号: 1318592501 * - API密钥: wx3e31b068be59ddc131b068be59ddc2 */ import { NextResponse } from 'next/server' import crypto from 'crypto' import { query, getConfig } from '@/lib/db' // 微信支付配置 - 2026-01-25 更新 // 小程序支付绑定状态: 审核中(申请单ID: 201554696918) const WECHAT_PAY_CONFIG = { appId: 'wxb8bbb2b10dec74aa', // 小程序AppID appSecret: '3c1fb1f63e6e052222bbcead9d07fe0c', // 小程序AppSecret(已更新) mchId: '1318592501', // 商户号 mchKey: 'wx3e31b068be59ddc131b068be59ddc2', // API密钥(v2) notifyUrl: 'https://soul.quwanzhi.com/api/miniprogram/pay/notify', // 支付回调地址 } // 生成随机字符串 function generateNonceStr(): string { return crypto.randomBytes(16).toString('hex') } // 生成签名 function generateSign(params: Record, key: string): string { const sortedKeys = Object.keys(params).sort() const signString = sortedKeys .filter((k) => params[k] && k !== 'sign') .map((k) => `${k}=${params[k]}`) .join('&') const signWithKey = `${signString}&key=${key}` return crypto.createHash('md5').update(signWithKey, 'utf8').digest('hex').toUpperCase() } // 对象转XML function dictToXml(data: Record): string { const xml = [''] for (const [key, value] of Object.entries(data)) { xml.push(`<${key}>`) } xml.push('') return xml.join('') } // XML转对象 function xmlToDict(xml: string): Record { const result: Record = {} const regex = /<(\w+)><\/\1>/g let match while ((match = regex.exec(xml)) !== null) { result[match[1]] = match[2] } const simpleRegex = /<(\w+)>([^<]*)<\/\1>/g while ((match = simpleRegex.exec(xml)) !== null) { if (!result[match[1]]) { result[match[1]] = match[2] } } return result } // 生成订单号 function generateOrderSn(): string { const now = new Date() const timestamp = now.getFullYear().toString() + (now.getMonth() + 1).toString().padStart(2, '0') + now.getDate().toString().padStart(2, '0') + now.getHours().toString().padStart(2, '0') + now.getMinutes().toString().padStart(2, '0') + now.getSeconds().toString().padStart(2, '0') const random = Math.floor(Math.random() * 1000000).toString().padStart(6, '0') return `MP${timestamp}${random}` } /** * POST - 创建小程序支付订单 */ export async function POST(request: Request) { try { const body = await request.json() const { openId, productType, productId, amount, description } = body if (!openId) { return NextResponse.json({ success: false, error: '缺少openId参数,请先登录' }, { status: 400 }) } if (!amount || amount <= 0) { return NextResponse.json({ success: false, error: '支付金额无效' }, { status: 400 }) } // === 根据推广配置计算好友优惠后的实际支付金额 === let finalAmount = amount try { // 读取推广/分销配置,获取好友优惠比例(如 5 表示 5%) const referralConfig = await getConfig('referral_config') const userDiscount = referralConfig?.userDiscount ? Number(referralConfig.userDiscount) : 0 // 若存在有效的推荐码且配置了优惠比例,则给好友打折 if (userDiscount > 0 && body.referralCode) { const discountRate = userDiscount / 100 const discounted = amount * (1 - discountRate) // 保证至少 0.01 元,并保留两位小数 finalAmount = Math.max(0.01, Math.round(discounted * 100) / 100) console.log('[MiniPay] 应用好友优惠:', { originalAmount: amount, discountPercent: userDiscount, finalAmount, referralCode: body.referralCode, }) } } catch (e) { console.warn('[MiniPay] 读取 referral_config.userDiscount 失败,使用原价金额:', e) finalAmount = amount } const orderSn = generateOrderSn() const totalFee = Math.round(finalAmount * 100) // 转换为分(单位分) const goodsBody = description || (productType === 'fullbook' ? '《一场Soul的创业实验》全书' : `章节购买-${productId}`) // 获取客户端IP const forwarded = request.headers.get('x-forwarded-for') const clientIp = forwarded ? forwarded.split(',')[0] : '127.0.0.1' // 构建统一下单参数 const params: Record = { appid: WECHAT_PAY_CONFIG.appId, mch_id: WECHAT_PAY_CONFIG.mchId, nonce_str: generateNonceStr(), body: goodsBody.slice(0, 128), out_trade_no: orderSn, total_fee: totalFee.toString(), spbill_create_ip: clientIp, notify_url: WECHAT_PAY_CONFIG.notifyUrl, trade_type: 'JSAPI', openid: openId, attach: JSON.stringify({ productType, productId, userId: body.userId || '' }), } // 生成签名 params.sign = generateSign(params, WECHAT_PAY_CONFIG.mchKey) console.log('[MiniPay] 创建订单:', { orderSn, totalFee, openId: openId.slice(0, 10) + '...', productType, productId, }) // === ✅ 1. 先插入订单到数据库(无论支付是否成功,都要有订单记录) === const userId = body.userId || openId // 优先使用 userId,否则用 openId let orderCreated = false // 查询当前用户的有效推荐人(用于订单归属与分销) let referrerId: string | null = null try { const bindings = await query(` SELECT referrer_id FROM referral_bindings WHERE referee_id = ? AND status = 'active' AND expiry_date > NOW() ORDER BY binding_date DESC LIMIT 1 `, [userId]) as any[] if (bindings.length > 0) { referrerId = bindings[0].referrer_id || null console.log('[MiniPay] 订单归属推荐人(绑定):', referrerId) } // 若绑定未查到且前端传了邀请码,按邀请码解析推荐人 if (!referrerId && body.referralCode) { const refUsers = await query(` SELECT id FROM users WHERE referral_code = ? LIMIT 1 `, [String(body.referralCode).trim()]) as any[] if (refUsers.length > 0) { referrerId = refUsers[0].id console.log('[MiniPay] 订单归属推荐人(邀请码):', referrerId) } } } catch (e) { console.warn('[MiniPay] 查询推荐人失败,继续创建订单:', e) } // 下单时使用的邀请码:优先用请求体,否则用推荐人当前邀请码(便于订单记录对账) let orderReferralCode: string | null = body.referralCode ? String(body.referralCode).trim() || null : null if (!orderReferralCode && referrerId) { try { const refRows = (await query(`SELECT referral_code FROM users WHERE id = ? LIMIT 1`, [referrerId]) as any[]) if (refRows.length > 0 && refRows[0].referral_code) { orderReferralCode = refRows[0].referral_code } } catch (_) { /* 忽略 */ } } try { // 检查是否已有相同产品的已支付订单 const existingOrders = await query(` SELECT id FROM orders WHERE user_id = ? AND product_type = ? AND product_id = ? AND status = 'paid' LIMIT 1 `, [userId, productType, productId || 'fullbook']) as any[] if (existingOrders.length > 0) { console.log('[MiniPay] ⚠️ 用户已购买该产品,但仍创建订单:', { userId, productType, productId }) } // 插入订单(含 referrer_id、referral_code,便于分销归属与对账) try { await query(` INSERT INTO orders ( id, order_sn, user_id, open_id, product_type, product_id, amount, description, status, transaction_id, referrer_id, referral_code, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW()) `, [ orderSn, orderSn, userId, openId, productType, productId || 'fullbook', finalAmount, goodsBody, 'created', null, referrerId, orderReferralCode ]) } catch (insertErr: any) { // 兼容:若表尚无 referrer_id 或 referral_code 列 const msg = (insertErr as any)?.message || '' const code = (insertErr as any)?.code || '' if (msg.includes('referrer_id') || msg.includes('referral_code') || code === 'ER_BAD_FIELD_ERROR') { try { await query(` INSERT INTO orders ( id, order_sn, user_id, open_id, product_type, product_id, amount, description, status, transaction_id, referrer_id, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW()) `, [ orderSn, orderSn, userId, openId, productType, productId || 'fullbook', finalAmount, goodsBody, 'created', null, referrerId ]) console.log('[MiniPay] 订单已插入(未含 referral_code,请执行 scripts/add_orders_referral_code.py)') } catch (e2: any) { if (e2?.message?.includes('referrer_id') || e2?.code === 'ER_BAD_FIELD_ERROR') { await query(` INSERT INTO orders ( id, order_sn, user_id, open_id, product_type, product_id, amount, description, status, transaction_id, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW()) `, [ orderSn, orderSn, userId, openId, productType, productId || 'fullbook', finalAmount, goodsBody, 'created', null ]) console.log('[MiniPay] 订单已插入(未含 referrer_id/referral_code,请执行迁移脚本)') } else { throw e2 } } } else { throw insertErr } } orderCreated = true console.log('[MiniPay] ✅ 订单已插入数据库:', { orderSn, userId, productType, productId, originalAmount: amount, finalAmount, }) } catch (dbError) { console.error('[MiniPay] ❌ 插入订单失败:', dbError) // 订单创建失败,但不中断支付流程 // 理由:微信支付成功后仍可以通过回调补记订单 } // 调用微信统一下单接口 const xmlData = dictToXml(params) const response = await fetch('https://api.mch.weixin.qq.com/pay/unifiedorder', { method: 'POST', headers: { 'Content-Type': 'application/xml' }, body: xmlData, }) const responseText = await response.text() const result = xmlToDict(responseText) console.log('[MiniPay] 微信响应:', { return_code: result.return_code, result_code: result.result_code, err_code: result.err_code, err_code_des: result.err_code_des, }) // 检查返回结果 if (result.return_code !== 'SUCCESS') { return NextResponse.json({ success: false, error: `微信支付请求失败: ${result.return_msg || '未知错误'}` }, { status: 500 }) } if (result.result_code !== 'SUCCESS') { return NextResponse.json({ success: false, error: `微信支付失败: ${result.err_code_des || result.err_code || '未知错误'}` }, { status: 500 }) } // 构建小程序支付参数 const prepayId = result.prepay_id if (!prepayId) { return NextResponse.json({ success: false, error: '微信支付返回数据异常' }, { status: 500 }) } const timestamp = Math.floor(Date.now() / 1000).toString() const nonceStr = generateNonceStr() const payParams: Record = { appId: WECHAT_PAY_CONFIG.appId, timeStamp: timestamp, nonceStr, package: `prepay_id=${prepayId}`, signType: 'MD5', } payParams.paySign = generateSign(payParams, WECHAT_PAY_CONFIG.mchKey) console.log('[MiniPay] 支付参数生成成功:', { orderSn, prepayId: prepayId.slice(0, 20) + '...' }) return NextResponse.json({ success: true, data: { orderSn, prepayId, payParams: { timeStamp: timestamp, nonceStr, package: `prepay_id=${prepayId}`, signType: 'MD5', paySign: payParams.paySign, } } }) } catch (error) { console.error('[MiniPay] 创建订单失败:', error) return NextResponse.json({ success: false, error: '创建支付订单失败' }, { status: 500 }) } } /** * GET - 查询订单状态 */ export async function GET(request: Request) { const { searchParams } = new URL(request.url) const orderSn = searchParams.get('orderSn') if (!orderSn) { return NextResponse.json({ success: false, error: '缺少订单号' }, { status: 400 }) } try { const params: Record = { appid: WECHAT_PAY_CONFIG.appId, mch_id: WECHAT_PAY_CONFIG.mchId, out_trade_no: orderSn, nonce_str: generateNonceStr(), } params.sign = generateSign(params, WECHAT_PAY_CONFIG.mchKey) const xmlData = dictToXml(params) const response = await fetch('https://api.mch.weixin.qq.com/pay/orderquery', { method: 'POST', headers: { 'Content-Type': 'application/xml' }, body: xmlData, }) const responseText = await response.text() const result = xmlToDict(responseText) if (result.return_code !== 'SUCCESS' || result.result_code !== 'SUCCESS') { return NextResponse.json({ success: true, data: { status: 'unknown', orderSn } }) } const tradeState = result.trade_state let status = 'paying' if (tradeState === 'SUCCESS') status = 'paid' else if (['CLOSED', 'REVOKED', 'PAYERROR'].includes(tradeState)) status = 'failed' else if (tradeState === 'REFUND') status = 'refunded' return NextResponse.json({ success: true, data: { status, orderSn, transactionId: result.transaction_id, totalFee: parseInt(result.total_fee || '0', 10), } }) } catch (error) { console.error('[MiniPay] 查询订单失败:', error) return NextResponse.json({ success: false, error: '查询订单失败' }, { status: 500 }) } }