LikeShop 单商户SaaS商城分销模块二开:佣金计算与结算逻辑实现

一、前言

这一篇聚焦单商户SaaS商城的分销模块,重点讲佣金计算 和结算逻辑这两个二开时最容易出问题的环节。

而单商户SaaS版的佣金是平台级别的成本------平台从自己的收入中拿出一部分作为佣金奖励给分销员。这意味着SaaS版的分销佣金直接影响平台的利润率,佣金计算的准确性和结算的时机控制比多商户版更关键。

另一个区别是SaaS版支持多开(多租户) ,一套系统可以给多个客户开SAAS账号使用。分销配置需要按租户隔离,每个租户可以独立设置自己的分销层级、佣金比例和结算规则。

这篇文章就从佣金计算引擎、结算条件判断、多租户隔离和售后退款处理四个层面,把SaaS版分销模块的二开要点拆开讲清楚。

二、分销体系的核心设计

2.1 分销层级与佣金分配

LikeShop的分销支持一级分销、二级分销和三级分销。佣金根据商品所设置的分销比例核算。举一个具体的例子:如果A是B的上级,B是C的上级,C是D的上级,那么A购买商品时自己是没有分销佣金的(佣金由下一级产生),如果B购买商品,那么A是以一级分销比例获得佣金;如果C购买商品,那么B是以一级分销比例获得佣金,A是以二级分销比例获得佣金。

这里有一个容易被忽略的约束:A购买商品时自己是没有分销佣金的。佣金只由"下级购买"产生,上级才能拿到。这个规则在佣金计算逻辑中必须严格实现。

分销自购返佣是另一个可配置的选项。当〖分销用户〗自己购买也能获得佣金时,自购佣金比例才会生效;如果关闭,则自购佣金比例无效。

2.2 分销员模式的三种类型

LikeShop支持三种分销员开通模式:人人分销 (会员注册即成为分销会员)、申请分销 (需要申请并审核通过)、指定分销(通过后台开通指定会员分销权)。

指定分销状态是按会员等级指定的,需要在平台后台设置用户设置中自定义可参与分销的会员等级,未指定的分销等级的会员不允许参与分销。

2.3 分销功能开启的两级开关

分销功能需要在两个层面开启。平台后台的分销设置中要开启分销功能 ,否则分销推广按钮不会显示、也不会建立新的分销关系。同时,商品必须是分销商品且设置了所属等级的佣金比例,佣金比例不能设置为0,设置为0时分销订单不产生;佣金小于1分钱时,分销订单也不产生。

三、佣金计算引擎的实现

3.1 佣金计算的基本规则

佣金计算金额为商品被成功购买发生的净交易额,即:佣金计算金额 = 商品总金额 - 优惠券金额 - 用户折扣金额 -/+ 商品改价金额 - 运费金额。

这个公式有几个关键约束:

运费不计算佣金。 购买发生的交易金额中非商品本身金额的部分不计佣,如快递费。

使用优惠券/积分等非现金支付的订单,非现金支付的部分不计算佣金。这意味着如果用户用积分抵扣了50元,佣金只能按实际现金支付的部分计算。

营销订单(秒杀、拼团、砍价)不计算佣金,商品实际佣金小于0.01的订单也不计算。

3.2 佣金计算的代码实现

以下是一个佣金计算引擎的代码示例,封装了上述所有规则:

