const crypto = require('crypto') const fetch = require('node-fetch') const { query } = require('../config/database') // 微信支付配置 const APPID = process.env.WECHAT_APPID const MCH_ID = process.env.WECHAT_MCH_ID const API_KEY = process.env.WECHAT_API_KEY const NOTIFY_URL = process.env.WECHAT_NOTIFY_URL const CERT_PATH = process.env.WECHAT_CERT_PATH const KEY_PATH = process.env.WECHAT_KEY_PATH // 微信支付API地址 const UNIFIED_ORDER_URL = 'https://api.mch.weixin.qq.com/pay/unifiedorder' const REFUND_URL = 'https://api.mch.weixin.qq.com/secapi/pay/refund' const ORDER_QUERY_URL = 'https://api.mch.weixin.qq.com/pay/orderquery' /** * 生成随机字符串 * @param {number} [length=32] - 字符串长度 * @returns {string} 随机字符串 */ function generateNonceStr(length = 32) { return crypto.randomBytes(length).toString('hex').slice(0, length) } /** * 生成微信支付签名 * @param {Object} params - 参与签名的参数对象 * @returns {string} 签名字符串 */ function generateSign(params) { // 按字典序排序参数 const sortedKeys = Object.keys(params).sort() // 拼接键值对(跳过空值和sign字段) const stringA = sortedKeys .filter(key => params[key] !== '' && params[key] !== undefined && params[key] !== null && key !== 'sign') .map(key => `${key}=${params[key]}`) .join('&') // 拼接API密钥 const stringSignTemp = `${stringA}&key=${API_KEY}` // MD5签名并转大写 return crypto.createHash('md5').update(stringSignTemp, 'utf8').digest('hex').toUpperCase() } /** * 对象转XML * @param {Object} obj - 需要转换的对象 * @returns {string} XML字符串 */ function buildXML(obj) { let xml = '' for (const key of Object.keys(obj)) { const val = obj[key] if (typeof val === 'number') { xml += `<${key}>${val}` } else { xml += `<${key}>` } } xml += '' return xml } /** * XML转对象 * @param {string} xml - XML字符串 * @returns {Promise} 解析后的对象 */ function parseXML(xml) { return new Promise((resolve, reject) => { const obj = {} const regex = /<(\w+)>(?:)?<\/\1>/g let match while ((match = regex.exec(xml)) !== null) { if (match[1] !== 'xml') { obj[match[1]] = match[2] } } resolve(obj) }) } /** * 创建支付订单 * 调用微信支付统一下单API(JSAPI),返回支付参数 */ async function createPayment(ctx) { const { orderId } = ctx.request.body if (!orderId) { ctx.body = { code: 400, message: '订单ID不能为空' } return } // 查询订单信息 const orders = await query('SELECT * FROM orders WHERE id = ?', [orderId]) if (orders.length === 0) { ctx.body = { code: 404, message: '订单不存在' } return } const order = orders[0] // 检查订单状态 if (order.status === 'paid') { ctx.body = { code: 400, message: '订单已支付' } return } // 检查支付配置 if (!APPID || !MCH_ID || !API_KEY || !NOTIFY_URL) { ctx.body = { code: 500, message: '微信支付配置不完整' } return } // 获取用户openid const user = ctx.state.user if (!user || !user.openid) { ctx.body = { code: 400, message: '无法获取用户openid,请先绑定微信' } return } // 构造统一下单参数 const nonceStr = generateNonceStr() const outTradeNo = orderId const totalFee = Math.round(parseFloat(order.total_price) * 100) // 金额转换为分 const body = `订单-${orderId}` const params = { appid: APPID, mch_id: MCH_ID, nonce_str: nonceStr, body: body, out_trade_no: outTradeNo, total_fee: totalFee, spbill_create_ip: ctx.ip || '127.0.0.1', notify_url: NOTIFY_URL, trade_type: 'JSAPI', openid: user.openid } // 生成签名 params.sign = generateSign(params) try { // 调用微信统一下单API const xmlData = buildXML(params) const response = await fetch(UNIFIED_ORDER_URL, { method: 'POST', body: xmlData, headers: { 'Content-Type': 'application/xml' } }) const resultXml = await response.text() const result = await parseXML(resultXml) if (result.return_code !== 'SUCCESS') { console.error('微信统一下单通信失败:', result.return_msg) ctx.body = { code: 500, message: `微信支付通信失败: ${result.return_msg}` } return } if (result.result_code !== 'SUCCESS') { console.error('微信统一下单业务失败:', result.err_code_des) ctx.body = { code: 500, message: `微信支付下单失败: ${result.err_code_des}` } return } // 生成小程序支付参数 const prepayId = result.prepay_id const payParams = { appId: APPID, timeStamp: Math.floor(Date.now() / 1000).toString(), nonceStr: generateNonceStr(), package: `prepay_id=${prepayId}`, signType: 'MD5' } payParams.paySign = generateSign(payParams) ctx.body = { code: 200, data: payParams } } catch (error) { console.error('创建支付失败:', error) ctx.body = { code: 500, message: '创建支付失败' } } } /** * 微信支付回调通知 * 验证签名,更新订单状态为paid */ async function paymentNotify(ctx) { try { const xml = ctx.request.body const result = await parseXML(xml) // 验证签名 const sign = result.sign const calculatedSign = generateSign(result) if (sign !== calculatedSign) { console.error('微信支付回调签名验证失败') ctx.body = '' return } // 检查支付结果 if (result.result_code !== 'SUCCESS') { console.error('微信支付回调支付失败:', result.err_code_des) ctx.body = '' return } const outTradeNo = result.out_trade_no const transactionId = result.transaction_id // 查询订单 const orders = await query('SELECT * FROM orders WHERE id = ?', [outTradeNo]) if (orders.length === 0) { console.error('微信支付回调订单不存在:', outTradeNo) ctx.body = '' return } const order = orders[0] // 防止重复通知 if (order.status === 'paid') { ctx.body = '' return } // 验证金额(微信返回的金额单位为分) const totalFee = parseInt(result.total_fee) const orderTotalFee = Math.round(parseFloat(order.total_price) * 100) if (totalFee !== orderTotalFee) { console.error('微信支付回调金额不一致:', { totalFee, orderTotalFee }) ctx.body = '' return } // 更新订单状态为paid await query( 'UPDATE orders SET status = ?, transaction_id = ? WHERE id = ? AND status != ?', ['paid', transactionId, outTradeNo, 'paid'] ) console.info('微信支付成功,订单已更新:', outTradeNo) ctx.body = '' } catch (error) { console.error('处理微信支付回调异常:', error) ctx.body = '' } } /** * 申请退款 * 调用微信退款API */ async function refundPayment(ctx) { const { orderId, reason } = ctx.request.body if (!orderId) { ctx.body = { code: 400, message: '订单ID不能为空' } return } // 查询订单信息 const orders = await query('SELECT * FROM orders WHERE id = ?', [orderId]) if (orders.length === 0) { ctx.body = { code: 404, message: '订单不存在' } return } const order = orders[0] // 检查订单状态,只有已支付的订单才能退款 if (order.status !== 'paid') { ctx.body = { code: 400, message: '订单未支付,无法退款' } return } // 检查支付配置 if (!APPID || !MCH_ID || !API_KEY) { ctx.body = { code: 500, message: '微信支付配置不完整' } return } // 构造退款参数 const nonceStr = generateNonceStr() const outTradeNo = orderId const outRefundNo = `RF${Date.now()}${Math.floor(Math.random() * 10000).toString().padStart(4, '0')}` const totalFee = Math.round(parseFloat(order.total_price) * 100) const refundFee = totalFee // 默认全额退款 const params = { appid: APPID, mch_id: MCH_ID, nonce_str: nonceStr, out_trade_no: outTradeNo, out_refund_no: outRefundNo, total_fee: totalFee, refund_fee: refundFee, refund_desc: reason || '用户申请退款' } // 生成签名 params.sign = generateSign(params) try { // 读取证书(微信退款API需要双向证书) let fetchOptions = { method: 'POST', body: buildXML(params), headers: { 'Content-Type': 'application/xml' } } if (CERT_PATH && KEY_PATH) { const fs = require('fs') fetchOptions.agent = new (require('https').Agent)({ cert: fs.readFileSync(CERT_PATH), key: fs.readFileSync(KEY_PATH) }) } const response = await fetch(REFUND_URL, fetchOptions) const resultXml = await response.text() const result = await parseXML(resultXml) if (result.return_code !== 'SUCCESS') { console.error('微信退款通信失败:', result.return_msg) ctx.body = { code: 500, message: `微信退款通信失败: ${result.return_msg}` } return } if (result.result_code !== 'SUCCESS') { console.error('微信退款业务失败:', result.err_code_des) ctx.body = { code: 500, message: `微信退款失败: ${result.err_code_des}` } return } // 更新订单状态为refunded await query( 'UPDATE orders SET status = ?, refund_no = ? WHERE id = ?', ['refunded', outRefundNo, orderId] ) ctx.body = { code: 200, data: { outRefundNo, refundFee: refundFee / 100 } } } catch (error) { console.error('申请退款失败:', error) ctx.body = { code: 500, message: '申请退款失败' } } } module.exports = { createPayment, paymentNotify, refundPayment }