Files
chunyu_project/search/views.py
T
2026-08-05 23:59:15 +08:00

240 lines
8.8 KiB
Python

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import AllowAny
from django.db.models import Q
from django.core.paginator import Paginator
from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi
from chunyu_project.common_schemas import success_response, error_response, unauthorized_response, not_found_response
from article.models import Article
from tool.models import Tool
from learn.models import Course
from apidirectory.models import ApiItem
from utils.response_codes import ResponseCode, create_standardized_response
SORT_MAPPING = {
'1': 'relevance',
'2': 'views',
'3': 'newest',
}
def normalize_sort(sort_value):
return SORT_MAPPING.get(sort_value, sort_value)
class GlobalSearchView(APIView):
permission_classes = [AllowAny]
@swagger_auto_schema(
tags=['搜索'],
operation_summary='全局搜索',
operation_description='根据关键词搜索文章、工具、课程和API,支持分页和排序',
manual_parameters=[
openapi.Parameter('q', openapi.IN_QUERY, description='搜索关键词', type=openapi.TYPE_STRING, required=True),
openapi.Parameter('type', openapi.IN_QUERY, description='内容类型筛选: all/article/tool/course/api', type=openapi.TYPE_STRING),
openapi.Parameter('page', openapi.IN_QUERY, description='页码', type=openapi.TYPE_INTEGER),
openapi.Parameter('page_size', openapi.IN_QUERY, description='每页数量', type=openapi.TYPE_INTEGER),
openapi.Parameter('sort', openapi.IN_QUERY, description='排序方式: relevance/views/newest (或数字 1/2/3)', type=openapi.TYPE_STRING),
],
responses={200: success_response}
)
def get(self, request):
q = request.query_params.get('q', '').strip()
content_type = request.query_params.get('type', 'all')
page = int(request.query_params.get('page', 1))
page_size = int(request.query_params.get('page_size', 12))
sort = normalize_sort(request.query_params.get('sort', 'relevance'))
if not q:
return create_standardized_response(data={
'results': [],
'total': 0,
'page': page,
'page_size': page_size,
}, code=ResponseCode.SUCCESS)
results = []
if content_type == 'all' or content_type == 'article':
articles = Article.objects.filter(
Q(title__icontains=q) | Q(excerpt__icontains=q) | Q(content__icontains=q)
).filter(status='published').order_by('-created_at')[:100]
for article in articles:
cover_url = ''
if article.cover and hasattr(article.cover, 'url'):
cover_url = request.build_absolute_uri(article.cover.url)
excerpt = article.excerpt or ''
if not excerpt and article.content:
excerpt = article.content[:120] + '...' if len(article.content) > 120 else article.content
results.append({
'id': article.id,
'type': '文章',
'title': article.title,
'desc': excerpt,
'cover': cover_url,
'views': article.views or 0,
'likes': article.likes or 0,
'url': f'/articles/{article.id}',
})
if content_type == 'all' or content_type == 'tool':
tools = Tool.objects.filter(
Q(name__icontains=q) | Q(description__icontains=q)
).order_by('-created_at')[:100]
for tool in tools:
cover_url = ''
if tool.icon and hasattr(tool.icon, 'url'):
cover_url = request.build_absolute_uri(tool.icon.url)
results.append({
'id': tool.id,
'type': '工具',
'title': tool.name,
'desc': tool.description or '',
'cover': cover_url,
'views': tool.views or 0,
'likes': tool.likes or 0,
'url': f'/use?tool={tool.id}',
})
if content_type == 'all' or content_type == 'course':
courses = Course.objects.filter(
Q(title__icontains=q) | Q(description__icontains=q)
).order_by('-created_at')[:100]
for course in courses:
cover_url = ''
if course.cover and hasattr(course.cover, 'url'):
cover_url = request.build_absolute_uri(course.cover.url)
results.append({
'id': course.id,
'type': '课程',
'title': course.title,
'desc': course.description or '',
'cover': cover_url,
'views': course.views or 0,
'likes': course.likes or 0,
'url': f'/courses/{course.id}',
})
if content_type == 'all' or content_type == 'api':
apis = ApiItem.objects.filter(
Q(name__icontains=q) | Q(description__icontains=q) | Q(url_path__icontains=q)
).filter(is_enabled=True).order_by('-created_at')[:100]
for api in apis:
desc = api.description or f'{api.method} {api.url_path}'
results.append({
'id': api.id,
'type': 'API',
'title': api.name,
'desc': desc,
'cover': api.image_url or '',
'views': api.views_count or 0,
'likes': 0,
'url': f'/api-detail/{api.id}',
})
if sort == 'views':
results.sort(key=lambda x: x['views'], reverse=True)
elif sort == 'newest':
pass
else:
results.sort(key=lambda x: x['views'] + x['likes'] * 10, reverse=True)
paginator = Paginator(results, page_size)
total = len(results)
try:
paginated_results = paginator.page(page)
except Exception:
paginated_results = []
return create_standardized_response(data={
'results': list(paginated_results) if hasattr(paginated_results, '__iter__') else paginated_results,
'total': total,
'page': page,
'page_size': page_size,
}, code=ResponseCode.SUCCESS)
class SearchSuggestionsView(APIView):
permission_classes = [AllowAny]
@swagger_auto_schema(
tags=['搜索'],
operation_summary='搜索建议',
operation_description='根据输入前缀返回搜索建议列表',
manual_parameters=[
openapi.Parameter('q', openapi.IN_QUERY, description='搜索前缀', type=openapi.TYPE_STRING, required=True),
openapi.Parameter('limit', openapi.IN_QUERY, description='返回数量上限', type=openapi.TYPE_INTEGER),
],
responses={200: success_response}
)
def get(self, request):
q = request.query_params.get('q', '').strip()
limit = int(request.query_params.get('limit', 8))
if not q:
return create_standardized_response(data=[], code=ResponseCode.SUCCESS)
suggestions = set()
article_titles = Article.objects.filter(
title__icontains=q
).filter(status='published').values_list('title', flat=True)[:5]
suggestions.update(article_titles)
tool_names = Tool.objects.filter(
name__icontains=q
).values_list('name', flat=True)[:5]
suggestions.update(tool_names)
course_titles = Course.objects.filter(
title__icontains=q
).values_list('title', flat=True)[:5]
suggestions.update(course_titles)
api_names = ApiItem.objects.filter(
name__icontains=q
).filter(is_enabled=True).values_list('name', flat=True)[:5]
suggestions.update(api_names)
result = list(suggestions)[:limit]
return create_standardized_response(data=result, code=ResponseCode.SUCCESS)
class HotKeywordsView(APIView):
permission_classes = [AllowAny]
@swagger_auto_schema(
tags=['搜索'],
operation_summary='获取热门搜索关键词',
operation_description='返回热门搜索关键词列表',
responses={200: success_response}
)
def get(self, request):
hot_keywords = [
'React Hooks',
'RESTful API',
'TypeScript',
'Docker部署',
'GraphQL',
'微服务架构',
'CI/CD',
'性能优化',
'Vue3',
'Python',
]
return create_standardized_response(data=hot_keywords, code=ResponseCode.SUCCESS)