基于 Laravel 開發會員分銷系統

基于 Laravel 開發會員分銷系統

最近,在 Sitesauce 現有基礎上新增會員系統,就具體實現細節寫了這篇文章。

筆記:我將從零開始構建該程序,這樣無論你處于什么階段都可以讀懂該文章。 當然如果你已經非常熟悉 laravel ,你可以在此 Rewardful 之類的平臺上完成該工作,這樣會節省不少時間。

為了概念明確,下文中 邀請者用上級替代,被邀請者使用下級替代

首先,我們要明確我們的需求:首先用戶可以通過程序分享鏈接邀請好友注冊,被邀請者可以通過鏈接注冊,從而綁定邀請關系,其次在下級消費的時候,上級都可以獲得相應的傭金。

現在我們要確定如何實現注冊。我原本打算使用 Fathom 的方法,只要將用戶引導到特定頁面,該用戶將會被標記為 special referral page ,待用戶完注冊,并將關系綁定。 但最終采用的是 Rewardful 的做法,通過向鏈接添加參數 ?via=miguel 來實現推薦頁面的構建。

好了,現在讓我們創建我們的注冊頁面,在注冊頁面程序會通過鏈接參數 via 匹配上級。 代碼很簡單,如果 via 存在那么將其存儲到 Cookie 30 天,由于我們有幾個不同子域名都需要該操作,所以我們將其添加到主域名下,這樣所有子域均可使用該 Cookie。下面視具體代碼:

import?Cookies?from?'js-cookie' const?via?=?new?URL(location.href).searchParams.get('via') if?(via)?{ ????Cookies.set('sitesauce_affiliate',?via,?{ ????????expires:?30, ????????domain:?'.sitesauce.app', ????????secure:?true, ????????sameSite:?'lax' ????}) }

這樣做的好處是當會員未通過此次分享注冊,而是事后自己注冊的時候,可以明確地知道該會員是通過那個上級的引薦而來的。我想更進一步,在新會員注冊的時候通過顯示一些標語以及上級者信息,從而使用戶明確知道這是來自會員(好友)的一個引薦鏈接,從而使注冊成功率更高,所以我添加了提示彈窗。效果如下:

想要實現上面效果的話,現在我們需要的不僅僅是上級標簽,還需要上級詳細信息,所以我們需要一個 API,該 API 會通過 via 匹配并提供上級詳細信息。

import?axios?from?'axios' import?Cookies?from?'js-cookie' const?via?=?new?URL(location.href).searchParams.get('via') if?(via)?{ ????axios.post(`https://app.sitesauce.app/api/affiliate/${encodeURIcomponent(this.via)}`).then(response?=>?{ ????????Cookies.set('sitesauce_affiliate',?response.data,?{?expires:?30,?domain:?'.sitesauce.app',?secure:?true,?sameSite:?'lax'?}) ????}).catch(error?=>?{ ????????if?(!error.response?||?error.response.status?!==?404)?return?console.log('Something?went?wrong') ????????console.log('Affiliate?does?not?exist.?Register?for?our?referral?program?here:?https://app.sitesauce.app/affiliate') ????}) }

在 URL 中你可以看到 encodeURIComponent,他的作用是保護我們不在受 Path Traversal vulnerability 的影響。 在我們發送請求到 /api/referral/:via,如果過有人惡意修改鏈接參數,像這樣: ?via=../../logout ,用戶在點擊之后可能會注銷,當然這可能沒有什么影響,但是不可避免得會有些其他的會帶來不可預期影響的操作。

由于 Sitesauce 在一些地方使用了 Alpine,所以我們在此基礎之上,將彈窗修改為 Alpine 組件,這樣有更好的擴展性。 這里要感謝下 Ryan ,在我的轉換無法正常工作時,給我提出了寶貴建議。

<div> ????<template> ????????<div> ????????????@@##@@ ????????????<p>Your?friend?<span></span>?has?invited?you?to?try?Sitesauce</p> ????????????<button>Start?your?trial</button> ????????</div> ????</template> </div> <script> import axios from &#39;axios&#39; import Cookies from &#39;js-cookie&#39; // 使用模板標簽 $nextTick ,進行代碼轉換,這里特別感謝下我的朋友 Ryan  window.component = () => ({     affiliate: null,     via: new URL(location.href).searchParams.get(&#39;via&#39;)     init() {         if (! this.via) return this.$nextTick(() => this.affiliate = Cookies.getJSON(&#39;sitesauce.affiliate&#39;))         axios.post(`https://app.sitesauce.app/api/affiliate/${encodeURIComponent(this.via)}`).then(response => {             this.$nextTick(() => this.affiliate = response.data)             Cookies.set(&#39;sitesauce.affiliate&#39;, response.data, {                 expires: 30, domain: &#39;.sitesauce.app&#39;, secure: true, sameSite: &#39;lax&#39;             })         }).catch(error => {             if (!error.response || error.response.status !== 404) return console.log(&#39;Something went wrong&#39;)             console.log(&#39;Affiliate does not exist. Register for our referral program here: https://app.sitesauce.app/affiliate&#39;)         })     } }) </script>

現在,完善 API,使其可以獲取有效數據。 除此之外,我們還需要新增幾個個字段到我們現有的數據庫,這個我們將在之后說明。下面是遷移文件:

