package user import ( "context" "fmt" "strconv" "strings" "time" "github.com/perfect-panel/server/internal/config" "github.com/perfect-panel/server/internal/model/user" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/constant" "github.com/perfect-panel/server/pkg/logger" "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "gorm.io/gorm" ) type DeleteAccountLogic struct { logger.Logger ctx context.Context svcCtx *svc.ServiceContext } // NewDeleteAccountLogic 创建注销账号逻辑实例 func NewDeleteAccountLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteAccountLogic { return &DeleteAccountLogic{ Logger: logger.WithContext(ctx), ctx: ctx, svcCtx: svcCtx, } } // DeleteAccount 保留兼容入口,统一走全量注销登录逻辑 func (l *DeleteAccountLogic) DeleteAccount() (resp *types.DeleteAccountResponse, err error) { return l.DeleteAccountAll() } // DeleteAccountAll 注销登录 + 退出家庭/解散家庭,不删除账号主体 func (l *DeleteAccountLogic) DeleteAccountAll() (resp *types.DeleteAccountResponse, err error) { currentUser, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) if !ok || currentUser == nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") } // 事务前:查找家庭关系,收集所有成员 ID allMemberIDs := []int64{currentUser.Id} var familyId int64 if relation, relErr := l.findActiveFamilyRelation(currentUser.Id); relErr == nil { familyId = relation.FamilyId var memberIDs []int64 if pluckErr := l.svcCtx.DB.WithContext(l.ctx). Model(&user.UserFamilyMember{}). Where("family_id = ? AND status = ? AND deleted_at IS NULL", familyId, user.FamilyMemberActive). Pluck("user_id", &memberIDs).Error; pluckErr == nil { idSet := map[int64]struct{}{currentUser.Id: {}} for _, id := range memberIDs { if id > 0 { if _, exists := idSet[id]; !exists { idSet[id] = struct{}{} allMemberIDs = append(allMemberIDs, id) } } } } } // 事务前:收集所有成员的 AuthMethods(事务后用于精确清缓存) allAuthMethods := make([]*user.AuthMethods, 0) for _, memberID := range allMemberIDs { if ams, amErr := l.svcCtx.UserModel.FindUserAuthMethods(l.ctx, memberID); amErr == nil { allAuthMethods = append(allAuthMethods, ams...) } } // 事务内:解散家庭 + 删除所有成员的 AuthMethods + Subscribe err = l.svcCtx.UserModel.Transaction(l.ctx, func(tx *gorm.DB) error { // 无论 member 还是 owner,都解散整个家庭 if familyId > 0 { now := time.Now() if txErr := tx.Model(&user.UserFamilyMember{}). Where("family_id = ? AND status = ?", familyId, user.FamilyMemberActive). Updates(map[string]interface{}{ "status": user.FamilyMemberRemoved, "left_at": now, }).Error; txErr != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "remove all family members failed") } if txErr := tx.Model(&user.UserFamily{}). Where("id = ?", familyId). Updates(map[string]interface{}{ "status": familyStatusDisabled, }).Error; txErr != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "disable family failed") } } // 删除所有成员的 AuthMethods 和 Subscribe for _, memberID := range allMemberIDs { if txErr := tx.Where("user_id = ?", memberID).Delete(&user.AuthMethods{}).Error; txErr != nil { l.Errorw("delete auth methods failed", logger.Field("user_id", memberID), logger.Field("error", txErr.Error())) } if txErr := tx.Where("user_id = ?", memberID).Delete(&user.Subscribe{}).Error; txErr != nil { l.Errorw("delete subscribes failed", logger.Field("user_id", memberID), logger.Field("error", txErr.Error())) } } return nil }) if err != nil { return nil, err } // 事务后:清除所有成员的 Session + 踢设备 for _, memberID := range allMemberIDs { l.clearAllSessions(memberID) var memberDevices []user.Device l.svcCtx.DB.WithContext(l.ctx). Model(&user.Device{}). Where("user_id = ?", memberID). Find(&memberDevices) for _, d := range memberDevices { l.svcCtx.DeviceManager.KickDevice(d.UserId, d.Identifier) } } // 清除 auth method 相关缓存(email key 等) if len(allAuthMethods) > 0 { var authCacheKeys []string for _, am := range allAuthMethods { if am.AuthType == "email" && am.AuthIdentifier != "" { authCacheKeys = append(authCacheKeys, fmt.Sprintf("cache:user:email:%s", am.AuthIdentifier)) } } if len(authCacheKeys) > 0 { if delErr := l.svcCtx.Redis.Del(l.ctx, authCacheKeys...).Err(); delErr != nil { l.Errorw("clear auth method cache failed", logger.Field("user_id", currentUser.Id), logger.Field("error", delErr.Error()), ) } } } // 清除所有成员的 user + subscribe 缓存 if cacheErr := l.clearUserAndSubscribeCaches(allMemberIDs); cacheErr != nil { l.Errorw("clear user related cache failed", logger.Field("user_id", currentUser.Id), logger.Field("affected_user_ids", allMemberIDs), logger.Field("error", cacheErr.Error()), ) } return &types.DeleteAccountResponse{ Success: true, Message: "注销成功", UserId: currentUser.Id, Code: 200, }, nil } func (l *DeleteAccountLogic) collectAffectedFamilyUserIDs(tx *gorm.DB, userID int64) ([]int64, error) { affected := []int64{userID} var relation struct { FamilyId int64 `gorm:"column:family_id"` } err := tx.Model(&user.UserFamilyMember{}). Select("user_family_member.family_id"). Joins("JOIN user_family ON user_family.id = user_family_member.family_id AND user_family.deleted_at IS NULL AND user_family.status = ?", user.FamilyStatusActive). Where("user_family_member.user_id = ? AND user_family_member.status = ? AND user_family_member.deleted_at IS NULL", userID, user.FamilyMemberActive). First(&relation).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return affected, nil } return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query family relation failed") } var memberIDs []int64 if err = tx.Model(&user.UserFamilyMember{}). Where("family_id = ? AND status = ? AND deleted_at IS NULL", relation.FamilyId, user.FamilyMemberActive). Pluck("user_id", &memberIDs).Error; err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query family members failed") } affected = append(affected, memberIDs...) idSet := make(map[int64]struct{}, len(affected)) unique := make([]int64, 0, len(affected)) for _, id := range affected { if id <= 0 { continue } if _, exists := idSet[id]; exists { continue } idSet[id] = struct{}{} unique = append(unique, id) } if len(unique) == 0 { return []int64{userID}, nil } return unique, nil } func (l *DeleteAccountLogic) clearUserAndSubscribeCaches(userIDs []int64) error { if len(userIDs) == 0 { return nil } idSet := make(map[int64]struct{}, len(userIDs)) uniqueIDs := make([]int64, 0, len(userIDs)) for _, userID := range userIDs { if userID <= 0 { continue } if _, exists := idSet[userID]; exists { continue } idSet[userID] = struct{}{} uniqueIDs = append(uniqueIDs, userID) } if len(uniqueIDs) == 0 { return nil } userModels := make([]*user.User, 0, len(uniqueIDs)) subscribeModels := make([]*user.Subscribe, 0, len(uniqueIDs)) for _, userID := range uniqueIDs { u, findErr := l.svcCtx.UserModel.FindOne(l.ctx, userID) switch { case findErr == nil: userModels = append(userModels, u) case errors.Is(findErr, gorm.ErrRecordNotFound): // no-op default: l.Errorw("find user for cache clearing failed", logger.Field("user_id", userID), logger.Field("error", findErr.Error()), ) } subscribeModels = append(subscribeModels, &user.Subscribe{UserId: userID}) subscribes, queryErr := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, userID) if queryErr != nil { l.Errorw("query user subscribes for cache clearing failed", logger.Field("user_id", userID), logger.Field("error", queryErr.Error()), ) continue } for _, subscribe := range subscribes { subscribeModels = append(subscribeModels, &user.Subscribe{ Id: subscribe.Id, UserId: subscribe.UserId, SubscribeId: subscribe.SubscribeId, Token: subscribe.Token, }) } } if len(userModels) > 0 { if err := l.svcCtx.UserModel.ClearUserCache(l.ctx, userModels...); err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "clear user cache failed") } } if len(subscribeModels) > 0 { if err := l.svcCtx.UserModel.ClearSubscribeCache(l.ctx, subscribeModels...); err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "clear subscribe cache failed") } } return nil } // clearAllSessions 清理指定用户的所有会话 func (l *DeleteAccountLogic) clearAllSessions(userId int64) { sessionsKey := fmt.Sprintf("%s%v", config.UserSessionsKeyPrefix, userId) sessionSet := make(map[string]struct{}) sessions, err := l.svcCtx.Redis.ZRange(l.ctx, sessionsKey, 0, -1).Result() if err != nil { l.Errorw("获取用户会话索引失败", logger.Field("user_id", userId), logger.Field("error", err.Error())) } else { for _, sessionID := range sessions { if sessionID != "" { sessionSet[sessionID] = 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 } } pipe := l.svcCtx.Redis.TxPipeline() for sessionID := range sessionSet { sessionKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionID) pipe.Del(l.ctx, sessionKey) 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("[SessionMonitor] 注销账号-清除所有Session", logger.Field("user_id", userId), logger.Field("count", len(sessionSet)), ) } // findActiveFamilyRelation 查找用户的活跃家庭成员记录 func (l *DeleteAccountLogic) findActiveFamilyRelation(userID int64) (*user.UserFamilyMember, error) { var relation user.UserFamilyMember err := l.svcCtx.DB.WithContext(l.ctx). Model(&user.UserFamilyMember{}). Where("user_id = ? AND status = ?", userID, user.FamilyMemberActive). First(&relation).Error if err != nil { return nil, err } return &relation, nil }