refactor(comment): unify status transitions with atomic count maintenance
All checks were successful
Build Backend / build (push) Successful in 3m31s
Build Backend / build-docker (push) Successful in 1m25s

Consolidate comment status changes through transitionStatus() to ensure
posts.comments_count and comments.replies_count are atomically maintained
within the same transaction, preventing inconsistencies between displayed
and stored counts.

- Remove separate ApplyPublishedStats() calls, now handled in UpdateModerationStatus
- Add cache invalidation for admin moderation operations
- Update report auto-hide to use unified status transition
This commit is contained in:
lafay
2026-06-21 17:17:38 +08:00
parent 322aa9eebb
commit 28cfcbf9a8
7 changed files with 285 additions and 139 deletions

View File

@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"with_you/internal/cache"
"with_you/internal/dto"
"with_you/internal/model"
"with_you/internal/repository"
@@ -24,13 +25,15 @@ type AdminCommentService interface {
type adminCommentServiceImpl struct {
commentRepo repository.CommentRepository
postRepo repository.PostRepository
cache cache.Cache
}
// NewAdminCommentService 创建管理端评论服务
func NewAdminCommentService(commentRepo repository.CommentRepository, postRepo repository.PostRepository) AdminCommentService {
func NewAdminCommentService(commentRepo repository.CommentRepository, postRepo repository.PostRepository, cacheBackend cache.Cache) AdminCommentService {
return &adminCommentServiceImpl{
commentRepo: commentRepo,
postRepo: postRepo,
cache: cacheBackend,
}
}
@@ -101,29 +104,49 @@ func (s *adminCommentServiceImpl) BatchDeleteComments(ctx context.Context, ids [
}
func (s *adminCommentServiceImpl) ModerateComment(ctx context.Context, commentID string, status model.CommentStatus, reason, reviewedBy string) (*dto.AdminCommentDetailResponse, error) {
_, err := s.commentRepo.GetAdminCommentByID(commentID)
if _, err := s.commentRepo.GetAdminCommentByID(commentID); err != nil {
return nil, err
}
// UpdateModerationStatus 内部已原子地维护帖子评论数/父评论回复数,
// 因此管理端审核与自动审核路径的计数行为完全一致。
updated, err := s.commentRepo.UpdateModerationStatus(commentID, status, reason, reviewedBy)
if err != nil {
return nil, err
}
if err := s.commentRepo.UpdateModerationStatus(commentID, status, reason, reviewedBy); err != nil {
return nil, err
// 状态变更后失效该帖子相关缓存,确保前端立即看到正确计数
if s.cache != nil {
cache.InvalidatePostDetail(s.cache, updated.PostID)
cache.InvalidatePostList(s.cache)
}
comment, err := s.commentRepo.GetAdminCommentByID(commentID)
if err != nil {
return nil, err
}
return s.convertCommentToAdminDetailResponse(comment), nil
return s.convertCommentToAdminDetailResponse(updated), nil
}
func (s *adminCommentServiceImpl) BatchModerateComments(ctx context.Context, ids []string, status model.CommentStatus, reviewedBy string) (*dto.AdminBatchOperationResponse, error) {
var failedIDs []string
// 收集受影响的帖子,用于审核完成后统一失效缓存
affectedPostIDs := make(map[string]struct{})
for _, id := range ids {
if err := s.commentRepo.UpdateModerationStatus(id, status, "", reviewedBy); err != nil {
// UpdateModerationStatus 内部已原子地维护计数,这里只需收集结果
updated, err := s.commentRepo.UpdateModerationStatus(id, status, "", reviewedBy)
if err != nil {
failedIDs = append(failedIDs, id)
continue
}
if updated != nil {
affectedPostIDs[updated.PostID] = struct{}{}
}
}
// 统一失效受影响帖子的缓存
if s.cache != nil {
for postID := range affectedPostIDs {
cache.InvalidatePostDetail(s.cache, postID)
}
cache.InvalidatePostList(s.cache)
}
if len(failedIDs) == 0 {

View File

@@ -154,18 +154,13 @@ func (s *commentService) reviewCommentAsync(
postOwnerID string,
) {
if s.hookManager == nil {
if err := s.commentRepo.UpdateModerationStatus(commentID, model.CommentStatusPublished, "", "system"); err != nil {
if _, err := s.commentRepo.UpdateModerationStatus(commentID, model.CommentStatusPublished, "", "system"); err != nil {
zap.L().Warn("Failed to publish comment without hook manager",
zap.Error(err),
)
return
}
if err := s.applyCommentPublishedStats(commentID); err != nil {
zap.L().Warn("Failed to apply published stats for comment",
zap.String("commentID", commentID),
zap.Error(err),
)
}
// 计数维护已在 UpdateModerationStatus 事务内完成,无需再单独调用
s.invalidatePostCaches(postID)
s.afterCommentPublished(userID, postID, commentID, parentID, parentUserID, postOwnerID)
return
@@ -210,31 +205,18 @@ func (s *commentService) reviewCommentAsync(
return
}
if updateErr := s.commentRepo.UpdateModerationStatus(commentID, model.CommentStatusPublished, "", result.ReviewedBy); updateErr != nil {
if _, updateErr := s.commentRepo.UpdateModerationStatus(commentID, model.CommentStatusPublished, "", result.ReviewedBy); updateErr != nil {
zap.L().Warn("Failed to publish comment",
zap.String("commentID", commentID),
zap.Error(updateErr),
)
return
}
if statsErr := s.applyCommentPublishedStats(commentID); statsErr != nil {
zap.L().Warn("Failed to apply published stats for comment",
zap.String("commentID", commentID),
zap.Error(statsErr),
)
}
// 计数维护已在 UpdateModerationStatus 事务内完成,无需再单独调用
s.invalidatePostCaches(postID)
s.afterCommentPublished(userID, postID, commentID, parentID, parentUserID, postOwnerID)
}
func (s *commentService) applyCommentPublishedStats(commentID string) error {
comment, err := s.commentRepo.GetByID(commentID)
if err != nil {
return err
}
return s.commentRepo.ApplyPublishedStats(comment)
}
func (s *commentService) invalidatePostCaches(postID string) {
cache.InvalidatePostDetail(s.cache, postID)
cache.InvalidatePostList(s.cache)

View File

@@ -188,7 +188,11 @@ func (s *reportServiceImpl) validateTargetExists(ctx context.Context, targetType
return nil
}
// hideTargetContent 隐藏目标内容
// hideTargetContent 隐藏目标内容
//
// 评论隐藏统一走 commentRepo.UpdateModerationStatus(published → deleted)
// 经由仓储内的统一状态转换层维护 posts.comments_count / comments.replies_count
// 避免此前用裸 Update 改 status 导致举报自动隐藏后计数漏减的问题。
func (s *reportServiceImpl) hideTargetContent(ctx context.Context, targetType model.ReportTargetType, targetID string) error {
switch targetType {
case model.ReportTargetTypePost:
@@ -205,9 +209,13 @@ func (s *reportServiceImpl) hideTargetContent(ctx context.Context, targetType mo
if err != nil {
return err
}
if comment != nil && comment.Status != model.CommentStatusDeleted {
comment.Status = model.CommentStatusDeleted
return s.commentRepo.Update(comment)
// 已是终态deleted/rejected 等非 published无需再隐藏也避免重复调整计数
if comment == nil || comment.Status != model.CommentStatusPublished {
return nil
}
// published → deleted统一状态转换层会原子维护计数永不为负
if _, err := s.commentRepo.UpdateModerationStatus(targetID, model.CommentStatusDeleted, "举报自动隐藏", "system"); err != nil {
return err
}
case model.ReportTargetTypeMessage:
// 消息隐藏通过更新状态实现