PAYATHON 2026

支付宝沙箱支付成功后无法调用接口,Vue 前端如何排查?

支付阿杰

结论

支付宝沙箱显示”支付成功”,只能说明支付宝侧已完成交易。Vue 页面不会因此自动继续调用某个接口。

问题通常出在以下几个环节:

  • 把 notify_url 当成前端回调地址。它其实是支付宝向后端发送异步通知的地址。
  • 提交支付表单后,浏览器跳转到支付宝页面,原来的 Vue 页面随之卸载,表单提交后的 JavaScript 无法按预期继续执行。
  • 浏览器跳转到 return_url 后,返回页没有重新查询订单状态或调用后续业务接口。
  • 后端通知地址无法从公网访问、验签失败,或者收到通知后没有正确更新订单。
  • Vue 已经发出请求,但请求受到路由跳转、CORS、请求拦截器或参数错误的影响。

完整流程应该是:后端创建订单并生成支付参数 → 前端跳转支付宝 → 支付宝通过 notify_url 通知后端 → 后端验签并更新订单 → Vue 返回页查询订单结果 → 确认支付成功后继续业务操作。

先确认”调用不到”发生在哪一层

不要只看页面有没有变化。先打开浏览器开发者工具的 Network 面板,点击支付后依次检查:

  1. Network 中有没有出现目标接口。
  2. 如果出现,查看 HTTP 状态码、请求参数和响应内容。
  3. 如果没有出现,说明前端代码没有执行到接口调用的位置。
  4. 如果页面已经跳转到支付宝,原页面的控制台日志会丢失。可以在开发者工具中开启 Preserve log。
  5. 支付完成后检查地址栏,确认支付宝最终返回了哪个 return_url,URL 中是否带有订单号等参数。

可以在调用接口前后加入明确的日志:

console.log('准备调用订单查询接口', orderNo)

try {
  const response = await queryOrder(orderNo)
  console.log('订单查询接口返回', response)
} catch (error) {
  console.error('订单查询接口失败', error)
}

如果连第一条日志都没有出现,应先检查组件生命周期、条件判断和路由跳转,不必急着排查 Axios。

不要在提交支付表单后等待调用接口

支付宝网页支付通常由后端返回一段 HTML 表单,前端将它写入页面后自动提交:

const div = document.createElement('div')
div.innerHTML = response.data
document.body.appendChild(div)
div.querySelector('form').submit()

await callExpectedApi() // 不应依赖这里继续执行

form.submit() 会把当前页面导航到支付宝收银台。离开页面后,当前 Vue 组件和 JavaScript 执行环境会被销毁,后面的接口调用并不可靠。

支付发起和支付后的处理应分开:

async function startPayment() {
  const { data } = await axios.post('/api/payment/alipay/create', {
    orderNo: currentOrderNo.value
  })

  const container = document.createElement('div')
  container.innerHTML = data.payForm
  document.body.appendChild(container)

  const form = container.querySelector('form')
  if (!form) {
    throw new Error('后端未返回有效的支付宝支付表单')
  }

  form.submit()
}

支付完成后的接口调用应该放在返回页或订单结果页中,不能依赖 form.submit() 后面的代码继续运行。

如果业务允许,也可以通过 window.open() 打开支付页,同时在原页面轮询订单状态。不过,浏览器可能拦截并非由用户操作直接触发的新窗口,所以这种方式不适合作为唯一方案。

区分 return_url 和 notify_url

这两个地址的用途不同。

return_url

return_url 是用户完成支付后,浏览器跳转回来的页面地址,适合展示支付结果并发起订单状态查询。

它不能作为确认支付成功的最终依据。用户可能关闭页面,网络也可能中断,URL 参数还可能被人为修改。

例如:

https://example.com/payment/result?orderNo=ORDER_123

在 Vue 路由中配置结果页:

{
  path: '/payment/result',
  name: 'PaymentResult',
  component: () => import('@/views/PaymentResult.vue')
}

结果页加载后主动查询后端:

