95 lines
2.9 KiB
Python
95 lines
2.9 KiB
Python
|
|
# -*- coding: utf-8 -*-
|
|||
|
|
"""OPC 智能园区后端 —— FastAPI 入口
|
|||
|
|
启动:uvicorn app.main:app --host 0.0.0.0 --port 8000
|
|||
|
|
或:python main.py
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import asyncio
|
|||
|
|
import logging
|
|||
|
|
import threading
|
|||
|
|
import time
|
|||
|
|
from contextlib import asynccontextmanager
|
|||
|
|
from pathlib import Path
|
|||
|
|
|
|||
|
|
from fastapi import FastAPI, Request
|
|||
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|||
|
|
from fastapi.responses import FileResponse, JSONResponse
|
|||
|
|
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 sim_engine
|
|||
|
|
|
|||
|
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s %(message)s")
|
|||
|
|
log = logging.getLogger("dpm.main")
|
|||
|
|
|
|||
|
|
DIST = Path(settings.DIST_DIR)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@asynccontextmanager
|
|||
|
|
async def lifespan(app: FastAPI):
|
|||
|
|
# 启动准备
|
|||
|
|
settings.MEDIA_DIR.mkdir(parents=True, exist_ok=True)
|
|||
|
|
bus.set_loop(asyncio.get_running_loop())
|
|||
|
|
hub.start()
|
|||
|
|
|
|||
|
|
stop = threading.Event()
|
|||
|
|
|
|||
|
|
def tick_loop():
|
|||
|
|
"""数据引擎每 2.2s 推进一次;MQTT 连接时同步推送快照"""
|
|||
|
|
while not stop.is_set():
|
|||
|
|
try:
|
|||
|
|
snap = sim_engine.tick()
|
|||
|
|
hub.publish_tick(snap)
|
|||
|
|
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("OPC 智能园区后端启动于 http://%s:%s", settings.HOST, settings.PORT)
|
|||
|
|
if settings.MQTT_ENABLED:
|
|||
|
|
log.info("MQTT Broker: %s:%s(前端 WS: %s)", settings.MQTT_HOST, settings.MQTT_PORT, settings.MQTT_WS_URL)
|
|||
|
|
try:
|
|||
|
|
yield
|
|||
|
|
finally:
|
|||
|
|
stop.set()
|
|||
|
|
hub.stop()
|
|||
|
|
log.info("OPC 智能园区后端已停止")
|
|||
|
|
|
|||
|
|
|
|||
|
|
app = FastAPI(title="OPC 智能园区后端", version="1.0.0", lifespan=lifespan)
|
|||
|
|
|
|||
|
|
app.add_middleware(
|
|||
|
|
CORSMiddleware,
|
|||
|
|
allow_origins=["*"],
|
|||
|
|
allow_methods=["*"],
|
|||
|
|
allow_headers=["*"],
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
app.include_router(router)
|
|||
|
|
|
|||
|
|
# 媒体资源静态服务(上传/播放的文件)
|
|||
|
|
settings.MEDIA_DIR.mkdir(parents=True, exist_ok=True)
|
|||
|
|
app.mount("/file", StaticFiles(directory=str(settings.MEDIA_DIR)), name="media")
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------- 前端静态托管 + SPA 回退(浏览器直接访问 :8000 即可) ----------
|
|||
|
|
|
|||
|
|
@app.get("/{path:path}", include_in_schema=False)
|
|||
|
|
async def spa_fallback(path: str):
|
|||
|
|
if not DIST.exists():
|
|||
|
|
return JSONResponse({"detail": "前端未构建(dist 目录不存在)"}, status_code=404)
|
|||
|
|
dist = DIST.resolve()
|
|||
|
|
if not path:
|
|||
|
|
target = dist / "index.html"
|
|||
|
|
else:
|
|||
|
|
target = (dist / path).resolve()
|
|||
|
|
if target.is_file() and target.is_relative_to(dist):
|
|||
|
|
return FileResponse(target)
|
|||
|
|
index = dist / "index.html"
|
|||
|
|
if index.exists():
|
|||
|
|
return FileResponse(index)
|
|||
|
|
return JSONResponse({"detail": "Not Found"}, status_code=404)
|