152 lines
4.1 KiB
Go
152 lines
4.1 KiB
Go
package biz
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"net/url"
|
||
"time"
|
||
|
||
"github.com/robfig/cron/v3"
|
||
)
|
||
|
||
type TimedTask struct {
|
||
ID uint
|
||
CreatedAt time.Time
|
||
UpdatedAt time.Time
|
||
Name, Description, Spec string
|
||
WithSeconds bool
|
||
ExecutorType, MethodName string
|
||
Params []byte
|
||
HTTPURL, HTTPMethod string
|
||
HTTPHeader []byte
|
||
HTTPBody string
|
||
HTTPAllowPrivate, Enabled bool
|
||
EnabledFilter *bool
|
||
}
|
||
type TimedTaskLog struct {
|
||
ID uint
|
||
CreatedAt time.Time
|
||
TaskID uint
|
||
TaskName, TriggerType string
|
||
StartedAt, FinishedAt time.Time
|
||
DurationMS int64
|
||
Status, ErrorMsg, Output string
|
||
}
|
||
|
||
const (
|
||
TaskExecutorMethod = "method"
|
||
TaskExecutorHTTP = "http"
|
||
TaskMethodClearDB = "ClearDB"
|
||
TaskMethodUploads = "CleanStaleUploads"
|
||
)
|
||
|
||
type TaskMethod struct {
|
||
Name string
|
||
Description string
|
||
}
|
||
|
||
var registeredTaskMethods = []TaskMethod{
|
||
{Name: TaskMethodClearDB, Description: "清理数据库过期日志(操作记录、JWT黑名单、定时任务日志)"},
|
||
{Name: TaskMethodUploads, Description: "清理过期大文件上传会话"},
|
||
}
|
||
|
||
func RegisteredTaskMethods() []TaskMethod {
|
||
return append([]TaskMethod(nil), registeredTaskMethods...)
|
||
}
|
||
|
||
func registeredTaskMethod(name string) bool {
|
||
for _, method := range registeredTaskMethods {
|
||
if method.Name == name {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
type TaskRepo interface {
|
||
CreateTask(context.Context, *TimedTask) error
|
||
UpdateTask(context.Context, *TimedTask) error
|
||
DeleteTask(context.Context, uint) error
|
||
FindTask(context.Context, uint) (*TimedTask, error)
|
||
ListTasks(context.Context, int, int, *TimedTask) ([]*TimedTask, int64, error)
|
||
ToggleTask(context.Context, uint, bool) error
|
||
RecordTaskLog(context.Context, *TimedTaskLog) error
|
||
ListTaskLogs(context.Context, int, int, uint, string) ([]*TimedTaskLog, int64, error)
|
||
CleanupLogs(context.Context) error
|
||
TaskNameExists(context.Context, string, uint) (bool, error)
|
||
}
|
||
type TaskUsecase struct{ TaskRepo }
|
||
|
||
func NewTaskUsecase(repo TaskRepo) *TaskUsecase { return &TaskUsecase{TaskRepo: repo} }
|
||
|
||
func (uc *TaskUsecase) Validate(value *TimedTask) error {
|
||
if value.Name == "" {
|
||
return errors.New("任务名不能为空")
|
||
}
|
||
var err error
|
||
if value.WithSeconds {
|
||
_, err = cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor).Parse(value.Spec)
|
||
} else {
|
||
_, err = cron.ParseStandard(value.Spec)
|
||
}
|
||
if err != nil {
|
||
return fmt.Errorf("cron 表达式非法: %w", err)
|
||
}
|
||
switch value.ExecutorType {
|
||
case TaskExecutorMethod:
|
||
if !registeredTaskMethod(value.MethodName) {
|
||
return errors.New("方法未注册")
|
||
}
|
||
if len(value.Params) > 0 && !json.Valid(value.Params) {
|
||
return errors.New("params 必须是合法 JSON")
|
||
}
|
||
case TaskExecutorHTTP:
|
||
parsed, parseErr := url.Parse(value.HTTPURL)
|
||
if parseErr != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Hostname() == "" {
|
||
return errors.New("httpUrl 必须是合法的 http/https 地址")
|
||
}
|
||
if len(value.HTTPHeader) > 0 {
|
||
headers := map[string]string{}
|
||
if json.Unmarshal(value.HTTPHeader, &headers) != nil {
|
||
return errors.New("httpHeader 必须是 JSON 对象")
|
||
}
|
||
}
|
||
default:
|
||
return errors.New("executorType 必须为 method 或 http")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (uc *TaskUsecase) Create(ctx context.Context, value *TimedTask) error {
|
||
if err := uc.Validate(value); err != nil {
|
||
return err
|
||
}
|
||
exists, err := uc.TaskNameExists(ctx, value.Name, 0)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if exists {
|
||
return fmt.Errorf("任务名 %s 已存在", value.Name)
|
||
}
|
||
return uc.CreateTask(ctx, value)
|
||
}
|
||
|
||
func (uc *TaskUsecase) Update(ctx context.Context, value *TimedTask) error {
|
||
if value.ID == 0 {
|
||
return errors.New("缺少任务 ID")
|
||
}
|
||
if err := uc.Validate(value); err != nil {
|
||
return err
|
||
}
|
||
exists, err := uc.TaskNameExists(ctx, value.Name, value.ID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if exists {
|
||
return fmt.Errorf("任务名 %s 已存在", value.Name)
|
||
}
|
||
return uc.UpdateTask(ctx, value)
|
||
}
|