Skip to content

Commit

Permalink
Keep track of user last visited time
Browse files Browse the repository at this point in the history
  • Loading branch information
summerblue committed Nov 1, 2017
1 parent 1b4fdd8 commit 79e6773
Show file tree
Hide file tree
Showing 10 changed files with 206 additions and 21 deletions.
18 changes: 18 additions & 0 deletions app/Console/Commands/SyncUserActivedAt.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use App\Models\User;

class SyncUserActivedAt extends Command
{
protected $signature = 'larabbs:sync-user-actived-at';
protected $description = '将用户最后登录时间从 Redis 同步到数据库中';

public function handle(User $user)
{
$user->syncUserActivedAt();
$this->info("同步成功!");
}
}
3 changes: 3 additions & 0 deletions app/Console/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ protected function schedule(Schedule $schedule)

// 一小时执行一次『活跃用户』数据生成的命令
$schedule->command('larabbs:calculate-active-user')->hourly();

// 每日零时执行一次
$schedule->command('larabbs:calculate-active-user')->dailyAt('00:00');
}

/**
Expand Down
1 change: 1 addition & 0 deletions app/Http/Controllers/TopicsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Auth;
use App\Handlers\ImageUploadHandler;
use App\Models\User;
use App\Models\Link;

class TopicsController extends Controller
{
Expand Down
74 changes: 53 additions & 21 deletions app/Http/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,56 +6,88 @@

class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* @var array
*/
// 全局中间件,最先调用
protected $middleware = [

// 检测是否应用是否进入『维护模式』
// 见:https://d.laravel-china.org/docs/5.5/configuration#maintenance-mode
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,

// 检测请求的数据是否过大
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,

// 对提交的请求参数进行 PHP 函数 `trim()` 处理
\App\Http\Middleware\TrimStrings::class,

// 将提交请求参数中空子串转换为 null
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,

// 修正代理服务器后的服务器参数
\App\Http\Middleware\TrustProxies::class,
];

/**
* The application's route middleware groups.
*
* @var array
*/
// 定义中间件组
protected $middlewareGroups = [

// Web 中间件组,应用于 routes/web.php 路由文件
'web' => [
// Cookie 加密解密
\App\Http\Middleware\EncryptCookies::class,

// 将 Cookie 添加到响应中
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,

// 开启会话
\Illuminate\Session\Middleware\StartSession::class,
// \Illuminate\Session\Middleware\AuthenticateSession::class,

// 认证用户,此中间件以后 Auth 类才能生效
// 见:https://d.laravel-china.org/docs/5.5/authentication
\Illuminate\Session\Middleware\AuthenticateSession::class,

// 将系统的错误数据注入到视图变量 $errors 中
\Illuminate\View\Middleware\ShareErrorsFromSession::class,

// 检验 CSRF ,防止跨站请求伪造的安全威胁
// 见:https://d.laravel-china.org/docs/5.5/csrf
\App\Http\Middleware\VerifyCsrfToken::class,

// 处理路由绑定
// 见:https://d.laravel-china.org/docs/5.5/routing#route-model-binding
\Illuminate\Routing\Middleware\SubstituteBindings::class,

// 记录用户最后活跃时间
\App\Http\Middleware\RecordLastActivedTime::class,
],

// API 中间件组,应用于 routes/api.php 路由文件
'api' => [
// 使用别名来调用中间件
// 请见:https://d.laravel-china.org/docs/5.5/middleware#为路由分配中间件
'throttle:60,1',
'bindings',
],
];

/**
* The application's route middleware.
*
* These middleware may be assigned to groups or used individually.
*
* @var array
*/
// 中间件别名设置,允许你使用别名调用中间件,例如上面的 api 中间件组调用
protected $routeMiddleware = [

// 只有登录用户才能访问,我们在控制器的构造方法中大量使用
'auth' => \Illuminate\Auth\Middleware\Authenticate::class,

// HTTP Basic Auth 认证
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,

// 处理路由绑定
// 见:https://d.laravel-china.org/docs/5.5/routing#route-model-binding
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,

// 用户授权功能
'can' => \Illuminate\Auth\Middleware\Authorize::class,

// 只有游客才能访问,在 register 和 login 请求中使用,只有未登录用户才能访问这些页面
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,

// 访问节流,类似于 『1 分钟只能请求 10 次』的需求,一般在 API 中使用
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
];
}
}
20 changes: 20 additions & 0 deletions app/Http/Middleware/RecordLastActivedTime.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace App\Http\Middleware;

