用 UniApp 做收款,最让人头疼的不是"能不能收到钱",而是同一套代码,打包成 App、H5、小程序之后,支付调用方式居然三套都不一样。很多人以为自己写一次就完了,结果一打包,H5 能付、小程序打不开、App 直接白屏,然后开始怀疑人生。
这篇文章不绕弯子,就讲三件事:UniApp 多端收款到底怎么调、每端的差异在哪、以及最重要的——支付结果怎么在多端之间可靠地同步回来。这三件事吃透了,你用 UniApp 接易支付就不会再卡壳。
先搞清楚:UniApp 多端收款的本质是什么
UniApp 的卖点是"一套代码,多端运行"。但支付这件事,恰恰是"一套代码"最难覆盖的地方。原因很简单:App、H5、小程序这三端的运行环境和支付能力,是由各自平台决定的,不是由 UniApp 统一的。
具体说:
- H5 端:本质是网页,可以跳转到一个支付页面,也可以内嵌 JSAPI,跟普通网站支付几乎一样。
- 微信小程序端:跑在微信里,只能用微信小程序允许的支付方式,很多网页跳转在这里是被限制的。
- App 端:是一个原生壳子,要做支付,要么内嵌网页(webview)走 H5 支付,要么调原生支付 SDK。
同一笔订单,在这三端的"发起支付"动作是不一样的,但"支付完成后同步状态"这件事又必须统一。这就是 UniApp 收款的核心矛盾:入口是散的,出口必须收拢。
理解了这一点,你就明白为什么不能把"UniApp 收款"当成一个简单的 API 调用去处理,而要把"调用"和"同步"拆成两层来设计。
第一层:多端如何"发起支付"
易支付本身提供的是标准 HTTP 接口,核心就两个动作:下单(创建订单)和查询(查订单状态)。UniApp 要做的,是在不同端用不同的方式,把用户引导到"付款"这个动作上去。
H5 端:最简单,但也最容易踩坑
H5 端跑在浏览器里,理论上你拿到支付链接后,直接 window.location.href 跳转过去就行,或者用 plus.webview 内嵌。但这里有两个坑:
第一,支付页面的域名和你的域名不同源。跳转过去付完钱,怎么跳回来?你得在易支付后台配置好"同步跳转地址"(return 地址),付完钱让用户回到你的页面。
第二,有些支付链接在微信内置浏览器里会被限制。如果你的 H5 是在微信里打开的(比如公众号菜单、好友分享),直接跳转某些支付链接可能被拦,这时候要走 JSAPI 的方式。关于 H5 和网页支付的区别,以及 JSAPI 到底怎么调,我之前在易支付 JSAPI 支付对接完整教程里拆得很细,做 H5 端之前建议先看一遍,能避开大部分环境限制的坑。
小程序端:最受限制,但路径最清晰
微信小程序端是最"规矩"的。小程序里不能随意外跳网页,所以支付要么用微信官方的小程序支付,要么走易支付提供的、适配小程序的能力(本质也是把微信支付包装了一层)。
小程序端的关键点在于:支付参数里必须带上小程序的 openid。因为微信支付是跟"用户"绑定的,你得先拿到用户的 openid,才能发起这笔支付。拿 openid 的流程是:小程序登录拿到 code → 后端用 code 换 openid → 后端用 openid 去易支付下单。
这里有个特别容易搞混的地方:openid 是"谁"的。同一家公司的公众号、小程序,openid 是不一样的,甚至不同小程序之间 openid 也不互通。所以下单时传的 openid,必须是"当前这个正在收款的小程序"对应的 openid,传错了支付就会失败,报错还特别隐晦。
App 端:最灵活,也最考验架构
App 端是自由度最高的。你有两条路可以走:
一条是内嵌 webview 走 H5 支付——把支付链接塞进一个原生 webview 里打开,用户在里面完成付款。这条路最简单,复用 H5 的逻辑就行,但体验一般,而且有些支付方式(比如特定渠道的唤起)在 webview 里表现不稳定。
另一条是调原生支付 SDK——由易支付或通道方提供 App 端的原生 SDK,你通过 UniApp 的 plus 或原生插件能力去调用。这条路体验好,但工作量也大。
对绝大多数用 UniApp 做收款的中小项目来说,第一条路(webview + H5)是性价比最高的选择。除非你有明确的 App 体验要求,否则不建议一上来就上原生 SDK,那是给自己找事。
第二层:多端如何"同步支付状态"
这是整篇文章的重头戏,也是最多人翻车的地方。支付状态同步,说到底是解决一个问题:用户付完钱之后,你的系统怎么知道"他已经付了",并且把这个事实可靠地反映到订单上。
很多人的做法是:前端付完钱,跳回来,前端直接改订单状态。这是大错特错的。前端永远不可信,跳回来不等于支付成功,用户可能中途关掉、可能网络断了、可能被风控拦了。把状态同步交给前端,等于把账本交给外人记。
正确的状态同步,要靠三条腿走路:回调、主动查询、前端轮询。三条腿互为兜底,缺一条都不稳。
腿一:后端异步回调,这是"权威来源"
用户付完钱,易支付会往你配置的 notify 地址 POST 一条通知,告诉你"这笔订单支付成功了"。这是最权威的状态来源,订单状态的最终变更,只能由这条回调触发。
回调处理要做的三件事,和任何支付集成都一样:验签、幂等、金额核对。
验签,就是确认这条通知真的是易支付发的,而不是别人伪造的。验签算法就是那套 MD5——参数按 ASCII 排序、拼 key、做 MD5、比对 sign。这块的细节非常多,参数里混了空值、中文没编码、排序不对,都会导致验签失败。我之前专门写过一篇MD5 签名完整解析,把排序、空值处理、各种报错根源都拆开了,签名这块搞不定的时候去对照着看,比自己瞎猜快得多。
幂等,就是回调可能给你发好几次(通道为了保证送达会重试),你的处理逻辑必须保证同一个订单号只处理一次,不能重复发货、重复加权益。
金额核对,就是回调里的金额必须和你本地订单金额一致,防止金额被篡改。
腿二:后端主动查询,这是"兜底"
回调不是 100% 可靠的。网络抖动、服务器临时不可用、回调地址被拦,都可能导致回调丢失。所以光靠回调不够,你还需要主动查询:定期调用易支付的订单查询接口,问一声"这笔订单现在什么状态"。
主动查询的时机怎么定?我的做法是:
- 用户发起支付后,先记一个"待确认"状态。
- 启动一个定时任务(或者用消息队列延迟任务),在支付发起后的几秒、几十秒、几分钟,分几次去查订单状态。
- 查到了"已支付",就更新订单;查不到就一直查到超时,然后把订单标记为"待人工确认"。
主动查询和回调是互补的:回调快、实时性强,但可能丢;查询慢、有延迟,但可靠。两个都做,才能把"漏单"的概率压到最低。
腿三:前端轮询,这是"体验层"
前两条腿是后端的事,解决的是"账对不对"。第三条腿是前端的,解决的是"用户体感好不好"。
用户付完钱,回到你的 App 或小程序,你不能让他在那儿干等,也不该让他看到一个"支付中"然后永远停在原地。正确做法是:回到页面后,前端向后端发起轮询,问"这笔订单付了没",直到拿到明确结果。
轮询要设好节奏和上限:比如每 2 秒问一次,最多问 15 次,问不到就提示"正在确认,请稍后刷新"。同时后端要配合前端轮询,返回一个明确的状态(待支付/已支付/已关闭),而不是含糊的"处理中"。
这三条腿的关系,一句话总结:回调定生死,查询兜底,轮询保体验。少了任何一个,都会在某个场景下出问题。
多端状态同步,最容易忽略的一个细节
上面说的是"单端"的状态同步。但 UniApp 是多端的,这里有一个几乎所有人都会忽略的问题:同一笔订单,用户可能先在 H5 发起支付,又切到小程序里看。
比如用户在你 H5 上下了单,付钱付到一半,觉得手机里的 App 更顺手,打开 App 继续。这时候:
- H5 端显示"支付中",App 端显示什么?
- 如果用户在 App 端付了钱,H5 端怎么知道?
- 如果用户在 H5 端付了钱,App 端的订单状态要不要跟着变?
答案很明确:不管用户在哪个端付的钱,订单状态必须全局一致。因为订单是同一个,账是同一本,不能 H5 说"已支付"、App 说"待支付",那用户会疯。
要做到这一点,核心就是订单状态只存一份,存在后端,所有端都从后端读。前端永远不自己"持有"状态,只是"展示"后端给的状态。这样不管用户切到哪个端,读到的都是同一份,就不会打架。
这也是为什么前面反复强调"状态变更只能由回调触发、前端只做展示"——因为只有把状态收拢在后端,多端才能保持一致。
多端打包时,参数和密钥的隔离要提前设计
UniApp 打包成多个端之后,有一个工程层面的坑,很多人到上线了才发现:不同端可能需要不同的密钥和配置。
举个最典型的例子。微信小程序端下单需要 openid,H5 端不需要;App 端可能要走不同的通道;抖音小程序和微信小程序,登录体系、openid 又不一样。如果你把所有的密钥、商户号、通道参数都写死在一份配置里打包,那某一个端就会拿着错误的参数去下单。
所以,在你开始写支付代码之前,就要先想清楚配置怎么按端隔离。我的建议是:
- 后端维护一份"端 → 配置"的映射,前端下单时带上自己的端标识(比如
platform: 'h5'或'wxmp')。 - 后端根据端标识,取对应的密钥和通道参数,再去易支付下单。
- 密钥永远只放后端,不打包进前端。前端只传业务参数,不碰密钥。
这个设计一旦做对了,后面加新的端(比如再加个抖音小程序)就是加一行配置的事,而不是改一堆代码。
一个能直接照做的落地流程
把上面这些串起来,一个完整的 UniApp 多端收款流程是这样的:
- 第一步:用户下单。前端把商品信息、金额、端标识传给后端,后端生成一笔本地订单,状态"待支付"。
- 第二步:后端去易支付下单。后端按端标识取配置,拼参数、做 MD5 签名,调易支付下单接口,拿到支付链接(或支付参数)。
- 第三步:前端发起支付。H5 跳转链接,小程序调 JSAPI,App 内嵌 webview。
- 第四步:用户付款。用户在支付页面完成付款。
- 第五步:后端收回调。易支付回调打到 notify 地址,后端验签、幂等、核对金额,通过后把订单改成"已支付"。
- 第六步:后端主动查询兜底。定时任务轮询查询接口,补上可能丢失的回调。
- 第七步:前端轮询确认。用户回到页面,前端轮询后端订单状态,拿到"已支付"后展示成功。
这七步里,第五、六步是后端的事,第七步是前端的事,第三步因端而异。把每一层的职责划清楚,代码就不会乱成一团。
几个真实场景的坑,提前说给你
最后说几个我在实际项目里反复遇到的坑,都是"看着简单、一踩就疼"的那种。
坑一:小程序拿 openid 的时机不对。很多人把"拿 openid"和"下单"放在一起做,结果用户还没登录、code 过期了,openid 拿到的是空值,下单直接失败。正确做法是:用户进入小程序就先静默登录拿 openid,缓存起来,下单时直接复用,别等到下单那一刻才去现拿。
坑二:H5 跳转支付后,回来找不到订单。原因是支付链接里没带好"回跳参数",或者 return 地址配错了,导致用户付完钱跳回来,前端不知道是哪笔订单。解决方法是:下单时把本地订单号作为参数透传,回跳时带上,前端拿到订单号再去轮询状态。
坑三:回调验签老失败,查了半天是编码问题。中文参数(比如商品名"会员卡")在拼接签名前没做 URL 编码,导致服务端算出来的 sign 和易支付算的不一致。这种问题特别隐蔽,因为肉眼看起来两个字符串"差不多",实际字节不一样。
坑四:App 端 webview 里支付被拦截。有些 App 内嵌 webview 打开第三方支付页,会被系统或安全策略拦。遇到这种情况,优先排查 webview 的 user-agent 和 referer 设置,很多时候是这两个字段没带对,被支付页当成了"非正常浏览器"。
这四个坑,每一个都能让你卡上半天。提前知道,遇到了就能快速定位,不至于无从下手。
关于"多端"最后想说的话
UniApp 让你用一套代码覆盖多端,这是它的价值;但支付状态同步这件事,恰恰需要你反过来——把"端"的差异隔离在前端,把"状态"的统一收敛在后端。前端负责把用户带到付款这一步,后端负责把"付没付"这件事管住。这个边界划清楚了,多端收款就不是什么难事,反而是一套很清爽的架构。
别怕多端复杂,怕的是你把调用和同步搅在一起。分开想,分开做,每一块都不难。