feat 接口

This commit is contained in:
zlgecc 2025-08-05 11:57:14 +08:00
parent 3bd5e77ce4
commit a8e24157e7
21 changed files with 2441 additions and 408 deletions

View 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
View 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"]
```

View File

@ -1,6 +1,7 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from routers import algorithms, events, devices
from fastapi.staticfiles import StaticFiles
from routers import algorithms, events, devices, dashboard, monitors, alarms, scenes, upload, auth
from core.database import engine
from models.base import Base
@ -22,10 +23,19 @@ app.add_middleware(
allow_headers=["*"],
)
# 静态文件服务
app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads")
# 注册路由
app.include_router(algorithms.router, prefix="/api/algorithms", tags=["算法管理"])
app.include_router(events.router, prefix="/api/events", tags=["事件管理"])
app.include_router(devices.router, prefix="/api/devices", tags=["设备管理"])
app.include_router(dashboard.router, prefix="/api/dashboard", tags=["仪表板"])
app.include_router(monitors.router, prefix="/api/monitors", tags=["监控管理"])
app.include_router(alarms.router, prefix="/api/alarms", tags=["告警管理"])
app.include_router(scenes.router, prefix="/api/scenes", tags=["场景管理"])
app.include_router(upload.router, prefix="/api/upload", tags=["文件上传"])
app.include_router(auth.router, prefix="/api/auth", tags=["用户认证"])
@app.get("/")
async def root():

View File

@ -1,5 +1,6 @@
from sqlalchemy import Column, String, Text, Boolean, Float, Integer
from .base import BaseModel
import json
class Algorithm(BaseModel):
__tablename__ = "algorithms"
@ -17,3 +18,24 @@ class Algorithm(BaseModel):
is_enabled = Column(Boolean, default=True, comment="是否启用")
creator = Column(String(50), comment="创建者")
tags = Column(Text, comment="标签JSON格式")
def to_dict(self) -> dict:
"""将模型实例转换为字典格式"""
return {
"id": self.id,
"name": self.name,
"description": self.description,
"version": self.version,
"model_path": self.model_path,
"config_path": self.config_path,
"status": self.status,
"accuracy": self.accuracy,
"detection_classes": self.detection_classes,
"input_size": self.input_size,
"inference_time": self.inference_time,
"is_enabled": self.is_enabled,
"creator": self.creator,
"tags": self.tags,
"created_at": self.created_at,
"updated_at": self.updated_at
}

View File

@ -1,21 +1,17 @@
# 边检CV算法接口服务
### 技术栈
- **后端框架**: FastAPI
- **数据库**: SQLite (SQLAlchemy ORM)
- **AI模型**: YOLOv11n
- **进程管理**: Supervisor
- **开发语言**: Python 3.8+
## 项目简介
### 项目结构
这是一个简化的边检计算机视觉算法管理系统API服务采用FastAPI框架开发。
## 项目结构
```
server/
├── app.py # FastAPI主应用
├── start.py # 启动脚本
├── requirements.txt # Python依赖
├── app.py # 主应用文件
├── requirements.txt # 依赖包列表
├── init_data.py # 初始化数据脚本
├── env.example # 环境变量示例
├── init_data.py # 示例数据初始化
├── core/ # 核心配置
│ └── database.py # 数据库配置
├── models/ # 数据模型
@ -24,154 +20,91 @@ server/
│ ├── algorithm.py # 算法模型
│ ├── device.py # 设备模型
│ └── event.py # 事件模型
├── schemas/ # Pydantic模型
│ ├── __init__.py
│ ├── algorithm.py # 算法相关模型
│ ├── device.py # 设备相关模型
│ └── event.py # 事件相关模型
└── routers/ # API路由
└── routers/ # 路由接口
├── __init__.py
├── algorithms.py # 算法管理接口
├── devices.py # 设备管理接口
└── events.py # 事件管理接口
```
### 功能模块
## 简化设计
#### 1. 算法管理模块
- 算法的增删改查
- 算法状态管理(启用/禁用)
- 算法版本管理
- 算法性能指标(准确率、推理时间等)
本项目采用简化的架构设计:
#### 2. 设备管理模块
- 设备信息管理(摄像头、传感器等)
- 设备状态监控
- 设备类型管理
- 设备地理位置信息
1. **去掉schemas层**:直接使用字典进行数据传递,减少代码复杂度
2. **简化响应格式**:所有接口直接返回字典格式,便于维护
3. **保持核心功能**保留所有必要的CRUD操作和业务逻辑
#### 3. 事件管理模块
- 事件记录和查询
- 事件状态管理
- 事件统计分析
- 告警管理
## API接口
### API接口
### 算法管理 (/api/algorithms)
- `POST /` - 创建算法
- `GET /` - 获取算法列表
- `GET /{id}` - 获取算法详情
- `PUT /{id}` - 更新算法
- `DELETE /{id}` - 删除算法
- `PATCH /{id}/status` - 更新算法状态
- `PATCH /{id}/enable` - 启用/禁用算法
#### 算法管理接口
- `POST /api/algorithms/` - 创建算法
- `GET /api/algorithms/` - 获取算法列表
- `GET /api/algorithms/{id}` - 获取算法详情
- `PUT /api/algorithms/{id}` - 更新算法
- `DELETE /api/algorithms/{id}` - 删除算法
- `PATCH /api/algorithms/{id}/status` - 更新算法状态
- `PATCH /api/algorithms/{id}/enable` - 启用/禁用算法
### 设备管理 (/api/devices)
- `POST /` - 创建设备
- `GET /` - 获取设备列表
- `GET /{id}` - 获取设备详情
- `PUT /{id}` - 更新设备
- `DELETE /{id}` - 删除设备
- `PATCH /{id}/status` - 更新设备状态
- `PATCH /{id}/enable` - 启用/禁用设备
- `GET /types/list` - 获取设备类型列表
- `GET /status/stats` - 获取设备状态统计
#### 设备管理接口
- `POST /api/devices/` - 创建设备
- `GET /api/devices/` - 获取设备列表
- `GET /api/devices/{id}` - 获取设备详情
- `PUT /api/devices/{id}` - 更新设备
- `DELETE /api/devices/{id}` - 删除设备
- `PATCH /api/devices/{id}/status` - 更新设备状态
- `PATCH /api/devices/{id}/enable` - 启用/禁用设备
- `GET /api/devices/types/list` - 获取设备类型列表
- `GET /api/devices/status/stats` - 获取设备状态统计
### 事件管理 (/api/events)
- `POST /` - 创建事件
- `GET /` - 获取事件列表
- `GET /{id}` - 获取事件详情
- `PUT /{id}` - 更新事件
- `DELETE /{id}` - 删除事件
- `PATCH /{id}/status` - 更新事件状态
- `GET /types/list` - 获取事件类型列表
- `GET /stats/summary` - 获取事件统计摘要
- `GET /stats/by-type` - 按类型统计事件
#### 事件管理接口
- `POST /api/events/` - 创建事件
- `GET /api/events/` - 获取事件列表
- `GET /api/events/{id}` - 获取事件详情
- `PUT /api/events/{id}` - 更新事件
- `DELETE /api/events/{id}` - 删除事件
- `PATCH /api/events/{id}/status` - 更新事件状态
- `GET /api/events/types/list` - 获取事件类型列表
- `GET /api/events/stats/summary` - 获取事件统计摘要
- `GET /api/events/stats/by-type` - 按类型统计事件
## 安装和运行
### 安装和运行
#### 1. 环境准备
1. 安装依赖:
```bash
# 创建虚拟环境
conda create -n border_inspection python=3.8
conda activate border_inspection
# 安装依赖
pip install -r requirements.txt
```
#### 2. 初始化数据库
2. 配置环境变量:
```bash
# 启动服务(会自动创建数据库表)
python app.py
cp env.example .env
# 编辑.env文件配置数据库连接等
```
#### 3. 初始化示例数据
3. 初始化数据
```bash
python init_data.py
```
#### 4. 启动服务
4. 启动服务
```bash
python app.py
```
服务将在 `http://localhost:8000` 启动
服务将在 http://localhost:8000 启动API文档访问 http://localhost:8000/docs
### API文档
## 技术栈
启动服务后可以访问以下地址查看API文档
- Swagger UI: `http://localhost:8000/docs`
- ReDoc: `http://localhost:8000/redoc`
- **框架**: FastAPI
- **数据库**: SQLAlchemy + SQLite
- **ORM**: SQLAlchemy
- **文档**: 自动生成OpenAPI文档
### 数据库设计
## 特点
#### 算法表 (algorithms)
- 基本信息:名称、描述、版本
- 模型信息:模型路径、配置文件路径
- 性能指标:准确率、推理时间、输入尺寸
- 状态管理:启用状态、算法状态
- 分类信息:检测类别、标签
#### 设备表 (devices)
- 基本信息:名称、类型、位置
- 连接信息IP地址、端口、用户名、密码
- 视频流RTSP地址、分辨率、帧率
- 状态信息:在线状态、最后心跳时间
- 地理位置:经纬度坐标
#### 事件表 (events)
- 事件信息类型、设备ID、算法ID
- 检测结果:置信度、边界框、检测对象
- 状态管理:事件状态、严重程度
- 告警信息:是否告警、告警发送状态
- 处理信息:处理人员、处理备注、解决时间
### 开发说明
1. **添加新模型**: 在 `models/` 目录下创建新的模型文件
2. **添加新接口**: 在 `routers/` 目录下创建新的路由文件
3. **添加新Schema**: 在 `schemas/` 目录下创建新的Pydantic模型
4. **数据库迁移**: 修改模型后重启服务,数据库表会自动更新
### 部署说明
#### 使用Supervisor管理进程
```ini
[program:border_inspection]
command=python /path/to/server/start.py
directory=/path/to/server
user=www-data
autostart=true
autorestart=true
stderr_logfile=/var/log/border_inspection.err.log
stdout_logfile=/var/log/border_inspection.out.log
```
#### 环境变量配置
复制 `env.example``.env` 并修改相应配置:
```bash
cp env.example .env
# 编辑 .env 文件
```
- 简化的架构,易于维护
- 完整的CRUD操作
- 支持分页和筛选
- 自动生成API文档
- 支持CORS跨域
- 健康检查接口

