diff --git a/internal/logic/public/user/unbindDeviceLogic.go b/internal/logic/public/user/unbindDeviceLogic.go index a286193..f63efa6 100644 --- a/internal/logic/public/user/unbindDeviceLogic.go +++ b/internal/logic/public/user/unbindDeviceLogic.go @@ -3,6 +3,7 @@ package user import ( "context" "fmt" + "time" "github.com/perfect-panel/server/internal/config" "github.com/perfect-panel/server/internal/model/user" @@ -73,9 +74,85 @@ func (l *UnbindDeviceLogic) logoutUnbind(userInfo *user.User, device *user.Devic // 2. 事务前收集受影响的家庭成员 ID(事务会改变家庭关系,之后查不到) familyMemberIDs := l.collectFamilyMemberIDs(userInfo.Id) - // 3. 事务:解绑家庭组 + 解绑非 device 的登录方式 + // 3. 事务前查询家庭关系,判断是否需要转移(而非解散) + var newOwnerUserID int64 + var transferredSubscribes []user.Subscribe + var relation user.UserFamilyMember + relErr := l.svcCtx.DB.WithContext(l.ctx). + Model(&user.UserFamilyMember{}). + Where("user_id = ? AND status = ?", userInfo.Id, user.FamilyMemberActive). + First(&relation).Error + isOwnerWithMembers := relErr == nil && relation.Role == user.FamilyRoleOwner && len(familyMemberIDs) > 0 + + // 4. 事务:根据角色决定转移还是解散 err := l.svcCtx.DB.Transaction(func(tx *gorm.DB) error { - // 解绑家庭组(owner 解散整个家庭,member 退出家庭) + if isOwnerWithMembers { + // Owner 且有其他 active member → 转移而非解散 + newOwnerUserID = familyMemberIDs[0] + + // 4a. 转移订阅 user_subscribe.user_id → newOwner + tx.Model(&user.Subscribe{}). + Where("user_id = ?", userInfo.Id). + Find(&transferredSubscribes) + if len(transferredSubscribes) > 0 { + if err := tx.Model(&user.Subscribe{}). + Where("user_id = ?", userInfo.Id). + Update("user_id", newOwnerUserID).Error; err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "transfer subscribes failed") + } + } + + // 4b. 转移邮箱 auth_method → newOwner + if err := tx.Model(&user.AuthMethods{}). + Where("user_id = ? AND auth_type = ?", userInfo.Id, "email"). + Update("user_id", newOwnerUserID).Error; err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "transfer email auth_method failed") + } + + // 4c. 提升 member 为 owner + if err := tx.Model(&user.UserFamilyMember{}). + Where("family_id = ? AND user_id = ? AND status = ?", + relation.FamilyId, newOwnerUserID, user.FamilyMemberActive). + Update("role", user.FamilyRoleOwner).Error; err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "promote member to owner failed") + } + + // 4d. 更新 family 的 owner_user_id + if err := tx.Model(&user.UserFamily{}). + Where("id = ?", relation.FamilyId). + Update("owner_user_id", newOwnerUserID).Error; err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update family owner failed") + } + + // 4e. 将当前 owner 标记为已退出 + now := time.Now() + if err := tx.Model(&user.UserFamilyMember{}). + Where("id = ?", relation.Id). + Updates(map[string]interface{}{ + "status": user.FamilyMemberRemoved, + "left_at": now, + }).Error; err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "mark owner as removed failed") + } + + // 邮箱 auth_method 已转移,不需要再删除 + // 删除其他非 device、非 email 的 auth_methods(如有) + if err := tx.Where("user_id = ? AND auth_type NOT IN (?,?)", userInfo.Id, "device", "email"). + Delete(&user.AuthMethods{}).Error; err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete other auth methods failed") + } + + l.Infow("owner logout: transferred to new owner", + logger.Field("old_owner", userInfo.Id), + logger.Field("new_owner", newOwnerUserID), + logger.Field("family_id", relation.FamilyId), + logger.Field("subscriptions_transferred", len(transferredSubscribes)), + ) + + return nil + } + + // 没有其他成员 或 当前用户是 member → 正常解散/退出流程 exitHelper := newFamilyExitHelper(l.ctx, l.svcCtx) if err := exitHelper.removeUserFromActiveFamily(tx, userInfo.Id, true); err != nil { return err @@ -93,7 +170,8 @@ func (l *UnbindDeviceLogic) logoutUnbind(userInfo *user.User, device *user.Devic return err } - // 4. 事务提交后立即清缓存(避免 KickDevice/clearAllSessions 触发重连时命中旧缓存) + // 5. 事务提交后清缓存 + // 5a. 清旧 owner 的邮箱缓存(不管是否转移,都要清 email cache key) for _, am := range authMethods { if am.AuthType == "email" && am.AuthIdentifier != "" { cacheKey := fmt.Sprintf("cache:user:email:%s", am.AuthIdentifier) @@ -106,6 +184,7 @@ func (l *UnbindDeviceLogic) logoutUnbind(userInfo *user.User, device *user.Devic } } } + // 5b. 清旧 owner 的用户缓存 if clearErr := l.svcCtx.UserModel.ClearUserCache(l.ctx, userInfo); clearErr != nil { l.Errorw("clear user cache failed", logger.Field("user_id", userInfo.Id), @@ -113,13 +192,23 @@ func (l *UnbindDeviceLogic) logoutUnbind(userInfo *user.User, device *user.Devic ) } - // 5. Kick 设备(缓存已清,重连时 FindOne 会查到最新数据) + // 5c. 如果有转移,清新 owner 的缓存 + 订阅缓存 + if newOwnerUserID > 0 { + if newOwnerUser, findErr := l.svcCtx.UserModel.FindOne(l.ctx, newOwnerUserID); findErr == nil { + _ = l.svcCtx.UserModel.ClearUserCache(l.ctx, newOwnerUser) + } + for i := range transferredSubscribes { + _ = l.svcCtx.UserModel.ClearSubscribeCache(l.ctx, &transferredSubscribes[i]) + } + } + + // 6. Kick 设备(缓存已清,重连时 FindOne 会查到最新数据) l.svcCtx.DeviceManager.KickDevice(device.UserId, device.Identifier) - // 6. 清除该用户所有 session(旧 token 全部失效) + // 7. 清除该用户所有 session(旧 token 全部失效) l.clearAllSessions(userInfo.Id) - // 7. 清理受影响的家庭成员缓存(家庭解散后成员需感知变化) + // 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 {