PAYATHON 2026

Android 游戏支付后切换后台导致 payment callback 取消

支付小周

结论

不要把支付是否成功完全依赖于一次 payment callback,也不要在 onStop() 或应用切入后台时主动取消支付请求。

支付流程应设计为可恢复的状态机:发起支付时保存订单信息;收到回调后进行处理;应用重新回到前台时,主动查询尚未完成的购买记录,并向业务服务器核验订单。这样,即使回调因 Activity 重建、进程回收或支付 SDK 页面切换而丢失,系统仍能继续完成发货和确认流程。

“先进入后台卡片页面再返回就能成功”通常只是因为生命周期切换的时序发生了变化,不能作为可靠的解决方案。

可能原因

支付页面通常由系统、应用商店或第三方支付 SDK 的 Activity 承载。游戏进入后台后,可能出现以下情况:

  • 游戏 Activity 执行 onPause()、onStop(),业务代码随之取消请求或注销监听。
  • 支付回调绑定在旧 Activity 上,而应用返回前台时 Activity 已经重建。
  • 游戏引擎失去焦点后暂停主线程,回调未能及时分发。
  • 支付 SDK 将页面退出解释为用户取消,但支付平台可能已经生成订单。
  • Android 在内存紧张时终止游戏进程,原本保存在内存中的订单状态随之丢失。
  • launchMode、intent-filter 或 onNewIntent() 配置有误,支付结果被返回到新的 Activity 实例。
  • SDK 连接在后台断开,应用返回前台后没有重新连接并补查订单。

具体原因取决于项目使用的是 Google Play Billing、厂商商店 SDK,还是支付宝、微信等第三方渠道。应先检查支付 SDK 返回的错误码和 Android 生命周期日志,不能只根据“callback 被取消”就认定订单失败。

处理步骤

1. 发起支付前持久化订单

打开支付页面前,先由服务器创建业务订单,并在本地记录订单号和当前状态。不要只把订单号保存在 Activity 字段或游戏引擎内存中。

建议至少保存:

  • 业务订单号
  • 商品 ID
  • 支付渠道
  • 发起时间
  • 当前状态,如 CREATED、PAYING、PAID、DELIVERED
  • 渠道返回的 purchase token 或 transaction ID

即使应用被销毁,也应能根据这些数据恢复查询。

2. 不要在生命周期回调中取消支付

检查 onPause()、onStop()、OnApplicationPause 等逻辑,避免执行类似操作:

override fun onStop() {
    super.onStop()

    // 不要因为进入后台就把业务订单标记为取消。
    // paymentRequest?.cancel()
    // orderRepository.markCancelled(orderId)
}

进入后台只表示当前界面不可见,不能证明用户取消了支付,也不能证明支付平台没有扣款。

只有渠道返回明确的取消结果,并且服务器查询也确认订单未支付时,才能把业务订单转为取消状态。

3. 回到前台后主动补查

支付回调只是实时通知,不能作为唯一的事实来源。应用恢复到前台时,应重新连接支付服务,查询当前账号下已经购买但尚未完成业务处理的订单。

下面以 Google Play Billing 为例说明处理思路。不同版本的 Billing Library 方法签名可能不同,应以项目实际依赖的版本为准:

class PaymentManager(
    private val billingClient: BillingClient,
    private val orderRepository: OrderRepository
) : PurchasesUpdatedListener {

    override fun onPurchasesUpdated(
        billingResult: BillingResult,
        purchases: MutableList<Purchase>?
    ) {
        when (billingResult.responseCode) {
            BillingClient.BillingResponseCode.OK -> {
                purchases.orEmpty().forEach(::processPurchase)
            }

            BillingClient.BillingResponseCode.USER_CANCELED -> {
                // 这里只表示本次 UI 流程报告取消。
                // 若本地存在待确认订单,仍应向服务端或渠道补查。
                reconcilePendingOrders()
            }

            else -> {
                // 网络错误、服务断开等情况不应直接判定订单失败。
                reconcilePendingOrders()
            }
        }
    }

    fun onAppForeground() {
        if (!billingClient.isReady) {
            connectAndQuery()
        } else {
            queryExistingPurchases()
        }
    }

    private fun connectAndQuery() {
        billingClient.startConnection(object : BillingClientStateListener {
            override fun onBillingSetupFinished(result: BillingResult) {
                if (result.responseCode == BillingClient.BillingResponseCode.OK) {
                    queryExistingPurchases()
                }
            }

            override fun onBillingServiceDisconnected() {
                // 记录断开状态,稍后重试。
            }
        })
    }

    private fun queryExistingPurchases() {
        val params = QueryPurchasesParams.newBuilder()
            .setProductType(BillingClient.ProductType.INAPP)
            .build()

        billingClient.queryPurchasesAsync(params) { result, purchases ->
            if (result.responseCode == BillingClient.BillingResponseCode.OK) {
                purchases.forEach(::processPurchase)
            }
        }
    }

    private fun processPurchase(purchase: Purchase) {
        if (purchase.purchaseState != Purchase.PurchaseState.PURCHASED) {
            return
        }

        // 不要只凭客户端字段发货。
        // 将 purchaseToken 和业务订单号发送给业务服务器核验。
        orderRepository.verifyWithServer(
            purchaseToken = purchase.purchaseToken,
            orderId = findOrderId(purchase)
        )
    }

    private fun reconcilePendingOrders() {
        orderRepository.queryPendingOrders()
        queryExistingPurchases()
    }

    private fun findOrderId(purchase: Purchase): String {
        return orderRepository.findOrderIdByPurchaseToken(purchase.purchaseToken)
    }
}

