package service import ( "context" "fmt" "strings" "carrot_bbs/internal/cache" "carrot_bbs/internal/model" "carrot_bbs/internal/pkg/cursor" "carrot_bbs/internal/pkg/hook" "carrot_bbs/internal/repository" "go.uber.org/zap" ) type CommentService struct { commentRepo repository.CommentRepository postRepo repository.PostRepository systemMessageService SystemMessageService cache cache.Cache postAIService *PostAIService logService *LogService hookManager *hook.Manager } func NewCommentService(commentRepo repository.CommentRepository, postRepo repository.PostRepository, systemMessageService SystemMessageService, postAIService *PostAIService, cacheBackend cache.Cache, hookManager *hook.Manager, logService *LogService) *CommentService { return &CommentService{ commentRepo: commentRepo, postRepo: postRepo, systemMessageService: systemMessageService, cache: cacheBackend, postAIService: postAIService, logService: logService, hookManager: hookManager, } } // Create 创建评论 func (s *CommentService) Create(ctx context.Context, postID, userID, content string, parentID *string, images string, imageURLs []string) (*model.Comment, error) { if s.postAIService != nil { // 采用异步审核,前端先立即返回 } // 获取帖子信息用于发送通知 post, err := s.postRepo.GetByID(postID) if err != nil { return nil, err } comment := &model.Comment{ PostID: postID, UserID: userID, Content: content, ParentID: parentID, Images: images, Status: model.CommentStatusPending, } // 如果有父评论,设置根评论ID var parentUserID string if parentID != nil { parent, err := s.commentRepo.GetByID(*parentID) if err == nil && parent != nil { if parent.RootID != nil { comment.RootID = parent.RootID } else { comment.RootID = parentID } parentUserID = parent.UserID } } err = s.commentRepo.Create(comment) if err != nil { return nil, err } // 记录操作日志 if s.logService != nil { s.logService.OperationLog.RecordOperation(&model.OperationLog{ UserID: userID, Operation: string(model.OpCommentCreate), Module: "comment", TargetType: "comment", TargetID: comment.ID, Status: "success", }) } // 重新查询以获取关联的 User comment, err = s.commentRepo.GetByID(comment.ID) if err != nil { return nil, err } go s.reviewCommentAsync(comment.ID, userID, postID, content, imageURLs, parentID, parentUserID, post.UserID) return comment, nil } func (s *CommentService) reviewCommentAsync( commentID, userID, postID, content string, imageURLs []string, parentID *string, parentUserID string, postOwnerID string, ) { if s.hookManager == nil { if err := s.commentRepo.UpdateModerationStatus(commentID, model.CommentStatusPublished); 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), ) } s.invalidatePostCaches(postID) s.afterCommentPublished(userID, postID, commentID, parentID, parentUserID, postOwnerID) return } var authorID uint fmt.Sscanf(userID, "%d", &authorID) result := &hook.ModerationResult{} s.hookManager.TriggerWithMetadata(context.Background(), hook.HookCommentPreModerate, authorID, &hook.CommentModerateHookData{ CommentID: commentID, PostID: postID, Content: content, Images: imageURLs, AuthorID: authorID, ParentID: parentID, }, map[string]interface{}{ "result": result, }) s.hookManager.Trigger(context.Background(), hook.HookCommentModerated, authorID, &hook.CommentModeratedHookData{ CommentID: commentID, PostID: postID, AuthorID: authorID, Approved: result.Approved, RejectReason: result.RejectReason, ReviewedBy: result.ReviewedBy, }) if !result.Approved { if delErr := s.commentRepo.Delete(commentID); delErr != nil { zap.L().Warn("Failed to delete rejected comment", zap.String("commentID", commentID), zap.Error(delErr), ) } s.notifyCommentModerationRejected(userID, result.RejectReason) return } if updateErr := s.commentRepo.UpdateModerationStatus(commentID, model.CommentStatusPublished); 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), ) } 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) } func (s *CommentService) afterCommentPublished(userID, postID, commentID string, parentID *string, parentUserID, postOwnerID string) { // 发送系统消息通知 if s.systemMessageService != nil { go func() { if parentID != nil && parentUserID != "" { // 回复评论,通知被回复的人 if parentUserID != userID { notifyErr := s.systemMessageService.SendReplyNotification(context.Background(), parentUserID, userID, postID, *parentID, commentID) if notifyErr != nil { zap.L().Error("Error sending reply notification", zap.Error(notifyErr), ) } } } else { // 评论帖子,通知帖子作者 if postOwnerID != userID { notifyErr := s.systemMessageService.SendCommentNotification(context.Background(), postOwnerID, userID, postID, commentID) if notifyErr != nil { zap.L().Error("Error sending comment notification", zap.Error(notifyErr), ) } } } }() } } func (s *CommentService) notifyCommentModerationRejected(userID, reason string) { if s.systemMessageService == nil || strings.TrimSpace(userID) == "" { return } content := "您发布的评论未通过AI审核,请修改后重试。" if strings.TrimSpace(reason) != "" { content = fmt.Sprintf("您发布的评论未通过AI审核,原因:%s。请修改后重试。", reason) } go func() { if err := s.systemMessageService.SendSystemAnnouncement( context.Background(), []string{userID}, "评论审核未通过", content, ); err != nil { zap.L().Warn("Failed to send comment moderation reject notification", zap.Error(err), ) } }() } // GetByID 根据ID获取评论 func (s *CommentService) GetByID(ctx context.Context, id string) (*model.Comment, error) { return s.commentRepo.GetByID(id) } // GetByPostID 获取帖子评论 func (s *CommentService) GetByPostID(ctx context.Context, postID string, page, pageSize int) ([]*model.Comment, int64, error) { // 使用带回复的查询,默认加载前3条回复 return s.commentRepo.GetByPostIDWithReplies(postID, page, pageSize, 3) } // GetRepliesByRootID 根据根评论ID分页获取回复 func (s *CommentService) GetRepliesByRootID(ctx context.Context, rootID string, page, pageSize int) ([]*model.Comment, int64, error) { return s.commentRepo.GetRepliesByRootID(rootID, page, pageSize) } // GetReplies 获取回复 func (s *CommentService) GetReplies(ctx context.Context, parentID string) ([]*model.Comment, error) { return s.commentRepo.GetReplies(parentID) } // Update 更新评论 func (s *CommentService) Update(ctx context.Context, comment *model.Comment) error { return s.commentRepo.Update(comment) } // Delete 删除评论 func (s *CommentService) Delete(ctx context.Context, id string) error { comment, err := s.commentRepo.GetByID(id) if err != nil { return err } if err := s.commentRepo.Delete(id); err != nil { return err } s.invalidatePostCaches(comment.PostID) return nil } // Like 点赞评论 func (s *CommentService) Like(ctx context.Context, commentID, userID string) error { // 获取评论信息用于发送通知 comment, err := s.commentRepo.GetByID(commentID) if err != nil { return err } err = s.commentRepo.Like(commentID, userID) if err != nil { return err } // 发送评论/回复点赞通知(只有不是给自己点赞时才发送) if s.systemMessageService != nil && comment.UserID != userID { go func() { var notifyErr error if comment.ParentID != nil { notifyErr = s.systemMessageService.SendLikeReplyNotification( context.Background(), comment.UserID, userID, comment.PostID, commentID, comment.Content, ) } else { notifyErr = s.systemMessageService.SendLikeCommentNotification( context.Background(), comment.UserID, userID, comment.PostID, commentID, comment.Content, ) } if notifyErr != nil { zap.L().Error("Error sending like notification", zap.Error(notifyErr), ) } }() } return nil } // Unlike 取消点赞评论 func (s *CommentService) Unlike(ctx context.Context, commentID, userID string) error { return s.commentRepo.Unlike(commentID, userID) } // IsLiked 检查是否已点赞 func (s *CommentService) IsLiked(ctx context.Context, commentID, userID string) bool { return s.commentRepo.IsLiked(commentID, userID) } // GetCommentsByCursor 游标分页获取帖子评论 func (s *CommentService) GetCommentsByCursor(ctx context.Context, postID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Comment], error) { return s.commentRepo.GetCommentsByCursor(ctx, postID, req) } // GetRepliesByCursor 游标分页获取根评论的回复 func (s *CommentService) GetRepliesByCursor(ctx context.Context, rootID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Comment], error) { return s.commentRepo.GetRepliesByCursor(ctx, rootID, req) }