PAYATHON 2026

如何在 JavaScript 中获取特定 URL 的 callback data?

支付小周

结论

付款平台的 callback(也叫 webhook)是发送到服务器的 HTTP 请求,不会直接进入用户浏览器。因此,receipt.php 中的 JavaScript 无法通过“监听” payment.php,获取它此前收到的 callback data。

通常应按以下流程处理:

  1. payment.php 接收付款平台发送的 callback,并验证其有效性。
  2. 使用订单号关联记录,将付款结果保存到数据库。
  3. receipt.php 请求付款状态查询接口。
  4. 查询接口从数据库读取付款结果并返回 JSON。
  5. JavaScript 根据返回结果显示收据。如果结果尚未确认,可以在短时间内轮询。

为什么 fetch() 或 $.getJSON() 获取不到数据

付款平台通常会向 notify URL 单独发起一次 POST 请求,例如:

Payment Provider
    └── POST /demo/payment.php

用户打开收据页面时,浏览器发起的是另一次请求:

Browser
    └── GET /demo/receipt.php

两次请求之间没有直接联系。之后调用:

fetch('/demo/payment.php');

或者:

$.getJSON('/demo/payment.php');

只会再次向 payment.php 发起 GET 请求,无法读取付款平台之前发送的 POST 内容。HTTP 接口不会自动保存历史请求。服务器必须主动把相关数据写入数据库、缓存或其他持久化存储,后续请求才能读取。

付款平台发出的服务器请求通常也不会携带用户浏览器的 PHP session cookie,所以不能依赖 $_SESSION 在 callback 和收据页面之间传递付款状态。

在 notify URL 中接收 callback

callback 的具体数据格式由付款平台决定,常见格式有 JSON 和表单数据。

如果 callback 的 Content-Type 是 application/json:

<?php
// payment.php

$rawBody = file_get_contents('php://input');
$callback = json_decode($rawBody, true);

if (!is_array($callback)) {
    http_response_code(400);
    exit('Invalid JSON');
}

$orderId = $callback['order_id'] ?? null;
$status = $callback['status'] ?? null;
$amount = $callback['amount'] ?? null;

if ($orderId === null || $status === null) {
    http_response_code(400);
    exit('Missing required fields');
}

// 必须先按照付款平台的文档验证签名。
// 验证通过后,再将订单状态写入数据库。

http_response_code(200);
echo 'OK';

如果付款平台发送的是 application/x-www-form-urlencoded 或 multipart/form-data,通常从 $_POST 读取:

<?php
// payment.php

$orderId = $_POST['order_id'] ?? null;
$status = $_POST['status'] ?? null;
$amount = $_POST['amount'] ?? null;

示例无法确定实际的字段名称、签名算法和付款成功状态值,这些信息要以付款平台的 callback 文档为准。

将付款结果保存到数据库

callback 验证通过后,应根据商户订单号更新订单。下面用 PDO 演示主要逻辑:

<?php
// payment.php

$rawBody = file_get_contents('php://input');
$callback = json_decode($rawBody, true);

if (!is_array($callback)) {
    http_response_code(400);
    exit('Invalid JSON');
}

$orderId = $callback['order_id'] ?? null;
$status = $callback['status'] ?? null;
$amount = $callback['amount'] ?? null;

if (!$orderId || !$status) {
    http_response_code(400);
    exit('Missing required fields');
}

// 此处先验证平台签名。
// if (!verifySignature($rawBody, $_SERVER, $callback)) {
//     http_response_code(401);
//     exit('Invalid signature');
// }

$pdo = new PDO(
    'mysql:host=localhost;dbname=payment_demo;charset=utf8mb4',
    'db_user',
    'db_password',
    [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    ]
);

$stmt = $pdo->prepare(
    'UPDATE orders
     SET payment_status = :status,
         paid_amount = :amount,
         callback_payload = :payload,
         updated_at = NOW()
     WHERE order_id = :order_id'
);

$stmt->execute([
    ':status' => $status,
    ':amount' => $amount,
    ':payload' => $rawBody,
    ':order_id' => $orderId,
]);

http_response_code(200);
echo 'OK';

实际项目中,付款金额应保存为数据库的 DECIMAL 类型,或者换算成最小货币单位后以整数保存,不要使用浮点数计算。

