package handlers import ( "encoding/json" "net/http" "strconv" "time" "github.com/gin-gonic/gin" "gorm.io/gorm" "everypublish/server/internal/api/response" "everypublish/server/internal/models" "everypublish/server/internal/task" "everypublish/server/internal/ws" ) // TaskHandler 发布任务处理器(状态机见 internal/task) type TaskHandler struct { DB *gorm.DB Dispatcher *ws.Dispatcher } type taskReq struct { Title string `json:"title" binding:"required"` Content string `json:"content"` Tags []string `json:"tags"` AccountIDs []uint64 `json:"accountIds"` MaterialIDs []uint64 `json:"materialIds"` ScheduleAt *int64 `json:"scheduleAt"` Priority int `json:"priority"` } func marshalJSONArray[T any](arr []T) string { raw, _ := json.Marshal(arr) if raw == nil { return "[]" } return string(raw) } // List 任务列表(状态筛选 + 分页) func (h *TaskHandler) List(c *gin.Context) { page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) size, _ := strconv.Atoi(c.DefaultQuery("size", "20")) if page < 1 { page = 1 } if size < 1 || size > 100 { size = 20 } q := h.DB.Model(&models.Task{}).Where("workspace_id = ?", c.GetUint64("wsid")) if s := c.Query("status"); s != "" { q = q.Where("status = ?", s) } if s := c.Query("schedule"); s != "" { if s == "scheduled" { q = q.Where("schedule_at is not null and schedule_at > ?", time.Now()) } else if s == "immediate" { q = q.Where("schedule_at is null or schedule_at <= ?", time.Now()) } } var total int64 q.Count(&total) var list []models.Task q.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&list) response.OK(c, gin.H{"list": list, "total": total, "page": page, "size": size}) } // Create 新建任务(draft) func (h *TaskHandler) Create(c *gin.Context) { var req taskReq if err := c.ShouldBindJSON(&req); err != nil { response.Fail(c, http.StatusBadRequest, 1001, "参数不合法:"+err.Error()) return } if len(req.AccountIDs) == 0 { response.Fail(c, http.StatusBadRequest, 1001, "至少选择一个平台账号") return } if len(req.MaterialIDs) == 0 { response.Fail(c, http.StatusBadRequest, 1001, "至少选择一个素材") return } priority := req.Priority if priority < 1 || priority > 10 { priority = 5 } row := models.Task{ WorkspaceID: c.GetUint64("wsid"), Title: req.Title, Content: req.Content, Tags: marshalJSONArray(req.Tags), AccountIDs: marshalJSONArray(req.AccountIDs), MaterialIDs: marshalJSONArray(req.MaterialIDs), Status: task.Draft, Priority: priority, MaxRetry: 3, CreatedBy: c.GetUint64("uid"), } if req.ScheduleAt != nil { ts := time.UnixMilli(*req.ScheduleAt) row.ScheduleAt = &ts } if err := h.DB.Create(&row).Error; err != nil { response.Fail(c, http.StatusInternalServerError, 5000, "创建失败") return } response.Audit(c, "task.create", "task:"+itoa(row.ID), gin.H{"title": req.Title}) response.OK(c, row) } // Get 任务详情 func (h *TaskHandler) Get(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { response.Fail(c, http.StatusBadRequest, 1001, "参数不合法") return } var row models.Task if err = h.DB.Where("id = ? and workspace_id = ?", id, c.GetUint64("wsid")).First(&row).Error; err != nil { response.Fail(c, http.StatusNotFound, 1004, "任务不存在") return } response.OK(c, row) } // Update 编辑草稿(仅 draft/rejected) func (h *TaskHandler) Update(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { response.Fail(c, http.StatusBadRequest, 1001, "参数不合法") return } var req taskReq if err = c.ShouldBindJSON(&req); err != nil { response.Fail(c, http.StatusBadRequest, 1001, "参数不合法") return } var row models.Task if err = h.DB.Where("id = ? and workspace_id = ?", id, c.GetUint64("wsid")).First(&row).Error; err != nil { response.Fail(c, http.StatusNotFound, 1004, "任务不存在") return } if row.Status != task.Draft && row.Status != task.Rejected { response.Fail(c, http.StatusConflict, 3005, "仅草稿或已驳回任务可编辑") return } updates := map[string]interface{}{ "title": req.Title, "content": req.Content, "tags": marshalJSONArray(req.Tags), "account_ids": marshalJSONArray(req.AccountIDs), "material_ids": marshalJSONArray(req.MaterialIDs), } if req.ScheduleAt != nil { ts := time.UnixMilli(*req.ScheduleAt) updates["schedule_at"] = &ts } if err = h.DB.Model(&row).Updates(updates).Error; err != nil { response.Fail(c, http.StatusInternalServerError, 5000, "更新失败") return } response.Audit(c, "task.update", "task:"+itoa(id), gin.H{"title": req.Title}) response.OK(c, nil) } // Delete 删除草稿 func (h *TaskHandler) Delete(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { response.Fail(c, http.StatusBadRequest, 1001, "参数不合法") return } var row models.Task if err = h.DB.Where("id = ? and workspace_id = ?", id, c.GetUint64("wsid")).First(&row).Error; err != nil { response.Fail(c, http.StatusNotFound, 1004, "任务不存在") return } if row.Status != task.Draft { response.Fail(c, http.StatusConflict, 3005, "仅草稿可删除") return } if err = h.DB.Delete(&row).Error; err != nil { response.Fail(c, http.StatusInternalServerError, 5000, "删除失败") return } response.Audit(c, "task.delete", "task:"+itoa(id), nil) response.OK(c, nil) } // apply 状态转移并落库 func (h *TaskHandler) apply(c *gin.Context, action task.Action, extra map[string]interface{}) *models.Task { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { response.Fail(c, http.StatusBadRequest, 1001, "参数不合法") return nil } var row models.Task if err = h.DB.Where("id = ? and workspace_id = ?", id, c.GetUint64("wsid")).First(&row).Error; err != nil { response.Fail(c, http.StatusNotFound, 1004, "任务不存在") return nil } next, ok := task.Next(row.Status, action) if !ok { response.Fail(c, http.StatusConflict, 3005, "当前状态("+row.Status+")不允许该操作("+string(action)+")") return nil } updates := map[string]interface{}{"status": next} for k, v := range extra { updates[k] = v } if err = h.DB.Model(&row).Updates(updates).Error; err != nil { response.Fail(c, http.StatusInternalServerError, 5000, "更新失败") return nil } row.Status = next return &row } // Submit 提交审核 func (h *TaskHandler) Submit(c *gin.Context) { if row := h.apply(c, task.ActionSubmit, nil); row != nil { response.Audit(c, "task.submit", "task:"+itoa(row.ID), nil) response.OK(c, row) } } // Approve 审核通过 → queued func (h *TaskHandler) Approve(c *gin.Context) { if row := h.apply(c, task.ActionApprove, nil); row != nil { _ = Notify(h.DB, c.GetUint64("wsid"), []uint64{row.CreatedBy}, "task", "任务已通过审核", row.Title) response.Audit(c, "task.approve", "task:"+itoa(row.ID), nil) if h.Dispatcher != nil { _ = h.Dispatcher.TryDispatch(row.ID) // 在线设备立即下发 } response.OK(c, row) } } // Reject 驳回(附批注) func (h *TaskHandler) Reject(c *gin.Context) { var req struct { Note string `json:"note"` } _ = c.ShouldBindJSON(&req) if row := h.apply(c, task.ActionReject, map[string]interface{}{"review_note": req.Note, "reviewed_by": c.GetUint64("uid")}); row != nil { _ = Notify(h.DB, c.GetUint64("wsid"), []uint64{row.CreatedBy}, "task", "任务被驳回", req.Note) response.Audit(c, "task.reject", "task:"+itoa(row.ID), gin.H{"note": req.Note}) response.OK(c, row) } } // Resubmit 驳回后重新提交 func (h *TaskHandler) Resubmit(c *gin.Context) { if row := h.apply(c, task.ActionResubmit, nil); row != nil { response.Audit(c, "task.resubmit", "task:"+itoa(row.ID), nil) response.OK(c, row) } } // Retry 失败重试 func (h *TaskHandler) Retry(c *gin.Context) { if row := h.apply(c, task.ActionRetry, map[string]interface{}{"error_message": ""}); row != nil { response.Audit(c, "task.retry", "task:"+itoa(row.ID), nil) if h.Dispatcher != nil { _ = h.Dispatcher.TryDispatch(row.ID) } response.OK(c, row) } } // Cancel 取消 func (h *TaskHandler) Cancel(c *gin.Context) { if row := h.apply(c, task.ActionCancel, nil); row != nil { response.Audit(c, "task.cancel", "task:"+itoa(row.ID), nil) response.OK(c, row) } }