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
+40 -59
View File
@@ -30,10 +30,13 @@ from typing import Optional, Tuple
from uuid import UUID
import typing
import logging
import app.services.globals as globals
import app.services.project_info as project_info
from app.infra.db.postgresql.scada import (
ScadaElementMappings,
load_realtime_element_mappings,
)
from app.services.time_api import parse_beijing_time, parse_clock_duration_seconds
from app.native.wndb.core.connection import project_connection, project_transaction
from app.native.wndb.core.connection import project_transaction
from app.native.wndb.core.database import refresh_materialized_views_after_commit
from app.infra.db.timescaledb.internal_queries import (
InternalQueries as TimescaleInternalQueries,
@@ -47,6 +50,8 @@ logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)
RESERVOIR_BASIC_HEIGHT = 250.35
def _primary_demand(demand_set: dict) -> dict:
"""Return sequence-zero demand, creating it when a junction has none."""
@@ -73,39 +78,12 @@ def _primary_demand_pattern(demand_set: dict) -> str:
return str(pattern)
def query_corresponding_element_id_and_query_id(name: str) -> None:
"""Load realtime device-to-element mappings from the new asset schema."""
target_maps = {
"reservoir_liquid_level": globals.reservoirs_id,
"tank_liquid_level": globals.tanks_id,
"fixed_pump": globals.fixed_pumps_id,
"variable_pump": globals.variable_pumps_id,
"pressure": globals.pressure_id,
"demand": globals.demand_id,
"quality": globals.quality_id,
}
for mapping in target_maps.values():
mapping.clear()
with project_connection(name) as conn, conn.cursor() as cur:
cur.execute(
"""
SELECT device_type, COALESCE(node_id, link_id) AS element_id,
api_query_id
FROM asset.scada_devices
WHERE transmission_mode = 'realtime'
AND api_query_id IS NOT NULL
"""
)
for record in cur.fetchall():
device_type = record["device_type"]
element_id = record["element_id"]
api_query_id = record["api_query_id"]
mapping = target_maps.get(str(device_type).lower())
if mapping is not None:
mapping[str(element_id)] = str(api_query_id)
def query_corresponding_element_id_and_query_id(name: str) -> ScadaElementMappings:
"""Return an immutable project-local SCADA-to-model mapping snapshot."""
return load_realtime_element_mappings(name)
def get_pattern_index(cur_datetime: str) -> int:
def get_pattern_index(cur_datetime: str, pattern_time_step: float) -> int:
"""
根据给定的日期时间字符串,计算并返回对应的模式索引。
:param cur_datetime: str, 当前的日期时间字符串,格式为“YYYY-MM-DD HH:MM:SS”。
@@ -115,18 +93,18 @@ def get_pattern_index(cur_datetime: str) -> int:
dt = datetime.strptime(cur_datetime, str_format)
hr = dt.hour
mnt = dt.minute
i = int((hr * 60 + mnt) / globals.PATTERN_TIME_STEP)
i = int((hr * 60 + mnt) / pattern_time_step)
return i
def get_pattern_index_str(current_time: str) -> str:
def get_pattern_index_str(current_time: str, pattern_time_step: float) -> str:
"""
根据当前时间获取时间步长的模式索引,并将其格式化为“HH:MM:00”字符串。
:param current_time: str, 当前时间,格式为"YYYY-MM-DD HH:MM:SS"
:return: str 以“HH:MM:00”格式返回
"""
i = get_pattern_index(current_time)
[minN, hrN] = modf(i * globals.PATTERN_TIME_STEP / 60)
i = get_pattern_index(current_time, pattern_time_step)
[minN, hrN] = modf(i * pattern_time_step / 60)
minN_str = str(int(minN * 60))
minN_str = minN_str.zfill(2)
hrN_str = str(int(hrN))
@@ -204,6 +182,7 @@ def run_simulation(
valve_control: dict[str, dict] = None,
scheme_username: str = "system",
scheme_detail: dict | None = None,
scada_mappings: ScadaElementMappings | None = None,
) -> UUID | None:
"""
传入需要修改的参数,改变数据库中对应位置的值,然后计算,返回结果
@@ -257,19 +236,20 @@ def run_simulation(
print(dic_time)
# 获取水力模拟步长,如’0:15:00‘
globals.hydraulic_timestep = dic_time["HYDRAULIC TIMESTEP"]
hydraulic_timestep = dic_time["HYDRAULIC TIMESTEP"]
# 转换为分钟浮点数,兼容 EPANET 的 H:MM 和 H:MM:SS 写法
globals.PATTERN_TIME_STEP = (
pattern_time_step = (
parse_clock_duration_seconds(
globals.hydraulic_timestep,
hydraulic_timestep,
field_name="HYDRAULIC TIMESTEP",
)
/ 60
)
project_scada = scada_mappings or load_realtime_element_mappings(name_c)
# 对输入的时间参数进行处理
pattern_start_time = convert_time_format(modify_pattern_start_time)
# 获取模拟开始时间是对应pattern的第几个数
modify_index = get_pattern_index(pattern_start_time)
modify_index = get_pattern_index(pattern_start_time, pattern_time_step)
# 遍历水泵的pattern_id,并根据输入的pump_pattern修改pattern的值
# for pump_pattern_id in pump_pattern_ids:
# # 检查pump_pattern中pump_pattern_id对应的第一个频率值是否为有效数字(非空、非NaN)。如果该值有效,则继续执行代码块。
@@ -284,7 +264,7 @@ def run_simulation(
# set_pattern(name_c, cs)
# 修改模拟开始的时间
str_pattern_start = get_pattern_index_str(
convert_time_format(modify_pattern_start_time)
convert_time_format(modify_pattern_start_time), pattern_time_step
)
dic_time = get_time(name_c)
dic_time["PATTERN START"] = str_pattern_start
@@ -295,18 +275,18 @@ def run_simulation(
cs.operations.append(dic_time)
set_time(name_c, cs)
# 根据SCADA实时数据进行修改,如果没有对应的SCADA数据,如未来的时间点,则不改变pg数据库的数据
if globals.reservoirs_id:
if project_scada.reservoirs:
# reservoirs_id = {'ZBBDJSCP000002': '2497', 'R00003': '2571'}
# 1.获取reservoir的SCADA数据,形式如{'2497': '3.1231', '2571': '2.7387'}
reservoir_SCADA_data_dict = TimescaleInternalQueries.query_scada_by_ids_time(
device_ids=list(globals.reservoirs_id.values()),
device_ids=list(project_scada.reservoirs.values()),
query_time=modify_pattern_start_time,
db_name=name,
)
# 2.构建出新字典,形式如{'ZBBDJSCP000002': '3.1231', 'R00003': '2.7387'}
reservoir_dict = {
key: reservoir_SCADA_data_dict[value]
for key, value in globals.reservoirs_id.items()
for key, value in project_scada.reservoirs.items()
}
# 3.修改reservoir液位模式
for reservoir_name, value in reservoir_dict.items():
@@ -316,20 +296,21 @@ def run_simulation(
name_c, get_reservoir(name_c, reservoir_name)["pattern"]
)
reservoir_pattern["factors"][modify_index] = (
float(value) + globals.RESERVOIR_BASIC_HEIGHT
float(value) + RESERVOIR_BASIC_HEIGHT
)
cs = ChangeSet()
cs.append(reservoir_pattern)
set_pattern(name_c, cs)
if globals.tanks_id:
if project_scada.tanks:
# 修改tank初始液位
tank_SCADA_data_dict = TimescaleInternalQueries.query_scada_by_ids_time(
device_ids=list(globals.tanks_id.values()),
device_ids=list(project_scada.tanks.values()),
query_time=modify_pattern_start_time,
db_name=name,
)
tank_dict = {
key: tank_SCADA_data_dict[value] for key, value in globals.tanks_id.items()
key: tank_SCADA_data_dict[value]
for key, value in project_scada.tanks.items()
}
for tank_name, value in tank_dict.items():
if value and float(value) != 0:
@@ -338,17 +319,17 @@ def run_simulation(
cs = ChangeSet()
cs.append(tank)
set_tank(name_c, cs)
if globals.fixed_pumps_id:
if project_scada.fixed_pumps:
# 修改工频泵的pattern
fixed_pump_SCADA_data_dict = TimescaleInternalQueries.query_scada_by_ids_time(
device_ids=list(globals.fixed_pumps_id.values()),
device_ids=list(project_scada.fixed_pumps.values()),
query_time=modify_pattern_start_time,
db_name=name,
)
# print(fixed_pump_SCADA_data_dict)
fixed_pump_dict = {
key: fixed_pump_SCADA_data_dict[value]
for key, value in globals.fixed_pumps_id.items()
for key, value in project_scada.fixed_pumps.items()
}
# print(fixed_pump_dict)
for fixed_pump_name, value in fixed_pump_dict.items():
@@ -362,18 +343,18 @@ def run_simulation(
cs = ChangeSet()
cs.append(pump_pattern)
set_pattern(name_c, cs)
if globals.variable_pumps_id:
if project_scada.variable_pumps:
# 修改变频泵的pattern
variable_pump_SCADA_data_dict = (
TimescaleInternalQueries.query_scada_by_ids_time(
device_ids=list(globals.variable_pumps_id.values()),
device_ids=list(project_scada.variable_pumps.values()),
query_time=modify_pattern_start_time,
db_name=name,
)
)
variable_pump_dict = {
key: variable_pump_SCADA_data_dict[value]
for key, value in globals.variable_pumps_id.items()
for key, value in project_scada.variable_pumps.items()
}
for variable_pump_name, value in variable_pump_dict.items():
if value:
@@ -384,16 +365,16 @@ def run_simulation(
cs = ChangeSet()
cs.append(pump_pattern)
set_pattern(name_c, cs)
if globals.demand_id:
if project_scada.demand:
# 基于实时数据,修改大用户节点的pattern
demand_SCADA_data_dict = TimescaleInternalQueries.query_scada_by_ids_time(
device_ids=list(globals.demand_id.values()),
device_ids=list(project_scada.demand.values()),
query_time=modify_pattern_start_time,
db_name=name,
)
demand_dict = {
key: demand_SCADA_data_dict[value]
for key, value in globals.demand_id.items()
for key, value in project_scada.demand.items()
}
for demand_name, value in demand_dict.items():
if value is not None and not np.isnan(float(value)):
@@ -421,7 +402,7 @@ def run_simulation(
if not np.isnan(modify_reservoir_head_pattern[reservoir_name][0]):
# 给 list 中的所有元素加上 RESERVOIR_BASIC_HEIGHT
modified_values = [
value + globals.RESERVOIR_BASIC_HEIGHT
value + RESERVOIR_BASIC_HEIGHT
for value in modify_reservoir_head_pattern[reservoir_name]
]
reservoir_pattern = get_pattern(