From 011a6101132e5282f25c1a7be319f5ffaffde2df Mon Sep 17 00:00:00 2001 From: freefire Date: Wed, 16 Sep 2026 19:33:35 +0800 Subject: [PATCH] =?UTF-8?q?fix(presence):=20=E8=BF=9B=E7=A8=8B=E5=86=85?= =?UTF-8?q?=E9=99=90=E9=A2=91=20last=5Fseen=20=E5=BF=83=E8=B7=B3=EF=BC=8C?= =?UTF-8?q?=E9=81=BF=E5=85=8D=E7=83=AD=E8=B7=AF=E5=BE=84=20UPDATE=20?= =?UTF-8?q?=E9=A3=8E=E6=9A=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- backend/service/presence.go | 53 ++++++++++++++++++++++++++++++------- 1 file changed, 44 insertions(+), 9 deletions(-) diff --git a/backend/service/presence.go b/backend/service/presence.go index 8a0175b..d1ff637 100644 --- a/backend/service/presence.go +++ b/backend/service/presence.go @@ -1,15 +1,50 @@ package service -import "time" +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 更新用户最近活跃时间(在线统计)。 -// SQL 条件限频:每用户每 60 秒最多真正写一次,其余为命中 0 行的空更新; -// 调用方以 goroutine 异步触发,不增加请求链路延迟。 +// +// 双层限频: +// 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) { - threshold := time.Now().Add(-time.Minute) - _ = 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 + 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 + }() }