main.py 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. """FastAPI 应用入口。"""
  2. from __future__ import annotations
  3. import asyncio
  4. import logging
  5. from contextlib import asynccontextmanager
  6. from typing import AsyncIterator
  7. from fastapi import FastAPI
  8. from app.exception_handlers import register_exception_handlers
  9. from app.response import ApiResponse
  10. from app.routers import search
  11. from app.schemas import CacheRefreshResult, CacheStatus
  12. from app.services.cache import faq_cache, scheduler_loop
  13. logging.basicConfig(
  14. level=logging.INFO,
  15. format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
  16. )
  17. logger = logging.getLogger(__name__)
  18. @asynccontextmanager
  19. async def lifespan(app: FastAPI) -> AsyncIterator[None]:
  20. """启动时初始化缓存 + 启动定时器;关闭时优雅取消。"""
  21. logger.info("启动:首次加载 FAQ 缓存…")
  22. await faq_cache.refresh()
  23. task = asyncio.create_task(scheduler_loop(), name="faq-scheduler")
  24. logger.info("启动:定时刷新任务已运行")
  25. try:
  26. yield
  27. finally:
  28. logger.info("关闭:取消定时任务…")
  29. task.cancel()
  30. try:
  31. await task
  32. except asyncio.CancelledError:
  33. pass
  34. app = FastAPI(
  35. title="Zendesk FAQ Keyword Search",
  36. description="封装 Zendesk Help Center 搜索;FAQ sec_ids 每日 0 点刷新缓存。",
  37. version="1.0.0",
  38. lifespan=lifespan,
  39. )
  40. # 注册全局异常处理器(统一错误响应格式 {code, data, msg})
  41. register_exception_handlers(app)
  42. # 路由
  43. app.include_router(search.router)
  44. @app.get(
  45. "/health",
  46. response_model=ApiResponse[dict[str, str]],
  47. tags=["meta"],
  48. )
  49. async def health() -> ApiResponse[dict[str, str]]:
  50. """健康检查。"""
  51. return ApiResponse.ok(data={"status": "ok"})
  52. @app.get(
  53. "/health/cache",
  54. response_model=ApiResponse[CacheStatus],
  55. tags=["meta"],
  56. )
  57. async def cache_status() -> ApiResponse[CacheStatus]:
  58. """缓存状态查看。"""
  59. snap = faq_cache.snapshot()
  60. return ApiResponse.ok(
  61. data=CacheStatus(
  62. sec_ids_count=len(snap["sec_ids"]),
  63. last_updated_at=snap["last_updated_at"],
  64. next_refresh_at=snap["next_refresh_at"],
  65. )
  66. )
  67. @app.post(
  68. "/admin/cache/refresh",
  69. response_model=ApiResponse[CacheRefreshResult],
  70. tags=["meta"],
  71. )
  72. async def refresh_cache_now() -> ApiResponse[CacheRefreshResult]:
  73. """手动触发刷新(运维 / 测试用途)。"""
  74. await faq_cache.refresh()
  75. snap = faq_cache.snapshot()
  76. return ApiResponse.ok(
  77. data=CacheRefreshResult(
  78. sec_ids_count=len(snap["sec_ids"]),
  79. last_updated_at=snap["last_updated_at"],
  80. ),
  81. msg="缓存刷新成功",
  82. )