240 lines
8.8 KiB
Python
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)
|