3、小程序端调起支付:wx.requestPayment接口、参数组装、支付回调处理、支付成功/失败页面

好,到了这一步,其实才是前端同学最熟悉的战场。后端把预支付订单搞定了,签名也验过了,接下来就该我们小程序端上场了。说白了,就是调起那个微信支付的弹窗,让用户输密码或者刷脸。

嗯,这里要注意。很多新手容易犯一个错:以为拿到 prepay_id 就能直接调支付了。其实还差一步——你得把参数组装成 wx.requestPayment 能认的格式。我刚开始做的时候也踩过这个坑,直接拿后端返回的 prepay_id 往里塞,结果弹窗死活不出来。

3.1 wx.requestPayment 接口长什么样

这个接口是微信官方提供的,专门用来调起支付弹窗。它的参数不多,但每个都很关键。我个人习惯是先列出来,再一个个讲清楚。

参数名 类型 必填 说明
timeStamp String 时间戳,从 1970 年 1 月 1 日 00:00:00 至今的秒数
nonceStr String 随机字符串,长度不超过 32 位
package String 订单详情扩展字符串,格式为 prepay_id=***
signType String 签名算法,目前固定为 RSAMD5
paySign String 签名,由后端生成,前端直接使用
success Function 支付成功的回调
fail Function 支付失败的回调
complete Function 无论成功失败都会触发

你想想看,这些参数里,timeStamp、nonceStr、package、signType、paySign 这五个,其实都是后端算好签名之后返回给你的。前端只需要原样传进去就行。但有一个坑——timeStamp 必须是字符串。我记得有一次后端返回的是数字,我直接用了,结果在 Android 上没问题,iOS 上直接报错。后来排查了半天才发现是类型问题。

注意:timeStamp 必须是字符串类型。如果后端返回的是数字,记得用 String() 转一下。

3.2 参数组装:前端到底要做什么

很多人觉得参数组装就是后端给啥我传啥。其实没那么简单。你想想看,后端返回的数据结构可能跟你接口要求的不一样。比如后端可能返回一个对象,里面包含了所有字段,但字段名可能叫 pay_info 或者 wx_pay_params。你需要把它拆开,重新组装。

我个人习惯是在前端写一个专门的函数来做这件事。这样万一后端改了字段名,我只需要改一个地方。

// 组装支付参数
function assemblePayParams(backendData) {
  // 后端返回的数据结构示例:
  // {
  //   prepay_id: 'wx1234567890',
  //   time_stamp: '1712345678',
  //   nonce_str: 'abc123',
  //   sign: 'xxxxxx',
  //   sign_type: 'RSA'
  // }

  return {
    timeStamp: String(backendData.time_stamp),  // 注意转字符串
    nonceStr: backendData.nonce_str,
    package: 'prepay_id=' + backendData.prepay_id,
    signType: backendData.sign_type,
    paySign: backendData.sign
  };
}

嗯,这里有个细节。package 字段的格式是固定的,必须是 prepay_id=xxx。我曾经见过有人直接传了 prepay_id 的值,忘了加前缀,结果调起支付失败。这种低级错误排查起来特别浪费时间。

3.3 支付回调处理:成功不等于真的成功

调起支付之后,用户会看到微信的支付弹窗。输入密码或者刷脸之后,会触发 success 回调。但这里有个非常重要的概念——前端的 success 回调不代表支付真的成功了

为什么会这样?因为微信的 success 回调只代表「用户完成了支付操作」,不代表「微信服务器确认了这笔交易」。说白了,用户可能输入了密码,但网络延迟导致微信服务器没收到确认。或者用户刷脸成功了,但后台系统还没更新订单状态。

所以,我建议的做法是:前端只做 UI 反馈,真正的支付结果以后端通知为准

核心原则:前端 success 回调只做页面跳转和提示,不要更新订单状态。真正的支付成功确认,必须依赖后端回调或主动查询。

那前端到底该怎么做?我一般这样处理:

wx.requestPayment({
  ...payParams,
  success(res) {
    // 支付操作完成,但结果不确定
    // 先跳转到支付中页面,显示「支付处理中...」
    wx.navigateTo({
      url: '/pages/pay-processing/pay-processing'
    });

    // 然后主动查询订单状态
    checkOrderStatus(orderId);
  },
  fail(err) {
    // 用户取消支付或支付失败
    console.error('支付失败', err);
    wx.navigateTo({
      url: '/pages/pay-fail/pay-fail?reason=' + encodeURIComponent(err.errMsg)
    });
  },
  complete() {
    // 无论成功失败,都执行
    // 可以用来关闭加载动画
  }
});

我曾经遇到过一个真实案例。用户支付成功了,前端也跳到了成功页,但后端因为网络波动没收到微信的回调。结果用户以为支付成功了,实际上订单状态还是「未支付」。后来我们加了一个主动查询的机制,前端在 success 回调后每隔 3 秒查一次订单状态,直到确认结果为止。

3.4 支付成功/失败页面:用户体验的最后一公里

支付结果页面虽然看起来简单,但做不好很影响用户体验。我个人习惯是设计三个页面:

  • 支付中页面:显示加载动画,告诉用户「正在确认支付结果」
  • 支付成功页面:显示绿色对勾,展示订单金额和商品信息,提供「查看订单」按钮
  • 支付失败页面:显示红色叉叉,展示失败原因,提供「重新支付」和「联系客服」按钮

这里有个小技巧。支付失败的原因有很多种,比如用户取消、余额不足、网络超时等。不同的原因应该展示不同的提示文案。我一般会根据 err.errMsg 来判断:

function getFailReason(errMsg) {
  if (errMsg.includes('cancel')) {
    return '您已取消支付';
  } else if (errMsg.includes('balance')) {
    return '余额不足,请更换支付方式';
  } else if (errMsg.includes('timeout')) {
    return '支付超时,请重试';
  } else {
    return '支付失败,请稍后重试';
  }
}

嗯,这里要注意。微信的 errMsg 在不同版本上可能不一样。我建议不要完全依赖字符串匹配,最好后端也返回一个错误码,前端根据错误码来展示文案。

小提示:支付成功页面可以加一个「返回首页」的按钮,但不要自动跳转。让用户自己决定下一步操作,体验会更好。

3.5 完整流程串起来

好了,我们把整个流程串起来看看。从用户点击「立即支付」开始,到看到支付结果页面结束:

  1. 用户点击支付按钮,前端显示加载动画
  2. 前端调用后端接口,获取预支付参数(prepay_id 等)
  3. 前端组装参数,调用 wx.requestPayment
  4. 微信弹出支付弹窗,用户输入密码或刷脸
  5. 触发 success 回调,前端跳转到支付中页面,同时主动查询订单状态
  6. 后端确认支付成功,前端收到通知,跳转到支付成功页面
  7. 如果支付失败或用户取消,跳转到支付失败页面

这个流程看起来简单,但每一步都有坑。我建议你在开发的时候,把每一步的日志都打出来。特别是 wx.requestPayment 的 success 和 fail 回调,里面的 errMsg 信息非常关键。

最后说一句。支付功能是跟钱打交道的,容不得半点马虎。我见过太多因为前端处理不当导致的支付问题。记住一句话:前端只做展示和引导,真正的支付确认交给后端。这样即使出了问题,也能保证数据的一致性。