hi-server/internal/logic/admin/user/deleteUserLogic.go
shanshanzhong df7303738a
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 7m18s
bug: 无订阅情况 出现下多笔订单 支付状态乱
2026-03-30 00:32:41 -07:00

259 lines
7.9 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"
"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. 最后删除 userUnscoped = 物理删除)
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()))
}
}