feat(quota): enhance quota task management with new request structures and processing logic

This commit is contained in:
Chang lue Tsen
2025-09-10 14:53:48 -04:00
parent 83c1c14b01
commit 3f5aac239b
15 changed files with 740 additions and 95 deletions
@@ -2,10 +2,18 @@ package marketing
import (
"context"
"strconv"
"time"
"github.com/hibiken/asynq"
"github.com/perfect-panel/server/internal/model/task"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/xerr"
queueType "github.com/perfect-panel/server/queue/types"
"github.com/pkg/errors"
)
type CreateQuotaTaskLogic struct {
@@ -14,7 +22,7 @@ type CreateQuotaTaskLogic struct {
svcCtx *svc.ServiceContext
}
// Create a quota task
// NewCreateQuotaTaskLogic Create a quota task
func NewCreateQuotaTaskLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateQuotaTaskLogic {
return &CreateQuotaTaskLogic{
Logger: logger.WithContext(ctx),
@@ -24,7 +32,72 @@ func NewCreateQuotaTaskLogic(ctx context.Context, svcCtx *svc.ServiceContext) *C
}
func (l *CreateQuotaTaskLogic) CreateQuotaTask(req *types.CreateQuotaTaskRequest) error {
// todo: add your logic here and delete this line
var subs []*user.Subscribe
query := l.svcCtx.DB.WithContext(l.ctx).Model(&user.Subscribe{})
if len(req.Subscribers) > 0 {
query = query.Where("`subscribe_id` IN ?", req.Subscribers)
}
if req.IsActive != nil && *req.IsActive {
query = query.Where("`status` IN ?", []int64{0, 1, 2}) // 0: Pending 1: Active 2: Finished
}
if req.StartTime != 0 {
start := time.UnixMilli(req.StartTime)
query = query.Where("`start_time` >= ?", start)
}
if req.EndTime != 0 {
end := time.UnixMilli(req.EndTime)
query = query.Where("`start_time` <= ?", end)
}
if err := query.Find(&subs).Error; err != nil {
l.Errorf("[CreateQuotaTask] find subscribers error: %v", err.Error())
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribers error")
}
if len(subs) == 0 {
return errors.Wrapf(xerr.NewErrMsg("No subscribers found"), "no subscribers found")
}
var subIds []int64
for _, sub := range subs {
subIds = append(subIds, sub.Id)
}
scopeInfo := task.QuotaScope{
Subscribers: req.Subscribers,
IsActive: req.IsActive,
StartTime: req.StartTime,
EndTime: req.EndTime,
}
scopeBytes, _ := scopeInfo.Marshal()
contentInfo := task.QuotaContent{
ResetTraffic: req.ResetTraffic,
Days: req.Days,
GiftType: req.GiftType,
GiftValue: req.GiftValue,
}
contentBytes, _ := contentInfo.Marshal()
// create task
newTask := &task.Task{
Type: task.TypeQuota,
Status: 0,
Scope: string(scopeBytes),
Content: string(contentBytes),
Total: uint64(len(subIds)),
Current: 0,
Errors: "",
}
if err := l.svcCtx.DB.WithContext(l.ctx).Model(&task.Task{}).Create(newTask).Error; err != nil {
l.Errorf("[CreateQuotaTask] create task error: %v", err.Error())
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create task error")
}
// enqueue task
queueTask := asynq.NewTask(queueType.ForthwithQuotaTask, []byte(strconv.FormatInt(newTask.Id, 10)))
if _, err := l.svcCtx.Queue.EnqueueContext(l.ctx, queueTask); err != nil {
l.Errorf("[CreateQuotaTask] enqueue task error: %v", err.Error())
return errors.Wrapf(xerr.NewErrCode(xerr.QueueEnqueueError), "enqueue task error")
}
logger.Infof("[CreateQuotaTask] Successfully created task with ID: %d", newTask.Id)
return nil
}
@@ -4,6 +4,7 @@ import (
"context"
"time"
"github.com/perfect-panel/server/internal/model/task"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
@@ -50,28 +51,29 @@ func (l *GetPreSendEmailCountLogic) GetPreSendEmailCount(req *types.GetPreSendEm
return query
}
var query *gorm.DB
switch req.Scope {
case "all":
scope := task.ParseScopeType(req.Scope)
switch scope {
case task.ScopeAll:
query = baseQuery()
case "active":
case task.ScopeActive:
query = baseQuery().
Joins("JOIN user_subscribe ON user.id = user_subscribe.user_id").
Where("user_subscribe.status IN ?", []int64{1, 2})
case "expired":
case task.ScopeExpired:
query = baseQuery().
Joins("JOIN user_subscribe ON user.id = user_subscribe.user_id").
Where("user_subscribe.status = ?", 3)
case "none":
case task.ScopeNone:
query = baseQuery().
Joins("LEFT JOIN user_subscribe ON user.id = user_subscribe.user_id").
Where("user_subscribe.user_id IS NULL")
case "skip":
case task.ScopeSkip:
// Skip scope does not require a count
query = nil
default:
l.Errorf("[CreateBatchSendEmailTask] Invalid scope: %v", req.Scope)
return nil, xerr.NewErrMsg("Invalid email scope")
@@ -3,6 +3,7 @@ package marketing
import (
"context"
"github.com/perfect-panel/server/internal/model/task"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/logger"
@@ -14,7 +15,7 @@ type QueryQuotaTaskListLogic struct {
svcCtx *svc.ServiceContext
}
// Query quota task list
// NewQueryQuotaTaskListLogic Query quota task list
func NewQueryQuotaTaskListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryQuotaTaskListLogic {
return &QueryQuotaTaskListLogic{
Logger: logger.WithContext(ctx),
@@ -24,7 +25,59 @@ func NewQueryQuotaTaskListLogic(ctx context.Context, svcCtx *svc.ServiceContext)
}
func (l *QueryQuotaTaskListLogic) QueryQuotaTaskList(req *types.QueryQuotaTaskListRequest) (resp *types.QueryQuotaTaskListResponse, err error) {
// todo: add your logic here and delete this line
var data []*task.Task
var count int64
query := l.svcCtx.DB.Model(&task.Task{}).Where("`type` = ?", task.TypeQuota)
if req.Page == 0 {
req.Page = 1
}
if req.Size == 0 {
req.Size = 20
}
return
if req.Status != nil {
query = query.Where("`status` = ?", *req.Status)
}
err = query.Count(&count).Offset((req.Page - 1) * req.Size).Limit(req.Size).Order("created_at DESC").Find(&data).Error
if err != nil {
l.Errorf("[QueryQuotaTaskList] failed to get quota tasks: %v", err)
return nil, err
}
var list []types.QuotaTask
for _, item := range data {
var scopeInfo task.QuotaScope
if err = scopeInfo.Unmarshal([]byte(item.Scope)); err != nil {
l.Errorf("[QueryQuotaTaskList] failed to unmarshal quota task scope: %v", err.Error())
continue
}
var contentInfo task.QuotaContent
if err = contentInfo.Unmarshal([]byte(item.Content)); err != nil {
l.Errorf("[QueryQuotaTaskList] failed to unmarshal quota task content: %v", err.Error())
continue
}
list = append(list, types.QuotaTask{
Id: item.Id,
Subscribers: scopeInfo.Subscribers,
IsActive: scopeInfo.IsActive,
StartTime: scopeInfo.StartTime,
EndTime: scopeInfo.EndTime,
ResetTraffic: contentInfo.ResetTraffic,
Days: contentInfo.Days,
GiftType: contentInfo.GiftType,
GiftValue: contentInfo.GiftValue,
Objects: scopeInfo.Objects,
Status: uint8(item.Status),
Total: int64(item.Total),
Current: int64(item.Current),
Errors: item.Errors,
CreatedAt: item.CreatedAt.UnixMilli(),
UpdatedAt: item.UpdatedAt.UnixMilli(),
})
}
return &types.QueryQuotaTaskListResponse{
Total: count,
List: list,
}, nil
}
@@ -2,7 +2,9 @@ package marketing
import (
"context"
"time"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/logger"
@@ -14,7 +16,7 @@ type QueryQuotaTaskPreCountLogic struct {
svcCtx *svc.ServiceContext
}
// Query quota task pre-count
// NewQueryQuotaTaskPreCountLogic Query quota task pre-count
func NewQueryQuotaTaskPreCountLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryQuotaTaskPreCountLogic {
return &QueryQuotaTaskPreCountLogic{
Logger: logger.WithContext(ctx),
@@ -24,7 +26,30 @@ func NewQueryQuotaTaskPreCountLogic(ctx context.Context, svcCtx *svc.ServiceCont
}
func (l *QueryQuotaTaskPreCountLogic) QueryQuotaTaskPreCount(req *types.QueryQuotaTaskPreCountRequest) (resp *types.QueryQuotaTaskPreCountResponse, err error) {
// todo: add your logic here and delete this line
tx := l.svcCtx.DB.WithContext(l.ctx).Model(&user.Subscribe{})
var count int64
return
if len(req.Subscribers) > 0 {
tx = tx.Where("`subscribe_id` IN ?", req.Subscribers)
}
if req.IsActive != nil && *req.IsActive {
tx = tx.Where("`status` IN ?", []int64{0, 1, 2}) // 0: Pending 1: Active 2: Finished
}
if req.StartTime != 0 {
start := time.UnixMilli(req.StartTime)
tx = tx.Where("`start_time` >= ?", start)
}
if req.EndTime != 0 {
end := time.UnixMilli(req.EndTime)
tx = tx.Where("`start_time` <= ?", end)
}
if err = tx.Count(&count).Error; err != nil {
l.Errorf("[QueryQuotaTaskPreCount] count error: %v", err.Error())
return nil, err
}
return &types.QueryQuotaTaskPreCountResponse{
Count: count,
}, nil
}
@@ -3,9 +3,12 @@ package marketing
import (
"context"
"github.com/perfect-panel/server/internal/model/task"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/xerr"
"github.com/pkg/errors"
)
type QueryQuotaTaskStatusLogic struct {
@@ -14,7 +17,7 @@ type QueryQuotaTaskStatusLogic struct {
svcCtx *svc.ServiceContext
}
// Query quota task status
// NewQueryQuotaTaskStatusLogic Query quota task status
func NewQueryQuotaTaskStatusLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryQuotaTaskStatusLogic {
return &QueryQuotaTaskStatusLogic{
Logger: logger.WithContext(ctx),
@@ -24,7 +27,16 @@ func NewQueryQuotaTaskStatusLogic(ctx context.Context, svcCtx *svc.ServiceContex
}
func (l *QueryQuotaTaskStatusLogic) QueryQuotaTaskStatus(req *types.QueryQuotaTaskStatusRequest) (resp *types.QueryQuotaTaskStatusResponse, err error) {
// todo: add your logic here and delete this line
return
var data *task.Task
err = l.svcCtx.DB.Model(&task.Task{}).Where("id = ? AND `type` = ?", req.Id, task.TypeQuota).First(&data).Error
if err != nil {
l.Errorf("[QueryQuotaTaskStatus] failed to get quota task: %v", err.Error())
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), " failed to get quota task: %v", err.Error())
}
return &types.QueryQuotaTaskStatusResponse{
Status: uint8(data.Status),
Current: int64(data.Current),
Total: int64(data.Total),
Errors: data.Errors,
}, nil
}
+1
View File
@@ -36,6 +36,7 @@ const (
ResetSubscribeTypeAuto uint16 = 231 // Auto reset
ResetSubscribeTypeAdvance uint16 = 232 // Advance reset
ResetSubscribeTypePaid uint16 = 233 // Paid reset
ResetSubscribeTypeQuota uint16 = 234 // Quota reset
BalanceTypeRecharge uint16 = 321 // Recharge
BalanceTypeWithdraw uint16 = 322 // Withdraw
BalanceTypePayment uint16 = 323 // Payment
+24 -15
View File
@@ -91,10 +91,11 @@ func (c *EmailContent) Unmarshal(data []byte) error {
}
type QuotaScope struct {
Type int8 `gorm:"not null;comment:Scope Type"`
RegisterStartTime int64 `json:"register_start_time"`
RegisterEndTime int64 `json:"register_end_time"`
Recipients []int64 `json:"recipients"` // list of user subs IDs
Subscribers []int64 `json:"subscribers"` // Subscribe IDs
IsActive *bool `json:"is_active"` // filter by active status
StartTime int64 `json:"start_time"` // filter by subscription start time
EndTime int64 `json:"end_time"` // filter by subscription end time
Objects []int64 `json:"recipients"` // list of user subs IDs
}
func (s *QuotaScope) Marshal() ([]byte, error) {
@@ -112,18 +113,26 @@ func (s *QuotaScope) Unmarshal(data []byte) error {
return json.Unmarshal(data, &aux)
}
type QuotaType int8
const (
QuotaTypeReset QuotaType = iota + 1 // Reset Subscribe Quota
QuotaTypeDays // Add Subscribe Days
QuotaTypeGift // Add Gift Amount
)
type QuotaContent struct {
Type int8 `json:"type"`
Days uint64 `json:"days,omitempty"` // days to add
Gift uint8 `json:"gift,omitempty"` // Invoice amount ratio(%) to gift amount
ResetTraffic bool `json:"reset_traffic"` // whether to reset traffic
Days uint64 `json:"days,omitempty"` // days to add
GiftType uint8 `json:"gift_type,omitempty"` // 1: Fixed, 2: Ratio
GiftValue uint64 `json:"gift_value,omitempty"` // value of the gift type
}
func (c *QuotaContent) Marshal() ([]byte, error) {
type Alias QuotaContent
return json.Marshal(&struct {
*Alias
}{
Alias: (*Alias)(c),
})
}
func (c *QuotaContent) Unmarshal(data []byte) error {
type Alias QuotaContent
aux := (*Alias)(c)
return json.Unmarshal(data, &aux)
}
func ParseScopeType(t int8) ScopeType {
+1 -1
View File
@@ -101,7 +101,7 @@ type customUserLogicModel interface {
DeleteDevice(ctx context.Context, id int64, tx ...*gorm.DB) error
ClearSubscribeCache(ctx context.Context, data ...*Subscribe) error
clearUserCache(ctx context.Context, data ...*User) error
ClearUserCache(ctx context.Context, data ...*User) error
QueryDailyUserStatisticsList(ctx context.Context, date time.Time) ([]UserStatisticsWithDate, error)
QueryMonthlyUserStatisticsList(ctx context.Context, date time.Time) ([]UserStatisticsWithDate, error)
+31 -27
View File
@@ -345,12 +345,14 @@ type CreatePaymentMethodRequest struct {
}
type CreateQuotaTaskRequest struct {
Scope int8 `json:"scope"`
RegisterStartTime int64 `json:"register_start_time"`
RegisterEndTime int64 `json:"register_end_time"`
QuotaType uint8 `json:"quota_type"`
Days uint64 `json:"days"` // Number of days for the quota
Gift uint8 `json:"gift"` // Invoice amount ratio(%) to gift amount for quota
Subscribers []int64 `json:"subscribers"`
IsActive *bool `json:"is_active"`
StartTime int64 `json:"start_time"`
EndTime int64 `json:"end_time"`
ResetTraffic bool `json:"reset_traffic"`
Days uint64 `json:"days"`
GiftType uint8 `json:"gift_type"`
GiftValue uint64 `json:"gift_value"`
}
type CreateServerRequest struct {
@@ -887,9 +889,9 @@ type GetPaymentMethodListResponse struct {
}
type GetPreSendEmailCountRequest struct {
Scope string `json:"scope"`
RegisterStartTime int64 `json:"register_start_time,omitempty"`
RegisterEndTime int64 `json:"register_end_time,omitempty"`
Scope int8 `json:"scope"`
RegisterStartTime int64 `json:"register_start_time,omitempty"`
RegisterEndTime int64 `json:"register_end_time,omitempty"`
}
type GetPreSendEmailCountResponse struct {
@@ -1534,7 +1536,6 @@ type QueryPurchaseOrderResponse struct {
type QueryQuotaTaskListRequest struct {
Page int `form:"page"`
Size int `form:"size"`
Scope *uint8 `form:"scope,omitempty"`
Status *uint8 `form:"status,omitempty"`
}
@@ -1544,9 +1545,10 @@ type QueryQuotaTaskListResponse struct {
}
type QueryQuotaTaskPreCountRequest struct {
Scope uint8 `json:"scope"`
RegisterStartTime int64 `json:"register_start_time"`
RegisterEndTime int64 `json:"register_end_time"`
Subscribers []int64 `json:"subscribers"`
IsActive *bool `json:"is_active"`
StartTime int64 `json:"start_time"`
EndTime int64 `json:"end_time"`
}
type QueryQuotaTaskPreCountResponse struct {
@@ -1614,20 +1616,22 @@ type QueryUserSubscribeListResponse struct {
}
type QuotaTask struct {
Id int64 `json:"id"`
Scope int8 `json:"scope"`
RegisterStartTime int64 `json:"register_start_time"`
RegisterEndTime int64 `json:"register_end_time"`
QuotaType uint8 `json:"quota_type"`
Days uint64 `json:"days"` // Number of days for the quota
Gift uint8 `json:"gift"` // Invoice amount ratio(%) to gift
Recipients []int64 `json:"recipients"` // UserSubscribe IDs of recipients
Status uint8 `json:"status"`
Total int64 `json:"total"`
Current int64 `json:"current"`
Errors string `json:"errors"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
Id int64 `json:"id"`
Subscribers []int64 `json:"subscribers"`
IsActive *bool `json:"is_active"`
StartTime int64 `json:"start_time"`
EndTime int64 `json:"end_time"`
ResetTraffic bool `json:"reset_traffic"`
Days uint64 `json:"days"`
GiftType uint8 `json:"gift_type"`
GiftValue uint64 `json:"gift_value"`
Objects []int64 `json:"objects"` // UserSubscribe IDs
Status uint8 `json:"status"`
Total int64 `json:"total"`
Current int64 `json:"current"`
Errors string `json:"errors"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
}
type RechargeOrderRequest struct {