Android 未安装支付宝APP时调起支付的返回与落地页问题
结论
这两个问题需要分开处理:
- 返回 App 后再次进入支付页,通常是因为支付逻辑写在
onResume()、页面加载回调或路由初始化代码中,用户返回时又触发了一遍。应只在用户主动操作时启动支付,并通过支付状态和防重复标记避免多次调起。 - 未安装支付宝时,支付一般会降级到 H5 收银台。承载页面的可能是系统浏览器、厂商浏览器、WebView,也可能是支付宝服务端的中间页。商户无法完全控制返回按钮和最终落地页。比较可靠的方案是配置明确的 HTTPS 返回地址,在商户自己的中转页提供“返回订单”和“重新支付”入口,再通过 Android App Links 或页面按钮返回 App。
不要依赖支付宝页面左上角的返回按钮,也不能因为用户已经返回 App 就认定支付成功。最终支付状态必须以服务端异步通知或主动查询结果为准。
为什么会重复进入支付页
用户从 App 前往 H5 收银台,再切回原页面时,Activity 或 Fragment 通常会执行 onResume()。如果支付代码放在以下位置,就可能再次发起支付:
Activity.onResume()Fragment.onResume()- 页面路由初始化
- WebView 的
onPageFinished() - 订单状态刷新后的回调
- Compose 中没有正确限定 key 的
LaunchedEffect
例如,下面的代码存在重复发起支付的风险:
override fun onResume() {
super.onResume()
startAlipay()
}
支付不应由页面生命周期直接触发,而应来自用户点击按钮或一个明确的一次性事件。
Android 侧避免重复调起
可以用一个简单的状态机管理支付流程:
enum class PayState {
IDLE,
LAUNCHING,
WAITING_RESULT,
FINISHED
}
用户点击支付按钮时,先检查当前状态:
class CheckoutActivity : AppCompatActivity() {
private var payState = PayState.IDLE
private fun onPayButtonClicked() {
if (payState == PayState.LAUNCHING ||
payState == PayState.WAITING_RESULT
) {
return
}
payState = PayState.LAUNCHING
requestPayOrder()
}
private fun requestPayOrder() {
// orderInfo 必须由服务端生成和签名
val orderInfo = obtainOrderInfoFromServer()
payState = PayState.WAITING_RESULT
Thread {
val result = PayTask(this).payV2(orderInfo, true)
runOnUiThread {
handleSdkResult(result)
}
}.start()
}
private fun handleSdkResult(result: Map<String, String>) {
// SDK 返回值只用于界面提示,不能作为最终到账依据
refreshOrderStatusFromServer()
}
private fun refreshOrderStatusFromServer() {
// 查询商户服务端订单状态
// 确认成功后设置 FINISHED;
// 未支付或已取消时恢复为 IDLE,允许用户主动重试。
}
}
如果页面重建后仍需保留状态,可以使用 ViewModel 和 SavedStateHandle 保存“是否已经发起过”:
class CheckoutViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
var payState: PayState
get() = savedStateHandle["pay_state"] ?: PayState.IDLE
set(value) {
savedStateHandle["pay_state"] = value
}
fun canLaunchPayment(): Boolean {
return payState == PayState.IDLE
}
}
支付按钮还可以增加短时间防抖,但防抖只能阻止快速连点,不能替代支付状态控制。
不要在返回时自动重新支付
用户从浏览器或收银台返回后,可以刷新订单状态,但不要马上重新调用支付接口:
private var shouldRefreshOrder = false
private fun launchPayment() {
if (payState != PayState.IDLE) return
shouldRefreshOrder = true
payState = PayState.WAITING_RESULT
// 调起支付
}
override fun onResume() {
super.onResume()
if (shouldRefreshOrder) {
shouldRefreshOrder = false
refreshOrderStatusFromServer()
}
}
这里的 onResume() 只查询订单,不再次发起支付。如果查询结果是未支付,应显示“继续支付”按钮,让用户决定是否重试。
如何保证用户能够返回
未安装支付宝时,承载 H5 收银台的容器并不固定。有的容器带工具栏,有的没有;有的会打开新页面,有的会覆盖当前 WebView。商户很难稳定控制第三方页面是否显示左上角返回按钮。
可以采用以下返回链路:
支付宝 H5 收银台
↓
商户 HTTPS 支付结果中转页
↓
Android App Links 返回 App
↓
App 向服务端查询订单状态
创建支付请求时,应按照当前接入产品的接口文档配置商户返回地址。如果接口支持 return_url,可以将其设置为商户自己的 HTTPS 页面,例如:
https://pay.example.com/alipay/return?orderNo=202609090001
支持哪些参数、参数应该放在哪个业务对象中,取决于实际使用的支付宝产品和接口版本。应以对应产品的当前文档为准,不要直接套用其他支付接口的参数。
中转页至少要提供两个清晰的入口:
<a href="https://www.example.com/orders/202609090001">
返回订单页
</a>
<a href="https://app.example.com/pay/result?orderNo=202609090001">
返回 App 查看结果
</a>
第二个 HTTPS 地址可以配置为 Android App Link。即使 App Link 没有成功唤起 App,用户也能留在正常的网页结果页,不会困在无法控制的支付页面里。
Android App Links 示例
在 Manifest 中声明可验证的 HTTPS 链接:
<activity
android:name=".PayResultActivity"
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="app.example.com"
android:pathPrefix="/pay/result" />
</intent-filter>
</activity>
对应域名还需要部署合法的 assetlinks.json。App 收到链接后,只读取订单号,然后向商户服务端查询状态:
class PayResultActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val orderNo = intent?.data
?.getQueryParameter("orderNo")
.orEmpty()
if (orderNo.isBlank()) {
finish()
return
}
queryOrderStatus(orderNo)
}
private fun queryOrderStatus(orderNo: String) {
// 调用商户服务端查询接口,展示支付成功、未支付或处理中
}
}
不要只根据 URL 中的 result=success、客户端 SDK 返回值或页面跳转结果,把订单状态改成“已支付”。这些数据可能被伪造,也可能因为网络问题或页面关闭而不完整。
如何控制不同手机上的落地行为
支付宝收银台及其中间页面可能因浏览器能力、系统版本、风控策略、应用安装情况和服务端配置而变化。商户通常无法强制所有设备显示完全一致的支付宝页面。
商户可以控制的是支付前后的自有页面:
- 支付前统一进入商户自己的确认页。
- 支付参数由服务端生成,客户端不要自行拼接签名内容。
- 配置固定的商户 HTTPS 返回地址。
- 返回地址指向商户中转页,不依赖第三方页面的导航。
- 中转页同时提供网页订单入口和返回 App 入口。
- App 恢复后只查询订单状态,不自动重新调起支付。
- 对同一个商户订单号做幂等控制,避免重复创建支付订单。
如果支付页面必须放在自有 WebView 中,还应保留可见的原生关闭按钮,不能只依靠网页自己的返回按钮:
binding.closeButton.setOnClickListener {
finish()
}
override fun onBackPressed() {
if (binding.webView.canGoBack()) {
binding.webView.goBack()
} else {
finish()
}
}
如果支付流程已经离开当前 WebView,进入系统浏览器或其他应用,这个原生按钮就无法控制外部页面。因此,HTTPS 返回页仍然需要作为兜底方案。
注意事项
- 支付订单应由服务端创建和签名,私钥不能放在 Android 客户端。
- 客户端返回、H5 跳转和 SDK 同步结果都不能作为最终支付凭证。
- 服务端收到异步通知后需要验签,并对订单更新做幂等处理。
- 用户返回时,订单可能仍在处理中。此时可以轮询查询,但不要自动重新支付。
return_url只处理用户页面跳转,不能保证异步通知已经完成。- 自定义 Scheme 返回 App 可能被浏览器拦截,应优先使用经过域名验证的 Android App Links。
- 不同支付宝产品的返回参数和降级行为可能不同,需要结合实际使用的 SDK、支付接口和当前官方文档确认。
备注:内容仅供参考。
