☰
H5支付跳转实战:通过tn构造paydata并唤起云闪付
2026/9/26 14:51:26 网站建设 项目流程

1. 从一个真实需求说起:为什么“tn唤起云闪付”值得单独写一篇

移动端H5支付跳转这件事,做过的人都知道,坑不比功能少。尤其是当业务方丢过来一个需求——“用户点一下按钮,直接拉起云闪付完成付款”——你打开文档一看,关键词就三个:tn、scheme、paydata。看起来简单,实际上从参数拼接到端内兼容,每一步都有细节。

这篇内容围绕的核心就是:如何通过tn参数构造paydata,再借助scheme协议在H5环境中唤起云闪付。它解决的是移动端网页支付场景下,用户不想手动打开App、不想扫码、不想复制卡号,只想“点一下就走”的体验问题。适合正在做聚合支付、H5收银台、App内嵌WebView支付的前端和全栈同学参考,也适合刚接触支付跳转、对scheme和paydata还没什么概念的新手。

先把三个关键词的关系理清楚,不然后面全是糊涂账:

  • tn:交易流水号,可以理解成这笔订单在支付系统里的“身份证号”。它由支付网关生成,商户侧拿到之后,后续所有跳转和查询都围绕它展开。
  • paydata:支付数据包,通常是对tn及其他参数做编码、签名后得到的一串内容。它不是简单地把tn拼在URL后面,而是需要按渠道要求组装。
  • scheme:移动端用来唤起本地App的协议格式,云闪付有自己的scheme前缀,H5页面通过location跳转或a标签点击触发。

这三者的关系,用一句话概括:tn是原料,paydata是加工后的半成品,scheme是送货的通道。通道不对,半成品再好也送不到;原料不对,加工出来也是废品。

我见过不少团队在这件事上翻车,不是因为技术多难,而是因为把“浏览器里能跳”和“App里能跳”当成了一回事。实际上,iOS的Safari、Android的Chrome、微信内置浏览器、各家的WebView,对scheme的处理策略完全不同。下面我从整体设计思路开始拆,把每一步的为什么讲透。

2. 整体设计思路与方案选型拆解

2.1 为什么不是直接跳转,而要经过paydata

很多人第一反应是:既然tn是订单号,那直接拼一个cloudpay://pay?tn=xxx不就行了?理论上某些渠道确实支持,但实际生产环境里,直接暴露tn的跳转方式有几个硬伤。

第一,安全性。tn本身是明文流水号,如果直接放在URL里,中间被截获或者被恶意替换,用户可能跳到错误的订单上。paydata的作用之一就是加入签名和校验字段,让支付渠道能确认“这个跳转请求确实来自合法商户”。

第二,参数扩展性。一笔支付不只是tn,还可能带金额、币种、商户号、回调地址、终端信息。这些参数如果全部裸拼在scheme里,长度不可控,而且不同渠道对参数顺序和编码要求不一样。paydata把这些内容打包成一个整体,渠道侧按约定解析,商户侧不用关心底层细节。

第三,兼容性。云闪付在不同版本、不同入口下,对scheme后面跟的参数格式要求有差异。paydata相当于一层适配层,把差异屏蔽掉。

所以整体思路是:商户服务端生成tn → 按渠道规则组装paydata → 前端拿到paydata后构造scheme URL → 触发跳转 → 云闪付解析并完成支付 → 回跳商户页面。这条链路里,前端只负责“跳”,服务端负责“算”,职责边界要清晰。

2.2 scheme跳转在H5里的三种触发方式对比

在H5里触发scheme,常见做法有三种,我分别说一下适用场景和坑。

触发方式写法优点缺点适用场景
location.hrefwindow.location.href = schemeUrl兼容性最好,代码简单部分浏览器会弹确认框,iOS可能无响应普通浏览器、App内WebView
a标签点击<a href="schemeUrl">支付</a>用户手势触发,成功率较高需要用户真实点击,不能程序自动触发需要用户确认的支付入口
iframe动态创建iframe设置src不跳离当前页现代浏览器限制多,基本已废弃不推荐

