PAYATHON 2026

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 读取并验证参数,数据库更新才能可靠执行。

备注:内容仅供参考。