package projects import ( "errors" "net/http" "strings" "time" "github.com/gin-gonic/gin" "gorm.io/gorm" "senlinai-agent/backend/internal/httpx" "senlinai-agent/backend/internal/models" ) // CronPlanDTO 是计划任务管理响应;MVP 只管理提醒元数据,不执行自主 Agent。 type CronPlanDTO struct { ID string `json:"id"` ProjectID string `json:"projectId"` Title string `json:"title"` Schedule string `json:"schedule"` NextRunAt *time.Time `json:"nextRunAt"` Enabled bool `json:"enabled"` LastResult string `json:"lastResult"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } // CronHandler 单独承载计划任务写接口。 type CronHandler struct { service *Service } // NewCronHandler 创建计划任务 HTTP registrar。 func NewCronHandler(service *Service) *CronHandler { return &CronHandler{service: service} } // Register 注册项目内计划任务创建接口。 func (h *CronHandler) Register(router gin.IRouter) { router.POST("/projects/:id/cron-plans", h.create) } func (h *CronHandler) create(c *gin.Context) { userID, project, ok := ownedProjectFromRequest(c) if !ok { return } var input struct { Title string `json:"title"` Schedule string `json:"schedule"` Enabled bool `json:"enabled"` NextRunAt string `json:"nextRunAt"` } if err := c.ShouldBindJSON(&input); err != nil { httpx.Error(c, http.StatusBadRequest, "invalid_request", "请求参数无效") return } nextRunAt, err := parseCronOptionalTime(input.NextRunAt) if err != nil { httpx.Error(c, http.StatusBadRequest, "invalid_request", "下次运行时间格式无效") return } plan, err := h.service.CreateCronPlan(userID, project.ID, CreateCronPlanInput{ Title: input.Title, Schedule: input.Schedule, Enabled: input.Enabled, NextRunAt: nextRunAt, }) if err != nil { writeCronError(c, err) return } c.JSON(http.StatusCreated, cronPlanDTO(*plan)) } func writeCronError(c *gin.Context, err error) { switch { case errors.Is(err, ErrCronTitleRequired), errors.Is(err, ErrCronScheduleRequired): httpx.Error(c, http.StatusBadRequest, "invalid_request", "计划任务参数无效") case errors.Is(err, gorm.ErrRecordNotFound): httpx.Error(c, http.StatusNotFound, "not_found", "项目不存在") default: httpx.Error(c, http.StatusInternalServerError, "internal_error", "计划任务创建失败") } } func cronPlanDTO(plan models.SaCronPlan) CronPlanDTO { return CronPlanDTO{ ID: plan.Identity, ProjectID: plan.ProjectIdentity, Title: plan.Title, Schedule: plan.Schedule, NextRunAt: projectUTCOptionalTime(plan.NextRunAt), Enabled: plan.Enabled, LastResult: plan.LastResult, CreatedAt: plan.CreatedAt.UTC(), UpdatedAt: plan.UpdatedAt.UTC(), } } func parseCronOptionalTime(value string) (*time.Time, error) { trimmed := strings.TrimSpace(value) if trimmed == "" { return nil, nil } parsed, err := time.Parse(time.RFC3339, trimmed) if err != nil { return nil, err } parsed = parsed.UTC() return &parsed, nil } func projectUTCOptionalTime(value *time.Time) *time.Time { if value == nil { return nil } result := value.UTC() return &result }