如何在 JavaScript 中获取特定 URL 的 callback data?
结论
付款平台的 callback(也叫 webhook)是发送到服务器的 HTTP 请求,不会直接进入用户浏览器。因此,receipt.php 中的 JavaScript 无法通过“监听” payment.php,获取它此前收到的 callback data。
通常应按以下流程处理:
payment.php接收付款平台发送的 callback,并验证其有效性。- 使用订单号关联记录,将付款结果保存到数据库。
receipt.php请求付款状态查询接口。- 查询接口从数据库读取付款结果并返回 JSON。
- 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 补查。
备注:内容仅供参考。
