[ Laravel 从入门到精通 ] 用户认证与授权系列 —— 通过监听注册登录、邮箱验证事件实现简单的积分功能

如果你的网站有积分功能,往往会给新注册激活的用户一定的初始积分,每日签到/登录积分奖励等,在 Laravel 框架驱动的应用中,我们可以通过监听相应的事件来设置用户积分,下面我们就来简单演示其实现过程,并以此为例介绍用户注册登录事件监听及处理。

用户注册、登录和邮箱验证事件类 Laravel 框架底层已经提供,我们只需要通过事件监听器或订阅者来监听处理这些事件即可。

注:关于事件类的定义、监听和订阅,可以查看事件文档了解明细。

由于我们之前在模型事件教程中已经创建一个用户事件订阅者类 UserEventSubscriber,这里,我们在其基础上编写用户注册登录相关事件及其处理。

为用户表新增积分字段

要实现积分功能,首先需要新增一个数据库迁移类为用户表新增一个积分字段:

 php artisan make:migration alter_users_add_point --table=users

编写对应的迁移文件代码如下:

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class AlterUsersAddPoint extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->integer('point')->unsigned()->default(0)->after('password');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn('point');
        });
    }
}

登录到数据库所在环境,运行 php artisan migrate 让上述迁移生效。这样,users 表中就有 point 字段了:

新增用户积分日志表

此外,我们还要创建一张积分日志表,用于记录用户积分变更明细,通过如下命令创建积分日志模型类及对应迁移类:

php artisan make:model PointLog -m

该命令会创建一个 PointLog 模型类及对应数据库迁移文件,编写迁移文件代码如下:

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreatePointLogsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('point_logs', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('user_id')->unsigned();
            $table->tinyInteger('type')->unsigned()->comment('操作类型');
            $table->smallInteger('value')->comment('积分变动值');
            $table->timestamps();
            $table->index('user_id');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('point_logs');
    }
}

登录到数据库所在环境,运行 php artisan migrate 让上述迁移生效。这样,就会在数据库中新增 point_logs 数据表。

然后在初始化 PointLog 模型类代码如下:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class PointLog extends Model
{
    const OPT_USER_REGISTER = 1;  // 用户注册
    const OPT_EMAIL_VERIFY = 2;   // 邮箱验证
    const OPT_USER_LOGIN = 3;     // 用户登录

    // 不同操作对应积分映射关系
    public static $OPT_POINT = [
        self::OPT_USER_REGISTER => 30,
        self::OPT_EMAIL_VERIFY => 20,
        self::OPT_USER_LOGIN => 5
    ];
}

在该模型类中,我们主要设置了一些常量属性用于标识不同的操作类型,并定义了不同操作类型映射的积分变动值。对于本例而言,业务逻辑比较简单,所以通过一个静态属性数组来映射这个关系,对于复杂应用,映射值很多,这样做就不合适了,可以通过新增一张表来存储不同操作对应积分值,也方便管理。

定义模型类之间的关联关系

最后,我们还要在 User 模型类中通过如下方法定义其与 PointLog 之间的一对多关联:

public function pointLogs()
{
    return $this->hasMany(PointLog::class);
}

新用户注册

准备好数据结构之后,接下来,我们来处理事件监听。

用户注册对应的事件类是 Registered,对应事件类如下:

<?php

namespace Illuminate\Auth\Events;

use Illuminate\Queue\SerializesModels;

class Registered
{
    use SerializesModels;

    /**
     * The authenticated user.
     *
     * @var \Illuminate\Contracts\Auth\Authenticatable
     */
    public $user;

    /**
     * Create a new event instance.
     *
     * @param  \Illuminate\Contracts\Auth\Authenticatable  $user
     * @return void
     */
    public function __construct($user)
    {
        $this->user = $user;
    }
}

该事件会在用户注册成功后触发,打开 UserEventSubscriber.php,在 subscribe 添加用户注册事件及对应的处理函数:

// 在文件顶部引入事件类完整命名空间
use Illuminate\Auth\Events\Registered;

$events->listen(
    Registered::class,
    UserEventSubscriber::class . '@onUserRegistered'
);

然后编写用户注册事件处理函数 onUserRegistered

// 在文件定义引入 PointLog
use App\PointLog;

/**
 * 处理用户注册成功事件
 * @param $event
 */
public function onUserRegistered($event) {
    // 用户注册成功后初始积分为30
    $event->user->point += PointLog::$OPT_POINT[PointLog::OPT_USER_REGISTER];
    $event->user->save();
    // 保存积分变更日志
    $pointLog = new PointLog();
    $pointLog->type = PointLog::OPT_USER_REGISTER;
    $pointLog->value = PointLog::$OPT_POINT[PointLog::OPT_USER_REGISTER];
    $event->user->pointLogs()->save($pointLog);
}

这里,我们在用户注册成功后将其积分字段值初始化为注册奖励积分,并通过关联模型插入方式记录积分变更日志。

新用户邮箱验证

如果开启了邮箱验证功能,需要等到用户验证邮箱后,该用户才算真正激活,可以正常登录。验证邮箱事件框架底层也提供了,对应事件类是 Verified,实现代码如下:

<?php

namespace Illuminate\Auth\Events;

use Illuminate\Queue\SerializesModels;

class Verified
{
    use SerializesModels;

    /**
     * The verified user.
     *
     * @var \Illuminate\Contracts\Auth\MustVerifyEmail
     */
    public $user;

    /**
     * Create a new event instance.
     *
     * @param  \Illuminate\Contracts\Auth\MustVerifyEmail  $user
     * @return void
     */
    public function __construct($user)
    {
        $this->user = $user;
    }
}