实测下来,a标签点击 + location.href兜底是最稳的组合。具体做法是:页面上放一个真实的可点击元素,用户点击后先尝试location.href,同时设置一个定时器,如果一段时间后页面没有进入后台(说明跳转失败),再给出“请手动打开云闪付”的提示。

注意:不要在页面加载时自动触发scheme跳转,很多浏览器会拦截,而且用户体验很差。支付动作必须由用户主动触发。

2.3 服务端组装paydata还是前端组装

这个问题我踩过坑。早期为了图方便,前端直接拿tn拼paydata,结果渠道升级参数规则后,前端发版跟不上,线上支付挂了半天。后来改成服务端组装,前端只拿最终串,问题少了很多。

服务端组装的好处:

  • 签名密钥不暴露在前端,安全性高
  • 参数规则变更只需服务端发版,前端无感
  • 可以对paydata做缓存和复用,减少重复计算
  • 方便做灰度,不同用户拿到不同版本的paydata

前端组装唯一的好处是少一次接口请求,但在支付场景下,这一次请求的耗时完全可以接受。所以我的建议很明确:paydata必须服务端生成,前端只负责跳转。

3. 核心细节解析与实操要点

3.1 tn的获取与校验:别拿到就用

tn通常由支付网关在创建订单后返回。服务端调用下单接口,拿到tn之后,不要直接透传给前端,先做几件事:

  • 校验tn格式是否符合渠道规范,比如长度、字符集
  • 确认tn对应的订单金额、商户号与当前业务一致
  • 记录tn与业务订单号的映射关系,方便后续对账和排查
  • 设置tn的有效期,过期后重新下单

我遇到过一种情况:测试环境拿到的tn在生产环境用不了,因为两个环境的商户号和密钥不同。所以环境隔离一定要做好,配置项不要写死。

3.2 paydata的组装规则与编码陷阱

paydata的组装规则各渠道略有差异,但大体流程是:

  1. 收集参数:tn、商户号、金额、币种、回调地址、时间戳、随机串
  2. 按key字典序排序
  3. 拼接成key=value&key=value形式
  4. 加上商户密钥做签名
  5. 将签名和原参数一起做URL编码或Base64编码
  6. 得到最终paydata

这里最大的坑是编码。不同渠道对paydata的编码要求不同,有的要求URL Encode一次,有的要求两次,有的要求Base64后再URL Encode。编码错了,云闪付解析出来就是乱码,直接报“参数错误”。

我的做法是:在服务端写一个专门的paydata组装函数,把编码规则做成配置项,不同渠道走不同分支。同时写单元测试,用固定的输入验证输出,渠道规则变更时先跑测试。

另一个坑是时间戳。有些渠道要求时间戳精确到秒,有些要求毫秒,还有些要求特定时区。时间戳不对,签名验证会失败。建议统一用服务端时间,并且和渠道文档核对清楚格式。

3.3 scheme URL的构造与转义

拿到paydata后,构造scheme URL的格式通常是:

cloudpay://pay?paydata=xxx

或者:

cloudpay://gateway?data=xxx

具体前缀和参数名以渠道文档为准。这里要注意:

  • paydata里如果包含特殊字符,需要做URL编码后再拼到scheme里
  • 整个scheme URL不要做二次编码,否则云闪付解析不了
  • 有些渠道要求scheme后面跟的路径区分大小写,不要想当然

我一般会在服务端直接返回完整的scheme URL,前端拿到后直接跳,减少前端拼错的可能。如果前端一定要自己拼,务必用encodeURIComponent处理paydata部分。

3.4 回跳地址的处理:支付完成后怎么回来

支付完成后,云闪付会尝试回跳商户页面。回跳地址一般在paydata里指定,或者在scheme里带callback参数。这里有几个细节:

  • 回跳地址必须是公网可访问的URL,不能是localhost
  • 回跳地址要做URL编码,否则参数会丢
  • 回跳后页面要能识别支付结果,通常通过URL参数里的订单号或状态码判断
  • 如果用户没有安装云闪付,回跳不会发生,要有兜底提示

我建议回跳地址指向一个专门的支付结果页,页面上轮询服务端查询订单状态,而不是单纯依赖URL参数。因为URL参数可能被篡改,而且有些渠道回跳时不带状态。

4. 实操过程与核心环节实现

4.1 服务端生成tn与paydata的完整流程

