PAYATHON 2026

Android 未登录支付宝时取消支付后自动二次拉起

支付小周

结论

取消支付后,支付宝自动再次拉起,通常不是支付宝主动重新发起了支付,而是应用返回前台时又执行了一次支付代码。

排查时应重点检查 Activity 或 Fragment 的 onResume()、onStart()、页面重建、重复点击、重复网络回调,以及支付结果处理中的自动重试逻辑。支付只能由用户明确触发,同时应结合“支付进行中”状态和订单号防止重复调用。

常见原因

在生命周期回调中发起支付

从支付宝返回应用时,业务页面会再次进入 onStart() 和 onResume()。如果这些方法包含支付调用,支付宝就会被再次拉起。

override fun onResume() {
    super.onResume()
    startAlipay() // 错误:从支付宝返回时会再次执行
}

用户未登录支付宝时,登录页、支付页和商户应用之间可能多次切换,这类生命周期问题也就更容易出现。

支付按钮被重复触发

用户连续点击按钮,或同一个点击事件被多个监听器处理,都可能启动两个支付任务。第一个支付流程取消后,队列中的第二个任务继续执行,表现出来就像“自动二次拉起”。

接口或状态观察者重复回调

以下情况也会导致支付逻辑被重复调用:

  • 网络请求重复提交;
  • LiveData、Flow、RxJava 被重复订阅;
  • 页面重建后再次消费旧事件;
  • 创建订单成功回调和页面恢复逻辑都调用了支付;
  • 支付取消后触发无条件重试。

将支付结果查询误写成重新支付

应用返回前台后可以查询订单状态,但如果查询结果是“未支付”,不应立即再次调用支付宝 SDK。是否重新支付,应由用户明确确认。

排查步骤

1. 查找所有支付入口

全局搜索支付宝 SDK 的调用,例如:

PayTask(activity).payV2(orderInfo, true)

检查调用是否出现在以下位置:

  • onCreate()、onStart()、onResume();
  • onActivityResult() 或第三方回调;
  • 支付结果处理逻辑;
  • 网络请求回调;
  • LiveData、StateFlow、SharedFlow 的观察代码;
  • 定时器、重试器或异常恢复代码。

最好将支付 SDK 的调用集中到一个统一入口。

2. 记录完整调用链

发起支付前,记录订单号、页面实例、线程和调用栈:

Log.d(
    "AliPay",
    "startPay orderId=$orderId activity=${this.hashCode()} thread=${Thread.currentThread().name}",
    Throwable("Pay invocation stack")
)

如果日志中出现两次 startPay,说明重复拉起来自应用自身。继续查看调用栈,就能定位第二次调用是从哪里触发的。

生命周期也应一并记录:

override fun onStart() {
    super.onStart()
    Log.d("AliPay", "onStart activity=${hashCode()}")
}

override fun onResume() {
    super.onResume()
    Log.d("AliPay", "onResume activity=${hashCode()}")
}

3. 检查页面是否被重新创建

重点检查以下信息:

  • Activity 的 onCreate() 是否执行了两次;
  • 是否同时存在两个业务页面实例;
  • Intent 是否被重复处理;
  • 旋转屏幕、系统回收或配置变化后,是否根据旧状态自动发起支付;
  • 恢复 savedInstanceState 后,是否再次消费了支付事件。

不要只通过修改 launchMode 来掩盖重复调用。即使使用 singleTop 或 singleTask,业务层仍需做好支付防重。

4. 检查支付宝返回结果

支付宝 SDK 的同步支付结果通常包含 resultStatus。一般情况下,6001 表示用户取消,但最终仍应以当前接入版本的支付宝 SDK 文档和服务端订单状态为准。

用户取消支付后,应用应恢复按钮状态,不应自动再次调用 payV2()。

推荐实现

下面的示例只允许用户点击时触发支付,并通过原子状态防止同一页面重复发起:

class PayActivity : AppCompatActivity() {

    private val paying = AtomicBoolean(false)

    private fun onPayButtonClicked(orderInfo: String, orderId: String) {
        if (!paying.compareAndSet(false, true)) {
            Log.w("AliPay", "Ignore duplicated payment: $orderId")
            return
        }

        lifecycleScope.launch {
            val result = try {
                withContext(Dispatchers.IO) {
                    PayTask(this@PayActivity).payV2(orderInfo, true)
                }
            } catch (e: Exception) {
                Log.e("AliPay", "Payment invocation failed", e)
                null
            } finally {
                paying.set(false)
            }

            handlePayResult(orderId, result)
        }
    }

    private fun handlePayResult(
        orderId: String,
        result: Map<String, String>?
    ) {
        if (result == null) {
            showPaymentFailure()
            return
        }

        when (result["resultStatus"]) {
            "9000" -> queryOrderStatusFromServer(orderId)
            "6001" -> showPaymentCancelled()
            else -> queryOrderStatusFromServer(orderId)
        }
    }
}

PayTask.payV2() 不应在主线程执行。示例通过 Dispatchers.IO 发起调用,再回到主线程处理结果。

如果由 ViewModel 管理支付状态,可以使用明确的状态控制:

sealed interface PaymentState {
    data object Idle : PaymentState
    data class Paying(val orderId: String) : PaymentState
    data class Finished(val orderId: String) : PaymentState
}

class PayViewModel : ViewModel() {

    private val _state = MutableStateFlow<PaymentState>(PaymentState.Idle)
    val state: StateFlow<PaymentState> = _state.asStateFlow()

    fun tryStartPayment(orderId: String): Boolean {
        if (_state.value is PaymentState.Paying) {
            return false
        }

        _state.value = PaymentState.Paying(orderId)
        return true
    }

    fun finishPayment(orderId: String) {
        _state.value = PaymentState.Finished(orderId)
    }
}

页面只应在用户点击时调用 tryStartPayment()。不要在观察到 Paying 状态后无条件拉起支付宝,否则页面重建或重新订阅时,仍可能再次消费该状态。

订单级防重

页面中的布尔变量只能防止当前实例重复调用。如果还要处理页面重建、多实例或接口多次回调,应根据商户订单号防重:

private val launchedOrderIds = mutableSetOf<String>()

private fun launchPaymentOnce(orderId: String, orderInfo: String) {
    if (!launchedOrderIds.add(orderId)) {
        Log.w("AliPay", "Order already launched: $orderId")
        return
    }

    onPayButtonClicked(orderInfo, orderId)
}

如果业务允许用户取消后再次支付,应在用户重新点击支付按钮时解除限制,而不是在 onResume() 中自动解除并重新发起。

服务端也需要保证:

  • 商户订单号唯一;
  • 创建订单接口具备幂等性;
  • 客户端展示支付成功前,先向服务端查询最终订单状态;
  • 对支付通知进行签名验证,并按订单号做幂等处理。

注意事项

  • 不要在 onResume() 中直接拉起支付宝。该方法可以用来刷新页面或查询订单状态,但不适合触发支付。
  • 不要只根据客户端返回值确认到账。支付结果应以支付宝异步通知或服务端主动查询结果为准。
  • resultStatus 为取消、失败或处理中时,可以提示用户重新操作,但不能自动重试支付。
  • 调用开始后应暂时禁用支付按钮,收到结果或发生异常后再恢复。
  • 如果应用日志中确实只有一次 SDK 调用,但支付宝页面出现了两次,应收集支付宝客户端版本、Android 版本、SDK 版本、完整调用日志和稳定复现步骤,再通过支付宝开放平台的技术支持渠道核查。在没有调用日志的情况下,不宜直接认定是支付宝客户端的问题。

备注:内容仅供参考。