1. 项目概述:从一串“tn”字符串到成功唤起云闪付支付的完整链路
“云闪付tn转链接,tn转url拉起云闪付支付——总算搞起来了”,这句话背后不是一句轻飘飘的感叹,而是一次典型的支付网关侧深度集成实战。我做支付系统对接十年,经手过银联、支付宝、微信、京东、PayPal等二十多个主流通道,但每次遇到“tn”这个字段,依然要重新翻文档、抓包、验签、调时序——它不像微信的prepay_id或支付宝的pay_order_id那样直白,而是一个高度封装、强时效、带签名的银联交易凭证。所谓“tn转url”,本质是把银联标准的Transaction Number(TN)通过银联开放平台提供的URL生成服务,构造成一个可被浏览器或App识别并跳转至云闪付客户端的scheme链接(如upay://...或upac://...),最终触发云闪付App完成支付确认。整个过程涉及RSA非对称加密验签、URL编码与解码、协议头构造、客户端唤起机制、超时与失败兜底策略五大核心环节。如果你正在开发电商H5、小程序跳转页、uni-app混合应用,或是为银行/商户后台构建统一支付中台,那么你大概率会卡在这个环节:后端生成了tn,前端却怎么也唤不起云闪付;或者唤起了,但提示“参数错误”“签名无效”“商户未授权”。这不是前端写错了一行JS,也不是后端少传了一个字段,而是银联支付协议在Web与Native边界上的一次精密咬合。本文不讲抽象概念,只复盘我在线上环境实测通过的完整路径:从tn原始值出发,到生成可点击、可分享、可埋点、可监控的最终拉起URL,每一步都附带真实参数、编码陷阱、验签逻辑和安卓/iOS双端兼容要点。适合支付开发工程师、全栈开发者、以及需要快速落地云闪付接入的中小商户技术负责人。
2. 核心原理拆解:为什么tn不能直接用?为什么必须“转”?
2.1 TN的本质:不是ID,而是银联交易会话的加密信封
很多人误以为tn就是一个数据库主键ID,类似订单号。这是最大的认知偏差。实际上,tn是银联网关在受理一笔支付请求后,返回给商户系统的、经过RSA私钥签名的结构化数据载荷(payload)的Base64编码字符串。它内部包含但不限于以下关键字段:
merId:商户在银联的唯一编号(15位数字)orderId:商户侧订单号(建议与业务订单号一致,便于对账)txnTime:交易时间戳(精确到秒,格式yyyyMMddHHmmss)txnAmt:交易金额(单位:分,整数字符串)currencyCode:币种(默认156,人民币)reqReserved:商户自定义透传字段(Base64编码,最大1024字节)sign:上述所有字段按字典序拼接后,用银联下发的RSA私钥进行SHA256withRSA签名的Base64结果
提示:tn本身不包含任何明文敏感信息(如卡号、密码),但它是一个“一次一密”的会话凭证。它的有效期极短——官方文档明确要求5分钟内必须使用,超时则银联网关拒绝受理。这也是为什么你看到tn生成后立刻刷新页面就失效的原因。
2.2 为什么不能直接用tn唤起云闪付?——协议层的三重隔离
直接拿tn字符串去拼upay://pay?tn=xxx是行不通的,原因有三:
协议头缺失:云闪付App注册的Android Intent Scheme或iOS Universal Link,并不识别裸tn。它只响应银联标准的
upac://或upay://协议,且该协议URL必须携带tn、sign、encoding、timestamp等至少4个强制参数,缺一不可。签名验证闭环:银联要求,用于唤起App的URL中携带的
sign,必须是对整个URL查询参数字符串(不含协议头和域名)按字典序拼接后,再用银联公钥验签。这个sign和tn内部的sign是两套独立签名,前者用于校验URL未被篡改,后者用于校验tn本身合法性。很多开发者混淆这两者,导致“tn有效但URL唤起失败”。编码安全要求:tn原始值含
+、/、=等特殊字符,在URL中会被浏览器自动转义或截断。例如,tn中常见的+在URL中会被当作空格处理,导致Base64解码失败。因此,tn必须经过两次URL编码:第一次是标准encodeURIComponent,第二次是对已编码结果中的%符号再做一次编码(即%→%25),这是银联开放平台明确规定的“双重编码”规则,也是90%失败案例的根源。
2.3 “转URL”的本质:构造一个银联认证的、可执行的支付指令包
所谓“tn转url”,就是将原始tn作为核心载荷,嵌入银联定义的标准URL模板,并补全所有必要参数,最终生成一个符合RFC 3986规范、能被云闪付App正确解析的URI。这个URI不是普通链接,而是一个支付指令包(Payment Instruction Packet),其结构如下:
upac://pay?tn={double_encoded_tn}&sign={url_sign}&encoding=utf-8×tamp={unix_timestamp_ms}其中:
{double_encoded_tn}:原始tn经两次encodeURIComponent后的结果{url_sign}:对tn={double_encoded_tn}&encoding=utf-8×tamp={ts}字符串按字典序拼接后,用商户RSA私钥签名的Base64值(注意:此处用的是商户私钥,不是银联私钥!){unix_timestamp_ms}:当前毫秒级时间戳(13位数字),用于防重放攻击,银联要求与服务器时间误差不超过5分钟
注意:
upac://是银联官方推荐的、兼容性最好的协议头。upay://在部分旧版本云闪付中可能失效。iOS端还需额外配置Associated Domains,否则Universal Link无法触发App唤起。
3. 实操全流程:从后端生成到前端唤起,每一步都踩过坑
3.1 后端服务:生成可拉起URL的完整代码逻辑(以Node.js为例)
我们以Express框架为例,展示一个生产环境可用的URL生成服务。关键点在于:签名逻辑必须严格遵循银联文档,编码必须双重,时间戳必须毫秒级,且所有参数名必须小写。
const crypto = require('crypto'); const fs = require('fs'); // 1. 加载商户RSA私钥(PEM格式,需提前从银联开放平台下载) const privateKey = fs.readFileSync('./cert/merchant_private_key.pem', 'utf8'); // 2. 定义URL生成函数 function generateUpayUrl(rawTn) { const timestamp = Date.now().toString(); // 13位毫秒时间戳 const encoding = 'utf-8'; // 3. 对原始tn进行双重URL编码 const onceEncoded = encodeURIComponent(rawTn); const doubleEncoded = encodeURIComponent(onceEncoded); // 关键!第二次编码 // 4. 构造待签名字符串:按参数名字典序拼接,key=value&key=value... // 注意:参数名必须小写,且顺序为 encoding, tn, timestamp const signString = `encoding=${encoding}&tn=${doubleEncoded}×tamp=${timestamp}`; // 5. 使用商户RSA私钥进行SHA256withRSA签名 const sign = crypto.sign('sha256', Buffer.from(signString), { key: privateKey, padding: crypto.constants.RSA_PKCS1_PSS_PADDING, }).toString('base64'); // 6. 组装最终URL const url = `upac://pay?tn=${doubleEncoded}&sign=${encodeURIComponent(sign)}&encoding=${encoding}×tamp=${timestamp}`; return url; } // 7. Express路由示例 app.get('/api/upay/url', (req, res) => { const { tn } = req.query; if (!tn || tn.length < 32) { return res.status(400).json({ error: 'Invalid tn' }); } try { const upayUrl = generateUpayUrl(tn); res.json({ url: upayUrl }); } catch (err) { console.error('URL generation failed:', err); res.status(500).json({ error: 'Internal server error' }); } });实操心得:我在测试时发现,Node.js的
crypto.sign默认使用PKCS#1 v1.5填充,但银联要求PSS填充。若不显式指定padding: crypto.constants.RSA_PKCS1_PSS_PADDING,签名虽能生成,但银联验签必失败。另外,signString拼接时务必用&连接,且不能有多余空格、换行、斜杠,否则验签失败。我曾因JSON.stringify后多了一个换行符,调试了6小时。
3.2 前端唤起:H5页面中安全、可靠、兼容的唤起方案
前端拿到后端返回的upac://...URL后,不能简单window.location.href = url。原因有二:一是iOS Safari对第三方Scheme限制极严,二是Android部分厂商浏览器会拦截跳转。必须采用“降级+检测+兜底”三重策略。
// 1. 封装唤起函数 function launchUpay(url) { // 创建隐藏iframe,避免页面跳转 const iframe = document.createElement('iframe'); iframe.style.display = 'none'; iframe.src = url; document.body.appendChild(iframe); // 设置超时检测:3秒内若页面未跳转,则认为唤起失败 const timeout = setTimeout(() => { document.body.removeChild(iframe); // 检测是否仍在当前页面(即唤起失败) if (document.visibilityState === 'visible') { // 尝试降级方案:跳转至云闪付H5支付页 window.location.href = `https://95516.com/pay?tn=${encodeURIComponent(rawTn)}`; } }, 3000); // 监听页面可见性变化,更精准判断唤起结果 const handleVisibilityChange = () => { if (document.visibilityState === 'hidden') { clearTimeout(timeout); document.removeEventListener('visibilitychange', handleVisibilityChange); } }; document.addEventListener('visibilitychange', handleVisibilityChange); } // 2. 调用示例(配合后端API) async function startUpayPayment() { try { const res = await fetch('/api/upay/url?tn=' + encodeURIComponent(tn)); const { url } = await res.json(); launchUpay(url); } catch (err) { console.error('Fetch URL failed:', err); alert('获取支付链接失败,请重试'); } }实操心得:单纯依赖
visibilityState在iOS上并不完全可靠。我在线上灰度时发现,iPhone 12以上机型在唤起云闪付后,Safari有时会短暂闪一下白屏再切到App,导致visibilityState短暂变为visible。因此,必须叠加3秒超时+页面可见性+用户主动操作(如点击按钮)三重判断。另外,iframe.src赋值后立即removeChild会导致唤起失败,必须等待超时或检测到跳转后再清理。
3.3 移动端App(Android/iOS)集成:WebView内唤起的特殊处理
如果你的应用是原生App内嵌WebView(如uni-app、React Native WebView),唤起逻辑需额外适配:
- Android:WebView默认禁止第三方Scheme跳转。需在
WebViewClient.shouldOverrideUrlLoading中拦截upac://协议,并调用Intent启动:
// Android Java代码 webView.setWebViewClient(new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { if (url.startsWith("upac://") || url.startsWith("upay://")) { Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); startActivity(intent); return true; } return false; } });- iOS:WKWebView需在
decidePolicyForNavigationAction中拦截,并用UIApplication.openURL:
// iOS Swift代码 func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { guard let url = navigationAction.request.url else { decisionHandler(.allow) return } if url.scheme?.lowercased() == "upac" || url.scheme?.lowercased() == "upay" { UIApplication.shared.open(url) decisionHandler(.cancel) return } decisionHandler(.allow) }注意:iOS 13+要求在
Info.plist中声明LSApplicationQueriesSchemes,添加upac和upay两个字符串,否则canOpenURL返回false,唤起静默失败。
4. 关键参数与签名详解:每一个字符都影响成败
4.1 TN双重编码的实操验证表
下表展示了同一段原始tn在不同编码阶段的输出,供你逐级比对调试:
| 编码阶段 | 原始tn片段(示意) | 输出结果 | 常见错误 |
|---|---|---|---|
| 原始tn | `MjAyNDEyMTExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExM......## 1. 项目概述:从一串“tn”字符串到成功唤起云闪付支付的完整链路 |
“云闪付tn转链接,tn转url拉起云闪付支付——总算搞起来了”,这句话背后不是一句轻飘飘的感叹,而是一次典型的支付网关侧深度集成实战。我做支付系统对接十年,经手过银联、支付宝、微信、京东、PayPal等二十多个主流通道,但每次遇到“tn”这个字段,依然要重新翻文档、抓包、验签、调时序——它不像微信的prepay_id或支付宝的pay_order_id那样直白,而是一个高度封装、强时效、带签名的银联交易凭证。所谓“tn转url”,本质是把银联标准的Transaction Number(TN)通过银联开放平台提供的URL生成服务,构造成一个可被浏览器或App识别并跳转至云闪付客户端的scheme链接(如upay://...或upac://...),最终触发云闪付App完成支付确认。整个过程涉及RSA非对称加密验签、URL编码与解码、协议头构造、客户端唤起机制、超时与失败兜底策略五大核心环节。如果你正在开发电商H5、小程序跳转页、uni-app混合应用,或是为银行/商户后台构建统一支付中台,那么你大概率会卡在这个环节:后端生成了tn,前端却怎么也唤不起云闪付;或者唤起了,但提示“参数错误”“签名无效”“商户未授权”。这不是前端写错了一行JS,也不是后端少传了一个字段,而是银联支付协议在Web与Native边界上的一次精密咬合。本文不讲抽象概念,只复盘我在线上环境实测通过的完整路径:从tn原始值出发,到生成可点击、可分享、可埋点、可监控的最终拉起URL,每一步都附带真实参数、编码陷阱、验签逻辑和安卓/iOS双端兼容要点。适合支付开发工程师、全栈开发者、以及需要快速落地云闪付接入的中小商户技术负责人。
2. 核心原理拆解:为什么tn不能直接用?为什么必须“转”?
2.1 TN的本质:不是ID,而是银联交易会话的加密信封
很多人误以为tn就是一个数据库主键ID,类似订单号。这是最大的认知偏差。实际上,tn是银联网关在受理一笔支付请求后,返回给商户系统的、经过RSA私钥签名的结构化数据载荷(payload)的Base64编码字符串。它内部包含但不限于以下关键字段:
merId:商户在银联的唯一编号(15位数字)orderId:商户侧订单号(建议与业务订单号一致,便于对账)txnTime:交易时间戳(精确到秒,格式yyyyMMddHHmmss)txnAmt:交易金额(单位:分,整数字符串)currencyCode:币种(默认156,人民币)reqReserved:商户自定义透传字段(Base64编码,最大1024字节)sign:上述所有字段按字典序拼接后,用银联下发的RSA私钥进行SHA256withRSA签名的Base64结果
提示:tn本身不包含任何明文敏感信息(如卡号、密码),但它是一个“一次一密”的会话凭证。它的有效期极短——官方文档明确要求5分钟内必须使用,超时则银联网关拒绝受理。这也是为什么你看到tn生成后立刻刷新页面就失效的原因。
2.2 为什么不能直接用tn唤起云闪付?——协议层的三重隔离
直接拿tn字符串去拼upay://pay?tn=xxx是行不通的,原因有三:
协议头缺失:云闪付App注册的Android Intent Scheme或iOS Universal Link,并不识别裸tn。它只响应银联标准的
upac://或upay://协议,且该协议URL必须携带tn、sign、encoding、timestamp等至少4个强制参数,缺一不可。签名验证闭环:银联要求,用于唤起App的URL中携带的
sign,必须是对整个URL查询参数字符串(不含协议头和域名)按字典序拼接后,再用银联公钥验签。这个sign和tn内部的sign是两套独立签名,前者用于校验URL未被篡改,后者用于校验tn本身合法性。很多开发者混淆这两者,导致“tn有效但URL唤起失败”。编码安全要求:tn原始值含
+、/、=等特殊字符,在URL中会被浏览器自动转义或截断。例如,tn中常见的+在URL中会被当作空格处理,导致Base64解码失败。因此,tn必须经过两次URL编码:第一次是标准encodeURIComponent,第二次是对已编码结果中的%符号再做一次编码(即%→%25),这是银联开放平台明确规定的“双重编码”规则,也是90%失败案例的根源。
2.3 “转URL”的本质:构造一个银联认证的、可执行的支付指令包
所谓“tn转url”,就是将原始tn作为核心载荷,嵌入银联定义的标准URL模板,并补全所有必要参数,最终生成一个符合RFC 3986规范、能被云闪付App正确解析的URI。这个URI不是普通链接,而是一个支付指令包(Payment Instruction Packet),其结构如下:
upac://pay?tn={double_encoded_tn}&sign={url_sign}&encoding=utf-8×tamp={unix_timestamp_ms}其中:
{double_encoded_tn}:原始tn经两次encodeURIComponent后的结果{url_sign}:对tn={double_encoded_tn}&encoding=utf-8×tamp={ts}字符串按字典序拼接后,用商户RSA私钥签名的Base64值(注意:此处用的是商户私钥,不是银联私钥!){unix_timestamp_ms}:当前毫秒级时间戳(13位数字),用于防重放攻击,银联要求与服务器时间误差不超过5分钟
注意:
upac://是银联官方推荐的、兼容性最好的协议头。upay://在部分旧版本云闪付中可能失效。iOS端还需额外配置Associated Domains,否则Universal Link无法触发App唤起。
3. 实操全流程:从后端生成到前端唤起,每一步都踩过坑
3.1 后端服务:生成可拉起URL的完整代码逻辑(以Node.js为例)
我们以Express框架为例,展示一个生产环境可用的URL生成服务。关键点在于:签名逻辑必须严格遵循银联文档,编码必须双重,时间戳必须毫秒级,且所有参数名必须小写。
const crypto = require('crypto'); const fs = require('fs'); // 1. 加载商户RSA私钥(PEM格式,需提前从银联开放平台下载) const privateKey = fs.readFileSync('./cert/merchant_private_key.pem', 'utf8'); // 2. 定义URL生成函数 function generateUpayUrl(rawTn) { const timestamp = Date.now().toString(); // 13位毫秒时间戳 const encoding = 'utf-8'; // 3. 对原始tn进行双重URL编码 const onceEncoded = encodeURIComponent(rawTn); const doubleEncoded = encodeURIComponent(onceEncoded); // 关键!第二次编码 // 4. 构造待签名字符串:按参数名字典序拼接,key=value&key=value... // 注意:参数名必须小写,且顺序为 encoding, tn, timestamp const signString = `encoding=${encoding}&tn=${doubleEncoded}×tamp=${timestamp}`; // 5. 使用商户RSA私钥进行SHA256withRSA签名 const sign = crypto.sign('sha256', Buffer.from(signString), { key: privateKey, padding: crypto.constants.RSA_PKCS1_PSS_PADDING, }).toString('base64'); // 6. 组装最终URL const url = `upac://pay?tn=${doubleEncoded}&sign=${encodeURIComponent(sign)}&encoding=${encoding}×tamp=${timestamp}`; return url; } // 7. Express路由示例 app.get('/api/upay/url', (req, res) => { const { tn } = req.query; if (!tn || tn.length < 32) { return res.status(400).json({ error: 'Invalid tn' }); } try { const upayUrl = generateUpayUrl(tn); res.json({ url: upayUrl }); } catch (err) { console.error('URL generation failed:', err); res.status(500).json({ error: 'Internal server error' }); } });实操心得:我在测试时发现,Node.js的
crypto.sign默认使用PKCS#1 v1.5填充,但银联要求PSS填充。若不显式指定padding: crypto.constants.RSA_PKCS1_PSS_PADDING,签名虽能生成,但银联验签必失败。另外,signString拼接时务必用&连接,且不能有多余空格、换行、斜杠,否则验签失败。我曾因JSON.stringify后多了一个换行符,调试了6小时。
3.2 前端唤起:H5页面中安全、可靠、兼容的唤起方案
前端拿到后端返回的upac://...URL后,不能简单window.location.href = url。原因有二:一是iOS Safari对第三方Scheme限制极严,二是Android部分厂商浏览器会拦截跳转。必须采用“降级+检测+兜底”三重策略。
// 1. 封装唤起函数 function launchUpay(url) { // 创建隐藏iframe,避免页面跳转 const iframe = document.createElement('iframe'); iframe.style.display = 'none'; iframe.src = url; document.body.appendChild(iframe); // 设置超时检测:3秒内若页面未跳转,则认为唤起失败 const timeout = setTimeout(() => { document.body.removeChild(iframe); // 检测是否仍在当前页面(即唤起失败) if (document.visibilityState === 'visible') { // 尝试降级方案:跳转至云闪付H5支付页 window.location.href = `https://95516.com/pay?tn=${encodeURIComponent(rawTn)}`; } }, 3000); // 监听页面可见性变化,更精准判断唤起结果 const handleVisibilityChange = () => { if (document.visibilityState === 'hidden') { clearTimeout(timeout); document.removeEventListener('visibilitychange', handleVisibilityChange); } }; document.addEventListener('visibilitychange', handleVisibilityChange); } // 2. 调用示例(配合后端API) async function startUpayPayment() { try { const res = await fetch('/api/upay/url?tn=' + encodeURIComponent(tn)); const { url } = await res.json(); launchUpay(url); } catch (err) { console.error('Fetch URL failed:', err); alert('获取支付链接失败,请重试'); } }实操心得:单纯依赖
visibilityState在iOS上并不完全可靠。我在线上灰度时发现,iPhone 12以上机型在唤起云闪付后,Safari有时会短暂闪一下白屏再切到App,导致visibilityState短暂变为visible。因此,必须叠加3秒超时+页面可见性+用户主动操作(如点击按钮)三重判断。另外,iframe.src赋值后立即removeChild会导致唤起失败,必须等待超时或检测到跳转后再清理。
3.3 移动端App(Android/iOS)集成:WebView内唤起的特殊处理
如果你的应用是原生App内嵌WebView(如uni-app、React Native WebView),唤起逻辑需额外适配:
- Android:WebView默认禁止第三方Scheme跳转。需在
WebViewClient.shouldOverrideUrlLoading中拦截upac://协议,并调用Intent启动:
// Android Java代码 webView.setWebViewClient(new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { if (url.startsWith("upac://") || url.startsWith("upay://")) { Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); startActivity(intent); return true; } return false; } });- iOS:WKWebView需在
decidePolicyForNavigationAction中拦截,并用UIApplication.openURL:
// iOS Swift代码 func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { guard let url = navigationAction.request.url else { decisionHandler(.allow) return } if url.scheme?.lowercased() == "upac" || url.scheme?.lowercased() == "upay" { UIApplication.shared.open(url) decisionHandler(.cancel) return } decisionHandler(.allow) }注意:iOS 13+要求在
Info.plist中声明LSApplicationQueriesSchemes,添加upac和upay两个字符串,否则canOpenURL返回false,唤起静默失败。
4. 关键参数与签名详解:每一个字符都影响成败
4.1 TN双重编码的实操验证表
下表展示了同一段原始tn在不同编码阶段的输出,供你逐级比对调试:
| 编码阶段 | 原始tn片段(示意) | 输出结果 | 常见错误 |
|---|---|---|---|
| 原始tn | MjAyNDEyMTExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExM...... | (过长,省略) | 直接使用原始tn,未编码 |
| 第一次encodeURIComponent | MjAyNDEyMTExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NT............ | `MjAyNDEyMTExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0NTY3ODkwMTIzNDU2Nzg5MDExMjM0...... |