feat(quota): add quota task creation and querying endpoints with updated data structures

This commit is contained in:
Chang lue Tsen 2025-09-09 13:39:05 -04:00
parent f4c6bd919b
commit d1be5febc3
18 changed files with 610 additions and 65 deletions

View File

@ -12,7 +12,7 @@ type (
CreateBatchSendEmailTaskRequest { CreateBatchSendEmailTaskRequest {
Subject string `json:"subject"` Subject string `json:"subject"`
Content string `json:"content"` Content string `json:"content"`
Scope string `json:"scope"` Scope int8 `json:"scope"`
RegisterStartTime int64 `json:"register_start_time,omitempty"` RegisterStartTime int64 `json:"register_start_time,omitempty"`
RegisterEndTime int64 `json:"register_end_time,omitempty"` RegisterEndTime int64 `json:"register_end_time,omitempty"`
Additional string `json:"additional,omitempty"` Additional string `json:"additional,omitempty"`
@ -25,7 +25,7 @@ type (
Subject string `json:"subject"` Subject string `json:"subject"`
Content string `json:"content"` Content string `json:"content"`
Recipients string `json:"recipients"` Recipients string `json:"recipients"`
Scope string `json:"scope"` Scope int8 `json:"scope"`
RegisterStartTime int64 `json:"register_start_time"` RegisterStartTime int64 `json:"register_start_time"`
RegisterEndTime int64 `json:"register_end_time"` RegisterEndTime int64 `json:"register_end_time"`
Additional string `json:"additional"` Additional string `json:"additional"`
@ -42,7 +42,7 @@ type (
GetBatchSendEmailTaskListRequest { GetBatchSendEmailTaskListRequest {
Page int `form:"page"` Page int `form:"page"`
Size int `form:"size"` Size int `form:"size"`
Scope string `form:"scope,omitempty"` Scope *int8 `form:"scope,omitempty"`
Status *uint8 `form:"status,omitempty"` Status *uint8 `form:"status,omitempty"`
} }
GetBatchSendEmailTaskListResponse { GetBatchSendEmailTaskListResponse {
@ -69,6 +69,57 @@ type (
Total int64 `json:"total"` Total int64 `json:"total"`
Errors string `json:"errors"` Errors string `json:"errors"`
} }
CreateQuotaTaskRequest {
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
}
QuotaTask {
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"`
}
QueryQuotaTaskPreCountRequest {
Scope uint8 `json:"scope"`
RegisterStartTime int64 `json:"register_start_time"`
RegisterEndTime int64 `json:"register_end_time"`
}
QueryQuotaTaskPreCountResponse {
Count int64 `json:"count"`
}
QueryQuotaTaskListRequest {
Page int `form:"page"`
Size int `form:"size"`
Scope *uint8 `form:"scope,omitempty"`
Status *uint8 `form:"status,omitempty"`
}
QueryQuotaTaskListResponse {
Total int64 `json:"total"`
List []QuotaTask `json:"list"`
}
QueryQuotaTaskStatusRequest {
Id int64 `json:"id"`
}
QueryQuotaTaskStatusResponse {
Status uint8 `json:"status"`
Current int64 `json:"current"`
Total int64 `json:"total"`
Errors string `json:"errors"`
}
) )
@server ( @server (
@ -96,5 +147,21 @@ service ppanel {
@doc "Get batch send email task status" @doc "Get batch send email task status"
@handler GetBatchSendEmailTaskStatus @handler GetBatchSendEmailTaskStatus
post /email/batch/status (GetBatchSendEmailTaskStatusRequest) returns (GetBatchSendEmailTaskStatusResponse) post /email/batch/status (GetBatchSendEmailTaskStatusRequest) returns (GetBatchSendEmailTaskStatusResponse)
@doc "Create a quota task"
@handler CreateQuotaTask
post /quota/create (CreateQuotaTaskRequest)
@doc "Query quota task pre-count"
@handler QueryQuotaTaskPreCount
post /quota/pre-count (QueryQuotaTaskPreCountRequest) returns (QueryQuotaTaskPreCountResponse)
@doc "Query quota task list"
@handler QueryQuotaTaskList
get /quota/list (QueryQuotaTaskListRequest) returns (QueryQuotaTaskListResponse)
@doc "Query quota task status"
@handler QueryQuotaTaskStatus
post /quota/status (QueryQuotaTaskStatusRequest) returns (QueryQuotaTaskStatusResponse)
} }

View File

@ -0,0 +1,14 @@
DROP TABLE IF EXISTS `email_task`;
CREATE TABLE `task` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
`type` tinyint NOT NULL COMMENT 'Task Type',
`scope` text COLLATE utf8mb4_general_ci COMMENT 'Task Scope',
`content` text COLLATE utf8mb4_general_ci COMMENT 'Task Content',
`status` tinyint NOT NULL DEFAULT '0' COMMENT 'Task Status: 0: Pending, 1: In Progress, 2: Completed, 3: Failed',
`errors` text COLLATE utf8mb4_general_ci COMMENT 'Task Errors',
`total` bigint unsigned NOT NULL DEFAULT '0' COMMENT 'Total Number',
`current` bigint unsigned NOT NULL DEFAULT '0' COMMENT 'Current Number',
`created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time',
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

View File

@ -0,0 +1,26 @@
package marketing
import (
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/logic/admin/marketing"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/result"
)
// Create a quota task
func CreateQuotaTaskHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
var req types.CreateQuotaTaskRequest
_ = c.ShouldBind(&req)
validateErr := svcCtx.Validate(&req)
if validateErr != nil {
result.ParamErrorResult(c, validateErr)
return
}
l := marketing.NewCreateQuotaTaskLogic(c.Request.Context(), svcCtx)
err := l.CreateQuotaTask(&req)
result.HttpResult(c, nil, err)
}
}

View File

@ -0,0 +1,26 @@
package marketing
import (
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/logic/admin/marketing"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/result"
)
// Query quota task list
func QueryQuotaTaskListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
var req types.QueryQuotaTaskListRequest
_ = c.ShouldBind(&req)
validateErr := svcCtx.Validate(&req)
if validateErr != nil {
result.ParamErrorResult(c, validateErr)
return
}
l := marketing.NewQueryQuotaTaskListLogic(c.Request.Context(), svcCtx)
resp, err := l.QueryQuotaTaskList(&req)
result.HttpResult(c, resp, err)
}
}

View File

@ -0,0 +1,26 @@
package marketing
import (
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/logic/admin/marketing"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/result"
)
// Query quota task pre-count
func QueryQuotaTaskPreCountHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
var req types.QueryQuotaTaskPreCountRequest
_ = c.ShouldBind(&req)
validateErr := svcCtx.Validate(&req)
if validateErr != nil {
result.ParamErrorResult(c, validateErr)
return
}
l := marketing.NewQueryQuotaTaskPreCountLogic(c.Request.Context(), svcCtx)
resp, err := l.QueryQuotaTaskPreCount(&req)
result.HttpResult(c, resp, err)
}
}

View File

@ -0,0 +1,26 @@
package marketing
import (
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/logic/admin/marketing"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/result"
)
// Query quota task status
func QueryQuotaTaskStatusHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
var req types.QueryQuotaTaskStatusRequest
_ = c.ShouldBind(&req)
validateErr := svcCtx.Validate(&req)
if validateErr != nil {
result.ParamErrorResult(c, validateErr)
return
}
l := marketing.NewQueryQuotaTaskStatusLogic(c.Request.Context(), svcCtx)
resp, err := l.QueryQuotaTaskStatus(&req)
result.HttpResult(c, resp, err)
}
}

View File

@ -253,6 +253,18 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
// Stop a batch send email task // Stop a batch send email task
adminMarketingGroupRouter.POST("/email/batch/stop", adminMarketing.StopBatchSendEmailTaskHandler(serverCtx)) adminMarketingGroupRouter.POST("/email/batch/stop", adminMarketing.StopBatchSendEmailTaskHandler(serverCtx))
// Create a quota task
adminMarketingGroupRouter.POST("/quota/create", adminMarketing.CreateQuotaTaskHandler(serverCtx))
// Query quota task list
adminMarketingGroupRouter.GET("/quota/list", adminMarketing.QueryQuotaTaskListHandler(serverCtx))
// Query quota task pre-count
adminMarketingGroupRouter.POST("/quota/pre-count", adminMarketing.QueryQuotaTaskPreCountHandler(serverCtx))
// Query quota task status
adminMarketingGroupRouter.POST("/quota/status", adminMarketing.QueryQuotaTaskStatusHandler(serverCtx))
} }
adminOrderGroupRouter := router.Group("/v1/admin/order") adminOrderGroupRouter := router.Group("/v1/admin/order")

View File

@ -24,7 +24,7 @@ type CreateBatchSendEmailTaskLogic struct {
svcCtx *svc.ServiceContext svcCtx *svc.ServiceContext
} }
// Create a batch send email task // NewCreateBatchSendEmailTaskLogic Create a batch send email task
func NewCreateBatchSendEmailTaskLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateBatchSendEmailTaskLogic { func NewCreateBatchSendEmailTaskLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateBatchSendEmailTaskLogic {
return &CreateBatchSendEmailTaskLogic{ return &CreateBatchSendEmailTaskLogic{
Logger: logger.WithContext(ctx), Logger: logger.WithContext(ctx),
@ -55,24 +55,27 @@ func (l *CreateBatchSendEmailTaskLogic) CreateBatchSendEmailTask(req *types.Crea
var query *gorm.DB var query *gorm.DB
switch req.Scope { scope := task.ParseScopeType(req.Scope)
case "all":
switch scope {
case task.ScopeAll:
query = baseQuery() query = baseQuery()
case "active": case task.ScopeActive:
query = baseQuery(). query = baseQuery().
Joins("JOIN user_subscribe ON user.id = user_subscribe.user_id"). Joins("JOIN user_subscribe ON user.id = user_subscribe.user_id").
Where("user_subscribe.status IN ?", []int64{1, 2}) Where("user_subscribe.status IN ?", []int64{1, 2})
case "expired": case task.ScopeExpired:
query = baseQuery(). query = baseQuery().
Joins("JOIN user_subscribe ON user.id = user_subscribe.user_id"). Joins("JOIN user_subscribe ON user.id = user_subscribe.user_id").
Where("user_subscribe.status = ?", 3) Where("user_subscribe.status = ?", 3)
case "none": case task.ScopeNone:
query = baseQuery(). query = baseQuery().
Joins("LEFT JOIN user_subscribe ON user.id = user_subscribe.user_id"). Joins("LEFT JOIN user_subscribe ON user.id = user_subscribe.user_id").
Where("user_subscribe.user_id IS NULL") Where("user_subscribe.user_id IS NULL")
default:
} }
if query != nil { if query != nil {
@ -85,7 +88,7 @@ func (l *CreateBatchSendEmailTaskLogic) CreateBatchSendEmailTask(req *types.Crea
} }
// 邮箱列表为空,返回错误 // 邮箱列表为空,返回错误
if len(emails) == 0 && req.Scope != "skip" { if len(emails) == 0 && scope != task.ScopeSkip {
l.Errorf("[CreateBatchSendEmailTask] No email addresses found for the specified scope") l.Errorf("[CreateBatchSendEmailTask] No email addresses found for the specified scope")
return xerr.NewErrMsg("No email addresses found for the specified scope") return xerr.NewErrMsg("No email addresses found for the specified scope")
} }
@ -96,41 +99,59 @@ func (l *CreateBatchSendEmailTaskLogic) CreateBatchSendEmailTask(req *types.Crea
var additionalEmails []string var additionalEmails []string
// 追加额外的邮箱地址(不覆盖) // 追加额外的邮箱地址(不覆盖)
if req.Additional != "" { if req.Additional != "" {
additionalEmails = strings.Split(req.Additional, "\n") additionalEmails = tool.RemoveDuplicateElements(strings.Split(req.Additional, "\n")...)
} }
if len(additionalEmails) == 0 && req.Scope == "skip" { if len(additionalEmails) == 0 && scope == task.ScopeSkip {
l.Errorf("[CreateBatchSendEmailTask] No additional email addresses provided for skip scope") l.Errorf("[CreateBatchSendEmailTask] No additional email addresses provided for skip scope")
return xerr.NewErrMsg("No additional email addresses provided for skip scope") return xerr.NewErrMsg("No additional email addresses provided for skip scope")
} }
var scheduledAt time.Time scheduledAt := time.Now().Add(10 * time.Second) // 默认延迟10秒执行,防止任务创建和执行时间过于接近
if req.Scheduled == 0 { if req.Scheduled != 0 {
scheduledAt = time.Now()
} else {
scheduledAt = time.Unix(req.Scheduled, 0) scheduledAt = time.Unix(req.Scheduled, 0)
if scheduledAt.Before(time.Now()) { if scheduledAt.Before(time.Now()) {
scheduledAt = time.Now() scheduledAt = time.Now()
} }
} }
taskInfo := &task.EmailTask{ scopeInfo := task.EmailScope{
Subject: req.Subject, Type: scope.Int8(),
Content: req.Content, RegisterStartTime: req.RegisterStartTime,
Recipients: strings.Join(emails, "\n"), RegisterEndTime: req.RegisterEndTime,
Scope: req.Scope, Recipients: emails,
RegisterStartTime: time.Unix(req.RegisterStartTime, 0), Additional: additionalEmails,
RegisterEndTime: time.Unix(req.RegisterEndTime, 0), Scheduled: req.Scheduled,
Additional: req.Additional,
Scheduled: scheduledAt,
Interval: req.Interval, Interval: req.Interval,
Limit: req.Limit, Limit: req.Limit,
}
scopeBytes, _ := scopeInfo.Marshal()
taskContent := task.EmailContent{
Subject: req.Subject,
Content: req.Content,
}
contentBytes, _ := taskContent.Marshal()
var total uint64
if additionalEmails != nil {
list := append(emails, additionalEmails...)
total = uint64(len(tool.RemoveDuplicateElements(list...)))
} else {
total = uint64(len(emails))
}
taskInfo := &task.Task{
Type: task.TypeEmail,
Scope: string(scopeBytes),
Content: string(contentBytes),
Status: 0, Status: 0,
Errors: "", Errors: "",
Total: uint64(len(emails) + len(additionalEmails)), Total: total,
Current: 0, Current: 0,
} }
if err = l.svcCtx.DB.Model(&task.EmailTask{}).Create(taskInfo).Error; err != nil { if err = l.svcCtx.DB.Model(&task.Task{}).Create(taskInfo).Error; err != nil {
l.Errorf("[CreateBatchSendEmailTask] Failed to create email task: %v", err.Error()) l.Errorf("[CreateBatchSendEmailTask] Failed to create email task: %v", err.Error())
return xerr.NewErrCode(xerr.DatabaseInsertError) return xerr.NewErrCode(xerr.DatabaseInsertError)
} }
@ -138,12 +159,12 @@ func (l *CreateBatchSendEmailTaskLogic) CreateBatchSendEmailTask(req *types.Crea
l.Infof("[CreateBatchSendEmailTask] Successfully created email task with ID: %d", taskInfo.Id) l.Infof("[CreateBatchSendEmailTask] Successfully created email task with ID: %d", taskInfo.Id)
t := asynq.NewTask(types2.ScheduledBatchSendEmail, []byte(strconv.FormatInt(taskInfo.Id, 10))) t := asynq.NewTask(types2.ScheduledBatchSendEmail, []byte(strconv.FormatInt(taskInfo.Id, 10)))
info, err := l.svcCtx.Queue.EnqueueContext(l.ctx, t, asynq.ProcessAt(taskInfo.Scheduled)) info, err := l.svcCtx.Queue.EnqueueContext(l.ctx, t, asynq.ProcessAt(scheduledAt))
if err != nil { if err != nil {
l.Errorf("[CreateBatchSendEmailTask] Failed to enqueue email task: %v", err.Error()) l.Errorf("[CreateBatchSendEmailTask] Failed to enqueue email task: %v", err.Error())
return xerr.NewErrCode(xerr.QueueEnqueueError) return xerr.NewErrCode(xerr.QueueEnqueueError)
} }
l.Infof("[CreateBatchSendEmailTask] Successfully enqueued email task with ID: %s, scheduled at: %s", info.ID, taskInfo.Scheduled) l.Infof("[CreateBatchSendEmailTask] Successfully enqueued email task with ID: %s, scheduled at: %s", info.ID, scheduledAt.Format(time.DateTime))
return nil return nil
} }

View File

@ -0,0 +1,30 @@
package marketing
import (
"context"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/logger"
)
type CreateQuotaTaskLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// Create a quota task
func NewCreateQuotaTaskLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateQuotaTaskLogic {
return &CreateQuotaTaskLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *CreateQuotaTaskLogic) CreateQuotaTask(req *types.CreateQuotaTaskRequest) error {
// todo: add your logic here and delete this line
return nil
}

View File

@ -2,12 +2,12 @@ package marketing
import ( import (
"context" "context"
"strings"
"github.com/perfect-panel/server/internal/model/task" "github.com/perfect-panel/server/internal/model/task"
"github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/logger" "github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/tool"
"github.com/perfect-panel/server/pkg/xerr" "github.com/perfect-panel/server/pkg/xerr"
) )
@ -28,12 +28,12 @@ func NewGetBatchSendEmailTaskListLogic(ctx context.Context, svcCtx *svc.ServiceC
func (l *GetBatchSendEmailTaskListLogic) GetBatchSendEmailTaskList(req *types.GetBatchSendEmailTaskListRequest) (resp *types.GetBatchSendEmailTaskListResponse, err error) { func (l *GetBatchSendEmailTaskListLogic) GetBatchSendEmailTaskList(req *types.GetBatchSendEmailTaskListRequest) (resp *types.GetBatchSendEmailTaskListResponse, err error) {
var tasks []*task.EmailTask var tasks []*task.Task
tx := l.svcCtx.DB.Model(&task.EmailTask{}) tx := l.svcCtx.DB.Model(&task.Task{}).Where("`type` = ?", task.TypeEmail)
if req.Status != nil { if req.Status != nil {
tx = tx.Where("status = ?", *req.Status) tx = tx.Where("status = ?", *req.Status)
} }
if req.Scope != "" { if req.Scope != nil {
tx = tx.Where("scope = ?", req.Scope) tx = tx.Where("scope = ?", req.Scope)
} }
if req.Page == 0 { if req.Page == 0 {
@ -49,7 +49,40 @@ func (l *GetBatchSendEmailTaskListLogic) GetBatchSendEmailTaskList(req *types.Ge
} }
list := make([]types.BatchSendEmailTask, 0) list := make([]types.BatchSendEmailTask, 0)
tool.DeepCopy(&list, tasks)
for _, t := range tasks {
var scopeInfo task.EmailScope
if err = scopeInfo.Unmarshal([]byte(t.Scope)); err != nil {
l.Errorf("[GetBatchSendEmailTaskList] failed to unmarshal email task scope: %v", err.Error())
continue
}
var contentInfo task.EmailContent
if err = contentInfo.Unmarshal([]byte(t.Content)); err != nil {
l.Errorf("[GetBatchSendEmailTaskList] failed to unmarshal email task content: %v", err.Error())
continue
}
list = append(list, types.BatchSendEmailTask{
Id: t.Id,
Subject: contentInfo.Subject,
Content: contentInfo.Content,
Recipients: strings.Join(scopeInfo.Recipients, "\n"),
Scope: scopeInfo.Type,
RegisterStartTime: scopeInfo.RegisterStartTime,
RegisterEndTime: scopeInfo.RegisterEndTime,
Additional: strings.Join(scopeInfo.Additional, "\n"),
Scheduled: scopeInfo.Scheduled,
Interval: scopeInfo.Interval,
Limit: scopeInfo.Limit,
Status: uint8(t.Status),
Errors: t.Errors,
Total: t.Total,
Current: t.Current,
CreatedAt: t.CreatedAt.UnixMilli(),
UpdatedAt: t.UpdatedAt.UnixMilli(),
})
}
return &types.GetBatchSendEmailTaskListResponse{ return &types.GetBatchSendEmailTaskListResponse{
List: list, List: list,
}, nil }, nil

View File

@ -0,0 +1,30 @@
package marketing
import (
"context"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/logger"
)
type QueryQuotaTaskListLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// Query quota task list
func NewQueryQuotaTaskListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryQuotaTaskListLogic {
return &QueryQuotaTaskListLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *QueryQuotaTaskListLogic) QueryQuotaTaskList(req *types.QueryQuotaTaskListRequest) (resp *types.QueryQuotaTaskListResponse, err error) {
// todo: add your logic here and delete this line
return
}

View File

@ -0,0 +1,30 @@
package marketing
import (
"context"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/logger"
)
type QueryQuotaTaskPreCountLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// Query quota task pre-count
func NewQueryQuotaTaskPreCountLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryQuotaTaskPreCountLogic {
return &QueryQuotaTaskPreCountLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *QueryQuotaTaskPreCountLogic) QueryQuotaTaskPreCount(req *types.QueryQuotaTaskPreCountRequest) (resp *types.QueryQuotaTaskPreCountResponse, err error) {
// todo: add your logic here and delete this line
return
}

View File

@ -0,0 +1,30 @@
package marketing
import (
"context"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/logger"
)
type QueryQuotaTaskStatusLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// Query quota task status
func NewQueryQuotaTaskStatusLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryQuotaTaskStatusLogic {
return &QueryQuotaTaskStatusLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *QueryQuotaTaskStatusLogic) QueryQuotaTaskStatus(req *types.QueryQuotaTaskStatusRequest) (resp *types.QueryQuotaTaskStatusResponse, err error) {
// todo: add your logic here and delete this line
return
}

View File

@ -1,27 +1,142 @@
package task package task
import "time" import (
"encoding/json"
"time"
)
type EmailTask struct { type Type int8
Id int64 `gorm:"column:id;primaryKey;autoIncrement;comment:ID"`
Subject string `gorm:"column:subject;type:varchar(255);not null;comment:Email Subject"` const (
Content string `gorm:"column:content;type:text;not null;comment:Email Content"` Undefined Type = -1
Recipients string `gorm:"column:recipient;type:text;not null;comment:Email Recipient"` TypeEmail = iota
Scope string `gorm:"column:scope;type:varchar(50);not null;comment:Email Scope"` TypeQuota
RegisterStartTime time.Time `gorm:"column:register_start_time;default:null;comment:Register Start Time"` )
RegisterEndTime time.Time `gorm:"column:register_end_time;default:null;comment:Register End Time"`
Additional string `gorm:"column:additional;type:text;default:null;comment:Additional Information"` type Task struct {
Scheduled time.Time `gorm:"column:scheduled;not null;comment:Scheduled Time"` Id int64 `gorm:"primaryKey;autoIncrement;comment:ID"`
Interval uint8 `gorm:"column:interval;not null;comment:Interval in Seconds"` Type int8 `gorm:"not null;comment:Task Type"`
Limit uint64 `gorm:"column:limit;not null;comment:Daily send limit"` Scope string `gorm:"type:text;comment:Task Scope"`
Status uint8 `gorm:"column:status;not null;comment:Daily Status"` Content string `gorm:"type:text;comment:Task Content"`
Errors string `gorm:"column:errors;type:text;not null;comment:Errors"` Status int8 `gorm:"not null;default:0;comment:Task Status: 0: Pending, 1: In Progress, 2: Completed, 3: Failed"`
Errors string `gorm:"type:text;comment:Task Errors"`
Total uint64 `gorm:"column:total;not null;default:0;comment:Total Number"` Total uint64 `gorm:"column:total;not null;default:0;comment:Total Number"`
Current uint64 `gorm:"column:current;not null;default:0;comment:Current Number"` Current uint64 `gorm:"column:current;not null;default:0;comment:Current Number"`
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
UpdatedAt time.Time `gorm:"comment:Update Time"` UpdatedAt time.Time `gorm:"comment:Update Time"`
} }
func (EmailTask) TableName() string { func (Task) TableName() string {
return "email_task" return "task"
}
type ScopeType int8
const (
ScopeAll ScopeType = iota + 1 // All users
ScopeActive // Active users
ScopeExpired // Expired users
ScopeNone // No Subscribe
ScopeSkip // Skip user filtering
)
func (t ScopeType) Int8() int8 {
return int8(t)
}
type EmailScope struct {
Type int8 `gorm:"not null;comment:Scope Type"`
RegisterStartTime int64 `json:"register_start_time"`
RegisterEndTime int64 `json:"register_end_time"`
Recipients []string `json:"recipients"` // list of email addresses
Additional []string `json:"additional"` // additional email addresses
Scheduled int64 `json:"scheduled"` // scheduled time (unix timestamp)
Interval uint8 `json:"interval"` // interval in seconds
Limit uint64 `json:"limit"` // daily send limit
}
func (s *EmailScope) Marshal() ([]byte, error) {
type Alias EmailScope
return json.Marshal(&struct {
*Alias
}{
Alias: (*Alias)(s),
})
}
func (s *EmailScope) Unmarshal(data []byte) error {
type Alias EmailScope
aux := (*Alias)(s)
return json.Unmarshal(data, &aux)
}
type EmailContent struct {
Subject string `json:"subject"`
Content string `json:"content"`
}
func (c *EmailContent) Marshal() ([]byte, error) {
type Alias EmailContent
return json.Marshal(&struct {
*Alias
}{
Alias: (*Alias)(c),
})
}
func (c *EmailContent) Unmarshal(data []byte) error {
type Alias EmailContent
aux := (*Alias)(c)
return json.Unmarshal(data, &aux)
}
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
}
func (s *QuotaScope) Marshal() ([]byte, error) {
type Alias QuotaScope
return json.Marshal(&struct {
*Alias
}{
Alias: (*Alias)(s),
})
}
func (s *QuotaScope) Unmarshal(data []byte) error {
type Alias QuotaScope
aux := (*Alias)(s)
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
}
func ParseScopeType(t int8) ScopeType {
switch t {
case 1:
return ScopeAll
case 2:
return ScopeActive
case 3:
return ScopeExpired
case 4:
return ScopeNone
default:
return ScopeSkip
}
} }

View File

@ -162,7 +162,7 @@ type BatchSendEmailTask struct {
Subject string `json:"subject"` Subject string `json:"subject"`
Content string `json:"content"` Content string `json:"content"`
Recipients string `json:"recipients"` Recipients string `json:"recipients"`
Scope string `json:"scope"` Scope int8 `json:"scope"`
RegisterStartTime int64 `json:"register_start_time"` RegisterStartTime int64 `json:"register_start_time"`
RegisterEndTime int64 `json:"register_end_time"` RegisterEndTime int64 `json:"register_end_time"`
Additional string `json:"additional"` Additional string `json:"additional"`
@ -274,7 +274,7 @@ type CreateAnnouncementRequest struct {
type CreateBatchSendEmailTaskRequest struct { type CreateBatchSendEmailTaskRequest struct {
Subject string `json:"subject"` Subject string `json:"subject"`
Content string `json:"content"` Content string `json:"content"`
Scope string `json:"scope"` Scope int8 `json:"scope"`
RegisterStartTime int64 `json:"register_start_time,omitempty"` RegisterStartTime int64 `json:"register_start_time,omitempty"`
RegisterEndTime int64 `json:"register_end_time,omitempty"` RegisterEndTime int64 `json:"register_end_time,omitempty"`
Additional string `json:"additional,omitempty"` Additional string `json:"additional,omitempty"`
@ -344,6 +344,15 @@ type CreatePaymentMethodRequest struct {
Enable *bool `json:"enable" validate:"required"` Enable *bool `json:"enable" validate:"required"`
} }
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
}
type CreateServerRequest struct { type CreateServerRequest struct {
Name string `json:"name"` Name string `json:"name"`
Country string `json:"country,omitempty"` Country string `json:"country,omitempty"`
@ -756,7 +765,7 @@ type GetAvailablePaymentMethodsResponse struct {
type GetBatchSendEmailTaskListRequest struct { type GetBatchSendEmailTaskListRequest struct {
Page int `form:"page"` Page int `form:"page"`
Size int `form:"size"` Size int `form:"size"`
Scope string `form:"scope,omitempty"` Scope *int8 `form:"scope,omitempty"`
Status *uint8 `form:"status,omitempty"` Status *uint8 `form:"status,omitempty"`
} }
@ -1522,6 +1531,39 @@ type QueryPurchaseOrderResponse struct {
Token string `json:"token,omitempty"` Token string `json:"token,omitempty"`
} }
type QueryQuotaTaskListRequest struct {
Page int `form:"page"`
Size int `form:"size"`
Scope *uint8 `form:"scope,omitempty"`
Status *uint8 `form:"status,omitempty"`
}
type QueryQuotaTaskListResponse struct {
Total int64 `json:"total"`
List []QuotaTask `json:"list"`
}
type QueryQuotaTaskPreCountRequest struct {
Scope uint8 `json:"scope"`
RegisterStartTime int64 `json:"register_start_time"`
RegisterEndTime int64 `json:"register_end_time"`
}
type QueryQuotaTaskPreCountResponse struct {
Count int64 `json:"count"`
}
type QueryQuotaTaskStatusRequest struct {
Id int64 `json:"id"`
}
type QueryQuotaTaskStatusResponse struct {
Status uint8 `json:"status"`
Current int64 `json:"current"`
Total int64 `json:"total"`
Errors string `json:"errors"`
}
type QuerySubscribeGroupListResponse struct { type QuerySubscribeGroupListResponse struct {
List []SubscribeGroup `json:"list"` List []SubscribeGroup `json:"list"`
Total int64 `json:"total"` Total int64 `json:"total"`
@ -1571,6 +1613,23 @@ type QueryUserSubscribeListResponse struct {
Total int64 `json:"total"` Total int64 `json:"total"`
} }
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"`
}
type RechargeOrderRequest struct { type RechargeOrderRequest struct {
Amount int64 `json:"amount"` Amount int64 `json:"amount"`
Payment int64 `json:"payment"` Payment int64 `json:"payment"`

View File

@ -3,7 +3,7 @@ package orm
import ( import (
"testing" "testing"
"github.com/perfect-panel/server/internal/model/node" "github.com/perfect-panel/server/internal/model/task"
"gorm.io/driver/mysql" "gorm.io/driver/mysql"
"gorm.io/gorm" "gorm.io/gorm"
@ -31,7 +31,7 @@ func TestMysql(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Failed to connect to MySQL: %v", err) t.Fatalf("Failed to connect to MySQL: %v", err)
} }
err = db.Migrator().AutoMigrate(&node.Server{}) err = db.Migrator().AutoMigrate(&task.Task{})
if err != nil { if err != nil {
t.Fatalf("Failed to auto migrate: %v", err) t.Fatalf("Failed to auto migrate: %v", err)
return return

View File

@ -44,8 +44,8 @@ func (l *BatchEmailLogic) ProcessTask(ctx context.Context, task *asynq.Task) err
return asynq.SkipRetry return asynq.SkipRetry
} }
tx := l.svcCtx.DB.WithContext(ctx) tx := l.svcCtx.DB.WithContext(ctx)
var taskInfo taskModel.EmailTask var taskInfo taskModel.Task
if err = tx.Model(&taskModel.EmailTask{}).Where("id = ?", taskID).First(&taskInfo).Error; err != nil { if err = tx.Model(&taskModel.Task{}).Where("id = ?", taskID).First(&taskInfo).Error; err != nil {
logger.WithContext(ctx).Error("[BatchEmailLogic] ProcessTask failed", logger.WithContext(ctx).Error("[BatchEmailLogic] ProcessTask failed",
logger.Field("error", err.Error()), logger.Field("error", err.Error()),
logger.Field("taskID", taskID), logger.Field("taskID", taskID),