PAYATHON 2026

安卓 APP 调用 alipays:// 发起支付宝支付报错如何排查?

支付小周

结论

alipays://platformapi/startapp 用于拉起支付宝并打开指定的支付宝小程序页面,它本身不是支付宝支付接口。该 URL 能否完成支付,取决于以下条件:

  • appId=2021003145623167 是有效且已发布的支付宝小程序 AppId,当前账号也有权访问。
  • page 与小程序中实际存在的页面路由完全一致。
  • query 整体经过一次 URL 编码,其中的每个参数也按照约定正确编码和解析。
  • 目标页面会主动调用支付宝支付 API,并使用服务端创建的合法交易凭证。
  • Android 能正确处理 alipays:// Scheme,设备上也安装了可用版本的支付宝。

示例 URL 最大的问题,是把完整的 Bearer Token 放进了外部 URL,而且 URL 很长。这会造成严重的凭证泄露风险,也可能因为编码、日志记录、页面跳转或 URL 长度限制而失败。该 Token 已经公开暴露,应立即作废或轮换,不能继续使用。

示例 JWT 中的 exp 还是一个固定的历史时间戳,按当前时间已经过期。如果错误发生在有效期结束后,后端鉴权必然失败。

先判断错误发生在哪一层

排查时,需要分清“拉不起支付宝”“打不开小程序”和“支付失败”。可以依次确认:

  1. Android 是否成功解析 alipays://。
  2. 支付宝是否成功拉起。
  3. 是否打开了正确的小程序。
  4. 小程序是否进入指定页面。
  5. 页面是否正确解析 query。
  6. 页面调用支付 API 时,是否拿到了合法的交易凭证。
  7. 支付宝返回了什么错误码。

如果没有支付宝错误码、Android Logcat、小程序控制台日志或服务端支付接口响应,仅凭这条 URL 无法判断唯一原因。

检查 appId 和 page

外层的 appId 应是支付宝小程序 AppId,不能填商户号、支付应用 AppId、业务系统应用编号或订单参数。

page 是小程序页面路由。不同接入方式处理前导 / 的方式可能不同。为减少兼容性问题,通常应按照小程序配置中的页面路径填写,例如:

pages/request-payment/request-payment

不要自行加上前导 /:

/pages/request-payment/request-payment

需要到对应小程序的路由配置中确认页面确实存在,并检查以下内容:

  • 路由大小写是否一致;
  • 页面是否已经随当前线上版本发布;
  • 测试版、体验版和正式版环境是否对应;
  • 页面是否有登录、会员或白名单限制;
  • 当前支付宝账号是否有权访问该版本。

可以先去掉所有业务参数,只验证小程序及目标页面能否打开:

alipays://platformapi/startapp?appId=2021003145623167&page=pages%2Frequest-payment%2Frequest-payment

如果这个最简 URL 仍然失败,应先检查 AppId、页面路由、发布状态和支付宝客户端,不必急着排查支付参数。

正确处理嵌套 query

query 的值本身也是一段查询字符串。例如,原始内容可能是:

ordersn=1116938922725771482&amount=100.00&channel=UniPay

把它放进外层 URL 前,需要将整段字符串作为一个参数值编码:

ordersn%3D1116938922725771482%26amount%3D100.00%26channel%3DUniPay

不要手工拼接 %3D、%26 等转义字符,否则很容易漏编码、重复编码,或者出现字符集不一致的问题。

示例中的这段内容需要特别检查:

token=Bearer+eyJ...

在部分查询字符串解析器中,+ 会被解释为空格;在其他场景中,它又可能被当作普通加号。如果业务确实需要传递 Bearer <token>,应将空格明确编码为 %20,不要依赖解析器对 + 的处理。

更合理的做法,是彻底避免通过 Scheme URL 传递 Bearer Token。

Android 端不要手工拼接 URL

Android 端可以使用 Uri.Builder 构造外层 URI,由系统处理参数编码:

val innerQuery = Uri.Builder()
    .appendQueryParameter("ordersn", "1116938922725771482")
    .appendQueryParameter("amount", "100.00")
    .appendQueryParameter("channel", "UniPay")
    .build()
    .encodedQuery
    .orEmpty()

val alipayUri = Uri.Builder()
    .scheme("alipays")
    .authority("platformapi")
    .appendPath("startapp")
    .appendQueryParameter("appId", "2021003145623167")
    .appendQueryParameter(
        "page",
        "pages/request-payment/request-payment"
    )
    .appendQueryParameter("query", innerQuery)
    .build()

val intent = Intent(Intent.ACTION_VIEW, alipayUri)

try {
    startActivity(intent)
} catch (e: ActivityNotFoundException) {
    // 未安装支付宝,或系统中没有应用能够处理 alipays://
}

编码层级需要处理正确:

  • 先将内层参数组成 a=1&b=2。
  • 再把整个内层字符串作为外层 query 的值。
  • 不要先手工进行百分号编码,再交给 appendQueryParameter,否则可能产生二次编码。例如,%26 会变成 %2526。

调试时可以同时打印:

Log.d("AlipayDeepLink", alipayUri.toString())
Log.d("AlipayDeepLink", Uri.decode(alipayUri.getQueryParameter("query")))

日志中的订单号、Token、手机号等敏感信息必须脱敏,生产环境不应输出完整 URL。