<script setup>
import { onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import axios from 'axios'

const route = useRoute()
const status = ref('QUERYING')
const message = ref('正在确认支付结果')

async function queryPaymentResult() {
  const orderNo = route.query.orderNo

  if (!orderNo) {
    status.value = 'ERROR'
    message.value = '缺少订单号'
    return
  }

  try {
    const { data } = await axios.get(`/api/orders/${orderNo}/payment-status`)

    status.value = data.status

    if (data.status === 'PAID') {
      message.value = '支付成功'
      await axios.post(`/api/orders/${orderNo}/after-payment`)
    } else {
      message.value = '支付结果确认中'
    }
  } catch (error) {
    status.value = 'ERROR'
    message.value = '查询支付结果失败,请稍后刷新'
    console.error(error)
  }
}

onMounted(queryPaymentResult)
</script>

<template>
  <section>
    <h1>{{ message }}</h1>
    <p>订单状态:{{ status }}</p>
  </section>
</template>

after-payment 必须设计为幂等接口,防止页面刷新或重复轮询导致重复发货、重复增加余额等问题。

notify_url

notify_url 是支付宝服务器向商户后端发送异步通知的地址。它不能填写 Vue 路由,也不能指向只有本机能够访问的地址。

支付宝服务器通常无法访问以下地址:

http://localhost:8080/alipay/notify
http://127.0.0.1:8080/alipay/notify
http://192.168.1.10:8080/alipay/notify

开发环境需要提供公网可访问的后端地址,并逐项确认:

  • 域名和端口可以从公网访问。
  • 网关、防火墙和反向代理允许支付宝发送请求。
  • 接口不要求登录态、JWT 或验证码。
  • CSRF 配置不会拦截该路径。
  • 后端能够接收支付宝实际发送的表单参数。
  • 验签使用沙箱应用对应的支付宝公钥或证书配置。
  • 处理成功后,后端会按照支付宝接口要求返回正确的响应。

具体的响应文本和验签方式,应以当前接入产品及所用 SDK 的文档为准。不能仅凭前端页面显示成功,就判断异步通知已经处理完成。

后端异步通知必须先验签

异步通知中的订单号、交易状态和金额都不能直接信任。后端至少要完成以下处理:

  1. 获取支付宝发送的全部参数。
  2. 使用支付宝 SDK 验证签名。
  3. 核对应用身份、商户订单号、交易金额和收款方信息。
  4. 确认交易状态属于业务认可的成功状态。
  5. 以幂等方式更新订单。
  6. 处理成功后,向支付宝返回约定的成功响应。

下面是流程示意。SDK 方法名需要根据实际使用的语言和版本调整:

@PostMapping("/api/payment/alipay/notify")
public String alipayNotify(HttpServletRequest request) {
    Map<String, String> params = extractParameters(request);

    boolean verified = verifyAlipaySignature(params);
    if (!verified) {
        return "failure";
    }

    String orderNo = params.get("out_trade_no");
    String tradeStatus = params.get("trade_status");
    String totalAmount = params.get("total_amount");

    Order order = orderService.findByOrderNo(orderNo);
    if (order == null) {
        return "failure";
    }

    if (!orderService.amountMatches(order, totalAmount)) {
        return "failure";
    }

    if (isAcceptedTradeStatus(tradeStatus)) {
        orderService.markPaidIdempotently(
            orderNo,
            params.get("trade_no")
        );
    }

    return "success";
}

不要在验签前更新订单,也不要因为前端传来了 status=success,就直接把订单状态改为已支付。

Vue 返回页最好轮询后端状态

用户返回页面时,支付宝的异步通知可能还没有处理完。因此,第一次查询得到 PENDING,不一定表示支付失败。

可以在短时间内轮询订单状态:

import axios from 'axios'

export async function waitForPaid(orderNo, options = {}) {
  const maxAttempts = options.maxAttempts ?? 10
  const interval = options.interval ?? 2000

  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
    const { data } = await axios.get(
      `/api/orders/${encodeURIComponent(orderNo)}/payment-status`
    )

    if (data.status === 'PAID') {
      return data
    }

    if (data.status === 'CLOSED' || data.status === 'FAILED') {
      throw new Error(`支付未成功,订单状态:${data.status}`)
    }

    await new Promise(resolve => setTimeout(resolve, interval))
  }

  throw new Error('支付结果仍在确认中')
}

