Joomla callback URL 参数无法更新 MySQL table
结论
一个 URL 只能用第一个 ? 引出查询字符串,后续参数要用 & 连接。正确的回调地址是:
https://example.com/index.php?option=com_componentname&controller=order&task=notify&gateway=paymentgateway&Response=1&status=10&code=123456-123456&token=f0021bfa-7ae4-48af-b4a2-d08d7a647b5e&lang=en&ignore=1
如果支付服务商固定使用 ? 拼接交易参数,就要给它一个不含查询字符串的入口,例如:
https://example.com/payment-status
再通过 Joomla 路由或服务器重写规则,将这个路径内部转发到对应的 component、controller 和 task,并保留支付服务商附加的查询参数。
仅将原 URL 设置为 SEF 别名未必有效。还要确认该路径确实执行了 order controller 的 notify task,而且查询字符串没有在跳转或重写时丢失。
两个问号为什么会导致失败
下面这个地址的结构不正确:
index.php?option=com_componentname&controller=order&task=notify&gateway=paymentgateway?Response=1&status=10
第二个 ? 不会开始一组新的查询参数。PHP 通常会把它视为 gateway 参数值的一部分,解析结果可能类似:
gateway = paymentgateway?Response=1
status = 10
这样一来,请求中没有独立的 Response 参数,gateway 的值也不再是 paymentgateway,路由或业务判断就可能失败。
把 ? 编码成 %3F 也解决不了问题,因为编码后的内容仍然属于参数值。
方案一:让支付服务商使用 & 追加参数
如果支付服务商允许 callback URL 自带参数,应要求它根据 URL 中是否已有查询字符串选择连接符:
- callback URL 没有查询字符串时,使用
? - callback URL 已有查询字符串时,使用
&
callback URL 配置为:
https://example.com/index.php?option=com_componentname&controller=order&task=notify&gateway=paymentgateway
服务商最终请求的地址应为:
https://example.com/index.php?option=com_componentname&controller=order&task=notify&gateway=paymentgateway&Response=1&status=10&code=123456-123456&token=f0021bfa-7ae4-48af-b4a2-d08d7a647b5e&lang=en&ignore=1
这是最直接的解决办法。
方案二:提供不带参数的回调路径
如果服务商始终按照 callback + "?" + 参数 生成地址,就给它提供一个不带参数的路径:
https://example.com/payment-status
需要为这个路径配置明确的 Joomla 路由,使其在内部对应:
option=com_componentname
controller=order
task=notify
gateway=paymentgateway
服务商请求的地址为:
https://example.com/payment-status?Response=1&status=10&code=123456-123456&token=f0021bfa-7ae4-48af-b4a2-d08d7a647b5e&lang=en&ignore=1
如果使用 Apache 重写,可以采用类似规则:
RewriteEngine On
RewriteRule ^payment-status/?$ index.php?option=com_componentname&controller=order&task=notify&gateway=paymentgateway [QSA,L]
QSA 会把原请求中的查询字符串追加到内部目标。如果没有它,Response、status、code 等参数可能在重写时丢失。
这条规则应放在 Joomla 通用前端控制器规则之前。具体位置取决于当前 .htaccess 和 SEF component 的规则,需避免请求被其他规则提前重定向。
也可以创建一个指向该 component 视图或任务的 Joomla 菜单项,并使用菜单项生成的 SEF URL。但必须实际验证这个 URL 最终是否执行了 notify task,不能只看页面能否打开。
在 Joomla 2.5 中读取参数
在 order controller 的 notify 方法中,可以通过 application input 读取回调参数:
public function notify()
{
$app = JFactory::getApplication();
$input = $app->input;
$response = $input->getInt('Response', 0);
$status = $input->getInt('status', 0);
$code = $input->getString('code', '');
$token = $input->getString('token', '');
$lang = $input->getCmd('lang', '');
$ignore = $input->getInt('ignore', 0);
// 验证并更新订单
}
如果服务商可能通过 POST 提交,应根据其接口文档确定参数来源:
$response = $input->getInt('Response', 0);
上面的写法会从请求输入中取值。如果需要严格区分 GET 和 POST,可以使用:
$response = $input->get->getInt('Response', 0); // GET
$status = $input->post->getInt('status', 0); // POST
还要注意参数名的大小写。Response 和 response 不应被当成同一个字段。
更新数据库示例
不要将回调参数直接拼接到 SQL 中。下面以 code 定位订单为例:
public function notify()
{
$app = JFactory::getApplication();
$input = $app->input;
$response = $input->getInt('Response', 0);
$status = $input->getInt('status', 0);
$code = $input->getString('code', '');
$token = $input->getString('token', '');
if ($response !== 1 || $status !== 10 || $code === '' || $token === '') {
header('HTTP/1.1 400 Bad Request');
echo 'INVALID';
$app->close();
}
/*
* 此处必须按照支付服务商文档验证签名、token、金额、币种和商户号。
* 不能仅凭 status=10 就认定付款成功。
*/
$db = JFactory::getDbo();
$query = $db->getQuery(true)
->update($db->quoteName('#__orders'))
->set($db->quoteName('payment_status') . ' = ' . $db->quote('paid'))
->set($db->quoteName('gateway_token') . ' = ' . $db->quote($token))
->where($db->quoteName('transaction_code') . ' = ' . $db->quote($code));
try {
$db->setQuery($query);
$db->query();
echo 'OK';
} catch (Exception $e) {
JLog::add(
'Payment callback database error: ' . $e->getMessage(),
JLog::ERROR,
'payment'
);
header('HTTP/1.1 500 Internal Server Error');
echo 'ERROR';
}
$app->close();
}
表名和字段名要替换为实际结构。最好使用内部订单编号定位订单,并用支付流水号进行交叉核验和防重复处理。
排查时要确认什么
先记录经过脱敏的回调请求信息,确认请求是否真正到达 notify:
JLog::add(
'Payment callback: method=' . $_SERVER['REQUEST_METHOD']
. ' query=' . json_encode($app->input->get->getArray()),
JLog::INFO,
'payment'
);
不要将完整 token、签名、银行卡信息或其他敏感数据长期写入日志。
还要逐项检查:
- callback URL 必须是包含域名的绝对地址,通常应使用 HTTPS。
- SEF component 是否触发了
301或302跳转。部分支付服务商不会按预期跟随跳转。 - 重写规则是否保留了原查询字符串。
- controller 文件和
notify方法是否确实执行。 - 服务商使用 GET、POST,还是两者都会使用。
- 参数名称及大小写是否与服务商文档一致。
- callback 通常是服务器之间的请求,不能依赖登录状态、浏览器 session 或当前用户。
- 支付回调可能重复发送,因此更新逻辑必须具备幂等性。
- 必须验证服务商签名,并核对订单号、金额、币种和商户身份。
- 只有数据库事务成功后,才能返回服务商要求的成功响应,否则服务商可能停止重试。
问题的根源不在 MySQL,而在回调 URL 的拼接方式和路由。先确保请求带着格式正确的查询字符串到达 notify,再通过 Joomla input API 读取并验证参数,数据库更新才能可靠执行。
备注:内容仅供参考。
