PAYATHON 2026

华为旧款设备首次通过webview调用支付宝H5支付失败

支付阿杰

结论

在 WebView 中统一拦截支付宝支付跳转使用的自定义协议,再通过 Android 原生 Intent 唤起支付宝 App。alipays://、intent:// 等非 HTTP URL 不应继续交给 WebView 加载。

部分较早的华为设备或旧版 WebView 在首次处理自定义协议跳转时并不稳定,可能忽略或拦截请求,也可能将其当作普通网页加载。第二次支付能够正常唤起支付宝,往往是因为页面状态、缓存或重定向链已经建立。这个问题应通过完善跳转处理逻辑来解决,不能依赖用户再次发起支付。

原因分析

用户在支付宝 H5 支付页面确认付款后,页面通常会尝试跳转到支付宝 App 对应的自定义协议,例如:

alipays://platformapi/startApp?...

有些场景下也会出现:

intent://...

WebView 主要用于加载 http:// 和 https:// 页面。非 HTTP 协议能否正常跳转,会受到以下因素影响:

  • 设备内置 WebView 的版本和厂商定制行为;
  • 应用是否实现了 WebViewClient.shouldOverrideUrlLoading();
  • 应用是否兼容了新旧两个 shouldOverrideUrlLoading() 重载;
  • 支付链接是否经过多次重定向;
  • 页面是否通过新窗口、表单提交或 JavaScript 发起跳转;
  • 应用是否错误地将 alipays:// URL 再次交给 WebView 加载。

如果代码只实现了其中一个回调,旧设备可能不会进入预期的拦截逻辑。

解决步骤

1. 为 WebView 设置自定义 WebViewClient

同时实现新旧两个 shouldOverrideUrlLoading() 方法,以兼容不同的 Android 和 WebView 版本。

2. 由原生代码处理支付宝协议

检测到 alipays:// 后,使用 Intent.ACTION_VIEW 打开 URI,并返回 true,表示应用已经处理了这次跳转。

3. 处理 intent 协议

遇到 intent:// URL 时,使用 Intent.parseUri() 解析。如果支付宝未安装或目标 Activity 不存在,应捕获异常并提示用户,必要时再处理可信的降级地址。

4. 普通网页仍由 WebView 加载

遇到 http:// 和 https:// 地址时返回 false。不要重复调用 loadUrl(),否则可能导致重定向异常。

Kotlin 示例

@SuppressLint("SetJavaScriptEnabled")
private fun initPayWebView(webView: WebView) {
    webView.settings.apply {
        javaScriptEnabled = true
        domStorageEnabled = true
    }

    webView.webViewClient = object : WebViewClient() {

        override fun shouldOverrideUrlLoading(
            view: WebView,
            request: WebResourceRequest
        ): Boolean {
            return handlePayUrl(view.context, request.url.toString())
        }

        @Suppress("DEPRECATION")
        override fun shouldOverrideUrlLoading(
            view: WebView,
            url: String
        ): Boolean {
            return handlePayUrl(view.context, url)
        }
    }
}

private fun handlePayUrl(context: Context, url: String): Boolean {
    return when {
        url.startsWith("alipays://", ignoreCase = true) -> {
            openExternalApp(context, Uri.parse(url))
            true
        }

        url.startsWith("intent://", ignoreCase = true) -> {
            openIntentUrl(context, url)
            true
        }

        url.startsWith("http://", ignoreCase = true) ||
            url.startsWith("https://", ignoreCase = true) -> {
            false
        }

        else -> {
            // 未知协议不直接放行,避免任意应用被恶意页面唤起。
            true
        }
    }
}

private fun openExternalApp(context: Context, uri: Uri) {
    try {
        val intent = Intent(Intent.ACTION_VIEW, uri).apply {
            addCategory(Intent.CATEGORY_BROWSABLE)
            if (context !is Activity) {
                addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
            }
        }
        context.startActivity(intent)
    } catch (e: ActivityNotFoundException) {
        Toast.makeText(context, "未安装支付宝或无法打开支付宝", Toast.LENGTH_SHORT).show()
    } catch (e: Exception) {
        Log.e("AliPayWebView", "Failed to open Alipay: $uri", e)
        Toast.makeText(context, "暂时无法打开支付宝", Toast.LENGTH_SHORT).show()
    }
}

