From 492276fe4651e9059997b241f80009f0544ff537 Mon Sep 17 00:00:00 2001 From: maoshen Date: Tue, 14 Apr 2026 02:59:37 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20AI=20=E5=AE=A1?= =?UTF-8?q?=E6=A0=B8=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 apps/core/ai_audit.py AI 审核服务 - 新增 apps/core/views.py API 视图 - 新增 apps/core/urls.py URL 路由 - 更新 config/urls.py 注册 AI 审核 API - 支持文章/评论/服务的自动审核 - 包含敏感词检测、广告检测、内容质量评估 --- backend/apps/core/ai_audit.py | 254 ++++++++++++++++++++++++++++++++++ backend/apps/core/urls.py | 17 +++ backend/apps/core/views.py | 124 +++++++++++++++++ backend/config/urls.py | 1 + frontend/nginx.conf | 30 ++-- 5 files changed, 404 insertions(+), 22 deletions(-) create mode 100644 backend/apps/core/ai_audit.py create mode 100644 backend/apps/core/urls.py create mode 100644 backend/apps/core/views.py diff --git a/backend/apps/core/ai_audit.py b/backend/apps/core/ai_audit.py new file mode 100644 index 0000000..70a52c3 --- /dev/null +++ b/backend/apps/core/ai_audit.py @@ -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() diff --git a/backend/apps/core/urls.py b/backend/apps/core/urls.py new file mode 100644 index 0000000..832860c --- /dev/null +++ b/backend/apps/core/urls.py @@ -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'), +] diff --git a/backend/apps/core/views.py b/backend/apps/core/views.py new file mode 100644 index 0000000..53bc401 --- /dev/null +++ b/backend/apps/core/views.py @@ -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': [ + '敏感词检测', + '广告检测', + '内容质量评估', + ] + }) diff --git a/backend/config/urls.py b/backend/config/urls.py index 0490088..40b85b6 100644 --- a/backend/config/urls.py +++ b/backend/config/urls.py @@ -27,6 +27,7 @@ urlpatterns = [ path('api/', include('apps.moderation.urls')), path('api/', include('apps.interactions.urls')), path('api/', include('apps.api.urls')), + path('api/', include('apps.core.urls')), # AI 审核 API # GraphQL path('graphql/', include('apps.api.graphql_urls')), diff --git a/frontend/nginx.conf b/frontend/nginx.conf index d3ca1bf..583aa79 100644 --- a/frontend/nginx.conf +++ b/frontend/nginx.conf @@ -1,42 +1,28 @@ server { listen 80; server_name localhost; - root /usr/share/nginx/html; index index.html index.htm; - location / { try_files $uri $uri/ /index.html; } - - location /api { + location /static/ { + alias /usr/share/nginx/html/static/; + expires 30d; + } + location /api/ { proxy_pass http://backend:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } - - location /graphql { + location /graphql/ { proxy_pass http://backend:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } - - location /media { + location /media/ { 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; -} \ No newline at end of file +}