371 lines
11 KiB
Go
371 lines
11 KiB
Go
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"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
)
|
|
|
|
const (
|
|
recoverySuccessMessage = "恢复完成,请使用该邮箱验证码登录查看订阅。"
|
|
recoveryAlreadyDoneMessage = "该订单已恢复,请使用恢复时绑定的邮箱登录查看订阅。"
|
|
recoveryInvalidOrderMessage = "订单信息无效,请确认订单号是否正确。"
|
|
recoveryInvalidCodeMessage = "邮箱验证码错误或已过期,请重新获取验证码。"
|
|
recoverySystemErrorMessage = "恢复暂时失败,请稍后再试或联系客服处理。"
|
|
)
|
|
|
|
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 recoveryFailed(recoveryInvalidOrderMessage), nil
|
|
}
|
|
|
|
if err := l.verifyEmailCode(email, code); err != nil {
|
|
return recoveryFailed(recoveryInvalidCodeMessage), nil
|
|
}
|
|
|
|
item, ok, err := recoveryconfig.FindOrder(orderNo)
|
|
if err != nil {
|
|
l.Errorw("[RecoverOrder] load recovery config failed", logger.Field("error", err.Error()))
|
|
return recoveryFailed(recoverySystemErrorMessage), nil
|
|
}
|
|
if !ok {
|
|
return recoveryFailed(recoveryInvalidOrderMessage), nil
|
|
}
|
|
|
|
if item.OrderStatus != "" && item.OrderStatus != "success" {
|
|
return recoveryFailed(recoveryInvalidOrderMessage), nil
|
|
}
|
|
if item.Quantity <= 0 || item.OrderMoneyCents <= 0 {
|
|
return recoveryFailed(recoveryInvalidOrderMessage), nil
|
|
}
|
|
claimOrderNo := item.OutOrderNo
|
|
if claimOrderNo == "" {
|
|
claimOrderNo = item.OrderNo
|
|
}
|
|
|
|
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 recoveryFailed(recoverySystemErrorMessage), nil
|
|
}
|
|
|
|
var (
|
|
claim recoverymodel.OrderRecoveryClaim
|
|
order ordermodel.Order
|
|
userSub usermodel.Subscribe
|
|
alreadyRecovered bool
|
|
)
|
|
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 = ?", claimOrderNo).
|
|
First(&claim).Error; err == nil {
|
|
alreadyRecovered = true
|
|
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: claimOrderNo,
|
|
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 recoveryFailed(recoverySystemErrorMessage), nil
|
|
}
|
|
|
|
if alreadyRecovered {
|
|
return &types.RecoverOrderResponse{
|
|
Success: true,
|
|
Message: recoveryAlreadyDoneMessage,
|
|
}, nil
|
|
}
|
|
|
|
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: recoverySuccessMessage,
|
|
}, nil
|
|
}
|
|
|
|
func recoveryFailed(message string) *types.RecoverOrderResponse {
|
|
return &types.RecoverOrderResponse{
|
|
Success: false,
|
|
Message: message,
|
|
}
|
|
}
|
|
|
|
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
|
|
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 errors.New("invalid verification code")
|
|
}
|
|
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()))
|
|
}
|
|
}
|
|
}
|