
1. 从 tn 到 paydata移动支付链接转换的核心逻辑移动端 H5 页面里拉起云闪付完成支付这件事看起来只是“点一下按钮跳过去”但真正做过支付接入的人都知道中间最麻烦的往往不是支付本身而是参数怎么传、链接怎么转、不同环境怎么兼容。标题里提到的tn、scheme、paydata其实就是这条链路里最关键的三个角色。先说tn。在银联体系里tn是交易流水号Transaction Number它由银联侧生成用来唯一标识一笔交易。你在接入云闪付支付时后端调用下单接口银联返回的核心字段之一就是tn。这个值本身不是链接也不是可以直接丢给浏览器跳转的 URL它更像是一张“取票凭证”。用户拿着这张凭证通过特定的入口进入云闪付云闪付再根据tn去查询这笔交易的详情最终完成付款。那scheme是什么简单理解scheme就是 App 之间互相唤起的“暗号”。比如alipays://是支付宝的 schemeupwrp://或uppay://是云闪付相关的 scheme。H5 页面本身运行在浏览器里浏览器没有权限直接打开另一个 App但可以通过访问一个符合规范的 scheme 链接让操作系统识别并尝试唤起对应 App。这就是所谓的“唤起”。paydata则是把tn和其他必要参数打包后形成的一个数据体。不同渠道对paydata的格式要求不一样有的要求 Base64 编码有的要求 JSON 字符串再做 URL Encode还有的要求直接拼接成 query string。标题里说“tn 转链接 paydata”本质上就是拿到后端返回的 tn按照云闪付要求的格式组装成 paydata再拼成 scheme 链接最终在 H5 里触发跳转。这条链路解决的核心问题是H5 页面无法直接调用云闪付 SDK但业务又需要在网页里完成支付。适合谁来参考主要是三类人一是做 H5 收银台的前端二是做聚合支付的后端三是需要在自己 App 内嵌 H5 里接入云闪付的移动端开发。只要你的场景里出现“网页里拉起云闪付”这套逻辑就绕不开。我见过不少团队一开始以为直接把tn拼到某个 URL 后面就行结果测试时发现安卓能跳、iOS 没反应或者云闪付打开了却提示“交易不存在”。问题基本都出在paydata的组装格式和 scheme 的兼容处理上。下面我按实际项目里的做法把整条链路拆开讲清楚。2. 核心细节解析tn、scheme、paydata 到底怎么配合2.1 tn 的获取时机与后端职责边界tn不是前端生成的也不是随便编一个就能用。它的来源只有一个后端调用银联的下单接口银联返回。这里有一个很容易踩的坑——下单和支付是两步。很多新手会以为调了下单接口用户就付钱了其实下单只是“占了一个交易号”用户还没付款。真正的扣款发生在云闪付 App 内用户确认之后。后端在下单时通常需要传这些信息商户号、订单号、金额、交易时间、回调地址、商品描述等。银联返回的报文里tn一般在tn字段或者respCode为成功后的业务字段里。后端拿到tn之后不应该直接把它丢给前端就完事因为前端还需要知道“用哪个 scheme 跳”“paydata 怎么拼”。比较稳妥的做法是后端把tn和组装好的paydata一起返回或者后端直接返回一个完整的跳转链接前端只负责触发。我个人的经验是能后端做的就不要让前端做。原因很简单paydata的组装规则可能会因为渠道版本变化而调整如果散落在前端代码里改起来要发版放在后端改完直接生效。而且paydata里可能包含签名或敏感字段放前端有泄露风险。2.2 scheme 的格式与常见变体云闪付的 scheme 在不同场景下写法有差异。常见的有这几种形态uppay://uppayment/pay?...这类是云闪付 App 的标准支付入口upwrp://开头的是银联钱包相关的唤起协议有些渠道会要求用https://开头的中间页再由中间页 302 到 scheme为什么会有这么多变体因为云闪付 App 本身在安卓和 iOS 上的 URL Scheme 注册不完全一致而且不同版本的 App 对参数解析的严格程度也不同。安卓上相对宽松iOS 上如果 scheme 没注册或者参数格式不对系统会直接静默失败用户点了没反应控制台也不一定报错。这里有个实操细节iOS 上 scheme 唤起必须在用户手势的同步调用栈里触发。什么意思就是你不能在setTimeout或者异步请求的回调里直接location.href scheme否则 iOS 会认为这不是用户主动行为从而拦截。正确的做法是用户点击按钮后先同步触发跳转或者用一个隐藏的iframe来承载 scheme。不过现在很多浏览器对 iframe 方式也有限制所以更稳的方案是点击按钮时直接window.location.href schemeUrl如果需要先请求后端拿参数那就提前把参数准备好点击时直接用。2.3 paydata 的组装规则与编码陷阱paydata是整条链路里最容易出错的地方。它的本质是一个字符串里面包含了tn以及其他渠道要求的字段。常见的组装方式有两种第一种是直接拼接tn1234567890mid商户号...第二种是 JSON 后 Base64const paydata btoa(JSON.stringify({ tn: 1234567890, mid: xxx }));然后把这个paydata作为参数拼到 scheme 后面uppay://uppayment/pay?paydataxxxxx坑在哪里URL Encode 的次数。有些渠道要求paydata先 Base64再 URL Encode 一次有些要求 Base64 后直接拼不能再 Encode还有些要求 Encode 两次。如果你 Encode 次数不对云闪付打开后会提示“参数错误”或者“交易不存在”。我遇到过最离谱的一次是后端 Encode 了一次前端拿到后又 Encode 了一次结果云闪付解析出来是乱码。另一个坑是字符集。Base64 之前一定要确认是 UTF-8如果后端用的是 GBK前端用btoa会直接报错因为btoa只支持 Latin-1 字符。这时候需要先用encodeURIComponent处理再 Base64或者用TextEncoder转成 Uint8Array 再 Base64。2.4 H5 环境下的兼容性差异H5 拉起云闪付在不同容器里表现完全不同环境表现注意事项微信内置浏览器通常无法直接唤起需要引导外部打开微信会拦截 scheme建议提示用户用浏览器打开支付宝内置浏览器可能被拦截视版本而定不建议在支付宝内拉起云闪付手机自带浏览器一般可以正常唤起iOS Safari 需要用户手势触发App 内嵌 WebView取决于 WebView 配置需要原生侧允许 scheme 跳转云闪付 App 内 H5可以直接跳转但场景较少微信里是最麻烦的。微信对 scheme 的拦截比较严格普通 H5 里直接跳基本没戏。常见的做法是做一个中间页提示用户“点击右上角在浏览器中打开”然后在外部浏览器里再触发 scheme。这个体验不算好但合规且稳定。3. 实操过程从 tn 到成功唤起云闪付的完整链路3.1 后端下单并返回 paydata假设后端已经调通了银联下单接口拿到了tn。接下来后端需要组装paydata。以下是一个常见的组装示例以某渠道要求 Base64 URL Encode 为例// Node.js 示例 const payload { tn: 202401011234567890, mid: 898110158110001, // 其他渠道要求的字段 }; const jsonStr JSON.stringify(payload); const base64Str Buffer.from(jsonStr, utf-8).toString(base64); const paydata encodeURIComponent(base64Str); // 最终返回给前端 res.json({ code: 0, data: { paydata: paydata, scheme: uppay://uppayment/pay?paydata${paydata} } });这里为什么要用Buffer.from而不是btoa因为 Node.js 环境里btoa对中文支持不好Buffer更稳。前端如果要做同样的处理可以用function toBase64(str) { const bytes new TextEncoder().encode(str); let binary ; bytes.forEach(b binary String.fromCharCode(b)); return btoa(binary); }3.2 前端触发 scheme 跳转前端拿到scheme后不要急着直接跳。先判断当前环境function isWechat() { return /MicroMessenger/i.test(navigator.userAgent); } function isIOS() { return /iPhone|iPad|iPod/i.test(navigator.userAgent); } function openUnionPay(schemeUrl) { if (isWechat()) { // 微信内提示外部打开 showGuideMask(); return; } if (isIOS()) { // iOS 必须在用户手势同步栈里触发 window.location.href schemeUrl; } else { // 安卓可以用 iframe 兜底 const iframe document.createElement(iframe); iframe.style.display none; iframe.src schemeUrl; document.body.appendChild(iframe); setTimeout(() { document.body.removeChild(iframe); }, 2000); } }安卓用 iframe 的原因是部分安卓浏览器直接改location.href会弹出一个“是否打开云闪付”的确认框如果用户点了取消页面可能白屏。用 iframe 可以避免页面跳走同时也能触发唤起。但注意现在很多现代浏览器对 iframe 唤起也有限制所以更稳的做法还是直接location.href然后设置一个定时器检测页面是否隐藏如果没隐藏说明唤起失败再引导用户下载或换方式。3.3 唤起失败的兜底与检测唤起成功和失败的判断在 H5 里没有标准事件。常用的检测手段是visibilitychangelet hasHidden false; document.addEventListener(visibilitychange, () { if (document.hidden) { hasHidden true; } }); // 触发跳转后 setTimeout(() { if (!hasHidden) { // 说明没有跳走唤起失败 showDownloadGuide(); } }, 2500);这个 2500ms 是经验值。太短了可能云闪付还没启动完太长了用户等得着急。我实测下来安卓中低端机可能需要 3 秒iOS 一般 1.5 秒内就有反应。所以可以做成 2 秒开始检测3 秒还没反应就提示。3.4 回调与订单状态确认用户跳去云闪付付款付完之后云闪付会回调后端配置的notifyUrl。前端这边不能只依赖用户返回页面来判断支付成功因为用户可能付完直接杀进程。正确的做法是后端收到回调后更新订单状态前端在用户返回页面时轮询后端订单状态接口轮询间隔建议 1.5 秒一次最多轮询 10 次如果轮询到成功跳转成功页如果超时提示“支付结果确认中请稍后查看订单”这里有个细节轮询接口要做防重放和签名校验不能只传订单号就返回状态否则容易被刷。4. 常见问题与排查技巧实录4.1 云闪付打开了但提示“交易不存在”这是最高频的问题。原因通常有三个tn过期了。银联的tn一般有有效期常见是 30 分钟超过后云闪付查不到交易paydata组装格式不对云闪付解析不出tn下单和唤起用的不是同一个商户环境比如下单是测试环境唤起是生产环境排查顺序先看后端下单返回的tn是否还在有效期再抓包看paydata解码后内容是否正确最后确认环境一致性。4.2 iOS 点击没反应iOS 上最常见的原因是异步回调里触发 scheme。比如// 错误做法 btn.onclick async () { const res await fetch(/api/getPayData); const data await res.json(); window.location.href data.scheme; // iOS 会拦截 };正确做法是提前把scheme拿到点击时直接跳let cachedScheme ; // 页面加载时就请求好 fetch(/api/getPayData).then(res res.json()).then(data { cachedScheme data.scheme; }); btn.onclick () { if (cachedScheme) { window.location.href cachedScheme; } };如果必须点击后再请求那就用一个同步的a标签href先设为javascript:void(0)点击后请求回来再改href并触发click()但这种方式在 iOS 上也不一定稳。最稳的还是提前缓存。4.3 微信内无法唤起微信内直接唤起云闪付基本不可行。可行的方案是做一个遮罩层引导用户点击右上角“在浏览器打开”或者用微信的wx.openUrl相关能力需要公众号配置或者提示用户复制链接到浏览器不要试图用各种 hack 绕过微信拦截一是容易被封二是体验很差。4.4 paydata 编码后长度超限有些渠道对paydata的长度有限制比如不能超过 1024 字符。如果字段太多Base64 后会超。这时候需要精简字段只传必要的tn和商户标识。另外URL Encode 后长度会增加约 30%如果接近上限可以考虑用短链接中转。4.5 常见问题速查表问题现象可能原因解决方向点击无反应iOS 异步触发 / scheme 错误改为同步触发检查 scheme提示交易不存在tn 过期 / paydata 格式错检查有效期和编码微信内打不开微信拦截 scheme引导外部浏览器打开安卓弹框后白屏location.href 直接跳转改用 iframe 或加兜底支付成功但订单未更新回调未处理 / 轮询未做检查 notifyUrl 和轮询逻辑paydata 解析乱码字符集不是 UTF-8统一用 UTF-8 编码4.6 实操心得几个让我少加班的小技巧第一个技巧把 scheme 和 paydata 的组装做成配置化。不同渠道的格式要求不一样如果硬编码在代码里每接一个渠道就要改一次。我一般会定义一个渠道配置表把 scheme 前缀、编码方式、参数字段名都放在配置里新增渠道只加配置不改逻辑。第二个技巧在测试环境准备一个“模拟唤起页”。因为云闪付的测试环境不一定随时可用我通常会做一个页面把scheme打印出来同时提供“复制 scheme”“手动跳转”按钮方便排查是参数问题还是唤起问题。第三个技巧日志要打全。前端在触发 scheme 前把scheme、paydata、userAgent、时间戳都上报到日志服务。这样用户反馈“付不了”的时候你能快速定位是哪个环节出了问题而不是靠猜。第四个技巧金额单位要确认。银联下单金额一般是分不是元。我见过有团队传了“1”以为是 1 元结果用户付了 1 分测试时没发现上线后才发现对账不平。这种低级错误一旦发生排查起来非常痛苦。5. 不同场景下的方案选型与扩展思路5.1 纯 H5 收银台 vs App 内嵌 H5纯 H5 收银台的特点是运行在浏览器里没有原生能力只能靠 scheme。这种场景下兼容性处理是重点尤其是微信和 iOS。App 内嵌 H5 则不同原生侧可以拦截 URL 请求识别到特定的 scheme 后直接调用原生模块唤起云闪付甚至可以直接集成云闪付 SDK。如果你们有自己的 App强烈建议走原生拦截方案稳定性和体验都会好很多。原生拦截的做法是WebView 设置shouldOverrideUrlLoading当 URL 以uppay://开头时不加载这个 URL而是取出参数调用原生云闪付 SDK。这样就不依赖系统 scheme 唤起了成功率接近 100%。5.2 多域名下的 H5 分发问题热搜词里提到了“uniapp 封装 h5 如何指向 2 个域名”这其实和支付链路也有关。有些团队会把收银台部署在多个域名下比如主站域名和备用域名。这时候要注意下单时配置的回调域名必须和实际访问域名一致否则银联回调可能被跨域拦截。另外如果用了 CDN要确保 scheme 跳转不被 CDN 的中间页拦截。5.3 与小程序跳转的对比小程序里拉起云闪付又是另一套逻辑。小程序不能直接用 scheme需要通过wx.navigateToMiniProgram或者云闪付提供的小程序跳转能力。如果你们同时有 H5 和小程序建议把支付参数组装逻辑抽成公共模块两端共用只是跳转方式不同。5.4 安全与合规注意事项支付链路涉及资金安全不能马虎。几个基本要求paydata里的敏感字段要加密或签名不能明文传回调接口要验签防止伪造回调订单状态查询接口要做权限校验不能凭订单号就能查前端不要存储tn或paydata到 localStorage用完即弃另外云闪付的接入需要商户资质个人开发者一般拿不到。如果你是在做聚合支付要确保上游渠道是合规的不要接来路不明的通道。5.5 性能与体验优化最后说几个体验上的优化点。第一预下单。用户进入收银台时就可以先调下单接口把tn和scheme准备好用户点击时直接跳减少等待。第二骨架屏。唤起云闪付需要时间页面上给个 loading 或者“正在打开云闪付”的提示避免用户以为卡死。第三失败引导。唤起失败时不要只提示“失败”要给具体指引比如“请确认已安装云闪付”“请用浏览器打开”等。我在实际项目里踩过最深的坑就是 iOS 上把 scheme 放在fetch回调里跳测试时用安卓一直没问题上线后 iOS 用户大面积反馈点不动。后来改成页面加载时预请求、点击时同步跳转问题才解决。所以如果你现在正在做这块记住一句话iOS 的 scheme 唤起必须发生在用户手势的同步调用栈里这一条能帮你省掉很多排查时间。