这里只涉及到node后端代码逻辑,前端只需要调用退款接口,传递订单号给后端。后端去根据订单号查询原订单信息
一、准备工作
1、商户证书配置
微信商户平台 → 账户中心 → API安全 → 下载以下文件:
API 证书(.p12 文件):用于退款接口双向认证
API 密钥(32位字符串):与支付功能共用
将证书文件(如 apiclient_cert.p12)保存到项目安全目录(如 certs/),切勿提交到代码仓库。
2、依赖安装
npm install axios xml2js fs https
二、退款接口核心实现
1.退款请求代码
// src/services/wechat-refund.js
const crypto = require('crypto');
const axios = require('axios');
const { parseStringPromise } = require('xml2js');
const fs = require('fs');
const path = require('path');
const config = require('../config/wechat-config');
// 加载商户证书(需绝对路径)
const certPath = path.resolve(__dirname, '../certs/apiclient_cert.p12');
/**
* 微信支付退款
* @param {Object} refundData 退款参数
* @returns {Promise<Object>} 退款结果
*/
async function requestRefund(refundData) {
// 基本参数
const params = {
appid: config.appId,
mch_id: config.mchId,
nonce_str: Math.random().toString(36).substr(2, 15), // 随机字符串生成函数
out_trade_no: refundData.out_trade_no, // 原支付订单号
out_refund_no: `REFUND_${Date.now()}`, // 退款单号(需唯一)
total_fee: refundData.total_fee, // 原订单金额(单位:分)
refund_fee: refundData.refund_fee, // 退款金额(单位:分)
notify_url: config.refundNotifyUrl // 退款结果通知地址(可选)
};
// 生成签名
params.sign = createSign(params);
// 构建 XML 请求体
const builder = new xml2js.Builder({ cdata: true, explicitArray: false });
const xmlData = builder.buildObject({ xml: params });
try {
// 发送退款请求(需携带证书)
const response = await axios.post(
'https://api.mch.weixin.qq.com/secapi/pay/refund',
xmlData,
{
headers: { 'Content-Type': 'application/xml' },
httpsAgent: new https.Agent({
pfx: fs.readFileSync(certPath),
passphrase: config.mchId // 证书密码通常是商户号
})
}
);
// 解析 XML 响应
const result = await parseXml(response.data);
if (result.return_code === 'SUCCESS' && result.result_code === 'SUCCESS') {
return { success: true, data: result };
} else {
return { success: false, error: result.err_code_des };
}
} catch (error) {
throw new Error(`退款请求失败: ${error.message}`);
}
}
// XML 解析
async function parseXml(xml) {
try {
const result = await parseStringPromise(xml);
const formatted = {};
for (const [key, value] of Object.entries(result.xml)) {
formatted[key] = value[0];
}
return formatted;
} catch (error) {
throw new Error('XML 解析失败');
}
}
// 生成签名
function createSign(params) {
const stringA = Object.keys(params)
.filter(key => params[key] !== '' && key !== 'sign') // 过滤空值和sign字段
.sort() // 按ASCII码升序排序
.map(key => `${key}=${params[key]}`)
.join('&');
const stringSignTemp = stringA + `&key=${config.apiKey}`;
const sign = crypto.createHash('md5').update(stringSignTemp).digest('hex').toUpperCase();
return sign;
}
module.exports = { requestRefund };
2.业务层调用实例
// app.js
const { requestRefund } = require('../services/wechat-refund');
app.post('/api/refund', async (req, res) => {
try {
const { orderId, refundAmount } = req.body;
// 1. 查询原订单信息(从数据库获取)
const order = await db.getOrderById(orderId);
if (!order || order.status !== 'PAID') {
return res.status(400).json({ code: -1, msg: '订单不可退款' });
}
// 2. 调用退款接口
const refundResult = await requestRefund({
out_trade_no: order.out_trade_no,
total_fee: order.total_fee,
refund_fee: refundAmount
});
if (refundResult.success) {
// 3. 更新数据库退款状态
await db.updateOrderRefund(orderId, 'PROCESSING');
res.json({ code: 0, data: refundResult.data });
} else {
res.status(500).json({ code: -1, msg: refundResult.error });
}
} catch (error) {
res.status(500).json({ code: -1, msg: error.message });
}
});