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
2 changes: 2 additions & 0 deletions backend/dto/ActivityLogResponse.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ type ActivityLogResponse struct {
ID uint `json:"id"`
UserID uint `json:"user_id"`
Username string `json:"username"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
ActionType string `json:"action_type"`
EntityType string `json:"entity_type"`
EntityID uint `json:"entity_id"`
Expand Down
3 changes: 3 additions & 0 deletions backend/handlers/activity/board_activities.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ func (h *BoardActivitiesHandler) GetBoardActivities(c *gin.Context) {
// 转换为响应格式
activities := make([]dto.ActivityLogResponse, len(logs))
for i, log := range logs {
// log.User.Nickname 和 log.User.Avatar 应该有值
activities[i] = convertToActivityLogResponse(log)
}

Expand All @@ -107,6 +108,8 @@ func convertToActivityLogResponse(log models.ActivityLog) dto.ActivityLogRespons
ID: log.ID,
UserID: log.UserID,
Username: log.Username,
Nickname: log.User.Nickname,
Avatar: log.User.Avatar,
ActionType: log.ActionType,
EntityType: log.EntityType,
EntityID: log.EntityID,
Expand Down
1 change: 1 addition & 0 deletions backend/handlers/activity/task_activities.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ func (h *TaskActivitiesHandler) GetTaskActivities(c *gin.Context) {
// 转换为响应格式
activities := make([]dto.ActivityLogResponse, len(logs))
for i, log := range logs {
// log.User.Nickname 和 log.User.Avatar 应该有值
activities[i] = convertToActivityLogResponse(log)
}

Expand Down
5 changes: 4 additions & 1 deletion backend/handlers/task/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,10 @@ func (h *TaskHandler) MoveTask(c *gin.Context) {
return
}

if err := h.taskService.MoveTask(uint(taskID), moveTaskRequest.NewColumnID, moveTaskRequest.NewOrder); err != nil {
userID := c.GetUint("user_id")
username := c.GetString("username") // 假设中间件中设置了username

if err := h.taskService.MoveTask(uint(taskID), moveTaskRequest.NewColumnID, moveTaskRequest.NewOrder, userID, username); err != nil {
if err == services.ErrTaskNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
Expand Down
118 changes: 118 additions & 0 deletions backend/handlers/user/profile.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package user

import (
"fmt"
"net/http"
"path/filepath"
"strings"
"time"

"progress-wall-backend/services"

Expand Down Expand Up @@ -47,3 +51,117 @@ func (h *ProfileHandler) GetProfile(c *gin.Context) {
"user": user,
})
}

// UploadAvatar 上传头像
func (h *ProfileHandler) UploadAvatar(c *gin.Context) {
userID := c.GetUint("user_id")
if userID == 0 {
c.JSON(http.StatusUnauthorized, gin.H{"error": "无法获取用户信息"})
return
}

// 获取上传的文件
file, err := c.FormFile("avatar")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请选择要上传的文件"})
return
}

// 检查文件大小 (例如限制为 2MB)
if file.Size > 2*1024*1024 {
c.JSON(http.StatusBadRequest, gin.H{"error": "文件大小不能超过2MB"})
return
}

// 检查文件类型
ext := strings.ToLower(filepath.Ext(file.Filename))
if ext != ".jpg" && ext != ".jpeg" && ext != ".png" && ext != ".gif" {
c.JSON(http.StatusBadRequest, gin.H{"error": "只支持 JPG, PNG, GIF 格式的图片"})
return
}

// 生成唯一文件名
filename := fmt.Sprintf("%d_%d%s", userID, time.Now().UnixNano(), ext)
// 确保目录存在
savePath := filepath.Join("uploads", "avatars", filename)

// 保存文件
// c.SaveUploadedFile 会自动打开和关闭文件流,无需手动处理
if err := c.SaveUploadedFile(file, savePath); err != nil {
// 记录具体错误日志以便排查
fmt.Printf("File save error: %v\n", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "文件保存失败,请检查服务器存储权限"})
return
}

// 生成访问 URL
// 注意:这里假设静态资源通过 /uploads 路径访问
avatarURL := "/uploads/avatars/" + filename

// 更新用户信息
user, err := h.userService.GetUserByID(userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取用户信息失败"})
return
}

user.Avatar = avatarURL
if err := h.userService.UpdateUser(user); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "更新头像信息失败"})
return
}

c.JSON(http.StatusOK, gin.H{
"message": "上传成功",
"url": avatarURL,
})
}

// UpdateProfileRequest 更新用户信息请求结构
type UpdateProfileRequest struct {
Nickname string `json:"nickname"`
Email string `json:"email"`
Phone string `json:"phone"`
}

// UpdateProfile 更新用户信息
func (h *ProfileHandler) UpdateProfile(c *gin.Context) {
userID := c.GetUint("user_id")
if userID == 0 {
c.JSON(http.StatusUnauthorized, gin.H{"error": "无法获取用户信息"})
return
}

var req UpdateProfileRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求数据"})
return
}

user, err := h.userService.GetUserByID(userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取用户信息失败"})
return
}

// 更新字段
if req.Nickname != "" {
user.Nickname = req.Nickname
}
// Email 更新可能需要验证唯一性等逻辑,这里暂时简化
if req.Email != "" {
user.Email = req.Email
}
if req.Phone != "" {
user.Phone = req.Phone
}

if err := h.userService.UpdateUser(user); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "更新用户信息失败"})
return
}

c.JSON(http.StatusOK, gin.H{
"user": user,
})
}
5 changes: 5 additions & 0 deletions backend/routes/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ func SetupRoutes(db *gorm.DB, cfg *config.Config) *gin.Engine {

r.Use(cors.New(corsConfig))

// 静态文件服务
r.Static("/uploads", "./uploads")

permService := services.NewPermissionService(db)
rbac := middleware.NewRBACMiddleware(permService, db)

Expand Down Expand Up @@ -77,6 +80,8 @@ func SetupRoutes(db *gorm.DB, cfg *config.Config) *gin.Engine {
{
// 用户相关
protected.GET("/user/profile", profileHandler.GetProfile)
protected.PUT("/user/profile", profileHandler.UpdateProfile)
protected.POST("/user/avatar", profileHandler.UploadAvatar)

// Team Routes
protected.POST("/teams", teamHandler.CreateTeam)
Expand Down
56 changes: 45 additions & 11 deletions backend/services/task_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,10 @@ func (s *TaskService) DeleteTask(taskID uint) error {
}

// MoveTask 移动任务到新列和新位置
func (s *TaskService) MoveTask(taskID uint, newColumnID uint, newOrder int) error {
func (s *TaskService) MoveTask(taskID uint, newColumnID uint, newOrder int, userId uint, userName string) error {
tx := s.db.Begin()
defer tx.Rollback()

defer func() {
if r := recover(); r != nil {
tx.Rollback()
Expand All @@ -106,69 +108,96 @@ func (s *TaskService) MoveTask(taskID uint, newColumnID uint, newOrder int) erro

// 获取任务
var task models.Task
if err := tx.First(&task, taskID).Error; err != nil {
tx.Rollback()
if err := tx.Preload("Column").First(&task, taskID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrTaskNotFound
}
return fmt.Errorf("查询任务失败: %v", err)
}

// 获取看板ID(通过Column)
var column models.Column
if err := tx.First(&column, task.ColumnID).Error; err != nil {
return fmt.Errorf("查询列失败: %v", err)
}
boardID := column.BoardID

oldColumnID := task.ColumnID
oldPosition := task.Position
oldColumnName := task.Column.Name

// 获取新列名称
var newColumn models.Column
if err := tx.First(&newColumn, newColumnID).Error; err != nil {
return fmt.Errorf("查询新列失败: %v", err)
}
newColumnName := newColumn.Name

// 如果移动到不同列,需要更新两个列中的任务位置
if oldColumnID != newColumnID {
// 从旧列中移除:将旧列中位置大于当前任务位置的所有任务位置减1
if err := tx.Model(&models.Task{}).
Where("column_id = ? AND position > ?", oldColumnID, oldPosition).
Update("position", gorm.Expr("position - 1")).Error; err != nil {
tx.Rollback()
return fmt.Errorf("更新旧列任务位置失败: %v", err)
}

// 在新列中插入:将新列中位置大于等于newOrder的所有任务位置加1
if err := tx.Model(&models.Task{}).
Where("column_id = ? AND position >= ?", newColumnID, newOrder).
Update("position", gorm.Expr("position + 1")).Error; err != nil {
tx.Rollback()
return fmt.Errorf("更新新列任务位置失败: %v", err)
}

// 更新任务的列ID和位置
if err := tx.Model(&task).
if err := tx.Model(&models.Task{}).Where("id = ?", task.ID).
Updates(map[string]interface{}{
"column_id": newColumnID,
"position": newOrder,
}).Error; err != nil {
tx.Rollback()
return fmt.Errorf("更新任务位置失败: %v", err)
}

// 记录跨列移动日志
log := models.ActivityLog{
UserID: userId,
Username: userName,
ActionType: models.ActionMove,
EntityType: models.EntityTask,
EntityID: task.ID,
BoardID: &boardID,
TaskID: &task.ID,
ProjectID: &task.ProjectID,
Description: fmt.Sprintf("moved this task from \"%s\" to \"%s\"", oldColumnName, newColumnName),
}
if err := s.createActivityLog(tx, &log); err != nil {
return fmt.Errorf("创建活动日志失败: %v", err)
}

} else {
// 同一列内移动
if oldPosition < newOrder {
// 向后移动:将位置在 (oldPosition, newOrder] 之间的任务位置减1
if err := tx.Model(&models.Task{}).
Where("column_id = ? AND position > ? AND position <= ?", newColumnID, oldPosition, newOrder).
Update("position", gorm.Expr("position - 1")).Error; err != nil {
tx.Rollback()
return fmt.Errorf("更新任务位置失败: %v", err)
}
} else if oldPosition > newOrder {
// 向前移动:将位置在 [newOrder, oldPosition) 之间的任务位置加1
if err := tx.Model(&models.Task{}).
Where("column_id = ? AND position >= ? AND position < ?", newColumnID, newOrder, oldPosition).
Update("position", gorm.Expr("position + 1")).Error; err != nil {
tx.Rollback()
return fmt.Errorf("更新任务位置失败: %v", err)
}
}

// 更新任务位置
if err := tx.Model(&task).Update("position", newOrder).Error; err != nil {
tx.Rollback()
if err := tx.Model(&models.Task{}).Where("id = ?", task.ID).Update("position", newOrder).Error; err != nil {
return fmt.Errorf("更新任务位置失败: %v", err)
}

// 同列移动暂不记录日志
}

// 提交事务并验证
Expand All @@ -177,3 +206,8 @@ func (s *TaskService) MoveTask(taskID uint, newColumnID uint, newOrder int) erro
}
return nil
}

// createActivityLog 创建活动日志的内部辅助方法
func (s *TaskService) createActivityLog(tx *gorm.DB, log *models.ActivityLog) error {
return tx.Create(log).Error
}
18 changes: 18 additions & 0 deletions backend/services/user_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,21 @@ func (s *UserService) GetUserByID(userID uint) (*models.User, error) {

return &user, nil
}

// UpdateUser 更新用户信息
// 参数 user: 包含更新信息的User对象,必须包含ID
// 返回 error: 更新失败时返回错误
func (s *UserService) UpdateUser(user *models.User) error {
if user == nil {
return errors.New("用户信息不能为空")
}
if user.ID == 0 {
return errors.New("用户ID不能为空")
}

result := s.db.Save(user)
if result.Error != nil {
return errors.New("更新用户失败")
}
return nil
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading