From d1be5febc3562f0815581ea492c0913411f8e131 Mon Sep 17 00:00:00 2001 From: Chang lue Tsen Date: Tue, 9 Sep 2025 13:39:05 -0400 Subject: [PATCH] feat(quota): add quota task creation and querying endpoints with updated data structures --- apis/admin/marketing.api | 73 +++++++- .../migrate/database/02113_task.down.sql | 0 initialize/migrate/database/02113_task.up.sql | 14 ++ .../admin/marketing/createQuotaTaskHandler.go | 26 +++ .../marketing/queryQuotaTaskListHandler.go | 26 +++ .../queryQuotaTaskPreCountHandler.go | 26 +++ .../marketing/queryQuotaTaskStatusHandler.go | 26 +++ internal/handler/routes.go | 12 ++ .../createBatchSendEmailTaskLogic.go | 79 +++++---- .../admin/marketing/createQuotaTaskLogic.go | 30 ++++ .../getBatchSendEmailTaskListLogic.go | 43 ++++- .../marketing/queryQuotaTaskListLogic.go | 30 ++++ .../marketing/queryQuotaTaskPreCountLogic.go | 30 ++++ .../marketing/queryQuotaTaskStatusLogic.go | 30 ++++ internal/model/task/task.go | 157 +++++++++++++++--- internal/types/types.go | 65 +++++++- pkg/orm/tool_test.go | 4 +- queue/logic/email/batchEmailLogic.go | 4 +- 18 files changed, 610 insertions(+), 65 deletions(-) create mode 100644 initialize/migrate/database/02113_task.down.sql create mode 100644 initialize/migrate/database/02113_task.up.sql create mode 100644 internal/handler/admin/marketing/createQuotaTaskHandler.go create mode 100644 internal/handler/admin/marketing/queryQuotaTaskListHandler.go create mode 100644 internal/handler/admin/marketing/queryQuotaTaskPreCountHandler.go create mode 100644 internal/handler/admin/marketing/queryQuotaTaskStatusHandler.go create mode 100644 internal/logic/admin/marketing/createQuotaTaskLogic.go create mode 100644 internal/logic/admin/marketing/queryQuotaTaskListLogic.go create mode 100644 internal/logic/admin/marketing/queryQuotaTaskPreCountLogic.go create mode 100644 internal/logic/admin/marketing/queryQuotaTaskStatusLogic.go diff --git a/apis/admin/marketing.api b/apis/admin/marketing.api index 3f039ff..dd0a1fa 100644 --- a/apis/admin/marketing.api +++ b/apis/admin/marketing.api @@ -12,7 +12,7 @@ type ( CreateBatchSendEmailTaskRequest { Subject string `json:"subject"` Content string `json:"content"` - Scope string `json:"scope"` + Scope int8 `json:"scope"` RegisterStartTime int64 `json:"register_start_time,omitempty"` RegisterEndTime int64 `json:"register_end_time,omitempty"` Additional string `json:"additional,omitempty"` @@ -25,7 +25,7 @@ type ( Subject string `json:"subject"` Content string `json:"content"` Recipients string `json:"recipients"` - Scope string `json:"scope"` + Scope int8 `json:"scope"` RegisterStartTime int64 `json:"register_start_time"` RegisterEndTime int64 `json:"register_end_time"` Additional string `json:"additional"` @@ -42,7 +42,7 @@ type ( GetBatchSendEmailTaskListRequest { Page int `form:"page"` Size int `form:"size"` - Scope string `form:"scope,omitempty"` + Scope *int8 `form:"scope,omitempty"` Status *uint8 `form:"status,omitempty"` } GetBatchSendEmailTaskListResponse { @@ -69,6 +69,57 @@ type ( Total int64 `json:"total"` 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 ( @@ -96,5 +147,21 @@ service ppanel { @doc "Get batch send email task status" @handler GetBatchSendEmailTaskStatus 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) } diff --git a/initialize/migrate/database/02113_task.down.sql b/initialize/migrate/database/02113_task.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/initialize/migrate/database/02113_task.up.sql b/initialize/migrate/database/02113_task.up.sql new file mode 100644 index 0000000..4e7b170 --- /dev/null +++ b/initialize/migrate/database/02113_task.up.sql @@ -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; \ No newline at end of file diff --git a/internal/handler/admin/marketing/createQuotaTaskHandler.go b/internal/handler/admin/marketing/createQuotaTaskHandler.go new file mode 100644 index 0000000..0fb088b --- /dev/null +++ b/internal/handler/admin/marketing/createQuotaTaskHandler.go @@ -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) + } +} diff --git a/internal/handler/admin/marketing/queryQuotaTaskListHandler.go b/internal/handler/admin/marketing/queryQuotaTaskListHandler.go new file mode 100644 index 0000000..3aaebdc --- /dev/null +++ b/internal/handler/admin/marketing/queryQuotaTaskListHandler.go @@ -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) + } +} diff --git a/internal/handler/admin/marketing/queryQuotaTaskPreCountHandler.go b/internal/handler/admin/marketing/queryQuotaTaskPreCountHandler.go new file mode 100644 index 0000000..bcf6bd7 --- /dev/null +++ b/internal/handler/admin/marketing/queryQuotaTaskPreCountHandler.go @@ -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) + } +} diff --git a/internal/handler/admin/marketing/queryQuotaTaskStatusHandler.go b/internal/handler/admin/marketing/queryQuotaTaskStatusHandler.go new file mode 100644 index 0000000..8d6cf9c --- /dev/null +++ b/internal/handler/admin/marketing/queryQuotaTaskStatusHandler.go @@ -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) + } +} diff --git a/internal/handler/routes.go b/internal/handler/routes.go index 5aa2f15..cdad62e 100644 --- a/internal/handler/routes.go +++ b/internal/handler/routes.go @@ -253,6 +253,18 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // Stop a batch send email task 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") diff --git a/internal/logic/admin/marketing/createBatchSendEmailTaskLogic.go b/internal/logic/admin/marketing/createBatchSendEmailTaskLogic.go index b45ad0d..d2f181b 100644 --- a/internal/logic/admin/marketing/createBatchSendEmailTaskLogic.go +++ b/internal/logic/admin/marketing/createBatchSendEmailTaskLogic.go @@ -24,7 +24,7 @@ type CreateBatchSendEmailTaskLogic struct { 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 { return &CreateBatchSendEmailTaskLogic{ Logger: logger.WithContext(ctx), @@ -55,24 +55,27 @@ func (l *CreateBatchSendEmailTaskLogic) CreateBatchSendEmailTask(req *types.Crea 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") + default: } 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") 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 // 追加额外的邮箱地址(不覆盖) 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") return xerr.NewErrMsg("No additional email addresses provided for skip scope") } - var scheduledAt time.Time - if req.Scheduled == 0 { - scheduledAt = time.Now() - } else { + scheduledAt := time.Now().Add(10 * time.Second) // 默认延迟10秒执行,防止任务创建和执行时间过于接近 + if req.Scheduled != 0 { scheduledAt = time.Unix(req.Scheduled, 0) if scheduledAt.Before(time.Now()) { scheduledAt = time.Now() } } - taskInfo := &task.EmailTask{ - Subject: req.Subject, - Content: req.Content, - Recipients: strings.Join(emails, "\n"), - Scope: req.Scope, - RegisterStartTime: time.Unix(req.RegisterStartTime, 0), - RegisterEndTime: time.Unix(req.RegisterEndTime, 0), - Additional: req.Additional, - Scheduled: scheduledAt, + scopeInfo := task.EmailScope{ + Type: scope.Int8(), + RegisterStartTime: req.RegisterStartTime, + RegisterEndTime: req.RegisterEndTime, + Recipients: emails, + Additional: additionalEmails, + Scheduled: req.Scheduled, Interval: req.Interval, Limit: req.Limit, - Status: 0, - Errors: "", - Total: uint64(len(emails) + len(additionalEmails)), - Current: 0, + } + scopeBytes, _ := scopeInfo.Marshal() + + taskContent := task.EmailContent{ + Subject: req.Subject, + Content: req.Content, } - if err = l.svcCtx.DB.Model(&task.EmailTask{}).Create(taskInfo).Error; err != nil { + 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, + Errors: "", + Total: total, + Current: 0, + } + + if err = l.svcCtx.DB.Model(&task.Task{}).Create(taskInfo).Error; err != nil { l.Errorf("[CreateBatchSendEmailTask] Failed to create email task: %v", err.Error()) 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) 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 { l.Errorf("[CreateBatchSendEmailTask] Failed to enqueue email task: %v", err.Error()) 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 } diff --git a/internal/logic/admin/marketing/createQuotaTaskLogic.go b/internal/logic/admin/marketing/createQuotaTaskLogic.go new file mode 100644 index 0000000..5e052dc --- /dev/null +++ b/internal/logic/admin/marketing/createQuotaTaskLogic.go @@ -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 +} diff --git a/internal/logic/admin/marketing/getBatchSendEmailTaskListLogic.go b/internal/logic/admin/marketing/getBatchSendEmailTaskListLogic.go index ec4f1bf..e3a3c7b 100644 --- a/internal/logic/admin/marketing/getBatchSendEmailTaskListLogic.go +++ b/internal/logic/admin/marketing/getBatchSendEmailTaskListLogic.go @@ -2,12 +2,12 @@ package marketing import ( "context" + "strings" "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/tool" "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) { - var tasks []*task.EmailTask - tx := l.svcCtx.DB.Model(&task.EmailTask{}) + var tasks []*task.Task + tx := l.svcCtx.DB.Model(&task.Task{}).Where("`type` = ?", task.TypeEmail) if req.Status != nil { tx = tx.Where("status = ?", *req.Status) } - if req.Scope != "" { + if req.Scope != nil { tx = tx.Where("scope = ?", req.Scope) } if req.Page == 0 { @@ -49,7 +49,40 @@ func (l *GetBatchSendEmailTaskListLogic) GetBatchSendEmailTaskList(req *types.Ge } 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{ List: list, }, nil diff --git a/internal/logic/admin/marketing/queryQuotaTaskListLogic.go b/internal/logic/admin/marketing/queryQuotaTaskListLogic.go new file mode 100644 index 0000000..fa4aae6 --- /dev/null +++ b/internal/logic/admin/marketing/queryQuotaTaskListLogic.go @@ -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 +} diff --git a/internal/logic/admin/marketing/queryQuotaTaskPreCountLogic.go b/internal/logic/admin/marketing/queryQuotaTaskPreCountLogic.go new file mode 100644 index 0000000..6f12b07 --- /dev/null +++ b/internal/logic/admin/marketing/queryQuotaTaskPreCountLogic.go @@ -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 +} diff --git a/internal/logic/admin/marketing/queryQuotaTaskStatusLogic.go b/internal/logic/admin/marketing/queryQuotaTaskStatusLogic.go new file mode 100644 index 0000000..3aa250e --- /dev/null +++ b/internal/logic/admin/marketing/queryQuotaTaskStatusLogic.go @@ -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 +} diff --git a/internal/model/task/task.go b/internal/model/task/task.go index c30fee2..06f5688 100644 --- a/internal/model/task/task.go +++ b/internal/model/task/task.go @@ -1,27 +1,142 @@ package task -import "time" +import ( + "encoding/json" + "time" +) -type EmailTask struct { - Id int64 `gorm:"column:id;primaryKey;autoIncrement;comment:ID"` - Subject string `gorm:"column:subject;type:varchar(255);not null;comment:Email Subject"` - Content string `gorm:"column:content;type:text;not null;comment:Email Content"` - Recipients string `gorm:"column:recipient;type:text;not null;comment:Email Recipient"` - Scope string `gorm:"column:scope;type:varchar(50);not null;comment:Email Scope"` - 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"` - Scheduled time.Time `gorm:"column:scheduled;not null;comment:Scheduled Time"` - Interval uint8 `gorm:"column:interval;not null;comment:Interval in Seconds"` - Limit uint64 `gorm:"column:limit;not null;comment:Daily send limit"` - Status uint8 `gorm:"column:status;not null;comment:Daily Status"` - Errors string `gorm:"column:errors;type:text;not null;comment:Errors"` - Total uint64 `gorm:"column:total;not null;default:0;comment:Total Number"` - Current uint64 `gorm:"column:current;not null;default:0;comment:Current Number"` - CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` - UpdatedAt time.Time `gorm:"comment:Update Time"` +type Type int8 + +const ( + Undefined Type = -1 + TypeEmail = iota + TypeQuota +) + +type Task struct { + Id int64 `gorm:"primaryKey;autoIncrement;comment:ID"` + Type int8 `gorm:"not null;comment:Task Type"` + Scope string `gorm:"type:text;comment:Task Scope"` + Content string `gorm:"type:text;comment:Task Content"` + 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"` + Current uint64 `gorm:"column:current;not null;default:0;comment:Current Number"` + CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` + UpdatedAt time.Time `gorm:"comment:Update Time"` } -func (EmailTask) TableName() string { - return "email_task" +func (Task) TableName() string { + 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 + } } diff --git a/internal/types/types.go b/internal/types/types.go index d47d3ec..599b38a 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -162,7 +162,7 @@ type BatchSendEmailTask struct { Subject string `json:"subject"` Content string `json:"content"` Recipients string `json:"recipients"` - Scope string `json:"scope"` + Scope int8 `json:"scope"` RegisterStartTime int64 `json:"register_start_time"` RegisterEndTime int64 `json:"register_end_time"` Additional string `json:"additional"` @@ -274,7 +274,7 @@ type CreateAnnouncementRequest struct { type CreateBatchSendEmailTaskRequest struct { Subject string `json:"subject"` Content string `json:"content"` - Scope string `json:"scope"` + Scope int8 `json:"scope"` RegisterStartTime int64 `json:"register_start_time,omitempty"` RegisterEndTime int64 `json:"register_end_time,omitempty"` Additional string `json:"additional,omitempty"` @@ -344,6 +344,15 @@ type CreatePaymentMethodRequest struct { 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 { Name string `json:"name"` Country string `json:"country,omitempty"` @@ -756,7 +765,7 @@ type GetAvailablePaymentMethodsResponse struct { type GetBatchSendEmailTaskListRequest struct { Page int `form:"page"` Size int `form:"size"` - Scope string `form:"scope,omitempty"` + Scope *int8 `form:"scope,omitempty"` Status *uint8 `form:"status,omitempty"` } @@ -1522,6 +1531,39 @@ type QueryPurchaseOrderResponse struct { 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 { List []SubscribeGroup `json:"list"` Total int64 `json:"total"` @@ -1571,6 +1613,23 @@ type QueryUserSubscribeListResponse struct { 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 { Amount int64 `json:"amount"` Payment int64 `json:"payment"` diff --git a/pkg/orm/tool_test.go b/pkg/orm/tool_test.go index e63e078..d415bbd 100644 --- a/pkg/orm/tool_test.go +++ b/pkg/orm/tool_test.go @@ -3,7 +3,7 @@ package orm import ( "testing" - "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/internal/model/task" "gorm.io/driver/mysql" "gorm.io/gorm" @@ -31,7 +31,7 @@ func TestMysql(t *testing.T) { if err != nil { t.Fatalf("Failed to connect to MySQL: %v", err) } - err = db.Migrator().AutoMigrate(&node.Server{}) + err = db.Migrator().AutoMigrate(&task.Task{}) if err != nil { t.Fatalf("Failed to auto migrate: %v", err) return diff --git a/queue/logic/email/batchEmailLogic.go b/queue/logic/email/batchEmailLogic.go index d9f9505..2aa8123 100644 --- a/queue/logic/email/batchEmailLogic.go +++ b/queue/logic/email/batchEmailLogic.go @@ -44,8 +44,8 @@ func (l *BatchEmailLogic) ProcessTask(ctx context.Context, task *asynq.Task) err return asynq.SkipRetry } tx := l.svcCtx.DB.WithContext(ctx) - var taskInfo taskModel.EmailTask - if err = tx.Model(&taskModel.EmailTask{}).Where("id = ?", taskID).First(&taskInfo).Error; err != nil { + var taskInfo taskModel.Task + if err = tx.Model(&taskModel.Task{}).Where("id = ?", taskID).First(&taskInfo).Error; err != nil { logger.WithContext(ctx).Error("[BatchEmailLogic] ProcessTask failed", logger.Field("error", err.Error()), logger.Field("taskID", taskID),