支付是商业类小程序的核心能力。小程序支付以平台支付为底层,通过 统一下单 → 调起支付 → 支付回调 三段式完成。本文梳理完整对接流程,并强调金额、幂等与验签这些最容易出问题的地方。
一、开通前置条件
对接支付前需要满足:
- 小程序主体为企业/个体工商户(个人主体不支持支付)
- 完成支付商户号开通与结算账户绑定
- 在开放平台后台配置支付目录、回调地址等参数
- 申请对应的商户密钥(API 密钥 / APIv3 密钥)
个人主体小程序无法开通支付,需要先升级主体或使用第三方支付通道,这是很多个人开发者第一个遇到的硬门槛。
二、统一下单
后端收到下单请求后,携带金额与订单号调用平台统一下单接口:
// Node.js 统一下单示例(简化)
const params = {
appid: APP_ID,
mch_id: MCH_ID,
description: '商品购买',
out_trade_no: orderNo, // 商户订单号,必须唯一
amount: { total: 1000 }, // 单位:分
notify_url: NOTIFY_URL, // 支付结果回调地址
payer: { openid: userOpenid },
};
// 签名后发送请求
const prepayRes = await requestPaymentOrder(params);
// 返回 prepay_id
几个关键点:
- 金额单位是分:以整数(分)传递,避免浮点数精度问题
- out_trade_no 唯一:商户订单号,全局唯一,重复会导致下单失败
- openid 从登录态获取:支付必须绑定发起支付的小程序用户
三、前端调起支付
统一下单成功拿到 prepay_id 后,后端按规范签名生成支付参数,返回给前端调起支付:
// 小程序端
wx.requestPayment({
timeStamp: payData.timeStamp,
nonceStr: payData.nonceStr,
package: 'prepay_id=' + payData.prepayId,
signType: 'RSA',
paySign: payData.paySign,
success() {
// 支付成功(最终以回调为准)
},
fail() {
// 用户取消或支付失败
},
});
前端 success 只代表用户完成了支付确认,业务结果必须以服务端回调为准。
四、支付回调与验签
支付完成后平台会向 notify_url 异步推送结果。回调处理是支付对接的核心:
// 回调验签(以 APIv3 为例)
const { createVerify, createPublicKey } = require('crypto');
function verifyNotify(headers, body, platformPubKey) {
const msg = headers['wechatpay-timestamp'] + '\n'
+ headers['wechatpay-nonce'] + '\n'
+ body + '\n';
const signature = headers['wechatpay-signature'];
const verify = createVerify('RSA-SHA256');
verify.update(msg);
verify.end();
return verify.verify(createPublicKey(platformPubKey), signature, 'base64');
}
回调处理的完整姿势:
- 验签:用平台公钥验证签名,验签失败直接拒绝
- 解密:回调报文是密文,用 APIv3 密钥解密
- 幂等处理:同一个订单可能收到多次回调,处理前检查订单状态
- 更新业务:校验金额与订单一致后更新订单状态、发发货
- 应答:返回成功应答,通知平台停止重试
五、退款与对账
退款同样通过 API 完成,注意:
- 退款金额不能超过原支付金额
- 退款也需要回调通知,同样要验签与幂等
- 定期拉取账单(T+1),与本地订单对账,及时发现差异
六、安全与常见坑
- 验签不过:检查是否用错了证书/密钥版本,时间戳过期需同步服务器时间
- 重复发货:回调重试导致重复发货,务必按订单状态加锁判断
- 金额校验:回调中的金额必须与下单金额一致,防止篡改
- 密钥保管:商户密钥只能存在服务端,绝不能出现在小程序代码中
- 回调地址安全:回调接口不做业务鉴权,但必须验签,防止伪造回调
小结
支付对接的成败在于细节:金额以分为单位、回调必须验签、业务处理必须幂等。把这三点做到位,再配合对账机制,支付链路就能稳定可靠。