diff --git a/server/app.py b/server/app.py new file mode 100644 index 0000000..fedec53 --- /dev/null +++ b/server/app.py @@ -0,0 +1,40 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from routers import algorithms, events, devices +from core.database import engine +from models.base import Base + +# 创建数据库表 +Base.metadata.create_all(bind=engine) + +app = FastAPI( + title="边检CV算法接口服务", + description="边检计算机视觉算法管理系统API", + version="1.0.0" +) + +# 配置CORS +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # 生产环境中应该指定具体域名 + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# 注册路由 +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.get("/") +async def root(): + return {"message": "边检CV算法接口服务"} + +@app.get("/health") +async def health_check(): + return {"status": "healthy"} + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000, reload=True, workers=10) \ No newline at end of file diff --git a/server/core/database.py b/server/core/database.py new file mode 100644 index 0000000..4d118a4 --- /dev/null +++ b/server/core/database.py @@ -0,0 +1,27 @@ +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker +import os + +# 数据库URL +SQLALCHEMY_DATABASE_URL = "sqlite:///./border_inspection.db" + +# 创建数据库引擎 +engine = create_engine( + SQLALCHEMY_DATABASE_URL, + connect_args={"check_same_thread": False} +) + +# 创建会话工厂 +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +# 创建基础模型类 +Base = declarative_base() + +# 依赖注入函数 +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() \ No newline at end of file diff --git a/server/env.example b/server/env.example new file mode 100644 index 0000000..f54c603 --- /dev/null +++ b/server/env.example @@ -0,0 +1,19 @@ +# 数据库配置 +DATABASE_URL=sqlite:///./border_inspection.db + +# 服务器配置 +HOST=0.0.0.0 +PORT=8000 + +# 安全配置 +SECRET_KEY=your-secret-key-here +ALGORITHM=HS256 +ACCESS_TOKEN_EXPIRE_MINUTES=30 + +# 文件上传配置 +UPLOAD_DIR=./uploads +MAX_FILE_SIZE=10485760 # 10MB + +# 日志配置 +LOG_LEVEL=INFO +LOG_FILE=./logs/app.log \ No newline at end of file diff --git a/server/init_data.py b/server/init_data.py new file mode 100644 index 0000000..9a1bd15 --- /dev/null +++ b/server/init_data.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python3 +""" +初始化示例数据脚本 +""" + +import sys +import os +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from core.database import SessionLocal +from models.algorithm import Algorithm +from models.device import Device +from models.event import Event +from datetime import datetime, timedelta +import json + +def init_sample_data(): + """初始化示例数据""" + db = SessionLocal() + + try: + # 创建示例算法 + algorithms = [ + { + "name": "YOLOv11n人员检测", + "description": "基于YOLOv11n的人员检测算法,适用于边检场景", + "version": "1.0.0", + "model_path": "/models/yolo11n.pt", + "config_path": "/configs/yolo11n.yaml", + "status": "active", + "accuracy": 0.95, + "detection_classes": json.dumps(["person"]), + "input_size": "640x640", + "inference_time": 15.5, + "is_enabled": True, + "creator": "admin", + "tags": json.dumps(["person", "detection", "yolo"]) + }, + { + "name": "车辆检测算法", + "description": "专门用于车辆检测的深度学习算法", + "version": "2.1.0", + "model_path": "/models/vehicle_detection.pt", + "config_path": "/configs/vehicle.yaml", + "status": "active", + "accuracy": 0.92, + "detection_classes": json.dumps(["car", "truck", "bus", "motorcycle"]), + "input_size": "640x640", + "inference_time": 18.2, + "is_enabled": True, + "creator": "admin", + "tags": json.dumps(["vehicle", "detection"]) + }, + { + "name": "人脸识别算法", + "description": "高精度人脸识别算法", + "version": "1.5.0", + "model_path": "/models/face_recognition.pt", + "config_path": "/configs/face.yaml", + "status": "inactive", + "accuracy": 0.98, + "detection_classes": json.dumps(["face"]), + "input_size": "512x512", + "inference_time": 25.0, + "is_enabled": False, + "creator": "admin", + "tags": json.dumps(["face", "recognition"]) + } + ] + + for alg_data in algorithms: + algorithm = Algorithm(**alg_data) + db.add(algorithm) + + db.commit() + print("✅ 算法数据初始化完成") + + # 创建示例设备 + devices = [ + { + "name": "港口A区主监控", + "device_type": "camera", + "location": "港口A区", + "ip_address": "192.168.1.100", + "port": 554, + "username": "admin", + "password": "admin123", + "rtsp_url": "rtsp://192.168.1.100:554/stream1", + "status": "online", + "resolution": "1920x1080", + "fps": 25, + "algorithm_id": 1, + "is_enabled": True, + "latitude": 22.3193, + "longitude": 114.1694, + "description": "港口A区主要监控摄像头", + "manufacturer": "Hikvision", + "model": "DS-2CD2T47G1-L", + "serial_number": "HK123456789" + }, + { + "name": "港口B区监控", + "device_type": "camera", + "location": "港口B区", + "ip_address": "192.168.1.101", + "port": 554, + "username": "admin", + "password": "admin123", + "rtsp_url": "rtsp://192.168.1.101:554/stream1", + "status": "online", + "resolution": "1920x1080", + "fps": 25, + "algorithm_id": 2, + "is_enabled": True, + "latitude": 22.3195, + "longitude": 114.1696, + "description": "港口B区监控摄像头", + "manufacturer": "Dahua", + "model": "IPC-HFW4431R-ZE", + "serial_number": "DH987654321" + }, + { + "name": "边检站门禁", + "device_type": "gate", + "location": "边检站入口", + "ip_address": "192.168.1.102", + "port": 80, + "username": "admin", + "password": "admin123", + "status": "online", + "is_enabled": True, + "latitude": 22.3190, + "longitude": 114.1690, + "description": "边检站入口门禁系统", + "manufacturer": "Suprema", + "model": "BioStation 2", + "serial_number": "SP123456789" + } + ] + + for dev_data in devices: + device = Device(**dev_data) + db.add(device) + + db.commit() + print("✅ 设备数据初始化完成") + + # 创建示例事件 + events = [ + { + "event_type": "person_detection", + "device_id": 1, + "algorithm_id": 1, + "severity": "medium", + "status": "pending", + "confidence": 0.95, + "bbox": json.dumps([100, 150, 80, 160]), + "image_path": "/events/images/person_001.jpg", + "description": "检测到人员进入监控区域", + "location": "港口A区", + "detected_objects": json.dumps([{"type": "person", "confidence": 0.95}]), + "processing_time": 15.5, + "is_alert": True, + "alert_sent": True + }, + { + "event_type": "vehicle_detection", + "device_id": 2, + "algorithm_id": 2, + "severity": "low", + "status": "resolved", + "confidence": 0.92, + "bbox": json.dumps([200, 100, 120, 80]), + "image_path": "/events/images/vehicle_001.jpg", + "description": "检测到车辆通过", + "location": "港口B区", + "detected_objects": json.dumps([{"type": "car", "confidence": 0.92}]), + "processing_time": 18.2, + "is_alert": False, + "alert_sent": False, + "operator_id": 1, + "resolution_notes": "正常车辆通行", + "resolved_at": datetime.utcnow() - timedelta(hours=2) + }, + { + "event_type": "intrusion", + "device_id": 1, + "algorithm_id": 1, + "severity": "high", + "status": "processing", + "confidence": 0.88, + "bbox": json.dumps([300, 200, 60, 120]), + "image_path": "/events/images/intrusion_001.jpg", + "description": "检测到可疑人员入侵", + "location": "港口A区", + "detected_objects": json.dumps([{"type": "person", "confidence": 0.88}]), + "processing_time": 16.0, + "is_alert": True, + "alert_sent": True + } + ] + + for evt_data in events: + event = Event(**evt_data) + db.add(event) + + db.commit() + print("✅ 事件数据初始化完成") + + print("\n🎉 所有示例数据初始化完成!") + + except Exception as e: + print(f"❌ 初始化数据时出错: {e}") + db.rollback() + finally: + db.close() + +if __name__ == "__main__": + init_sample_data() \ No newline at end of file diff --git a/server/models/__init__.py b/server/models/__init__.py new file mode 100644 index 0000000..8e1e9f7 --- /dev/null +++ b/server/models/__init__.py @@ -0,0 +1,6 @@ +from .algorithm import Algorithm +from .event import Event +from .device import Device +from .base import Base + +__all__ = ["Algorithm", "Event", "Device", "Base"] \ No newline at end of file diff --git a/server/models/algorithm.py b/server/models/algorithm.py new file mode 100644 index 0000000..0860472 --- /dev/null +++ b/server/models/algorithm.py @@ -0,0 +1,19 @@ +from sqlalchemy import Column, String, Text, Boolean, Float, Integer +from .base import BaseModel + +class Algorithm(BaseModel): + __tablename__ = "algorithms" + + name = Column(String(100), nullable=False, comment="算法名称") + description = Column(Text, comment="算法描述") + version = Column(String(20), nullable=False, comment="算法版本") + model_path = Column(String(255), comment="模型文件路径") + config_path = Column(String(255), comment="配置文件路径") + status = Column(String(20), default="inactive", comment="算法状态: active, inactive, training") + accuracy = Column(Float, comment="算法准确率") + detection_classes = Column(Text, comment="检测类别,JSON格式") + input_size = Column(String(20), comment="输入尺寸,如: 640x640") + inference_time = Column(Float, comment="推理时间(ms)") + is_enabled = Column(Boolean, default=True, comment="是否启用") + creator = Column(String(50), comment="创建者") + tags = Column(Text, comment="标签,JSON格式") \ No newline at end of file diff --git a/server/models/base.py b/server/models/base.py new file mode 100644 index 0000000..1be8229 --- /dev/null +++ b/server/models/base.py @@ -0,0 +1,12 @@ +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy import Column, Integer, DateTime +from datetime import datetime + +Base = declarative_base() + +class BaseModel(Base): + __abstract__ = True + + id = Column(Integer, primary_key=True, index=True) + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) \ No newline at end of file diff --git a/server/models/device.py b/server/models/device.py new file mode 100644 index 0000000..0aa036e --- /dev/null +++ b/server/models/device.py @@ -0,0 +1,26 @@ +from sqlalchemy import Column, String, Text, Boolean, Integer, Float, DateTime +from .base import BaseModel + +class Device(BaseModel): + __tablename__ = "devices" + + name = Column(String(100), nullable=False, comment="设备名称") + device_type = Column(String(50), nullable=False, comment="设备类型: camera, sensor, etc") + location = Column(String(200), comment="设备位置") + ip_address = Column(String(50), comment="IP地址") + port = Column(Integer, comment="端口号") + username = Column(String(50), comment="用户名") + password = Column(String(100), comment="密码") + rtsp_url = Column(String(500), comment="RTSP流地址") + status = Column(String(20), default="offline", comment="设备状态: online, offline, error") + resolution = Column(String(20), comment="分辨率,如: 1920x1080") + fps = Column(Integer, comment="帧率") + algorithm_id = Column(Integer, comment="关联的算法ID") + is_enabled = Column(Boolean, default=True, comment="是否启用") + last_heartbeat = Column(DateTime, comment="最后心跳时间") + latitude = Column(Float, comment="纬度") + longitude = Column(Float, comment="经度") + description = Column(Text, comment="设备描述") + manufacturer = Column(String(100), comment="制造商") + model = Column(String(100), comment="设备型号") + serial_number = Column(String(100), comment="序列号") \ No newline at end of file diff --git a/server/models/event.py b/server/models/event.py new file mode 100644 index 0000000..f9d9aaf --- /dev/null +++ b/server/models/event.py @@ -0,0 +1,24 @@ +from sqlalchemy import Column, String, Text, Boolean, Integer, Float, DateTime, JSON +from .base import BaseModel + +class Event(BaseModel): + __tablename__ = "events" + + event_type = Column(String(50), nullable=False, comment="事件类型: person_detection, vehicle_detection, intrusion, etc") + device_id = Column(Integer, nullable=False, comment="关联设备ID") + algorithm_id = Column(Integer, comment="关联算法ID") + severity = Column(String(20), default="medium", comment="严重程度: low, medium, high, critical") + status = Column(String(20), default="pending", comment="事件状态: pending, processing, resolved, ignored") + confidence = Column(Float, comment="置信度") + bbox = Column(Text, comment="边界框坐标,JSON格式: [x, y, width, height]") + image_path = Column(String(500), comment="事件图片路径") + video_path = Column(String(500), comment="事件视频路径") + description = Column(Text, comment="事件描述") + location = Column(String(200), comment="事件发生位置") + detected_objects = Column(Text, comment="检测到的对象,JSON格式") + processing_time = Column(Float, comment="处理时间(ms)") + is_alert = Column(Boolean, default=False, comment="是否触发告警") + alert_sent = Column(Boolean, default=False, comment="是否已发送告警") + operator_id = Column(Integer, comment="处理人员ID") + resolution_notes = Column(Text, comment="处理备注") + resolved_at = Column(DateTime, comment="解决时间") \ No newline at end of file diff --git a/server/readme.md b/server/readme.md new file mode 100644 index 0000000..c47c57b --- /dev/null +++ b/server/readme.md @@ -0,0 +1,177 @@ +# 边检CV算法接口服务 + +### 技术栈 +- **后端框架**: FastAPI +- **数据库**: SQLite (SQLAlchemy ORM) +- **AI模型**: YOLOv11n +- **进程管理**: Supervisor +- **开发语言**: Python 3.8+ + +### 项目结构 + +``` +server/ +├── app.py # FastAPI主应用 +├── start.py # 启动脚本 +├── requirements.txt # Python依赖 +├── env.example # 环境变量示例 +├── init_data.py # 示例数据初始化 +├── core/ # 核心配置 +│ └── database.py # 数据库配置 +├── models/ # 数据模型 +│ ├── __init__.py +│ ├── base.py # 基础模型 +│ ├── algorithm.py # 算法模型 +│ ├── device.py # 设备模型 +│ └── event.py # 事件模型 +├── schemas/ # Pydantic模型 +│ ├── __init__.py +│ ├── algorithm.py # 算法相关模型 +│ ├── device.py # 设备相关模型 +│ └── event.py # 事件相关模型 +└── routers/ # API路由 + ├── __init__.py + ├── algorithms.py # 算法管理接口 + ├── devices.py # 设备管理接口 + └── events.py # 事件管理接口 +``` + +### 功能模块 + +#### 1. 算法管理模块 +- 算法的增删改查 +- 算法状态管理(启用/禁用) +- 算法版本管理 +- 算法性能指标(准确率、推理时间等) + +#### 2. 设备管理模块 +- 设备信息管理(摄像头、传感器等) +- 设备状态监控 +- 设备类型管理 +- 设备地理位置信息 + +#### 3. 事件管理模块 +- 事件记录和查询 +- 事件状态管理 +- 事件统计分析 +- 告警管理 + +### API接口 + +#### 算法管理接口 +- `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` - 启用/禁用算法 + +#### 设备管理接口 +- `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` - 获取设备状态统计 + +#### 事件管理接口 +- `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. 环境准备 +```bash +# 创建虚拟环境 +conda create -n border_inspection python=3.8 +conda activate border_inspection + +# 安装依赖 +pip install -r requirements.txt +``` + +#### 2. 初始化数据库 +```bash +# 启动服务(会自动创建数据库表) +python start.py +``` + +#### 3. 初始化示例数据 +```bash +python init_data.py +``` + +#### 4. 启动服务 +```bash +python start.py +``` + +服务将在 `http://localhost:8000` 启动 + +### API文档 + +启动服务后,可以访问以下地址查看API文档: +- Swagger UI: `http://localhost:8000/docs` +- ReDoc: `http://localhost:8000/redoc` + +### 数据库设计 + +#### 算法表 (algorithms) +- 基本信息:名称、描述、版本 +- 模型信息:模型路径、配置文件路径 +- 性能指标:准确率、推理时间、输入尺寸 +- 状态管理:启用状态、算法状态 +- 分类信息:检测类别、标签 + +#### 设备表 (devices) +- 基本信息:名称、类型、位置 +- 连接信息:IP地址、端口、用户名、密码 +- 视频流:RTSP地址、分辨率、帧率 +- 状态信息:在线状态、最后心跳时间 +- 地理位置:经纬度坐标 + +#### 事件表 (events) +- 事件信息:类型、设备ID、算法ID +- 检测结果:置信度、边界框、检测对象 +- 状态管理:事件状态、严重程度 +- 告警信息:是否告警、告警发送状态 +- 处理信息:处理人员、处理备注、解决时间 + +### 开发说明 + +1. **添加新模型**: 在 `models/` 目录下创建新的模型文件 +2. **添加新接口**: 在 `routers/` 目录下创建新的路由文件 +3. **添加新Schema**: 在 `schemas/` 目录下创建新的Pydantic模型 +4. **数据库迁移**: 修改模型后重启服务,数据库表会自动更新 + +### 部署说明 + +#### 使用Supervisor管理进程 +```ini +[program:border_inspection] +command=python /path/to/server/start.py +directory=/path/to/server +user=www-data +autostart=true +autorestart=true +stderr_logfile=/var/log/border_inspection.err.log +stdout_logfile=/var/log/border_inspection.out.log +``` + +#### 环境变量配置 +复制 `env.example` 为 `.env` 并修改相应配置: +```bash +cp env.example .env +# 编辑 .env 文件 +``` \ No newline at end of file diff --git a/server/requirements.txt b/server/requirements.txt new file mode 100644 index 0000000..5a7a9ec --- /dev/null +++ b/server/requirements.txt @@ -0,0 +1,12 @@ +fastapi==0.104.1 +uvicorn==0.24.0 +sqlalchemy==2.0.23 +pydantic==2.5.0 +python-multipart==0.0.6 +python-jose[cryptography]==3.3.0 +passlib[bcrypt]==1.7.4 +python-dotenv==1.0.0 +aiofiles==23.2.1 +pillow==10.1.0 +opencv-python==4.8.1.78 +ultralytics==8.0.196 \ No newline at end of file diff --git a/server/routers/__init__.py b/server/routers/__init__.py new file mode 100644 index 0000000..590f101 --- /dev/null +++ b/server/routers/__init__.py @@ -0,0 +1 @@ +# 路由包 \ No newline at end of file diff --git a/server/routers/algorithms.py b/server/routers/algorithms.py new file mode 100644 index 0000000..fe3a890 --- /dev/null +++ b/server/routers/algorithms.py @@ -0,0 +1,130 @@ +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session +from typing import List, Optional +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="创建算法") +async def create_algorithm( + algorithm: AlgorithmCreate, + db: Session = Depends(get_db) +): + """创建新的算法""" + db_algorithm = Algorithm(**algorithm.dict()) + db.add(db_algorithm) + db.commit() + db.refresh(db_algorithm) + return db_algorithm + +@router.get("/", response_model=AlgorithmListResponse, summary="获取算法列表") +async def get_algorithms( + skip: int = Query(0, ge=0, description="跳过记录数"), + limit: int = Query(10, ge=1, le=100, description="返回记录数"), + name: Optional[str] = Query(None, description="算法名称"), + status: Optional[str] = Query(None, description="算法状态"), + is_enabled: Optional[bool] = Query(None, description="是否启用"), + db: Session = Depends(get_db) +): + """获取算法列表,支持分页和筛选""" + query = db.query(Algorithm) + + if name: + query = query.filter(Algorithm.name.contains(name)) + if status: + query = query.filter(Algorithm.status == status) + if is_enabled is not None: + query = query.filter(Algorithm.is_enabled == is_enabled) + + total = query.count() + algorithms = query.offset(skip).limit(limit).all() + + return AlgorithmListResponse( + algorithms=algorithms, + total=total, + page=skip // limit + 1, + size=limit + ) + +@router.get("/{algorithm_id}", response_model=AlgorithmResponse, summary="获取算法详情") +async def get_algorithm( + algorithm_id: int, + db: Session = Depends(get_db) +): + """根据ID获取算法详情""" + 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="更新算法") +async def update_algorithm( + algorithm_id: int, + algorithm: AlgorithmUpdate, + db: Session = Depends(get_db) +): + """更新算法信息""" + db_algorithm = db.query(Algorithm).filter(Algorithm.id == algorithm_id).first() + if not db_algorithm: + raise HTTPException(status_code=404, detail="算法不存在") + + update_data = algorithm.dict(exclude_unset=True) + for field, value in update_data.items(): + setattr(db_algorithm, field, value) + + db.commit() + db.refresh(db_algorithm) + return db_algorithm + +@router.delete("/{algorithm_id}", summary="删除算法") +async def delete_algorithm( + algorithm_id: int, + db: Session = Depends(get_db) +): + """删除算法""" + algorithm = db.query(Algorithm).filter(Algorithm.id == algorithm_id).first() + if not algorithm: + raise HTTPException(status_code=404, detail="算法不存在") + + db.delete(algorithm) + db.commit() + return {"message": "算法删除成功"} + +@router.patch("/{algorithm_id}/status", response_model=AlgorithmResponse, summary="更新算法状态") +async def update_algorithm_status( + algorithm_id: int, + status: str = Query(..., description="新状态"), + db: Session = Depends(get_db) +): + """更新算法状态""" + algorithm = db.query(Algorithm).filter(Algorithm.id == algorithm_id).first() + if not algorithm: + raise HTTPException(status_code=404, detail="算法不存在") + + algorithm.status = status + db.commit() + db.refresh(algorithm) + return algorithm + +@router.patch("/{algorithm_id}/enable", response_model=AlgorithmResponse, summary="启用/禁用算法") +async def toggle_algorithm_enabled( + algorithm_id: int, + enabled: bool = Query(..., description="是否启用"), + db: Session = Depends(get_db) +): + """启用或禁用算法""" + algorithm = db.query(Algorithm).filter(Algorithm.id == algorithm_id).first() + if not algorithm: + raise HTTPException(status_code=404, detail="算法不存在") + + algorithm.is_enabled = enabled + db.commit() + db.refresh(algorithm) + return algorithm \ No newline at end of file diff --git a/server/routers/devices.py b/server/routers/devices.py new file mode 100644 index 0000000..283a6fe --- /dev/null +++ b/server/routers/devices.py @@ -0,0 +1,165 @@ +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session +from typing import List, Optional +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="创建设备") +async def create_device( + device: DeviceCreate, + db: Session = Depends(get_db) +): + """创建新的设备""" + db_device = Device(**device.dict()) + db.add(db_device) + db.commit() + db.refresh(db_device) + return db_device + +@router.get("/", response_model=DeviceListResponse, summary="获取设备列表") +async def get_devices( + skip: int = Query(0, ge=0, description="跳过记录数"), + limit: int = Query(10, ge=1, le=100, description="返回记录数"), + name: Optional[str] = Query(None, description="设备名称"), + device_type: Optional[str] = Query(None, description="设备类型"), + status: Optional[str] = Query(None, description="设备状态"), + location: Optional[str] = Query(None, description="设备位置"), + is_enabled: Optional[bool] = Query(None, description="是否启用"), + db: Session = Depends(get_db) +): + """获取设备列表,支持分页和筛选""" + query = db.query(Device) + + if name: + query = query.filter(Device.name.contains(name)) + if device_type: + query = query.filter(Device.device_type == device_type) + if status: + query = query.filter(Device.status == status) + if location: + query = query.filter(Device.location.contains(location)) + if is_enabled is not None: + query = query.filter(Device.is_enabled == is_enabled) + + total = query.count() + devices = query.offset(skip).limit(limit).all() + + return DeviceListResponse( + devices=devices, + total=total, + page=skip // limit + 1, + size=limit + ) + +@router.get("/{device_id}", response_model=DeviceResponse, summary="获取设备详情") +async def get_device( + device_id: int, + db: Session = Depends(get_db) +): + """根据ID获取设备详情""" + 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="更新设备") +async def update_device( + device_id: int, + device: DeviceUpdate, + db: Session = Depends(get_db) +): + """更新设备信息""" + db_device = db.query(Device).filter(Device.id == device_id).first() + if not db_device: + raise HTTPException(status_code=404, detail="设备不存在") + + update_data = device.dict(exclude_unset=True) + for field, value in update_data.items(): + setattr(db_device, field, value) + + db.commit() + db.refresh(db_device) + return db_device + +@router.delete("/{device_id}", summary="删除设备") +async def delete_device( + device_id: int, + db: Session = Depends(get_db) +): + """删除设备""" + device = db.query(Device).filter(Device.id == device_id).first() + if not device: + raise HTTPException(status_code=404, detail="设备不存在") + + db.delete(device) + db.commit() + return {"message": "设备删除成功"} + +@router.patch("/{device_id}/status", response_model=DeviceResponse, summary="更新设备状态") +async def update_device_status( + device_id: int, + status: str = Query(..., description="新状态"), + db: Session = Depends(get_db) +): + """更新设备状态""" + device = db.query(Device).filter(Device.id == device_id).first() + if not device: + raise HTTPException(status_code=404, detail="设备不存在") + + device.status = status + db.commit() + db.refresh(device) + return device + +@router.patch("/{device_id}/enable", response_model=DeviceResponse, summary="启用/禁用设备") +async def toggle_device_enabled( + device_id: int, + enabled: bool = Query(..., description="是否启用"), + db: Session = Depends(get_db) +): + """启用或禁用设备""" + device = db.query(Device).filter(Device.id == device_id).first() + if not device: + raise HTTPException(status_code=404, detail="设备不存在") + + device.is_enabled = enabled + db.commit() + db.refresh(device) + return device + +@router.get("/types/list", summary="获取设备类型列表") +async def get_device_types(): + """获取所有设备类型""" + return { + "types": [ + {"value": "camera", "label": "摄像头"}, + {"value": "sensor", "label": "传感器"}, + {"value": "gate", "label": "门禁"}, + {"value": "alarm", "label": "报警器"}, + {"value": "other", "label": "其他"} + ] + } + +@router.get("/status/stats", summary="获取设备状态统计") +async def get_device_status_stats(db: Session = Depends(get_db)): + """获取设备状态统计信息""" + total = db.query(Device).count() + online = db.query(Device).filter(Device.status == "online").count() + offline = db.query(Device).filter(Device.status == "offline").count() + error = db.query(Device).filter(Device.status == "error").count() + + return { + "total": total, + "online": online, + "offline": offline, + "error": error, + "online_rate": round(online / total * 100, 2) if total > 0 else 0 + } \ No newline at end of file diff --git a/server/routers/events.py b/server/routers/events.py new file mode 100644 index 0000000..341f553 --- /dev/null +++ b/server/routers/events.py @@ -0,0 +1,213 @@ +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session +from typing import List, Optional +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="创建事件") +async def create_event( + event: EventCreate, + db: Session = Depends(get_db) +): + """创建新的事件""" + db_event = Event(**event.dict()) + db.add(db_event) + db.commit() + db.refresh(db_event) + return db_event + +@router.get("/", response_model=EventListResponse, summary="获取事件列表") +async def get_events( + skip: int = Query(0, ge=0, description="跳过记录数"), + limit: int = Query(10, ge=1, le=100, description="返回记录数"), + event_type: Optional[str] = Query(None, description="事件类型"), + device_id: Optional[int] = Query(None, description="设备ID"), + algorithm_id: Optional[int] = Query(None, description="算法ID"), + severity: Optional[str] = Query(None, description="严重程度"), + status: Optional[str] = Query(None, description="事件状态"), + is_alert: Optional[bool] = Query(None, description="是否告警"), + start_time: Optional[str] = Query(None, description="开始时间"), + end_time: Optional[str] = Query(None, description="结束时间"), + db: Session = Depends(get_db) +): + """获取事件列表,支持分页和筛选""" + query = db.query(Event) + + if event_type: + query = query.filter(Event.event_type == event_type) + if device_id: + query = query.filter(Event.device_id == device_id) + if algorithm_id: + query = query.filter(Event.algorithm_id == algorithm_id) + if severity: + query = query.filter(Event.severity == severity) + if status: + query = query.filter(Event.status == status) + if is_alert is not None: + query = query.filter(Event.is_alert == is_alert) + 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() + events = query.offset(skip).limit(limit).all() + + return EventListResponse( + events=events, + total=total, + page=skip // limit + 1, + size=limit + ) + +@router.get("/{event_id}", response_model=EventResponse, summary="获取事件详情") +async def get_event( + event_id: int, + db: Session = Depends(get_db) +): + """根据ID获取事件详情""" + 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="更新事件") +async def update_event( + event_id: int, + event: EventUpdate, + db: Session = Depends(get_db) +): + """更新事件信息""" + db_event = db.query(Event).filter(Event.id == event_id).first() + if not db_event: + raise HTTPException(status_code=404, detail="事件不存在") + + update_data = event.dict(exclude_unset=True) + for field, value in update_data.items(): + setattr(db_event, field, value) + + db.commit() + db.refresh(db_event) + return db_event + +@router.delete("/{event_id}", summary="删除事件") +async def delete_event( + event_id: int, + db: Session = Depends(get_db) +): + """删除事件""" + event = db.query(Event).filter(Event.id == event_id).first() + if not event: + raise HTTPException(status_code=404, detail="事件不存在") + + db.delete(event) + db.commit() + return {"message": "事件删除成功"} + +@router.patch("/{event_id}/status", response_model=EventResponse, summary="更新事件状态") +async def update_event_status( + event_id: int, + status: str = Query(..., description="新状态"), + resolution_notes: Optional[str] = Query(None, description="处理备注"), + db: Session = Depends(get_db) +): + """更新事件状态""" + event = db.query(Event).filter(Event.id == event_id).first() + if not event: + raise HTTPException(status_code=404, detail="事件不存在") + + event.status = status + if resolution_notes: + event.resolution_notes = resolution_notes + + # 如果状态为resolved,设置解决时间 + if status == "resolved": + event.resolved_at = datetime.utcnow() + + db.commit() + db.refresh(event) + return event + +@router.get("/types/list", summary="获取事件类型列表") +async def get_event_types(): + """获取所有事件类型""" + return { + "types": [ + {"value": "person_detection", "label": "人员检测"}, + {"value": "vehicle_detection", "label": "车辆检测"}, + {"value": "intrusion", "label": "入侵检测"}, + {"value": "face_recognition", "label": "人脸识别"}, + {"value": "license_plate", "label": "车牌识别"}, + {"value": "object_detection", "label": "物体检测"}, + {"value": "behavior_analysis", "label": "行为分析"}, + {"value": "other", "label": "其他"} + ] + } + +@router.get("/stats/summary", summary="获取事件统计摘要") +async def get_event_stats_summary(db: Session = Depends(get_db)): + """获取事件统计摘要""" + total = db.query(Event).count() + pending = db.query(Event).filter(Event.status == "pending").count() + processing = db.query(Event).filter(Event.status == "processing").count() + resolved = db.query(Event).filter(Event.status == "resolved").count() + ignored = db.query(Event).filter(Event.status == "ignored").count() + alerts = db.query(Event).filter(Event.is_alert == True).count() + + # 按严重程度统计 + critical = db.query(Event).filter(Event.severity == "critical").count() + high = db.query(Event).filter(Event.severity == "high").count() + medium = db.query(Event).filter(Event.severity == "medium").count() + low = db.query(Event).filter(Event.severity == "low").count() + + return { + "total": total, + "pending": pending, + "processing": processing, + "resolved": resolved, + "ignored": ignored, + "alerts": alerts, + "severity": { + "critical": critical, + "high": high, + "medium": medium, + "low": low + } + } + +@router.get("/stats/by-type", summary="按类型统计事件") +async def get_event_stats_by_type(db: Session = Depends(get_db)): + """按事件类型统计""" + from sqlalchemy import func + + stats = db.query( + Event.event_type, + func.count(Event.id).label('count') + ).group_by(Event.event_type).all() + + return { + "stats": [ + {"type": stat.event_type, "count": stat.count} + for stat in stats + ] + } \ No newline at end of file diff --git a/server/schemas/__init__.py b/server/schemas/__init__.py new file mode 100644 index 0000000..4a0c1d5 --- /dev/null +++ b/server/schemas/__init__.py @@ -0,0 +1,5 @@ +from .algorithm import * +from .device import * +from .event import * + +__all__ = ["algorithm", "device", "event"] \ No newline at end of file diff --git a/server/schemas/algorithm.py b/server/schemas/algorithm.py new file mode 100644 index 0000000..bab7716 --- /dev/null +++ b/server/schemas/algorithm.py @@ -0,0 +1,50 @@ +from pydantic import BaseModel, Field +from typing import Optional, List, Dict, Any +from datetime import datetime + +class AlgorithmBase(BaseModel): + name: str = Field(..., description="算法名称") + description: Optional[str] = Field(None, description="算法描述") + version: str = Field(..., description="算法版本") + model_path: Optional[str] = Field(None, description="模型文件路径") + config_path: Optional[str] = Field(None, description="配置文件路径") + status: str = Field("inactive", description="算法状态") + accuracy: Optional[float] = Field(None, description="算法准确率") + detection_classes: Optional[str] = Field(None, description="检测类别") + input_size: Optional[str] = Field(None, description="输入尺寸") + inference_time: Optional[float] = Field(None, description="推理时间") + is_enabled: bool = Field(True, description="是否启用") + creator: Optional[str] = Field(None, description="创建者") + tags: Optional[str] = Field(None, description="标签") + +class AlgorithmCreate(AlgorithmBase): + pass + +class AlgorithmUpdate(BaseModel): + name: Optional[str] = None + description: Optional[str] = None + version: Optional[str] = None + model_path: Optional[str] = None + config_path: Optional[str] = None + status: Optional[str] = None + accuracy: Optional[float] = None + detection_classes: Optional[str] = None + input_size: Optional[str] = None + inference_time: Optional[float] = None + is_enabled: Optional[bool] = None + creator: Optional[str] = None + tags: Optional[str] = None + +class AlgorithmResponse(AlgorithmBase): + id: int + created_at: datetime + updated_at: datetime + + class Config: + from_attributes = True + +class AlgorithmListResponse(BaseModel): + algorithms: List[AlgorithmResponse] + total: int + page: int + size: int \ No newline at end of file diff --git a/server/schemas/device.py b/server/schemas/device.py new file mode 100644 index 0000000..3ed1bbe --- /dev/null +++ b/server/schemas/device.py @@ -0,0 +1,63 @@ +from pydantic import BaseModel, Field +from typing import Optional, List, Dict, Any +from datetime import datetime + +class DeviceBase(BaseModel): + name: str = Field(..., description="设备名称") + device_type: str = Field(..., description="设备类型") + location: Optional[str] = Field(None, description="设备位置") + ip_address: Optional[str] = Field(None, description="IP地址") + port: Optional[int] = Field(None, description="端口号") + username: Optional[str] = Field(None, description="用户名") + password: Optional[str] = Field(None, description="密码") + rtsp_url: Optional[str] = Field(None, description="RTSP流地址") + status: str = Field("offline", description="设备状态") + resolution: Optional[str] = Field(None, description="分辨率") + fps: Optional[int] = Field(None, description="帧率") + algorithm_id: Optional[int] = Field(None, description="关联的算法ID") + is_enabled: bool = Field(True, description="是否启用") + latitude: Optional[float] = Field(None, description="纬度") + longitude: Optional[float] = Field(None, description="经度") + description: Optional[str] = Field(None, description="设备描述") + manufacturer: Optional[str] = Field(None, description="制造商") + model: Optional[str] = Field(None, description="设备型号") + serial_number: Optional[str] = Field(None, description="序列号") + +class DeviceCreate(DeviceBase): + pass + +class DeviceUpdate(BaseModel): + name: Optional[str] = None + device_type: Optional[str] = None + location: Optional[str] = None + ip_address: Optional[str] = None + port: Optional[int] = None + username: Optional[str] = None + password: Optional[str] = None + rtsp_url: Optional[str] = None + status: Optional[str] = None + resolution: Optional[str] = None + fps: Optional[int] = None + algorithm_id: Optional[int] = None + is_enabled: Optional[bool] = None + latitude: Optional[float] = None + longitude: Optional[float] = None + description: Optional[str] = None + manufacturer: Optional[str] = None + model: Optional[str] = None + serial_number: Optional[str] = None + +class DeviceResponse(DeviceBase): + id: int + created_at: datetime + updated_at: datetime + last_heartbeat: Optional[datetime] = None + + class Config: + from_attributes = True + +class DeviceListResponse(BaseModel): + devices: List[DeviceResponse] + total: int + page: int + size: int \ No newline at end of file diff --git a/server/schemas/event.py b/server/schemas/event.py new file mode 100644 index 0000000..1bb6fca --- /dev/null +++ b/server/schemas/event.py @@ -0,0 +1,59 @@ +from pydantic import BaseModel, Field +from typing import Optional, List, Dict, Any +from datetime import datetime + +class EventBase(BaseModel): + event_type: str = Field(..., description="事件类型") + device_id: int = Field(..., description="关联设备ID") + algorithm_id: Optional[int] = Field(None, description="关联算法ID") + severity: str = Field("medium", description="严重程度") + status: str = Field("pending", description="事件状态") + confidence: Optional[float] = Field(None, description="置信度") + bbox: Optional[str] = Field(None, description="边界框坐标") + image_path: Optional[str] = Field(None, description="事件图片路径") + video_path: Optional[str] = Field(None, description="事件视频路径") + description: Optional[str] = Field(None, description="事件描述") + location: Optional[str] = Field(None, description="事件发生位置") + detected_objects: Optional[str] = Field(None, description="检测到的对象") + processing_time: Optional[float] = Field(None, description="处理时间") + is_alert: bool = Field(False, description="是否触发告警") + alert_sent: bool = Field(False, description="是否已发送告警") + operator_id: Optional[int] = Field(None, description="处理人员ID") + resolution_notes: Optional[str] = Field(None, description="处理备注") + +class EventCreate(EventBase): + pass + +class EventUpdate(BaseModel): + event_type: Optional[str] = None + device_id: Optional[int] = None + algorithm_id: Optional[int] = None + severity: Optional[str] = None + status: Optional[str] = None + confidence: Optional[float] = None + bbox: Optional[str] = None + image_path: Optional[str] = None + video_path: Optional[str] = None + description: Optional[str] = None + location: Optional[str] = None + detected_objects: Optional[str] = None + processing_time: Optional[float] = None + is_alert: Optional[bool] = None + alert_sent: Optional[bool] = None + operator_id: Optional[int] = None + resolution_notes: Optional[str] = None + +class EventResponse(EventBase): + id: int + created_at: datetime + updated_at: datetime + resolved_at: Optional[datetime] = None + + class Config: + from_attributes = True + +class EventListResponse(BaseModel): + events: List[EventResponse] + total: int + page: int + size: int \ No newline at end of file