如何用 PHP 创建 payment gateway return URL
结论
将 return URL 设置为服务器上的 PHP 文件,例如:
https://example.com/payment/return.php
PHP 可以直接接收 payment gateway 通过 POST 表单或 GET 查询参数发送的数据,不需要 JavaScript,也不要继续使用普通的 something.html 文件。
如果暂时无法确认 gateway 使用哪种请求方式,可以先同时兼容 POST 和 GET。不过在正式上线前,仍需向 payment gateway 确认请求方式、checkSum 算法以及成功响应的具体要求。
可直接使用的 PHP 示例
<?php
declare(strict_types=1);
header('Content-Type: text/html; charset=UTF-8');
/**
* 优先读取 POST,其次读取 GET。
* 不使用 $_REQUEST,避免 Cookie 或 PHP 配置影响参数来源。
*/
function getGatewayParameter(string $name): ?string
{
if (isset($_POST[$name]) && !is_array($_POST[$name])) {
return trim((string) $_POST[$name]);
}
if (isset($_GET[$name]) && !is_array($_GET[$name])) {
return trim((string) $_GET[$name]);
}
return null;
}
$status = getGatewayParameter('status');
$customerReferenceNo = getGatewayParameter('customerReferenceNo');
$referenceNo = getGatewayParameter('referenceNo');
$merchantId = getGatewayParameter('merchantId');
$amount = getGatewayParameter('amount');
$checkSum = getGatewayParameter('checkSum');
$requiredValues = [
$status,
$customerReferenceNo,
$referenceNo,
$merchantId,
$amount,
$checkSum,
];
$hasMissingData = in_array(null, $requiredValues, true)
|| in_array('', $requiredValues, true);
$message = 'ERROR. NO DATA FOUND';
$messageClass = 'error';
if (!$hasMissingData) {
/*
* 必须先按照 payment gateway 文档验证 checkSum。
* 下面只能演示处理位置,不能代替真实验签。
*/
$isValidCheckSum = true;
if (!$isValidCheckSum) {
$message = 'Invalid payment response.';
} elseif (!preg_match('/^-?\d{1,4}$/', $status)) {
$message = 'Invalid transaction status.';
} elseif ($status === '0') {
/*
* 验签成功后再保存数据库。
*
* 建议使用 referenceNo 或 gateway 提供的唯一交易编号建立
* UNIQUE 约束,防止刷新页面或重复回调造成重复入库。
*/
$message = sprintf(
'Your transaction is successful and your transaction reference no for any further communication is %s.',
$referenceNo
);
$messageClass = 'success';
} elseif ((int) $status < 0) {
$message = sprintf(
'Your transaction failed and your transaction reference no for any further communication is %s.',
$referenceNo
);
} else {
$message = 'Unknown transaction status.';
}
}
$safeMessage = htmlspecialchars(
$message,
ENT_QUOTES | ENT_SUBSTITUTE,
'UTF-8'
);
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Payment Result</title>
<style>
body {
margin: 0;
padding: 40px 20px;
font-family: Arial, sans-serif;
background: #f5f5f5;
color: #222;
}
.result {
max-width: 700px;
margin: 60px auto;
padding: 30px;
text-align: center;
background: #fff;
border-radius: 8px;
}
.success {
color: #16794a;
}
.error {
color: #b42318;
}
</style>
</head>
<body>
<main class="result">
<p class="<?= $messageClass ?>">
<?= $safeMessage ?>
</p>
</main>
</body>
</html>
将文件保存为 return.php,部署到能够运行 PHP 的 Web 服务器,然后把该文件的 HTTPS 地址提供给 payment gateway。
POST 和 GET 分别如何接收
如果 gateway 使用 POST,可以从 $_POST 中读取参数:
$status = $_POST['status'] ?? null;
如果使用 GET,例如:
https://example.com/payment/return.php?status=0&referenceNo=ABC123
则从 $_GET 中读取参数:
$status = $_GET['status'] ?? null;
示例里的 getGatewayParameter() 会先读取 POST,如果没有找到参数,再读取 GET,因此可以兼容两种方式。
不要直接使用 $_REQUEST。它可能同时包含 GET、POST 和 Cookie 数据,参数的优先级还会受到 PHP 配置影响。
排查接入问题时,最好记录请求方法,但不要将完整的支付参数或敏感信息写入公开日志:
$requestMethod = $_SERVER['REQUEST_METHOD'] ?? 'UNKNOWN';
为什么不能直接使用 something.html
普通的 .html 文件通常只会作为静态内容返回,服务器不会执行其中的 PHP。应将页面改为:
something.php
同时确认服务器已经正确配置 PHP。
有些服务器确实可以配置为在 .html 文件中执行 PHP,但通常没有必要这样做,也会增加维护和安全方面的问题。这里同样不需要 JavaScript,gateway 提交的数据可以直接由服务器端 PHP 接收。
field specification 的可能含义
这些缩写通常用来描述字段的最大长度和允许使用的字符:
4N:通常表示最多 4 位数字。30AN:通常表示最多 30 个字母或数字。32AN:通常表示最多 32 个字母或数字。10AN:通常表示最多 10 个字母或数字。64AN:通常表示最多 64 个字母或数字。9.2AN:这种写法并不标准,可能表示金额包含 9 位整数和 2 位小数,也可能是该 gateway 自己定义的格式。
N 一般表示 numeric,AN 一般表示 alphanumeric。不过,不同 payment gateway 对字符范围的定义可能不同,有些还允许空格、连字符或其他符号。
特别是 amount 的 9.2AN,必须以该 gateway 的正式文档或技术支持答复为准,不能只根据缩写推断。
数据入库前必须验证
收到参数并不代表交易确实成功。任何人都可以通过浏览器自行构造请求,例如:
https://example.com/payment/return.php?status=0&referenceNo=FAKE
因此,不能只检查:
$status === '0'
就更新订单状态。至少要完成以下检查:
- 验证
checkSum,并严格使用 gateway 规定的字段顺序、密钥和哈希算法。 - 检查
merchantId是否属于当前商户。 - 根据
customerReferenceNo查找本地订单。 - 比较回传金额与订单应付金额。
- 确认订单尚未成功处理,避免重复回调。
- 如果 gateway 提供订单查询 API,必要时主动查询并再次确认交易状态。
校验 checksum 时,应使用常量时间比较:
$isValidCheckSum = hash_equals($expectedCheckSum, $checkSum);
$expectedCheckSum 必须严格按照 gateway 文档规定的方式生成。字段拼接顺序、大小写、金额格式或字符编码只要有一处不同,都可能导致校验失败。
数据库处理建议
写入数据库时应使用预处理语句,不要直接拼接 SQL:
$stmt = $pdo->prepare(
'INSERT INTO payment_transactions
(status, customer_reference_no, reference_no, merchant_id, amount, checksum)
VALUES
(:status, :customer_reference_no, :reference_no, :merchant_id, :amount, :checksum)'
);
$stmt->execute([
':status' => $status,
':customer_reference_no' => $customerReferenceNo,
':reference_no' => $referenceNo,
':merchant_id' => $merchantId,
':amount' => $amount,
':checksum' => $checkSum,
]);
不要直接用浮点数比较金额。可以按照 gateway 规定的精度使用数据库 DECIMAL 类型,也可以统一换算为最小货币单位,例如“分”,再按整数处理。
还应为 gateway 的唯一交易编号设置唯一约束。payment gateway 可能重复发送通知,用户也可能刷新 return 页面,因此回调处理必须具备幂等性,避免重复记账。
return URL 和后台通知地址可能不是一回事
有些 payment gateway 会将用户浏览器重定向到 return URL,另一些则会由服务器直接请求 callback、webhook 或 notification URL。它们的用途不同:
- return URL:主要用于向用户展示支付结果,但浏览器可能被关闭,请求也可能中断。
- callback/webhook URL:用于向服务器发送较为可靠的通知,以便更新订单状态。
- 查询 API:用于主动核实最终的交易结果。
如果 gateway 同时提供这些地址,应以经过验签的服务器通知或交易查询结果作为入账依据,不要只依赖用户浏览器返回的页面。
还要确认 gateway 是否要求回调处理完成后返回固定文本,例如 OK、SUCCESS,或者返回特定的 HTTP 状态码。具体要求必须遵循其接口文档。
备注:内容仅供参考。
