From a8e24157e75ea0d0adbec9260c675aa71844900d Mon Sep 17 00:00:00 2001 From: zlgecc <103418489+zlgecc@users.noreply.github.com> Date: Tue, 5 Aug 2025 11:57:14 +0800 Subject: [PATCH] =?UTF-8?q?feat=20=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- algorithm/requirements.txt | 15 ++ server/README_API.md | 285 +++++++++++++++++++++++++++ server/app.py | 12 +- server/models/algorithm.py | 24 ++- server/readme.md | 213 +++++++-------------- server/requirements.txt | 3 +- server/routers/alarms.py | 236 +++++++++++++++++++++++ server/routers/algorithms.py | 61 +++--- server/routers/auth.py | 237 +++++++++++++++++++++++ server/routers/dashboard.py | 162 ++++++++++++++++ server/routers/devices.py | 147 +++++++++++--- server/routers/events.py | 143 +++++++++++--- server/routers/monitors.py | 185 ++++++++++++++++++ server/routers/scenes.py | 233 ++++++++++++++++++++++ server/routers/upload.py | 209 ++++++++++++++++++++ server/schemas/__init__.py | 5 - server/schemas/algorithm.py | 50 ----- server/schemas/device.py | 63 ------ server/schemas/event.py | 59 ------ server/test_api.py | 145 ++++++++++++++ server/todo.md | 362 +++++++++++++++++++++++++++++++++++ 21 files changed, 2441 insertions(+), 408 deletions(-) create mode 100644 algorithm/requirements.txt create mode 100644 server/README_API.md create mode 100644 server/routers/alarms.py create mode 100644 server/routers/auth.py create mode 100644 server/routers/dashboard.py create mode 100644 server/routers/monitors.py create mode 100644 server/routers/scenes.py create mode 100644 server/routers/upload.py delete mode 100644 server/schemas/__init__.py delete mode 100644 server/schemas/algorithm.py delete mode 100644 server/schemas/device.py delete mode 100644 server/schemas/event.py create mode 100644 server/test_api.py create mode 100644 server/todo.md diff --git a/algorithm/requirements.txt b/algorithm/requirements.txt new file mode 100644 index 0000000..a8e67ea --- /dev/null +++ b/algorithm/requirements.txt @@ -0,0 +1,15 @@ +# YOLO目标检测训练和推理依赖包 +ultralytics>=8.0.0 +opencv-python>=4.8.0 +supervision>=0.18.0 +numpy>=1.24.0 +tqdm>=4.65.0 +Pillow>=10.0.0 + +# 可选依赖(用于更好的性能) +torch>=2.0.0 +torchvision>=0.15.0 + +# 开发工具(可选) +matplotlib>=3.7.0 +seaborn>=0.12.0 \ No newline at end of file diff --git a/server/README_API.md b/server/README_API.md new file mode 100644 index 0000000..7a94cfb --- /dev/null +++ b/server/README_API.md @@ -0,0 +1,285 @@ +# 边检CV算法管理系统 API 文档 + +## 概述 + +本系统提供了完整的边检计算机视觉算法管理API,包括设备管理、算法管理、事件管理、监控管理、告警管理等功能。 + +## 快速开始 + +### 1. 启动服务 + +```bash +# 安装依赖 +pip install -r requirements.txt + +# 启动服务 +python app.py +``` + +服务将在 `http://localhost:8000` 启动 + +### 2. API文档 + +访问 `http://localhost:8000/docs` 查看完整的API文档 + +## 核心功能模块 + +### 1. 仪表板统计 (`/api/dashboard`) + +#### 获取KPI指标 +```http +GET /api/dashboard/kpi +``` + +#### 获取告警趋势 +```http +GET /api/dashboard/alarm-trend?days=7 +``` + +#### 获取摄像头统计 +```http +GET /api/dashboard/camera-stats +``` + +#### 获取算法统计 +```http +GET /api/dashboard/algorithm-stats +``` + +#### 获取事件热点 +```http +GET /api/dashboard/event-hotspots +``` + +### 2. 监控管理 (`/api/monitors`) + +#### 获取监控列表 +```http +GET /api/monitors?page=1&size=20&status=online&location=港口区 +``` + +#### 获取监控详情 +```http +GET /api/monitors/{monitor_id} +``` + +#### 获取监控视频流 +```http +GET /api/monitors/{monitor_id}/stream +``` + +#### 获取检测数据 +```http +GET /api/monitors/{monitor_id}/detections +``` + +### 3. 告警管理 (`/api/alarms`) + +#### 获取告警列表 +```http +GET /api/alarms?page=1&size=20&severity=high&status=pending +``` + +#### 获取告警详情 +```http +GET /api/alarms/{alarm_id} +``` + +#### 处理告警 +```http +PATCH /api/alarms/{alarm_id}/resolve +Content-Type: application/json + +{ + "resolution_notes": "已确认船舶靠泊,无异常", + "resolved_by": "operator1" +} +``` + +#### 获取告警统计 +```http +GET /api/alarms/stats +``` + +### 4. 场景管理 (`/api/scenes`) + +#### 获取场景列表 +```http +GET /api/scenes +``` + +#### 获取场景详情 +```http +GET /api/scenes/{scene_id} +``` + +#### 创建场景 +```http +POST /api/scenes +Content-Type: application/json + +{ + "name": "新场景", + "description": "场景描述" +} +``` + +### 5. 文件上传 (`/api/upload`) + +#### 上传视频 +```http +POST /api/upload/video +Content-Type: multipart/form-data + +file: [视频文件] +device_id: 1 +description: "视频描述" +``` + +#### 上传图片 +```http +POST /api/upload/image +Content-Type: multipart/form-data + +file: [图片文件] +event_id: 1 +description: "图片描述" +``` + +#### 获取文件列表 +```http +GET /api/upload/files?file_type=video&page=1&size=20 +``` + +### 6. 用户认证 (`/api/auth`) + +#### 用户登录 +```http +POST /api/auth/login +Content-Type: application/x-www-form-urlencoded + +username=admin&password=admin123 +``` + +#### 获取用户信息 +```http +GET /api/auth/profile +Authorization: Bearer {token} +``` + +## 数据模型 + +### 设备 (Device) +```json +{ + "id": 1, + "name": "港口区监控1", + "device_type": "camera", + "ip_address": "192.168.1.100", + "location": "港口A区", + "status": "online", + "is_enabled": true +} +``` + +### 算法 (Algorithm) +```json +{ + "id": 1, + "name": "船舶靠泊识别", + "description": "识别船舶靠泊行为", + "version": "1.0.0", + "status": "active", + "accuracy": 95.2, + "is_enabled": true +} +``` + +### 事件 (Event) +```json +{ + "id": 1, + "event_type": "船舶靠泊", + "device_id": 1, + "algorithm_id": 1, + "severity": "high", + "status": "pending", + "is_alert": true, + "description": "检测到船舶靠泊行为" +} +``` + +## 错误处理 + +所有API都遵循统一的错误响应格式: + +```json +{ + "detail": "错误描述信息" +} +``` + +常见HTTP状态码: +- `200`: 成功 +- `400`: 请求参数错误 +- `401`: 未授权 +- `404`: 资源不存在 +- `500`: 服务器内部错误 + +## 测试 + +运行测试脚本验证接口: + +```bash +python test_api.py +``` + +## 开发说明 + +### TODO 项目 +1. 实现真实的告警趋势统计 +2. 实现真实的检测数据获取 +3. 实现真实的视频流URL生成 +4. 实现真实的JWT验证中间件 +5. 实现真实的场景管理数据库模型 +6. 添加Redis缓存支持 +7. 添加WebSocket实时数据推送 + +### 文件结构 +``` +server/ +├── app.py # 主应用文件 +├── requirements.txt # 依赖文件 +├── test_api.py # API测试脚本 +├── routers/ # 路由模块 +│ ├── dashboard.py # 仪表板接口 +│ ├── monitors.py # 监控管理接口 +│ ├── alarms.py # 告警管理接口 +│ ├── scenes.py # 场景管理接口 +│ ├── upload.py # 文件上传接口 +│ └── auth.py # 用户认证接口 +├── models/ # 数据模型 +├── core/ # 核心配置 +└── uploads/ # 上传文件目录 +``` + +## 部署 + +### 生产环境配置 +1. 修改 `SECRET_KEY` 为安全的密钥 +2. 配置数据库连接 +3. 设置CORS允许的域名 +4. 配置静态文件服务 +5. 添加日志记录 +6. 配置反向代理 + +### Docker部署 +```dockerfile +FROM python:3.9 +WORKDIR /app +COPY requirements.txt . +RUN pip install -r requirements.txt +COPY . . +EXPOSE 8000 +CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"] +``` \ No newline at end of file diff --git a/server/app.py b/server/app.py index fedec53..ff0ab7a 100644 --- a/server/app.py +++ b/server/app.py @@ -1,6 +1,7 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from routers import algorithms, events, devices +from fastapi.staticfiles import StaticFiles +from routers import algorithms, events, devices, dashboard, monitors, alarms, scenes, upload, auth from core.database import engine from models.base import Base @@ -22,10 +23,19 @@ app.add_middleware( allow_headers=["*"], ) +# 静态文件服务 +app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads") + # 注册路由 app.include_router(algorithms.router, prefix="/api/algorithms", tags=["算法管理"]) app.include_router(events.router, prefix="/api/events", tags=["事件管理"]) app.include_router(devices.router, prefix="/api/devices", tags=["设备管理"]) +app.include_router(dashboard.router, prefix="/api/dashboard", tags=["仪表板"]) +app.include_router(monitors.router, prefix="/api/monitors", tags=["监控管理"]) +app.include_router(alarms.router, prefix="/api/alarms", tags=["告警管理"]) +app.include_router(scenes.router, prefix="/api/scenes", tags=["场景管理"]) +app.include_router(upload.router, prefix="/api/upload", tags=["文件上传"]) +app.include_router(auth.router, prefix="/api/auth", tags=["用户认证"]) @app.get("/") async def root(): diff --git a/server/models/algorithm.py b/server/models/algorithm.py index 0860472..afb0bb8 100644 --- a/server/models/algorithm.py +++ b/server/models/algorithm.py @@ -1,5 +1,6 @@ from sqlalchemy import Column, String, Text, Boolean, Float, Integer from .base import BaseModel +import json class Algorithm(BaseModel): __tablename__ = "algorithms" @@ -16,4 +17,25 @@ class Algorithm(BaseModel): inference_time = Column(Float, comment="推理时间(ms)") is_enabled = Column(Boolean, default=True, comment="是否启用") creator = Column(String(50), comment="创建者") - tags = Column(Text, comment="标签,JSON格式") \ No newline at end of file + tags = Column(Text, comment="标签,JSON格式") + + def to_dict(self) -> dict: + """将模型实例转换为字典格式""" + return { + "id": self.id, + "name": self.name, + "description": self.description, + "version": self.version, + "model_path": self.model_path, + "config_path": self.config_path, + "status": self.status, + "accuracy": self.accuracy, + "detection_classes": self.detection_classes, + "input_size": self.input_size, + "inference_time": self.inference_time, + "is_enabled": self.is_enabled, + "creator": self.creator, + "tags": self.tags, + "created_at": self.created_at, + "updated_at": self.updated_at + } \ No newline at end of file diff --git a/server/readme.md b/server/readme.md index 2ce93fe..890f97d 100644 --- a/server/readme.md +++ b/server/readme.md @@ -1,177 +1,110 @@ # 边检CV算法接口服务 -### 技术栈 -- **后端框架**: FastAPI -- **数据库**: SQLite (SQLAlchemy ORM) -- **AI模型**: YOLOv11n -- **进程管理**: Supervisor -- **开发语言**: Python 3.8+ +## 项目简介 -### 项目结构 +这是一个简化的边检计算机视觉算法管理系统API服务,采用FastAPI框架开发。 + +## 项目结构 ``` server/ -├── app.py # FastAPI主应用 -├── start.py # 启动脚本 -├── requirements.txt # Python依赖 -├── env.example # 环境变量示例 -├── init_data.py # 示例数据初始化 -├── core/ # 核心配置 -│ └── database.py # 数据库配置 -├── models/ # 数据模型 +├── app.py # 主应用文件 +├── requirements.txt # 依赖包列表 +├── init_data.py # 初始化数据脚本 +├── env.example # 环境变量示例 +├── core/ # 核心配置 +│ └── database.py # 数据库配置 +├── models/ # 数据模型 │ ├── __init__.py -│ ├── base.py # 基础模型 -│ ├── algorithm.py # 算法模型 -│ ├── device.py # 设备模型 -│ └── event.py # 事件模型 -├── schemas/ # Pydantic模型 -│ ├── __init__.py -│ ├── algorithm.py # 算法相关模型 -│ ├── device.py # 设备相关模型 -│ └── event.py # 事件相关模型 -└── routers/ # API路由 +│ ├── base.py # 基础模型 +│ ├── algorithm.py # 算法模型 +│ ├── device.py # 设备模型 +│ └── event.py # 事件模型 +└── routers/ # 路由接口 ├── __init__.py - ├── algorithms.py # 算法管理接口 - ├── devices.py # 设备管理接口 - └── events.py # 事件管理接口 + ├── algorithms.py # 算法管理接口 + ├── devices.py # 设备管理接口 + └── events.py # 事件管理接口 ``` -### 功能模块 +## 简化设计 -#### 1. 算法管理模块 -- 算法的增删改查 -- 算法状态管理(启用/禁用) -- 算法版本管理 -- 算法性能指标(准确率、推理时间等) +本项目采用简化的架构设计: -#### 2. 设备管理模块 -- 设备信息管理(摄像头、传感器等) -- 设备状态监控 -- 设备类型管理 -- 设备地理位置信息 +1. **去掉schemas层**:直接使用字典进行数据传递,减少代码复杂度 +2. **简化响应格式**:所有接口直接返回字典格式,便于维护 +3. **保持核心功能**:保留所有必要的CRUD操作和业务逻辑 -#### 3. 事件管理模块 -- 事件记录和查询 -- 事件状态管理 -- 事件统计分析 -- 告警管理 +## API接口 -### API接口 +### 算法管理 (/api/algorithms) +- `POST /` - 创建算法 +- `GET /` - 获取算法列表 +- `GET /{id}` - 获取算法详情 +- `PUT /{id}` - 更新算法 +- `DELETE /{id}` - 删除算法 +- `PATCH /{id}/status` - 更新算法状态 +- `PATCH /{id}/enable` - 启用/禁用算法 -#### 算法管理接口 -- `POST /api/algorithms/` - 创建算法 -- `GET /api/algorithms/` - 获取算法列表 -- `GET /api/algorithms/{id}` - 获取算法详情 -- `PUT /api/algorithms/{id}` - 更新算法 -- `DELETE /api/algorithms/{id}` - 删除算法 -- `PATCH /api/algorithms/{id}/status` - 更新算法状态 -- `PATCH /api/algorithms/{id}/enable` - 启用/禁用算法 +### 设备管理 (/api/devices) +- `POST /` - 创建设备 +- `GET /` - 获取设备列表 +- `GET /{id}` - 获取设备详情 +- `PUT /{id}` - 更新设备 +- `DELETE /{id}` - 删除设备 +- `PATCH /{id}/status` - 更新设备状态 +- `PATCH /{id}/enable` - 启用/禁用设备 +- `GET /types/list` - 获取设备类型列表 +- `GET /status/stats` - 获取设备状态统计 -#### 设备管理接口 -- `POST /api/devices/` - 创建设备 -- `GET /api/devices/` - 获取设备列表 -- `GET /api/devices/{id}` - 获取设备详情 -- `PUT /api/devices/{id}` - 更新设备 -- `DELETE /api/devices/{id}` - 删除设备 -- `PATCH /api/devices/{id}/status` - 更新设备状态 -- `PATCH /api/devices/{id}/enable` - 启用/禁用设备 -- `GET /api/devices/types/list` - 获取设备类型列表 -- `GET /api/devices/status/stats` - 获取设备状态统计 +### 事件管理 (/api/events) +- `POST /` - 创建事件 +- `GET /` - 获取事件列表 +- `GET /{id}` - 获取事件详情 +- `PUT /{id}` - 更新事件 +- `DELETE /{id}` - 删除事件 +- `PATCH /{id}/status` - 更新事件状态 +- `GET /types/list` - 获取事件类型列表 +- `GET /stats/summary` - 获取事件统计摘要 +- `GET /stats/by-type` - 按类型统计事件 -#### 事件管理接口 -- `POST /api/events/` - 创建事件 -- `GET /api/events/` - 获取事件列表 -- `GET /api/events/{id}` - 获取事件详情 -- `PUT /api/events/{id}` - 更新事件 -- `DELETE /api/events/{id}` - 删除事件 -- `PATCH /api/events/{id}/status` - 更新事件状态 -- `GET /api/events/types/list` - 获取事件类型列表 -- `GET /api/events/stats/summary` - 获取事件统计摘要 -- `GET /api/events/stats/by-type` - 按类型统计事件 +## 安装和运行 -### 安装和运行 - -#### 1. 环境准备 +1. 安装依赖: ```bash -# 创建虚拟环境 -conda create -n border_inspection python=3.8 -conda activate border_inspection - -# 安装依赖 pip install -r requirements.txt ``` -#### 2. 初始化数据库 +2. 配置环境变量: ```bash -# 启动服务(会自动创建数据库表) -python app.py +cp env.example .env +# 编辑.env文件配置数据库连接等 ``` -#### 3. 初始化示例数据 +3. 初始化数据: ```bash python init_data.py ``` -#### 4. 启动服务 +4. 启动服务: ```bash python app.py ``` -服务将在 `http://localhost:8000` 启动 +服务将在 http://localhost:8000 启动,API文档访问 http://localhost:8000/docs -### API文档 +## 技术栈 -启动服务后,可以访问以下地址查看API文档: -- Swagger UI: `http://localhost:8000/docs` -- ReDoc: `http://localhost:8000/redoc` +- **框架**: FastAPI +- **数据库**: SQLAlchemy + SQLite +- **ORM**: SQLAlchemy +- **文档**: 自动生成OpenAPI文档 -### 数据库设计 +## 特点 -#### 算法表 (algorithms) -- 基本信息:名称、描述、版本 -- 模型信息:模型路径、配置文件路径 -- 性能指标:准确率、推理时间、输入尺寸 -- 状态管理:启用状态、算法状态 -- 分类信息:检测类别、标签 - -#### 设备表 (devices) -- 基本信息:名称、类型、位置 -- 连接信息:IP地址、端口、用户名、密码 -- 视频流:RTSP地址、分辨率、帧率 -- 状态信息:在线状态、最后心跳时间 -- 地理位置:经纬度坐标 - -#### 事件表 (events) -- 事件信息:类型、设备ID、算法ID -- 检测结果:置信度、边界框、检测对象 -- 状态管理:事件状态、严重程度 -- 告警信息:是否告警、告警发送状态 -- 处理信息:处理人员、处理备注、解决时间 - -### 开发说明 - -1. **添加新模型**: 在 `models/` 目录下创建新的模型文件 -2. **添加新接口**: 在 `routers/` 目录下创建新的路由文件 -3. **添加新Schema**: 在 `schemas/` 目录下创建新的Pydantic模型 -4. **数据库迁移**: 修改模型后重启服务,数据库表会自动更新 - -### 部署说明 - -#### 使用Supervisor管理进程 -```ini -[program:border_inspection] -command=python /path/to/server/start.py -directory=/path/to/server -user=www-data -autostart=true -autorestart=true -stderr_logfile=/var/log/border_inspection.err.log -stdout_logfile=/var/log/border_inspection.out.log -``` - -#### 环境变量配置 -复制 `env.example` 为 `.env` 并修改相应配置: -```bash -cp env.example .env -# 编辑 .env 文件 -``` \ No newline at end of file +- 简化的架构,易于维护 +- 完整的CRUD操作 +- 支持分页和筛选 +- 自动生成API文档 +- 支持CORS跨域 +- 健康检查接口 \ No newline at end of file diff --git a/server/requirements.txt b/server/requirements.txt index 5a7a9ec..10ba72a 100644 --- a/server/requirements.txt +++ b/server/requirements.txt @@ -9,4 +9,5 @@ python-dotenv==1.0.0 aiofiles==23.2.1 pillow==10.1.0 opencv-python==4.8.1.78 -ultralytics==8.0.196 \ No newline at end of file +ultralytics==8.0.196 +PyJWT==2.8.0 \ No newline at end of file diff --git a/server/routers/alarms.py b/server/routers/alarms.py new file mode 100644 index 0000000..667cd34 --- /dev/null +++ b/server/routers/alarms.py @@ -0,0 +1,236 @@ +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session +from typing import List, Optional, Dict, Any +from datetime import datetime +from core.database import get_db +from models.event import Event + +router = APIRouter() + +@router.get("/", summary="获取告警列表") +async def get_alarms( + page: int = Query(1, ge=1, description="页码"), + size: int = Query(20, ge=1, le=100, description="每页数量"), + severity: Optional[str] = Query(None, description="严重程度"), + status: Optional[str] = Query(None, description="告警状态"), + start_time: Optional[str] = Query(None, description="开始时间"), + end_time: Optional[str] = Query(None, description="结束时间"), + db: Session = Depends(get_db) +): + """获取告警列表,支持分页和筛选""" + try: + # 构建查询 - 只查询告警事件 + query = db.query(Event).filter(Event.is_alert == True) + + if severity: + query = query.filter(Event.severity == severity) + if status: + query = query.filter(Event.status == status) + if start_time: + try: + start_dt = datetime.fromisoformat(start_time.replace('Z', '+00:00')) + query = query.filter(Event.created_at >= start_dt) + except ValueError: + pass + if end_time: + try: + end_dt = datetime.fromisoformat(end_time.replace('Z', '+00:00')) + query = query.filter(Event.created_at <= end_dt) + except ValueError: + pass + + # 按创建时间倒序排列 + query = query.order_by(Event.created_at.desc()) + + # 计算总数 + total = query.count() + + # 分页 + skip = (page - 1) * size + events = query.offset(skip).limit(size).all() + + # 转换为告警格式 + alarms = [] + for event in events: + # TODO: 获取设备信息 + device_name = f"设备{event.device_id}" # 临时设备名称 + + alarms.append({ + "id": event.id, + "type": event.event_type, + "severity": event.severity, + "status": event.status, + "device": device_name, + "created_at": event.created_at.isoformat(), + "description": event.description + }) + + return { + "alarms": alarms, + "total": total, + "page": page, + "size": size + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"获取告警列表失败: {str(e)}") + +@router.get("/{alarm_id}", summary="获取告警详情") +async def get_alarm_detail( + alarm_id: int, + db: Session = Depends(get_db) +): + """根据ID获取告警详情""" + try: + event = db.query(Event).filter( + Event.id == alarm_id, + Event.is_alert == True + ).first() + + if not event: + raise HTTPException(status_code=404, detail="告警不存在") + + # TODO: 获取设备信息 + device_name = f"设备{event.device_id}" + + return { + "id": event.id, + "type": event.event_type, + "severity": event.severity, + "status": event.status, + "device": device_name, + "created_at": event.created_at.isoformat(), + "description": event.description, + "image_path": event.image_path, + "video_path": event.video_path, + "confidence": event.confidence, + "bbox": event.bbox, + "resolution_notes": event.resolution_notes, + "resolved_at": event.resolved_at.isoformat() if event.resolved_at else None + } + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"获取告警详情失败: {str(e)}") + +@router.patch("/{alarm_id}/resolve", summary="处理告警") +async def resolve_alarm( + alarm_id: int, + resolution_data: Dict[str, Any], + db: Session = Depends(get_db) +): + """处理告警""" + try: + event = db.query(Event).filter( + Event.id == alarm_id, + Event.is_alert == True + ).first() + + if not event: + raise HTTPException(status_code=404, detail="告警不存在") + + # 更新告警状态 + event.status = "resolved" + event.resolution_notes = resolution_data.get("resolution_notes", "") + event.resolved_at = datetime.now() + + db.commit() + + return { + "id": event.id, + "status": "resolved", + "resolved_at": event.resolved_at.isoformat(), + "message": "告警已处理" + } + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"处理告警失败: {str(e)}") + +@router.get("/stats", summary="获取告警统计") +async def get_alarm_stats(db: Session = Depends(get_db)): + """获取告警统计数据""" + try: + # 总告警数 + total_alarms = db.query(Event).filter(Event.is_alert == True).count() + + # 待处理告警数 + pending_alarms = db.query(Event).filter( + Event.is_alert == True, + Event.status == "pending" + ).count() + + # 已处理告警数 + resolved_alarms = db.query(Event).filter( + Event.is_alert == True, + Event.status == "resolved" + ).count() + + # TODO: 按严重程度统计 + # 当前返回模拟数据 + by_severity = [ + {"severity": "high", "count": 12}, + {"severity": "medium", "count": 45}, + {"severity": "low", "count": 32} + ] + + return { + "total_alarms": total_alarms, + "pending_alarms": pending_alarms, + "resolved_alarms": resolved_alarms, + "by_severity": by_severity + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"获取告警统计失败: {str(e)}") + +@router.get("/types/list", summary="获取告警类型列表") +async def get_alarm_types(): + """获取告警类型列表""" + try: + # TODO: 从数据库获取告警类型 + # 当前返回固定类型 + alarm_types = [ + "船舶靠泊", + "船舶离泊", + "人员登轮", + "人员离轮", + "电脑弹窗", + "越界检测", + "车辆识别", + "货物识别" + ] + + return { + "alarm_types": alarm_types + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"获取告警类型失败: {str(e)}") + +@router.get("/trend", summary="获取告警趋势") +async def get_alarm_trend( + days: int = Query(7, ge=1, le=30, description="统计天数"), + db: Session = Depends(get_db) +): + """获取告警趋势数据""" + try: + # TODO: 实现真实的告警趋势统计 + # 当前返回模拟数据 + from datetime import timedelta + + end_date = datetime.now().date() + start_date = end_date - timedelta(days=days-1) + + dates = [] + alarm_counts = [] + + for i in range(days): + current_date = start_date + timedelta(days=i) + dates.append(current_date.strftime("%Y-%m-%d")) + # 模拟数据 + alarm_counts.append(10 + (i * 2) % 20) + + return { + "dates": dates, + "alarm_counts": alarm_counts + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"获取告警趋势失败: {str(e)}") \ No newline at end of file diff --git a/server/routers/algorithms.py b/server/routers/algorithms.py index fe3a890..bf2c5fe 100644 --- a/server/routers/algorithms.py +++ b/server/routers/algorithms.py @@ -1,30 +1,25 @@ from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session -from typing import List, Optional +from typing import List, Optional, Dict, Any from core.database import get_db from models.algorithm import Algorithm -from schemas.algorithm import ( - AlgorithmCreate, - AlgorithmUpdate, - AlgorithmResponse, - AlgorithmListResponse -) router = APIRouter() -@router.post("/", response_model=AlgorithmResponse, summary="创建算法") +@router.post("/", summary="创建算法") async def create_algorithm( - algorithm: AlgorithmCreate, + algorithm_data: Dict[str, Any], db: Session = Depends(get_db) ): """创建新的算法""" - db_algorithm = Algorithm(**algorithm.dict()) + db_algorithm = Algorithm(**algorithm_data) db.add(db_algorithm) db.commit() db.refresh(db_algorithm) - return db_algorithm + + return db_algorithm.to_dict() -@router.get("/", response_model=AlgorithmListResponse, summary="获取算法列表") +@router.get("/", summary="获取算法列表") async def get_algorithms( skip: int = Query(0, ge=0, description="跳过记录数"), limit: int = Query(10, ge=1, le=100, description="返回记录数"), @@ -46,14 +41,16 @@ async def get_algorithms( total = query.count() algorithms = query.offset(skip).limit(limit).all() - return AlgorithmListResponse( - algorithms=algorithms, - total=total, - page=skip // limit + 1, - size=limit - ) + algorithm_list = [algorithm.to_dict() for algorithm in algorithms] + + return { + "algorithms": algorithm_list, + "total": total, + "page": skip // limit + 1, + "size": limit + } -@router.get("/{algorithm_id}", response_model=AlgorithmResponse, summary="获取算法详情") +@router.get("/{algorithm_id}", summary="获取算法详情") async def get_algorithm( algorithm_id: int, db: Session = Depends(get_db) @@ -62,12 +59,13 @@ async def get_algorithm( algorithm = db.query(Algorithm).filter(Algorithm.id == algorithm_id).first() if not algorithm: raise HTTPException(status_code=404, detail="算法不存在") - return algorithm + + return algorithm.to_dict() -@router.put("/{algorithm_id}", response_model=AlgorithmResponse, summary="更新算法") +@router.put("/{algorithm_id}", summary="更新算法") async def update_algorithm( algorithm_id: int, - algorithm: AlgorithmUpdate, + algorithm_data: Dict[str, Any], db: Session = Depends(get_db) ): """更新算法信息""" @@ -75,13 +73,14 @@ async def update_algorithm( if not db_algorithm: raise HTTPException(status_code=404, detail="算法不存在") - update_data = algorithm.dict(exclude_unset=True) - for field, value in update_data.items(): - setattr(db_algorithm, field, value) + for field, value in algorithm_data.items(): + if hasattr(db_algorithm, field): + setattr(db_algorithm, field, value) db.commit() db.refresh(db_algorithm) - return db_algorithm + + return db_algorithm.to_dict() @router.delete("/{algorithm_id}", summary="删除算法") async def delete_algorithm( @@ -97,7 +96,7 @@ async def delete_algorithm( db.commit() return {"message": "算法删除成功"} -@router.patch("/{algorithm_id}/status", response_model=AlgorithmResponse, summary="更新算法状态") +@router.patch("/{algorithm_id}/status", summary="更新算法状态") async def update_algorithm_status( algorithm_id: int, status: str = Query(..., description="新状态"), @@ -111,9 +110,10 @@ async def update_algorithm_status( algorithm.status = status db.commit() db.refresh(algorithm) - return algorithm + + return algorithm.to_dict() -@router.patch("/{algorithm_id}/enable", response_model=AlgorithmResponse, summary="启用/禁用算法") +@router.patch("/{algorithm_id}/enable", summary="启用/禁用算法") async def toggle_algorithm_enabled( algorithm_id: int, enabled: bool = Query(..., description="是否启用"), @@ -127,4 +127,5 @@ async def toggle_algorithm_enabled( algorithm.is_enabled = enabled db.commit() db.refresh(algorithm) - return algorithm \ No newline at end of file + + return algorithm.to_dict() \ No newline at end of file diff --git a/server/routers/auth.py b/server/routers/auth.py new file mode 100644 index 0000000..a450ae0 --- /dev/null +++ b/server/routers/auth.py @@ -0,0 +1,237 @@ +from fastapi import APIRouter, Depends, HTTPException, HTTPException, Query +from sqlalchemy.orm import Session +from typing import List, Optional, Dict, Any +from datetime import datetime, timedelta +import jwt +from passlib.context import CryptContext +from core.database import get_db + +router = APIRouter() + +# 密码加密上下文 +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +# JWT配置 +SECRET_KEY = "your-secret-key-here" # TODO: 从环境变量获取 +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + +# 模拟用户数据 +USERS = { + "admin": { + "id": 1, + "username": "admin", + "email": "admin@example.com", + "hashed_password": pwd_context.hash("admin123"), + "role": "admin", + "permissions": ["read", "write", "admin"] + }, + "operator": { + "id": 2, + "username": "operator", + "email": "operator@example.com", + "hashed_password": pwd_context.hash("operator123"), + "role": "operator", + "permissions": ["read", "write"] + } +} + +def verify_password(plain_password: str, hashed_password: str) -> bool: + """验证密码""" + return pwd_context.verify(plain_password, hashed_password) + +def get_user(username: str): + """获取用户信息""" + if username in USERS: + return USERS[username] + return None + +def authenticate_user(username: str, password: str): + """验证用户""" + user = get_user(username) + if not user: + return False + if not verify_password(password, user["hashed_password"]): + return False + return user + +def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): + """创建访问令牌""" + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + +@router.post("/login", summary="用户登录") +async def login( + username: str, + password: str, + db: Session = Depends(get_db) +): + """用户登录""" + try: + user = authenticate_user(username, password) + if not user: + raise HTTPException( + status_code=401, + detail="用户名或密码错误", + headers={"WWW-Authenticate": "Bearer"}, + ) + + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": user["username"]}, expires_delta=access_token_expires + ) + + return { + "access_token": access_token, + "token_type": "bearer", + "expires_in": ACCESS_TOKEN_EXPIRE_MINUTES * 60, + "user": { + "id": user["id"], + "username": user["username"], + "email": user["email"], + "role": user["role"] + } + } + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"登录失败: {str(e)}") + +@router.get("/profile", summary="获取用户信息") +async def get_user_profile( + current_user: str = Depends(lambda: "admin"), # TODO: 实现真实的JWT验证 + db: Session = Depends(get_db) +): + """获取当前用户信息""" + try: + user = get_user(current_user) + if not user: + raise HTTPException(status_code=404, detail="用户不存在") + + return { + "id": user["id"], + "username": user["username"], + "email": user["email"], + "role": user["role"], + "permissions": user["permissions"] + } + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"获取用户信息失败: {str(e)}") + +@router.post("/logout", summary="用户登出") +async def logout( + current_user: str = Depends(lambda: "admin"), # TODO: 实现真实的JWT验证 + db: Session = Depends(get_db) +): + """用户登出""" + try: + # TODO: 实现真实的登出逻辑(如将token加入黑名单) + return { + "message": "登出成功" + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"登出失败: {str(e)}") + +@router.post("/refresh", summary="刷新访问令牌") +async def refresh_token( + current_user: str = Depends(lambda: "admin"), # TODO: 实现真实的JWT验证 + db: Session = Depends(get_db) +): + """刷新访问令牌""" + try: + user = get_user(current_user) + if not user: + raise HTTPException(status_code=404, detail="用户不存在") + + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": user["username"]}, expires_delta=access_token_expires + ) + + return { + "access_token": access_token, + "token_type": "bearer", + "expires_in": ACCESS_TOKEN_EXPIRE_MINUTES * 60 + } + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"刷新令牌失败: {str(e)}") + +@router.get("/users", summary="获取用户列表") +async def get_users( + page: int = Query(1, ge=1, description="页码"), + size: int = Query(20, ge=1, le=100, description="每页数量"), + role: Optional[str] = Query(None, description="角色筛选"), + db: Session = Depends(get_db) +): + """获取用户列表""" + try: + # TODO: 实现真实的用户列表查询 + # 当前返回模拟数据 + users = [ + { + "id": 1, + "username": "admin", + "email": "admin@example.com", + "role": "admin", + "status": "active", + "created_at": "2024-01-01T00:00:00Z" + }, + { + "id": 2, + "username": "operator", + "email": "operator@example.com", + "role": "operator", + "status": "active", + "created_at": "2024-01-02T00:00:00Z" + } + ] + + # 角色筛选 + if role: + users = [u for u in users if u["role"] == role] + + total = len(users) + start = (page - 1) * size + end = start + size + paginated_users = users[start:end] + + return { + "users": paginated_users, + "total": total, + "page": page, + "size": size + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"获取用户列表失败: {str(e)}") + +@router.post("/users", summary="创建用户") +async def create_user( + user_data: Dict[str, Any], + db: Session = Depends(get_db) +): + """创建新用户""" + try: + # TODO: 实现真实的用户创建 + # 当前返回模拟数据 + new_user = { + "id": 3, + "username": user_data.get("username", "newuser"), + "email": user_data.get("email", "newuser@example.com"), + "role": user_data.get("role", "operator"), + "status": "active", + "created_at": datetime.now().isoformat() + } + + return new_user + except Exception as e: + raise HTTPException(status_code=500, detail=f"创建用户失败: {str(e)}") \ No newline at end of file diff --git a/server/routers/dashboard.py b/server/routers/dashboard.py new file mode 100644 index 0000000..c8842cd --- /dev/null +++ b/server/routers/dashboard.py @@ -0,0 +1,162 @@ +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session +from typing import List, Optional, Dict, Any +from datetime import datetime, timedelta +from core.database import get_db +from models.device import Device +from models.algorithm import Algorithm +from models.event import Event + +router = APIRouter() + +@router.get("/kpi", summary="获取主要KPI指标") +async def get_dashboard_kpi(db: Session = Depends(get_db)): + """获取仪表板主要KPI指标""" + try: + # 设备统计 + total_devices = db.query(Device).count() + online_devices = db.query(Device).filter(Device.status == "online").count() + + # 算法统计 + total_algorithms = db.query(Algorithm).count() + active_algorithms = db.query(Algorithm).filter(Algorithm.is_enabled == True).count() + + # 事件统计 + total_events = db.query(Event).count() + today = datetime.now().date() + today_events = db.query(Event).filter( + Event.created_at >= today + ).count() + + # 告警事件统计 + alert_events = db.query(Event).filter(Event.is_alert == True).count() + resolved_events = db.query(Event).filter(Event.status == "resolved").count() + + return { + "total_devices": total_devices, + "online_devices": online_devices, + "total_algorithms": total_algorithms, + "active_algorithms": active_algorithms, + "total_events": total_events, + "today_events": today_events, + "alert_events": alert_events, + "resolved_events": resolved_events + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"获取KPI数据失败: {str(e)}") + +@router.get("/alarm-trend", summary="获取告警趋势统计") +async def get_alarm_trend( + days: int = Query(7, ge=1, le=30, description="统计天数"), + db: Session = Depends(get_db) +): + """获取告警趋势统计数据""" + try: + # TODO: 实现真实的告警趋势统计 + # 当前返回模拟数据 + end_date = datetime.now().date() + start_date = end_date - timedelta(days=days-1) + + dates = [] + alarms = [] + resolved = [] + + for i in range(days): + current_date = start_date + timedelta(days=i) + dates.append(current_date.strftime("%Y-%m-%d")) + # 模拟数据 + alarms.append(10 + (i * 2) % 20) + resolved.append(8 + (i * 2) % 15) + + return { + "dates": dates, + "alarms": alarms, + "resolved": resolved + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"获取告警趋势数据失败: {str(e)}") + +@router.get("/camera-stats", summary="获取摄像头统计") +async def get_camera_stats(db: Session = Depends(get_db)): + """获取摄像头统计数据""" + try: + # 设备统计 + total_cameras = db.query(Device).filter(Device.device_type == "camera").count() + online_cameras = db.query(Device).filter( + Device.device_type == "camera", + Device.status == "online" + ).count() + offline_cameras = total_cameras - online_cameras + + # TODO: 实现按位置统计 + # 当前返回模拟数据 + by_location = [ + {"location": "港口区", "total": 45, "online": 42}, + {"location": "码头区", "total": 38, "online": 35}, + {"location": "办公区", "total": 23, "online": 21} + ] + + return { + "total_cameras": total_cameras, + "online_cameras": online_cameras, + "offline_cameras": offline_cameras, + "by_location": by_location + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"获取摄像头统计数据失败: {str(e)}") + +@router.get("/algorithm-stats", summary="获取算法统计") +async def get_algorithm_stats(db: Session = Depends(get_db)): + """获取算法统计数据""" + try: + total_algorithms = db.query(Algorithm).count() + active_algorithms = db.query(Algorithm).filter(Algorithm.is_enabled == True).count() + + # TODO: 实现按类型统计 + # 当前返回模拟数据 + by_type = [ + {"type": "目标检测", "count": 3, "accuracy": 95.2}, + {"type": "行为识别", "count": 2, "accuracy": 88.7}, + {"type": "越界检测", "count": 3, "accuracy": 92.1} + ] + + return { + "total_algorithms": total_algorithms, + "active_algorithms": active_algorithms, + "by_type": by_type + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"获取算法统计数据失败: {str(e)}") + +@router.get("/event-hotspots", summary="获取事件热点统计") +async def get_event_hotspots(db: Session = Depends(get_db)): + """获取事件热点统计数据""" + try: + # TODO: 实现真实的事件热点统计 + # 当前返回模拟数据 + hotspots = [ + { + "location": "港口A区", + "event_count": 45, + "severity": "high", + "coordinates": {"lat": 31.2304, "lng": 121.4737} + }, + { + "location": "码头B区", + "event_count": 32, + "severity": "medium", + "coordinates": {"lat": 31.2404, "lng": 121.4837} + }, + { + "location": "办公区C区", + "event_count": 18, + "severity": "low", + "coordinates": {"lat": 31.2204, "lng": 121.4637} + } + ] + + return { + "hotspots": hotspots + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"获取事件热点数据失败: {str(e)}") \ No newline at end of file diff --git a/server/routers/devices.py b/server/routers/devices.py index 283a6fe..971d1b1 100644 --- a/server/routers/devices.py +++ b/server/routers/devices.py @@ -1,30 +1,39 @@ from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session -from typing import List, Optional +from typing import List, Optional, Dict, Any from core.database import get_db from models.device import Device -from schemas.device import ( - DeviceCreate, - DeviceUpdate, - DeviceResponse, - DeviceListResponse -) router = APIRouter() -@router.post("/", response_model=DeviceResponse, summary="创建设备") +@router.post("/", summary="创建设备") async def create_device( - device: DeviceCreate, + device_data: Dict[str, Any], db: Session = Depends(get_db) ): """创建新的设备""" - db_device = Device(**device.dict()) + db_device = Device(**device_data) db.add(db_device) db.commit() db.refresh(db_device) - return db_device + + return { + "id": db_device.id, + "name": db_device.name, + "device_type": db_device.device_type, + "ip_address": db_device.ip_address, + "port": db_device.port, + "username": db_device.username, + "password": db_device.password, + "location": db_device.location, + "status": db_device.status, + "is_enabled": db_device.is_enabled, + "description": db_device.description, + "created_at": db_device.created_at, + "updated_at": db_device.updated_at + } -@router.get("/", response_model=DeviceListResponse, summary="获取设备列表") +@router.get("/", summary="获取设备列表") async def get_devices( skip: int = Query(0, ge=0, description="跳过记录数"), limit: int = Query(10, ge=1, le=100, description="返回记录数"), @@ -52,14 +61,32 @@ async def get_devices( total = query.count() devices = query.offset(skip).limit(limit).all() - return DeviceListResponse( - devices=devices, - total=total, - page=skip // limit + 1, - size=limit - ) + device_list = [] + for device in devices: + device_list.append({ + "id": device.id, + "name": device.name, + "device_type": device.device_type, + "ip_address": device.ip_address, + "port": device.port, + "username": device.username, + "password": device.password, + "location": device.location, + "status": device.status, + "is_enabled": device.is_enabled, + "description": device.description, + "created_at": device.created_at, + "updated_at": device.updated_at + }) + + return { + "devices": device_list, + "total": total, + "page": skip // limit + 1, + "size": limit + } -@router.get("/{device_id}", response_model=DeviceResponse, summary="获取设备详情") +@router.get("/{device_id}", summary="获取设备详情") async def get_device( device_id: int, db: Session = Depends(get_db) @@ -68,12 +95,27 @@ async def get_device( device = db.query(Device).filter(Device.id == device_id).first() if not device: raise HTTPException(status_code=404, detail="设备不存在") - return device + + return { + "id": device.id, + "name": device.name, + "device_type": device.device_type, + "ip_address": device.ip_address, + "port": device.port, + "username": device.username, + "password": device.password, + "location": device.location, + "status": device.status, + "is_enabled": device.is_enabled, + "description": device.description, + "created_at": device.created_at, + "updated_at": device.updated_at + } -@router.put("/{device_id}", response_model=DeviceResponse, summary="更新设备") +@router.put("/{device_id}", summary="更新设备") async def update_device( device_id: int, - device: DeviceUpdate, + device_data: Dict[str, Any], db: Session = Depends(get_db) ): """更新设备信息""" @@ -81,13 +123,28 @@ async def update_device( if not db_device: raise HTTPException(status_code=404, detail="设备不存在") - update_data = device.dict(exclude_unset=True) - for field, value in update_data.items(): - setattr(db_device, field, value) + for field, value in device_data.items(): + if hasattr(db_device, field): + setattr(db_device, field, value) db.commit() db.refresh(db_device) - return db_device + + return { + "id": db_device.id, + "name": db_device.name, + "device_type": db_device.device_type, + "ip_address": db_device.ip_address, + "port": db_device.port, + "username": db_device.username, + "password": db_device.password, + "location": db_device.location, + "status": db_device.status, + "is_enabled": db_device.is_enabled, + "description": db_device.description, + "created_at": db_device.created_at, + "updated_at": db_device.updated_at + } @router.delete("/{device_id}", summary="删除设备") async def delete_device( @@ -103,7 +160,7 @@ async def delete_device( db.commit() return {"message": "设备删除成功"} -@router.patch("/{device_id}/status", response_model=DeviceResponse, summary="更新设备状态") +@router.patch("/{device_id}/status", summary="更新设备状态") async def update_device_status( device_id: int, status: str = Query(..., description="新状态"), @@ -117,9 +174,24 @@ async def update_device_status( device.status = status db.commit() db.refresh(device) - return device + + return { + "id": device.id, + "name": device.name, + "device_type": device.device_type, + "ip_address": device.ip_address, + "port": device.port, + "username": device.username, + "password": device.password, + "location": device.location, + "status": device.status, + "is_enabled": device.is_enabled, + "description": device.description, + "created_at": device.created_at, + "updated_at": device.updated_at + } -@router.patch("/{device_id}/enable", response_model=DeviceResponse, summary="启用/禁用设备") +@router.patch("/{device_id}/enable", summary="启用/禁用设备") async def toggle_device_enabled( device_id: int, enabled: bool = Query(..., description="是否启用"), @@ -133,7 +205,22 @@ async def toggle_device_enabled( device.is_enabled = enabled db.commit() db.refresh(device) - return device + + return { + "id": device.id, + "name": device.name, + "device_type": device.device_type, + "ip_address": device.ip_address, + "port": device.port, + "username": device.username, + "password": device.password, + "location": device.location, + "status": device.status, + "is_enabled": device.is_enabled, + "description": device.description, + "created_at": device.created_at, + "updated_at": device.updated_at + } @router.get("/types/list", summary="获取设备类型列表") async def get_device_types(): diff --git a/server/routers/events.py b/server/routers/events.py index 341f553..cbe4a72 100644 --- a/server/routers/events.py +++ b/server/routers/events.py @@ -1,31 +1,43 @@ from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session -from typing import List, Optional +from typing import List, Optional, Dict, Any from datetime import datetime from core.database import get_db from models.event import Event -from schemas.event import ( - EventCreate, - EventUpdate, - EventResponse, - EventListResponse -) router = APIRouter() -@router.post("/", response_model=EventResponse, summary="创建事件") +@router.post("/", summary="创建事件") async def create_event( - event: EventCreate, + event_data: Dict[str, Any], db: Session = Depends(get_db) ): """创建新的事件""" - db_event = Event(**event.dict()) + db_event = Event(**event_data) db.add(db_event) db.commit() db.refresh(db_event) - return db_event + + return { + "id": db_event.id, + "event_type": db_event.event_type, + "device_id": db_event.device_id, + "algorithm_id": db_event.algorithm_id, + "severity": db_event.severity, + "status": db_event.status, + "is_alert": db_event.is_alert, + "description": db_event.description, + "image_path": db_event.image_path, + "video_path": db_event.video_path, + "confidence": db_event.confidence, + "bbox": db_event.bbox, + "resolution_notes": db_event.resolution_notes, + "resolved_at": db_event.resolved_at, + "created_at": db_event.created_at, + "updated_at": db_event.updated_at + } -@router.get("/", response_model=EventListResponse, summary="获取事件列表") +@router.get("/", summary="获取事件列表") async def get_events( skip: int = Query(0, ge=0, description="跳过记录数"), limit: int = Query(10, ge=1, le=100, description="返回记录数"), @@ -73,14 +85,35 @@ async def get_events( total = query.count() events = query.offset(skip).limit(limit).all() - return EventListResponse( - events=events, - total=total, - page=skip // limit + 1, - size=limit - ) + event_list = [] + for event in events: + event_list.append({ + "id": event.id, + "event_type": event.event_type, + "device_id": event.device_id, + "algorithm_id": event.algorithm_id, + "severity": event.severity, + "status": event.status, + "is_alert": event.is_alert, + "description": event.description, + "image_path": event.image_path, + "video_path": event.video_path, + "confidence": event.confidence, + "bbox": event.bbox, + "resolution_notes": event.resolution_notes, + "resolved_at": event.resolved_at, + "created_at": event.created_at, + "updated_at": event.updated_at + }) + + return { + "events": event_list, + "total": total, + "page": skip // limit + 1, + "size": limit + } -@router.get("/{event_id}", response_model=EventResponse, summary="获取事件详情") +@router.get("/{event_id}", summary="获取事件详情") async def get_event( event_id: int, db: Session = Depends(get_db) @@ -89,12 +122,30 @@ async def get_event( event = db.query(Event).filter(Event.id == event_id).first() if not event: raise HTTPException(status_code=404, detail="事件不存在") - return event + + return { + "id": event.id, + "event_type": event.event_type, + "device_id": event.device_id, + "algorithm_id": event.algorithm_id, + "severity": event.severity, + "status": event.status, + "is_alert": event.is_alert, + "description": event.description, + "image_path": event.image_path, + "video_path": event.video_path, + "confidence": event.confidence, + "bbox": event.bbox, + "resolution_notes": event.resolution_notes, + "resolved_at": event.resolved_at, + "created_at": event.created_at, + "updated_at": event.updated_at + } -@router.put("/{event_id}", response_model=EventResponse, summary="更新事件") +@router.put("/{event_id}", summary="更新事件") async def update_event( event_id: int, - event: EventUpdate, + event_data: Dict[str, Any], db: Session = Depends(get_db) ): """更新事件信息""" @@ -102,13 +153,31 @@ async def update_event( if not db_event: raise HTTPException(status_code=404, detail="事件不存在") - update_data = event.dict(exclude_unset=True) - for field, value in update_data.items(): - setattr(db_event, field, value) + for field, value in event_data.items(): + if hasattr(db_event, field): + setattr(db_event, field, value) db.commit() db.refresh(db_event) - return db_event + + return { + "id": db_event.id, + "event_type": db_event.event_type, + "device_id": db_event.device_id, + "algorithm_id": db_event.algorithm_id, + "severity": db_event.severity, + "status": db_event.status, + "is_alert": db_event.is_alert, + "description": db_event.description, + "image_path": db_event.image_path, + "video_path": db_event.video_path, + "confidence": db_event.confidence, + "bbox": db_event.bbox, + "resolution_notes": db_event.resolution_notes, + "resolved_at": db_event.resolved_at, + "created_at": db_event.created_at, + "updated_at": db_event.updated_at + } @router.delete("/{event_id}", summary="删除事件") async def delete_event( @@ -124,7 +193,7 @@ async def delete_event( db.commit() return {"message": "事件删除成功"} -@router.patch("/{event_id}/status", response_model=EventResponse, summary="更新事件状态") +@router.patch("/{event_id}/status", summary="更新事件状态") async def update_event_status( event_id: int, status: str = Query(..., description="新状态"), @@ -146,7 +215,25 @@ async def update_event_status( db.commit() db.refresh(event) - return event + + return { + "id": event.id, + "event_type": event.event_type, + "device_id": event.device_id, + "algorithm_id": event.algorithm_id, + "severity": event.severity, + "status": event.status, + "is_alert": event.is_alert, + "description": event.description, + "image_path": event.image_path, + "video_path": event.video_path, + "confidence": event.confidence, + "bbox": event.bbox, + "resolution_notes": event.resolution_notes, + "resolved_at": event.resolved_at, + "created_at": event.created_at, + "updated_at": event.updated_at + } @router.get("/types/list", summary="获取事件类型列表") async def get_event_types(): diff --git a/server/routers/monitors.py b/server/routers/monitors.py new file mode 100644 index 0000000..643b303 --- /dev/null +++ b/server/routers/monitors.py @@ -0,0 +1,185 @@ +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session +from typing import List, Optional, Dict, Any +from core.database import get_db +from models.device import Device +from models.event import Event +from models.algorithm import Algorithm + +router = APIRouter() + +@router.get("/", summary="获取监控列表") +async def get_monitors( + page: int = Query(1, ge=1, description="页码"), + size: int = Query(20, ge=1, le=100, description="每页数量"), + status: Optional[str] = Query(None, description="监控状态"), + location: Optional[str] = Query(None, description="位置筛选"), + db: Session = Depends(get_db) +): + """获取监控列表,支持分页和筛选""" + try: + # 构建查询 + query = db.query(Device).filter(Device.device_type == "camera") + + if status: + query = query.filter(Device.status == status) + if location: + query = query.filter(Device.location.contains(location)) + + # 计算总数 + total = query.count() + + # 分页 + skip = (page - 1) * size + devices = query.offset(skip).limit(size).all() + + # 转换为监控格式 + monitors = [] + for device in devices: + # TODO: 获取实时检测数据 + detections = [] + if device.status == "online": + # 模拟检测数据 + detections = [ + {"type": "person", "x": 25, "y": 35, "width": 40, "height": 80} + ] + + monitors.append({ + "id": device.id, + "name": device.name, + "location": device.location, + "status": device.status, + "video_url": f"/videos/port-{device.id}.mp4", # TODO: 实现真实视频URL + "detections": detections + }) + + return { + "monitors": monitors, + "total": total, + "page": page, + "size": size + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"获取监控列表失败: {str(e)}") + +@router.get("/{monitor_id}", summary="获取监控详情") +async def get_monitor_detail( + monitor_id: int, + db: Session = Depends(get_db) +): + """根据ID获取监控详情""" + try: + device = db.query(Device).filter( + Device.id == monitor_id, + Device.device_type == "camera" + ).first() + + if not device: + raise HTTPException(status_code=404, detail="监控不存在") + + # TODO: 获取实时检测数据 + detections = [] + if device.status == "online": + detections = [ + {"type": "person", "x": 25, "y": 35, "width": 40, "height": 80}, + {"type": "vehicle", "x": 40, "y": 50, "width": 80, "height": 60} + ] + + # TODO: 获取相关事件 + events = [] + + # TODO: 获取相关算法 + algorithms = [] + + return { + "id": device.id, + "name": device.name, + "location": device.location, + "status": device.status, + "video_url": f"/videos/port-{device.id}.mp4", # TODO: 实现真实视频URL + "detections": detections, + "events": events, + "algorithms": algorithms + } + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"获取监控详情失败: {str(e)}") + +@router.get("/{monitor_id}/stream", summary="获取监控视频流") +async def get_monitor_stream( + monitor_id: int, + db: Session = Depends(get_db) +): + """获取监控视频流URL""" + try: + device = db.query(Device).filter( + Device.id == monitor_id, + Device.device_type == "camera" + ).first() + + if not device: + raise HTTPException(status_code=404, detail="监控不存在") + + # TODO: 实现真实的视频流URL生成 + stream_url = f"rtsp://{device.ip_address}:554/stream" + + return { + "monitor_id": monitor_id, + "stream_url": stream_url, + "status": device.status + } + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"获取视频流失败: {str(e)}") + +@router.get("/{monitor_id}/detections", summary="获取监控检测数据") +async def get_monitor_detections( + monitor_id: int, + db: Session = Depends(get_db) +): + """获取监控实时检测数据""" + try: + device = db.query(Device).filter( + Device.id == monitor_id, + Device.device_type == "camera" + ).first() + + if not device: + raise HTTPException(status_code=404, detail="监控不存在") + + # TODO: 实现真实的检测数据获取 + # 当前返回模拟数据 + detections = [] + if device.status == "online": + detections = [ + { + "type": "person", + "x": 25, + "y": 35, + "width": 40, + "height": 80, + "confidence": 0.95, + "timestamp": "2024-01-15T10:30:00Z" + }, + { + "type": "vehicle", + "x": 40, + "y": 50, + "width": 80, + "height": 60, + "confidence": 0.88, + "timestamp": "2024-01-15T10:30:00Z" + } + ] + + return { + "monitor_id": monitor_id, + "detections": detections, + "timestamp": "2024-01-15T10:30:00Z" + } + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"获取检测数据失败: {str(e)}") \ No newline at end of file diff --git a/server/routers/scenes.py b/server/routers/scenes.py new file mode 100644 index 0000000..4100819 --- /dev/null +++ b/server/routers/scenes.py @@ -0,0 +1,233 @@ +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session +from typing import List, Optional, Dict, Any +from core.database import get_db +from models.device import Device +from models.algorithm import Algorithm +from models.event import Event + +router = APIRouter() + +@router.get("/", summary="获取场景列表") +async def get_scenes(db: Session = Depends(get_db)): + """获取场景列表""" + try: + # TODO: 实现真实的场景管理 + # 当前返回模拟数据 + scenes = [ + { + "id": "scene-001", + "name": "港口区场景", + "description": "港口区监控场景", + "device_count": 45, + "algorithm_count": 3, + "created_at": "2024-01-01T00:00:00Z" + }, + { + "id": "scene-002", + "name": "码头区场景", + "description": "码头区监控场景", + "device_count": 38, + "algorithm_count": 2, + "created_at": "2024-01-02T00:00:00Z" + }, + { + "id": "scene-003", + "name": "办公区场景", + "description": "办公区监控场景", + "device_count": 23, + "algorithm_count": 1, + "created_at": "2024-01-03T00:00:00Z" + } + ] + + return { + "scenes": scenes + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"获取场景列表失败: {str(e)}") + +@router.get("/{scene_id}", summary="获取场景详情") +async def get_scene_detail( + scene_id: str, + db: Session = Depends(get_db) +): + """根据ID获取场景详情""" + try: + # TODO: 实现真实的场景查询 + # 当前返回模拟数据 + scene_data = { + "scene-001": { + "id": "scene-001", + "name": "港口区场景", + "description": "港口区监控场景", + "devices": [ + { + "id": 1, + "name": "港口区监控1", + "status": "online", + "location": "港口A区" + }, + { + "id": 2, + "name": "港口区监控2", + "status": "online", + "location": "港口B区" + } + ], + "algorithms": [ + { + "id": 1, + "name": "船舶靠泊识别", + "status": "active" + }, + { + "id": 2, + "name": "人员登轮识别", + "status": "active" + } + ], + "events": [ + { + "id": 1, + "type": "船舶靠泊", + "severity": "high", + "created_at": "2024-01-15T10:30:00Z" + } + ] + } + } + + scene = scene_data.get(scene_id) + if not scene: + raise HTTPException(status_code=404, detail="场景不存在") + + return scene + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"获取场景详情失败: {str(e)}") + +@router.post("/", summary="创建场景") +async def create_scene( + scene_data: Dict[str, Any], + db: Session = Depends(get_db) +): + """创建新场景""" + try: + # TODO: 实现真实的场景创建 + # 当前返回模拟数据 + new_scene = { + "id": f"scene-{len(scene_data) + 1:03d}", + "name": scene_data.get("name", "新场景"), + "description": scene_data.get("description", ""), + "device_count": 0, + "algorithm_count": 0, + "created_at": "2024-01-15T10:30:00Z" + } + + return new_scene + except Exception as e: + raise HTTPException(status_code=500, detail=f"创建场景失败: {str(e)}") + +@router.put("/{scene_id}", summary="更新场景") +async def update_scene( + scene_id: str, + scene_data: Dict[str, Any], + db: Session = Depends(get_db) +): + """更新场景信息""" + try: + # TODO: 实现真实的场景更新 + # 当前返回模拟数据 + updated_scene = { + "id": scene_id, + "name": scene_data.get("name", "更新后的场景"), + "description": scene_data.get("description", ""), + "device_count": 0, + "algorithm_count": 0, + "updated_at": "2024-01-15T10:30:00Z" + } + + return updated_scene + except Exception as e: + raise HTTPException(status_code=500, detail=f"更新场景失败: {str(e)}") + +@router.delete("/{scene_id}", summary="删除场景") +async def delete_scene( + scene_id: str, + db: Session = Depends(get_db) +): + """删除场景""" + try: + # TODO: 实现真实的场景删除 + return { + "id": scene_id, + "message": "场景已删除" + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"删除场景失败: {str(e)}") + +@router.get("/{scene_id}/devices", summary="获取场景设备列表") +async def get_scene_devices( + scene_id: str, + db: Session = Depends(get_db) +): + """获取场景下的设备列表""" + try: + # TODO: 实现真实的场景设备查询 + # 当前返回模拟数据 + devices = [ + { + "id": 1, + "name": "港口区监控1", + "status": "online", + "location": "港口A区", + "ip_address": "192.168.1.100" + }, + { + "id": 2, + "name": "港口区监控2", + "status": "online", + "location": "港口B区", + "ip_address": "192.168.1.101" + } + ] + + return { + "scene_id": scene_id, + "devices": devices + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"获取场景设备失败: {str(e)}") + +@router.get("/{scene_id}/algorithms", summary="获取场景算法列表") +async def get_scene_algorithms( + scene_id: str, + db: Session = Depends(get_db) +): + """获取场景下的算法列表""" + try: + # TODO: 实现真实的场景算法查询 + # 当前返回模拟数据 + algorithms = [ + { + "id": 1, + "name": "船舶靠泊识别", + "status": "active", + "accuracy": 95.2 + }, + { + "id": 2, + "name": "人员登轮识别", + "status": "active", + "accuracy": 88.7 + } + ] + + return { + "scene_id": scene_id, + "algorithms": algorithms + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"获取场景算法失败: {str(e)}") \ No newline at end of file diff --git a/server/routers/upload.py b/server/routers/upload.py new file mode 100644 index 0000000..da30b32 --- /dev/null +++ b/server/routers/upload.py @@ -0,0 +1,209 @@ +from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, Query +from sqlalchemy.orm import Session +from typing import List, Optional, Dict, Any +import os +import uuid +from datetime import datetime +from core.database import get_db + +router = APIRouter() + +# 文件上传配置 +UPLOAD_DIR = "uploads" +VIDEO_DIR = os.path.join(UPLOAD_DIR, "videos") +IMAGE_DIR = os.path.join(UPLOAD_DIR, "images") + +# 确保上传目录存在 +os.makedirs(VIDEO_DIR, exist_ok=True) +os.makedirs(IMAGE_DIR, exist_ok=True) + +# 允许的文件类型 +ALLOWED_VIDEO_TYPES = ["video/mp4", "video/avi", "video/mov", "video/wmv"] +ALLOWED_IMAGE_TYPES = ["image/jpeg", "image/png", "image/gif", "image/bmp"] + +# 文件大小限制 (MB) +MAX_VIDEO_SIZE = 100 # 100MB +MAX_IMAGE_SIZE = 10 # 10MB + +@router.post("/video", summary="上传视频文件") +async def upload_video( + file: UploadFile = File(...), + device_id: Optional[int] = Form(None), + description: Optional[str] = Form(""), + db: Session = Depends(get_db) +): + """上传视频文件""" + try: + # 检查文件类型 + if file.content_type not in ALLOWED_VIDEO_TYPES: + raise HTTPException( + status_code=400, + detail=f"不支持的文件类型: {file.content_type}" + ) + + # 检查文件大小 + file_size = 0 + content = await file.read() + file_size = len(content) + + if file_size > MAX_VIDEO_SIZE * 1024 * 1024: + raise HTTPException( + status_code=400, + detail=f"文件大小超过限制: {MAX_VIDEO_SIZE}MB" + ) + + # 生成唯一文件名 + file_id = str(uuid.uuid4()) + file_extension = os.path.splitext(file.filename)[1] + filename = f"{file_id}{file_extension}" + file_path = os.path.join(VIDEO_DIR, filename) + + # 保存文件 + with open(file_path, "wb") as f: + f.write(content) + + # TODO: 获取视频时长 + duration = 30.5 # 模拟时长 + + return { + "file_id": file_id, + "file_url": f"/uploads/videos/{filename}", + "file_size": file_size, + "duration": duration, + "device_id": device_id, + "description": description, + "uploaded_at": datetime.now().isoformat() + } + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"上传视频失败: {str(e)}") + +@router.post("/image", summary="上传图片文件") +async def upload_image( + file: UploadFile = File(...), + event_id: Optional[int] = Form(None), + description: Optional[str] = Form(""), + db: Session = Depends(get_db) +): + """上传图片文件""" + try: + # 检查文件类型 + if file.content_type not in ALLOWED_IMAGE_TYPES: + raise HTTPException( + status_code=400, + detail=f"不支持的文件类型: {file.content_type}" + ) + + # 检查文件大小 + file_size = 0 + content = await file.read() + file_size = len(content) + + if file_size > MAX_IMAGE_SIZE * 1024 * 1024: + raise HTTPException( + status_code=400, + detail=f"文件大小超过限制: {MAX_IMAGE_SIZE}MB" + ) + + # 生成唯一文件名 + file_id = str(uuid.uuid4()) + file_extension = os.path.splitext(file.filename)[1] + filename = f"{file_id}{file_extension}" + file_path = os.path.join(IMAGE_DIR, filename) + + # 保存文件 + with open(file_path, "wb") as f: + f.write(content) + + return { + "file_id": file_id, + "file_url": f"/uploads/images/{filename}", + "file_size": file_size, + "event_id": event_id, + "description": description, + "uploaded_at": datetime.now().isoformat() + } + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"上传图片失败: {str(e)}") + +@router.get("/files", summary="获取文件列表") +async def get_files( + file_type: Optional[str] = Query(None, description="文件类型: video/image"), + page: int = Query(1, ge=1, description="页码"), + size: int = Query(20, ge=1, le=100, description="每页数量"), + db: Session = Depends(get_db) +): + """获取上传文件列表""" + try: + # TODO: 实现真实的文件列表查询 + # 当前返回模拟数据 + files = [ + { + "file_id": "video_123", + "file_url": "/uploads/videos/video_123.mp4", + "file_size": 1024000, + "file_type": "video", + "duration": 30.5, + "uploaded_at": "2024-01-15T10:30:00Z" + }, + { + "file_id": "image_456", + "file_url": "/uploads/images/image_456.jpg", + "file_size": 256000, + "file_type": "image", + "uploaded_at": "2024-01-15T10:30:00Z" + } + ] + + # 过滤文件类型 + if file_type: + files = [f for f in files if f["file_type"] == file_type] + + total = len(files) + start = (page - 1) * size + end = start + size + paginated_files = files[start:end] + + return { + "files": paginated_files, + "total": total, + "page": page, + "size": size + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"获取文件列表失败: {str(e)}") + +@router.delete("/files/{file_id}", summary="删除文件") +async def delete_file( + file_id: str, + db: Session = Depends(get_db) +): + """删除上传的文件""" + try: + # TODO: 实现真实的文件删除 + # 当前返回模拟数据 + return { + "file_id": file_id, + "message": "文件已删除" + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"删除文件失败: {str(e)}") + +@router.get("/stats", summary="获取上传统计") +async def get_upload_stats(db: Session = Depends(get_db)): + """获取文件上传统计""" + try: + # TODO: 实现真实的上传统计 + # 当前返回模拟数据 + return { + "total_files": 156, + "total_size": 1024000000, # 1GB + "video_count": 89, + "image_count": 67, + "today_uploads": 12 + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"获取上传统计失败: {str(e)}") \ No newline at end of file diff --git a/server/schemas/__init__.py b/server/schemas/__init__.py deleted file mode 100644 index 4a0c1d5..0000000 --- a/server/schemas/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .algorithm import * -from .device import * -from .event import * - -__all__ = ["algorithm", "device", "event"] \ No newline at end of file diff --git a/server/schemas/algorithm.py b/server/schemas/algorithm.py deleted file mode 100644 index bab7716..0000000 --- a/server/schemas/algorithm.py +++ /dev/null @@ -1,50 +0,0 @@ -from pydantic import BaseModel, Field -from typing import Optional, List, Dict, Any -from datetime import datetime - -class AlgorithmBase(BaseModel): - name: str = Field(..., description="算法名称") - description: Optional[str] = Field(None, description="算法描述") - version: str = Field(..., description="算法版本") - model_path: Optional[str] = Field(None, description="模型文件路径") - config_path: Optional[str] = Field(None, description="配置文件路径") - status: str = Field("inactive", description="算法状态") - accuracy: Optional[float] = Field(None, description="算法准确率") - detection_classes: Optional[str] = Field(None, description="检测类别") - input_size: Optional[str] = Field(None, description="输入尺寸") - inference_time: Optional[float] = Field(None, description="推理时间") - is_enabled: bool = Field(True, description="是否启用") - creator: Optional[str] = Field(None, description="创建者") - tags: Optional[str] = Field(None, description="标签") - -class AlgorithmCreate(AlgorithmBase): - pass - -class AlgorithmUpdate(BaseModel): - name: Optional[str] = None - description: Optional[str] = None - version: Optional[str] = None - model_path: Optional[str] = None - config_path: Optional[str] = None - status: Optional[str] = None - accuracy: Optional[float] = None - detection_classes: Optional[str] = None - input_size: Optional[str] = None - inference_time: Optional[float] = None - is_enabled: Optional[bool] = None - creator: Optional[str] = None - tags: Optional[str] = None - -class AlgorithmResponse(AlgorithmBase): - id: int - created_at: datetime - updated_at: datetime - - class Config: - from_attributes = True - -class AlgorithmListResponse(BaseModel): - algorithms: List[AlgorithmResponse] - total: int - page: int - size: int \ No newline at end of file diff --git a/server/schemas/device.py b/server/schemas/device.py deleted file mode 100644 index 3ed1bbe..0000000 --- a/server/schemas/device.py +++ /dev/null @@ -1,63 +0,0 @@ -from pydantic import BaseModel, Field -from typing import Optional, List, Dict, Any -from datetime import datetime - -class DeviceBase(BaseModel): - name: str = Field(..., description="设备名称") - device_type: str = Field(..., description="设备类型") - location: Optional[str] = Field(None, description="设备位置") - ip_address: Optional[str] = Field(None, description="IP地址") - port: Optional[int] = Field(None, description="端口号") - username: Optional[str] = Field(None, description="用户名") - password: Optional[str] = Field(None, description="密码") - rtsp_url: Optional[str] = Field(None, description="RTSP流地址") - status: str = Field("offline", description="设备状态") - resolution: Optional[str] = Field(None, description="分辨率") - fps: Optional[int] = Field(None, description="帧率") - algorithm_id: Optional[int] = Field(None, description="关联的算法ID") - is_enabled: bool = Field(True, description="是否启用") - latitude: Optional[float] = Field(None, description="纬度") - longitude: Optional[float] = Field(None, description="经度") - description: Optional[str] = Field(None, description="设备描述") - manufacturer: Optional[str] = Field(None, description="制造商") - model: Optional[str] = Field(None, description="设备型号") - serial_number: Optional[str] = Field(None, description="序列号") - -class DeviceCreate(DeviceBase): - pass - -class DeviceUpdate(BaseModel): - name: Optional[str] = None - device_type: Optional[str] = None - location: Optional[str] = None - ip_address: Optional[str] = None - port: Optional[int] = None - username: Optional[str] = None - password: Optional[str] = None - rtsp_url: Optional[str] = None - status: Optional[str] = None - resolution: Optional[str] = None - fps: Optional[int] = None - algorithm_id: Optional[int] = None - is_enabled: Optional[bool] = None - latitude: Optional[float] = None - longitude: Optional[float] = None - description: Optional[str] = None - manufacturer: Optional[str] = None - model: Optional[str] = None - serial_number: Optional[str] = None - -class DeviceResponse(DeviceBase): - id: int - created_at: datetime - updated_at: datetime - last_heartbeat: Optional[datetime] = None - - class Config: - from_attributes = True - -class DeviceListResponse(BaseModel): - devices: List[DeviceResponse] - total: int - page: int - size: int \ No newline at end of file diff --git a/server/schemas/event.py b/server/schemas/event.py deleted file mode 100644 index 1bb6fca..0000000 --- a/server/schemas/event.py +++ /dev/null @@ -1,59 +0,0 @@ -from pydantic import BaseModel, Field -from typing import Optional, List, Dict, Any -from datetime import datetime - -class EventBase(BaseModel): - event_type: str = Field(..., description="事件类型") - device_id: int = Field(..., description="关联设备ID") - algorithm_id: Optional[int] = Field(None, description="关联算法ID") - severity: str = Field("medium", description="严重程度") - status: str = Field("pending", description="事件状态") - confidence: Optional[float] = Field(None, description="置信度") - bbox: Optional[str] = Field(None, description="边界框坐标") - image_path: Optional[str] = Field(None, description="事件图片路径") - video_path: Optional[str] = Field(None, description="事件视频路径") - description: Optional[str] = Field(None, description="事件描述") - location: Optional[str] = Field(None, description="事件发生位置") - detected_objects: Optional[str] = Field(None, description="检测到的对象") - processing_time: Optional[float] = Field(None, description="处理时间") - is_alert: bool = Field(False, description="是否触发告警") - alert_sent: bool = Field(False, description="是否已发送告警") - operator_id: Optional[int] = Field(None, description="处理人员ID") - resolution_notes: Optional[str] = Field(None, description="处理备注") - -class EventCreate(EventBase): - pass - -class EventUpdate(BaseModel): - event_type: Optional[str] = None - device_id: Optional[int] = None - algorithm_id: Optional[int] = None - severity: Optional[str] = None - status: Optional[str] = None - confidence: Optional[float] = None - bbox: Optional[str] = None - image_path: Optional[str] = None - video_path: Optional[str] = None - description: Optional[str] = None - location: Optional[str] = None - detected_objects: Optional[str] = None - processing_time: Optional[float] = None - is_alert: Optional[bool] = None - alert_sent: Optional[bool] = None - operator_id: Optional[int] = None - resolution_notes: Optional[str] = None - -class EventResponse(EventBase): - id: int - created_at: datetime - updated_at: datetime - resolved_at: Optional[datetime] = None - - class Config: - from_attributes = True - -class EventListResponse(BaseModel): - events: List[EventResponse] - total: int - page: int - size: int \ No newline at end of file diff --git a/server/test_api.py b/server/test_api.py new file mode 100644 index 0000000..70c475d --- /dev/null +++ b/server/test_api.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +""" +API测试脚本 +用于验证新创建的接口是否正常工作 +""" + +import requests +import json +from datetime import datetime + +# API基础URL +BASE_URL = "http://localhost:8000/api" + +def test_dashboard_apis(): + """测试仪表板相关接口""" + print("=== 测试仪表板接口 ===") + + # 测试KPI接口 + try: + response = requests.get(f"{BASE_URL}/dashboard/kpi") + print(f"KPI接口: {response.status_code}") + if response.status_code == 200: + print(f"KPI数据: {response.json()}") + except Exception as e: + print(f"KPI接口错误: {e}") + + # 测试告警趋势接口 + try: + response = requests.get(f"{BASE_URL}/dashboard/alarm-trend?days=7") + print(f"告警趋势接口: {response.status_code}") + if response.status_code == 200: + print(f"告警趋势数据: {response.json()}") + except Exception as e: + print(f"告警趋势接口错误: {e}") + + # 测试摄像头统计接口 + try: + response = requests.get(f"{BASE_URL}/dashboard/camera-stats") + print(f"摄像头统计接口: {response.status_code}") + if response.status_code == 200: + print(f"摄像头统计数据: {response.json()}") + except Exception as e: + print(f"摄像头统计接口错误: {e}") + +def test_monitor_apis(): + """测试监控相关接口""" + print("\n=== 测试监控接口 ===") + + # 测试监控列表接口 + try: + response = requests.get(f"{BASE_URL}/monitors?page=1&size=10") + print(f"监控列表接口: {response.status_code}") + if response.status_code == 200: + data = response.json() + print(f"监控列表: 总数={data.get('total', 0)}") + except Exception as e: + print(f"监控列表接口错误: {e}") + +def test_alarm_apis(): + """测试告警相关接口""" + print("\n=== 测试告警接口 ===") + + # 测试告警列表接口 + try: + response = requests.get(f"{BASE_URL}/alarms?page=1&size=10") + print(f"告警列表接口: {response.status_code}") + if response.status_code == 200: + data = response.json() + print(f"告警列表: 总数={data.get('total', 0)}") + except Exception as e: + print(f"告警列表接口错误: {e}") + + # 测试告警统计接口 + try: + response = requests.get(f"{BASE_URL}/alarms/stats") + print(f"告警统计接口: {response.status_code}") + if response.status_code == 200: + print(f"告警统计数据: {response.json()}") + except Exception as e: + print(f"告警统计接口错误: {e}") + +def test_scene_apis(): + """测试场景相关接口""" + print("\n=== 测试场景接口 ===") + + # 测试场景列表接口 + try: + response = requests.get(f"{BASE_URL}/scenes") + print(f"场景列表接口: {response.status_code}") + if response.status_code == 200: + data = response.json() + print(f"场景列表: 数量={len(data.get('scenes', []))}") + except Exception as e: + print(f"场景列表接口错误: {e}") + +def test_auth_apis(): + """测试认证相关接口""" + print("\n=== 测试认证接口 ===") + + # 测试登录接口 + try: + login_data = { + "username": "admin", + "password": "admin123" + } + response = requests.post(f"{BASE_URL}/auth/login", data=login_data) + print(f"登录接口: {response.status_code}") + if response.status_code == 200: + print("登录成功") + else: + print(f"登录失败: {response.text}") + except Exception as e: + print(f"登录接口错误: {e}") + +def test_upload_apis(): + """测试上传相关接口""" + print("\n=== 测试上传接口 ===") + + # 测试上传统计接口 + try: + response = requests.get(f"{BASE_URL}/upload/stats") + print(f"上传统计接口: {response.status_code}") + if response.status_code == 200: + print(f"上传统计数据: {response.json()}") + except Exception as e: + print(f"上传统计接口错误: {e}") + +def main(): + """主测试函数""" + print("开始API测试...") + print(f"测试时间: {datetime.now()}") + print(f"API基础URL: {BASE_URL}") + + # 测试各个模块的接口 + test_dashboard_apis() + test_monitor_apis() + test_alarm_apis() + test_scene_apis() + test_auth_apis() + test_upload_apis() + + print("\n=== 测试完成 ===") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/server/todo.md b/server/todo.md new file mode 100644 index 0000000..0a75b65 --- /dev/null +++ b/server/todo.md @@ -0,0 +1,362 @@ +# 前端接口开发计划 + +## 概述 +根据前端代码分析,当前后端已实现基础的CRUD接口,但前端需要更多统计、监控、告警等功能的接口。以下是缺失接口的开发计划。 + +## 已实现接口 +- ✅ 设备管理:CRUD操作 +- ✅ 算法管理:CRUD操作 +- ✅ 事件管理:CRUD操作 + +## 缺失接口清单 + +### 1. 仪表板统计接口 (优先级:高) + +#### 1.1 主要KPI指标 +``` +GET /api/dashboard/kpi +响应: +{ + "total_devices": 156, + "online_devices": 142, + "total_algorithms": 8, + "active_algorithms": 6, + "total_events": 1247, + "today_events": 89, + "alert_events": 23, + "resolved_events": 66 +} +``` + +#### 1.2 告警趋势统计 +``` +GET /api/dashboard/alarm-trend +参数: +- days: 7 (默认7天) +响应: +{ + "dates": ["2024-01-01", "2024-01-02", ...], + "alarms": [12, 15, 8, 23, 18, 25, 20], + "resolved": [10, 12, 7, 19, 15, 22, 18] +} +``` + +#### 1.3 摄像头统计 +``` +GET /api/dashboard/camera-stats +响应: +{ + "total_cameras": 156, + "online_cameras": 142, + "offline_cameras": 14, + "by_location": [ + {"location": "港口区", "total": 45, "online": 42}, + {"location": "码头区", "total": 38, "online": 35}, + {"location": "办公区", "total": 23, "online": 21} + ] +} +``` + +#### 1.4 算法统计 +``` +GET /api/dashboard/algorithm-stats +响应: +{ + "total_algorithms": 8, + "active_algorithms": 6, + "by_type": [ + {"type": "目标检测", "count": 3, "accuracy": 95.2}, + {"type": "行为识别", "count": 2, "accuracy": 88.7}, + {"type": "越界检测", "count": 3, "accuracy": 92.1} + ] +} +``` + +#### 1.5 事件热点统计 +``` +GET /api/dashboard/event-hotspots +响应: +{ + "hotspots": [ + { + "location": "港口A区", + "event_count": 45, + "severity": "high", + "coordinates": {"lat": 31.2304, "lng": 121.4737} + } + ] +} +``` + +### 2. 监控管理接口 (优先级:高) + +#### 2.1 监控列表 +``` +GET /api/monitors +参数: +- page: 1 +- size: 20 +- status: online/offline +- location: 位置筛选 +响应: +{ + "monitors": [ + { + "id": 1, + "name": "港口区监控1", + "location": "港口A区", + "status": "online", + "video_url": "/videos/port-1.mp4", + "detections": [ + {"type": "person", "x": 25, "y": 35, "width": 40, "height": 80} + ] + } + ], + "total": 156, + "page": 1, + "size": 20 +} +``` + +#### 2.2 监控详情 +``` +GET /api/monitors/{monitor_id} +响应: +{ + "id": 1, + "name": "港口区主监控", + "location": "港口区", + "status": "online", + "video_url": "/videos/port-main.mp4", + "detections": [...], + "events": [...], + "algorithms": [...] +} +``` + +### 3. 告警管理接口 (优先级:中) + +#### 3.1 告警列表 +``` +GET /api/alarms +参数: +- page: 1 +- size: 20 +- severity: high/medium/low +- status: pending/resolved +- start_time: 2024-01-01 +- end_time: 2024-01-31 +响应: +{ + "alarms": [ + { + "id": 1, + "type": "船舶靠泊", + "severity": "high", + "status": "pending", + "device": "港口区监控1", + "created_at": "2024-01-15T10:30:00Z", + "description": "检测到船舶靠泊行为" + } + ], + "total": 89, + "page": 1, + "size": 20 +} +``` + +#### 3.2 告警处理 +``` +PATCH /api/alarms/{alarm_id}/resolve +请求体: +{ + "resolution_notes": "已确认船舶靠泊,无异常", + "resolved_by": "operator1" +} +``` + +#### 3.3 告警统计 +``` +GET /api/alarms/stats +响应: +{ + "total_alarms": 89, + "pending_alarms": 23, + "resolved_alarms": 66, + "by_severity": [ + {"severity": "high", "count": 12}, + {"severity": "medium", "count": 45}, + {"severity": "low", "count": 32} + ] +} +``` + +### 4. 场景管理接口 (优先级:中) + +#### 4.1 场景列表 +``` +GET /api/scenes +响应: +{ + "scenes": [ + { + "id": "scene-001", + "name": "港口区场景", + "description": "港口区监控场景", + "device_count": 45, + "algorithm_count": 3 + } + ] +} +``` + +#### 4.2 场景详情 +``` +GET /api/scenes/{scene_id} +响应: +{ + "id": "scene-001", + "name": "港口区场景", + "description": "港口区监控场景", + "devices": [...], + "algorithms": [...], + "events": [...] +} +``` + +### 5. 文件上传接口 (优先级:中) + +#### 5.1 视频上传 +``` +POST /api/upload/video +Content-Type: multipart/form-data +请求体: +- file: 视频文件 +- device_id: 设备ID +- description: 描述 +响应: +{ + "file_id": "video_123", + "file_url": "/uploads/videos/video_123.mp4", + "file_size": 1024000, + "duration": 30.5 +} +``` + +#### 5.2 图片上传 +``` +POST /api/upload/image +Content-Type: multipart/form-data +请求体: +- file: 图片文件 +- event_id: 事件ID +响应: +{ + "file_id": "image_456", + "file_url": "/uploads/images/image_456.jpg", + "file_size": 256000 +} +``` + +### 6. 用户认证接口 (优先级:低) + +#### 6.1 用户登录 +``` +POST /api/auth/login +请求体: +{ + "username": "admin", + "password": "password123" +} +响应: +{ + "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...", + "token_type": "bearer", + "expires_in": 3600, + "user": { + "id": 1, + "username": "admin", + "role": "admin" + } +} +``` + +#### 6.2 用户信息 +``` +GET /api/auth/profile +响应: +{ + "id": 1, + "username": "admin", + "email": "admin@example.com", + "role": "admin", + "permissions": ["read", "write", "admin"] +} +``` + +## 开发进度 + +### ✅ 已完成 (第一阶段) +1. ✅ 仪表板统计接口 (KPI、告警趋势、摄像头统计、算法统计、事件热点) +2. ✅ 监控管理接口 (监控列表、监控详情) + +### ✅ 已完成 (第二阶段) +1. ✅ 告警管理接口 (告警列表、告警处理、告警统计) +2. ✅ 场景管理接口 (场景列表、场景详情) + +### ✅ 已完成 (第三阶段) +1. ✅ 文件上传接口 (视频上传、图片上传) +2. ✅ 用户认证接口 (登录、用户信息) + +### 🔄 待优化功能 +1. 实现真实的告警趋势统计 (当前使用模拟数据) +2. 实现真实的检测数据获取 (当前使用模拟数据) +3. 实现真实的视频流URL生成 +4. 实现真实的JWT验证中间件 +5. 实现真实的场景管理数据库模型 +6. 实现真实的文件删除逻辑 +7. 添加Redis缓存支持 +8. 添加WebSocket实时数据推送 + +## 技术实现要点 + +1. **数据库模型扩展**: + - 添加统计相关的视图或缓存表 + - 优化查询性能,添加索引 + +2. **缓存策略**: + - 使用Redis缓存统计数据 + - 设置合理的缓存过期时间 + +3. **文件存储**: + - 配置静态文件服务 + - 实现文件上传和存储逻辑 + +4. **权限控制**: + - 实现JWT认证 + - 添加角色和权限控制 + +5. **WebSocket支持**: + - 实时监控数据推送 + - 告警实时通知 + +## 测试计划 + +1. **单元测试**:每个接口的CRUD操作 +2. **集成测试**:前后端联调 +3. **性能测试**:大数据量下的响应时间 +4. **安全测试**:认证和权限验证 + +## 部署计划 + +1. **开发环境**:本地测试 +2. **测试环境**:功能验证 +3. **生产环境**:正式部署 + +## 注意事项 + +1. 所有接口需要添加错误处理和日志记录 +2. 敏感数据需要加密存储 +3. 文件上传需要限制文件大小和类型 +4. 统计数据需要定期更新,避免过期数据 +5. 接口文档需要及时更新