All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 7m18s
259 lines
7.9 KiB
Go
259 lines
7.9 KiB
Go
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()))
|
||
}
|
||
}
|