Android 如何从 Chrome 返回 Activity 后获取支付结果
结论
从 Chrome 返回 Activity 时,不能仅凭 onResume()、返回键或页面跳转来判断支付成功或取消。
更可靠的处理流程如下:
- 发起支付前,由服务端创建订单并保存唯一的
orderId。 - 支付平台通过服务端回调更新订单状态。
- Chrome 通过 Deep Link 或 Android App Link 唤起应用时,只传递
orderId等定位信息。 Activity收到跳转后,向自己的服务端查询订单状态。- 如果用户直接按返回键回到应用,也在
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:暂时无法确认,例如网络异常或订单不存在。
具体状态名称应按照支付平台及业务服务端的接口定义调整。
支付结果应由服务端确认
正常情况下,支付平台会向商户服务端发送异步通知。服务端收到通知后,需要:
- 验证通知签名。
- 校验商户号、订单号、金额和币种。
- 确认订单没有被重复处理。
- 更新订单状态。
- 向支付平台返回规定的确认响应。
- 必要时主动调用支付平台的订单查询接口。
浏览器返回地址中的参数只能用于定位订单,不能作为付款凭证。即使返回链接中带有类似参数:
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 不等于支付回调
Deep Link 只负责将用户带回应用,并提供订单定位信息。用户可能没有跳回应用,但服务端已经收到成功通知。用户也可能已经回到应用,而异步通知还没有到达。
处理进程被终止的情况
支付期间,Android 进程可能被系统回收。待支付的 orderId 应能恢复,确保用户重新打开应用后,仍可在订单页面查询最终状态。
优先使用 HTTPS App Link
自定义 Scheme 可能被其他应用声明。条件允许时,应优先使用带域名验证的 Android App Link,并在服务端配置对应的 assetlinks.json。无论采用哪种返回方式,支付结果都必须由服务端确认。
备注:内容仅供参考。
