如何通过 POST 正确接收 payment gateway 响应?
结论
CS-Cart 4 的支付回调不应直接提交到支付处理器的 PHP 文件,也不应先调用 fn_change_order_status()。正确的处理方式是:
- 将网关的 POST 回调地址设为 CS-Cart 的
payment_notification.notify路由。 - 在支付处理器脚本的
PAYMENT_NOTIFICATION分支中读取响应。 - 验证签名、订单号、金额和币种。
- 使用
$pp_response['order_status']指定订单状态。 - 先调用
fn_finish_payment(),再调用fn_order_placement_routines()。
如果订单一直显示为 Incomplete,通常不是因为 CS-Cart 无法读取 POST,而是回调没有进入支付通知路由,或者处理器没有按照 CS-Cart 的支付流程提交状态。
设置正确的回调地址
假设支付处理器文件是:
app/payments/my_gateway.php
提交支付请求时,应生成类似下面的回调 URL:
$return_url = fn_url(
'payment_notification.notify?payment=my_gateway&order_id=' . $order_id,
AREA,
'current'
);
生成的地址通常类似:
https://example.com/index.php?dispatch=payment_notification.notify&payment=my_gateway&order_id=123
将这个地址作为通知地址、回调地址或返回地址发送给支付平台。具体参数名由网关决定,例如:
$post_data = [
'orderid' => $order_id,
'return_url' => $return_url,
'notify_url' => $return_url,
];
其中,payment=my_gateway 要与处理器文件名 my_gateway.php 对应,但不包含 .php。
如果网关区分「服务器通知地址」和「用户返回地址」,应优先通过服务器通知更新订单,不能把用户返回页面作为判断支付成功的唯一依据。
在处理器中接收 POST 响应
支付处理器可以使用下面的结构:
<?php
use Tygh\Registry;
if (!defined('BOOTSTRAP')) {
die('Access denied');
}
if (defined('PAYMENT_NOTIFICATION')) {
if ($mode === 'notify') {
$order_id = isset($_POST['orderid'])
? (int) $_POST['orderid']
: (int) ($_REQUEST['order_id'] ?? 0);
$status = isset($_POST['status'])
? strtoupper(trim((string) $_POST['status']))
: '';
if (!$order_id) {
exit('Invalid order ID');
}
$order_info = fn_get_order_info($order_id);
if (empty($order_info)) {
exit('Order not found');
}
$pp_response = [];
/*
* 必须在这里验证网关签名,并核对订单金额、币种和交易号。
* verify_gateway_response() 仅为示意函数,需要按网关文档实现。
*/
$is_valid = verify_gateway_response($_POST, $order_info);
if ($is_valid && $status === 'SUCCESS') {
$pp_response['order_status'] = 'P';
$pp_response['reason_text'] = 'Payment completed';
if (!empty($_POST['transaction_id'])) {
$pp_response['transaction_id'] =
(string) $_POST['transaction_id'];
}
} else {
$pp_response['order_status'] = 'F';
$pp_response['reason_text'] = $is_valid
? 'Gateway returned status: ' . $status
: 'Invalid payment response';
}
fn_finish_payment($order_id, $pp_response);
fn_order_placement_routines('route', $order_id);
}
} else {
/*
* 此分支用于创建支付请求,并将买家跳转到支付网关。
*/
}
这里的 verify_gateway_response() 不是 CS-Cart 的内置函数,只用于表示网关验证步骤。你需要按照网关文档自行实现,例如重新计算签名,或者调用网关提供的查询接口确认交易。
为什么不应直接调用 fn_change_order_status()
原代码先执行:
fn_change_order_status($orderid, 'P');
然后才调用:
fn_finish_payment($orderid, $_POST, false);
这种写法有两个问题。
首先,fn_finish_payment() 接收的是符合 CS-Cart 格式的支付结果数组,不是未经整理的整个 $_POST。网关返回的 status 字段不会被自动解释为 CS-Cart 的订单状态。
正确的结果格式至少应包含:
$pp_response = [
'order_status' => 'P',
'reason_text' => 'Payment completed',
];
其次,fn_finish_payment() 会保存支付信息并推进订单状态。提前调用 fn_change_order_status() 可能引起重复的状态变更和通知,也可能绕过支付处理器原本应执行的逻辑。
因此,应由 fn_finish_payment() 根据 $pp_response['order_status'] 更新订单状态。
$_POST 与 $_REQUEST 应该怎样选择
如果网关明确通过 POST 发送数据,可以直接读取:
$status = $_POST['status'] ?? '';
$order_id = (int) ($_POST['orderid'] ?? 0);
不过,回调 URL 中的 order_id、payment 等 CS-Cart 路由参数通常放在查询字符串里,所以处理器经常会同时使用 $_REQUEST:
$order_id = (int) ($_POST['orderid'] ?? $_REQUEST['order_id'] ?? 0);
真正需要注意的不是必须选择 $_POST 还是 $_REQUEST,而是请求必须先进入 CS-Cart 的这个路由:
payment_notification.notify
如果通知地址写成:
https://example.com/app/payments/my_gateway.php
请求会绕过 CS-Cart 的初始化和支付通知分发,处理器通常无法正常工作。
还需要检查的事项
回调是否真的到达网站
网关页面显示 SUCCESS,只能说明网关认为支付成功,不能证明通知已经到达 CS-Cart。需要检查:
- 回调 URL 能否从公网访问。
- HTTPS 证书是否有效。
- 网关请求是否遇到超时、重定向或防火墙拦截。
- POST 字段名是否确实是
status和orderid。 - 网关是否只把结果提交到了浏览器返回地址。
调试时可以记录字段名和必要的非敏感值,但不要记录银行卡资料、完整密钥或其他敏感支付数据。
必须验证回调真实性
不能只根据下面的判断把订单设为已付款:
if ($_POST['status'] === 'SUCCESS') {
// ...
}
任何知道回调地址的人都可能伪造请求。至少要核对:
- 网关签名或消息摘要;
- 商户账号;
- CS-Cart 订单号;
- 实付金额;
- 币种;
- 网关交易号;
- 交易状态。
如果网关支持服务端交易查询,必要时还应主动查询一次交易结果。
防止重复通知
支付网关可能多次发送同一笔交易的通知。处理器应检查订单当前状态和网关交易号,避免重复处理已经成功的订单:
if ($order_info['status'] === 'P') {
exit('OK');
}
实际实现时还应使用交易号作为幂等依据,因为不同店铺可能会使用自定义的成功订单状态。
正确返回网关要求的响应
有些网关只有收到特定文本才会停止重试,例如:
echo 'OK';
exit;
如果 fn_order_placement_routines('route', ...) 已经触发跳转,处理器就无法再返回指定文本。服务器之间的通知是否跳转,应由网关协议决定:
fn_finish_payment($order_id, $pp_response);
echo 'OK';
exit;
用户浏览器返回时则可以使用:
fn_order_placement_routines('route', $order_id);
如果网关同时提供异步通知和浏览器返回,最好分开处理。异步通知负责确认支付并更新订单,浏览器返回只负责展示订单结果。
备注:内容仅供参考。
