专注小程序 / APP 开发,用代码改变生活

获取方案
全国分站 主站首页 北京分站 石家庄分站 唐山分站 保定分站 廊坊分站 沧州分站 郑州分站 邯郸分站

小程序支付对接实战指南

小程序开发 2026-11-03 0 阅读 0 点赞 原创

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

小程序支付对接实战指南

一、支付链路先理清楚

微信小程序支付的核心是「服务端主导、客户端拉起」。完整链路如下:

  • 小程序端调用自己的服务端接口,提交商品信息与 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)

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

好想法,值得被认真交付

从小程序、APP 到全栈网站,一站式把想法变成可落地的产品

微信咨询

微信扫码,直接沟通需求