245 lines
7.6 KiB
Python
245 lines
7.6 KiB
Python
import logging
|
|
from typing import Any
|
|
from urllib.parse import quote
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
|
from fastapi.responses import StreamingResponse
|
|
from starlette.concurrency import run_in_threadpool
|
|
|
|
from app.algorithms.sensor import (
|
|
pressure_sensor_placement_kmeans,
|
|
pressure_sensor_placement_sensitivity,
|
|
)
|
|
from app.auth.metadata_dependencies import get_current_metadata_user
|
|
from app.auth.project_dependencies import ProjectContext, get_project_context
|
|
from app.domain.schemas.sensor_placement import (
|
|
SensorPlacementExportRequest,
|
|
SensorPlacementOptimizeRequest,
|
|
SensorPlacementSchemeResponse,
|
|
SensorPlacementUpdateRequest,
|
|
)
|
|
from app.services.sensor_placement import (
|
|
SensorPlacementConflictError,
|
|
SensorPlacementNotFoundError,
|
|
SensorPlacementValidationError,
|
|
build_sensor_placement_workbook,
|
|
can_edit_sensor_placement,
|
|
get_sensor_placement_scheme,
|
|
update_sensor_placement_scheme,
|
|
)
|
|
|
|
router = APIRouter()
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _project_network(network: str, project_context: ProjectContext) -> str:
|
|
if network != project_context.project_code:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="请求的管网不属于当前项目",
|
|
)
|
|
return project_context.project_code
|
|
|
|
|
|
def _can_modify_project(project_context: ProjectContext) -> bool:
|
|
return project_context.project_role == "member"
|
|
|
|
|
|
def _require_project_write(
|
|
project_context: ProjectContext,
|
|
) -> None:
|
|
if not _can_modify_project(project_context):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="当前项目角色为只读,不能修改监测点方案",
|
|
)
|
|
|
|
|
|
def _service_http_error(exc: Exception) -> HTTPException:
|
|
if isinstance(exc, SensorPlacementNotFoundError):
|
|
return HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=str(exc),
|
|
)
|
|
if isinstance(exc, SensorPlacementConflictError):
|
|
return HTTPException(
|
|
status_code=status.HTTP_409_CONFLICT,
|
|
detail=str(exc),
|
|
)
|
|
return HTTPException(
|
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
detail=str(exc),
|
|
)
|
|
|
|
|
|
def _get_scheme_response(
|
|
network: str,
|
|
scheme_id: int,
|
|
current_user: Any,
|
|
project_context: ProjectContext,
|
|
) -> dict[str, Any]:
|
|
try:
|
|
scheme = get_sensor_placement_scheme(network, scheme_id)
|
|
return {
|
|
**scheme,
|
|
"can_edit": (
|
|
_can_modify_project(project_context)
|
|
and can_edit_sensor_placement(current_user, scheme)
|
|
),
|
|
}
|
|
except (
|
|
SensorPlacementNotFoundError,
|
|
SensorPlacementValidationError,
|
|
) as exc:
|
|
raise _service_http_error(exc) from exc
|
|
|
|
|
|
@router.post(
|
|
"/sensor-placement-schemes/optimize",
|
|
response_model=SensorPlacementSchemeResponse,
|
|
summary="创建并返回监测点优化方案",
|
|
)
|
|
async def optimize_sensor_placement_scheme(
|
|
payload: SensorPlacementOptimizeRequest,
|
|
project_context: ProjectContext = Depends(get_project_context),
|
|
current_user=Depends(get_current_metadata_user),
|
|
) -> dict[str, Any]:
|
|
network = _project_network(payload.network, project_context)
|
|
_require_project_write(project_context)
|
|
optimizer = (
|
|
pressure_sensor_placement_sensitivity
|
|
if payload.method == "sensitivity"
|
|
else pressure_sensor_placement_kmeans
|
|
)
|
|
try:
|
|
created = await run_in_threadpool(
|
|
optimizer,
|
|
name=network,
|
|
scheme_name=payload.scheme_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}
|
|
except (
|
|
SensorPlacementConflictError,
|
|
SensorPlacementValidationError,
|
|
ValueError,
|
|
) as exc:
|
|
raise _service_http_error(exc) from exc
|
|
except Exception as exc:
|
|
logger.exception("Sensor placement optimization failed")
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail="监测点优化失败,请稍后重试",
|
|
) from exc
|
|
|
|
|
|
@router.get(
|
|
"/sensor-placement-schemes/{scheme_id}",
|
|
response_model=SensorPlacementSchemeResponse,
|
|
summary="获取监测点方案详情",
|
|
)
|
|
async def get_sensor_placement_scheme_detail(
|
|
scheme_id: int,
|
|
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(
|
|
_project_network(network, project_context),
|
|
scheme_id,
|
|
current_user,
|
|
project_context,
|
|
)
|
|
|
|
|
|
@router.put(
|
|
"/sensor-placement-schemes/{scheme_id}",
|
|
response_model=SensorPlacementSchemeResponse,
|
|
summary="覆盖保存监测点方案",
|
|
)
|
|
async def overwrite_sensor_placement_scheme(
|
|
scheme_id: int,
|
|
payload: SensorPlacementUpdateRequest,
|
|
network: str = Query(..., min_length=1),
|
|
project_context: ProjectContext = Depends(get_project_context),
|
|
current_user=Depends(get_current_metadata_user),
|
|
) -> dict[str, Any]:
|
|
network = _project_network(network, project_context)
|
|
_require_project_write(project_context)
|
|
scheme = _get_scheme_response(
|
|
network,
|
|
scheme_id,
|
|
current_user,
|
|
project_context,
|
|
)
|
|
if not scheme["can_edit"]:
|
|
raise HTTPException(status_code=403, detail="无权修改该监测点方案")
|
|
|
|
try:
|
|
updated = update_sensor_placement_scheme(
|
|
network,
|
|
scheme_id,
|
|
expected_sensor_location=payload.expected_sensor_location,
|
|
sensor_location=payload.sensor_location,
|
|
)
|
|
return {**updated, "can_edit": True}
|
|
except (
|
|
SensorPlacementConflictError,
|
|
SensorPlacementNotFoundError,
|
|
SensorPlacementValidationError,
|
|
) as exc:
|
|
raise _service_http_error(exc) from exc
|
|
|
|
|
|
@router.post(
|
|
"/sensor-placement-schemes/{scheme_id}/exports/excel",
|
|
summary="导出监测点工程清单",
|
|
)
|
|
async def export_sensor_placement_excel(
|
|
scheme_id: int,
|
|
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(
|
|
network,
|
|
scheme_id,
|
|
current_user,
|
|
project_context,
|
|
)
|
|
if (
|
|
payload.sensor_location != scheme["sensor_location"]
|
|
and not scheme["can_edit"]
|
|
):
|
|
raise HTTPException(status_code=403, detail="无权导出该方案的未保存草稿")
|
|
|
|
try:
|
|
workbook = await run_in_threadpool(
|
|
build_sensor_placement_workbook,
|
|
network=network,
|
|
scheme=scheme,
|
|
sensor_location=payload.sensor_location,
|
|
adjustment_status=payload.adjustment_status,
|
|
)
|
|
except SensorPlacementValidationError as exc:
|
|
raise _service_http_error(exc) from exc
|
|
|
|
filename = f"{scheme['scheme_name']}_监测点清单.xlsx"
|
|
encoded_filename = quote(filename)
|
|
return StreamingResponse(
|
|
workbook,
|
|
media_type=(
|
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
),
|
|
headers={
|
|
"Content-Disposition": (
|
|
f"attachment; filename*=UTF-8''{encoded_filename}"
|
|
)
|
|
},
|
|
)
|