hi-server/internal/logic/public/user/unbindDeviceLogic.go
shanshanzhong a542a6df46
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 7m45s
fix: 修复 退出登录 auth问题
2026-03-09 09:04:47 -07:00

180 lines
5.6 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. 事务提交后立即清缓存(避免 KickDevice/clearAllSessions 触发重连时命中旧缓存)
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()),
)
}
}
}
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()),
)
}
// 5. Kick 设备(缓存已清,重连时 FindOne 会查到最新数据)
l.svcCtx.DeviceManager.KickDevice(device.UserId, device.Identifier)
// 6. 清除该用户所有 session旧 token 全部失效)
l.clearAllSessions(userInfo.Id)
// 7. 清理受影响的家庭成员缓存(家庭解散后成员需感知变化)
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)),
)
}