import logging from contextlib import asynccontextmanager from fastapi import FastAPI, Request from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles from app.api import api_router from app.api.camera import capture_pair_via_mqtt from app.services import CameraUnavailableError, get_camera_service from app.core.config import settings logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) settings.static.ensure_directories() @asynccontextmanager async def lifespan(_: FastAPI): """ FastAPI 生命周期管理。 启动阶段会尝试预热相机,这样第一次请求拍照时更快。 但这里不会因为相机异常而阻止整个服务启动, 因为有时你可能还需要先看接口文档、做健康检查或排查环境。 """ camera_service = get_camera_service() try: camera_service.initialize() logger.info("树莓派相机预热完成。") except CameraUnavailableError as exc: logger.warning("相机预热失败,拍照接口暂时不可用:%s", exc) try: yield finally: camera_service.close() logger.info("相机资源已关闭。") app = FastAPI( title=settings.app.title, version=settings.app.version, description=settings.app.description, lifespan=lifespan, ) app.include_router(api_router, prefix="/api") app.mount("/static", StaticFiles(directory=str(settings.static.root_dir)), name="static") @app.get("/", include_in_schema=False) def read_root() -> FileResponse: return FileResponse(settings.static.index_path) @app.api_route("/capture-pair", methods=["GET", "POST"], include_in_schema=False) def capture_pair_alias(request: Request): return capture_pair_via_mqtt(request) @app.get("/health", summary="健康检查") def health_check() -> dict[str, object]: """ 健康检查接口。 这个接口不强依赖相机真实可用,只返回服务当前状态和相机是否已初始化。 在部署脚本、容器探针或反向代理检查中都比较实用。 """ camera_service = get_camera_service() return { "status": "ok", "camera_initialized": camera_service.is_initialized, "app_name": settings.app.title, "app_version": settings.app.version, }