hi-server/internal/logic/public/user/deleteAccountLogic.go
shanshanzhong 2cc1124dd8
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 7m41s
fix: 任何人注销账号都解散整个家庭组,删除所有成员的登录方式和订阅
统一 member/owner 注销逻辑:无论谁注销,都解散家庭、
删除所有成员的 AuthMethods + Subscribe、踢出所有设备、清除所有缓存。

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-12 06:23:11 -07:00

382 lines
12 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"
"strconv"
"strings"
"time"
"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 DeleteAccountLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// NewDeleteAccountLogic 创建注销账号逻辑实例
func NewDeleteAccountLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteAccountLogic {
return &DeleteAccountLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
// DeleteAccount 保留兼容入口,统一走全量注销登录逻辑
func (l *DeleteAccountLogic) DeleteAccount() (resp *types.DeleteAccountResponse, err error) {
return l.DeleteAccountAll()
}
// DeleteAccountAll 注销登录 + 退出家庭/解散家庭,不删除账号主体
func (l *DeleteAccountLogic) DeleteAccountAll() (resp *types.DeleteAccountResponse, err error) {
currentUser, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
if !ok || currentUser == nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
}
// 事务前:查找家庭关系,收集所有成员 ID
allMemberIDs := []int64{currentUser.Id}
var familyId int64
if relation, relErr := l.findActiveFamilyRelation(currentUser.Id); relErr == nil {
familyId = relation.FamilyId
var memberIDs []int64
if pluckErr := l.svcCtx.DB.WithContext(l.ctx).
Model(&user.UserFamilyMember{}).
Where("family_id = ? AND status = ? AND deleted_at IS NULL", familyId, user.FamilyMemberActive).
Pluck("user_id", &memberIDs).Error; pluckErr == nil {
idSet := map[int64]struct{}{currentUser.Id: {}}
for _, id := range memberIDs {
if id > 0 {
if _, exists := idSet[id]; !exists {
idSet[id] = struct{}{}
allMemberIDs = append(allMemberIDs, id)
}
}
}
}
}
// 事务前:收集所有成员的 AuthMethods事务后用于精确清缓存
allAuthMethods := make([]*user.AuthMethods, 0)
for _, memberID := range allMemberIDs {
if ams, amErr := l.svcCtx.UserModel.FindUserAuthMethods(l.ctx, memberID); amErr == nil {
allAuthMethods = append(allAuthMethods, ams...)
}
}
// 事务内:解散家庭 + 删除所有成员的 AuthMethods + Subscribe
err = l.svcCtx.UserModel.Transaction(l.ctx, func(tx *gorm.DB) error {
// 无论 member 还是 owner都解散整个家庭
if familyId > 0 {
now := time.Now()
if txErr := tx.Model(&user.UserFamilyMember{}).
Where("family_id = ? AND status = ?", familyId, user.FamilyMemberActive).
Updates(map[string]interface{}{
"status": user.FamilyMemberRemoved,
"left_at": now,
}).Error; txErr != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "remove all family members failed")
}
if txErr := tx.Model(&user.UserFamily{}).
Where("id = ?", familyId).
Updates(map[string]interface{}{
"status": familyStatusDisabled,
}).Error; txErr != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "disable family failed")
}
}
// 删除所有成员的 AuthMethods 和 Subscribe
for _, memberID := range allMemberIDs {
if txErr := tx.Where("user_id = ?", memberID).Delete(&user.AuthMethods{}).Error; txErr != nil {
l.Errorw("delete auth methods failed", logger.Field("user_id", memberID), logger.Field("error", txErr.Error()))
}
if txErr := tx.Where("user_id = ?", memberID).Delete(&user.Subscribe{}).Error; txErr != nil {
l.Errorw("delete subscribes failed", logger.Field("user_id", memberID), logger.Field("error", txErr.Error()))
}
}
return nil
})
if err != nil {
return nil, err
}
// 事务后:清除所有成员的 Session + 踢设备
for _, memberID := range allMemberIDs {
l.clearAllSessions(memberID)
var memberDevices []user.Device
l.svcCtx.DB.WithContext(l.ctx).
Model(&user.Device{}).
Where("user_id = ?", memberID).
Find(&memberDevices)
for _, d := range memberDevices {
l.svcCtx.DeviceManager.KickDevice(d.UserId, d.Identifier)
}
}
// 清除 auth method 相关缓存email key 等)
if len(allAuthMethods) > 0 {
var authCacheKeys []string
for _, am := range allAuthMethods {
if am.AuthType == "email" && am.AuthIdentifier != "" {
authCacheKeys = append(authCacheKeys, fmt.Sprintf("cache:user:email:%s", am.AuthIdentifier))
}
}
if len(authCacheKeys) > 0 {
if delErr := l.svcCtx.Redis.Del(l.ctx, authCacheKeys...).Err(); delErr != nil {
l.Errorw("clear auth method cache failed",
logger.Field("user_id", currentUser.Id),
logger.Field("error", delErr.Error()),
)
}
}
}
// 清除所有成员的 user + subscribe 缓存
if cacheErr := l.clearUserAndSubscribeCaches(allMemberIDs); cacheErr != nil {
l.Errorw("clear user related cache failed",
logger.Field("user_id", currentUser.Id),
logger.Field("affected_user_ids", allMemberIDs),
logger.Field("error", cacheErr.Error()),
)
}
return &types.DeleteAccountResponse{
Success: true,
Message: "注销成功",
UserId: currentUser.Id,
Code: 200,
}, nil
}
func (l *DeleteAccountLogic) collectAffectedFamilyUserIDs(tx *gorm.DB, userID int64) ([]int64, error) {
affected := []int64{userID}
var relation struct {
FamilyId int64 `gorm:"column:family_id"`
}
err := tx.Model(&user.UserFamilyMember{}).
Select("user_family_member.family_id").
Joins("JOIN user_family ON user_family.id = user_family_member.family_id AND user_family.deleted_at IS NULL AND user_family.status = ?", user.FamilyStatusActive).
Where("user_family_member.user_id = ? AND user_family_member.status = ? AND user_family_member.deleted_at IS NULL", userID, user.FamilyMemberActive).
First(&relation).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return affected, nil
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query family relation failed")
}
var memberIDs []int64
if err = tx.Model(&user.UserFamilyMember{}).
Where("family_id = ? AND status = ? AND deleted_at IS NULL", relation.FamilyId, user.FamilyMemberActive).
Pluck("user_id", &memberIDs).Error; err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query family members failed")
}
affected = append(affected, memberIDs...)
idSet := make(map[int64]struct{}, len(affected))
unique := make([]int64, 0, len(affected))
for _, id := range affected {
if id <= 0 {
continue
}
if _, exists := idSet[id]; exists {
continue
}
idSet[id] = struct{}{}
unique = append(unique, id)
}
if len(unique) == 0 {
return []int64{userID}, nil
}
return unique, nil
}
func (l *DeleteAccountLogic) clearUserAndSubscribeCaches(userIDs []int64) error {
if len(userIDs) == 0 {
return nil
}
idSet := make(map[int64]struct{}, len(userIDs))
uniqueIDs := make([]int64, 0, len(userIDs))
for _, userID := range userIDs {
if userID <= 0 {
continue
}
if _, exists := idSet[userID]; exists {
continue
}
idSet[userID] = struct{}{}
uniqueIDs = append(uniqueIDs, userID)
}
if len(uniqueIDs) == 0 {
return nil
}
userModels := make([]*user.User, 0, len(uniqueIDs))
subscribeModels := make([]*user.Subscribe, 0, len(uniqueIDs))
for _, userID := range uniqueIDs {
u, findErr := l.svcCtx.UserModel.FindOne(l.ctx, userID)
switch {
case findErr == nil:
userModels = append(userModels, u)
case errors.Is(findErr, gorm.ErrRecordNotFound):
// no-op
default:
l.Errorw("find user for cache clearing failed",
logger.Field("user_id", userID),
logger.Field("error", findErr.Error()),
)
}
subscribeModels = append(subscribeModels, &user.Subscribe{UserId: userID})
subscribes, queryErr := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, userID)
if queryErr != nil {
l.Errorw("query user subscribes for cache clearing failed",
logger.Field("user_id", userID),
logger.Field("error", queryErr.Error()),
)
continue
}
for _, subscribe := range subscribes {
subscribeModels = append(subscribeModels, &user.Subscribe{
Id: subscribe.Id,
UserId: subscribe.UserId,
SubscribeId: subscribe.SubscribeId,
Token: subscribe.Token,
})
}
}
if len(userModels) > 0 {
if err := l.svcCtx.UserModel.ClearUserCache(l.ctx, userModels...); err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "clear user cache failed")
}
}
if len(subscribeModels) > 0 {
if err := l.svcCtx.UserModel.ClearSubscribeCache(l.ctx, subscribeModels...); err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "clear subscribe cache failed")
}
}
return nil
}
// clearAllSessions 清理指定用户的所有会话
func (l *DeleteAccountLogic) clearAllSessions(userId int64) {
sessionsKey := fmt.Sprintf("%s%v", config.UserSessionsKeyPrefix, userId)
sessionSet := make(map[string]struct{})
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()))
} else {
for _, sessionID := range sessions {
if sessionID != "" {
sessionSet[sessionID] = struct{}{}
}
}
}
userIDText := strconv.FormatInt(userId, 10)
pattern := fmt.Sprintf("%s:*", config.SessionIdKey)
var cursor uint64
for {
keys, nextCursor, scanErr := l.svcCtx.Redis.Scan(l.ctx, cursor, pattern, 200).Result()
if scanErr != nil {
l.Errorw("扫描会话键失败", logger.Field("user_id", userId), logger.Field("error", scanErr.Error()))
break
}
for _, sessionKey := range keys {
value, getErr := l.svcCtx.Redis.Get(l.ctx, sessionKey).Result()
if getErr != nil || value != userIDText {
continue
}
sessionID := strings.TrimPrefix(sessionKey, config.SessionIdKey+":")
if sessionID == "" || strings.HasPrefix(sessionID, "detail:") {
continue
}
sessionSet[sessionID] = struct{}{}
}
cursor = nextCursor
if cursor == 0 {
break
}
}
deviceKeySet := make(map[string]struct{})
devicePattern := fmt.Sprintf("%s:*", config.DeviceCacheKeyKey)
cursor = 0
for {
keys, nextCursor, scanErr := l.svcCtx.Redis.Scan(l.ctx, cursor, devicePattern, 200).Result()
if scanErr != nil {
l.Errorw("扫描设备会话映射失败", logger.Field("user_id", userId), logger.Field("error", scanErr.Error()))
break
}
for _, deviceKey := range keys {
sessionID, getErr := l.svcCtx.Redis.Get(l.ctx, deviceKey).Result()
if getErr != nil {
continue
}
if _, exists := sessionSet[sessionID]; exists {
deviceKeySet[deviceKey] = struct{}{}
}
}
cursor = nextCursor
if cursor == 0 {
break
}
}
pipe := l.svcCtx.Redis.TxPipeline()
for sessionID := range sessionSet {
sessionKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionID)
pipe.Del(l.ctx, sessionKey)
pipe.Del(l.ctx, fmt.Sprintf("%s:detail:%s", config.SessionIdKey, sessionID))
pipe.ZRem(l.ctx, sessionsKey, sessionID)
}
pipe.Del(l.ctx, sessionsKey)
for deviceKey := range deviceKeySet {
pipe.Del(l.ctx, deviceKey)
}
if _, err = pipe.Exec(l.ctx); err != nil {
l.Errorw("清理会话缓存失败", logger.Field("user_id", userId), logger.Field("error", err.Error()))
}
l.Infow("[SessionMonitor] 注销账号-清除所有Session",
logger.Field("user_id", userId),
logger.Field("count", len(sessionSet)),
)
}
// findActiveFamilyRelation 查找用户的活跃家庭成员记录
func (l *DeleteAccountLogic) findActiveFamilyRelation(userID int64) (*user.UserFamilyMember, error) {
var relation user.UserFamilyMember
err := l.svcCtx.DB.WithContext(l.ctx).
Model(&user.UserFamilyMember{}).
Where("user_id = ? AND status = ?", userID, user.FamilyMemberActive).
First(&relation).Error
if err != nil {
return nil, err
}
return &relation, nil
}