main.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. """FastAPI 应用入口。
  2. 启动命令 ``uv run uvicorn app.main:app --reload`` 中:
  3. - ``app.main`` 表示本文件(app/main.py);
  4. - 最后的 ``app`` 表示文件末尾创建的 FastAPI 实例。
  5. """
  6. from collections.abc import Awaitable, Callable
  7. from datetime import UTC, datetime
  8. from fastapi import FastAPI, Request
  9. from fastapi.exceptions import RequestValidationError
  10. from fastapi.middleware.cors import CORSMiddleware
  11. from fastapi.responses import JSONResponse
  12. from starlette.responses import Response
  13. from app.api.v1.admin_auth import create_admin_router
  14. from app.api.v1.admin_business import create_admin_business_router
  15. from app.api.v1.agent import create_agent_router
  16. from app.api.v1.enrollment import create_enrollment_router
  17. from app.api.v1.h5_auth import create_h5_router
  18. from app.api.v1.h5_products import create_h5_product_router
  19. from app.api.v1.system import router as system_router
  20. from app.core.config import Settings, get_settings
  21. from app.core.errors import AppError
  22. from app.core.identifiers import new_request_id
  23. from app.core.passwords import PasswordService
  24. from app.core.security import JwtService
  25. from app.domains.agent.repository import AgentRepository
  26. from app.domains.agent.runtime import (
  27. AgentRuntime,
  28. UnavailableAgentRuntime,
  29. )
  30. from app.domains.agent.service import AgentThreadService
  31. from app.domains.agent.tools import build_agent_tool_registry
  32. from app.domains.attribution.repository import (
  33. AttributionRepository,
  34. InMemoryAttributionRepository,
  35. )
  36. from app.domains.attribution.service import AttributionService
  37. from app.domains.catalog.repository import CatalogRepository
  38. from app.domains.catalog.service import ProductCatalogService
  39. from app.domains.enrollment.repository import EnrollmentRepository
  40. from app.domains.enrollment.service import EnrollmentService
  41. from app.domains.identity.repository import (
  42. IdentityRepository,
  43. )
  44. from app.domains.identity.service import AdminAuthService, H5AuthService
  45. from app.harness.kernel import AgentKernel
  46. from app.harness.model_gateway import DeepSeekModelGateway
  47. from app.infrastructure.mysql.agent_repositories import SqlAlchemyAgentRepository
  48. from app.infrastructure.mysql.attribution_repositories import (
  49. SqlAlchemyAttributionRepository,
  50. )
  51. from app.infrastructure.mysql.enrollment_repositories import (
  52. SqlAlchemyEnrollmentRepository,
  53. )
  54. from app.infrastructure.mysql.repositories import (
  55. SqlAlchemyCatalogRepository,
  56. SqlAlchemyIdentityRepository,
  57. )
  58. from app.infrastructure.mysql.sessions import (
  59. DatabaseName,
  60. create_mysql_engine,
  61. create_session_factory,
  62. )
  63. from app.infrastructure.redis.agent_state import (
  64. AgentStateStore,
  65. InMemoryAgentStateStore,
  66. RedisAgentStateStore,
  67. )
  68. def create_app(
  69. *,
  70. settings: Settings | None = None,
  71. identity_repository: IdentityRepository | None = None,
  72. catalog_repository: CatalogRepository | None = None,
  73. agent_repository: AgentRepository | None = None,
  74. agent_runtime: AgentRuntime | None = None,
  75. enrollment_repository: EnrollmentRepository | None = None,
  76. attribution_repository: AttributionRepository | None = None,
  77. agent_state_store: AgentStateStore | None = None,
  78. clock: Callable[[], datetime] | None = None,
  79. ) -> FastAPI:
  80. """创建并组装一个 FastAPI 应用。
  81. 参数后的 ``| None`` 表示该参数可以不传。正常启动时使用真实配置和
  82. MySQL 仓储;测试时可以传入假的仓储和时钟,因此测试不必连接真实数据库。
  83. 开头的 ``*`` 表示后面的参数必须写出参数名,可避免调用时传错位置。
  84. """
  85. app_settings = settings or get_settings()
  86. # 只有未注入测试仓储时才创建真实数据库连接。
  87. core_engine = None
  88. agent_engine = None
  89. if identity_repository is None or catalog_repository is None or enrollment_repository is None:
  90. core_engine = create_mysql_engine(app_settings, DatabaseName.CORE)
  91. session_factory = create_session_factory(core_engine)
  92. # ``a or b``:优先使用调用方传入的对象,否则创建生产环境的默认对象。
  93. repository = identity_repository or SqlAlchemyIdentityRepository(session_factory)
  94. product_repository = catalog_repository or SqlAlchemyCatalogRepository(session_factory)
  95. if agent_repository is None:
  96. agent_engine = create_mysql_engine(app_settings, DatabaseName.AGENT)
  97. agent_session_factory = create_session_factory(agent_engine)
  98. runtime_repository: AgentRepository = SqlAlchemyAgentRepository(agent_session_factory)
  99. else:
  100. runtime_repository = agent_repository
  101. # 用可替换的 clock 获取时间,可以让与时间有关的测试保持稳定。
  102. current_time = clock or (lambda: datetime.now(UTC))
  103. # Service 负责业务规则,Repository 负责数据读写,Router 负责接收 HTTP 请求。
  104. jwt_service = JwtService(app_settings)
  105. h5_auth_service = H5AuthService(app_settings, repository, jwt_service)
  106. catalog_service = ProductCatalogService(product_repository, current_time)
  107. attribution_repository_adapter = attribution_repository or (
  108. InMemoryAttributionRepository()
  109. if app_settings.app_env == "test"
  110. else SqlAlchemyAttributionRepository(session_factory)
  111. )
  112. attribution_service = AttributionService(
  113. attribution_repository_adapter,
  114. current_time,
  115. )
  116. transactional_repository = enrollment_repository or SqlAlchemyEnrollmentRepository(
  117. session_factory
  118. )
  119. enrollment_service = EnrollmentService(
  120. transactional_repository,
  121. catalog_service,
  122. current_time,
  123. app_settings.mock_payment_callback_secret,
  124. attribution_service,
  125. )
  126. admin_auth_service = AdminAuthService(
  127. app_settings,
  128. repository,
  129. jwt_service,
  130. PasswordService(),
  131. )
  132. configured_agent_runtime = agent_runtime
  133. if configured_agent_runtime is None and app_settings.deepseek_api_key:
  134. configured_agent_runtime = AgentKernel(
  135. settings=app_settings,
  136. model_gateway=DeepSeekModelGateway(app_settings),
  137. tools=build_agent_tool_registry(
  138. catalog_service,
  139. enrollment_service,
  140. attribution_service,
  141. ),
  142. )
  143. runtime_state_store = agent_state_store or (
  144. InMemoryAgentStateStore(app_settings.agent_rate_limit)
  145. if app_settings.app_env == "test"
  146. else RedisAgentStateStore(app_settings)
  147. )
  148. thread_service = AgentThreadService(
  149. runtime_repository,
  150. current_time,
  151. configured_agent_runtime or UnavailableAgentRuntime(),
  152. runtime_state_store,
  153. )
  154. application = FastAPI(
  155. title="智保通第一阶段API",
  156. version="0.1.0",
  157. )
  158. application.state.identity_repository = repository
  159. application.state.catalog_repository = product_repository
  160. application.state.core_engine = core_engine
  161. application.state.agent_engine = agent_engine
  162. application.state.agent_state_store = runtime_state_store
  163. application.state.attribution_repository = attribution_repository_adapter
  164. application.router.add_event_handler("shutdown", runtime_state_store.close)
  165. # CORS 决定哪些前端地址可以在浏览器中调用本后端。
  166. application.add_middleware(
  167. CORSMiddleware,
  168. allow_origins=app_settings.app_cors_origins,
  169. allow_credentials=True,
  170. allow_methods=["*"],
  171. allow_headers=["*"],
  172. )
  173. @application.middleware("http")
  174. async def request_context(
  175. request: Request,
  176. call_next: Callable[[Request], Awaitable[Response]],
  177. ) -> Response:
  178. # 每个请求都有 request_id,方便把前端报错与后端日志对应起来。
  179. request.state.request_id = request.headers.get("X-Request-ID") or new_request_id()
  180. response = await call_next(request)
  181. response.headers["X-Request-ID"] = request.state.request_id
  182. return response
  183. @application.exception_handler(AppError)
  184. async def app_error_handler(request: Request, error: AppError) -> JSONResponse:
  185. """把业务层抛出的 AppError 统一转换成 JSON 错误响应。"""
  186. return JSONResponse(
  187. status_code=error.status_code,
  188. content={
  189. "success": False,
  190. "error": {
  191. "code": error.code,
  192. "message": error.message,
  193. "details": error.details or {},
  194. "retryable": error.retryable,
  195. },
  196. "meta": {
  197. "request_id": getattr(request.state, "request_id", new_request_id()),
  198. },
  199. },
  200. )
  201. @application.exception_handler(RequestValidationError)
  202. async def validation_error_handler(
  203. request: Request,
  204. error: RequestValidationError,
  205. ) -> JSONResponse:
  206. """把框架参数校验错误转换为稳定包络,不回传原始敏感输入。"""
  207. fields = [
  208. {
  209. "field": ".".join(str(part) for part in item["loc"]),
  210. "message": item["msg"],
  211. }
  212. for item in error.errors()
  213. ]
  214. return JSONResponse(
  215. status_code=400,
  216. content={
  217. "success": False,
  218. "error": {
  219. "code": "VALIDATION_ERROR",
  220. "message": "请求参数无效",
  221. "details": {"fields": fields},
  222. "retryable": False,
  223. },
  224. "meta": {
  225. "request_id": getattr(request.state, "request_id", new_request_id()),
  226. },
  227. },
  228. )
  229. # 注册路由;统一前缀使所有第一版接口都从 /api/v1 开始。
  230. application.include_router(system_router, prefix="/api/v1")
  231. application.include_router(
  232. create_h5_router(
  233. h5_auth_service,
  234. app_settings,
  235. attribution_service,
  236. ),
  237. prefix="/api/v1",
  238. )
  239. application.include_router(
  240. create_h5_product_router(catalog_service, h5_auth_service),
  241. prefix="/api/v1",
  242. )
  243. application.include_router(
  244. create_admin_router(admin_auth_service, app_settings),
  245. prefix="/api/v1",
  246. )
  247. application.include_router(
  248. create_agent_router(
  249. thread_service,
  250. h5_auth_service,
  251. admin_auth_service,
  252. ),
  253. prefix="/api/v1",
  254. )
  255. application.include_router(
  256. create_enrollment_router(
  257. enrollment_service,
  258. h5_auth_service,
  259. app_settings,
  260. ),
  261. prefix="/api/v1",
  262. )
  263. application.include_router(
  264. create_admin_business_router(
  265. enrollment_service,
  266. admin_auth_service,
  267. catalog_service,
  268. attribution_service,
  269. ),
  270. prefix="/api/v1",
  271. )
  272. return application
  273. # Uvicorn 导入 ``app.main:app`` 时,会执行这里并得到可运行的 ASGI 应用。
  274. app = create_app()