diff --git a/app/Console/Commands/SyncUserActivedAt.php b/app/Console/Commands/SyncUserActivedAt.php new file mode 100644 index 0000000..a37499b --- /dev/null +++ b/app/Console/Commands/SyncUserActivedAt.php @@ -0,0 +1,44 @@ +syncUserActivedAt(); + $this->info('同步成功'); + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index f9e4410..70e3aad 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -27,6 +27,7 @@ class Kernel extends ConsoleKernel // $schedule->command('inspire') // ->hourly(); $schedule->command('larabbs:calculate-active-user')->hourly(); + $schedule->command('larabbs:sync-user-actived-at')->dailyAt('00:05'); } /** diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 93bf68b..6d4e6ad 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -2,6 +2,7 @@ namespace App\Http; +use App\Http\Middleware\RecordLastActivedTime; use Illuminate\Foundation\Http\Kernel as HttpKernel; class Kernel extends HttpKernel @@ -10,34 +11,50 @@ 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 = [ + // 检测是否应用是否进入『维护模式』 \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, + // 认证用户,此中间件以后 Auth 类才能生效 // \Illuminate\Session\Middleware\AuthenticateSession::class, + // 将系统的错误数据注入到视图变量 $errors 中 \Illuminate\View\Middleware\ShareErrorsFromSession::class, + // 检验 CSRF ,防止跨站请求伪造的安全威胁 \App\Http\Middleware\VerifyCsrfToken::class, + // 处理路由绑定 \Illuminate\Routing\Middleware\SubstituteBindings::class, + RecordLastActivedTime::class, ], - + // API 中间件组,应用于 routes/api.php 路由文件 'api' => [ + // 使用别名来调用中间件 'throttle:60,1', 'bindings', ], @@ -45,17 +62,22 @@ class Kernel extends HttpKernel /** * The application's route middleware. - * + * 中间件别名设置,允许你使用别名调用中间件,例如上面的 api 中间件组调用 * These middleware may be assigned to groups or used individually. * * @var array */ protected $routeMiddleware = [ + //只有登录用户才能访问,我们在控制器的构造方法中大量使用 'auth' => \Illuminate\Auth\Middleware\Authenticate::class, 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, + // 处理路由绑定 '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, ]; } diff --git a/app/Http/Middleware/RecordLastActivedTime.php b/app/Http/Middleware/RecordLastActivedTime.php new file mode 100644 index 0000000..18d239a --- /dev/null +++ b/app/Http/Middleware/RecordLastActivedTime.php @@ -0,0 +1,25 @@ +recordLastActivedAt(); + } + return $response; + } +} diff --git a/app/Models/User.php b/app/Models/User.php index c6ef919..08dc6df 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -2,12 +2,18 @@ namespace App\Models; -use App\Models\Traits\ActiveUserHelper; +use App\Traits\ActiveUserHelper; +use App\Traits\LastActivedAtHelper; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Illuminate\Support\Facades\Auth; use Spatie\Permission\Traits\HasRoles; +/** + * Class User + * @property string avatar + * @package App\Models + */ class User extends Authenticatable { use Notifiable { @@ -15,6 +21,7 @@ class User extends Authenticatable } use HasRoles; use ActiveUserHelper; + use LastActivedAtHelper; /** * The attributes that are mass assignable. @@ -34,11 +41,23 @@ class User extends Authenticatable 'password', 'remember_token', ]; + /** + * @return string + */ public function getHeaderAttribute() { return asset($this->avatar); } + public function getLastActiveATAttribute($value) + { + return $this->getLastActivedAt($value); + } + + + /** + * @param $path + */ public function setAvatarAttribute($path) { if (!starts_with($path, 'http')) { @@ -47,21 +66,34 @@ class User extends Authenticatable $this->attributes['avatar'] = $path; } + /** + * @param $model + * @return bool + */ public function isAuthorOf($model) { return $this->id == $model->user_id; } + /** + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ public function replies() { return $this->hasMany(Reply::class, 'user_id'); } + /** + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ public function topics() { return $this->hasMany(Topic::class); } + /** + * @param $value + */ public function setPasswordAttribute($value) { if (!isset($value[59])) { @@ -70,6 +102,9 @@ class User extends Authenticatable $this->attributes['password'] = $value; } + /** + * @param $instance + */ public function notify($instance) { if ($this->id == Auth::id()) { diff --git a/app/Models/Traits/ActiveUserHelper.php b/app/Traits/ActiveUserHelper.php similarity index 99% rename from app/Models/Traits/ActiveUserHelper.php rename to app/Traits/ActiveUserHelper.php index 488e607..912f50a 100644 --- a/app/Models/Traits/ActiveUserHelper.php +++ b/app/Traits/ActiveUserHelper.php @@ -6,7 +6,7 @@ * Time: 17:16 */ -namespace App\Models\Traits; +namespace App\Traits; use App\Models\Reply; diff --git a/app/Traits/LastActivedAtHelper.php b/app/Traits/LastActivedAtHelper.php new file mode 100644 index 0000000..84d5ec1 --- /dev/null +++ b/app/Traits/LastActivedAtHelper.php @@ -0,0 +1,80 @@ +toDateString(); + + // Redis 哈希表的命名,如:larabbs_last_actived_at_2017-10-21 + $hash = $this->hash_prefix . $date; + + // 字段名称,如:user_1 + $field = $this->field_prefix . $this->id; + + // 当前时间,如:2017-10-21 08:35:15 + $now = Carbon::now()->toDateTimeString(); + + // 数据写入 Redis ,字段已存在会被更新 + Redis::hSet($hash, $field, $now); + } + + public function syncUserActivedAt() + { + // 获取昨天的日期,格式如:2017-10-21 + $yesterday_date = Carbon::yesterday()->toDateString(); + // Redis 哈希表的命名,如:larabbs_last_actived_at_2017-10-21 + $hash = $this->hash_prefix . $yesterday_date; + + // 从 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_active_at = $actived_at; + $user->save(); + } + } + + // 以数据库为中心的存储,既已同步,即可删除 + Redis::del($hash); + } + + public function getLastActivedAt($value) + { + $date = Carbon::now()->toDateString(); + + // Redis 哈希表的命名,如:larabbs_last_actived_at_2017-10-21 + $hash = $this->hash_prefix . $date; + $field = $this->field_prefix . $this->id; + $date = Redis::hGet($hash, $field) ?: $value; + return $date ? new Carbon($date) : $this->created_at; + } +} \ No newline at end of file diff --git a/database/migrations/2018_06_02_215326_add_references.php b/database/migrations/2018_06_02_215326_add_references.php new file mode 100644 index 0000000..6bc5cab --- /dev/null +++ b/database/migrations/2018_06_02_215326_add_references.php @@ -0,0 +1,41 @@ +foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + }); + + Schema::table('replies', function (Blueprint $table) { + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + $table->foreign('topic_id')->references('id')->on('topics')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('topics', function (Blueprint $table) { + $table->dropForeign(['user_id']); + }); + + Schema::table('replies', function (Blueprint $table) { + $table->dropForeign(['user_id', 'topic_id']); + }); + } +} diff --git a/database/migrations/2018_06_02_230607_add_last_actived_at_to_users_table.php b/database/migrations/2018_06_02_230607_add_last_actived_at_to_users_table.php new file mode 100644 index 0000000..5153717 --- /dev/null +++ b/database/migrations/2018_06_02_230607_add_last_actived_at_to_users_table.php @@ -0,0 +1,32 @@ +timestamp('last_active_at')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('last_active_at'); + }); + } +} diff --git a/resources/views/users/show.blade.php b/resources/views/users/show.blade.php index 7af82ab..555f707 100644 --- a/resources/views/users/show.blade.php +++ b/resources/views/users/show.blade.php @@ -19,6 +19,8 @@
{{ $user->created_at->diffForHumans() }}
+{{ $user->last_active_at->diffForHumans() }}