php 复制代码
// server/app/common/logic/DistributionLogic.php
class DistributionLogic
{
    /**
     * 计算订单的分销佣金
     * @param int $orderId 订单ID
     * @return array 各分销层级的佣金明细
     */
    public static function calculateCommission(int $orderId): array
    {
        // 1. 查询订单及商品
        $order = OrderModel::find($orderId);
        if (!$order) {
            return [];
        }

        // 2. 检查订单类型:营销订单不计算佣金
        if (in_array($order['order_type'], [1, 2, 3])) {
            // 1-秒杀;2-拼团;3-砍价
            return [];
        }

        // 3. 检查分销功能是否开启
        $distribEnabled = ConfigModel::where('key', 'distribution_enabled')
            ->where('tenant_id', $order['tenant_id'])
            ->value('value');
        if (!$distribEnabled) {
            return [];
        }

        // 4. 查询订单商品,计算可计佣金额
        $orderGoodsList = OrderGoodsModel::where('order_id', $orderId)->select();
        $commissionBase = 0;

        foreach ($orderGoodsList as $goods) {
            // 检查商品是否为分销商品
            $isDistribGoods = GoodsModel::where('id', $goods['goods_id'])
                ->where('is_distribution', 1)
                ->find();
            if (!$isDistribGoods) {
                continue;
            }

            // 计算该商品的净交易额
            // 净交易额 = 商品总价 - 优惠券分摊 - 积分抵扣 - 运费分摊
            $goodsNetAmount = self::calculateGoodsNetAmount($goods, $order);
            $commissionBase += $goodsNetAmount;
        }

        if ($commissionBase <= 0) {
            return [];
        }

        // 5. 查找分销关系链
        $buyerId = $order['user_id'];
        $relationChain = self::getDistributionChain($buyerId, $order['tenant_id']);
        if (empty($relationChain)) {
            return [];
        }

        // 6. 按层级计算佣金
        $commissions = [];
        $levels = [1 => '一级', 2 => '二级', 3 => '三级'];

        foreach ($levels as $level => $levelName) {
            if (!isset($relationChain[$level - 1])) {
                break; // 没有该层级的上级
            }

            $distributor = $relationChain[$level - 1];

            // 检查分销商状态是否正常
            if ($distributor['status'] != 1) {
                continue;
            }

            // 获取该层级的佣金比例
            $rate = self::getCommissionRate(
                $distributor['level'],
                $level,
                $order['tenant_id']
            );

            if ($rate <= 0) {
                continue;
            }

            $commissionAmount = round($commissionBase * $rate / 100, 2);

            // 佣金小于0.01不产生
            if ($commissionAmount < 0.01) {
                continue;
            }

            $commissions[] = [
                'distributor_id' => $distributor['user_id'],
                'level'          => $level,
                'level_name'     => $levelName,
                'rate'           => $rate,
                'amount'         => $commissionAmount,
                'base_amount'    => $commissionBase,
            ];
        }

        return $commissions;
    }

    /**
     * 计算商品的净交易额
     */
    private static function calculateGoodsNetAmount(array $goods, array $order): float
    {
        $goodsTotal = $goods['total_price'];

        // 扣除优惠券分摊
        $couponDeduct = self::allocateCoupon($goods, $order);
        // 扣除积分抵扣分摊
        $integralDeduct = self::allocateIntegral($goods, $order);
        // 扣除运费分摊(运费不计佣)
        $freightDeduct = self::allocateFreight($goods, $order);

        $netAmount = $goodsTotal - $couponDeduct - $integralDeduct - $freightDeduct;

        return max($netAmount, 0);
    }

    /**
     * 获取分销关系链
     * @return array 按层级排列的分销商列表
     */
    private static function getDistributionChain(int $buyerId, int $tenantId): array
    {
        $chain = [];
        $currentId = $buyerId;
        $maxLevel = ConfigModel::where('key', 'distribution_level')
            ->where('tenant_id', $tenantId)
            ->value('value') ?? 2;

        for ($i = 0; $i < $maxLevel; $i++) {
            $user = UserModel::where('id', $currentId)
                ->where('tenant_id', $tenantId)
                ->find();
            if (!$user || !$user['invite_user_id']) {
                break;
            }

            $parent = UserModel::where('id', $user['invite_user_id'])
                ->where('tenant_id', $tenantId)
                ->find();
            if (!$parent) {
                break;
            }

            // 检查上级的分销状态
            $distributor = DistributionUserModel::where('user_id', $parent['id'])
                ->where('tenant_id', $tenantId)
                ->find();
            if (!$distributor || $distributor['status'] != 1) {
                break;
            }

            $chain[] = [
                'user_id' => $parent['id'],
                'level'   => $distributor['level'],
                'status'  => $distributor['status'],
            ];

            $currentId = $parent['id'];
        }

        return $chain;
    }

    /**
     * 获取指定层级的佣金比例
     */
    private static function getCommissionRate(int $distributorLevel, int $level, int $tenantId): float
    {
        $rateConfig = DistributionLevelModel::where('level', $distributorLevel)
            ->where('tenant_id', $tenantId)
            ->find();

        if (!$rateConfig) {
            return 0;
        }

        $rateField = 'level_' . $level . '_rate';
        return (float)($rateConfig[$rateField] ?? 0);
    }
}

3.3 佣金记录的数据结构

佣金记录用于查看每笔订单的分销佣金明细,佣金状态分为待结算、已返佣、已失效三种。订单结算后才会进行返佣,结算前如果有售后退款,则佣金会失效。

佣金记录的核心字段包括:订单编号、实际付款金额、规格ID、商品数量、创建时间、佣金金额、佣金状态、佣金状态描述。

四、结算逻辑的实现

4.1 结算的三个必要条件

佣金结算需要同时满足三个条件:商品已确认收货 + 已过商品售后期 + 已过结算时间,并且还需要定时任务执行正常,如未配置定时任务需要参考通用部署文档进行配置。