可以通过进程级生命周期监听应用返回前台:

class App : Application(), DefaultLifecycleObserver {

    lateinit var paymentManager: PaymentManager

    override fun onCreate() {
        super.onCreate()
        ProcessLifecycleOwner.get().lifecycle.addObserver(this)
    }

    override fun onStart(owner: LifecycleOwner) {
        paymentManager.onAppForeground()
    }
}

如果支付 SDK 不支持客户端补查,应调用该渠道提供的服务端订单查询接口。如果渠道既没有补查接口,也没有可靠的服务端通知,就只能按照其 SDK 文档提供的恢复机制处理,不能自行假设取消回调等同于支付失败。

4. 由服务器确认最终支付状态

推荐采用以下流程:

  1. 客户端请求业务服务器创建订单。
  2. 客户端携带订单信息拉起支付。
  3. 客户端收到回调后,把凭证提交给服务器。
  4. 服务器调用支付渠道接口验证凭证或查询订单。
  5. 服务器以订单号、purchase token 或 transaction ID 进行幂等处理。
  6. 确认支付成功后发货,并记录发货状态。
  7. 客户端重新进入前台时,从服务器同步订单结果。

服务端伪代码如下:

if transaction does not exist:
    verify transaction with payment provider

if verification failed:
    return PENDING or FAILED

if transaction has not been delivered:
    grant product
    mark transaction as delivered

return SUCCESS

必须通过数据库唯一约束或事务,保证同一笔支付只发货一次。支付回调和前台补查可能同时到达,不能依赖客户端来避免重复请求。

5. 正确处理确认或消耗

以 Google Play Billing 为例,支付验证和发货完成后,还需要根据商品类型调用 acknowledgePurchase() 或 consumeAsync()。不要在服务端完成验证前确认或消耗,也不要因为收到重复回调而重复发货。

private fun acknowledgeIfNeeded(purchase: Purchase) {
    if (purchase.isAcknowledged) {
        return
    }

    val params = AcknowledgePurchaseParams.newBuilder()
        .setPurchaseToken(purchase.purchaseToken)
        .build()

    billingClient.acknowledgePurchase(params) { result ->
        if (result.responseCode != BillingClient.BillingResponseCode.OK) {
            // 保留待确认状态,稍后重试。
        }
    }
}

确认和消耗的规则取决于商品类型及支付渠道,不能混用。

检查 Activity 和回调配置

如果支付结果通过 Intent 或链接返回,还需要检查入口 Activity:

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

同时确认:

  • launchMode 是否符合支付 SDK 的要求。
  • 支付回调 Activity 是否已在 AndroidManifest.xml 中正确声明。
  • intent-filter 的 scheme、host 和 path 是否完全匹配。
  • 使用 Activity Result API 时,launcher 是否在 Activity 或 Fragment 的创建阶段注册。
  • Activity 重建后,支付监听器是否重新注册。
  • 游戏引擎恢复时,原生层是否缓存并重新投递尚未消费的支付结果。

不要为了保留回调而盲目设置 android:configChanges 或禁止 Activity 重建。这些做法可能暂时掩盖问题,但无法解决进程被系统终止后产生的数据丢失。

建议增加的日志

至少记录以下信息,以便判断取消发生在哪一层:

orderId
productId
payment provider
Activity instance identifier
onCreate/onStart/onResume/onPause/onStop/onDestroy
payment callback response code
purchase token or transaction ID
server verification result
acknowledge/consume result

日志中不要输出完整的支付凭证、用户隐私信息或密钥。

注意事项

  • “取消回调”不一定表示支付失败,必须结合服务端订单状态判断。
  • 不要直接在客户端发放高价值商品,因为客户端状态可能被篡改。
  • 不要因为超时或应用切入后台就立即创建新订单,否则可能造成重复扣款或重复发货。
  • 补查应设置重试和退避策略,避免每次 onResume() 都发起高频请求。
  • 分别处理 PENDING、PURCHASED、CANCELED、FAILED 等状态,不要简单合并为成功和失败两种结果。
  • 测试应覆盖 Activity 重建、进程被杀、断网、重复回调、支付过程中切入后台,以及支付完成后立即切入后台等场景。

支付回调用来及时传递结果,持久化和订单查询用来恢复流程,服务端验证则负责判断支付最终是否成功。这样,整个支付流程就不会依赖某一次前后台切换的时序。

备注:内容仅供参考。