2026-03-09 21:28:58 +08:00
|
|
|
|
package service
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"errors"
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"log"
|
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
2026-03-10 12:58:23 +08:00
|
|
|
|
"carrot_bbs/internal/cache"
|
2026-03-09 21:28:58 +08:00
|
|
|
|
"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
|
2026-03-10 12:58:23 +08:00
|
|
|
|
cache cache.Cache
|
2026-03-09 21:28:58 +08:00
|
|
|
|
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,
|
2026-03-10 12:58:23 +08:00
|
|
|
|
cache: cache.GetCache(),
|
2026-03-09 21:28:58 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-03-10 12:58:23 +08:00
|
|
|
|
if err := s.applyCommentPublishedStats(commentID); err != nil {
|
|
|
|
|
|
log.Printf("[WARN] Failed to apply published stats for comment %s: %v", commentID, err)
|
|
|
|
|
|
}
|
|
|
|
|
|
s.invalidatePostCaches(postID)
|
2026-03-09 21:28:58 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-03-10 12:58:23 +08:00
|
|
|
|
if statsErr := s.applyCommentPublishedStats(commentID); statsErr != nil {
|
|
|
|
|
|
log.Printf("[WARN] Failed to apply published stats for comment %s: %v", commentID, statsErr)
|
|
|
|
|
|
}
|
|
|
|
|
|
s.invalidatePostCaches(postID)
|
2026-03-09 21:28:58 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-03-10 12:58:23 +08:00
|
|
|
|
if statsErr := s.applyCommentPublishedStats(commentID); statsErr != nil {
|
|
|
|
|
|
log.Printf("[WARN] Failed to apply published stats for comment %s: %v", commentID, statsErr)
|
|
|
|
|
|
}
|
|
|
|
|
|
s.invalidatePostCaches(postID)
|
2026-03-09 21:28:58 +08:00
|
|
|
|
s.afterCommentPublished(userID, postID, commentID, parentID, parentUserID, postOwnerID)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 12:58:23 +08:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 21:28:58 +08:00
|
|
|
|
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 {
|
2026-03-09 22:18:53 +08:00
|
|
|
|
log.Printf("[ERROR] Error sending reply notification: %v", notifyErr)
|
2026-03-09 21:28:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 评论帖子,通知帖子作者
|
|
|
|
|
|
if postOwnerID != userID {
|
|
|
|
|
|
notifyErr := s.systemMessageService.SendCommentNotification(context.Background(), postOwnerID, userID, postID, commentID)
|
|
|
|
|
|
if notifyErr != nil {
|
2026-03-09 22:18:53 +08:00
|
|
|
|
log.Printf("[ERROR] Error sending comment notification: %v", notifyErr)
|
2026-03-09 21:28:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 推送评论行为到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 {
|
2026-03-10 12:58:23 +08:00
|
|
|
|
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
|
2026-03-09 21:28:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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 {
|
2026-03-09 22:18:53 +08:00
|
|
|
|
log.Printf("[ERROR] Error sending like notification: %v", notifyErr)
|
2026-03-09 21:28:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
}()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
}
|