ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PHP代理分销系统实战:从零搭建三级分佣与自动结算后台

PHP代理分销系统实战:从零搭建三级分佣与自动结算后台 简介这份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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;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.100010%, updated_at INT UNSIGNED NOT NULL, UNIQUE KEY uk_level (agent_level, commission_level) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这张表按「代理等级 × 分佣层级」两个维度配比例。比如一级代理拿直推 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 偷取samesiteLax兼顾跨站跳转和基本安全。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 可提现余额不足]; } // 写提现申请status0 审核中 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是否每单只有一套记录、金额是否对得上。再并发跑两次任务确认没有重复分佣。这套幂等 补偿的模式我在好几个项目里都用过比单纯依赖支付回调可靠得多。最后说个我自己的习惯每次改完结算相关代码我都会拿一笔真实金额手算一遍三级分佣和数据库里的记录逐行对。这个动作花不了五分钟但帮我拦下过至少三次上线事故。分销系统里钱算错比功能少更致命。希望帮到你。本文还有配套的精品资源点击获取
返回列表