feat: add hook system, QR code login, and layered cache
- Implement extensible hook system for content moderation with builtin and AI-powered moderation hooks - Add QR code login feature with SSE for real-time status updates and scan/confirm/cancel workflow - Introduce layered cache with local LRU + Redis backend for improved read performance - Refactor post and comment services to use hook-based moderation instead of direct AI service calls - Add local cache configuration options (size, buckets, ttl)
This commit is contained in:
@@ -2,19 +2,18 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"carrot_bbs/internal/cache"
|
||||
"carrot_bbs/internal/model"
|
||||
"carrot_bbs/internal/pkg/gorse"
|
||||
"carrot_bbs/internal/pkg/hook"
|
||||
"carrot_bbs/internal/repository"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// CommentService 评论服务
|
||||
type CommentService struct {
|
||||
commentRepo *repository.CommentRepository
|
||||
postRepo *repository.PostRepository
|
||||
@@ -23,10 +22,10 @@ type CommentService struct {
|
||||
gorseClient gorse.Client
|
||||
postAIService *PostAIService
|
||||
logService *LogService
|
||||
hookManager *hook.Manager
|
||||
}
|
||||
|
||||
// NewCommentService 创建评论服务
|
||||
func NewCommentService(commentRepo *repository.CommentRepository, postRepo *repository.PostRepository, systemMessageService SystemMessageService, gorseClient gorse.Client, postAIService *PostAIService, cacheBackend cache.Cache) *CommentService {
|
||||
func NewCommentService(commentRepo *repository.CommentRepository, postRepo *repository.PostRepository, systemMessageService SystemMessageService, gorseClient gorse.Client, postAIService *PostAIService, cacheBackend cache.Cache, hookManager *hook.Manager) *CommentService {
|
||||
return &CommentService{
|
||||
commentRepo: commentRepo,
|
||||
postRepo: postRepo,
|
||||
@@ -35,6 +34,7 @@ func NewCommentService(commentRepo *repository.CommentRepository, postRepo *repo
|
||||
gorseClient: gorseClient,
|
||||
postAIService: postAIService,
|
||||
logService: nil,
|
||||
hookManager: hookManager,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,10 +113,9 @@ func (s *CommentService) reviewCommentAsync(
|
||||
parentUserID string,
|
||||
postOwnerID string,
|
||||
) {
|
||||
// 未启用AI时,直接通过审核并发送后续通知
|
||||
if s.postAIService == nil || !s.postAIService.IsEnabled() {
|
||||
if s.hookManager == nil {
|
||||
if err := s.commentRepo.UpdateModerationStatus(commentID, model.CommentStatusPublished); err != nil {
|
||||
zap.L().Warn("Failed to publish comment without AI moderation",
|
||||
zap.L().Warn("Failed to publish comment without hook manager",
|
||||
zap.Error(err),
|
||||
)
|
||||
return
|
||||
@@ -132,40 +131,38 @@ func (s *CommentService) reviewCommentAsync(
|
||||
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 {
|
||||
zap.L().Warn("Failed to delete rejected comment",
|
||||
zap.String("commentID", commentID),
|
||||
zap.Error(delErr),
|
||||
)
|
||||
}
|
||||
s.notifyCommentModerationRejected(userID, rejectedErr.Reason)
|
||||
return
|
||||
}
|
||||
var authorID uint
|
||||
fmt.Sscanf(userID, "%d", &authorID)
|
||||
|
||||
// 审核服务异常时降级放行,避免评论长期pending
|
||||
if updateErr := s.commentRepo.UpdateModerationStatus(commentID, model.CommentStatusPublished); updateErr != nil {
|
||||
zap.L().Warn("Failed to publish comment after moderation error",
|
||||
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(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),
|
||||
zap.Error(delErr),
|
||||
)
|
||||
}
|
||||
s.invalidatePostCaches(postID)
|
||||
zap.L().Warn("Comment moderation failed, fallback publish comment",
|
||||
zap.String("commentID", commentID),
|
||||
zap.Error(err),
|
||||
)
|
||||
s.afterCommentPublished(userID, postID, commentID, parentID, parentUserID, postOwnerID)
|
||||
s.notifyCommentModerationRejected(userID, result.RejectReason)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user