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:
Pine
2026-08-17 21:25:46 +08:00
parent 7f1c2ac4f0
commit 856ff88440
34 changed files with 3639 additions and 378 deletions
+94
View File
@@ -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)