PAYATHON 2026

android 中如何实现 payment gateway 支付回跳?

支付阿杰

结论

这种方案可行。Android 应用可以用系统浏览器或 Custom Tabs 打开支付网关,支付完成后,再通过 Android App Links 返回应用。

建议按以下流程实现:

  1. App 请求自己的服务器创建支付订单。
  2. 服务器返回支付地址,App 在浏览器中打开该地址。
  3. 支付网关完成交易后,跳转到网站的 HTTPS 回调页。
  4. 回调页通过 Android App Link 唤起 App。
  5. App 携带订单号向服务器查询最终支付状态。

浏览器回跳中即使带有“支付成功”参数,也不能直接作为支付结果。最终状态必须由服务器根据网关签名、服务端通知或订单查询接口确认。

推荐的整体流程

Android App
    │ 创建订单
    ▼
业务服务器
    │ 返回 paymentUrl
    ▼
系统浏览器 / Custom Tabs
    │ 提交支付
    ▼
Payment Gateway
    │ returnUrl / callbackUrl
    ▼
https://pay.example.com/payment/result
    │ Android App Link
    ▼
Android App
    │ 查询订单状态
    ▼
业务服务器

这里有两类作用不同的回调:

  • 浏览器回跳负责把用户带回 App,主要用于改善使用体验。
  • 服务端通知负责确认交易结果,不能依赖用户是否返回 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 都只能把它当成“返回后查询订单”的信号。

服务端必须完成的验证

支付结果应由服务器完成以下处理:

  1. 创建内部订单,并生成不可预测的 state 或一次性关联值。
  2. 保存内部订单号与网关交易号的对应关系。
  3. 按网关文档生成签名或校验值。
  4. 接收网关发送的服务端通知。
  5. 验证签名、商户号、订单号、金额和币种。
  6. 必要时调用网关的订单查询接口再次确认。
  7. 以幂等方式更新订单,避免重复通知导致重复发货。
  8. App 只向自己的服务器查询最终状态。

回跳 URL 最好只包含内部订单标识或短期有效的一次性 token,例如:

https://pay.example.com/payment/result?token=ONE_TIME_TOKEN

不要在 URL 中暴露卡号、支付密码、完整网关响应或能够重放的签名材料。

是否应该使用 WebView

除非支付网关明确支持,否则不建议直接在 WebView 中加载支付页面,原因包括:

  • 银行验证页面可能禁止嵌入。
  • Cookie、弹窗和多级重定向的处理可能不一致。
  • 某些 UPI、银行或验证应用可能无法正常唤起。
  • 网关可能因安全或合规要求拒绝 WebView。
  • App 需要自行处理更多生命周期和导航问题。

通常可以按以下顺序选择:

  1. 支付网关官方 Android SDK。
  2. Custom Tabs 加 Android App Links。
  3. 系统浏览器加 Android App Links。
  4. 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。这样,网关配置和服务端逻辑可以共用,两个客户端分别负责处理深链和展示支付状态。

备注:内容仅供参考。