117 lines
4.5 KiB
Python
117 lines
4.5 KiB
Python
"""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])
|