From 914d9429b00fe097eeae9e71da39ba3a170ee396 Mon Sep 17 00:00:00 2001 From: LinXin Date: Sun, 23 Nov 2025 16:09:49 +0800 Subject: [PATCH] feat: add CURD for project structure --- backend/docs/API.md | 211 +++++++++++++++++++++++++--- backend/handlers/project/project.go | 177 +++++++++++++++++++++++ backend/routes/routes.go | 9 ++ backend/services/errors.go | 1 + backend/services/project_service.go | 89 ++++++++++++ 5 files changed, 471 insertions(+), 16 deletions(-) create mode 100644 backend/handlers/project/project.go create mode 100644 backend/services/project_service.go diff --git a/backend/docs/API.md b/backend/docs/API.md index 401f175..54bfa04 100644 --- a/backend/docs/API.md +++ b/backend/docs/API.md @@ -115,9 +115,173 @@ Authorization: Bearer --- +## 项目相关接口 + +### 4. 获取用户的所有项目 + +**GET** `/api/projects` + +**需要认证**: 是 + +**响应** (200 OK): +```json +{ + "projects": [ + { + "id": 1, + "name": "我的项目", + "description": "项目描述", + "status": 1, + "start_date": "2025-11-20T10:00:00Z", + "end_date": null, + "owner_id": 1, + "created_at": "2025-11-20T10:00:00Z", + "updated_at": "2025-11-20T10:00:00Z", + "owner": { + "id": 1, + "username": "testuser", + "email": "test@example.com" + } + } + ] +} +``` + +--- + +### 5. 创建项目 + +**POST** `/api/projects` + +**需要认证**: 是 + +**请求体**: +```json +{ + "name": "string (必填)", + "description": "string (可选)", + "status": "number (可选, 1=进行中, 2=已完成, 3=已暂停, 4=已取消, 默认1)", + "start_date": "string (可选, ISO 8601格式)", + "end_date": "string (可选, ISO 8601格式)" +} +``` + +**响应** (201 Created): +```json +{ + "id": 1, + "name": "我的项目", + "description": "项目描述", + "status": 1, + "start_date": "2025-11-20T10:00:00Z", + "end_date": null, + "owner_id": 1, + "created_at": "2025-11-20T10:00:00Z", + "updated_at": "2025-11-20T10:00:00Z" +} +``` + +**错误响应**: +- `400 Bad Request`: 请求参数错误 +- `401 Unauthorized`: 未认证或token无效 + +--- + +### 6. 获取单个项目 + +**GET** `/api/projects/:projectId` + +**需要认证**: 是 + +**路径参数**: +- `projectId`: 项目ID (number) + +**响应** (200 OK): +```json +{ + "id": 1, + "name": "我的项目", + "description": "项目描述", + "status": 1, + "start_date": "2025-11-20T10:00:00Z", + "end_date": null, + "owner_id": 1, + "created_at": "2025-11-20T10:00:00Z", + "updated_at": "2025-11-20T10:00:00Z", + "owner": { + "id": 1, + "username": "testuser", + "email": "test@example.com" + }, + "boards": [], + "members": [] +} +``` + +**错误响应**: +- `400 Bad Request`: 无效的项目ID +- `404 Not Found`: 项目不存在 + +--- + +### 7. 更新项目 + +**PUT** `/api/projects/:projectId` + +**需要认证**: 是 + +**路径参数**: +- `projectId`: 项目ID (number) + +**请求体** (所有字段可选): +```json +{ + "name": "string (可选)", + "description": "string (可选)", + "status": "number (可选, 1=进行中, 2=已完成, 3=已暂停, 4=已取消)", + "start_date": "string (可选, ISO 8601格式)", + "end_date": "string (可选, ISO 8601格式)" +} +``` + +**响应** (200 OK): +```json +{ + "message": "更新成功" +} +``` + +**错误响应**: +- `400 Bad Request`: 请求参数错误 +- `404 Not Found`: 项目不存在 + +--- + +### 8. 删除项目 + +**DELETE** `/api/projects/:projectId` + +**需要认证**: 是 + +**路径参数**: +- `projectId`: 项目ID (number) + +**响应** (200 OK): +```json +{ + "message": "删除成功" +} +``` + +**错误响应**: +- `400 Bad Request`: 无效的项目ID +- `404 Not Found`: 项目不存在 + +--- + ## 看板相关接口 -### 4. 获取用户的所有看板 +### 9. 获取用户的所有看板 **GET** `/api/boards` @@ -145,7 +309,7 @@ Authorization: Bearer --- -### 5. 创建看板 +### 10. 创建看板 **POST** `/api/boards` @@ -179,7 +343,7 @@ Authorization: Bearer --- -### 6. 获取单个看板(包含嵌套的列和任务) +### 11. 获取单个看板(包含嵌套的列和任务) **GET** `/api/boards/:boardId` @@ -252,7 +416,7 @@ Authorization: Bearer --- -### 7. 更新看板 +### 12. 更新看板 **PUT** `/api/boards/:boardId` @@ -285,7 +449,7 @@ Authorization: Bearer --- -### 8. 删除看板 +### 13. 删除看板 **DELETE** `/api/boards/:boardId` @@ -309,7 +473,7 @@ Authorization: Bearer ## 列相关接口 -### 9. 获取看板的所有列 +### 14. 获取看板的所有列 **GET** `/api/boards/:boardId/columns` @@ -339,7 +503,7 @@ Authorization: Bearer --- -### 10. 创建列 +### 15. 创建列 **POST** `/api/boards/:boardId/columns` @@ -376,7 +540,7 @@ Authorization: Bearer --- -### 11. 获取单个列 +### 16. 获取单个列 **GET** `/api/columns/:columnId` @@ -418,7 +582,7 @@ Authorization: Bearer --- -### 12. 更新列 +### 17. 更新列 **PUT** `/api/columns/:columnId` @@ -451,7 +615,7 @@ Authorization: Bearer --- -### 13. 删除列 +### 18. 删除列 **DELETE** `/api/columns/:columnId` @@ -475,7 +639,7 @@ Authorization: Bearer ## 任务相关接口 -### 14. 获取列的所有任务 +### 19. 获取列的所有任务 **GET** `/api/columns/:columnId/tasks` @@ -513,7 +677,7 @@ Authorization: Bearer --- -### 15. 创建任务 +### 20. 创建任务 **POST** `/api/columns/:columnId/tasks` @@ -561,7 +725,7 @@ Authorization: Bearer --- -### 16. 获取单个任务 +### 21. 获取单个任务 **GET** `/api/tasks/:taskId` @@ -614,7 +778,7 @@ Authorization: Bearer --- -### 17. 更新任务 +### 22. 更新任务 **PUT** `/api/tasks/:taskId` @@ -652,7 +816,7 @@ Authorization: Bearer --- -### 18. 删除任务 +### 23. 删除任务 **DELETE** `/api/tasks/:taskId` @@ -674,7 +838,7 @@ Authorization: Bearer --- -### 19. 移动任务(拖拽排序) +### 24. 移动任务(拖拽排序) **PATCH** `/api/tasks/:taskId/move` @@ -727,6 +891,20 @@ Authorization: Bearer ## 数据模型说明 +### Project (项目) + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | number | 项目ID | +| name | string | 项目名称 | +| description | string | 项目描述 | +| status | number | 项目状态(1=进行中, 2=已完成, 3=已暂停, 4=已取消) | +| start_date | string | 项目开始时间(ISO 8601格式) | +| end_date | string | 项目结束时间(ISO 8601格式) | +| owner_id | number | 所有者用户ID | +| created_at | string | 创建时间 | +| updated_at | string | 更新时间 | + ### Board (看板) | 字段 | 类型 | 说明 | @@ -840,4 +1018,5 @@ Authorization: Bearer ## 更新日志 - **2025-11-20**: 初始版本,包含看板、列、任务的完整CRUD接口和拖拽排序功能 +- **2025-11-20**: 新增项目(Project)的完整CRUD接口 diff --git a/backend/handlers/project/project.go b/backend/handlers/project/project.go new file mode 100644 index 0000000..21e0b55 --- /dev/null +++ b/backend/handlers/project/project.go @@ -0,0 +1,177 @@ +package project + +import ( + "net/http" + "strconv" + "time" + + "progress-wall-backend/models" + "progress-wall-backend/services" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +// ProjectHandler 项目处理器 +type ProjectHandler struct { + projectService *services.ProjectService +} + +// NewProjectHandler 创建项目处理器 +func NewProjectHandler(db *gorm.DB) *ProjectHandler { + return &ProjectHandler{ + projectService: services.NewProjectService(db), + } +} + +// GetProject 获取单个项目 +func (h *ProjectHandler) GetProject(c *gin.Context) { + projectID, err := strconv.ParseUint(c.Param("projectId"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效的项目ID"}) + return + } + + project, err := h.projectService.GetProjectByID(uint(projectID)) + if err != nil { + if err == services.ErrProjectNotFound { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, project) +} + +// GetProjects 获取用户的所有项目 +func (h *ProjectHandler) GetProjects(c *gin.Context) { + userID := c.GetUint("user_id") + if userID == 0 { + c.JSON(http.StatusUnauthorized, gin.H{"error": "无法获取用户信息"}) + return + } + + projects, err := h.projectService.GetProjectsByUserID(userID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"projects": projects}) +} + +// CreateProject 创建项目 +func (h *ProjectHandler) CreateProject(c *gin.Context) { + userID := c.GetUint("user_id") + if userID == 0 { + c.JSON(http.StatusUnauthorized, gin.H{"error": "无法获取用户信息"}) + return + } + + var createProjectRequest struct { + Name string `json:"name" binding:"required"` + Description string `json:"description"` + Status *int `json:"status"` + StartDate *time.Time `json:"start_date"` + EndDate *time.Time `json:"end_date"` + } + + if err := c.ShouldBindJSON(&createProjectRequest); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"}) + return + } + + status := models.ProjectStatusActive + if createProjectRequest.Status != nil { + status = models.ProjectStatus(*createProjectRequest.Status) + } + + project := &models.Project{ + Name: createProjectRequest.Name, + Description: createProjectRequest.Description, + Status: status, + StartDate: createProjectRequest.StartDate, + EndDate: createProjectRequest.EndDate, + OwnerID: userID, + } + + if err := h.projectService.CreateProject(project); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusCreated, project) +} + +// UpdateProject 更新项目 +func (h *ProjectHandler) UpdateProject(c *gin.Context) { + projectID, err := strconv.ParseUint(c.Param("projectId"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效的项目ID"}) + return + } + + var updateProjectRequest struct { + Name *string `json:"name"` + Description *string `json:"description"` + Status *models.ProjectStatus `json:"status"` + StartDate *time.Time `json:"start_date"` + EndDate *time.Time `json:"end_date"` + } + + if err := c.ShouldBindJSON(&updateProjectRequest); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"}) + return + } + + updates := make(map[string]interface{}) + if updateProjectRequest.Name != nil { + updates["name"] = *updateProjectRequest.Name + } + if updateProjectRequest.Description != nil { + updates["description"] = *updateProjectRequest.Description + } + if updateProjectRequest.Status != nil { + updates["status"] = *updateProjectRequest.Status + } + if updateProjectRequest.StartDate != nil { + updates["start_date"] = *updateProjectRequest.StartDate + } + if updateProjectRequest.EndDate != nil { + updates["end_date"] = *updateProjectRequest.EndDate + } + + if err := h.projectService.UpdateProject(uint(projectID), updates); err != nil { + if err == services.ErrProjectNotFound { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "更新成功"}) +} + +// DeleteProject 删除项目 +func (h *ProjectHandler) DeleteProject(c *gin.Context) { + projectID, err := strconv.ParseUint(c.Param("projectId"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效的项目ID"}) + return + } + + if err := h.projectService.DeleteProject(uint(projectID)); err != nil { + if err == services.ErrProjectNotFound { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "删除成功"}) +} + diff --git a/backend/routes/routes.go b/backend/routes/routes.go index f245a23..173a341 100644 --- a/backend/routes/routes.go +++ b/backend/routes/routes.go @@ -8,6 +8,7 @@ import ( "progress-wall-backend/handlers/auth" "progress-wall-backend/handlers/board" "progress-wall-backend/handlers/column" + "progress-wall-backend/handlers/project" "progress-wall-backend/handlers/task" "progress-wall-backend/handlers/user" "progress-wall-backend/middleware" @@ -38,6 +39,7 @@ func SetupRoutes(db *gorm.DB, cfg *config.Config) *gin.Engine { loginHandler := auth.NewLoginHandler(db, cfg) registerHandler := auth.NewRegisterHandler(db, cfg) profileHandler := user.NewProfileHandler(db) + projectHandler := project.NewProjectHandler(db) boardHandler := board.NewBoardHandler(db) columnHandler := column.NewColumnHandler(db) taskHandler := task.NewTaskHandler(db) @@ -61,6 +63,13 @@ func SetupRoutes(db *gorm.DB, cfg *config.Config) *gin.Engine { // 用户相关 protected.GET("/user/profile", profileHandler.GetProfile) + // 项目相关 + protected.GET("/projects", projectHandler.GetProjects) + protected.POST("/projects", projectHandler.CreateProject) + protected.GET("/projects/:projectId", projectHandler.GetProject) + protected.PUT("/projects/:projectId", projectHandler.UpdateProject) + protected.DELETE("/projects/:projectId", projectHandler.DeleteProject) + // 看板相关 protected.GET("/boards", boardHandler.GetBoards) protected.POST("/boards", boardHandler.CreateBoard) diff --git a/backend/services/errors.go b/backend/services/errors.go index bda6b18..ec3d998 100644 --- a/backend/services/errors.go +++ b/backend/services/errors.go @@ -14,5 +14,6 @@ var ( ErrBoardNotFound = errors.New("看板不存在") ErrColumnNotFound = errors.New("列不存在") ErrTaskNotFound = errors.New("任务不存在") + ErrProjectNotFound = errors.New("项目不存在") ErrAccessDenied = errors.New("没有访问权限") ) diff --git a/backend/services/project_service.go b/backend/services/project_service.go new file mode 100644 index 0000000..6b7688b --- /dev/null +++ b/backend/services/project_service.go @@ -0,0 +1,89 @@ +package services + +import ( + "errors" + "fmt" + + "progress-wall-backend/models" + + "gorm.io/gorm" +) + +// ProjectService 项目服务 +type ProjectService struct { + db *gorm.DB +} + +// NewProjectService 创建项目服务 +func NewProjectService(db *gorm.DB) *ProjectService { + return &ProjectService{ + db: db, + } +} + +// GetProjectByID 根据ID获取项目 +func (s *ProjectService) GetProjectByID(projectID uint) (*models.Project, error) { + var project models.Project + result := s.db. + Preload("Owner"). + Preload("Boards"). + Preload("Members"). + First(&project, projectID) + + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, ErrProjectNotFound + } + return nil, fmt.Errorf("查询项目失败: %v", result.Error) + } + + return &project, nil +} + +// GetProjectsByUserID 获取用户的所有项目 +func (s *ProjectService) GetProjectsByUserID(userID uint) ([]models.Project, error) { + var projects []models.Project + result := s.db. + Where("owner_id = ?", userID). + Preload("Owner"). + Order("created_at DESC"). + Find(&projects) + + if result.Error != nil { + return nil, errors.New("查询项目列表失败") + } + + return projects, nil +} + +// CreateProject 创建项目 +func (s *ProjectService) CreateProject(project *models.Project) error { + if err := s.db.Create(project).Error; err != nil { + return fmt.Errorf("创建项目失败: %v", err) + } + return nil +} + +// UpdateProject 更新项目 +func (s *ProjectService) UpdateProject(projectID uint, updates map[string]interface{}) error { + result := s.db.Model(&models.Project{}).Where("id = ?", projectID).Updates(updates) + if result.Error != nil { + return fmt.Errorf("更新项目失败: %v", result.Error) + } + if result.RowsAffected == 0 { + return ErrProjectNotFound + } + return nil +} + +// DeleteProject 删除项目(软删除) +func (s *ProjectService) DeleteProject(projectID uint) error { + result := s.db.Delete(&models.Project{}, projectID) + if result.Error != nil { + return fmt.Errorf("删除项目失败: %v", result.Error) + } + if result.RowsAffected == 0 { + return ErrProjectNotFound + } + return nil +}