0
0
0

Thinkphp+Uniapp开发的多端商城系统源码H5小程序APP支持DIY模板直播分销

职场区 3253582893 1月前
793

源码获取:ym7k.com/11647/

ThinkPHP + UniApp 多端商城系统:1-10 级分销代理的工程架构与风险边界

引子:1-10 级分销是一个工程问题,也是一个合规问题

“支持分销代理,设置分销级别 1-10 级”——这句话在商城系统的产品描述中很常见,但它的工程含义远比字面复杂。

从技术角度看,10 级分销意味着系统需要维护一条最长 10 层的分销关系链,并在订单完成时向上追溯 10 层计算佣金。这涉及关系链的存储效率、佣金计算的幂等性、层级比例的配置管理,以及高并发下的性能问题。

从合规角度看,10 级分销在某些司法管辖区可能触及传销的法律边界。《禁止传销条例》对“团队计酬”有明确限制,分销层级的设置需要与合规团队确认。工程上的灵活性不等于法律上的合法性。

本文从多端商城架构、分销关系的存储模型、1-10 级佣金的计算引擎、层级比例的配置系统,到分销合规的工程边界,展开这类系统的完整技术分析。

一、多端架构:H5、小程序、APP 的统一与分化

1.1 三端的支付差异

多端商城的核心差异在支付环节。

php
<?php
// app/common/service/PaymentService.php

namespace app\common\service;

class PaymentService
{
    /**
     * 创建支付订单(分端适配)
     */
    public function create(array $order, string $terminal, string $openid = ''): array
    {
        switch ($terminal) {
            case 'mp':
                return $this->createMiniPay($order, $openid);
            case 'h5':
                return $this->createH5Pay($order);
            case 'app':
                return $this->createAppPay($order);
            default:
                throw new \Exception('不支持的终端');
        }
    }

    protected function createMiniPay(array $order, string $openid): array
    {
        // 小程序 JSAPI 支付
        $params = [
            'appid' => config('wxpay.mini_appid'),
            'mch_id' => config('wxpay.mch_id'),
            'out_trade_no' => $order['order_no'],
            'total_fee' => intval($order['pay_amount'] * 100),
            'body' => $order['subject'],
            'openid' => $openid,
            'notify_url' => config('wxpay.notify_url'),
            'trade_type' => 'JSAPI',
        ];
        $result = $this->wxUnifiedOrder($params);

        return [
            'timeStamp' => (string)time(),
            'nonceStr' => $result['nonce_str'],
            'package' => 'prepay_id=' . $result['prepay_id'],
            'signType' => 'MD5',
            'paySign' => $this->wxSign($result, config('wxpay.key')),
        ];
    }

    protected function createH5Pay(array $order): array
    {
        // H5 支付
        $params = [
            'appid' => config('wxpay.app_appid'),
            'mch_id' => config('wxpay.mch_id'),
            'out_trade_no' => $order['order_no'],
            'total_fee' => intval($order['pay_amount'] * 100),
            'body' => $order['subject'],
            'notify_url' => config('wxpay.notify_url'),
            'trade_type' => 'MWEB',
            'scene_info' => json_encode(['h5_info' => ['type' => 'Wap']]),
        ];
        $result = $this->wxUnifiedOrder($params);

        return ['mweb_url' => $result['mweb_url']];
    }

    protected function createAppPay(array $order): array
    {
        // APP 支付
        $params = [
            'appid' => config('wxpay.app_appid'),
            'mch_id' => config('wxpay.mch_id'),
            'out_trade_no' => $order['order_no'],
            'total_fee' => intval($order['pay_amount'] * 100),
            'body' => $order['subject'],
            'notify_url' => config('wxpay.notify_url'),
            'trade_type' => 'APP',
        ];
        $result = $this->wxUnifiedOrder($params);

        return [
            'appid' => $result['appid'],
            'partnerid' => $result['mch_id'],
            'prepayid' => $result['prepay_id'],
            'package' => 'Sign=WXPay',
            'noncestr' => $result['nonce_str'],
            'timestamp' => (string)time(),
            'sign' => $this->wxSign($result, config('wxpay.key')),
        ];
    }

