refactor(db)!: adopt project-routed pooled databases

Reorganize WNDB by responsibility and remove legacy scheme endpoints.\n\nRoute analysis and time-series access through project pools, preserve transactional realtime replacement, and refresh GIS materialized views after writes.\n\nAdd database architecture documentation, live pooling coverage, API contract updates, and executable container verification.\n\nBREAKING CHANGE: legacy scheme APIs and flat app.native.wndb module imports are removed.
This commit is contained in:
2026-08-25 18:35:05 +08:00
parent fdbcc5c033
commit fa188af0b1
181 changed files with 8446 additions and 33546 deletions
+52 -36
View File
@@ -1,6 +1,7 @@
import logging
from typing import Any
from urllib.parse import quote
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Path, Query, status
from fastapi.responses import StreamingResponse
@@ -26,8 +27,9 @@ from app.services.sensor_placement import (
build_sensor_placement_workbook,
can_edit_sensor_placement,
get_sensor_placement_candidate,
get_sensor_placement_scheme,
update_sensor_placement_scheme,
get_sensor_placement_run,
list_sensor_placement_runs,
update_sensor_placement_run,
)
router = APIRouter()
@@ -74,19 +76,19 @@ def _service_http_error(exc: Exception) -> HTTPException:
)
def _get_scheme_response(
def _get_run_response(
network: str,
scheme_id: int,
run_id: UUID,
current_user: Any,
project_context: ProjectContext,
) -> dict[str, Any]:
try:
scheme = get_sensor_placement_scheme(network, scheme_id)
run = get_sensor_placement_run(network, run_id)
return {
**scheme,
**run,
"can_edit": (
_can_modify_project(project_context)
and can_edit_sensor_placement(current_user, scheme)
and can_edit_sensor_placement(current_user, run)
),
}
except (
@@ -116,7 +118,7 @@ async def get_sensor_placement_candidate_detail(
@router.post(
"/sensor-placement-optimization-runs",
"/sensor-placement-runs",
response_model=SensorPlacementSchemeResponse,
summary="创建并返回监测点优化方案",
)
@@ -136,13 +138,13 @@ async def optimize_sensor_placement_scheme(
created = await run_in_threadpool(
optimizer,
name=network,
scheme_name=payload.scheme_name,
scheme_name=payload.run_name,
sensor_number=payload.sensor_count,
min_diameter=payload.min_diameter,
username=current_user.username,
)
scheme = get_sensor_placement_scheme(network, int(created["id"]))
return {**scheme, "can_edit": True}
run = get_sensor_placement_run(network, created["run_id"])
return {**run, "can_edit": True}
except (
SensorPlacementConflictError,
SensorPlacementValidationError,
@@ -158,31 +160,45 @@ async def optimize_sensor_placement_scheme(
@router.get(
"/sensor-placement-schemes/{scheme_id}",
"/sensor-placement-runs",
response_model=list[SensorPlacementSchemeResponse],
summary="获取监测点优化运行",
)
async def get_sensor_placement_runs(
project_context: ProjectContext = Depends(get_project_context),
) -> list[dict[str, Any]]:
return await run_in_threadpool(
list_sensor_placement_runs,
project_context.project_code,
)
@router.get(
"/sensor-placement-runs/{run_id}",
response_model=SensorPlacementSchemeResponse,
summary="获取监测点方案详情",
)
async def get_sensor_placement_scheme_detail(
scheme_id: int,
async def get_sensor_placement_run_detail(
run_id: UUID,
network: str = Query(..., min_length=1),
project_context: ProjectContext = Depends(get_project_context),
current_user=Depends(get_current_metadata_user),
) -> dict[str, Any]:
return _get_scheme_response(
return _get_run_response(
_project_network(network, project_context),
scheme_id,
run_id,
current_user,
project_context,
)
@router.put(
"/sensor-placement-schemes/{scheme_id}",
"/sensor-placement-runs/{run_id}",
response_model=SensorPlacementSchemeResponse,
summary="覆盖保存监测点方案",
)
async def overwrite_sensor_placement_scheme(
scheme_id: int,
async def overwrite_sensor_placement_run(
run_id: UUID,
payload: SensorPlacementUpdateRequest,
network: str = Query(..., min_length=1),
project_context: ProjectContext = Depends(get_project_context),
@@ -190,21 +206,21 @@ async def overwrite_sensor_placement_scheme(
) -> dict[str, Any]:
network = _project_network(network, project_context)
_require_project_write(project_context)
scheme = _get_scheme_response(
run = _get_run_response(
network,
scheme_id,
run_id,
current_user,
project_context,
)
if not scheme["can_edit"]:
raise HTTPException(status_code=403, detail="无权修改该监测点方案")
if not run["can_edit"]:
raise HTTPException(status_code=403, detail="无权修改该监测点优化运行")
try:
updated = update_sensor_placement_scheme(
updated = update_sensor_placement_run(
network,
scheme_id,
expected_sensor_location=payload.expected_sensor_location,
sensor_location=payload.sensor_location,
run_id,
expected_sensor_locations=payload.expected_sensor_locations,
sensor_locations=payload.sensor_locations,
)
return {**updated, "can_edit": True}
except (
@@ -216,26 +232,26 @@ async def overwrite_sensor_placement_scheme(
@router.post(
"/sensor-placement-schemes/{scheme_id}/exports/excel",
"/sensor-placement-runs/{run_id}/exports/excel",
summary="导出监测点工程清单",
)
async def export_sensor_placement_excel(
scheme_id: int,
run_id: UUID,
payload: SensorPlacementExportRequest,
network: str = Query(..., min_length=1),
project_context: ProjectContext = Depends(get_project_context),
current_user=Depends(get_current_metadata_user),
) -> StreamingResponse:
network = _project_network(network, project_context)
scheme = _get_scheme_response(
run = _get_run_response(
network,
scheme_id,
run_id,
current_user,
project_context,
)
if (
payload.sensor_location != scheme["sensor_location"]
and not scheme["can_edit"]
payload.sensor_locations != run["sensor_locations"]
and not run["can_edit"]
):
raise HTTPException(status_code=403, detail="无权导出该方案的未保存草稿")
@@ -243,14 +259,14 @@ async def export_sensor_placement_excel(
workbook = await run_in_threadpool(
build_sensor_placement_workbook,
network=network,
scheme=scheme,
sensor_location=payload.sensor_location,
scheme=run,
sensor_location=payload.sensor_locations,
adjustment_status=payload.adjustment_status,
)
except SensorPlacementValidationError as exc:
raise _service_http_error(exc) from exc
filename = f"{scheme['scheme_name']}_监测点清单.xlsx"
filename = f"{run['name']}_监测点清单.xlsx"
encoded_filename = quote(filename)
return StreamingResponse(
workbook,