feat: 拆出 FastAPI 独立后端 + MQTT 控制 + 阿里云 AI
后端(backend/,FastAPI :10085): - 全部数据 REST 接口:dashboard 快照 / 企业分区 / 播放列表 / 媒体 / 设置 - MQTT 控制通道:opc/display/command(切页/播放/卡片/通知), 管理端 POST /api/display/command → MQTT 广播 → 所有大屏同步响应 - 阿里云 DashScope:通义千问 LLM + 函数调用(工具经 MQTT 广播), paraformer-realtime-v2 语音识别 /api/ai/asr - 媒体资源统一由后端存储返回(上传/列表/静态服务) - SSE /api/events 保留作 MQTT 不可用时的兼容回退 前端: - config.js + .env.local 配置后端地址与 MQTT 账号(前端 dpm / 服务端 dpmserver) - mqtt.js 客户端 + useMqttControl(MQTT 驱动切页/媒体/卡片/通知) - DpmOverlays 全局覆盖层(通知 toast + 企业/分区/总览卡片) - useParkSim 改为后端 API 数据源(离线回退本地模拟) - AiChatPanel:对话走后端 LLM(工具调用),语音走本地录音 + 后端 ASR - MediaScreen:媒体控制走 MQTT(保留 SSE 回退),修复 useEffect TDZ Rust:lib.rs 移除内嵌 HTTP 服务器,只保留薄壳(窗口/权限/自启/npc 隧道) 安全:backend/.env、.env.local、media、data.json 已 gitignore
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
# -*- 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)
|
||||
Reference in New Issue
Block a user