android 中如何实现 payment gateway 支付回跳?
结论
这种方案可行。Android 应用可以用系统浏览器或 Custom Tabs 打开支付网关,支付完成后,再通过 Android App Links 返回应用。
建议按以下流程实现:
- App 请求自己的服务器创建支付订单。
- 服务器返回支付地址,App 在浏览器中打开该地址。
- 支付网关完成交易后,跳转到网站的 HTTPS 回调页。
- 回调页通过 Android App Link 唤起 App。
- App 携带订单号向服务器查询最终支付状态。
浏览器回跳中即使带有“支付成功”参数,也不能直接作为支付结果。最终状态必须由服务器根据网关签名、服务端通知或订单查询接口确认。
推荐的整体流程
Android App
│ 创建订单
▼
业务服务器
│ 返回 paymentUrl
▼
系统浏览器 / Custom Tabs
│ 提交支付
▼
Payment Gateway
│ returnUrl / callbackUrl
▼
https://pay.example.com/payment/result
│ Android App Link
▼
Android App
│ 查询订单状态
▼
业务服务器
这里有两类作用不同的回调:
- 浏览器回跳负责把用户带回 App,主要用于改善使用体验。
- 服务端通知负责确认交易结果,不能依赖用户是否返回 App。
即使用户支付后直接关闭浏览器,服务器也应该能接收支付通知,或者主动查询订单状态。
使用 Android App Links 返回 App
假设支付网关最终跳转到:
https://pay.example.com/payment/result?orderId=ORDER_123
在 AndroidManifest.xml 中注册这个地址:
<activity
android:name=".PaymentResultActivity"
android:exported="true">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="pay.example.com"
android:pathPrefix="/payment/result" />
</intent-filter>
</activity>
然后在对应域名下部署:
https://pay.example.com/.well-known/assetlinks.json
示例内容:
[
{
"relation": [
"delegate_permission/common.handle_all_urls"
],
"target": {
"namespace": "android_app",
"package_name": "com.example.app",
"sha256_cert_fingerprints": [
"AA:BB:CC:DD:EE:FF:..."
]
}
}
]
package_name 和证书指纹必须与正式安装包一致。测试包和正式包如果使用不同签名,需要分别配置。
在 App 中处理回跳
Kotlin 示例:
class PaymentResultActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
handlePaymentLink(intent)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
handlePaymentLink(intent)
}
private fun handlePaymentLink(intent: Intent) {
val uri = intent.data ?: return
if (uri.scheme != "https" ||
uri.host != "pay.example.com" ||
uri.path != "/payment/result"
) {
finish()
return
}
val orderId = uri.getQueryParameter("orderId")
if (orderId.isNullOrBlank()) {
finish()
return
}
// 不要根据 URL 中的 status 直接判定支付成功。
// 使用 orderId 请求业务服务器,查询已经验证过的订单状态。
queryPaymentStatus(orderId)
}
private fun queryPaymentStatus(orderId: String) {
// 调用自己的后端,例如:
// GET /api/payments/{orderId}/status
}
}
收到回跳后,App 应先显示“正在确认支付结果”,再向业务服务器查询。订单状态至少可能包括:
PENDING
SUCCESS
FAILED
CANCELLED
网关回跳和服务端通知的到达顺序并不固定,所以出现 PENDING 很正常。App 可以每隔数秒重试一次,连续尝试几次,但不要无限轮询。
打开支付页面
与直接调用外部浏览器相比,Android Custom Tabs 的体验通常更连贯,同时仍由浏览器负责 Cookie、TLS 和页面跳转。
val paymentUri = Uri.parse(paymentUrl)
val customTabsIntent = CustomTabsIntent.Builder()
.setShowTitle(true)
.build()
customTabsIntent.launchUrl(this, paymentUri)
依赖示例:
dependencies {
implementation("androidx.browser:browser:<version>")
}
<version> 应根据项目当前使用的 AndroidX 版本确定,不要直接照搬未经验证的版本号。
如果设备上没有可用的 Custom Tabs 实现,可以退回普通浏览器:
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(paymentUrl))
startActivity(intent)
支付网关只接受 HTTPS 返回地址时怎么办
有些支付网关不允许将自定义协议设为 returnUrl,例如:
myapp://payment/result
遇到这种情况,应让网关先跳转到自己的 HTTPS 页面,再由该页面通过 Android App Link 打开 App:
Payment Gateway
→ https://pay.example.com/payment/result
→ Android App Link
→ Android App
这个 HTTPS 页面也可以处理无法返回 App 的情况:
- 已安装 App 时,通过 App Link 打开 App。
- 未安装 App 时,显示网页支付结果。
- App Link 没有自动触发时,提供“返回 App”按钮。
- 用户在桌面浏览器中支付时,仍可正常查看结果页面。
不要让页面只依靠 JavaScript 自动跳转,也不要把敏感支付数据放进 URL。
自定义 Scheme 可以作为备用方案
也可以注册:
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="myapp"
android:host="payment" />
</intent-filter>
对应地址:
myapp://payment/result?orderId=ORDER_123
不过,自定义 Scheme 无法像 App Links 一样验证域名归属,其他应用也可能注册相同的 Scheme。它更适合用作兼容性备用方案,不能作为可信的支付结果来源。
无论采用哪种深链,App 都只能把它当成“返回后查询订单”的信号。
服务端必须完成的验证
支付结果应由服务器完成以下处理:
- 创建内部订单,并生成不可预测的
state或一次性关联值。 - 保存内部订单号与网关交易号的对应关系。
- 按网关文档生成签名或校验值。
- 接收网关发送的服务端通知。
- 验证签名、商户号、订单号、金额和币种。
- 必要时调用网关的订单查询接口再次确认。
- 以幂等方式更新订单,避免重复通知导致重复发货。
- App 只向自己的服务器查询最终状态。
回跳 URL 最好只包含内部订单标识或短期有效的一次性 token,例如:
https://pay.example.com/payment/result?token=ONE_TIME_TOKEN
不要在 URL 中暴露卡号、支付密码、完整网关响应或能够重放的签名材料。
是否应该使用 WebView
除非支付网关明确支持,否则不建议直接在 WebView 中加载支付页面,原因包括:
- 银行验证页面可能禁止嵌入。
- Cookie、弹窗和多级重定向的处理可能不一致。
- 某些 UPI、银行或验证应用可能无法正常唤起。
- 网关可能因安全或合规要求拒绝 WebView。
- App 需要自行处理更多生命周期和导航问题。
通常可以按以下顺序选择:
- 支付网关官方 Android SDK。
- Custom Tabs 加 Android App Links。
- 系统浏览器加 Android App Links。
- WebView,仅在网关明确允许并提供集成规范时使用。
关于 TPSL India
能否直接采用上述方案,取决于 TPSL 当前商户接口提供的能力。需要查看分配给商户的接入文档,确认以下内容:
returnUrl是否必须使用 HTTPS。- 浏览器返回地址和服务端通知地址能否分开设置。
- 如何验证回调签名。
- 是否提供订单状态查询接口。
- 是否提供 Android 和 iOS 官方 SDK。
- 是否允许在 WebView 中加载支付页面。
- 是否允许为每笔订单设置动态返回地址。
如果现有接口只有网页回跳,没有可靠的服务端通知或订单查询能力,应先联系 TPSL 技术支持,确认其移动端接入方案。不能只靠拦截浏览器 URL 判断支付是否成功。
iPhone 的实现方式
iOS 可以沿用相同的服务端支付流程,客户端使用对应的系统能力:
- 根据网关流程,使用
ASWebAuthenticationSession或系统浏览器打开支付页面。 - 使用 Universal Links 从 HTTPS 地址返回 App。
- 使用 Associated Domains 验证 App 与域名的关系。
- App 返回后向业务服务器查询订单状态。
建议从一开始就为两个平台使用同一个回跳地址:
https://pay.example.com/payment/result
Android 将其配置为 App Link,iOS 将其配置为 Universal Link。这样,网关配置和服务端逻辑可以共用,两个客户端分别负责处理深链和展示支付状态。
备注:内容仅供参考。
