From d6437f043f5e1636b106115e8b22f0e044f2a41e Mon Sep 17 00:00:00 2001 From: shanshanzhong Date: Mon, 9 Mar 2026 01:12:38 -0700 Subject: [PATCH] =?UTF-8?q?fix:=20=E9=80=80=E5=87=BA=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E6=97=B6=E8=A7=A3=E7=BB=91=E9=82=AE=E7=AE=B1=E5=92=8C=E5=AE=B6?= =?UTF-8?q?=E5=BA=AD=E7=BB=84=EF=BC=8C=E6=B8=85=E9=99=A4=E6=89=80=E6=9C=89?= =?UTF-8?q?session?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - logoutUnbind 新增删除非 device 类型的 auth_methods(解绑邮箱) - 清除用户所有 session 而非仅当前 session - 事务前收集家庭成员 ID,事务后清理成员缓存 - 清理邮箱相关 Redis 缓存 --- .../logic/public/user/unbindDeviceLogic.go | 124 ++++++++++++++++-- 1 file changed, 112 insertions(+), 12 deletions(-) diff --git a/internal/logic/public/user/unbindDeviceLogic.go b/internal/logic/public/user/unbindDeviceLogic.go index b490ae8..94b1517 100644 --- a/internal/logic/public/user/unbindDeviceLogic.go +++ b/internal/logic/public/user/unbindDeviceLogic.go @@ -50,32 +50,132 @@ func (l *UnbindDeviceLogic) UnbindDevice(req *types.UnbindDeviceRequest) error { } func (l *UnbindDeviceLogic) logoutUnbind(userInfo *user.User, device *user.Device, currentSessionID string) error { + // 1. 事务前查出 AuthMethods,用于事务后清邮箱缓存 + authMethods, _ := l.svcCtx.UserModel.FindUserAuthMethods(l.ctx, userInfo.Id) + + // 2. 事务前收集受影响的家庭成员 ID(事务会改变家庭关系,之后查不到) + familyMemberIDs := l.collectFamilyMemberIDs(userInfo.Id) + + // 3. 事务:解绑家庭组 + 解绑非 device 的登录方式 err := l.svcCtx.DB.Transaction(func(tx *gorm.DB) error { + // 解绑家庭组(owner 解散整个家庭,member 退出家庭) exitHelper := newFamilyExitHelper(l.ctx, l.svcCtx) - return exitHelper.removeUserFromActiveFamily(tx, userInfo.Id, true) + if err := exitHelper.removeUserFromActiveFamily(tx, userInfo.Id, true); err != nil { + return err + } + + // 解绑邮箱等非 device 类型的 auth_methods(保留 device 绑定) + if err := tx.Where("user_id = ? AND auth_type != ?", userInfo.Id, "device"). + Delete(&user.AuthMethods{}).Error; err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete non-device auth methods failed") + } + + return nil }) if err != nil { return err } + // 4. Kick 设备 l.svcCtx.DeviceManager.KickDevice(device.UserId, device.Identifier) - if currentSessionID != "" { - sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, currentSessionID) - _ = l.svcCtx.Redis.Del(l.ctx, sessionIdCacheKey).Err() + // 5. 清除该用户所有 session(旧 token 全部失效) + l.clearAllSessions(userInfo.Id) - sessionsKey := fmt.Sprintf("%s%v", config.UserSessionsKeyPrefix, userInfo.Id) - _ = l.svcCtx.Redis.ZRem(l.ctx, sessionsKey, currentSessionID).Err() - } - - deviceCacheKey := fmt.Sprintf("%v:%v", config.DeviceCacheKeyKey, device.Identifier) - if sessionId, redisErr := l.svcCtx.Redis.Get(l.ctx, deviceCacheKey).Result(); redisErr == nil && sessionId == currentSessionID { - _ = l.svcCtx.Redis.Del(l.ctx, deviceCacheKey).Err() + // 6. 清理邮箱相关缓存 + for _, am := range authMethods { + if am.AuthType == "email" && am.AuthIdentifier != "" { + cacheKey := fmt.Sprintf("cache:user:email:%s", am.AuthIdentifier) + if delErr := l.svcCtx.Redis.Del(l.ctx, cacheKey).Err(); delErr != nil { + l.Errorw("clear email cache failed", + logger.Field("user_id", userInfo.Id), + logger.Field("email", am.AuthIdentifier), + logger.Field("error", delErr.Error()), + ) + } + } } + // 7. 清理当前用户缓存 if clearErr := l.svcCtx.UserModel.ClearUserCache(l.ctx, userInfo); clearErr != nil { - l.Errorw("clear user cache failed", logger.Field("user_id", userInfo.Id), logger.Field("error", clearErr.Error())) + l.Errorw("clear user cache failed", + logger.Field("user_id", userInfo.Id), + logger.Field("error", clearErr.Error()), + ) + } + + // 8. 清理受影响的家庭成员缓存(家庭解散后成员需感知变化) + for _, memberID := range familyMemberIDs { + if memberUser, findErr := l.svcCtx.UserModel.FindOne(l.ctx, memberID); findErr == nil { + if clearErr := l.svcCtx.UserModel.ClearUserCache(l.ctx, memberUser); clearErr != nil { + l.Errorw("clear family member cache failed", + logger.Field("member_id", memberID), + logger.Field("error", clearErr.Error()), + ) + } + } } return nil } + +// collectFamilyMemberIDs 收集当前用户所在家庭的其他成员 ID(需在事务前调用) +func (l *UnbindDeviceLogic) collectFamilyMemberIDs(userID int64) []int64 { + var result struct { + FamilyId int64 + } + err := l.svcCtx.DB.WithContext(l.ctx). + Model(&user.UserFamilyMember{}). + Select("family_id"). + Where("user_id = ? AND status = ?", userID, user.FamilyMemberActive). + First(&result).Error + if err != nil || result.FamilyId == 0 { + return nil + } + + var memberIDs []int64 + l.svcCtx.DB.WithContext(l.ctx). + Model(&user.UserFamilyMember{}). + Where("family_id = ? AND status = ? AND user_id != ?", result.FamilyId, user.FamilyMemberActive, userID). + Pluck("user_id", &memberIDs) + + return memberIDs +} + +// clearAllSessions 清除指定用户的所有会话 +func (l *UnbindDeviceLogic) clearAllSessions(userId int64) { + sessionsKey := fmt.Sprintf("%s%v", config.UserSessionsKeyPrefix, userId) + 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()), + ) + return + } + + if len(sessions) == 0 { + return + } + + pipe := l.svcCtx.Redis.TxPipeline() + for _, sessionID := range sessions { + if sessionID == "" { + continue + } + pipe.Del(l.ctx, fmt.Sprintf("%v:%v", config.SessionIdKey, sessionID)) + } + pipe.Del(l.ctx, sessionsKey) + + if _, err = pipe.Exec(l.ctx); err != nil { + l.Errorw("清理会话缓存失败", + logger.Field("user_id", userId), + logger.Field("error", err.Error()), + ) + } + + l.Infow("退出登录-清除所有Session", + logger.Field("user_id", userId), + logger.Field("count", len(sessions)), + ) +}