# AI 值班 Web 平台 - 详细需求规格说明书 (PRD) **版本**: v8.0 (Execution Ready) **日期**: 2025-XX-XX **项目代号**: AI-Watch-Pro **核心架构**: SPA Served by Backend (Vue3 build -> FastAPI static) **包管理**: `uv` (Python) **关键技术**: OpenCV Grab-Only, WebSocket, APScheduler, MySQL --- ## 1. 系统总体架构 ### 1.1 技术栈清单 * **后端 (Server)**: * 语言: Python 3.10+ (由 `uv` 管理) * Web 框架: FastAPI * 服务器: Uvicorn * 视觉处理: OpenCV (`opencv-python-headless`) - 多线程 * 数据库: MySQL 8.0 (ORM: SQLAlchemy, Driver: pymysql) * 任务调度: APScheduler (AsyncIOScheduler) * AI 客户端: OpenAI SDK (兼容通义千问/GPT-4o/Gemini) * **前端 (Client)**: * 框架: Vue 3 (Composition API) * 构建: Vite * UI 库: Element Plus * 状态管理: Pinia * 图表: ECharts * **部署模式**: 前端 `npm run build` 生成静态文件,由 FastAPI 的 `StaticFiles` 统一托管,实现单端口服务。 ### 1.2 目录结构规范 ```text ai-watch-platform/ ├── .venv/ # uv 虚拟环境 ├── backend/ │ ├── app/ │ │ ├── api/ # REST API 路由 (v1) │ │ ├── core/ # 配置 (config.py, security.py) │ │ ├── models/ # SQLAlchemy 数据库模型 │ │ ├── schemas/ # Pydantic 数据校验模型 │ │ ├── services/ # 核心业务逻辑 │ │ │ ├── video_core.py # [重点] 视频流线程池管理 │ │ │ ├── llm_agent.py # AI 调用封装 │ │ │ └── scheduler.py # 定时任务 │ │ ├── static/ # 存放运行时生成的报警截图 │ │ └── main.py # 启动入口 (含前端托管逻辑) │ ├── dist/ # [Vue打包产物] css/js/index.html │ └── requirements.txt # 依赖列表 ├── frontend/ # Vue 源代码 ├── run.py # 根目录启动脚本 └── pyproject.toml # uv 配置 (可选) ``` --- ## 2. 数据库设计 (Schema) 所有表均使用 `utf8mb4` 字符集。 ### 2.1 用户表 (`users`) *虽然目前只有 Admin,但预留扩展性。* | 字段 | 类型 | 说明 | | :--- | :--- | :--- | | `id` | INT (PK) | 自增主键 | | `username` | VARCHAR(50) | 唯一,默认 `admin` | | `hashed_password` | VARCHAR(255)| 加密后的密码 | | `is_active` | BOOLEAN | 默认 True | ### 2.2 摄像头表 (`cameras`) | 字段 | 类型 | 说明 | | :--- | :--- | :--- | | `id` | INT (PK) | 自增 | | `name` | VARCHAR(100)| 摄像头备注名 (如: 1号车间) | | `stream_url` | VARCHAR(500)| RTSP 或 HTTP-FLV/MP4 地址 | | `status` | INT | 0:离线, 1:在线 (由后台心跳更新) | | `created_at` | DATETIME | 创建时间 | ### 2.3 模型配置表 (`model_configs`) | 字段 | 类型 | 说明 | | :--- | :--- | :--- | | `id` | INT (PK) | | | `name` | VARCHAR(50) | 配置别名 (如: GPT-4o-Mini) | | `base_url` | VARCHAR(255)| API 基础地址 | | `api_key` | VARCHAR(255)| 密钥 (前端展示时需脱敏) | | `model_name` | VARCHAR(100)| 实际模型ID (e.g., gpt-4o) | ### 2.4 值班任务表 (`tasks`) | 字段 | 类型 | 说明 | | :--- | :--- | :--- | | `id` | INT (PK) | | | `name` | VARCHAR(100)| 任务名 | | `model_config_id`| INT (FK) | 关联使用的模型 | | `camera_ids` | JSON | 摄像头ID数组 `[1, 5, 12]` | | `prompt` | TEXT | 检测提示词 | | `cron_expression`| VARCHAR(50) | 轮询频率 (如 `*/5 * * * *`) | | `is_running` | BOOLEAN | 开/关状态 | ### 2.5 报警日志表 (`task_logs`) | 字段 | 类型 | 说明 | | :--- | :--- | :--- | | `id` | BIGINT (PK)| | | `task_id` | INT (FK) | 关联任务 | | `camera_id` | INT (FK) | 关联摄像头 | | `check_time` | DATETIME | 巡检时间 | | `snapshot_path` | VARCHAR(255)| **关键**: 图片相对路径 `/static/snapshots/...` | | `ai_result` | TEXT | AI 返回的完整分析 | | `is_alarm` | BOOLEAN | 是否异常 | --- ## 3. 功能模块详细规格 ### 3.1 认证模块 (Auth Module) * **前端逻辑**: * 登录页输入账号 `admin` / `HNYZ0821`。 * 调用 `POST /api/v1/login`。 * 获取 JWT Token 存入 localStorage。 * Axios 拦截器:所有请求头自动携带 `Authorization: Bearer `。 * 如果 Token 失效 (401),自动跳转回登录页。 * **后端逻辑**: * 验证用户名密码。 * 签发 JWT (过期时间建议设为 7 天)。 ### 3.2 仪表盘模块 (Dashboard & Real-time Monitor) 此模块需解决 20 路并发显示的性能问题。 * **功能描述**: * 展示 20 个摄像头的实时画面(低延迟)。 * 展示今日异常统计、任务运行状态。 * **后端实现 (WebSocket + Grab-Only)**: * **WebSocket 端点**: `ws://host/ws/stream`。 * **推流循环**: * 创建一个后台 `asyncio` 任务。 * **每 1000ms (1秒)** 循环一次。 * 调用 `VideoManager.get_all_snapshots()`。 * 该方法遍历所有 20 个 `LowCostStream` 对象,触发一次 `retrieve()` 解码,获取最新帧。 * 将 20 张图片压缩为 JPEG (质量 60),转 Base64。 * 打包成 JSON: `{"cam_1": "base64...", "cam_2": "base64..."}`。 * 通过 WebSocket 群发给所有已连接的客户端。 * **前端实现**: * 建立 WebSocket 连接。 * 接收到 JSON 后,解析并直接修改对应 `` 标签的 `src` 属性。 * **优化**: 如果某摄像头 ID 在 JSON 中为 `null`,显示“信号中断”占位图。 ### 3.3 视频资源管理模块 (Video Manager) * **功能**: 增删改查摄像头。 * **后端核心逻辑 (`VideoManager` 单例)**: * **初始化**: FastAPI 启动时 (`@app.on_event("startup")`),从数据库读取所有摄像头,并为每一个创建一个 `LowCostStream` 线程。 * **Grab-Only 线程逻辑**: * 死循环执行 `cap.grab()` (只清空缓冲区,不解码,CPU < 1%)。 * 当 `get_snapshot()` 被调用时,执行一次 `cap.retrieve()` (解码,消耗 CPU)。 * **断线重连**: 如果 `grab()` 失败,线程休眠 5 秒后尝试 `cap.open()`。 * **新增 API**: 写入数据库 -> 调用 `VideoManager.add_stream(url)` 启动新线程。 * **删除 API**: 删除数据库记录 -> 调用 `VideoManager.stop_stream(id)` 销毁线程。 ### 3.4 任务调度模块 (AI Task Scheduler) 这是系统的“大脑”。 * **功能**: 定义何时、看哪里、查什么。 * **输入**: * 选择 10 个摄像头。 * Prompt: "检查工人是否佩戴安全帽,若未佩戴返回异常"。 * 频率: 每 5 分钟。 * **后端逻辑 (APScheduler)**: 1. 任务创建时,向调度器添加一个 Job。 2. **Job 执行流程**: * **步骤 A (极速截帧)**: 遍历这 10 个摄像头,调用 `video_manager.get_snapshot(id)`。由于是内存操作,10 路截帧耗时 < 0.5秒。 * **步骤 B (落盘)**: 将获取到的图片写入 `/backend/app/static/snapshots/{date}/{uuid}.jpg`。 * **步骤 C (AI 分析)**: * 使用 `asyncio.gather` 并发调用 LLM 接口 (防止一个请求卡住后面 9 个)。 * 构造请求: Image (Base64/URL) + Prompt。 * 系统提示词 (System Prompt): "你是一个安防助手。请简短回答。如果发现异常,请在回复开头加上 [ALARM] 标记。" * **步骤 D (入库)**: * 解析 AI 回复,检测是否包含 `[ALARM]`。 * 将 `image_path` (相对路径), `ai_result`, `is_alarm` 写入 `task_logs` 表。 ### 3.5 报警与报表模块 (Reporting) * **功能**: 历史回溯与导出。 * **前端**: * 表格展示日志,支持按“仅看异常”筛选。 * **图片预览**: 点击缩略图,弹出 `` 显示大图。图片源地址为 `/static/snapshots/...`。 * **后端**: * **静态文件挂载**: 必须在 `main.py` 中配置 `app.mount("/static", ...)`,否则前端无法访问磁盘上的图片。 * **导出 API**: * 查询数据库 -> 生成 Excel (使用 `pandas` 或 `openpyxl`)。 * Excel 中需包含完整的 HTTP 链接 (如 `http://192.168.1.10:8000/static/...`),以便用户下载后点击查看。 --- ## 4. API 接口定义 (RESTful v1) 所有接口前缀: `/api/v1` | 方法 | 路径 | 描述 | 请求体/参数 | | :--- | :--- | :--- | :--- | | **Auth** | | | | | POST | `/login` | 获取 Token | `{"username": "admin", "password": "..."}` | | **Cameras** | | | | | GET | `/cameras` | 获取列表 | | | POST | `/cameras` | 新增摄像头 | `{"name": "...", "url": "..."}` | | DELETE | `/cameras/{id}` | 删除摄像头 | | | **Models** | | | | | GET | `/models` | 获取模型配置 | | | POST | `/models` | 新增配置 | `{"base_url": "...", "api_key": "..."}` | | **Tasks** | | | | | GET | `/tasks` | 获取任务列表 | | | POST | `/tasks` | 创建任务 | `{"camera_ids": [1,2], "prompt": "...", "cron": "*/5..."}` | | POST | `/tasks/{id}/toggle`| 启停任务 | `{"running": true}` | | **Logs** | | | | | GET | `/logs` | 获取日志 | `?page=1&only_alarm=true` | | GET | `/logs/export` | 导出Excel | | --- ## 5. 开发环境准备 (uv 指南) 为了确保依赖环境一致,请严格执行以下命令。 ### 5.1 初始化 ```bash # 1. 创建虚拟环境 (速度极快) uv venv --python python@3.13 .venv # 2. 激活环境 # Windows .venv\Scripts\activate # Linux/Mac source .venv/bin/activate ``` ### 5.2 安装依赖 创建 `backend/requirements.txt`: ```text fastapi==0.109.0 uvicorn[standard]==0.27.0 sqlalchemy==2.0.25 pymysql==1.1.0 opencv-python-headless==4.9.0.80 apscheduler==3.10.4 openai==1.10.0 python-multipart==0.0.6 python-jose[cryptography]==3.3.0 passlib[bcrypt]==1.7.4 openpyxl==3.1.2 websockets==12.0 ``` 执行安装: ```bash uv pip install -r backend/requirements.txt ``` --- ## 6. 部署与交付逻辑 这是实现 **"后端启动前端"** 的最终步骤。 1. **前端打包**: 在 `frontend/` 目录下运行 `npm run build`。 配置 `vite.config.ts` 将 `outDir` 设置为 `../backend/dist`。 2. **后端静态托管**: 在 `backend/app/main.py` 底部添加: ```python # 必须放在所有 API 路由之后 # 1. 挂载 Vue 的静态资源 (js/css/img) app.mount("/assets", StaticFiles(directory="../backend/dist/assets"), name="assets") # 2. 挂载报警截图 (数据) app.mount("/static", StaticFiles(directory="backend/app/static"), name="static") # 3. Catch-All 路由 (SPA 核心) @app.get("/{full_path:path}") async def catch_all(full_path: str): # 允许直接访问 dist 根目录文件 (如 favicon.ico) dist_path = "../backend/dist" file_path = os.path.join(dist_path, full_path) if os.path.exists(file_path) and os.path.isfile(file_path): return FileResponse(file_path) # 否则一律返回 index.html return FileResponse(os.path.join(dist_path, "index.html")) ``` 3. **启动命令**: ```bash uvicorn backend.app.main:app --host 0.0.0.0 --port 8000 ``` 此时,访问 `http://localhost:8000` 即可看到完整的 Web 平台。