PAYATHON 2026

Android 通过 alipays:// 协议唤起支付宝 App 支付失败

支付小周

结论

如果 Android 已经成功打开支付宝,但支付宝提示”操作已超时,请重新发起支付”,通常说明 alipays:// 协议已经正常唤起,问题出在支付宝收到的支付信息无效。常见原因有:

  • 支付订单、支付 token 或二维码内容已过期;
  • alipays://platformapi/startApp 中的嵌套 URL 或订单参数编码不正确;
  • 参数被重复编码或截断,或者经过 Android 的 Uri、WebView、重定向后发生了变化;
  • 客户端重复使用了之前生成的支付链接;
  • 服务端生成的订单参数、签名或支付宝应用配置有误。

iOS 能成功唤起,并不代表两端最终传给支付宝的字符串完全一致。排查时要比较 Android 和 iOS 实际打开的完整 URI,不能只比较服务端返回值。

alipays://platformapi/startApp 是支付宝客户端的跳转协议。App 内支付应优先使用支付宝提供的 Android App 支付 SDK,不建议将自行拼接的协议作为主要支付链路。

先判断失败发生在哪一层

需要先区分两种情况。

支付宝 App 没有打开

重点检查:

  • 设备是否安装了支付宝;
  • Intent 是否正确;
  • URI 是否被 WebView 拦截;
  • Android 11 及以上系统是否因包可见性配置导致安装检测失败;
  • 是否误将 alipays:// 当作普通 HTTPS 地址处理;
  • 系统或浏览器是否禁止当前场景启动外部应用。

支付宝已经打开,但提示超时

这说明 Android 侧基本已经成功调起协议,问题更可能出在订单有效期、参数编码、签名或支付链接本身。

不要把这个提示直接归因于 Android 无法识别 alipays://。

排查步骤

1. 每次支付都重新创建订单

不要缓存或重复使用之前生成的支付 URI、二维码内容、订单字符串或支付 token。

用户点击”立即支付”后,客户端应向业务服务端请求一笔新订单,并立即调起支付宝。服务端同时记录:

  • 商户订单号;
  • 创建时间;
  • 支付渠道;
  • 返回给客户端的原始支付参数;
  • 支付宝接口返回结果;
  • 订单当前状态。

如果使用同一条 URI 多次测试,第二次或间隔一段时间后出现”操作已超时”,应先确认 URI 中是否包含一次性或短时有效的支付凭证。

2. 比较 Android 和 iOS 最终使用的 URI

分别记录两端调用系统跳转接口前的完整字符串,然后逐字符比较:

  • 参数名称和数量是否一致;
  • &、=、?、# 是否被截断或重新解释;
  • % 是否变成 %25;
  • + 是否被当成空格;
  • Base64 内容中的 +、/、= 是否被修改;
  • 中文及非 ASCII 字符是否使用 UTF-8;
  • 嵌套 URL 是否只编码了一次;
  • Android 拼接 URI 时是否遗漏参数。

日志可能包含订单号、签名或 token。线上环境必须脱敏,不要把完整支付凭证上传到公共日志平台。

3. 检查嵌套参数的 URL 编码

如果协议参数中还包含 URL、二维码内容或订单参数,应编码参数值,不要把原始值直接拼入 URI。

例如:

import android.net.Uri

val nestedUrl = serverResponse.payUrl

val schemeUri = Uri.Builder()
    .scheme("alipays")
    .authority("platformapi")
    .appendPath("startApp")
    .appendQueryParameter("appId", serverResponse.appId)
    .appendQueryParameter("url", nestedUrl)
    .build()

Uri.Builder.appendQueryParameter() 会对参数值进行必要的编码。不要先完整编码 nestedUrl,再将它交给 appendQueryParameter(),否则可能造成重复编码。

如果服务端返回的已经是完整 alipays:// URI,客户端原则上应原样使用:

val paymentUri = Uri.parse(serverResponse.alipayScheme)

此时不要再对整个字符串调用 URLEncoder.encode()。整体编码会破坏协议头和查询参数结构。

4. 使用原生 Intent 调起

import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri

