feat 接口
This commit is contained in:
parent
3bd5e77ce4
commit
a8e24157e7
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 import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
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 core.database import engine
|
||||||
from models.base import Base
|
from models.base import Base
|
||||||
|
|
||||||
@ -22,10 +23,19 @@ app.add_middleware(
|
|||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 静态文件服务
|
||||||
|
app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads")
|
||||||
|
|
||||||
# 注册路由
|
# 注册路由
|
||||||
app.include_router(algorithms.router, prefix="/api/algorithms", tags=["算法管理"])
|
app.include_router(algorithms.router, prefix="/api/algorithms", tags=["算法管理"])
|
||||||
app.include_router(events.router, prefix="/api/events", tags=["事件管理"])
|
app.include_router(events.router, prefix="/api/events", tags=["事件管理"])
|
||||||
app.include_router(devices.router, prefix="/api/devices", 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("/")
|
@app.get("/")
|
||||||
async def root():
|
async def root():
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from sqlalchemy import Column, String, Text, Boolean, Float, Integer
|
from sqlalchemy import Column, String, Text, Boolean, Float, Integer
|
||||||
from .base import BaseModel
|
from .base import BaseModel
|
||||||
|
import json
|
||||||
|
|
||||||
class Algorithm(BaseModel):
|
class Algorithm(BaseModel):
|
||||||
__tablename__ = "algorithms"
|
__tablename__ = "algorithms"
|
||||||
@ -16,4 +17,25 @@ class Algorithm(BaseModel):
|
|||||||
inference_time = Column(Float, comment="推理时间(ms)")
|
inference_time = Column(Float, comment="推理时间(ms)")
|
||||||
is_enabled = Column(Boolean, default=True, comment="是否启用")
|
is_enabled = Column(Boolean, default=True, comment="是否启用")
|
||||||
creator = Column(String(50), comment="创建者")
|
creator = Column(String(50), comment="创建者")
|
||||||
tags = Column(Text, comment="标签,JSON格式")
|
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
|
||||||
|
}
|
213
server/readme.md
213
server/readme.md
@ -1,177 +1,110 @@
|
|||||||
# 边检CV算法接口服务
|
# 边检CV算法接口服务
|
||||||
|
|
||||||
### 技术栈
|
## 项目简介
|
||||||
- **后端框架**: FastAPI
|
|
||||||
- **数据库**: SQLite (SQLAlchemy ORM)
|
|
||||||
- **AI模型**: YOLOv11n
|
|
||||||
- **进程管理**: Supervisor
|
|
||||||
- **开发语言**: Python 3.8+
|
|
||||||
|
|
||||||
### 项目结构
|
这是一个简化的边检计算机视觉算法管理系统API服务,采用FastAPI框架开发。
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
```
|
```
|
||||||
server/
|
server/
|
||||||
├── app.py # FastAPI主应用
|
├── app.py # 主应用文件
|
||||||
├── start.py # 启动脚本
|
├── requirements.txt # 依赖包列表
|
||||||
├── requirements.txt # Python依赖
|
├── init_data.py # 初始化数据脚本
|
||||||
├── env.example # 环境变量示例
|
├── env.example # 环境变量示例
|
||||||
├── init_data.py # 示例数据初始化
|
├── core/ # 核心配置
|
||||||
├── core/ # 核心配置
|
│ └── database.py # 数据库配置
|
||||||
│ └── database.py # 数据库配置
|
├── models/ # 数据模型
|
||||||
├── models/ # 数据模型
|
|
||||||
│ ├── __init__.py
|
│ ├── __init__.py
|
||||||
│ ├── base.py # 基础模型
|
│ ├── base.py # 基础模型
|
||||||
│ ├── algorithm.py # 算法模型
|
│ ├── algorithm.py # 算法模型
|
||||||
│ ├── device.py # 设备模型
|
│ ├── device.py # 设备模型
|
||||||
│ └── event.py # 事件模型
|
│ └── event.py # 事件模型
|
||||||
├── schemas/ # Pydantic模型
|
└── routers/ # 路由接口
|
||||||
│ ├── __init__.py
|
|
||||||
│ ├── algorithm.py # 算法相关模型
|
|
||||||
│ ├── device.py # 设备相关模型
|
|
||||||
│ └── event.py # 事件相关模型
|
|
||||||
└── routers/ # API路由
|
|
||||||
├── __init__.py
|
├── __init__.py
|
||||||
├── algorithms.py # 算法管理接口
|
├── algorithms.py # 算法管理接口
|
||||||
├── devices.py # 设备管理接口
|
├── devices.py # 设备管理接口
|
||||||
└── events.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` - 启用/禁用算法
|
||||||
|
|
||||||
#### 算法管理接口
|
### 设备管理 (/api/devices)
|
||||||
- `POST /api/algorithms/` - 创建算法
|
- `POST /` - 创建设备
|
||||||
- `GET /api/algorithms/` - 获取算法列表
|
- `GET /` - 获取设备列表
|
||||||
- `GET /api/algorithms/{id}` - 获取算法详情
|
- `GET /{id}` - 获取设备详情
|
||||||
- `PUT /api/algorithms/{id}` - 更新算法
|
- `PUT /{id}` - 更新设备
|
||||||
- `DELETE /api/algorithms/{id}` - 删除算法
|
- `DELETE /{id}` - 删除设备
|
||||||
- `PATCH /api/algorithms/{id}/status` - 更新算法状态
|
- `PATCH /{id}/status` - 更新设备状态
|
||||||
- `PATCH /api/algorithms/{id}/enable` - 启用/禁用算法
|
- `PATCH /{id}/enable` - 启用/禁用设备
|
||||||
|
- `GET /types/list` - 获取设备类型列表
|
||||||
|
- `GET /status/stats` - 获取设备状态统计
|
||||||
|
|
||||||
#### 设备管理接口
|
### 事件管理 (/api/events)
|
||||||
- `POST /api/devices/` - 创建设备
|
- `POST /` - 创建事件
|
||||||
- `GET /api/devices/` - 获取设备列表
|
- `GET /` - 获取事件列表
|
||||||
- `GET /api/devices/{id}` - 获取设备详情
|
- `GET /{id}` - 获取事件详情
|
||||||
- `PUT /api/devices/{id}` - 更新设备
|
- `PUT /{id}` - 更新事件
|
||||||
- `DELETE /api/devices/{id}` - 删除设备
|
- `DELETE /{id}` - 删除事件
|
||||||
- `PATCH /api/devices/{id}/status` - 更新设备状态
|
- `PATCH /{id}/status` - 更新事件状态
|
||||||
- `PATCH /api/devices/{id}/enable` - 启用/禁用设备
|
- `GET /types/list` - 获取事件类型列表
|
||||||
- `GET /api/devices/types/list` - 获取设备类型列表
|
- `GET /stats/summary` - 获取事件统计摘要
|
||||||
- `GET /api/devices/status/stats` - 获取设备状态统计
|
- `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
|
```bash
|
||||||
# 创建虚拟环境
|
|
||||||
conda create -n border_inspection python=3.8
|
|
||||||
conda activate border_inspection
|
|
||||||
|
|
||||||
# 安装依赖
|
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 2. 初始化数据库
|
2. 配置环境变量:
|
||||||
```bash
|
```bash
|
||||||
# 启动服务(会自动创建数据库表)
|
cp env.example .env
|
||||||
python app.py
|
# 编辑.env文件配置数据库连接等
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 3. 初始化示例数据
|
3. 初始化数据:
|
||||||
```bash
|
```bash
|
||||||
python init_data.py
|
python init_data.py
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 4. 启动服务
|
4. 启动服务:
|
||||||
```bash
|
```bash
|
||||||
python app.py
|
python app.py
|
||||||
```
|
```
|
||||||
|
|
||||||
服务将在 `http://localhost:8000` 启动
|
服务将在 http://localhost:8000 启动,API文档访问 http://localhost:8000/docs
|
||||||
|
|
||||||
### API文档
|
## 技术栈
|
||||||
|
|
||||||
启动服务后,可以访问以下地址查看API文档:
|
- **框架**: FastAPI
|
||||||
- Swagger UI: `http://localhost:8000/docs`
|
- **数据库**: SQLAlchemy + SQLite
|
||||||
- ReDoc: `http://localhost:8000/redoc`
|
- **ORM**: SQLAlchemy
|
||||||
|
- **文档**: 自动生成OpenAPI文档
|
||||||
|
|
||||||
### 数据库设计
|
## 特点
|
||||||
|
|
||||||
#### 算法表 (algorithms)
|
- 简化的架构,易于维护
|
||||||
- 基本信息:名称、描述、版本
|
- 完整的CRUD操作
|
||||||
- 模型信息:模型路径、配置文件路径
|
- 支持分页和筛选
|
||||||
- 性能指标:准确率、推理时间、输入尺寸
|
- 自动生成API文档
|
||||||
- 状态管理:启用状态、算法状态
|
- 支持CORS跨域
|
||||||
- 分类信息:检测类别、标签
|
- 健康检查接口
|
||||||
|
|
||||||
#### 设备表 (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 文件
|
|
||||||
```
|
|
@ -9,4 +9,5 @@ python-dotenv==1.0.0
|
|||||||
aiofiles==23.2.1
|
aiofiles==23.2.1
|
||||||
pillow==10.1.0
|
pillow==10.1.0
|
||||||
opencv-python==4.8.1.78
|
opencv-python==4.8.1.78
|
||||||
ultralytics==8.0.196
|
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 fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from typing import List, Optional
|
from typing import List, Optional, Dict, Any
|
||||||
from core.database import get_db
|
from core.database import get_db
|
||||||
from models.algorithm import Algorithm
|
from models.algorithm import Algorithm
|
||||||
from schemas.algorithm import (
|
|
||||||
AlgorithmCreate,
|
|
||||||
AlgorithmUpdate,
|
|
||||||
AlgorithmResponse,
|
|
||||||
AlgorithmListResponse
|
|
||||||
)
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.post("/", response_model=AlgorithmResponse, summary="创建算法")
|
@router.post("/", summary="创建算法")
|
||||||
async def create_algorithm(
|
async def create_algorithm(
|
||||||
algorithm: AlgorithmCreate,
|
algorithm_data: Dict[str, Any],
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""创建新的算法"""
|
"""创建新的算法"""
|
||||||
db_algorithm = Algorithm(**algorithm.dict())
|
db_algorithm = Algorithm(**algorithm_data)
|
||||||
db.add(db_algorithm)
|
db.add(db_algorithm)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(db_algorithm)
|
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(
|
async def get_algorithms(
|
||||||
skip: int = Query(0, ge=0, description="跳过记录数"),
|
skip: int = Query(0, ge=0, description="跳过记录数"),
|
||||||
limit: int = Query(10, ge=1, le=100, description="返回记录数"),
|
limit: int = Query(10, ge=1, le=100, description="返回记录数"),
|
||||||
@ -46,14 +41,16 @@ async def get_algorithms(
|
|||||||
total = query.count()
|
total = query.count()
|
||||||
algorithms = query.offset(skip).limit(limit).all()
|
algorithms = query.offset(skip).limit(limit).all()
|
||||||
|
|
||||||
return AlgorithmListResponse(
|
algorithm_list = [algorithm.to_dict() for algorithm in algorithms]
|
||||||
algorithms=algorithms,
|
|
||||||
total=total,
|
return {
|
||||||
page=skip // limit + 1,
|
"algorithms": algorithm_list,
|
||||||
size=limit
|
"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(
|
async def get_algorithm(
|
||||||
algorithm_id: int,
|
algorithm_id: int,
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
@ -62,12 +59,13 @@ async def get_algorithm(
|
|||||||
algorithm = db.query(Algorithm).filter(Algorithm.id == algorithm_id).first()
|
algorithm = db.query(Algorithm).filter(Algorithm.id == algorithm_id).first()
|
||||||
if not algorithm:
|
if not algorithm:
|
||||||
raise HTTPException(status_code=404, detail="算法不存在")
|
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(
|
async def update_algorithm(
|
||||||
algorithm_id: int,
|
algorithm_id: int,
|
||||||
algorithm: AlgorithmUpdate,
|
algorithm_data: Dict[str, Any],
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""更新算法信息"""
|
"""更新算法信息"""
|
||||||
@ -75,13 +73,14 @@ async def update_algorithm(
|
|||||||
if not db_algorithm:
|
if not db_algorithm:
|
||||||
raise HTTPException(status_code=404, detail="算法不存在")
|
raise HTTPException(status_code=404, detail="算法不存在")
|
||||||
|
|
||||||
update_data = algorithm.dict(exclude_unset=True)
|
for field, value in algorithm_data.items():
|
||||||
for field, value in update_data.items():
|
if hasattr(db_algorithm, field):
|
||||||
setattr(db_algorithm, field, value)
|
setattr(db_algorithm, field, value)
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(db_algorithm)
|
db.refresh(db_algorithm)
|
||||||
return db_algorithm
|
|
||||||
|
return db_algorithm.to_dict()
|
||||||
|
|
||||||
@router.delete("/{algorithm_id}", summary="删除算法")
|
@router.delete("/{algorithm_id}", summary="删除算法")
|
||||||
async def delete_algorithm(
|
async def delete_algorithm(
|
||||||
@ -97,7 +96,7 @@ async def delete_algorithm(
|
|||||||
db.commit()
|
db.commit()
|
||||||
return {"message": "算法删除成功"}
|
return {"message": "算法删除成功"}
|
||||||
|
|
||||||
@router.patch("/{algorithm_id}/status", response_model=AlgorithmResponse, summary="更新算法状态")
|
@router.patch("/{algorithm_id}/status", summary="更新算法状态")
|
||||||
async def update_algorithm_status(
|
async def update_algorithm_status(
|
||||||
algorithm_id: int,
|
algorithm_id: int,
|
||||||
status: str = Query(..., description="新状态"),
|
status: str = Query(..., description="新状态"),
|
||||||
@ -111,9 +110,10 @@ async def update_algorithm_status(
|
|||||||
algorithm.status = status
|
algorithm.status = status
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(algorithm)
|
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(
|
async def toggle_algorithm_enabled(
|
||||||
algorithm_id: int,
|
algorithm_id: int,
|
||||||
enabled: bool = Query(..., description="是否启用"),
|
enabled: bool = Query(..., description="是否启用"),
|
||||||
@ -127,4 +127,5 @@ async def toggle_algorithm_enabled(
|
|||||||
algorithm.is_enabled = enabled
|
algorithm.is_enabled = enabled
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(algorithm)
|
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 fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from typing import List, Optional
|
from typing import List, Optional, Dict, Any
|
||||||
from core.database import get_db
|
from core.database import get_db
|
||||||
from models.device import Device
|
from models.device import Device
|
||||||
from schemas.device import (
|
|
||||||
DeviceCreate,
|
|
||||||
DeviceUpdate,
|
|
||||||
DeviceResponse,
|
|
||||||
DeviceListResponse
|
|
||||||
)
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.post("/", response_model=DeviceResponse, summary="创建设备")
|
@router.post("/", summary="创建设备")
|
||||||
async def create_device(
|
async def create_device(
|
||||||
device: DeviceCreate,
|
device_data: Dict[str, Any],
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""创建新的设备"""
|
"""创建新的设备"""
|
||||||
db_device = Device(**device.dict())
|
db_device = Device(**device_data)
|
||||||
db.add(db_device)
|
db.add(db_device)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(db_device)
|
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(
|
async def get_devices(
|
||||||
skip: int = Query(0, ge=0, description="跳过记录数"),
|
skip: int = Query(0, ge=0, description="跳过记录数"),
|
||||||
limit: int = Query(10, ge=1, le=100, description="返回记录数"),
|
limit: int = Query(10, ge=1, le=100, description="返回记录数"),
|
||||||
@ -52,14 +61,32 @@ async def get_devices(
|
|||||||
total = query.count()
|
total = query.count()
|
||||||
devices = query.offset(skip).limit(limit).all()
|
devices = query.offset(skip).limit(limit).all()
|
||||||
|
|
||||||
return DeviceListResponse(
|
device_list = []
|
||||||
devices=devices,
|
for device in devices:
|
||||||
total=total,
|
device_list.append({
|
||||||
page=skip // limit + 1,
|
"id": device.id,
|
||||||
size=limit
|
"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(
|
async def get_device(
|
||||||
device_id: int,
|
device_id: int,
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
@ -68,12 +95,27 @@ async def get_device(
|
|||||||
device = db.query(Device).filter(Device.id == device_id).first()
|
device = db.query(Device).filter(Device.id == device_id).first()
|
||||||
if not device:
|
if not device:
|
||||||
raise HTTPException(status_code=404, detail="设备不存在")
|
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(
|
async def update_device(
|
||||||
device_id: int,
|
device_id: int,
|
||||||
device: DeviceUpdate,
|
device_data: Dict[str, Any],
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""更新设备信息"""
|
"""更新设备信息"""
|
||||||
@ -81,13 +123,28 @@ async def update_device(
|
|||||||
if not db_device:
|
if not db_device:
|
||||||
raise HTTPException(status_code=404, detail="设备不存在")
|
raise HTTPException(status_code=404, detail="设备不存在")
|
||||||
|
|
||||||
update_data = device.dict(exclude_unset=True)
|
for field, value in device_data.items():
|
||||||
for field, value in update_data.items():
|
if hasattr(db_device, field):
|
||||||
setattr(db_device, field, value)
|
setattr(db_device, field, value)
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(db_device)
|
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="删除设备")
|
@router.delete("/{device_id}", summary="删除设备")
|
||||||
async def delete_device(
|
async def delete_device(
|
||||||
@ -103,7 +160,7 @@ async def delete_device(
|
|||||||
db.commit()
|
db.commit()
|
||||||
return {"message": "设备删除成功"}
|
return {"message": "设备删除成功"}
|
||||||
|
|
||||||
@router.patch("/{device_id}/status", response_model=DeviceResponse, summary="更新设备状态")
|
@router.patch("/{device_id}/status", summary="更新设备状态")
|
||||||
async def update_device_status(
|
async def update_device_status(
|
||||||
device_id: int,
|
device_id: int,
|
||||||
status: str = Query(..., description="新状态"),
|
status: str = Query(..., description="新状态"),
|
||||||
@ -117,9 +174,24 @@ async def update_device_status(
|
|||||||
device.status = status
|
device.status = status
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(device)
|
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(
|
async def toggle_device_enabled(
|
||||||
device_id: int,
|
device_id: int,
|
||||||
enabled: bool = Query(..., description="是否启用"),
|
enabled: bool = Query(..., description="是否启用"),
|
||||||
@ -133,7 +205,22 @@ async def toggle_device_enabled(
|
|||||||
device.is_enabled = enabled
|
device.is_enabled = enabled
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(device)
|
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="获取设备类型列表")
|
@router.get("/types/list", summary="获取设备类型列表")
|
||||||
async def get_device_types():
|
async def get_device_types():
|
||||||
|
@ -1,31 +1,43 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from typing import List, Optional
|
from typing import List, Optional, Dict, Any
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from core.database import get_db
|
from core.database import get_db
|
||||||
from models.event import Event
|
from models.event import Event
|
||||||
from schemas.event import (
|
|
||||||
EventCreate,
|
|
||||||
EventUpdate,
|
|
||||||
EventResponse,
|
|
||||||
EventListResponse
|
|
||||||
)
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.post("/", response_model=EventResponse, summary="创建事件")
|
@router.post("/", summary="创建事件")
|
||||||
async def create_event(
|
async def create_event(
|
||||||
event: EventCreate,
|
event_data: Dict[str, Any],
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""创建新的事件"""
|
"""创建新的事件"""
|
||||||
db_event = Event(**event.dict())
|
db_event = Event(**event_data)
|
||||||
db.add(db_event)
|
db.add(db_event)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(db_event)
|
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(
|
async def get_events(
|
||||||
skip: int = Query(0, ge=0, description="跳过记录数"),
|
skip: int = Query(0, ge=0, description="跳过记录数"),
|
||||||
limit: int = Query(10, ge=1, le=100, description="返回记录数"),
|
limit: int = Query(10, ge=1, le=100, description="返回记录数"),
|
||||||
@ -73,14 +85,35 @@ async def get_events(
|
|||||||
total = query.count()
|
total = query.count()
|
||||||
events = query.offset(skip).limit(limit).all()
|
events = query.offset(skip).limit(limit).all()
|
||||||
|
|
||||||
return EventListResponse(
|
event_list = []
|
||||||
events=events,
|
for event in events:
|
||||||
total=total,
|
event_list.append({
|
||||||
page=skip // limit + 1,
|
"id": event.id,
|
||||||
size=limit
|
"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(
|
async def get_event(
|
||||||
event_id: int,
|
event_id: int,
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
@ -89,12 +122,30 @@ async def get_event(
|
|||||||
event = db.query(Event).filter(Event.id == event_id).first()
|
event = db.query(Event).filter(Event.id == event_id).first()
|
||||||
if not event:
|
if not event:
|
||||||
raise HTTPException(status_code=404, detail="事件不存在")
|
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(
|
async def update_event(
|
||||||
event_id: int,
|
event_id: int,
|
||||||
event: EventUpdate,
|
event_data: Dict[str, Any],
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""更新事件信息"""
|
"""更新事件信息"""
|
||||||
@ -102,13 +153,31 @@ async def update_event(
|
|||||||
if not db_event:
|
if not db_event:
|
||||||
raise HTTPException(status_code=404, detail="事件不存在")
|
raise HTTPException(status_code=404, detail="事件不存在")
|
||||||
|
|
||||||
update_data = event.dict(exclude_unset=True)
|
for field, value in event_data.items():
|
||||||
for field, value in update_data.items():
|
if hasattr(db_event, field):
|
||||||
setattr(db_event, field, value)
|
setattr(db_event, field, value)
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(db_event)
|
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="删除事件")
|
@router.delete("/{event_id}", summary="删除事件")
|
||||||
async def delete_event(
|
async def delete_event(
|
||||||
@ -124,7 +193,7 @@ async def delete_event(
|
|||||||
db.commit()
|
db.commit()
|
||||||
return {"message": "事件删除成功"}
|
return {"message": "事件删除成功"}
|
||||||
|
|
||||||
@router.patch("/{event_id}/status", response_model=EventResponse, summary="更新事件状态")
|
@router.patch("/{event_id}/status", summary="更新事件状态")
|
||||||
async def update_event_status(
|
async def update_event_status(
|
||||||
event_id: int,
|
event_id: int,
|
||||||
status: str = Query(..., description="新状态"),
|
status: str = Query(..., description="新状态"),
|
||||||
@ -146,7 +215,25 @@ async def update_event_status(
|
|||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(event)
|
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="获取事件类型列表")
|
@router.get("/types/list", summary="获取事件类型列表")
|
||||||
async def get_event_types():
|
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