fix(C-07):搜索边界收口+结果跳转死链修复

q超长截断100字符、page/page_size健壮解析、types白名单;文章/工具/课程/接口结果URL对齐前端现行路由(/post、/utility、/course-learn、/open-api-detail)。
This commit is contained in:
chunyu
2026-09-15 15:20:19 +08:00
parent 3618323192
commit 1e95db0bda
3 changed files with 133 additions and 10 deletions
+43 -10
View File
@@ -22,6 +22,12 @@ SORT_MAPPING = {
'3': 'newest',
}
# C-07 边界收口:q 超长截断、page/page_size 健壮解析、types 白名单。
MAX_Q_LEN = 100
MAX_PAGE_SIZE = 50
DEFAULT_PAGE_SIZE = 12
CONTENT_TYPES = ('all', 'article', 'tool', 'course', 'api')
def normalize_sort(sort_value):
return SORT_MAPPING.get(sort_value, sort_value)
@@ -44,11 +50,30 @@ class GlobalSearchView(APIView):
responses={200: success_response}
)
async 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'))
raw_q = request.query_params.get('q', '')
if not isinstance(raw_q, str):
raw_q = str(raw_q)
q = raw_q.strip()[:MAX_Q_LEN]
raw_type = request.query_params.get('type', 'all')
if not isinstance(raw_type, str):
raw_type = 'all'
content_type = raw_type.strip().lower() or 'all'
if content_type not in CONTENT_TYPES:
content_type = 'all'
try:
page = int(request.query_params.get('page', 1))
except (TypeError, ValueError):
page = 1
try:
page_size = int(request.query_params.get('page_size', DEFAULT_PAGE_SIZE))
except (TypeError, ValueError):
page_size = DEFAULT_PAGE_SIZE
page = max(page, 1)
page_size = min(max(page_size, 1), MAX_PAGE_SIZE)
raw_sort = request.query_params.get('sort', 'relevance')
if not isinstance(raw_sort, str):
raw_sort = str(raw_sort)
sort = normalize_sort(raw_sort)
if not q:
return create_standardized_response(data={
@@ -82,7 +107,7 @@ class GlobalSearchView(APIView):
'cover': cover_url,
'views': article.views or 0,
'likes': article.likes or 0,
'url': f'/articles/{article.id}',
'url': f'/post/{article.id}',
})
if content_type == 'all' or content_type == 'tool':
@@ -99,7 +124,7 @@ class GlobalSearchView(APIView):
'cover': '',
'views': tool.usage_count or 0,
'likes': 0,
'url': f'/use?tool={tool.id}',
'url': tool.url_path or '/utility',
})
if content_type == 'all' or content_type == 'course':
courses = Course.objects.filter(
@@ -119,6 +144,7 @@ class GlobalSearchView(APIView):
'cover': cover_url,
'views': 0,
'likes': 0,
'url': f'/course-learn?id={course.id}',
})
if content_type == 'all' or content_type == 'api':
@@ -136,7 +162,7 @@ class GlobalSearchView(APIView):
'cover': api.image_url or '',
'views': api.views_count or 0,
'likes': 0,
'url': f'/api-detail/{api.id}',
'url': f'/open-api-detail/{api.id}',
})
if sort == 'views':
@@ -176,8 +202,15 @@ class SearchSuggestionsView(APIView):
responses={200: success_response}
)
async def get(self, request):
q = request.query_params.get('q', '').strip()
limit = int(request.query_params.get('limit', 8))
raw_q = request.query_params.get('q', '')
if not isinstance(raw_q, str):
raw_q = str(raw_q)
q = raw_q.strip()[:MAX_Q_LEN]
try:
limit = int(request.query_params.get('limit', 8))
except (TypeError, ValueError):
limit = 8
limit = min(max(limit, 1), MAX_PAGE_SIZE)
if not q:
return create_standardized_response(data=[], code=ResponseCode.SUCCESS)