class?AddAffiliateColumnsToUsersTable?extends?Migration { ????/** ?????*?遷移執行 ?????* ?????*?@return?void ?????*/ ????public?function?up() ????{ ????????Schema::table('users',?function?(Blueprint?$table)?{ ????????????$table-&gt;string('affiliate_tag')-&gt;nullable(); ????????????$table-&gt;string('referred_by')-&gt;nullable(); ????????????$table-&gt;string('paypal_email')-&gt;nullable(); ????????????$table-&gt;timestamp('cashed_out_at')-&gt;nullable(); ????????}); ????} ????/** ?????*?Reverse?the?migrations. ?????* ?????*?@return?void ?????*/ ????public?function?down() ????{ ????????Schema::table('users',?function?(Blueprint?$table)?{ ????????????$table-&gt;dropColumn('affiliate_tag',?'referred_by',?'paypal_email',?'cashed_out_at'); ????????}); ????} }

通過路由自定義鍵名功能實現參數綁定 (可在 Laravel 7.X 上使用),構建我們的 API 路由。

Route::post('api/affiliate/{user:affiliate_tag}',?function?(User?$user)?{ ????return?$user-&gt;only('id',?'name',?'avatar',?'affiliate_tag'); })-&gt;middleware('throttle:30,1');

在開始注冊操作之前,首先讀取我們的 Cookie,由于未進行加密,所以我們需要將其加入到 EncryptCookies 的 except 字段中,將其排除。

//?中間件:app/Http/Middleware/EncryptCookies.php use?IlluminateCookieMiddlewareEncryptCookies?as?Middleware; class?EncryptCookies?extends?Middleware { ????/** ????*?The?names?of?the?cookies?that?should?not?be?encrypted ????* ????*?@var?array ????*/ ????protected?$except?=?[ ????????'sitesauce_referral', ????]; }

現在通過 RegisterController 的 authenticated 方法執行注冊相應的邏輯,其間,我們通過上面的方式獲取 Cooke,并通過該 Cooke 找到相應的上級,最終實現將下級與上級關聯。

/** ?*?上級用戶已經注冊 ?* ?*?@param?IlluminateHttpRequest?$request ?*?@param?AppUser?$user ?*/ protected?function?registered(Request?$request,?User?$user) { ????if?(!?$request-&gt;hasCookie('sitesauce.referral'))?return; ????$referral?=?json_decode($request-&gt;cookie('sitesauce_referral'),?true)['affiliate_tag']; ????if?(!?User::where('affiliate_tag',?$referral)-&gt;exists())?return; ????$user-&gt;update([ ????????'referred_by'?=&gt;?$referral, ????]); }

我們還需要一個方法來獲取我的下級用戶;列表,通過 ORM 的 hasMany 方法很簡單的就實現了。

class?User?extends?Model { ????public?function?referred() ????{ ????????return?$this-&gt;hasMany(self::class,?'referred_by',?'affiliate_tag'); ????} }

現在,讓我們構建我們的注冊頁面,在用戶注冊時候,他們可以選擇喜好標簽,并需要他們提供 PayPal 電子郵件 以以便之后的提現操作。下面是效果預覽:

會員注冊后,我們還需要提供電子郵箱的變更,以及標簽變更的相關入口。在其變更標簽之后,我們需要級聯更新其下級用戶的的標簽,以確保兩者的統一。這涉及到了數據庫的事務操作,為保證操作的原子性,我們需要在事務中完成以上兩個操作。代碼如下:

public?function?update(Request?$request) ????{ ????????$request-&gt;validate([ ????????????'affiliate_tag'?=&gt;?['required',?'string',?'min:3',?'max:255',?Rule::unique('users')-&gt;ignoreModel($request-&gt;user())], ????????????'paypal_email'?=&gt;?['required',?'string',?'max:255',?'email'], ????????]); ????????DB::transaction(function?()?use?($request)?{ ????????????if?($request-&gt;input('affiliate_tag')?!=?$request-&gt;user()-&gt;affiliate_tag)?{ ????????????????User::where('referred_by',?$request-&gt;user()-&gt;affiliate_tag) ????????????????????-&gt;update(['referred_by'?=&gt;?$request-&gt;input('affiliate_tag')]); ????????????} ????????????$request-&gt;user()-&gt;update([ ????????????????'affiliate_tag'?=&gt;?$request-&gt;input('affiliate_tag'), ????????????????'paypal_email'?=&gt;?$request-&gt;input('paypal_email'), ????????????]); ????????}); ????????return?redirect()-&gt;route('affiliate'); ????}

下面我們需要確定會員收益的計算方式,可以通過下級用戶的所有消費金額的百分比作為傭金返給上級,為了計算方便,我們僅僅計算自上次結款日期之后的數據。我們使用擴展 Mattias’ percentages package 使計算簡單明了。

use?MattiasgeniarPercentagePercentage; const?COMMISSION_PERCENTAGE?=?20; public?function?getReferralBalance()?:?int { ????return?Percentage::of(static::COMISSION_PERCENTAGE, ????????$this-&gt;referred ????????????-&gt;map(fn?(User?$user)?=&gt;?$user-&gt;invoices(false,?['created'?=&gt;?['gte'?=&gt;?optional($this-&gt;cashed_out_at)-&gt;timestamp]])) ????????????-&gt;flatten() ????????????-&gt;map(fn?(StripeInvoice?$invoice)?=&gt;?$invoice-&gt;subtotal) ????????????-&gt;sum() ????); }

最后我們以月結的方式向上級發放傭金,并更新 cashed_out_at 字段為本次傭金發放時間。效果如下:

至此,希望以上文檔對你有幫助。祝你快樂每天。

推薦教程:《Laravel教程

基于 Laravel 開發會員分銷系統

? 版權聲明
THE END
喜歡就支持一下吧
點贊9 分享