package service import ( "context" "encoding/json" "fmt" "strings" "sync" "time" "carrot_bbs/internal/model" "carrot_bbs/internal/pkg/openai" "go.uber.org/zap" "gorm.io/gorm" ) // ==================== 内容审核服务接口和实现 ==================== // AuditResult 审核结果 type AuditResult struct { Pass bool `json:"pass"` // 是否通过 Risk string `json:"risk"` // 风险等级: low, medium, high Labels []string `json:"labels"` // 标签列表 Suggest string `json:"suggest"` // 建议: pass, review, block Detail string `json:"detail"` // 详细说明 Provider string `json:"provider"` // 服务提供商 } // AuditService 内容审核服务接口 type AuditService interface { // AuditText 审核文本 AuditText(ctx context.Context, text string, auditType string) (*AuditResult, error) // AuditImage 审核图片 AuditImage(ctx context.Context, imageURL string) (*AuditResult, error) // AuditPost 审核帖子 AuditPost(ctx context.Context, title, content string, images []string) (*AuditResult, error) // AuditComment 审核评论 AuditComment(ctx context.Context, content string, images []string) (*AuditResult, error) // GetAuditResult 获取审核结果 GetAuditResult(ctx context.Context, auditID string) (*AuditResult, error) // IsEnabled 检查审核服务是否启用 IsEnabled() bool } // auditServiceImpl 内容审核服务实现 type auditServiceImpl struct { db *gorm.DB openaiClient openai.Client config *AuditConfig mu sync.RWMutex } // AuditConfig 内容审核服务配置 type AuditConfig struct { Enabled bool `mapstructure:"enabled" yaml:"enabled"` StrictModeration bool `mapstructure:"strict_moderation" yaml:"strict_moderation"` // 严格模式:审核失败时拒绝;非严格模式:审核失败时放行 Timeout int `mapstructure:"timeout" yaml:"timeout"` } // NewAuditService 创建内容审核服务 func NewAuditService(db *gorm.DB, openaiClient openai.Client, config *AuditConfig) AuditService { return &auditServiceImpl{ db: db, openaiClient: openaiClient, config: config, } } // IsEnabled 检查审核服务是否启用 func (s *auditServiceImpl) IsEnabled() bool { return s != nil && s.config != nil && s.config.Enabled && s.openaiClient != nil && s.openaiClient.IsEnabled() } // AuditText 审核文本 func (s *auditServiceImpl) AuditText(ctx context.Context, text string, auditType string) (*AuditResult, error) { if !s.IsEnabled() { return &AuditResult{ Pass: true, Risk: "low", Suggest: "pass", Detail: "Audit service disabled", Provider: "ai", }, nil } if text == "" { return &AuditResult{ Pass: true, Risk: "low", Suggest: "pass", Detail: "Empty text", Provider: "ai", }, nil } // 使用 AI 审核文本 approved, reason, err := s.openaiClient.ModerateComment(ctx, text, nil) if err != nil { zap.L().Error("AI audit text error", zap.Error(err), ) if s.config.StrictModeration { return &AuditResult{ Pass: false, Risk: "high", Suggest: "review", Detail: fmt.Sprintf("Audit error: %v", err), Provider: "ai", }, err } // 非严格模式下,审核失败时放行 return &AuditResult{ Pass: true, Risk: "low", Suggest: "pass", Detail: fmt.Sprintf("Audit fallback pass due to error: %v", err), Provider: "ai", }, nil } result := &AuditResult{ Pass: approved, Provider: "ai", Detail: reason, } if approved { result.Risk = "low" result.Suggest = "pass" } else { result.Risk = "high" result.Suggest = "block" result.Labels = []string{"ai_rejected"} } // 记录审核日志 go s.saveAuditLog(ctx, "text", text, "", auditType, result) return result, nil } // AuditImage 审核图片 func (s *auditServiceImpl) AuditImage(ctx context.Context, imageURL string) (*AuditResult, error) { if !s.IsEnabled() { return &AuditResult{ Pass: true, Risk: "low", Suggest: "pass", Detail: "Audit service disabled", Provider: "ai", }, nil } if imageURL == "" { return &AuditResult{ Pass: true, Risk: "low", Suggest: "pass", Detail: "Empty image URL", Provider: "ai", }, nil } // 使用 AI 审核图片 approved, reason, err := s.openaiClient.ModerateComment(ctx, "", []string{imageURL}) if err != nil { zap.L().Error("AI audit image error", zap.Error(err), ) if s.config.StrictModeration { return &AuditResult{ Pass: false, Risk: "high", Suggest: "review", Detail: fmt.Sprintf("Audit error: %v", err), Provider: "ai", }, err } // 非严格模式下,审核失败时放行 return &AuditResult{ Pass: true, Risk: "low", Suggest: "pass", Detail: fmt.Sprintf("Audit fallback pass due to error: %v", err), Provider: "ai", }, nil } result := &AuditResult{ Pass: approved, Provider: "ai", Detail: reason, } if approved { result.Risk = "low" result.Suggest = "pass" } else { result.Risk = "high" result.Suggest = "block" result.Labels = []string{"ai_rejected"} } // 记录审核日志 go s.saveAuditLog(ctx, "image", "", imageURL, "image", result) return result, nil } // AuditPost 审核帖子 func (s *auditServiceImpl) AuditPost(ctx context.Context, title, content string, images []string) (*AuditResult, error) { if !s.IsEnabled() { return &AuditResult{ Pass: true, Risk: "low", Suggest: "pass", Detail: "Audit service disabled", Provider: "ai", }, nil } // 使用 AI 审核帖子 approved, reason, err := s.openaiClient.ModeratePost(ctx, title, content, images) if err != nil { zap.L().Error("AI audit post error", zap.Error(err), ) if s.config.StrictModeration { return &AuditResult{ Pass: false, Risk: "high", Suggest: "review", Detail: fmt.Sprintf("Audit error: %v", err), Provider: "ai", }, err } // 非严格模式下,审核失败时放行 return &AuditResult{ Pass: true, Risk: "low", Suggest: "pass", Detail: fmt.Sprintf("Audit fallback pass due to error: %v", err), Provider: "ai", }, nil } result := &AuditResult{ Pass: approved, Provider: "ai", Detail: reason, } if approved { result.Risk = "low" result.Suggest = "pass" } else { result.Risk = "high" result.Suggest = "block" result.Labels = []string{"ai_rejected"} } // 记录审核日志 contentSummary := fmt.Sprintf("标题: %s, 内容: %s", title, content) if len(content) > 200 { contentSummary = fmt.Sprintf("标题: %s, 内容: %s...", title, content[:200]) } go s.saveAuditLog(ctx, "post", contentSummary, strings.Join(images, ","), "post", result) return result, nil } // AuditComment 审核评论 func (s *auditServiceImpl) AuditComment(ctx context.Context, content string, images []string) (*AuditResult, error) { if !s.IsEnabled() { return &AuditResult{ Pass: true, Risk: "low", Suggest: "pass", Detail: "Audit service disabled", Provider: "ai", }, nil } // 使用 AI 审核评论 approved, reason, err := s.openaiClient.ModerateComment(ctx, content, images) if err != nil { zap.L().Error("AI audit comment error", zap.Error(err), ) if s.config.StrictModeration { return &AuditResult{ Pass: false, Risk: "high", Suggest: "review", Detail: fmt.Sprintf("Audit error: %v", err), Provider: "ai", }, err } // 非严格模式下,审核失败时放行 return &AuditResult{ Pass: true, Risk: "low", Suggest: "pass", Detail: fmt.Sprintf("Audit fallback pass due to error: %v", err), Provider: "ai", }, nil } result := &AuditResult{ Pass: approved, Provider: "ai", Detail: reason, } if approved { result.Risk = "low" result.Suggest = "pass" } else { result.Risk = "high" result.Suggest = "block" result.Labels = []string{"ai_rejected"} } // 记录审核日志 contentSummary := content if len(content) > 200 { contentSummary = content[:200] + "..." } go s.saveAuditLog(ctx, "comment", contentSummary, strings.Join(images, ","), "comment", result) return result, nil } // GetAuditResult 获取审核结果 func (s *auditServiceImpl) GetAuditResult(ctx context.Context, auditID string) (*AuditResult, error) { if s.db == nil || auditID == "" { return nil, fmt.Errorf("invalid audit ID") } var auditLog model.AuditLog if err := s.db.Where("id = ?", auditID).First(&auditLog).Error; err != nil { return nil, err } result := &AuditResult{ Pass: auditLog.Result == model.AuditResultPass, Risk: string(auditLog.RiskLevel), Suggest: auditLog.Suggestion, Detail: auditLog.Detail, } // 解析标签 if auditLog.Labels != "" { json.Unmarshal([]byte(auditLog.Labels), &result.Labels) } return result, nil } // saveAuditLog 保存审核日志 func (s *auditServiceImpl) saveAuditLog(ctx context.Context, contentType, content, imageURL, auditType string, result *AuditResult) { if s.db == nil { return } labelsJSON := "" if len(result.Labels) > 0 { if data, err := json.Marshal(result.Labels); err == nil { labelsJSON = string(data) } } auditLog := model.AuditLog{ ContentType: contentType, Content: content, ContentURL: imageURL, AuditType: auditType, Labels: labelsJSON, Suggestion: result.Suggest, Detail: result.Detail, Source: model.AuditSourceAuto, Status: "completed", } if result.Pass { auditLog.Result = model.AuditResultPass } else if result.Suggest == "review" { auditLog.Result = model.AuditResultReview } else { auditLog.Result = model.AuditResultBlock } switch result.Risk { case "low": auditLog.RiskLevel = model.AuditRiskLevelLow case "medium": auditLog.RiskLevel = model.AuditRiskLevelMedium case "high": auditLog.RiskLevel = model.AuditRiskLevelHigh default: auditLog.RiskLevel = model.AuditRiskLevelLow } if err := s.db.Create(&auditLog).Error; err != nil { zap.L().Error("Failed to save audit log", zap.Error(err), ) } } // ==================== 审核结果回调处理 ==================== // AuditCallback 审核回调处理 type AuditCallback struct { service AuditService } // NewAuditCallback 创建审核回调处理 func NewAuditCallback(service AuditService) *AuditCallback { return &AuditCallback{ service: service, } } // HandleTextCallback 处理文本审核回调 func (c *AuditCallback) HandleTextCallback(ctx context.Context, auditID string, result *AuditResult) error { if c.service == nil || auditID == "" || result == nil { return fmt.Errorf("invalid parameters") } zap.L().Debug("Processing text audit callback", zap.String("auditID", auditID), zap.Bool("pass", result.Pass), zap.String("risk", result.Risk), ) // 根据审核结果执行相应操作 // 例如: 更新帖子状态、发送通知等 return nil } // HandleImageCallback 处理图片审核回调 func (c *AuditCallback) HandleImageCallback(ctx context.Context, auditID string, result *AuditResult) error { if c.service == nil || auditID == "" || result == nil { return fmt.Errorf("invalid parameters") } zap.L().Debug("Processing image audit callback", zap.String("auditID", auditID), zap.Bool("pass", result.Pass), zap.String("risk", result.Risk), ) // 根据审核结果执行相应操作 // 例如: 更新图片状态、删除违规图片等 return nil } // ==================== 辅助函数 ==================== // IsContentSafe 判断内容是否安全 func IsContentSafe(result *AuditResult) bool { if result == nil { return true } return result.Pass && result.Suggest != "block" } // NeedReview 判断内容是否需要人工复审 func NeedReview(result *AuditResult) bool { if result == nil { return false } return result.Suggest == "review" } // GetRiskLevel 获取风险等级 func GetRiskLevel(result *AuditResult) string { if result == nil { return "low" } return result.Risk } // FormatAuditResult 格式化审核结果为字符串 func FormatAuditResult(result *AuditResult) string { if result == nil { return "{}" } data, _ := json.Marshal(result) return string(data) } // ParseAuditResult 从字符串解析审核结果 func ParseAuditResult(data string) (*AuditResult, error) { if data == "" { return nil, fmt.Errorf("empty data") } var result AuditResult if err := json.Unmarshal([]byte(data), &result); err != nil { return nil, err } return &result, nil } // ==================== 审核日志查询 ==================== // GetAuditLogs 获取审核日志列表 func GetAuditLogs(db *gorm.DB, targetType string, targetID string, result string, page, pageSize int) ([]model.AuditLog, int64, error) { query := db.Model(&model.AuditLog{}) if targetType != "" { query = query.Where("target_type = ?", targetType) } if targetID != "" { query = query.Where("target_id = ?", targetID) } if result != "" { query = query.Where("result = ?", result) } var total int64 if err := query.Count(&total).Error; err != nil { return nil, 0, err } var logs []model.AuditLog offset := (page - 1) * pageSize if err := query.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&logs).Error; err != nil { return nil, 0, err } return logs, total, nil } // ==================== 定时任务 ==================== // AuditScheduler 审核调度器 type AuditScheduler struct { db *gorm.DB service AuditService interval time.Duration stopCh chan bool } // NewAuditScheduler 创建审核调度器 func NewAuditScheduler(db *gorm.DB, service AuditService, interval time.Duration) *AuditScheduler { return &AuditScheduler{ db: db, service: service, interval: interval, stopCh: make(chan bool), } } // Start 启动调度器 func (s *AuditScheduler) Start() { go func() { ticker := time.NewTicker(s.interval) defer ticker.Stop() for { select { case <-ticker.C: s.processPendingAudits() case <-s.stopCh: return } } }() } // Stop 停止调度器 func (s *AuditScheduler) Stop() { s.stopCh <- true } // processPendingAudits 处理待审核内容 func (s *AuditScheduler) processPendingAudits() { // 查询待审核的内容 // 1. 查询审核状态为 pending 的记录 // 2. 调用审核服务 // 3. 更新审核状态 // 示例逻辑,实际需要根据业务需求实现 zap.L().Debug("Processing pending audits") } // CleanupOldLogs 清理旧的审核日志 func CleanupOldLogs(db *gorm.DB, days int) error { // 清理指定天数之前的审核日志 cutoffTime := time.Now().AddDate(0, 0, -days) return db.Where("created_at < ? AND result = ?", cutoffTime, model.AuditResultPass).Delete(&model.AuditLog{}).Error }