diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index 6851d1e..0a81568 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -47,7 +47,7 @@ jobs: run: go build -v -o server ./cmd/server - name: Upload artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: backend-server-linux-amd64 path: server @@ -70,7 +70,7 @@ jobs: password: ${{ secrets.GIT_TOKEN }} - name: Download artifact - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: backend-server-linux-amd64 diff --git a/internal/dto/dto.go b/internal/dto/dto.go index 082f275..2293f0d 100644 --- a/internal/dto/dto.go +++ b/internal/dto/dto.go @@ -149,6 +149,7 @@ type PostResponse struct { IsVote bool `json:"is_vote"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` + ContentEditedAt string `json:"content_edited_at,omitempty"` Author *UserResponse `json:"author"` IsLiked bool `json:"is_liked"` IsFavorited bool `json:"is_favorited"` @@ -172,6 +173,7 @@ type PostDetailResponse struct { IsVote bool `json:"is_vote"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` + ContentEditedAt string `json:"content_edited_at,omitempty"` Author *UserResponse `json:"author"` IsLiked bool `json:"is_liked"` IsFavorited bool `json:"is_favorited"` @@ -918,9 +920,10 @@ type AdminPostListResponse struct { RejectReason string `json:"reject_reason,omitempty"` ReviewedAt string `json:"reviewed_at,omitempty"` ReviewedBy string `json:"reviewed_by,omitempty"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` - Author *UserResponse `json:"author"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + ContentEditedAt string `json:"content_edited_at,omitempty"` + Author *UserResponse `json:"author"` } // AdminPostDetailResponse 管理端帖子详情响应 @@ -944,10 +947,11 @@ type AdminPostDetailResponse struct { IsVote bool `json:"is_vote"` RejectReason string `json:"reject_reason,omitempty"` ReviewedAt string `json:"reviewed_at,omitempty"` - ReviewedBy string `json:"reviewed_by,omitempty"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` - Author *UserResponse `json:"author"` + ReviewedBy string `json:"reviewed_by,omitempty"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + ContentEditedAt string `json:"content_edited_at,omitempty"` + Author *UserResponse `json:"author"` } // AdminModeratePostRequest 审核帖子请求 diff --git a/internal/dto/post_converter.go b/internal/dto/post_converter.go index c89b695..f2e806e 100644 --- a/internal/dto/post_converter.go +++ b/internal/dto/post_converter.go @@ -48,25 +48,26 @@ func ConvertPostToResponse(post *model.Post, isLiked, isFavorited bool) *PostRes } return &PostResponse{ - ID: post.ID, - UserID: post.UserID, - Title: post.Title, - Content: post.Content, - Images: images, - Status: string(post.Status), - LikesCount: post.LikesCount, - CommentsCount: post.CommentsCount, - FavoritesCount: post.FavoritesCount, - SharesCount: post.SharesCount, - ViewsCount: post.ViewsCount, - IsPinned: post.IsPinned, - IsLocked: post.IsLocked, - IsVote: post.IsVote, - CreatedAt: FormatTime(post.CreatedAt), - UpdatedAt: FormatTime(post.UpdatedAt), - Author: author, - IsLiked: isLiked, - IsFavorited: isFavorited, + ID: post.ID, + UserID: post.UserID, + Title: post.Title, + Content: post.Content, + Images: images, + Status: string(post.Status), + LikesCount: post.LikesCount, + CommentsCount: post.CommentsCount, + FavoritesCount: post.FavoritesCount, + SharesCount: post.SharesCount, + ViewsCount: post.ViewsCount, + IsPinned: post.IsPinned, + IsLocked: post.IsLocked, + IsVote: post.IsVote, + CreatedAt: FormatTime(post.CreatedAt), + UpdatedAt: FormatTime(post.UpdatedAt), + ContentEditedAt: FormatTimePointer(post.ContentEditedAt), + Author: author, + IsLiked: isLiked, + IsFavorited: isFavorited, } } @@ -87,25 +88,26 @@ func ConvertPostToDetailResponse(post *model.Post, isLiked, isFavorited bool) *P } return &PostDetailResponse{ - ID: post.ID, - UserID: post.UserID, - Title: post.Title, - Content: post.Content, - Images: images, - Status: string(post.Status), - LikesCount: post.LikesCount, - CommentsCount: post.CommentsCount, - FavoritesCount: post.FavoritesCount, - SharesCount: post.SharesCount, - ViewsCount: post.ViewsCount, - IsPinned: post.IsPinned, - IsLocked: post.IsLocked, - IsVote: post.IsVote, - CreatedAt: FormatTime(post.CreatedAt), - UpdatedAt: FormatTime(post.UpdatedAt), - Author: author, - IsLiked: isLiked, - IsFavorited: isFavorited, + ID: post.ID, + UserID: post.UserID, + Title: post.Title, + Content: post.Content, + Images: images, + Status: string(post.Status), + LikesCount: post.LikesCount, + CommentsCount: post.CommentsCount, + FavoritesCount: post.FavoritesCount, + SharesCount: post.SharesCount, + ViewsCount: post.ViewsCount, + IsPinned: post.IsPinned, + IsLocked: post.IsLocked, + IsVote: post.IsVote, + CreatedAt: FormatTime(post.CreatedAt), + UpdatedAt: FormatTime(post.UpdatedAt), + ContentEditedAt: FormatTimePointer(post.ContentEditedAt), + Author: author, + IsLiked: isLiked, + IsFavorited: isFavorited, } } diff --git a/internal/handler/post_handler.go b/internal/handler/post_handler.go index 3e85cc6..7662b79 100644 --- a/internal/handler/post_handler.go +++ b/internal/handler/post_handler.go @@ -112,9 +112,10 @@ func (h *PostHandler) GetByID(c *gin.Context) { IsPinned: post.IsPinned, IsLocked: post.IsLocked, IsVote: post.IsVote, - CreatedAt: dto.FormatTime(post.CreatedAt), - UpdatedAt: dto.FormatTime(post.UpdatedAt), - Author: authorWithFollowStatus, + CreatedAt: dto.FormatTime(post.CreatedAt), + UpdatedAt: dto.FormatTime(post.UpdatedAt), + ContentEditedAt: dto.FormatTimePointer(post.ContentEditedAt), + Author: authorWithFollowStatus, IsLiked: isLiked, IsFavorited: isFavorited, } diff --git a/internal/model/post.go b/internal/model/post.go index 25cf1b3..22fd82d 100644 --- a/internal/model/post.go +++ b/internal/model/post.go @@ -60,6 +60,8 @@ type Post struct { // 时间戳 CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index:idx_posts_status_created,priority:2,sort:desc;index:idx_posts_user_status_created,priority:3,sort:desc;index:idx_posts_hot_score_created,priority:2,sort:desc"` UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime:false"` + // ContentEditedAt 仅在实际修改标题/正文/图片时更新,供前端展示「已编辑」;与统计类更新解耦 + ContentEditedAt *time.Time `json:"content_edited_at,omitempty" gorm:"column:content_edited_at"` } // BeforeCreate 创建前生成UUID diff --git a/internal/repository/comment_repo.go b/internal/repository/comment_repo.go index c634e2a..286d693 100644 --- a/internal/repository/comment_repo.go +++ b/internal/repository/comment_repo.go @@ -73,6 +73,7 @@ func (r *CommentRepository) Delete(id string) error { UpdateColumns(map[string]interface{}{ "comments_count": gorm.Expr("comments_count - 1"), "hot_score": gorm.Expr("likes_count * 2 + (comments_count - 1) * 3 + views_count * 0.1"), + "updated_at": gorm.Expr("updated_at"), }).Error; err != nil { return err } @@ -102,6 +103,7 @@ func (r *CommentRepository) ApplyPublishedStats(comment *model.Comment) error { UpdateColumns(map[string]any{ "comments_count": gorm.Expr("comments_count + 1"), "hot_score": gorm.Expr("likes_count * 2 + (comments_count + 1) * 3 + views_count * 0.1"), + "updated_at": gorm.Expr("updated_at"), }).Error; err != nil { return err } diff --git a/internal/repository/post_repo.go b/internal/repository/post_repo.go index 9adb6c1..ac090a3 100644 --- a/internal/repository/post_repo.go +++ b/internal/repository/post_repo.go @@ -61,7 +61,8 @@ func (r *PostRepository) Create(post *model.Post, images []string) error { } } - return nil + // 新建帖子:保证 updated_at 与 created_at 一致(避免历史 NULL/库默认值导致前端误判为已编辑) + return tx.Model(post).Where("id = ?", post.ID).UpdateColumn("updated_at", post.CreatedAt).Error }) } @@ -77,14 +78,18 @@ func (r *PostRepository) GetByID(id string) (*model.Post, error) { // Update 更新帖子 func (r *PostRepository) Update(post *model.Post) error { - post.UpdatedAt = time.Now() + now := time.Now() + post.UpdatedAt = now + post.ContentEditedAt = &now return r.db.Save(post).Error } // UpdateWithImages 更新帖子及其图片(images=nil 表示不更新图片) func (r *PostRepository) UpdateWithImages(post *model.Post, images *[]string) error { return r.db.Transaction(func(tx *gorm.DB) error { - post.UpdatedAt = time.Now() + now := time.Now() + post.UpdatedAt = now + post.ContentEditedAt = &now if err := tx.Save(post).Error; err != nil { return err } @@ -120,6 +125,7 @@ func (r *PostRepository) UpdateModerationStatus(postID string, status model.Post "reviewed_at": gorm.Expr("CURRENT_TIMESTAMP"), "reviewed_by": reviewedBy, "reject_reason": rejectReason, + "updated_at": gorm.Expr("updated_at"), // 防止 MySQL ON UPDATE CURRENT_TIMESTAMP 误刷新 } return r.db.Model(&model.Post{}).Where("id = ?", postID).UpdateColumns(updates).Error } @@ -258,6 +264,7 @@ func (r *PostRepository) Like(postID, userID string) error { UpdateColumns(map[string]any{ "likes_count": gorm.Expr("likes_count + 1"), "hot_score": gorm.Expr("(likes_count + 1) * 2 + comments_count * 3 + views_count * 0.1"), + "updated_at": gorm.Expr("updated_at"), }).Error }) } @@ -276,6 +283,7 @@ func (r *PostRepository) Unlike(postID, userID string) error { UpdateColumns(map[string]any{ "likes_count": gorm.Expr("likes_count - 1"), "hot_score": gorm.Expr("(likes_count - 1) * 2 + comments_count * 3 + views_count * 0.1"), + "updated_at": gorm.Expr("updated_at"), }).Error } return nil @@ -338,9 +346,12 @@ func (r *PostRepository) Favorite(postID, userID string) error { return err } - // 增加帖子收藏数 + // 增加帖子收藏数(显式固定 updated_at,避免 MySQL ON UPDATE 误刷新) return tx.Model(&model.Post{}).Where("id = ?", postID). - UpdateColumn("favorites_count", gorm.Expr("favorites_count + 1")).Error + UpdateColumns(map[string]any{ + "favorites_count": gorm.Expr("favorites_count + 1"), + "updated_at": gorm.Expr("updated_at"), + }).Error }) } @@ -354,7 +365,10 @@ func (r *PostRepository) Unfavorite(postID, userID string) error { if result.RowsAffected > 0 { // 减少帖子收藏数 return tx.Model(&model.Post{}).Where("id = ?", postID). - UpdateColumn("favorites_count", gorm.Expr("favorites_count - 1")).Error + UpdateColumns(map[string]any{ + "favorites_count": gorm.Expr("favorites_count - 1"), + "updated_at": gorm.Expr("updated_at"), + }).Error } return nil }) @@ -401,6 +415,7 @@ func (r *PostRepository) IncrementViews(postID string) error { UpdateColumns(map[string]interface{}{ "views_count": gorm.Expr("views_count + 1"), "hot_score": gorm.Expr("likes_count * 2 + comments_count * 3 + (views_count + 1) * 0.1"), + "updated_at": gorm.Expr("updated_at"), }).Error } @@ -537,7 +552,7 @@ func (r *PostRepository) createWithTx(tx *gorm.DB, post *model.Post, images []st } } - return nil + return tx.Model(post).Where("id = ?", post.ID).UpdateColumn("updated_at", post.CreatedAt).Error } // GetByIDWithContext 根据ID获取帖子(支持事务) @@ -552,7 +567,9 @@ func (r *PostRepository) GetByIDWithContext(ctx context.Context, id string) (*mo // UpdateWithContext 更新帖子(支持事务) func (r *PostRepository) UpdateWithContext(ctx context.Context, post *model.Post) error { - post.UpdatedAt = time.Now() + now := time.Now() + post.UpdatedAt = now + post.ContentEditedAt = &now return r.getDB(ctx).WithContext(ctx).Save(post).Error } @@ -706,14 +723,20 @@ func (r *PostRepository) BatchUpdateStatus(ids []string, status model.PostStatus func (r *PostRepository) UpdatePinStatus(postID string, isPinned bool) error { // 置顶状态属于管理操作,不应影响帖子内容更新时间(updated_at) return r.db.Model(&model.Post{}).Where("id = ?", postID). - UpdateColumn("is_pinned", isPinned).Error + UpdateColumns(map[string]any{ + "is_pinned": isPinned, + "updated_at": gorm.Expr("updated_at"), + }).Error } // UpdateFeatureStatus 更新帖子加精状态 func (r *PostRepository) UpdateFeatureStatus(postID string, isFeatured bool) error { // 加精状态属于管理操作,不应影响帖子内容更新时间(updated_at) return r.db.Model(&model.Post{}).Where("id = ?", postID). - UpdateColumn("is_featured", isFeatured).Error + UpdateColumns(map[string]any{ + "is_featured": isFeatured, + "updated_at": gorm.Expr("updated_at"), + }).Error } // ========== Cursor Pagination Methods ========== diff --git a/internal/service/admin_post_service.go b/internal/service/admin_post_service.go index eda8692..06cda00 100644 --- a/internal/service/admin_post_service.go +++ b/internal/service/admin_post_service.go @@ -1,4 +1,4 @@ -package service +package service import ( "context" @@ -278,25 +278,26 @@ func convertPostToAdminListResponse(post *model.Post) dto.AdminPostListResponse } return dto.AdminPostListResponse{ - ID: post.ID, - UserID: post.UserID, - Title: post.Title, - Content: post.Content, - Images: images, - Status: string(post.Status), - LikesCount: post.LikesCount, - CommentsCount: post.CommentsCount, - FavoritesCount: post.FavoritesCount, - ViewsCount: post.ViewsCount, - IsPinned: post.IsPinned, - IsFeatured: post.IsFeatured, - IsLocked: post.IsLocked, - RejectReason: post.RejectReason, - ReviewedAt: dto.FormatTimePointer(post.ReviewedAt), - ReviewedBy: post.ReviewedBy, - CreatedAt: post.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), - UpdatedAt: post.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"), - Author: author, + ID: post.ID, + UserID: post.UserID, + Title: post.Title, + Content: post.Content, + Images: images, + Status: string(post.Status), + LikesCount: post.LikesCount, + CommentsCount: post.CommentsCount, + FavoritesCount: post.FavoritesCount, + ViewsCount: post.ViewsCount, + IsPinned: post.IsPinned, + IsFeatured: post.IsFeatured, + IsLocked: post.IsLocked, + RejectReason: post.RejectReason, + ReviewedAt: dto.FormatTimePointer(post.ReviewedAt), + ReviewedBy: post.ReviewedBy, + CreatedAt: post.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), + UpdatedAt: post.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"), + ContentEditedAt: dto.FormatTimePointer(post.ContentEditedAt), + Author: author, } } @@ -319,28 +320,29 @@ func convertPostToAdminDetailResponse(post *model.Post) *dto.AdminPostDetailResp } return &dto.AdminPostDetailResponse{ - ID: post.ID, - UserID: post.UserID, - CommunityID: post.CommunityID, - Title: post.Title, - Content: post.Content, - Images: images, - Status: string(post.Status), - LikesCount: post.LikesCount, - CommentsCount: post.CommentsCount, - FavoritesCount: post.FavoritesCount, - SharesCount: post.SharesCount, - ViewsCount: post.ViewsCount, - HotScore: post.HotScore, - IsPinned: post.IsPinned, - IsFeatured: post.IsFeatured, - IsLocked: post.IsLocked, - IsVote: post.IsVote, - RejectReason: post.RejectReason, - ReviewedAt: dto.FormatTimePointer(post.ReviewedAt), - ReviewedBy: post.ReviewedBy, - CreatedAt: post.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), - UpdatedAt: post.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"), - Author: author, + ID: post.ID, + UserID: post.UserID, + CommunityID: post.CommunityID, + Title: post.Title, + Content: post.Content, + Images: images, + Status: string(post.Status), + LikesCount: post.LikesCount, + CommentsCount: post.CommentsCount, + FavoritesCount: post.FavoritesCount, + SharesCount: post.SharesCount, + ViewsCount: post.ViewsCount, + HotScore: post.HotScore, + IsPinned: post.IsPinned, + IsFeatured: post.IsFeatured, + IsLocked: post.IsLocked, + IsVote: post.IsVote, + RejectReason: post.RejectReason, + ReviewedAt: dto.FormatTimePointer(post.ReviewedAt), + ReviewedBy: post.ReviewedBy, + CreatedAt: post.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), + UpdatedAt: post.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"), + ContentEditedAt: dto.FormatTimePointer(post.ContentEditedAt), + Author: author, } } diff --git a/internal/service/vote_service.go b/internal/service/vote_service.go index 10c798f..6ed0106 100644 --- a/internal/service/vote_service.go +++ b/internal/service/vote_service.go @@ -283,8 +283,10 @@ func (s *VoteService) convertToPostResponse(post *model.Post, currentUserID stri IsPinned: post.IsPinned, IsLocked: post.IsLocked, IsVote: post.IsVote, - CreatedAt: dto.FormatTime(post.CreatedAt), - Images: make([]dto.PostImageResponse, 0, len(post.Images)), + CreatedAt: dto.FormatTime(post.CreatedAt), + UpdatedAt: dto.FormatTime(post.UpdatedAt), + ContentEditedAt: dto.FormatTimePointer(post.ContentEditedAt), + Images: make([]dto.PostImageResponse, 0, len(post.Images)), } // 转换图片