PayPal 支付状态查询(2026-06-08)

变更背景

PayPal 支付存在一个问题:即使银行 decline(拒绝扣款),PayPal 页面仍会显示"支付成功"。买家看到成功后离开,但商户实际未收到钱。

解决方案:在 PayPal 返回页面增加轮询逻辑,查询实际 capture 状态,根据结果显示不同 UI。

前端需要对接的内容

1. PayPal 回调 URL 现在携带paypal_order_id

用户从 PayPal 支付完成后,后端会重定向到前端的 callback URL。

变更前:

https://your-frontend.com/paypal/return

变更后:

https://your-frontend.com/paypal/return?paypal_order_id=2L146185MU803905Y

前端可以从 URL query params 中获取 paypal_order_id,用于调用支付状态查询接口。

2. 支付状态查询接口

POST /api/user/orders/payment_status

用途:

  • 查询 PayPal 远程订单和 capture 的实际状态。
  • 前端在 PayPal 返回页面轮询此接口,判断支付是否真正成功。

请求参数:

字段类型必填说明
paypal_order_idstringPayPal 订单 ID,从回调 URL 的 query params 中获取

请求示例:

{
  "paypal_order_id": "2L146185MU803905Y"
}

响应示例(capture 成功):

{
  "data": {
    "paypal_order_id": "2L146185MU803905Y",
    "order_status": "completed",
    "capture_status": "completed",
    "capture_id": "9TU30802256203744"
  }
}

响应示例(capture 被拒):

{
  "data": {
    "paypal_order_id": "2L146185MU803905Y",
    "order_status": "approved",
    "capture_status": "declined",
    "capture_id": "9TU30802256203744"
  }
}

响应示例(capture 处理中):

{
  "data": {
    "paypal_order_id": "2L146185MU803905Y",
    "order_status": "approved",
    "capture_status": "pending",
    "capture_id": "3A384378MG7692232"
  }
}

响应示例(查询失败,返回空状态):

{
  "data": {
    "paypal_order_id": "2L146185MU803905Y",
    "order_status": null,
    "capture_status": null,
    "capture_id": null
  }
}

字段说明

字段说明
order_statusPayPal 远程订单状态:created / approved / completed / voided
capture_statuscapture 状态:completed(成功)/ declined(被拒)/ pending(处理中)/ null(无 capture 记录)
capture_idPayPal capture ID,可用于后续退款等操作

前端对接方案

PayPal 返回页面轮询逻辑

1. 页面加载,从 URL 获取 paypal_order_id 2. 显示 loading + "正在查询支付结果" 3. 调用 POST /api/user/orders/payment_status 4. 根据 capture_status 判断: - completed → 显示 "支付完成" - declined → 显示 "Paypal 扣款失败,请检查支付方式" - pending → 显示 "配货正在处理中,请等待配货处理结果",5 秒后重试 - null → 显示 loading,5 秒后重试 5. 最多轮询 N 次(建议 12 次,即 1 分钟),超时提示用户稍后查看订单状态

建议的前端实现

async function pollPaymentStatus(paypalOrderId: string) {
  const MAX_RETRIES = 12
  const INTERVAL = 5000 // 5 秒

  for (let i = 0; i < MAX_RETRIES; i++) {
    const { data } = await api.post('/api/user/orders/payment_status', {
      paypal_order_id: paypalOrderId,
    })

    if (data.capture_status === 'completed') {
      return 'success'
    }

    if (data.capture_status === 'declined') {
      return 'failed'
    }

    // pending 或 null,继续轮询
    await sleep(INTERVAL)
  }

  return 'timeout'
}

注意事项

  1. 不要依赖 PayPal 页面的"成功"提示:PayPal 页面显示成功不代表真正扣款成功,必须通过此接口确认。
  2. 轮询间隔建议 5 秒:正常情况 capture 很快完成(1-2 秒),异常情况可能 pending 数天。
  3. capture_status 为 declined 时:引导用户检查支付方式或更换支付方式重新支付。
  4. capture_status 为 pending 时:这是 PayPal 正在处理中的正常状态,不要标记为失败。