main.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. """FastAPI 应用入口(骨架工程)"""
  2. from contextlib import asynccontextmanager
  3. import logging
  4. from datetime import datetime
  5. from fastapi import FastAPI, Request
  6. from fastapi.middleware.cors import CORSMiddleware
  7. from fastapi.responses import JSONResponse
  8. from sqlalchemy.exc import SQLAlchemyError
  9. from app.core.config import settings
  10. from app.core.database import SessionLocal
  11. from app.core.api_request_log_middleware import ApiRequestLogMiddleware
  12. from app.core.api_crypto_middleware import ApiCryptoMiddleware
  13. from app.models import models # noqa: F401
  14. from app.api import public_router, menu_router, admin_router, workflow_router, basic_info_router, collection_protocol_router, leave_router, operations_router, ethnic_router, sample_reception_router, qc_router, research_donation_router, production_router, pilot_service_router, insurance_company_router, material_router, storage_release_review_router, sample_sms_notification_router, storage_entry_sms_notification_router, pilot_application_router, pilot_checkup_router, pilot_delivery_router, pilot_infusion_confirm_router, outbound_release_review_router
  15. @asynccontextmanager
  16. async def lifespan(app: FastAPI):
  17. """应用生命周期管理"""
  18. from app.core import login_crypto
  19. login_crypto.ensure_initialized()
  20. from app.tasks import init_scheduled_tasks
  21. try:
  22. init_scheduled_tasks(SessionLocal)
  23. except Exception as e:
  24. logging.warning("定时任务调度器启动失败: %s", e)
  25. yield
  26. from app.tasks import shutdown_scheduler
  27. try:
  28. shutdown_scheduler()
  29. except Exception:
  30. pass
  31. app = FastAPI(
  32. title=settings.APP_NAME,
  33. description="zkxw 管理端骨架后端(鉴权与菜单)",
  34. version="1.0.0",
  35. docs_url="/docs",
  36. redoc_url="/redoc",
  37. lifespan=lifespan,
  38. )
  39. _cors_kw: dict = {
  40. "allow_origins": settings.BACKEND_CORS_ORIGINS,
  41. "allow_credentials": True,
  42. "allow_methods": ["*"],
  43. "allow_headers": ["*"],
  44. }
  45. if settings.DEBUG:
  46. _cors_kw["allow_origin_regex"] = r"^https?://(localhost|127\.0\.0\.1)(:\d+)?$"
  47. app.add_middleware(CORSMiddleware, **_cors_kw)
  48. app.add_middleware(ApiRequestLogMiddleware)
  49. app.add_middleware(ApiCryptoMiddleware)
  50. logger = logging.getLogger("sqxp")
  51. def _error_json(status_code: int, code: int, message: str) -> JSONResponse:
  52. return JSONResponse(
  53. status_code=status_code,
  54. content={
  55. "code": code,
  56. "message": message,
  57. "data": None,
  58. "timestamp": int(datetime.now().timestamp() * 1000),
  59. },
  60. )
  61. @app.exception_handler(SQLAlchemyError)
  62. async def sqlalchemy_exception_handler(request: Request, exc: SQLAlchemyError):
  63. """数据库异常:返回 JSON 并保留 CORS 头,避免浏览器只报跨域错误"""
  64. logger.exception("Database error on %s %s", request.method, request.url.path)
  65. if settings.DEBUG:
  66. message = f"数据库不可用:{exc.orig if getattr(exc, 'orig', None) else exc}"
  67. else:
  68. message = "数据库不可用,请确认 MySQL 已启动且连接配置正确"
  69. return _error_json(503, 503, message)
  70. @app.exception_handler(Exception)
  71. async def unhandled_exception_handler(request: Request, exc: Exception):
  72. """未捕获异常:统一 JSON 500,确保跨域场景下仍有 Access-Control-Allow-Origin"""
  73. logger.exception("Unhandled error on %s %s", request.method, request.url.path)
  74. if settings.DEBUG:
  75. message = str(exc) or "服务器内部错误"
  76. else:
  77. message = "服务器内部错误,请稍后重试"
  78. return _error_json(500, 500, message)
  79. app.include_router(public_router, prefix=settings.API_V1_PREFIX)
  80. app.include_router(menu_router, prefix=settings.API_V1_PREFIX)
  81. app.include_router(admin_router, prefix=settings.API_V1_PREFIX)
  82. app.include_router(workflow_router, prefix=settings.API_V1_PREFIX)
  83. app.include_router(basic_info_router, prefix=settings.API_V1_PREFIX)
  84. app.include_router(collection_protocol_router, prefix=settings.API_V1_PREFIX)
  85. app.include_router(leave_router, prefix=settings.API_V1_PREFIX)
  86. app.include_router(operations_router, prefix=settings.API_V1_PREFIX)
  87. app.include_router(ethnic_router, prefix=settings.API_V1_PREFIX)
  88. app.include_router(sample_reception_router, prefix=settings.API_V1_PREFIX)
  89. app.include_router(qc_router, prefix=settings.API_V1_PREFIX)
  90. app.include_router(research_donation_router, prefix=settings.API_V1_PREFIX)
  91. app.include_router(production_router, prefix=settings.API_V1_PREFIX)
  92. app.include_router(pilot_service_router, prefix=settings.API_V1_PREFIX)
  93. app.include_router(insurance_company_router, prefix=settings.API_V1_PREFIX)
  94. app.include_router(material_router, prefix=settings.API_V1_PREFIX)
  95. app.include_router(storage_release_review_router, prefix=settings.API_V1_PREFIX)
  96. app.include_router(sample_sms_notification_router, prefix=settings.API_V1_PREFIX)
  97. app.include_router(storage_entry_sms_notification_router, prefix=settings.API_V1_PREFIX)
  98. app.include_router(pilot_application_router, prefix=settings.API_V1_PREFIX)
  99. app.include_router(pilot_checkup_router, prefix=settings.API_V1_PREFIX)
  100. app.include_router(pilot_delivery_router, prefix=settings.API_V1_PREFIX)
  101. app.include_router(pilot_infusion_confirm_router, prefix=settings.API_V1_PREFIX)
  102. app.include_router(outbound_release_review_router, prefix=settings.API_V1_PREFIX)
  103. def _mask_secret(value: str) -> str:
  104. if not value:
  105. return "(empty)"
  106. if len(value) <= 6:
  107. return "*" * len(value)
  108. return f"{value[:3]}***{value[-3:]}"
  109. _api_crypto_boot_msg = (
  110. "API_CRYPTO_BOOT "
  111. f"enabled={settings.API_ENCRYPT_ENABLED} "
  112. f"fail_open={settings.API_ENCRYPT_FAIL_OPEN} "
  113. f"fixed_secret={_mask_secret(settings.API_ENCRYPT_FIXED_SECRET)} "
  114. f"ts_skew_sec={settings.API_ENCRYPT_TS_SKEW_SEC} "
  115. f"nonce_ttl_sec={settings.API_ENCRYPT_NONCE_TTL_SEC} "
  116. f"exclude_count={len([x for x in (settings.API_ENCRYPT_EXCLUDE_PATHS or '').split(',') if x.strip()])}"
  117. )
  118. print(_api_crypto_boot_msg)
  119. logger.info(_api_crypto_boot_msg)
  120. @app.get("/")
  121. def root():
  122. return {"name": settings.APP_NAME, "version": "1.0.0", "docs": "/docs"}
  123. @app.get("/health")
  124. def health_check():
  125. return {"status": "healthy"}