Files
backend/internal/service/audit_service.go
lafay 5d6c982c9c refactor: replace standard log with zap logger and optimize code patterns
- Migrate all log.Printf/log.Println calls to structured zap logging
- Use cmp.Or for cleaner default value handling
- Replace manual loops with slices.ContainsFunc/IndexFunc
- Add batch query methods (IsLikedBatch, IsFavoritedBatch) to solve N+1 problem
- Update JSON tags from omitempty to omitzero for numeric fields
- Use errgroup-style wg.Go for worker goroutines
- Remove duplicate casbin/v2 dependency (keeping v3)
- Add plans/ to gitignore
2026-03-17 00:47:17 +08:00

598 lines
14 KiB
Go

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
}