51 lines
1.4 KiB
Go
51 lines
1.4 KiB
Go
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
|
||
}()
|
||
}
|