这三个条件的含义是:

商品已确认收货------订单状态必须已经完成,用户没有申请退货。

已过商品售后期------售后期在平台后台的交易设置中配置,对账结算只能结算已过售后期的订单,还在售后期内的订单暂时不能结算。

已过结算时间------结算时间在平台后台的分销结算设置中配置,设置了订单完成后多少天可以尝试结算。

4.2 结算时机与售后期的独立配置

这里有一个二开时极其容易踩坑的设计:商城的佣金"结算时机"与"订单售后退款时长"是独立设置的,两边的设置时间可以不一样。

官方文档举了一个例子:后台分销的"结算时机"设置为5分钟,交易设置的"订单售后退款时长"设置为7天。小C购买分销商品,收到货并点击确认收货的5分钟后,当定时任务执行完的时候,该佣金就已经发放出去了,然后在这7天里小C依旧可以对此商品发起售后申请,因为他的订单还处于订单售后退款时长内。

设置不合理的话,很容易出现"既能结算佣金给到该用户上级,事后该用户又能对这笔订单发起售后流程"的情况 。二开时建议在结算逻辑中增加一个校验:结算时间不能短于售后期,或者在结算前强制检查订单是否仍在售后期内。

4.3 结算的代码实现

php 复制代码
// server/app/common/logic/DistributionSettleLogic.php
class DistributionSettleLogic
{
    /**
     * 定时任务:扫描并结算到期的分销佣金
     */
    public static function settleDueCommissions(): array
    {
        $now = time();
        $result = ['settled' => 0, 'skipped' => 0, 'errors' => 0];

        // 1. 查询所有待结算的佣金记录
        $pendingRecords = DistributionCommissionModel::where('status', 0) // 0-待结算
            ->where('create_time', '<', $now - 60) // 至少1分钟前的记录
            ->limit(200)
            ->select();

        foreach ($pendingRecords as $record) {
            try {
                // 2. 查询订单
                $order = OrderModel::find($record['order_id']);
                if (!$order) {
                    self::invalidateCommission($record['id'], '订单不存在');
                    $result['skipped']++;
                    continue;
                }

                // 3. 检查订单状态:必须已完成或待收货
                if (!in_array($order['order_status'], [2, 3])) {
                    $result['skipped']++;
                    continue;
                }

                // 4. 检查是否在售后期内
                $afterSaleDays = ConfigModel::where('key', 'after_sale_days')
                    ->where('tenant_id', $record['tenant_id'])
                    ->value('value') ?? 7;
                $afterSaleDeadline = $order['finish_time'] + $afterSaleDays * 86400;
                if ($now < $afterSaleDeadline) {
                    $result['skipped']++;
                    continue; // 还在售后期内,跳过
                }

                // 5. 检查是否在售后中
                $inAfterSale = AfterSaleModel::where('order_id', $order['id'])
                    ->whereIn('status', [1, 2]) // 1-售后中;2-售后成功
                    ->find();
                if ($inAfterSale) {
                    // 有售后记录,佣金失效
                    self::invalidateCommission($record['id'], '订单存在售后记录');
                    $result['skipped']++;
                    continue;
                }

                // 6. 检查结算时机
                $settleDelay = ConfigModel::where('key', 'distribution_settle_delay')
                    ->where('tenant_id', $record['tenant_id'])
                    ->value('value') ?? 0;
                $settleTime = $order['finish_time'] + $settleDelay * 86400;
                if ($now < $settleTime) {
                    $result['skipped']++;
                    continue; // 未到结算时机
                }

                // 7. 执行结算
                self::settleCommission($record);
                $result['settled']++;

            } catch (Exception $e) {
                Log::error("佣金结算失败:{$record['id']},{$e->getMessage()}");
                $result['errors']++;
            }
        }

        return $result;
    }

    /**
     * 执行佣金结算
     */
    private static function settleCommission(array $record): void
    {
        Db::startTrans();
        try {
            // 更新佣金状态为已返佣
            DistributionCommissionModel::where('id', $record['id'])
                ->where('status', 0)
                ->update([
                    'status'      => 1, // 1-已返佣
                    'settle_time' => time(),
                    'update_time' => time(),
                ]);

            // 增加分销商的佣金收益
            UserModel::where('id', $record['distributor_id'])
                ->inc('earnings', $record['amount'])
                ->update(['update_time' => time()]);

            // 记录账户流水
            AccountLogModel::create([
                'user_id'       => $record['distributor_id'],
                'source_type'   => 3, // 分销佣金
                'source_id'     => $record['id'],
                'source_sn'     => $record['order_sn'],
                'change_amount' => $record['amount'],
                'change_type'   => 1, // 增加
                'remark'        => '分销佣金结算',
                'tenant_id'     => $record['tenant_id'],
                'create_time'   => time(),
            ]);

            Db::commit();
        } catch (Exception $e) {
            Db::rollback();
            throw $e;
        }
    }

