package service import ( "sync" "time" ) // touchInterval 进程内与 SQL 侧一致的限频窗口:每用户最多真正写库一次。 const touchInterval = time.Minute // touchState 单用户心跳限频状态(进程内)。 type touchState struct { mu sync.Mutex last time.Time } // lastSeenTouch 按 userID 缓存上次尝试写库时间,避免热路径反复发 0 行 UPDATE。 var lastSeenTouch sync.Map // uint -> *touchState // TouchLastSeen 更新用户最近活跃时间(在线统计)。 // // 双层限频: // 1. 进程内:60s 窗口内直接返回,不发 SQL(消掉 HTTP 并行 + WS ping 的风暴); // 2. SQL WHERE:跨进程 / 重启后仍保证全局约 1 次/分钟真正写入。 // // mutex 只保护「要不要写」的判定,不横跨 SQL;判定通过后乐观记下 last, // 再异步 Exec,避免卡住 WS readPump。写库失败时 last_seen 最多偏旧 60s, // 远小于 5 分钟在线窗口。 func (s *AuthService) TouchLastSeen(userID uint) { v, _ := lastSeenTouch.LoadOrStore(userID, &touchState{}) st := v.(*touchState) st.mu.Lock() now := time.Now() if !st.last.IsZero() && now.Sub(st.last) < touchInterval { st.mu.Unlock() return } st.last = now st.mu.Unlock() threshold := now.Add(-touchInterval) go func() { _ = s.db.Exec( `UPDATE users SET last_seen_at = NOW() WHERE id = ? AND (last_seen_at IS NULL OR last_seen_at < ?)`, userID, threshold, ).Error }() }