search.py 2.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
  1. """搜索接口路由。"""
  2. from __future__ import annotations
  3. import logging
  4. from fastapi import APIRouter, Query
  5. from app.response import ApiResponse, BusinessError, ResponseCode
  6. from app.schemas import SearchData, SearchRequest
  7. from app.services.cache import faq_cache
  8. from app.services.zendesk_client import ZendeskError, search_articles
  9. logger = logging.getLogger(__name__)
  10. router = APIRouter(tags=["search"])
  11. async def _do_search(req: SearchRequest) -> ApiResponse[SearchData]:
  12. """共享的搜索逻辑:使用内存中的 FAQ sec_ids 调用 Zendesk。"""
  13. snapshot = faq_cache.snapshot()
  14. sec_ids: list[int] = snapshot["sec_ids"]
  15. if not sec_ids:
  16. logger.warning("FAQ sec_ids 缓存为空,本次搜索不带 section 过滤")
  17. try:
  18. data = await search_articles(
  19. query=req.query,
  20. section_ids=sec_ids,
  21. locale=req.locale,
  22. page=req.page,
  23. per_page=req.per_page,
  24. )
  25. except ZendeskError as exc:
  26. raise BusinessError(ResponseCode.UPSTREAM_ERROR, msg=str(exc)) from exc
  27. return ApiResponse.ok(
  28. data=SearchData(
  29. query=req.query,
  30. count=int(data.get("count", 0)),
  31. page=req.page,
  32. per_page=req.per_page,
  33. next_page=data.get("next_page"),
  34. sec_ids_used=sec_ids,
  35. results=data.get("results", []),
  36. )
  37. )
  38. @router.get(
  39. "/search",
  40. response_model=ApiResponse[SearchData],
  41. summary="按 QUERY 搜索 FAQ 文章",
  42. )
  43. async def search_get(
  44. query: str = Query(..., min_length=1, description="搜索关键词"),
  45. locale: str | None = Query(default=None),
  46. page: int = Query(default=1, ge=1),
  47. per_page: int = Query(default=25, ge=1, le=100),
  48. ) -> ApiResponse[SearchData]:
  49. """GET 版本,便于浏览器直接测试。"""
  50. return await _do_search(
  51. SearchRequest(
  52. query=query, locale=locale, page=page, per_page=per_page
  53. )
  54. )
  55. @router.post(
  56. "/search",
  57. response_model=ApiResponse[SearchData],
  58. summary="按 QUERY 搜索 FAQ 文章 (POST)",
  59. )
  60. async def search_post(req: SearchRequest) -> ApiResponse[SearchData]:
  61. """POST 版本,请求体:{"query": "...", "locale": "...", ...}。"""
  62. return await _do_search(req)