Next.js中统计用户总会话时长的最佳实现方案
存储用户总登录时长的实现方案
最佳实现方案(精度高、覆盖场景全)
这个方案通过前后端配合,精准记录用户的登录时段,覆盖主动登出、页面关闭/刷新、异常退出等所有场景:
1. 后端(Laravel Sanctum)准备
- 给用户表添加两个字段:
// 生成迁移文件 php artisan make:migration add_login_duration_fields_to_users_table // 迁移文件内容 public function up() { Schema::table('users', function (Blueprint $table) { $table->unsignedInteger('total_login_duration')->default(0); // 总时长,单位秒 $table->timestamp('last_login_start')->nullable(); // 上次登录开始时间 }); } - 登录接口改造:用户成功通过Sanctum验证后,记录登录开始时间
// 在登录逻辑中添加 auth()->user()->update([ 'last_login_start' => now(), ]); - 新增时长更新接口:接收前端传来的本次登录时长,累加到总时长
public function updateLoginDuration(Request $request) { $user = auth()->user(); $duration = $request->input('duration'); // 单位秒 if ($duration > 0) { $user->increment('total_login_duration', $duration); } // 重置登录开始时间 $user->update(['last_login_start' => null]); return response()->json(['success' => true]); } - 定时任务兜底:处理用户未主动触发登出的情况(比如直接关浏览器)
// 在App\Console\Kernel中添加定时任务 protected function schedule(Schedule $schedule) { $schedule->call(function () { User::whereNotNull('last_login_start')->each(function ($user) { $duration = now()->diffInSeconds($user->last_login_start); if ($duration > 0) { $user->increment('total_login_duration', $duration); } $user->update(['last_login_start' => null]); }); })->hourly(); // 每小时执行一次,可根据需求调整频率 }
2. 前端(Next.js + Next Auth)处理
- 登录成功时记录登录开始时间到本地存储
// 在Next Auth的signIn回调中添加 import { signIn } from 'next-auth/react'; const handleLogin = async () => { const result = await signIn('credentials', { // 你的登录参数 redirect: false, }); if (result.ok) { localStorage.setItem('loginStartTime', Date.now().toString()); } }; - 监听登出事件,计算时长并发送给后端
import { signOut } from 'next-auth/react'; const handleLogout = async () => { const loginStartTime = localStorage.getItem('loginStartTime'); if (loginStartTime) { const duration = Math.floor((Date.now() - parseInt(loginStartTime)) / 1000); if (duration > 0) { // 发送请求到Laravel接口 await fetch('/api/update-login-duration', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-XSRF-TOKEN': getCookie('XSRF-TOKEN'), // 处理Sanctum的CSRF }, body: JSON.stringify({ duration }), }); } localStorage.removeItem('loginStartTime'); } await signOut(); }; - 监听页面关闭/刷新事件,兜底发送时长数据
useEffect(() => { const handleBeforeUnload = async (e) => { const loginStartTime = localStorage.getItem('loginStartTime'); if (loginStartTime) { const duration = Math.floor((Date.now() - parseInt(loginStartTime)) / 1000); if (duration > 0) { // 使用navigator.sendBeacon确保页面关闭时请求能发送成功 const formData = new FormData(); formData.append('duration', duration); formData.append('_token', getCookie('XSRF-TOKEN')); navigator.sendBeacon('/api/update-login-duration', formData); } } }; window.addEventListener('beforeunload', handleBeforeUnload); return () => window.removeEventListener('beforeunload', handleBeforeUnload); }, []);
简便实现方案(快速落地、精度稍低)
如果对时长精度要求不高,比如只需要统计大致的活跃时长,可以用更简单的方式:
1. 后端准备
只需要给用户表添加total_login_duration字段(和上面的迁移一致),新增一个接收时长增量的接口即可。
2. 前端处理
- 每次页面加载或路由切换时,记录当前时间,和上次记录的时间对比,计算间隔并发送给后端
useEffect(() => { const lastActiveTime = localStorage.getItem('lastActiveTime'); const currentTime = Date.now(); if (lastActiveTime) { const duration = Math.floor((currentTime - parseInt(lastActiveTime)) / 1000); if (duration > 30) { // 过滤短时间切换,比如小于30秒的不统计 fetch('/api/update-login-duration', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-XSRF-TOKEN': getCookie('XSRF-TOKEN'), }, body: JSON.stringify({ duration }), }); } } localStorage.setItem('lastActiveTime', currentTime.toString()); }, [router.asPath]); // 监听路由变化 - 这种方式不需要精确记录登录登出,只要用户在站点内活跃,就会累加时长,适合快速实现。
内容的提问来源于stack exchange,提问作者A. Atal
相关产品推荐
相关产品推荐

