From 99755b216f11d3e0d1cb17e6c43acd06f1eb9d5b Mon Sep 17 00:00:00 2001 From: dark Date: Thu, 10 Jul 2025 13:56:42 +0800 Subject: [PATCH] api --- .../handler/statistics/get_overview.go | 193 ++++++- .../handler/statistics/get_task_statistics.go | 336 ++++++++++++- .../handler/statistics/get_user_statistics.go | 447 ++++++++++++++++- .../handler/task/add_task_attachment.go | 79 ++- .../internal/handler/task/add_task_comment.go | 72 ++- backend/internal/handler/task/create_task.go | 100 +++- backend/internal/handler/task/delete_task.go | 59 ++- .../handler/task/delete_task_attachment.go | 62 ++- .../handler/task/delete_task_comment.go | 55 +- backend/internal/handler/task/get_task.go | 37 +- .../handler/task/get_task_attachments.go | 38 +- .../handler/task/get_task_comments.go | 39 +- backend/internal/handler/task/get_tasks.go | 121 ++++- backend/internal/handler/task/update_task.go | 95 +++- .../handler/task/update_task_status.go | 77 ++- backend/internal/handler/task/upload_file.go | 64 ++- backend/model/model.go | 10 + backend/pkg/database/database.go | 15 + frontend/src/api/index.ts | 351 ++++++++++++- frontend/src/views/Task/index.vue | 469 ++++++++---------- 20 files changed, 2425 insertions(+), 294 deletions(-) diff --git a/backend/internal/handler/statistics/get_overview.go b/backend/internal/handler/statistics/get_overview.go index 1ba668c..4d67e25 100644 --- a/backend/internal/handler/statistics/get_overview.go +++ b/backend/internal/handler/statistics/get_overview.go @@ -1,7 +1,196 @@ package statistics -import "github.com/gin-gonic/gin" +import ( + "net/http" + "strconv" + "time" + + "task-track-backend/model" + "task-track-backend/pkg/database" + + "github.com/gin-gonic/gin" +) + +// OverviewResponse 总览统计响应 +type OverviewResponse struct { + // 基本统计 + TotalTasks int64 `json:"total_tasks"` + TotalUsers int64 `json:"total_users"` + TotalOrganizations int64 `json:"total_organizations"` + + // 任务状态统计 + TasksByStatus map[string]int64 `json:"tasks_by_status"` + + // 任务优先级统计 + TasksByPriority map[string]int64 `json:"tasks_by_priority"` + + // 最近7天任务创建数 + RecentTasksCount int64 `json:"recent_tasks_count"` + + // 最近7天任务完成数 + RecentCompletedTasks int64 `json:"recent_completed_tasks"` + + // 今日任务统计 + TodayTasks struct { + Created int64 `json:"created"` + Completed int64 `json:"completed"` + } `json:"today_tasks"` + + // 本周任务统计 + WeeklyTasks struct { + Created int64 `json:"created"` + Completed int64 `json:"completed"` + } `json:"weekly_tasks"` + + // 本月任务统计 + MonthlyTasks struct { + Created int64 `json:"created"` + Completed int64 `json:"completed"` + } `json:"monthly_tasks"` +} func (h *StatisticsHandler) GetOverview(c *gin.Context) { - // TODO: 实现统计概览接口 + // 获取组织ID参数(可选) + organizationIDStr := c.Query("organization_id") + var organizationID uint + if organizationIDStr != "" { + if id, err := strconv.ParseUint(organizationIDStr, 10, 32); err == nil { + organizationID = uint(id) + } + } + + db := database.GetDB() + var response OverviewResponse + + // 基础查询构建器 + baseTaskQuery := db.Model(&model.Task{}) + if organizationID > 0 { + baseTaskQuery = baseTaskQuery.Where("organization_id = ?", organizationID) + } + + // 1. 基本统计 + // 总任务数 + if err := baseTaskQuery.Count(&response.TotalTasks).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取任务统计失败"}) + return + } + + // 总用户数 + userQuery := db.Model(&model.User{}) + if err := userQuery.Count(&response.TotalUsers).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取用户统计失败"}) + return + } + + // 总组织数 + if err := db.Model(&model.Organization{}).Count(&response.TotalOrganizations).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取组织统计失败"}) + return + } + + // 2. 任务状态统计 + var statusStats []struct { + Status string `json:"status"` + Count int64 `json:"count"` + } + + statusQuery := baseTaskQuery.Select("status, count(*) as count").Group("status") + if err := statusQuery.Find(&statusStats).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取任务状态统计失败"}) + return + } + + response.TasksByStatus = make(map[string]int64) + for _, stat := range statusStats { + response.TasksByStatus[stat.Status] = stat.Count + } + + // 3. 任务优先级统计 + var priorityStats []struct { + Priority string `json:"priority"` + Count int64 `json:"count"` + } + + priorityQuery := baseTaskQuery.Select("priority, count(*) as count").Group("priority") + if err := priorityQuery.Find(&priorityStats).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取任务优先级统计失败"}) + return + } + + response.TasksByPriority = make(map[string]int64) + for _, stat := range priorityStats { + response.TasksByPriority[stat.Priority] = stat.Count + } + + // 4. 时间范围统计 + now := time.Now() + + // 今日开始时间 + todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) + + // 本周开始时间(周一) + weekStart := todayStart.AddDate(0, 0, -int(now.Weekday())+1) + if now.Weekday() == time.Sunday { + weekStart = weekStart.AddDate(0, 0, -7) + } + + // 本月开始时间 + monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) + + // 最近7天开始时间 + sevenDaysAgo := now.AddDate(0, 0, -7) + + // 今日任务统计 + todayTaskQuery := baseTaskQuery.Where("created_at >= ?", todayStart) + if err := todayTaskQuery.Count(&response.TodayTasks.Created).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取今日任务统计失败"}) + return + } + + todayCompletedQuery := baseTaskQuery.Where("status = ? AND updated_at >= ?", "completed", todayStart) + if err := todayCompletedQuery.Count(&response.TodayTasks.Completed).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取今日完成任务统计失败"}) + return + } + + // 本周任务统计 + weekTaskQuery := baseTaskQuery.Where("created_at >= ?", weekStart) + if err := weekTaskQuery.Count(&response.WeeklyTasks.Created).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取本周任务统计失败"}) + return + } + + weekCompletedQuery := baseTaskQuery.Where("status = ? AND updated_at >= ?", "completed", weekStart) + if err := weekCompletedQuery.Count(&response.WeeklyTasks.Completed).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取本周完成任务统计失败"}) + return + } + + // 本月任务统计 + monthTaskQuery := baseTaskQuery.Where("created_at >= ?", monthStart) + if err := monthTaskQuery.Count(&response.MonthlyTasks.Created).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取本月任务统计失败"}) + return + } + + monthCompletedQuery := baseTaskQuery.Where("status = ? AND updated_at >= ?", "completed", monthStart) + if err := monthCompletedQuery.Count(&response.MonthlyTasks.Completed).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取本月完成任务统计失败"}) + return + } + + // 最近7天任务统计 + recentTaskQuery := baseTaskQuery.Where("created_at >= ?", sevenDaysAgo) + if err := recentTaskQuery.Count(&response.RecentTasksCount).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取最近任务统计失败"}) + return + } + + recentCompletedQuery := baseTaskQuery.Where("status = ? AND updated_at >= ?", "completed", sevenDaysAgo) + if err := recentCompletedQuery.Count(&response.RecentCompletedTasks).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取最近完成任务统计失败"}) + return + } + + c.JSON(http.StatusOK, response) } diff --git a/backend/internal/handler/statistics/get_task_statistics.go b/backend/internal/handler/statistics/get_task_statistics.go index a0d6ef0..e50152e 100644 --- a/backend/internal/handler/statistics/get_task_statistics.go +++ b/backend/internal/handler/statistics/get_task_statistics.go @@ -1,7 +1,339 @@ package statistics -import "github.com/gin-gonic/gin" +import ( + "net/http" + "strconv" + "time" + + "task-track-backend/model" + "task-track-backend/pkg/database" + + "github.com/gin-gonic/gin" +) + +// TaskStatisticsResponse 任务统计响应 +type TaskStatisticsResponse struct { + // 基本统计 + TotalTasks int64 `json:"total_tasks"` + PendingTasks int64 `json:"pending_tasks"` + InProgressTasks int64 `json:"in_progress_tasks"` + CompletedTasks int64 `json:"completed_tasks"` + CancelledTasks int64 `json:"cancelled_tasks"` + + // 按状态统计 + TasksByStatus []TaskStatusStat `json:"tasks_by_status"` + + // 按优先级统计 + TasksByPriority []TaskPriorityStat `json:"tasks_by_priority"` + + // 按类型统计 + TasksByType []TaskTypeStat `json:"tasks_by_type"` + + // 按创建者统计 + TasksByCreator []TaskCreatorStat `json:"tasks_by_creator"` + + // 按分配者统计 + TasksByAssignee []TaskAssigneeStat `json:"tasks_by_assignee"` + + // 时间趋势统计(最近30天) + DailyTrend []DailyTaskStat `json:"daily_trend"` + + // 完成率统计 + CompletionRate float64 `json:"completion_rate"` + + // 平均完成时间(小时) + AverageCompletionTime float64 `json:"average_completion_time"` + + // 逾期任务统计 + OverdueTasks int64 `json:"overdue_tasks"` + + // 即将到期任务统计(7天内) + UpcomingTasks int64 `json:"upcoming_tasks"` +} + +// TaskStatusStat 任务状态统计 +type TaskStatusStat struct { + Status string `json:"status"` + Count int64 `json:"count"` + Percentage float64 `json:"percentage"` +} + +// TaskPriorityStat 任务优先级统计 +type TaskPriorityStat struct { + Priority string `json:"priority"` + Count int64 `json:"count"` + Percentage float64 `json:"percentage"` +} + +// TaskTypeStat 任务类型统计 +type TaskTypeStat struct { + Type string `json:"type"` + Count int64 `json:"count"` + Percentage float64 `json:"percentage"` +} + +// TaskCreatorStat 任务创建者统计 +type TaskCreatorStat struct { + CreatorID uint `json:"creator_id"` + CreatorName string `json:"creator_name"` + Count int64 `json:"count"` +} + +// TaskAssigneeStat 任务分配者统计 +type TaskAssigneeStat struct { + AssigneeID uint `json:"assignee_id"` + AssigneeName string `json:"assignee_name"` + Count int64 `json:"count"` +} + +// DailyTaskStat 每日任务统计 +type DailyTaskStat struct { + Date string `json:"date"` + Created int64 `json:"created"` + Completed int64 `json:"completed"` +} func (h *StatisticsHandler) GetTaskStatistics(c *gin.Context) { - // TODO: 实现任务统计接口 + // 获取组织ID参数(可选) + organizationIDStr := c.Query("organization_id") + var organizationID uint + if organizationIDStr != "" { + if id, err := strconv.ParseUint(organizationIDStr, 10, 32); err == nil { + organizationID = uint(id) + } + } + + // 获取时间范围参数 + daysStr := c.DefaultQuery("days", "30") + days, err := strconv.Atoi(daysStr) + if err != nil { + days = 30 + } + + db := database.GetDB() + var response TaskStatisticsResponse + + // 基础查询构建器 + baseTaskQuery := db.Model(&model.Task{}) + if organizationID > 0 { + baseTaskQuery = baseTaskQuery.Where("organization_id = ?", organizationID) + } + + // 1. 基本统计 + if err := baseTaskQuery.Count(&response.TotalTasks).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取任务总数失败"}) + return + } + + // 各状态任务数 + if err := baseTaskQuery.Where("status = ?", "pending").Count(&response.PendingTasks).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取待处理任务数失败"}) + return + } + + if err := baseTaskQuery.Where("status = ?", "in_progress").Count(&response.InProgressTasks).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取进行中任务数失败"}) + return + } + + if err := baseTaskQuery.Where("status = ?", "completed").Count(&response.CompletedTasks).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取已完成任务数失败"}) + return + } + + if err := baseTaskQuery.Where("status = ?", "cancelled").Count(&response.CancelledTasks).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取已取消任务数失败"}) + return + } + + // 2. 按状态统计 + var statusStats []struct { + Status string `json:"status"` + Count int64 `json:"count"` + } + + statusQuery := baseTaskQuery.Select("status, count(*) as count").Group("status") + if err := statusQuery.Find(&statusStats).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取任务状态统计失败"}) + return + } + + response.TasksByStatus = make([]TaskStatusStat, len(statusStats)) + for i, stat := range statusStats { + percentage := float64(0) + if response.TotalTasks > 0 { + percentage = float64(stat.Count) / float64(response.TotalTasks) * 100 + } + response.TasksByStatus[i] = TaskStatusStat{ + Status: stat.Status, + Count: stat.Count, + Percentage: percentage, + } + } + + // 3. 按优先级统计 + var priorityStats []struct { + Priority string `json:"priority"` + Count int64 `json:"count"` + } + + priorityQuery := baseTaskQuery.Select("priority, count(*) as count").Group("priority") + if err := priorityQuery.Find(&priorityStats).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取任务优先级统计失败"}) + return + } + + response.TasksByPriority = make([]TaskPriorityStat, len(priorityStats)) + for i, stat := range priorityStats { + percentage := float64(0) + if response.TotalTasks > 0 { + percentage = float64(stat.Count) / float64(response.TotalTasks) * 100 + } + response.TasksByPriority[i] = TaskPriorityStat{ + Priority: stat.Priority, + Count: stat.Count, + Percentage: percentage, + } + } + + // 4. 按类型统计 + var typeStats []struct { + Type string `json:"type"` + Count int64 `json:"count"` + } + + typeQuery := baseTaskQuery.Select("type, count(*) as count").Group("type") + if err := typeQuery.Find(&typeStats).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取任务类型统计失败"}) + return + } + + response.TasksByType = make([]TaskTypeStat, len(typeStats)) + for i, stat := range typeStats { + percentage := float64(0) + if response.TotalTasks > 0 { + percentage = float64(stat.Count) / float64(response.TotalTasks) * 100 + } + response.TasksByType[i] = TaskTypeStat{ + Type: stat.Type, + Count: stat.Count, + Percentage: percentage, + } + } + + // 5. 按创建者统计 + var creatorStats []struct { + CreatorID uint `json:"creator_id"` + CreatorName string `json:"creator_name"` + Count int64 `json:"count"` + } + + creatorQuery := baseTaskQuery.Select("creator_id, users.real_name as creator_name, count(*) as count"). + Joins("LEFT JOIN users ON tasks.creator_id = users.id"). + Group("creator_id, users.real_name"). + Order("count desc"). + Limit(10) + if err := creatorQuery.Find(&creatorStats).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取创建者统计失败"}) + return + } + + response.TasksByCreator = make([]TaskCreatorStat, len(creatorStats)) + for i, stat := range creatorStats { + response.TasksByCreator[i] = TaskCreatorStat{ + CreatorID: stat.CreatorID, + CreatorName: stat.CreatorName, + Count: stat.Count, + } + } + + // 6. 按分配者统计 + var assigneeStats []struct { + AssigneeID uint `json:"assignee_id"` + AssigneeName string `json:"assignee_name"` + Count int64 `json:"count"` + } + + assigneeQuery := baseTaskQuery.Select("assignee_id, users.real_name as assignee_name, count(*) as count"). + Joins("LEFT JOIN users ON tasks.assignee_id = users.id"). + Where("assignee_id IS NOT NULL"). + Group("assignee_id, users.real_name"). + Order("count desc"). + Limit(10) + if err := assigneeQuery.Find(&assigneeStats).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取分配者统计失败"}) + return + } + + response.TasksByAssignee = make([]TaskAssigneeStat, len(assigneeStats)) + for i, stat := range assigneeStats { + response.TasksByAssignee[i] = TaskAssigneeStat{ + AssigneeID: stat.AssigneeID, + AssigneeName: stat.AssigneeName, + Count: stat.Count, + } + } + + // 7. 时间趋势统计 + now := time.Now() + startDate := now.AddDate(0, 0, -days) + + response.DailyTrend = make([]DailyTaskStat, 0) + + for i := 0; i < days; i++ { + date := startDate.AddDate(0, 0, i) + dateStr := date.Format("2006-01-02") + + // 当天创建的任务数 + var createdCount int64 + createdQuery := baseTaskQuery.Where("DATE(created_at) = ?", dateStr) + if err := createdQuery.Count(&createdCount).Error; err != nil { + createdCount = 0 + } + + // 当天完成的任务数 + var completedCount int64 + completedQuery := baseTaskQuery.Where("status = ? AND DATE(updated_at) = ?", "completed", dateStr) + if err := completedQuery.Count(&completedCount).Error; err != nil { + completedCount = 0 + } + + response.DailyTrend = append(response.DailyTrend, DailyTaskStat{ + Date: dateStr, + Created: createdCount, + Completed: completedCount, + }) + } + + // 8. 完成率 + if response.TotalTasks > 0 { + response.CompletionRate = float64(response.CompletedTasks) / float64(response.TotalTasks) * 100 + } + + // 9. 平均完成时间 + var avgCompletionTime struct { + AverageHours float64 `json:"average_hours"` + } + + avgQuery := baseTaskQuery.Select("AVG(TIMESTAMPDIFF(HOUR, created_at, updated_at)) as average_hours"). + Where("status = ?", "completed") + if err := avgQuery.Scan(&avgCompletionTime).Error; err == nil { + response.AverageCompletionTime = avgCompletionTime.AverageHours + } + + // 10. 逾期任务统计 + now = time.Now() + overdueQuery := baseTaskQuery.Where("end_time < ? AND status != ?", now, "completed") + if err := overdueQuery.Count(&response.OverdueTasks).Error; err != nil { + response.OverdueTasks = 0 + } + + // 11. 即将到期任务统计 + sevenDaysLater := now.AddDate(0, 0, 7) + upcomingQuery := baseTaskQuery.Where("end_time BETWEEN ? AND ? AND status != ?", now, sevenDaysLater, "completed") + if err := upcomingQuery.Count(&response.UpcomingTasks).Error; err != nil { + response.UpcomingTasks = 0 + } + + c.JSON(http.StatusOK, response) } diff --git a/backend/internal/handler/statistics/get_user_statistics.go b/backend/internal/handler/statistics/get_user_statistics.go index f329c48..ec5f11a 100644 --- a/backend/internal/handler/statistics/get_user_statistics.go +++ b/backend/internal/handler/statistics/get_user_statistics.go @@ -1,7 +1,450 @@ package statistics -import "github.com/gin-gonic/gin" +import ( + "fmt" + "net/http" + "strconv" + "time" + + "task-track-backend/model" + "task-track-backend/pkg/database" + + "github.com/gin-gonic/gin" +) + +// UserStatisticsResponse 用户统计响应 +type UserStatisticsResponse struct { + // 基本统计 + TotalUsers int64 `json:"total_users"` + ActiveUsers int64 `json:"active_users"` // 最近30天有活动的用户 + + // 用户任务统计 + UserTaskStats []UserTaskStat `json:"user_task_stats"` + + // 用户效率统计 + UserEfficiencyStats []UserEfficiencyStat `json:"user_efficiency_stats"` + + // 用户活跃度统计 + UserActivityStats []UserActivityStat `json:"user_activity_stats"` + + // 用户完成率排行 + UserCompletionRank []UserCompletionRank `json:"user_completion_rank"` + + // 用户任务分配统计 + UserAssignmentStats []UserAssignmentStat `json:"user_assignment_stats"` + + // 总体用户效率指标 + OverallEfficiency struct { + AverageCompletionRate float64 `json:"average_completion_rate"` + AverageTasksPerUser float64 `json:"average_tasks_per_user"` + AverageCompletionTime float64 `json:"average_completion_time"` + } `json:"overall_efficiency"` +} + +// UserTaskStat 用户任务统计 +type UserTaskStat struct { + UserID uint `json:"user_id"` + Username string `json:"username"` + RealName string `json:"real_name"` + CreatedTasks int64 `json:"created_tasks"` + AssignedTasks int64 `json:"assigned_tasks"` + CompletedTasks int64 `json:"completed_tasks"` + PendingTasks int64 `json:"pending_tasks"` + InProgressTasks int64 `json:"in_progress_tasks"` +} + +// UserEfficiencyStat 用户效率统计 +type UserEfficiencyStat struct { + UserID uint `json:"user_id"` + Username string `json:"username"` + RealName string `json:"real_name"` + CompletionRate float64 `json:"completion_rate"` + AverageCompletionTime float64 `json:"average_completion_time"` + TasksCompletedThisWeek int64 `json:"tasks_completed_this_week"` + TasksCompletedThisMonth int64 `json:"tasks_completed_this_month"` +} + +// UserActivityStat 用户活跃度统计 +type UserActivityStat struct { + UserID uint `json:"user_id"` + Username string `json:"username"` + RealName string `json:"real_name"` + LastActivity time.Time `json:"last_activity"` + CommentsCount int64 `json:"comments_count"` + TasksCreatedThisWeek int64 `json:"tasks_created_this_week"` + TasksCreatedThisMonth int64 `json:"tasks_created_this_month"` +} + +// UserCompletionRank 用户完成率排行 +type UserCompletionRank struct { + UserID uint `json:"user_id"` + Username string `json:"username"` + RealName string `json:"real_name"` + CompletionRate float64 `json:"completion_rate"` + CompletedTasks int64 `json:"completed_tasks"` + TotalTasks int64 `json:"total_tasks"` + Rank int `json:"rank"` +} + +// UserAssignmentStat 用户任务分配统计 +type UserAssignmentStat struct { + UserID uint `json:"user_id"` + Username string `json:"username"` + RealName string `json:"real_name"` + UrgentTasks int64 `json:"urgent_tasks"` + HighPriorityTasks int64 `json:"high_priority_tasks"` + MediumPriorityTasks int64 `json:"medium_priority_tasks"` + LowPriorityTasks int64 `json:"low_priority_tasks"` + OverdueTasks int64 `json:"overdue_tasks"` +} func (h *StatisticsHandler) GetUserStatistics(c *gin.Context) { - // TODO: 实现用户统计接口 + // 获取组织ID参数(可选) + organizationIDStr := c.Query("organization_id") + var organizationID uint + if organizationIDStr != "" { + if id, err := strconv.ParseUint(organizationIDStr, 10, 32); err == nil { + organizationID = uint(id) + } + } + + // 获取用户ID参数(可选,如果提供则只查询该用户) + userIDStr := c.Query("user_id") + var userID uint + if userIDStr != "" { + if id, err := strconv.ParseUint(userIDStr, 10, 32); err == nil { + userID = uint(id) + } + } + + db := database.GetDB() + var response UserStatisticsResponse + + // 基础用户查询构建器 + baseUserQuery := db.Model(&model.User{}) + if organizationID > 0 { + baseUserQuery = baseUserQuery.Joins("JOIN user_organizations ON users.id = user_organizations.user_id"). + Where("user_organizations.organization_id = ?", organizationID) + } + if userID > 0 { + baseUserQuery = baseUserQuery.Where("users.id = ?", userID) + } + + // 1. 基本统计 + if err := baseUserQuery.Count(&response.TotalUsers).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取用户总数失败"}) + return + } + + // 活跃用户数(最近30天有任务活动) + thirtyDaysAgo := time.Now().AddDate(0, 0, -30) + activeUserQuery := baseUserQuery.Joins("JOIN tasks ON users.id = tasks.creator_id OR users.id = tasks.assignee_id"). + Where("tasks.created_at >= ? OR tasks.updated_at >= ?", thirtyDaysAgo, thirtyDaysAgo). + Distinct("users.id") + if err := activeUserQuery.Count(&response.ActiveUsers).Error; err != nil { + response.ActiveUsers = 0 + } + + // 2. 用户任务统计 + var userTaskStats []UserTaskStat + + // 构建用户任务统计查询 + userTaskQuery := ` + SELECT + u.id as user_id, + u.username, + u.real_name, + COALESCE(created_tasks.count, 0) as created_tasks, + COALESCE(assigned_tasks.count, 0) as assigned_tasks, + COALESCE(completed_tasks.count, 0) as completed_tasks, + COALESCE(pending_tasks.count, 0) as pending_tasks, + COALESCE(in_progress_tasks.count, 0) as in_progress_tasks + FROM users u + LEFT JOIN ( + SELECT creator_id, COUNT(*) as count + FROM tasks + WHERE 1=1 %s + GROUP BY creator_id + ) created_tasks ON u.id = created_tasks.creator_id + LEFT JOIN ( + SELECT assignee_id, COUNT(*) as count + FROM tasks + WHERE assignee_id IS NOT NULL %s + GROUP BY assignee_id + ) assigned_tasks ON u.id = assigned_tasks.assignee_id + LEFT JOIN ( + SELECT assignee_id, COUNT(*) as count + FROM tasks + WHERE assignee_id IS NOT NULL AND status = 'completed' %s + GROUP BY assignee_id + ) completed_tasks ON u.id = completed_tasks.assignee_id + LEFT JOIN ( + SELECT assignee_id, COUNT(*) as count + FROM tasks + WHERE assignee_id IS NOT NULL AND status = 'pending' %s + GROUP BY assignee_id + ) pending_tasks ON u.id = pending_tasks.assignee_id + LEFT JOIN ( + SELECT assignee_id, COUNT(*) as count + FROM tasks + WHERE assignee_id IS NOT NULL AND status = 'in_progress' %s + GROUP BY assignee_id + ) in_progress_tasks ON u.id = in_progress_tasks.assignee_id + WHERE 1=1 %s + ORDER BY (COALESCE(created_tasks.count, 0) + COALESCE(assigned_tasks.count, 0)) DESC + LIMIT 20 + ` + + // 构建WHERE条件 + orgCondition := "" + userCondition := "" + if organizationID > 0 { + orgCondition = "AND organization_id = " + strconv.FormatUint(uint64(organizationID), 10) + } + if userID > 0 { + userCondition = "AND u.id = " + strconv.FormatUint(uint64(userID), 10) + } + + finalQuery := fmt.Sprintf(userTaskQuery, orgCondition, orgCondition, orgCondition, orgCondition, orgCondition, userCondition) + + if err := db.Raw(finalQuery).Scan(&userTaskStats).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取用户任务统计失败"}) + return + } + + response.UserTaskStats = userTaskStats + + // 3. 用户效率统计 + var userEfficiencyStats []UserEfficiencyStat + + now := time.Now() + weekStart := now.AddDate(0, 0, -int(now.Weekday())+1) + monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) + + efficiencyQuery := ` + SELECT + u.id as user_id, + u.username, + u.real_name, + CASE + WHEN total_assigned.count > 0 THEN + (completed_assigned.count * 100.0 / total_assigned.count) + ELSE 0 + END as completion_rate, + COALESCE(avg_completion.avg_hours, 0) as average_completion_time, + COALESCE(week_completed.count, 0) as tasks_completed_this_week, + COALESCE(month_completed.count, 0) as tasks_completed_this_month + FROM users u + LEFT JOIN ( + SELECT assignee_id, COUNT(*) as count + FROM tasks + WHERE assignee_id IS NOT NULL %s + GROUP BY assignee_id + ) total_assigned ON u.id = total_assigned.assignee_id + LEFT JOIN ( + SELECT assignee_id, COUNT(*) as count + FROM tasks + WHERE assignee_id IS NOT NULL AND status = 'completed' %s + GROUP BY assignee_id + ) completed_assigned ON u.id = completed_assigned.assignee_id + LEFT JOIN ( + SELECT assignee_id, AVG(TIMESTAMPDIFF(HOUR, created_at, updated_at)) as avg_hours + FROM tasks + WHERE assignee_id IS NOT NULL AND status = 'completed' %s + GROUP BY assignee_id + ) avg_completion ON u.id = avg_completion.assignee_id + LEFT JOIN ( + SELECT assignee_id, COUNT(*) as count + FROM tasks + WHERE assignee_id IS NOT NULL AND status = 'completed' AND updated_at >= '%s' %s + GROUP BY assignee_id + ) week_completed ON u.id = week_completed.assignee_id + LEFT JOIN ( + SELECT assignee_id, COUNT(*) as count + FROM tasks + WHERE assignee_id IS NOT NULL AND status = 'completed' AND updated_at >= '%s' %s + GROUP BY assignee_id + ) month_completed ON u.id = month_completed.assignee_id + WHERE 1=1 %s + HAVING total_assigned.count > 0 + ORDER BY completion_rate DESC + LIMIT 20 + ` + + finalEfficiencyQuery := fmt.Sprintf(efficiencyQuery, + orgCondition, orgCondition, orgCondition, + weekStart.Format("2006-01-02"), orgCondition, + monthStart.Format("2006-01-02"), orgCondition, + userCondition) + + if err := db.Raw(finalEfficiencyQuery).Scan(&userEfficiencyStats).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取用户效率统计失败"}) + return + } + + response.UserEfficiencyStats = userEfficiencyStats + + // 4. 用户活跃度统计 + var userActivityStats []UserActivityStat + + activityQuery := ` + SELECT + u.id as user_id, + u.username, + u.real_name, + COALESCE(MAX(GREATEST(t.created_at, t.updated_at)), u.created_at) as last_activity, + COALESCE(comments.count, 0) as comments_count, + COALESCE(week_created.count, 0) as tasks_created_this_week, + COALESCE(month_created.count, 0) as tasks_created_this_month + FROM users u + LEFT JOIN tasks t ON u.id = t.creator_id OR u.id = t.assignee_id + LEFT JOIN ( + SELECT user_id, COUNT(*) as count + FROM task_comments + GROUP BY user_id + ) comments ON u.id = comments.user_id + LEFT JOIN ( + SELECT creator_id, COUNT(*) as count + FROM tasks + WHERE created_at >= '%s' %s + GROUP BY creator_id + ) week_created ON u.id = week_created.creator_id + LEFT JOIN ( + SELECT creator_id, COUNT(*) as count + FROM tasks + WHERE created_at >= '%s' %s + GROUP BY creator_id + ) month_created ON u.id = month_created.creator_id + WHERE 1=1 %s + GROUP BY u.id, u.username, u.real_name, u.created_at, comments.count, week_created.count, month_created.count + ORDER BY last_activity DESC + LIMIT 20 + ` + + finalActivityQuery := fmt.Sprintf(activityQuery, + weekStart.Format("2006-01-02"), orgCondition, + monthStart.Format("2006-01-02"), orgCondition, + userCondition) + + if err := db.Raw(finalActivityQuery).Scan(&userActivityStats).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取用户活跃度统计失败"}) + return + } + + response.UserActivityStats = userActivityStats + + // 5. 用户完成率排行 + var userCompletionRank []UserCompletionRank + + for i, stat := range userEfficiencyStats { + rank := UserCompletionRank{ + UserID: stat.UserID, + Username: stat.Username, + RealName: stat.RealName, + CompletionRate: stat.CompletionRate, + Rank: i + 1, + } + + // 获取用户的总任务数和完成任务数 + for _, taskStat := range userTaskStats { + if taskStat.UserID == stat.UserID { + rank.TotalTasks = taskStat.AssignedTasks + rank.CompletedTasks = taskStat.CompletedTasks + break + } + } + + userCompletionRank = append(userCompletionRank, rank) + } + + response.UserCompletionRank = userCompletionRank + + // 6. 用户任务分配统计 + var userAssignmentStats []UserAssignmentStat + + assignmentQuery := ` + SELECT + u.id as user_id, + u.username, + u.real_name, + COALESCE(urgent.count, 0) as urgent_tasks, + COALESCE(high_priority.count, 0) as high_priority_tasks, + COALESCE(medium_priority.count, 0) as medium_priority_tasks, + COALESCE(low_priority.count, 0) as low_priority_tasks, + COALESCE(overdue.count, 0) as overdue_tasks + FROM users u + LEFT JOIN ( + SELECT assignee_id, COUNT(*) as count + FROM tasks + WHERE assignee_id IS NOT NULL AND priority = 'urgent' %s + GROUP BY assignee_id + ) urgent ON u.id = urgent.assignee_id + LEFT JOIN ( + SELECT assignee_id, COUNT(*) as count + FROM tasks + WHERE assignee_id IS NOT NULL AND priority = 'high' %s + GROUP BY assignee_id + ) high_priority ON u.id = high_priority.assignee_id + LEFT JOIN ( + SELECT assignee_id, COUNT(*) as count + FROM tasks + WHERE assignee_id IS NOT NULL AND priority = 'medium' %s + GROUP BY assignee_id + ) medium_priority ON u.id = medium_priority.assignee_id + LEFT JOIN ( + SELECT assignee_id, COUNT(*) as count + FROM tasks + WHERE assignee_id IS NOT NULL AND priority = 'low' %s + GROUP BY assignee_id + ) low_priority ON u.id = low_priority.assignee_id + LEFT JOIN ( + SELECT assignee_id, COUNT(*) as count + FROM tasks + WHERE assignee_id IS NOT NULL AND end_time < NOW() AND status != 'completed' %s + GROUP BY assignee_id + ) overdue ON u.id = overdue.assignee_id + WHERE 1=1 %s + ORDER BY (COALESCE(urgent.count, 0) + COALESCE(high_priority.count, 0)) DESC + LIMIT 20 + ` + + finalAssignmentQuery := fmt.Sprintf(assignmentQuery, + orgCondition, orgCondition, orgCondition, orgCondition, orgCondition, userCondition) + + if err := db.Raw(finalAssignmentQuery).Scan(&userAssignmentStats).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取用户任务分配统计失败"}) + return + } + + response.UserAssignmentStats = userAssignmentStats + + // 7. 总体效率指标 + if len(userEfficiencyStats) > 0 { + var totalCompletionRate float64 + var totalTasks int64 + var totalCompletionTime float64 + var validTimeCount int64 + + for _, stat := range userEfficiencyStats { + totalCompletionRate += stat.CompletionRate + if stat.AverageCompletionTime > 0 { + totalCompletionTime += stat.AverageCompletionTime + validTimeCount++ + } + } + + for _, stat := range userTaskStats { + totalTasks += stat.AssignedTasks + } + + response.OverallEfficiency.AverageCompletionRate = totalCompletionRate / float64(len(userEfficiencyStats)) + if response.TotalUsers > 0 { + response.OverallEfficiency.AverageTasksPerUser = float64(totalTasks) / float64(response.TotalUsers) + } + if validTimeCount > 0 { + response.OverallEfficiency.AverageCompletionTime = totalCompletionTime / float64(validTimeCount) + } + } + + c.JSON(http.StatusOK, response) } diff --git a/backend/internal/handler/task/add_task_attachment.go b/backend/internal/handler/task/add_task_attachment.go index de47957..6a9a7ce 100644 --- a/backend/internal/handler/task/add_task_attachment.go +++ b/backend/internal/handler/task/add_task_attachment.go @@ -1,9 +1,86 @@ package task import ( + "fmt" + "net/http" + "path/filepath" + "strconv" + "time" + + "task-track-backend/model" + "task-track-backend/pkg/database" + "github.com/gin-gonic/gin" ) func (h *TaskHandler) AddTaskAttachment(c *gin.Context) { - // ... 复制原有实现 ... + // 获取任务ID + taskIDStr := c.Param("id") + taskID, err := strconv.ParseUint(taskIDStr, 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效的任务ID"}) + return + } + + // 获取上传的用户ID + userIDStr := c.PostForm("user_id") + if userIDStr == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "用户ID不能为空"}) + return + } + + userID, err := strconv.ParseUint(userIDStr, 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效的用户ID"}) + return + } + + // 处理文件上传 + file, err := c.FormFile("file") + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "文件上传失败"}) + return + } + + db := database.GetDB() + + // 检查任务是否存在 + var existingTask model.Task + if err := db.First(&existingTask, uint(taskID)).Error; err != nil { + if err.Error() == "record not found" { + c.JSON(http.StatusNotFound, gin.H{"error": "任务不存在"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "查询任务失败"}) + return + } + + // 生成文件名和路径 + timestamp := time.Now().UnixNano() + fileName := file.Filename + filePath := fmt.Sprintf("uploads/%d_%s", timestamp, fileName) + + // 保存文件 + if err := c.SaveUploadedFile(file, filePath); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "保存文件失败"}) + return + } + + // 创建附件记录 + attachment := model.TaskAttachment{ + TaskID: uint(taskID), + FileName: fileName, + FilePath: filePath, + FileSize: file.Size, + FileType: filepath.Ext(fileName), + UploadedBy: uint(userID), + CreatedAt: time.Now(), + } + + if err := db.Create(&attachment).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "创建附件记录失败"}) + return + } + + c.JSON(http.StatusCreated, attachment) } diff --git a/backend/internal/handler/task/add_task_comment.go b/backend/internal/handler/task/add_task_comment.go index 6e880f1..2fead4d 100644 --- a/backend/internal/handler/task/add_task_comment.go +++ b/backend/internal/handler/task/add_task_comment.go @@ -1,9 +1,79 @@ package task import ( + "net/http" + "strconv" + "time" + + "task-track-backend/model" + "task-track-backend/pkg/database" + "github.com/gin-gonic/gin" ) +// AddTaskCommentRequest 添加任务评论请求结构 +type AddTaskCommentRequest struct { + Content string `json:"content" binding:"required"` + UserID uint `json:"user_id" binding:"required"` +} + func (h *TaskHandler) AddTaskComment(c *gin.Context) { - // ... 复制原有实现 ... + // 获取任务ID + taskIDStr := c.Param("id") + taskID, err := strconv.ParseUint(taskIDStr, 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效的任务ID"}) + return + } + + var req AddTaskCommentRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + db := database.GetDB() + + // 检查任务是否存在 + var existingTask model.Task + if err := db.First(&existingTask, uint(taskID)).Error; err != nil { + if err.Error() == "record not found" { + c.JSON(http.StatusNotFound, gin.H{"error": "任务不存在"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "查询任务失败"}) + return + } + + // 检查用户是否存在 + var user model.User + if err := db.First(&user, req.UserID).Error; err != nil { + if err.Error() == "record not found" { + c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "查询用户失败"}) + return + } + + // 创建评论 + comment := model.TaskComment{ + TaskID: uint(taskID), + UserID: req.UserID, + Content: req.Content, + CreatedAt: time.Now(), + } + + if err := db.Create(&comment).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "创建评论失败"}) + return + } + + // 预加载用户信息 + if err := db.Preload("User").First(&comment, comment.ID).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取评论失败"}) + return + } + + c.JSON(http.StatusCreated, comment) } diff --git a/backend/internal/handler/task/create_task.go b/backend/internal/handler/task/create_task.go index fd2a3df..bd79ac0 100644 --- a/backend/internal/handler/task/create_task.go +++ b/backend/internal/handler/task/create_task.go @@ -1,9 +1,107 @@ package task import ( + "net/http" + "time" + + "task-track-backend/model" + "task-track-backend/pkg/database" + "github.com/gin-gonic/gin" ) +// CreateTaskRequest 创建任务请求结构 +type CreateTaskRequest struct { + Title string `json:"title" binding:"required"` + Description string `json:"description"` + Type string `json:"type"` + Priority string `json:"priority"` + AssigneeID *uint `json:"assignee_id"` + StartTime *time.Time `json:"start_time"` + EndTime *time.Time `json:"end_time"` + OrganizationID uint `json:"organization_id" binding:"required"` + CreatorID uint `json:"creator_id" binding:"required"` +} + func (h *TaskHandler) CreateTask(c *gin.Context) { - // ... 复制原有实现 ... + var req CreateTaskRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "请求参数错误: " + err.Error(), + }) + return + } + + // 验证优先级 + if req.Priority != "" && req.Priority != "urgent" && req.Priority != "high" && req.Priority != "medium" && req.Priority != "low" { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "优先级值无效,必须是 urgent、high、medium 或 low", + }) + return + } + + // 如果没有指定优先级,默认为 medium + if req.Priority == "" { + req.Priority = "medium" + } + + // 验证时间逻辑 + if req.StartTime != nil && req.EndTime != nil && req.StartTime.After(*req.EndTime) { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "开始时间不能晚于结束时间", + }) + return + } + + // 创建任务对象 + task := model.Task{ + Title: req.Title, + Description: req.Description, + Type: req.Type, + Priority: req.Priority, + Status: "pending", // 默认状态 + CreatorID: req.CreatorID, + OrganizationID: req.OrganizationID, + StartTime: req.StartTime, + EndTime: req.EndTime, + } + + // 如果指定了执行者 + if req.AssigneeID != nil { + task.AssigneeID = *req.AssigneeID + } + + // 保存到数据库 + db := database.GetDB() + if err := db.Create(&task).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "message": "创建任务失败: " + err.Error(), + }) + return + } + + // 返回成功响应 + c.JSON(http.StatusCreated, gin.H{ + "code": 201, + "message": "任务创建成功", + "data": gin.H{ + "id": task.ID, + "title": task.Title, + "description": task.Description, + "type": task.Type, + "priority": task.Priority, + "status": task.Status, + "creator_id": task.CreatorID, + "assignee_id": task.AssigneeID, + "organization_id": task.OrganizationID, + "start_time": task.StartTime, + "end_time": task.EndTime, + "created_at": task.CreatedAt, + "updated_at": task.UpdatedAt, + }, + }) } diff --git a/backend/internal/handler/task/delete_task.go b/backend/internal/handler/task/delete_task.go index f8f39e9..97260b5 100644 --- a/backend/internal/handler/task/delete_task.go +++ b/backend/internal/handler/task/delete_task.go @@ -1,9 +1,66 @@ package task import ( + "net/http" + "strconv" + + "task-track-backend/model" + "task-track-backend/pkg/database" + "github.com/gin-gonic/gin" ) func (h *TaskHandler) DeleteTask(c *gin.Context) { - // ... 复制原有实现 ... + // 获取任务ID + taskIDStr := c.Param("id") + taskID, err := strconv.ParseUint(taskIDStr, 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效的任务ID"}) + return + } + + db := database.GetDB() + + // 检查任务是否存在 + var existingTask model.Task + if err := db.First(&existingTask, uint(taskID)).Error; err != nil { + if err.Error() == "record not found" { + c.JSON(http.StatusNotFound, gin.H{"error": "任务不存在"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "查询任务失败"}) + return + } + + // 开始事务 + tx := db.Begin() + + // 删除相关的评论 + if err := tx.Where("task_id = ?", taskID).Delete(&model.TaskComment{}).Error; err != nil { + tx.Rollback() + c.JSON(http.StatusInternalServerError, gin.H{"error": "删除任务评论失败"}) + return + } + + // 删除相关的附件 + if err := tx.Where("task_id = ?", taskID).Delete(&model.TaskAttachment{}).Error; err != nil { + tx.Rollback() + c.JSON(http.StatusInternalServerError, gin.H{"error": "删除任务附件失败"}) + return + } + + // 删除任务 + if err := tx.Delete(&existingTask).Error; err != nil { + tx.Rollback() + c.JSON(http.StatusInternalServerError, gin.H{"error": "删除任务失败"}) + return + } + + // 提交事务 + if err := tx.Commit().Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "提交事务失败"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "任务删除成功"}) } diff --git a/backend/internal/handler/task/delete_task_attachment.go b/backend/internal/handler/task/delete_task_attachment.go index 9357ff9..8630202 100644 --- a/backend/internal/handler/task/delete_task_attachment.go +++ b/backend/internal/handler/task/delete_task_attachment.go @@ -1,9 +1,69 @@ package task import ( + "net/http" + "os" + "strconv" + + "task-track-backend/model" + "task-track-backend/pkg/database" + "github.com/gin-gonic/gin" ) func (h *TaskHandler) DeleteTaskAttachment(c *gin.Context) { - // ... 复制原有实现 ... + // 获取任务ID + taskIDStr := c.Param("id") + taskID, err := strconv.ParseUint(taskIDStr, 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效的任务ID"}) + return + } + + // 获取附件ID + attachmentIDStr := c.Param("attachment_id") + attachmentID, err := strconv.ParseUint(attachmentIDStr, 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效的附件ID"}) + return + } + + db := database.GetDB() + + // 检查任务是否存在 + var existingTask model.Task + if err := db.First(&existingTask, uint(taskID)).Error; err != nil { + if err.Error() == "record not found" { + c.JSON(http.StatusNotFound, gin.H{"error": "任务不存在"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "查询任务失败"}) + return + } + + // 检查附件是否存在且属于该任务 + var existingAttachment model.TaskAttachment + if err := db.Where("id = ? AND task_id = ?", uint(attachmentID), uint(taskID)). + First(&existingAttachment).Error; err != nil { + if err.Error() == "record not found" { + c.JSON(http.StatusNotFound, gin.H{"error": "附件不存在"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "查询附件失败"}) + return + } + + // 删除物理文件 + if err := os.Remove(existingAttachment.FilePath); err != nil { + // 即使文件删除失败,也继续删除数据库记录 + // 记录日志但不返回错误 + } + + // 删除数据库记录 + if err := db.Delete(&existingAttachment).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "删除附件失败"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "附件删除成功"}) } diff --git a/backend/internal/handler/task/delete_task_comment.go b/backend/internal/handler/task/delete_task_comment.go index 417403a..337989d 100644 --- a/backend/internal/handler/task/delete_task_comment.go +++ b/backend/internal/handler/task/delete_task_comment.go @@ -1,9 +1,62 @@ package task import ( + "net/http" + "strconv" + + "task-track-backend/model" + "task-track-backend/pkg/database" + "github.com/gin-gonic/gin" ) func (h *TaskHandler) DeleteTaskComment(c *gin.Context) { - // ... 复制原有实现 ... + // 获取任务ID + taskIDStr := c.Param("id") + taskID, err := strconv.ParseUint(taskIDStr, 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效的任务ID"}) + return + } + + // 获取评论ID + commentIDStr := c.Param("comment_id") + commentID, err := strconv.ParseUint(commentIDStr, 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效的评论ID"}) + return + } + + db := database.GetDB() + + // 检查任务是否存在 + var existingTask model.Task + if err := db.First(&existingTask, uint(taskID)).Error; err != nil { + if err.Error() == "record not found" { + c.JSON(http.StatusNotFound, gin.H{"error": "任务不存在"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "查询任务失败"}) + return + } + + // 检查评论是否存在且属于该任务 + var existingComment model.TaskComment + if err := db.Where("id = ? AND task_id = ?", uint(commentID), uint(taskID)). + First(&existingComment).Error; err != nil { + if err.Error() == "record not found" { + c.JSON(http.StatusNotFound, gin.H{"error": "评论不存在"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "查询评论失败"}) + return + } + + // 删除评论 + if err := db.Delete(&existingComment).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "删除评论失败"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "评论删除成功"}) } diff --git a/backend/internal/handler/task/get_task.go b/backend/internal/handler/task/get_task.go index 58c84d5..2a9b99b 100644 --- a/backend/internal/handler/task/get_task.go +++ b/backend/internal/handler/task/get_task.go @@ -1,9 +1,44 @@ package task import ( + "net/http" + "strconv" + + "task-track-backend/model" + "task-track-backend/pkg/database" + "github.com/gin-gonic/gin" ) func (h *TaskHandler) GetTask(c *gin.Context) { - // ... 复制原有实现 ... + // 获取任务ID + taskIDStr := c.Param("id") + taskID, err := strconv.ParseUint(taskIDStr, 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效的任务ID"}) + return + } + + db := database.GetDB() + + var task model.Task + // 预加载相关数据:创建者、分配者、组织、评论、附件 + err = db.Preload("Creator"). + Preload("Assignee"). + Preload("Organization"). + Preload("Comments"). + Preload("Comments.User"). + Preload("Attachments"). + First(&task, uint(taskID)).Error + + if err != nil { + if err.Error() == "record not found" { + c.JSON(http.StatusNotFound, gin.H{"error": "任务不存在"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取任务失败"}) + return + } + + c.JSON(http.StatusOK, task) } diff --git a/backend/internal/handler/task/get_task_attachments.go b/backend/internal/handler/task/get_task_attachments.go index f39da4f..3c2f3d0 100644 --- a/backend/internal/handler/task/get_task_attachments.go +++ b/backend/internal/handler/task/get_task_attachments.go @@ -1,9 +1,45 @@ package task import ( + "net/http" + "strconv" + + "task-track-backend/model" + "task-track-backend/pkg/database" + "github.com/gin-gonic/gin" ) func (h *TaskHandler) GetTaskAttachments(c *gin.Context) { - // ... 复制原有实现 ... + // 获取任务ID + taskIDStr := c.Param("id") + taskID, err := strconv.ParseUint(taskIDStr, 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效的任务ID"}) + return + } + + db := database.GetDB() + + // 检查任务是否存在 + var existingTask model.Task + if err := db.First(&existingTask, uint(taskID)).Error; err != nil { + if err.Error() == "record not found" { + c.JSON(http.StatusNotFound, gin.H{"error": "任务不存在"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "查询任务失败"}) + return + } + + // 获取附件列表 + var attachments []model.TaskAttachment + if err := db.Where("task_id = ?", uint(taskID)). + Order("created_at DESC"). + Find(&attachments).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取附件列表失败"}) + return + } + + c.JSON(http.StatusOK, attachments) } diff --git a/backend/internal/handler/task/get_task_comments.go b/backend/internal/handler/task/get_task_comments.go index 547193e..dc73d5e 100644 --- a/backend/internal/handler/task/get_task_comments.go +++ b/backend/internal/handler/task/get_task_comments.go @@ -1,9 +1,46 @@ package task import ( + "net/http" + "strconv" + + "task-track-backend/model" + "task-track-backend/pkg/database" + "github.com/gin-gonic/gin" ) func (h *TaskHandler) GetTaskComments(c *gin.Context) { - // ... 复制原有实现 ... + // 获取任务ID + taskIDStr := c.Param("id") + taskID, err := strconv.ParseUint(taskIDStr, 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效的任务ID"}) + return + } + + db := database.GetDB() + + // 检查任务是否存在 + var existingTask model.Task + if err := db.First(&existingTask, uint(taskID)).Error; err != nil { + if err.Error() == "record not found" { + c.JSON(http.StatusNotFound, gin.H{"error": "任务不存在"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "查询任务失败"}) + return + } + + // 获取评论列表 + var comments []model.TaskComment + if err := db.Where("task_id = ?", uint(taskID)). + Preload("User"). + Order("created_at DESC"). + Find(&comments).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取评论列表失败"}) + return + } + + c.JSON(http.StatusOK, comments) } diff --git a/backend/internal/handler/task/get_tasks.go b/backend/internal/handler/task/get_tasks.go index 9813224..3d48d5a 100644 --- a/backend/internal/handler/task/get_tasks.go +++ b/backend/internal/handler/task/get_tasks.go @@ -1,9 +1,128 @@ package task import ( + "net/http" + + "task-track-backend/model" + "task-track-backend/pkg/database" + "github.com/gin-gonic/gin" ) +// GetTasksQuery 获取任务列表查询参数 +type GetTasksQuery struct { + Page int `form:"page" binding:"min=1"` + PageSize int `form:"page_size" binding:"min=1,max=100"` + Status string `form:"status"` + Priority string `form:"priority"` + Type string `form:"type"` + AssigneeID uint `form:"assignee_id"` + CreatorID uint `form:"creator_id"` + OrganizationID uint `form:"organization_id"` + Search string `form:"search"` + SortBy string `form:"sort_by"` + SortOrder string `form:"sort_order"` +} + +// GetTasksResponse 获取任务列表响应 +type GetTasksResponse struct { + Tasks []model.Task `json:"tasks"` + Total int64 `json:"total"` + Page int `json:"page"` + PageSize int `json:"page_size"` + TotalPages int `json:"total_pages"` +} + func (h *TaskHandler) GetTasks(c *gin.Context) { - // ... 复制原有实现 ... + var query GetTasksQuery + + // 设置默认值 + query.Page = 1 + query.PageSize = 10 + query.SortBy = "created_at" + query.SortOrder = "desc" + + if err := c.ShouldBindQuery(&query); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + db := database.GetDB() + + // 构建查询 + taskQuery := db.Model(&model.Task{}) + + // 预加载相关数据 + taskQuery = taskQuery.Preload("Creator").Preload("Assignee").Preload("Organization") + + // 应用过滤条件 + if query.Status != "" { + taskQuery = taskQuery.Where("status = ?", query.Status) + } + + if query.Priority != "" { + taskQuery = taskQuery.Where("priority = ?", query.Priority) + } + + if query.Type != "" { + taskQuery = taskQuery.Where("type = ?", query.Type) + } + + if query.AssigneeID > 0 { + taskQuery = taskQuery.Where("assignee_id = ?", query.AssigneeID) + } + + if query.CreatorID > 0 { + taskQuery = taskQuery.Where("creator_id = ?", query.CreatorID) + } + + if query.OrganizationID > 0 { + taskQuery = taskQuery.Where("organization_id = ?", query.OrganizationID) + } + + // 搜索功能 + if query.Search != "" { + searchTerm := "%" + query.Search + "%" + taskQuery = taskQuery.Where("title LIKE ? OR description LIKE ?", searchTerm, searchTerm) + } + + // 计算总数 + var total int64 + if err := taskQuery.Count(&total).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "计算任务总数失败"}) + return + } + + // 计算总页数 + totalPages := int((total + int64(query.PageSize) - 1) / int64(query.PageSize)) + + // 应用排序 + orderBy := query.SortBy + if query.SortOrder == "asc" { + orderBy += " ASC" + } else { + orderBy += " DESC" + } + taskQuery = taskQuery.Order(orderBy) + + // 应用分页 + offset := (query.Page - 1) * query.PageSize + taskQuery = taskQuery.Offset(offset).Limit(query.PageSize) + + // 执行查询 + var tasks []model.Task + if err := taskQuery.Find(&tasks).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取任务列表失败"}) + return + } + + response := GetTasksResponse{ + Tasks: tasks, + Total: total, + Page: query.Page, + PageSize: query.PageSize, + TotalPages: totalPages, + } + + c.JSON(http.StatusOK, response) } diff --git a/backend/internal/handler/task/update_task.go b/backend/internal/handler/task/update_task.go index dd77541..c01d02b 100644 --- a/backend/internal/handler/task/update_task.go +++ b/backend/internal/handler/task/update_task.go @@ -1,9 +1,102 @@ package task import ( + "net/http" + "strconv" + "time" + + "task-track-backend/model" + "task-track-backend/pkg/database" + "github.com/gin-gonic/gin" ) +// UpdateTaskRequest 更新任务请求结构 +type UpdateTaskRequest struct { + Title *string `json:"title"` + Description *string `json:"description"` + Type *string `json:"type"` + Priority *string `json:"priority"` + Status *string `json:"status"` + AssigneeID *uint `json:"assignee_id"` + StartTime *time.Time `json:"start_time"` + EndTime *time.Time `json:"end_time"` +} + func (h *TaskHandler) UpdateTask(c *gin.Context) { - // ... 复制原有实现 ... + // 获取任务ID + taskIDStr := c.Param("id") + taskID, err := strconv.ParseUint(taskIDStr, 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效的任务ID"}) + return + } + + var req UpdateTaskRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + db := database.GetDB() + + // 检查任务是否存在 + var existingTask model.Task + if err := db.First(&existingTask, uint(taskID)).Error; err != nil { + if err.Error() == "record not found" { + c.JSON(http.StatusNotFound, gin.H{"error": "任务不存在"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "查询任务失败"}) + return + } + + // 构建更新数据 + updateData := make(map[string]interface{}) + + if req.Title != nil { + updateData["title"] = *req.Title + } + if req.Description != nil { + updateData["description"] = *req.Description + } + if req.Type != nil { + updateData["type"] = *req.Type + } + if req.Priority != nil { + updateData["priority"] = *req.Priority + } + if req.Status != nil { + updateData["status"] = *req.Status + } + if req.AssigneeID != nil { + updateData["assignee_id"] = *req.AssigneeID + } + if req.StartTime != nil { + updateData["start_time"] = *req.StartTime + } + if req.EndTime != nil { + updateData["end_time"] = *req.EndTime + } + + // 设置更新时间 + updateData["updated_at"] = time.Now() + + // 执行更新 + if err := db.Model(&existingTask).Updates(updateData).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "更新任务失败"}) + return + } + + // 重新查询更新后的任务,包含关联数据 + var updatedTask model.Task + if err := db.Preload("Creator"). + Preload("Assignee"). + Preload("Organization"). + First(&updatedTask, uint(taskID)).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取更新后的任务失败"}) + return + } + + c.JSON(http.StatusOK, updatedTask) } diff --git a/backend/internal/handler/task/update_task_status.go b/backend/internal/handler/task/update_task_status.go index 562fb50..62e8a79 100644 --- a/backend/internal/handler/task/update_task_status.go +++ b/backend/internal/handler/task/update_task_status.go @@ -1,9 +1,84 @@ package task import ( + "net/http" + "strconv" + "time" + + "task-track-backend/model" + "task-track-backend/pkg/database" + "github.com/gin-gonic/gin" ) +// UpdateTaskStatusRequest 更新任务状态请求结构 +type UpdateTaskStatusRequest struct { + Status string `json:"status" binding:"required"` +} + func (h *TaskHandler) UpdateTaskStatus(c *gin.Context) { - // ... 复制原有实现 ... + // 获取任务ID + taskIDStr := c.Param("id") + taskID, err := strconv.ParseUint(taskIDStr, 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效的任务ID"}) + return + } + + var req UpdateTaskStatusRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // 验证状态值 + validStatuses := []string{"pending", "in_progress", "completed", "cancelled"} + isValidStatus := false + for _, status := range validStatuses { + if req.Status == status { + isValidStatus = true + break + } + } + + if !isValidStatus { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效的任务状态"}) + return + } + + db := database.GetDB() + + // 检查任务是否存在 + var existingTask model.Task + if err := db.First(&existingTask, uint(taskID)).Error; err != nil { + if err.Error() == "record not found" { + c.JSON(http.StatusNotFound, gin.H{"error": "任务不存在"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "查询任务失败"}) + return + } + + // 更新任务状态 + updateData := map[string]interface{}{ + "status": req.Status, + "updated_at": time.Now(), + } + + if err := db.Model(&existingTask).Updates(updateData).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "更新任务状态失败"}) + return + } + + // 重新查询更新后的任务 + var updatedTask model.Task + if err := db.Preload("Creator"). + Preload("Assignee"). + Preload("Organization"). + First(&updatedTask, uint(taskID)).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取更新后的任务失败"}) + return + } + + c.JSON(http.StatusOK, updatedTask) } diff --git a/backend/internal/handler/task/upload_file.go b/backend/internal/handler/task/upload_file.go index f0566a7..e4d30b4 100644 --- a/backend/internal/handler/task/upload_file.go +++ b/backend/internal/handler/task/upload_file.go @@ -1,9 +1,71 @@ package task import ( + "fmt" + "net/http" + "path/filepath" + "time" + "github.com/gin-gonic/gin" ) +// UploadFileResponse 上传文件响应结构 +type UploadFileResponse struct { + FileName string `json:"file_name"` + FilePath string `json:"file_path"` + FileSize int64 `json:"file_size"` + FileType string `json:"file_type"` +} + func (h *TaskHandler) UploadFile(c *gin.Context) { - // ... 复制原有实现 ... + // 处理文件上传 + file, err := c.FormFile("file") + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "文件上传失败"}) + return + } + + // 验证文件大小 (限制为 10MB) + maxSize := int64(10 * 1024 * 1024) + if file.Size > maxSize { + c.JSON(http.StatusBadRequest, gin.H{"error": "文件大小超过限制"}) + return + } + + // 验证文件类型 + allowedTypes := []string{".jpg", ".jpeg", ".png", ".gif", ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".txt"} + fileExt := filepath.Ext(file.Filename) + isAllowed := false + for _, allowedType := range allowedTypes { + if fileExt == allowedType { + isAllowed = true + break + } + } + + if !isAllowed { + c.JSON(http.StatusBadRequest, gin.H{"error": "不支持的文件类型"}) + return + } + + // 生成文件名和路径 + timestamp := time.Now().UnixNano() + fileName := file.Filename + filePath := fmt.Sprintf("uploads/%d_%s", timestamp, fileName) + + // 保存文件 + if err := c.SaveUploadedFile(file, filePath); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "保存文件失败"}) + return + } + + // 返回文件信息 + response := UploadFileResponse{ + FileName: fileName, + FilePath: filePath, + FileSize: file.Size, + FileType: fileExt, + } + + c.JSON(http.StatusOK, response) } diff --git a/backend/model/model.go b/backend/model/model.go index 23f817f..464e961 100644 --- a/backend/model/model.go +++ b/backend/model/model.go @@ -65,6 +65,13 @@ type Task struct { CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` + + // 关联关系 + Creator User `json:"creator" gorm:"foreignKey:CreatorID"` + Assignee User `json:"assignee" gorm:"foreignKey:AssigneeID"` + Organization Organization `json:"organization" gorm:"foreignKey:OrganizationID"` + Comments []TaskComment `json:"comments" gorm:"foreignKey:TaskID"` + Attachments []TaskAttachment `json:"attachments" gorm:"foreignKey:TaskID"` } // TaskTag 任务标签表 @@ -92,6 +99,9 @@ type TaskComment struct { Content string `json:"content" gorm:"type:text;not null"` CreatedAt time.Time `json:"created_at"` DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` + + // 关联关系 + User User `json:"user" gorm:"foreignKey:UserID"` } // TaskAttachment 任务附件表 diff --git a/backend/pkg/database/database.go b/backend/pkg/database/database.go index 234f3e8..a1f0b17 100644 --- a/backend/pkg/database/database.go +++ b/backend/pkg/database/database.go @@ -13,6 +13,8 @@ import ( "gorm.io/gorm/logger" ) +var db *gorm.DB + func Init(cfg config.DatabaseConfig) (*gorm.DB, error) { // 首先尝试连接到数据库 db, err := connectToDatabase(cfg) @@ -41,9 +43,22 @@ func Init(cfg config.DatabaseConfig) (*gorm.DB, error) { return nil, fmt.Errorf("failed to auto migrate models: %w", err) } + // 设置全局数据库实例 + setDB(db) + return db, nil } +// GetDB 获取全局数据库实例 +func GetDB() *gorm.DB { + return db +} + +// setDB 设置全局数据库实例 +func setDB(database *gorm.DB) { + db = database +} + func connectToDatabase(cfg config.DatabaseConfig) (*gorm.DB, error) { dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=%s&parseTime=True&loc=Local", cfg.Username, diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 26ebf9e..f71dbb5 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -33,19 +33,72 @@ api.interceptors.response.use( } ) +// 认证相关API +export const authApi = { + // 登录 + login: (data: { username: string; password: string }) => api.post('/auth/login', data), + + // 注册 + register: (data: { username: string; email: string; password: string; real_name?: string }) => + api.post('/auth/register', data), + + // 测试登录 + testLogin: () => api.post('/auth/test-login'), + + // 登出 + logout: () => api.post('/auth/logout'), + + // 刷新token + refresh: () => api.post('/auth/refresh'), + + // 获取用户信息 + getMe: () => api.get('/auth/me') +} + // 任务相关API export const taskApi = { // 获取任务列表 - getTasks: (params?: any) => api.get('/tasks', { params }), + getTasks: (params?: { + page?: number; + page_size?: number; + status?: string; + priority?: string; + type?: string; + assignee_id?: number; + creator_id?: number; + organization_id?: number; + search?: string; + sort_by?: string; + sort_order?: string; + }) => api.get('/tasks', { params }), // 获取单个任务详情 getTask: (id: number) => api.get(`/tasks/${id}`), // 创建任务 - createTask: (data: any) => api.post('/tasks', data), + createTask: (data: { + title: string; + description?: string; + type?: string; + priority?: string; + assignee_id?: number; + start_time?: string; + end_time?: string; + organization_id: number; + creator_id: number; + }) => api.post('/tasks', data), // 更新任务 - updateTask: (id: number, data: any) => api.post(`/tasks/update/${id}`, data), + updateTask: (id: number, data: { + title?: string; + description?: string; + type?: string; + priority?: string; + status?: string; + assignee_id?: number; + start_time?: string; + end_time?: string; + }) => api.post(`/tasks/update/${id}`, data), // 删除任务 deleteTask: (id: number) => api.post(`/tasks/delete/${id}`), @@ -55,12 +108,17 @@ export const taskApi = { // 任务评论相关 getTaskComments: (taskId: number) => api.get(`/tasks/${taskId}/comments`), - addTaskComment: (taskId: number, data: any) => api.post(`/tasks/${taskId}/comments`, data), - deleteTaskComment: (taskId: number, commentId: number) => api.delete(`/tasks/${taskId}/comments/${commentId}`), + addTaskComment: (taskId: number, data: { content: string; user_id: number }) => + api.post(`/tasks/${taskId}/comments`, data), + deleteTaskComment: (taskId: number, commentId: number) => + api.delete(`/tasks/${taskId}/comments/${commentId}`), // 任务附件相关 getTaskAttachments: (taskId: number) => api.get(`/tasks/${taskId}/attachments`), - addTaskAttachment: (taskId: number, data: any) => api.post(`/tasks/${taskId}/attachments`, data), + addTaskAttachment: (taskId: number, data: FormData) => + api.post(`/tasks/${taskId}/attachments`, data, { + headers: { 'Content-Type': 'multipart/form-data' } + }), deleteTaskAttachment: (attachmentId: number) => api.delete(`/attachments/${attachmentId}`), // 上传文件 @@ -68,16 +126,289 @@ export const taskApi = { const formData = new FormData() formData.append('file', file) return api.post('/upload', formData, { - headers: { - 'Content-Type': 'multipart/form-data' - } + headers: { 'Content-Type': 'multipart/form-data' } }) } } // 用户相关API export const userApi = { - getUsers: () => api.get('/users') + // 获取用户列表 + getUsers: () => api.get('/users'), + + // 获取单个用户详情 + getUser: (id: number) => api.get(`/users/${id}`), + + // 更新用户 + updateUser: (id: number, data: { + username?: string; + email?: string; + real_name?: string; + phone?: string; + avatar?: string; + status?: number; + }) => api.post(`/users/update/${id}`, data), + + // 删除用户 + deleteUser: (id: number) => api.post(`/users/delete/${id}`) +} + +// 组织相关API +export const organizationApi = { + // 获取组织列表 + getOrganizations: () => api.get('/organizations'), + + // 创建组织 + createOrganization: (data: { + name: string; + code: string; + parent_id?: number; + level?: number; + sort?: number; + }) => api.post('/organizations', data), + + // 获取单个组织详情 + getOrganization: (id: number) => api.get(`/organizations/${id}`), + + // 更新组织 + updateOrganization: (id: number, data: { + name?: string; + code?: string; + parent_id?: number; + level?: number; + sort?: number; + status?: number; + }) => api.post(`/organizations/update/${id}`, data), + + // 删除组织 + deleteOrganization: (id: number) => api.post(`/organizations/delete/${id}`), + + // 获取组织用户 + getOrganizationUsers: (id: number) => api.get(`/organizations/${id}/users`) +} + +// 统计相关API +export const statisticsApi = { + // 获取总览统计 + getOverview: (params?: { organization_id?: number }) => + api.get('/statistics/overview', { params }), + + // 获取任务统计 + getTaskStatistics: (params?: { + organization_id?: number; + days?: number + }) => api.get('/statistics/tasks', { params }), + + // 获取用户统计 + getUserStatistics: (params?: { + organization_id?: number; + user_id?: number + }) => api.get('/statistics/users', { params }) +} + +// 通用API +export const commonApi = { + // 测试接口 + test: () => api.get('/test') +} + +// 导出所有API +export const apiClient = { + auth: authApi, + task: taskApi, + user: userApi, + organization: organizationApi, + statistics: statisticsApi, + common: commonApi +} + +// 类型定义 +export interface Task { + id: number; + title: string; + description?: string; + type?: string; + priority: string; + status: string; + creator_id: number; + assignee_id?: number; + organization_id: number; + start_time?: string; + end_time?: string; + completed_at?: string; + created_at: string; + updated_at: string; + creator?: User; + assignee?: User; + organization?: Organization; + comments?: TaskComment[]; + attachments?: TaskAttachment[]; +} + +export interface User { + id: number; + username: string; + email: string; + real_name?: string; + phone?: string; + avatar?: string; + status: number; + created_at: string; + updated_at: string; +} + +export interface Organization { + id: number; + name: string; + code: string; + parent_id: number; + level: number; + sort: number; + status: number; + created_at: string; + updated_at: string; +} + +export interface TaskComment { + id: number; + task_id: number; + user_id: number; + content: string; + created_at: string; + user?: User; +} + +export interface TaskAttachment { + id: number; + task_id: number; + file_name: string; + file_path: string; + file_size: number; + file_type: string; + uploaded_by: number; + created_at: string; +} + +// 统计响应类型 +export interface OverviewResponse { + total_tasks: number; + total_users: number; + total_organizations: number; + tasks_by_status: Record; + tasks_by_priority: Record; + recent_tasks_count: number; + recent_completed_tasks: number; + today_tasks: { + created: number; + completed: number; + }; + weekly_tasks: { + created: number; + completed: number; + }; + monthly_tasks: { + created: number; + completed: number; + }; +} + +export interface TaskStatisticsResponse { + total_tasks: number; + pending_tasks: number; + in_progress_tasks: number; + completed_tasks: number; + cancelled_tasks: number; + tasks_by_status: Array<{ + status: string; + count: number; + percentage: number; + }>; + tasks_by_priority: Array<{ + priority: string; + count: number; + percentage: number; + }>; + tasks_by_type: Array<{ + type: string; + count: number; + percentage: number; + }>; + tasks_by_creator: Array<{ + creator_id: number; + creator_name: string; + count: number; + }>; + tasks_by_assignee: Array<{ + assignee_id: number; + assignee_name: string; + count: number; + }>; + daily_trend: Array<{ + date: string; + created: number; + completed: number; + }>; + completion_rate: number; + average_completion_time: number; + overdue_tasks: number; + upcoming_tasks: number; +} + +export interface UserStatisticsResponse { + total_users: number; + active_users: number; + user_task_stats: Array<{ + user_id: number; + username: string; + real_name: string; + created_tasks: number; + assigned_tasks: number; + completed_tasks: number; + pending_tasks: number; + in_progress_tasks: number; + }>; + user_efficiency_stats: Array<{ + user_id: number; + username: string; + real_name: string; + completion_rate: number; + average_completion_time: number; + tasks_completed_this_week: number; + tasks_completed_this_month: number; + }>; + user_activity_stats: Array<{ + user_id: number; + username: string; + real_name: string; + last_activity: string; + comments_count: number; + tasks_created_this_week: number; + tasks_created_this_month: number; + }>; + user_completion_rank: Array<{ + user_id: number; + username: string; + real_name: string; + completion_rate: number; + completed_tasks: number; + total_tasks: number; + rank: number; + }>; + user_assignment_stats: Array<{ + user_id: number; + username: string; + real_name: string; + urgent_tasks: number; + high_priority_tasks: number; + medium_priority_tasks: number; + low_priority_tasks: number; + overdue_tasks: number; + }>; + overall_efficiency: { + average_completion_rate: number; + average_tasks_per_user: number; + average_completion_time: number; + }; } export default api diff --git a/frontend/src/views/Task/index.vue b/frontend/src/views/Task/index.vue index 459f861..91efbba 100644 --- a/frontend/src/views/Task/index.vue +++ b/frontend/src/views/Task/index.vue @@ -4,11 +4,15 @@

任务管理

- + + + 看板视图 - + + + 创建任务
@@ -20,7 +24,7 @@ - + @@ -29,7 +33,7 @@ - + @@ -38,7 +42,7 @@ - + 搜索 重置 @@ -50,7 +54,7 @@ - + - + - + - + - + - + - + - +
- +
- + - + - + - + - + @@ -154,52 +138,27 @@ - + - + - + - + - + - + - + - +
将文件拖到此处,或点击上传 @@ -212,20 +171,15 @@ - + - + - +
@@ -235,7 +189,7 @@ 编辑任务
- + {{ taskDetail.title }} @@ -284,12 +238,7 @@ 标签
- + {{ tag.name }}
@@ -300,54 +249,42 @@ - +
- + + +
{{ attachment.file_name }}
{{ formatFileSize(attachment.file_size) }}
- + 下载 - + 删除
- +
暂无附件
@@ -358,24 +295,13 @@ - +
- +
- + 发表评论
@@ -383,34 +309,24 @@
-
+
{{ comment.user_name }} {{ formatDate(comment.created_at) }} - + 删除
{{ comment.content }}
- +
暂无评论
- +