首页 php框架 Laravel 基于 Laravel 开发会员分销系统

基于 Laravel 开发会员分销系统

May 28, 2020 am 10:16 AM
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 x-data="{ ...component() } x-cloak x-init="init()">
    <template x-if="affiliate">
        <div>
            <img :src="affiliate.avatar" class="h-8 w-8 rounded-full mr-2">
            <p>Your friend <span x-text="affiliate.name"></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(&#39;users&#39;, function (Blueprint $table) {
            $table->string(&#39;affiliate_tag&#39;)->nullable();
            $table->string(&#39;referred_by&#39;)->nullable();
            $table->string(&#39;paypal_email&#39;)->nullable();
            $table->timestamp(&#39;cashed_out_at&#39;)->nullable();
        });
    }
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table(&#39;users&#39;, function (Blueprint $table) {
            $table->dropColumn(&#39;affiliate_tag&#39;, &#39;referred_by&#39;, &#39;paypal_email&#39;, &#39;cashed_out_at&#39;);
        });
    }
}
登录后复制

通过路由自定义键名功能实现参数绑定 (可在 Laravel 7.X 上使用),构建我们的 API 路由。

Route::post(&#39;api/affiliate/{user:affiliate_tag}&#39;, function (User $user) {
    return $user->only(&#39;id&#39;, &#39;name&#39;, &#39;avatar&#39;, &#39;affiliate_tag&#39;);
})->middleware(&#39;throttle:30,1&#39;);
登录后复制

在开始注册操作之前,首先读取我们的 Cookie,由于未进行加密,所以我们需要将其加入到 EncryptCookies 的 except 字段中,将其排除。

// 中间件:app/Http/Middleware/EncryptCookies.php
use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
class EncryptCookies extends Middleware
{
    /**
    * The names of the cookies that should not be encrypted
    *
    * @var array
    */
    protected $except = [
        &#39;sitesauce_referral&#39;,
    ];
}
登录后复制

现在通过 RegisterController 的 authenticated 方法执行注册相应的逻辑,其间,我们通过上面的方式获取 Cooke,并通过该 Cooke 找到相应的上级,最终实现将下级与上级关联。

/**
 * 上级用户已经注册
 *
 * @param \Illuminate\Http\Request $request
 * @param \App\User $user
 */
protected function registered(Request $request, User $user)
{
    if (! $request->hasCookie(&#39;sitesauce.referral&#39;)) return;
    $referral = json_decode($request->cookie(&#39;sitesauce_referral&#39;), true)[&#39;affiliate_tag&#39;];
    if (! User::where(&#39;affiliate_tag&#39;, $referral)->exists()) return;
    $user->update([
        &#39;referred_by&#39; => $referral,
    ]);
}
登录后复制

我们还需要一个方法来获取我的下级用户;列表,通过 ORM 的 hasMany 方法很简单的就实现了。

class User extends Model
{
    public function referred()
    {
        return $this->hasMany(self::class, &#39;referred_by&#39;, &#39;affiliate_tag&#39;);
    }
}
登录后复制

现在,让我们构建我们的注册页面,在用户注册时候,他们可以选择喜好标签,并需要他们提供 PayPal 电子邮件 以以便之后的提现操作。下面是效果预览:

会员注册后,我们还需要提供电子邮箱的变更,以及标签变更的相关入口。在其变更标签之后,我们需要级联更新其下级用户的的标签,以确保两者的统一。这涉及到了数据库的事务操作,为保证操作的原子性,我们需要在事务中完成以上两个操作。代码如下:

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

下面我们需要确定会员收益的计算方式,可以通过下级用户的所有消费金额的百分比作为佣金返给上级,为了计算方便,我们仅仅计算自上次结款日期之后的数据。我们使用扩展 Mattias’ percentages package 使计算简单明了。

use Mattiasgeniar\Percentage\Percentage;
const COMMISSION_PERCENTAGE = 20;
public function getReferralBalance() : int
{
    return Percentage::of(static::COMISSION_PERCENTAGE,
        $this->referred
            ->map(fn (User $user) => $user->invoices(false, [&#39;created&#39; => [&#39;gte&#39; => optional($this->cashed_out_at)->timestamp]]))
            ->flatten()
            ->map(fn (\Stripe\Invoice $invoice) => $invoice->subtotal)
            ->sum()
    );
}
登录后复制

最后我们以月结的方式向上级发放佣金,并更新 cashed_out_at 字段为本次佣金发放时间。效果如下:

至此,希望以上文档对你有帮助。祝你快乐每天。

推荐教程:《Laravel教程

以上是基于 Laravel 开发会员分销系统的详细内容。更多信息请关注PHP中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

在Laravel中如何获取邮件发送失败时的退信代码? 在Laravel中如何获取邮件发送失败时的退信代码? Apr 01, 2025 pm 02:45 PM

Laravel邮件发送失败时的退信代码获取方法在使用Laravel开发应用时,经常会遇到需要发送验证码的情况。而在实�...

Laravel计划任务不执行:schedule:run命令后任务未运行怎么办? Laravel计划任务不执行:schedule:run命令后任务未运行怎么办? Mar 31, 2025 pm 11:24 PM

Laravel计划任务运行无响应排查在使用Laravel的计划任务调度时,不少开发者会遇到这样的问题:schedule:run...

在 Laravel 中,如何处理邮件发送验证码失败的情况? 在 Laravel 中,如何处理邮件发送验证码失败的情况? Mar 31, 2025 pm 11:48 PM

Laravel邮件发送验证码失败时的处理方法在使用Laravel...

在dcat admin中如何实现点击添加数据的自定义表格功能? 在dcat admin中如何实现点击添加数据的自定义表格功能? Apr 01, 2025 am 07:09 AM

在dcatadmin(laravel-admin)中如何实现自定义点击添加数据的表格功能在使用dcat...

Laravel - 转储服务器 Laravel - 转储服务器 Aug 27, 2024 am 10:51 AM

Laravel - 转储服务器 - Laravel 转储服务器随 Laravel 5.7 版本一起提供。以前的版本不包括任何转储服务器。转储服务器将成为 laravel/laravel Composer 文件中的开发依赖项。

Laravel Redis连接共享:为何select方法会影响其他连接? Laravel Redis连接共享:为何select方法会影响其他连接? Apr 01, 2025 am 07:45 AM

Laravel框架中Redis连接的共享与select方法的影响在使用Laravel框架和Redis时,开发者可能会遇到一个问题:通过配置...

Laravel多租户扩展stancl/tenancy:如何自定义租户数据库连接的主机地址? Laravel多租户扩展stancl/tenancy:如何自定义租户数据库连接的主机地址? Apr 01, 2025 am 09:09 AM

在Laravel多租户扩展包stancl/tenancy中自定义租户数据库连接使用Laravel多租户扩展包stancl/tenancy构建多租户应用时,...

Laravel - 操作 URL Laravel - 操作 URL Aug 27, 2024 am 10:51 AM

Laravel - Action URL - Laravel 5.7 引入了一项名为“可调用操作 URL”的新功能。此功能类似于 Laravel 5.6 中的功能,即在操作方法中接受字符串。 Laravel 5.7 引入新语法的主要目的是直接

See all articles