"""server-core 统一入口(dispatcher)—— 对外生产服务 opc.pinesound.cn 单入口。 按「端口专属前缀」把请求分流到对应子应用,每端口自带前缀子路由、互不冲突: /park/* → 园区子应用(app.park,大屏/MQTT/园区数据) /api/*、/uploads/*、/SpXvScDiDT.txt → 培训子应用(app.training,独立 data/opc.db) /auth /opc /admin /government /investor …→ 平台应用(app.main,身份 + 业务域,各端口子路由) 启动(server-core 目录): uv run uvicorn dispatcher:app --host 0.0.0.0 --port 8090 --reload """ from __future__ import annotations import logging from app.main import app as core_app from app.park.app import app as park_app from app.training.main import app as training_app logger = logging.getLogger(__name__) ROUTE_TRAINING_PREFIXES = ("/api", "/uploads", "/oss", "/SpXvScDiDT.txt") ROUTE_PARK_PREFIX = "/park" # training_app 特有的端点前缀(旧培训/活动/预约子应用);除此之外的 /api/* 走 core_app TRAINING_EXCLUSIVE_PREFIXES = ( "/api/auth/", "/api/tasks/claim-by-code", "/api/tasks/my", "/api/events", "/api/bookings", "/api/checkins", "/api/tests", "/api/policy/", "/api/plan/", "/api/survey/", "/api/ops/", "/api/upload", "/api/park/", "/api/park-admission", "/api/health", "/api/oss/", "/api/plan-logs", "/api/policy-logs", "/api/survey-logs", ) def _prefix_app(path: str): """按路径前缀匹配目标子应用;未命中返回 None 交由平台应用(app.main)。""" # OPC 收款认证/分账/进件接口:pay_router 挂载在平台应用(app.main),路径 /opc/pay/* # 前端统一走 /api 前缀,故 /api/opc/pay/* 需例外路由到平台应用(而非 training 子应用) if path.startswith("/api/opc/pay/") or path == "/api/opc/pay": return core_app if path.startswith(ROUTE_PARK_PREFIX): return park_app # /api/* 默认走 core_app(新平台功能:通知/信用/大厅/任务等) # 只有 training_app 特有的旧端点才走 training_app if path.startswith("/api/") or path == "/api": for prefix in TRAINING_EXCLUSIVE_PREFIXES: if path.startswith(prefix): return training_app return core_app if path.startswith(ROUTE_TRAINING_PREFIXES): return training_app return None class Dispatcher: """极简 ASGI 分发器:驱动子应用 lifespan(核心库初始化)+ 按路径路由。""" def __init__(self, apps: list): self._apps = apps async def _run_lifespan(self, receive, send): """依次进入各子应用 lifespan(核心应用负责建表/种子),按启动/停止消息配对退出。""" started: list = [] try: while True: message = await receive() if message["type"] == "lifespan.startup": for a in self._apps: cm = a.router.lifespan_context(app=a) await cm.__aenter__() started.append(cm) await send({"type": "lifespan.startup.complete"}) elif message["type"] == "lifespan.shutdown": for cm in reversed(started): await cm.__aexit__(None, None, None) started = [] await send({"type": "lifespan.shutdown.complete"}) return except BaseException: for cm in reversed(started): try: await cm.__aexit__(None, None, None) except Exception: # noqa: BLE001 pass raise async def __call__(self, scope, receive, send): if scope["type"] == "lifespan": await self._run_lifespan(receive, send) return path = scope.get("path", "") target = _prefix_app(path) or core_app # core_app 的端点不带 /api 前缀(如 /hall/tasks、/credit/overview), # 前端统一走 /api 前缀,故路由到 core_app 时需去掉 /api 前缀 if target is core_app and path.startswith("/api/"): scope = dict(scope) scope["path"] = path[4:] # 去掉 "/api" if "raw_path" in scope: scope["raw_path"] = scope["path"].encode() await target(scope, receive, send) app = Dispatcher([core_app, training_app, park_app])