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 版本、完整调用日志和稳定复现步骤,再通过支付宝开放平台的技术支持渠道核查。在没有调用日志的情况下,不宜直接认定是支付宝客户端的问题。
备注:内容仅供参考。
