Files
server-core/dispatcher.py
T

117 lines
4.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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])