简介:这份PHP代理分销系统是一套面向中小电商企业、代理分销创业者及PHP开发者的电子商务解决方案,重点解决多级代理管理、佣金结算与商城运营一体化的问题,适合具备一定PHP基础、希望研究电商系统架构或进行二次开发的技术人员。压缩包为rar格式,整体约11.5MB,上游未提供具体文件数量与类型明细,从描述看应包含前后台源码、数据库脚本及接口集成相关文件。系统前端覆盖用户注册登录、商品展示、购物车、订单处理与客户服务,后端则提供商品管理、代理分销审核与等级设定、佣金计算、订单发货退款、用户管理及销售数据统计等模块,并可能采用MVC架构与MySQL数据库,集成支付宝、微信支付及物流接口。目前已有1349人学习下载,读者可借此理解代理分销业务逻辑、掌握电商系统二次开发思路,并学习数据加密、防SQL注入与XSS等安全处理要点。
1. PHP代理分销系统:从零搭建一套能跑通结算的分销后台
手里有个做跨境电商的朋友,去年找我救火。他招了三十多个代理帮他卖货,结算全靠微信群发 Excel,月底对账对到凌晨三点,还出过两次多算佣金的乌龙。他问我能不能用 PHP 搞一套代理分销系统,让代理自己看业绩、自己提现,后台自动算钱。这个需求其实非常典型——PHP代理分销系统本质就是一套「多级代理 + 订单归因 + 佣金结算」的后台,用 PHP 写完全够用,Laravel、ThinkPHP 甚至原生 PHP 都能落地。它解决的核心问题就三个:代理从哪来、订单算谁的、钱怎么分。适合中小团队自建,也适合接私活快速交付。下面我按自己实际做过的路径,把选型、表结构、归因逻辑、结算和踩坑一次讲透。
2. 代理分销系统的数据模型怎么设计才不返工
代理分销最容易翻车的地方不是代码,是表结构。我见过太多人一开始只建了users和orders两张表,做到三级分佣时发现根本算不清谁是谁的下级,只能推倒重来。所以这一章先把模型立住,后面写代码才不会返工。
2.1 代理层级用邻接表还是路径枚举
代理关系本质是一棵树。常见两种存法:邻接表(每行存parent_id)和路径枚举(每行存path字段,如1/5/12/)。邻接表写入简单,但查「某代理的所有下级」要递归;路径枚举查询快,WHERE path LIKE '1/5/%'一条 SQL 搞定,代价是移动节点时要更新整棵子树。
我的选择是路径枚举 + 冗余 parent_id。原因很实际:分销系统里查下级是高频操作(算团队业绩、看下级订单),移动节点几乎不发生。用路径枚举把递归查询变成一次 LIKE,性能差距在代理上千人时非常明显。
CREATE TABLE `agents` ( `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, `user_id` INT UNSIGNED NOT NULL COMMENT '关联用户', `parent_id` INT UNSIGNED DEFAULT 0 COMMENT '直接上级代理ID', `path` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '层级路径 如 1/5/12/', `level` TINYINT UNSIGNED DEFAULT 1 COMMENT '代理等级', `status` TINYINT DEFAULT 1 COMMENT '1正常 0冻结', `created_at` INT UNSIGNED NOT NULL, UNIQUE KEY `uk_user` (`user_id`), KEY `idx_path` (`path`), KEY `idx_parent` (`parent_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;path字段存的是从根到当前节点的 ID 链,末尾带斜杠,这样LIKE '1/5/%'不会误匹配到1/50/。level冗余存层级深度,避免每次数斜杠。status用来冻结违规代理,冻结后不参与分佣但保留关系。
2.2 订单归因表与佣金流水表
订单和代理的关系要单独一张归因表,不要直接往orders上加agent_id。因为一笔订单可能同时触发多个层级的分佣,而且归因结果需要可追溯、可撤销。
CREATE TABLE `order_attribution` ( `id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, `order_id` BIGINT UNSIGNED NOT NULL, `agent_id` INT UNSIGNED NOT NULL COMMENT '直接归属代理', `bind_type` TINYINT DEFAULT 1 COMMENT '1链接 2优惠码 3手动', `created_at` INT UNSIGNED NOT NULL, UNIQUE KEY `uk_order` (`order_id`), KEY `idx_agent` (`agent_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `commission_log` ( `id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, `order_id` BIGINT UNSIGNED NOT NULL, `agent_id` INT UNSIGNED NOT NULL, `level` TINYINT UNSIGNED NOT NULL COMMENT '第几级分佣', `rate` DECIMAL(5,4) NOT NULL COMMENT '佣金比例', `amount` DECIMAL(10,2) NOT NULL COMMENT '佣金金额', `status` TINYINT DEFAULT 0 COMMENT '0待结算 1已结算 2已撤销', `created_at` INT UNSIGNED NOT NULL, KEY `idx_agent_status` (`agent_id`, `status`), KEY `idx_order` (`order_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;commission_log是核心账本。每一笔分佣单独一行,level记录是第几级拿的,rate记录当时用的比例。为什么比例要落库?因为代理等级会变、活动比例会调,事后对账必须能还原「当时按什么比例算的」。金额用DECIMAL不用FLOAT,这是血泪经验,浮点数算钱迟早出分位误差。
2.3 佣金比例配置表
比例不要写死在代码里。运营随时要调,写死一次改一次代码,迟早出事。
CREATE TABLE `commission_rule` ( `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, `agent_level` TINYINT UNSIGNED NOT NULL COMMENT '代理等级', `commission_level` TINYINT UNSIGNED NOT NULL COMMENT '第几级分佣', `rate` DECIMAL(5,4) NOT NULL COMMENT '比例 0.1000=10%', `updated_at` INT UNSIGNED NOT NULL, UNIQUE KEY `uk_level` (`agent_level`, `commission_level`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这张表按「代理等级 × 分佣层级」两个维度配比例。比如一级代理拿直推 10%、二级 5%、三级 2%。查询时按订单归属代理的等级去匹配,取不到就回退到默认等级。
提示:表建好后先插几条测试数据,把三级分佣的查询 SQL 跑通再写业务代码,能省掉后面大量调试时间。
3. 用 PHP 实现订单归因与三级分佣计算
模型立住了,接下来是这套系统最核心的两段逻辑:订单怎么归到代理头上,以及归因之后怎么把佣金一层层算出来。这两段写错,钱就分错,所以我会写得细一点。
3.1 代理链接与优惠码的归因实现
归因方式常见三种:专属链接带参数、优惠码、后台手动指定。链接归因最常用,做法是代理分享的 URL 带一个?aid=代理ID,用户点进来时把aid写进 Cookie 或 Session,下单时读取。
<?php // 落地页入口:捕获代理参数并写入 Cookie function captureAgentRef(): void { if (!empty($_GET['aid'])) { $aid = (int)$_GET['aid']; // 校验代理是否存在且正常 $agent = getAgentById($aid); if ($agent && $agent['status'] === 1) { // 30天归因窗口,httponly 防脚本读取 setcookie('agent_ref', (string)$aid, [ 'expires' => time() + 86400 * 30, 'path' => '/', 'httponly' => true, 'samesite' => 'Lax', ]); } } } // 下单时读取归因 function resolveAttribution(int $orderId): ?int { $aid = isset($_COOKIE['agent_ref']) ? (int)$_COOKIE['agent_ref'] : 0; if ($aid <= 0) { return null; // 无归因,走自然流量 } // 写入归因表,唯一索引保证一单只归一次 $sql = "INSERT IGNORE INTO order_attribution (order_id, agent_id, bind_type, created_at) VALUES (?, ?, 1, ?)"; $affected = db_execute($sql, [$orderId, $aid, time()]); return $affected > 0 ? $aid : null; }captureAgentRef在用户访问落地页时执行,把aid存进 Cookie,有效期 30 天,这是行业常见的归因窗口。httponly防止 XSS 偷取,samesite=Lax兼顾跨站跳转和基本安全。resolveAttribution在下单时调用,用INSERT IGNORE配合order_id的唯一索引,保证同一订单重复调用也只归因一次——这点很重要,支付回调经常重试,没有唯一约束就会重复归因。
参数说明:归因窗口86400 * 30可按业务调整,快消品可以短到 7 天,客单价高的可以拉到 90 天。bind_type区分归因来源,方便后面分析哪种方式转化好。
3.2 三级分佣的递归计算与落库
归因拿到直接代理后,要沿着path往上找上级,按层级算佣金。用路径枚举的好处在这里体现:一次查询就能拿到所有上级。
<?php // 计算并落库一笔订单的分佣 function settleCommission(int $orderId, float $orderAmount): array { // 1. 取归因代理 $attr = db_query_one("SELECT agent_id FROM order_attribution WHERE order_id = ?", [$orderId]); if (!$attr) { return []; // 无归因不分佣 } $directAgentId = (int)$attr['agent_id']; // 2. 取直接代理信息 $direct = db_query_one("SELECT id, path, level FROM agents WHERE id = ?", [$directAgentId]); if (!$direct) { return []; } // 3. 从 path 解析出所有上级(含自己),path 形如 1/5/12/ $chain = array_filter(explode('/', trim($direct['path'], '/'))); $chain = array_reverse($chain); // 从直接代理往上 $result = []; $maxLevel = 3; // 最多三级分佣 foreach ($chain as $idx => $agentId) { $commissionLevel = $idx + 1; if ($commissionLevel > $maxLevel) { break; } $agent = db_query_one("SELECT id, level, status FROM agents WHERE id = ?", [(int)$agentId]); if (!$agent || $agent['status'] !== 1) { continue; // 冻结代理跳过,但层级继续往上 } // 4. 查比例 $rule = db_query_one( "SELECT rate FROM commission_rule WHERE agent_level = ? AND commission_level = ?", [$agent['level'], $commissionLevel] ); if (!$rule) { continue; } $rate = (float)$rule['rate']; $amount = round($orderAmount * $rate, 2); // 5. 落库,唯一约束防重复 $sql = "INSERT IGNORE INTO commission_log (order_id, agent_id, level, rate, amount, status, created_at) VALUES (?, ?, ?, ?, ?, 0, ?)"; db_execute($sql, [$orderId, $agent['id'], $commissionLevel, $rate, $amount, time()]); $result[] = ['agent_id' => $agent['id'], 'level' => $commissionLevel, 'amount' => $amount]; } return $result; }这段逻辑有几个关键点。第一,path反转后从直接代理往上遍历,idx + 1就是分佣层级。第二,冻结代理用continue跳过而不是break,因为冻结的是这一级,上面的上级该拿还得拿。第三,金额用round(..., 2)保留两位,和DECIMAL(10,2)对齐。第四,INSERT IGNORE配合唯一约束,防止支付回调重试导致重复分佣。
参数说明:$maxLevel = 3控制分佣深度,改成 2 就是二级分销,改成 5 就是五级。commission_rule查不到就跳过,意味着没配比例的层级不分佣,这是安全的默认行为。
3.3 结算状态流转与提现冻结
佣金算出来是「待结算」,不能马上让代理提现。常见做法是订单过了售后期(比如 7 天)才把status从 0 改成 1,代理才能申请提现。提现时再冻结对应金额,防止重复提。
<?php // 订单确认收货 N 天后,把待结算佣金转为可结算 function confirmCommission(int $orderId): int { $sql = "UPDATE commission_log SET status = 1 WHERE order_id = ? AND status = 0"; return db_execute($sql, [$orderId]); } // 代理申请提现:校验可提现余额 function requestWithdraw(int $agentId, float $amount): array { // 可提现 = 已结算总额 - 已提现和提现中的总额 $settled = (float)db_query_one( "SELECT COALESCE(SUM(amount),0) AS s FROM commission_log WHERE agent_id = ? AND status = 1", [$agentId] )['s']; $withdrawn = (float)db_query_one( "SELECT COALESCE(SUM(amount),0) AS s FROM withdraw_log WHERE agent_id = ? AND status IN (0,1)", [$agentId] )['s']; $available = round($settled - $withdrawn, 2); if ($amount <= 0 || $amount > $available) { return ['ok' => false, 'msg' => '可提现余额不足']; } // 写提现申请,status=0 审核中 db_execute( "INSERT INTO withdraw_log (agent_id, amount, status, created_at) VALUES (?, ?, 0, ?)", [$agentId, $amount, time()] ); return ['ok' => true, 'available' => $available - $amount]; }confirmCommission由定时任务或订单状态变更触发。requestWithdraw里可提现余额是「已结算减已提现和提现中」,提现中的也要扣掉,否则代理连点两次就能超额提现。这是最容易出的资金漏洞,务必用事务包住查询和插入。
注意:提现涉及真金白银,
requestWithdraw的余额校验和插入必须在同一个数据库事务里,并且对agent_id加行锁,否则并发下必然超提。
4. 分销系统上线前必须排查的 5 个坑
这套系统我前后交付过几版,下面这 5 个坑是每次都会遇到或者差点出事的,按「现象 → 原因 → 解决」列出来,你上线前对着查一遍。
4.1 支付回调重复触发导致佣金翻倍
现象:同一笔订单在commission_log里出现两套分佣记录,代理余额虚高。原因:支付平台回调会重试,settleCommission被调了多次,而早期版本没加唯一约束。解决:commission_log加UNIQUE KEY (order_id, agent_id, level),插入用INSERT IGNORE;同时在订单表加settled标记,结算前先判断。
4.2 归因 Cookie 被清导致订单算成自然流量
现象:代理反馈明明是自己推的客户,订单却没算到自己头上。原因:用户中途清了 Cookie,或者从 App 内跳转时 Cookie 没带上。解决:归因参数除了写 Cookie,下单页再透传一次aid到表单隐藏域;对高价值客户,允许后台手动补归因,但要有操作日志。
4.3 浮点数算佣金出现分位误差
现象:对账时总金额差几分钱,代理投诉。原因:早期用FLOAT存金额,累加后精度丢失。解决:金额字段一律DECIMAL(10,2),PHP 侧用round($x, 2),涉及累加用bcadd或先转整数分再算。这个坑不踩一次不会信,踩了就记住了。
4.4 代理层级过深导致递归查询超时
现象:代理发展到几千人、层级十几层时,团队业绩页加载超过 10 秒。原因:用邻接表递归查下级,每层一次 SQL。解决:改用路径枚举,WHERE path LIKE '1/5/%'一次查完;再对path建索引。如果已经用了邻接表,加一张闭包表做冗余。
4.5 提现并发导致超额提现
现象:代理快速点两次提现,两笔都成功,余额变负。原因:余额校验和插入之间没有锁,两个请求都读到同样的可用余额。解决:整个提现逻辑包在事务里,SELECT ... FOR UPDATE锁住该代理的佣金记录,或者用 Redis 分布式锁按agent_id加锁。
5. 用定时任务和幂等设计把结算做稳
前面把主流程跑通了,但真正让系统在生产环境不出事的,是结算的幂等和补偿。我一般会加一个每日定时任务,扫「已确认收货但佣金还是待结算」的订单,补一次结算,同时用幂等键防止重复。
<?php // 每日补偿任务:扫描超期未结算的订单 function dailySettleJob(): void { // 取 7 天前确认收货、佣金仍待结算的订单 $deadline = time() - 86400 * 7; $orders = db_query_all( "SELECT o.id, o.amount FROM orders o LEFT JOIN commission_log c ON c.order_id = o.id AND c.status = 1 WHERE o.status = 'received' AND o.received_at < ? AND c.id IS NULL LIMIT 500", [$deadline] ); foreach ($orders as $order) { // 幂等键:settle:{order_id},用 Redis SETNX 防并发重复 $lockKey = 'settle:' . $order['id']; if (!redis_setnx($lockKey, 1, 300)) { continue; // 已有任务在处理 } try { settleCommission((int)$order['id'], (float)$order['amount']); confirmCommission((int)$order['id']); } finally { redis_del($lockKey); } } }这个任务的关键是幂等键settle:{order_id},用 RedisSETNX加 5 分钟过期,保证同一订单同一时间只有一个进程在处理。LIMIT 500防止一次拉太多把内存打满,剩下的下一轮再处理。try...finally保证锁一定释放,哪怕结算抛异常。
验证方法很直接:本地造 100 笔订单,手动把received_at改成 8 天前,跑一次任务,检查commission_log是否每单只有一套记录、金额是否对得上。再并发跑两次任务,确认没有重复分佣。这套幂等 + 补偿的模式,我在好几个项目里都用过,比单纯依赖支付回调可靠得多。
最后说个我自己的习惯:每次改完结算相关代码,我都会拿一笔真实金额手算一遍三级分佣,和数据库里的记录逐行对。这个动作花不了五分钟,但帮我拦下过至少三次上线事故。分销系统里钱算错,比功能少更致命。希望帮到你。
本文还有配套的精品资源,点击获取