feat(quota): add quota task creation and querying endpoints with updated data structures
This commit is contained in:
parent
f4c6bd919b
commit
d1be5febc3
@ -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)
|
||||
}
|
||||
|
||||
|
||||
0
initialize/migrate/database/02113_task.down.sql
Normal file
0
initialize/migrate/database/02113_task.down.sql
Normal file
14
initialize/migrate/database/02113_task.up.sql
Normal file
14
initialize/migrate/database/02113_task.up.sql
Normal 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;
|
||||
26
internal/handler/admin/marketing/createQuotaTaskHandler.go
Normal file
26
internal/handler/admin/marketing/createQuotaTaskHandler.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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")
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
30
internal/logic/admin/marketing/createQuotaTaskLogic.go
Normal file
30
internal/logic/admin/marketing/createQuotaTaskLogic.go
Normal 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
|
||||
}
|
||||
@ -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
|
||||
|
||||
30
internal/logic/admin/marketing/queryQuotaTaskListLogic.go
Normal file
30
internal/logic/admin/marketing/queryQuotaTaskListLogic.go
Normal 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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
30
internal/logic/admin/marketing/queryQuotaTaskStatusLogic.go
Normal file
30
internal/logic/admin/marketing/queryQuotaTaskStatusLogic.go
Normal 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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"`
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user