一、支付流程核心架构
1.1 支付交互时序图
bash
小程序端 → 商户服务端 → 微信支付API → 回调通知
1. 创建本地订单 →
2. 请求统一下单 →
3. 返回支付参数 →
4. 发起微信支付 →
5. 接收支付结果 →
6. 验证回调签名 →
7. 更新订单状态
1.2 必备配置清单
- ✅ 微信支付商户号(mch_id)
- ✅ API v3密钥(32位随机字符串)
- ✅ 小程序appId与商户号绑定
- ✅ 服务器IP白名单配置
- ✅ 下载API证书(apiclient_cert.p12)
二、Node.js服务端核心实现
2.1 初始化支付模块
javascript
// wechat-pay.js
const crypto = require('crypto');
const fs = require('fs');
const axios = require('axios');
const config = {
appId: 'wx1234567890', // 小程序ID
mchId: '1600000000', // 商户号
apiKey: 'your_api_v3_key',// API密钥
notifyUrl: 'https://yourdomain.com/pay/notify' // 支付回调地址
};
// 证书加载
const cert = {
pfx: fs.readFileSync('./apiclient_cert.p12'),
passphrase: config.mchId // 商户号作为密码
};
// 创建带证书的axios实例
const payClient = axios.create({
baseURL: 'https://api.mch.weixin.qq.com',
httpsAgent: new https.Agent(cert)
});
2.2 统一下单接口
javascript
async function createPayment(orderInfo) {
const nonceStr = crypto.randomBytes(16).toString('hex');
const timestamp = Math.floor(Date.now() / 1000);
const params = {
appid: config.appId,
mchid: config.mchId,
description: orderInfo.desc,
out_trade_no: orderInfo.tradeNo,
notify_url: config.notifyUrl,
amount: {
total: orderInfo.amount, // 单位:分
currency: 'CNY'
},
payer: {
openid: orderInfo.openid // 小程序获取的用户openid
}
};
// 构造签名
const signature = generateSignature('POST', '/v3/pay/transactions/jsapi',
timestamp, nonceStr, params);
try {
const response = await payClient.post('/v3/pay/transactions/jsapi', params, {
headers: {
'Authorization': `WECHATPAY2-SHA256-RSA2048 ${signature}`,
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
return response.data;
} catch (error) {
console.error('统一下单失败:', error.response.data);
throw new Error('支付创建失败');
}
}
// 签名生成算法
function generateSignature(method, url, timestamp, nonce, body) {
const signStr = `${method}\n${url}\n${timestamp}\n${nonce}\n${JSON.stringify(body)}\n`;
const sign = crypto.createSign('RSA-SHA256');
sign.update(signStr);
return sign.sign(config.apiKey, 'base64');
}
三、小程序端支付触发
3.1 获取支付参数
javascript
// pages/pay.js
Page({
handlePayment() {
wx.login({
success: (res) => {
wx.request({
url: '/api/create-payment',
method: 'POST',
data: {
code: res.code,
amount: 100, // 单位分
goodsId: '123'
},
success: (res) => {
this.invokePayment(res.data);
}
});
}
});
},
invokePayment(payParams) {
wx.requestPayment({
timeStamp: payParams.timeStamp,
nonceStr: payParams.nonceStr,
package: payParams.package,
signType: 'RSA',
paySign: payParams.paySign,
success: (res) => {
console.log('支付成功', res);
},
fail: (err) => {
console.error('支付失败', err);
}
});
}
});
四、支付回调处理
4.1 接收通知接口
javascript
// routes/pay.js
router.post('/pay/notify', async (ctx) => {
const headers = ctx.headers;
const body = ctx.request.body;
// 验证签名
const verifyResult = verifySignature(
headers['wechatpay-timestamp'],
headers['wechatpay-nonce'],
headers['wechatpay-signature'],
ctx.request.rawBody
);
if (!verifyResult) {
ctx.status = 403;
return;
}
// 处理业务逻辑
if (body.event_type === 'TRANSACTION.SUCCESS') {
const result = await processPayment(body.resource.ciphertext);
if (result.success) {
ctx.status = 200;
ctx.body = { code: 'SUCCESS', message: 'OK' };
}
}
});
// 解密回调数据
async function processPayment(ciphertext) {
const decrypt = crypto.createDecipheriv('aes-256-gcm', config.apiKey, '');
let decrypted = decrypt.update(ciphertext, 'base64', 'utf8');
decrypted += decrypt.final('utf8');
return JSON.parse(decrypted);
}
五、关键安全策略
5.1 防重复支付
javascript
// 使用Redis实现幂等性控制
const redis = require('redis');
const client = redis.createClient();
async function checkDuplicate(tradeNo) {
const key = `payment:${tradeNo}`;
const exists = await client.set(key, 'LOCK', 'NX', 'EX', 300);
return exists === null;
}
5.2 支付状态查询
javascript
async function queryPaymentStatus(tradeNo) {
const url = `/v3/pay/transactions/out-trade-no/${tradeNo}?mchid=${config.mchId}`;
const response = await payClient.get(url);
return {
status: response.data.trade_state,
amount: response.data.amount.total
};
}
六、调试技巧与常见问题
6.1 沙箱环境配置
javascript
// 修改请求地址为沙箱环境
const payClient = axios.create({
baseURL: 'https://api.mch.weixin.qq.com/sandboxnew',
// ...其他配置不变
});
6.2 典型错误排查表
错误码 | 原因分析 | 解决方案 |
---|---|---|
APPID_MCHID_NOT_MATCH | 小程序与商户号未绑定 | 登录商户平台-关联设置 |
INVALID_REQUEST | 签名验证失败 | 检查参数顺序和编码格式 |
USERPAYING | 用户支付中 | 建议10秒后查询订单状态 |
OUT_TRADE_NO_USED | 商户订单号重复 | 检查订单号生成逻辑 |
结语:支付系统的三重境界
- 能用:完成基础支付流程
- 可靠:处理网络超时、异步通知、对账机制
- 安全:防御重放攻击、保证数据一致性、实现熔断降级
建议开发完成后进行以下验证:
- 模拟支付金额篡改(前端传1元,实际支付0.01元)
- 测试网络中断后的订单状态一致性
- 验证高并发场景下的库存扣减逻辑
支付系统开发如同走钢丝,唯有严谨与敬畏方能平稳落地。愿本文能助您少走弯路,代码永无PAYMENT_ERROR
!