Skip to content

小程序调通了,打包成 H5 一条路直接断 ​

小程序调通了。同一套代码打成 H5,调起支付那条路直接断。

后端只有一个 createOrder。前端一套代码编译到小程序、H5、App。
调起这一步没有任何共用的余地:三个编译目标拆出五种写法,
H5 自己就是两种——微信里一种,微信外一种。

先数一下后端的常量 ​

「五个端」不是我拍的,是后端常量文件里数出来的。

那个文件 108 行、43 个常量。里头「支付方式」只有 4 个:微信、支付宝、余额、购物金。

而「支付渠道」有 12 个:

支付方式渠道常量个数
微信公众号、小程序、视频号、网页、Native 扫码、App-iOS、App-安卓7
支付宝手机网页、App、PC3
余额余额1
购物金购物金1

后三行的渠道值跟支付方式值一模一样——alipay 的渠道就叫 alipay。
也就是说,12 个渠道里有 8 个是微信一家撑起来的复杂度(7 个渠道,加上那个跟渠道同名的支付方式)。

最值得看的是微信那 7 个里的最后两个:wechatIos 和 wechatAndroid 是分开的两个常量。

同一件事——「App 里唤起微信支付」——安卓和 iOS 被当成两个渠道。

(顺带一个诚实的细节:支付宝 PC 那个常量,注释写的是「支付宝App」,
跟上一行一字不差。复制粘贴没改注释。 12 个里出现一次,
这个比例大概是所有常量文件的常态。)

而这 12 个常量,在 17 个类里被引用了 166 次。

166 这个数才是这篇的分量所在。它说明「你是哪个端」这个信息不是在入口判断一次然后往下传——它在整条链路上被反复追问:下单要判、调起要判、回调要判、退款要判、对账要判。

前端那五种写法,只是这 166 处里最后露在外面的几处。

谁会卡在这 ​

一套 UniApp 代码要发小程序 + H5 + App,支付这块想抽成一个公共方法的人。

抽到一半会发现:入参不一样、调用的 API 不一样、支付完的跳转方式不一样、连「取消支付」这件事怎么感知都不一样。

踩错的代价 ​

最耗时的踩法是在小程序上调通了,以为搞定了。

小程序的支付最规整:后端返回一个对象,前端 uni.requestPayment 一调,成功失败都有回调。写完一测,通了。

然后打包 H5,发现根本调不起来——uni.requestPayment 在 H5 上是空实现。

再打包 App,能调起来了,但支付完页面不跳转——因为你用的是小程序那套 success 回调里的跳转逻辑,App 的回调时机不一样。

三个端三次返工,每次都要重新走一遍「打包 → 上传 → 真机测」。这个循环比写代码慢十倍。

后端返回什么,决定前端能不能统一 ​

先说一个容易被忽略的事:同一个下单接口,给不同端返回的 payload 是不同的。

端后端返回前端拿它干什么
小程序timeStamp / nonceStr / package / signType / paySign传给 uni.requestPayment
微信内 H5同上,但字段叫 timestamp,中间那个 S 是小写传给 JS-SDK 的 chooseWXPay
微信外 H5mwebUrl直接 location.href 跳过去
App一个完整的支付参数对象传给 uni.requestPayment

注意第二行和第一行:同一个字段,一个叫 timeStamp,一个叫 timestamp。
差的是中间那个 S 的大小写,签名就过不了。这不是谁写错了,是微信这两套 API 本来就这么定义的。

所以下单接口需要一个 payChannel 之类的参数,前端告诉后端「我是哪个端」,后端组装对应的结构。想用一个返回体喂所有端,喂不了。

五种调起,一种一种来 ​

一、小程序 ​

js
uni.requestPayment({
  // 字符串,不是数字
  timeStamp: cfg.timeStamp,
  nonceStr:  cfg.nonceStr,
  // 带 prepay_id= 前缀
  package:   cfg.package,
  signType:  'RSA',
  paySign:   cfg.paySign,
  success() {
    // 只表示"用户点了确认并且没报错"
    // 不表示钱到账了,别在这改订单状态
    redirect(resultPage)
  },
  fail(e) {
    if (e.errMsg.includes('cancel')) {
      // 用户取消,不是错误
      return
    }
    tip(e.errMsg)
  }
})

success 那两行注释是这一节唯一重要的东西。前端的成功回调不是支付成功,订单状态只能由后端收到微信回调之后改。前端做的是跳到一个「支付结果」页,那个页面去轮询后端。

二、微信内置浏览器里的 H5 ​

这里用的是微信 JS-SDK,不是 uni.requestPayment:

js
if (wechat.isWeixin()) {
  wechat.pay(cfg).then(() => {
    // JS-SDK 的 resolve 才算用户走完流程
    setTimeout(
      () => redirect(resultPage), 500)
  }).catch(() => {
    tip('取消支付')
  })
}

两个细节:

  • then 里那个 500 毫秒的延时不是凑数。微信的支付浮层关闭和页面跳转抢时机,跳太快会看到浮层残影,或者跳转被浮层拦住。
  • 取消支付走的是 catch,跟小程序的 fail 里判断 errMsg 不一样。

三、微信外的 H5(Safari、Chrome、其他 App 的内置浏览器) ​

这个端最特殊:它不「调起」,它是跳走。

js
// mwebUrl 由后端调 H5 支付下单拿到
const back = location.protocol + '//'
           + location.host + resultPage
location.href = cfg.mwebUrl
              + '&redirect_url='
              + encodeURIComponent(back)

用户会跳到微信的收银台页面,付完之后微信把他送回你的 redirect_url。

这里有三个坑:

  1. redirect_url 必须是完整 URL,带协议带域名,而且要 URL 编码——不编码的话里面的 # 和 & 会把参数截断
  2. 域名要在商户平台配置过,没配的话跳过去直接报「商家参数格式有误」
  3. 用户可能不回来。他付完之后直接关掉页面了,你的 redirect_url 根本没被访问。所以订单状态绝对不能依赖这个跳转

第三条是这一节的重点。H5 支付的回跳是尽力而为的,不是保证。

四、App ​

js
uni.requestPayment({
  // 这两个字段小程序都不需要
  provider: 'wxpay',
  orderInfo: cfg,           // 整个对象丢进去
  success() { redirect(resultPage) },
  fail(e) { tip(e.errMsg) }
})

跟小程序像,但入参结构完全不同:小程序是把五个字段平铺,App 是 provider + orderInfo 两个字段。

App 端还要额外注意:微信支付需要在打包配置里填 AppID 并且打包后才生效。用真机调试或者基座跑,微信会拒绝,报的还是很含糊的「支付失败」。

五、余额支付 / 积分支付 ​

这条路径最容易被漏掉,因为它压根不调微信——
它不是一个「端」,是一种支付方式,但它跟前面四条共用同一个入口。

js
// 余额、积分、后台改价为 0 的订单
// 后端直接扣完返回成功,前端跳结果页
redirect(resultPage)

但它必须走同一个入口方法。我见过的做法是余额支付单独写一条路径,结果后来加「支付成功送优惠券」这个逻辑,微信那条路加了,余额这条忘了。

所以代码长什么样 ​

不是一个函数,是一个分发器:

js
function pay(type, cfg, resultPage) {
  // 先按支付方式分,再按端分
  switch (type) {
    case 'balance':
    case 'points':
      return redirect(resultPage)
    case 'wxpay':
      return payByWx(cfg, resultPage)
    case 'alipay':
      return payByAli(cfg, resultPage)
  }
}

payByWx 内部才用条件编译分端。两层分发不要压成一层——支付方式是业务概念,端是环境概念,压在一个 switch 里,加一种支付方式就要动所有端的分支。

常见报错对照表 ​

现象真正的原因怎么查
H5 上 uni.requestPayment 没反应,也不报错H5 上这个 API 是空实现H5 必须走 JS-SDK 或 mwebUrl
微信内 H5 报「签名错误」后端返回的是小程序那套字段名 timeStampJS-SDK 要的是 timestamp,S 小写
微信外 H5 跳过去报「商家参数格式有误」redirect_url 的域名没在商户平台配置商户平台 → H5 支付 → 授权域名
跳回来参数丢了一半redirect_url 没做 URL 编码encodeURIComponent 包一下
App 上一直「支付失败」,日志没细节用基座或真机调试跑的打包成 apk / ipa 再测
支付成功了但订单还是待支付前端 success 里改的状态,后端回调没到状态只能由后端回调改,前端只跳页面
小程序上正常,App 上入参报错直接复用了小程序的平铺入参App 要 provider + orderInfo

一个可以省事的地方 ​

前面说了这么多不能统一,但有一件事必须统一:支付结果页。

不管从哪个端、哪种方式付的,最后都跳到同一个 resultPage,由它拿订单号去轮询后端。

这样做的好处是,前面五条路径每一条都只负责「把用户送到这个页面」,谁都不负责判断支付成没成。判断只有一处,就是那个页面。

支付这块最容易乱的就是每条路径都自己判断一遍成功失败,五个端五套判断逻辑,改一次漏三处。

能统一的那一处,恰恰是唯一跟「端」无关的那一处。
这条规律在多端交付里反复出现:差异都在入口,共性都在结果。


这条链路上的其他几篇

签名-B

大粽子