Android 11 及以上的应用可见性

如果代码会在启动前使用 resolveActivity() 或 queryIntentActivities() 检测支付宝,那么在 Android 11 及以上版本中,还要处理包可见性配置。可以声明需要查询的 Scheme:

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

    <application ... />
</manifest>

这里需要区分两种情况:

  • resolveActivity() 查询不到结果,不一定表示支付宝没有安装,也可能是包可见性限制导致的。
  • 直接调用 startActivity() 时抛出 ActivityNotFoundException,才表示当前系统没有能够处理该 URI 的 Activity。

如果请求从 WebView 发起,还要在 shouldOverrideUrlLoading 中拦截非 HTTP Scheme,再通过外部 Intent 处理:

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

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

    return false
}

Scheme 参数不能代替支付凭证

下面这些看起来都是业务自定义参数:

ordersn
description
order_title
amount
channel
deduct_coin
deduct_type
deduct_balance
customPayUrl
token

这些参数出现在 alipays:// URL 中,并不会让支付宝自动创建或校验支付。只有目标小程序页面主动读取并处理它们时,这些参数才有意义。

通常,正确的支付链路如下:

  1. Android APP 拉起支付宝小程序,只传递短期、一次性的业务标识。
  2. 小程序使用该标识向业务服务端请求订单。
  3. 业务服务端校验用户、金额和订单状态。
  4. 服务端调用支付宝接口创建交易并完成签名。
  5. 小程序使用服务端返回的交易凭证调用支付 API。
  6. 服务端根据支付宝异步通知确认最终支付结果。

支付金额、商品标题和订单状态不能只以 URL 参数为准,否则用户可以通过修改 URL 篡改金额。客户端显示“支付成功”,也不能作为最终入账依据。

推荐缩短参数

Deep Link 中不应包含完整 Token 或大段业务数据。建议只传一个短期、一次性的 requestId:

alipays://platformapi/startapp?appId=2021003145623167&page=pages%2Frequest-payment%2Frequest-payment&query=requestId%3D7f4c2a8e

小程序取得 requestId 后,再向服务端换取订单信息。这个标识应满足以下要求:

  • 有效期短;
  • 只能使用一次;
  • 与用户和订单绑定;
  • 不直接包含敏感信息;
  • 使用后立即失效;
  • 服务端能够防止重放。

这样可以减少 URL 过长、编码错误、日志泄露和凭证重放等风险。

小程序端也要确认解析方式

页面收到参数后,应先检查实际取得的值,不要根据生成的 URL 推测结果。例如:

Page({
  onLoad(options) {
    console.log("page options:", options);

    const requestId = options.requestId;
    if (!requestId) {
      my.alert({
        title: "参数错误",
        content: "缺少 requestId"
      });
      return;
    }

    // 使用 requestId 向服务端换取支付所需信息
  }
});

如果页面取得的是完整的 query 字符串,而不是拆分后的各个字段,就需要按照当前小程序启动参数的实际结构自行解析。具体结构应以对应支付宝小程序版本和官方接口说明为准,不能假设所有启动方式都会自动展开嵌套参数。

建议的最小化排查顺序

逐步增加参数,通常更容易找到问题:

  1. 只测试 alipays://platformapi/startapp 能否拉起支付宝。
  2. 加入正确的 appId,确认能否进入目标小程序。
  3. 加入 page,确认目标页面是否存在。
  4. 加入一个简单参数,例如 query=test%3D1。
  5. 在小程序页面记录实际收到的参数。
  6. 改为传递一次性 requestId。
  7. 检查服务端创建交易接口的响应和支付宝错误码。
  8. 检查小程序调用支付 API 时使用的交易凭证。
  9. 最后验证异步通知、订单状态和重复支付处理。

排查过程中还应记录:

  • Android 系统版本;
  • 支付宝客户端版本;
  • 请求由 APP 原生页面、浏览器还是 WebView 发起;
  • 是否成功拉起支付宝;
  • 是否成功进入小程序及指定页面;
  • 小程序实际收到的启动参数;
  • 支付 API 返回的完整错误码和脱敏后的错误信息;
  • 服务端创建订单的请求与响应;
  • 订单号、AppId、商户身份和运行环境是否匹配。

安全注意事项

示例 URL 中的 JWT 已经属于泄露凭证。即使它现在已经过期,也要检查相关系统中是否还有类似的传参方式,并采取以下措施:

  • 立即撤销或轮换仍可能有效的凭证;
  • 检查服务器、APP、WebView、代理和统计平台的日志;
  • 禁止在 URL、二维码、剪贴板和前端日志中携带访问令牌;
  • 不要在客户端保存支付宝私钥,也不要由客户端生成支付签名;
  • 通过 HTTPS 从业务服务端获取支付凭证;
  • 由服务端校验订单金额、商户、用户、有效期和幂等性;
  • 最终支付结果以服务端查询结果或支付宝异步通知为准。

排查时不能只看 alipays:// 字符串是否“看起来正确”。应先验证 Scheme、AppId 和页面路由,再检查嵌套编码,最后排查小程序的支付调用和服务端的交易创建流程。原 URL 最需要优先整改的部分,是删除 Bearer Token、缩短 query,并改为使用服务端签发的一次性业务标识。

备注:内容仅供参考。