20 changed files with 2425 additions and 294 deletions
@ -1,7 +1,196 @@ |
|||||
package statistics |
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) { |
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) |
||||
} |
} |
||||
|
|||||
@ -1,7 +1,339 @@ |
|||||
package statistics |
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) { |
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) |
||||
} |
} |
||||
|
|||||
@ -1,7 +1,450 @@ |
|||||
package statistics |
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) { |
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) |
||||
} |
} |
||||
|
|||||
@ -1,9 +1,86 @@ |
|||||
package task |
package task |
||||
|
|
||||
import ( |
import ( |
||||
|
"fmt" |
||||
|
"net/http" |
||||
|
"path/filepath" |
||||
|
"strconv" |
||||
|
"time" |
||||
|
|
||||
|
"task-track-backend/model" |
||||
|
"task-track-backend/pkg/database" |
||||
|
|
||||
"github.com/gin-gonic/gin" |
"github.com/gin-gonic/gin" |
||||
) |
) |
||||
|
|
||||
func (h *TaskHandler) AddTaskAttachment(c *gin.Context) { |
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) |
||||
} |
} |
||||
|
|||||
@ -1,9 +1,79 @@ |
|||||
package task |
package task |
||||
|
|
||||
import ( |
import ( |
||||
|
"net/http" |
||||
|
"strconv" |
||||
|
"time" |
||||
|
|
||||
|
"task-track-backend/model" |
||||
|
"task-track-backend/pkg/database" |
||||
|
|
||||
"github.com/gin-gonic/gin" |
"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) { |
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) |
||||
} |
} |
||||
|
|||||
@ -1,9 +1,107 @@ |
|||||
package task |
package task |
||||
|
|
||||
import ( |
import ( |
||||
|
"net/http" |
||||
|
"time" |
||||
|
|
||||
|
"task-track-backend/model" |
||||
|
"task-track-backend/pkg/database" |
||||
|
|
||||
"github.com/gin-gonic/gin" |
"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) { |
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, |
||||
|
}, |
||||
|
}) |
||||
} |
} |
||||
|
|||||
@ -1,9 +1,66 @@ |
|||||
package task |
package task |
||||
|
|
||||
import ( |
import ( |
||||
|
"net/http" |
||||
|
"strconv" |
||||
|
|
||||
|
"task-track-backend/model" |
||||
|
"task-track-backend/pkg/database" |
||||
|
|
||||
"github.com/gin-gonic/gin" |
"github.com/gin-gonic/gin" |
||||
) |
) |
||||
|
|
||||
func (h *TaskHandler) DeleteTask(c *gin.Context) { |
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": "任务删除成功"}) |
||||
} |
} |
||||
|
|||||
@ -1,9 +1,69 @@ |
|||||
package task |
package task |
||||
|
|
||||
import ( |
import ( |
||||
|
"net/http" |
||||
|
"os" |
||||
|
"strconv" |
||||
|
|
||||
|
"task-track-backend/model" |
||||
|
"task-track-backend/pkg/database" |
||||
|
|
||||
"github.com/gin-gonic/gin" |
"github.com/gin-gonic/gin" |
||||
) |
) |
||||
|
|
||||
func (h *TaskHandler) DeleteTaskAttachment(c *gin.Context) { |
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": "附件删除成功"}) |
||||
} |
} |
||||
|
|||||
@ -1,9 +1,62 @@ |
|||||
package task |
package task |
||||
|
|
||||
import ( |
import ( |
||||
|
"net/http" |
||||
|
"strconv" |
||||
|
|
||||
|
"task-track-backend/model" |
||||
|
"task-track-backend/pkg/database" |
||||
|
|
||||
"github.com/gin-gonic/gin" |
"github.com/gin-gonic/gin" |
||||
) |
) |
||||
|
|
||||
func (h *TaskHandler) DeleteTaskComment(c *gin.Context) { |
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": "评论删除成功"}) |
||||
} |
} |
||||
|
|||||
@ -1,9 +1,44 @@ |
|||||
package task |
package task |
||||
|
|
||||
import ( |
import ( |
||||
|
"net/http" |
||||
|
"strconv" |
||||
|
|
||||
|
"task-track-backend/model" |
||||
|
"task-track-backend/pkg/database" |
||||
|
|
||||
"github.com/gin-gonic/gin" |
"github.com/gin-gonic/gin" |
||||
) |
) |
||||
|
|
||||
func (h *TaskHandler) GetTask(c *gin.Context) { |
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) |
||||
} |
} |
||||
|
|||||
@ -1,9 +1,45 @@ |
|||||
package task |
package task |
||||
|
|
||||
import ( |
import ( |
||||
|
"net/http" |
||||
|
"strconv" |
||||
|
|
||||
|
"task-track-backend/model" |
||||
|
"task-track-backend/pkg/database" |
||||
|
|
||||
"github.com/gin-gonic/gin" |
"github.com/gin-gonic/gin" |
||||
) |
) |
||||
|
|
||||
func (h *TaskHandler) GetTaskAttachments(c *gin.Context) { |
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) |
||||
} |
} |
||||
|
|||||
@ -1,9 +1,46 @@ |
|||||
package task |
package task |
||||
|
|
||||
import ( |
import ( |
||||
|
"net/http" |
||||
|
"strconv" |
||||
|
|
||||
|
"task-track-backend/model" |
||||
|
"task-track-backend/pkg/database" |
||||
|
|
||||
"github.com/gin-gonic/gin" |
"github.com/gin-gonic/gin" |
||||
) |
) |
||||
|
|
||||
func (h *TaskHandler) GetTaskComments(c *gin.Context) { |
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) |
||||
} |
} |
||||
|
|||||
@ -1,9 +1,128 @@ |
|||||
package task |
package task |
||||
|
|
||||
import ( |
import ( |
||||
|
"net/http" |
||||
|
|
||||
|
"task-track-backend/model" |
||||
|
"task-track-backend/pkg/database" |
||||
|
|
||||
"github.com/gin-gonic/gin" |
"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) { |
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) |
||||
} |
} |
||||
|
|||||
@ -1,9 +1,102 @@ |
|||||
package task |
package task |
||||
|
|
||||
import ( |
import ( |
||||
|
"net/http" |
||||
|
"strconv" |
||||
|
"time" |
||||
|
|
||||
|
"task-track-backend/model" |
||||
|
"task-track-backend/pkg/database" |
||||
|
|
||||
"github.com/gin-gonic/gin" |
"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) { |
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) |
||||
} |
} |
||||
|
|||||
@ -1,9 +1,84 @@ |
|||||
package task |
package task |
||||
|
|
||||
import ( |
import ( |
||||
|
"net/http" |
||||
|
"strconv" |
||||
|
"time" |
||||
|
|
||||
|
"task-track-backend/model" |
||||
|
"task-track-backend/pkg/database" |
||||
|
|
||||
"github.com/gin-gonic/gin" |
"github.com/gin-gonic/gin" |
||||
) |
) |
||||
|
|
||||
|
// UpdateTaskStatusRequest 更新任务状态请求结构
|
||||
|
type UpdateTaskStatusRequest struct { |
||||
|
Status string `json:"status" binding:"required"` |
||||
|
} |
||||
|
|
||||
func (h *TaskHandler) UpdateTaskStatus(c *gin.Context) { |
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) |
||||
} |
} |
||||
|
|||||
@ -1,9 +1,71 @@ |
|||||
package task |
package task |
||||
|
|
||||
import ( |
import ( |
||||
|
"fmt" |
||||
|
"net/http" |
||||
|
"path/filepath" |
||||
|
"time" |
||||
|
|
||||
"github.com/gin-gonic/gin" |
"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) { |
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) |
||||
} |
} |
||||
|
|||||
Loading…
Reference in new issue