import json from fastapi import APIRouter, Request, HTTPException, Query, Path, Body from fastapi.responses import PlainTextResponse from typing import Any, Dict, List 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("/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