假设我们有一个支付下单接口,服务端处理逻辑如下:

import hashlib import time import urllib.parse import base64 def generate_tn(order_id, amount): # 调用支付网关下单接口,获取tn # 这里用伪代码表示 tn = payment_gateway.create_order(order_id, amount) return tn def build_paydata(tn, merchant_id, amount, currency, callback_url, secret_key): params = { 'tn': tn, 'merchantId': merchant_id, 'amount': str(amount), 'currency': currency, 'callbackUrl': callback_url, 'timestamp': str(int(time.time())), 'nonce': 'abc123xyz' } # 按key字典序排序 sorted_keys = sorted(params.keys()) sign_str = '&'.join([f'{k}={params[k]}' for k in sorted_keys]) # 加上密钥做签名 sign_str_with_key = sign_str + '&key=' + secret_key signature = hashlib.md5(sign_str_with_key.encode('utf-8')).hexdigest().upper() params['sign'] = signature # 拼接成paydata paydata_raw = '&'.join([f'{k}={params[k]}' for k in sorted(params.keys())]) # URL编码 paydata = urllib.parse.quote(paydata_raw, safe='') return paydata def build_scheme_url(paydata): return f'cloudpay://pay?paydata={paydata}'

这段代码的关键点:

  • 签名前先排序,保证顺序一致
  • 签名用MD5还是SHA256看渠道要求,不要自己选
  • paydata做URL编码时,safe=''表示所有特殊字符都编码
  • 最终scheme URL里paydata已经是编码后的,不要再编码

4.2 前端H5跳转的完整实现

前端拿到scheme URL后,跳转逻辑如下:

