main.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. """FastAPI 应用入口(骨架工程)"""
  2. import asyncio
  3. from contextlib import asynccontextmanager
  4. import logging
  5. from datetime import datetime
  6. from fastapi import FastAPI, Request
  7. from fastapi.middleware.cors import CORSMiddleware
  8. from fastapi.responses import JSONResponse
  9. from sqlalchemy import text
  10. from sqlalchemy.exc import SQLAlchemyError
  11. from app.core.config import settings
  12. from app.core.database import SessionLocal, engine
  13. from app.core.api_request_log_middleware import ApiRequestLogMiddleware
  14. from app.core.api_crypto_middleware import ApiCryptoMiddleware
  15. from app.models import models # noqa: F401
  16. import app.models.bx_models # noqa: F401
  17. import app.models.db_models # noqa: F401
  18. import app.models.pl_models # noqa: F401
  19. import app.models.duty_models # noqa: F401
  20. from app.api import (
  21. public_router,
  22. menu_router,
  23. admin_router,
  24. workflow_router,
  25. leave_router,
  26. crm_router,
  27. monitor_router,
  28. bx_router,
  29. db_router,
  30. mini_auth_router,
  31. )
  32. from app.api.pl import pl_router
  33. from app.api.pl.public_transport import router as public_pl_transport_router
  34. from app.api.pl.public_transport_site import router as public_pl_transport_site_router
  35. from app.api.pl.public_site import router as public_pl_site_router
  36. from app.api.pl.public_io_order import (
  37. inbound_public_router as public_pl_inbound_router,
  38. outbound_public_router as public_pl_outbound_router,
  39. )
  40. from app.api.db.public_inventory import router as public_db_inventory_router
  41. from app.api.db.public_vehicles import router as public_db_vehicles_router
  42. from app.api.duty import router as duty_router
  43. from app.api.public_duty import router as public_duty_router
  44. @asynccontextmanager
  45. async def lifespan(app: FastAPI):
  46. """应用生命周期管理"""
  47. from app.core import login_crypto
  48. login_crypto.ensure_initialized()
  49. from app.tasks import init_scheduled_tasks
  50. try:
  51. init_scheduled_tasks(SessionLocal)
  52. except Exception as e:
  53. logging.warning("定时任务调度器启动失败: %s", e)
  54. keepalive_task = asyncio.create_task(_db_pool_keepalive_loop())
  55. yield
  56. keepalive_task.cancel()
  57. try:
  58. await keepalive_task
  59. except asyncio.CancelledError:
  60. pass
  61. from app.tasks import shutdown_scheduler
  62. try:
  63. shutdown_scheduler()
  64. except Exception:
  65. pass
  66. async def _db_pool_keepalive_loop() -> None:
  67. """定期 ping MySQL,避免远程库/NAT 回收空闲 TCP 后首请求长时间卡住。"""
  68. interval = max(60, int(settings.DB_KEEPALIVE_INTERVAL_SEC))
  69. def _ping() -> None:
  70. db = SessionLocal()
  71. try:
  72. db.execute(text("SELECT 1"))
  73. except Exception:
  74. try:
  75. engine.dispose()
  76. except Exception:
  77. pass
  78. finally:
  79. db.close()
  80. while True:
  81. await asyncio.sleep(interval)
  82. try:
  83. await asyncio.to_thread(_ping)
  84. except Exception as exc:
  85. logging.debug("db pool keepalive failed: %s", exc)
  86. app = FastAPI(
  87. title=settings.APP_NAME,
  88. description="山峡化工生产一体化门户管理端后端",
  89. version="1.0.0",
  90. docs_url="/docs",
  91. redoc_url="/redoc",
  92. lifespan=lifespan,
  93. )
  94. _cors_kw: dict = {
  95. "allow_origins": settings.BACKEND_CORS_ORIGINS,
  96. "allow_credentials": True,
  97. "allow_methods": ["*"],
  98. "allow_headers": ["*"],
  99. }
  100. if settings.DEBUG:
  101. _cors_kw["allow_origin_regex"] = r"^https?://(localhost|127\.0\.0\.1)(:\d+)?$"
  102. app.add_middleware(CORSMiddleware, **_cors_kw)
  103. app.add_middleware(ApiRequestLogMiddleware)
  104. app.add_middleware(ApiCryptoMiddleware)
  105. logger = logging.getLogger("shanxia2.0")
  106. def _error_json(status_code: int, code: int, message: str) -> JSONResponse:
  107. return JSONResponse(
  108. status_code=status_code,
  109. content={
  110. "code": code,
  111. "message": message,
  112. "data": None,
  113. "timestamp": int(datetime.now().timestamp() * 1000),
  114. },
  115. )
  116. @app.exception_handler(SQLAlchemyError)
  117. async def sqlalchemy_exception_handler(request: Request, exc: SQLAlchemyError):
  118. """数据库异常:返回 JSON 并保留 CORS 头,避免浏览器只报跨域错误"""
  119. logger.exception("Database error on %s %s", request.method, request.url.path)
  120. if settings.DEBUG:
  121. message = f"数据库不可用:{exc.orig if getattr(exc, 'orig', None) else exc}"
  122. else:
  123. message = "数据库不可用,请确认 MySQL 已启动且连接配置正确"
  124. return _error_json(503, 503, message)
  125. @app.exception_handler(Exception)
  126. async def unhandled_exception_handler(request: Request, exc: Exception):
  127. """未捕获异常:统一 JSON 500,确保跨域场景下仍有 Access-Control-Allow-Origin"""
  128. logger.exception("Unhandled error on %s %s", request.method, request.url.path)
  129. if settings.DEBUG:
  130. message = str(exc) or "服务器内部错误"
  131. else:
  132. message = "服务器内部错误,请稍后重试"
  133. return _error_json(500, 500, message)
  134. app.include_router(public_router, prefix=settings.API_V1_PREFIX)
  135. app.include_router(mini_auth_router, prefix=settings.API_V1_PREFIX)
  136. app.include_router(public_pl_transport_router, prefix=settings.API_V1_PREFIX)
  137. app.include_router(public_pl_transport_site_router, prefix=settings.API_V1_PREFIX)
  138. app.include_router(public_pl_site_router, prefix=settings.API_V1_PREFIX)
  139. app.include_router(public_pl_inbound_router, prefix=settings.API_V1_PREFIX)
  140. app.include_router(public_pl_outbound_router, prefix=settings.API_V1_PREFIX)
  141. app.include_router(public_db_inventory_router, prefix=settings.API_V1_PREFIX)
  142. app.include_router(public_db_vehicles_router, prefix=settings.API_V1_PREFIX)
  143. app.include_router(public_duty_router, prefix=settings.API_V1_PREFIX)
  144. app.include_router(menu_router, prefix=settings.API_V1_PREFIX)
  145. app.include_router(admin_router, prefix=settings.API_V1_PREFIX)
  146. app.include_router(workflow_router, prefix=settings.API_V1_PREFIX)
  147. app.include_router(leave_router, prefix=settings.API_V1_PREFIX)
  148. app.include_router(crm_router, prefix=settings.API_V1_PREFIX)
  149. app.include_router(bx_router, prefix=settings.API_V1_PREFIX)
  150. app.include_router(db_router, prefix=settings.API_V1_PREFIX)
  151. app.include_router(pl_router, prefix=settings.API_V1_PREFIX)
  152. app.include_router(duty_router, prefix=settings.API_V1_PREFIX)
  153. app.include_router(monitor_router, prefix=settings.API_V1_PREFIX)
  154. def _mask_secret(value: str) -> str:
  155. if not value:
  156. return "(empty)"
  157. if len(value) <= 6:
  158. return "*" * len(value)
  159. return f"{value[:3]}***{value[-3:]}"
  160. _api_crypto_boot_msg = (
  161. "API_CRYPTO_BOOT "
  162. f"enabled={settings.API_ENCRYPT_ENABLED} "
  163. f"fail_open={settings.API_ENCRYPT_FAIL_OPEN} "
  164. f"fixed_secret={_mask_secret(settings.API_ENCRYPT_FIXED_SECRET)} "
  165. f"ts_skew_sec={settings.API_ENCRYPT_TS_SKEW_SEC} "
  166. f"nonce_ttl_sec={settings.API_ENCRYPT_NONCE_TTL_SEC} "
  167. f"exclude_count={len([x for x in (settings.API_ENCRYPT_EXCLUDE_PATHS or '').split(',') if x.strip()])}"
  168. )
  169. print(_api_crypto_boot_msg)
  170. logger.info(_api_crypto_boot_msg)
  171. @app.get("/")
  172. def root():
  173. return {"name": settings.APP_NAME, "version": "1.0.0", "docs": "/docs"}
  174. @app.get("/health")
  175. def health_check():
  176. return {"status": "healthy"}