588 lines
20 KiB
Python
588 lines
20 KiB
Python
import json
|
|
from fastapi import APIRouter, Request, HTTPException, Query, Path, Body, Depends
|
|
from fastapi.responses import PlainTextResponse
|
|
from typing import Any, Dict, List
|
|
from app.infra.db.metadb.repositories.metadata_repository import MetadataRepository
|
|
from app.auth.project_dependencies import get_metadata_repository
|
|
from app.domain.schemas.metadata import ProjectMetaResponse, GeoServerConfigResponse
|
|
import app.services.project_info as project_info
|
|
from app.infra.db.postgresql.database import get_database_instance as get_pg_db
|
|
from app.infra.db.timescaledb.database import get_database_instance as get_ts_db
|
|
from app.services.tjnetwork import (
|
|
ChangeSet,
|
|
list_project,
|
|
have_project,
|
|
create_project,
|
|
delete_project,
|
|
is_project_open,
|
|
open_project,
|
|
close_project,
|
|
copy_project,
|
|
import_inp,
|
|
export_inp,
|
|
read_inp,
|
|
dump_inp,
|
|
get_all_vertices,
|
|
get_all_scada_elements,
|
|
get_all_district_metering_areas,
|
|
get_all_service_areas,
|
|
get_all_virtual_districts,
|
|
get_extension_data,
|
|
convert_inp_v3_to_v2,
|
|
)
|
|
|
|
# For inp file upload/download
|
|
import os
|
|
from fastapi import Response, status
|
|
from fastapi.responses import FileResponse
|
|
inpDir = "data/" # Assuming data directory exists or is defined somewhere.
|
|
# In main.py it was likely global. For safety, let's use a relative path or get from config.
|
|
# But let's stick to what main.py probably used or a default.
|
|
|
|
router = APIRouter()
|
|
lockedPrjs: Dict[str, str] = {}
|
|
|
|
@router.get("/project_info/", summary="获取项目信息", description="从数据库获取项目的详细信息,包括地图范围等。", response_model=ProjectMetaResponse)
|
|
async def get_project_info_endpoint(
|
|
network: str = Query(..., description="管网名称(或项目代码)"),
|
|
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
|
):
|
|
"""
|
|
获取项目信息
|
|
|
|
- **network**: 管网名称(或项目代码)
|
|
"""
|
|
project_detail = await metadata_repo.get_project_detail_by_code(network)
|
|
if not project_detail:
|
|
raise HTTPException(status_code=404, detail=f"Project {network} not found")
|
|
|
|
geoserver_payload = None
|
|
if project_detail.geoserver:
|
|
geoserver_payload = GeoServerConfigResponse(
|
|
gs_base_url=project_detail.geoserver.gs_base_url,
|
|
gs_admin_user=project_detail.geoserver.gs_admin_user,
|
|
gs_datastore_name=project_detail.geoserver.gs_datastore_name,
|
|
default_extent=project_detail.geoserver.default_extent,
|
|
srid=project_detail.geoserver.srid,
|
|
)
|
|
|
|
return ProjectMetaResponse(
|
|
project_id=project_detail.project_id,
|
|
name=project_detail.name,
|
|
code=project_detail.code,
|
|
description=project_detail.description,
|
|
gs_workspace=project_detail.gs_workspace,
|
|
map_extent=project_detail.map_extent,
|
|
status=project_detail.status,
|
|
project_role="viewer", # Default role for public access
|
|
geoserver=geoserver_payload
|
|
)
|
|
|
|
@router.get("/listprojects/", summary="获取项目列表", description="获取服务器上所有可用的供水管网项目名称列表。")
|
|
async def list_projects_endpoint() -> list[str]:
|
|
"""
|
|
获取项目列表
|
|
|
|
返回所有已创建项目的名称列表。
|
|
"""
|
|
return list_project()
|
|
|
|
@router.get("/haveproject/", summary="检查项目是否存在", description="检查指定名称的项目是否存在。")
|
|
async def have_project_endpoint(
|
|
network: str = Query(..., description="管网名称(或数据库名称)")
|
|
):
|
|
"""
|
|
检查项目是否存在
|
|
|
|
- **network**: 管网名称(或数据库名称)
|
|
"""
|
|
return have_project(network)
|
|
|
|
@router.post("/createproject/", summary="创建新项目", description="创建一个新的供水管网项目。如果项目已存在,可能会覆盖或报错(取决于底层实现)。")
|
|
async def create_project_endpoint(
|
|
network: str = Query(..., description="管网名称(或数据库名称)")
|
|
):
|
|
"""
|
|
创建新项目
|
|
|
|
- **network**: 管网名称(或数据库名称)
|
|
"""
|
|
create_project(network)
|
|
return network
|
|
|
|
@router.post("/deleteproject/", summary="删除项目", description="永久删除指定的供水管网项目。此操作不可恢复。")
|
|
async def delete_project_endpoint(
|
|
network: str = Query(..., description="管网名称(或数据库名称)")
|
|
):
|
|
"""
|
|
删除项目
|
|
|
|
- **network**: 管网名称(或数据库名称)
|
|
"""
|
|
delete_project(network)
|
|
return True
|
|
|
|
@router.get("/isprojectopen/", summary="检查项目是否已打开", description="检查指定项目是否已被加载到内存中。")
|
|
async def is_project_open_endpoint(
|
|
network: str = Query(..., description="管网名称(或数据库名称)")
|
|
):
|
|
"""
|
|
检查项目是否已打开
|
|
|
|
- **network**: 管网名称(或数据库名称)
|
|
"""
|
|
return is_project_open(network)
|
|
|
|
@router.post("/openproject/", summary="打开项目", description="将指定项目加载到内存中,并初始化数据库连接池。")
|
|
async def open_project_endpoint(
|
|
network: str = Query(..., description="管网名称(或数据库名称)")
|
|
):
|
|
"""
|
|
打开项目
|
|
|
|
- **network**: 管网名称(或数据库名称)
|
|
"""
|
|
open_project(network)
|
|
|
|
# 尝试连接指定数据库
|
|
try:
|
|
# 初始化 PostgreSQL 连接池
|
|
pg_instance = await get_pg_db(network)
|
|
async with pg_instance.get_connection() as conn:
|
|
async with conn.cursor() as cur:
|
|
await cur.execute("SELECT 1")
|
|
|
|
# 初始化 TimescaleDB 连接池
|
|
ts_instance = await get_ts_db(network)
|
|
async with ts_instance.get_connection() as conn:
|
|
async with conn.cursor() as cur:
|
|
await cur.execute("SELECT 1")
|
|
|
|
except Exception as e:
|
|
# 记录错误但不阻断项目打开,或者根据需求决定是否阻断
|
|
# 这里选择打印错误,因为 open_project 原本只负责原生部分
|
|
print(f"Failed to connect to databases for {network}: {str(e)}")
|
|
# 如果数据库连接是必须的,可以抛出异常:
|
|
# raise HTTPException(status_code=500, detail=f"Database connection failed: {str(e)}")
|
|
|
|
return network
|
|
|
|
@router.post("/closeproject/", summary="关闭项目", description="将指定项目从内存中卸载,释放资源。")
|
|
async def close_project_endpoint(
|
|
network: str = Query(..., description="管网名称(或数据库名称)")
|
|
):
|
|
"""
|
|
关闭项目
|
|
|
|
- **network**: 管网名称(或数据库名称)
|
|
"""
|
|
close_project(network)
|
|
return True
|
|
|
|
@router.post("/copyproject/", summary="复制项目", description="将现有项目复制为新项目。")
|
|
async def copy_project_endpoint(
|
|
source: str = Query(..., description="管网名称(或数据库名称)"),
|
|
target: str = Query(..., description="管网名称(或数据库名称)")
|
|
):
|
|
"""
|
|
复制项目
|
|
|
|
- **source**: 管网名称(或数据库名称)
|
|
- **target**: 管网名称(或数据库名称)
|
|
"""
|
|
copy_project(source, target)
|
|
return True
|
|
|
|
@router.post("/importinp/", summary="导入 INP 文件内容", description="将 INP 格式的文本内容导入到指定项目中。")
|
|
async def import_inp_endpoint(
|
|
req: Request,
|
|
network: str = Query(..., description="管网名称(或数据库名称)")
|
|
):
|
|
"""
|
|
导入 INP 文件内容
|
|
|
|
- **network**: 管网名称(或数据库名称)
|
|
- **req**: 请求体,需包含 `{"inp": "..."}` 结构
|
|
"""
|
|
jo_root = await req.json()
|
|
inp_text = jo_root["inp"]
|
|
ps = {"inp": inp_text}
|
|
ret = import_inp(network, ChangeSet(ps))
|
|
print(ret)
|
|
return ret
|
|
|
|
@router.get("/exportinp/", response_model=None, summary="导出项目为 ChangeSet", description="导出项目的变更集 (ChangeSet),包含顶点、SCADA 元素、DMA、SA、VD 等信息。")
|
|
async def export_inp_endpoint(
|
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
version: str = Query(..., description="版本号 (通常用于增量更新)")
|
|
) -> ChangeSet:
|
|
"""
|
|
导出项目为 ChangeSet
|
|
|
|
- **network**: 管网名称(或数据库名称)
|
|
- **version**: 版本号
|
|
"""
|
|
cs = export_inp(network, version)
|
|
op = cs.operations[0]
|
|
open_project(network)
|
|
op["vertex"] = json.dumps(get_all_vertices(network))
|
|
op["scada"] = json.dumps(get_all_scada_elements(network))
|
|
op["dma"] = json.dumps(get_all_district_metering_areas(network))
|
|
op["sa"] = json.dumps(get_all_service_areas(network))
|
|
op["vd"] = json.dumps(get_all_virtual_districts(network))
|
|
op["legend"] = get_extension_data(network, "legend")
|
|
|
|
db = get_extension_data(network, "scada_db")
|
|
print(db)
|
|
scada_db = ""
|
|
if db:
|
|
scada_db = db
|
|
print(scada_db)
|
|
op["scada_db"] = scada_db
|
|
|
|
close_project(network)
|
|
|
|
return cs
|
|
|
|
@router.post("/readinp/", summary="读取 INP 文件到项目", description="从服务器文件系统中读取指定的 INP 文件并加载到项目中。")
|
|
async def read_inp_endpoint(
|
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
inp: str = Query(..., description="INP 文件名 (不包含路径)")
|
|
) -> bool:
|
|
"""
|
|
读取 INP 文件到项目
|
|
|
|
- **network**: 管网名称(或数据库名称)
|
|
- **inp**: INP 文件名
|
|
"""
|
|
read_inp(network, inp)
|
|
return True
|
|
|
|
@router.get("/dumpinp/", summary="导出项目到 INP 文件", description="将项目当前状态保存为 INP 文件到服务器文件系统。")
|
|
async def dump_inp_endpoint(
|
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
inp: str = Query(..., description="目标文件名")
|
|
) -> bool:
|
|
"""
|
|
导出项目到 INP 文件
|
|
|
|
- **network**: 管网名称(或数据库名称)
|
|
- **inp**: 目标文件名
|
|
"""
|
|
dump_inp(network, inp)
|
|
return True
|
|
|
|
@router.get("/isprojectlocked/", summary="检查项目是否被锁定", description="检查指定项目是否处于锁定状态。")
|
|
async def is_project_locked_endpoint(
|
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
req: Request = None
|
|
):
|
|
"""
|
|
检查项目是否被锁定
|
|
|
|
- **network**: 管网名称(或数据库名称)
|
|
"""
|
|
return network in lockedPrjs.keys()
|
|
|
|
@router.get("/isprojectlockedbyme/", summary="检查项目是否被当前用户锁定", description="检查指定项目是否被当前客户端 (IP) 锁定。")
|
|
async def is_project_locked_by_me_endpoint(
|
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
req: Request = None
|
|
):
|
|
"""
|
|
检查项目是否被当前用户锁定
|
|
|
|
- **network**: 管网名称(或数据库名称)
|
|
"""
|
|
client_host = req.client.host
|
|
return lockedPrjs.get(network) == client_host
|
|
|
|
# 0 successfully locked
|
|
# 1 already locked by you
|
|
# 2 locked by others
|
|
@router.post("/lockproject/", summary="锁定项目", description="锁定指定项目以防止并发修改。")
|
|
async def lock_project_endpoint(
|
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
req: Request = None
|
|
):
|
|
"""
|
|
锁定项目
|
|
|
|
返回值:
|
|
- **0**: 锁定成功
|
|
- **1**: 已被当前用户锁定
|
|
- **2**: 已被其他用户锁定
|
|
"""
|
|
client_host = req.client.host
|
|
if not network in lockedPrjs.keys():
|
|
lockedPrjs[network] = client_host
|
|
return 0
|
|
else:
|
|
if lockedPrjs.get(network) == client_host:
|
|
return 1
|
|
else:
|
|
return 2
|
|
|
|
@router.post("/unlockproject/", summary="解锁项目", description="释放对项目的锁定。")
|
|
def unlock_project_endpoint(
|
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
req: Request = None
|
|
):
|
|
"""
|
|
解锁项目
|
|
|
|
只有锁定者才能解锁。
|
|
"""
|
|
client_host = req.client.host
|
|
if lockedPrjs.get(network) == client_host:
|
|
print("delete key")
|
|
del lockedPrjs[network]
|
|
return True
|
|
|
|
return False
|
|
|
|
# inp file operations
|
|
@router.post("/uploadinp/", status_code=status.HTTP_200_OK, summary="上传 INP 文件", description="上传 INP 文件到服务器数据目录。")
|
|
async def fastapi_upload_inp(
|
|
afile: bytes = Body(..., description="文件二进制内容"),
|
|
name: str = Query(..., description="保存的文件名")
|
|
):
|
|
"""
|
|
上传 INP 文件
|
|
|
|
- **afile**: 文件内容
|
|
- **name**: 文件名
|
|
"""
|
|
if not os.path.exists(inpDir):
|
|
os.makedirs(inpDir, exist_ok=True)
|
|
|
|
filePath = inpDir + str(name)
|
|
with open(filePath, "wb") as f:
|
|
f.write(afile)
|
|
return True
|
|
|
|
@router.get("/downloadinp/", status_code=status.HTTP_200_OK, summary="下载 INP 文件", description="从服务器数据目录下载指定的 INP 文件。")
|
|
async def fastapi_download_inp(
|
|
name: str = Query(..., description="文件名"),
|
|
response: Response = None
|
|
):
|
|
"""
|
|
下载 INP 文件
|
|
|
|
- **name**: 文件名
|
|
"""
|
|
filePath = inpDir + name
|
|
if os.path.exists(filePath):
|
|
return FileResponse(
|
|
filePath, media_type="application/octet-stream", filename="inp.inp"
|
|
)
|
|
else:
|
|
response.status_code = status.HTTP_400_BAD_REQUEST
|
|
return True
|
|
|
|
# DingZQ, 2024-12-28, convert v3 to v2
|
|
@router.get("/convertv3tov2/", response_model=None, summary="转换 INP V3 为 V2", description="将 EPANET 3.0 格式的 INP 内容转换为 2.x 格式。")
|
|
async def fastapi_convert_v3_to_v2(
|
|
req: Request
|
|
) -> ChangeSet:
|
|
"""
|
|
转换 INP V3 为 V2
|
|
|
|
- **req**: 请求体,需包含 `{"inp": "..."}` 结构
|
|
"""
|
|
network = "v3Tov2"
|
|
jo_root = await req.json()
|
|
inp = jo_root["inp"]
|
|
cs = convert_inp_v3_to_v2(inp)
|
|
op = cs.operations[0]
|
|
open_project(network)
|
|
op["vertex"] = json.dumps(get_all_vertices(network))
|
|
op["scada"] = json.dumps(get_all_scada_elements(network))
|
|
op["dma"] = json.dumps(get_all_district_metering_areas(network))
|
|
op["sa"] = json.dumps(get_all_service_areas(network))
|
|
op["vd"] = json.dumps(get_all_virtual_districts(network))
|
|
op["legend"] = get_extension_data(network, "legend")
|
|
|
|
db = get_extension_data(network, "scada_db")
|
|
print(db)
|
|
scada_db = ""
|
|
if db:
|
|
scada_db = db
|
|
print(scada_db)
|
|
op["scada_db"] = scada_db
|
|
|
|
close_project(network)
|
|
|
|
return cs
|
|
|
|
@router.post("/readinp/", summary="读取 INP 文件到项目", description="从服务器文件系统中读取指定的 INP 文件并加载到项目中。")
|
|
async def read_inp_endpoint(
|
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
inp: str = Query(..., description="INP 文件名 (不包含路径)")
|
|
) -> bool:
|
|
"""
|
|
读取 INP 文件到项目
|
|
|
|
- **network**: 管网名称(或数据库名称)
|
|
- **inp**: INP 文件名
|
|
"""
|
|
read_inp(network, inp)
|
|
return True
|
|
|
|
@router.get("/dumpinp/", summary="导出项目到 INP 文件", description="将项目当前状态保存为 INP 文件到服务器文件系统。")
|
|
async def dump_inp_endpoint(
|
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
inp: str = Query(..., description="目标文件名")
|
|
) -> bool:
|
|
"""
|
|
导出项目到 INP 文件
|
|
|
|
- **network**: 管网名称(或数据库名称)
|
|
- **inp**: 目标文件名
|
|
"""
|
|
dump_inp(network, inp)
|
|
return True
|
|
|
|
@router.get("/isprojectlocked/", summary="检查项目是否被锁定", description="检查指定项目是否处于锁定状态。")
|
|
async def is_project_locked_endpoint(
|
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
req: Request = None
|
|
):
|
|
"""
|
|
检查项目是否被锁定
|
|
|
|
- **network**: 管网名称(或数据库名称)
|
|
"""
|
|
return network in lockedPrjs.keys()
|
|
|
|
@router.get("/isprojectlockedbyme/", summary="检查项目是否被当前用户锁定", description="检查指定项目是否被当前客户端 (IP) 锁定。")
|
|
async def is_project_locked_by_me_endpoint(
|
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
req: Request = None
|
|
):
|
|
"""
|
|
检查项目是否被当前用户锁定
|
|
|
|
- **network**: 管网名称(或数据库名称)
|
|
"""
|
|
client_host = req.client.host
|
|
return lockedPrjs.get(network) == client_host
|
|
|
|
# 0 successfully locked
|
|
# 1 already locked by you
|
|
# 2 locked by others
|
|
@router.post("/lockproject/", summary="锁定项目", description="锁定指定项目以防止并发修改。")
|
|
async def lock_project_endpoint(
|
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
req: Request = None
|
|
):
|
|
"""
|
|
锁定项目
|
|
|
|
返回值:
|
|
- **0**: 锁定成功
|
|
- **1**: 已被当前用户锁定
|
|
- **2**: 已被其他用户锁定
|
|
"""
|
|
client_host = req.client.host
|
|
if not network in lockedPrjs.keys():
|
|
lockedPrjs[network] = client_host
|
|
return 0
|
|
else:
|
|
if lockedPrjs.get(network) == client_host:
|
|
return 1
|
|
else:
|
|
return 2
|
|
|
|
@router.post("/unlockproject/", summary="解锁项目", description="释放对项目的锁定。")
|
|
def unlock_project_endpoint(
|
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
req: Request = None
|
|
):
|
|
"""
|
|
解锁项目
|
|
|
|
只有锁定者才能解锁。
|
|
"""
|
|
client_host = req.client.host
|
|
if lockedPrjs.get(network) == client_host:
|
|
print("delete key")
|
|
del lockedPrjs[network]
|
|
return True
|
|
|
|
return False
|
|
|
|
# inp file operations
|
|
@router.post("/uploadinp/", status_code=status.HTTP_200_OK, summary="上传 INP 文件", description="上传 INP 文件到服务器数据目录。")
|
|
async def fastapi_upload_inp(
|
|
afile: bytes = Body(..., description="文件二进制内容"),
|
|
name: str = Query(..., description="保存的文件名")
|
|
):
|
|
"""
|
|
上传 INP 文件
|
|
|
|
- **afile**: 文件内容
|
|
- **name**: 文件名
|
|
"""
|
|
if not os.path.exists(inpDir):
|
|
os.makedirs(inpDir, exist_ok=True)
|
|
|
|
filePath = inpDir + str(name)
|
|
with open(filePath, "wb") as f:
|
|
f.write(afile)
|
|
return True
|
|
|
|
@router.get("/downloadinp/", status_code=status.HTTP_200_OK, summary="下载 INP 文件", description="从服务器数据目录下载指定的 INP 文件。")
|
|
async def fastapi_download_inp(
|
|
name: str = Query(..., description="文件名"),
|
|
response: Response = None
|
|
):
|
|
"""
|
|
下载 INP 文件
|
|
|
|
- **name**: 文件名
|
|
"""
|
|
filePath = inpDir + name
|
|
if os.path.exists(filePath):
|
|
return FileResponse(
|
|
filePath, media_type="application/octet-stream", filename="inp.inp"
|
|
)
|
|
else:
|
|
response.status_code = status.HTTP_400_BAD_REQUEST
|
|
return True
|
|
|
|
# DingZQ, 2024-12-28, convert v3 to v2
|
|
@router.get("/convertv3tov2/", response_model=None, summary="转换 INP V3 为 V2", description="将 EPANET 3.0 格式的 INP 内容转换为 2.x 格式。")
|
|
async def fastapi_convert_v3_to_v2(
|
|
req: Request
|
|
) -> ChangeSet:
|
|
"""
|
|
转换 INP V3 为 V2
|
|
|
|
- **req**: 请求体,需包含 `{"inp": "..."}` 结构
|
|
"""
|
|
network = "v3Tov2"
|
|
jo_root = await req.json()
|
|
inp = jo_root["inp"]
|
|
cs = convert_inp_v3_to_v2(inp)
|
|
op = cs.operations[0]
|
|
open_project(network)
|
|
op["vertex"] = json.dumps(get_all_vertices(network))
|
|
op["scada"] = json.dumps(get_all_scada_elements(network))
|
|
op["dma"] = json.dumps(get_all_district_metering_areas(network))
|
|
op["sa"] = json.dumps(get_all_service_areas(network))
|
|
op["vd"] = json.dumps(get_all_virtual_districts(network))
|
|
op["legend"] = get_extension_data(network, "legend")
|
|
|
|
db = get_extension_data(network, "scada_db")
|
|
print(db)
|
|
scada_db = ""
|
|
if db:
|
|
scada_db = db
|
|
print(scada_db)
|
|
op["scada_db"] = scada_db
|
|
|
|
close_project(network)
|
|
|
|
return cs
|