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) }