Files
backend/internal/service/comment_service.go
lafay 2f584c1f2c
All checks were successful
Build Backend / build (push) Successful in 5m10s
Build Backend / build-docker (push) Successful in 2m22s
This is a breaking change that renames the entire project from "Carrot BBS" to "WithYou".
The Go module name has been changed from `carrot_bbs` to `with_you`, which requires
all import paths to be updated throughout the codebase and by external consumers.

BREAKING CHANGE: Module name changed from carrot_bbs to with_you. All imports
and dependencies must be updated from carrot_bbs/* to with_you/*.
2026-04-22 16:01:59 +08:00

355 lines
10 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"
"fmt"
"strings"
"with_you/internal/cache"
"with_you/internal/model"
"with_you/internal/pkg/cursor"
"with_you/internal/pkg/hook"
"with_you/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, "", "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),
)
}
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]any{
"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 result.NeedsReview {
s.invalidatePostCaches(postID)
return
}
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, "", 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),
)
}
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)
}