refactor(backend)!: separate algorithm and data layers

Reorganize algorithm packages by business responsibility, move orchestration into services, and keep database access behind pooled repositories.

Harden analysis API validation, remove unsafe legacy simulation endpoints, and add regression and architecture boundary coverage.

BREAKING CHANGE: legacy algorithm module paths and obsolete simulation endpoints are removed.
This commit is contained in:
2026-09-04 17:30:55 +08:00
parent 9b095c7439
commit 5966d039de
91 changed files with 1418 additions and 4020 deletions
+108
View File
@@ -1,5 +1,8 @@
from contextlib import contextmanager
from datetime import datetime
import fcntl
from io import BytesIO
from pathlib import Path
from typing import Any
from uuid import UUID
@@ -8,8 +11,12 @@ from openpyxl.styles import Alignment, Font, PatternFill
from openpyxl.worksheet.worksheet import Worksheet
from openpyxl.utils import get_column_letter
from pyproj import Transformer
import wntr
from app.algorithms.pressure_sensor_placement import kmeans_placement
from app.algorithms.pressure_sensor_placement import sensitivity_placement
from app.infra.db.postgresql import sensor_placement as sensor_placement_repository
from app.native.wndb.inp.exporter import dump_inp
class SensorPlacementNotFoundError(LookupError):
@@ -24,6 +31,107 @@ class SensorPlacementConflictError(RuntimeError):
pass
def _sensor_inp_path(project_code: str) -> Path:
if (
not project_code
or project_code in {".", ".."}
or "/" in project_code
or "\\" in project_code
or "\x00" in project_code
):
raise SensorPlacementValidationError("管网名称不是有效的项目标识")
return Path("db_inp") / f"{project_code}.db.inp"
@contextmanager
def _sensor_inp_lock(project_code: str):
inp_path = _sensor_inp_path(project_code)
inp_path.parent.mkdir(parents=True, exist_ok=True)
lock_path = inp_path.with_suffix(".sensor.lock")
with lock_path.open("w", encoding="utf-8") as lock_file:
try:
fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError as exc:
raise SensorPlacementConflictError(
"当前项目已有监测点优化任务正在运行,请稍后重试"
) from exc
try:
yield inp_path
finally:
fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
def _create_validated_placement(
project_code: str,
*,
run_name: str,
min_diameter: int,
created_by: str,
sensor_locations: list[str],
) -> dict[str, Any]:
validate_sensor_placement_nodes(project_code, sensor_locations)
return sensor_placement_repository.create_sensor_placement(
project_code,
run_name=run_name,
min_diameter=min_diameter,
created_by=created_by,
sensor_locations=sensor_locations,
)
def optimize_sensor_placement_by_sensitivity(
project_code: str,
run_name: str,
sensor_count: int,
min_diameter: int,
created_by: str,
) -> dict[str, Any]:
"""Run sensitivity placement and persist the validated result."""
with _sensor_inp_lock(project_code):
inp_path = _sensor_inp_path(project_code)
dump_inp(project_code, str(inp_path), "2")
network_model = wntr.network.WaterNetworkModel(str(inp_path))
sensor_locations = sensitivity_placement.optimize_sensor_placement(
network_model,
sensor_num=sensor_count,
min_diameter=min_diameter,
)
return _create_validated_placement(
project_code,
run_name=run_name,
min_diameter=min_diameter,
created_by=created_by,
sensor_locations=sensor_locations,
)
def optimize_sensor_placement_by_kmeans(
project_code: str,
run_name: str,
sensor_count: int,
min_diameter: int,
created_by: str,
) -> dict[str, Any]:
"""Export the model, run K-means placement, and persist the result."""
with _sensor_inp_lock(project_code) as inp_path:
dump_inp(project_code, str(inp_path), "2")
network_model = wntr.network.WaterNetworkModel(str(inp_path))
sensor_locations = kmeans_placement.optimize_sensor_placement(
network_model,
sensor_count=sensor_count,
min_diameter_mm=min_diameter,
)
return _create_validated_placement(
project_code,
run_name=run_name,
min_diameter=min_diameter,
created_by=created_by,
sensor_locations=sensor_locations,
)
_to_wgs84 = Transformer.from_crs("EPSG:3857", "EPSG:4326", always_xy=True)
_STATUS_LABELS = {
"current": "当前方案",