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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions backend/database/migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ func Migrate(db *gorm.DB) error {
&models.Attachment{},
&models.Label{},
&models.TaskLabel{},

// 活动日志
&models.ActivityLog{},
)

if err != nil {
Expand Down
30 changes: 30 additions & 0 deletions backend/dto/ActivityLogResponse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package dto

import "time"

// ActivityLogResponse 活动日志响应
type ActivityLogResponse struct {
ID uint `json:"id"`
UserID uint `json:"user_id"`
Username string `json:"username"`
ActionType string `json:"action_type"`
EntityType string `json:"entity_type"`
EntityID uint `json:"entity_id"`
Description string `json:"description"`
CreatedAt time.Time `json:"created_at"`
}

// ActivityLogListResponse 活动日志列表响应
type ActivityLogListResponse struct {
Data []ActivityLogResponse `json:"data"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
TotalPages int `json:"total_pages"`
}

// PaginationQuery 分页查询参数
type PaginationQuery struct {
Page int `form:"page" binding:"omitempty,min=1"`
PageSize int `form:"limit" binding:"omitempty,min=1,max=100"`
}
115 changes: 115 additions & 0 deletions backend/handlers/activity/board_activities.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package activity

import (
"math"
"net/http"
"progress-wall-backend/dto"
"progress-wall-backend/models"
"progress-wall-backend/repository"
"progress-wall-backend/services"
"strconv"

"github.com/gin-gonic/gin"
"gorm.io/gorm"
)

// BoardActivitiesHandler 看板活动日志处理器adctiv
type BoardActivitiesHandler struct {
activityService *services.ActivityLogService
}

// NewBoardActivitiesHandler 创建看板活动日志处理器
func NewBoardActivitiesHandler(db *gorm.DB) *BoardActivitiesHandler {
activityRepo := repository.NewActivityLogRepository(db)
activityService := services.NewActivityLogService(activityRepo, db)

return &BoardActivitiesHandler{
activityService: activityService,
}
}

// GetBoardActivities 获取看板活动日志
// GET /api/boards/:board_id/activities
func (h *BoardActivitiesHandler) GetBoardActivities(c *gin.Context) {
// 获取当前登录用户ID
userID := c.GetUint("user_id")
if userID == 0 {
c.JSON(http.StatusUnauthorized, gin.H{"error": "未登录"})
return
}

// 获取看板ID
boardIDStr := c.Param("board_id")
boardID, err := strconv.ParseUint(boardIDStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的看板ID"})
return
}

// 验证用户是否有权访问该看板
if err := h.activityService.VerifyBoardAccess(userID, uint(boardID)); err != nil {
switch err {
case services.ErrBoardNotFound:
c.JSON(http.StatusNotFound, gin.H{"error": "看板不存在"})
case services.ErrAccessDenied:
c.JSON(http.StatusForbidden, gin.H{"error": "没有权限访问该看板"})
default:
c.JSON(http.StatusInternalServerError, gin.H{"error": "验证权限失败"})
}
return
}

// 获取分页参数
var query dto.PaginationQuery
if err := c.ShouldBindQuery(&query); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的分页参数"})
return
}

// 设置默认值
if query.Page == 0 {
query.Page = 1
}
if query.PageSize == 0 {
query.PageSize = 20
}

// 获取活动日志
logs, total, err := h.activityService.GetBoardActivities(uint(boardID), query.Page, query.PageSize)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取活动日志失败"})
return
}

// 转换为响应格式
activities := make([]dto.ActivityLogResponse, len(logs))
for i, log := range logs {
activities[i] = convertToActivityLogResponse(log)
}

// 计算总页数
totalPages := int(math.Ceil(float64(total) / float64(query.PageSize)))

// 返回响应
c.JSON(http.StatusOK, dto.ActivityLogListResponse{
Data: activities,
Total: total,
Page: query.Page,
PageSize: query.PageSize,
TotalPages: totalPages,
})
}

// convertToActivityLogResponse 转换模型为响应DTO
func convertToActivityLogResponse(log models.ActivityLog) dto.ActivityLogResponse {
return dto.ActivityLogResponse{
ID: log.ID,
UserID: log.UserID,
Username: log.Username,
ActionType: log.ActionType,
EntityType: log.EntityType,
EntityID: log.EntityID,
Description: log.Description,
CreatedAt: log.CreatedAt,
}
}
100 changes: 100 additions & 0 deletions backend/handlers/activity/task_activities.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package activity

import (
"math"
"net/http"
"progress-wall-backend/dto"
"progress-wall-backend/repository"
"progress-wall-backend/services"
"strconv"

"github.com/gin-gonic/gin"
"gorm.io/gorm"
)

// TaskActivitiesHandler 任务活动日志处理器
type TaskActivitiesHandler struct {
activityService *services.ActivityLogService
}

// NewTaskActivitiesHandler 创建任务活动日志处理器
func NewTaskActivitiesHandler(db *gorm.DB) *TaskActivitiesHandler {
activityRepo := repository.NewActivityLogRepository(db)
activityService := services.NewActivityLogService(activityRepo, db)

return &TaskActivitiesHandler{
activityService: activityService,
}
}

// GetTaskActivities 获取任务活动日志
// GET /api/tasks/:task_id/activities
func (h *TaskActivitiesHandler) GetTaskActivities(c *gin.Context) {
// 获取当前登录用户ID
userID := c.GetUint("user_id")
if userID == 0 {
c.JSON(http.StatusUnauthorized, gin.H{"error": "未登录"})
return
}

// 获取任务ID
taskIDStr := c.Param("task_id")
taskID, err := strconv.ParseUint(taskIDStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的任务ID"})
return
}

// 验证用户是否有权访问该任务
if err := h.activityService.VerifyTaskAccess(userID, uint(taskID)); err != nil {
switch err {
case services.ErrTaskNotFound:
c.JSON(http.StatusNotFound, gin.H{"error": "任务不存在"})
case services.ErrAccessDenied:
c.JSON(http.StatusForbidden, gin.H{"error": "没有权限访问该任务"})
default:
c.JSON(http.StatusInternalServerError, gin.H{"error": "验证权限失败"})
}
return
}

// 获取分页参数
var query dto.PaginationQuery
if err := c.ShouldBindQuery(&query); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的分页参数"})
return
}

// 设置默认值
if query.Page == 0 {
query.Page = 1
}
if query.PageSize == 0 {
query.PageSize = 20
}

// 获取活动日志
logs, total, err := h.activityService.GetTaskActivities(uint(taskID), query.Page, query.PageSize)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取活动日志失败"})
return
}

// 转换为响应格式
activities := make([]dto.ActivityLogResponse, len(logs))
for i, log := range logs {
activities[i] = convertToActivityLogResponse(log)
}

// 计算总页数
totalPages := int(math.Ceil(float64(total) / float64(query.PageSize)))

// 返回响应
c.JSON(http.StatusOK, dto.ActivityLogListResponse{
Data: activities,
Total: total,
Page: query.Page,
PageSize: query.PageSize,
TotalPages: totalPages,
})
}
62 changes: 62 additions & 0 deletions backend/models/activity_log.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package models

import (
"time"

"gorm.io/gorm"
)

// ActivityLog 活动日志表
type ActivityLog struct {
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
UserID uint `json:"user_id" gorm:"not null;index;comment:'执行操作的用户ID'"`
Username string `json:"username" gorm:"size:50;not null;comment:'执行操作的用户名(冗余字段,优化查询)'"`
ActionType string `json:"action_type" gorm:"size:50;not null;index;comment:'操作类型:create/update/delete/move/comment等'"`
EntityType string `json:"entity_type" gorm:"size:50;not null;index;comment:'实体类型:board/task/comment/attachment等'"`
EntityID uint `json:"entity_id" gorm:"not null;index;comment:'实体ID'"`
BoardID *uint `json:"board_id" gorm:"index;comment:'关联的看板ID(用于看板级别查询)'"`
TaskID *uint `json:"task_id" gorm:"index;comment:'关联的任务ID(用于任务级别查询)'"`
ProjectID *uint `json:"project_id" gorm:"index;comment:'关联的项目ID'"`
Description string `json:"description" gorm:"type:text;not null;comment:'操作描述文本'"`
Metadata string `json:"metadata" gorm:"type:json;comment:'额外的元数据(JSON格式)'"`
IPAddress string `json:"ip_address" gorm:"size:45;comment:'操作者IP地址'"`
UserAgent string `json:"user_agent" gorm:"size:255;comment:'用户代理字符串'"`
CreatedAt time.Time `json:"created_at" gorm:"index"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`

// 关联关系
User User `json:"user,omitempty" gorm:"foreignKey:UserID"`
Board *Board `json:"board,omitempty" gorm:"foreignKey:BoardID"`
Task *Task `json:"task,omitempty" gorm:"foreignKey:TaskID"`
Project *Project `json:"project,omitempty" gorm:"foreignKey:ProjectID"`
}

// TableName 指定表名
func (ActivityLog) TableName() string {
return "activity_logs"
}

// ActivityActionType 定义常用的操作类型
const (
ActionCreate = "create"
ActionUpdate = "update"
ActionDelete = "delete"
ActionMove = "move"
ActionComment = "comment"
ActionAttach = "attach"
ActionAssign = "assign"
ActionArchive = "archive"
ActionRestore = "restore"
)

// ActivityEntityType 定义常用的实体类型
const (
EntityBoard = "board"
EntityColumn = "column"
EntityTask = "task"
EntityComment = "comment"
EntityAttachment = "attachment"
EntityLabel = "label"
EntityProject = "project"
)
Loading