hi-server/internal/logic/public/user/deleteAccountLogic.go
shanshanzhong 26f6400e74
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 8m3s
feat: 苹果支付uuid 及设备逻辑
2026-03-10 19:53:19 -07:00

337 lines
10 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"
"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")
}
// 事务前先查出 AuthMethods用于事务后精确清缓存
authMethods, _ := l.svcCtx.UserModel.FindUserAuthMethods(l.ctx, currentUser.Id)
affectedUserIDs := []int64{currentUser.Id}
err = l.svcCtx.UserModel.Transaction(l.ctx, func(tx *gorm.DB) error {
familyUserIDs, collectErr := l.collectAffectedFamilyUserIDs(tx, currentUser.Id)
if collectErr != nil {
return collectErr
}
affectedUserIDs = familyUserIDs
exitHelper := newFamilyExitHelper(l.ctx, l.svcCtx)
if removeErr := exitHelper.removeUserFromActiveFamily(tx, currentUser.Id, true); removeErr != nil {
return removeErr
}
// 解绑所有登录方式(邮箱、手机等)
if err := tx.Where("user_id = ?", currentUser.Id).Delete(&user.AuthMethods{}).Error; err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "delete user auth methods failed")
}
// 删除该用户的所有订阅
if err := tx.Where("user_id = ?", currentUser.Id).Delete(&user.Subscribe{}).Error; err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "delete user subscribes failed")
}
return nil
})
if err != nil {
return nil, err
}
l.clearAllSessions(currentUser.Id)
// Kick all affected family member devices + clear their sessions
for _, memberUserID := range affectedUserIDs {
if memberUserID == currentUser.Id {
continue
}
var memberDevices []user.Device
l.svcCtx.DB.WithContext(l.ctx).
Model(&user.Device{}).
Where("user_id = ?", memberUserID).
Find(&memberDevices)
for _, d := range memberDevices {
l.svcCtx.DeviceManager.KickDevice(d.UserId, d.Identifier)
}
l.clearAllSessions(memberUserID)
}
// 主动清 auth method 相关缓存(含 email/mobile 等 key避免缓存未命中时无法生成正确 key
if len(authMethods) > 0 {
var authCacheKeys []string
for _, am := range authMethods {
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()),
)
}
}
}
if cacheErr := l.clearUserAndSubscribeCaches(affectedUserIDs); cacheErr != nil {
l.Errorw("clear user related cache failed",
logger.Field("user_id", currentUser.Id),
logger.Field("affected_user_ids", affectedUserIDs),
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)),
)
}