PAYATHON 2026

MultiMerch 8 使用第三方结账时不向卖家发送订单通知

支付阿杰

明确结论

问题不在 country() 或 customfield() 这些 Ajax 接口,也不必先修改你贴出的 checkout.php 片段。

客户和管理员仍能收到订单邮件,说明 Ajax Best Checkout 至少完成了 OpenCart 的订单创建或状态更新。只有卖家邮件丢失,最可能有以下几个原因:

  1. Ajax Best Checkout 使用了自己的“确认订单” controller,导致 multimerch_core.xml 中用于插入以下代码的 vQmod 搜索规则没有命中实际执行路径:

    $this->MsLoader->MsMail->sendOrderMails($order_id);
  2. 多个 OCMOD/vQmod 修改叠加后,原始目标代码发生了变化,导致 MultiMerch 的精确匹配失效。

  3. MultiMerch 把邮件模板改成了读取 $seller_text,但普通客户或管理员邮件没有传入这个变量,因此出现:

    Undefined index: seller_text
    

multimerch_core.xml 负责发送卖家订单邮件,是这次排查的重点。multimerch_core_transactions.xml 主要处理卖家订单数据、余额和事件记录,并不是直接发送订单邮件的入口。

应先确认最终运行的代码中是否存在 sendOrderMails($order_id),再把调用补到真正完成订单确认的位置。不要根据扩展文件名猜测执行路径,也不要把卖家邮件发送逻辑加到国家、地区或自定义字段等 Ajax 请求中。

先确认实际执行的订单流程

OpenCart 2.3 的结账扩展通常仍会调用以下模型方法之一:

$this->model_checkout_order->addOrder($order_data);
$this->model_checkout_order->addOrderHistory(
    $order_id,
    $order_status_id,
    $comment,
    $notify
);

支付扩展也可能在支付成功回调中执行:

$this->model_checkout_order->addOrderHistory($order_id, $order_status_id);

Ajax Best Checkout 的入口 controller 不一定是最终确认订单的文件。你提供的代码只有国家和自定义字段接口,不包含订单创建、订单状态更新或邮件发送逻辑,因此与这次故障没有直接关系。

在扩展目录和最终修改缓存中搜索以下关键字:

