hi-server/internal/logic/public/user/unbindDeviceLogic.go
shanshanzhong d6437f043f
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 7m54s
fix: 退出登录时解绑邮箱和家庭组,清除所有session
- logoutUnbind 新增删除非 device 类型的 auth_methods(解绑邮箱)
- 清除用户所有 session 而非仅当前 session
- 事务前收集家庭成员 ID,事务后清理成员缓存
- 清理邮箱相关 Redis 缓存
2026-03-09 01:12:38 -07:00

182 lines
5.5 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"
"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 UnbindDeviceLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// Unbind Device
func NewUnbindDeviceLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UnbindDeviceLogic {
return &UnbindDeviceLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *UnbindDeviceLogic) UnbindDevice(req *types.UnbindDeviceRequest) error {
userInfo, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
if !ok {
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
}
device, err := l.svcCtx.UserModel.FindOneDevice(l.ctx, req.Id)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DeviceNotExist), "find device")
}
if device.UserId != userInfo.Id {
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "device not belong to user")
}
currentSessionID, _ := l.ctx.Value(constant.CtxKeySessionID).(string)
return l.logoutUnbind(userInfo, device, currentSessionID)
}
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)
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)
// 5. 清除该用户所有 session旧 token 全部失效)
l.clearAllSessions(userInfo.Id)
// 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()),
)
}
// 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)),
)
}