PAYATHON 2026

如何为自定义产品集成优惠券、结账与支付功能?

支付阿杰

结论

不必从零开发支付系统,也不必完全放弃购物车扩展。更合适的方案是把成熟的电商扩展作为“交易后端”,使用它提供的订单、优惠券、结账和支付功能,产品选择页面则继续由自定义 Joomla 组件负责。

HikaShop、VirtueMart 等扩展都可以纳入评估。对于“自定义前端、标准化交易后端”这种模式,通常可以优先考虑 HikaShop。不过,产品选项、优惠券规则和支付渠道是否符合要求,还要看扩展版本、商业版授权和支付插件。实施前应在测试站逐项验证。

不建议新项目继续使用 Joomla 3.0。Joomla 3 系列已经停止维护,Joomla 3.0 失去安全支持的时间更早。新项目应选择仍受支持的 Joomla 版本,并确认电商扩展和支付插件与该版本兼容。

推荐架构

整个流程可以分为以下环节:

Joomla 登录用户
    ↓
自定义产品选择页面
    ↓
服务端计算产品、选项和价格
    ↓
电商扩展创建购物车或订单
    ↓
电商扩展校验优惠券
    ↓
标准结账页面
    ↓
支付插件
    ↓
支付回调更新订单状态

这里的“购物车”可以只作为后台数据结构存在,不一定要向用户展示传统的商品列表、数量调整和购物车页面。用户配置好产品后,可以直接进入结账流程。

各部分的职责可以这样划分:

  • Joomla 负责登录、用户身份和权限。
  • 自定义组件负责特殊的产品选择流程。
  • 电商扩展负责优惠券、税费、订单编号、支付状态和后台管理。
  • 支付插件负责签名、回调和支付结果验证。
  • 系统不需要自行保存银行卡等敏感支付信息。

扩展选择建议

HikaShop

如果需要保留完全定制的产品选择页面,同时复用优惠券、订单和支付流程,可以优先评估 HikaShop。测试时应重点确认:

  • 能否通过 PHP API、购物车接口或插件事件添加商品;
  • 是否支持产品自定义字段或订单自定义字段;
  • 优惠券能否按产品、分类、用户组、日期和最低金额设置限制;
  • 是否有兼容所需支付渠道的插件;
  • 能否隐藏标准商品页和购物车页;
  • 自定义数据能否显示在订单后台、邮件和发票中。

不同版本提供的接口可能不同,不能直接认定某个历史版本的示例代码也适用于当前版本。

VirtueMart

VirtueMart 也提供商品、优惠券、订单和支付插件体系,适合熟悉其数据模型或插件开发方式的团队。不过,它与模板、路由及产品模型的结合相对紧密。如果产品不是标准 SKU,而是根据大量参数动态生成,定制所需的工作可能更多。

自定义 Joomla 组件

如果现有扩展无法表达业务规则,可以开发 Joomla 组件来管理配置流程,但支付和优惠券仍应尽量交给成熟系统。只有当优惠券规则非常特殊时,才有必要自行实现完整的优惠券模块。

自定义产品页的实现方式

自定义页面不应接收浏览器提交的最终价格。浏览器只需提交产品 ID、选项 ID 和优惠券代码,服务端必须重新计算价格。

下面是一个简化的 Joomla 表单示例:

<form action="<?php echo JRoute::_('index.php?option=com_customorder'); ?>"
      method="post">

    <input type="hidden" name="task" value="checkout.create">

    <label for="product_id">产品</label>
    <select name="product_id" id="product_id" required>
        <option value="101">基础方案</option>
        <option value="102">专业方案</option>
    </select>

    <label for="service_level">服务等级</label>
    <select name="options[service_level]" id="service_level">
        <option value="standard">标准</option>
        <option value="priority">优先</option>
    </select>

    <label for="coupon_code">优惠券</label>
    <input type="text" name="coupon_code" id="coupon_code">

    <button type="submit">进入结账</button>

    <?php echo JHtml::_('form.token'); ?>
</form>

控制器需要检查登录状态、CSRF token 和所有选项:

defined('_JEXEC') or die;

class CustomOrderControllerCheckout extends JControllerLegacy
{
    public function create()
    {
        JSession::checkToken('post') or jexit('Invalid Token');

        $user = JFactory::getUser();

        if ($user->guest) {
            $this->setRedirect(
                JRoute::_('index.php?option=com_users&view=login', false),
                '请先登录'
            );
            return;
        }

        $input = JFactory::getApplication()->input;

        $productId = $input->post->getInt('product_id');
        $options = $input->post->get('options', array(), 'array');
        $couponCode = trim(
            $input->post->getString('coupon_code', '')
        );

        $model = $this->getModel('Checkout');

        try {
            $checkoutUrl = $model->createCheckout(
                $user->id,
                $productId,
                $options,
                $couponCode
            );

            $this->setRedirect($checkoutUrl);
        } catch (RuntimeException $e) {
            $this->setRedirect(
                JRoute::_('index.php?option=com_customorder&view=configure', false),
                $e->getMessage(),
                'error'
            );
        }
    }
}

