package recovery import ( "context" "errors" "strings" "time" "github.com/google/uuid" commonLogic "github.com/perfect-panel/server/internal/logic/common" ordermodel "github.com/perfect-panel/server/internal/model/order" recoverymodel "github.com/perfect-panel/server/internal/model/recovery" subscribemodel "github.com/perfect-panel/server/internal/model/subscribe" usermodel "github.com/perfect-panel/server/internal/model/user" recoveryconfig "github.com/perfect-panel/server/internal/recovery" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/authmethod" "github.com/perfect-panel/server/pkg/constant" "github.com/perfect-panel/server/pkg/logger" "github.com/perfect-panel/server/pkg/tool" "github.com/perfect-panel/server/pkg/uuidx" "github.com/perfect-panel/server/pkg/xerr" pkgerrors "github.com/pkg/errors" "gorm.io/gorm" "gorm.io/gorm/clause" ) const genericRecoveryError = "order recovery information is invalid" type RecoverOrderLogic struct { logger.Logger ctx context.Context svcCtx *svc.ServiceContext } func NewRecoverOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RecoverOrderLogic { return &RecoverOrderLogic{ Logger: logger.WithContext(ctx), ctx: ctx, svcCtx: svcCtx, } } func (l *RecoverOrderLogic) RecoverOrder(req *types.RecoverOrderRequest) (*types.RecoverOrderResponse, error) { orderNo := strings.TrimSpace(req.OrderNo) email := strings.ToLower(strings.TrimSpace(req.Email)) code := strings.TrimSpace(req.Code) if orderNo == "" || email == "" || code == "" { return nil, pkgerrors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), genericRecoveryError) } if err := l.verifyEmailCode(email, code); err != nil { return nil, err } item, ok, err := recoveryconfig.FindOrder(orderNo) if err != nil { l.Errorw("[RecoverOrder] load recovery config failed", logger.Field("error", err.Error())) return nil, pkgerrors.Wrapf(xerr.NewErrCode(xerr.ERROR), genericRecoveryError) } if !ok { return nil, pkgerrors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), genericRecoveryError) } if item.OrderStatus != "" && item.OrderStatus != "success" { return nil, pkgerrors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), genericRecoveryError) } if item.Quantity <= 0 || item.OrderMoneyCents <= 0 { return nil, pkgerrors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), genericRecoveryError) } sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, item.SubscribeId) if err != nil { l.Errorw("[RecoverOrder] subscribe not found", logger.Field("subscribe_id", item.SubscribeId), logger.Field("error", err.Error())) return nil, pkgerrors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), genericRecoveryError) } var ( claim recoverymodel.OrderRecoveryClaim order ordermodel.Order userSub usermodel.Subscribe ) err = l.svcCtx.DB.WithContext(l.ctx).Transaction(func(tx *gorm.DB) error { if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Model(&recoverymodel.OrderRecoveryClaim{}). Where("order_no = ?", orderNo). First(&claim).Error; err == nil { return nil } else if !errors.Is(err, gorm.ErrRecordNotFound) { return err } u, err := l.findOrCreateUser(tx, email) if err != nil { return err } claim = recoverymodel.OrderRecoveryClaim{ OrderNo: orderNo, Email: email, UserId: u.Id, SubscribeId: item.SubscribeId, ClaimedAt: time.Now(), } if err := tx.Model(&recoverymodel.OrderRecoveryClaim{}).Create(&claim).Error; err != nil { return err } order, err = l.createRecoveredOrder(tx, u.Id, item, sub) if err != nil { return err } userSub, err = l.createOrExtendSubscribe(tx, u.Id, order.Id, item, sub) if err != nil { return err } claim.UserSubscribeId = userSub.Id return tx.Model(&recoverymodel.OrderRecoveryClaim{}). Where("id = ?", claim.Id). Updates(map[string]any{ "order_id": order.Id, "user_subscribe_id": userSub.Id, }).Error }) if err != nil { l.Errorw("[RecoverOrder] recover failed", logger.Field("order_no", orderNo), logger.Field("error", err.Error())) return nil, pkgerrors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), genericRecoveryError) } l.clearCaches(&userSub, item.SubscribeId, email) l.Infof("[RecoverOrder] recovered order subscription order_no=%s email=%s user_id=%d subscribe_id=%d user_subscribe_id=%d", orderNo, email, claim.UserId, claim.SubscribeId, claim.UserSubscribeId) return &types.RecoverOrderResponse{ Success: true, Message: "recovery completed, please login with your email verification code", }, nil } func (l *RecoverOrderLogic) createRecoveredOrder(tx *gorm.DB, userId int64, item recoveryconfig.Order, sub *subscribemodel.Subscribe) (ordermodel.Order, error) { var existing ordermodel.Order orderNo := item.OutOrderNo if orderNo == "" { orderNo = item.OrderNo } if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Model(&ordermodel.Order{}). Where("order_no = ?", orderNo). First(&existing).Error; err == nil { if existing.UserId != userId { return ordermodel.Order{}, errors.New("recovered order number already belongs to another user") } return existing, nil } else if !errors.Is(err, gorm.ErrRecordNotFound) { return ordermodel.Order{}, err } paidAt := time.Now() if item.PaidAt > 0 { paidAt = time.Unix(item.PaidAt, 0) } quantity := item.Quantity if quantity <= 0 { quantity = 1 } price := sub.UnitPrice * quantity amount := item.OrderMoneyCents if amount <= 0 { amount = price } o := ordermodel.Order{ UserId: userId, SubscriptionUserId: userId, OrderNo: orderNo, Type: 1, Quantity: quantity, Price: price, Amount: amount, Discount: price - amount, PaymentId: 2, Method: "EPay", TradeNo: strings.TrimSpace(item.PaymentOrderNo), Status: 5, SubscribeId: item.SubscribeId, IsNew: true, CreatedAt: paidAt, UpdatedAt: paidAt, } if o.Discount < 0 { o.Discount = 0 } if err := tx.Model(&ordermodel.Order{}).Create(&o).Error; err != nil { return ordermodel.Order{}, err } return o, nil } func (l *RecoverOrderLogic) createOrExtendSubscribe(tx *gorm.DB, userId int64, orderId int64, item recoveryconfig.Order, sub *subscribemodel.Subscribe) (usermodel.Subscribe, error) { now := time.Now() var existing usermodel.Subscribe err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Model(&usermodel.Subscribe{}). Where("user_id = ? AND subscribe_id = ?", userId, item.SubscribeId). Order("expire_time DESC"). Order("id DESC"). First(&existing).Error if err == nil { base := now if existing.ExpireTime.After(now) { base = existing.ExpireTime } existing.OrderId = orderId existing.ExpireTime = base.Add(time.Duration(item.Quantity) * 24 * time.Hour) existing.Status = 1 existing.FinishedAt = nil existing.NodeGroupId = sub.NodeGroupId existing.Traffic = sub.Traffic existing.Download = 0 existing.Upload = 0 if strings.TrimSpace(item.Note) != "" { existing.Note = strings.TrimSpace(item.Note) } if err := tx.Model(&usermodel.Subscribe{}).Where("id = ?", existing.Id).Save(&existing).Error; err != nil { return usermodel.Subscribe{}, err } return existing, nil } else if !errors.Is(err, gorm.ErrRecordNotFound) { return usermodel.Subscribe{}, err } orderNo := item.OutOrderNo if orderNo == "" { orderNo = item.OrderNo } userSub := usermodel.Subscribe{ UserId: userId, OrderId: orderId, SubscribeId: item.SubscribeId, NodeGroupId: sub.NodeGroupId, StartTime: now, ExpireTime: now.Add(time.Duration(item.Quantity) * 24 * time.Hour), Traffic: sub.Traffic, Download: 0, Upload: 0, Token: uuidx.SubscribeToken(orderNo), UUID: uuid.New().String(), Status: 1, Note: strings.TrimSpace(item.Note), } if err := tx.Model(&usermodel.Subscribe{}).Create(&userSub).Error; err != nil { return usermodel.Subscribe{}, err } return userSub, nil } func (l *RecoverOrderLogic) verifyEmailCode(email string, code string) error { checker := commonLogic.NewCheckVerificationCodeLogic(l.ctx, l.svcCtx) resp, err := checker.CheckVerificationCodeWithBehavior(&types.CheckVerificationCodeRequest{ Method: authmethod.Email, Account: email, Code: code, Type: uint8(constant.Security), }, commonLogic.VerifyCodeCheckBehavior{ Source: "order_recovery", Consume: true, AllowSceneFallback: true, }) if err != nil { return err } if resp == nil || !resp.Status { return pkgerrors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), genericRecoveryError) } return nil } func (l *RecoverOrderLogic) findOrCreateUser(tx *gorm.DB, email string) (*usermodel.User, error) { var auth usermodel.AuthMethods if err := tx.Model(&usermodel.AuthMethods{}). Where("auth_type = ? AND auth_identifier = ?", authmethod.Email, email). First(&auth).Error; err == nil { var u usermodel.User if err := tx.Model(&usermodel.User{}).Where("id = ?", auth.UserId).First(&u).Error; err != nil { return nil, err } u.AuthMethods = []usermodel.AuthMethods{auth} return &u, nil } else if !errors.Is(err, gorm.ErrRecordNotFound) { return nil, err } u := &usermodel.User{ Password: tool.EncodePassWord(uuid.NewString()), Algo: "default", AuthMethods: []usermodel.AuthMethods{ { AuthType: authmethod.Email, AuthIdentifier: email, Verified: true, }, }, } if err := tx.Create(u).Error; err != nil { return nil, err } referCode := uuidx.UserInviteCode(u.Id) if err := tx.Model(&usermodel.User{}).Where("id = ?", u.Id).Update("refer_code", referCode).Error; err != nil { return nil, err } u.ReferCode = referCode return u, nil } func (l *RecoverOrderLogic) clearCaches(userSub *usermodel.Subscribe, subscribeId int64, email string) { if userSub != nil && userSub.Id > 0 { if err := l.svcCtx.UserModel.ClearSubscribeCache(l.ctx, userSub); err != nil { l.Errorw("[RecoverOrder] clear user subscribe cache failed", logger.Field("error", err.Error())) } if userSub.UserId > 0 { if err := l.svcCtx.UserModel.ClearUserCache(l.ctx, &usermodel.User{ Id: userSub.UserId, AuthMethods: []usermodel.AuthMethods{ { UserId: userSub.UserId, AuthType: authmethod.Email, AuthIdentifier: email, }, }, }); err != nil { l.Errorw("[RecoverOrder] clear user cache failed", logger.Field("error", err.Error())) } } } if subscribeId > 0 { if err := l.svcCtx.SubscribeModel.ClearCache(l.ctx, subscribeId); err != nil { l.Errorw("[RecoverOrder] clear subscribe cache failed", logger.Field("error", err.Error())) } } if l.svcCtx.NodeModel != nil { if err := l.svcCtx.NodeModel.ClearServerAllCache(l.ctx); err != nil { l.Errorw("[RecoverOrder] clear server cache failed", logger.Field("error", err.Error())) } } }