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
+3 -3
View File
@@ -13,21 +13,21 @@ router = APIRouter()
@router.get("/network-schemas/scada-device", summary="获取 SCADA 设备结构")
async def get_scada_device_schema(
def get_scada_device_schema(
network: str = Query(..., description="管网名称(或数据库名称)"),
) -> dict[str, dict[str, Any]]:
return get_scada_info_schema(network)
@router.get("/scada-devices", summary="获取 SCADA 设备列表")
async def get_scada_devices(
def get_scada_devices(
network: str = Query(..., description="管网名称(或数据库名称)"),
) -> list[dict[str, Any]]:
return get_all_scada_info(network)
@router.get("/scada-devices/detail", summary="获取 SCADA 设备")
async def get_scada_device(
def get_scada_device(
network: str = Query(..., description="管网名称(或数据库名称)"),
device_id: str = Query(..., description="SCADA 设备 ID"),
) -> dict[str, Any]:
+2 -2
View File
@@ -185,7 +185,7 @@ async def get_sensor_placement_runs(
response_model=SensorPlacementSchemeResponse,
summary="获取监测点方案详情",
)
async def get_sensor_placement_run_detail(
def get_sensor_placement_run_detail(
run_id: UUID,
network: str = Query(..., min_length=1),
project_context: ProjectContext = Depends(get_project_context),
@@ -204,7 +204,7 @@ async def get_sensor_placement_run_detail(
response_model=SensorPlacementSchemeResponse,
summary="覆盖保存监测点方案",
)
async def overwrite_sensor_placement_run(
def overwrite_sensor_placement_run(
run_id: UUID,
payload: SensorPlacementUpdateRequest,
network: str = Query(..., min_length=1),
+10 -5
View File
@@ -6,7 +6,6 @@ from fastapi import APIRouter, Depends, HTTPException, Query, Path, Body
from fastapi.responses import PlainTextResponse
from app.auth.keycloak_dependencies import get_current_keycloak_username
import app.services.simulation as simulation
import app.services.globals as globals
from app.services.tjnetwork import (
run_project,
run_project_return_dict,
@@ -115,12 +114,16 @@ def run_simulation_manually_by_date(
if hydraulic_step_seconds <= 0:
raise ValueError("HYDRAULIC TIMESTEP must be greater than 0.")
hydraulic_step = timedelta(seconds=hydraulic_step_seconds)
scada_mappings = simulation.query_corresponding_element_id_and_query_id(
network_name
)
current_time = start_time
while current_time < end_datetime:
simulation.run_simulation(
name=network_name,
simulation_type="realtime",
modify_pattern_start_time=current_time.isoformat(timespec="seconds"),
scada_mappings=scada_mappings,
)
current_time += hydraulic_step
@@ -497,9 +500,11 @@ def fastapi_pressure_regulation(data: PressureRegulation = Body(..., description
支持固定泵和变速泵的独立控制。
"""
item = data.model_dump()
simulation.query_corresponding_element_id_and_query_id(item["network"])
fixed_pumps = set(globals.fixed_pumps_id.keys())
variable_pumps = set(globals.variable_pumps_id.keys())
scada_mappings = simulation.query_corresponding_element_id_and_query_id(
item["network"]
)
fixed_pumps = set(scada_mappings.fixed_pumps)
variable_pumps = set(scada_mappings.variable_pumps)
fixed_pump_pattern: dict[str, list] = {}
variable_pump_pattern: dict[str, list] = {}
for pump_id, values in item["pump_control"].items():
@@ -515,6 +520,7 @@ def fastapi_pressure_regulation(data: PressureRegulation = Body(..., description
modify_fixed_pump_pattern=fixed_pump_pattern or None,
modify_variable_pump_pattern=variable_pump_pattern or None,
scheme_name=item["scheme_name"],
scada_mappings=scada_mappings,
)
return "success"
@@ -667,7 +673,6 @@ def fastapi_run_simulation_manually_by_date(
"""
item = data.model_dump()
try:
simulation.query_corresponding_element_id_and_query_id(item["name"])
start_time = parse_utc_time(item["start_time"], field_name="start_time")
run_simulation_manually_by_date(
item["name"], start_time, item["duration"]
+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"}