简介:面向企业营销推广场景的 PHP 三级报单分销系统源码,适合需要搭建会员裂变与分销商城的企业或 PHP 开发者使用。系统围绕三级分销模式展开,会员可发展上下线关系并依据层级获取佣金,同时内置完整商城功能,涵盖商品展示、购物车、订单管理及支付宝/微信支付接口,并支持订单跟踪等完整购物流程,能自动适配手机端访问,便于在移动互联网环境下开展推广。资源包共 745 个文件,压缩包大小 16.26MB,主要包括 PHP 业务逻辑、JavaScript 交互脚本、CSS 样式表、GIF 动图等前端素材,另附 SQL 数据库文件和 license 说明,便于部署与二次开发。后台支持自定义会员等级与手动报单操作,可针对不同等级设置差异化权益,灵活调整积分和订单分配。目前已有 259 人学习,适合想快速落地三级分销电商平台的开发者参考使用。
1. 企业三级推广报单分销源码,真正要理清的是哪几条链路
在培训、直播、私域电商这类强运营业务里,二级分销往往撑不起团队扩张的激励预期,层数再往上叠,模式本身的边界又会变得模糊。企业三级推广报单分销源码,指的是把“会员注册、推荐关系绑定、订单报单、佣金结算”这一整条动线用代码稳定跑起来的一套业务系统。它解决的核心问题是:推广员做完动作之后,推荐关系有没有被准确记录,业绩有没有按规则分出去。
同样叫源码,后台模板怎么换都行,但底层链路却是区分一套分销系统能不能用的分水岭。推荐关系是直接存路径还是递归算层级,报单流程怎么把审核与分佣解耦,佣金结算在并发报单下怎样避免重复发放,这三件事没定好,界面做得再漂亮也会在真实运营的第一个月出问题。下面按数据模型、注册实现、结算脚本、上线验证的顺序,把整条链路补完整。
2. 三级分销数据建模:推荐关系表、报单主表、分佣流水表的分工
拿到任何一套企业三级推广报单分销源码,第一件事不是搭环境跑后台,而是先把数据库表结构翻出来看。分销类业务的绝大多数 bug,早期都出在关系表与订单表的设计欠考虑上。
2.1 推荐关系表只存直接上级,不冗余整条路径
三级分销的推荐关系是一棵多叉树:一个会员可以有多个下级,但只能有一个直接上级。许多新手的做法是把整条祖先链冗余成一个字段,比如parent_path存1-5-28-103,查三级时直接切分字符串。小流量阶段这没问题,一旦出现关系调整或转移上级,路径字段的维护成本会瞬间失控。
单亲指向模型只记录pid,需要查询上级时从当前节点向上回溯两步即可。这也是多数企业三级推广报单分销源码的常见做法,核心建表语句如下:
CREATE TABLE `members` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `username` varchar(32) NOT NULL DEFAULT '', `mobile` varchar(20) NOT NULL DEFAULT '', `pid` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '直接推荐人ID,0表示无', `level` tinyint(4) NOT NULL DEFAULT '0' COMMENT '会员等级', `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '1正常 0冻结', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_pid` (`pid`), KEY `idx_mobile` (`mobile`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员表';两个容易忽视的细节。第一,pid必须建索引,字段类型要和id完全一致,两边都带unsigned,否则关联查询会因隐式转换放弃索引,百万级用户量下回表成本快速放大。第二,mobile建唯一索引,这是防重复注册的第一道闸门。很多源码只在应用层做一次 select 判断,并发注册时照样能插进两条相同手机号。
关系调整是另一个考量点。如果运营后台支持变更上级,单亲模型只需更新叶子行的pid,不需要批量改写任何路径字段;而全路径方案要同步更新子树下每个节点的parent_path,树深度一超过五层,一次转移操作就能拖垮整个库。
2.2 报单主表把审核与结算拆成两个状态
“报单”在不同行业含义不同。在会销和直销场景里,它指线下签单后由上级或管理员代录的购买记录;在私域电商里,则可能是用户下单后等待复核的特殊订单。不管哪种,报单表的核心设计都是把归属人、推荐链快照、审核状态分开存。
CREATE TABLE `orders` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '订单号', `member_id` int(11) unsigned NOT NULL COMMENT '下单会员ID', `sponsor_id` int(11) unsigned NOT NULL COMMENT '报单人ID,即触发佣金的会员', `amount` decimal(10,2) NOT NULL DEFAULT '0.00', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0待审核 1已通过 2已驳回 3已结算', `remark` varchar(255) DEFAULT NULL COMMENT '驳回原因等', `audit_time` datetime DEFAULT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_member` (`member_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='报单订单表';状态流转拆成四档,而不是用一个布尔字段表示“是否已结算”:
| 状态值 | 含义 | 触发方 | 备注 |
|---|---|---|---|
| 0 | 待审核 | 会员或管理员提交 | 此时不产生佣金 |
| 1 | 已通过 | 运营审核 | 等待结算脚本处理 |
| 2 | 已驳回 | 运营驳回 | 可修改后重新提交 |
| 3 | 已结算 | 定时任务 | 分佣流水已写入 |
审核与结算分开的原因是角色不同、时机不同:审核是人工操作,结算是任务程序。二者混在一起,结算出错时很难判定是没审核还是没执行。实际项目中我还会再加一个settle_time字段记录结算完成时间,排查延迟问题时能直接定位。
2.3 分佣流水表带上唯一键防止重复发放
分佣流水表是整个链路里最容易被忽略、又最关键的一张表。最典型的故障是:结算脚本半夜跑批,某个订单处理到一半进程被杀或数据库连接超时,任务重新执行时把已结算的佣金又发了一遍。用户投诉的往往不是少发,而是多发之后被平台追回,这对分销体系是致命的信任损伤。
CREATE TABLE `commission_records` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `order_id` int(11) unsigned NOT NULL, `member_id` int(11) unsigned NOT NULL COMMENT '获得佣金的会员ID', `sponsor_id` int(11) unsigned NOT NULL COMMENT '触发佣金的报单会员', `level` tinyint(4) NOT NULL DEFAULT '0' COMMENT '1/2/3 表示第几级', `amount` decimal(10,2) NOT NULL DEFAULT '0.00', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0待发放 1已发放 2冻结', `unique_key` varchar(60) NOT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_unique_key` (`unique_key`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分佣流水表';unique_key的内容是order_id + member_id + level拼接字符串,配合唯一索引后,同一订单、同一上级、同一层级只能存在一条佣金记录。结算脚本不需要加分布式锁,只要用INSERT IGNORE或ON DUPLICATE KEY UPDATE,数据库层就把重复挡掉了。
提示:唯一键不要只拼
order_id + member_id,否则同一订单给同一上级的多个层级佣金会被误判成重复数据。
3. 会员推广注册的 PHP 实现:推荐位校验、推广码解析与防重复绑定
前面两张表定好后,注册接口就是整个会员推广注册系统源码里最核心的代码块。注册要做的不仅是 insert 一条会员数据,还要正确解析用户是从谁的推广链接进来的,并且保证这段关系不会被并发请求破坏。
3.1 注册事务里先锁推荐人,再检查直属下级数量
从会员表设计延伸出一个常见业务约束:每个会员的直属下级人数设上限,比如 30 人或 5 人。如果不在数据库层面做保护,两个用户同时带同一个推广码注册,都可能读到未满的结果,各自插入一条,最终把上限顶破。
// PHP + PDO 风格的注册核心逻辑 public function register(string $username, string $mobile, string $password, int $parentId = 0): int { $this->db->beginTransaction(); try { // 1. 锁推荐人行,避免并发下直属下级数超限 if ($parentId > 0) { $parent = $this->db->query( 'SELECT id, status FROM members WHERE id = ? FOR UPDATE', [$parentId] ); if (!$parent || (int)$parent['status'] !== 1) { throw new \Exception('推荐人不存在或已被冻结'); } $cnt = $this->db->query( 'SELECT COUNT(*) AS c FROM members WHERE pid = ?', [$parentId] ); if ((int)$cnt['c'] >= 30) { throw new \Exception('该推荐人的直属下级已达上限'); } } // 2. 手机号和用户名查重 $exists = $this->db->query( 'SELECT id FROM members WHERE mobile = ? OR username = ?', [$mobile, $username] ); if ($exists) { throw new \Exception('手机号或用户名已被注册'); } // 3. 插入会员并提交 $this->db->execute( 'INSERT INTO members (username, mobile, pid, status) VALUES (?, ?, ?, 1)', [$username, $mobile, $parentId] ); $userId = (int)$this->db->lastInsertId(); $this->db->commit(); return $userId; } catch (\Throwable $e) { $this->db->rollBack(); throw $e; } }这段代码有四个关键点。FOR UPDATE在事务中锁住了推荐人行,让后面的 COUNT 检查和 INSERT 变成串行操作,这是并发安全的根本。查重用一条 SQL 同时检查mobile和username,避免分开两次查询造成时间窗口。插入语句只写username, mobile, pid, status,其余字段交给默认值。事务把锁、查重、插入包在一起,任何一步异常都会回滚,不会留下只有会员却没有推荐关系的孤儿数据。
3.2 推广链接参数 aId 的解析与 cookie 兜底
会员推广注册系统源码都会涉及推广链接的生成与解析。最常见的做法是在链接里带一个会员 ID 参数,比如/register.php?aId=123。用户点开注册链接后,页面需要把这个参数存起来,保证用户先逛首页、再进注册页时推荐关系不丢。
// 第一步:解析链接参数 $parentId = isset($_GET['aId']) ? (int)$_GET['aId'] : 0; // 第二步:写入cookie,有效期设7天,HttpOnly 防止脚本读取 if ($parentId > 0) { setcookie('inviter_id', (string)$parentId, time() + 7 * 86400, '/', '', false, true); } // 第三步:注册时优先取链接参数,其次取cookie $finalParentId = $parentId > 0 ? $parentId : (int)($_COOKIE['inviter_id'] ?? 0);逻辑的关键在第三步:当用户不是直接点开注册页,而是先访问首页、注册时才进入注册页时,aId已经丢失,cookie 里的inviter_id还能兜底。HttpOnly防止脚本读取 cookie,同时参数转成 int 后,aId就不可能作为注入载荷进入 SQL。有些系统把参数名取成uid、code、refer,这些名称容易和现有字段混淆,建议统一叫aId,后续换推广渠道时只需要改一处映射。
3.3 已登录用户再次点推广链接,要不要改绑定
这是产品层面必须提前拍板的逻辑:一个老用户点了别人的推广链接,是否把上级改成新的人。多数源码默认不改绑定,pid一旦写入就贯穿生命周期,只有运营后台手动调整才变更。原因有两点:业绩归属需要稳定预期,频繁换上级会让计酬陷入纠纷;技术上也能避免设计复杂的换绑状态机。
| 场景 | 绑定行为 | 实现方式 |
|---|---|---|
| 新用户访问注册页 | 首次写入绑定 | 插入 pid |
| 老用户点推广链接 | 不改变绑定 | 仅记录点击日志 |
| 运营强制调级 | 手动改 pid 并重算业绩 | 事务内更新并写操作日志 |
若产品确实需要“7 天内可改绑一次”,正确顺序是:先检查该会员是否已有佣金流水,若有,不能直接改pid,必须把历史业绩归属同步重算,否则对账必然不平。
4. 报单佣金计算:三级分佣算法、结算状态机与防重复发放
订单审核通过后,佣金计算立刻被触发。三级分销的分佣规则一般是第一、二、三级分别按不同比例从订单金额里提成。比例如何配置、计算时如何防止精度丢失、结算任务如何与报单状态对齐,这三个问题构成这一章的全部内容。
4.1 向上取两级上线的迭代写法
拿到一张通过的报单,先要知道该给谁分钱。规则是:报单人自己的一级上线拿一级佣金,二级上线拿二级佣金,三级上线拿三级佣金。报单人若处于会员树的前三层,最多只能凑出两条或一条佣金记录。
function getUpline(int $memberId, int $maxDepth = 3): array { $upline = []; $currentId = $memberId; for ($i = 1; $i <= $maxDepth; $i++) { $row = $this->db->query('SELECT pid FROM members WHERE id = ?', [$currentId]); if (!$row || (int)$row['pid'] <= 0) { break; } $currentId = (int)$row['pid']; $upline[$i] = $currentId; } return $upline; }为什么不一次性 join 出两层级?因为members表在单亲模型下就是单向链表,join 写法也可以,但可读性和这个循环没有数量级差别。真正影响性能的是调用次数:每笔订单都实时跑这段循环,并发下单场景会把数据库打爆。实操上我会把这段逻辑放在异步任务里,订单审核通过只标记“待结算”,计算佣金交给独立队列进程。
4.2 三级分佣比例配置与金额精度
分佣比例不应写死在代码里,运营后台会配置一组三级比例参数,任务执行时再从配置表读取。常见做法是把比例存成整数:一级 1000 表示 10.00%,避免浮点数参与比较运算。
| 层级 | 配置值(int) | 百分比 |
|---|---|---|
| 一级 | 1000 | 10.00% |
| 二级 | 500 | 5.00% |
| 三级 | 200 | 2.00% |
配套的佣金计算与落库代码:
public function settleOrder(array $order): void { $rates = [1 => 1000, 2 => 500, 3 => 200]; // 从配置表读取 $upline = getUpline((int)$order['sponsor_id'], 3); $baseAmount = (int)round($order['commission_base'] * 100); // 单位转换成分 foreach ($upline as $level => $memberId) { // 按百分比计算,全部用整数分参与乘除 $amountCents = (int)round($baseAmount * $rates[$level] / 10000); $amountYuan = $amountCents / 100; // 转回元存入流水表 $uniqueKey = $order['id'] . '-' . $memberId . '-' . $level; $this->db->execute( "INSERT IGNORE INTO commission_records (order_id, member_id, sponsor_id, level, amount, status, unique_key) VALUES (?, ?, ?, ?, ?, 1, ?)", [$order['id'], $memberId, $order['sponsor_id'], $level, $amountYuan, $uniqueKey] ); } // 更新订单状态为已结算,防止再次进入任务队列 $this->db->execute( 'UPDATE orders SET status = 3, settle_time = NOW() WHERE id = ? AND status = 1', [$order['id']] ); }这段代码的关键设计有两处。第一,金额先用整数分参与乘除,最后再转回元,彻底避开0.1 + 0.2这类浮点误差;第二,INSERT IGNORE结合唯一键,即使脚本重复执行,已写入的佣金流水也不会产生第二份。末尾更新订单状态时特意加了status = 1条件,下一轮扫描不会把同一笔订单捞起来。
报单的分佣基数是另一个踩坑点。订单金额可能包含运费、优惠券抵扣、积分抵扣,佣金按哪个数算,必须在审核通过时固化。常见做法是在orders表增加commission_base字段,审核时把“实付金额 - 运费 - 抵扣”算好写入,结算脚本只认这个字段。有的系统采用递减式分佣,订单金额越高、上级比例越低,本质是为了控制资金杠杆风险,此类规则要保证历史配置可回溯,否则运营改错一次比例,对账就是灾难。
4.3 结算脚本的批次策略与失败重试
企业环境下我写一个常驻 CLI 进程循环扫描订单表,每次取 100 条状态为 1 的订单做批量结算。单条失败不能中断整批,要记录失败原因到日志表;同一订单重试超过 3 次后置为异常状态,交由运营手工介入。
while (true) { $orders = $this->db->queryAll( 'SELECT * FROM orders WHERE status = 1 ORDER BY id ASC LIMIT 100' ); if (!$orders) { sleep(2); continue; } foreach ($orders as $order) { try { $this->settleOrder($order); } catch (\Throwable $e) { // 记日志后继续处理下一单,避免单点阻塞队列 $this->logger->error("settle failed: order={$order['id']} err={$e->getMessage()}"); } } }这个循环配合订单状态字段,天然支持崩溃恢复。进程在任何位置被杀,重启后只会重新扫描状态为 1 的订单,已写入的佣金流水由唯一键兜底,不会被重复发放。需要横向扩容时,把循环改成多进程,每个进程按order_id范围分片处理,不能让两个进程同时处理同一订单。
5. 上线前造数脚本与佣金对账:验证三级分销正确性的两条路径
前三章把模型与代码都定好后,最容易被跳过的一步是上线前验证。三级分销这种链路一旦出错,用肉眼在后台几乎发现不了,只能在测试环境里模拟多层推广关系,跑一遍报单与结算,再用对账 SQL 把结果和手工算数对齐。
5.1 生成 500 人、十层深度的推广网
造数脚本的要点是让每个新注册用户从已有用户中随机抽取推荐人,保证树深度能超过三级。用 PHP CLI 执行下面的逻辑即可:
// 造数脚本:先插根节点,再逐层生成 $rootId = 1; for ($i = 2; $i <= 500; $i++) { $pid = mt_rand(1, $i - 1); // 只会选择已存在的用户做上线 $this->db->execute( "INSERT INTO members (username, mobile, pid) VALUES (?, ?, ?)", ["user_{$i}", "138" . str_pad((string)$i, 8, "0", STR_PAD_LEFT), $pid] ); }mt_rand(1, $i - 1)保证每个新节点都挂在已存在节点下方,自然形成一棵深度可超十层的推荐树。造完数据后,再向orders表插入 200 笔状态为已通过的报单,随机指定sponsor_id,触发结算脚本。
5.2 三张对账 SQL 找出分佣异常
结算完成后再执行三组查询:
-- 1. 检查是否存在同一订单重复结算 SELECT order_id, member_id, level, COUNT(*) AS cnt FROM commission_records GROUP BY order_id, member_id, level HAVING cnt > 1; -- 2. 按订单汇总佣金,与订单金额比例比对 SELECT order_id, status, SUM(amount) AS total_commission, MAX(amount) AS max_commission FROM commission_records GROUP BY order_id ORDER BY order_id; -- 3. 找出结算后仍未生成流水的订单 SELECT o.id FROM orders o LEFT JOIN commission_records c ON c.order_id = o.id WHERE o.status = 3 AND c.id IS NULL;第一组 SQL 利用唯一索引做反向检查,理论结果应恒为空;第二组手工挑选几笔订单,按三级比例算出期望佣金总和,和total_commission对齐;第三组专治订单已是已结算、但佣金记录缺失的问题,这种问题多半是任务在更新订单状态前崩溃造成的。三组查询全部通过时,整套三级推广报单分销源码的链路才算真正闭环,可以放心交给运营去铺推广活动。
本文还有配套的精品资源,点击获取