This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
package recovery
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/perfect-panel/server/internal/logic/public/recovery"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/result"
|
||||
)
|
||||
|
||||
func RecoverOrderHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
var req types.RecoverOrderRequest
|
||||
_ = c.ShouldBind(&req)
|
||||
validateErr := svcCtx.Validate(&req)
|
||||
if validateErr != nil {
|
||||
result.ParamErrorResult(c, validateErr)
|
||||
return
|
||||
}
|
||||
|
||||
l := recovery.NewRecoverOrderLogic(c.Request.Context(), svcCtx)
|
||||
resp, err := l.RecoverOrder(&req)
|
||||
result.HttpResult(c, resp, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package recovery
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/perfect-panel/server/internal/logic/public/recovery"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/result"
|
||||
)
|
||||
|
||||
func SendCodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
var req types.RecoverySendCodeRequest
|
||||
_ = c.ShouldBind(&req)
|
||||
validateErr := svcCtx.Validate(&req)
|
||||
if validateErr != nil {
|
||||
result.ParamErrorResult(c, validateErr)
|
||||
return
|
||||
}
|
||||
|
||||
l := recovery.NewSendCodeLogic(c.Request.Context(), svcCtx)
|
||||
resp, err := l.SendCode(&req)
|
||||
result.HttpResult(c, resp, err)
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@ import (
|
||||
publicOrder "github.com/perfect-panel/server/internal/handler/public/order"
|
||||
publicPayment "github.com/perfect-panel/server/internal/handler/public/payment"
|
||||
publicPortal "github.com/perfect-panel/server/internal/handler/public/portal"
|
||||
publicRecovery "github.com/perfect-panel/server/internal/handler/public/recovery"
|
||||
publicRedemption "github.com/perfect-panel/server/internal/handler/public/redemption"
|
||||
publicSubscribe "github.com/perfect-panel/server/internal/handler/public/subscribe"
|
||||
publicTicket "github.com/perfect-panel/server/internal/handler/public/ticket"
|
||||
@@ -932,6 +933,17 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
publicRedemptionGroupRouter.POST("/", publicRedemption.RedeemCodeHandler(serverCtx))
|
||||
}
|
||||
|
||||
publicRecoveryGroupRouter := router.Group("/v1/public/recovery")
|
||||
publicRecoveryGroupRouter.Use(middleware.DeviceMiddleware(serverCtx))
|
||||
|
||||
{
|
||||
// Send recovery verification code
|
||||
publicRecoveryGroupRouter.POST("/send_code", publicRecovery.SendCodeHandler(serverCtx))
|
||||
|
||||
// Recover order subscription
|
||||
publicRecoveryGroupRouter.POST("/order", publicRecovery.RecoverOrderHandler(serverCtx))
|
||||
}
|
||||
|
||||
publicSubscribeGroupRouter := router.Group("/v1/public/subscribe")
|
||||
publicSubscribeGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx))
|
||||
|
||||
|
||||
@@ -0,0 +1,350 @@
|
||||
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()))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package recovery
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/perfect-panel/server/internal/config"
|
||||
commonLogic "github.com/perfect-panel/server/internal/logic/common"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/constant"
|
||||
"github.com/perfect-panel/server/pkg/limit"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
"github.com/perfect-panel/server/pkg/random"
|
||||
"github.com/perfect-panel/server/pkg/xerr"
|
||||
queue "github.com/perfect-panel/server/queue/types"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type SendCodeLogic struct {
|
||||
logger.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewSendCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SendCodeLogic {
|
||||
return &SendCodeLogic{
|
||||
Logger: logger.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *SendCodeLogic) SendCode(req *types.RecoverySendCodeRequest) (*types.SendCodeResponse, error) {
|
||||
email := strings.ToLower(strings.TrimSpace(req.Email))
|
||||
if email == "" {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "email is required")
|
||||
}
|
||||
|
||||
scene := constant.Security.String()
|
||||
cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, scene, email)
|
||||
limiter := limit.NewPeriodLimit(60, 1, l.svcCtx.Redis, fmt.Sprintf("%s:%s:%s", config.SendIntervalKeyPrefix, "email", scene))
|
||||
permit, err := limiter.Take(email)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Failed to take limit")
|
||||
}
|
||||
if !limiter.ParsePermitState(permit) {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.TooManyRequests), "send email too many requests")
|
||||
}
|
||||
permit, err = l.svcCtx.AuthLimiter.Take(fmt.Sprintf("%s:%s:%s", "email", scene, email))
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Failed to take limit")
|
||||
}
|
||||
if !l.svcCtx.AuthLimiter.ParsePermitState(permit) {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.TodaySendCountExceedsLimit), "send email too many requests")
|
||||
}
|
||||
|
||||
code := random.Key(6, 0)
|
||||
expireTime := l.svcCtx.Config.VerifyCode.VerifyCodeExpireTime
|
||||
if expireTime == 0 {
|
||||
expireTime = 900
|
||||
}
|
||||
val, _ := json.Marshal(commonLogic.CacheKeyPayload{
|
||||
Code: code,
|
||||
LastAt: time.Now().Unix(),
|
||||
})
|
||||
if err = l.svcCtx.Redis.Set(l.ctx, cacheKey, string(val), time.Second*time.Duration(expireTime)).Err(); err != nil {
|
||||
return nil, errors.Wrap(xerr.NewErrCode(xerr.ERROR), "Failed to set verification code")
|
||||
}
|
||||
|
||||
expireMinutes := expireTime / 60
|
||||
taskPayload := queue.SendEmailPayload{
|
||||
Type: queue.EmailTypeVerify,
|
||||
Scene: scene,
|
||||
Email: email,
|
||||
Subject: "Verification code",
|
||||
Content: map[string]interface{}{
|
||||
"Type": uint8(constant.Security),
|
||||
"SiteLogo": l.svcCtx.Config.Site.SiteLogo,
|
||||
"SiteName": l.svcCtx.Config.Site.SiteName,
|
||||
"Expire": expireMinutes,
|
||||
"Code": code,
|
||||
},
|
||||
}
|
||||
payload, err := json.Marshal(taskPayload)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(xerr.NewErrCode(xerr.ERROR), "Failed to marshal task payload")
|
||||
}
|
||||
task := asynq.NewTask(queue.ForthwithSendEmail, payload, asynq.MaxRetry(3))
|
||||
if _, err = l.svcCtx.Queue.Enqueue(task); err != nil {
|
||||
return nil, errors.Wrap(xerr.NewErrCode(xerr.ERROR), "Failed to enqueue task")
|
||||
}
|
||||
|
||||
resp := &types.SendCodeResponse{Status: true}
|
||||
if l.svcCtx.Config.Model == constant.DevMode {
|
||||
resp.Code = code
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package recovery
|
||||
|
||||
import "time"
|
||||
|
||||
type OrderRecoveryClaim struct {
|
||||
Id int64 `gorm:"primaryKey"`
|
||||
OrderNo string `gorm:"type:varchar(255);not null;uniqueIndex:idx_order_recovery_claim_order_no;comment:Recovered Order No"`
|
||||
Email string `gorm:"type:varchar(255);not null;index:idx_order_recovery_claim_email;comment:Claim Email"`
|
||||
UserId int64 `gorm:"type:bigint;not null;default:0;comment:User ID"`
|
||||
SubscribeId int64 `gorm:"type:bigint;not null;default:0;comment:Subscribe ID"`
|
||||
OrderId int64 `gorm:"type:bigint;not null;default:0;comment:Recovered Order ID"`
|
||||
UserSubscribeId int64 `gorm:"type:bigint;not null;default:0;comment:User Subscribe ID"`
|
||||
ClaimedAt time.Time `gorm:"comment:Claimed Time"`
|
||||
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
|
||||
UpdatedAt time.Time `gorm:"comment:Update Time"`
|
||||
}
|
||||
|
||||
func (OrderRecoveryClaim) TableName() string {
|
||||
return "order_recovery_claims"
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package recovery
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
//go:embed recovery_orders.json
|
||||
var recoveryOrdersJSON []byte
|
||||
|
||||
type Config struct {
|
||||
Orders []Order `json:"orders"`
|
||||
}
|
||||
|
||||
type Order struct {
|
||||
OutOrderNo string `json:"out_order_no,omitempty"`
|
||||
OrderNo string `json:"order_no"`
|
||||
OrderStatus string `json:"order_status,omitempty"`
|
||||
SubscribeId int64 `json:"subscribe_id"`
|
||||
LegacyID string `json:"订阅id,omitempty"`
|
||||
ExpireAt int64 `json:"expire_at"`
|
||||
Quantity int64 `json:"quantity,omitempty"`
|
||||
PaidAt int64 `json:"paid_at,omitempty"`
|
||||
PaymentOrderNo string `json:"payment_order_no,omitempty"`
|
||||
OrderMoneyCents int64 `json:"order_money_cents,omitempty"`
|
||||
Note string `json:"note"`
|
||||
}
|
||||
|
||||
var (
|
||||
loadOnce sync.Once
|
||||
orders map[string]Order
|
||||
loadErr error
|
||||
)
|
||||
|
||||
func FindOrder(orderNo string) (Order, bool, error) {
|
||||
loadOnce.Do(func() {
|
||||
orders, loadErr = loadOrders()
|
||||
})
|
||||
if loadErr != nil {
|
||||
return Order{}, false, loadErr
|
||||
}
|
||||
item, ok := orders[strings.TrimSpace(orderNo)]
|
||||
return item, ok, nil
|
||||
}
|
||||
|
||||
func loadOrders() (map[string]Order, error) {
|
||||
var cfg Config
|
||||
if err := json.Unmarshal(recoveryOrdersJSON, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("parse recovery orders json: %w", err)
|
||||
}
|
||||
|
||||
result := make(map[string]Order, len(cfg.Orders))
|
||||
for i, item := range cfg.Orders {
|
||||
item.OutOrderNo = strings.TrimSpace(item.OutOrderNo)
|
||||
item.OrderNo = strings.TrimSpace(item.OrderNo)
|
||||
item.OrderStatus = strings.ToLower(strings.TrimSpace(item.OrderStatus))
|
||||
if item.OutOrderNo == "" {
|
||||
item.OutOrderNo = item.OrderNo
|
||||
}
|
||||
if item.OrderNo == "" {
|
||||
item.OrderNo = item.OutOrderNo
|
||||
}
|
||||
if item.OrderStatus == "" {
|
||||
item.OrderStatus = "success"
|
||||
}
|
||||
if item.SubscribeId == 0 && strings.TrimSpace(item.LegacyID) != "" {
|
||||
id, err := strconv.ParseInt(strings.TrimSpace(item.LegacyID), 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("orders[%d] legacy subscribe id is invalid", i)
|
||||
}
|
||||
item.SubscribeId = id
|
||||
}
|
||||
if item.OutOrderNo == "" {
|
||||
return nil, fmt.Errorf("orders[%d] out_order_no is required", i)
|
||||
}
|
||||
if item.SubscribeId <= 0 {
|
||||
return nil, fmt.Errorf("orders[%d] subscribe_id is required", i)
|
||||
}
|
||||
if item.ExpireAt <= 0 {
|
||||
return nil, fmt.Errorf("orders[%d] expire_at is required", i)
|
||||
}
|
||||
if item.Quantity <= 0 {
|
||||
item.Quantity = quantityFromAmount(item.OrderMoneyCents)
|
||||
}
|
||||
if item.Quantity <= 0 {
|
||||
return nil, fmt.Errorf("orders[%d] quantity is required", i)
|
||||
}
|
||||
if item.PaidAt < 0 {
|
||||
return nil, fmt.Errorf("orders[%d] paid_at is invalid", i)
|
||||
}
|
||||
if item.OrderMoneyCents < 0 {
|
||||
return nil, fmt.Errorf("orders[%d] order_money_cents is invalid", i)
|
||||
}
|
||||
if _, exists := result[item.OutOrderNo]; exists {
|
||||
return nil, fmt.Errorf("duplicate recovery out_order_no %s", item.OutOrderNo)
|
||||
}
|
||||
result[item.OutOrderNo] = item
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func quantityFromAmount(amount int64) int64 {
|
||||
switch amount {
|
||||
case 1953:
|
||||
return 7
|
||||
case 4193:
|
||||
return 30
|
||||
case 9093:
|
||||
return 90
|
||||
case 31500:
|
||||
return 365
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -65,6 +65,7 @@ func initServer(svc *svc.ServiceContext) *gin.Engine {
|
||||
|
||||
// register handlers
|
||||
handler.RegisterHandlers(r, svc)
|
||||
r.StaticFile("/order-recovery.html", "./public/order-recovery.html")
|
||||
// register subscribe handler
|
||||
handler.RegisterSubscribeHandlers(r, svc)
|
||||
// register telegram handler
|
||||
|
||||
@@ -2235,6 +2235,17 @@ type RechargeOrderResponse struct {
|
||||
OrderNo string `json:"order_no"`
|
||||
}
|
||||
|
||||
type RecoverOrderRequest struct {
|
||||
OrderNo string `json:"order_no" validate:"required"`
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Code string `json:"code" validate:"required"`
|
||||
}
|
||||
|
||||
type RecoverOrderResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type RedeemCodeRequest struct {
|
||||
Code string `json:"code" validate:"required"`
|
||||
}
|
||||
@@ -2359,6 +2370,10 @@ type ResetTrafficOrderResponse struct {
|
||||
OrderNo string `json:"order_no"`
|
||||
}
|
||||
|
||||
type RecoverySendCodeRequest struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
}
|
||||
|
||||
type ResetUserSubscribeTokenRequest struct {
|
||||
UserSubscribeId int64 `json:"user_subscribe_id"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user