    protected function wxUnifiedOrder(array $params): array
    {
        // 调用微信统一下单接口
        return [];
    }

    protected function wxSign(array $params, string $key): string
    {
        ksort($params);
        $str = urldecode(http_build_query($params)) . '&key=' . $key;
        return strtoupper(md5($str));
    }
}

这个支付服务的核心设计是:订单号和金额统一,支付参数分端构造。 无论用户从哪个终端下单,订单创建逻辑完全共享,只有支付调用层需要分端适配。

1.2 DIY 模板的多端适配

DIY 模板允许商家在后台拖拽配置首页布局。同一份配置在不同终端上需要不同的渲染方式。

javascript
// components/diy/adapter.js

export function detectTerminal() {
  // #ifdef MP-WEIXIN
  return 'mp';
  // #endif

  // #ifdef H5
  const ua = navigator.userAgent;
  if (/MicroMessenger/i.test(ua)) return 'oa';
  if (!/Android|iPhone|iPad|Mobile/i.test(ua)) return 'pc';
  return 'h5';
  // #endif

  // #ifdef APP-PLUS
  return 'app';
  // #endif
}

export function adaptComponents(components, terminal) {
  return components.map(comp => {
    const adapted = JSON.parse(JSON.stringify(comp));

    if (terminal === 'pc') {
      if (comp.type === 'nav_grid') {
        adapted.config.columns = Math.min(comp.config.columns || 5, 4);
      }
      if (comp.type === 'goods_list') {
        adapted.config.columns = 4;
      }
    }

    if (terminal === 'mp') {
      if (comp.type === 'banner') {
        adapted.config.height = Math.min(comp.config.height || 320, 400);
      }
    }

    return adapted;
  });
}

DIY 配置是"一次配置,多端适配"的。 商家在后台配置一次,系统根据终端自动调整列数、高度等参数,不需要为每个终端单独配置。

二、1-10 级分销:关系链的存储模型

2.1 关系链的存储方案

1-10 级分销的核心工程问题是:如何高效地存储和查询一条最长 10 层的关系链。

有三种方案。

方案一:邻接表(parent_id)。 每个用户只存直接上级的 ID。查询某用户的所有上级需要递归查询,10 层需要 10 次查询。

方案二:路径枚举(path)。 每个用户存储从根节点到自己的完整路径,如 1-5-12-30。查询某用户的所有上级只需解析路径,一次查询。

方案三:闭包表(closure table)。 用独立的表存储所有祖先-后代关系。查询效率高,但存储空间大(每个用户有 N 条记录,N 为层级数)。

对于 1-10 级分销,方案二(路径枚举)是最优选择。10 层的路径最多约 20-30 个字符,存储开销小,查询效率高。

