hi-server/internal/logic/admin/user/kickOfflineByUserDeviceLogic.go
shanshanzhong e4ec85c176
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 7m37s
fix: clearAllSessions 改用 SCAN 查找 session,修复会话清理无效
根因:登录时只写了 auth:session_id:{sessionId} (Redis SET),
从未写入 auth:user_sessions:{userId} sorted set,
导致 clearAllSessions 用 ZRange 永远返回空,session 根本没被清除。

修复:改用 SCAN auth:session_id:* 遍历所有 session key,
按 value 匹配 userId 找出该用户的全部 session 后删除,
同时清理关联的 device cache key。

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-12 02:47:32 -07:00

134 lines
3.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package user
import (
"context"
"fmt"
"strconv"
"strings"
"github.com/perfect-panel/server/internal/config"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/xerr"
"github.com/pkg/errors"
)
type KickOfflineByUserDeviceLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// kick offline user device
func NewKickOfflineByUserDeviceLogic(ctx context.Context, svcCtx *svc.ServiceContext) *KickOfflineByUserDeviceLogic {
return &KickOfflineByUserDeviceLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *KickOfflineByUserDeviceLogic) KickOfflineByUserDevice(req *types.KickOfflineRequest) error {
device, err := l.svcCtx.UserModel.FindOneDevice(l.ctx, req.Id)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get Device error: %v", err.Error())
}
l.svcCtx.DeviceManager.KickDevice(device.UserId, device.Identifier)
device.Online = false
err = l.svcCtx.UserModel.UpdateDevice(l.ctx, device)
if err != nil {
l.Logger.Error("[KickOfflineByUserDeviceLogic] Update Device Error:", logger.Field("err", err.Error()))
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update Device error: %v", err.Error())
}
// 清除该用户的所有会话,确保旧 token 失效
l.clearAllSessions(device.UserId)
return nil
}
// clearAllSessions 清除指定用户的所有会话(通过 SCAN 查找,不依赖 sorted set
func (l *KickOfflineByUserDeviceLogic) clearAllSessions(userId int64) {
sessionSet := make(map[string]struct{})
userIDText := strconv.FormatInt(userId, 10)
pattern := fmt.Sprintf("%s:*", config.SessionIdKey)
var cursor uint64
for {
keys, nextCursor, scanErr := l.svcCtx.Redis.Scan(l.ctx, cursor, pattern, 200).Result()
if scanErr != nil {
l.Errorw("扫描会话键失败", logger.Field("user_id", userId), logger.Field("error", scanErr.Error()))
break
}
for _, sessionKey := range keys {
value, getErr := l.svcCtx.Redis.Get(l.ctx, sessionKey).Result()
if getErr != nil || value != userIDText {
continue
}
sessionID := strings.TrimPrefix(sessionKey, config.SessionIdKey+":")
if sessionID == "" || strings.HasPrefix(sessionID, "detail:") {
continue
}
sessionSet[sessionID] = struct{}{}
}
cursor = nextCursor
if cursor == 0 {
break
}
}
deviceKeySet := make(map[string]struct{})
devicePattern := fmt.Sprintf("%s:*", config.DeviceCacheKeyKey)
cursor = 0
for {
keys, nextCursor, scanErr := l.svcCtx.Redis.Scan(l.ctx, cursor, devicePattern, 200).Result()
if scanErr != nil {
l.Errorw("扫描设备会话映射失败", logger.Field("user_id", userId), logger.Field("error", scanErr.Error()))
break
}
for _, deviceKey := range keys {
sessionID, getErr := l.svcCtx.Redis.Get(l.ctx, deviceKey).Result()
if getErr != nil {
continue
}
if _, exists := sessionSet[sessionID]; exists {
deviceKeySet[deviceKey] = struct{}{}
}
}
cursor = nextCursor
if cursor == 0 {
break
}
}
if len(sessionSet) == 0 {
return
}
sessionsKey := fmt.Sprintf("%s%v", config.UserSessionsKeyPrefix, userId)
pipe := l.svcCtx.Redis.TxPipeline()
for sessionID := range sessionSet {
pipe.Del(l.ctx, fmt.Sprintf("%v:%v", config.SessionIdKey, sessionID))
pipe.Del(l.ctx, fmt.Sprintf("%s:detail:%s", config.SessionIdKey, sessionID))
pipe.ZRem(l.ctx, sessionsKey, sessionID)
}
pipe.Del(l.ctx, sessionsKey)
for deviceKey := range deviceKeySet {
pipe.Del(l.ctx, deviceKey)
}
if _, err := pipe.Exec(l.ctx); err != nil {
l.Errorw("清理会话缓存失败",
logger.Field("user_id", userId),
logger.Field("error", err.Error()),
)
}
l.Infow("[KickOffline] 管理员踢设备-清除所有Session",
logger.Field("user_id", userId),
logger.Field("count", len(sessionSet)),
)
}