PAYATHON 2026

如何在 Joomla 2.5 的 VirtueMart 中实现 Affiliate program

支付阿杰

结论

Joomla 2.5 和同期的 VirtueMart 都没有提供完整的原生 Affiliate program。仅靠修改付款插件,也无法安全实现 split payments。

可以考虑以下方案:

  1. 使用兼容 Joomla 2.5 和 VirtueMart 2.x 的联盟营销扩展,记录推荐关系与佣金。
  2. 监听 VirtueMart 的订单事件,在付款成功、退款或取消时更新佣金。
  3. 接入明确支持 marketplace、connected accounts 或分账功能的支付服务商。
  4. 如果支付服务商没有原生分账功能,则由商户统一收款,再按结算周期向 Affiliate 付款。不要尝试在普通支付交易中自行拆分资金。

Joomla 2.5 和 VirtueMart 2.x 已经非常陈旧,通常无法兼容支付服务商的新版 SDK、TLS 要求和现有扩展。正式实施前,应优先评估升级 Joomla 和 VirtueMart。

推荐的系统结构

联盟营销与资金分账应拆成两个独立模块:

Affiliate 链接
    ↓
记录 affiliate_id
    ↓
用户下单
    ↓
订单保存 affiliate_id
    ↓
支付成功
    ├── 创建或确认佣金记录
    └── 由支付平台执行分账,或进入周期性人工结算

系统至少要保存以下数据:

  • Affiliate 账户及其支付平台账户标识
  • 点击来源和推荐 Cookie
  • VirtueMart 订单 ID
  • 订单金额、可计算佣金的净额和币种
  • 佣金比例或固定佣金
  • 佣金状态,如 pending、approved、paid、reversed
  • 支付交易 ID、分账 ID 和退款状态

浏览器 Cookie 不能作为唯一依据。创建订单时,要把 Affiliate 标识写入数据库,并与订单 ID 绑定。否则用户清除 Cookie 后,系统将无法可靠处理退款、拒付和结算。

实现步骤

1. 确认实际版本

仅知道使用的是“Joomla 2.5 的 VirtueMart”,还无法判断接口是否兼容,需要确认:

  • Joomla 的完整版本号
  • VirtueMart 的完整版本号
  • PHP 版本
  • 当前支付插件
  • 商户及 Affiliate 所在国家或地区
  • 分账是实时进行,还是在订单完成后周期结算

VirtueMart 各小版本的插件事件和订单状态处理方式可能不同,示例中的事件名称不一定适用于所有版本。

2. 记录 Affiliate 来源

Affiliate 链接可以使用类似格式:

https://example.com/index.php?option=com_virtuemart&aff=12345

检测到合法的 aff 参数后,将其写入 Cookie。以下代码只演示基本思路,实际项目还要使用 Joomla 2.5 对应的输入过滤和 Cookie API:

<?php

$affiliateId = isset($_GET['aff']) ? (int) $_GET['aff'] : 0;

if ($affiliateId > 0 && affiliateExists($affiliateId)) {
    setcookie(
        'affiliate_id',
        (string) $affiliateId,
        time() + 30 * 24 * 60 * 60,
        '/',
        '',
        !empty($_SERVER['HTTPS']),
        true
    );
}

必须通过数据库检查 Affiliate 是否存在并处于启用状态,不能直接信任 URL 参数。

订单创建后,需要将推荐关系写入数据库:

<?php

function attachAffiliateToOrder($orderId)
{
    $affiliateId = isset($_COOKIE['affiliate_id'])
        ? (int) $_COOKIE['affiliate_id']
        : 0;

    if ($affiliateId <= 0 || !affiliateExists($affiliateId)) {
        return;
    }

    saveOrderAffiliate($orderId, $affiliateId);
}

affiliateExists() 和 saveOrderAffiliate() 只是业务层示意函数,不属于 VirtueMart 官方 API,需要根据实际数据表和插件结构自行实现。

3. 在正确的订单状态计算佣金

用户点击“确认订单”时,不应立即将佣金设为可支付状态。比较安全的处理流程是:

  • 订单创建:佣金记为 pending
  • 支付成功:确认交易已经入账
  • 订单完成且已满足退款期要求:佣金改为 approved
  • 已向 Affiliate 付款:改为 paid
  • 订单取消、退款或拒付:改为 reversed

佣金可以按以下方式计算:

<?php

function calculateCommission($eligibleAmount, $rate)
{
    if ($eligibleAmount < 0 || $rate < 0 || $rate > 1) {
        throw new InvalidArgumentException('Invalid commission data');
    }

    return round($eligibleAmount * $rate, 2);
}

