feat(scada): add project monitoring replay
Generic Container CI/CD / test-build-publish (push) Successful in 3m39s
Server CI/CD v2 / build-test-publish-and-deploy (push) Successful in 3m39s

This commit is contained in:
2026-11-04 14:27:21 +08:00
parent f25423dec4
commit 26fa5d1909
53 changed files with 5153 additions and 20 deletions
+44 -1
View File
@@ -1,10 +1,13 @@
from fastapi import APIRouter, Depends, HTTPException, Query, Path, Body
from typing import List
from datetime import datetime
from uuid import UUID
from psycopg import AsyncConnection
from pydantic import BaseModel, Field, field_validator
from app.domain.schemas.scada import ScadaLatestReadingsResponse
from app.domain.schemas.scada import ScadaLatestReadingsResponse, ScadaRawPage
from app.auth.project_dependencies import get_project_context, ProjectContext
from app.services.scada_raw import read_raw_page, validate_window
from app.infra.db.postgresql.scada import ScadaInfoRepository
from app.infra.db.timescaledb.repositories.scada import ScadaRepository
from .dependencies import get_postgres_connection, get_timescale_connection
@@ -13,6 +16,46 @@ router = APIRouter()
SCADA_BATCH_MAX_ITEMS = 10_000
@router.get("/timeseries/scada-readings/raw", response_model=ScadaRawPage,
summary="按时间顺序分页读取 SCADA 原始值")
async def get_scada_raw(
start_time: datetime, end_time: datetime, device_ids: str,
limit: int = Query(500, ge=1, le=500),
cursor: str | None = Query(None, max_length=2048),
replay_id: UUID | None = None,
conn: AsyncConnection = Depends(get_timescale_connection),
postgres_conn: AsyncConnection = Depends(get_postgres_connection),
ctx: ProjectContext = Depends(get_project_context),
) -> ScadaRawPage:
try:
validate_window(start_time, end_time)
except ValueError as exc:
raise HTTPException(422, str(exc)) from exc
ids = list(dict.fromkeys(item.strip() for item in device_ids.split(",") if item.strip()))
if not ids or len(ids) > 500:
raise HTTPException(422, "请选择 1 至 500 个 SCADA 设备")
existing = await ScadaInfoRepository.get_existing_device_ids(postgres_conn, ids)
if set(ids) != existing:
raise HTTPException(422, "查询包含当前项目未配置的 SCADA 设备")
try:
through = None
if replay_id is not None:
from app.infra.db.postgresql.scada_replays import ScadaReplayRepository
from app.services.scada_replay import ScadaReplayService
repo = ScadaReplayRepository(postgres_conn, ctx.project_id)
await ScadaReplayService(repo, ctx).ensure_storage()
replay = await repo.get(replay_id)
if not set(ids) <= set(replay["config"]["device_ids"]):
raise HTTPException(403, "设备超出回放范围")
if start_time < datetime.fromisoformat(replay["start_time"]) or end_time > datetime.fromisoformat(replay["end_time"]):
raise HTTPException(422, "查询超出选定回放区间")
through = datetime.fromisoformat(replay["virtual_now"])
return await read_raw_page(conn, project_id=str(ctx.project_id), device_ids=ids,
start=start_time, end=end_time, limit=limit, cursor=cursor, through=through)
except ValueError as exc:
raise HTTPException(422, str(exc)) from exc
class ScadaReadingBatchItem(BaseModel):
time: datetime
device_id: str = Field(min_length=1)