这段代码只用于说明 Joomla 3 风格的入口结构,并未调用任何电商扩展的真实 API。createCheckout() 应调用所选扩展公开且受支持的接口,不要直接向扩展的数据库表写入订单。

服务端价格与优惠券校验

模型层的主要逻辑可以按以下方式组织:

public function createCheckout(
    $userId,
    $productId,
    array $options,
    $couponCode
) {
    $product = $this->productRepository->findPublished($productId);

    if (!$product) {
        throw new RuntimeException('产品不存在或已下架');
    }

    $validatedOptions = $this->optionValidator->validate(
        $product,
        $options
    );

    $subtotal = $this->priceCalculator->calculate(
        $product,
        $validatedOptions
    );

    $discount = $this->couponService->validateAndCalculate(
        $couponCode,
        $userId,
        $product,
        $subtotal
    );

    $orderDraft = array(
        'user_id'  => $userId,
        'product'  => $product,
        'options'  => $validatedOptions,
        'subtotal' => $subtotal,
        'discount' => $discount,
        'total'    => max(0, $subtotal - $discount),
    );

    return $this->commerceService->createCheckout($orderDraft);
}

这些服务名仅用于说明架构,并不是 Joomla 或某个扩展的内置类。实际开发时,需要将它们替换为所选电商系统提供的 API。

服务端至少要检查优惠券的以下条件:

  • 是否启用;
  • 生效时间和失效时间;
  • 总使用次数;
  • 单个用户的使用次数;
  • 适用的产品或分类;
  • 最低订单金额;
  • 采用固定金额还是百分比折扣;
  • 是否允许与其他优惠叠加;
  • 退款或支付失败后如何处理使用次数。

用户刚输入优惠券时,不要永久增加使用次数。比较稳妥的处理方式是,在创建订单时预留优惠券名额,支付成功后再确认使用。如果订单超时、取消或支付失败,则释放预留。最终规则需要与实际业务要求一致。

支付流程

建议使用托管结账页或支付服务商提供的官方 SDK:

  1. 服务端创建本地待支付订单。
  2. 使用本地订单号,请求支付服务商创建支付交易。
  3. 将用户重定向到支付页面。
  4. 支付服务商通过服务器回调通知支付结果。
  5. 服务端验证签名、金额、币种和交易状态。
  6. 在数据库事务中把订单更新为已支付。
  7. 随后发送邮件、生成授权或交付产品。

不能因为浏览器跳转到了“支付成功”页面,就把订单标记为已支付。该请求可以被用户伪造,浏览器跳转也可能因网络问题丢失。订单状态必须以经过验证的服务器回调或主动查询结果为准。

回调处理还要具备幂等性。同一笔支付通知即使多次到达,也只能触发一次交付。

不建议直接修改扩展数据库

直接向第三方扩展的数据表插入购物车或订单,虽然看起来省事,但风险很高。扩展升级后,字段、状态值和关联关系都可能发生变化。这种做法还可能绕过库存、税费、邮件和插件事件。

应按以下顺序选择集成方式:

  1. 扩展公开 API;
  2. 官方插件事件或适配器;
  3. 官方支持的覆盖机制;
  4. 数据库集成。

如果最终只能在数据库层面集成,最好增加独立的适配层,并明确记录所依赖的表结构和状态规则。

实施前需要确认的事项

可以先做一个最小原型,只验证一条完整链路:登录用户在自定义页面选择一个产品和相关选项,使用一张测试优惠券,再完成沙箱支付。

原型通过后,还需要确认:

  • 游客是否可以购买,还是必须登录;
  • 产品选项是否会影响价格、税率或库存;
  • 优惠券能否与用户组价格同时使用;
  • 采用哪种币种和舍入规则;
  • 如何处理支付失败、取消、超时和退款;
  • 订单中是否要保留配置快照;
  • 后台、邮件和发票能否完整显示产品选项;
  • 扩展升级后,自定义适配代码是否仍然可用。

选择扩展时,不能只看它是否具备“优惠券”和“支付”功能,还要检查它有没有稳定的扩展接口。对于高度定制的产品流程,接口质量通常比自带的商品页面更值得关注。

备注:内容仅供参考。