Merge branch 'main' of http://36.103.203.89:3000/gezhonglin/border_Inspection
This commit is contained in:
commit
85175e60a5
15
algorithm/requirements.txt
Normal file
15
algorithm/requirements.txt
Normal file
@ -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
|
285
server/README_API.md
Normal file
285
server/README_API.md
Normal file
@ -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"]
|
||||
```
|
@ -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():
|
||||
|
@ -1,5 +1,6 @@
|
||||
from sqlalchemy import Column, String, Text, Boolean, Float, Integer
|
||||
from .base import BaseModel
|
||||
import json
|
||||
|
||||
class Algorithm(BaseModel):
|
||||
__tablename__ = "algorithms"
|
||||
@ -17,3 +18,24 @@ class Algorithm(BaseModel):
|
||||
is_enabled = Column(Boolean, default=True, comment="是否启用")
|
||||
creator = Column(String(50), comment="创建者")
|
||||
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
|
||||
}
|
193
server/readme.md
193
server/readme.md
@ -1,21 +1,17 @@
|
||||
# 边检CV算法接口服务
|
||||
|
||||
### 技术栈
|
||||
- **后端框架**: FastAPI
|
||||
- **数据库**: SQLite (SQLAlchemy ORM)
|
||||
- **AI模型**: YOLOv11n
|
||||
- **进程管理**: Supervisor
|
||||
- **开发语言**: Python 3.8+
|
||||
## 项目简介
|
||||
|
||||
### 项目结构
|
||||
这是一个简化的边检计算机视觉算法管理系统API服务,采用FastAPI框架开发。
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
server/
|
||||
├── app.py # FastAPI主应用
|
||||
├── start.py # 启动脚本
|
||||
├── requirements.txt # Python依赖
|
||||
├── app.py # 主应用文件
|
||||
├── requirements.txt # 依赖包列表
|
||||
├── init_data.py # 初始化数据脚本
|
||||
├── env.example # 环境变量示例
|
||||
├── init_data.py # 示例数据初始化
|
||||
├── core/ # 核心配置
|
||||
│ └── database.py # 数据库配置
|
||||
├── models/ # 数据模型
|
||||
@ -24,154 +20,91 @@ server/
|
||||
│ ├── algorithm.py # 算法模型
|
||||
│ ├── device.py # 设备模型
|
||||
│ └── event.py # 事件模型
|
||||
├── schemas/ # Pydantic模型
|
||||
│ ├── __init__.py
|
||||
│ ├── algorithm.py # 算法相关模型
|
||||
│ ├── device.py # 设备相关模型
|
||||
│ └── event.py # 事件相关模型
|
||||
└── routers/ # API路由
|
||||
└── routers/ # 路由接口
|
||||
├── __init__.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 start.py
|
||||
cp env.example .env
|
||||
# 编辑.env文件配置数据库连接等
|
||||
```
|
||||
|
||||
#### 3. 初始化示例数据
|
||||
3. 初始化数据:
|
||||
```bash
|
||||
python init_data.py
|
||||
```
|
||||
|
||||
#### 4. 启动服务
|
||||
4. 启动服务:
|
||||
```bash
|
||||
python start.py
|
||||
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 文件
|
||||
```
|
||||
- 简化的架构,易于维护
|
||||
- 完整的CRUD操作
|
||||
- 支持分页和筛选
|
||||
- 自动生成API文档
|
||||
- 支持CORS跨域
|
||||
- 健康检查接口
|
@ -10,3 +10,4 @@ aiofiles==23.2.1
|
||||
pillow==10.1.0
|
||||
opencv-python==4.8.1.78
|
||||
ultralytics==8.0.196
|
||||
PyJWT==2.8.0
|
236
server/routers/alarms.py
Normal file
236
server/routers/alarms.py
Normal file
@ -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)}")
|
@ -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
|
||||
|
||||
@router.get("/", response_model=AlgorithmListResponse, summary="获取算法列表")
|
||||
return db_algorithm.to_dict()
|
||||
|
||||
@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]
|
||||
|
||||
@router.get("/{algorithm_id}", response_model=AlgorithmResponse, summary="获取算法详情")
|
||||
return {
|
||||
"algorithms": algorithm_list,
|
||||
"total": total,
|
||||
"page": skip // limit + 1,
|
||||
"size": limit
|
||||
}
|
||||
|
||||
@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
|
||||
|
||||
@router.put("/{algorithm_id}", response_model=AlgorithmResponse, summary="更新算法")
|
||||
return algorithm.to_dict()
|
||||
|
||||
@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():
|
||||
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
|
||||
|
||||
@router.patch("/{algorithm_id}/enable", response_model=AlgorithmResponse, summary="启用/禁用算法")
|
||||
return algorithm.to_dict()
|
||||
|
||||
@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
|
||||
|
||||
return algorithm.to_dict()
|
237
server/routers/auth.py
Normal file
237
server/routers/auth.py
Normal file
@ -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)}")
|
162
server/routers/dashboard.py
Normal file
162
server/routers/dashboard.py
Normal file
@ -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)}")
|
@ -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
|
||||
|
||||
@router.get("/", response_model=DeviceListResponse, summary="获取设备列表")
|
||||
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("/", 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
|
||||
})
|
||||
|
||||
@router.get("/{device_id}", response_model=DeviceResponse, summary="获取设备详情")
|
||||
return {
|
||||
"devices": device_list,
|
||||
"total": total,
|
||||
"page": skip // limit + 1,
|
||||
"size": limit
|
||||
}
|
||||
|
||||
@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
|
||||
|
||||
@router.put("/{device_id}", response_model=DeviceResponse, summary="更新设备")
|
||||
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}", 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():
|
||||
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
|
||||
|
||||
@router.patch("/{device_id}/enable", response_model=DeviceResponse, summary="启用/禁用设备")
|
||||
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", 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():
|
||||
|
@ -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
|
||||
|
||||
@router.get("/", response_model=EventListResponse, summary="获取事件列表")
|
||||
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("/", 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
|
||||
})
|
||||
|
||||
@router.get("/{event_id}", response_model=EventResponse, summary="获取事件详情")
|
||||
return {
|
||||
"events": event_list,
|
||||
"total": total,
|
||||
"page": skip // limit + 1,
|
||||
"size": limit
|
||||
}
|
||||
|
||||
@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
|
||||
|
||||
@router.put("/{event_id}", response_model=EventResponse, summary="更新事件")
|
||||
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}", 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():
|
||||
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():
|
||||
|
185
server/routers/monitors.py
Normal file
185
server/routers/monitors.py
Normal file
@ -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)}")
|
233
server/routers/scenes.py
Normal file
233
server/routers/scenes.py
Normal file
@ -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)}")
|
209
server/routers/upload.py
Normal file
209
server/routers/upload.py
Normal file
@ -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)}")
|
@ -1,5 +0,0 @@
|
||||
from .algorithm import *
|
||||
from .device import *
|
||||
from .event import *
|
||||
|
||||
__all__ = ["algorithm", "device", "event"]
|
@ -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
|
@ -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
|
@ -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
|
145
server/test_api.py
Normal file
145
server/test_api.py
Normal file
@ -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()
|
362
server/todo.md
Normal file
362
server/todo.md
Normal file
@ -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. 接口文档需要及时更新
|
Loading…
x
Reference in New Issue
Block a user