PAYATHON 2026

Vue2 调用支付宝支付接口返回 form 后无法跳转

支付老李

结论

接口返回的是 HTML 字符串。通过 Axios、Fetch 等 AJAX 请求获取后,浏览器只会把它作为响应数据处理,不会自动渲染其中的 <form>,也不会执行 <script>document.forms[0].submit()</script>。

前端需要解析返回内容,创建真实的 <form> 节点并挂载到当前页面,然后调用原生 submit()。表单提交后,浏览器会跳转到支付宝收银台。

Vue2 处理方式

下面的示例使用 Axios,并在当前窗口打开支付宝页面:

<template>
  <button :disabled="paying" @click="goToAlipay">
    {{ paying ? '正在跳转...' : '支付宝支付' }}
  </button>
</template>

<script>
import axios from 'axios'

export default {
  data() {
    return {
      paying: false
    }
  },

  methods: {
    async goToAlipay() {
      if (this.paying) return

      this.paying = true

      try {
        const response = await axios.post(
          '/api/alipay/page-pay',
          {
            orderId: this.orderId
          },
          {
            responseType: 'text'
          }
        )

        const html = response.data

        if (typeof html !== 'string' || !html.trim()) {
          throw new Error('支付接口未返回有效的 form')
        }

        const parser = new DOMParser()
        const documentNode = parser.parseFromString(html, 'text/html')
        const sourceForm = documentNode.querySelector('form')

        if (!sourceForm) {
          throw new Error('支付接口返回内容中没有 form')
        }

        const action = (sourceForm.getAttribute('action') || '').trim()
        const actionUrl = new URL(action, window.location.href)

        // 防止接口异常或返回内容被篡改后跳转到未知站点
        if (
          actionUrl.protocol !== 'https:' ||
          actionUrl.hostname !== 'openapi.alipay.com'
        ) {
          throw new Error('支付宝网关地址不合法')
        }

        const form = document.createElement('form')
        form.method = (sourceForm.getAttribute('method') || 'post').toLowerCase()
        form.action = actionUrl.href
        form.style.display = 'none'

        sourceForm
          .querySelectorAll('input, select, textarea')
          .forEach(control => {
            if (!control.name || control.disabled) return

            const input = document.createElement('input')
            input.type = 'hidden'
            input.name = control.name
            input.value = control.value
            form.appendChild(input)
          })

        document.body.appendChild(form)

        // 使用原生方法,避免表单中存在 name="submit" 时覆盖 form.submit
        HTMLFormElement.prototype.submit.call(form)
      } catch (error) {
        this.paying = false
        console.error('发起支付宝支付失败:', error)
        this.$message?.error(error.message || '发起支付失败,请稍后重试')
      }
    }
  }
}
</script>

这段代码不依赖返回内容中的 <script>。即使支付宝 SDK 返回的脚本没有执行,前端也会主动提交解析出的表单。

还要注意,问题示例中的 action 开头有一个空格:

action=" https://openapi.alipay.com/gateway.do..."

读取地址后需要调用 trim()。最好在后端生成表单时就去掉这个空格。

简单实现

如果返回内容完全来自可信的自有后端,也可以先将其插入页面,再查找并提交表单:

axios.post('/api/alipay/page-pay', params, {
  responseType: 'text'
}).then(({ data }) => {
  const container = document.createElement('div')
  container.style.display = 'none'
  container.innerHTML = data

  const form = container.querySelector('form')

  if (!form) {
    throw new Error('未找到支付宝支付表单')
  }

  form.action = form.action.trim()
  document.body.appendChild(container)

  HTMLFormElement.prototype.submit.call(form)
})

不要只写:

this.payHtml = response.data

然后使用:

<div v-html="payHtml"></div>

通过 v-html 插入的 <script> 通常不会执行,因此返回内容里的自动提交代码不会生效。直接插入未经验证的 HTML 还可能带来 XSS 风险,建议优先解析并重建表单。

如果需要在新窗口打开

等异步请求结束后再调用 window.open(),浏览器可能会将其视为弹窗并拦截。可以在按钮点击事件中先同步创建窗口,再将表单提交到该窗口:

