package tasks import ( "errors" "net/http" "strings" "time" "github.com/gin-gonic/gin" "github.com/google/uuid" "gorm.io/gorm" "senlinai-agent/backend/internal/httpx" "senlinai-agent/backend/internal/logic/auth" ) // Handler 只注册任务写接口,项目核心 handler 不再承担任务职责。 type Handler struct { service *Service } // NewHandler 创建任务 HTTP registrar。 func NewHandler(service *Service) *Handler { return &Handler{service: service} } // Register 将任务写接口注册到上层提供的 /api/v1 路由组。 func (h *Handler) Register(router gin.IRouter) { router.POST("/projects/:id/tasks", h.create) router.PATCH("/projects/:id/tasks/:taskId", h.update) } type createTaskRequest struct { Title string `json:"title"` Description string `json:"description"` Status string `json:"status"` DueAt string `json:"dueAt"` Tag string `json:"tag"` } type updateTaskRequest struct { Title string `json:"title"` Description string `json:"description"` Status string `json:"status"` Completed bool `json:"completed"` NextProjectID string `json:"nextProjectId"` Tag string `json:"tag"` } func (h *Handler) create(c *gin.Context) { userID, ok := currentUser(c) if !ok { return } projectIdentity, ok := httpx.IdentityParam(c, "id") if !ok { return } var input createTaskRequest if err := c.ShouldBindJSON(&input); err != nil { httpx.Error(c, http.StatusBadRequest, "invalid_request", "请求参数无效") return } dueAt, err := parseOptionalTime(input.DueAt) if err != nil { httpx.Error(c, http.StatusBadRequest, "invalid_request", "截止时间格式无效") return } task, err := h.service.Create(userID, projectIdentity, CreateTaskInput{ Title: input.Title, Description: input.Description, Status: input.Status, DueAt: dueAt, Tag: input.Tag, }) if err != nil { writeTaskError(c, err) return } c.JSON(http.StatusCreated, task) } func (h *Handler) update(c *gin.Context) { userID, ok := currentUser(c) if !ok { return } projectIdentity, ok := httpx.IdentityParam(c, "id") if !ok { return } taskIdentity, ok := httpx.IdentityParam(c, "taskId") if !ok { return } var input updateTaskRequest if err := c.ShouldBindJSON(&input); err != nil { httpx.Error(c, http.StatusBadRequest, "invalid_request", "请求参数无效") return } nextProjectIdentity := "" if strings.TrimSpace(input.NextProjectID) != "" { var valid bool nextProjectIdentity, valid = parseIdentity(input.NextProjectID) if !valid { httpx.Error(c, http.StatusBadRequest, "invalid_identity", "nextProjectId 必须是 UUIDv7") return } } task, err := h.service.Update(userID, projectIdentity, taskIdentity, UpdateTaskInput{ Title: input.Title, Description: input.Description, Status: input.Status, Completed: input.Completed, NextProjectIdentity: nextProjectIdentity, Tag: input.Tag, }) if err != nil { writeTaskError(c, err) return } c.JSON(http.StatusOK, task) } func currentUser(c *gin.Context) (uint, bool) { userID, ok := auth.CurrentUserID(c) if !ok { httpx.Error(c, http.StatusUnauthorized, "unauthorized", "未登录或登录已失效") return 0, false } return userID, true } func parseOptionalTime(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 parseIdentity(value string) (string, bool) { trimmed := strings.TrimSpace(value) identity, err := uuid.Parse(trimmed) if err != nil || identity.Version() != 7 || identity.Variant() != uuid.RFC4122 { return "", false } return identity.String(), true } func writeTaskError(c *gin.Context, err error) { switch { case errors.Is(err, gorm.ErrRecordNotFound): httpx.Error(c, http.StatusNotFound, "not_found", "任务或项目不存在") case errors.Is(err, ErrTaskTitleRequired): httpx.Error(c, http.StatusBadRequest, "invalid_request", "任务标题不能为空") default: httpx.Error(c, http.StatusInternalServerError, "internal_error", "任务操作失败") } }