from __future__ import annotations from datetime import datetime from fastapi import APIRouter, Depends, Query, Request, Response from fastapi.responses import FileResponse, StreamingResponse from sqlalchemy.orm import Session from app.db.session import get_db from app.schemas.common import Page from app.schemas.video import ( VideoAnalysisEventRead, VideoChannelCreate, VideoChannelRead, VideoChannelUpdate, VideoEventUpdate, VideoModuleConfig, VideoModuleSummaryRead, VideoPlatformOverview, VideoRecordRead, VideoSnapshotRead, ) from app.services import video_platform_service as video_service router = APIRouter(prefix="/video-platform", tags=["工业视频平台"]) @router.get("/overview", response_model=VideoPlatformOverview, summary="视频平台总览") def overview( mine_id: str = Query(...), db: Session = Depends(get_db), ): return video_service.platform_overview(db, mine_id) @router.get("/modules", response_model=list[VideoModuleConfig], summary="模块页签与检索项配置") def modules( mine_id: str = Query(...), db: Session = Depends(get_db), ): return video_service.module_configs(db, mine_id) @router.get( "/modules/{module}/summary", response_model=VideoModuleSummaryRead, summary="模块状态总览与统计卡片", ) def module_summary( module: str, mine_id: str = Query(...), db: Session = Depends(get_db), ): return video_service.module_summary(db, mine_id, module) # --------------------------------------------------------------------------- # 视频通道(通道管理、设备树、分屏预览) # --------------------------------------------------------------------------- @router.get("/channels", response_model=list[VideoChannelRead], summary="视频通道列表") def list_channels( mine_id: str = Query(...), module: str | None = Query(None, description="door/local_fan/main_fan"), status: str | None = Query(None, description="online/offline/fault"), keyword: str | None = Query(None, description="通道名称、安装地点或编码"), limit: int = Query(200, ge=1, le=500), db: Session = Depends(get_db), ): return video_service.list_channels( db, mine_id, module=module, status=status, keyword=keyword, limit=limit ) @router.post( "/channels", response_model=VideoChannelRead, status_code=201, summary="新增视频通道", ) def create_channel(payload: VideoChannelCreate, db: Session = Depends(get_db)): return video_service.create_channel(db, payload) @router.get( "/channels/{channel_id}", response_model=VideoChannelRead, summary="视频通道详情", ) def channel_detail(channel_id: str, db: Session = Depends(get_db)): return video_service.get_channel(db, channel_id) @router.patch( "/channels/{channel_id}", response_model=VideoChannelRead, summary="更新视频通道", ) def update_channel( channel_id: str, payload: VideoChannelUpdate, db: Session = Depends(get_db), ): return video_service.update_channel(db, channel_id, payload) @router.delete( "/channels/{channel_id}", status_code=204, summary="删除视频通道", ) def delete_channel(channel_id: str, db: Session = Depends(get_db)) -> Response: video_service.delete_channel(db, channel_id) return Response(status_code=204) @router.get( "/channels/{channel_id}/stream", summary="通道视频流(读取数据库中的磁盘路径,支持 Range)", ) def channel_stream( channel_id: str, request: Request, db: Session = Depends(get_db), ): path, content_type = video_service.resolve_channel_media(channel_id, db) file_size = path.stat().st_size range_header = request.headers.get("range") start, end = video_service.parse_range_header(range_header, file_size) headers = { "Accept-Ranges": "bytes", "Content-Length": str(max(end - start + 1, 0)), "Cache-Control": "no-store", } if range_header: headers["Content-Range"] = f"bytes {start}-{end}/{file_size}" return StreamingResponse( video_service.iter_file_range(path, start, end), status_code=206, media_type=content_type, headers=headers, ) return StreamingResponse( video_service.iter_file_range(path, start, end), status_code=200, media_type=content_type, headers=headers, ) @router.get( "/channels/{channel_id}/snapshot", summary="通道最近识别抓拍图(抓图按钮)", ) def channel_snapshot(channel_id: str, db: Session = Depends(get_db)): path, content_type = video_service.resolve_channel_latest_snapshot(channel_id, db) return FileResponse(path, media_type=content_type, filename=path.name) # --------------------------------------------------------------------------- # 识别抓拍 # --------------------------------------------------------------------------- @router.get( "/snapshots", response_model=Page[VideoSnapshotRead], summary="识别抓拍图像列表(时间、类型、设备检索)", ) def list_snapshots( mine_id: str = Query(...), module: str | None = Query(None), snapshot_type: str | None = Query(None, description="vehicle_passage/person_passage 等"), event_type: str | None = Query(None, description="告警类型或操作类型名称"), channel_id: str | None = Query(None), start: datetime | None = Query(None, description="抓拍时间起"), end: datetime | None = Query(None, description="抓拍时间止"), keyword: str | None = Query(None, description="标题、车牌、人员或类型关键字"), page: int = Query(1, ge=1), page_size: int = Query(24, ge=1, le=200), db: Session = Depends(get_db), ): items, total = video_service.list_snapshots( db, mine_id, module=module, snapshot_type=snapshot_type, event_type=event_type, channel_id=channel_id, start=start, end=end, keyword=keyword, page=page, page_size=page_size, ) return Page(items=items, total=total, page=page, page_size=page_size) @router.get( "/snapshots/{snapshot_id}", response_model=VideoSnapshotRead, summary="识别抓拍详情", ) def snapshot_detail(snapshot_id: str, db: Session = Depends(get_db)): return video_service.get_snapshot(db, snapshot_id) @router.get("/snapshots/{snapshot_id}/image", summary="识别抓拍原图") def snapshot_image(snapshot_id: str, db: Session = Depends(get_db)): path, content_type = video_service.resolve_snapshot_media(snapshot_id, db) return FileResponse(path, media_type=content_type, filename=path.name) @router.get("/snapshots/{snapshot_id}/thumbnail", summary="识别抓拍缩略图") def snapshot_thumbnail(snapshot_id: str, db: Session = Depends(get_db)): path, content_type = video_service.resolve_snapshot_thumbnail(snapshot_id, db) return FileResponse(path, media_type=content_type, filename=path.name) # --------------------------------------------------------------------------- # 视频分析事件 # --------------------------------------------------------------------------- @router.get( "/events", response_model=list[VideoAnalysisEventRead], summary="视频分析事件与实时告警", ) def list_events( mine_id: str = Query(...), module: str | None = Query(None), level: str | None = Query(None, description="danger/warning/info"), status: str | None = Query(None, description="pending/processing/handled/ignored"), event_type: str | None = Query(None), start: datetime | None = Query(None), end: datetime | None = Query(None), limit: int = Query(100, ge=1, le=1000), db: Session = Depends(get_db), ): return video_service.list_events( db, mine_id, module=module, level=level, status=status, event_type=event_type, start=start, end=end, limit=limit, ) @router.patch( "/events/{event_id}", response_model=VideoAnalysisEventRead, summary="更新事件处理状态", ) def update_event( event_id: str, payload: VideoEventUpdate, db: Session = Depends(get_db), ): return video_service.update_event(db, event_id, payload) # --------------------------------------------------------------------------- # 通行与操作记录 # --------------------------------------------------------------------------- @router.get( "/records", response_model=Page[VideoRecordRead], summary="通行与操作记录列表", ) def list_records( mine_id: str = Query(...), module: str | None = Query(None), record_type: str | None = Query( None, description=( "vehicle_passage/person_passage/door_unclosed/person_track/" "area_stay/person_warn/person_detect/device_operation/device_warn" ), ), channel_id: str | None = Query(None), start: datetime | None = Query(None), end: datetime | None = Query(None), keyword: str | None = Query(None), page: int = Query(1, ge=1), page_size: int = Query(50, ge=1, le=200), db: Session = Depends(get_db), ): items, total = video_service.list_records( db, mine_id, module=module, record_type=record_type, channel_id=channel_id, start=start, end=end, keyword=keyword, page=page, page_size=page_size, ) return Page(items=items, total=total, page=page, page_size=page_size)