refactor(comment): unify status transitions with atomic count maintenance
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:
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
// 消息隐藏通过更新状态实现
|
||||
|
||||
Reference in New Issue
Block a user