Files
backend/internal/service/comment_service.go
lan 4c0177149a Clean backend debug logging and standardize error reporting.
This removes verbose trace output in handlers/services and keeps only actionable error-level logs.
2026-03-09 22:20:44 +08:00

272 lines
8.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"context"
"errors"
"fmt"
"log"
"strings"
"carrot_bbs/internal/model"
"carrot_bbs/internal/pkg/gorse"
"carrot_bbs/internal/repository"
)
// CommentService 评论服务
type CommentService struct {
commentRepo *repository.CommentRepository
postRepo *repository.PostRepository
systemMessageService SystemMessageService
gorseClient gorse.Client
postAIService *PostAIService
}
// NewCommentService 创建评论服务
func NewCommentService(commentRepo *repository.CommentRepository, postRepo *repository.PostRepository, systemMessageService SystemMessageService, gorseClient gorse.Client, postAIService *PostAIService) *CommentService {
return &CommentService{
commentRepo: commentRepo,
postRepo: postRepo,
systemMessageService: systemMessageService,
gorseClient: gorseClient,
postAIService: postAIService,
}
}
// 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
}
// 重新查询以获取关联的 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,
) {
// 未启用AI时直接通过审核并发送后续通知
if s.postAIService == nil || !s.postAIService.IsEnabled() {
if err := s.commentRepo.UpdateModerationStatus(commentID, model.CommentStatusPublished); err != nil {
log.Printf("[WARN] Failed to publish comment without AI moderation: %v", err)
return
}
s.afterCommentPublished(userID, postID, commentID, parentID, parentUserID, postOwnerID)
return
}
err := s.postAIService.ModerateComment(context.Background(), content, imageURLs)
if err != nil {
var rejectedErr *CommentModerationRejectedError
if errors.As(err, &rejectedErr) {
if delErr := s.commentRepo.Delete(commentID); delErr != nil {
log.Printf("[WARN] Failed to delete rejected comment %s: %v", commentID, delErr)
}
s.notifyCommentModerationRejected(userID, rejectedErr.Reason)
return
}
// 审核服务异常时降级放行避免评论长期pending
if updateErr := s.commentRepo.UpdateModerationStatus(commentID, model.CommentStatusPublished); updateErr != nil {
log.Printf("[WARN] Failed to publish comment %s after moderation error: %v", commentID, updateErr)
return
}
log.Printf("[WARN] Comment moderation failed, fallback publish comment=%s err=%v", commentID, err)
s.afterCommentPublished(userID, postID, commentID, parentID, parentUserID, postOwnerID)
return
}
if updateErr := s.commentRepo.UpdateModerationStatus(commentID, model.CommentStatusPublished); updateErr != nil {
log.Printf("[WARN] Failed to publish comment %s: %v", commentID, updateErr)
return
}
s.afterCommentPublished(userID, postID, commentID, parentID, parentUserID, postOwnerID)
}
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 {
log.Printf("[ERROR] Error sending reply notification: %v", notifyErr)
}
}
} else {
// 评论帖子,通知帖子作者
if postOwnerID != userID {
notifyErr := s.systemMessageService.SendCommentNotification(context.Background(), postOwnerID, userID, postID, commentID)
if notifyErr != nil {
log.Printf("[ERROR] Error sending comment notification: %v", notifyErr)
}
}
}
}()
}
// 推送评论行为到Gorse异步
go func() {
if s.gorseClient.IsEnabled() {
if err := s.gorseClient.InsertFeedback(context.Background(), gorse.FeedbackTypeComment, userID, postID); err != nil {
log.Printf("[WARN] Failed to insert comment feedback to Gorse: %v", err)
}
}
}()
}
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 {
log.Printf("[WARN] Failed to send comment moderation reject notification: %v", 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 {
return s.commentRepo.Delete(id)
}
// 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 {
log.Printf("[ERROR] Error sending like notification: %v", 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)
}