PAYATHON 2026

Android 如何从 Chrome 返回 Activity 后获取支付结果

支付小周

结论

从 Chrome 返回 Activity 时,不能仅凭 onResume()、返回键或页面跳转来判断支付成功或取消。

更可靠的处理流程如下:

  1. 发起支付前,由服务端创建订单并保存唯一的 orderId。
  2. 支付平台通过服务端回调更新订单状态。
  3. Chrome 通过 Deep Link 或 Android App Link 唤起应用时,只传递 orderId 等定位信息。
  4. Activity 收到跳转后,向自己的服务端查询订单状态。
  5. 如果用户直接按返回键回到应用,也在 onResume() 中查询服务端,不要自行判定支付已取消。

支付结果应以支付平台的服务端通知或服务端主动查询结果为准。

为什么不能直接在 Activity 中判断

Chrome 和 Android 应用是两个独立的环境。应用只能知道自己重新进入了前台,无法确定用户是在网页中完成了支付、关闭了页面,还是仍在等待支付结果。

下面的代码只能说明 Activity 已恢复:

override fun onResume() {
    super.onResume()
    // 这里只能确认 Activity 回到前台,不能确认支付成功
}

以下情况都可能触发类似的生命周期变化:

  • 支付成功后跳回应用
  • 用户按返回键退出 Chrome
  • 用户切换任务回到应用
  • 支付页面超时
  • 银行已经扣款,但支付平台尚未完成状态同步
  • 支付仍在处理中

所以,“返回应用”不代表”支付成功”,“没有成功参数”也不代表”支付取消”。

推荐的支付流程

1. 由服务端创建支付订单

应用先请求自己的服务端创建订单。服务端返回 orderId 和支付地址:

{
  "orderId": "ORDER_20260901_001",
  "paymentUrl": "https://payment.example.com/pay/..."
}

应用保存当前订单号,再打开 Chrome:

private var pendingOrderId: String? = null

private fun startPayment(orderId: String, paymentUrl: String) {
    pendingOrderId = orderId

    val intent = Intent(Intent.ACTION_VIEW, Uri.parse(paymentUrl))
    startActivity(intent)
}

实际项目中,不要只用普通成员变量保存待支付订单,因为进程可能被系统终止。可以将 orderId 放入 ViewModel、SavedStateHandle 或持久化存储,并在订单结束后及时清除。

2. 配置返回应用的链接

如果支付平台允许设置返回地址,可以配置 Deep Link,例如:

myapp://payment/result?orderId=ORDER_20260901_001

条件允许时,更适合使用经过域名验证的 Android App Link,例如:

https://app.example.com/payment/result?orderId=ORDER_20260901_001

使用自定义 Scheme 时,可以在 AndroidManifest.xml 中配置:

<activity
    android:name=".PaymentResultActivity"
    android:exported="true"
    android:launchMode="singleTop">

    <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"
            android:path="/result" />
    </intent-filter>
</activity>

如果 Activity 已经存在,launchMode="singleTop" 可以通过 onNewIntent() 将新的跳转交给当前实例。具体启动行为仍由任务栈和启动模式配置决定。

3. 接收 Chrome 的返回跳转

首次创建 Activity 和已有实例收到新 Intent 的情况都要处理:

class PaymentResultActivity : AppCompatActivity() {

    private var pendingOrderId: String? = null
    private var paymentLaunched = false
    private var checkingPayment = false

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        handlePaymentReturn(intent)
    }

    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        setIntent(intent)
        handlePaymentReturn(intent)
    }

    private fun handlePaymentReturn(intent: Intent?) {
        val uri = intent?.data ?: return

        if (uri.scheme == "myapp" &&
            uri.host == "payment" &&
            uri.path == "/result"
        ) {
            val orderId = uri.getQueryParameter("orderId") ?: return
            queryPaymentStatus(orderId)
        }
    }

    override fun onResume() {
        super.onResume()

        // 覆盖用户直接按返回键离开 Chrome、未触发 Deep Link 的情况
        if (paymentLaunched && !checkingPayment) {
            pendingOrderId?.let(::queryPaymentStatus)
        }
    }

    private fun queryPaymentStatus(orderId: String) {
        checkingPayment = true

        // 调用自己的服务端查询订单,完成后将 checkingPayment 设回 false
    }
}

