feat: 添加 AI 审核模块

- 新增 apps/core/ai_audit.py AI 审核服务
- 新增 apps/core/views.py API 视图
- 新增 apps/core/urls.py URL 路由
- 更新 config/urls.py 注册 AI 审核 API
- 支持文章/评论/服务的自动审核
- 包含敏感词检测、广告检测、内容质量评估
This commit is contained in:
maoshen
2026-04-14 02:59:37 +00:00
parent 08f2315567
commit 492276fe46
5 changed files with 404 additions and 22 deletions

View File

@@ -0,0 +1,254 @@
"""
AI 审核模块 - 自动审核内容
提供敏感词检测、内容质量评估等功能
"""
import re
from typing import Dict, List, Tuple
class AIAuditService:
"""AI 审核服务类"""
# 敏感词库(示例,实际应该从数据库或配置文件加载)
SENSITIVE_WORDS = [
'暴力', '恐怖', '色情', '赌博', '毒品',
'诈骗', '传销', '假币', '枪支', '弹药',
]
# 广告关键词
AD_KEYWORDS = [
'加微信', 'QQ 群', '联系电话', '手机号',
'www.', '.com', '.cn', 'http',
]
# 最小内容长度
MIN_CONTENT_LENGTH = 10
@classmethod
def check_sensitive_words(cls, text: str) -> Tuple[bool, List[str]]:
"""
检查敏感词
Args:
text: 待检查文本
Returns:
(是否包含敏感词,敏感词列表)
"""
found_words = []
for word in cls.SENSITIVE_WORDS:
if word in text:
found_words.append(word)
return len(found_words) > 0, found_words
@classmethod
def check_advertisement(cls, text: str) -> Tuple[bool, List[str]]:
"""
检查广告内容
Args:
text: 待检查文本
Returns:
(是否包含广告,广告关键词列表)
"""
found_keywords = []
for keyword in cls.AD_KEYWORDS:
if keyword in text:
found_keywords.append(keyword)
return len(found_keywords) > 0, found_keywords
@classmethod
def check_content_quality(cls, text: str) -> Dict:
"""
检查内容质量
Args:
text: 待检查文本
Returns:
质量评估结果
"""
result = {
'is_valid': True,
'issues': [],
'score': 100,
}
# 检查长度
if len(text) < cls.MIN_CONTENT_LENGTH:
result['is_valid'] = False
result['issues'].append(f'内容太短,最少需要{cls.MIN_CONTENT_LENGTH}个字符')
result['score'] -= 50
# 检查重复字符(刷屏检测)
if len(set(text)) < len(text) * 0.3:
result['is_valid'] = False
result['issues'].append('内容包含大量重复字符')
result['score'] -= 30
# 检查全角字符比例
chinese_chars = len(re.findall(r'[\u4e00-\u9fa5]', text))
if chinese_chars / max(len(text), 1) < 0.1:
result['issues'].append('中文内容比例较低')
result['score'] -= 10
return result
@classmethod
def audit_article(cls, title: str, content: str) -> Dict:
"""
审核文章
Args:
title: 文章标题
content: 文章内容
Returns:
审核结果
"""
result = {
'approved': True,
'reason': '',
'details': {},
}
# 检查标题
sensitive, words = cls.check_sensitive_words(title)
if sensitive:
result['approved'] = False
result['reason'] = f'标题包含敏感词:{", ".join(words)}'
result['details']['sensitive_words'] = words
return result
# 检查内容
sensitive, words = cls.check_sensitive_words(content)
if sensitive:
result['approved'] = False
result['reason'] = f'内容包含敏感词:{", ".join(words)}'
result['details']['sensitive_words'] = words
return result
# 检查广告
is_ad, keywords = cls.check_advertisement(content)
if is_ad:
result['approved'] = False
result['reason'] = f'内容疑似广告:{", ".join(keywords)}'
result['details']['ad_keywords'] = keywords
return result
# 检查内容质量
quality = cls.check_content_quality(content)
if not quality['is_valid']:
result['approved'] = False
result['reason'] = f'内容质量不达标:{", ".join(quality["issues"])}'
result['details']['quality'] = quality
return result
result['reason'] = '审核通过'
result['details']['quality_score'] = quality['score']
return result
@classmethod
def audit_comment(cls, content: str) -> Dict:
"""
审核评论
Args:
content: 评论内容
Returns:
审核结果
"""
result = {
'approved': True,
'reason': '',
'details': {},
}
# 检查敏感词
sensitive, words = cls.check_sensitive_words(content)
if sensitive:
result['approved'] = False
result['reason'] = f'包含敏感词:{", ".join(words)}'
result['details']['sensitive_words'] = words
return result
# 检查广告
is_ad, keywords = cls.check_advertisement(content)
if is_ad:
result['approved'] = False
result['reason'] = f'疑似广告:{", ".join(keywords)}'
result['details']['ad_keywords'] = keywords
return result
# 检查内容质量
quality = cls.check_content_quality(content)
if not quality['is_valid']:
result['approved'] = False
result['reason'] = f'内容质量不达标:{", ".join(quality["issues"])}'
result['details']['quality'] = quality
return result
result['reason'] = '审核通过'
return result
@classmethod
def audit_service(cls, name: str, description: str) -> Dict:
"""
审核特色服务
Args:
name: 服务名称
description: 服务描述
Returns:
审核结果
"""
# 合并名称和描述进行检查
full_text = f"{name} {description}"
result = {
'approved': True,
'reason': '',
'details': {},
}
# 检查敏感词
sensitive, words = cls.check_sensitive_words(full_text)
if sensitive:
result['approved'] = False
result['reason'] = f'包含敏感词:{", ".join(words)}'
result['details']['sensitive_words'] = words
return result
# 检查广告(服务本身可以包含联系方式,这里放宽检查)
# 只检查明显的垃圾广告
spam_keywords = ['加微信', 'QQ 群', '点击链接']
found_spam = [kw for kw in spam_keywords if kw in full_text]
if found_spam:
result['approved'] = False
result['reason'] = f'包含垃圾广告内容:{", ".join(found_spam)}'
result['details']['spam_keywords'] = found_spam
return result
# 检查内容质量
quality = cls.check_content_quality(description)
if not quality['is_valid']:
result['approved'] = False
result['reason'] = f'描述质量不达标:{", ".join(quality["issues"])}'
result['details']['quality'] = quality
return result
result['reason'] = '审核通过'
return result
# 单例实例
ai_audit_service = AIAuditService()

