Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -2,11 +2,7 @@ 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"
|
||||
@@ -50,84 +46,11 @@ func (l *KickOfflineByUserDeviceLogic) KickOfflineByUserDevice(req *types.KickOf
|
||||
|
||||
// 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 {
|
||||
if err := clearAllSessions(l.ctx, l.svcCtx, userId); err != nil {
|
||||
l.Errorw("清理会话缓存失败",
|
||||
logger.Field("user_id", userId),
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
l.Infow("[KickOffline] 管理员踢设备-清除所有Session",
|
||||
logger.Field("user_id", userId),
|
||||
logger.Field("count", len(sessionSet)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/perfect-panel/server/internal/config"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
)
|
||||
|
||||
func clearAllSessions(ctx context.Context, svcCtx *svc.ServiceContext, userID int64) error {
|
||||
userIDText := strconv.FormatInt(userID, 10)
|
||||
sessionSet := make(map[string]struct{})
|
||||
|
||||
pattern := fmt.Sprintf("%s:*", config.SessionIdKey)
|
||||
var cursor uint64
|
||||
for {
|
||||
keys, nextCursor, err := svcCtx.Redis.Scan(ctx, cursor, pattern, 200).Result()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, sessionKey := range keys {
|
||||
value, err := svcCtx.Redis.Get(ctx, sessionKey).Result()
|
||||
if err != 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
|
||||
}
|
||||
}
|
||||
|
||||
if len(sessionSet) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
deviceKeySet := make(map[string]struct{})
|
||||
devicePattern := fmt.Sprintf("%s:*", config.DeviceCacheKeyKey)
|
||||
cursor = 0
|
||||
for {
|
||||
keys, nextCursor, err := svcCtx.Redis.Scan(ctx, cursor, devicePattern, 200).Result()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, deviceKey := range keys {
|
||||
sessionID, err := svcCtx.Redis.Get(ctx, deviceKey).Result()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if _, exists := sessionSet[sessionID]; exists {
|
||||
deviceKeySet[deviceKey] = struct{}{}
|
||||
}
|
||||
}
|
||||
cursor = nextCursor
|
||||
if cursor == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
sessionsKey := fmt.Sprintf("%s%v", config.UserSessionsKeyPrefix, userID)
|
||||
pipe := svcCtx.Redis.TxPipeline()
|
||||
for sessionID := range sessionSet {
|
||||
pipe.Del(ctx, fmt.Sprintf("%v:%v", config.SessionIdKey, sessionID))
|
||||
pipe.Del(ctx, fmt.Sprintf("%s:detail:%s", config.SessionIdKey, sessionID))
|
||||
pipe.ZRem(ctx, sessionsKey, sessionID)
|
||||
}
|
||||
pipe.Del(ctx, sessionsKey)
|
||||
for deviceKey := range deviceKeySet {
|
||||
pipe.Del(ctx, deviceKey)
|
||||
}
|
||||
_, err := pipe.Exec(ctx)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
"github.com/perfect-panel/server/internal/config"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
func TestClearAllSessions(t *testing.T) {
|
||||
redisServer, err := miniredis.Run()
|
||||
if err != nil {
|
||||
t.Fatalf("miniredis.Run() error = %v", err)
|
||||
}
|
||||
defer redisServer.Close()
|
||||
|
||||
rdb := redis.NewClient(&redis.Options{Addr: redisServer.Addr()})
|
||||
defer rdb.Close()
|
||||
|
||||
svcCtx := &svc.ServiceContext{Redis: rdb}
|
||||
ctx := context.Background()
|
||||
|
||||
userID := int64(42)
|
||||
sessionID := "session-a"
|
||||
otherSessionID := "session-b"
|
||||
userSessionKey := config.SessionIdKey + ":" + sessionID
|
||||
userDetailKey := config.SessionIdKey + ":detail:" + sessionID
|
||||
otherSessionKey := config.SessionIdKey + ":" + otherSessionID
|
||||
userSessionsZSet := config.UserSessionsKeyPrefix + "42"
|
||||
deviceKey := config.DeviceCacheKeyKey + ":device-1"
|
||||
unrelatedDeviceKey := config.DeviceCacheKeyKey + ":device-2"
|
||||
|
||||
setString(t, redisServer, userSessionKey, "42")
|
||||
setString(t, redisServer, userDetailKey, "detail")
|
||||
setString(t, redisServer, otherSessionKey, "99")
|
||||
setString(t, redisServer, deviceKey, sessionID)
|
||||
setString(t, redisServer, unrelatedDeviceKey, otherSessionID)
|
||||
if _, err := redisServer.ZAdd(userSessionsZSet, 1, sessionID); err != nil {
|
||||
t.Fatalf("seed session zset: %v", err)
|
||||
}
|
||||
|
||||
if err := clearAllSessions(ctx, svcCtx, userID); err != nil {
|
||||
t.Fatalf("clearAllSessions() error = %v", err)
|
||||
}
|
||||
|
||||
assertMissing(t, redisServer, userSessionKey)
|
||||
assertMissing(t, redisServer, userDetailKey)
|
||||
assertMissing(t, redisServer, deviceKey)
|
||||
assertMissing(t, redisServer, userSessionsZSet)
|
||||
|
||||
if !redisServer.Exists(otherSessionKey) {
|
||||
t.Fatalf("unrelated session %q should remain", otherSessionKey)
|
||||
}
|
||||
if !redisServer.Exists(unrelatedDeviceKey) {
|
||||
t.Fatalf("unrelated device mapping %q should remain", unrelatedDeviceKey)
|
||||
}
|
||||
}
|
||||
|
||||
func setString(t *testing.T, server *miniredis.Miniredis, key, value string) {
|
||||
t.Helper()
|
||||
if err := server.Set(key, value); err != nil {
|
||||
t.Fatalf("set %q: %v", key, err)
|
||||
}
|
||||
}
|
||||
|
||||
func assertMissing(t *testing.T, server *miniredis.Miniredis, key string) {
|
||||
t.Helper()
|
||||
if server.Exists(key) {
|
||||
t.Fatalf("expected key %q to be removed", key)
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,14 @@ func (l *UpdateUserBasicInfoLogic) UpdateUserBasicInfo(req *types.UpdateUserBasi
|
||||
if req.Avatar != "" && !tool.IsValidImageSize(req.Avatar, 1024) {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Invalid Image Size")
|
||||
}
|
||||
if req.Enable != nil && !*req.Enable {
|
||||
if userInfo.IsAdmin != nil && *userInfo.IsAdmin {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "admin user cannot be disabled")
|
||||
}
|
||||
if userInfo.Id == 2 {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "demo user cannot be disabled")
|
||||
}
|
||||
}
|
||||
|
||||
err = l.svcCtx.UserModel.Transaction(l.ctx, func(tx *gorm.DB) error {
|
||||
if req.Balance != nil && userInfo.Balance != *req.Balance {
|
||||
@@ -176,6 +184,19 @@ func (l *UpdateUserBasicInfoLogic) UpdateUserBasicInfo(req *types.UpdateUserBasi
|
||||
}
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "Update User Error")
|
||||
}
|
||||
if req.Enable != nil {
|
||||
if cacheErr := logicCommon.InvalidateUserEnableCache(l.ctx, l.svcCtx, userInfo.Id); cacheErr != nil {
|
||||
l.Errorw("[UpdateUserBasicInfoLogic] clear enable cache failed", logger.Field("err", cacheErr.Error()), logger.Field("userId", req.UserId))
|
||||
}
|
||||
if !*req.Enable {
|
||||
if sessionErr := clearAllSessions(l.ctx, l.svcCtx, userInfo.Id); sessionErr != nil {
|
||||
l.Errorw("[UpdateUserBasicInfoLogic] clear sessions failed", logger.Field("err", sessionErr.Error()), logger.Field("userId", req.UserId))
|
||||
}
|
||||
for _, device := range userInfo.UserDevices {
|
||||
l.svcCtx.DeviceManager.KickDevice(userInfo.Id, device.Identifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user