小程序调通了,打包成 H5 一条路直接断
小程序调通了。同一套代码打成 H5,调起支付那条路直接断。
后端只有一个 createOrder。前端一套代码编译到小程序、H5、App。
调起这一步没有任何共用的余地:三个编译目标拆出五种写法,
H5 自己就是两种——微信里一种,微信外一种。
先数一下后端的常量
「五个端」不是我拍的,是后端常量文件里数出来的。
那个文件 108 行、43 个常量。里头「支付方式」只有 4 个:微信、支付宝、余额、购物金。
而「支付渠道」有 12 个:
| 支付方式 | 渠道常量 | 个数 |
|---|---|---|
| 微信 | 公众号、小程序、视频号、网页、Native 扫码、App-iOS、App-安卓 | 7 |
| 支付宝 | 手机网页、App、PC | 3 |
| 余额 | 余额 | 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 |
| 微信外 H5 | mwebUrl | 直接 location.href 跳过去 |
| App | 一个完整的支付参数对象 | 传给 uni.requestPayment |
注意第二行和第一行:同一个字段,一个叫 timeStamp,一个叫 timestamp。
差的是中间那个 S 的大小写,签名就过不了。这不是谁写错了,是微信这两套 API 本来就这么定义的。
所以下单接口需要一个 payChannel 之类的参数,前端告诉后端「我是哪个端」,后端组装对应的结构。想用一个返回体喂所有端,喂不了。
五种调起,一种一种来
一、小程序
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:
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 的内置浏览器)
这个端最特殊:它不「调起」,它是跳走。
// mwebUrl 由后端调 H5 支付下单拿到
const back = location.protocol + '//'
+ location.host + resultPage
location.href = cfg.mwebUrl
+ '&redirect_url='
+ encodeURIComponent(back)用户会跳到微信的收银台页面,付完之后微信把他送回你的 redirect_url。
这里有三个坑:
redirect_url必须是完整 URL,带协议带域名,而且要 URL 编码——不编码的话里面的#和&会把参数截断- 域名要在商户平台配置过,没配的话跳过去直接报「商家参数格式有误」
- 用户可能不回来。他付完之后直接关掉页面了,你的
redirect_url根本没被访问。所以订单状态绝对不能依赖这个跳转
第三条是这一节的重点。H5 支付的回跳是尽力而为的,不是保证。
四、App
uni.requestPayment({
// 这两个字段小程序都不需要
provider: 'wxpay',
orderInfo: cfg, // 整个对象丢进去
success() { redirect(resultPage) },
fail(e) { tip(e.errMsg) }
})跟小程序像,但入参结构完全不同:小程序是把五个字段平铺,App 是 provider + orderInfo 两个字段。
App 端还要额外注意:微信支付需要在打包配置里填 AppID 并且打包后才生效。用真机调试或者基座跑,微信会拒绝,报的还是很含糊的「支付失败」。
五、余额支付 / 积分支付
这条路径最容易被漏掉,因为它压根不调微信——
它不是一个「端」,是一种支付方式,但它跟前面四条共用同一个入口。
// 余额、积分、后台改价为 0 的订单
// 后端直接扣完返回成功,前端跳结果页
redirect(resultPage)但它必须走同一个入口方法。我见过的做法是余额支付单独写一条路径,结果后来加「支付成功送优惠券」这个逻辑,微信那条路加了,余额这条忘了。
所以代码长什么样
不是一个函数,是一个分发器:
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 报「签名错误」 | 后端返回的是小程序那套字段名 timeStamp | JS-SDK 要的是 timestamp,S 小写 |
| 微信外 H5 跳过去报「商家参数格式有误」 | redirect_url 的域名没在商户平台配置 | 商户平台 → H5 支付 → 授权域名 |
| 跳回来参数丢了一半 | redirect_url 没做 URL 编码 | encodeURIComponent 包一下 |
| App 上一直「支付失败」,日志没细节 | 用基座或真机调试跑的 | 打包成 apk / ipa 再测 |
| 支付成功了但订单还是待支付 | 前端 success 里改的状态,后端回调没到 | 状态只能由后端回调改,前端只跳页面 |
| 小程序上正常,App 上入参报错 | 直接复用了小程序的平铺入参 | App 要 provider + orderInfo |
一个可以省事的地方
前面说了这么多不能统一,但有一件事必须统一:支付结果页。
不管从哪个端、哪种方式付的,最后都跳到同一个 resultPage,由它拿订单号去轮询后端。
这样做的好处是,前面五条路径每一条都只负责「把用户送到这个页面」,谁都不负责判断支付成没成。判断只有一处,就是那个页面。
支付这块最容易乱的就是每条路径都自己判断一遍成功失败,五个端五套判断逻辑,改一次漏三处。
能统一的那一处,恰恰是唯一跟「端」无关的那一处。
这条规律在多端交付里反复出现:差异都在入口,共性都在结果。
这条链路上的其他几篇