View File

@ -10,3 +10,4 @@ aiofiles==23.2.1
pillow==10.1.0
opencv-python==4.8.1.78
ultralytics==8.0.196
PyJWT==2.8.0

236
server/routers/alarms.py Normal file
View 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)}")

View File

@ -1,30 +1,25 @@
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from typing import List, Optional
from typing import List, Optional, Dict, Any
from core.database import get_db
from models.algorithm import Algorithm
from schemas.algorithm import (
AlgorithmCreate,
AlgorithmUpdate,
AlgorithmResponse,
AlgorithmListResponse
)
router = APIRouter()
@router.post("/", response_model=AlgorithmResponse, summary="创建算法")
@router.post("/", summary="创建算法")
async def create_algorithm(
algorithm: AlgorithmCreate,
algorithm_data: Dict[str, Any],
db: Session = Depends(get_db)
):
"""创建新的算法"""
db_algorithm = Algorithm(**algorithm.dict())
db_algorithm = Algorithm(**algorithm_data)
db.add(db_algorithm)
db.commit()
db.refresh(db_algorithm)
return db_algorithm
@router.get("/", response_model=AlgorithmListResponse, summary="获取算法列表")
return db_algorithm.to_dict()
@router.get("/", summary="获取算法列表")
async def get_algorithms(
skip: int = Query(0, ge=0, description="跳过记录数"),
limit: int = Query(10, ge=1, le=100, description="返回记录数"),
@ -46,14 +41,16 @@ async def get_algorithms(
total = query.count()
algorithms = query.offset(skip).limit(limit).all()
return AlgorithmListResponse(
algorithms=algorithms,
total=total,
page=skip // limit + 1,
size=limit
)
algorithm_list = [algorithm.to_dict() for algorithm in algorithms]
@router.get("/{algorithm_id}", response_model=AlgorithmResponse, summary="获取算法详情")
return {
"algorithms": algorithm_list,
"total": total,
"page": skip // limit + 1,
"size": limit
}
@router.get("/{algorithm_id}", summary="获取算法详情")
async def get_algorithm(
algorithm_id: int,
db: Session = Depends(get_db)
@ -62,12 +59,13 @@ async def get_algorithm(
algorithm = db.query(Algorithm).filter(Algorithm.id == algorithm_id).first()
if not algorithm:
raise HTTPException(status_code=404, detail="算法不存在")
return algorithm
@router.put("/{algorithm_id}", response_model=AlgorithmResponse, summary="更新算法")
return algorithm.to_dict()
@router.put("/{algorithm_id}", summary="更新算法")
async def update_algorithm(
algorithm_id: int,
algorithm: AlgorithmUpdate,
algorithm_data: Dict[str, Any],
db: Session = Depends(get_db)
):
"""更新算法信息"""
@ -75,13 +73,14 @@ async def update_algorithm(
if not db_algorithm:
raise HTTPException(status_code=404, detail="算法不存在")
update_data = algorithm.dict(exclude_unset=True)
for field, value in update_data.items():
for field, value in algorithm_data.items():
if hasattr(db_algorithm, field):
setattr(db_algorithm, field, value)
db.commit()
db.refresh(db_algorithm)
return db_algorithm
return db_algorithm.to_dict()
@router.delete("/{algorithm_id}", summary="删除算法")
async def delete_algorithm(
@ -97,7 +96,7 @@ async def delete_algorithm(
db.commit()
return {"message": "算法删除成功"}
@router.patch("/{algorithm_id}/status", response_model=AlgorithmResponse, summary="更新算法状态")
@router.patch("/{algorithm_id}/status", summary="更新算法状态")
async def update_algorithm_status(
algorithm_id: int,
status: str = Query(..., description="新状态"),
@ -111,9 +110,10 @@ async def update_algorithm_status(
algorithm.status = status
db.commit()
db.refresh(algorithm)
return algorithm
@router.patch("/{algorithm_id}/enable", response_model=AlgorithmResponse, summary="启用/禁用算法")
return algorithm.to_dict()
@router.patch("/{algorithm_id}/enable", summary="启用/禁用算法")
async def toggle_algorithm_enabled(
algorithm_id: int,
enabled: bool = Query(..., description="是否启用"),
@ -127,4 +127,5 @@ async def toggle_algorithm_enabled(
algorithm.is_enabled = enabled
db.commit()
db.refresh(algorithm)
return algorithm
return algorithm.to_dict()

237
server/routers/auth.py Normal file
View 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
View 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)}")

View File

@ -1,30 +1,39 @@
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from typing import List, Optional
from typing import List, Optional, Dict, Any
from core.database import get_db
from models.device import Device
from schemas.device import (
DeviceCreate,
DeviceUpdate,
DeviceResponse,
DeviceListResponse
)
router = APIRouter()
@router.post("/", response_model=DeviceResponse, summary="创建设备")
@router.post("/", summary="创建设备")
async def create_device(
device: DeviceCreate,
device_data: Dict[str, Any],
db: Session = Depends(get_db)
):
"""创建新的设备"""
db_device = Device(**device.dict())
db_device = Device(**device_data)
db.add(db_device)
db.commit()
db.refresh(db_device)
return db_device
@router.get("/", response_model=DeviceListResponse, summary="获取设备列表")
return {
"id": db_device.id,
"name": db_device.name,
"device_type": db_device.device_type,
"ip_address": db_device.ip_address,
"port": db_device.port,
"username": db_device.username,
"password": db_device.password,
"location": db_device.location,
"status": db_device.status,
"is_enabled": db_device.is_enabled,
"description": db_device.description,
"created_at": db_device.created_at,
"updated_at": db_device.updated_at
}
@router.get("/", summary="获取设备列表")
async def get_devices(
skip: int = Query(0, ge=0, description="跳过记录数"),
limit: int = Query(10, ge=1, le=100, description="返回记录数"),
@ -52,14 +61,32 @@ async def get_devices(
total = query.count()
devices = query.offset(skip).limit(limit).all()
return DeviceListResponse(
devices=devices,
total=total,
page=skip // limit + 1,
size=limit
)
device_list = []
for device in devices:
device_list.append({
"id": device.id,
"name": device.name,
"device_type": device.device_type,
"ip_address": device.ip_address,
"port": device.port,
"username": device.username,
"password": device.password,
"location": device.location,
"status": device.status,
"is_enabled": device.is_enabled,
"description": device.description,
"created_at": device.created_at,
"updated_at": device.updated_at
})
@router.get("/{device_id}", response_model=DeviceResponse, summary="获取设备详情")
return {
"devices": device_list,
"total": total,
"page": skip // limit + 1,
"size": limit
}
@router.get("/{device_id}", summary="获取设备详情")
async def get_device(
device_id: int,
db: Session = Depends(get_db)
@ -68,12 +95,27 @@ async def get_device(
device = db.query(Device).filter(Device.id == device_id).first()
if not device:
raise HTTPException(status_code=404, detail="设备不存在")
return device
@router.put("/{device_id}", response_model=DeviceResponse, summary="更新设备")
return {
"id": device.id,
"name": device.name,
"device_type": device.device_type,
"ip_address": device.ip_address,
"port": device.port,
"username": device.username,
"password": device.password,
"location": device.location,
"status": device.status,
"is_enabled": device.is_enabled,
"description": device.description,
"created_at": device.created_at,
"updated_at": device.updated_at
}
@router.put("/{device_id}", summary="更新设备")
async def update_device(
device_id: int,
device: DeviceUpdate,
device_data: Dict[str, Any],
db: Session = Depends(get_db)
):
"""更新设备信息"""
@ -81,13 +123,28 @@ async def update_device(
if not db_device:
raise HTTPException(status_code=404, detail="设备不存在")
update_data = device.dict(exclude_unset=True)
for field, value in update_data.items():
for field, value in device_data.items():
if hasattr(db_device, field):
setattr(db_device, field, value)
db.commit()
db.refresh(db_device)
return db_device
return {
"id": db_device.id,
"name": db_device.name,
"device_type": db_device.device_type,
"ip_address": db_device.ip_address,
"port": db_device.port,
"username": db_device.username,
"password": db_device.password,
"location": db_device.location,
"status": db_device.status,
"is_enabled": db_device.is_enabled,
"description": db_device.description,
"created_at": db_device.created_at,
"updated_at": db_device.updated_at
}
@router.delete("/{device_id}", summary="删除设备")
async def delete_device(
@ -103,7 +160,7 @@ async def delete_device(
db.commit()
return {"message": "设备删除成功"}
@router.patch("/{device_id}/status", response_model=DeviceResponse, summary="更新设备状态")
@router.patch("/{device_id}/status", summary="更新设备状态")
async def update_device_status(
device_id: int,
status: str = Query(..., description="新状态"),
@ -117,9 +174,24 @@ async def update_device_status(
device.status = status
db.commit()
db.refresh(device)
return device
@router.patch("/{device_id}/enable", response_model=DeviceResponse, summary="启用/禁用设备")
return {
"id": device.id,
"name": device.name,
"device_type": device.device_type,
"ip_address": device.ip_address,
"port": device.port,
"username": device.username,
"password": device.password,
"location": device.location,
"status": device.status,
"is_enabled": device.is_enabled,
"description": device.description,
"created_at": device.created_at,
"updated_at": device.updated_at
}
@router.patch("/{device_id}/enable", summary="启用/禁用设备")
async def toggle_device_enabled(
device_id: int,
enabled: bool = Query(..., description="是否启用"),
@ -133,7 +205,22 @@ async def toggle_device_enabled(
device.is_enabled = enabled
db.commit()
db.refresh(device)
return device
return {
"id": device.id,
"name": device.name,
"device_type": device.device_type,
"ip_address": device.ip_address,
"port": device.port,
"username": device.username,
"password": device.password,
"location": device.location,
"status": device.status,
"is_enabled": device.is_enabled,
"description": device.description,
"created_at": device.created_at,
"updated_at": device.updated_at
}
@router.get("/types/list", summary="获取设备类型列表")
async def get_device_types():

View File

@ -1,31 +1,43 @@
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from typing import List, Optional
from typing import List, Optional, Dict, Any
from datetime import datetime
from core.database import get_db
from models.event import Event
from schemas.event import (
EventCreate,
EventUpdate,
EventResponse,
EventListResponse
)
router = APIRouter()
@router.post("/", response_model=EventResponse, summary="创建事件")
@router.post("/", summary="创建事件")
async def create_event(
event: EventCreate,
event_data: Dict[str, Any],
db: Session = Depends(get_db)
):
"""创建新的事件"""
db_event = Event(**event.dict())
db_event = Event(**event_data)
db.add(db_event)
db.commit()
db.refresh(db_event)
return db_event
@router.get("/", response_model=EventListResponse, summary="获取事件列表")
return {
"id": db_event.id,
"event_type": db_event.event_type,
"device_id": db_event.device_id,
"algorithm_id": db_event.algorithm_id,
"severity": db_event.severity,
"status": db_event.status,
"is_alert": db_event.is_alert,
"description": db_event.description,
"image_path": db_event.image_path,
"video_path": db_event.video_path,
"confidence": db_event.confidence,
"bbox": db_event.bbox,
"resolution_notes": db_event.resolution_notes,
"resolved_at": db_event.resolved_at,
"created_at": db_event.created_at,
"updated_at": db_event.updated_at
}
@router.get("/", summary="获取事件列表")
async def get_events(
skip: int = Query(0, ge=0, description="跳过记录数"),
limit: int = Query(10, ge=1, le=100, description="返回记录数"),
@ -73,14 +85,35 @@ async def get_events(
total = query.count()
events = query.offset(skip).limit(limit).all()
return EventListResponse(
events=events,
total=total,
page=skip // limit + 1,
size=limit
)
event_list = []
for event in events:
event_list.append({
"id": event.id,
"event_type": event.event_type,
"device_id": event.device_id,
"algorithm_id": event.algorithm_id,
"severity": event.severity,
"status": event.status,
"is_alert": event.is_alert,
"description": event.description,
"image_path": event.image_path,
"video_path": event.video_path,
"confidence": event.confidence,
"bbox": event.bbox,
"resolution_notes": event.resolution_notes,
"resolved_at": event.resolved_at,
"created_at": event.created_at,
"updated_at": event.updated_at
})
@router.get("/{event_id}", response_model=EventResponse, summary="获取事件详情")
return {
"events": event_list,
"total": total,
"page": skip // limit + 1,
"size": limit
}
@router.get("/{event_id}", summary="获取事件详情")
async def get_event(
event_id: int,
db: Session = Depends(get_db)
@ -89,12 +122,30 @@ async def get_event(
event = db.query(Event).filter(Event.id == event_id).first()
if not event:
raise HTTPException(status_code=404, detail="事件不存在")
return event
@router.put("/{event_id}", response_model=EventResponse, summary="更新事件")
return {
"id": event.id,
"event_type": event.event_type,
"device_id": event.device_id,
"algorithm_id": event.algorithm_id,
"severity": event.severity,
"status": event.status,
"is_alert": event.is_alert,
"description": event.description,
"image_path": event.image_path,
"video_path": event.video_path,
"confidence": event.confidence,
"bbox": event.bbox,
"resolution_notes": event.resolution_notes,
"resolved_at": event.resolved_at,
"created_at": event.created_at,
"updated_at": event.updated_at
}
@router.put("/{event_id}", summary="更新事件")
async def update_event(
event_id: int,
event: EventUpdate,
event_data: Dict[str, Any],
db: Session = Depends(get_db)
):
"""更新事件信息"""
@ -102,13 +153,31 @@ async def update_event(
if not db_event:
raise HTTPException(status_code=404, detail="事件不存在")
update_data = event.dict(exclude_unset=True)
for field, value in update_data.items():
for field, value in event_data.items():
if hasattr(db_event, field):
setattr(db_event, field, value)
db.commit()
db.refresh(db_event)
return db_event
return {
"id": db_event.id,
"event_type": db_event.event_type,
"device_id": db_event.device_id,
"algorithm_id": db_event.algorithm_id,
"severity": db_event.severity,
"status": db_event.status,
"is_alert": db_event.is_alert,
"description": db_event.description,
"image_path": db_event.image_path,
"video_path": db_event.video_path,
"confidence": db_event.confidence,
"bbox": db_event.bbox,
"resolution_notes": db_event.resolution_notes,
"resolved_at": db_event.resolved_at,
"created_at": db_event.created_at,
"updated_at": db_event.updated_at
}
@router.delete("/{event_id}", summary="删除事件")
async def delete_event(
@ -124,7 +193,7 @@ async def delete_event(
db.commit()
return {"message": "事件删除成功"}
@router.patch("/{event_id}/status", response_model=EventResponse, summary="更新事件状态")
@router.patch("/{event_id}/status", summary="更新事件状态")
async def update_event_status(
event_id: int,
status: str = Query(..., description="新状态"),
@ -146,7 +215,25 @@ async def update_event_status(
db.commit()
db.refresh(event)
return event
return {
"id": event.id,
"event_type": event.event_type,
"device_id": event.device_id,
"algorithm_id": event.algorithm_id,
"severity": event.severity,
"status": event.status,
"is_alert": event.is_alert,
"description": event.description,
"image_path": event.image_path,
"video_path": event.video_path,
"confidence": event.confidence,
"bbox": event.bbox,
"resolution_notes": event.resolution_notes,
"resolved_at": event.resolved_at,
"created_at": event.created_at,
"updated_at": event.updated_at
}
@router.get("/types/list", summary="获取事件类型列表")
async def get_event_types():

185
server/routers/monitors.py Normal file
View 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
View 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
View 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)}")

View File

@ -1,5 +0,0 @@
from .algorithm import *
from .device import *
from .event import *
__all__ = ["algorithm", "device", "event"]

View File

@ -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

View File

@ -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

View File

@ -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
View 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
View 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. 接口文档需要及时更新