package usecase import ( "context" "strings" "haixun-backend/internal/library/clock" app "haixun-backend/internal/library/errors" "haixun-backend/internal/library/errors/code" jobcron "haixun-backend/internal/model/job/cron" "haixun-backend/internal/model/job/domain/entity" domrepo "haixun-backend/internal/model/job/domain/repository" domusecase "haixun-backend/internal/model/job/domain/usecase" ) func (u *jobUseCase) ListSchedules(ctx context.Context, scope, scopeID string, page, pageSize int64) ([]*entity.Schedule, int64, int64, int64, int64, error) { if page <= 0 { page = 1 } if pageSize <= 0 { pageSize = 20 } if pageSize > 200 { pageSize = 200 } offset := (page - 1) * pageSize items, total, err := u.schedules.List(ctx, domrepo.ScheduleListFilter{ Scope: scope, ScopeID: scopeID, }, offset, pageSize) if err != nil { return nil, 0, 0, 0, 0, err } totalPages := int64(0) if total > 0 { totalPages = (total + pageSize - 1) / pageSize } return items, total, page, pageSize, totalPages, nil } func (u *jobUseCase) GetSchedule(ctx context.Context, scheduleID string) (*entity.Schedule, error) { return u.schedules.FindByID(ctx, scheduleID) } func (u *jobUseCase) CreateSchedule(ctx context.Context, req domusecase.CreateScheduleRequest) (*entity.Schedule, error) { templateType := strings.TrimSpace(req.TemplateType) scope := strings.TrimSpace(req.Scope) scopeID := strings.TrimSpace(req.ScopeID) cronExpr := strings.TrimSpace(req.Cron) if templateType == "" || scope == "" || scopeID == "" || cronExpr == "" { return nil, app.For(code.Job).InputMissingRequired("template_type, scope, scope_id, and cron are required") } if _, err := u.templates.FindByType(ctx, templateType); err != nil { return nil, err } timezone := req.Timezone if timezone == "" { timezone = "UTC" } nextRunAt, err := jobcron.NextRunAt(cronExpr, timezone, clock.Now()) if err != nil { return nil, app.For(code.Job).InputInvalidFormat(err.Error()) } schedule := &entity.Schedule{ TemplateType: templateType, Scope: scope, ScopeID: scopeID, Enabled: req.Enabled, Cron: cronExpr, Timezone: timezone, PayloadTemplate: req.PayloadTemplate, NextRunAt: nextRunAt, } return u.schedules.Create(ctx, schedule) } func (u *jobUseCase) UpdateSchedule(ctx context.Context, req domusecase.UpdateScheduleRequest) (*entity.Schedule, error) { schedule, err := u.schedules.FindByID(ctx, req.ID) if err != nil { return nil, err } if cronExpr := strings.TrimSpace(req.Cron); cronExpr != "" { schedule.Cron = cronExpr } if tz := strings.TrimSpace(req.Timezone); tz != "" { schedule.Timezone = tz } if req.PayloadTemplate != nil { schedule.PayloadTemplate = req.PayloadTemplate } if req.Enabled != nil { schedule.Enabled = *req.Enabled } nextRunAt, err := jobcron.NextRunAt(schedule.Cron, schedule.Timezone, clock.Now()) if err != nil { return nil, app.For(code.Job).InputInvalidFormat(err.Error()) } schedule.NextRunAt = nextRunAt return u.schedules.Update(ctx, schedule) } func (u *jobUseCase) EnableSchedule(ctx context.Context, scheduleID string) (*entity.Schedule, error) { schedule, err := u.schedules.FindByID(ctx, scheduleID) if err != nil { return nil, err } schedule.Enabled = true nextRunAt, err := jobcron.NextRunAt(schedule.Cron, schedule.Timezone, clock.Now()) if err != nil { return nil, app.For(code.Job).InputInvalidFormat(err.Error()) } schedule.NextRunAt = nextRunAt return u.schedules.Update(ctx, schedule) } func (u *jobUseCase) DisableSchedule(ctx context.Context, scheduleID string) (*entity.Schedule, error) { schedule, err := u.schedules.FindByID(ctx, scheduleID) if err != nil { return nil, err } schedule.Enabled = false return u.schedules.Update(ctx, schedule) } func (u *jobUseCase) RunSchedulerTick(ctx context.Context, holder string) (int, error) { ok, err := u.queue.TrySchedulerLock(ctx, holder, 55) if err != nil { return 0, err } if !ok { return 0, nil } defer func() { _ = u.queue.ReleaseSchedulerLock(ctx, holder) }() now := clock.NowUnixNano() due, err := u.schedules.FindDue(ctx, now, 50) if err != nil { return 0, err } created := 0 var firstErr error for _, schedule := range due { payload := buildScheduleRunPayload(schedule, now) if _, err := u.CreateRun(ctx, domusecase.CreateRunRequest{ TemplateType: schedule.TemplateType, Scope: schedule.Scope, ScopeID: schedule.ScopeID, Payload: payload, }); err != nil { if firstErr == nil { firstErr = err } continue } created++ lastRun := now schedule.LastRunAt = &lastRun nextRunAt, calcErr := jobcron.NextRunAt(schedule.Cron, schedule.Timezone, clock.Now()) if calcErr != nil { continue } schedule.NextRunAt = nextRunAt _, _ = u.schedules.Update(ctx, schedule) } return created, firstErr }