Files
server-core/app/park/app.py
T
Pine 81f4dfd0ad feat(park): 园区多租户 — SQLite(tenants表+companies+kb) + 每租户引擎 + 认证 + 作用域隔离
- tenants.py:park_tenants/park_companies/park_kb_docs 三表(serverdata/park/park.db),
  每园区独立 name/intro(两段式)/auth(账密 salted hash)/data(大屏锚点)/agent/kb;
  ensure_default_tenant 自愈建默认园区+灌 39 家企业。
- sim_engine 重构为 SimEngine(data)——每租户独立实例,get_engine(tid)/refresh_engine(tid)。
- auth.py:/park/auth/login(账密→长效 tenant JWT)、/park/auth/me、require_tenant(401)。
- mqtt:register/publish_command/publish_tick 按 tenant_id 作用域(拓扑为 command/tid/clientId)。
- routers:/park/tenants CRUD + 全部数据/指令端点租户作用域(authorization token 或 ?tenant_id)。
- app.py lifespan init_db + 逐租户 tick/publish;tools._query_companies 读租户库。
- .gitignore 增 serverdata/park/。TestClient:建租户/登录/快照隔离/企业CRUD/指令鉴权(无token401) 全过。
2026-08-24 18:06:51 +08:00

88 lines
3.0 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.
# -*- coding: utf-8 -*-
"""园区子应用(app/park)—— FastAPI 子应用组装。
由 server-core/dispatcher.py 以 `/park` 前缀路由对外;独立 lifespan:
启动 MQTT hub、数据 tick 线程(sim_engine → publish_tick);s2s 实时语音栈延后。
"""
from __future__ import annotations
import asyncio
import logging
import threading
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from .config import settings
from .event_bus import bus
from .mqtt import hub
from .routers import router
from .sim_engine import get_engine, sim_engine
from . import tenants
log = logging.getLogger("dpm.park")
settings.MEDIA_DIR.mkdir(parents=True, exist_ok=True)
@asynccontextmanager
async def lifespan(app: FastAPI):
# SQLite 多租户数据初始化(建表 + 默认园区)
try:
tenants.init_db()
except Exception as e: # noqa: BLE001
log.warning("园区 DB 初始化失败: %s", e)
bus.set_loop(asyncio.get_running_loop())
hub.start()
stop = threading.Event()
# s2s 实时语音栈(可选):后台线程起 RealtimeServer(:8765),仅 S2S_ENABLED=1
# 依赖 speech_to_speechvendor/s2s-cloud,需先 `uv pip install -e app/park/vendor/s2s-cloud`
if settings.S2S_ENABLED:
try:
from .s2s_bridge import start_s2s_backend
start_s2s_backend(stop)
except Exception as e: # noqa: BLE001
log.warning("s2s 语音栈启动失败(S2S_ENABLED=1 但依赖未装?: %s", e)
def tick_loop():
while not stop.is_set():
try:
# 多租户:逐园区 tick + 按租户发布
for t in tenants.list_tenants():
try:
snap = get_engine(t["id"]).tick()
hub.publish_tick(snap, t["id"])
except Exception as e: # noqa: BLE001
log.warning("园区 %s tick 异常: %s", t.get("id"), e)
except Exception as e: # noqa: BLE001
log.warning("园区数据 tick 异常: %s", e)
stop.wait(settings.MQTT_TICK_INTERVAL)
threading.Thread(target=tick_loop, daemon=True).start()
log.info("园区子应用 /park 启动(MQTT %s:%s%s",
settings.MQTT_HOST, settings.MQTT_PORT,
"已启用" if settings.MQTT_ENABLED else "已禁用")
try:
yield
finally:
stop.set()
hub.stop()
app = FastAPI(title="OPC 智能园区子应用", version="1.0.0", lifespan=lifespan)
# 关键:内部路由本为 /api/*,挂载到 /park 下 → 外部地址 /park/api/*
app.include_router(router, prefix="/park")
# 媒体静态服务(上传/播放文件)
app.mount("/park/file", StaticFiles(directory=str(settings.MEDIA_DIR)), name="park-media")
# 管理后台静态资源(如有)
_static_dir = Path(__file__).resolve().parent / "static"
if _static_dir.exists():
app.mount("/park/static", StaticFiles(directory=str(_static_dir)), name="park-static")