use Closure;
use Auth;

class RecordLastActivedTime
{
public function handle($request, Closure $next)
{
// 如果是登录用户的话
if (Auth::check()) {
// 记录最后登录时间
Auth::user()->recordLastActivedAt();
}

return $next($request);
}
}
84 changes: 84 additions & 0 deletions app/Models/Traits/LastActivedAtHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

namespace App\Models\Traits;

use Redis;
use Carbon\Carbon;

trait LastActivedAtHelper
{
// 缓存相关
protected $hash_prefix = 'larabbs_last_actived_at_';
protected $field_prefix = 'user_';

public function recordLastActivedAt()
{
// 获取今日 Redis 哈希表名称,如:larabbs_last_actived_at_2017-10-21
$hash = $this->getHashFromDateString(Carbon::now()->toDateString());

// 字段名称,如:user_1
$field = $this->getHashField();

// 当前时间,如:2017-10-21 08:35:15
$now = Carbon::now()->toDateTimeString();

// 数据写入 Redis ,字段已存在会被更新
Redis::hSet($hash, $field, $now);
}

public function syncUserActivedAt()
{
// 获取昨日的哈希表名称,如:larabbs_last_actived_at_2017-10-21
$hash = $this->getHashFromDateString(Carbon::now()->subDay()->toDateString());

// 从 Redis 中获取所有哈希表里的数据
$dates = Redis::hGetAll($hash);

// 遍历,并同步到数据库中
foreach ($dates as $user_id => $actived_at) {
// 会将 `user_1` 转换为 1
$user_id = str_replace($this->field_prefix, '', $user_id);

// 只有当用户存在时才更新到数据库中
if ($user = $this->find($user_id)) {
$user->last_actived_at = $actived_at;
$user->save();
}
}

// 以数据库为中心的存储,既已同步,即可删除
Redis::del($hash);
}

public function getLastActivedAtAttribute($value)
{
// 获取今日对应的哈希表名称
$hash = $this->getHashFromDateString(Carbon::now()->toDateString());

// 字段名称,如:user_1
$field = $this->getHashField();

// 三元运算符,优先选择 Redis 的数据,否则使用数据库中
$datetime = Redis::hGet($hash, $field) ? : $value;

// 如果存在的话,返回时间对应的 Carbon 实体
if ($datetime) {
return new Carbon($datetime);
} else {
// 否则使用用户注册时间
return $this->created_at;
}
}

public function getHashFromDateString($date)
{
// Redis 哈希表的命名,如:larabbs_last_actived_at_2017-10-21
return $this->hash_prefix . $date;
}

public function getHashField()
{
// 字段名称,如:user_1
return $this->field_prefix . $this->id;
}
}
1 change: 1 addition & 0 deletions app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

class User extends Authenticatable
{
use Traits\LastActivedAtHelper;
use Traits\ActiveUserHelper;
use HasRoles;
use Notifiable {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

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

class AddLastActivedAtToUsersTable extends Migration
{
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->timestamp('last_actived_at')->nullable();
});
}

public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('last_actived_at');
});
}
}
3 changes: 3 additions & 0 deletions resources/views/users/show.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
<hr>
<h4><strong>注册于</strong></h4>
<p>{{ $user->created_at->diffForHumans() }}</p>
<hr>
<h4><strong>最后活跃</strong></h4>
<p title="{{ $user->last_actived_at }}">{{ $user->last_actived_at->diffForHumans() }}</p>
</div>
</div>
</div>
Expand Down
1 change: 1 addition & 0 deletions storage/administrator_settings/site.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"site_name":"ss - Powered by LaraBBS","contact_email":"[email protected]","seo_description":"sdf","seo_keyword":null}

0 comments on commit 79e6773

Please sign in to comment.