CodeIgniter 集成 VoguePay 支付后无法返回商户网站
结论
支付本身已经成功,问题出在回调地址和通知处理逻辑上:
- View 必须向 VoguePay 提交公网可访问的
notify_url、success_url和fail_url。 notify()不能写死v_transaction_id=11111,而要读取 VoguePay 实际返回的transaction_id。- 查询交易详情的 URL 不能被换行拆开,尤其不能在
?后换行。 - 通知接口和浏览器返回页面要分开处理。通知接口负责验签、查单和更新订单,成功页面只展示结果。
- 如果 CodeIgniter 开启了 CSRF,第三方服务器向通知接口发起 POST 请求时可能被拦截。需要为该路由配置例外,或按项目现有的 CSRF 机制处理。
- 当前代码在确认交易通过后,没有明确更新订单、返回响应或跳转。即使通知已经到达,用户也看不到完整的支付成功流程。
推荐的支付表单配置
下面的示例使用 VoguePay 旧版表单参数名。不同账户或接口版本支持的字段可能不同,请以当前 VoguePay 商户后台和接口文档为准。
<form method="post" action="https://voguepay.com/pay/">
<input type="hidden" name="v_merchant_id" value="<?= html_escape($merchant_id) ?>">
<input type="hidden" name="merchant_ref" value="<?= html_escape($order_reference)) ?>">
<input type="hidden" name="memo" value="Order payment">
<input type="hidden" name="total" value="<?= html_escape($amount) ?>">
<input
type="hidden"
name="notify_url"
value="<?= site_url('vgniter/notify') ?>"
>
<input
type="hidden"
name="success_url"
value="<?= site_url('vgniter/success') ?>"
>
<input
type="hidden"
name="fail_url"
value="<?= site_url('vgniter/failed') ?>"
>
<button type="submit">Pay Now</button>
</form>
这三个地址各有用途:
notify_url:接收 VoguePay 服务器发送的交易通知。success_url:付款完成后,用户浏览器返回的页面。fail_url:付款失败或取消后,用户浏览器返回的页面。
不要把 notify_url 当作普通的支付成功页面。服务器通知可能早于或晚于用户跳转,也可能重复发送。
修改 Controller
可以按下面的方式处理:
<?php
defined('BASEPATH') OR exit('No direct script access allowed');
class Vgniter extends CI_Controller
{
public function __construct()
{
parent::__construct();
$this->load->library('Vgniter_lib');
$this->load->helper(array('url', 'security'));
}
public function index()
{
$this->template->loadData(
'activeLink',
array('vgniter' => array('general' => 1))
);
if (!$this->settings->info->payment_enabled) {
$this->template->error(lang('error_60'));
return;
}
$this->template->loadContent('vgniter/index.php', array());
}
public function notify()
{
$transactionId = $this->input->post('transaction_id', true);
/*
* 某些接口流程可能通过查询参数返回 transaction_id。
* 只有在当前 VoguePay 接口确实如此时才保留该回退逻辑。
*/
if (empty($transactionId)) {
$transactionId = $this->input->get('transaction_id', true);
}
if (empty($transactionId)) {
return $this->respond(400, 'Missing transaction_id');
}
$query = http_build_query(array(
'v_transaction_id' => $transactionId,
'type' => 'json',
'demo' => 'true'
));
$url = 'https://voguepay.com/?' . $query;
$json = @file_get_contents($url);
if ($json === false) {
log_message(
'error',
'Unable to verify VoguePay transaction: ' . $transactionId
);
return $this->respond(502, 'Unable to verify transaction');
}
$transaction = json_decode($json, true);
if (!is_array($transaction) || json_last_error() !== JSON_ERROR_NONE) {
log_message(
'error',
'Invalid VoguePay verification response: ' . $transactionId
);
return $this->respond(502, 'Invalid verification response');
}
if (
empty($transaction['transaction_id']) ||
(string) $transaction['transaction_id'] !== (string) $transactionId
) {
return $this->respond(400, 'Transaction ID mismatch');
}
if (
empty($transaction['merchant_id']) ||
(string) $transaction['merchant_id'] !== (string) $this->config->item('voguepay_merchant_id')
) {
return $this->respond(400, 'Merchant ID mismatch');
}
if (
empty($transaction['status']) ||
$transaction['status'] !== 'Approved'
) {
return $this->respond(400, 'Transaction not approved');
}
$merchantReference = $transaction['merchant_ref'] ?? '';
if ($merchantReference === '') {
return $this->respond(400, 'Missing merchant reference');
}
/*
* 必须根据 merchant_ref 查询本地订单。
*
* 示例:
* $order = $this->order_model->findByReference($merchantReference);
*/
$order = $this->order_model->findByReference($merchantReference);
if (!$order) {
return $this->respond(404, 'Order not found');
}
/*
* 金额不要直接用浮点数比较。
* 如果数据库保存的是最小货币单位,应将双方统一转换后再比较。
*/
$expectedAmount = number_format((float) $order->amount, 2, '.', '');
$paidAmount = number_format(
(float) ($transaction['total'] ?? 0),
2,
'.',
''
);
if ($paidAmount !== $expectedAmount) {
log_message(
'error',
'VoguePay amount mismatch for order: ' . $merchantReference
);
return $this->respond(400, 'Amount mismatch');
}
/*
* 通知可能被重复发送,更新必须具有幂等性。
*/
if ($order->payment_status !== 'paid') {
$this->order_model->markAsPaid(
$order->id,
$transactionId,
$transaction
);
}
return $this->respond(200, 'OK');
}
public function success()
{
/*
* 不要仅凭浏览器跳转就把订单标记为已支付。
* 应从数据库读取 notify() 已验证并保存的支付状态。
*/
$reference = $this->input->get('merchant_ref', true);
$this->template->loadContent(
'vgniter/success.php',
array('reference' => $reference)
);
}
public function failed()
{
$this->template->loadContent('vgniter/failed.php', array());
}
private function respond($statusCode, $message)
{
return $this->output
->set_status_header($statusCode)
->set_content_type('text/plain', 'utf-8')
->set_output($message);
}
}
示例里的 order_model、findByReference() 和 markAsPaid() 只是占位写法,需要替换为项目实际使用的订单模型和方法。
原代码中的具体问题
1. 交易编号被写死
原代码始终查询:
v_transaction_id=11111
这个编号与当前支付交易无关,应该改为读取回调中的值:
$transactionId = $this->input->post('transaction_id', true);
再安全地构造查询参数:
$url = 'https://voguepay.com/?' . http_build_query(array(
'v_transaction_id' => $transactionId,
'type' => 'json',
'demo' => 'true'
));
2. 验证地址被错误换行
原字符串类似这样:
'https://voguepay.com/?
v_transaction_id=11111&type=json&demo=true'
换行会成为 URL 的一部分,可能导致请求失败。URL 必须连续构造,不能紧接着 ? 换行。
3. View 没有提供回调地址
支付表单没有提交 notify_url 时,VoguePay 不知道该把通知发送到哪里。如果 success_url 也没有正确设置,支付完成后,用户浏览器就无法返回商户网站。
建议使用下面的方式生成地址:
site_url('vgniter/notify')
不要手工拼接相对路径。
4. 通知接口不应使用 redirect()
调用 notify_url 的是 VoguePay 服务器,不是用户浏览器。因此,下面的逻辑通常起不到预期作用:
redirect(base_url('vgniter/failed'));
VoguePay 服务器只会收到 HTTP 重定向响应,用户所在的页面不会跟着跳转。通知接口应该返回明确的 HTTP 状态码和简短文本:
return $this->output
->set_status_header(400)
->set_output('Transaction not approved');
用户浏览器的跳转应由 success_url 和 fail_url 处理。
5. 缺少真实的订单校验
仅检查下面这个条件并不够:
$transaction['status'] === 'Approved'
还需要至少验证以下内容:
transaction_id是否与回调值一致;merchant_id是否属于当前商户;merchant_ref是否能对应到本地订单;- 实付金额是否与订单金额一致;
- 该笔交易是否已经处理过;
- 生产环境是否误用了
demo=true。
如果 VoguePay 当前接口提供签名或校验散列,还需要按照官方规则验签。
CodeIgniter 路由和 CSRF
先确保这些路由可以被外部请求访问。例如:
$route['vgniter/notify'] = 'vgniter/notify';
$route['vgniter/success'] = 'vgniter/success';
$route['vgniter/failed'] = 'vgniter/failed';
如果启用了 CodeIgniter 3 CSRF,VoguePay 无法携带站点生成的 CSRF token,通知请求可能在进入 Controller 前就被拦截并返回 403。确认框架版本和安全要求后,可以为通知路由配置例外:
$config['csrf_exclude_uris'] = array(
'vgniter/notify'
);
通知接口排除 CSRF 检查后,安全性必须由服务端查单、商户号校验、金额校验和接口签名来保证,不能直接信任 POST 内容。
部署时还要检查
notify_url必须是公网可访问的完整 HTTPS 地址,不能使用localhost或内网地址。- 通知路由不能要求用户登录,也不能依赖浏览器 Session。
- 查看 Web 服务器访问日志和 CodeIgniter 日志,确认通知是否已经到达。
- 确认防火墙、WAF 和反向代理没有拦截 VoguePay 请求。
- 生产环境应移除
demo=true,具体参数仍以当前 VoguePay 接口要求为准。 file_get_contents()依赖 PHP 的allow_url_fopen。如果服务器禁用了该配置,需要改用 cURL,并设置连接超时、响应超时和 TLS 校验。- 支付通知可能延迟到达,也可能重复发送。订单更新必须具有幂等性,最好保留原始通知和查单结果,以便审计。
- 浏览器到达
success_url并不表示付款已经通过。最终支付状态只能以服务器端验证结果为准。
备注:内容仅供参考。
