小程序支付对接是电商、会员、预约类项目绕不开的一环。它不复杂,但链路长、状态多,任何一环处理不当都会出现「用户扣了钱、订单没变」的尴尬。本文以微信小程序为例,梳理从下单到回调的完整实现,并给出可直接复用的代码结构。

一、支付链路先理清楚
微信小程序支付的核心是「服务端主导、客户端拉起」。完整链路如下:
- 小程序端调用自己的服务端接口,提交商品信息与 openid
- 服务端调用微信统一下单接口,拿到 prepay_id
- 服务端对 prepay_id 等参数做二次签名,返回给小程序端
- 小程序端调用
wx.requestPayment拉起支付 - 微信服务器异步回调服务端,服务端验签、更新订单、返回成功
- 小程序端支付成功后主动查询一次订单状态,做兜底
关键认知:支付成功与否,以服务端收到的回调为准,不能只信客户端返回的 success。客户端结果只用于 UI 提示。
二、服务端统一下单
以 Node.js 为例,使用官方 SDK 或自行组装请求均可。核心参数包括 appid、mch_id、out_trade_no、total_fee、openid、notify_url、trade_type=JSAPI。签名使用商户 API 密钥做 MD5 或 HMAC-SHA256。
const params = {
appid: APPID,
mch_id: MCH_ID,
nonce_str: randomStr(32),
body: '会员开通-月卡',
out_trade_no: orderNo,
total_fee: 990,
spbill_create_ip: clientIp,
notify_url: 'https://api.example.com/pay/notify',
trade_type: 'JSAPI',
openid: userOpenid
};
params.sign = sign(params, MCH_KEY);
const xml = toXml(params);
const res = await post('https://api.mch.weixin.qq.com/pay/unifiedorder', xml);
const data = fromXml(res);
if (data.return_code !== 'SUCCESS' || data.result_code !== 'SUCCESS') {
throw new Error(data.err_code_des || data.return_msg);
}
// 二次签名,供小程序端拉起支付
const payParams = {
appId: APPID,
timeStamp: String(Math.floor(Date.now() / 1000)),
nonceStr: randomStr(32),
package: 'prepay_id=' + data.prepay_id,
signType: 'MD5'
};
payParams.paySign = sign(payParams, MCH_KEY);
return payParams;注意 total_fee 单位是分,且必须是整数;out_trade_no 要保证全局唯一,建议用「业务前缀 + 时间戳 + 随机数」。
三、小程序端拉起支付
async function pay(orderId) {
const res = await request('/api/pay/prepay', { orderId });
if (res.code !== 0) return;
wx.requestPayment({
timeStamp: res.data.timeStamp,
nonceStr: res.data.nonceStr,
package: res.data.package,
signType: res.data.signType,
paySign: res.data.paySign,
success: () => {
// 仅提示,最终状态以服务端为准
wx.showToast({ title: '支付成功' });
pollOrderStatus(orderId);
},
fail: (err) => {
if (err.errMsg.includes('cancel')) {
wx.showToast({ title: '已取消支付', icon: 'none' });
}
}
});
}支付成功后建议轮询 2~3 次订单状态,每次间隔 1 秒,覆盖回调延迟的窗口。
四、回调处理是重中之重
回调接口必须做到三件事:验签、幂等、快速响应。
- 验签:用同样的签名规则校验
sign字段,不通过直接返回失败 - 幂等:以
out_trade_no为唯一键,先查订单状态,已支付则直接返回成功,避免重复发货 - 快速响应:微信要求 5 秒内返回,业务处理耗时长时先落库再异步处理
app.post('/pay/notify', async (req, res) => {
const data = fromXml(req.rawBody);
if (data.return_code !== 'SUCCESS') return res.send(failXml());
if (data.sign !== sign(data, MCH_KEY)) return res.send(failXml());
const order = await db.order.findOne({ orderNo: data.out_trade_no });
if (!order) return res.send(failXml());
if (order.status === 'paid') return res.send(successXml());
if (data.result_code === 'SUCCESS') {
await db.order.update(order.id, { status: 'paid', transactionId: data.transaction_id });
// 发货、开通会员等业务逻辑
}
res.send(successXml());
});返回内容必须是 XML 且 return_code 为 SUCCESS,否则微信会持续重试,最多 8 次。
五、云开发场景的简化方案
如果项目使用微信云开发,可以跳过自建服务器,用云函数调用支付。云函数内通过 cloud.getWXContext() 直接拿到 openid,省去用户身份传递;回调则配置云函数 HTTP 触发路径。这样能显著降低运维成本,适合中小型项目快速上线。
六、几个容易忽略的细节
- 商户证书与 API 密钥不要写进小程序端代码,全部放服务端环境变量
- 退款走
refund接口,同样需要回调或主动查询确认结果 - 金额校验在服务端完成,客户端传来的价格一律不可信
- 测试阶段可用沙箱环境,但沙箱与正式环境的密钥不同,注意区分
- 订单表建议保留原始回调报文,便于对账和排查争议
把下单、拉起、回调、兜底查询这四段串起来,小程序支付对接就基本稳定了。真正决定线上质量的往往不是主流程,而是回调的幂等和异常分支的处理。

评论 (0)
还没有评论,快来抢沙发吧~