"""admin 应用工厂(T-P2-09):极简运营后台的装配层。 与客户端 API 的装配差异只有三点:路由前缀 ``/admin/api/v1``(区分流量)、 鉴权换成 ``X-Admin-Token``、**空令牌时一个业务路由都不注册**——「未部署」 的语义用装配期决定,而不是每个请求里判空(404 更干净,扫描器也探不到面)。 """ from __future__ import annotations from collections.abc import AsyncIterator from contextlib import asynccontextmanager from fastapi import FastAPI from apps.admin.routers import assets, gaps, kb, rules, stats, users from packages.shared.config import Settings, get_settings from packages.shared.db import dispose_engine, get_session_factory from packages.shared.handlers import install_exception_handlers from packages.shared.logging import configure_logging, get_logger from packages.shared.middleware import RequestContextMiddleware from packages.shared.observability import setup_tracing logger = get_logger(__name__) #: 与客户端 API(/api/v1)区分的固定前缀;不提供 /v1 别名(后台没有历史包袱)。 ADMIN_API_PREFIX = "/admin/api/v1" @asynccontextmanager async def lifespan(app: FastAPI) -> AsyncIterator[None]: settings: Settings = app.state.admin_settings # 启动即触碰共享 engine(懒加载单例,复用 packages.shared.db): # 连接配置有问题应该在部署的那一刻暴露,而不是等顾问点第一个按钮。 get_session_factory() logger.info( "admin_started", env=settings.app_env, routes_registered=bool(settings.admin_token), ) try: yield finally: await dispose_engine() logger.info("admin_stopped") def create_admin_app(settings: Settings | None = None) -> FastAPI: cfg = settings or get_settings() configure_logging( level=cfg.log_level, json_output=cfg.log_json, extra_redact_keys=cfg.extra_redact_keys ) # 空令牌 = 等价未部署:业务路由、openapi、docs 全部不装配。 deployed = bool(cfg.admin_token) app = FastAPI( title="美搭运营后台", version="0.1.0", description=( "极简运营后台:知识实体/规则维护、素材登记审核、发布编排与缺口看板" "(T-P2-09)。鉴权:请求头 X-Admin-Token。" ), lifespan=lifespan, openapi_url=f"{ADMIN_API_PREFIX}/openapi.json" if deployed else None, docs_url=f"{ADMIN_API_PREFIX}/docs" if deployed and cfg.is_local else None, redoc_url=None, ) app.state.admin_settings = cfg app.add_middleware(RequestContextMiddleware) install_exception_handlers(app) setup_tracing(app, cfg) if deployed: routers = (kb.router, rules.router, assets.router, gaps.router, users.router, stats.router) for router in routers: app.include_router(router, prefix=ADMIN_API_PREFIX) else: logger.warning( "admin_routes_disabled", reason="ADMIN_TOKEN 为空:全部 admin 端点不注册(等价未部署)", ) return app