hi-server/internal/logic/public/user/unbindDeviceLogic.go
shanshanzhong 9c197442a6
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 7m45s
fix: 退出登录时删除用户订阅并清理订阅缓存
- 事务内删除用户所有订阅记录
- 事务后清理订阅缓存、套餐缓存、节点缓存
2026-03-09 01:31:04 -07:00

228 lines
7.2 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. 事务前查出用户订阅,用于事务后清订阅缓存
userSubscribes, _ := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, userInfo.Id)
// 4. 事务:解绑家庭组 + 解绑非 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")
}
// 删除该用户的所有订阅(解绑后不应保留原账号的订阅)
if err := tx.Where("user_id = ?", userInfo.Id).
Delete(&user.Subscribe{}).Error; err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "delete user subscribes 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. 清理订阅相关缓存
if len(userSubscribes) > 0 {
subscribeModels := make([]*user.Subscribe, 0, len(userSubscribes)+1)
subscribeModels = append(subscribeModels, &user.Subscribe{UserId: userInfo.Id})
subscribeIDSet := make(map[int64]struct{})
for _, sub := range userSubscribes {
subscribeModels = append(subscribeModels, &user.Subscribe{
Id: sub.Id,
UserId: sub.UserId,
SubscribeId: sub.SubscribeId,
Token: sub.Token,
})
if sub.SubscribeId > 0 {
subscribeIDSet[sub.SubscribeId] = struct{}{}
}
}
if clearErr := l.svcCtx.UserModel.ClearSubscribeCache(l.ctx, subscribeModels...); clearErr != nil {
l.Errorw("clear subscribe cache failed",
logger.Field("user_id", userInfo.Id),
logger.Field("error", clearErr.Error()),
)
}
for subscribeID := range subscribeIDSet {
if clearErr := l.svcCtx.SubscribeModel.ClearCache(l.ctx, subscribeID); clearErr != nil {
l.Errorw("clear subscribe plan cache failed",
logger.Field("subscribe_id", subscribeID),
logger.Field("error", clearErr.Error()),
)
}
}
if clearErr := l.svcCtx.NodeModel.ClearServerAllCache(l.ctx); clearErr != nil {
l.Errorw("clear server cache failed",
logger.Field("error", clearErr.Error()),
)
}
}
// 9. 清理受影响的家庭成员缓存(家庭解散后成员需感知变化)
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)),
)
}