2026-01-25 22:34:33 +08:00
|
|
|
|
"""SmartEDT 后端服务入口。
|
|
|
|
|
|
|
|
|
|
|
|
主要职责:
|
|
|
|
|
|
- 加载配置与初始化日志
|
|
|
|
|
|
- 初始化数据库 schema/TimescaleDB
|
|
|
|
|
|
- 构造核心服务(仿真、监控、鉴权/RBAC)
|
|
|
|
|
|
- 挂载 HTTP/WebSocket 路由并启动 uvicorn
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
2026-01-19 14:27:41 +08:00
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
|
|
import asyncio
|
|
|
|
|
|
import logging
|
|
|
|
|
|
import multiprocessing
|
|
|
|
|
|
import platform
|
|
|
|
|
|
import sys
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
|
|
# 修复 Windows 下 psycopg 异步连接的 Event Loop 问题
|
|
|
|
|
|
if platform.system() == "Windows":
|
|
|
|
|
|
# 1. 强制设置 SelectorEventLoopPolicy
|
|
|
|
|
|
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
|
|
|
|
|
|
|
|
|
|
# 2. Monkeypatch 阻止 uvicorn 覆盖策略 (uvicorn 默认在 Windows 上强制使用 Proactor)
|
|
|
|
|
|
# 这对 psycopg 3 (SQLAlchemy async) 是必须的,因为 Proactor 不支持 add_reader/add_writer
|
|
|
|
|
|
original_set_policy = asyncio.set_event_loop_policy
|
|
|
|
|
|
def patched_set_policy(policy):
|
|
|
|
|
|
# 如果尝试设置 Proactor,则忽略并保持 Selector
|
|
|
|
|
|
if isinstance(policy, asyncio.WindowsProactorEventLoopPolicy):
|
|
|
|
|
|
return
|
|
|
|
|
|
original_set_policy(policy)
|
|
|
|
|
|
asyncio.set_event_loop_policy = patched_set_policy
|
|
|
|
|
|
|
|
|
|
|
|
import uvicorn
|
|
|
|
|
|
from dotenv import load_dotenv
|
|
|
|
|
|
from fastapi import FastAPI, WebSocket
|
|
|
|
|
|
|
|
|
|
|
|
from contextlib import asynccontextmanager
|
|
|
|
|
|
|
|
|
|
|
|
from backend.config.settings import load_settings
|
|
|
|
|
|
from backend.database.engine import create_engine, create_session_factory
|
|
|
|
|
|
from backend.database.schema import init_schema, init_timescaledb
|
|
|
|
|
|
from backend.services.broadcaster import Broadcaster
|
|
|
|
|
|
from backend.services.simulation_manager import SimulationManager
|
|
|
|
|
|
from backend.services.server_monitor import ServerMonitorService
|
2026-01-25 22:34:33 +08:00
|
|
|
|
from backend.services.unity_socket_client import UnitySocketClient
|
2026-01-19 14:27:41 +08:00
|
|
|
|
from backend.device.mock_vehicle import MockVehicleDevice
|
2026-01-25 22:34:33 +08:00
|
|
|
|
from backend.api import auth_routes, rbac_routes, routes, unity_routes, user_routes, ws
|
2026-01-19 14:27:41 +08:00
|
|
|
|
from backend.utils import configure_logging
|
|
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger("backend")
|
|
|
|
|
|
|
|
|
|
|
|
def _default_backend_log_file() -> Path | None:
|
2026-01-25 22:34:33 +08:00
|
|
|
|
"""在打包运行态下返回默认日志文件路径;开发态返回 None。"""
|
2026-01-19 14:27:41 +08:00
|
|
|
|
if getattr(sys, "frozen", False):
|
|
|
|
|
|
exe_dir = Path(sys.executable).resolve().parent
|
|
|
|
|
|
return exe_dir / "logs" / "backend.log"
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def _force_windows_selector_event_loop_for_uvicorn() -> None:
|
2026-01-25 22:34:33 +08:00
|
|
|
|
"""避免 uvicorn 在 Windows 上切换到 ProactorEventLoop(与 psycopg async 不兼容)。"""
|
2026-01-19 14:27:41 +08:00
|
|
|
|
if platform.system() != "Windows":
|
|
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
|
|
import uvicorn.loops.asyncio as uvicorn_asyncio_loop
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
def _selector_loop_factory(use_subprocess: bool = False):
|
|
|
|
|
|
return asyncio.SelectorEventLoop
|
|
|
|
|
|
|
|
|
|
|
|
uvicorn_asyncio_loop.asyncio_loop_factory = _selector_loop_factory
|
|
|
|
|
|
|
|
|
|
|
|
# 全局单例容器(简单实现)
|
|
|
|
|
|
class Container:
|
2026-01-25 22:34:33 +08:00
|
|
|
|
"""简易全局容器:集中创建与持有配置、DB 引擎、session 工厂与各服务单例。"""
|
|
|
|
|
|
|
2026-01-19 14:27:41 +08:00
|
|
|
|
def __init__(self):
|
|
|
|
|
|
load_dotenv()
|
|
|
|
|
|
self.settings = load_settings()
|
|
|
|
|
|
configure_logging(
|
|
|
|
|
|
"INFO" if not self.settings.server.debug else "DEBUG",
|
|
|
|
|
|
log_file=_default_backend_log_file(),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
self.file_root = self.settings.files.root_path
|
|
|
|
|
|
self.file_root.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
|
|
self.engine = create_engine(self.settings.database)
|
|
|
|
|
|
self.session_factory = create_session_factory(self.engine)
|
|
|
|
|
|
self.broadcaster = Broadcaster()
|
2026-01-25 22:34:33 +08:00
|
|
|
|
self.unity_client = UnitySocketClient(self.settings.unity.host, self.settings.unity.port)
|
2026-01-19 14:27:41 +08:00
|
|
|
|
|
|
|
|
|
|
# 实例化服务
|
|
|
|
|
|
self.simulation_manager = SimulationManager(
|
|
|
|
|
|
self.session_factory,
|
2026-01-25 22:34:33 +08:00
|
|
|
|
self.broadcaster,
|
|
|
|
|
|
unity_client=self.unity_client,
|
2026-01-19 14:27:41 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 实例化监控服务
|
|
|
|
|
|
self.server_monitor = ServerMonitorService(
|
|
|
|
|
|
self.session_factory,
|
|
|
|
|
|
self.broadcaster
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
container = Container()
|
|
|
|
|
|
|
|
|
|
|
|
@asynccontextmanager
|
|
|
|
|
|
async def lifespan(app: FastAPI):
|
2026-01-25 22:34:33 +08:00
|
|
|
|
"""FastAPI 生命周期:启动初始化与停机清理。"""
|
2026-01-19 14:27:41 +08:00
|
|
|
|
# 启动前初始化
|
|
|
|
|
|
await init_schema(container.engine)
|
|
|
|
|
|
if container.settings.database.timescaledb:
|
|
|
|
|
|
try:
|
|
|
|
|
|
await init_timescaledb(container.engine)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
# 可能是已存在或权限问题,仅打印日志
|
|
|
|
|
|
logger.warning("TimescaleDB init warning: %s", e)
|
|
|
|
|
|
|
|
|
|
|
|
# 注册默认设备
|
|
|
|
|
|
mock_dev = MockVehicleDevice(device_id="mock_01")
|
|
|
|
|
|
await container.simulation_manager.register_device(mock_dev)
|
|
|
|
|
|
|
|
|
|
|
|
# 启动监控服务
|
|
|
|
|
|
await container.server_monitor.start()
|
|
|
|
|
|
|
|
|
|
|
|
yield
|
|
|
|
|
|
|
|
|
|
|
|
# 停机清理
|
|
|
|
|
|
await container.server_monitor.stop()
|
|
|
|
|
|
await container.simulation_manager.stop()
|
|
|
|
|
|
await container.engine.dispose()
|
|
|
|
|
|
|
|
|
|
|
|
app = FastAPI(title="SmartEDT Backend", version="0.1.0", lifespan=lifespan)
|
|
|
|
|
|
app.include_router(routes.get_router(simulation_manager=container.simulation_manager, file_root=container.file_root))
|
2026-01-25 22:34:33 +08:00
|
|
|
|
app.include_router(auth_routes.get_router(session_factory=container.session_factory))
|
|
|
|
|
|
app.include_router(rbac_routes.get_router(session_factory=container.session_factory))
|
|
|
|
|
|
app.include_router(user_routes.get_router(session_factory=container.session_factory))
|
|
|
|
|
|
app.include_router(unity_routes.get_router(simulation_manager=container.simulation_manager, session_factory=container.session_factory))
|
2026-01-19 14:27:41 +08:00
|
|
|
|
|
|
|
|
|
|
@app.websocket("/ws")
|
|
|
|
|
|
async def ws_endpoint(websocket: WebSocket):
|
|
|
|
|
|
await ws.websocket_handler(websocket, broadcaster=container.broadcaster)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main() -> None:
|
2026-01-25 22:34:33 +08:00
|
|
|
|
"""命令行入口:解析参数并启动 uvicorn。"""
|
2026-01-19 14:27:41 +08:00
|
|
|
|
parser = argparse.ArgumentParser()
|
|
|
|
|
|
parser.add_argument("--host", default=None)
|
|
|
|
|
|
parser.add_argument("--port", type=int, default=None)
|
|
|
|
|
|
parser.add_argument("--debug", action="store_true")
|
|
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
|
|
|
|
|
|
settings = load_settings()
|
|
|
|
|
|
host = args.host or settings.server.host
|
|
|
|
|
|
port = args.port or settings.server.port
|
|
|
|
|
|
debug = args.debug or settings.server.debug
|
|
|
|
|
|
|
|
|
|
|
|
_force_windows_selector_event_loop_for_uvicorn()
|
|
|
|
|
|
|
|
|
|
|
|
if getattr(sys, 'frozen', False):
|
|
|
|
|
|
uvicorn.run(app, host=host, port=port, log_level="debug" if debug else "info")
|
|
|
|
|
|
else:
|
|
|
|
|
|
uvicorn.run("backend.main:app", host=host, port=port, reload=debug, log_level="debug" if debug else "info")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
multiprocessing.freeze_support()
|
|
|
|
|
|
main()
|