页面中的调用方式:

onMounted(async () => {
  try {
    const result = await waitForPaid(route.query.orderNo)
    paymentStatus.value = result.status
  } catch (error) {
    console.error(error)
    paymentStatus.value = 'UNKNOWN'
  }
})

轮询要设置次数和间隔,不能无限请求。超过等待时间后,可以提示用户稍后刷新页面,或者前往订单详情查看。

检查 Axios 和 Vue 代码

如果 Network 面板中完全没有请求,需要继续检查下面这些问题。

异步函数是否真的被调用

onMounted(() => {
  queryPaymentResult()
})

不要只声明函数引用却不执行:

onMounted(() => {
  queryPaymentResult // 不会调用
})

条件判断是否提前结束

async function handlePaymentResult() {
  const orderNo = route.query.orderNo

  if (!orderNo) {
    console.warn('没有获取到 orderNo')
    return
  }

  await queryOrder(orderNo)
}

检查支付宝返回的参数名称是否与 Vue 读取的名称一致。例如,地址中使用的是 out_trade_no,代码读取的却是 orderNo,后续逻辑就可能直接结束。

比较稳妥的做法是由自己的后端生成 return_url,并在 URL 中带上系统内部订单号。返回页只使用这个订单号查询后端,不能直接信任 URL 中的支付结果。

请求是否被拦截器中断

检查 Axios 请求拦截器:

axios.interceptors.request.use(
  config => {
    return config
  },
  error => Promise.reject(error)
)

如果成功回调中漏掉了 return config,请求就无法正常发送。

还要检查响应拦截器是否会在遇到 401、业务错误码或空响应时执行路由跳转。这种跳转可能让人误以为接口没有被调用。

是否存在跨域问题

如果控制台出现 CORS 错误,说明浏览器已经尝试发送请求,只是后端不允许当前前端来源访问。应在后端或反向代理中配置允许的 Origin、Header 和 Method。

关闭浏览器安全策略不能作为正式解决方案。

接口地址是否被拼错

检查 Axios 的 baseURL 和请求路径:

const api = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: 10000
})

如果 baseURL 末尾与接口路径开头的斜杠处理不一致,或者生产环境变量没有注入,请求可能会发到 Vue 静态站点,而不是后端服务。

按完整链路逐段验证

排查时不必同时盯着所有代码,可以按照支付链路依次确认:

  1. 创建支付订单接口是否成功,商户订单号是否已保存到数据库。
  2. 前端是否拿到了有效的支付表单或跳转地址。
  3. 支付完成后,支付宝沙箱后台是否显示对应交易。
  4. 浏览器最终是否跳转到正确的 return_url。
  5. 返回页是否取得订单号,并调用订单查询接口。
  6. 后端日志中是否记录了 notify_url 请求。
  7. 异步通知是否验签成功,订单金额和订单号是否匹配。
  8. 数据库中的订单状态是否已经更新为已支付。
  9. Vue 查询接口返回的是否为数据库中的真实订单状态。
  10. 支付后的业务接口是否有幂等保护。

如果后端完全没有 notify_url 日志,先检查通知地址能否从公网访问、地址配置是否正确,以及网关是否拦截请求。

如果后端收到了通知,但订单没有更新,应重点检查参数解析、验签、公钥配置、金额校验和事务回滚。

如果订单已经更新,而 Vue 仍未显示支付成功,再检查返回页路由、订单查询接口和前端状态处理。

支付结果最终要以后端验签后的结果为准。Vue 负责发起支付、展示等待状态和查询订单结果,不负责确认支付的真实性,也不应直接决定订单是否已经支付。

备注:内容仅供参考。