PAYATHON 2026

vue h5 调用支付宝app失败后如何在原页面支付?

支付老李

结论

唤起支付宝 App 失败后,可以继续在当前 App 的 H5 WebView 中打开支付宝收银台,无需主动调用系统默认浏览器。

需要注意以下几点:

  • “支付宝 App 支付”和“H5 支付”是两种不同的接入方式。
  • H5 页面不能直接调用支付宝原生支付 SDK。使用原生 SDK,需要宿主 App 的 Android 或 iOS 代码配合。
  • 未安装支付宝时,alipays:// 等 Scheme 不会自动、可靠地切换到 H5 支付,也不会自动生成支付页面。
  • 正确的处理方式是:唤起 App 失败后,前端请求服务端创建手机网站支付订单,再将支付宝返回的支付表单提交到当前 WebView。
  • 支付流程能否一直留在当前 WebView,还要看宿主 App 是否允许 WebView 加载支付宝域名、处理重定向和打开新窗口。

如果没有“优先使用原生 App 支付”的硬性要求,通常直接接入支付宝手机网站支付更稳妥。支付宝收银台会根据设备环境展示可用的支付方式。

为什么调用 App 失败后可能打开默认浏览器

页面通过 Scheme、Universal Link 或 Android Intent 唤起支付宝时,具体行为由操作系统和宿主 App 决定。

例如:

alipays://...

如果设备没有安装支付宝,可能出现以下情况:

  • Scheme 没有响应;
  • 系统提示找不到能够处理该链接的应用;
  • Android Intent 配置了浏览器回退地址,因而打开默认浏览器;
  • 宿主 App 拦截外部链接,并将其交给系统浏览器;
  • 页面停留在原 WebView,但支付流程没有启动。

因此,“唤起支付宝 App”并不是自带 H5 降级能力的支付接口。业务方需要通过服务端自行创建 H5 支付页面。

推荐实现流程

可以将支付流程分为两条路径:

  1. 前端向业务服务端发起创建订单请求。
  2. 尝试通过支付宝支持的跳转地址唤起支付宝 App。
  3. 根据页面可见性变化,判断页面是否可能已成功跳转。
  4. 如果页面在限定时间内仍然可见,请求服务端创建 H5 支付。
  5. 服务端调用支付宝手机网站支付接口。
  6. 前端将服务端返回的 HTML 表单写入当前页面并自动提交。
  7. 支付完成后,通过同步返回地址恢复页面,并由服务端根据异步通知确认最终支付状态。

页面可见性只能用来改善交互体验,无法准确判断用户是否安装了支付宝,也不能用来判断支付是否成功。

Vue 端示例

下面的示例展示了“先尝试唤起 App,失败后在当前 WebView 发起 H5 支付”的基本结构。appPayUrl 应由服务端根据实际接入方案返回,不应由前端拼接带签名的支付参数。

<script setup>
import { ref } from 'vue'

const paying = ref(false)

async function createOrder() {
  const response = await fetch('/api/pay/orders', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      productId: 'PRODUCT_ID'
    })
  })

  if (!response.ok) {
    throw new Error('创建订单失败')
  }

  return response.json()
}

async function startH5Pay(orderId) {
  const response = await fetch('/api/pay/alipay/wap', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ orderId })
  })

  if (!response.ok) {
    throw new Error('创建支付宝 H5 支付失败')
  }

  const { formHtml } = await response.json()

  // formHtml 是服务端调用 alipay.trade.wap.pay 后返回的完整表单。
  document.open()
  document.write(formHtml)
  document.close()
}

function tryOpenAlipay(appPayUrl, onFallback) {
  let pageHidden = false
  let fallbackExecuted = false

  const handleVisibilityChange = () => {
    if (document.hidden) {
      pageHidden = true
    }
  }

  const cleanup = () => {
    document.removeEventListener(
      'visibilitychange',
      handleVisibilityChange
    )
  }

  const fallback = () => {
    if (fallbackExecuted) return
    fallbackExecuted = true
    cleanup()
    onFallback()
  }

  document.addEventListener(
    'visibilitychange',
    handleVisibilityChange
  )

  // 应由真实的用户点击事件触发,否则可能被浏览器或 WebView 拦截。
  window.location.href = appPayUrl

  window.setTimeout(() => {
    if (!pageHidden && document.visibilityState === 'visible') {
      fallback()
    } else {
      cleanup()
    }
  }, 1800)
}

async function pay() {
  if (paying.value) return

  paying.value = true

  try {
    const { orderId, appPayUrl } = await createOrder()

    if (!appPayUrl) {
      await startH5Pay(orderId)
      return
    }

    tryOpenAlipay(appPayUrl, async () => {
      try {
        await startH5Pay(orderId)
      } catch (error) {
        paying.value = false
        console.error(error)
        alert('支付页面打开失败,请稍后重试')
      }
    })
  } catch (error) {
    paying.value = false
    console.error(error)
    alert('创建支付订单失败,请稍后重试')
  }
}
</script>

<template>
  <button :disabled="paying" @click="pay">
    {{ paying ? '正在打开支付...' : '支付宝支付' }}
  </button>