该事件会在用户验证邮箱后触发,我们在订阅者 UserEventSubscribersubscribe 方法中新增对该事件的订阅:

// 在文件顶部引入事件类完整命名空间
use Illuminate\Auth\Events\Verified;

$events->listen(
    Verified::class,
    UserEventSubscriber::class . '@onEmailVerified'
);

然后编写 onEmailVerified 方法来处理用户验证邮箱事件:

/**
 * 处理验证邮箱事件
 * @param $event
 */
public function onEmailVerified($event) {
    // 用户验证邮箱后增加20积分
    $event->user->point += PointLog::$OPT_POINT[PointLog::OPT_EMAIL_VERIFY];
    $event->user->save();
    // 保存积分变更日志
    $pointLog = new PointLog();
    $pointLog->type = PointLog::OPT_EMAIL_VERIFY;
    $pointLog->value = PointLog::$OPT_POINT[PointLog::OPT_EMAIL_VERIFY];
    $event->user->pointLogs()->save($pointLog);
}

验证邮箱后,为该用户新增 20 积分值,并记录积分变更日志。

用户每日登录

最后,我们来看用户登录事件的处理。用户登录事件类 Login 定义如下:

<?php

namespace Illuminate\Auth\Events;

use Illuminate\Queue\SerializesModels;

class Login
{
    use SerializesModels;

    /**
     * The authentication guard name.
     *
     * @var string
     */
    public $guard;

    /**
     * The authenticated user.
     *
     * @var \Illuminate\Contracts\Auth\Authenticatable
     */
    public $user;

    /**
     * Indicates if the user should be "remembered".
     *
     * @var bool
     */
    public $remember;

    /**
     * Create a new event instance.
     *
     * @param  string $guard
     * @param  \Illuminate\Contracts\Auth\Authenticatable  $user
     * @param  bool  $remember
     * @return void
     */
    public function __construct($guard, $user, $remember)
    {
        $this->user = $user;
        $this->guard = $guard;
        $this->remember = $remember;
    }
}

该事件会在用户登录成功后触发,同样,我们在订阅者 UserEventSubscribersubscribe 方法中新增对该事件的订阅:

// 在文件顶部引入事件类完整命名空间
use Illuminate\Auth\Events\Login; 

$events->listen(
    Login::class,
    UserEventSubscriber::class . '@onUserLogin'
);

然后编写事件处理函数 onUserLogin

// 在文件顶部引入 Carbon
use Carbon\Carbon;

/**
 * 处理用户登录事件
 * @param $event
 */
public function onUserLogin($event) {
    $pointLog = PointLog::where('user_id', $event->user->id)->where('type', PointLog::OPT_USER_LOGIN)->orderBy('created_at', 'desc')->first();
    $firstLoginToday = false;
    if (!$pointLog) {
        // 注册后首次登录
        $firstLoginToday = true;
    } else {
        $lastLoginTime = new Carbon($pointLog->created_at);
        if ($lastLoginTime->isYesterday()) {
            // 上次登录时间是昨天
            $firstLoginToday = true;
        }
    }
    if ($firstLoginToday) {
        // 用户每日首次登录成功后增加5积分
        $event->user->point += PointLog::$OPT_POINT[PointLog::OPT_USER_LOGIN];
        $event->user->save();
        // 保存积分变更日志
        $pointLog = new PointLog();
        $pointLog->type = PointLog::OPT_USER_LOGIN;
        $pointLog->value = PointLog::$OPT_POINT[PointLog::OPT_USER_LOGIN];
        $event->user->pointLogs()->save($pointLog);
    }
}

该处理函数相对复杂一点,因为我们只在每日首次登录才会对用户进行积分奖励,这个包含两种情况的判断,一种是用户首次登录,一种是上次登录时间是昨天,只有在这两种场景下,才会更新用户积分,同时记录积分变更日志。

测试用户登录注册流程积分变更

接下来,我们来演示一个新用户从注册到登录过程中的积分变更,并以此验证相应事件处理函数是否监听到对应事件并处理成功。

在浏览器访问新用户注册页面 http://blog.test/register,填写注册表单:

点击注册按钮提交表单进行注册,页面跳转到邮箱验证页面:

说明注册成功。我们去查看 users 表和 point_logs 新增记录:

users 表中 point 字段值是 35,point_logs 中有两条记录,一条表示注册,一条表示登录,这意味着虽然邮箱未验证,但用户已经登录成功,只是未验证邮箱被中间件拦截,不能访问认证页面罢了。

下面我们来验证邮箱,登录到注册邮箱,在收件箱点开收到的验证邮件:

点击验证邮箱按钮进行验证,验证成功后会跳转到如下页面:

表示邮箱已验证,用户已激活,这个时候再到数据库查看 users 表和 point_logs 表记录:

会看到用户积分已变更,对应积分记录已生成,正是验证邮箱事件对应处理函数实现的。

为了验证用户每日登录只奖励一次积分,我们退出登录状态重新登录,如果用户积分没有变动,则表示代码是没有问题的。

好了,这篇教程我们以用户注册、登录、验证邮箱为例,演示了如何通过事件订阅者监听这些事件并编写事件处理函数实现简单的积分奖励功能,你可以以此为基础实现应用的完整积分系统,或者监听处理其它用户认证事件,比如登录失败、用户退出、密码重置等。

学院君 has written 1243 articles

Laravel学院院长,终身学习者

积分:167652 等级:P12 职业:手艺人 城市:杭州

1 条回复

  1. Fantasy Fantasy says:

    院长,我有个地方是太明白,就是UserEventSubscriber监听的是全局的事件,去 UserEventSubscriber里的subscribe匹配对应的吗?不太明白一个事件怎么触发的UserEventSubscriber这个订阅者的

登录后才能进行评论,立即登录?