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:
@@ -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]:
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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"}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user