sql
CREATE TABLE `distribution` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) NOT NULL COMMENT '分销商用户ID',
  `parent_id` int(11) DEFAULT NULL COMMENT '直接上级ID',
  `path` varchar(500) NOT NULL COMMENT '关系路径,如 1-5-12-30',
  `level` tinyint(1) NOT NULL DEFAULT '1' COMMENT '自身深度(1=一级分销商)',
  `invite_code` varchar(20) DEFAULT NULL COMMENT '邀请码',
  `total_commission` decimal(12,2) NOT NULL DEFAULT '0.00' COMMENT '累计佣金',
  `settled_commission` decimal(12,2) NOT NULL DEFAULT '0.00' COMMENT '已结算佣金',
  `status` tinyint(1) NOT NULL DEFAULT '1',
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_user` (`user_id`),
  UNIQUE KEY `uk_invite_code` (`invite_code`),
  KEY `idx_parent` (`parent_id`),
  KEY `idx_path` (`path`(191))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

path 字段存储从根节点到当前节点的完整路径。例如,用户 30 的上级链是 1 → 5 → 12 → 30,那么 path = '1-5-12-30'。

这个设计的优势是:查询某用户的所有上级只需一次查询。

php
<?php
// app/common/service/DistributionService.php

namespace app\common\service;

use think\facade\Db;

class DistributionService
{
    /**
     * 绑定分销关系
     */
    public function bind(int $userId, int $parentId): bool
    {
        // 检查是否已有上级
        $exists = Db::name('distribution')->where('user_id', $userId)->find();
        if ($exists) {
            return false;
        }

        // 检查是否形成环
        if ($this->wouldCreateCycle($userId, $parentId)) {
            throw new \Exception('不能绑定自己的下级为上级');
        }

        // 获取上级信息
        $parent = Db::name('distribution')->where('user_id', $parentId)->find();

        if ($parent) {
            $path = $parent['path'] . '-' . $userId;
            $level = $parent['level'] + 1;
        } else {
            $path = $parentId . '-' . $userId;
            $level = 1;
        }

        Db::name('distribution')->insert([
            'user_id' => $userId,
            'parent_id' => $parentId,
            'path' => $path,
            'level' => $level,
            'invite_code' => $this->generateInviteCode(),
            'created_at' => date('Y-m-d H:i:s'),
        ]);

        return true;
    }

    /**
     * 检查是否形成环
     */
    protected function wouldCreateCycle(int $userId, int $parentId): bool
    {
        $parent = Db::name('distribution')->where('user_id', $parentId)->find();
        if (!$parent) return false;

        $pathParts = explode('-', $parent['path']);
        return in_array((string)$userId, $pathParts);
    }

    /**
     * 获取某用户的所有上级(最多 10 级)
     */
    public function getAncestors(int $userId, int $maxLevel = 10): array
    {
        $relation = Db::name('distribution')->where('user_id', $userId)->find();
        if (!$relation) return [];

        $pathParts = explode('-', $relation['path']);
        array_pop($pathParts); // 移除自己
        $ancestors = array_reverse($pathParts); // 最近的上级在前

        // 只取前 maxLevel 级
        $ancestors = array_slice($ancestors, 0, $maxLevel);

        // 批量查询上级信息
        if (empty($ancestors)) return [];

        $users = Db::name('user')
            ->whereIn('id', $ancestors)
            ->field('id, nickname, avatar')
            ->select()
            ->toArray();
        $userMap = array_column($users, null, 'id');

        $result = [];
        foreach ($ancestors as $index => $ancestorId) {
            if (isset($userMap[$ancestorId])) {
                $result[] = [
                    'user_id' => $ancestorId,
                    'level' => $index + 1,
                    'nickname' => $userMap[$ancestorId]['nickname'],
                ];
            }
        }

        return $result;
    }

    protected function generateInviteCode(): string
    {
        do {
            $code = strtoupper(substr(md5(uniqid('', true)), 0, 8));
        } while (Db::name('distribution')->where('invite_code', $code)->count());
        return $code;
    }
}

getAncestors 方法是核心。它从 path 中解析出所有上级,一次查询获取完整的关系链。时间复杂度是 O(1)(不考虑批量查询用户信息的开销)。

三、1-10 级佣金计算

3.1 层级比例的配置

10 级分销需要为每一级配置佣金比例。这个配置需要灵活的存储和查询。

sql
CREATE TABLE `distribution_config` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `level` tinyint(1) NOT NULL COMMENT '分销层级 1-10',
  `rate` decimal(5,2) NOT NULL COMMENT '佣金比例(%)',
  `min_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '最低订单金额',
  `status` tinyint(1) NOT NULL DEFAULT '1',
  `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_level` (`level`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

配置示例:

sql
INSERT INTO `distribution_config` (`level`, `rate`, `status`) VALUES
(1, 15.00, 1),
(2, 10.00, 1),
(3, 8.00, 1),
(4, 5.00, 1),
(5, 3.00, 1),
(6, 2.00, 1),
(7, 1.50, 1),
(8, 1.00, 1),
(9, 0.50, 1),
(10, 0.30, 1);

比例递减是关键设计。 一级分销商拿最多(15%),越往上比例越低。这符合商业逻辑:直接推荐人贡献最大,越远的上级贡献越小。

3.2 佣金计算引擎

php
<?php
// app/common/service/CommissionService.php

namespace app\common\service;

use think\facade\Db;

class CommissionService
{
    /**
     * 结算分销佣金(支持 1-10 级)
     */
    public function settle(int $orderId): void
    {
        $order = Db::name('order')->where('id', $orderId)->find();
        if (!$order) return;

        // 幂等检查
        $exists = Db::name('commission')
            ->where('order_id', $orderId)
            ->count();
        if ($exists) return;

        // 获取所有上级(最多 10 级)
        $distributionService = new DistributionService();
        $ancestors = $distributionService->getAncestors($order['user_id'], 10);

        if (empty($ancestors)) return;

        // 获取各级佣金比例
        $configs = Db::name('distribution_config')
            ->where('status', 1)
            ->column('rate', 'level');

        Db::startTrans();
        try {
            foreach ($ancestors as $ancestor) {
                $level = $ancestor['level'];
                $rate = $configs[$level] ?? 0;
                if ($rate <= 0) continue;

                $amount = round($order['pay_amount'] * $rate / 100, 2);
                if ($amount <= 0) continue;

                Db::name('commission')->insert([
                    'order_id' => $orderId,
                    'user_id' => $ancestor['user_id'],
                    'from_user_id' => $order['user_id'],
                    'level' => $level,
                    'order_amount' => $order['pay_amount'],
                    'rate' => $rate,
                    'amount' => $amount,
                    'status' => 0, // 待结算
                    'created_at' => date('Y-m-d H:i:s'),
                ]);

                // 更新分销商累计佣金
                Db::name('distribution')
                    ->where('user_id', $ancestor['user_id'])
                    ->inc('total_commission', $amount)
                    ->update();
            }

            Db::commit();

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

    /**
     * 批量结算(用于定时任务)
     */
    public function batchSettle(int $limit = 100): array
    {
        $orders = Db::name('order')
            ->where('status', OrderStateMachine::COMPLETED)
            ->where('commission_settled', 0)
            ->limit($limit)
            ->select();

        $settled = 0;
        $failed = 0;

        foreach ($orders as $order) {
            try {
                $this->settle($order['id']);
                Db::name('order')
                    ->where('id', $order['id'])
                    ->update(['commission_settled' => 1]);
                $settled++;
            } catch (\Exception $e) {
                $failed++;
                \think\facade\Log::error('佣金结算失败', [
                    'order_id' => $order['id'],
                    'error' => $e->getMessage(),
                ]);
            }
        }

        return ['settled' => $settled, 'failed' => $failed];
    }
}

这个引擎的核心设计有三点。

一次性获取所有上级。 getAncestors 从 path 中解析出所有上级,避免递归查询。

幂等性保证。 通过 order_id 检查佣金是否已结算,防止重复计算。

比例递减的灵活性。 每一级的比例从 distribution_config 表读取,运营人员可以随时调整。

3.3 佣金结算的时机

佣金的计算和结算需要区分两个概念。

计算时机:订单完成时。 订单状态变为"已完成"(用户确认收货或自动确认)时,系统计算佣金,写入 commission 表,状态为"待结算"。

结算时机:售后期结束后。 佣金不是立即到账,而是有一段"冻结期"(通常 7-15 天),售后期结束后才真正结算到余额。

php
<?php
// app/job/SettleCommission.php

namespace app\job;

use think\queue\Job;
use think\facade\Db;

class SettleCommission
{
    public function fire(Job $job, $data)
    {
        $commissionId = $data['commission_id'];

        Db::startTrans();
        try {
            $commission = Db::name('commission')
                ->where('id', $commissionId)
                ->where('status', 0)
                ->lock(true)
                ->find();

            if (!$commission) {
                Db::commit();
                $job->delete();
                return;
            }

            // 更新佣金状态
            Db::name('commission')
                ->where('id', $commissionId)
                ->update([
                    'status' => 1,
                    'settled_at' => date('Y-m-d H:i:s'),
                ]);

            // 增加用户余额
            Db::name('user')
                ->where('id', $commission['user_id'])
                ->inc('balance', $commission['amount'])
                ->update();

            // 更新分销商已结算佣金
            Db::name('distribution')
                ->where('user_id', $commission['user_id'])
                ->inc('settled_commission', $commission['amount'])
                ->update();

            // 记录余额流水
            Db::name('balance_log')->insert([
                'user_id' => $commission['user_id'],
                'amount' => $commission['amount'],
                'type' => 'commission',
                'related_id' => $commissionId,
                'remark' => '分销佣金结算',
                'created_at' => date('Y-m-d H:i:s'),
            ]);

            Db::commit();
            $job->delete();

        } catch (\Exception $e) {
            Db::rollback();
            \think\facade\Log::error('佣金结算失败', [
                'commission_id' => $commissionId,
                'error' => $e->getMessage(),
            ]);
            $job->delete();
        }
    }
}

结算延迟是风险控制的关键。 如果订单发生退款,佣金需要被追回。如果佣金已经结算,追回会变得复杂。延迟结算给系统留出了处理退款的时间窗口。

四、分销的合规边界

4.1 法律框架

1-10 级分销的合规性取决于具体的佣金计算方式。

合规的分销: 佣金来源于真实的商品销售利润,分销商通过推广商品获得佣金。分销层级的存在是为了奖励不同层级的推广贡献。

违规的传销: 要求参加者缴纳费用或购买商品获得加入资格,按照一定顺序组成层级,直接或间接以发展人员数量作为计酬或返利依据。

关键区别在于:佣金是否来源于真实的商品销售。 如果佣金只与"拉人头"的数量挂钩,而与商品销售无关,就触及了传销的红线。

4.2 工程上的合规设计

系统需要在工程层面提供合规保障。

佣金来源标记。 每条佣金记录都关联具体的订单,证明佣金来源于真实销售。

sql
ALTER TABLE `commission` ADD COLUMN `order_id` int(11) NOT NULL COMMENT '来源订单' AFTER `user_id`;
ALTER TABLE `commission` ADD KEY `idx_order` (`order_id`);

分销层级可配置。 允许运营人员将层级从 10 级调低到 3 级或更低,以适应不同地区的法规要求。

佣金比例上限。 所有层级的佣金总和不应超过商品利润的合理范围。系统可以设置"佣金总额不超过订单金额的 X%"的校验。

php
<?php
// 佣金计算时校验总额
public function validateTotalCommission(float $orderAmount, float $totalCommission): void
{
    $maxRatio = config('distribution.max_total_ratio', 30); // 最高 30%
    $actualRatio = $totalCommission / $orderAmount * 100;

    if ($actualRatio > $maxRatio) {
        throw new \Exception(
            "佣金总额占比 {$actualRatio}% 超过上限 {$maxRatio}%"
        );
    }
}

分销商资质审核。 申请成为分销商需要满足一定条件(如实名认证、购买过商品),避免"零成本加入"的传销特征。

五、多端分销的展示

5.1 分销中心的数据展示

分销中心是分销商查看自己业绩和佣金的页面。它需要在 H5、小程序、APP 三端一致展示。

vue
<!-- pages/distribution/index.vue -->
<template>
  <view class="distribution-center">
    <!-- 收益概览 -->
    <view class="earnings-card">
      <view class="total">
        <text class="label">累计收益</text>
        <text class="amount">¥{{ stats.total_commission }}</text>
      </view>
      <view class="row">
        <view class="item">
          <text class="value">¥{{ stats.settled_commission }}</text>
          <text class="label">已结算</text>
        </view>
        <view class="item">
          <text class="value">¥{{ stats.pending_commission }}</text>
          <text class="label">待结算</text>
        </view>
      </view>
    </view>

    <!-- 团队概览 -->
    <view class="team-card">
      <view class="title">我的团队</view>
      <view class="levels">
        <view v-for="level in teamStats" :key="level.level" class="level-item">
          <text class="level-name">{{ level.level }}级</text>
          <text class="level-count">{{ level.count }}人</text>
        </view>
      </view>
    </view>

    <!-- 佣金明细 -->
    <view class="commission-list">
      <view class="title">佣金明细</view>
      <view v-for="item in commissions" :key="item.id" class="commission-item">
        <view class="info">
          <text class="from">来自 {{ item.from_nickname }}</text>
          <text class="time">{{ item.created_at }}</text>
        </view>
        <view class="amount">
          <text class="level">{{ item.level }}级</text>
          <text class="value">+¥{{ item.amount }}</text>
        </view>
      </view>
    </view>
  </view>
</template>

5.2 邀请码分享

分销商通过邀请码邀请下级。邀请码的分享需要适配三端:小程序生成小程序码,H5 生成链接,APP 生成分享卡片。

javascript
// utils/share.js

export async function shareInviteCode(inviteCode) {
  const shareUrl = `https://your-domain.com/register?invite_code=${inviteCode}`;

  // #ifdef MP-WEIXIN
  // 小程序:生成小程序码
  const res = await uni.request({
    url: '/api/share/mini_qrcode',
    method: 'POST',
    data: { invite_code: inviteCode, page: 'pages/register/register' },
  });
  return { type: 'image', url: res.data.url };
  // #endif

  // #ifdef H5
  // H5:生成分享链接
  return { type: 'link', url: shareUrl };
  // #endif

  // #ifdef APP-PLUS
  // APP:生成分享卡片
  return {
    type: 'card',
    title: '邀请你加入',
    url: shareUrl,
  };
  // #endif
}

六、边界与张力

ThinkPHP + UniApp 多端商城系统的 1-10 级分销在工程实践中,有几个需要清醒认识的边界。

第一,10 级分销的法律风险。 在中国,三级以上的分销层级需要特别谨慎。《禁止传销条例》对"团队计酬"有明确限制。系统需要提供层级配置能力,让运营人员根据当地法规调整层级数量。

第二,佣金计算的总和上限。 10 级佣金的累加可能超过商品利润。系统需要设置佣金总额上限,避免"卖一单亏一单"的情况。

第三,路径枚举的长度限制。 如果分销层级很深(如 50 级),path 字段会变得很长,varchar(500) 可能不够用。系统需要评估层级深度,选择合适的数据类型。

第四,佣金结算的并发问题。 高并发下,多个订单同时结算佣金可能导致余额更新的竞争。系统需要使用乐观锁或队列处理。

第五,多端分享的一致性。 小程序码、H5 链接、APP 分享卡片的生成方式完全不同。系统需要在服务端统一生成,客户端只负责展示。

结语:分销系统的工程本质是"关系链的变现"

ThinkPHP + UniApp 多端商城系统的 1-10 级分销,其工程本质可以归结为一句话:把用户之间的推荐关系转化为可量化、可结算的佣金。

路径枚举解决了关系链的存储效率问题,一次性查询获取所有上级。层级配置解决了佣金比例的灵活调整问题,运营人员可以随时修改。幂等设计解决了重复结算的问题,order_id 是唯一的结算依据。延迟结算解决了退款追回的问题,售后期结束后才真正到账。

最新回复 (0)

    暂无评论

请先登录后发表评论!

返回
请先登录后发表评论!