佣金比例为 10% 时,$rate 应传入 0.10。还要事先确定佣金基数是否包含:

  • 税费
  • 运费
  • 优惠金额
  • 礼品卡
  • 已退款商品
  • 支付手续费

金额计算最好使用支付服务商要求的最小货币单位,例如“分”,避免依赖浮点数:

<?php

$orderAmountMinor = 10000;       // 100.00
$commissionRate   = 1000;        // 10.00%,以万分比表示

$commissionMinor = intdiv(
    $orderAmountMinor * $commissionRate,
    10000
);

4. 选择真正支持分账的支付服务

需要选择具备以下功能的支付产品,而不是普通 payment gateway:

  • Affiliate 或卖家身份认证
  • Connected account 或子商户账户
  • 一笔交易关联多个收款方
  • 平台佣金
  • 延迟结算
  • 部分退款和全额退款
  • 拒付后的资金追偿
  • Webhook 签名验证
  • 幂等请求
  • 对账和税务报表

能否向 Affiliate 实时分账,取决于支付服务商、签约主体、经营国家和当地监管要求。不能只根据产品名称判断某个接口是否可用,应以商户账户当前开通的官方功能为准。

支付请求的业务结构通常如下:

<?php

$paymentRequest = array(
    'amount'         => 10000,
    'currency'       => 'CNY',
    'order_id'       => 'VM-10001',
    'affiliate_id'   => 12345,
    'affiliate_share'=> 1000,
    'platform_share' => 9000,
    'idempotency_key'=> 'payment-VM-10001'
);

这些字段只是与平台无关的业务模型,并非某个网关的真实 API 参数。实际字段必须按照所选支付服务商的官方文档填写。

5. 编写独立的 VirtueMart 支付插件

支付逻辑应封装为 VirtueMart payment plugin,不要直接修改 VirtueMart 核心文件。插件通常需要完成以下工作:

  1. 创建支付请求。
  2. 将订单号映射到支付平台交易号。
  3. 接收浏览器返回结果。
  4. 接收并验证服务端 Webhook。
  5. 更新 VirtueMart 订单状态。
  6. 创建分账或佣金记录。
  7. 处理退款和分账回退。

不能只根据浏览器跳转结果判断付款是否成功。订单状态应以通过签名验证的服务端通知或主动查询结果为准:

<?php

function handleWebhook($rawBody, $signature)
{
    if (!verifyGatewaySignature($rawBody, $signature)) {
        http_response_code(400);
        return;
    }

    $event = json_decode($rawBody, true);

    if (!is_array($event) || empty($event['event_id'])) {
        http_response_code(400);
        return;
    }

    if (eventAlreadyProcessed($event['event_id'])) {
        http_response_code(200);
        return;
    }

    if ($event['type'] === 'payment.succeeded') {
        markOrderAsPaid($event['order_id'], $event['transaction_id']);
        approvePendingCommission($event['order_id']);
    }

    if ($event['type'] === 'payment.refunded') {
        reverseCommission($event['order_id'], $event['refund_amount']);
    }

    markEventAsProcessed($event['event_id']);
    http_response_code(200);
}

示例中的函数和事件名称仍然只是架构示意,不代表 VirtueMart 或任何支付网关的真实接口。

更稳妥的替代方案

如果 Affiliate 只是推广者,并非商品的实际销售方,通常无需在顾客付款时实时拆分资金。更容易维护的流程是:

  1. 顾客将全款支付给商户。
  2. 系统记录 Affiliate 的待确认佣金。
  3. 退款期结束后批准佣金。
  4. 每周或每月生成结算单。
  5. 通过银行转账或支付服务商的批量付款功能支付佣金。

这种方式对旧版 VirtueMart 的改动较少,也便于处理退款、拒付、最低提现金额和税务资料。至于是否符合相关法律和支付服务商条款,还需要根据经营地区确认。

注意事项

  • 不要修改 VirtueMart 核心代码,否则会增加后续修复和迁移的难度。
  • 不要在站点中保存银行卡号等敏感支付数据。
  • API 密钥必须保存在服务器端,不能写入 JavaScript、URL 或日志。
  • Webhook 必须验证签名、金额、币种、订单号和商户账户。
  • 所有支付和分账操作都要具备幂等性,防止重复通知引发重复付款。
  • 不能只通过 URL 参数为 Affiliate 指定收款账户,否则可能造成佣金劫持。
  • 退款时要按原佣金比例冲销,并处理佣金已经支付但无法追回的情况。
  • Cookie 使用、推广跟踪、税务申报和向第三方付款,可能涉及隐私与金融监管要求。
  • Joomla 2.5 已不适合作为新支付系统的长期基础。即使旧站目前仍能运行,也建议先在隔离测试环境中完成升级和数据迁移,再接入现代支付服务。

备注:内容仅供参考。