提供付款状态查询接口

可以创建一个只返回当前订单状态的接口,例如 payment-status.php:

<?php
// payment-status.php

header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');

$orderId = $_GET['order_id'] ?? '';

if ($orderId === '') {
    http_response_code(400);
    echo json_encode(['error' => 'Missing order_id']);
    exit;
}

$pdo = new PDO(
    'mysql:host=localhost;dbname=payment_demo;charset=utf8mb4',
    'db_user',
    'db_password',
    [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    ]
);

$stmt = $pdo->prepare(
    'SELECT order_id, payment_status, paid_amount, updated_at
     FROM orders
     WHERE order_id = :order_id'
);

$stmt->execute([':order_id' => $orderId]);
$order = $stmt->fetch();

if (!$order) {
    http_response_code(404);
    echo json_encode(['error' => 'Order not found']);
    exit;
}

echo json_encode([
    'order_id' => $order['order_id'],
    'status' => $order['payment_status'],
    'amount' => $order['paid_amount'],
    'updated_at' => $order['updated_at'],
]);

查询接口不能因为请求中提供了某个 order_id,就直接返回敏感的付款信息。接口还要确认当前用户是否有权查看该订单,或者要求使用难以猜测且有有效期的查询令牌。

在收据页面查询并显示结果

付款平台发送 callback 和浏览器跳转到收据页的顺序并不固定。用户可能已经到达 receipt.php,callback 几秒后才抵达服务器。遇到这种情况,可以设置有限次数的轮询:

<div id="payment-result">正在确认付款结果……</div>

<script>
window.addEventListener('load', () => {
    const params = new URLSearchParams(window.location.search);
    const orderId = params.get('order_id');

    if (!orderId) {
        document.getElementById('payment-result').textContent = '缺少订单号';
        return;
    }

    pollPaymentStatus(orderId);
});

async function pollPaymentStatus(orderId) {
    const resultElement = document.getElementById('payment-result');
    const maxAttempts = 15;
    const interval = 2000;

    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
        try {
            const response = await fetch(
                `/demo/payment-status.php?order_id=${encodeURIComponent(orderId)}`,
                {
                    method: 'GET',
                    headers: {
                        'Accept': 'application/json'
                    },
                    cache: 'no-store'
                }
            );

            if (!response.ok) {
                throw new Error(`HTTP ${response.status}`);
            }

            const data = await response.json();

            if (data.status === 'paid') {
                resultElement.textContent =
                    `付款成功,订单号:${data.order_id},金额:${data.amount}`;
                return;
            }

            if (data.status === 'failed') {
                resultElement.textContent = '付款失败';
                return;
            }

            resultElement.textContent =
                `正在确认付款结果……(${attempt}/${maxAttempts})`;
        } catch (error) {
            console.error('查询付款状态失败:', error);
        }

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

    resultElement.textContent = '暂时无法确认付款结果,请稍后刷新页面。';
}
</script>

假设收据地址是:

https://test.com/demo/receipt.php?order_id=ORDER123

其中的 ORDER123 应当是商户创建付款请求时生成并保存的订单号,不能完全信任浏览器或 callback 临时提供的值。

必须注意的安全问题

不能根据用户返回 receipt.php 时携带的查询参数,直接把订单标记为付款成功。浏览器参数可以被修改。付款是否成功,只能以经过签名验证的服务器 callback 为准,或者以服务器主动调用付款平台订单查询 API 后得到的结果为准。

还要处理以下事项:

  • 按照付款平台文档验证 callback 的签名、时间戳和商户信息。
  • 核对订单号、币种和付款金额是否与原订单一致。
  • callback 可能重复发送,处理逻辑必须具备幂等性。
  • 数据库更新成功后,才能向付款平台返回成功响应。
  • 不要把签名密钥、数据库密码或付款平台密钥放进 JavaScript。
  • 保存必要的 callback 日志,但不要记录银行卡号、密钥等敏感信息。
  • payment.php 必须能被付款平台从公网访问,也不能依赖浏览器页面一直保持打开。
  • 如果 callback 长时间没有到达,可以由服务器调用付款平台提供的订单查询 API 补查。

备注:内容仅供参考。