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:
254
backend/apps/core/ai_audit.py
Normal file
254
backend/apps/core/ai_audit.py
Normal 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
17
backend/apps/core/urls.py
Normal 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
124
backend/apps/core/views.py
Normal 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': [
|
||||||
|
'敏感词检测',
|
||||||
|
'广告检测',
|
||||||
|
'内容质量评估',
|
||||||
|
]
|
||||||
|
})
|
||||||
@@ -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')),
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user