分銷(xiāo)系統(tǒng):自動(dòng)分潤(rùn)、代理關(guān)系鏈與實(shí)時(shí)結(jié)算實(shí)現(xiàn))
簡(jiǎn)介這是一套基于PHP開(kāi)發(fā)的代理分銷(xiāo)型商城系統(tǒng)面向中小型電商企業(yè)、創(chuàng)業(yè)團(tuán)隊(duì)及PHP學(xué)習(xí)者解決多級(jí)代理管理、傭金自動(dòng)結(jié)算與分銷(xiāo)渠道拓展等核心運(yùn)營(yíng)需求。系統(tǒng)采用MVC架構(gòu)前后端功能完備前端支持用戶(hù)注冊(cè)登錄、商品展示、購(gòu)物車(chē)與訂單全流程后端涵蓋商品管理、代理審核與等級(jí)設(shè)定、訂單履約、數(shù)據(jù)統(tǒng)計(jì)及第三方支付支付寶/微信與物流接口集成。資源包為11.5MB的RAR壓縮文件含完整可運(yùn)行代碼主要包含PHP業(yè)務(wù)邏輯文件、MySQL數(shù)據(jù)庫(kù)腳本、HTML/CSS/JS前端頁(yè)面及配置文檔結(jié)構(gòu)清晰便于二次開(kāi)發(fā)與功能定制。目前已有1349人學(xué)習(xí)下載讀者可直接部署調(diào)試深入理解電商分銷(xiāo)模型實(shí)現(xiàn)、PHP安全防護(hù)防SQL注入/XSS、傭金計(jì)算邏輯及高復(fù)用模塊設(shè)計(jì)思路。1. 這不是個(gè)“代理后臺(tái)”而是一套能跑通三級(jí)分潤(rùn)、自動(dòng)結(jié)算、實(shí)時(shí)查傭的 PHP 商城分銷(xiāo)閉環(huán)系統(tǒng)你手上有款自營(yíng)商品想讓朋友幫你賣(mài)但又怕對(duì)賬扯皮、傭金算錯(cuò)、下級(jí)代理拉人沒(méi)動(dòng)力——?jiǎng)e急著寫(xiě) Excel 表格或手動(dòng)打款。這個(gè) PHP 代理分銷(xiāo)系統(tǒng)從注冊(cè)即生成專(zhuān)屬推廣鏈接、下單自動(dòng)綁定上級(jí)、訂單完成觸發(fā)多級(jí)分潤(rùn)計(jì)算支持一級(jí)、二級(jí)、三級(jí)到后臺(tái)實(shí)時(shí)查看各級(jí)代理的團(tuán)隊(duì)規(guī)模、成交額、待結(jié)傭金、已提現(xiàn)記錄全部走完完整業(yè)務(wù)流。它不依賴(lài) SaaS 平臺(tái)代碼全開(kāi)源數(shù)據(jù)庫(kù)結(jié)構(gòu)清晰核心邏輯集中在app/Logic/Commission.php和app/Model/Agent.php里沒(méi)有黑匣子調(diào)用也沒(méi)有隱藏的第三方服務(wù)鉤子。適合中小團(tuán)隊(duì)快速部署一個(gè)可控、可審計(jì)、能二次開(kāi)發(fā)的分銷(xiāo)底座尤其適合已有 PHP 技術(shù)棧、想把分銷(xiāo)模塊嵌入現(xiàn)有商城的開(kāi)發(fā)者。如果你正卡在“怎么讓代理關(guān)系鏈自動(dòng)建?!薄霸趺幢苊庵貜?fù)結(jié)算”“怎么讓下級(jí)代理看到自己能賺多少”這三個(gè)問(wèn)題上這個(gè)項(xiàng)目就是為解這三道題寫(xiě)的。2. 環(huán)境準(zhǔn)備與一鍵部署從 Nginx PHP 7.4 到后臺(tái)可登錄的完整路徑2.1 基礎(chǔ)運(yùn)行環(huán)境PHP 版本、擴(kuò)展與目錄權(quán)限硬性要求該項(xiàng)目基于 Laravel 8.x 框架封裝非原生 PHP 寫(xiě)就因此必須滿(mǎn)足 Laravel 的底層約束。常見(jiàn)翻車(chē)點(diǎn)在于 PHP 版本和擴(kuò)展缺失——它不兼容 PHP 8.0實(shí)測(cè) PHP 7.4.33 是最穩(wěn)版本PHP 擴(kuò)展中mbstring、openssl、pdo_mysql、tokenizer、xml、ctype、json必須啟用缺一不可。特別注意bcmath擴(kuò)展分潤(rùn)計(jì)算中所有金額運(yùn)算如 0.01 × 0.15都強(qiáng)制使用bcadd()/bcmul()若未開(kāi)啟會(huì)導(dǎo)致傭金歸零。Nginx 配置需啟用try_files $uri $uri/ /index.php?$query_string;不能簡(jiǎn)單用 Apache 的.htaccess替代。部署時(shí) Web 根目錄必須指向public/而非項(xiàng)目根目錄否則路由全部 404。# 檢查 PHP 環(huán)境是否達(dá)標(biāo)執(zhí)行后應(yīng)無(wú)報(bào)錯(cuò)且返回版本號(hào) php -v php -m | grep -E (mbstring|openssl|pdo_mysql|tokenizer|xml|ctype|json|bcmath)提示若用寶塔面板PHP 設(shè)置頁(yè)中「禁用函數(shù)」列表里務(wù)必刪掉putenv和pcntl_exec——Laravel 的 Artisan 命令行工具依賴(lài)它們否則php artisan migrate會(huì)靜默失敗。2.2 數(shù)據(jù)庫(kù)初始化表結(jié)構(gòu)設(shè)計(jì)意圖與關(guān)鍵字段說(shuō)明項(xiàng)目使用 MySQL 5.7共 12 張核心表其中agents代理主表、agent_relations上下級(jí)關(guān)系表、orders訂單主表、commission_logs分潤(rùn)流水表四張是業(yè)務(wù)心臟。agent_relations表采用「單向父子」設(shè)計(jì)每條記錄存parent_id和child_id并帶level字段值為 1/2/3不冗余存儲(chǔ)整條關(guān)系鏈靠遞歸查詢(xún)構(gòu)建團(tuán)隊(duì)樹(shù)。commission_logs表中type字段區(qū)分order_settle訂單結(jié)算、withdraw_apply提現(xiàn)申請(qǐng)、withdraw_success提現(xiàn)成功三類(lèi)動(dòng)作status字段控制狀態(tài)機(jī)流轉(zhuǎn)0待處理1已完成2已駁回這是后續(xù)做財(cái)務(wù)對(duì)賬的唯一可信源。-- 創(chuàng)建代理關(guān)系表關(guān)鍵聯(lián)合索引加速上級(jí)查下級(jí)、下級(jí)查上級(jí)雙向查詢(xún) CREATE TABLE agent_relations ( id bigint unsigned NOT NULL AUTO_INCREMENT, parent_id bigint unsigned NOT NULL COMMENT 上級(jí)代理ID, child_id bigint unsigned NOT NULL COMMENT 下級(jí)代理ID, level tinyint unsigned NOT NULL DEFAULT 1 COMMENT 層級(jí)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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci;2.3 安裝流程五步走完從解壓到登錄后臺(tái)上傳并解壓將源碼包上傳至服務(wù)器/var/www/distribution/確保storage/和bootstrap/cache/目錄可寫(xiě)chmod -R 775 storage bootstrap/cache配置.env復(fù)制.env.example為.env修改DB_DATABASE、DB_USERNAME、DB_PASSWORD關(guān)鍵一步設(shè)置APP_URLhttp://your-domain.com必須帶協(xié)議和域名不能是 localhost否則推廣鏈接生成異常安裝依賴(lài)在項(xiàng)目根目錄執(zhí)行composer install --no-dev --optimize-autoloader跳過(guò) dev 包減少體積執(zhí)行遷移php artisan migrate --force--force是必須參數(shù)生產(chǎn)環(huán)境默認(rèn)禁止生成密鑰 清緩存php artisan key:generate php artisan config:clear php artisan cache:clear完成后訪(fǎng)問(wèn)http://your-domain.com/admin/login初始賬號(hào)密碼均為admin / admin123首次登錄后強(qiáng)制修改。3. 核心模塊拆解分潤(rùn)引擎、代理關(guān)系鏈、推廣鏈接生成三大主干邏輯3.1 分潤(rùn)引擎Commission::calculateForOrder()如何保證金額不溢出、不漏算分潤(rùn)不是簡(jiǎn)單乘法而是帶規(guī)則的條件計(jì)算。核心方法位于app/Logic/Commission.php入口是calculateForOrder(Order $order)。它接收一個(gè)訂單對(duì)象返回一個(gè)Collection每項(xiàng)含agent_id、amount、level、rule_key。關(guān)鍵設(shè)計(jì)有三點(diǎn)精度控制所有金額以「分」為單位存入數(shù)據(jù)庫(kù)orders.total_amount是整數(shù)計(jì)算時(shí)先轉(zhuǎn)元再乘比例最后round(bcmul($base, $rate, 2), 0)回到分避免浮點(diǎn)誤差累積層級(jí)穿透按agent_relations表反查該訂單買(mǎi)家的全部上級(jí)但只取 level ≤ 3 的記錄并去重防止 A→B→C→D 中 C 同時(shí)是 A 的二級(jí)和 B 的一級(jí)被重復(fù)計(jì)規(guī)則隔離不同商品類(lèi)目可配置不同分潤(rùn)率$order-product-category-commission_rate優(yōu)先于全局默認(rèn)率且支持「滿(mǎn) 1000 元額外加 1%」這類(lèi)階梯規(guī)則由CommissionRuleManager類(lèi)動(dòng)態(tài)加載。// app/Logic/Commission.php 節(jié)選核心計(jì)算片段 public function calculateForOrder(Order $order): Collection { $buyerId $order-user_id; // 1. 反查 buyer 的所有上級(jí)代理含層級(jí) $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); // 轉(zhuǎn)元 $amountInCents round(bcmul($baseAmount, $rate, 2), 0) * 100; // 轉(zhuǎn)回分 return (object)[ agent_id $upline-parent_id, amount (int)$amountInCents, level $upline-level, rule_key $this-currentRuleKey ]; }); }注意$amountInCents必須是整數(shù)類(lèi)型因?yàn)閿?shù)據(jù)庫(kù)字段是BIGINT若傳 float 會(huì)觸發(fā) MySQL 截?cái)鄬?dǎo)致分潤(rùn)變 0。3.2 代理關(guān)系鏈Agent::buildTeamTree()怎么高效查出三級(jí)團(tuán)隊(duì)結(jié)構(gòu)前端「我的團(tuán)隊(duì)」頁(yè)面要展示我A→ 直推 5 人 → 每人再推若干 → 最多展開(kāi)到三級(jí)。如果每次請(qǐng)求都遞歸查 3 層MySQL 會(huì)瞬間變慢。本項(xiàng)目采用「預(yù)生成 緩存」策略當(dāng)新代理注冊(cè)并綁定上級(jí)時(shí)觸發(fā)Agent::syncTeamAncestors()將該代理的所有祖先最多 3 級(jí)寫(xiě)入agent_ancestors表結(jié)構(gòu)agent_id,ancestor_id,distance。查團(tuán)隊(duì)時(shí)只需一條 JOINSELECT 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表在代理注冊(cè)、上級(jí)變更、關(guān)系刪除時(shí)由 Observer 自動(dòng)維護(hù)避免運(yùn)行時(shí)計(jì)算開(kāi)銷(xiāo)。這也是為什么項(xiàng)目要求 MySQL 5.7——需要窗口函數(shù)支持ROW_NUMBER()做距離去重。3.3 推廣鏈接生成ShareLinkGenerator::make()如何防刷、防篡改、帶來(lái)源追蹤每個(gè)代理的推廣鏈接形如https://yoursite.com/register?refabc123其中ref參數(shù)不是代理 ID而是加密字符串。生成邏輯在app/Services/ShareLinkGenerator.php使用hash_hmac(sha256, $agentId . $timestamp, config(app.key))生成簽名將$agentId、$timestamp、$signature三者拼接后 base64url 編碼非標(biāo)準(zhǔn) base64/替換為-_鏈接過(guò)期時(shí)間設(shè)為 7 天解碼后校驗(yàn) timestamp 是否超時(shí)、signature 是否匹配任一失敗則拒絕注冊(cè)。這樣設(shè)計(jì)杜絕了「手動(dòng)改 ref 參數(shù)綁定任意上級(jí)」的漏洞也避免了代理 ID 泄露帶來(lái)的信息風(fēng)險(xiǎn)。4. 避坑指南上線(xiàn)前必須驗(yàn)證的 4 個(gè)致命細(xì)節(jié)4.1 現(xiàn)象后臺(tái)顯示「今日新增代理 0」但實(shí)際有用戶(hù)通過(guò)推廣鏈接注冊(cè)原因.env中APP_URL未配置為真實(shí)域名或 Nginx 未透?jìng)鱔-Forwarded-Proto頭導(dǎo)致URL::full()生成的注冊(cè)鏈接是http://localhost/register?refxxx用戶(hù)點(diǎn)擊后跳轉(zhuǎn)失敗前端 JS 捕獲不到 ref 參數(shù)。解決確認(rèn)APP_URL為https://your-domain.com并在 Nginx 配置中加入proxy_set_header X-Forwarded-Proto $scheme;。4.2 現(xiàn)象訂單完成但commission_logs表無(wú)記錄代理后臺(tái)「待結(jié)算傭金」始終為 0原因訂單狀態(tài)變更事件未正確觸發(fā)。項(xiàng)目監(jiān)聽(tīng)OrderStatusUpdated事件但默認(rèn)隊(duì)列驅(qū)動(dòng)為sync同步執(zhí)行若APP_DEBUGfalse且日志級(jí)別設(shè)為error事件內(nèi)異常會(huì)被靜默吞掉。解決臨時(shí)切到database隊(duì)列.env中設(shè)QUEUE_CONNECTIONdatabase運(yùn)行php artisan queue:work --once觀(guān)察failed_jobs表是否有報(bào)錯(cuò)或打開(kāi)APP_DEBUGtrue查看具體異常。4.3 現(xiàn)象代理 A 推薦 BB 又推薦 C但 C 下單后A 的二級(jí)分潤(rùn)未到賬原因agent_relations表中缺少 A→C 的 level2 記錄。系統(tǒng)只在 B 注冊(cè)時(shí)自動(dòng)生成 A→Blevel1但不會(huì)自動(dòng)補(bǔ)全 A→Clevel2需靠Agent::syncTeamAncestors()在 C 注冊(cè)時(shí)補(bǔ)全。若該方法未被調(diào)用如注冊(cè)流程繞過(guò)AgentRegisterService關(guān)系鏈就斷了。解決檢查注冊(cè)控制器是否調(diào)用AgentRegisterService::handle($request)或手動(dòng)執(zhí)行php artisan agent:sync-ancestors補(bǔ)全歷史數(shù)據(jù)。4.4 現(xiàn)象提現(xiàn)申請(qǐng)?zhí)峤缓鬆顟B(tài)卡在「審核中」財(cái)務(wù)后臺(tái)無(wú)通知原因提現(xiàn)審核依賴(lài)App\Observers\WithdrawalObserver但該 Observer 未在A(yíng)ppServiceProvider::boot()中注冊(cè)或withdrawals表的status字段默認(rèn)值為pending但 Observer 的updating方法只監(jiān)聽(tīng)status從pending變更為approved/rejected若數(shù)據(jù)庫(kù)默認(rèn)值寫(xiě)錯(cuò)如寫(xiě)成processing事件不觸發(fā)。解決確認(rèn)withdrawals.status默認(rèn)值為pending并在A(yíng)ppServiceProvider中添加Withdrawal::observe(WithdrawalObserver::class);。5. 二次開(kāi)發(fā)實(shí)戰(zhàn)如何接入微信支付回調(diào)、增加邀請(qǐng)返現(xiàn)、導(dǎo)出代理業(yè)績(jī) Excel5.1 接入微信支付回調(diào)復(fù)用現(xiàn)有訂單模型只改三處微信支付成功后會(huì) POST 通知到你的notify_url。本項(xiàng)目已預(yù)留/api/payment/wechat/notify路由但控制器空實(shí)現(xiàn)。你需要在app/Http/Controllers/Api/Payment/WechatNotifyController.php中解析微信 XML 回調(diào)校驗(yàn)簽名根據(jù)out_trade_no查orders表確認(rèn)訂單存在且status unpaid調(diào)用OrderService::markAsPaid($order, wechat)—— 此方法內(nèi)部會(huì)觸發(fā)OrderStatusUpdated事件自動(dòng)完成分潤(rùn)結(jié)算。// app/Services/OrderService.php 中補(bǔ)充 public function markAsPaid(Order $order, string $channel): void { DB::transaction(function () use ($order, $channel) { $order-update([ status paid, paid_at now(), payment_channel $channel ]); // 關(guān)鍵觸發(fā)事件讓 Commission 邏輯自動(dòng)跑 event(new OrderStatusUpdated($order, unpaid, paid)); }); }提示微信回調(diào)地址必須在微信商戶(hù)平臺(tái)配置為https://your-domain.com/api/payment/wechat/notify且域名需備案。測(cè)試時(shí)用「微信支付沙箱環(huán)境」避免真金白銀扣款。5.2 增加邀請(qǐng)返現(xiàn)在注冊(cè)流程中插入現(xiàn)金獎(jiǎng)勵(lì)邏輯需求新用戶(hù)通過(guò)代理鏈接注冊(cè)除建立代理關(guān)系外再給邀請(qǐng)人上級(jí)發(fā) 5 元現(xiàn)金紅包。不走分潤(rùn)池單獨(dú)記賬。新增表invitation_bonusesid,inviter_id,invitee_id,amount_cents,status,created_at在A(yíng)gentRegisterService::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() ]); // 同時(shí)更新 inviter 的 wallet_balance 字段需提前在 users 表加此字段 DB::table(users)-where(id, $referrerId)-increment(wallet_balance, 500); }5.3 導(dǎo)出代理業(yè)績(jī) Excel用 PhpSpreadsheet 替換原生 CSV原項(xiàng)目導(dǎo)出用fputcsv格式簡(jiǎn)陋。升級(jí)為 Excel 需composer require phpoffice/phpspreadsheet新建app/Exports/AgentPerformanceExport.php繼承Maatwebsite\Excel\Concerns\FromCollection在導(dǎo)出控制器中調(diào)用Excel::download(new AgentPerformanceExport($agentId), agent-performance.xlsx)。關(guān)鍵點(diǎn)AgentPerformanceExport的collection()方法應(yīng)返回一個(gè)包含name,team_size,total_orders,total_commission的集合數(shù)據(jù)來(lái)自聯(lián)表查詢(xún)而非多次 DB 請(qǐng)求。// 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(); }從那以后我每次接手分銷(xiāo)類(lèi)項(xiàng)目都會(huì)先翻app/Logic/Commission.php和database/migrations/*commission*遷移文件確認(rèn)分潤(rùn)精度處理、關(guān)系鏈存儲(chǔ)方式、事件觸發(fā)時(shí)機(jī)這三點(diǎn)是否對(duì)齊業(yè)務(wù)需求。哪怕只是加個(gè)「滿(mǎn) 1000 返 50」的活動(dòng)我也強(qiáng)制走一遍php artisan tinker手動(dòng)調(diào)用Commission::calculateForOrder()打印出每級(jí)金額盯著小數(shù)點(diǎn)后兩位——這比寫(xiě)十遍單元測(cè)試更能暴露浮點(diǎn)陷阱。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取