refactor(db)!: clean up business SQL access

- make realtime replacement and analysis result writes transactional\n- consolidate SCADA repositories and remove process-global project state\n- validate SCADA batches and use indexed GIS-backed business queries\n\nBREAKING CHANGE: remove the public analysis result writer and the pipeline-health network_name query parameter.
This commit is contained in:
2026-08-28 11:37:36 +08:00
parent b74799a39d
commit 9b095c7439
34 changed files with 859 additions and 921 deletions
+1 -22
View File
@@ -1,7 +1,7 @@
from datetime import datetime
from uuid import UUID
from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query
from fastapi import APIRouter, Depends, HTTPException, Query
from psycopg import AsyncConnection
from app.infra.db.timescaledb.repositories.analysis import AnalysisResultsRepository
@@ -11,27 +11,6 @@ from .dependencies import get_timescale_connection
router = APIRouter()
@router.post("/timeseries/analysis/runs/{run_id}/results", status_code=201)
async def store_analysis_results(
run_id: UUID = Path(..., description="分析运行 ID"),
payload: dict = Body(...),
conn: AsyncConnection = Depends(get_timescale_connection),
):
try:
node_rows = payload.get("node_results", [])
link_rows = payload.get("link_results", [])
await AnalysisResultsRepository.store_results(
conn, run_id, node_rows, link_rows
)
return {
"run_id": run_id,
"node_count": len(node_rows),
"link_count": len(link_rows),
}
except ValueError as exc:
raise HTTPException(status_code=409, detail=str(exc)) from exc
@router.get("/timeseries/analysis/runs/{run_id}/nodes/{node_id}")
async def get_analysis_node_series(
run_id: UUID,
+2 -3
View File
@@ -226,8 +226,8 @@ async def clean_scada_data(
@router.get("/pipeline-health-predictions", summary="预测管道健康状况")
async def predict_pipeline_health(
query_time: datetime = Query(..., description="查询时间"),
network_name: str = Query(..., description="管网名称(或数据库名称)"),
timescale_conn: AsyncConnection = Depends(get_timescale_connection),
postgres_conn: AsyncConnection = Depends(get_postgres_connection),
):
"""
预测管道健康状况
@@ -237,7 +237,6 @@ async def predict_pipeline_health(
Args:
query_time: 查询时间
network_name: 管网名称(或数据库名称)
timescale_conn: TimescaleDB连接
Returns:
@@ -248,7 +247,7 @@ async def predict_pipeline_health(
"""
try:
return await CompositeQueries.predict_pipeline_health(
timescale_conn, network_name, query_time
timescale_conn, postgres_conn, query_time
)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
+40 -3
View File
@@ -2,17 +2,41 @@ from fastapi import APIRouter, Depends, HTTPException, Query, Path, Body
from typing import List
from datetime import datetime
from psycopg import AsyncConnection
from pydantic import BaseModel, Field, field_validator
from app.infra.db.postgresql.scada import ScadaInfoRepository
from app.infra.db.timescaledb.repositories.scada import ScadaRepository
from .dependencies import get_timescale_connection
from .dependencies import get_postgres_connection, get_timescale_connection
router = APIRouter()
SCADA_BATCH_MAX_ITEMS = 10_000
class ScadaReadingBatchItem(BaseModel):
time: datetime
device_id: str = Field(min_length=1)
monitored_value: float | None = None
cleaned_value: float | None = None
@field_validator("device_id")
@classmethod
def normalize_device_id(cls, value: str) -> str:
normalized = value.strip()
if not normalized:
raise ValueError("device_id must not be blank")
return normalized
@router.post("/timeseries/scada-readings/batches", status_code=201, summary="批量插入SCADA监测数据")
async def insert_scada_data(
data: List[dict] = Body(..., description="SCADA设备监测数据列表"),
data: List[ScadaReadingBatchItem] = Body(
...,
min_length=1,
max_length=SCADA_BATCH_MAX_ITEMS,
description="SCADA设备监测数据列表",
),
conn: AsyncConnection = Depends(get_timescale_connection),
postgres_conn: AsyncConnection = Depends(get_postgres_connection),
):
"""
批量插入SCADA监测数据
@@ -25,7 +49,20 @@ async def insert_scada_data(
Returns:
插入成功的记录数
"""
await ScadaRepository.insert_scada_batch(conn, data)
rows = [item.model_dump() for item in data]
requested_ids = list(dict.fromkeys(item["device_id"] for item in rows))
existing_ids = await ScadaInfoRepository.get_existing_device_ids(
postgres_conn, requested_ids
)
missing_ids = [
device_id for device_id in requested_ids if device_id not in existing_ids
]
if missing_ids:
raise HTTPException(
status_code=422,
detail=f"SCADA devices do not exist in BizDB: {', '.join(missing_ids)}",
)
await ScadaRepository.insert_scada_batch(conn, rows)
return {"message": f"Inserted {len(data)} records"}