不要在首次打开 Activity 时无条件查询,还要避免 onResume() 和 onNewIntent() 同时触发重复请求。查询接口应支持幂等,多次查询同一个订单不能产生副作用。

以服务端查询结果更新界面

假设服务端提供以下接口:

GET /api/payments/{orderId}/status

接口返回明确的业务状态:

{
  "orderId": "ORDER_20260901_001",
  "status": "SUCCESS"
}

Android 端可以按状态分别处理:

enum class PaymentStatus {
    SUCCESS,
    FAILED,
    CANCELLED,
    PENDING,
    UNKNOWN
}

private fun handlePaymentStatus(status: PaymentStatus) {
    when (status) {
        PaymentStatus.SUCCESS -> {
            showPaymentSuccess()
            clearPendingOrder()
        }

        PaymentStatus.FAILED -> {
            showPaymentFailed()
            clearPendingOrder()
        }

        PaymentStatus.CANCELLED -> {
            showPaymentCancelled()
            clearPendingOrder()
        }

        PaymentStatus.PENDING -> {
            showPaymentPending()
        }

        PaymentStatus.UNKNOWN -> {
            showUnableToConfirm()
        }
    }
}

各状态的含义如下:

  • SUCCESS:服务端已确认支付成功。
  • FAILED:支付平台明确返回失败。
  • CANCELLED:支付平台明确记录为取消。
  • PENDING:支付仍在处理中,稍后可以再次查询。
  • UNKNOWN:暂时无法确认,例如网络异常或订单不存在。

具体状态名称应按照支付平台及业务服务端的接口定义调整。

支付结果应由服务端确认

正常情况下,支付平台会向商户服务端发送异步通知。服务端收到通知后,需要:

  1. 验证通知签名。
  2. 校验商户号、订单号、金额和币种。
  3. 确认订单没有被重复处理。
  4. 更新订单状态。
  5. 向支付平台返回规定的确认响应。
  6. 必要时主动调用支付平台的订单查询接口。

浏览器返回地址中的参数只能用于定位订单,不能作为付款凭证。即使返回链接中带有类似参数:

myapp://payment/result?orderId=123&status=success

也不能直接执行以下逻辑:

// 不安全:URL 参数可以被伪造
if (uri.getQueryParameter("status") == "success") {
    deliverProduct()
}

应当取得 orderId,再查询自己的服务端:

val orderId = uri.getQueryParameter("orderId") ?: return
queryPaymentStatus(orderId)

发货、充值、开通会员等业务操作也应由服务端在确认支付成功后执行,不能只依赖客户端显示的状态。

如何处理用户按返回键

用户从 Chrome 返回时,不要立即显示”支付已取消”。返回键只表示用户离开了支付页面,此时银行端可能已经受理交易。

可以按查询结果更新界面:

  • 首次查询为 SUCCESS:显示支付成功。
  • 查询为 FAILED 或明确的 CANCELLED:显示对应结果。
  • 查询为 PENDING:显示”支付结果确认中”,并提供”重新查询”按钮。
  • 查询失败:提示网络异常,保留订单,允许用户稍后查看。
  • 长时间没有最终结果:由服务端按照支付平台的规则主动查询或关闭订单。

注意事项

不要信任客户端传入的金额和状态

查询订单时,服务端应根据已登录用户及服务端保存的订单关系进行鉴权,不能只凭任意 orderId 返回敏感信息。金额、支付状态和商品内容都以服务端记录为准。

防止重复处理

支付回调、主动查询和客户端查询可能几乎同时发生。服务端必须进行幂等处理,避免同一订单被重复发货、重复充值或重复记账。

Deep Link 只负责将用户带回应用,并提供订单定位信息。用户可能没有跳回应用,但服务端已经收到成功通知。用户也可能已经回到应用,而异步通知还没有到达。

处理进程被终止的情况

支付期间,Android 进程可能被系统回收。待支付的 orderId 应能恢复,确保用户重新打开应用后,仍可在订单页面查询最终状态。

自定义 Scheme 可能被其他应用声明。条件允许时,应优先使用带域名验证的 Android App Link,并在服务端配置对应的 assetlinks.json。无论采用哪种返回方式,支付结果都必须由服务端确认。

备注:内容仅供参考。