    /**
     * 佣金失效
     */
    private static function invalidateCommission(int $commissionId, string $reason): void
    {
        DistributionCommissionModel::where('id', $commissionId)
            ->where('status', 0)
            ->update([
                'status'      => 2, // 2-已失效
                'invalid_reason' => $reason,
                'update_time' => time(),
            ]);
    }
}

4.4 售后对佣金的影响

售后处理对佣金的影响是分销模块二开中最复杂的环节。根据官方的售后处理规则:

售后成功的订单,对应的佣金将失效 。如果一个订单有多个商品,除售后的商品外,其他商品依照佣金规则正常进行结算。售后不成功的订单,佣金正常结算给用户。

如果消费者在分销订单结算时间后发起售后申请的,结算的分销佣金平台不予追回,请谨慎设置分销订单结算时间。

这意味着二开时需要在售后退款成功回调中增加佣金失效处理逻辑:

php 复制代码
// server/app/common/logic/AfterSaleRefundLogic.php 中的退款成功处理
class AfterSaleRefundLogic
{
    public static function refundSuccess(int $afterSaleId): void
    {
        // ... 原有的退款逻辑

        // 新增:佣金失效处理
        $afterSale = AfterSaleModel::find($afterSaleId);
        $orderGoods = OrderGoodsModel::find($afterSale['order_goods_id']);

        // 查询该订单商品对应的待结算佣金
        $pendingCommissions = DistributionCommissionModel::where('order_id', $afterSale['order_id'])
            ->where('order_goods_id', $afterSale['order_goods_id'])
            ->where('status', 0) // 待结算
            ->select();

        foreach ($pendingCommissions as $commission) {
            DistributionSettleLogic::invalidateCommission(
                $commission['id'],
                '售后退款成功,佣金失效'
            );
        }
    }
}

五、SaaS多租户的隔离设计

5.1 租户隔离的核心字段

单商户SaaS版支持多开,一套系统给多个客户开SAAS账号使用。分销配置需要按租户隔离,所有涉及分销的数据表和配置表都需要增加 tenant_id 字段。

需要按租户隔离的数据包括:

数据 隔离方式
分销配置(层级、自购返佣等) ConfigModel 的 tenant_id
分销等级佣金比例 DistributionLevelModel 的 tenant_id
分销关系链 UserModel 的 invite_user_id + tenant_id
佣金记录 DistributionCommissionModel 的 tenant_id
结算时间配置 ConfigModel 的 tenant_id

5.2 租户隔离的代码约束

在佣金计算和结算逻辑中,所有查询必须带上 tenant_id 过滤条件 。这是SaaS版二开最核心的安全约束------如果漏了 tenant_id,A租户的佣金数据会被B租户看到,这是最严重的业务风险。

php 复制代码
// ❌ 错误:没有租户隔离
$rate = DistributionLevelModel::where('level', $level)->value('rate');

// ✅ 正确:带上租户隔离
$rate = DistributionLevelModel::where('level', $level)
    ->where('tenant_id', $tenantId)
    ->value('rate');

六、二开实战:几个常见的改造场景

场景一:新增"团队分红"佣金类型

需求:除了常规的一级、二级分销佣金外,增加"团队分红"------当分销员的直推团队达到一定人数时,额外获得团队订单的分红。

改造步骤:

第一步 :在分销等级表中新增 team_bonus_rate 字段,存储团队分红比例。

第二步:在佣金计算引擎中增加团队分红的计算分支:

php 复制代码
// 在 calculateCommission 中增加
if ($distributor['team_size'] >= $teamThreshold) {
    $teamBonus = round($commissionBase * $teamBonusRate / 100, 2);
    if ($teamBonus >= 0.01) {
        $commissions[] = [
            'distributor_id' => $distributor['user_id'],
            'level'          => 0, // 0表示团队分红
            'level_name'     => '团队分红',
            'rate'           => $teamBonusRate,
            'amount'         => $teamBonus,
        ];
    }
}

第三步 :在佣金记录表中增加 commission_type 字段,区分常规佣金和团队分红。

场景二:修改结算条件为"按周期结算"

需求:当前结算条件是"订单完成后X天",业务要求改为"按自然月结算"------每月1号结算上月所有已过售后期的订单佣金。

改造步骤:

第一步:在结算设置中增加"结算方式"配置项,支持"按订单完成时间"和"按自然月"两种方式。

第二步:在定时任务中增加按自然月结算的判断逻辑:

php 复制代码
// 按自然月结算
$lastMonthStart = strtotime(date('Y-m-01', strtotime('-1 month')));
$lastMonthEnd = strtotime(date('Y-m-01')) - 1;

$pendingRecords = DistributionCommissionModel::where('status', 0)
    ->where('create_time', '>=', $lastMonthStart)
    ->where('create_time', '<=', $lastMonthEnd)
    ->select();

场景三:增加佣金提现手续费的自定义配置

LikeShop支持5种佣金支付方式:钱包余额、微信零钱、银行卡、微信收款码、支付宝收款码。佣金提现至钱包余额时不需要扣除手续费,其他提现方式均需要手续费。

如果业务要求"提现到微信零钱的手续费按提现金额的2%收取",需要在提现逻辑中增加手续费计算:

php 复制代码
// 提现手续费计算
$withdrawAmount = $params['amount'];
$feeRate = 0;
if ($params['type'] == 2) { // 微信零钱
    $feeRate = ConfigModel::where('key', 'withdraw_fee_wechat')->value('value') ?? 0;
}
$fee = round($withdrawAmount * $feeRate / 100, 2);
$actualAmount = $withdrawAmount - $fee;

七、二开避坑清单

坑一:结算时间短于售后期。 官方明确提醒,结算时机和售后时长独立设置,设置不合理会导致"佣金已发放但用户仍可发起售后"。建议在配置层面增加校验:结算时间不能短于售后期。

坑二:佣金计算没有扣除运费和优惠券分摊。 运费不计佣,优惠券和积分抵扣的非现金部分也不计佣。佣金计算引擎中必须正确处理这些分摊逻辑。

坑三:SaaS版查询没有加 tenant_id。 这是SaaS版最严重的业务风险。所有分销相关的查询必须带上 tenant_id 过滤条件。

坑四:售后退款没有触发佣金失效。 售后成功的订单,对应的待结算佣金必须失效。需要在退款成功回调中增加佣金失效处理逻辑。

坑五:分销关系链查询没有检查上级状态。 如果上级的分销资格被冻结或过期,佣金不应该继续计算。getDistributionChain 方法中必须检查每一层上级的 status 字段。

坑六:营销订单错误地计算了佣金。 秒杀、拼团、砍价订单不计算佣金,佣金计算引擎中必须在入口处过滤掉这些订单类型。

八、总结

LikeShop单商户SaaS版分销模块二开的核心可以概括为三条线:

佣金计算引擎:核心公式为"商品总金额 - 优惠券 - 积分抵扣 - 运费",扣除所有非商品金额部分。营销订单不计算佣金,佣金比例不能为0,佣金小于0.01不产生。

结算逻辑:结算需要同时满足"已确认收货 + 已过售后期 + 已过结算时间"三个条件,通过定时任务扫描执行。结算时机和售后时长独立配置,设置不当会导致佣金已发但售后仍可发起。

多租户隔离 :SaaS版所有分销数据必须按 tenant_id 隔离,佣金计算和结算逻辑中的每一处查询都要带上租户过滤条件。

二开时守住三条底线:佣金计算扣除所有非商品金额、结算前强制校验售后状态、所有查询加 tenant_id 过滤。把这三点处理好,SaaS版的分销模块就能稳定支撑多租户运营。

相关推荐
wuminyu4 小时前
C++协程实现接收端的零拷贝Buffer管理原理剖析
java·linux·c语言·jvm·c++
m0_380743875 小时前
C++中的freopen的用法实例详解
开发语言·c++
Escalating_xu5 小时前
【Python】基础语法(1):常量、变量、类型、输入输出与运算符
开发语言·python
jimy15 小时前
polymorph.cpp里面的“析构函数、智能指针”问题
开发语言·c++
m0_380743875 小时前
PHP 8.3 缓存清除不生效怎么排查
开发语言·php
朝朝辞暮i6 小时前
C++ 第 6 课:switch —— 多状态选择
开发语言·c++·算法
小凡geo6 小时前
本地商家 GEO:用脚本一键生成 FAQPage 结构化数据,让 AI 切片更稳
开发语言·人工智能·python·microsoft·搜索引擎·ai
朝朝辞暮i6 小时前
C++ 第 4 课:if / else —— 让程序自己做决定
开发语言·c++
波力海苔夹心脆6757 小时前
C# 值类型与引用类型详解:存储位置、赋值机制、参数传递、相等比较、装箱拆箱与常见陷阱
开发语言·jvm·经验分享·笔记·c#·.net