Files
zkxw/backend/app/main.py
T
zlwu 5fe1193a2c feat(lab-sample): 实验室样本处理三表台账与部门字段 ACL
按造血/单个核/间充质聚合展示,部门员工只改本部门、主管可改全部,并支持筛选与 Excel 导出;同步纠正采集接受人串权与严格菜单守卫。
2026-11-12 12:48:05 +08:00

196 lines
9.1 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, market_mobile_router, sample_storage_cert_qc_router, customer_service_dict_router, sample_cert_insurance_router, sample_code_correction_router, lab_sample_process_router, lab_sample_processing_router, project_management_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(market_mobile_router, prefix=settings.API_V1_PREFIX)
app.include_router(sample_storage_cert_qc_router, prefix=settings.API_V1_PREFIX)
app.include_router(customer_service_dict_router, prefix=settings.API_V1_PREFIX)
app.include_router(sample_cert_insurance_router, prefix=settings.API_V1_PREFIX)
app.include_router(sample_code_correction_router, prefix=settings.API_V1_PREFIX)
app.include_router(lab_sample_process_router, prefix=settings.API_V1_PREFIX)
app.include_router(lab_sample_processing_router, prefix=settings.API_V1_PREFIX)
app.include_router(project_management_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"}