addOrder(
addOrderHistory(
order_id
order_status_id
checkout/success
sendOrderMails

重点检查这些位置:

catalog/controller/extension/module/xtensions/checkout/
catalog/controller/extension/payment/
system/storage/modification/catalog/model/checkout/order.php
vqmod/vqcache/

不同主机环境中的 storage 目录位置可能不同,应以 config.php 里的 DIR_STORAGE 为准。

检查 vQmod 是否插入了邮件调用

打开 multimerch_core.xml,找到包含以下代码的完整 <operation>:

$this->MsLoader->MsMail->sendOrderMails($order_id);

除了插入的内容,还要查看该 operation 中的:

<file name="...">
<search ...>
<add position="...">

需要确认以下几点:

  • <file> 指向哪个原始文件;
  • <search> 匹配的完整代码是什么;
  • 代码插在订单创建之后,还是订单状态更新之后。

随后检查最终生成的文件,例如:

system/storage/modification/catalog/model/checkout/order.php

以及可能存在的:

vqmod/vqcache/vq2-catalog_model_checkout_order.php
vqmod/vqcache/vq2-system_storage_modification_catalog_model_checkout_order.php

在最终执行文件中搜索:

sendOrderMails

根据搜索结果继续排查。

最终文件中没有 sendOrderMails

这基本可以确认 vQmod 没有匹配成功。常见原因是 Ajax Best Checkout 的 OCMOD 已经改写了目标代码,使 MultiMerch 的 <search> 无法精确匹配。

此时检查:

vqmod/logs/
system/storage/logs/

重点查找类似记录:

SEARCH NOT FOUND
ABORTING
multimerch_core.xml
catalog/model/checkout/order.php

修复时,应调整 multimerch_core.xml 中负责邮件调用的那个 operation,使其匹配当前服务器上的 OpenCart 最终代码。不要通过修改 multimerch_core_transactions.xml 来代替邮件调用。

例如,原规则可能依赖一整段已被其他扩展改写的代码:

<search><![CDATA[
$this->model_checkout_order->addOrderHistory($order_id, $order_status_id);
]]></search>

可以改为匹配目标方法中更稳定、只出现一次的语句。具体匹配内容必须以服务器上的目标文件为准,不能直接照搬示例。

匹配规则也不能过于宽泛。例如只搜索:

<search><![CDATA[
$order_id
]]></search>

可能会在多个位置插入邮件调用,导致卖家收到重复通知。

最终文件中已有 sendOrderMails

如果调用已经存在,可以在调用前后临时写入日志,确认代码是否执行,以及 $order_id 是否有效:

$this->log->write(
    'MultiMerch seller mail before: order_id=' . (int)$order_id
);

$this->MsLoader->MsMail->sendOrderMails($order_id);

$this->log->write(
    'MultiMerch seller mail after: order_id=' . (int)$order_id
);

如果日志中只有 before,没有 after,说明 sendOrderMails() 内部抛出了错误或提前终止。接着检查同一时间的 PHP、OpenCart 和邮件传输日志。

如果两条日志都有,但卖家仍未收到邮件,则继续确认:

  • ms_order_product_data 是否已经为该订单生成;
  • 订单商品能否关联到正确的 seller_id;
  • 卖家账号是否填写了有效邮箱;
  • MultiMerch 中的卖家订单通知是否启用;
  • 邮件是否进入垃圾箱,或被 SMTP 服务拒绝。

单独修复 seller_text 错误

错误出现在:

catalog/view/theme/journal2/template/mail/order.tpl

经过修改后,该模板直接读取了:

$seller_text

但客户和管理员的标准订单邮件不一定会传入这个变量,因此模板需要允许该字段不存在。

如果第 93 行类似:

<?php echo $seller_text; ?>

可以改成:

<?php if (!empty($seller_text)) { ?>
    <?php echo $seller_text; ?>
<?php } ?>

或者:

<?php echo isset($seller_text) ? $seller_text : ''; ?>

更合适的做法是在负责渲染邮件的 controller 或 MultiMerch 邮件类中提供默认值:

$data['seller_text'] = isset($data['seller_text'])
    ? $data['seller_text']
    : '';

这样模板始终可以取得该键。

需要注意的是,消除 Undefined index 只能解决模板兼容问题,不能证明卖家邮件发送函数已经执行。如果 sendOrderMails() 根本没有被调用,只修改模板不会恢复卖家通知。

还要查明是哪条 vQmod/OCMOD 修改把 seller_text 加进了 order.tpl。最好在对应的 XML 修改中加入条件判断,不要直接编辑:

vqmod/vqcache/
system/storage/modification/

这些目录存放的是生成文件,刷新修改缓存后,直接编辑的内容会被覆盖。

补偿调用应该放在哪里

如果确认 Ajax Best Checkout 完全绕过了 MultiMerch 原来的挂载点,可以把卖家邮件调用加到该扩展真正完成订单确认的位置。这个位置通常需要满足以下条件:

  • 已经取得有效的 $order_id;
  • 订单商品已经写入数据库;
  • createMsOrderDataEntries($order_id) 已经完成;
  • 订单状态已更新为有效状态;
  • 即将跳转到成功页;
  • 每个订单只执行一次。

调用形式可以是:

if (class_exists('MsHelper') && MsHelper::isInstalled()) {
    $this->MsLoader->MsMail->sendOrderMails($order_id);
}

如果当前 controller 尚未通过 MultiMerch 的核心修改获得 MsLoader,可以按照现有 MultiMerch 代码的初始化方式调用:

if (class_exists('MsHelper') && MsHelper::isInstalled()) {
    MsLoader::getInstance()
        ->setRegistry($this->registry)
        ->MsMail
        ->sendOrderMails($order_id);
}

具体使用哪种形式,取决于 MultiMerch 8 在该站点中的 loader 初始化方式。应优先复制同版本 multimerch_core.xml 中已经使用并验证过的调用形式,不要自行创建新的 MsLoader 实例。

不要放进通用 Ajax controller

以下接口在结账过程中可能被多次调用:

country()
customfield()
shipping()
payment()
validate()
save()

如果把邮件调用放进这些方法,可能产生以下问题:

  • 订单尚未创建,$order_id 不存在;
  • 页面每次刷新都会发送邮件;
  • 更换地址或配送方式时重复发送;
  • 支付失败时也会通知卖家。

因此,应修改 Ajax Best Checkout 的最终确认动作、支付成功回调或统一的订单状态更新模型,而不是你贴出的国家和自定义字段方法。

优先修复统一订单模型的挂载点

如果 Ajax Best Checkout 最终仍然调用:

catalog/model/checkout/order.php

最好修复 multimerch_core.xml 对该模型的匹配规则,而不是只修改 Ajax Best Checkout controller。这样,标准结账、Ajax 结账和其他支付方式都能使用同一个卖家通知入口。

调用顺序必须保证先创建 MultiMerch 订单商品数据,再发送卖家邮件。概念上的顺序如下:

$this->MsLoader->MsTransaction->createMsOrderDataEntries($order_id);

// OpenCart 保存订单状态、商品及其他订单信息

$this->MsLoader->MsMail->sendOrderMails($order_id);

如果先发送邮件,再创建 ms_order_product_data,MultiMerch 可能无法判断各件商品属于哪个卖家,最终找不到收件人或生成空白邮件内容。

multimerch_core_transactions.xml 虽然不直接发送邮件,但它能否正常执行会影响 sendOrderMails() 获取卖家订单数据。因此,还要检查对应的生成文件中是否存在:

$this->MsLoader->MsTransaction->createMsOrderDataEntries($order_id);

同时确认 ms_order_product_data 表中确实有新订单的记录。

防止重复发送

把调用补到 addOrderHistory() 一类方法时要格外谨慎,因为订单状态可能被多次更新。支付扩展、管理员操作和回调重试都可能再次执行该方法。

不能在每次状态变化时都无条件运行:

$this->MsLoader->MsMail->sendOrderMails($order_id);

至少应把条件限制为订单第一次从状态 0 进入有效状态。示意代码如下:

$order_info = $this->getOrder($order_id);
$old_order_status_id = $order_info
    ? (int)$order_info['order_status_id']
    : 0;

// 执行原有的订单状态更新逻辑

if (
    $old_order_status_id === 0 &&
    (int)$order_status_id > 0 &&
    class_exists('MsHelper') &&
    MsHelper::isInstalled()
) {
    $this->MsLoader->MsMail->sendOrderMails($order_id);
}

这只是调用条件的示例。变量的获取位置需要根据实际的 addOrderHistory() 代码调整,并确保读取的是更新前的状态。

如果 MultiMerch 本身已经实现发送记录或事件去重,应优先使用原有机制,避免另外建立一套不兼容的状态判断。

是否改用 OpenCart event

可以使用 event,但不建议把它作为首选方案。MultiMerch 8 已经通过 vQmod 集成订单流程,额外建立 event 后,如果原调用恢复,可能会导致重复发信。

只有同时满足以下条件,才适合改用 event:

  • 已确认 MultiMerch 的 vQmod 无法稳定匹配第三方结账代码;
  • 已确认当前安装的 OpenCart 2.3 支持对应的 model event route;
  • event 能取得 $order_id 和更新前后的订单状态;
  • 已移除或禁用原有的重复邮件挂载点;
  • 已加入每个订单只发送一次的保护。

不同的 OpenCart 2.3 修改版和扩展可能使用不同的 event route。在没有查看实际代码和已注册事件之前,不能断定应使用某个固定 route。

建议的处理顺序

  1. 在最终修改文件和 vQmod 缓存中搜索 sendOrderMails。
  2. 查看 multimerch_core.xml 中该调用对应的 <file> 和 <search>。
  3. 检查 vQmod 日志中是否出现 SEARCH NOT FOUND。
  4. 找到 Ajax Best Checkout 真正执行 addOrder() 或 addOrderHistory() 的确认代码。
  5. 确认 createMsOrderDataEntries($order_id) 已执行,并检查 ms_order_product_data 中的新订单数据。
  6. 修正 multimerch_core.xml 的匹配规则,优先恢复统一模型中的调用。
  7. 如果第三方结账完全绕过该模型,再到最终确认或支付成功的位置补充调用。
  8. 为模板中的 seller_text 添加默认值或存在性判断。
  9. 清理 vQmod 缓存,并刷新 OpenCart 修改缓存。
  10. 先用只包含一个卖家商品的测试订单验证,再测试包含多个卖家商品的订单。
  11. 确认每个卖家只收到一封邮件,同时客户和管理员邮件仍能正常发送。

修改前应备份相关 XML 和模板。不要直接修改 vqmod/vqcache 或 system/storage/modification 下的生成文件,因为这些修改会在刷新缓存后消失。

备注:内容仅供参考。