feat(session): 实现同平台抢登互踢功能,优化会话踢下线逻辑

新增同平台会话踢下线能力,将iOS/Android/mobile归为同一移动分组,web仅匹配web;扩展会话存储接口支持按平台、带原因踢除会话,同步更新集群广播逻辑与各调用点,统一传递踢下线原因参数
This commit is contained in:
Austin
2026-09-16 10:23:27 +08:00
parent dbb0a06db0
commit 4280b535c2
6 changed files with 124 additions and 9 deletions
+50 -2
View File
@@ -19,6 +19,7 @@ const (
realtimeTargetTopic = "topic"
realtimeTargetUser = "user"
realtimeTargetDevice = "device"
realtimeTargetPlatf = "platform"
)
func startRedisRealtimeFanout(ctx context.Context) {
@@ -105,6 +106,45 @@ func publishRealtimeEvictDevice(tenantID types.TenantID, uid types.Uid, deviceID
immetrics.IncRealtimeFanout("publish", realtimeKindSessionControl, realtimeTargetDevice, "ok")
}
// publishRealtimeEvictUserPlatform evicts all sessions of the user on the given
// platform across cluster nodes. skipSid (the new login session) is part of the
// dedup key because repeated logins on the same platform within the dedup window
// must each reach the subscriber nodes.
func publishRealtimeEvictUserPlatform(tenantID types.TenantID, uid types.Uid, platf, reason, skipSid string) {
if !tenantID.IsValid() || uid.IsZero() || strings.TrimSpace(platf) == "" {
immetrics.IncRealtimeFanout("publish", realtimeKindSessionControl, realtimeTargetPlatf, "skip_invalid")
return
}
msg := NoErrEvicted("", "", types.TimeNow())
msg.TenantID = tenantID
msg.AsUser = uid.UserId()
if reason != "" {
msg.Ctrl.Params = map[string]any{"reason": reason}
}
target := uid.UserId() + ":" + strings.TrimSpace(platf)
dedupKey := "evict:platform:" + target + ":" + skipSid
if err := store.PublishRealtimeFanout(currentNodeID(), tenantID, realtimeKindSessionControl, realtimeTargetPlatf, target, skipSid, dedupKey, msg); err != nil {
immetrics.IncRealtimeFanout("publish", realtimeKindSessionControl, realtimeTargetPlatf, "error")
logs.Warn.Printf("redis realtime fanout: publish evict platform=%s failed: %v", target, err)
return
}
immetrics.IncRealtimeFanout("publish", realtimeKindSessionControl, realtimeTargetPlatf, "ok")
}
// fanoutEvictReason extracts the machine-readable eviction reason from a
// session-control fanout message, if present.
func fanoutEvictReason(msg *ServerComMessage) string {
if msg == nil || msg.Ctrl == nil {
return ""
}
params, ok := msg.Ctrl.Params.(map[string]any)
if !ok {
return ""
}
reason, _ := params["reason"].(string)
return reason
}
func deliverRealtimeFanoutEnvelope(envelope *store.RealtimeFanoutEnvelope) {
if envelope == nil || !envelope.TenantID.IsValid() || len(envelope.Message) == 0 {
immetrics.IncRealtimeFanout("receive", realtimeKindMessage, realtimeTargetTopic, "skip_invalid")
@@ -195,7 +235,15 @@ func deliverRealtimeSessionControl(envelope *store.RealtimeFanoutEnvelope, msg *
immetrics.IncRealtimeFanout("deliver", envelope.Kind, envelope.TargetType, "skip_invalid_target")
return
}
globals.sessionStore.EvictUserDevice(types.ParseUserId(uidText), deviceID)
globals.sessionStore.EvictUserDevice(types.ParseUserId(uidText), deviceID, fanoutEvictReason(msg))
immetrics.IncRealtimeFanout("deliver", envelope.Kind, envelope.TargetType, "evicted")
case realtimeTargetPlatf:
uidText, platf, ok := strings.Cut(envelope.Target, ":")
if !ok {
immetrics.IncRealtimeFanout("deliver", envelope.Kind, envelope.TargetType, "skip_invalid_target")
return
}
globals.sessionStore.EvictUserPlatform(types.ParseUserId(uidText), platf, envelope.SkipSid, fanoutEvictReason(msg))
immetrics.IncRealtimeFanout("deliver", envelope.Kind, envelope.TargetType, "evicted")
case realtimeTargetUser:
uid := types.ParseUserId(envelope.Target)
@@ -203,7 +251,7 @@ func deliverRealtimeSessionControl(envelope *store.RealtimeFanoutEnvelope, msg *
immetrics.IncRealtimeFanout("deliver", envelope.Kind, envelope.TargetType, "skip_invalid_target")
return
}
globals.sessionStore.EvictUser(uid, envelope.SkipSid)
globals.sessionStore.EvictUser(uid, envelope.SkipSid, fanoutEvictReason(msg))
immetrics.IncRealtimeFanout("deliver", envelope.Kind, envelope.TargetType, "evicted")
}
_ = msg