17
backend/apps/core/urls.py Normal file
View File

@@ -0,0 +1,17 @@
"""
AI 审核 API URL 配置
"""
from django.urls import path
from .views import (
audit_article,
audit_comment,
audit_service,
audit_status,
)
urlpatterns = [
path('audit/article/', audit_article, name='audit-article'),
path('audit/comment/', audit_comment, name='audit-comment'),
path('audit/service/', audit_service, name='audit-service'),
path('audit/status/', audit_status, name='audit-status'),
]

124
backend/apps/core/views.py Normal file
View File

@@ -0,0 +1,124 @@
"""
AI 审核 API 视图
"""
from rest_framework import viewsets, permissions, status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated, IsAdminUser
from .ai_audit import AIAuditService
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def audit_article(request):
"""
审核文章
请求体:
{
"title": "文章标题",
"content": "文章内容"
}
返回:
{
"approved": true/false,
"reason": "审核结果说明",
"details": {...}
}
"""
title = request.data.get('title', '')
content = request.data.get('content', '')
if not title or not content:
return Response(
{'error': '标题和内容不能为空'},
status=status.HTTP_400_BAD_REQUEST
)
result = AIAuditService.audit_article(title, content)
return Response(result)
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def audit_comment(request):
"""
审核评论
请求体:
{
"content": "评论内容"
}
返回:
{
"approved": true/false,
"reason": "审核结果说明",
"details": {...}
}
"""
content = request.data.get('content', '')
if not content:
return Response(
{'error': '评论内容不能为空'},
status=status.HTTP_400_BAD_REQUEST
)
result = AIAuditService.audit_comment(content)
return Response(result)
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def audit_service(request):
"""
审核特色服务
请求体:
{
"name": "服务名称",
"description": "服务描述"
}
返回:
{
"approved": true/false,
"reason": "审核结果说明",
"details": {...}
}
"""
name = request.data.get('name', '')
description = request.data.get('description', '')
if not name or not description:
return Response(
{'error': '服务名称和描述不能为空'},
status=status.HTTP_400_BAD_REQUEST
)
result = AIAuditService.audit_service(name, description)
return Response(result)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def audit_status(request):
"""
获取 AI 审核服务状态
"""
return Response({
'status': 'active',
'service': 'AI Audit Service',
'version': '1.0.0',
'features': [
'敏感词检测',
'广告检测',
'内容质量评估',
]
})

View File

@@ -27,6 +27,7 @@ urlpatterns = [
path('api/', include('apps.moderation.urls')), path('api/', include('apps.moderation.urls')),
path('api/', include('apps.interactions.urls')), path('api/', include('apps.interactions.urls')),
path('api/', include('apps.api.urls')), path('api/', include('apps.api.urls')),
path('api/', include('apps.core.urls')), # AI 审核 API
# GraphQL # GraphQL
path('graphql/', include('apps.api.graphql_urls')), path('graphql/', include('apps.api.graphql_urls')),

View File

@@ -1,42 +1,28 @@
server { server {
listen 80; listen 80;
server_name localhost; server_name localhost;
root /usr/share/nginx/html; root /usr/share/nginx/html;
index index.html index.htm; index index.html index.htm;
location / { location / {
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html;
} }
location /static/ {
location /api { alias /usr/share/nginx/html/static/;
expires 30d;
}
location /api/ {
proxy_pass http://backend:8000; proxy_pass http://backend:8000;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
} }
location /graphql/ {
location /graphql {
proxy_pass http://backend:8000; proxy_pass http://backend:8000;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
} }
location /media/ {
location /media {
proxy_pass http://backend:8000; proxy_pass http://backend:8000;
} }
location /static {
# Try local static files first, then proxy to backend
try_files $uri $uri/ @backend_static;
}
location @backend_static {
proxy_pass http://backend:8000;
}
gzip on;
gzip_comp_level 5;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
} }