188 lines
8.4 KiB
Python
188 lines
8.4 KiB
Python
"""FastAPI 应用入口(骨架工程)"""
|
|
from contextlib import asynccontextmanager
|
|
import logging
|
|
from datetime import datetime
|
|
|
|
from fastapi import FastAPI, Request
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.responses import JSONResponse
|
|
from sqlalchemy.exc import SQLAlchemyError
|
|
|
|
from app.core.config import settings
|
|
from app.core.database import SessionLocal
|
|
from app.core.api_request_log_middleware import ApiRequestLogMiddleware
|
|
from app.core.api_crypto_middleware import ApiCryptoMiddleware
|
|
from app.models import models # noqa: F401
|
|
from app.api import public_router, menu_router, admin_router, workflow_router, basic_info_router, collection_protocol_router, protocol_sample_payment_router, protocol_refund_record_router, presign_order_router, sample_freight_router, leave_router, operations_router, ethnic_router, sample_reception_router, sample_production_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, qc_sms_notification_router, protocol_sample_collection_router, research_donation_sample_collection_router, pilot_application_router, pilot_checkup_router, pilot_delivery_router, pilot_infusion_confirm_router, pilot_infusion_schedule_mgmt_router, outbound_release_review_router, clinical_payment_router, env_detect_router, cleanroom_garment_router, market_router, pre_transit_sample_router, oa_approval_router, mini_auth_router, mini_menu_router, sample_storage_cert_qc_router
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
"""应用生命周期管理"""
|
|
from app.core import login_crypto
|
|
|
|
login_crypto.ensure_initialized()
|
|
|
|
from app.tasks import init_scheduled_tasks
|
|
|
|
try:
|
|
init_scheduled_tasks(SessionLocal)
|
|
except Exception as e:
|
|
logging.warning("定时任务调度器启动失败: %s", e)
|
|
|
|
from app.services.cn_holiday_sync import start_cn_holiday_sync_loop, stop_cn_holiday_sync_loop
|
|
|
|
try:
|
|
start_cn_holiday_sync_loop(SessionLocal)
|
|
except Exception as e:
|
|
logging.warning("节假日同步线程启动失败: %s", e)
|
|
|
|
yield
|
|
|
|
try:
|
|
stop_cn_holiday_sync_loop()
|
|
except Exception:
|
|
pass
|
|
|
|
from app.tasks import shutdown_scheduler
|
|
|
|
try:
|
|
shutdown_scheduler()
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
app = FastAPI(
|
|
title=settings.APP_NAME,
|
|
description="zkxw 管理端骨架后端(鉴权与菜单)",
|
|
version="1.0.0",
|
|
docs_url="/docs" if settings.DEBUG else None,
|
|
redoc_url="/redoc" if settings.DEBUG else None,
|
|
openapi_url="/openapi.json" if settings.DEBUG else None,
|
|
lifespan=lifespan,
|
|
)
|
|
|
|
_cors_kw: dict = {
|
|
"allow_origins": settings.BACKEND_CORS_ORIGINS,
|
|
"allow_credentials": True,
|
|
"allow_methods": ["*"],
|
|
"allow_headers": ["*"],
|
|
}
|
|
if settings.DEBUG:
|
|
_cors_kw["allow_origin_regex"] = r"^https?://(localhost|127\.0\.0\.1)(:\d+)?$"
|
|
app.add_middleware(CORSMiddleware, **_cors_kw)
|
|
app.add_middleware(ApiRequestLogMiddleware)
|
|
app.add_middleware(ApiCryptoMiddleware)
|
|
|
|
logger = logging.getLogger("sqxp")
|
|
|
|
|
|
def _error_json(status_code: int, code: int, message: str) -> JSONResponse:
|
|
return JSONResponse(
|
|
status_code=status_code,
|
|
content={
|
|
"code": code,
|
|
"message": message,
|
|
"data": None,
|
|
"timestamp": int(datetime.now().timestamp() * 1000),
|
|
},
|
|
)
|
|
|
|
|
|
@app.exception_handler(SQLAlchemyError)
|
|
async def sqlalchemy_exception_handler(request: Request, exc: SQLAlchemyError):
|
|
"""数据库异常:返回 JSON 并保留 CORS 头,避免浏览器只报跨域错误"""
|
|
logger.exception("Database error on %s %s", request.method, request.url.path)
|
|
if settings.DEBUG:
|
|
message = f"数据库不可用:{exc.orig if getattr(exc, 'orig', None) else exc}"
|
|
else:
|
|
message = "数据库不可用,请确认 MySQL 已启动且连接配置正确"
|
|
return _error_json(503, 503, message)
|
|
|
|
|
|
@app.exception_handler(Exception)
|
|
async def unhandled_exception_handler(request: Request, exc: Exception):
|
|
"""未捕获异常:统一 JSON 500,确保跨域场景下仍有 Access-Control-Allow-Origin"""
|
|
logger.exception("Unhandled error on %s %s", request.method, request.url.path)
|
|
if settings.DEBUG:
|
|
message = str(exc) or "服务器内部错误"
|
|
else:
|
|
message = "服务器内部错误,请稍后重试"
|
|
return _error_json(500, 500, message)
|
|
|
|
|
|
app.include_router(public_router, prefix=settings.API_V1_PREFIX)
|
|
app.include_router(menu_router, prefix=settings.API_V1_PREFIX)
|
|
app.include_router(admin_router, prefix=settings.API_V1_PREFIX)
|
|
app.include_router(workflow_router, prefix=settings.API_V1_PREFIX)
|
|
app.include_router(basic_info_router, prefix=settings.API_V1_PREFIX)
|
|
app.include_router(collection_protocol_router, prefix=settings.API_V1_PREFIX)
|
|
app.include_router(protocol_sample_payment_router, prefix=settings.API_V1_PREFIX)
|
|
app.include_router(protocol_refund_record_router, prefix=settings.API_V1_PREFIX)
|
|
app.include_router(presign_order_router, prefix=settings.API_V1_PREFIX)
|
|
app.include_router(sample_freight_router, prefix=settings.API_V1_PREFIX)
|
|
app.include_router(leave_router, prefix=settings.API_V1_PREFIX)
|
|
app.include_router(operations_router, prefix=settings.API_V1_PREFIX)
|
|
app.include_router(ethnic_router, prefix=settings.API_V1_PREFIX)
|
|
app.include_router(sample_reception_router, prefix=settings.API_V1_PREFIX)
|
|
app.include_router(sample_production_reception_router, prefix=settings.API_V1_PREFIX)
|
|
app.include_router(qc_router, prefix=settings.API_V1_PREFIX)
|
|
app.include_router(research_donation_router, prefix=settings.API_V1_PREFIX)
|
|
app.include_router(production_router, prefix=settings.API_V1_PREFIX)
|
|
app.include_router(pilot_service_router, prefix=settings.API_V1_PREFIX)
|
|
app.include_router(insurance_company_router, prefix=settings.API_V1_PREFIX)
|
|
app.include_router(material_router, prefix=settings.API_V1_PREFIX)
|
|
app.include_router(storage_release_review_router, prefix=settings.API_V1_PREFIX)
|
|
app.include_router(sample_sms_notification_router, prefix=settings.API_V1_PREFIX)
|
|
app.include_router(storage_entry_sms_notification_router, prefix=settings.API_V1_PREFIX)
|
|
app.include_router(qc_sms_notification_router, prefix=settings.API_V1_PREFIX)
|
|
app.include_router(protocol_sample_collection_router, prefix=settings.API_V1_PREFIX)
|
|
app.include_router(research_donation_sample_collection_router, prefix=settings.API_V1_PREFIX)
|
|
app.include_router(pilot_application_router, prefix=settings.API_V1_PREFIX)
|
|
app.include_router(pilot_checkup_router, prefix=settings.API_V1_PREFIX)
|
|
app.include_router(pilot_delivery_router, prefix=settings.API_V1_PREFIX)
|
|
app.include_router(pilot_infusion_confirm_router, prefix=settings.API_V1_PREFIX)
|
|
app.include_router(pilot_infusion_schedule_mgmt_router, prefix=settings.API_V1_PREFIX)
|
|
app.include_router(outbound_release_review_router, prefix=settings.API_V1_PREFIX)
|
|
app.include_router(clinical_payment_router, prefix=settings.API_V1_PREFIX)
|
|
app.include_router(env_detect_router, prefix=settings.API_V1_PREFIX)
|
|
app.include_router(cleanroom_garment_router, prefix=settings.API_V1_PREFIX)
|
|
app.include_router(market_router, prefix=settings.API_V1_PREFIX)
|
|
app.include_router(pre_transit_sample_router, prefix=settings.API_V1_PREFIX)
|
|
app.include_router(oa_approval_router, prefix=settings.API_V1_PREFIX)
|
|
app.include_router(mini_auth_router, prefix=settings.API_V1_PREFIX)
|
|
app.include_router(mini_menu_router, prefix=settings.API_V1_PREFIX)
|
|
app.include_router(sample_storage_cert_qc_router, prefix=settings.API_V1_PREFIX)
|
|
|
|
|
|
def _mask_secret(value: str) -> str:
|
|
if not value:
|
|
return "(empty)"
|
|
if len(value) <= 6:
|
|
return "*" * len(value)
|
|
return f"{value[:3]}***{value[-3:]}"
|
|
|
|
|
|
_api_crypto_boot_msg = (
|
|
"API_CRYPTO_BOOT "
|
|
f"enabled={settings.API_ENCRYPT_ENABLED} "
|
|
f"fail_open={settings.API_ENCRYPT_FAIL_OPEN} "
|
|
f"fixed_secret={_mask_secret(settings.API_ENCRYPT_FIXED_SECRET)} "
|
|
f"ts_skew_sec={settings.API_ENCRYPT_TS_SKEW_SEC} "
|
|
f"nonce_ttl_sec={settings.API_ENCRYPT_NONCE_TTL_SEC} "
|
|
f"exclude_count={len([x for x in (settings.API_ENCRYPT_EXCLUDE_PATHS or '').split(',') if x.strip()])}"
|
|
)
|
|
print(_api_crypto_boot_msg)
|
|
logger.info(_api_crypto_boot_msg)
|
|
|
|
|
|
@app.get("/")
|
|
def root():
|
|
docs = "/docs" if settings.DEBUG else None
|
|
return {"name": settings.APP_NAME, "version": "1.0.0", "docs": docs}
|
|
|
|
|
|
@app.get("/health")
|
|
def health_check():
|
|
return {"status": "healthy"}
|