function launchCloudPay(schemeUrl) { // 记录跳转前时间 var startTime = Date.now(); // 尝试跳转 window.location.href = schemeUrl; // 设置定时器检测是否跳转成功 var timer = setTimeout(function() { var endTime = Date.now(); // 如果时间差小于2000毫秒,说明页面没有进入后台,跳转可能失败 if (endTime - startTime < 2000) { // 显示提示,引导用户手动打开 showFallbackTip(); } }, 2000); // 页面进入后台时清除定时器 document.addEventListener('visibilitychange', function() { if (document.hidden) { clearTimeout(timer); } }); } function showFallbackTip() { // 弹出一个提示层,告诉用户手动打开云闪付 var tip = document.getElementById('fallback-tip'); if (tip) { tip.style.display = 'block'; } }

这段代码的意图:

  • location.href触发scheme跳转
  • 用时间差判断是否跳转成功,因为跳转成功后页面会进入后台,定时器可能不执行
  • visibilitychange监听页面可见性变化,跳转成功时清除定时器
  • 跳转失败时显示兜底提示,避免用户卡在空白页

提示:2000毫秒不是固定值,可以根据实际体验调整。太短会误判,太长用户等待久。我一般用1500到2500之间。

4.3 不同环境的兼容处理

环境表现处理方式
iOS Safari可能弹“是否打开云闪付”确认框正常,用户确认即可
Android Chrome通常直接跳转正常
微信内置浏览器可能拦截scheme引导用户用外部浏览器打开
App内WebView取决于App是否允许scheme需要App侧配置白名单
云闪付未安装无响应或报错显示下载引导

微信内置浏览器是重灾区。微信对scheme跳转限制很严,基本跳不了。我的做法是:在微信里检测到需要跳转云闪付时,直接显示一个遮罩层,引导用户“点击右上角,在浏览器中打开”。虽然体验差一点,但比用户点了没反应好。

App内WebView的情况,需要和客户端同学沟通,把云闪付的scheme加入WebView的URL白名单,否则会被拦截。这个不是前端能单独解决的。

4.4 支付结果查询与订单状态同步

跳转云闪付后,商户服务端不能干等回跳。正确的做法是:

  1. 用户发起支付时,服务端记录订单状态为“支付中”
  2. 前端跳转后,页面开始轮询服务端查询订单状态
  3. 服务端收到渠道异步通知后,更新订单状态为“已支付”
  4. 前端轮询到“已支付”后,展示成功页面

轮询频率建议:前10秒每1秒一次,之后每3秒一次,最多轮询30次。超过30次还没结果,提示用户“支付结果确认中,请稍后查看订单”。

这里有个坑:有些渠道的异步通知有延迟,可能几分钟后才到。所以前端轮询要有超时机制,不能无限等。同时服务端要提供主动查询接口,定时补偿查询未完成的订单。

5. 常见问题与排查技巧实录

5.1 跳转无反应怎么办

这是最高频的问题。排查顺序:

  1. 检查scheme URL是否正确,可以在手机浏览器地址栏直接输入测试
  2. 检查paydata是否编码正确,解码后看参数是否完整
  3. 检查是否在微信内,微信内基本跳不了
  4. 检查App内WebView是否允许scheme
  5. 检查云闪付是否安装,未安装肯定无反应

我一般会先在服务端打日志,把生成的scheme URL完整记录下来,然后手动在手机上测试。如果手动能跳,说明代码逻辑没问题,是环境问题;如果手动也跳不了,说明URL本身有问题。

5.2 报“参数错误”或“签名验证失败”

这类问题通常是paydata组装有问题。排查点:

  • 参数排序是否正确
  • 签名算法是否和渠道一致
  • 编码方式是否匹配
  • 时间戳是否在有效期内
  • 商户密钥是否用对

我遇到过一次,渠道要求签名用SHA256,但代码里写的是MD5,结果一直报签名失败。后来对着文档逐字核对才发现。所以渠道文档一定要仔细看,不要凭经验想当然。

5.3 支付完成后没有回跳

回跳失败的原因:

  • 回跳地址没有做URL编码
  • 回跳地址不是公网地址
  • 渠道不支持回跳,只能靠异步通知
  • 用户手动关闭了云闪付

我的建议是:不要强依赖回跳,以服务端异步通知为准。回跳只是提升体验,不是支付结果的唯一来源。

5.4 常见问题速查表

问题现象可能原因解决方法
点击无反应scheme错误/环境限制检查URL,换浏览器测试
报参数错误paydata编码或排序错误核对渠道文档,检查编码
签名失败算法或密钥错误确认签名算法和密钥
不回跳回跳地址问题检查编码和公网可达性
重复支付订单状态未同步服务端做幂等处理
轮询超时异步通知延迟增加补偿查询机制

5.5 几个我踩过的坑

第一个坑:测试环境用生产密钥。有次排查半天,发现测试环境配置里密钥写成了生产的,导致签名一直失败。后来加了环境校验,启动时检查密钥前缀。

第二个坑:paydata长度超限。某些渠道对scheme URL总长度有限制,paydata太长会被截断。解决办法是精简参数,只传必要字段,或者用短链接中转。

第三个坑:iOS上location.href被拦截。iOS的Safari对非用户手势触发的跳转限制很严,必须放在click事件里同步执行。如果中间有异步请求,跳转就会失效。所以scheme URL要提前准备好,不要在click里现请求。

第四个坑:Android WebView返回后页面白屏。跳转云闪付再回来,WebView可能重新加载页面,导致状态丢失。解决办法是用sessionStorage保存支付状态,页面加载时先读缓存。

6. 一些延伸思考与个人体会

这套方案跑通之后,我最大的体会是:支付跳转的稳定性,不取决于代码写得多漂亮,而取决于对异常路径的覆盖有多全。正常流程谁都能写,但用户没装App、在微信里打开、网络断了、跳转被拦截,这些情况才是真正考验实现质量的地方。

另外,tn和paydata的生成一定要放在服务端,前端只做跳转。这个边界划清楚之后,后面渠道升级、参数调整,前端基本不用动。我见过把签名逻辑放前端的项目,渠道一改规则就要发版,运维成本很高。

如果后续要扩展,可以考虑把这套逻辑封装成一个独立的支付跳转服务,对外提供统一的“创建支付并跳转”接口,内部适配不同渠道。这样新接一个渠道时,只需要在服务端加一个适配器,前端完全无感。对于多渠道聚合支付场景,这种架构会省很多事。

最后分享一个小技巧:在scheme URL里加一个_t参数,值为当前时间戳。这样每次跳转的URL都不一样,可以避免某些浏览器或WebView的缓存问题。虽然是个小改动,但实测能减少一些莫名其妙的跳转失败。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询