private fun openIntentUrl(context: Context, url: String) {
    try {
        val intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME).apply {
            addCategory(Intent.CATEGORY_BROWSABLE)
            component = null
            selector = null

            if (context !is Activity) {
                addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
            }
        }
        context.startActivity(intent)
    } catch (e: ActivityNotFoundException) {
        Toast.makeText(context, "未找到可处理支付请求的应用", Toast.LENGTH_SHORT).show()
    } catch (e: Exception) {
        Log.e("AliPayWebView", "Failed to handle intent URL", e)
        Toast.makeText(context, "支付请求无法打开", Toast.LENGTH_SHORT).show()
    }
}

Java 示例

webView.getSettings().setJavaScriptEnabled(true);
webView.getSettings().setDomStorageEnabled(true);

webView.setWebViewClient(new WebViewClient() {
    @Override
    public boolean shouldOverrideUrlLoading(
            WebView view,
            WebResourceRequest request
    ) {
        return handleUrl(view.getContext(), request.getUrl().toString());
    }

    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        return handleUrl(view.getContext(), url);
    }
});

private boolean handleUrl(Context context, String url) {
    if (url.startsWith("alipays://")) {
        try {
            Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
            intent.addCategory(Intent.CATEGORY_BROWSABLE);

            if (!(context instanceof Activity)) {
                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            }

            context.startActivity(intent);
        } catch (ActivityNotFoundException e) {
            Toast.makeText(context, "未安装支付宝或无法打开支付宝",
                    Toast.LENGTH_SHORT).show();
        }
        return true;
    }

    if (url.startsWith("intent://")) {
        try {
            Intent intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
            intent.addCategory(Intent.CATEGORY_BROWSABLE);
            intent.setComponent(null);
            intent.setSelector(null);

            if (!(context instanceof Activity)) {
                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            }

            context.startActivity(intent);
        } catch (Exception e) {
            Toast.makeText(context, "支付请求无法打开",
                    Toast.LENGTH_SHORT).show();
        }
        return true;
    }

    return !(url.startsWith("http://") || url.startsWith("https://"));
}

首次仍然无法唤起时如何排查

记录首次和第二次支付时的完整跳转信息:

Log.d("AliPayWebView", "redirect url=$url")

排查时主要确认以下几点:

  • 首次支付是否进入了 shouldOverrideUrlLoading();
  • 实际使用的是 alipays://、intent://,还是其他中间地址;
  • 回调中是否再次调用了 webView.loadUrl(url);
  • 是否抛出了 ActivityNotFoundException 或 URISyntaxException;
  • 支付页是否通过新窗口打开。

如果支付页使用 window.open(),还要根据业务需要配置多窗口处理:

webView.settings.setSupportMultipleWindows(true)
webView.webChromeClient = object : WebChromeClient() {
    override fun onCreateWindow(
        view: WebView,
        isDialog: Boolean,
        isUserGesture: Boolean,
        resultMsg: Message
    ): Boolean {
        // 应根据实际支付页面的跳转方式创建临时 WebView,
        // 或将可信 URL 转交给当前 WebView。
        return false
    }
}

在没有确认页面行为前,不要直接返回 true,否则可能已经创建了窗口,却没有继续加载支付地址。

注意事项

  • 必须同时覆盖两个 shouldOverrideUrlLoading() 方法,因为旧设备可能只调用旧签名。
  • 成功拦截后必须返回 true,否则 WebView 仍会尝试加载自定义协议。
  • 普通 HTTP/HTTPS 地址通常应返回 false,不要无条件再次调用 loadUrl()。
  • 不要清理支付过程中需要使用的 Cookie、缓存或页面状态。
  • 只允许可信协议和可信支付来源,防止网页通过任意 URI 唤起其他应用。
  • 不要将完整支付 URL、授权参数或订单信息写入生产日志。
  • 支付结果应以服务端异步通知和订单查询结果为准,不能只根据支付宝 App 是否成功唤起、页面回跳结果或前端提示来判断。
  • 如果首次跳转完全没有进入任何 URL 拦截回调,需要结合真实支付页面排查表单提交、新窗口和 WebView 内核行为,不能仅凭设备品牌认定唯一原因。

备注:内容仅供参考。