</template>

代码中的超时时间只用于改善体验,无法准确检测支付宝是否安装。部分手机跳转 App 时不会及时触发 visibilitychange,有些设备则会过一段时间才弹出确认框。

服务端创建 H5 支付

服务端需要调用支付宝手机网站支付接口 alipay.trade.wap.pay,然后将 SDK 生成的完整 HTML 表单返回给前端。

下面是伪代码。具体类名和配置方式应以项目使用的支付宝服务端 SDK 为准:

app.post('/api/pay/alipay/wap', async (req, res) => {
  const { orderId } = req.body

  const order = await findOrder(orderId)

  if (!order || order.status !== 'UNPAID') {
    return res.status(400).json({
      message: '订单不存在或状态不允许支付'
    })
  }

  const formHtml = await alipayClient.pageExecute(
    'alipay.trade.wap.pay',
    {
      bizContent: {
        out_trade_no: order.paymentNo,
        total_amount: order.amount,
        subject: order.subject,
        product_code: 'QUICK_WAP_WAY'
      },
      return_url: 'https://example.com/pay/alipay/return',
      notify_url: 'https://api.example.com/pay/alipay/notify'
    },
    'POST'
  )

  res.json({ formHtml })
})

支付宝 SDK 应负责签名和生成表单。应用私钥、支付宝公钥和签名逻辑必须保存在服务端,不能写入 Vue 代码。

在原 WebView 中提交支付表单

除了使用 document.write(),还可以创建临时容器,将表单插入 DOM 后提交:

function submitAlipayForm(formHtml) {
  const container = document.createElement('div')
  container.style.display = 'none'
  container.innerHTML = formHtml

  document.body.appendChild(container)

  const form = container.querySelector('form')

  if (!form) {
    container.remove()
    throw new Error('支付宝支付表单无效')
  }

  form.submit()
}

如果返回内容依赖内联脚本自动提交,使用 innerHTML 插入后,脚本通常不会执行。显式查找 <form> 并调用 submit() 会更可靠。

不要用 iframe 嵌入支付宝收银台。支付页面可能受到安全策略、Cookie、重定向或页面嵌套限制,容易发生无法登录、无法跳转或页面空白等问题。

宿主 App 需要配合的配置

如果 H5 页面运行在自有 App 的 WebView 中,原生端至少要确认以下事项:

  • WebView 可以访问 HTTPS 页面和支付宝相关域名;
  • 页面跳转不会被统一交给系统默认浏览器;
  • WebView 能够正确处理 location.href、表单提交和多次重定向;
  • 普通 https:// 支付地址优先在当前 WebView 中加载;
  • alipays:// 等外部 Scheme 交给系统或支付宝 App 处理;
  • WebView 支持必要的 Cookie、DOM Storage 和 JavaScript;
  • WebView 能够正确处理 window.open 或新窗口跳转;
  • Android、iOS 的 Scheme 白名单或查询限制已按实际接入方式配置。

Android WebView 可以根据 URL 类型分别处理:

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

    return when (scheme) {
        "http", "https" -> {
            // 返回 false,让普通网页继续在当前 WebView 中加载。
            false
        }

        "alipays" -> {
            try {
                val intent = Intent(Intent.ACTION_VIEW, uri)
                view.context.startActivity(intent)
            } catch (e: ActivityNotFoundException) {
                // 未安装支付宝时不要强制打开系统浏览器,
                // 由 H5 页面执行手机网站支付降级逻辑。
            }
            true
        }

        else -> true
    }
}

实际项目还需要校验允许跳转的 Scheme 和域名,不能将任意外部 URL 直接交给系统处理。

支付结果必须由服务端确认

return_url 只是用户支付后的页面跳转地址。用户可能没有返回页面,也可能遇到网络中断或参数被修改,因此不能仅凭 return_url 将订单标记为支付成功。

可靠的处理流程如下:

  • 服务端接收支付宝异步通知;
  • 验证通知签名;
  • 校验商户号、订单号、金额和订单状态;
  • 更新本地订单;
  • H5 返回页面调用业务接口查询订单状态;
  • 服务端确认支付成功后,页面再显示支付成功。
async function queryPaymentResult(orderId) {
  const response = await fetch(`/api/pay/orders/${orderId}`)
  const order = await response.json()

  if (order.status === 'PAID') {
    // 展示支付成功页面
  } else {
    // 展示处理中或允许用户重新查询
  }
}

更稳妥的选择

如果需求只是“有支付宝就用支付宝 App,没有就继续网页支付”,可以优先统一从 alipay.trade.wap.pay 进入支付宝收银台,由支付宝根据当前环境提供可用的支付方式。这比前端自行判断设备是否安装支付宝更稳定。

如果产品必须明确区分原生 App 支付和 H5 支付,宿主 App 就需要接入支付宝原生支付 SDK,并通过 JSBridge 接收 Vue 页面发起的支付请求。原生调用失败或确认未安装支付宝后,再通知 H5 执行 alipay.trade.wap.pay。纯 H5 无法像原生 App 那样可靠地检测安装状态或调用支付功能。

备注:内容仅供参考。