async openAlipayWindow() {
  const targetName = `alipay_pay_${Date.now()}`
  const payWindow = window.open('', targetName)

  if (!payWindow) {
    this.$message?.error('支付窗口被浏览器拦截,请允许本站打开弹窗')
    return
  }

  try {
    const { data } = await axios.post('/api/alipay/page-pay', {
      orderId: this.orderId
    }, {
      responseType: 'text'
    })

    const parsed = new DOMParser().parseFromString(data, 'text/html')
    const sourceForm = parsed.querySelector('form')

    if (!sourceForm) {
      throw new Error('未找到支付宝支付表单')
    }

    const actionUrl = new URL(
      (sourceForm.getAttribute('action') || '').trim()
    )

    if (
      actionUrl.protocol !== 'https:' ||
      actionUrl.hostname !== 'openapi.alipay.com'
    ) {
      throw new Error('支付宝网关地址不合法')
    }

    const form = document.createElement('form')
    form.method = 'post'
    form.action = actionUrl.href
    form.target = targetName
    form.style.display = 'none'

    sourceForm.querySelectorAll('input[name]').forEach(item => {
      const input = document.createElement('input')
      input.type = 'hidden'
      input.name = item.name
      input.value = item.value
      form.appendChild(input)
    })

    document.body.appendChild(form)
    HTMLFormElement.prototype.submit.call(form)
    form.remove()
  } catch (error) {
    payWindow.close()
    console.error(error)
  }
}

如果不需要保留当前页面,直接在当前窗口提交最省事,也不会遇到弹窗拦截。

return_url 和 notify_url 的区别

表单跳转和回调地址正常工作是两回事。

return_url

return_url 是前台同步跳转地址。用户完成支付后,支付宝会通过浏览器将用户带回这个地址。

该地址可以先指向后端验签接口,再由后端跳转到 Vue 页面。例如:

支付宝
  → https://www.example.com/api/alipay/return
  → 后端验签并查询订单
  → 302 跳转到 https://www.example.com/#/pay/result?orderNo=xxx

不要只根据 return_url 参数将订单标记为支付成功。用户可能关闭页面或遇到网络中断,请求也可能被伪造。结果页应调用后端订单查询接口,并以后端记录的订单状态为准。

notify_url

notify_url 是支付宝服务器向商户服务器发送异步通知的地址。它不会进入 Vue 路由,也不依赖浏览器触发。

后端收到通知后需要:

  1. 按支付宝规则验证签名。
  2. 核对 app_id、商户订单号、金额、卖家信息等字段。
  3. 根据 trade_status 更新订单。
  4. 做幂等处理,避免重复通知造成重复入账。
  5. 处理成功后返回支付宝要求的成功响应,通常是纯文本:
success

具体字段和验签方法应以当前使用的支付宝服务端 SDK 及对应接口文档为准。

回调地址常见问题

示例中使用了以下地址:

http://www.itgl.com.cn/alipay/returnUrl
http://www.itgl.com.cn/alipay/notifyUrl

实际部署时建议改用公网可访问的 HTTPS 地址,并检查以下事项:

  • notify_url 不能填写 localhost、内网 IP 或 Vue 前端路由。
  • 服务器、防火墙和网关必须允许支付宝访问 notify_url。
  • 回调接口不能要求登录态,也不能依赖 Cookie。
  • 如果项目启用了 CSRF 防护,需要为支付宝回调配置相应的例外和验签机制。
  • 不要在收到异步通知前直接返回业务处理失败页面。
  • 后端日志应记录订单号、支付宝交易号、验签结果和处理结果,但不能记录私钥等敏感信息。
  • 如果配置了 CSP,需要确认 form-action 允许向 https://openapi.alipay.com 提交表单。

更推荐的后端返回格式

后端可以返回整段 HTML,但在前后端分离项目中,结构化数据更容易处理:

{
  "action": "https://openapi.alipay.com/gateway.do?...",
  "method": "post",
  "fields": {
    "biz_content": "{\"out_trade_no\":\"20240318095411184\", ...}"
  }
}

Vue 根据 action 和 fields 创建并提交表单,不需要解析 HTML,也不依赖返回内容中的脚本。

无论采用哪种格式,支付宝请求参数和签名都必须由服务端生成,私钥不能放入 Vue 项目。前端只负责提交服务端生成的支付表单。订单是否支付成功,应以后端验签后的异步通知或主动查询结果为准。

备注:内容仅供参考。