简介:这是一套基于PHP开发的代理分销型商城系统,面向中小型电商企业、创业团队及PHP学习者,解决多级代理管理、佣金自动结算与分销渠道拓展等核心运营需求。系统采用MVC架构,前后端功能完备:前端支持用户注册登录、商品展示、购物车与订单全流程;后端涵盖商品管理、代理审核与等级设定、订单履约、数据统计及第三方支付(支付宝/微信)与物流接口集成。资源包为11.5MB的RAR压缩文件,含完整可运行代码,主要包含PHP业务逻辑文件、MySQL数据库脚本、HTML/CSS/JS前端页面及配置文档,结构清晰便于二次开发与功能定制。目前已有1349人学习下载,读者可直接部署调试,深入理解电商分销模型实现、PHP安全防护(防SQL注入/XSS)、佣金计算逻辑及高复用模块设计思路。
1. 这不是个“代理后台”,而是一套能跑通三级分润、自动结算、实时查佣的 PHP 商城分销闭环系统
你手上有款自营商品,想让朋友帮你卖,但又怕对账扯皮、佣金算错、下级代理拉人没动力——别急着写 Excel 表格或手动打款。这个 PHP 代理分销系统,从注册即生成专属推广链接、下单自动绑定上级、订单完成触发多级分润计算(支持一级、二级、三级),到后台实时查看各级代理的团队规模、成交额、待结佣金、已提现记录,全部走完完整业务流。它不依赖 SaaS 平台,代码全开源,数据库结构清晰,核心逻辑集中在app/Logic/Commission.php和app/Model/Agent.php里,没有黑匣子调用,也没有隐藏的第三方服务钩子。适合中小团队快速部署一个可控、可审计、能二次开发的分销底座,尤其适合已有 PHP 技术栈、想把分销模块嵌入现有商城的开发者。如果你正卡在“怎么让代理关系链自动建模”“怎么避免重复结算”“怎么让下级代理看到自己能赚多少”这三个问题上,这个项目就是为解这三道题写的。
2. 环境准备与一键部署:从 Nginx + PHP 7.4 到后台可登录的完整路径
2.1 基础运行环境:PHP 版本、扩展与目录权限硬性要求
该项目基于 Laravel 8.x 框架封装(非原生 PHP 写就),因此必须满足 Laravel 的底层约束。常见翻车点在于 PHP 版本和扩展缺失——它不兼容 PHP 8.0+,实测 PHP 7.4.33 是最稳版本;PHP 扩展中mbstring、openssl、pdo_mysql、tokenizer、xml、ctype、json必须启用,缺一不可。特别注意bcmath扩展:分润计算中所有金额运算(如 0.01 × 0.15)都强制使用bcadd()/bcmul(),若未开启会导致佣金归零。Nginx 配置需启用try_files $uri $uri/ /index.php?$query_string;,不能简单用 Apache 的.htaccess替代。部署时 Web 根目录必须指向public/,而非项目根目录,否则路由全部 404。
# 检查 PHP 环境是否达标(执行后应无报错且返回版本号) php -v php -m | grep -E "(mbstring|openssl|pdo_mysql|tokenizer|xml|ctype|json|bcmath)"提示:若用宝塔面板,PHP 设置页中「禁用函数」列表里务必删掉
putenv和pcntl_exec——Laravel 的 Artisan 命令行工具依赖它们,否则php artisan migrate会静默失败。
2.2 数据库初始化:表结构设计意图与关键字段说明
项目使用 MySQL 5.7+,共 12 张核心表,其中agents(代理主表)、agent_relations(上下级关系表)、orders(订单主表)、commission_logs(分润流水表)四张是业务心脏。agent_relations表采用「单向父子」设计:每条记录存parent_id和child_id,并带level字段(值为 1/2/3),不冗余存储整条关系链,靠递归查询构建团队树。commission_logs表中type字段区分order_settle(订单结算)、withdraw_apply(提现申请)、withdraw_success(提现成功)三类动作,status字段控制状态机流转(0=待处理,1=已完成,2=已驳回),这是后续做财务对账的唯一可信源。
-- 创建代理关系表(关键:联合索引加速上级查下级、下级查上级双向查询) CREATE TABLE `agent_relations` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, `parent_id` bigint unsigned NOT NULL COMMENT '上级代理ID', `child_id` bigint unsigned NOT NULL COMMENT '下级代理ID', `level` tinyint unsigned NOT NULL DEFAULT '1' COMMENT '层级:1=直推,2=间推,3=再间推', `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_parent_child` (`parent_id`,`child_id`), KEY `idx_child` (`child_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;2.3 安装流程:五步走完,从解压到登录后台
- 上传并解压:将源码包上传至服务器
/var/www/distribution/,确保storage/和bootstrap/cache/目录可写(chmod -R 775 storage bootstrap/cache) - 配置
.env:复制.env.example为.env,修改DB_DATABASE、DB_USERNAME、DB_PASSWORD,关键一步:设置APP_URL=http://your-domain.com(必须带协议和域名,不能是 localhost,否则推广链接生成异常) - 安装依赖:在项目根目录执行
composer install --no-dev --optimize-autoloader(跳过 dev 包,减少体积) - 执行迁移:
php artisan migrate --force(--force是必须参数,生产环境默认禁止) - 生成密钥 & 清缓存:
php artisan key:generate && php artisan config:clear && php artisan cache:clear
完成后访问http://your-domain.com/admin/login,初始账号密码均为admin / admin123(首次登录后强制修改)。
3. 核心模块拆解:分润引擎、代理关系链、推广链接生成三大主干逻辑
3.1 分润引擎:Commission::calculateForOrder()如何保证金额不溢出、不漏算
分润不是简单乘法,而是带规则的条件计算。核心方法位于app/Logic/Commission.php,入口是calculateForOrder(Order $order)。它接收一个订单对象,返回一个Collection,每项含agent_id、amount、level、rule_key。关键设计有三点:
- 精度控制:所有金额以「分」为单位存入数据库(
orders.total_amount是整数),计算时先转元再乘比例,最后round(bcmul($base, $rate, 2), 0)回到分,避免浮点误差累积; - 层级穿透:按
agent_relations表反查该订单买家的全部上级,但只取 level ≤ 3 的记录,并去重(防止 A→B→C→D 中 C 同时是 A 的二级和 B 的一级,被重复计); - 规则隔离:不同商品类目可配置不同分润率,
$order->product->category->commission_rate优先于全局默认率,且支持「满 1000 元额外加 1%」这类阶梯规则,由CommissionRuleManager类动态加载。
// app/Logic/Commission.php 节选:核心计算片段 public function calculateForOrder(Order $order): Collection { $buyerId = $order->user_id; // 1. 反查 buyer 的所有上级代理(含层级) $uplines = DB::table('agent_relations') ->where('child_id', $buyerId) ->where('level', '<=', 3) ->get(['parent_id', 'level']); return $uplines->map(function ($upline) use ($order) { $rate = $this->getEffectiveRate($order, $upline->level); $baseAmount = bcmul((string)$order->total_amount, '0.01', 2); // 转元 $amountInCents = round(bcmul($baseAmount, $rate, 2), 0) * 100; // 转回分 return (object)[ 'agent_id' => $upline->parent_id, 'amount' => (int)$amountInCents, 'level' => $upline->level, 'rule_key' => $this->currentRuleKey ]; }); }注意:
$amountInCents必须是整数类型,因为数据库字段是BIGINT,若传 float 会触发 MySQL 截断,导致分润变 0。
3.2 代理关系链:Agent::buildTeamTree()怎么高效查出三级团队结构
前端「我的团队」页面要展示:我(A)→ 直推 5 人 → 每人再推若干 → 最多展开到三级。如果每次请求都递归查 3 层,MySQL 会瞬间变慢。本项目采用「预生成 + 缓存」策略:当新代理注册并绑定上级时,触发Agent::syncTeamAncestors(),将该代理的所有祖先(最多 3 级)写入agent_ancestors表(结构:agent_id,ancestor_id,distance)。查团队时只需一条 JOIN:
SELECT u.*, a.distance FROM users u JOIN agent_ancestors a ON u.id = a.agent_id WHERE a.ancestor_id = ? AND a.distance <= 3 ORDER BY a.distance, u.created_at;agent_ancestors表在代理注册、上级变更、关系删除时由 Observer 自动维护,避免运行时计算开销。这也是为什么项目要求 MySQL 5.7+——需要窗口函数支持ROW_NUMBER()做距离去重。
3.3 推广链接生成:ShareLinkGenerator::make()如何防刷、防篡改、带来源追踪
每个代理的推广链接形如https://yoursite.com/register?ref=abc123,其中ref参数不是代理 ID,而是加密字符串。生成逻辑在app/Services/ShareLinkGenerator.php:
- 使用
hash_hmac('sha256', $agentId . $timestamp, config('app.key'))生成签名; - 将
$agentId、$timestamp、$signature三者拼接后 base64url 编码(非标准 base64,+/替换为-_); - 链接过期时间设为 7 天,解码后校验 timestamp 是否超时、signature 是否匹配,任一失败则拒绝注册。
这样设计杜绝了「手动改 ref 参数绑定任意上级」的漏洞,也避免了代理 ID 泄露带来的信息风险。
4. 避坑指南:上线前必须验证的 4 个致命细节
4.1 现象:后台显示「今日新增代理 0」,但实际有用户通过推广链接注册
原因:.env中APP_URL未配置为真实域名,或 Nginx 未透传X-Forwarded-Proto头,导致URL::full()生成的注册链接是http://localhost/register?ref=xxx,用户点击后跳转失败,前端 JS 捕获不到 ref 参数。
解决:确认APP_URL为https://your-domain.com,并在 Nginx 配置中加入proxy_set_header X-Forwarded-Proto $scheme;。
4.2 现象:订单完成,但commission_logs表无记录,代理后台「待结算佣金」始终为 0
原因:订单状态变更事件未正确触发。项目监听OrderStatusUpdated事件,但默认队列驱动为sync(同步执行),若APP_DEBUG=false且日志级别设为error,事件内异常会被静默吞掉。
解决:临时切到database队列(.env中设QUEUE_CONNECTION=database),运行php artisan queue:work --once,观察failed_jobs表是否有报错;或打开APP_DEBUG=true查看具体异常。
4.3 现象:代理 A 推荐 B,B 又推荐 C,但 C 下单后,A 的二级分润未到账
原因:agent_relations表中缺少 A→C 的 level=2 记录。系统只在 B 注册时自动生成 A→B(level=1),但不会自动补全 A→C(level=2),需靠Agent::syncTeamAncestors()在 C 注册时补全。若该方法未被调用(如注册流程绕过AgentRegisterService),关系链就断了。
解决:检查注册控制器是否调用AgentRegisterService::handle($request);或手动执行php artisan agent:sync-ancestors补全历史数据。
4.4 现象:提现申请提交后,状态卡在「审核中」,财务后台无通知
原因:提现审核依赖App\Observers\WithdrawalObserver,但该 Observer 未在AppServiceProvider::boot()中注册,或withdrawals表的status字段默认值为pending,但 Observer 的updating方法只监听status从pending变更为approved/rejected,若数据库默认值写错(如写成processing),事件不触发。
解决:确认withdrawals.status默认值为'pending',并在AppServiceProvider中添加Withdrawal::observe(WithdrawalObserver::class);。
5. 二次开发实战:如何接入微信支付回调、增加邀请返现、导出代理业绩 Excel
5.1 接入微信支付回调:复用现有订单模型,只改三处
微信支付成功后,会 POST 通知到你的notify_url。本项目已预留/api/payment/wechat/notify路由,但控制器空实现。你需要:
- 在
app/Http/Controllers/Api/Payment/WechatNotifyController.php中,解析微信 XML 回调,校验签名; - 根据
out_trade_no查orders表,确认订单存在且status = 'unpaid'; - 调用
OrderService::markAsPaid($order, 'wechat')—— 此方法内部会触发OrderStatusUpdated事件,自动完成分润结算。
// app/Services/OrderService.php 中补充 public function markAsPaid(Order $order, string $channel): void { DB::transaction(function () use ($order, $channel) { $order->update([ 'status' => 'paid', 'paid_at' => now(), 'payment_channel' => $channel ]); // 关键:触发事件,让 Commission 逻辑自动跑 event(new OrderStatusUpdated($order, 'unpaid', 'paid')); }); }提示:微信回调地址必须在微信商户平台配置为
https://your-domain.com/api/payment/wechat/notify,且域名需备案。测试时用「微信支付沙箱环境」,避免真金白银扣款。
5.2 增加邀请返现:在注册流程中插入现金奖励逻辑
需求:新用户通过代理链接注册,除建立代理关系外,再给邀请人(上级)发 5 元现金红包。不走分润池,单独记账。
- 新增表
invitation_bonuses(id,inviter_id,invitee_id,amount_cents,status,created_at); - 在
AgentRegisterService::handle()的末尾,添加:if ($referrerId) { DB::table('invitation_bonuses')->insert([ 'inviter_id' => $referrerId, 'invitee_id' => $user->id, 'amount_cents' => 500, // 5元=500分 'status' => 'pending', 'created_at' => now() ]); // 同时更新 inviter 的 wallet_balance 字段(需提前在 users 表加此字段) DB::table('users')->where('id', $referrerId)->increment('wallet_balance', 500); }
5.3 导出代理业绩 Excel:用 PhpSpreadsheet 替换原生 CSV
原项目导出用fputcsv,格式简陋。升级为 Excel 需:
composer require phpoffice/phpspreadsheet;- 新建
app/Exports/AgentPerformanceExport.php,继承Maatwebsite\Excel\Concerns\FromCollection; - 在导出控制器中调用
Excel::download(new AgentPerformanceExport($agentId), 'agent-performance.xlsx')。
关键点:AgentPerformanceExport的collection()方法应返回一个包含name,team_size,total_orders,total_commission的集合,数据来自联表查询,而非多次 DB 请求。
// app/Exports/AgentPerformanceExport.php public function collection() { return DB::table('agents') ->select( 'users.name', DB::raw('COUNT(DISTINCT ar2.child_id) as team_size'), DB::raw('COUNT(DISTINCT o.id) as total_orders'), DB::raw('SUM(cl.amount) as total_commission') ) ->join('users', 'agents.user_id', '=', 'users.id') ->leftJoin('agent_relations as ar1', 'agents.user_id', '=', 'ar1.parent_id') ->leftJoin('agent_relations as ar2', 'ar1.child_id', '=', 'ar2.parent_id') ->leftJoin('orders as o', 'ar2.child_id', '=', 'o.user_id') ->leftJoin('commission_logs as cl', 'ar2.child_id', '=', 'cl.agent_id') ->where('agents.user_id', $this->agentId) ->groupBy('users.name') ->get(); }从那以后我每次接手分销类项目,都会先翻app/Logic/Commission.php和database/migrations/*commission*迁移文件,确认分润精度处理、关系链存储方式、事件触发时机这三点是否对齐业务需求。哪怕只是加个「满 1000 返 50」的活动,我也强制走一遍php artisan tinker手动调用Commission::calculateForOrder(),打印出每级金额,盯着小数点后两位——这比写十遍单元测试更能暴露浮点陷阱。希望帮到你。
本文还有配套的精品资源,点击获取