fun openAlipay(context: Context, scheme: String): Boolean {
    return try {
        val intent = Intent(Intent.ACTION_VIEW, Uri.parse(scheme)).apply {
            addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
        }
        context.startActivity(intent)
        true
    } catch (e: ActivityNotFoundException) {
        false
    } catch (e: SecurityException) {
        false
    }
}

如果业务要求只能由支付宝处理,可以在充分验证兼容性后指定支付宝包名:

val intent = Intent(Intent.ACTION_VIEW, Uri.parse(scheme)).apply {
    setPackage("com.eg.android.AlipayGphone")
}

指定包名可能降低对不同发行渠道、设备环境及未来客户端变化的兼容性。除非支付宝接入文档明确要求,否则更适合通过 URI 调起,并处理没有可用应用的情况。

5. 检查 Android 11 及以上的包可见性

如果代码会先通过 PackageManager 检测支付宝是否安装,Android 11 及以上系统可能需要在 AndroidManifest.xml 中声明查询范围:

<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <queries>
        <package android:name="com.eg.android.AlipayGphone" />

        <intent>
            <action android:name="android.intent.action.VIEW" />
            <data android:scheme="alipays" />
        </intent>
    </queries>

</manifest>

包可见性主要影响 resolveActivity()、queryIntentActivities() 等方法的查询结果。即使查询结果为空,也不能直接认定支付宝未安装,还要结合目标 Android 版本和实际 startActivity() 结果判断。

6. 检查 WebView 拦截逻辑

如果协议来自 H5 页面,Android WebView 需要单独处理非 HTTP(S) URI:

override fun shouldOverrideUrlLoading(
    view: WebView,
    request: WebResourceRequest
): Boolean {
    val uri = request.url

    if (uri.scheme.equals("alipays", ignoreCase = true)) {
        return try {
            view.context.startActivity(
                Intent(Intent.ACTION_VIEW, uri)
            )
            true
        } catch (e: ActivityNotFoundException) {
            true
        }
    }

    return false
}

还要检查:

  • 页面是否经过多次 302 重定向;
  • WebView 是否把协议交给了错误的下载器或网络请求组件;
  • 是否配置了 URL 白名单;
  • 前端路由是否多次调用 decodeURIComponent() 或 encodeURIComponent();
  • 用户手势与实际调起之间的间隔是否过长。

推荐的解决方式

对于标准 App 支付,业务服务端应调用支付宝相关接口,生成签名后的订单信息,再由 Android 客户端通过支付宝 App 支付 SDK 发起支付。客户端不能保存商户私钥,也不应自行生成服务端签名。

典型调用形式如下:

import com.alipay.sdk.app.PayTask

Thread {
    val payTask = PayTask(activity)

    // orderInfo 必须由可信业务服务端生成并签名
    val result = payTask.payV2(orderInfo, true)

    activity.runOnUiThread {
        val resultStatus = result["resultStatus"]
        val memo = result["memo"]

        // 客户端结果仅用于界面提示。
        // 最终支付状态应由服务端主动查询或异步通知确认。
    }
}.start()

具体依赖版本、接口参数和 Android 配置,应以当前支付宝开放平台文档及已申请产品的接入说明为准,不要直接套用来源不明的协议参数。

注意事项

  • alipays:// 能打开支付宝,并不代表支付参数有效。
  • 不要重复使用一次性支付链接或历史订单参数。
  • 不要对完整 Scheme URI 进行整体 URL 编码。
  • 嵌套 URL 应按参数值编码,而且只能编码一次。
  • 支付签名必须在服务端完成,商户私钥不能放入 Android APK。
  • 不要仅凭客户端返回结果将订单标记为支付成功。
  • 最终结果应以支付宝异步通知或服务端主动查询为准。
  • 如果 Android 和 iOS 传入的 URI 字节完全一致,订单仍在有效期内,服务端签名也正确,但只有 Android 失败,应收集脱敏后的订单号、发生时间、支付宝客户端版本、Android 版本和 SDK 日志,再通过支付宝开放平台技术支持进一步核查。

备注:内容仅供参考。