package user import ( "context" "fmt" "os" "strings" usermodel "github.com/perfect-panel/server/internal/model/user" "github.com/perfect-panel/server/internal/config" "github.com/perfect-panel/server/internal/model/iap/apple" "github.com/perfect-panel/server/internal/model/log" "github.com/perfect-panel/server/internal/model/logmessage" "github.com/perfect-panel/server/internal/model/order" "github.com/perfect-panel/server/internal/model/ticket" "github.com/perfect-panel/server/internal/model/traffic" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" pkglogger "github.com/perfect-panel/server/pkg/logger" "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "gorm.io/gorm" ) type DeleteUserLogic struct { ctx context.Context svcCtx *svc.ServiceContext pkglogger.Logger } func NewDeleteUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteUserLogic { return &DeleteUserLogic{ ctx: ctx, svcCtx: svcCtx, Logger: pkglogger.WithContext(ctx), } } func (l *DeleteUserLogic) DeleteUser(req *types.GetDetailRequest) error { isDemo := strings.ToLower(os.Getenv("PPANEL_MODE")) == "demo" if req.Id == 2 && isDemo { return errors.Wrapf(xerr.NewErrCodeMsg(503, "Demo mode does not allow deletion of the admin user"), "delete user failed: cannot delete admin user in demo mode") } return l.purgeUser(req.Id) } // purgeUser 硬删除用户及其所有关联数据,无孤儿数据残留。 // 删除顺序:先删子表(引用 user_id),再删主表(user)。 func (l *DeleteUserLogic) purgeUser(userID int64) error { // 1. 事务前:收集需要清缓存的数据 userInfo, err := l.svcCtx.UserModel.FindOne(l.ctx, userID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil } return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user failed: %v", err) } authMethods, _ := l.svcCtx.UserModel.FindUserAuthMethods(l.ctx, userID) subscribes, _ := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, userID) // 2. 查 ticket id 列表(ticket_follow 通过 ticket_id 关联,需要先查再删) var ticketIDs []int64 l.svcCtx.DB.WithContext(l.ctx).Model(&ticket.Ticket{}). Where("user_id = ?", userID).Pluck("id", &ticketIDs) // 3. 查家庭关系(需要处理家庭解散) var familyMember usermodel.UserFamilyMember isFamilyOwner := false var familyID int64 if err := l.svcCtx.DB.WithContext(l.ctx). Model(&usermodel.UserFamilyMember{}). Where("user_id = ?", userID). First(&familyMember).Error; err == nil { familyID = familyMember.FamilyId var family usermodel.UserFamily if err2 := l.svcCtx.DB.WithContext(l.ctx). Where("id = ?", familyID).First(&family).Error; err2 == nil { isFamilyOwner = (family.OwnerUserId == userID) } } // 4. 事务内:按顺序删除所有关联表,最后删 user err = l.svcCtx.DB.WithContext(l.ctx).Transaction(func(tx *gorm.DB) error { // 4a. 家庭处理 if familyID > 0 { if isFamilyOwner { // 家主:解散整个家庭(删所有成员记录 + 删家庭) if e := tx.Unscoped().Where("family_id = ?", familyID). Delete(&usermodel.UserFamilyMember{}).Error; e != nil { return e } if e := tx.Unscoped().Where("id = ?", familyID). Delete(&usermodel.UserFamily{}).Error; e != nil { return e } } else { // 成员:只删自己的成员记录 if e := tx.Unscoped().Where("user_id = ? AND family_id = ?", userID, familyID). Delete(&usermodel.UserFamilyMember{}).Error; e != nil { return e } } } // 4b. 用户认证方式 if e := tx.Unscoped().Where("user_id = ?", userID). Delete(&usermodel.AuthMethods{}).Error; e != nil { return e } // 4c. 订阅 if e := tx.Unscoped().Where("user_id = ?", userID). Delete(&usermodel.Subscribe{}).Error; e != nil { return e } // 4d. 设备 + 设备在线记录 if e := tx.Where("user_id = ?", userID). Delete(&usermodel.Device{}).Error; e != nil { return e } if e := tx.Where("user_id = ?", userID). Delete(&usermodel.DeviceOnlineRecord{}).Error; e != nil { return e } // 4e. 提现记录 if e := tx.Where("user_id = ?", userID). Delete(&usermodel.Withdrawal{}).Error; e != nil { return e } // 4f. 订单 if e := tx.Where("user_id = ?", userID). Delete(&order.Order{}).Error; e != nil { return e } // 4g. 流量日志 if e := tx.Where("user_id = ?", userID). Delete(&traffic.TrafficLog{}).Error; e != nil { return e } // 4h. 系统日志(object_id = userID) if e := tx.Where("object_id = ?", userID). Delete(&log.SystemLog{}).Error; e != nil { return e } // 4i. 工单 follow + 工单 if len(ticketIDs) > 0 { if e := tx.Where("ticket_id IN ?", ticketIDs). Delete(&ticket.Follow{}).Error; e != nil { return e } } if e := tx.Where("user_id = ?", userID). Delete(&ticket.Ticket{}).Error; e != nil { return e } // 4j. Apple IAP 交易记录 if e := tx.Where("user_id = ?", userID). Delete(&apple.Transaction{}).Error; e != nil { return e } // 4k. 日志消息 if e := tx.Where("user_id = ?", userID). Delete(&logmessage.LogMessage{}).Error; e != nil { return e } // 4l. 最后删除 user(Unscoped = 物理删除) if e := tx.Unscoped().Where("id = ?", userID). Delete(&usermodel.User{}).Error; e != nil { return e } return nil }) if err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "purge user %d failed: %v", userID, err) } // 5. 事务后:清缓存 + 踢设备 l.cleanupAfterDelete(userID, userInfo, authMethods, subscribes) return nil } func (l *DeleteUserLogic) cleanupAfterDelete( userID int64, userInfo *usermodel.User, authMethods []*usermodel.AuthMethods, subscribes []*usermodel.SubscribeDetails, ) { // 踢设备 var devices []usermodel.Device l.svcCtx.DB.WithContext(l.ctx).Where("user_id = ?", userID).Find(&devices) for _, d := range devices { l.svcCtx.DeviceManager.KickDevice(d.UserId, d.Identifier) } // 清 session l.clearAllSessions(userID) // 清 email 缓存 var emailKeys []string for _, am := range authMethods { if am.AuthType == "email" && am.AuthIdentifier != "" { emailKeys = append(emailKeys, fmt.Sprintf("cache:user:email:%s", am.AuthIdentifier)) } } if len(emailKeys) > 0 { if e := l.svcCtx.Redis.Del(l.ctx, emailKeys...).Err(); e != nil { l.Errorw("clear email cache failed", pkglogger.Field("user_id", userID), pkglogger.Field("error", e.Error())) } } // 清 user 缓存 if userInfo != nil { if e := l.svcCtx.UserModel.ClearUserCache(l.ctx, userInfo); e != nil { l.Errorw("clear user cache failed", pkglogger.Field("user_id", userID), pkglogger.Field("error", e.Error())) } } // 清订阅缓存 subModels := make([]*usermodel.Subscribe, 0, len(subscribes)+1) subModels = append(subModels, &usermodel.Subscribe{UserId: userID}) for _, s := range subscribes { subModels = append(subModels, &usermodel.Subscribe{ Id: s.Id, UserId: s.UserId, SubscribeId: s.SubscribeId, Token: s.Token, }) } if e := l.svcCtx.UserModel.ClearSubscribeCache(l.ctx, subModels...); e != nil { l.Errorw("clear subscribe cache failed", pkglogger.Field("user_id", userID), pkglogger.Field("error", e.Error())) } } // clearAllSessions 清理用户所有 session func (l *DeleteUserLogic) clearAllSessions(userID int64) { sessionsKey := fmt.Sprintf("%s%d", config.UserSessionsKeyPrefix, userID) sessions, _ := l.svcCtx.Redis.ZRange(l.ctx, sessionsKey, 0, -1).Result() pipe := l.svcCtx.Redis.TxPipeline() for _, sid := range sessions { pipe.Del(l.ctx, fmt.Sprintf("%s:%s", config.SessionIdKey, sid)) pipe.Del(l.ctx, fmt.Sprintf("%s:detail:%s", config.SessionIdKey, sid)) pipe.ZRem(l.ctx, sessionsKey, sid) } pipe.Del(l.ctx, sessionsKey) if _, e := pipe.Exec(l.ctx); e != nil { l.Errorw("clear sessions failed", pkglogger.Field("user_id", userID), pkglogger.Field("error", e.Error())) } }