用数据库users表的last_seen字段存最后活跃时间最可靠,登录后每次认证请求更新,配合可配置的在线阈值(如15分钟)查询,注意时区一致和中间件全覆盖。

怎么存最后活跃时间:别用 session,改用数据库字段
Session 本身不带时间戳,且可能被清理或跨设备失效,无法可靠反映用户“在线”。Laravel 默认的 last_activity 存在 users 表里最稳——只要用户登录后做任意一次认证态请求,就更新它。
- 加字段:
php artisan make:migration add_last_seen_to_users_table,在迁移里添加$table->timestamp('last_seen')->nullable() - 更新时机:在中间件里调用
auth()->user()?->update(['last_seen' => now()]),不要放在登录成功那一刻(那只能记一次) - 别用
touch():它只更新updated_at,和业务语义无关,后续查“在线”时容易混淆 - 注意并发:高并发下频繁更新
last_seen可能引发小量 DB 压力,但比轮询 session 存储强得多
怎么判断“在线”:15 分钟规则不是硬编码,要可配
“在线”本质是“最近 X 分钟内有过动作”,这个 X 必须抽成配置项,不能写死在模型或查询里。否则改个超时时间得翻好几处代码。
- 定义配置:
config('auth.online_threshold_minutes'),默认设为15 - 查询写法:
User::where('last_seen', '>=', now()->subMinutes(config('auth.online_threshold_minutes'))) - 避免用
Carbon::now()->subMinutes(15)直接写死:测试时难 mock,部署后改阈值要发版 - MySQL 的
TIMESTAMP字段对时区敏感,确保 DB 和 PHP 时区一致(推荐全设为 UTC)
怎么避免误判“离线”:请求没触发更新?检查中间件注册顺序
常见现象:用户明明在刷页面,last_seen 却停在几分钟前——大概率是中间件没覆盖到某些路由,比如 API 路由漏了认证中间件,或前端静态资源请求意外走了 auth 流程。
- 确认中间件组:
web中间件组必须包含auth和你自定义的更新逻辑中间件 - 别把更新逻辑塞进
Auth::user()的 getter 里:它不保证每次都被调用,且可能触发 N+1 - 排除干扰路径:给
api/*单独加一套逻辑(如用 token 最后刷新时间),别复用web的last_seen - 前端埋点辅助验证:在关键页面加载后发个轻量
POST /ping,专门更新last_seen,绕过中间件遗漏风险
Laravel Scout 或缓存要不要介入:不用,除非并发真上万
实时查 users 表几十行数据,MySQL 几乎无压力。加 Redis 缓存 online_user_ids 或引入 Scout,反而增加一致性维护成本。
- 简单场景直接查 DB:索引加在
last_seen字段上,SELECT id FROM users WHERE last_seen >= ?很快 - 缓存只适合“批量展示在线人数”这种弱一致性需求,比如首页显示“当前在线 247 人”,用
Cache::remember('online_count', 60, fn() => ...) - Scout 是为全文搜索设计的,拿它查时间范围属于杀鸡用牛刀,同步延迟还会导致状态滞后
- 真正卡点不在查询,而在高频更新:如果每秒几百次
UPDATE users SET last_seen = NOW() WHERE id = ?,才值得考虑队列异步化(但先压测,别预设瓶颈)
最常被忽略的是时区和中间件作用域——DB 时间和 PHP 时间差一小时,或者只在 dashboard/* 更新 last_seen,用户在首页逛半天也不会被标记为在线。


















