Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions backend/database/migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package database

import (
"log"

"progress-wall-backend/models"

"gorm.io/gorm"
Expand All @@ -13,8 +14,10 @@ func Migrate(db *gorm.DB) error {

// 迁移所有模型
err := db.AutoMigrate(
// 用户和权限相关
// 用户相关
&models.User{},

// 团队相关
&models.Team{},
&models.TeamMember{},

Expand All @@ -34,7 +37,6 @@ func Migrate(db *gorm.DB) error {
// 活动日志
&models.ActivityLog{},
)

if err != nil {
log.Printf("数据库迁移失败: %v", err)
return err
Expand Down
39 changes: 28 additions & 11 deletions backend/handlers/project/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ func NewProjectHandler(db *gorm.DB) *ProjectHandler {
}

// GetProject 获取单个项目
// GET /api/projects/:projectId
func (h *ProjectHandler) GetProject(c *gin.Context) {
projectID, err := strconv.ParseUint(c.Param("projectId"), 10, 32)
if err != nil {
Expand All @@ -45,7 +46,8 @@ func (h *ProjectHandler) GetProject(c *gin.Context) {
c.JSON(http.StatusOK, project)
}

// GetProjects 获取用户的所有项目
// GetProjects 获取用户的所有项目 (Deprecated or kept for "All Projects" view)
// GET /api/projects
func (h *ProjectHandler) GetProjects(c *gin.Context) {
userID := c.GetUint("user_id")
if userID == 0 {
Expand All @@ -62,34 +64,43 @@ func (h *ProjectHandler) GetProjects(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"projects": projects})
}

// CreateProject 创建项目
// POST /api/teams/:teamId/projects
func (h *ProjectHandler) CreateProject(c *gin.Context) {
userID := c.GetUint("user_id") // Set by AuthMiddleware
userID := c.GetUint("user_id")
if userID == 0 {
c.JSON(http.StatusUnauthorized, gin.H{"error": "无法获取用户信息"})
return
}

teamIDStr := c.Param("teamId")
teamID, err := strconv.ParseUint(teamIDStr, 10, 32) // Should already validated by RBAC Middleware
teamID, err := strconv.ParseUint(teamIDStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid Team ID"})
return
}

var createProjectRequest struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
Name string `json:"name" binding:"required,min=1,max=100"`
Description string `json:"description" binding:"max=500"`
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": "请求参数错误"})
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误: " + err.Error()})
return
}

// Validate dates
if createProjectRequest.StartDate != nil && createProjectRequest.EndDate != nil {
if createProjectRequest.EndDate.Before(*createProjectRequest.StartDate) {
c.JSON(http.StatusBadRequest, gin.H{"error": "结束时间不能早于开始时间"})
return
}
}

status := models.ProjectStatusActive
if createProjectRequest.Status != nil {
status = models.ProjectStatus(*createProjectRequest.Status)
Expand All @@ -113,11 +124,15 @@ func (h *ProjectHandler) CreateProject(c *gin.Context) {
c.JSON(http.StatusCreated, project)
}

// Gets all projects for a specific team.
// GetTeamProjects Gets all projects for a specific team.
// GET /api/teams/:teamId/projects
func (h *ProjectHandler) GetTeamProjects(c *gin.Context) {
teamIDStr := c.Param("teamId")
teamID, _ := strconv.ParseUint(teamIDStr, 10, 32) // Already validated by RBAC Middleware
teamID, err := strconv.ParseUint(teamIDStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid Team ID"})
return
}

projects, err := h.projectService.GetTeamProjects(uint(teamID))
if err != nil {
Expand All @@ -129,6 +144,7 @@ func (h *ProjectHandler) GetTeamProjects(c *gin.Context) {
}

// UpdateProject 更新项目
// PUT /api/projects/:projectId
func (h *ProjectHandler) UpdateProject(c *gin.Context) {
projectID, err := strconv.ParseUint(c.Param("projectId"), 10, 32)
if err != nil {
Expand All @@ -137,15 +153,15 @@ func (h *ProjectHandler) UpdateProject(c *gin.Context) {
}

var updateProjectRequest struct {
Name *string `json:"name"`
Description *string `json:"description"`
Name *string `json:"name" binding:"omitempty,min=1,max=100"`
Description *string `json:"description" binding:"omitempty,max=500"`
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": "请求参数错误"})
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误: " + err.Error()})
return
}

Expand Down Expand Up @@ -179,6 +195,7 @@ func (h *ProjectHandler) UpdateProject(c *gin.Context) {
}

// DeleteProject 删除项目
// DELETE /api/projects/:projectId
func (h *ProjectHandler) DeleteProject(c *gin.Context) {
projectID, err := strconv.ParseUint(c.Param("projectId"), 10, 32)
if err != nil {
Expand Down
95 changes: 58 additions & 37 deletions backend/handlers/team/team.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,20 @@ func NewTeamHandler(db *gorm.DB) *TeamHandler {
}
}

// POST /api/teams
func (h *TeamHandler) CreateTeam(c *gin.Context) {
userID := c.GetUint("user_id") // Get from AuthMiddleware

var req struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
}
type CreateTeamRequest struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
}

// CreateTeam 创建团队
func (h *TeamHandler) CreateTeam(c *gin.Context) {
var req CreateTeamRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid parameters"})
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

userID := c.GetUint("user_id")
team, err := h.teamService.CreateTeam(req.Name, req.Description, userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
Expand All @@ -44,67 +44,88 @@ func (h *TeamHandler) CreateTeam(c *gin.Context) {
c.JSON(http.StatusCreated, team)
}

// Gets all teams a user belongs to.
// GET /api/teams
// GetMyTeams 获取当前用户的团队列表
func (h *TeamHandler) GetMyTeams(c *gin.Context) {
userID := c.GetUint("user_id")

teams, err := h.teamService.GetUserTeams(userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch teams: " + err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

c.JSON(http.StatusOK, gin.H{"teams": teams})
}

// POST /api/teams/:teamId/members
func (h *TeamHandler) AddMember(c *gin.Context) {
// GetTeam 获取单个团队详情
func (h *TeamHandler) GetTeam(c *gin.Context) {
teamIDStr := c.Param("teamId")
teamID, err := strconv.ParseUint(teamIDStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid Team ID"})
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid team ID"})
return
}

// Who to add, and what role
var req struct {
UserID uint `json:"user_id" binding:"required"`
Role models.TeamRole `json:"role"`
}

if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid parameters"})
team, err := h.teamService.GetTeamByID(uint(teamID))
if err != nil {
if err == services.ErrTeamNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Team not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

// Default to Member if role is invalid
if req.Role != models.TeamRoleAdmin {
req.Role = models.TeamRoleMember
c.JSON(http.StatusOK, team)
}

// GetTeamMembers 获取团队成员
func (h *TeamHandler) GetTeamMembers(c *gin.Context) {
teamIDStr := c.Param("teamId")
teamID, err := strconv.ParseUint(teamIDStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid team ID"})
return
}

if err := h.teamService.AddMember(uint(teamID), req.UserID, req.Role); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
members, err := h.teamService.GetMembers(uint(teamID))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

c.JSON(http.StatusOK, gin.H{"message": "Member added successfully"})
c.JSON(http.StatusOK, gin.H{"members": members})
}

// GET /api/teams/:teamId/members
func (h *TeamHandler) GetTeamMembers(c *gin.Context) {
// AddMember 添加成员
func (h *TeamHandler) AddMember(c *gin.Context) {
teamIDStr := c.Param("teamId")
teamID, err := strconv.ParseUint(teamIDStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid Team ID"})
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid team ID"})
return
}

members, err := h.teamService.GetTeamMembers(uint(teamID))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch team members: " + err.Error()})
var req struct {
UserID uint `json:"user_id" binding:"required"`
Role models.TeamRole `json:"role"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

c.JSON(http.StatusOK, gin.H{"members": members})
if req.Role == 0 {
req.Role = models.TeamRoleMember // Default to member
}

if err := h.teamService.AddTeamMember(uint(teamID), req.UserID, req.Role); err != nil {
if err == services.ErrUserAlreadyMember {
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

c.JSON(http.StatusOK, gin.H{"message": "Member added successfully"})
}
19 changes: 16 additions & 3 deletions backend/routes/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ func SetupRoutes(db *gorm.DB, cfg *config.Config) *gin.Engine {
// Team Routes
protected.POST("/teams", teamHandler.CreateTeam)
protected.GET("/teams", teamHandler.GetMyTeams)
protected.GET("/teams/:teamId",
rbac.RequireTeamAccess("view", "teamId"),
teamHandler.GetTeam,
)
protected.GET("/teams/:teamId/members",
rbac.RequireTeamAccess("view", "teamId"),
teamHandler.GetTeamMembers,
Expand All @@ -91,9 +95,18 @@ func SetupRoutes(db *gorm.DB, cfg *config.Config) *gin.Engine {
projectHandler.GetTeamProjects,
)
protected.GET("/projects", projectHandler.GetProjects)
protected.GET("/projects/:projectId", projectHandler.GetProject)
protected.PUT("/projects/:projectId", projectHandler.UpdateProject)
protected.DELETE("/projects/:projectId", projectHandler.DeleteProject)
protected.GET("/projects/:projectId",
rbac.RequireProjectAccess("view", "projectId", "project"),
projectHandler.GetProject,
)
protected.PUT("/projects/:projectId",
rbac.RequireProjectAccess("manage", "projectId", "project"),
projectHandler.UpdateProject,
)
protected.DELETE("/projects/:projectId",
rbac.RequireProjectAccess("manage", "projectId", "project"),
projectHandler.DeleteProject,
)

// 看板相关
protected.GET("/boards", boardHandler.GetBoards)
Expand Down
17 changes: 9 additions & 8 deletions backend/services/project_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,19 @@ import (
"gorm.io/gorm"
)

// ProjectService 项目服务
// ProjectService provides methods for managing projects.
type ProjectService struct {
db *gorm.DB
}

// NewProjectService 创建项目服务
// NewProjectService creates a new instance of ProjectService.
func NewProjectService(db *gorm.DB) *ProjectService {
return &ProjectService{
db: db,
}
}

// GetProjectByID 根据ID获取项目
// GetProjectByID retrieves a project by its ID, including its owner, boards, and members.
func (s *ProjectService) GetProjectByID(projectID uint) (*models.Project, error) {
var project models.Project
result := s.db.
Expand All @@ -40,7 +40,7 @@ func (s *ProjectService) GetProjectByID(projectID uint) (*models.Project, error)
return &project, nil
}

// GetProjectsByUserID 获取用户的所有项目
// GetProjectsByUserID retrieves all projects owned by a specific user.
func (s *ProjectService) GetProjectsByUserID(userID uint) ([]models.Project, error) {
var projects []models.Project
result := s.db.
Expand All @@ -56,7 +56,8 @@ func (s *ProjectService) GetProjectsByUserID(userID uint) ([]models.Project, err
return projects, nil
}

// Creates a new project under a team and assigns the creator as ProjectAdmin.
// CreateProject creates a new project under a team and assigns the creator as ProjectAdmin.
// It executes the creation and member assignment within a transaction.
func (s *ProjectService) CreateProject(project *models.Project) error {
tx := s.db.Begin()
defer func() {
Expand Down Expand Up @@ -94,14 +95,14 @@ func (s *ProjectService) CreateProject(project *models.Project) error {
return nil
}

// Gets all projects for a specific team.
// GetTeamProjects retrieves all projects associated with a specific team.
func (s *ProjectService) GetTeamProjects(teamID uint) ([]models.Project, error) {
var projects []models.Project
err := s.db.Where("team_id = ?", teamID).Find(&projects).Error
return projects, err
}

// UpdateProject 更新项目
// UpdateProject updates the fields of a project identified by projectID.
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 {
Expand All @@ -113,7 +114,7 @@ func (s *ProjectService) UpdateProject(projectID uint, updates map[string]interf
return nil
}

// DeleteProject 删除项目(软删除)
// DeleteProject soft-deletes a project by its ID.
func (s *ProjectService) DeleteProject(projectID uint) error {
result := s.db.Delete(&models.Project{}, projectID)
if result.Error != nil {
Expand Down
Loading