package projects import ( "errors" "net/http" "strconv" "strings" "time" "github.com/gin-gonic/gin" "gorm.io/gorm" "senlinai-agent/backend/internal/httpx" "senlinai-agent/backend/internal/logic/auth" "senlinai-agent/backend/internal/logic/files" ) type Handler struct { service *Service fileService *files.Service } func NewHandler(service *Service, fileServices ...*files.Service) *Handler { var fileService *files.Service if len(fileServices) > 0 { fileService = fileServices[0] } return &Handler{service: service, fileService: fileService} } func (h *Handler) Register(router gin.IRouter) { router.POST("/projects", h.createProject) router.GET("/projects", h.listProjects) router.GET("/projects/:id", h.getProject) router.PATCH("/projects/:id", h.updateProject) router.GET("/projects/:id/dashboard", h.dashboard) router.GET("/projects/:id/workspace", h.workspace) router.GET("/projects/:id/tags", h.listTags) router.POST("/projects/:id/tasks", h.createTask) router.PATCH("/projects/:id/tasks/:taskID", h.updateTask) router.POST("/projects/:id/sources", h.uploadSource) router.POST("/projects/:id/cron-plans", h.createCronPlan) router.POST("/projects/:id/tags", h.createTag) } func (h *Handler) createProject(c *gin.Context) { userID, ok := auth.CurrentUserID(c) if !ok { httpx.Error(c, http.StatusUnauthorized, "unauthorized", "未登录或登录已失效") return } var input CreateProjectRequest if err := c.ShouldBindJSON(&input); err != nil { httpx.Error(c, http.StatusBadRequest, "invalid_request", "请求参数无效") return } project, err := h.service.CreateProjectWithInput(userID, input) if err != nil { writeProjectError(c, err) return } c.JSON(http.StatusCreated, projectDTO(*project)) } func (h *Handler) listProjects(c *gin.Context) { userID, ok := auth.CurrentUserID(c) if !ok { httpx.Error(c, http.StatusUnauthorized, "unauthorized", "未登录或登录已失效") return } projects, err := h.service.ListProjects(userID) if err != nil { httpx.Error(c, http.StatusInternalServerError, "internal_error", "项目列表加载失败") return } c.JSON(http.StatusOK, projects) } func (h *Handler) getProject(c *gin.Context) { userID, ok := auth.CurrentUserID(c) if !ok { httpx.Error(c, http.StatusUnauthorized, "unauthorized", "未登录或登录已失效") return } identity, ok := httpx.IdentityParam(c, "id") if !ok { return } project, err := h.service.GetProject(userID, identity) if err != nil { writeProjectError(c, err) return } c.JSON(http.StatusOK, project) } func (h *Handler) updateProject(c *gin.Context) { userID, ok := auth.CurrentUserID(c) if !ok { httpx.Error(c, http.StatusUnauthorized, "unauthorized", "未登录或登录已失效") return } identity, ok := httpx.IdentityParam(c, "id") if !ok { return } var input UpdateProjectRequest if err := c.ShouldBindJSON(&input); err != nil { httpx.Error(c, http.StatusBadRequest, "invalid_request", "请求参数无效") return } if err := h.service.UpdateProject(userID, identity, input); err != nil { writeProjectError(c, err) return } c.Status(http.StatusNoContent) } func (h *Handler) dashboard(c *gin.Context) { userID, ok := auth.CurrentUserID(c) if !ok { c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"}) return } projectID, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project id"}) return } dashboard, err := h.service.Dashboard(userID, uint(projectID)) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load dashboard"}) return } c.JSON(http.StatusOK, dashboard) } func (h *Handler) workspace(c *gin.Context) { userID, ok := auth.CurrentUserID(c) if !ok { c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"}) return } projectIdentity, ok := httpx.IdentityParam(c, "id") if !ok { return } workspace, err := h.service.Workspace(userID, projectIdentity) if err != nil { writeProjectError(c, err) return } c.JSON(http.StatusOK, workspace) } func (h *Handler) listTags(c *gin.Context) { userID, ok := auth.CurrentUserID(c) if !ok { c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"}) return } projectID, ok := parseProjectID(c) if !ok { return } tags, err := h.service.ListProjectTags(userID, projectID) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, tags) } func (h *Handler) createTask(c *gin.Context) { userID, ok := auth.CurrentUserID(c) if !ok { c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"}) return } projectID, ok := parseProjectID(c) if !ok { return } var input struct { Title string `json:"title"` Description string `json:"description"` Status string `json:"status"` DueAt string `json:"dueAt"` Tag string `json:"tag"` } if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } dueAt, err := parseOptionalTime(input.DueAt) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid dueAt"}) return } task, err := h.service.CreateTask(userID, projectID, CreateTaskInput{ Title: input.Title, Description: input.Description, Status: input.Status, DueAt: dueAt, Tag: input.Tag, }) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, task) } func (h *Handler) updateTask(c *gin.Context) { userID, ok := auth.CurrentUserID(c) if !ok { c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"}) return } projectID, ok := parseProjectID(c) if !ok { return } taskID, ok := parseTaskID(c) if !ok { return } var input struct { Title string `json:"title"` Description string `json:"description"` Status string `json:"status"` Completed bool `json:"completed"` NextProjectID uint `json:"nextProjectId"` Tag string `json:"tag"` } if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } task, err := h.service.UpdateTask(userID, projectID, taskID, UpdateTaskInput{ Title: input.Title, Description: input.Description, Status: input.Status, Completed: input.Completed, NextProjectID: input.NextProjectID, Tag: input.Tag, }) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, task) } func (h *Handler) uploadSource(c *gin.Context) { userID, ok := auth.CurrentUserID(c) if !ok { c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"}) return } if h.fileService == nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "file service is not configured"}) return } projectID, ok := parseProjectID(c) if !ok { return } if err := ensureProjectOwner(userID, projectID); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } fileHeader, err := c.FormFile("file") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "file is required"}) return } file, err := fileHeader.Open() if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "failed to read file"}) return } defer file.Close() stored, err := h.fileService.Save(projectID, fileHeader.Filename, file) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to store file"}) return } title := c.PostForm("title") if strings.TrimSpace(title) == "" { title = stored.OriginalName } source, err := h.service.CreateFileSource(userID, projectID, CreateFileSourceInput{ Title: title, FilePath: stored.RelativePath, }) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, source) } func (h *Handler) createCronPlan(c *gin.Context) { userID, ok := auth.CurrentUserID(c) if !ok { c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"}) return } projectID, ok := parseProjectID(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 { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } nextRunAt, err := parseOptionalTime(input.NextRunAt) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid nextRunAt"}) return } plan, err := h.service.CreateCronPlan(userID, projectID, CreateCronPlanInput{ Title: input.Title, Schedule: input.Schedule, Enabled: input.Enabled, NextRunAt: nextRunAt, }) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, plan) } func (h *Handler) createTag(c *gin.Context) { userID, ok := auth.CurrentUserID(c) if !ok { c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"}) return } projectID, ok := parseProjectID(c) if !ok { return } var input struct { Name string `json:"name"` } if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } tag, err := h.service.CreateProjectTag(userID, projectID, input.Name) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, tag) } func parseProjectID(c *gin.Context) (uint, bool) { projectID, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project id"}) return 0, false } return uint(projectID), true } func writeProjectError(c *gin.Context, err error) { switch { case errors.Is(err, gorm.ErrRecordNotFound): httpx.Error(c, http.StatusNotFound, "not_found", "项目不存在") case errors.Is(err, ErrProjectIdentifierConflict): httpx.Error(c, http.StatusConflict, "conflict", "项目标识已存在") case errors.Is(err, ErrProjectNameRequired), errors.Is(err, ErrProjectIdentifierRequired): httpx.Error(c, http.StatusBadRequest, "invalid_request", "请求参数无效") default: httpx.Error(c, http.StatusInternalServerError, "internal_error", "项目操作失败") } } func parseTaskID(c *gin.Context) (uint, bool) { taskID, err := strconv.ParseUint(c.Param("taskID"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid task id"}) return 0, false } return uint(taskID), 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 } return &parsed, nil }