Compare commits
16
Commits
18253f2fe0
...
8201448029
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8201448029 | ||
|
|
b383cf7a4b | ||
|
|
78135c8a50 | ||
|
|
41bf5e9d92 | ||
|
|
4ced14ee04 | ||
|
|
d7cd4fe326 | ||
|
|
a018850257 | ||
|
|
c7799e951a | ||
|
|
3dfb9cd56f | ||
|
|
91f25c8747 | ||
|
|
9bff6a556a | ||
|
|
cad8527ab6 | ||
|
|
5af2571b48 | ||
|
|
6a85a7fd78 | ||
|
|
a475c7b2c3 | ||
|
|
d3eea23e27 |
@@ -45,6 +45,7 @@ METADATA_DB_PASSWORD="password"
|
|||||||
KEYCLOAK_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"
|
KEYCLOAK_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"
|
||||||
KEYCLOAK_ALGORITHM=RS256
|
KEYCLOAK_ALGORITHM=RS256
|
||||||
KEYCLOAK_AUDIENCE="account"
|
KEYCLOAK_AUDIENCE="account"
|
||||||
|
KEYCLOAK_ACCESS_TOKEN_MAX_AGE_SECONDS=900
|
||||||
|
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|||||||
@@ -13,10 +13,29 @@ TJWater metadata stores only business snapshots and authorization data:
|
|||||||
The backend does not accept passwords, does not issue local JWTs, and does not
|
The backend does not accept passwords, does not issue local JWTs, and does not
|
||||||
trust frontend-supplied user IDs.
|
trust frontend-supplied user IDs.
|
||||||
|
|
||||||
|
## Fixed Project RBAC
|
||||||
|
|
||||||
|
Project roles are stored directly in
|
||||||
|
`user_project_membership.project_role`; there is no separate role table or
|
||||||
|
user-defined permission editor in this delivery.
|
||||||
|
|
||||||
|
| Role | Main access |
|
||||||
|
| --- | --- |
|
||||||
|
| `modeler` | Model upload/import, simulation, burst, risk, and optimization analysis |
|
||||||
|
| `dispatcher` | SCADA cleaning, simulation and burst analysis |
|
||||||
|
| `auditor` | Project read access and project-scoped audit logs |
|
||||||
|
| `viewer` | WebGIS and read-only risk results |
|
||||||
|
|
||||||
|
Legacy `owner`, `admin`, and `member` values remain supported for existing
|
||||||
|
records. The backend is the authorization boundary; the frontend uses
|
||||||
|
`GET /api/v1/access/context` only to hide unavailable menus and guard routes.
|
||||||
|
System admins receive environment, membership, and global-audit permissions,
|
||||||
|
but still need a project membership for project business APIs.
|
||||||
|
|
||||||
## Login Snapshot Refresh
|
## Login Snapshot Refresh
|
||||||
|
|
||||||
Every authenticated metadata-user resolution validates the Keycloak access token
|
Every authenticated metadata-user resolution validates the Keycloak access token
|
||||||
and reads `sub`, `preferred_username` or `username`, and `email` claims. The
|
and reads `sub`, `preferred_username`, and `email` claims. The
|
||||||
backend finds `users` by `keycloak_id = sub`, rejects inactive or missing users,
|
backend finds `users` by `keycloak_id = sub`, rejects inactive or missing users,
|
||||||
then refreshes `username`, `email`, and `last_login_at`.
|
then refreshes `username`, `email`, and `last_login_at`.
|
||||||
|
|
||||||
@@ -71,20 +90,15 @@ python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().d
|
|||||||
Keep keys stable for the lifetime of encrypted metadata. Rotating a key requires
|
Keep keys stable for the lifetime of encrypted metadata. Rotating a key requires
|
||||||
decrypting with the old key and re-encrypting with the new key.
|
decrypting with the old key and re-encrypting with the new key.
|
||||||
|
|
||||||
## Metadata Schema Patches
|
|
||||||
|
|
||||||
Apply metadata patches in order:
|
|
||||||
|
|
||||||
1. `resources/sql/004_metadata_auth_management.sql`
|
|
||||||
2. `resources/sql/005_metadata_project_configuration.sql`
|
|
||||||
|
|
||||||
`004` creates Keycloak-backed metadata users and project memberships. `005`
|
|
||||||
creates project and project database routing tables with uniqueness, role/type,
|
|
||||||
and pool-size constraints.
|
|
||||||
|
|
||||||
## Frontend System Management
|
## Frontend System Management
|
||||||
|
|
||||||
`/system-admin` is shown only after `GET /api/v1/admin/me` confirms metadata
|
`/system-admin` is shown only when `GET /api/v1/access/context` returns
|
||||||
admin access. The page lets admins maintain metadata users, project members,
|
`environment.manage`. The page lets admins maintain metadata users, project
|
||||||
projects, project database routing for `biz_data` and `iot_data`, connection
|
members, projects, project database routing for `biz_data` and `iot_data`, and
|
||||||
health checks. This replaces direct SQL editing for normal project onboarding.
|
connection health checks. This replaces direct SQL editing for normal project
|
||||||
|
onboarding.
|
||||||
|
|
||||||
|
Hydraulic model authoring is outside the Web application. Models are prepared
|
||||||
|
in the desktop modeling client and uploaded/imported by an authorized modeler;
|
||||||
|
the system administrator configures the project environment and database
|
||||||
|
routing.
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ Headers keep standard HTTP casing:
|
|||||||
|
|
||||||
## Internal vs Customer Difference
|
## Internal vs Customer Difference
|
||||||
|
|
||||||
The customer backend retains local auth/user-management routes under `/api/v1/auth` and `/api/v1/users`; the internal backend has migrated to Keycloak/metadata admin routes. Treat those Customer-only auth routes as delivery compatibility surface, not a source for new internal API naming.
|
Both backend editions use Keycloak-backed metadata users and expose user administration through `/api/v1/admin/users`. Project-local user routes such as `/api/v1/users` are no longer part of the public contract.
|
||||||
|
|
||||||
## Recommendation
|
## Recommendation
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ FROM condaforge/miniforge3:latest AS runtime-base
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 使用清华大学开源软件镜像站加速 Conda/Mamba 包下载。
|
||||||
|
RUN conda config --system --remove-key channels || true && \
|
||||||
|
conda config --system --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/ && \
|
||||||
|
conda config --system --set show_channel_urls yes
|
||||||
|
|
||||||
ENV PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple \
|
ENV PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple \
|
||||||
PIP_TRUSTED_HOST=pypi.tuna.tsinghua.edu.cn \
|
PIP_TRUSTED_HOST=pypi.tuna.tsinghua.edu.cn \
|
||||||
UV_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple
|
UV_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple
|
||||||
|
|||||||
@@ -149,14 +149,16 @@ def valve_isolation_analysis(
|
|||||||
|
|
||||||
must_close_valves.sort()
|
must_close_valves.sort()
|
||||||
optional_valves.sort()
|
optional_valves.sort()
|
||||||
|
isolatable = bool(must_close_valves)
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
"accident_elements": target_elements,
|
"accident_elements": target_elements,
|
||||||
"disabled_valves": disabled_valves,
|
"disabled_valves": disabled_valves,
|
||||||
"affected_nodes": sorted(affected_nodes),
|
"affected_nodes": sorted(affected_nodes) if isolatable else [],
|
||||||
|
"affected_node_count": len(affected_nodes),
|
||||||
"must_close_valves": must_close_valves,
|
"must_close_valves": must_close_valves,
|
||||||
"optional_valves": optional_valves,
|
"optional_valves": optional_valves,
|
||||||
"isolatable": len(must_close_valves) > 0,
|
"isolatable": isolatable,
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(target_elements) == 1:
|
if len(target_elements) == 1:
|
||||||
|
|||||||
@@ -1,14 +1,77 @@
|
|||||||
import psycopg
|
from contextlib import contextmanager
|
||||||
|
import fcntl
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from app.algorithms.sensor import kmeans as kmeans_sensor
|
from app.algorithms.sensor import kmeans as kmeans_sensor
|
||||||
from app.algorithms.sensor import sensitivity
|
from app.algorithms.sensor import sensitivity
|
||||||
from app.core.config import get_pgconn_string
|
from app.native.wndb.s42_sensor_placement import create_sensor_placement
|
||||||
|
from app.services.sensor_placement import (
|
||||||
|
SensorPlacementConflictError,
|
||||||
|
SensorPlacementValidationError,
|
||||||
|
validate_sensor_placement_nodes,
|
||||||
|
)
|
||||||
from app.services.tjnetwork import dump_inp
|
from app.services.tjnetwork import dump_inp
|
||||||
|
|
||||||
|
|
||||||
|
def _sensor_inp_path(name: str) -> Path:
|
||||||
|
if (
|
||||||
|
not name
|
||||||
|
or name in {".", ".."}
|
||||||
|
or "/" in name
|
||||||
|
or "\\" in name
|
||||||
|
or "\x00" in name
|
||||||
|
):
|
||||||
|
raise SensorPlacementValidationError("管网名称不是有效的项目标识")
|
||||||
|
return Path("db_inp") / f"{name}.db.inp"
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def _sensor_inp_lock(name: str):
|
||||||
|
inp_path = _sensor_inp_path(name)
|
||||||
|
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(
|
||||||
|
name: str,
|
||||||
|
*,
|
||||||
|
scheme_name: str,
|
||||||
|
min_diameter: int,
|
||||||
|
username: str,
|
||||||
|
sensor_location: list[str],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
validate_sensor_placement_nodes(name, sensor_location)
|
||||||
|
return create_sensor_placement(
|
||||||
|
name,
|
||||||
|
scheme_name=scheme_name,
|
||||||
|
min_diameter=min_diameter,
|
||||||
|
username=username,
|
||||||
|
sensor_location=sensor_location,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def pressure_sensor_placement_sensitivity(
|
def pressure_sensor_placement_sensitivity(
|
||||||
name: str, scheme_name: str, sensor_number: int, min_diameter: int, username: str
|
name: str,
|
||||||
) -> None:
|
scheme_name: str,
|
||||||
|
sensor_number: int,
|
||||||
|
min_diameter: int,
|
||||||
|
username: str,
|
||||||
|
) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
基于改进灵敏度法进行压力监测点优化布置
|
基于改进灵敏度法进行压力监测点优化布置
|
||||||
:param name: 数据库名称
|
:param name: 数据库名称
|
||||||
@@ -16,41 +79,32 @@ def pressure_sensor_placement_sensitivity(
|
|||||||
:param sensor_number: 传感器数目
|
:param sensor_number: 传感器数目
|
||||||
:param min_diameter: 最小管径
|
:param min_diameter: 最小管径
|
||||||
:param username: 用户名
|
:param username: 用户名
|
||||||
:return:
|
:return: 新建的监测点方案
|
||||||
"""
|
"""
|
||||||
sensor_location = sensitivity.get_ID(
|
with _sensor_inp_lock(name):
|
||||||
name=name, sensor_num=sensor_number, min_diameter=min_diameter
|
sensor_location = sensitivity.get_ID(
|
||||||
|
name=name,
|
||||||
|
sensor_num=sensor_number,
|
||||||
|
min_diameter=min_diameter,
|
||||||
|
)
|
||||||
|
return _create_validated_placement(
|
||||||
|
name,
|
||||||
|
scheme_name=scheme_name,
|
||||||
|
min_diameter=min_diameter,
|
||||||
|
username=username,
|
||||||
|
sensor_location=sensor_location,
|
||||||
)
|
)
|
||||||
try:
|
|
||||||
conn_string = get_pgconn_string(db_name=name)
|
|
||||||
with psycopg.connect(conn_string) as conn:
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
sql = """
|
|
||||||
INSERT INTO sensor_placement (scheme_name, sensor_number, min_diameter, username, sensor_location)
|
|
||||||
VALUES (%s, %s, %s, %s, %s)
|
|
||||||
"""
|
|
||||||
|
|
||||||
cur.execute(
|
|
||||||
sql,
|
|
||||||
(
|
|
||||||
scheme_name,
|
|
||||||
sensor_number,
|
|
||||||
min_diameter,
|
|
||||||
username,
|
|
||||||
sensor_location,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
conn.commit()
|
|
||||||
print("方案信息存储成功!")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"存储方案信息时出错:{e}")
|
|
||||||
|
|
||||||
|
|
||||||
# 2025/08/21
|
# 2025/08/21
|
||||||
# 基于kmeans聚类法进行压力监测点优化布置
|
# 基于kmeans聚类法进行压力监测点优化布置
|
||||||
def pressure_sensor_placement_kmeans(
|
def pressure_sensor_placement_kmeans(
|
||||||
name: str, scheme_name: str, sensor_number: int, min_diameter: int, username: str
|
name: str,
|
||||||
) -> None:
|
scheme_name: str,
|
||||||
|
sensor_number: int,
|
||||||
|
min_diameter: int,
|
||||||
|
username: str,
|
||||||
|
) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
基于聚类法进行压力监测点优化布置
|
基于聚类法进行压力监测点优化布置
|
||||||
:param name: 数据库名称(注意,此处数据库名称也是inp文件名称,inp文件与pg库名要一样)
|
:param name: 数据库名称(注意,此处数据库名称也是inp文件名称,inp文件与pg库名要一样)
|
||||||
@@ -58,34 +112,20 @@ def pressure_sensor_placement_kmeans(
|
|||||||
:param sensor_number: 传感器数目
|
:param sensor_number: 传感器数目
|
||||||
:param min_diameter: 最小管径
|
:param min_diameter: 最小管径
|
||||||
:param username: 用户名
|
:param username: 用户名
|
||||||
:return:
|
:return: 新建的监测点方案
|
||||||
"""
|
"""
|
||||||
# dump_inp
|
# dump_inp
|
||||||
inp_name = f"./db_inp/{name}.db.inp"
|
with _sensor_inp_lock(name) as inp_path:
|
||||||
dump_inp(name, inp_name, "2")
|
dump_inp(name, str(inp_path), "2")
|
||||||
sensor_location = kmeans_sensor.kmeans_sensor_placement(
|
sensor_location = kmeans_sensor.kmeans_sensor_placement(
|
||||||
name=name, sensor_num=sensor_number, min_diameter=min_diameter
|
name=name,
|
||||||
|
sensor_num=sensor_number,
|
||||||
|
min_diameter=min_diameter,
|
||||||
|
)
|
||||||
|
return _create_validated_placement(
|
||||||
|
name,
|
||||||
|
scheme_name=scheme_name,
|
||||||
|
min_diameter=min_diameter,
|
||||||
|
username=username,
|
||||||
|
sensor_location=sensor_location,
|
||||||
)
|
)
|
||||||
try:
|
|
||||||
conn_string = get_pgconn_string(db_name=name)
|
|
||||||
with psycopg.connect(conn_string) as conn:
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
sql = """
|
|
||||||
INSERT INTO sensor_placement (scheme_name, sensor_number, min_diameter, username, sensor_location)
|
|
||||||
VALUES (%s, %s, %s, %s, %s)
|
|
||||||
"""
|
|
||||||
|
|
||||||
cur.execute(
|
|
||||||
sql,
|
|
||||||
(
|
|
||||||
scheme_name,
|
|
||||||
sensor_number,
|
|
||||||
min_diameter,
|
|
||||||
username,
|
|
||||||
sensor_location,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
conn.commit()
|
|
||||||
print("方案信息存储成功!")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"存储方案信息时出错:{e}")
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,89 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from fastapi import FastAPI, HTTPException, Request
|
||||||
|
from fastapi.exceptions import RequestValidationError
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
class ProblemDetails(BaseModel):
|
||||||
|
"""RFC 9457 compatible error response used by the REST contract."""
|
||||||
|
|
||||||
|
type: str
|
||||||
|
title: str
|
||||||
|
status: int
|
||||||
|
detail: str
|
||||||
|
instance: str
|
||||||
|
code: str
|
||||||
|
trace_id: str
|
||||||
|
errors: list[dict[str, Any]] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
def _trace_id(request: Request) -> str:
|
||||||
|
return request.headers.get("X-Request-Id") or str(uuid4())
|
||||||
|
|
||||||
|
|
||||||
|
def _problem_response(
|
||||||
|
request: Request,
|
||||||
|
*,
|
||||||
|
status_code: int,
|
||||||
|
title: str,
|
||||||
|
detail: str,
|
||||||
|
code: str,
|
||||||
|
errors: list[dict[str, Any]] | None = None,
|
||||||
|
) -> JSONResponse:
|
||||||
|
problem = ProblemDetails(
|
||||||
|
type=f"https://tjwater.example/problems/{code.replace('_', '-')}",
|
||||||
|
title=title,
|
||||||
|
status=status_code,
|
||||||
|
detail=detail,
|
||||||
|
instance=request.url.path,
|
||||||
|
code=code,
|
||||||
|
trace_id=_trace_id(request),
|
||||||
|
errors=errors or [],
|
||||||
|
)
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=status_code,
|
||||||
|
content=problem.model_dump(mode="json"),
|
||||||
|
media_type="application/problem+json",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def install_problem_details_handlers(app: FastAPI) -> None:
|
||||||
|
@app.exception_handler(RequestValidationError)
|
||||||
|
async def validation_error_handler(
|
||||||
|
request: Request,
|
||||||
|
exc: RequestValidationError,
|
||||||
|
) -> JSONResponse:
|
||||||
|
return _problem_response(
|
||||||
|
request,
|
||||||
|
status_code=422,
|
||||||
|
title="Validation error",
|
||||||
|
detail="Request validation failed",
|
||||||
|
code="validation_error",
|
||||||
|
errors=exc.errors(),
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.exception_handler(HTTPException)
|
||||||
|
async def http_error_handler(request: Request, exc: HTTPException) -> JSONResponse:
|
||||||
|
detail = exc.detail if isinstance(exc.detail, str) else str(exc.detail)
|
||||||
|
code_by_status = {
|
||||||
|
401: "unauthenticated",
|
||||||
|
403: "forbidden",
|
||||||
|
404: "not_found",
|
||||||
|
409: "conflict",
|
||||||
|
422: "validation_error",
|
||||||
|
503: "dependency_unavailable",
|
||||||
|
}
|
||||||
|
return _problem_response(
|
||||||
|
request,
|
||||||
|
status_code=exc.status_code,
|
||||||
|
title=code_by_status.get(exc.status_code, "request_error")
|
||||||
|
.replace("_", " ")
|
||||||
|
.title(),
|
||||||
|
detail=detail,
|
||||||
|
code=code_by_status.get(exc.status_code, "request_error"),
|
||||||
|
)
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
from fastapi import APIRouter, Depends, Header
|
||||||
|
|
||||||
|
from app.auth.metadata_dependencies import (
|
||||||
|
get_current_metadata_user,
|
||||||
|
get_metadata_repository,
|
||||||
|
)
|
||||||
|
from app.auth.permissions import resolve_permissions
|
||||||
|
from app.auth.project_dependencies import resolve_project_context
|
||||||
|
from app.domain.schemas.access import AccessContextResponse
|
||||||
|
from app.infra.db.metadb.repositories.metadata_repository import MetadataRepository
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/access-context", response_model=AccessContextResponse)
|
||||||
|
async def get_access_context(
|
||||||
|
x_project_id: str | None = Header(default=None, alias="X-Project-Id"),
|
||||||
|
current_user=Depends(get_current_metadata_user),
|
||||||
|
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
||||||
|
) -> AccessContextResponse:
|
||||||
|
project_context = (
|
||||||
|
await resolve_project_context(x_project_id, current_user, metadata_repo)
|
||||||
|
if x_project_id
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
permissions = resolve_permissions(
|
||||||
|
project_role=project_context.project_role if project_context else None,
|
||||||
|
system_role=current_user.role,
|
||||||
|
is_superuser=current_user.is_superuser,
|
||||||
|
)
|
||||||
|
return AccessContextResponse(
|
||||||
|
user_id=current_user.id,
|
||||||
|
username=current_user.username,
|
||||||
|
system_role=current_user.role,
|
||||||
|
is_system_admin=current_user.is_superuser or current_user.role == "admin",
|
||||||
|
project_id=project_context.project_id if project_context else None,
|
||||||
|
project_role=project_context.project_role if project_context else None,
|
||||||
|
permissions=sorted(permissions),
|
||||||
|
)
|
||||||
@@ -151,14 +151,14 @@ async def _upsert_and_audit_metadata_user(
|
|||||||
return MetadataUserResponse.model_validate(user)
|
return MetadataUserResponse.model_validate(user)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/me", response_model=MetadataUserResponse)
|
@router.get("/admin/users/me", response_model=MetadataUserResponse)
|
||||||
async def get_metadata_admin_me(
|
async def get_metadata_admin_me(
|
||||||
current_user=Depends(get_current_metadata_admin),
|
current_user=Depends(get_current_metadata_admin),
|
||||||
) -> MetadataUserResponse:
|
) -> MetadataUserResponse:
|
||||||
return MetadataUserResponse.model_validate(current_user)
|
return MetadataUserResponse.model_validate(current_user)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/users/sync", response_model=MetadataUserResponse)
|
@router.post("/admin/user-syncs", response_model=MetadataUserResponse)
|
||||||
async def sync_metadata_user(
|
async def sync_metadata_user(
|
||||||
payload: MetadataUserSyncRequest,
|
payload: MetadataUserSyncRequest,
|
||||||
current_user=Depends(get_current_metadata_admin),
|
current_user=Depends(get_current_metadata_admin),
|
||||||
@@ -184,7 +184,7 @@ async def sync_metadata_user(
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/users/sync/batch", response_model=List[MetadataUserSyncResult])
|
@router.post("/admin/user-syncs/batches", response_model=List[MetadataUserSyncResult])
|
||||||
async def sync_metadata_users_batch(
|
async def sync_metadata_users_batch(
|
||||||
payload: MetadataUsersBatchSyncRequest,
|
payload: MetadataUsersBatchSyncRequest,
|
||||||
current_user=Depends(get_current_metadata_admin),
|
current_user=Depends(get_current_metadata_admin),
|
||||||
@@ -228,7 +228,7 @@ async def sync_metadata_users_batch(
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
@router.get("/users", response_model=List[MetadataUserResponse])
|
@router.get("/admin/users", response_model=List[MetadataUserResponse])
|
||||||
async def list_metadata_users(
|
async def list_metadata_users(
|
||||||
skip: int = Query(0, ge=0),
|
skip: int = Query(0, ge=0),
|
||||||
limit: int = Query(100, ge=1, le=1000),
|
limit: int = Query(100, ge=1, le=1000),
|
||||||
@@ -239,7 +239,7 @@ async def list_metadata_users(
|
|||||||
return [MetadataUserResponse.model_validate(user) for user in users]
|
return [MetadataUserResponse.model_validate(user) for user in users]
|
||||||
|
|
||||||
|
|
||||||
@router.get("/projects", response_model=List[AdminProjectResponse])
|
@router.get("/admin/projects", response_model=List[AdminProjectResponse])
|
||||||
async def list_admin_projects(
|
async def list_admin_projects(
|
||||||
current_user=Depends(get_current_metadata_admin),
|
current_user=Depends(get_current_metadata_admin),
|
||||||
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
||||||
@@ -249,7 +249,7 @@ async def list_admin_projects(
|
|||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/projects",
|
"/admin/projects",
|
||||||
response_model=AdminProjectResponse,
|
response_model=AdminProjectResponse,
|
||||||
status_code=status.HTTP_201_CREATED,
|
status_code=status.HTTP_201_CREATED,
|
||||||
)
|
)
|
||||||
@@ -266,6 +266,7 @@ async def create_admin_project(
|
|||||||
gs_workspace=payload.gs_workspace,
|
gs_workspace=payload.gs_workspace,
|
||||||
map_extent=payload.map_extent,
|
map_extent=payload.map_extent,
|
||||||
status=payload.status,
|
status=payload.status,
|
||||||
|
creator_user_id=current_user.id,
|
||||||
)
|
)
|
||||||
except IntegrityError as exc:
|
except IntegrityError as exc:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -292,7 +293,7 @@ async def create_admin_project(
|
|||||||
|
|
||||||
|
|
||||||
@router.patch(
|
@router.patch(
|
||||||
"/projects/{project_id}",
|
"/admin/projects/{project_id}",
|
||||||
response_model=AdminProjectResponse,
|
response_model=AdminProjectResponse,
|
||||||
)
|
)
|
||||||
async def update_admin_project(
|
async def update_admin_project(
|
||||||
@@ -331,7 +332,7 @@ async def update_admin_project(
|
|||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/projects/{project_id}/databases",
|
"/admin/projects/{project_id}/databases",
|
||||||
response_model=List[ProjectDatabaseResponse],
|
response_model=List[ProjectDatabaseResponse],
|
||||||
)
|
)
|
||||||
async def list_project_databases(
|
async def list_project_databases(
|
||||||
@@ -347,7 +348,7 @@ async def list_project_databases(
|
|||||||
|
|
||||||
|
|
||||||
@router.put(
|
@router.put(
|
||||||
"/projects/{project_id}/databases",
|
"/admin/projects/{project_id}/databases",
|
||||||
response_model=ProjectDatabaseResponse,
|
response_model=ProjectDatabaseResponse,
|
||||||
)
|
)
|
||||||
async def upsert_project_database(
|
async def upsert_project_database(
|
||||||
@@ -420,7 +421,7 @@ async def upsert_project_database(
|
|||||||
|
|
||||||
|
|
||||||
@router.delete(
|
@router.delete(
|
||||||
"/projects/{project_id}/databases/{db_role}",
|
"/admin/projects/{project_id}/databases/{db_role}",
|
||||||
status_code=status.HTTP_204_NO_CONTENT,
|
status_code=status.HTTP_204_NO_CONTENT,
|
||||||
)
|
)
|
||||||
async def delete_project_database(
|
async def delete_project_database(
|
||||||
@@ -448,7 +449,7 @@ async def delete_project_database(
|
|||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/projects/{project_id}/databases/{db_role}/health",
|
"/admin/projects/{project_id}/databases/{db_role}/health-checks",
|
||||||
response_model=ProjectDatabaseHealthResponse,
|
response_model=ProjectDatabaseHealthResponse,
|
||||||
)
|
)
|
||||||
async def check_project_database_health(
|
async def check_project_database_health(
|
||||||
@@ -503,7 +504,7 @@ async def check_project_database_health(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/users/{user_id}", response_model=MetadataUserResponse)
|
@router.get("/admin/users/{user_id}", response_model=MetadataUserResponse)
|
||||||
async def get_metadata_user(
|
async def get_metadata_user(
|
||||||
user_id: UUID = Path(...),
|
user_id: UUID = Path(...),
|
||||||
current_user=Depends(get_current_metadata_admin),
|
current_user=Depends(get_current_metadata_admin),
|
||||||
@@ -515,7 +516,7 @@ async def get_metadata_user(
|
|||||||
return MetadataUserResponse.model_validate(user)
|
return MetadataUserResponse.model_validate(user)
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/users/{user_id}", response_model=MetadataUserResponse)
|
@router.patch("/admin/users/{user_id}", response_model=MetadataUserResponse)
|
||||||
async def update_metadata_user(
|
async def update_metadata_user(
|
||||||
payload: MetadataUserUpdateRequest,
|
payload: MetadataUserUpdateRequest,
|
||||||
user_id: UUID = Path(...),
|
user_id: UUID = Path(...),
|
||||||
@@ -548,7 +549,7 @@ async def update_metadata_user(
|
|||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/projects/{project_id}/members",
|
"/admin/projects/{project_id}/members",
|
||||||
response_model=List[ProjectMemberResponse],
|
response_model=List[ProjectMemberResponse],
|
||||||
)
|
)
|
||||||
async def list_project_members(
|
async def list_project_members(
|
||||||
@@ -566,7 +567,7 @@ async def list_project_members(
|
|||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/projects/{project_id}/members",
|
"/admin/projects/{project_id}/members",
|
||||||
response_model=ProjectMemberResponse,
|
response_model=ProjectMemberResponse,
|
||||||
status_code=status.HTTP_201_CREATED,
|
status_code=status.HTTP_201_CREATED,
|
||||||
)
|
)
|
||||||
@@ -621,7 +622,7 @@ async def add_project_member(
|
|||||||
|
|
||||||
|
|
||||||
@router.patch(
|
@router.patch(
|
||||||
"/projects/{project_id}/members/{user_id}",
|
"/admin/projects/{project_id}/members/{user_id}",
|
||||||
response_model=ProjectMemberResponse,
|
response_model=ProjectMemberResponse,
|
||||||
)
|
)
|
||||||
async def update_project_member(
|
async def update_project_member(
|
||||||
@@ -667,7 +668,7 @@ async def update_project_member(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/projects/{project_id}/members/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
|
@router.delete("/admin/projects/{project_id}/members/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
async def remove_project_member(
|
async def remove_project_member(
|
||||||
project_id: UUID = Path(...),
|
project_id: UUID = Path(...),
|
||||||
user_id: UUID = Path(...),
|
user_id: UUID = Path(...),
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from app.auth.project_dependencies import (
|
|||||||
ProjectContext,
|
ProjectContext,
|
||||||
get_project_context,
|
get_project_context,
|
||||||
)
|
)
|
||||||
|
from app.auth.permissions import permissions_for_context
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -22,10 +23,11 @@ class AgentAuthContextResponse(BaseModel):
|
|||||||
project_id: str
|
project_id: str
|
||||||
network: str
|
network: str
|
||||||
project_role: str
|
project_role: str
|
||||||
|
permissions: list[str]
|
||||||
token_expires_at: str | None = None
|
token_expires_at: str | None = None
|
||||||
|
|
||||||
|
|
||||||
@router.get("/agent/auth/context", response_model=AgentAuthContextResponse)
|
@router.get("/agent-auth-context", response_model=AgentAuthContextResponse)
|
||||||
async def get_agent_auth_context(
|
async def get_agent_auth_context(
|
||||||
ctx: ProjectContext = Depends(get_project_context),
|
ctx: ProjectContext = Depends(get_project_context),
|
||||||
current_user=Depends(get_current_metadata_user),
|
current_user=Depends(get_current_metadata_user),
|
||||||
@@ -46,5 +48,6 @@ async def get_agent_auth_context(
|
|||||||
project_id=str(ctx.project_id),
|
project_id=str(ctx.project_id),
|
||||||
network=ctx.project_code,
|
network=ctx.project_code,
|
||||||
project_role=ctx.project_role,
|
project_role=ctx.project_role,
|
||||||
|
permissions=sorted(permissions_for_context(ctx)),
|
||||||
token_expires_at=token_expires_at,
|
token_expires_at=token_expires_at,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,56 +1,52 @@
|
|||||||
"""
|
|
||||||
审计日志 API 接口
|
|
||||||
|
|
||||||
仅管理员可访问
|
|
||||||
"""
|
|
||||||
|
|
||||||
from typing import List, Optional
|
|
||||||
from uuid import UUID
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from fastapi import APIRouter, Depends, Query, Path
|
from typing import Literal
|
||||||
from app.domain.schemas.audit import AuditLogResponse
|
from uuid import UUID
|
||||||
from app.infra.db.metadb.repositories.audit_repository import AuditRepository
|
|
||||||
|
from fastapi import APIRouter, Depends, Query, Request, status
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.auth.metadata_dependencies import (
|
from app.auth.metadata_dependencies import (
|
||||||
get_current_metadata_admin,
|
get_current_metadata_admin,
|
||||||
get_current_metadata_user,
|
get_current_metadata_user,
|
||||||
)
|
)
|
||||||
|
from app.core.audit import AuditAction, log_audit_event
|
||||||
|
from app.domain.schemas.audit import AuditLogResponse
|
||||||
from app.infra.db.metadb.database import get_metadata_session
|
from app.infra.db.metadb.database import get_metadata_session
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from app.infra.db.metadb.repositories.audit_repository import AuditRepository
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
class SessionAuditEventRequest(BaseModel):
|
||||||
|
event: Literal["login", "logout"]
|
||||||
|
|
||||||
|
|
||||||
async def get_audit_repository(
|
async def get_audit_repository(
|
||||||
session: AsyncSession = Depends(get_metadata_session),
|
session: AsyncSession = Depends(get_metadata_session),
|
||||||
) -> AuditRepository:
|
) -> AuditRepository:
|
||||||
"""获取审计日志仓储"""
|
|
||||||
return AuditRepository(session)
|
return AuditRepository(session)
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/logs",
|
"/audit-logs",
|
||||||
summary="查询审计日志",
|
summary="查询审计日志",
|
||||||
description="查询审计日志(仅管理员)",
|
description="查询审计日志(仅管理员)",
|
||||||
response_model=List[AuditLogResponse],
|
response_model=list[AuditLogResponse],
|
||||||
)
|
)
|
||||||
async def get_audit_logs(
|
async def get_audit_logs(
|
||||||
user_id: Optional[UUID] = Query(None, description="按用户ID过滤"),
|
user_id: UUID | None = Query(None, description="按用户ID过滤"),
|
||||||
project_id: Optional[UUID] = Query(None, description="按项目ID过滤"),
|
project_id: UUID | None = Query(None, description="按项目ID过滤"),
|
||||||
action: Optional[str] = Query(None, description="按操作类型过滤"),
|
action: str | None = Query(None, description="按操作类型过滤"),
|
||||||
resource_type: Optional[str] = Query(None, description="按资源类型过滤"),
|
resource_type: str | None = Query(None, description="按资源类型过滤"),
|
||||||
start_time: Optional[datetime] = Query(None, description="开始时间"),
|
start_time: datetime | None = Query(None, description="开始时间"),
|
||||||
end_time: Optional[datetime] = Query(None, description="结束时间"),
|
end_time: datetime | None = Query(None, description="结束时间"),
|
||||||
skip: int = Query(0, ge=0, description="跳过记录数"),
|
skip: int = Query(0, ge=0, description="跳过记录数"),
|
||||||
limit: int = Query(100, ge=1, le=1000, description="限制记录数"),
|
limit: int = Query(100, ge=1, le=1000, description="限制记录数"),
|
||||||
current_user=Depends(get_current_metadata_admin),
|
_current_user=Depends(get_current_metadata_admin),
|
||||||
audit_repo: AuditRepository = Depends(get_audit_repository),
|
audit_repo: AuditRepository = Depends(get_audit_repository),
|
||||||
) -> List[AuditLogResponse]:
|
) -> list[AuditLogResponse]:
|
||||||
"""
|
return await audit_repo.get_logs(
|
||||||
查询审计日志
|
|
||||||
|
|
||||||
支持按用户、时间、操作类型等条件过滤,仅管理员可访问
|
|
||||||
"""
|
|
||||||
logs = await audit_repo.get_logs(
|
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
action=action,
|
action=action,
|
||||||
@@ -60,29 +56,23 @@ async def get_audit_logs(
|
|||||||
skip=skip,
|
skip=skip,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
)
|
)
|
||||||
return logs
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/logs/count",
|
"/audit-logs/count",
|
||||||
summary="获取审计日志总数",
|
summary="获取审计日志总数",
|
||||||
description="获取审计日志总数(仅管理员)",
|
description="获取审计日志总数(仅管理员)",
|
||||||
)
|
)
|
||||||
async def get_audit_logs_count(
|
async def get_audit_logs_count(
|
||||||
user_id: Optional[UUID] = Query(None, description="按用户ID过滤"),
|
user_id: UUID | None = Query(None, description="按用户ID过滤"),
|
||||||
project_id: Optional[UUID] = Query(None, description="按项目ID过滤"),
|
project_id: UUID | None = Query(None, description="按项目ID过滤"),
|
||||||
action: Optional[str] = Query(None, description="按操作类型过滤"),
|
action: str | None = Query(None, description="按操作类型过滤"),
|
||||||
resource_type: Optional[str] = Query(None, description="按资源类型过滤"),
|
resource_type: str | None = Query(None, description="按资源类型过滤"),
|
||||||
start_time: Optional[datetime] = Query(None, description="开始时间"),
|
start_time: datetime | None = Query(None, description="开始时间"),
|
||||||
end_time: Optional[datetime] = Query(None, description="结束时间"),
|
end_time: datetime | None = Query(None, description="结束时间"),
|
||||||
current_user=Depends(get_current_metadata_admin),
|
_current_user=Depends(get_current_metadata_admin),
|
||||||
audit_repo: AuditRepository = Depends(get_audit_repository),
|
audit_repo: AuditRepository = Depends(get_audit_repository),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""
|
|
||||||
获取审计日志总数
|
|
||||||
|
|
||||||
获取符合条件的审计日志的总数,仅管理员可访问
|
|
||||||
"""
|
|
||||||
count = await audit_repo.get_log_count(
|
count = await audit_repo.get_log_count(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
@@ -94,27 +84,42 @@ async def get_audit_logs_count(
|
|||||||
return {"count": count}
|
return {"count": count}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/audit-events", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
|
async def record_session_event(
|
||||||
|
payload: SessionAuditEventRequest,
|
||||||
|
request: Request,
|
||||||
|
current_user=Depends(get_current_metadata_user),
|
||||||
|
session: AsyncSession = Depends(get_metadata_session),
|
||||||
|
) -> None:
|
||||||
|
await log_audit_event(
|
||||||
|
action=AuditAction.LOGIN if payload.event == "login" else AuditAction.LOGOUT,
|
||||||
|
user_id=current_user.id,
|
||||||
|
resource_type="session",
|
||||||
|
resource_id=str(current_user.keycloak_id),
|
||||||
|
ip_address=request.client.host if request.client else None,
|
||||||
|
request_method=request.method,
|
||||||
|
request_path=request.url.path,
|
||||||
|
response_status=status.HTTP_204_NO_CONTENT,
|
||||||
|
session=session,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/logs/my",
|
"/audit-logs/mine",
|
||||||
summary="查询我的审计日志",
|
summary="查询我的审计日志",
|
||||||
description="查询当前用户的审计日志",
|
description="查询当前用户的审计日志",
|
||||||
response_model=List[AuditLogResponse],
|
response_model=list[AuditLogResponse],
|
||||||
)
|
)
|
||||||
async def get_my_audit_logs(
|
async def get_my_audit_logs(
|
||||||
action: Optional[str] = Query(None, description="按操作类型过滤"),
|
action: str | None = Query(None, description="按操作类型过滤"),
|
||||||
start_time: Optional[datetime] = Query(None, description="开始时间"),
|
start_time: datetime | None = Query(None, description="开始时间"),
|
||||||
end_time: Optional[datetime] = Query(None, description="结束时间"),
|
end_time: datetime | None = Query(None, description="结束时间"),
|
||||||
skip: int = Query(0, ge=0, description="跳过记录数"),
|
skip: int = Query(0, ge=0, description="跳过记录数"),
|
||||||
limit: int = Query(100, ge=1, le=1000, description="限制记录数"),
|
limit: int = Query(100, ge=1, le=1000, description="限制记录数"),
|
||||||
current_user=Depends(get_current_metadata_user),
|
current_user=Depends(get_current_metadata_user),
|
||||||
audit_repo: AuditRepository = Depends(get_audit_repository),
|
audit_repo: AuditRepository = Depends(get_audit_repository),
|
||||||
) -> List[AuditLogResponse]:
|
) -> list[AuditLogResponse]:
|
||||||
"""
|
return await audit_repo.get_logs(
|
||||||
查询当前用户的审计日志
|
|
||||||
|
|
||||||
普通用户只能查看自己的操作记录
|
|
||||||
"""
|
|
||||||
logs = await audit_repo.get_logs(
|
|
||||||
user_id=current_user.id,
|
user_id=current_user.id,
|
||||||
action=action,
|
action=action,
|
||||||
start_time=start_time,
|
start_time=start_time,
|
||||||
@@ -122,4 +127,3 @@ async def get_my_audit_logs(
|
|||||||
skip=skip,
|
skip=skip,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
)
|
)
|
||||||
return logs
|
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query, Path, Body
|
from fastapi import APIRouter, Depends, HTTPException, Body
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from app.auth.keycloak_dependencies import get_current_keycloak_username
|
from app.auth.keycloak_dependencies import get_current_keycloak_username
|
||||||
from app.services.burst_detection import (
|
from app.services.burst_detection import (
|
||||||
get_burst_detection_scheme_detail,
|
|
||||||
list_burst_detection_schemes,
|
|
||||||
run_burst_detection,
|
run_burst_detection,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -30,6 +28,16 @@ class BurstDetectionRequest(BaseModel):
|
|||||||
points_per_day: int = Field(1440, description="每天的数据点数")
|
points_per_day: int = Field(1440, description="每天的数据点数")
|
||||||
mu: int = Field(100, description="异常值检测的参数")
|
mu: int = Field(100, description="异常值检测的参数")
|
||||||
iforest_params: dict[str, Any] | None = Field(None, description="隔离森林算法参数")
|
iforest_params: dict[str, Any] | None = Field(None, description="隔离森林算法参数")
|
||||||
|
target_time: datetime | None = Field(
|
||||||
|
None,
|
||||||
|
description="目标侦测时刻;为空时自动使用最近一个完整的监测时刻",
|
||||||
|
)
|
||||||
|
sampling_interval_minutes: int | None = Field(
|
||||||
|
None,
|
||||||
|
ge=1,
|
||||||
|
le=1440,
|
||||||
|
description="采样间隔(分钟);为空时根据压力 SCADA 传输频率自动推断",
|
||||||
|
)
|
||||||
scada_start: datetime | None = Field(None, description="SCADA数据起始时间")
|
scada_start: datetime | None = Field(None, description="SCADA数据起始时间")
|
||||||
scada_end: datetime | None = Field(None, description="SCADA数据结束时间")
|
scada_end: datetime | None = Field(None, description="SCADA数据结束时间")
|
||||||
sensor_nodes: list[str] | None = Field(None, description="传感器节点列表")
|
sensor_nodes: list[str] | None = Field(None, description="传感器节点列表")
|
||||||
@@ -40,7 +48,7 @@ class BurstDetectionRequest(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/detect/",
|
"/burst-detections",
|
||||||
summary="执行爆管检测",
|
summary="执行爆管检测",
|
||||||
description="基于压力观测数据和其他参数执行爆管检测分析"
|
description="基于压力观测数据和其他参数执行爆管检测分析"
|
||||||
)
|
)
|
||||||
@@ -68,64 +76,3 @@ async def detect_burst(
|
|||||||
return run_burst_detection(**data.model_dump(), username=username)
|
return run_burst_detection(**data.model_dump(), username=username)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise HTTPException(status_code=400, detail=str(exc))
|
raise HTTPException(status_code=400, detail=str(exc))
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/schemes/",
|
|
||||||
summary="查询爆管检测方案列表",
|
|
||||||
description="获取指定网络的所有爆管检测方案"
|
|
||||||
)
|
|
||||||
async def query_burst_detection_schemes(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
query_date: datetime | None = Query(None, description="查询日期(可选)"),
|
|
||||||
) -> list[dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
获取爆管检测方案列表。
|
|
||||||
|
|
||||||
查询指定网络的所有已配置的爆管检测方案,
|
|
||||||
可按日期进行筛选。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
network: 管网名称(或数据库名称)
|
|
||||||
query_date: 查询日期(可选)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
爆管检测方案列表
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
HTTPException: 当查询失败时
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return list_burst_detection_schemes(network=network, query_date=query_date)
|
|
||||||
except Exception as exc:
|
|
||||||
raise HTTPException(status_code=400, detail=str(exc))
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/schemes/{scheme_name}",
|
|
||||||
summary="获取爆管检测方案详情",
|
|
||||||
description="获取指定爆管检测方案的详细信息"
|
|
||||||
)
|
|
||||||
async def query_burst_detection_scheme_detail(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
scheme_name: str = Path(..., description="爆管检测方案名称"),
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
获取爆管检测方案详情。
|
|
||||||
|
|
||||||
查询指定爆管检测方案的完整配置和参数信息。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
network: 管网名称(或数据库名称)
|
|
||||||
scheme_name: 爆管检测方案名称
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
包含方案详情的字典
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
HTTPException: 当查询失败时
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return get_burst_detection_scheme_detail(network=network, scheme_name=scheme_name)
|
|
||||||
except Exception as exc:
|
|
||||||
raise HTTPException(status_code=400, detail=str(exc))
|
|
||||||
|
|||||||
@@ -3,13 +3,11 @@ from datetime import datetime
|
|||||||
|
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query, Path, Body
|
from fastapi import APIRouter, Depends, HTTPException, Body
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from app.auth.keycloak_dependencies import get_current_keycloak_username
|
from app.auth.keycloak_dependencies import get_current_keycloak_username
|
||||||
from app.services.burst_location import (
|
from app.services.burst_location import (
|
||||||
get_burst_location_scheme_detail,
|
|
||||||
list_burst_location_schemes,
|
|
||||||
run_burst_location_by_network,
|
run_burst_location_by_network,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -40,7 +38,7 @@ class BurstLocationRequest(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/locate/",
|
"/burst-locations",
|
||||||
summary="执行爆管定位",
|
summary="执行爆管定位",
|
||||||
description="基于压力和流量数据定位管网中的爆管位置"
|
description="基于压力和流量数据定位管网中的爆管位置"
|
||||||
)
|
)
|
||||||
@@ -68,64 +66,3 @@ async def locate_burst(
|
|||||||
return run_burst_location_by_network(**data.model_dump(), username=username)
|
return run_burst_location_by_network(**data.model_dump(), username=username)
|
||||||
except (TypeError, ValueError) as exc:
|
except (TypeError, ValueError) as exc:
|
||||||
raise HTTPException(status_code=400, detail=str(exc))
|
raise HTTPException(status_code=400, detail=str(exc))
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/schemes/",
|
|
||||||
summary="查询爆管定位方案列表",
|
|
||||||
description="获取指定网络的所有爆管定位方案"
|
|
||||||
)
|
|
||||||
async def query_burst_schemes(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
query_date: datetime | None = Query(None, description="查询日期(可选)")
|
|
||||||
) -> list[dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
获取爆管定位方案列表。
|
|
||||||
|
|
||||||
查询指定网络的所有已配置的爆管定位方案,
|
|
||||||
可按日期进行筛选。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
network: 管网名称(或数据库名称)
|
|
||||||
query_date: 查询日期(可选)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
爆管定位方案列表
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
HTTPException: 当查询失败时
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return list_burst_location_schemes(network=network, query_date=query_date)
|
|
||||||
except Exception as exc:
|
|
||||||
raise HTTPException(status_code=400, detail=str(exc))
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/schemes/{scheme_name}",
|
|
||||||
summary="获取爆管定位方案详情",
|
|
||||||
description="获取指定爆管定位方案的详细信息"
|
|
||||||
)
|
|
||||||
async def query_burst_scheme_detail(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
scheme_name: str = Path(..., description="爆管定位方案名称")
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
获取爆管定位方案详情。
|
|
||||||
|
|
||||||
查询指定爆管定位方案的完整配置和参数信息。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
network: 管网名称(或数据库名称)
|
|
||||||
scheme_name: 爆管定位方案名称
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
包含方案详情的字典
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
HTTPException: 当查询失败时
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return get_burst_location_scheme_detail(network=network, scheme_name=scheme_name)
|
|
||||||
except Exception as exc:
|
|
||||||
raise HTTPException(status_code=400, detail=str(exc))
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from app.infra.cache.redis_client import redis_client
|
|||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.post("/clearrediskey/", summary="清除单个缓存键", description="根据键名清除单个Redis缓存")
|
@router.delete("/redis-keys/detail", summary="清除单个缓存键", description="根据键名清除单个Redis缓存")
|
||||||
async def fastapi_clear_redis_key(key: str = Query(..., description="缓存键名")):
|
async def fastapi_clear_redis_key(key: str = Query(..., description="缓存键名")):
|
||||||
"""
|
"""
|
||||||
清除单个缓存键
|
清除单个缓存键
|
||||||
@@ -14,7 +14,7 @@ async def fastapi_clear_redis_key(key: str = Query(..., description="缓存键
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@router.post("/clearrediskeys/", summary="清除匹配的缓存键", description="根据模式清除匹配的Redis缓存键")
|
@router.delete("/redis-keys", summary="清除匹配的缓存键", description="根据模式清除匹配的Redis缓存键")
|
||||||
async def fastapi_clear_redis_keys(keys: str = Query(..., description="缓存键模式(支持通配符)")):
|
async def fastapi_clear_redis_keys(keys: str = Query(..., description="缓存键模式(支持通配符)")):
|
||||||
"""
|
"""
|
||||||
清除匹配的缓存键
|
清除匹配的缓存键
|
||||||
@@ -29,7 +29,7 @@ async def fastapi_clear_redis_keys(keys: str = Query(..., description="缓存键
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@router.post("/clearallredis/", summary="清除所有缓存", description="清空整个Redis数据库的所有缓存")
|
@router.delete("/all-redis", summary="清除所有缓存", description="清空整个Redis数据库的所有缓存")
|
||||||
async def fastapi_clear_all_redis():
|
async def fastapi_clear_all_redis():
|
||||||
"""
|
"""
|
||||||
清除所有缓存
|
清除所有缓存
|
||||||
@@ -40,7 +40,7 @@ async def fastapi_clear_all_redis():
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@router.get("/queryredis/", summary="查询缓存键列表", description="获取Redis中所有的缓存键")
|
@router.get("/redis", summary="查询缓存键列表", description="获取Redis中所有的缓存键")
|
||||||
async def fastapi_query_redis():
|
async def fastapi_query_redis():
|
||||||
"""
|
"""
|
||||||
查询缓存键列表
|
查询缓存键列表
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from app.services.tjnetwork import (
|
|||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.get("/getcontrolschema/", summary="获取控制架构", description="获取网络中控制对象的架构定义")
|
@router.get("/network-schemas/control", summary="获取控制架构", description="获取网络中控制对象的架构定义")
|
||||||
async def fastapi_get_control_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
async def fastapi_get_control_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||||
"""获取控制架构。
|
"""获取控制架构。
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ async def fastapi_get_control_schema(network: str = Query(..., description="管
|
|||||||
"""
|
"""
|
||||||
return get_control_schema(network)
|
return get_control_schema(network)
|
||||||
|
|
||||||
@router.get("/getcontrolproperties/", summary="获取控制属性", description="获取指定网络中的控制属性信息")
|
@router.get("/controls/properties", summary="获取控制属性", description="获取指定网络中的控制属性信息")
|
||||||
async def fastapi_get_control_properties(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
|
async def fastapi_get_control_properties(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
|
||||||
"""获取控制属性。
|
"""获取控制属性。
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ async def fastapi_get_control_properties(network: str = Query(..., description="
|
|||||||
"""
|
"""
|
||||||
return get_control(network)
|
return get_control(network)
|
||||||
|
|
||||||
@router.post("/setcontrolproperties/", response_model=None, summary="设置控制属性", description="更新指定网络中的控制属性")
|
@router.patch("/controls/properties", response_model=None, summary="设置控制属性", description="更新指定网络中的控制属性")
|
||||||
async def fastapi_set_control_properties(
|
async def fastapi_set_control_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
req: Request = None
|
||||||
@@ -41,7 +41,7 @@ async def fastapi_set_control_properties(
|
|||||||
props = await req.json()
|
props = await req.json()
|
||||||
return set_control(network, ChangeSet(props))
|
return set_control(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.get("/getruleschema/", summary="获取规则架构", description="获取网络中规则对象的架构定义")
|
@router.get("/rule-schemas", summary="获取规则架构", description="获取网络中规则对象的架构定义")
|
||||||
async def fastapi_get_rule_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
async def fastapi_get_rule_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||||
"""获取规则架构。
|
"""获取规则架构。
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ async def fastapi_get_rule_schema(network: str = Query(..., description="管网
|
|||||||
"""
|
"""
|
||||||
return get_rule_schema(network)
|
return get_rule_schema(network)
|
||||||
|
|
||||||
@router.get("/getruleproperties/", summary="获取规则属性", description="获取指定网络中的规则属性信息")
|
@router.get("/rule-properties", summary="获取规则属性", description="获取指定网络中的规则属性信息")
|
||||||
async def fastapi_get_rule_properties(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
|
async def fastapi_get_rule_properties(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
|
||||||
"""获取规则属性。
|
"""获取规则属性。
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ async def fastapi_get_rule_properties(network: str = Query(..., description="管
|
|||||||
"""
|
"""
|
||||||
return get_rule(network)
|
return get_rule(network)
|
||||||
|
|
||||||
@router.post("/setruleproperties/", response_model=None, summary="设置规则属性", description="更新指定网络中的规则属性")
|
@router.patch("/rule-properties", response_model=None, summary="设置规则属性", description="更新指定网络中的规则属性")
|
||||||
async def fastapi_set_rule_properties(
|
async def fastapi_set_rule_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
req: Request = None
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from app.services.tjnetwork import (
|
|||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.get("/getcurveschema", summary="获取曲线架构", description="获取网络中曲线对象的架构定义")
|
@router.get("/network-schemas/curve", summary="获取曲线架构", description="获取网络中曲线对象的架构定义")
|
||||||
async def fastapi_get_curve_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
async def fastapi_get_curve_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||||
"""获取曲线架构。
|
"""获取曲线架构。
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ async def fastapi_get_curve_schema(network: str = Query(..., description="管网
|
|||||||
"""
|
"""
|
||||||
return get_curve_schema(network)
|
return get_curve_schema(network)
|
||||||
|
|
||||||
@router.post("/addcurve/", response_model=None, summary="添加曲线", description="在网络中添加一条新的曲线")
|
@router.post("/curves", response_model=None, summary="添加曲线", description="在网络中添加一条新的曲线")
|
||||||
async def fastapi_add_curve(
|
async def fastapi_add_curve(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
curve: str = Query(..., description="曲线ID"),
|
curve: str = Query(..., description="曲线ID"),
|
||||||
@@ -38,7 +38,7 @@ async def fastapi_add_curve(
|
|||||||
} | props
|
} | props
|
||||||
return add_curve(network, ChangeSet(ps))
|
return add_curve(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.post("/deletecurve/", response_model=None, summary="删除曲线", description="从网络中删除指定的曲线")
|
@router.delete("/curves", response_model=None, summary="删除曲线", description="从网络中删除指定的曲线")
|
||||||
async def fastapi_delete_curve(
|
async def fastapi_delete_curve(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
curve: str = Query(..., description="曲线ID")
|
curve: str = Query(..., description="曲线ID")
|
||||||
@@ -50,7 +50,7 @@ async def fastapi_delete_curve(
|
|||||||
ps = {"id": curve}
|
ps = {"id": curve}
|
||||||
return delete_curve(network, ChangeSet(ps))
|
return delete_curve(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.get("/getcurveproperties/", summary="获取曲线属性", description="获取指定曲线的属性信息")
|
@router.get("/curves/properties", summary="获取曲线属性", description="获取指定曲线的属性信息")
|
||||||
async def fastapi_get_curve_properties(
|
async def fastapi_get_curve_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
curve: str = Query(..., description="曲线ID")
|
curve: str = Query(..., description="曲线ID")
|
||||||
@@ -61,7 +61,7 @@ async def fastapi_get_curve_properties(
|
|||||||
"""
|
"""
|
||||||
return get_curve(network, curve)
|
return get_curve(network, curve)
|
||||||
|
|
||||||
@router.post("/setcurveproperties/", response_model=None, summary="设置曲线属性", description="更新指定曲线的属性")
|
@router.patch("/curves/properties", response_model=None, summary="设置曲线属性", description="更新指定曲线的属性")
|
||||||
async def fastapi_set_curve_properties(
|
async def fastapi_set_curve_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
curve: str = Query(..., description="曲线ID"),
|
curve: str = Query(..., description="曲线ID"),
|
||||||
@@ -75,7 +75,7 @@ async def fastapi_set_curve_properties(
|
|||||||
ps = {"id": curve} | props
|
ps = {"id": curve} | props
|
||||||
return set_curve(network, ChangeSet(ps))
|
return set_curve(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.get("/getcurves/", summary="获取所有曲线", description="获取网络中的所有曲线列表")
|
@router.get("/curves", summary="获取所有曲线", description="获取网络中的所有曲线列表")
|
||||||
async def fastapi_get_curves(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[str]:
|
async def fastapi_get_curves(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[str]:
|
||||||
"""获取所有曲线。
|
"""获取所有曲线。
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ async def fastapi_get_curves(network: str = Query(..., description="管网名称
|
|||||||
"""
|
"""
|
||||||
return get_curves(network)
|
return get_curves(network)
|
||||||
|
|
||||||
@router.get("/iscurve/", summary="检查曲线存在性", description="检查指定的曲线是否存在")
|
@router.get("/curves/existence", summary="检查曲线存在性", description="检查指定的曲线是否存在")
|
||||||
async def fastapi_is_curve(
|
async def fastapi_is_curve(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
curve: str = Query(..., description="曲线ID")
|
curve: str = Query(..., description="曲线ID")
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ from app.services.tjnetwork import (
|
|||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.get("/gettimeschema", summary="获取时间选项架构", description="获取网络中时间选项的架构定义")
|
@router.get("/network-schemas/time", summary="获取时间选项架构", description="获取网络中时间选项的架构定义")
|
||||||
async def fastapi_get_time_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
async def fastapi_get_time_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||||
"""获取时间选项架构。
|
"""获取时间选项架构。
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ async def fastapi_get_time_schema(network: str = Query(..., description="管网
|
|||||||
"""
|
"""
|
||||||
return get_time_schema(network)
|
return get_time_schema(network)
|
||||||
|
|
||||||
@router.get("/gettimeproperties/", summary="获取时间选项属性", description="获取指定网络中的时间选项属性信息")
|
@router.get("/network-options/time", summary="获取时间选项属性", description="获取指定网络中的时间选项属性信息")
|
||||||
async def fastapi_get_time_properties(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
|
async def fastapi_get_time_properties(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
|
||||||
"""获取时间选项属性。
|
"""获取时间选项属性。
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ async def fastapi_get_time_properties(network: str = Query(..., description="管
|
|||||||
"""
|
"""
|
||||||
return get_time(network)
|
return get_time(network)
|
||||||
|
|
||||||
@router.post("/settimeproperties/", response_model=None, summary="设置时间选项属性", description="更新指定网络中的时间选项属性")
|
@router.patch("/time-properties", response_model=None, summary="设置时间选项属性", description="更新指定网络中的时间选项属性")
|
||||||
async def fastapi_set_time_properties(
|
async def fastapi_set_time_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
req: Request = None
|
||||||
@@ -47,7 +47,7 @@ async def fastapi_set_time_properties(
|
|||||||
props = await req.json()
|
props = await req.json()
|
||||||
return set_time(network, ChangeSet(props))
|
return set_time(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.get("/getenergyschema/", summary="获取能耗选项架构", description="获取网络中能耗选项的架构定义")
|
@router.get("/network-schemas/energy", summary="获取能耗选项架构", description="获取网络中能耗选项的架构定义")
|
||||||
async def fastapi_get_energy_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
async def fastapi_get_energy_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||||
"""获取能耗选项架构。
|
"""获取能耗选项架构。
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ async def fastapi_get_energy_schema(network: str = Query(..., description="管
|
|||||||
"""
|
"""
|
||||||
return get_energy_schema(network)
|
return get_energy_schema(network)
|
||||||
|
|
||||||
@router.get("/getenergyproperties/", summary="获取能耗选项属性", description="获取指定网络中的能耗选项属性信息")
|
@router.get("/network-options/energy", summary="获取能耗选项属性", description="获取指定网络中的能耗选项属性信息")
|
||||||
async def fastapi_get_energy_properties(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
|
async def fastapi_get_energy_properties(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
|
||||||
"""获取能耗选项属性。
|
"""获取能耗选项属性。
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ async def fastapi_get_energy_properties(network: str = Query(..., description="
|
|||||||
"""
|
"""
|
||||||
return get_energy(network)
|
return get_energy(network)
|
||||||
|
|
||||||
@router.post("/setenergyproperties/", response_model=None, summary="设置能耗选项属性", description="更新指定网络中的能耗选项属性")
|
@router.patch("/energy-properties", response_model=None, summary="设置能耗选项属性", description="更新指定网络中的能耗选项属性")
|
||||||
async def fastapi_set_energy_properties(
|
async def fastapi_set_energy_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
req: Request = None
|
||||||
@@ -75,7 +75,7 @@ async def fastapi_set_energy_properties(
|
|||||||
props = await req.json()
|
props = await req.json()
|
||||||
return set_energy(network, ChangeSet(props))
|
return set_energy(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.get("/getpumpenergyschema/", summary="获取泵能耗选项架构", description="获取网络中泵能耗选项的架构定义")
|
@router.get("/network-schemas/pump-energy", summary="获取泵能耗选项架构", description="获取网络中泵能耗选项的架构定义")
|
||||||
async def fastapi_get_pump_energy_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
async def fastapi_get_pump_energy_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||||
"""获取泵能耗选项架构。
|
"""获取泵能耗选项架构。
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ async def fastapi_get_pump_energy_schema(network: str = Query(..., description="
|
|||||||
"""
|
"""
|
||||||
return get_pump_energy_schema(network)
|
return get_pump_energy_schema(network)
|
||||||
|
|
||||||
@router.get("/getpumpenergyproperties//", summary="获取泵能耗属性", description="获取指定泵的能耗属性信息")
|
@router.get("/network-options/pump-energy", summary="获取泵能耗属性", description="获取指定泵的能耗属性信息")
|
||||||
async def fastapi_get_pump_energy_proeprties(
|
async def fastapi_get_pump_energy_proeprties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pump: str = Query(..., description="泵ID")
|
pump: str = Query(..., description="泵ID")
|
||||||
@@ -94,7 +94,7 @@ async def fastapi_get_pump_energy_proeprties(
|
|||||||
"""
|
"""
|
||||||
return get_pump_energy(network, pump)
|
return get_pump_energy(network, pump)
|
||||||
|
|
||||||
@router.get("/setpumpenergyproperties//", response_model=None, summary="设置泵能耗属性", description="更新指定泵的能耗属性")
|
@router.patch("/network-options/pump-energy", response_model=None, summary="设置泵能耗属性", description="更新指定泵的能耗属性")
|
||||||
async def fastapi_set_pump_energy_properties(
|
async def fastapi_set_pump_energy_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pump: str = Query(..., description="泵ID"),
|
pump: str = Query(..., description="泵ID"),
|
||||||
@@ -108,7 +108,7 @@ async def fastapi_set_pump_energy_properties(
|
|||||||
ps = {"id": pump} | props
|
ps = {"id": pump} | props
|
||||||
return set_pump_energy(network, ChangeSet(ps))
|
return set_pump_energy(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.get("/getoptionschema/", summary="获取选项架构", description="获取网络中选项对象的架构定义")
|
@router.get("/network-schemas/option", summary="获取选项架构", description="获取网络中选项对象的架构定义")
|
||||||
async def fastapi_get_option_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
async def fastapi_get_option_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||||
"""获取选项架构。
|
"""获取选项架构。
|
||||||
|
|
||||||
@@ -116,7 +116,7 @@ async def fastapi_get_option_schema(network: str = Query(..., description="管
|
|||||||
"""
|
"""
|
||||||
return get_option_v3_schema(network)
|
return get_option_v3_schema(network)
|
||||||
|
|
||||||
@router.get("/getoptionproperties/", summary="获取选项属性", description="获取指定网络中的选项属性信息")
|
@router.get("/network-options", summary="获取选项属性", description="获取指定网络中的选项属性信息")
|
||||||
async def fastapi_get_option_properties(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
|
async def fastapi_get_option_properties(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
|
||||||
"""获取选项属性。
|
"""获取选项属性。
|
||||||
|
|
||||||
@@ -124,7 +124,7 @@ async def fastapi_get_option_properties(network: str = Query(..., description="
|
|||||||
"""
|
"""
|
||||||
return get_option_v3(network)
|
return get_option_v3(network)
|
||||||
|
|
||||||
@router.post("/setoptionproperties/", response_model=None, summary="设置选项属性", description="更新指定网络中的选项属性")
|
@router.patch("/network-options", response_model=None, summary="设置选项属性", description="更新指定网络中的选项属性")
|
||||||
async def fastapi_set_option_properties(
|
async def fastapi_set_option_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
req: Request = None
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from app.services.tjnetwork import (
|
|||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.get("/getpatternschema", summary="获取模式架构", description="获取网络中模式对象的架构定义")
|
@router.get("/network-schemas/pattern", summary="获取模式架构", description="获取网络中模式对象的架构定义")
|
||||||
async def fastapi_get_pattern_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
async def fastapi_get_pattern_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||||
"""获取模式架构。
|
"""获取模式架构。
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ async def fastapi_get_pattern_schema(network: str = Query(..., description="管
|
|||||||
"""
|
"""
|
||||||
return get_pattern_schema(network)
|
return get_pattern_schema(network)
|
||||||
|
|
||||||
@router.post("/addpattern/", response_model=None, summary="添加模式", description="在网络中添加一个新的模式")
|
@router.post("/patterns", response_model=None, summary="添加模式", description="在网络中添加一个新的模式")
|
||||||
async def fastapi_add_pattern(
|
async def fastapi_add_pattern(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pattern: str = Query(..., description="模式ID"),
|
pattern: str = Query(..., description="模式ID"),
|
||||||
@@ -38,7 +38,7 @@ async def fastapi_add_pattern(
|
|||||||
} | props
|
} | props
|
||||||
return add_pattern(network, ChangeSet(ps))
|
return add_pattern(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.post("/deletepattern/", response_model=None, summary="删除模式", description="从网络中删除指定的模式")
|
@router.delete("/patterns", response_model=None, summary="删除模式", description="从网络中删除指定的模式")
|
||||||
async def fastapi_delete_pattern(
|
async def fastapi_delete_pattern(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pattern: str = Query(..., description="模式ID")
|
pattern: str = Query(..., description="模式ID")
|
||||||
@@ -50,7 +50,7 @@ async def fastapi_delete_pattern(
|
|||||||
ps = {"id": pattern}
|
ps = {"id": pattern}
|
||||||
return delete_pattern(network, ChangeSet(ps))
|
return delete_pattern(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.get("/getpatternproperties/", summary="获取模式属性", description="获取指定模式的属性信息")
|
@router.get("/patterns/properties", summary="获取模式属性", description="获取指定模式的属性信息")
|
||||||
async def fastapi_get_pattern_properties(
|
async def fastapi_get_pattern_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pattern: str = Query(..., description="模式ID")
|
pattern: str = Query(..., description="模式ID")
|
||||||
@@ -61,7 +61,7 @@ async def fastapi_get_pattern_properties(
|
|||||||
"""
|
"""
|
||||||
return get_pattern(network, pattern)
|
return get_pattern(network, pattern)
|
||||||
|
|
||||||
@router.post("/setpatternproperties/", response_model=None, summary="设置模式属性", description="更新指定模式的属性")
|
@router.patch("/patterns/properties", response_model=None, summary="设置模式属性", description="更新指定模式的属性")
|
||||||
async def fastapi_set_pattern_properties(
|
async def fastapi_set_pattern_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pattern: str = Query(..., description="模式ID"),
|
pattern: str = Query(..., description="模式ID"),
|
||||||
@@ -75,7 +75,7 @@ async def fastapi_set_pattern_properties(
|
|||||||
ps = {"id": pattern} | props
|
ps = {"id": pattern} | props
|
||||||
return set_pattern(network, ChangeSet(ps))
|
return set_pattern(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.get("/ispattern/", summary="检查模式存在性", description="检查指定的模式是否存在")
|
@router.get("/patterns/existence", summary="检查模式存在性", description="检查指定的模式是否存在")
|
||||||
async def fastapi_is_pattern(
|
async def fastapi_is_pattern(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pattern: str = Query(..., description="模式ID")
|
pattern: str = Query(..., description="模式ID")
|
||||||
@@ -86,7 +86,7 @@ async def fastapi_is_pattern(
|
|||||||
"""
|
"""
|
||||||
return is_pattern(network, pattern)
|
return is_pattern(network, pattern)
|
||||||
|
|
||||||
@router.get("/getpatterns/", summary="获取所有模式", description="获取网络中的所有模式列表")
|
@router.get("/patterns", summary="获取所有模式", description="获取网络中的所有模式列表")
|
||||||
async def fastapi_get_patterns(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[str]:
|
async def fastapi_get_patterns(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[str]:
|
||||||
"""获取所有模式。
|
"""获取所有模式。
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ from app.services.tjnetwork import (
|
|||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.get("/getqualityschema/", summary="获取水质架构", description="获取网络中水质对象的架构定义")
|
@router.get("/network-schemas/quality", summary="获取水质架构", description="获取网络中水质对象的架构定义")
|
||||||
async def fastapi_get_quality_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
async def fastapi_get_quality_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||||
"""获取水质架构。
|
"""获取水质架构。
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ async def fastapi_get_quality_schema(network: str = Query(..., description="管
|
|||||||
"""
|
"""
|
||||||
return get_quality_schema(network)
|
return get_quality_schema(network)
|
||||||
|
|
||||||
@router.get("/getqualityproperties/", summary="获取水质属性", description="获取指定节点的水质属性信息")
|
@router.get("/quality-configurations/properties", summary="获取水质属性", description="获取指定节点的水质属性信息")
|
||||||
async def fastapi_get_quality_properties(
|
async def fastapi_get_quality_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
node: str = Query(..., description="节点ID")
|
node: str = Query(..., description="节点ID")
|
||||||
@@ -51,7 +51,7 @@ async def fastapi_get_quality_properties(
|
|||||||
"""
|
"""
|
||||||
return get_quality(network, node)
|
return get_quality(network, node)
|
||||||
|
|
||||||
@router.post("/setqualityproperties/", response_model=None, summary="设置水质属性", description="更新指定节点的水质属性")
|
@router.patch("/quality-configurations/properties", response_model=None, summary="设置水质属性", description="更新指定节点的水质属性")
|
||||||
async def fastapi_set_quality_properties(
|
async def fastapi_set_quality_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
req: Request = None
|
||||||
@@ -63,7 +63,7 @@ async def fastapi_set_quality_properties(
|
|||||||
props = await req.json()
|
props = await req.json()
|
||||||
return set_quality(network, ChangeSet(props))
|
return set_quality(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.get("/getemitterschema", summary="获取发射器架构", description="获取网络中发射器对象的架构定义")
|
@router.get("/network-schemas/emitter", summary="获取发射器架构", description="获取网络中发射器对象的架构定义")
|
||||||
async def fastapi_get_emitter_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
async def fastapi_get_emitter_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||||
"""获取发射器架构。
|
"""获取发射器架构。
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ async def fastapi_get_emitter_schema(network: str = Query(..., description="管
|
|||||||
"""
|
"""
|
||||||
return get_emitter_schema(network)
|
return get_emitter_schema(network)
|
||||||
|
|
||||||
@router.get("/getemitterproperties/", summary="获取发射器属性", description="获取指定连接点的发射器属性信息")
|
@router.get("/emitters/properties", summary="获取发射器属性", description="获取指定连接点的发射器属性信息")
|
||||||
async def fastapi_get_emitter_properties(
|
async def fastapi_get_emitter_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
junction: str = Query(..., description="连接点ID")
|
junction: str = Query(..., description="连接点ID")
|
||||||
@@ -82,7 +82,7 @@ async def fastapi_get_emitter_properties(
|
|||||||
"""
|
"""
|
||||||
return get_emitter(network, junction)
|
return get_emitter(network, junction)
|
||||||
|
|
||||||
@router.post("/setemitterproperties/", response_model=None, summary="设置发射器属性", description="更新指定连接点的发射器属性")
|
@router.patch("/emitters/properties", response_model=None, summary="设置发射器属性", description="更新指定连接点的发射器属性")
|
||||||
async def fastapi_set_emitter_properties(
|
async def fastapi_set_emitter_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
junction: str = Query(..., description="连接点ID"),
|
junction: str = Query(..., description="连接点ID"),
|
||||||
@@ -96,7 +96,7 @@ async def fastapi_set_emitter_properties(
|
|||||||
ps = {"junction": junction} | props
|
ps = {"junction": junction} | props
|
||||||
return set_emitter(network, ChangeSet(ps))
|
return set_emitter(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.get("/getsourcechema/", summary="获取水源架构", description="获取网络中水源对象的架构定义")
|
@router.get("/network-schemas/source", summary="获取水源架构", description="获取网络中水源对象的架构定义")
|
||||||
async def fastapi_get_source_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
async def fastapi_get_source_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||||
"""获取水源架构。
|
"""获取水源架构。
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ async def fastapi_get_source_schema(network: str = Query(..., description="管
|
|||||||
"""
|
"""
|
||||||
return get_source_schema(network)
|
return get_source_schema(network)
|
||||||
|
|
||||||
@router.get("/getsource/", summary="获取水源属性", description="获取指定节点的水源属性信息")
|
@router.get("/sources/detail", summary="获取水源属性", description="获取指定节点的水源属性信息")
|
||||||
async def fastapi_get_source(
|
async def fastapi_get_source(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
node: str = Query(..., description="节点ID")
|
node: str = Query(..., description="节点ID")
|
||||||
@@ -115,7 +115,7 @@ async def fastapi_get_source(
|
|||||||
"""
|
"""
|
||||||
return get_source(network, node)
|
return get_source(network, node)
|
||||||
|
|
||||||
@router.post("/setsource/", response_model=None, summary="设置水源属性", description="更新指定节点的水源属性")
|
@router.patch("/sources", response_model=None, summary="设置水源属性", description="更新指定节点的水源属性")
|
||||||
async def fastapi_set_source(
|
async def fastapi_set_source(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
req: Request = None
|
||||||
@@ -127,7 +127,7 @@ async def fastapi_set_source(
|
|||||||
props = await req.json()
|
props = await req.json()
|
||||||
return set_source(network, ChangeSet(props))
|
return set_source(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.post("/addsource/", response_model=None, summary="添加水源", description="在网络中添加一个新的水源")
|
@router.post("/sources", response_model=None, summary="添加水源", description="在网络中添加一个新的水源")
|
||||||
async def fastapi_add_source(
|
async def fastapi_add_source(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
req: Request = None
|
||||||
@@ -139,7 +139,7 @@ async def fastapi_add_source(
|
|||||||
props = await req.json()
|
props = await req.json()
|
||||||
return add_source(network, ChangeSet(props))
|
return add_source(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.post("/deletesource/", response_model=None, summary="删除水源", description="从网络中删除指定节点的水源")
|
@router.delete("/sources", response_model=None, summary="删除水源", description="从网络中删除指定节点的水源")
|
||||||
async def fastapi_delete_source(
|
async def fastapi_delete_source(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
node: str = Query(..., description="节点ID")
|
node: str = Query(..., description="节点ID")
|
||||||
@@ -151,7 +151,7 @@ async def fastapi_delete_source(
|
|||||||
props = {"node": node}
|
props = {"node": node}
|
||||||
return delete_source(network, ChangeSet(props))
|
return delete_source(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.get("/getreactionschema/", summary="获取反应架构", description="获取网络中反应对象的架构定义")
|
@router.get("/network-schemas/reaction", summary="获取反应架构", description="获取网络中反应对象的架构定义")
|
||||||
async def fastapi_get_reaction_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
async def fastapi_get_reaction_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||||
"""获取反应架构。
|
"""获取反应架构。
|
||||||
|
|
||||||
@@ -159,7 +159,7 @@ async def fastapi_get_reaction_schema(network: str = Query(..., description="管
|
|||||||
"""
|
"""
|
||||||
return get_reaction_schema(network)
|
return get_reaction_schema(network)
|
||||||
|
|
||||||
@router.get("/getreaction/", summary="获取反应属性", description="获取指定网络中的反应属性信息")
|
@router.get("/reactions/detail", summary="获取反应属性", description="获取指定网络中的反应属性信息")
|
||||||
async def fastapi_get_reaction(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
|
async def fastapi_get_reaction(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
|
||||||
"""获取反应属性。
|
"""获取反应属性。
|
||||||
|
|
||||||
@@ -167,7 +167,7 @@ async def fastapi_get_reaction(network: str = Query(..., description="管网名
|
|||||||
"""
|
"""
|
||||||
return get_reaction(network)
|
return get_reaction(network)
|
||||||
|
|
||||||
@router.post("/setreaction/", response_model=None, summary="设置反应属性", description="更新指定网络中的反应属性")
|
@router.patch("/reactions", response_model=None, summary="设置反应属性", description="更新指定网络中的反应属性")
|
||||||
async def fastapi_set_reaction(
|
async def fastapi_set_reaction(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
req: Request = None
|
||||||
@@ -179,7 +179,7 @@ async def fastapi_set_reaction(
|
|||||||
props = await req.json()
|
props = await req.json()
|
||||||
return set_reaction(network, ChangeSet(props))
|
return set_reaction(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.get("/getpipereactionschema/", summary="获取管道反应架构", description="获取网络中管道反应对象的架构定义")
|
@router.get("/network-schemas/pipe-reaction", summary="获取管道反应架构", description="获取网络中管道反应对象的架构定义")
|
||||||
async def fastapi_get_pipe_reaction_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
async def fastapi_get_pipe_reaction_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||||
"""获取管道反应架构。
|
"""获取管道反应架构。
|
||||||
|
|
||||||
@@ -187,7 +187,7 @@ async def fastapi_get_pipe_reaction_schema(network: str = Query(..., description
|
|||||||
"""
|
"""
|
||||||
return get_pipe_reaction_schema(network)
|
return get_pipe_reaction_schema(network)
|
||||||
|
|
||||||
@router.get("/getpipereaction/", summary="获取管道反应属性", description="获取指定管道的反应属性信息")
|
@router.get("/pipe-reactions/detail", summary="获取管道反应属性", description="获取指定管道的反应属性信息")
|
||||||
async def fastapi_get_pipe_reaction(
|
async def fastapi_get_pipe_reaction(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pipe: str = Query(..., description="管道ID")
|
pipe: str = Query(..., description="管道ID")
|
||||||
@@ -198,7 +198,7 @@ async def fastapi_get_pipe_reaction(
|
|||||||
"""
|
"""
|
||||||
return get_pipe_reaction(network, pipe)
|
return get_pipe_reaction(network, pipe)
|
||||||
|
|
||||||
@router.post("/setpipereaction/", response_model=None, summary="设置管道反应属性", description="更新指定管道的反应属性")
|
@router.patch("/pipe-reactions", response_model=None, summary="设置管道反应属性", description="更新指定管道的反应属性")
|
||||||
async def fastapi_set_pipe_reaction(
|
async def fastapi_set_pipe_reaction(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
req: Request = None
|
||||||
@@ -210,7 +210,7 @@ async def fastapi_set_pipe_reaction(
|
|||||||
props = await req.json()
|
props = await req.json()
|
||||||
return set_pipe_reaction(network, ChangeSet(props))
|
return set_pipe_reaction(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.get("/gettankreactionschema/", summary="获取水池反应架构", description="获取网络中水池反应对象的架构定义")
|
@router.get("/network-schemas/tank-reaction", summary="获取水池反应架构", description="获取网络中水池反应对象的架构定义")
|
||||||
async def fastapi_get_tank_reaction_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
async def fastapi_get_tank_reaction_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||||
"""获取水池反应架构。
|
"""获取水池反应架构。
|
||||||
|
|
||||||
@@ -218,7 +218,7 @@ async def fastapi_get_tank_reaction_schema(network: str = Query(..., description
|
|||||||
"""
|
"""
|
||||||
return get_tank_reaction_schema(network)
|
return get_tank_reaction_schema(network)
|
||||||
|
|
||||||
@router.get("/gettankreaction/", summary="获取水池反应属性", description="获取指定水池的反应属性信息")
|
@router.get("/tank-reactions/detail", summary="获取水池反应属性", description="获取指定水池的反应属性信息")
|
||||||
async def fastapi_get_tank_reaction(
|
async def fastapi_get_tank_reaction(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水池ID")
|
tank: str = Query(..., description="水池ID")
|
||||||
@@ -229,7 +229,7 @@ async def fastapi_get_tank_reaction(
|
|||||||
"""
|
"""
|
||||||
return get_tank_reaction(network, tank)
|
return get_tank_reaction(network, tank)
|
||||||
|
|
||||||
@router.post("/settankreaction/", response_model=None, summary="设置水池反应属性", description="更新指定水池的反应属性")
|
@router.patch("/tank-reactions", response_model=None, summary="设置水池反应属性", description="更新指定水池的反应属性")
|
||||||
async def fastapi_set_tank_reaction(
|
async def fastapi_set_tank_reaction(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
req: Request = None
|
||||||
@@ -241,7 +241,7 @@ async def fastapi_set_tank_reaction(
|
|||||||
props = await req.json()
|
props = await req.json()
|
||||||
return set_tank_reaction(network, ChangeSet(props))
|
return set_tank_reaction(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.get("/getmixingschema/", summary="获取混合架构", description="获取网络中混合对象的架构定义")
|
@router.get("/network-schemas/mixing", summary="获取混合架构", description="获取网络中混合对象的架构定义")
|
||||||
async def fastapi_get_mixing_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
async def fastapi_get_mixing_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||||
"""获取混合架构。
|
"""获取混合架构。
|
||||||
|
|
||||||
@@ -249,7 +249,7 @@ async def fastapi_get_mixing_schema(network: str = Query(..., description="管
|
|||||||
"""
|
"""
|
||||||
return get_mixing_schema(network)
|
return get_mixing_schema(network)
|
||||||
|
|
||||||
@router.get("/getmixing/", summary="获取混合属性", description="获取指定水池的混合属性信息")
|
@router.get("/mixing-configurations/detail", summary="获取混合属性", description="获取指定水池的混合属性信息")
|
||||||
async def fastapi_get_mixing(
|
async def fastapi_get_mixing(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水池ID")
|
tank: str = Query(..., description="水池ID")
|
||||||
@@ -260,7 +260,7 @@ async def fastapi_get_mixing(
|
|||||||
"""
|
"""
|
||||||
return get_mixing(network, tank)
|
return get_mixing(network, tank)
|
||||||
|
|
||||||
@router.post("/setmixing/", response_model=None, summary="设置混合属性", description="更新指定水池的混合属性")
|
@router.patch("/mixing-configurations", response_model=None, summary="设置混合属性", description="更新指定水池的混合属性")
|
||||||
async def fastapi_set_mixing(
|
async def fastapi_set_mixing(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
req: Request = None
|
||||||
@@ -272,7 +272,7 @@ async def fastapi_set_mixing(
|
|||||||
props = await req.json()
|
props = await req.json()
|
||||||
return api.set_mixing(network, ChangeSet(props))
|
return api.set_mixing(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.post("/addmixing/", response_model=None, summary="添加混合", description="在网络中添加一个新的混合")
|
@router.post("/mixing-configurations", response_model=None, summary="添加混合", description="在网络中添加一个新的混合")
|
||||||
async def fastapi_add_mixing(
|
async def fastapi_add_mixing(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
req: Request = None
|
||||||
@@ -284,7 +284,7 @@ async def fastapi_add_mixing(
|
|||||||
props = await req.json()
|
props = await req.json()
|
||||||
return add_mixing(network, ChangeSet(props))
|
return add_mixing(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.post("/deletemixing/", response_model=None, summary="删除混合", description="从网络中删除指定的混合")
|
@router.delete("/mixing-configurations", response_model=None, summary="删除混合", description="从网络中删除指定的混合")
|
||||||
async def fastapi_delete_mixing(
|
async def fastapi_delete_mixing(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
req: Request = None
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import json
|
|||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.get("/getvertexschema/", summary="获取图形元素架构", description="获取网络中图形元素对象的架构定义")
|
@router.get("/network-schemas/vertex", summary="获取图形元素架构", description="获取网络中图形元素对象的架构定义")
|
||||||
async def fastapi_get_vertex_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
async def fastapi_get_vertex_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||||
"""获取图形元素架构。
|
"""获取图形元素架构。
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ async def fastapi_get_vertex_schema(network: str = Query(..., description="管
|
|||||||
"""
|
"""
|
||||||
return get_vertex_schema(network)
|
return get_vertex_schema(network)
|
||||||
|
|
||||||
@router.get("/getvertexproperties/", summary="获取图形元素属性", description="获取指定图形元素的属性信息")
|
@router.get("/visual-elements/properties", summary="获取图形元素属性", description="获取指定图形元素的属性信息")
|
||||||
async def fastapi_get_vertex_properties(
|
async def fastapi_get_vertex_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
link: str = Query(..., description="图形元素链接")
|
link: str = Query(..., description="图形元素链接")
|
||||||
@@ -43,7 +43,7 @@ async def fastapi_get_vertex_properties(
|
|||||||
"""
|
"""
|
||||||
return get_vertex(network, link)
|
return get_vertex(network, link)
|
||||||
|
|
||||||
@router.post("/setvertexproperties/", response_model=None, summary="设置图形元素属性", description="更新指定图形元素的属性")
|
@router.patch("/visual-elements/properties", response_model=None, summary="设置图形元素属性", description="更新指定图形元素的属性")
|
||||||
async def fastapi_set_vertex_properties(
|
async def fastapi_set_vertex_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
req: Request = None
|
||||||
@@ -55,7 +55,7 @@ async def fastapi_set_vertex_properties(
|
|||||||
props = await req.json()
|
props = await req.json()
|
||||||
return set_vertex(network, ChangeSet(props))
|
return set_vertex(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.post("/addvertex/", response_model=None, summary="添加图形元素", description="在网络中添加一个新的图形元素")
|
@router.post("/visual-elements", response_model=None, summary="添加图形元素", description="在网络中添加一个新的图形元素")
|
||||||
async def fastapi_add_vertex(
|
async def fastapi_add_vertex(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
req: Request = None
|
||||||
@@ -67,7 +67,7 @@ async def fastapi_add_vertex(
|
|||||||
props = await req.json()
|
props = await req.json()
|
||||||
return add_vertex(network, ChangeSet(props))
|
return add_vertex(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.post("/deletevertex/", response_model=None, summary="删除图形元素", description="从网络中删除指定的图形元素")
|
@router.delete("/visual-elements", response_model=None, summary="删除图形元素", description="从网络中删除指定的图形元素")
|
||||||
async def fastapi_delete_vertex(
|
async def fastapi_delete_vertex(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
req: Request = None
|
||||||
@@ -79,7 +79,7 @@ async def fastapi_delete_vertex(
|
|||||||
props = await req.json()
|
props = await req.json()
|
||||||
return delete_vertex(network, ChangeSet(props))
|
return delete_vertex(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.get("/getallvertexlinks/", response_class=PlainTextResponse, summary="获取所有图形元素链接", description="获取网络中的所有图形元素链接列表")
|
@router.get("/visual-elements/links", response_class=PlainTextResponse, summary="获取所有图形元素链接", description="获取网络中的所有图形元素链接列表")
|
||||||
async def fastapi_get_all_vertex_links(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[str]:
|
async def fastapi_get_all_vertex_links(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[str]:
|
||||||
"""获取所有图形元素链接。
|
"""获取所有图形元素链接。
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ async def fastapi_get_all_vertex_links(network: str = Query(..., description="
|
|||||||
"""
|
"""
|
||||||
return json.dumps(get_all_vertex_links(network))
|
return json.dumps(get_all_vertex_links(network))
|
||||||
|
|
||||||
@router.get("/getallvertices/", response_class=PlainTextResponse, summary="获取所有图形元素", description="获取网络中的所有图形元素详细信息")
|
@router.get("/all-vertices", response_class=PlainTextResponse, summary="获取所有图形元素", description="获取网络中的所有图形元素详细信息")
|
||||||
async def fastapi_get_all_vertices(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[dict[str, Any]]:
|
async def fastapi_get_all_vertices(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[dict[str, Any]]:
|
||||||
"""获取所有图形元素。
|
"""获取所有图形元素。
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ async def fastapi_get_all_vertices(network: str = Query(..., description="管网
|
|||||||
"""
|
"""
|
||||||
return json.dumps(get_all_vertices(network))
|
return json.dumps(get_all_vertices(network))
|
||||||
|
|
||||||
@router.get("/getlabelschema/", summary="获取标签架构", description="获取网络中标签对象的架构定义")
|
@router.get("/network-schemas/label", summary="获取标签架构", description="获取网络中标签对象的架构定义")
|
||||||
async def fastapi_get_label_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
async def fastapi_get_label_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||||
"""获取标签架构。
|
"""获取标签架构。
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ async def fastapi_get_label_schema(network: str = Query(..., description="管网
|
|||||||
"""
|
"""
|
||||||
return get_label_schema(network)
|
return get_label_schema(network)
|
||||||
|
|
||||||
@router.get("/getlabelproperties/", summary="获取标签属性", description="获取指定坐标处的标签属性信息")
|
@router.get("/labels/properties", summary="获取标签属性", description="获取指定坐标处的标签属性信息")
|
||||||
async def fastapi_get_label_properties(
|
async def fastapi_get_label_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
x: float = Query(..., description="X坐标"),
|
x: float = Query(..., description="X坐标"),
|
||||||
@@ -115,7 +115,7 @@ async def fastapi_get_label_properties(
|
|||||||
"""
|
"""
|
||||||
return get_label(network, x, y)
|
return get_label(network, x, y)
|
||||||
|
|
||||||
@router.post("/setlabelproperties/", response_model=None, summary="设置标签属性", description="更新指定标签的属性")
|
@router.patch("/labels/properties", response_model=None, summary="设置标签属性", description="更新指定标签的属性")
|
||||||
async def fastapi_set_label_properties(
|
async def fastapi_set_label_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
req: Request = None
|
||||||
@@ -127,7 +127,7 @@ async def fastapi_set_label_properties(
|
|||||||
props = await req.json()
|
props = await req.json()
|
||||||
return set_label(network, ChangeSet(props))
|
return set_label(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.post("/addlabel/", response_model=None, summary="添加标签", description="在网络中添加一个新的标签")
|
@router.post("/labels", response_model=None, summary="添加标签", description="在网络中添加一个新的标签")
|
||||||
async def fastapi_add_label(
|
async def fastapi_add_label(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
req: Request = None
|
||||||
@@ -139,7 +139,7 @@ async def fastapi_add_label(
|
|||||||
props = await req.json()
|
props = await req.json()
|
||||||
return add_label(network, ChangeSet(props))
|
return add_label(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.post("/deletelabel/", response_model=None, summary="删除标签", description="从网络中删除指定的标签")
|
@router.delete("/labels", response_model=None, summary="删除标签", description="从网络中删除指定的标签")
|
||||||
async def fastapi_delete_label(
|
async def fastapi_delete_label(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
req: Request = None
|
||||||
@@ -151,7 +151,7 @@ async def fastapi_delete_label(
|
|||||||
props = await req.json()
|
props = await req.json()
|
||||||
return delete_label(network, ChangeSet(props))
|
return delete_label(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.get("/getbackdropschema/", summary="获取背景架构", description="获取网络中背景对象的架构定义")
|
@router.get("/network-schemas/backdrop", summary="获取背景架构", description="获取网络中背景对象的架构定义")
|
||||||
async def fastapi_get_backdrop_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
async def fastapi_get_backdrop_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||||
"""获取背景架构。
|
"""获取背景架构。
|
||||||
|
|
||||||
@@ -159,7 +159,7 @@ async def fastapi_get_backdrop_schema(network: str = Query(..., description="管
|
|||||||
"""
|
"""
|
||||||
return get_backdrop_schema(network)
|
return get_backdrop_schema(network)
|
||||||
|
|
||||||
@router.get("/getbackdropproperties/", summary="获取背景属性", description="获取指定网络的背景属性信息")
|
@router.get("/backdrops/properties", summary="获取背景属性", description="获取指定网络的背景属性信息")
|
||||||
async def fastapi_get_backdrop_properties(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
|
async def fastapi_get_backdrop_properties(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
|
||||||
"""获取背景属性。
|
"""获取背景属性。
|
||||||
|
|
||||||
@@ -167,7 +167,7 @@ async def fastapi_get_backdrop_properties(network: str = Query(..., description=
|
|||||||
"""
|
"""
|
||||||
return get_backdrop(network)
|
return get_backdrop(network)
|
||||||
|
|
||||||
@router.post("/setbackdropproperties/", response_model=None, summary="设置背景属性", description="更新指定网络的背景属性")
|
@router.patch("/backdrops/properties", response_model=None, summary="设置背景属性", description="更新指定网络的背景属性")
|
||||||
async def fastapi_set_backdrop_properties(
|
async def fastapi_set_backdrop_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
req: Request = None
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from app.services.tjnetwork import (
|
|||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getallextensiondatakeys/",
|
"/all-extension-data-keys",
|
||||||
summary="获取所有扩展数据键",
|
summary="获取所有扩展数据键",
|
||||||
description="获取指定网络的所有扩展数据的键列表"
|
description="获取指定网络的所有扩展数据的键列表"
|
||||||
)
|
)
|
||||||
@@ -32,7 +32,7 @@ async def get_all_extension_data_keys_endpoint(
|
|||||||
return get_all_extension_data_keys(network)
|
return get_all_extension_data_keys(network)
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getallextensiondata/",
|
"/all-extension-datas",
|
||||||
summary="获取所有扩展数据",
|
summary="获取所有扩展数据",
|
||||||
description="获取指定网络的所有扩展数据"
|
description="获取指定网络的所有扩展数据"
|
||||||
)
|
)
|
||||||
@@ -53,7 +53,7 @@ async def get_all_extension_data_endpoint(
|
|||||||
return get_all_extension_data(network)
|
return get_all_extension_data(network)
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getextensiondata/",
|
"/extension-datas",
|
||||||
summary="获取指定扩展数据",
|
summary="获取指定扩展数据",
|
||||||
description="获取指定网络中指定键的扩展数据值"
|
description="获取指定网络中指定键的扩展数据值"
|
||||||
)
|
)
|
||||||
@@ -75,8 +75,8 @@ async def get_extension_data_endpoint(
|
|||||||
"""
|
"""
|
||||||
return get_extension_data(network, key)
|
return get_extension_data(network, key)
|
||||||
|
|
||||||
@router.post(
|
@router.patch(
|
||||||
"/setextensiondata/",
|
"/extension-datas",
|
||||||
response_model=None,
|
response_model=None,
|
||||||
summary="设置扩展数据",
|
summary="设置扩展数据",
|
||||||
description="设置指定网络中的扩展数据"
|
description="设置指定网络中的扩展数据"
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ router = APIRouter()
|
|||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/tianditu/geocode",
|
"/geocoding-requests",
|
||||||
summary="Tianditu Geocoding",
|
summary="Tianditu Geocoding",
|
||||||
description="调用天地图地理编码服务,将结构化地址转换为经纬度",
|
description="调用天地图地理编码服务,将结构化地址转换为经纬度",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,13 +2,11 @@ import os
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query, Path, Body
|
from fastapi import APIRouter, Depends, HTTPException, Body
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from app.auth.keycloak_dependencies import get_current_keycloak_username
|
from app.auth.keycloak_dependencies import get_current_keycloak_username
|
||||||
from app.services.leakage_identifier import (
|
from app.services.leakage_identifier import (
|
||||||
get_leakage_identify_scheme_detail,
|
|
||||||
list_leakage_identify_schemes,
|
|
||||||
run_leakage_identification,
|
run_leakage_identification,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -40,7 +38,7 @@ class LeakageIdentifyRequest(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/identify/",
|
"/leakage-identifications",
|
||||||
summary="执行漏损识别",
|
summary="执行漏损识别",
|
||||||
description="基于压力观测数据和遗传算法识别管网中的漏损位置和大小"
|
description="基于压力观测数据和遗传算法识别管网中的漏损位置和大小"
|
||||||
)
|
)
|
||||||
@@ -68,66 +66,3 @@ async def identify_leakage(
|
|||||||
return run_leakage_identification(**data.model_dump(), username=username)
|
return run_leakage_identification(**data.model_dump(), username=username)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise HTTPException(status_code=400, detail=str(exc))
|
raise HTTPException(status_code=400, detail=str(exc))
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/schemes/",
|
|
||||||
summary="查询漏损识别方案列表",
|
|
||||||
description="获取指定网络的所有漏损识别方案"
|
|
||||||
)
|
|
||||||
async def query_leakage_schemes(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
query_date: datetime | None = Query(None, description="查询日期(可选)")
|
|
||||||
) -> list[dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
获取漏损识别方案列表。
|
|
||||||
|
|
||||||
查询指定网络的所有已配置的漏损识别方案,
|
|
||||||
可按日期进行筛选。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
network: 管网名称(或数据库名称)
|
|
||||||
query_date: 查询日期(可选)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
漏损识别方案列表
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
HTTPException: 当查询失败时
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return list_leakage_identify_schemes(network=network, query_date=query_date)
|
|
||||||
except Exception as exc:
|
|
||||||
raise HTTPException(status_code=400, detail=str(exc))
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/schemes/{scheme_name}",
|
|
||||||
summary="获取漏损识别方案详情",
|
|
||||||
description="获取指定漏损识别方案的详细信息"
|
|
||||||
)
|
|
||||||
async def query_leakage_scheme_detail(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
scheme_name: str = Path(..., description="漏损识别方案名称")
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
获取漏损识别方案详情。
|
|
||||||
|
|
||||||
查询指定漏损识别方案的完整配置和参数信息。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
network: 管网名称(或数据库名称)
|
|
||||||
scheme_name: 漏损识别方案名称
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
包含方案详情的字典
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
HTTPException: 当查询失败时
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return get_leakage_identify_scheme_detail(
|
|
||||||
network=network, scheme_name=scheme_name
|
|
||||||
)
|
|
||||||
except Exception as exc:
|
|
||||||
raise HTTPException(status_code=400, detail=str(exc))
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ router = APIRouter()
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/meta/project", summary="获取项目元数据", description="获取当前项目的元数据和配置信息", response_model=ProjectMetaResponse)
|
@router.get("/projects/current/metadata", summary="获取项目元数据", description="获取当前项目的元数据和配置信息", response_model=ProjectMetaResponse)
|
||||||
async def get_project_metadata(
|
async def get_project_metadata(
|
||||||
ctx: ProjectContext = Depends(get_project_context),
|
ctx: ProjectContext = Depends(get_project_context),
|
||||||
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
||||||
@@ -52,7 +52,7 @@ async def get_project_metadata(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/meta/projects", summary="列出用户项目", description="获取当前用户有权限的所有项目列表", response_model=list[ProjectSummaryResponse])
|
@router.get("/projects", summary="列出用户项目", description="获取当前用户有权限的所有项目列表", response_model=list[ProjectSummaryResponse])
|
||||||
async def list_user_projects(
|
async def list_user_projects(
|
||||||
current_user=Depends(get_current_metadata_user),
|
current_user=Depends(get_current_metadata_user),
|
||||||
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
||||||
@@ -88,7 +88,7 @@ async def list_user_projects(
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@router.get("/meta/db/health", summary="检查数据库健康状态", description="检查项目数据库连接的健康状况")
|
@router.get("/projects/current/database-health", summary="检查数据库健康状态", description="检查项目数据库连接的健康状况")
|
||||||
async def project_db_health(
|
async def project_db_health(
|
||||||
pg_session: AsyncSession = Depends(get_project_pg_session),
|
pg_session: AsyncSession = Depends(get_project_pg_session),
|
||||||
ts_conn: AsyncConnection = Depends(get_project_timescale_connection),
|
ts_conn: AsyncConnection = Depends(get_project_timescale_connection),
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ from app.services.tjnetwork import (
|
|||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/getjson/", summary="获取JSON示例", description="获取JSON格式响应示例")
|
|
||||||
async def fastapi_get_json():
|
async def fastapi_get_json():
|
||||||
"""
|
"""
|
||||||
获取JSON示例
|
获取JSON示例
|
||||||
@@ -29,7 +28,6 @@ async def fastapi_get_json():
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/sensor-placement-schemes", summary="获取所有传感器位置", description="获取网络中所有传感器的放置位置信息")
|
@router.get("/sensor-placement-schemes", summary="获取所有传感器位置", description="获取网络中所有传感器的放置位置信息")
|
||||||
@router.get("/getallsensorplacements/", summary="获取所有传感器位置(旧路径)", description="获取网络中所有传感器的放置位置信息", deprecated=True)
|
|
||||||
async def fastapi_get_all_sensor_placements(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[dict[Any, Any]]:
|
async def fastapi_get_all_sensor_placements(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[dict[Any, Any]]:
|
||||||
"""
|
"""
|
||||||
获取所有传感器位置
|
获取所有传感器位置
|
||||||
@@ -39,7 +37,7 @@ async def fastapi_get_all_sensor_placements(network: str = Query(..., descriptio
|
|||||||
return get_all_sensor_placements(network)
|
return get_all_sensor_placements(network)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/getallburstlocateresults/", summary="获取所有爆管定位结果", description="获取网络中所有爆管定位的分析结果")
|
@router.get("/burst-locations", summary="获取所有爆管定位结果", description="获取网络中所有爆管定位的分析结果")
|
||||||
async def fastapi_get_all_burst_locate_results(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[dict[Any, Any]]:
|
async def fastapi_get_all_burst_locate_results(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[dict[Any, Any]]:
|
||||||
"""
|
"""
|
||||||
获取所有爆管定位结果
|
获取所有爆管定位结果
|
||||||
@@ -54,7 +52,6 @@ class Item(BaseModel):
|
|||||||
str_info: str
|
str_info: str
|
||||||
|
|
||||||
|
|
||||||
@router.post("/test_dict/", summary="测试字典处理", description="测试处理字典类型数据")
|
|
||||||
async def fastapi_test_dict(data: Item) -> dict[str, str]:
|
async def fastapi_test_dict(data: Item) -> dict[str, str]:
|
||||||
"""
|
"""
|
||||||
测试字典处理
|
测试字典处理
|
||||||
|
|||||||
@@ -0,0 +1,188 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
from tempfile import NamedTemporaryFile
|
||||||
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
|
from fastapi import (
|
||||||
|
APIRouter,
|
||||||
|
Depends,
|
||||||
|
File,
|
||||||
|
HTTPException,
|
||||||
|
Path as ApiPath,
|
||||||
|
Request,
|
||||||
|
UploadFile,
|
||||||
|
status,
|
||||||
|
)
|
||||||
|
|
||||||
|
from app.auth.metadata_dependencies import (
|
||||||
|
get_current_metadata_admin,
|
||||||
|
get_metadata_repository,
|
||||||
|
)
|
||||||
|
from app.core.audit import AuditAction, log_audit_event
|
||||||
|
from app.infra.db.metadb.repositories.metadata_repository import MetadataRepository
|
||||||
|
from app.services.network_import import network_update
|
||||||
|
from app.services.tjnetwork import run_inp
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
MAX_INP_FILE_BYTES = 50 * 1024 * 1024
|
||||||
|
INP_SECTIONS = ("[TITLE]", "[JUNCTIONS]", "[RESERVOIRS]", "[TANKS]", "[PIPES]")
|
||||||
|
|
||||||
|
|
||||||
|
async def _get_active_project(project_id: UUID, metadata_repo: MetadataRepository):
|
||||||
|
project = await metadata_repo.get_project_by_id(project_id)
|
||||||
|
if project is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Project not found",
|
||||||
|
)
|
||||||
|
if project.status != "active":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Project is not active",
|
||||||
|
)
|
||||||
|
return project
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_inp_bytes(content: bytes, filename: str) -> str:
|
||||||
|
if Path(filename).suffix.lower() != ".inp":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Only .inp model files are accepted",
|
||||||
|
)
|
||||||
|
if not content:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="INP file is empty",
|
||||||
|
)
|
||||||
|
if len(content) > MAX_INP_FILE_BYTES:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
|
||||||
|
detail="INP file exceeds the 50 MiB limit",
|
||||||
|
)
|
||||||
|
for encoding in ("utf-8-sig", "gb18030"):
|
||||||
|
try:
|
||||||
|
text = content.decode(encoding)
|
||||||
|
break
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="INP file encoding is not supported",
|
||||||
|
)
|
||||||
|
upper_text = text.upper()
|
||||||
|
if not any(section in upper_text for section in INP_SECTIONS):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Invalid INP file structure",
|
||||||
|
)
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
async def _read_upload(file: UploadFile) -> tuple[bytes, str]:
|
||||||
|
filename = Path(file.filename or "").name
|
||||||
|
content = await file.read(MAX_INP_FILE_BYTES + 1)
|
||||||
|
_validate_inp_bytes(content, filename)
|
||||||
|
return content, filename
|
||||||
|
|
||||||
|
|
||||||
|
async def _audit_model_change(
|
||||||
|
*,
|
||||||
|
request: Request,
|
||||||
|
current_user,
|
||||||
|
metadata_repo: MetadataRepository,
|
||||||
|
project_id: UUID,
|
||||||
|
action: str,
|
||||||
|
) -> None:
|
||||||
|
await log_audit_event(
|
||||||
|
action=AuditAction.UPDATE,
|
||||||
|
user_id=current_user.id,
|
||||||
|
project_id=project_id,
|
||||||
|
resource_type="hydraulic_model",
|
||||||
|
resource_id=action,
|
||||||
|
request_data={"operation": action},
|
||||||
|
ip_address=request.client.host if request.client else None,
|
||||||
|
request_method=request.method,
|
||||||
|
request_path=request.url.path,
|
||||||
|
response_status=status.HTTP_200_OK,
|
||||||
|
session=metadata_repo.session,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _run_uploaded_inp(content: bytes) -> str:
|
||||||
|
target_dir = Path("inp")
|
||||||
|
target_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
model_name = f"admin_model_{uuid4().hex}"
|
||||||
|
target_path = target_dir / f"{model_name}.inp"
|
||||||
|
target_path.write_bytes(content)
|
||||||
|
return run_inp(model_name)
|
||||||
|
|
||||||
|
|
||||||
|
async def _update_from_inp(content: bytes) -> None:
|
||||||
|
temp_path: Path | None = None
|
||||||
|
try:
|
||||||
|
with NamedTemporaryFile(suffix=".inp", delete=False) as temp_file:
|
||||||
|
temp_file.write(content)
|
||||||
|
temp_path = Path(temp_file.name)
|
||||||
|
network_update(str(temp_path))
|
||||||
|
finally:
|
||||||
|
if temp_path is not None:
|
||||||
|
temp_path.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
async def _apply_model_update(content: bytes) -> None:
|
||||||
|
try:
|
||||||
|
await _update_from_inp(content)
|
||||||
|
except Exception as exc:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail=f"数据库操作失败: {exc}",
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/admin/projects/{project_id}/model-imports",
|
||||||
|
summary="导入桌面端水力模型",
|
||||||
|
)
|
||||||
|
async def import_project_model(
|
||||||
|
request: Request,
|
||||||
|
project_id: UUID = ApiPath(...),
|
||||||
|
file: UploadFile = File(..., description="桌面端导出的 INP 模型文件"),
|
||||||
|
current_user=Depends(get_current_metadata_admin),
|
||||||
|
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
||||||
|
) -> dict:
|
||||||
|
project = await _get_active_project(project_id, metadata_repo)
|
||||||
|
content, filename = await _read_upload(file)
|
||||||
|
result = await _run_uploaded_inp(content)
|
||||||
|
await _audit_model_change(
|
||||||
|
request=request,
|
||||||
|
current_user=current_user,
|
||||||
|
metadata_repo=metadata_repo,
|
||||||
|
project_id=project.id,
|
||||||
|
action="import",
|
||||||
|
)
|
||||||
|
return {"project_id": str(project.id), "filename": filename, "result": result}
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch(
|
||||||
|
"/admin/projects/{project_id}/model-imports",
|
||||||
|
summary="更新桌面端水力模型",
|
||||||
|
)
|
||||||
|
async def update_project_model(
|
||||||
|
request: Request,
|
||||||
|
project_id: UUID = ApiPath(...),
|
||||||
|
file: UploadFile = File(..., description="桌面端导出的 INP 模型文件"),
|
||||||
|
current_user=Depends(get_current_metadata_admin),
|
||||||
|
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
||||||
|
) -> dict:
|
||||||
|
project = await _get_active_project(project_id, metadata_repo)
|
||||||
|
content, filename = await _read_upload(file)
|
||||||
|
await _apply_model_update(content)
|
||||||
|
await _audit_model_change(
|
||||||
|
request=request,
|
||||||
|
current_user=current_user,
|
||||||
|
metadata_repo=metadata_repo,
|
||||||
|
project_id=project.id,
|
||||||
|
action="update",
|
||||||
|
)
|
||||||
|
return {"project_id": str(project.id), "filename": filename, "updated": True}
|
||||||
@@ -18,7 +18,7 @@ router = APIRouter()
|
|||||||
############################################################
|
############################################################
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getdemandschema",
|
"/network-schemas/demand",
|
||||||
summary="获取需水量属性架构",
|
summary="获取需水量属性架构",
|
||||||
description="获取指定水网中需水量(Demand)的属性架构定义"
|
description="获取指定水网中需水量(Demand)的属性架构定义"
|
||||||
)
|
)
|
||||||
@@ -32,7 +32,7 @@ async def fastapi_get_demand_schema(network: str = Query(..., description="管
|
|||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getdemandproperties/",
|
"/demands/properties",
|
||||||
summary="获取需水量属性",
|
summary="获取需水量属性",
|
||||||
description="获取指定水网中节点的需水量属性信息"
|
description="获取指定水网中节点的需水量属性信息"
|
||||||
)
|
)
|
||||||
@@ -49,8 +49,8 @@ async def fastapi_get_demand_properties(
|
|||||||
|
|
||||||
|
|
||||||
# example: set_demand(p, ChangeSet({'junction': 'j1', 'demands': [{'demand': 10.0, 'pattern': None, 'category': 'x'}, {'demand': 20.0, 'pattern': None, 'category': None}]}))
|
# example: set_demand(p, ChangeSet({'junction': 'j1', 'demands': [{'demand': 10.0, 'pattern': None, 'category': 'x'}, {'demand': 20.0, 'pattern': None, 'category': None}]}))
|
||||||
@router.post(
|
@router.patch(
|
||||||
"/setdemandproperties/",
|
"/demands/properties",
|
||||||
response_model=None,
|
response_model=None,
|
||||||
summary="设置需水量属性",
|
summary="设置需水量属性",
|
||||||
description="设置指定水网中节点的需水量属性信息"
|
description="设置指定水网中节点的需水量属性信息"
|
||||||
@@ -72,8 +72,8 @@ async def fastapi_set_demand_properties(
|
|||||||
############################################################
|
############################################################
|
||||||
# water distribution 36.[Water Distribution]
|
# water distribution 36.[Water Distribution]
|
||||||
############################################################
|
############################################################
|
||||||
@router.get(
|
@router.post(
|
||||||
"/calculatedemandtonodes/",
|
"/demands/to-nodes",
|
||||||
summary="计算需水量到节点分配",
|
summary="计算需水量到节点分配",
|
||||||
description="将总需水量按指定方式分配到多个节点"
|
description="将总需水量按指定方式分配到多个节点"
|
||||||
)
|
)
|
||||||
@@ -97,8 +97,8 @@ async def fastapi_calculate_demand_to_nodes(
|
|||||||
nodes = props["nodes"]
|
nodes = props["nodes"]
|
||||||
return calculate_demand_to_nodes(network, demand, nodes)
|
return calculate_demand_to_nodes(network, demand, nodes)
|
||||||
|
|
||||||
@router.get(
|
@router.post(
|
||||||
"/calculatedemandtoregion/",
|
"/demands/to-region",
|
||||||
summary="计算需水量到区域分配",
|
summary="计算需水量到区域分配",
|
||||||
description="将总需水量按区域特征分配到该区域内的节点"
|
description="将总需水量按区域特征分配到该区域内的节点"
|
||||||
)
|
)
|
||||||
@@ -122,8 +122,8 @@ async def fastapi_calculate_demand_to_region(
|
|||||||
region = props["region"]
|
region = props["region"]
|
||||||
return calculate_demand_to_region(network, demand, region)
|
return calculate_demand_to_region(network, demand, region)
|
||||||
|
|
||||||
@router.get(
|
@router.post(
|
||||||
"/calculatedemandtonetwork/",
|
"/demands/to-network",
|
||||||
summary="计算需水量到整网分配",
|
summary="计算需水量到整网分配",
|
||||||
description="将需水量均匀分配到整个水网的所有需水节点"
|
description="将需水量均匀分配到整个水网的所有需水节点"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ router = APIRouter()
|
|||||||
############################################################
|
############################################################
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/isnode/",
|
"/nodes/existence",
|
||||||
summary="检查节点有效性",
|
summary="检查节点有效性",
|
||||||
description="检查指定ID是否为水网中的有效节点"
|
description="检查指定ID是否为水网中的有效节点"
|
||||||
)
|
)
|
||||||
@@ -57,7 +57,7 @@ async def fastapi_is_node(
|
|||||||
return is_node(network, node)
|
return is_node(network, node)
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/isjunction/",
|
"/junctions/existence",
|
||||||
summary="检查是否为接点",
|
summary="检查是否为接点",
|
||||||
description="检查指定ID是否为水网中的接点(需求点)"
|
description="检查指定ID是否为水网中的接点(需求点)"
|
||||||
)
|
)
|
||||||
@@ -69,7 +69,7 @@ async def fastapi_is_junction(
|
|||||||
return is_junction(network, node)
|
return is_junction(network, node)
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/isreservoir/",
|
"/reservoirs/existence",
|
||||||
summary="检查是否为水源",
|
summary="检查是否为水源",
|
||||||
description="检查指定ID是否为水网中的水源(水库/河流)"
|
description="检查指定ID是否为水网中的水源(水库/河流)"
|
||||||
)
|
)
|
||||||
@@ -81,7 +81,7 @@ async def fastapi_is_reservoir(
|
|||||||
return is_reservoir(network, node)
|
return is_reservoir(network, node)
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/istank/",
|
"/tanks/existence",
|
||||||
summary="检查是否为蓄水池",
|
summary="检查是否为蓄水池",
|
||||||
description="检查指定ID是否为水网中的蓄水池"
|
description="检查指定ID是否为水网中的蓄水池"
|
||||||
)
|
)
|
||||||
@@ -93,7 +93,7 @@ async def fastapi_is_tank(
|
|||||||
return is_tank(network, node)
|
return is_tank(network, node)
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/islink/",
|
"/links/existence",
|
||||||
summary="检查管线有效性",
|
summary="检查管线有效性",
|
||||||
description="检查指定ID是否为水网中的有效管线"
|
description="检查指定ID是否为水网中的有效管线"
|
||||||
)
|
)
|
||||||
@@ -105,7 +105,7 @@ async def fastapi_is_link(
|
|||||||
return is_link(network, link)
|
return is_link(network, link)
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/ispipe/",
|
"/pipes/existence",
|
||||||
summary="检查是否为管道",
|
summary="检查是否为管道",
|
||||||
description="检查指定ID是否为水网中的管道"
|
description="检查指定ID是否为水网中的管道"
|
||||||
)
|
)
|
||||||
@@ -117,7 +117,7 @@ async def fastapi_is_pipe(
|
|||||||
return is_pipe(network, link)
|
return is_pipe(network, link)
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/ispump/",
|
"/pumps/existence",
|
||||||
summary="检查是否为泵",
|
summary="检查是否为泵",
|
||||||
description="检查指定ID是否为水网中的泵"
|
description="检查指定ID是否为水网中的泵"
|
||||||
)
|
)
|
||||||
@@ -129,7 +129,7 @@ async def fastapi_is_pump(
|
|||||||
return is_pump(network, link)
|
return is_pump(network, link)
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/isvalve/",
|
"/valves/existence",
|
||||||
summary="检查是否为阀门",
|
summary="检查是否为阀门",
|
||||||
description="检查指定ID是否为水网中的阀门"
|
description="检查指定ID是否为水网中的阀门"
|
||||||
)
|
)
|
||||||
@@ -141,7 +141,7 @@ async def fastapi_is_valve(
|
|||||||
return is_valve(network, link)
|
return is_valve(network, link)
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getnodetype/",
|
"/node-types",
|
||||||
summary="获取节点类型",
|
summary="获取节点类型",
|
||||||
description="获取指定节点的类型(接点/水源/蓄水池)"
|
description="获取指定节点的类型(接点/水源/蓄水池)"
|
||||||
)
|
)
|
||||||
@@ -153,7 +153,7 @@ async def fastapi_get_node_type(
|
|||||||
return get_node_type(network, node)
|
return get_node_type(network, node)
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getlinktype/",
|
"/link-types",
|
||||||
summary="获取管线类型",
|
summary="获取管线类型",
|
||||||
description="获取指定管线的类型(管道/泵/阀门)"
|
description="获取指定管线的类型(管道/泵/阀门)"
|
||||||
)
|
)
|
||||||
@@ -165,7 +165,7 @@ async def fastapi_get_link_type(
|
|||||||
return get_link_type(network, link)
|
return get_link_type(network, link)
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getelementtype/",
|
"/element-types",
|
||||||
summary="获取元素类型",
|
summary="获取元素类型",
|
||||||
description="获取指定元素的类型(节点或管线)"
|
description="获取指定元素的类型(节点或管线)"
|
||||||
)
|
)
|
||||||
@@ -177,7 +177,7 @@ async def fastapi_get_element_type(
|
|||||||
return get_element_type(network, element)
|
return get_element_type(network, element)
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getelementtypevalue/",
|
"/element-type-values",
|
||||||
summary="获取元素类型值",
|
summary="获取元素类型值",
|
||||||
description="获取指定元素的类型数值标识"
|
description="获取指定元素的类型数值标识"
|
||||||
)
|
)
|
||||||
@@ -189,7 +189,7 @@ async def fastapi_get_element_type_value(
|
|||||||
return get_element_type_value(network, element)
|
return get_element_type_value(network, element)
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getnodes/",
|
"/nodes",
|
||||||
summary="获取所有节点",
|
summary="获取所有节点",
|
||||||
description="获取指定水网中的所有节点ID列表"
|
description="获取指定水网中的所有节点ID列表"
|
||||||
)
|
)
|
||||||
@@ -198,7 +198,7 @@ async def fastapi_get_nodes(network: str = Query(..., description="管网名称
|
|||||||
return get_nodes(network)
|
return get_nodes(network)
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getlinks/",
|
"/links",
|
||||||
summary="获取所有管线",
|
summary="获取所有管线",
|
||||||
description="获取指定水网中的所有管线ID列表"
|
description="获取指定水网中的所有管线ID列表"
|
||||||
)
|
)
|
||||||
@@ -207,7 +207,7 @@ async def fastapi_get_links(network: str = Query(..., description="管网名称
|
|||||||
return get_links(network)
|
return get_links(network)
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getnodelinks/",
|
"/node-links",
|
||||||
summary="获取节点的关联管线",
|
summary="获取节点的关联管线",
|
||||||
description="获取指定节点连接的所有管线ID列表"
|
description="获取指定节点连接的所有管线ID列表"
|
||||||
)
|
)
|
||||||
@@ -223,7 +223,7 @@ def get_node_links_endpoint(
|
|||||||
############################################################
|
############################################################
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getnodeproperties/",
|
"/node-properties",
|
||||||
summary="获取节点属性",
|
summary="获取节点属性",
|
||||||
description="获取指定节点的所有属性信息"
|
description="获取指定节点的所有属性信息"
|
||||||
)
|
)
|
||||||
@@ -235,7 +235,7 @@ async def fast_get_node_properties(
|
|||||||
return get_node_properties(network, node)
|
return get_node_properties(network, node)
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getlinkproperties/",
|
"/link-properties",
|
||||||
summary="获取管线属性",
|
summary="获取管线属性",
|
||||||
description="获取指定管线的所有属性信息"
|
description="获取指定管线的所有属性信息"
|
||||||
)
|
)
|
||||||
@@ -247,7 +247,7 @@ async def fast_get_link_properties(
|
|||||||
return get_link_properties(network, link)
|
return get_link_properties(network, link)
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getscadaproperties/",
|
"/scada-properties",
|
||||||
summary="获取SCADA点属性",
|
summary="获取SCADA点属性",
|
||||||
description="获取指定SCADA点的属性信息"
|
description="获取指定SCADA点的属性信息"
|
||||||
)
|
)
|
||||||
@@ -259,7 +259,7 @@ async def fast_get_scada_properties(
|
|||||||
return get_scada_info(network, scada)
|
return get_scada_info(network, scada)
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getallscadaproperties/",
|
"/all-scada-properties",
|
||||||
summary="获取所有SCADA点属性",
|
summary="获取所有SCADA点属性",
|
||||||
description="获取指定水网中所有SCADA点的属性信息"
|
description="获取指定水网中所有SCADA点的属性信息"
|
||||||
)
|
)
|
||||||
@@ -270,7 +270,7 @@ async def fast_get_all_scada_properties(
|
|||||||
return get_all_scada_info(network)
|
return get_all_scada_info(network)
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getelementpropertieswithtype/",
|
"/element-properties-with-types",
|
||||||
summary="获取指定类型元素属性",
|
summary="获取指定类型元素属性",
|
||||||
description="获取指定类型的元素属性信息"
|
description="获取指定类型的元素属性信息"
|
||||||
)
|
)
|
||||||
@@ -283,7 +283,7 @@ async def fast_get_element_properties_with_type(
|
|||||||
return get_element_properties_with_type(network, elementtype, element)
|
return get_element_properties_with_type(network, elementtype, element)
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getelementproperties/",
|
"/element-properties",
|
||||||
summary="获取元素属性",
|
summary="获取元素属性",
|
||||||
description="获取指定元素的属性信息"
|
description="获取指定元素的属性信息"
|
||||||
)
|
)
|
||||||
@@ -299,7 +299,7 @@ async def fast_get_element_properties(
|
|||||||
############################################################
|
############################################################
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/gettitleschema/",
|
"/title-schemas",
|
||||||
summary="获取标题属性架构",
|
summary="获取标题属性架构",
|
||||||
description="获取指定水网的标题(标题)属性架构定义"
|
description="获取指定水网的标题(标题)属性架构定义"
|
||||||
)
|
)
|
||||||
@@ -310,7 +310,7 @@ async def fast_get_title_schema(
|
|||||||
return get_title_schema(network)
|
return get_title_schema(network)
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/gettitle/",
|
"/titles",
|
||||||
summary="获取水网标题属性",
|
summary="获取水网标题属性",
|
||||||
description="获取指定水网的标题(Title)信息"
|
description="获取指定水网的标题(Title)信息"
|
||||||
)
|
)
|
||||||
@@ -318,8 +318,8 @@ async def fast_get_title(network: str = Query(..., description="管网名称(
|
|||||||
"""获取水网的标题属性。"""
|
"""获取水网的标题属性。"""
|
||||||
return get_title(network)
|
return get_title(network)
|
||||||
|
|
||||||
@router.get(
|
@router.patch(
|
||||||
"/settitle/",
|
"/titles",
|
||||||
response_model=None,
|
response_model=None,
|
||||||
summary="设置水网标题属性",
|
summary="设置水网标题属性",
|
||||||
description="设置指定水网的标题(Title)信息"
|
description="设置指定水网的标题(Title)信息"
|
||||||
@@ -337,7 +337,7 @@ async def fastapi_set_title(
|
|||||||
############################################################
|
############################################################
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getstatusschema",
|
"/status-schemas",
|
||||||
summary="获取状态属性架构",
|
summary="获取状态属性架构",
|
||||||
description="获取指定水网的状态(Status)属性架构定义"
|
description="获取指定水网的状态(Status)属性架构定义"
|
||||||
)
|
)
|
||||||
@@ -348,7 +348,7 @@ async def fastapi_get_status_schema(
|
|||||||
return get_status_schema(network)
|
return get_status_schema(network)
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getstatus/",
|
"/status",
|
||||||
summary="获取管线状态",
|
summary="获取管线状态",
|
||||||
description="获取指定管线的状态信息"
|
description="获取指定管线的状态信息"
|
||||||
)
|
)
|
||||||
@@ -359,8 +359,8 @@ async def fastapi_get_status(
|
|||||||
"""获取管线的状态属性。"""
|
"""获取管线的状态属性。"""
|
||||||
return get_status(network, link)
|
return get_status(network, link)
|
||||||
|
|
||||||
@router.post(
|
@router.patch(
|
||||||
"/setstatus/",
|
"/status-properties",
|
||||||
response_model=None,
|
response_model=None,
|
||||||
summary="设置管线状态",
|
summary="设置管线状态",
|
||||||
description="设置指定管线的状态信息"
|
description="设置指定管线的状态信息"
|
||||||
@@ -379,8 +379,8 @@ async def fastapi_set_status_properties(
|
|||||||
# General Deletion
|
# General Deletion
|
||||||
############################################################
|
############################################################
|
||||||
|
|
||||||
@router.post(
|
@router.delete(
|
||||||
"/deletenode/",
|
"/nodes",
|
||||||
response_model=None,
|
response_model=None,
|
||||||
summary="删除节点",
|
summary="删除节点",
|
||||||
description="删除指定的节点(接点/水源/蓄水池)"
|
description="删除指定的节点(接点/水源/蓄水池)"
|
||||||
@@ -399,8 +399,8 @@ async def fastapi_delete_node(
|
|||||||
return delete_tank(network, ChangeSet(ps))
|
return delete_tank(network, ChangeSet(ps))
|
||||||
return ChangeSet() # Should probably raise error or return empty
|
return ChangeSet() # Should probably raise error or return empty
|
||||||
|
|
||||||
@router.post(
|
@router.delete(
|
||||||
"/deletelink/",
|
"/links",
|
||||||
response_model=None,
|
response_model=None,
|
||||||
summary="删除管线",
|
summary="删除管线",
|
||||||
description="删除指定的管线(管道/泵/阀门)"
|
description="删除指定的管线(管道/泵/阀门)"
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
from fastapi import APIRouter, Request, Depends, Query, Path, Body
|
from typing import Any
|
||||||
from typing import Any, List, Dict, Union
|
|
||||||
|
from fastapi import APIRouter, Query
|
||||||
|
|
||||||
from app.services.tjnetwork import (
|
from app.services.tjnetwork import (
|
||||||
Any,
|
|
||||||
get_all_scada_info,
|
|
||||||
get_major_node_coords,
|
get_major_node_coords,
|
||||||
get_major_pipe_nodes,
|
get_major_pipe_nodes,
|
||||||
get_network_in_extent,
|
get_network_in_extent,
|
||||||
get_network_link_nodes,
|
get_network_link_nodes,
|
||||||
get_network_node_coords,
|
|
||||||
get_node_coord,
|
get_node_coord,
|
||||||
)
|
)
|
||||||
from app.auth.metadata_dependencies import get_current_metadata_user
|
|
||||||
from app.infra.cache.redis_client import redis_client, encode_datetime, decode_datetime
|
|
||||||
import msgpack
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -35,7 +31,7 @@ router = APIRouter()
|
|||||||
# return set_coord(network, ChangeSet(props))
|
# return set_coord(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getnodecoord/",
|
"/node-coords",
|
||||||
summary="获取节点坐标",
|
summary="获取节点坐标",
|
||||||
description="获取指定节点的地理坐标(X, Y)"
|
description="获取指定节点的地理坐标(X, Y)"
|
||||||
)
|
)
|
||||||
@@ -48,7 +44,7 @@ async def fastapi_get_node_coord(
|
|||||||
|
|
||||||
# Additional geometry queries found in main.py logic (implicit or explicit)
|
# Additional geometry queries found in main.py logic (implicit or explicit)
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getnetworkinextent/",
|
"/network-in-extents",
|
||||||
summary="获取范围内的网络元素",
|
summary="获取范围内的网络元素",
|
||||||
description="获取指定地理范围内的网络节点和管线"
|
description="获取指定地理范围内的网络节点和管线"
|
||||||
)
|
)
|
||||||
@@ -63,34 +59,7 @@ async def fastapi_get_network_in_extent(
|
|||||||
return get_network_in_extent(network, x1, y1, x2, y2)
|
return get_network_in_extent(network, x1, y1, x2, y2)
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getnetworkgeometries/",
|
"/majornode-coords",
|
||||||
dependencies=[Depends(get_current_metadata_user)],
|
|
||||||
summary="获取完整网络几何信息",
|
|
||||||
description="获取整个水网的所有节点、管线和SCADA点的几何信息(需要身份验证)"
|
|
||||||
)
|
|
||||||
async def fastapi_get_network_geometries(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
|
||||||
) -> dict[str, Any] | None:
|
|
||||||
"""获取完整的网络几何信息,包括所有节点、管线和SCADA点。结果从缓存返回。"""
|
|
||||||
cache_key = f"getnetworkgeometries_{network}"
|
|
||||||
data = redis_client.get(cache_key)
|
|
||||||
if data:
|
|
||||||
loaded_dict = msgpack.unpackb(data, object_hook=decode_datetime)
|
|
||||||
return loaded_dict
|
|
||||||
|
|
||||||
coords = get_network_node_coords(network)
|
|
||||||
nodes = []
|
|
||||||
for node_id, coord in coords.items():
|
|
||||||
nodes.append(f"{node_id}:{coord['type']}:{coord['x']}:{coord['y']}")
|
|
||||||
links = get_network_link_nodes(network)
|
|
||||||
scadas = get_all_scada_info(network)
|
|
||||||
|
|
||||||
results = {"nodes": nodes, "links": links, "scadas": scadas}
|
|
||||||
redis_client.set(cache_key, msgpack.packb(results, default=encode_datetime))
|
|
||||||
return results
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/getmajornodecoords/",
|
|
||||||
summary="获取主要节点坐标",
|
summary="获取主要节点坐标",
|
||||||
description="获取直径大于等于指定值的节点坐标"
|
description="获取直径大于等于指定值的节点坐标"
|
||||||
)
|
)
|
||||||
@@ -102,7 +71,7 @@ async def fastapi_get_majornode_coords(
|
|||||||
return get_major_node_coords(network, diameter)
|
return get_major_node_coords(network, diameter)
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getmajorpipenodes/",
|
"/major-pipe-nodes",
|
||||||
summary="获取主要管道节点",
|
summary="获取主要管道节点",
|
||||||
description="获取直径大于等于指定值的管道的节点ID"
|
description="获取直径大于等于指定值的管道的节点ID"
|
||||||
)
|
)
|
||||||
@@ -114,7 +83,7 @@ async def fastapi_get_major_pipe_nodes(
|
|||||||
return get_major_pipe_nodes(network, diameter)
|
return get_major_pipe_nodes(network, diameter)
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getnetworklinknodes/",
|
"/network-link-nodes",
|
||||||
summary="获取网络管线节点",
|
summary="获取网络管线节点",
|
||||||
description="获取指定水网所有管线的起点和终点节点"
|
description="获取指定水网所有管线的起点和终点节点"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from app.services.tjnetwork import (
|
|||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.get("/getjunctionschema", summary="获取节点架构", description="获取指定项目的节点属性架构和数据类型定义。")
|
@router.get("/network-schemas/junction", summary="获取节点架构", description="获取指定项目的节点属性架构和数据类型定义。")
|
||||||
async def fast_get_junction_schema(
|
async def fast_get_junction_schema(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||||
) -> dict[str, dict[str, Any]]:
|
) -> dict[str, dict[str, Any]]:
|
||||||
@@ -27,7 +27,7 @@ async def fast_get_junction_schema(
|
|||||||
"""
|
"""
|
||||||
return get_junction_schema(network)
|
return get_junction_schema(network)
|
||||||
|
|
||||||
@router.post("/addjunction/", response_model=None, summary="添加节点", description="在供水网络中添加新的节点,指定节点ID和空间坐标。")
|
@router.post("/junctions", response_model=None, summary="添加节点", description="在供水网络中添加新的节点,指定节点ID和空间坐标。")
|
||||||
async def fastapi_add_junction(
|
async def fastapi_add_junction(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
junction: str = Query(..., description="节点 ID"),
|
junction: str = Query(..., description="节点 ID"),
|
||||||
@@ -51,7 +51,7 @@ async def fastapi_add_junction(
|
|||||||
ps = {"id": junction, "x": x, "y": y, "elevation": z}
|
ps = {"id": junction, "x": x, "y": y, "elevation": z}
|
||||||
return add_junction(network, ChangeSet(ps))
|
return add_junction(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.post("/deletejunction/", response_model=None, summary="删除节点", description="从供水网络中删除指定的节点。")
|
@router.delete("/junctions", response_model=None, summary="删除节点", description="从供水网络中删除指定的节点。")
|
||||||
async def fastapi_delete_junction(
|
async def fastapi_delete_junction(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
junction: str = Query(..., description="节点 ID")
|
junction: str = Query(..., description="节点 ID")
|
||||||
@@ -69,7 +69,7 @@ async def fastapi_delete_junction(
|
|||||||
ps = {"id": junction}
|
ps = {"id": junction}
|
||||||
return delete_junction(network, ChangeSet(ps))
|
return delete_junction(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.get("/getjunctionelevation/", summary="获取节点标高", description="获取指定节点的标高(海拔高度)。")
|
@router.get("/junctions/elevation", summary="获取节点标高", description="获取指定节点的标高(海拔高度)。")
|
||||||
async def fastapi_get_junction_elevation(
|
async def fastapi_get_junction_elevation(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
junction: str = Query(..., description="节点 ID")
|
junction: str = Query(..., description="节点 ID")
|
||||||
@@ -87,7 +87,7 @@ async def fastapi_get_junction_elevation(
|
|||||||
ps = get_junction(network, junction)
|
ps = get_junction(network, junction)
|
||||||
return ps["elevation"]
|
return ps["elevation"]
|
||||||
|
|
||||||
@router.get("/getjunctionx/", summary="获取节点 X 坐标", description="获取指定节点的 X 坐标值。")
|
@router.get("/junctions/x", summary="获取节点 X 坐标", description="获取指定节点的 X 坐标值。")
|
||||||
async def fastapi_get_junction_x(
|
async def fastapi_get_junction_x(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
junction: str = Query(..., description="节点 ID")
|
junction: str = Query(..., description="节点 ID")
|
||||||
@@ -105,7 +105,7 @@ async def fastapi_get_junction_x(
|
|||||||
ps = get_junction(network, junction)
|
ps = get_junction(network, junction)
|
||||||
return ps["x"]
|
return ps["x"]
|
||||||
|
|
||||||
@router.get("/getjunctiony/", summary="获取节点 Y 坐标", description="获取指定节点的 Y 坐标值。")
|
@router.get("/junctions/y", summary="获取节点 Y 坐标", description="获取指定节点的 Y 坐标值。")
|
||||||
async def fastapi_get_junction_y(
|
async def fastapi_get_junction_y(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
junction: str = Query(..., description="节点 ID")
|
junction: str = Query(..., description="节点 ID")
|
||||||
@@ -123,7 +123,7 @@ async def fastapi_get_junction_y(
|
|||||||
ps = get_junction(network, junction)
|
ps = get_junction(network, junction)
|
||||||
return ps["y"]
|
return ps["y"]
|
||||||
|
|
||||||
@router.get("/getjunctioncoord/", summary="获取节点坐标", description="获取指定节点的 X 和 Y 坐标。")
|
@router.get("/junctions/coord", summary="获取节点坐标", description="获取指定节点的 X 和 Y 坐标。")
|
||||||
async def fastapi_get_junction_coord(
|
async def fastapi_get_junction_coord(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
junction: str = Query(..., description="节点 ID")
|
junction: str = Query(..., description="节点 ID")
|
||||||
@@ -142,7 +142,7 @@ async def fastapi_get_junction_coord(
|
|||||||
coord = {"x": ps["x"], "y": ps["y"]}
|
coord = {"x": ps["x"], "y": ps["y"]}
|
||||||
return coord
|
return coord
|
||||||
|
|
||||||
@router.get("/getjunctiondemand/", summary="获取节点需水量", description="获取指定节点的需水量。")
|
@router.get("/junctions/demand", summary="获取节点需水量", description="获取指定节点的需水量。")
|
||||||
async def fastapi_get_junction_demand(
|
async def fastapi_get_junction_demand(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
junction: str = Query(..., description="节点 ID")
|
junction: str = Query(..., description="节点 ID")
|
||||||
@@ -160,7 +160,7 @@ async def fastapi_get_junction_demand(
|
|||||||
ps = get_junction(network, junction)
|
ps = get_junction(network, junction)
|
||||||
return ps["demand"]
|
return ps["demand"]
|
||||||
|
|
||||||
@router.get("/getjunctionpattern/", summary="获取节点需水模式", description="获取指定节点的需水模式标识。")
|
@router.get("/junctions/pattern", summary="获取节点需水模式", description="获取指定节点的需水模式标识。")
|
||||||
async def fastapi_get_junction_pattern(
|
async def fastapi_get_junction_pattern(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
junction: str = Query(..., description="节点 ID")
|
junction: str = Query(..., description="节点 ID")
|
||||||
@@ -178,7 +178,7 @@ async def fastapi_get_junction_pattern(
|
|||||||
ps = get_junction(network, junction)
|
ps = get_junction(network, junction)
|
||||||
return ps["pattern"]
|
return ps["pattern"]
|
||||||
|
|
||||||
@router.post("/setjunctionelevation/", response_model=None, summary="设置节点标高", description="设置指定节点的标高值。")
|
@router.patch("/junctions/elevation", response_model=None, summary="设置节点标高", description="设置指定节点的标高值。")
|
||||||
async def fastapi_set_junction_elevation(
|
async def fastapi_set_junction_elevation(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
junction: str = Query(..., description="节点 ID"),
|
junction: str = Query(..., description="节点 ID"),
|
||||||
@@ -198,7 +198,7 @@ async def fastapi_set_junction_elevation(
|
|||||||
ps = {"id": junction, "elevation": elevation}
|
ps = {"id": junction, "elevation": elevation}
|
||||||
return set_junction(network, ChangeSet(ps))
|
return set_junction(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.post("/setjunctionx/", response_model=None, summary="设置节点 X 坐标", description="设置指定节点的 X 坐标值。")
|
@router.patch("/junctions/x", response_model=None, summary="设置节点 X 坐标", description="设置指定节点的 X 坐标值。")
|
||||||
async def fastapi_set_junction_x(
|
async def fastapi_set_junction_x(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
junction: str = Query(..., description="节点 ID"),
|
junction: str = Query(..., description="节点 ID"),
|
||||||
@@ -218,7 +218,7 @@ async def fastapi_set_junction_x(
|
|||||||
ps = {"id": junction, "x": x}
|
ps = {"id": junction, "x": x}
|
||||||
return set_junction(network, ChangeSet(ps))
|
return set_junction(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.post("/setjunctiony/", response_model=None, summary="设置节点 Y 坐标", description="设置指定节点的 Y 坐标值。")
|
@router.patch("/junctions/y", response_model=None, summary="设置节点 Y 坐标", description="设置指定节点的 Y 坐标值。")
|
||||||
async def fastapi_set_junction_y(
|
async def fastapi_set_junction_y(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
junction: str = Query(..., description="节点 ID"),
|
junction: str = Query(..., description="节点 ID"),
|
||||||
@@ -238,7 +238,7 @@ async def fastapi_set_junction_y(
|
|||||||
ps = {"id": junction, "y": y}
|
ps = {"id": junction, "y": y}
|
||||||
return set_junction(network, ChangeSet(ps))
|
return set_junction(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.post("/setjunctioncoord/", response_model=None, summary="设置节点坐标", description="设置指定节点的 X 和 Y 坐标。")
|
@router.patch("/junctions/coord", response_model=None, summary="设置节点坐标", description="设置指定节点的 X 和 Y 坐标。")
|
||||||
async def fastapi_set_junction_coord(
|
async def fastapi_set_junction_coord(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
junction: str = Query(..., description="节点 ID"),
|
junction: str = Query(..., description="节点 ID"),
|
||||||
@@ -260,7 +260,7 @@ async def fastapi_set_junction_coord(
|
|||||||
ps = {"id": junction, "x": x, "y": y}
|
ps = {"id": junction, "x": x, "y": y}
|
||||||
return set_junction(network, ChangeSet(ps))
|
return set_junction(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.post("/setjunctiondemand/", response_model=None, summary="设置节点需水量", description="设置指定节点的需水量。")
|
@router.patch("/junctions/demand", response_model=None, summary="设置节点需水量", description="设置指定节点的需水量。")
|
||||||
async def fastapi_set_junction_demand(
|
async def fastapi_set_junction_demand(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
junction: str = Query(..., description="节点 ID"),
|
junction: str = Query(..., description="节点 ID"),
|
||||||
@@ -280,7 +280,7 @@ async def fastapi_set_junction_demand(
|
|||||||
ps = {"id": junction, "demand": demand}
|
ps = {"id": junction, "demand": demand}
|
||||||
return set_junction(network, ChangeSet(ps))
|
return set_junction(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.post("/setjunctionpattern/", response_model=None, summary="设置节点需水模式", description="设置指定节点的需水模式标识。")
|
@router.patch("/junctions/pattern", response_model=None, summary="设置节点需水模式", description="设置指定节点的需水模式标识。")
|
||||||
async def fastapi_set_junction_pattern(
|
async def fastapi_set_junction_pattern(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
junction: str = Query(..., description="节点 ID"),
|
junction: str = Query(..., description="节点 ID"),
|
||||||
@@ -300,7 +300,7 @@ async def fastapi_set_junction_pattern(
|
|||||||
ps = {"id": junction, "pattern": pattern}
|
ps = {"id": junction, "pattern": pattern}
|
||||||
return set_junction(network, ChangeSet(ps))
|
return set_junction(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.get("/getjunctionproperties/", summary="获取节点属性", description="获取指定节点的所有属性信息。")
|
@router.get("/junctions/properties", summary="获取节点属性", description="获取指定节点的所有属性信息。")
|
||||||
async def fastapi_get_junction_properties(
|
async def fastapi_get_junction_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
junction: str = Query(..., description="节点 ID")
|
junction: str = Query(..., description="节点 ID")
|
||||||
@@ -317,7 +317,7 @@ async def fastapi_get_junction_properties(
|
|||||||
"""
|
"""
|
||||||
return get_junction(network, junction)
|
return get_junction(network, junction)
|
||||||
|
|
||||||
@router.get("/getalljunctionproperties/", summary="获取所有节点属性", description="获取指定项目中所有节点的属性信息。")
|
@router.get("/junctions", summary="获取所有节点属性", description="获取指定项目中所有节点的属性信息。")
|
||||||
async def fastapi_get_all_junction_properties(
|
async def fastapi_get_all_junction_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
@@ -337,7 +337,7 @@ async def fastapi_get_all_junction_properties(
|
|||||||
results = get_all_junctions(network)
|
results = get_all_junctions(network)
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@router.post("/setjunctionproperties/", response_model=None, summary="批量设置节点属性", description="批量设置指定节点的多个属性。")
|
@router.patch("/junctions/properties", response_model=None, summary="批量设置节点属性", description="批量设置指定节点的多个属性。")
|
||||||
async def fastapi_set_junction_properties(
|
async def fastapi_set_junction_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
junction: str = Query(..., description="节点 ID"),
|
junction: str = Query(..., description="节点 ID"),
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from app.services.tjnetwork import (
|
|||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.get("/getpipeschema", summary="获取管道模式", description="获取管道对象的模式定义,包含所有可用字段及其类型")
|
@router.get("/network-schemas/pipe", summary="获取管道模式", description="获取管道对象的模式定义,包含所有可用字段及其类型")
|
||||||
async def fastapi_get_pipe_schema(
|
async def fastapi_get_pipe_schema(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||||
) -> dict[str, dict[str, Any]]:
|
) -> dict[str, dict[str, Any]]:
|
||||||
@@ -29,7 +29,7 @@ async def fastapi_get_pipe_schema(
|
|||||||
"""
|
"""
|
||||||
return get_pipe_schema(network)
|
return get_pipe_schema(network)
|
||||||
|
|
||||||
@router.post("/addpipe/", response_model=None, summary="添加管道", description="向网络中添加新的管道,需要提供管道的基本参数如长度、管径、粗糙度等")
|
@router.post("/pipes", response_model=None, summary="添加管道", description="向网络中添加新的管道,需要提供管道的基本参数如长度、管径、粗糙度等")
|
||||||
async def fastapi_add_pipe(
|
async def fastapi_add_pipe(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pipe: str = Query(..., description="管道标识符"),
|
pipe: str = Query(..., description="管道标识符"),
|
||||||
@@ -70,7 +70,7 @@ async def fastapi_add_pipe(
|
|||||||
}
|
}
|
||||||
return add_pipe(network, ChangeSet(ps))
|
return add_pipe(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.post("/deletepipe/", response_model=None, summary="删除管道", description="从网络中删除指定的管道")
|
@router.delete("/pipes", response_model=None, summary="删除管道", description="从网络中删除指定的管道")
|
||||||
async def fastapi_delete_pipe(
|
async def fastapi_delete_pipe(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pipe: str = Query(..., description="要删除的管道ID")
|
pipe: str = Query(..., description="要删除的管道ID")
|
||||||
@@ -88,7 +88,7 @@ async def fastapi_delete_pipe(
|
|||||||
ps = {"id": pipe}
|
ps = {"id": pipe}
|
||||||
return delete_pipe(network, ChangeSet(ps))
|
return delete_pipe(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.get("/getpipenode1/", summary="获取管道起始节点", description="获取指定管道的起始节点ID")
|
@router.get("/pipes/node1", summary="获取管道起始节点", description="获取指定管道的起始节点ID")
|
||||||
async def fastapi_get_pipe_node1(
|
async def fastapi_get_pipe_node1(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pipe: str = Query(..., description="管道ID")
|
pipe: str = Query(..., description="管道ID")
|
||||||
@@ -106,7 +106,7 @@ async def fastapi_get_pipe_node1(
|
|||||||
ps = get_pipe(network, pipe)
|
ps = get_pipe(network, pipe)
|
||||||
return ps["node1"]
|
return ps["node1"]
|
||||||
|
|
||||||
@router.get("/getpipenode2/", summary="获取管道终止节点", description="获取指定管道的终止节点ID")
|
@router.get("/pipes/node2", summary="获取管道终止节点", description="获取指定管道的终止节点ID")
|
||||||
async def fastapi_get_pipe_node2(
|
async def fastapi_get_pipe_node2(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pipe: str = Query(..., description="管道ID")
|
pipe: str = Query(..., description="管道ID")
|
||||||
@@ -124,7 +124,7 @@ async def fastapi_get_pipe_node2(
|
|||||||
ps = get_pipe(network, pipe)
|
ps = get_pipe(network, pipe)
|
||||||
return ps["node2"]
|
return ps["node2"]
|
||||||
|
|
||||||
@router.get("/getpipelength/", summary="获取管道长度", description="获取指定管道的长度")
|
@router.get("/pipes/length", summary="获取管道长度", description="获取指定管道的长度")
|
||||||
async def fastapi_get_pipe_length(
|
async def fastapi_get_pipe_length(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pipe: str = Query(..., description="管道ID")
|
pipe: str = Query(..., description="管道ID")
|
||||||
@@ -142,7 +142,7 @@ async def fastapi_get_pipe_length(
|
|||||||
ps = get_pipe(network, pipe)
|
ps = get_pipe(network, pipe)
|
||||||
return ps["length"]
|
return ps["length"]
|
||||||
|
|
||||||
@router.get("/getpipediameter/", summary="获取管道管径", description="获取指定管道的管径")
|
@router.get("/pipes/diameter", summary="获取管道管径", description="获取指定管道的管径")
|
||||||
async def fastapi_get_pipe_diameter(
|
async def fastapi_get_pipe_diameter(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pipe: str = Query(..., description="管道ID")
|
pipe: str = Query(..., description="管道ID")
|
||||||
@@ -160,7 +160,7 @@ async def fastapi_get_pipe_diameter(
|
|||||||
ps = get_pipe(network, pipe)
|
ps = get_pipe(network, pipe)
|
||||||
return ps["diameter"]
|
return ps["diameter"]
|
||||||
|
|
||||||
@router.get("/getpiperoughness/", summary="获取管道粗糙度", description="获取指定管道的粗糙度")
|
@router.get("/pipes/roughness", summary="获取管道粗糙度", description="获取指定管道的粗糙度")
|
||||||
async def fastapi_get_pipe_roughness(
|
async def fastapi_get_pipe_roughness(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pipe: str = Query(..., description="管道ID")
|
pipe: str = Query(..., description="管道ID")
|
||||||
@@ -178,7 +178,7 @@ async def fastapi_get_pipe_roughness(
|
|||||||
ps = get_pipe(network, pipe)
|
ps = get_pipe(network, pipe)
|
||||||
return ps["roughness"]
|
return ps["roughness"]
|
||||||
|
|
||||||
@router.get("/getpipeminorloss/", summary="获取管道局部阻力系数", description="获取指定管道的局部阻力系数")
|
@router.get("/pipes/minor-loss", summary="获取管道局部阻力系数", description="获取指定管道的局部阻力系数")
|
||||||
async def fastapi_get_pipe_minor_loss(
|
async def fastapi_get_pipe_minor_loss(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pipe: str = Query(..., description="管道ID")
|
pipe: str = Query(..., description="管道ID")
|
||||||
@@ -196,7 +196,7 @@ async def fastapi_get_pipe_minor_loss(
|
|||||||
ps = get_pipe(network, pipe)
|
ps = get_pipe(network, pipe)
|
||||||
return ps["minor_loss"]
|
return ps["minor_loss"]
|
||||||
|
|
||||||
@router.get("/getpipestatus/", summary="获取管道状态", description="获取指定管道的状态(开启或关闭)")
|
@router.get("/pipes/status", summary="获取管道状态", description="获取指定管道的状态(开启或关闭)")
|
||||||
async def fastapi_get_pipe_status(
|
async def fastapi_get_pipe_status(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pipe: str = Query(..., description="管道ID")
|
pipe: str = Query(..., description="管道ID")
|
||||||
@@ -214,7 +214,7 @@ async def fastapi_get_pipe_status(
|
|||||||
ps = get_pipe(network, pipe)
|
ps = get_pipe(network, pipe)
|
||||||
return ps["status"]
|
return ps["status"]
|
||||||
|
|
||||||
@router.post("/setpipenode1/", response_model=None, summary="设置管道起始节点", description="设置指定管道的起始节点")
|
@router.patch("/pipes/node1", response_model=None, summary="设置管道起始节点", description="设置指定管道的起始节点")
|
||||||
async def fastapi_set_pipe_node1(
|
async def fastapi_set_pipe_node1(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pipe: str = Query(..., description="管道ID"),
|
pipe: str = Query(..., description="管道ID"),
|
||||||
@@ -234,7 +234,7 @@ async def fastapi_set_pipe_node1(
|
|||||||
ps = {"id": pipe, "node1": node1}
|
ps = {"id": pipe, "node1": node1}
|
||||||
return set_pipe(network, ChangeSet(ps))
|
return set_pipe(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.post("/setpipenode2/", response_model=None, summary="设置管道终止节点", description="设置指定管道的终止节点")
|
@router.patch("/pipes/node2", response_model=None, summary="设置管道终止节点", description="设置指定管道的终止节点")
|
||||||
async def fastapi_set_pipe_node2(
|
async def fastapi_set_pipe_node2(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pipe: str = Query(..., description="管道ID"),
|
pipe: str = Query(..., description="管道ID"),
|
||||||
@@ -254,7 +254,7 @@ async def fastapi_set_pipe_node2(
|
|||||||
ps = {"id": pipe, "node2": node2}
|
ps = {"id": pipe, "node2": node2}
|
||||||
return set_pipe(network, ChangeSet(ps))
|
return set_pipe(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.post("/setpipelength/", response_model=None, summary="设置管道长度", description="设置指定管道的长度")
|
@router.patch("/pipes/length", response_model=None, summary="设置管道长度", description="设置指定管道的长度")
|
||||||
async def fastapi_set_pipe_length(
|
async def fastapi_set_pipe_length(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pipe: str = Query(..., description="管道ID"),
|
pipe: str = Query(..., description="管道ID"),
|
||||||
@@ -274,7 +274,7 @@ async def fastapi_set_pipe_length(
|
|||||||
ps = {"id": pipe, "length": length}
|
ps = {"id": pipe, "length": length}
|
||||||
return set_pipe(network, ChangeSet(ps))
|
return set_pipe(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.post("/setpipediameter/", response_model=None, summary="设置管道管径", description="设置指定管道的管径")
|
@router.patch("/pipes/diameter", response_model=None, summary="设置管道管径", description="设置指定管道的管径")
|
||||||
async def fastapi_set_pipe_diameter(
|
async def fastapi_set_pipe_diameter(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pipe: str = Query(..., description="管道ID"),
|
pipe: str = Query(..., description="管道ID"),
|
||||||
@@ -294,7 +294,7 @@ async def fastapi_set_pipe_diameter(
|
|||||||
ps = {"id": pipe, "diameter": diameter}
|
ps = {"id": pipe, "diameter": diameter}
|
||||||
return set_pipe(network, ChangeSet(ps))
|
return set_pipe(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.post("/setpiperoughness/", response_model=None, summary="设置管道粗糙度", description="设置指定管道的粗糙度")
|
@router.patch("/pipes/roughness", response_model=None, summary="设置管道粗糙度", description="设置指定管道的粗糙度")
|
||||||
async def fastapi_set_pipe_roughness(
|
async def fastapi_set_pipe_roughness(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pipe: str = Query(..., description="管道ID"),
|
pipe: str = Query(..., description="管道ID"),
|
||||||
@@ -314,7 +314,7 @@ async def fastapi_set_pipe_roughness(
|
|||||||
ps = {"id": pipe, "roughness": roughness}
|
ps = {"id": pipe, "roughness": roughness}
|
||||||
return set_pipe(network, ChangeSet(ps))
|
return set_pipe(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.post("/setpipeminorloss/", response_model=None, summary="设置管道局部阻力系数", description="设置指定管道的局部阻力系数")
|
@router.patch("/pipes/minor-loss", response_model=None, summary="设置管道局部阻力系数", description="设置指定管道的局部阻力系数")
|
||||||
async def fastapi_set_pipe_minor_loss(
|
async def fastapi_set_pipe_minor_loss(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pipe: str = Query(..., description="管道ID"),
|
pipe: str = Query(..., description="管道ID"),
|
||||||
@@ -334,7 +334,7 @@ async def fastapi_set_pipe_minor_loss(
|
|||||||
ps = {"id": pipe, "minor_loss": minor_loss}
|
ps = {"id": pipe, "minor_loss": minor_loss}
|
||||||
return set_pipe(network, ChangeSet(ps))
|
return set_pipe(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.post("/setpipestatus/", response_model=None, summary="设置管道状态", description="设置指定管道的状态(开启或关闭)")
|
@router.patch("/pipes/status", response_model=None, summary="设置管道状态", description="设置指定管道的状态(开启或关闭)")
|
||||||
async def fastapi_set_pipe_status(
|
async def fastapi_set_pipe_status(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pipe: str = Query(..., description="管道ID"),
|
pipe: str = Query(..., description="管道ID"),
|
||||||
@@ -354,7 +354,7 @@ async def fastapi_set_pipe_status(
|
|||||||
ps = {"id": pipe, "status": status}
|
ps = {"id": pipe, "status": status}
|
||||||
return set_pipe(network, ChangeSet(ps))
|
return set_pipe(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.get("/getpipeproperties/", summary="获取管道属性", description="获取指定管道的所有属性信息")
|
@router.get("/pipes/properties", summary="获取管道属性", description="获取指定管道的所有属性信息")
|
||||||
async def fastapi_get_pipe_properties(
|
async def fastapi_get_pipe_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pipe: str = Query(..., description="管道ID")
|
pipe: str = Query(..., description="管道ID")
|
||||||
@@ -371,7 +371,7 @@ async def fastapi_get_pipe_properties(
|
|||||||
"""
|
"""
|
||||||
return get_pipe(network, pipe)
|
return get_pipe(network, pipe)
|
||||||
|
|
||||||
@router.get("/getallpipeproperties/", summary="获取所有管道属性", description="获取网络中所有管道的属性信息列表")
|
@router.get("/pipes", summary="获取所有管道属性", description="获取网络中所有管道的属性信息列表")
|
||||||
async def fastapi_get_all_pipe_properties(
|
async def fastapi_get_all_pipe_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
@@ -389,7 +389,7 @@ async def fastapi_get_all_pipe_properties(
|
|||||||
results = get_all_pipes(network)
|
results = get_all_pipes(network)
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@router.post("/setpipeproperties/", response_model=None, summary="设置管道属性", description="批量设置指定管道的多个属性")
|
@router.patch("/pipes/properties", response_model=None, summary="设置管道属性", description="批量设置指定管道的多个属性")
|
||||||
async def fastapi_set_pipe_properties(
|
async def fastapi_set_pipe_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pipe: str = Query(..., description="管道ID"),
|
pipe: str = Query(..., description="管道ID"),
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from app.services.tjnetwork import (
|
|||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.get("/getpumpschema", summary="获取水泵模式", description="获取水泵对象的模式定义,包含所有可用字段及其类型")
|
@router.get("/network-schemas/pump", summary="获取水泵模式", description="获取水泵对象的模式定义,包含所有可用字段及其类型")
|
||||||
async def fastapi_get_pump_schema(
|
async def fastapi_get_pump_schema(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||||
) -> dict[str, dict[str, Any]]:
|
) -> dict[str, dict[str, Any]]:
|
||||||
@@ -28,7 +28,7 @@ async def fastapi_get_pump_schema(
|
|||||||
"""
|
"""
|
||||||
return get_pump_schema(network)
|
return get_pump_schema(network)
|
||||||
|
|
||||||
@router.post("/addpump/", response_model=None, summary="添加水泵", description="向网络中添加新的水泵,需要提供水泵的基本参数如功率等")
|
@router.post("/pumps", response_model=None, summary="添加水泵", description="向网络中添加新的水泵,需要提供水泵的基本参数如功率等")
|
||||||
async def fastapi_add_pump(
|
async def fastapi_add_pump(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pump: str = Query(..., description="水泵标识符"),
|
pump: str = Query(..., description="水泵标识符"),
|
||||||
@@ -52,7 +52,7 @@ async def fastapi_add_pump(
|
|||||||
ps = {"id": pump, "node1": node1, "node2": node2, "power": power}
|
ps = {"id": pump, "node1": node1, "node2": node2, "power": power}
|
||||||
return add_pump(network, ChangeSet(ps))
|
return add_pump(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.post("/deletepump/", response_model=None, summary="删除水泵", description="从网络中删除指定的水泵")
|
@router.delete("/pumps", response_model=None, summary="删除水泵", description="从网络中删除指定的水泵")
|
||||||
async def fastapi_delete_pump(
|
async def fastapi_delete_pump(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pump: str = Query(..., description="要删除的水泵ID")
|
pump: str = Query(..., description="要删除的水泵ID")
|
||||||
@@ -70,7 +70,7 @@ async def fastapi_delete_pump(
|
|||||||
ps = {"id": pump}
|
ps = {"id": pump}
|
||||||
return delete_pump(network, ChangeSet(ps))
|
return delete_pump(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.get("/getpumpnode1/", summary="获取水泵起始节点", description="获取指定水泵的起始节点ID")
|
@router.get("/pumps/node1", summary="获取水泵起始节点", description="获取指定水泵的起始节点ID")
|
||||||
async def fastapi_get_pump_node1(
|
async def fastapi_get_pump_node1(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pump: str = Query(..., description="水泵ID")
|
pump: str = Query(..., description="水泵ID")
|
||||||
@@ -88,7 +88,7 @@ async def fastapi_get_pump_node1(
|
|||||||
ps = get_pump(network, pump)
|
ps = get_pump(network, pump)
|
||||||
return ps["node1"]
|
return ps["node1"]
|
||||||
|
|
||||||
@router.get("/getpumpnode2/", summary="获取水泵终止节点", description="获取指定水泵的终止节点ID")
|
@router.get("/pumps/node2", summary="获取水泵终止节点", description="获取指定水泵的终止节点ID")
|
||||||
async def fastapi_get_pump_node2(
|
async def fastapi_get_pump_node2(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pump: str = Query(..., description="水泵ID")
|
pump: str = Query(..., description="水泵ID")
|
||||||
@@ -106,7 +106,7 @@ async def fastapi_get_pump_node2(
|
|||||||
ps = get_pump(network, pump)
|
ps = get_pump(network, pump)
|
||||||
return ps["node2"]
|
return ps["node2"]
|
||||||
|
|
||||||
@router.post("/setpumpnode1/", response_model=None, summary="设置水泵起始节点", description="设置指定水泵的起始节点")
|
@router.patch("/pumps/node1", response_model=None, summary="设置水泵起始节点", description="设置指定水泵的起始节点")
|
||||||
async def fastapi_set_pump_node1(
|
async def fastapi_set_pump_node1(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pump: str = Query(..., description="水泵ID"),
|
pump: str = Query(..., description="水泵ID"),
|
||||||
@@ -126,7 +126,7 @@ async def fastapi_set_pump_node1(
|
|||||||
ps = {"id": pump, "node1": node1}
|
ps = {"id": pump, "node1": node1}
|
||||||
return set_pump(network, ChangeSet(ps))
|
return set_pump(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.post("/setpumpnode2/", response_model=None, summary="设置水泵终止节点", description="设置指定水泵的终止节点")
|
@router.patch("/pumps/node2", response_model=None, summary="设置水泵终止节点", description="设置指定水泵的终止节点")
|
||||||
async def fastapi_set_pump_node2(
|
async def fastapi_set_pump_node2(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pump: str = Query(..., description="水泵ID"),
|
pump: str = Query(..., description="水泵ID"),
|
||||||
@@ -146,7 +146,7 @@ async def fastapi_set_pump_node2(
|
|||||||
ps = {"id": pump, "node2": node2}
|
ps = {"id": pump, "node2": node2}
|
||||||
return set_pump(network, ChangeSet(ps))
|
return set_pump(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.get("/getpumpproperties/", summary="获取水泵属性", description="获取指定水泵的所有属性信息")
|
@router.get("/pumps/properties", summary="获取水泵属性", description="获取指定水泵的所有属性信息")
|
||||||
async def fastapi_get_pump_properties(
|
async def fastapi_get_pump_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pump: str = Query(..., description="水泵ID")
|
pump: str = Query(..., description="水泵ID")
|
||||||
@@ -163,7 +163,7 @@ async def fastapi_get_pump_properties(
|
|||||||
"""
|
"""
|
||||||
return get_pump(network, pump)
|
return get_pump(network, pump)
|
||||||
|
|
||||||
@router.get("/getallpumpproperties/", summary="获取所有水泵属性", description="获取网络中所有水泵的属性信息列表")
|
@router.get("/pumps", summary="获取所有水泵属性", description="获取网络中所有水泵的属性信息列表")
|
||||||
async def fastapi_get_all_pump_properties(
|
async def fastapi_get_all_pump_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
@@ -181,7 +181,7 @@ async def fastapi_get_all_pump_properties(
|
|||||||
results = get_all_pumps(network)
|
results = get_all_pumps(network)
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@router.post("/setpumpproperties/", response_model=None, summary="设置水泵属性", description="批量设置指定水泵的多个属性")
|
@router.patch("/pumps/properties", response_model=None, summary="设置水泵属性", description="批量设置指定水泵的多个属性")
|
||||||
async def fastapi_set_pump_properties(
|
async def fastapi_set_pump_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pump: str = Query(..., description="水泵ID"),
|
pump: str = Query(..., description="水泵ID"),
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ router = APIRouter()
|
|||||||
############################################################
|
############################################################
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getregionschema/",
|
"/network-schemas/region",
|
||||||
summary="获取区域属性架构",
|
summary="获取区域属性架构",
|
||||||
description="获取指定水网的区域属性架构定义"
|
description="获取指定水网的区域属性架构定义"
|
||||||
)
|
)
|
||||||
@@ -56,7 +56,7 @@ async def fastapi_get_region_schema(
|
|||||||
return get_region_schema(network)
|
return get_region_schema(network)
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getregion/",
|
"/regions/detail",
|
||||||
summary="获取区域信息",
|
summary="获取区域信息",
|
||||||
description="获取指定ID的区域详细信息"
|
description="获取指定ID的区域详细信息"
|
||||||
)
|
)
|
||||||
@@ -67,8 +67,8 @@ async def fastapi_get_region(
|
|||||||
"""获取区域的详细信息。"""
|
"""获取区域的详细信息。"""
|
||||||
return get_region(network, id)
|
return get_region(network, id)
|
||||||
|
|
||||||
@router.post(
|
@router.patch(
|
||||||
"/setregion/",
|
"/regions",
|
||||||
response_model=None,
|
response_model=None,
|
||||||
summary="设置区域属性",
|
summary="设置区域属性",
|
||||||
description="修改指定区域的属性信息"
|
description="修改指定区域的属性信息"
|
||||||
@@ -82,7 +82,7 @@ async def fastapi_set_region(
|
|||||||
return set_region(network, ChangeSet(props))
|
return set_region(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/addregion/",
|
"/regions",
|
||||||
response_model=None,
|
response_model=None,
|
||||||
summary="添加新区域",
|
summary="添加新区域",
|
||||||
description="向水网添加一个新的区域"
|
description="向水网添加一个新的区域"
|
||||||
@@ -95,8 +95,8 @@ async def fastapi_add_region(
|
|||||||
props = await req.json()
|
props = await req.json()
|
||||||
return add_region(network, ChangeSet(props))
|
return add_region(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.post(
|
@router.delete(
|
||||||
"/deleteregion/",
|
"/regions",
|
||||||
response_model=None,
|
response_model=None,
|
||||||
summary="删除区域",
|
summary="删除区域",
|
||||||
description="删除指定的区域"
|
description="删除指定的区域"
|
||||||
@@ -114,8 +114,8 @@ async def fastapi_delete_region(
|
|||||||
# district_metering_area 33
|
# district_metering_area 33
|
||||||
############################################################
|
############################################################
|
||||||
|
|
||||||
@router.get(
|
@router.post(
|
||||||
"/calculatedistrictmeteringareaforregion/",
|
"/district-metering-areas/for-region",
|
||||||
summary="计算区域内DMA分区",
|
summary="计算区域内DMA分区",
|
||||||
description="为指定区域计算区域计量(DMA)分区方案"
|
description="为指定区域计算区域计量(DMA)分区方案"
|
||||||
)
|
)
|
||||||
@@ -141,8 +141,8 @@ async def fastapi_calculate_district_metering_area_for_region(
|
|||||||
network, region, part_count, part_type
|
network, region, part_count, part_type
|
||||||
)
|
)
|
||||||
|
|
||||||
@router.get(
|
@router.post(
|
||||||
"/calculatedistrictmeteringareafornetwork/",
|
"/district-metering-areas/for-network",
|
||||||
summary="计算整网DMA分区",
|
summary="计算整网DMA分区",
|
||||||
description="为整个水网计算区域计量(DMA)分区方案"
|
description="为整个水网计算区域计量(DMA)分区方案"
|
||||||
)
|
)
|
||||||
@@ -165,7 +165,7 @@ async def fastapi_calculate_district_metering_area_for_network(
|
|||||||
return calculate_district_metering_area_for_network(network, part_count, part_type)
|
return calculate_district_metering_area_for_network(network, part_count, part_type)
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getdistrictmeteringareaschema/",
|
"/network-schemas/district-metering-area",
|
||||||
summary="获取DMA属性架构",
|
summary="获取DMA属性架构",
|
||||||
description="获取指定水网的区域计量(DMA)属性架构定义"
|
description="获取指定水网的区域计量(DMA)属性架构定义"
|
||||||
)
|
)
|
||||||
@@ -176,7 +176,7 @@ async def fastapi_get_district_metering_area_schema(
|
|||||||
return get_district_metering_area_schema(network)
|
return get_district_metering_area_schema(network)
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getdistrictmeteringarea/",
|
"/district-metering-areas/detail",
|
||||||
summary="获取DMA信息",
|
summary="获取DMA信息",
|
||||||
description="获取指定ID的区域计量(DMA)详细信息"
|
description="获取指定ID的区域计量(DMA)详细信息"
|
||||||
)
|
)
|
||||||
@@ -187,8 +187,8 @@ async def fastapi_get_district_metering_area(
|
|||||||
"""获取DMA的详细信息。"""
|
"""获取DMA的详细信息。"""
|
||||||
return get_district_metering_area(network, id)
|
return get_district_metering_area(network, id)
|
||||||
|
|
||||||
@router.post(
|
@router.patch(
|
||||||
"/setdistrictmeteringarea/",
|
"/district-metering-areas",
|
||||||
response_model=None,
|
response_model=None,
|
||||||
summary="设置DMA属性",
|
summary="设置DMA属性",
|
||||||
description="修改指定DMA的属性信息"
|
description="修改指定DMA的属性信息"
|
||||||
@@ -202,7 +202,7 @@ async def fastapi_set_district_metering_area(
|
|||||||
return set_district_metering_area(network, ChangeSet(props))
|
return set_district_metering_area(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/adddistrictmeteringarea/",
|
"/district-metering-areas",
|
||||||
response_model=None,
|
response_model=None,
|
||||||
summary="添加新DMA",
|
summary="添加新DMA",
|
||||||
description="向水网添加一个新的区域计量(DMA)"
|
description="向水网添加一个新的区域计量(DMA)"
|
||||||
@@ -222,8 +222,8 @@ async def fastapi_add_district_metering_area(
|
|||||||
props["boundary"] = newBoundary
|
props["boundary"] = newBoundary
|
||||||
return add_district_metering_area(network, ChangeSet(props))
|
return add_district_metering_area(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.post(
|
@router.delete(
|
||||||
"/deletedistrictmeteringarea/",
|
"/district-metering-areas",
|
||||||
response_model=None,
|
response_model=None,
|
||||||
summary="删除DMA",
|
summary="删除DMA",
|
||||||
description="删除指定的区域计量(DMA)"
|
description="删除指定的区域计量(DMA)"
|
||||||
@@ -237,7 +237,7 @@ async def fastapi_delete_district_metering_area(
|
|||||||
return delete_district_metering_area(network, ChangeSet(props))
|
return delete_district_metering_area(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getalldistrictmeteringareaids/",
|
"/district-metering-areas/ids",
|
||||||
summary="获取所有DMA ID",
|
summary="获取所有DMA ID",
|
||||||
description="获取指定水网中所有DMA的ID列表"
|
description="获取指定水网中所有DMA的ID列表"
|
||||||
)
|
)
|
||||||
@@ -248,7 +248,7 @@ async def fastapi_get_all_district_metering_area_ids(
|
|||||||
return get_all_district_metering_area_ids(network)
|
return get_all_district_metering_area_ids(network)
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getalldistrictmeteringareas/",
|
"/district-metering-areas",
|
||||||
summary="获取所有DMA",
|
summary="获取所有DMA",
|
||||||
description="获取指定水网中所有DMA的详细信息"
|
description="获取指定水网中所有DMA的详细信息"
|
||||||
)
|
)
|
||||||
@@ -259,7 +259,7 @@ async def getalldistrictmeteringareas(
|
|||||||
return get_all_district_metering_areas(network)
|
return get_all_district_metering_areas(network)
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/generatedistrictmeteringarea/",
|
"/district-metering-area-generation-runs",
|
||||||
response_model=None,
|
response_model=None,
|
||||||
summary="生成DMA分区",
|
summary="生成DMA分区",
|
||||||
description="根据参数自动生成水网的DMA分区方案"
|
description="根据参数自动生成水网的DMA分区方案"
|
||||||
@@ -276,7 +276,7 @@ async def fastapi_generate_district_metering_area(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/generatesubdistrictmeteringarea/",
|
"/sub-district-metering-areas",
|
||||||
response_model=None,
|
response_model=None,
|
||||||
summary="生成DMA子分区",
|
summary="生成DMA子分区",
|
||||||
description="为指定DMA生成子DMA分区"
|
description="为指定DMA生成子DMA分区"
|
||||||
@@ -298,8 +298,8 @@ async def fastapi_generate_sub_district_metering_area(
|
|||||||
# service_area 34
|
# service_area 34
|
||||||
############################################################
|
############################################################
|
||||||
|
|
||||||
@router.get(
|
@router.post(
|
||||||
"/calculateservicearea/",
|
"/service-area-calculations",
|
||||||
summary="计算服务区",
|
summary="计算服务区",
|
||||||
description="计算指定水网的服务区分区,返回全部时间步结果"
|
description="计算指定水网的服务区分区,返回全部时间步结果"
|
||||||
)
|
)
|
||||||
@@ -310,7 +310,7 @@ async def fastapi_calculate_service_area(
|
|||||||
return calculate_service_area(network)
|
return calculate_service_area(network)
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getserviceareaschema/",
|
"/network-schemas/service-area",
|
||||||
summary="获取服务区属性架构",
|
summary="获取服务区属性架构",
|
||||||
description="获取指定水网的服务区属性架构定义"
|
description="获取指定水网的服务区属性架构定义"
|
||||||
)
|
)
|
||||||
@@ -321,7 +321,7 @@ async def fastapi_get_service_area_schema(
|
|||||||
return get_service_area_schema(network)
|
return get_service_area_schema(network)
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getservicearea/",
|
"/service-areas/detail",
|
||||||
summary="获取服务区信息",
|
summary="获取服务区信息",
|
||||||
description="获取指定ID的服务区详细信息"
|
description="获取指定ID的服务区详细信息"
|
||||||
)
|
)
|
||||||
@@ -332,8 +332,8 @@ async def fastapi_get_service_area(
|
|||||||
"""获取服务区的详细信息。"""
|
"""获取服务区的详细信息。"""
|
||||||
return get_service_area(network, id)
|
return get_service_area(network, id)
|
||||||
|
|
||||||
@router.post(
|
@router.patch(
|
||||||
"/setservicearea/",
|
"/service-areas",
|
||||||
response_model=None,
|
response_model=None,
|
||||||
summary="设置服务区属性",
|
summary="设置服务区属性",
|
||||||
description="修改指定服务区的属性信息"
|
description="修改指定服务区的属性信息"
|
||||||
@@ -347,7 +347,7 @@ async def fastapi_set_service_area(
|
|||||||
return set_service_area(network, ChangeSet(props))
|
return set_service_area(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/addservicearea/",
|
"/service-areas",
|
||||||
response_model=None,
|
response_model=None,
|
||||||
summary="添加新服务区",
|
summary="添加新服务区",
|
||||||
description="向水网添加一个新的服务区"
|
description="向水网添加一个新的服务区"
|
||||||
@@ -360,8 +360,8 @@ async def fastapi_add_service_area(
|
|||||||
props = await req.json()
|
props = await req.json()
|
||||||
return add_service_area(network, ChangeSet(props))
|
return add_service_area(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.post(
|
@router.delete(
|
||||||
"/deleteservicearea/",
|
"/service-areas",
|
||||||
response_model=None,
|
response_model=None,
|
||||||
summary="删除服务区",
|
summary="删除服务区",
|
||||||
description="删除指定的服务区"
|
description="删除指定的服务区"
|
||||||
@@ -375,7 +375,7 @@ async def fastapi_delete_service_area(
|
|||||||
return delete_service_area(network, ChangeSet(props))
|
return delete_service_area(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getallserviceareas/",
|
"/service-areas",
|
||||||
summary="获取所有服务区",
|
summary="获取所有服务区",
|
||||||
description="获取指定水网中的所有服务区信息"
|
description="获取指定水网中的所有服务区信息"
|
||||||
)
|
)
|
||||||
@@ -386,7 +386,7 @@ async def fastapi_get_all_service_areas(
|
|||||||
return get_all_service_areas(network)
|
return get_all_service_areas(network)
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/generateservicearea/",
|
"/service-area-generation-runs",
|
||||||
response_model=None,
|
response_model=None,
|
||||||
summary="生成服务区分区",
|
summary="生成服务区分区",
|
||||||
description="根据参数自动生成水网的服务区分区"
|
description="根据参数自动生成水网的服务区分区"
|
||||||
@@ -403,8 +403,8 @@ async def fastapi_generate_service_area(
|
|||||||
# virtual_district 35
|
# virtual_district 35
|
||||||
############################################################
|
############################################################
|
||||||
|
|
||||||
@router.get(
|
@router.post(
|
||||||
"/calculatevirtualdistrict/",
|
"/virtual-district-calculations",
|
||||||
summary="计算虚拟分区",
|
summary="计算虚拟分区",
|
||||||
description="根据指定的压力监测节点作为中心节点计算虚拟分区方案"
|
description="根据指定的压力监测节点作为中心节点计算虚拟分区方案"
|
||||||
)
|
)
|
||||||
@@ -416,7 +416,7 @@ async def fastapi_calculate_virtual_district(
|
|||||||
return calculate_virtual_district(network, centers)
|
return calculate_virtual_district(network, centers)
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getvirtualdistrictschema/",
|
"/network-schemas/virtual-district",
|
||||||
summary="获取虚拟分区属性架构",
|
summary="获取虚拟分区属性架构",
|
||||||
description="获取指定水网的虚拟分区属性架构定义"
|
description="获取指定水网的虚拟分区属性架构定义"
|
||||||
)
|
)
|
||||||
@@ -427,7 +427,7 @@ async def fastapi_get_virtual_district_schema(
|
|||||||
return get_virtual_district_schema(network)
|
return get_virtual_district_schema(network)
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getvirtualdistrict/",
|
"/virtual-districts/detail",
|
||||||
summary="获取虚拟分区信息",
|
summary="获取虚拟分区信息",
|
||||||
description="获取指定ID的虚拟分区详细信息"
|
description="获取指定ID的虚拟分区详细信息"
|
||||||
)
|
)
|
||||||
@@ -438,8 +438,8 @@ async def fastapi_get_virtual_district(
|
|||||||
"""获取虚拟分区的详细信息。"""
|
"""获取虚拟分区的详细信息。"""
|
||||||
return get_virtual_district(network, id)
|
return get_virtual_district(network, id)
|
||||||
|
|
||||||
@router.post(
|
@router.patch(
|
||||||
"/setvirtualdistrict/",
|
"/virtual-districts",
|
||||||
response_model=None,
|
response_model=None,
|
||||||
summary="设置虚拟分区属性",
|
summary="设置虚拟分区属性",
|
||||||
description="修改指定虚拟分区的属性信息"
|
description="修改指定虚拟分区的属性信息"
|
||||||
@@ -453,7 +453,7 @@ async def fastapi_set_virtual_district(
|
|||||||
return set_virtual_district(network, ChangeSet(props))
|
return set_virtual_district(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/addvirtualdistrict/",
|
"/virtual-districts",
|
||||||
response_model=None,
|
response_model=None,
|
||||||
summary="添加新虚拟分区",
|
summary="添加新虚拟分区",
|
||||||
description="向水网添加一个新的虚拟分区"
|
description="向水网添加一个新的虚拟分区"
|
||||||
@@ -466,8 +466,8 @@ async def fastapi_add_virtual_district(
|
|||||||
props = await req.json()
|
props = await req.json()
|
||||||
return add_virtual_district(network, ChangeSet(props))
|
return add_virtual_district(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.post(
|
@router.delete(
|
||||||
"/deletevirtualdistrict/",
|
"/virtual-districts",
|
||||||
response_model=None,
|
response_model=None,
|
||||||
summary="删除虚拟分区",
|
summary="删除虚拟分区",
|
||||||
description="删除指定的虚拟分区"
|
description="删除指定的虚拟分区"
|
||||||
@@ -481,7 +481,7 @@ async def fastapi_delete_virtual_district(
|
|||||||
return delete_virtual_district(network, ChangeSet(props))
|
return delete_virtual_district(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getallvirtualdistrict/",
|
"/virtual-districts",
|
||||||
summary="获取所有虚拟分区",
|
summary="获取所有虚拟分区",
|
||||||
description="获取指定水网中的所有虚拟分区信息"
|
description="获取指定水网中的所有虚拟分区信息"
|
||||||
)
|
)
|
||||||
@@ -492,7 +492,7 @@ async def fastapi_get_all_virtual_district(
|
|||||||
return get_all_virtual_districts(network)
|
return get_all_virtual_districts(network)
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/generatevirtualdistrict/",
|
"/virtual-district-generation-runs",
|
||||||
response_model=None,
|
response_model=None,
|
||||||
summary="生成虚拟分区",
|
summary="生成虚拟分区",
|
||||||
description="根据参数自动生成虚拟分区方案"
|
description="根据参数自动生成虚拟分区方案"
|
||||||
@@ -506,8 +506,8 @@ async def fastapi_generate_virtual_district(
|
|||||||
props = await req.json()
|
props = await req.json()
|
||||||
return generate_virtual_district(network, props["centers"], inflate_delta)
|
return generate_virtual_district(network, props["centers"], inflate_delta)
|
||||||
|
|
||||||
@router.get(
|
@router.post(
|
||||||
"/calculatedistrictmeteringareafornodes/",
|
"/district-metering-areas/for-nodes",
|
||||||
summary="计算节点DMA分区",
|
summary="计算节点DMA分区",
|
||||||
description="为指定节点集计算区域计量(DMA)分区方案"
|
description="为指定节点集计算区域计量(DMA)分区方案"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from app.services.tjnetwork import (
|
|||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getreservoirschema",
|
"/network-schemas/reservoir",
|
||||||
summary="获取水库模式",
|
summary="获取水库模式",
|
||||||
description="获取指定供水网络中所有水库的模式/属性字段定义"
|
description="获取指定供水网络中所有水库的模式/属性字段定义"
|
||||||
)
|
)
|
||||||
@@ -35,7 +35,7 @@ async def fast_get_reservoir_schema(
|
|||||||
return get_reservoir_schema(network)
|
return get_reservoir_schema(network)
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/addreservoir/",
|
"/reservoirs",
|
||||||
response_model=None,
|
response_model=None,
|
||||||
summary="添加水库",
|
summary="添加水库",
|
||||||
description="在指定供水网络中添加新的水库/水源节点"
|
description="在指定供水网络中添加新的水库/水源节点"
|
||||||
@@ -65,8 +65,8 @@ async def fastapi_add_reservoir(
|
|||||||
ps = {"id": reservoir, "x": x, "y": y, "head": head}
|
ps = {"id": reservoir, "x": x, "y": y, "head": head}
|
||||||
return add_reservoir(network, ChangeSet(ps))
|
return add_reservoir(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.post(
|
@router.delete(
|
||||||
"/deletereservoir/",
|
"/reservoirs",
|
||||||
response_model=None,
|
response_model=None,
|
||||||
summary="删除水库",
|
summary="删除水库",
|
||||||
description="从指定供水网络中删除指定的水库/水源节点"
|
description="从指定供水网络中删除指定的水库/水源节点"
|
||||||
@@ -91,7 +91,7 @@ async def fastapi_delete_reservoir(
|
|||||||
return delete_reservoir(network, ChangeSet(ps))
|
return delete_reservoir(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getreservoirhead/",
|
"/reservoirs/head",
|
||||||
summary="获取水库水头",
|
summary="获取水库水头",
|
||||||
description="获取指定水库的供水水头/总水头值"
|
description="获取指定水库的供水水头/总水头值"
|
||||||
)
|
)
|
||||||
@@ -115,7 +115,7 @@ async def fastapi_get_reservoir_head(
|
|||||||
return ps["head"]
|
return ps["head"]
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getreservoirpattern/",
|
"/reservoirs/pattern",
|
||||||
summary="获取水库模式",
|
summary="获取水库模式",
|
||||||
description="获取指定水库的运行模式/供水模式"
|
description="获取指定水库的运行模式/供水模式"
|
||||||
)
|
)
|
||||||
@@ -139,7 +139,7 @@ async def fastapi_get_reservoir_pattern(
|
|||||||
return ps["pattern"]
|
return ps["pattern"]
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getreservoirx/",
|
"/reservoirs/x",
|
||||||
summary="获取水库X坐标",
|
summary="获取水库X坐标",
|
||||||
description="获取指定水库的X坐标位置"
|
description="获取指定水库的X坐标位置"
|
||||||
)
|
)
|
||||||
@@ -163,7 +163,7 @@ async def fastapi_get_reservoir_x(
|
|||||||
return ps["x"]
|
return ps["x"]
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getreservoiry/",
|
"/reservoirs/y",
|
||||||
summary="获取水库Y坐标",
|
summary="获取水库Y坐标",
|
||||||
description="获取指定水库的Y坐标位置"
|
description="获取指定水库的Y坐标位置"
|
||||||
)
|
)
|
||||||
@@ -187,7 +187,7 @@ async def fastapi_get_reservoir_y(
|
|||||||
return ps["y"]
|
return ps["y"]
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getreservoircoord/",
|
"/reservoirs/coord",
|
||||||
summary="获取水库坐标",
|
summary="获取水库坐标",
|
||||||
description="获取指定水库的平面坐标(X和Y坐标)"
|
description="获取指定水库的平面坐标(X和Y坐标)"
|
||||||
)
|
)
|
||||||
@@ -211,8 +211,8 @@ async def fastapi_get_reservoir_coord(
|
|||||||
coord = {"id": reservoir, "x": ps["x"], "y": ps["y"]}
|
coord = {"id": reservoir, "x": ps["x"], "y": ps["y"]}
|
||||||
return coord
|
return coord
|
||||||
|
|
||||||
@router.post(
|
@router.patch(
|
||||||
"/setreservoirhead/",
|
"/reservoirs/head",
|
||||||
response_model=None,
|
response_model=None,
|
||||||
summary="设置水库水头",
|
summary="设置水库水头",
|
||||||
description="更新指定水库的供水水头/总水头值"
|
description="更新指定水库的供水水头/总水头值"
|
||||||
@@ -238,8 +238,8 @@ async def fastapi_set_reservoir_head(
|
|||||||
ps = {"id": reservoir, "head": head}
|
ps = {"id": reservoir, "head": head}
|
||||||
return set_reservoir(network, ChangeSet(ps))
|
return set_reservoir(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.post(
|
@router.patch(
|
||||||
"/setreservoirpattern/",
|
"/reservoirs/pattern",
|
||||||
response_model=None,
|
response_model=None,
|
||||||
summary="设置水库模式",
|
summary="设置水库模式",
|
||||||
description="更新指定水库的运行模式/供水模式"
|
description="更新指定水库的运行模式/供水模式"
|
||||||
@@ -265,8 +265,8 @@ async def fastapi_set_reservoir_pattern(
|
|||||||
ps = {"id": reservoir, "pattern": pattern}
|
ps = {"id": reservoir, "pattern": pattern}
|
||||||
return set_reservoir(network, ChangeSet(ps))
|
return set_reservoir(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.post(
|
@router.patch(
|
||||||
"/setreservoirx/",
|
"/reservoirs/x",
|
||||||
response_model=None,
|
response_model=None,
|
||||||
summary="设置水库X坐标",
|
summary="设置水库X坐标",
|
||||||
description="更新指定水库的X坐标位置"
|
description="更新指定水库的X坐标位置"
|
||||||
@@ -292,8 +292,8 @@ async def fastapi_set_reservoir_x(
|
|||||||
ps = {"id": reservoir, "x": x}
|
ps = {"id": reservoir, "x": x}
|
||||||
return set_reservoir(network, ChangeSet(ps))
|
return set_reservoir(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.post(
|
@router.patch(
|
||||||
"/setreservoiry/",
|
"/reservoirs/y",
|
||||||
response_model=None,
|
response_model=None,
|
||||||
summary="设置水库Y坐标",
|
summary="设置水库Y坐标",
|
||||||
description="更新指定水库的Y坐标位置"
|
description="更新指定水库的Y坐标位置"
|
||||||
@@ -319,8 +319,8 @@ async def fastapi_set_reservoir_y(
|
|||||||
ps = {"id": reservoir, "y": y}
|
ps = {"id": reservoir, "y": y}
|
||||||
return set_reservoir(network, ChangeSet(ps))
|
return set_reservoir(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.post(
|
@router.patch(
|
||||||
"/setreservoircoord/",
|
"/reservoirs/coord",
|
||||||
response_model=None,
|
response_model=None,
|
||||||
summary="设置水库坐标",
|
summary="设置水库坐标",
|
||||||
description="更新指定水库的平面坐标(X和Y坐标)"
|
description="更新指定水库的平面坐标(X和Y坐标)"
|
||||||
@@ -349,7 +349,7 @@ async def fastapi_set_reservoir_coord(
|
|||||||
return set_reservoir(network, ChangeSet(ps))
|
return set_reservoir(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getreservoirproperties/",
|
"/reservoirs/properties",
|
||||||
summary="获取水库属性",
|
summary="获取水库属性",
|
||||||
description="获取指定水库的所有属性"
|
description="获取指定水库的所有属性"
|
||||||
)
|
)
|
||||||
@@ -372,7 +372,7 @@ async def fastapi_get_reservoir_properties(
|
|||||||
return get_reservoir(network, reservoir)
|
return get_reservoir(network, reservoir)
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getallreservoirproperties/",
|
"/reservoirs",
|
||||||
summary="获取所有水库属性",
|
summary="获取所有水库属性",
|
||||||
description="获取指定供水网络中所有水库的属性"
|
description="获取指定供水网络中所有水库的属性"
|
||||||
)
|
)
|
||||||
@@ -393,8 +393,8 @@ async def fastapi_get_all_reservoir_properties(
|
|||||||
results = get_all_reservoirs(network)
|
results = get_all_reservoirs(network)
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@router.post(
|
@router.patch(
|
||||||
"/setreservoirproperties/",
|
"/reservoirs/properties",
|
||||||
response_model=None,
|
response_model=None,
|
||||||
summary="设置水库属性",
|
summary="设置水库属性",
|
||||||
description="批量更新指定水库的多个属性"
|
description="批量更新指定水库的多个属性"
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ router = APIRouter()
|
|||||||
############################################################
|
############################################################
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/gettagschema/",
|
"/network-schemas/tag",
|
||||||
summary="获取标签属性架构",
|
summary="获取标签属性架构",
|
||||||
description="获取指定水网的标签(Tag)属性架构定义"
|
description="获取指定水网的标签(Tag)属性架构定义"
|
||||||
)
|
)
|
||||||
@@ -27,7 +27,7 @@ async def fastapi_get_tag_schema(
|
|||||||
return get_tag_schema(network)
|
return get_tag_schema(network)
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/gettag/",
|
"/tags/detail",
|
||||||
summary="获取标签信息",
|
summary="获取标签信息",
|
||||||
description="获取指定类型和ID的标签信息"
|
description="获取指定类型和ID的标签信息"
|
||||||
)
|
)
|
||||||
@@ -40,7 +40,7 @@ async def fastapi_get_tag(
|
|||||||
return get_tag(network, t_type, id)
|
return get_tag(network, t_type, id)
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/gettags/",
|
"/tags",
|
||||||
summary="获取所有标签",
|
summary="获取所有标签",
|
||||||
description="获取指定水网中的所有标签信息"
|
description="获取指定水网中的所有标签信息"
|
||||||
)
|
)
|
||||||
@@ -51,8 +51,8 @@ async def fastapi_get_tags(
|
|||||||
tags = get_tags(network)
|
tags = get_tags(network)
|
||||||
return tags
|
return tags
|
||||||
|
|
||||||
@router.post(
|
@router.patch(
|
||||||
"/settag/",
|
"/tags",
|
||||||
response_model=None,
|
response_model=None,
|
||||||
summary="设置标签",
|
summary="设置标签",
|
||||||
description="为指定元素设置或修改标签信息"
|
description="为指定元素设置或修改标签信息"
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from app.services.tjnetwork import (
|
|||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.get("/gettankschema", summary="获取水箱模式", description="获取指定网络的水箱数据结构模式定义")
|
@router.get("/network-schemas/tank", summary="获取水箱模式", description="获取指定网络的水箱数据结构模式定义")
|
||||||
async def fast_get_tank_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
async def fast_get_tank_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
获取水箱的数据结构模式。
|
获取水箱的数据结构模式。
|
||||||
@@ -26,7 +26,7 @@ async def fast_get_tank_schema(network: str = Query(..., description="管网名
|
|||||||
"""
|
"""
|
||||||
return get_tank_schema(network)
|
return get_tank_schema(network)
|
||||||
|
|
||||||
@router.post("/addtank/", summary="新增水箱", description="向指定网络中新增一个水箱", response_model=None)
|
@router.post("/tanks", summary="新增水箱", description="向指定网络中新增一个水箱", response_model=None)
|
||||||
async def fastapi_add_tank(
|
async def fastapi_add_tank(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID"),
|
tank: str = Query(..., description="水箱ID"),
|
||||||
@@ -70,7 +70,7 @@ async def fastapi_add_tank(
|
|||||||
}
|
}
|
||||||
return add_tank(network, ChangeSet(ps))
|
return add_tank(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.post("/deletetank/", summary="删除水箱", description="删除指定网络中的水箱", response_model=None)
|
@router.delete("/tanks", summary="删除水箱", description="删除指定网络中的水箱", response_model=None)
|
||||||
async def fastapi_delete_tank(
|
async def fastapi_delete_tank(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID")
|
tank: str = Query(..., description="水箱ID")
|
||||||
@@ -88,7 +88,7 @@ async def fastapi_delete_tank(
|
|||||||
ps = {"id": tank}
|
ps = {"id": tank}
|
||||||
return delete_tank(network, ChangeSet(ps))
|
return delete_tank(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.get("/gettankelevation/", summary="获取水箱标高", description="获取指定水箱的标高值")
|
@router.get("/tanks/elevation", summary="获取水箱标高", description="获取指定水箱的标高值")
|
||||||
async def fastapi_get_tank_elevation(
|
async def fastapi_get_tank_elevation(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID")
|
tank: str = Query(..., description="水箱ID")
|
||||||
@@ -106,7 +106,7 @@ async def fastapi_get_tank_elevation(
|
|||||||
ps = get_tank(network, tank)
|
ps = get_tank(network, tank)
|
||||||
return ps["elevation"]
|
return ps["elevation"]
|
||||||
|
|
||||||
@router.get("/gettankinitlevel/", summary="获取水箱初始水位", description="获取指定水箱的初始水位值")
|
@router.get("/tanks/init-level", summary="获取水箱初始水位", description="获取指定水箱的初始水位值")
|
||||||
async def fastapi_get_tank_init_level(
|
async def fastapi_get_tank_init_level(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID")
|
tank: str = Query(..., description="水箱ID")
|
||||||
@@ -124,7 +124,7 @@ async def fastapi_get_tank_init_level(
|
|||||||
ps = get_tank(network, tank)
|
ps = get_tank(network, tank)
|
||||||
return ps["init_level"]
|
return ps["init_level"]
|
||||||
|
|
||||||
@router.get("/gettankminlevel/", summary="获取水箱最小水位", description="获取指定水箱的最小水位值")
|
@router.get("/tanks/min-level", summary="获取水箱最小水位", description="获取指定水箱的最小水位值")
|
||||||
async def fastapi_get_tank_min_level(
|
async def fastapi_get_tank_min_level(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID")
|
tank: str = Query(..., description="水箱ID")
|
||||||
@@ -142,7 +142,7 @@ async def fastapi_get_tank_min_level(
|
|||||||
ps = get_tank(network, tank)
|
ps = get_tank(network, tank)
|
||||||
return ps["min_level"]
|
return ps["min_level"]
|
||||||
|
|
||||||
@router.get("/gettankmaxlevel/", summary="获取水箱最大水位", description="获取指定水箱的最大水位值")
|
@router.get("/tanks/max-level", summary="获取水箱最大水位", description="获取指定水箱的最大水位值")
|
||||||
async def fastapi_get_tank_max_level(
|
async def fastapi_get_tank_max_level(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID")
|
tank: str = Query(..., description="水箱ID")
|
||||||
@@ -160,7 +160,7 @@ async def fastapi_get_tank_max_level(
|
|||||||
ps = get_tank(network, tank)
|
ps = get_tank(network, tank)
|
||||||
return ps["max_level"]
|
return ps["max_level"]
|
||||||
|
|
||||||
@router.get("/gettankdiameter/", summary="获取水箱直径", description="获取指定水箱的直径值")
|
@router.get("/tanks/diameter", summary="获取水箱直径", description="获取指定水箱的直径值")
|
||||||
async def fastapi_get_tank_diameter(
|
async def fastapi_get_tank_diameter(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID")
|
tank: str = Query(..., description="水箱ID")
|
||||||
@@ -178,7 +178,7 @@ async def fastapi_get_tank_diameter(
|
|||||||
ps = get_tank(network, tank)
|
ps = get_tank(network, tank)
|
||||||
return ps["diameter"]
|
return ps["diameter"]
|
||||||
|
|
||||||
@router.get("/gettankminvol/", summary="获取水箱最小体积", description="获取指定水箱的最小体积值")
|
@router.get("/tanks/min-vol", summary="获取水箱最小体积", description="获取指定水箱的最小体积值")
|
||||||
async def fastapi_get_tank_min_vol(
|
async def fastapi_get_tank_min_vol(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID")
|
tank: str = Query(..., description="水箱ID")
|
||||||
@@ -196,7 +196,7 @@ async def fastapi_get_tank_min_vol(
|
|||||||
ps = get_tank(network, tank)
|
ps = get_tank(network, tank)
|
||||||
return ps["min_vol"]
|
return ps["min_vol"]
|
||||||
|
|
||||||
@router.get("/gettankvolcurve/", summary="获取水箱容积曲线", description="获取指定水箱的容积曲线标识")
|
@router.get("/tanks/vol-curve", summary="获取水箱容积曲线", description="获取指定水箱的容积曲线标识")
|
||||||
async def fastapi_get_tank_vol_curve(
|
async def fastapi_get_tank_vol_curve(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID")
|
tank: str = Query(..., description="水箱ID")
|
||||||
@@ -214,7 +214,7 @@ async def fastapi_get_tank_vol_curve(
|
|||||||
ps = get_tank(network, tank)
|
ps = get_tank(network, tank)
|
||||||
return ps["vol_curve"]
|
return ps["vol_curve"]
|
||||||
|
|
||||||
@router.get("/gettankoverflow/", summary="获取水箱溢流口", description="获取指定水箱的溢流口配置")
|
@router.get("/tanks/overflow", summary="获取水箱溢流口", description="获取指定水箱的溢流口配置")
|
||||||
async def fastapi_get_tank_overflow(
|
async def fastapi_get_tank_overflow(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID")
|
tank: str = Query(..., description="水箱ID")
|
||||||
@@ -232,7 +232,7 @@ async def fastapi_get_tank_overflow(
|
|||||||
ps = get_tank(network, tank)
|
ps = get_tank(network, tank)
|
||||||
return ps["overflow"]
|
return ps["overflow"]
|
||||||
|
|
||||||
@router.get("/gettankx/", summary="获取水箱X坐标", description="获取指定水箱的X坐标值")
|
@router.get("/tanks/x", summary="获取水箱X坐标", description="获取指定水箱的X坐标值")
|
||||||
async def fastapi_get_tank_x(
|
async def fastapi_get_tank_x(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID")
|
tank: str = Query(..., description="水箱ID")
|
||||||
@@ -250,7 +250,7 @@ async def fastapi_get_tank_x(
|
|||||||
ps = get_tank(network, tank)
|
ps = get_tank(network, tank)
|
||||||
return ps["x"]
|
return ps["x"]
|
||||||
|
|
||||||
@router.get("/gettanky/", summary="获取水箱Y坐标", description="获取指定水箱的Y坐标值")
|
@router.get("/tanks/y", summary="获取水箱Y坐标", description="获取指定水箱的Y坐标值")
|
||||||
async def fastapi_get_tank_y(
|
async def fastapi_get_tank_y(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID")
|
tank: str = Query(..., description="水箱ID")
|
||||||
@@ -268,7 +268,7 @@ async def fastapi_get_tank_y(
|
|||||||
ps = get_tank(network, tank)
|
ps = get_tank(network, tank)
|
||||||
return ps["y"]
|
return ps["y"]
|
||||||
|
|
||||||
@router.get("/gettankcoord/", summary="获取水箱坐标", description="获取指定水箱的X和Y坐标")
|
@router.get("/tanks/coord", summary="获取水箱坐标", description="获取指定水箱的X和Y坐标")
|
||||||
async def fastapi_get_tank_coord(
|
async def fastapi_get_tank_coord(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID")
|
tank: str = Query(..., description="水箱ID")
|
||||||
@@ -287,7 +287,7 @@ async def fastapi_get_tank_coord(
|
|||||||
coord = {"x": ps["x"], "y": ps["y"]}
|
coord = {"x": ps["x"], "y": ps["y"]}
|
||||||
return coord
|
return coord
|
||||||
|
|
||||||
@router.post("/settankelevation/", summary="设置水箱标高", description="设置指定水箱的标高值", response_model=None)
|
@router.patch("/tanks/elevation", summary="设置水箱标高", description="设置指定水箱的标高值", response_model=None)
|
||||||
async def fastapi_set_tank_elevation(
|
async def fastapi_set_tank_elevation(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID"),
|
tank: str = Query(..., description="水箱ID"),
|
||||||
@@ -307,7 +307,7 @@ async def fastapi_set_tank_elevation(
|
|||||||
ps = {"id": tank, "elevation": elevation}
|
ps = {"id": tank, "elevation": elevation}
|
||||||
return set_tank(network, ChangeSet(ps))
|
return set_tank(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.post("/settankinitlevel/", summary="设置水箱初始水位", description="设置指定水箱的初始水位值", response_model=None)
|
@router.patch("/tanks/init-level", summary="设置水箱初始水位", description="设置指定水箱的初始水位值", response_model=None)
|
||||||
async def fastapi_set_tank_init_level(
|
async def fastapi_set_tank_init_level(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID"),
|
tank: str = Query(..., description="水箱ID"),
|
||||||
@@ -327,7 +327,7 @@ async def fastapi_set_tank_init_level(
|
|||||||
ps = {"id": tank, "init_level": init_level}
|
ps = {"id": tank, "init_level": init_level}
|
||||||
return set_tank(network, ChangeSet(ps))
|
return set_tank(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.post("/settankminlevel/", summary="设置水箱最小水位", description="设置指定水箱的最小水位值", response_model=None)
|
@router.patch("/tanks/min-level", summary="设置水箱最小水位", description="设置指定水箱的最小水位值", response_model=None)
|
||||||
async def fastapi_set_tank_min_level(
|
async def fastapi_set_tank_min_level(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID"),
|
tank: str = Query(..., description="水箱ID"),
|
||||||
@@ -347,7 +347,7 @@ async def fastapi_set_tank_min_level(
|
|||||||
ps = {"id": tank, "min_level": min_level}
|
ps = {"id": tank, "min_level": min_level}
|
||||||
return set_tank(network, ChangeSet(ps))
|
return set_tank(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.post("/settankmaxlevel/", summary="设置水箱最大水位", description="设置指定水箱的最大水位值", response_model=None)
|
@router.patch("/tanks/max-level", summary="设置水箱最大水位", description="设置指定水箱的最大水位值", response_model=None)
|
||||||
async def fastapi_set_tank_max_level(
|
async def fastapi_set_tank_max_level(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID"),
|
tank: str = Query(..., description="水箱ID"),
|
||||||
@@ -367,7 +367,7 @@ async def fastapi_set_tank_max_level(
|
|||||||
ps = {"id": tank, "max_level": max_level}
|
ps = {"id": tank, "max_level": max_level}
|
||||||
return set_tank(network, ChangeSet(ps))
|
return set_tank(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.post("/settankdiameter/", summary="设置水箱直径", description="设置指定水箱的直径值", response_model=None)
|
@router.patch("/tanks/diameter", summary="设置水箱直径", description="设置指定水箱的直径值", response_model=None)
|
||||||
async def fastapi_set_tank_diameter(
|
async def fastapi_set_tank_diameter(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID"),
|
tank: str = Query(..., description="水箱ID"),
|
||||||
@@ -387,7 +387,7 @@ async def fastapi_set_tank_diameter(
|
|||||||
ps = {"id": tank, "diameter": diameter}
|
ps = {"id": tank, "diameter": diameter}
|
||||||
return set_tank(network, ChangeSet(ps))
|
return set_tank(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.post("/settankminvol/", summary="设置水箱最小体积", description="设置指定水箱的最小体积值", response_model=None)
|
@router.patch("/tanks/min-vol", summary="设置水箱最小体积", description="设置指定水箱的最小体积值", response_model=None)
|
||||||
async def fastapi_set_tank_min_vol(
|
async def fastapi_set_tank_min_vol(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID"),
|
tank: str = Query(..., description="水箱ID"),
|
||||||
@@ -407,7 +407,7 @@ async def fastapi_set_tank_min_vol(
|
|||||||
ps = {"id": tank, "min_vol": min_vol}
|
ps = {"id": tank, "min_vol": min_vol}
|
||||||
return set_tank(network, ChangeSet(ps))
|
return set_tank(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.post("/settankvolcurve/", summary="设置水箱容积曲线", description="设置指定水箱的容积曲线标识", response_model=None)
|
@router.patch("/tanks/vol-curve", summary="设置水箱容积曲线", description="设置指定水箱的容积曲线标识", response_model=None)
|
||||||
async def fastapi_set_tank_vol_curve(
|
async def fastapi_set_tank_vol_curve(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID"),
|
tank: str = Query(..., description="水箱ID"),
|
||||||
@@ -427,7 +427,7 @@ async def fastapi_set_tank_vol_curve(
|
|||||||
ps = {"id": tank, "vol_curve": vol_curve}
|
ps = {"id": tank, "vol_curve": vol_curve}
|
||||||
return set_tank(network, ChangeSet(ps))
|
return set_tank(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.post("/settankoverflow/", summary="设置水箱溢流口", description="设置指定水箱的溢流口配置", response_model=None)
|
@router.patch("/tanks/overflow", summary="设置水箱溢流口", description="设置指定水箱的溢流口配置", response_model=None)
|
||||||
async def fastapi_set_tank_overflow(
|
async def fastapi_set_tank_overflow(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID"),
|
tank: str = Query(..., description="水箱ID"),
|
||||||
@@ -447,7 +447,7 @@ async def fastapi_set_tank_overflow(
|
|||||||
ps = {"id": tank, "overflow": overflow}
|
ps = {"id": tank, "overflow": overflow}
|
||||||
return set_tank(network, ChangeSet(ps))
|
return set_tank(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.post("/settankx/", summary="设置水箱X坐标", description="设置指定水箱的X坐标值", response_model=None)
|
@router.patch("/tanks/x", summary="设置水箱X坐标", description="设置指定水箱的X坐标值", response_model=None)
|
||||||
async def fastapi_set_tank_x(
|
async def fastapi_set_tank_x(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID"),
|
tank: str = Query(..., description="水箱ID"),
|
||||||
@@ -467,7 +467,7 @@ async def fastapi_set_tank_x(
|
|||||||
ps = {"id": tank, "x": x}
|
ps = {"id": tank, "x": x}
|
||||||
return set_tank(network, ChangeSet(ps))
|
return set_tank(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.post("/settanky/", summary="设置水箱Y坐标", description="设置指定水箱的Y坐标值", response_model=None)
|
@router.patch("/tanks/y", summary="设置水箱Y坐标", description="设置指定水箱的Y坐标值", response_model=None)
|
||||||
async def fastapi_set_tank_y(
|
async def fastapi_set_tank_y(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID"),
|
tank: str = Query(..., description="水箱ID"),
|
||||||
@@ -487,7 +487,7 @@ async def fastapi_set_tank_y(
|
|||||||
ps = {"id": tank, "y": y}
|
ps = {"id": tank, "y": y}
|
||||||
return set_tank(network, ChangeSet(ps))
|
return set_tank(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.post("/settankcoord/", summary="设置水箱坐标", description="设置指定水箱的X和Y坐标", response_model=None)
|
@router.patch("/tanks/coord", summary="设置水箱坐标", description="设置指定水箱的X和Y坐标", response_model=None)
|
||||||
async def fastapi_set_tank_coord(
|
async def fastapi_set_tank_coord(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID"),
|
tank: str = Query(..., description="水箱ID"),
|
||||||
@@ -509,7 +509,7 @@ async def fastapi_set_tank_coord(
|
|||||||
ps = {"id": tank, "x": x, "y": y}
|
ps = {"id": tank, "x": x, "y": y}
|
||||||
return set_tank(network, ChangeSet(ps))
|
return set_tank(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.get("/gettankproperties/", summary="获取水箱属性", description="获取指定水箱的所有属性")
|
@router.get("/tanks/properties", summary="获取水箱属性", description="获取指定水箱的所有属性")
|
||||||
async def fastapi_get_tank_properties(
|
async def fastapi_get_tank_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID")
|
tank: str = Query(..., description="水箱ID")
|
||||||
@@ -526,7 +526,7 @@ async def fastapi_get_tank_properties(
|
|||||||
"""
|
"""
|
||||||
return get_tank(network, tank)
|
return get_tank(network, tank)
|
||||||
|
|
||||||
@router.get("/getalltankproperties/", summary="获取所有水箱属性", description="获取指定网络中所有水箱的属性")
|
@router.get("/tanks", summary="获取所有水箱属性", description="获取指定网络中所有水箱的属性")
|
||||||
async def fastapi_get_all_tank_properties(
|
async def fastapi_get_all_tank_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
@@ -544,7 +544,7 @@ async def fastapi_get_all_tank_properties(
|
|||||||
results = get_all_tanks(network)
|
results = get_all_tanks(network)
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@router.post("/settankproperties/", summary="设置水箱属性", description="批量设置指定水箱的多个属性", response_model=None)
|
@router.patch("/tanks/properties", summary="设置水箱属性", description="批量设置指定水箱的多个属性", response_model=None)
|
||||||
async def fastapi_set_tank_properties(
|
async def fastapi_set_tank_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID"),
|
tank: str = Query(..., description="水箱ID"),
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from app.services.tjnetwork import (
|
|||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getvalveschema",
|
"/network-schemas/valve",
|
||||||
summary="获取阀门架构",
|
summary="获取阀门架构",
|
||||||
description="获取指定水网中所有阀门的架构和字段定义",
|
description="获取指定水网中所有阀门的架构和字段定义",
|
||||||
)
|
)
|
||||||
@@ -30,7 +30,7 @@ async def fastapi_get_valve_schema(
|
|||||||
return get_valve_schema(network)
|
return get_valve_schema(network)
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/addvalve/",
|
"/valves",
|
||||||
response_model=None,
|
response_model=None,
|
||||||
summary="添加阀门",
|
summary="添加阀门",
|
||||||
description="在指定的水网中添加新的阀门",
|
description="在指定的水网中添加新的阀门",
|
||||||
@@ -62,8 +62,8 @@ async def fastapi_add_valve(
|
|||||||
|
|
||||||
return add_valve(network, ChangeSet(ps))
|
return add_valve(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.post(
|
@router.delete(
|
||||||
"/deletevalve/",
|
"/valves",
|
||||||
response_model=None,
|
response_model=None,
|
||||||
summary="删除阀门",
|
summary="删除阀门",
|
||||||
description="从指定的水网中删除指定的阀门",
|
description="从指定的水网中删除指定的阀门",
|
||||||
@@ -81,7 +81,7 @@ async def fastapi_delete_valve(
|
|||||||
return delete_valve(network, ChangeSet(ps))
|
return delete_valve(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getvalvenode1/",
|
"/valves/node1",
|
||||||
summary="获取阀门起点节点",
|
summary="获取阀门起点节点",
|
||||||
description="获取指定阀门连接的起点节点ID",
|
description="获取指定阀门连接的起点节点ID",
|
||||||
)
|
)
|
||||||
@@ -98,7 +98,7 @@ async def fastapi_get_valve_node1(
|
|||||||
return ps["node1"]
|
return ps["node1"]
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getvalvenode2/",
|
"/valves/node2",
|
||||||
summary="获取阀门终点节点",
|
summary="获取阀门终点节点",
|
||||||
description="获取指定阀门连接的终点节点ID",
|
description="获取指定阀门连接的终点节点ID",
|
||||||
)
|
)
|
||||||
@@ -115,7 +115,7 @@ async def fastapi_get_valve_node2(
|
|||||||
return ps["node2"]
|
return ps["node2"]
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getvalvediameter/",
|
"/valves/diameter",
|
||||||
summary="获取阀门直径",
|
summary="获取阀门直径",
|
||||||
description="获取指定阀门的直径",
|
description="获取指定阀门的直径",
|
||||||
)
|
)
|
||||||
@@ -132,7 +132,7 @@ async def fastapi_get_valve_diameter(
|
|||||||
return ps["diameter"]
|
return ps["diameter"]
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getvalvetype/",
|
"/valves/type",
|
||||||
summary="获取阀门类型",
|
summary="获取阀门类型",
|
||||||
description="获取指定阀门的类型",
|
description="获取指定阀门的类型",
|
||||||
)
|
)
|
||||||
@@ -149,7 +149,7 @@ async def fastapi_get_valve_type(
|
|||||||
return ps["type"]
|
return ps["type"]
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getvalvesetting/",
|
"/valves/setting",
|
||||||
summary="获取阀门开度",
|
summary="获取阀门开度",
|
||||||
description="获取指定阀门的开度/设置值",
|
description="获取指定阀门的开度/设置值",
|
||||||
)
|
)
|
||||||
@@ -166,7 +166,7 @@ async def fastapi_get_valve_setting(
|
|||||||
return ps["setting"]
|
return ps["setting"]
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getvalveminorloss/",
|
"/valves/minor-loss",
|
||||||
summary="获取阀门损失系数",
|
summary="获取阀门损失系数",
|
||||||
description="获取指定阀门的损失系数",
|
description="获取指定阀门的损失系数",
|
||||||
)
|
)
|
||||||
@@ -182,8 +182,8 @@ async def fastapi_get_valve_minor_loss(
|
|||||||
ps = get_valve(network, valve)
|
ps = get_valve(network, valve)
|
||||||
return ps["minor_loss"]
|
return ps["minor_loss"]
|
||||||
|
|
||||||
@router.post(
|
@router.patch(
|
||||||
"/setvalvenode1/",
|
"/valves/node1",
|
||||||
response_model=None,
|
response_model=None,
|
||||||
summary="设置阀门起点节点",
|
summary="设置阀门起点节点",
|
||||||
description="设置指定阀门的起点节点",
|
description="设置指定阀门的起点节点",
|
||||||
@@ -201,8 +201,8 @@ async def fastapi_set_valve_node1(
|
|||||||
ps = {"id": valve, "node1": node1}
|
ps = {"id": valve, "node1": node1}
|
||||||
return set_valve(network, ChangeSet(ps))
|
return set_valve(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.post(
|
@router.patch(
|
||||||
"/setvalvenode2/",
|
"/valves/node2",
|
||||||
response_model=None,
|
response_model=None,
|
||||||
summary="设置阀门终点节点",
|
summary="设置阀门终点节点",
|
||||||
description="设置指定阀门的终点节点",
|
description="设置指定阀门的终点节点",
|
||||||
@@ -220,8 +220,8 @@ async def fastapi_set_valve_node2(
|
|||||||
ps = {"id": valve, "node2": node2}
|
ps = {"id": valve, "node2": node2}
|
||||||
return set_valve(network, ChangeSet(ps))
|
return set_valve(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.post(
|
@router.patch(
|
||||||
"/setvalvenodediameter/",
|
"/valves/diameter",
|
||||||
response_model=None,
|
response_model=None,
|
||||||
summary="设置阀门直径",
|
summary="设置阀门直径",
|
||||||
description="设置指定阀门的直径",
|
description="设置指定阀门的直径",
|
||||||
@@ -239,8 +239,8 @@ async def fastapi_set_valve_diameter(
|
|||||||
ps = {"id": valve, "diameter": diameter}
|
ps = {"id": valve, "diameter": diameter}
|
||||||
return set_valve(network, ChangeSet(ps))
|
return set_valve(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.post(
|
@router.patch(
|
||||||
"/setvalvetype/",
|
"/valves/type",
|
||||||
response_model=None,
|
response_model=None,
|
||||||
summary="设置阀门类型",
|
summary="设置阀门类型",
|
||||||
description="设置指定阀门的类型",
|
description="设置指定阀门的类型",
|
||||||
@@ -258,8 +258,8 @@ async def fastapi_set_valve_type(
|
|||||||
ps = {"id": valve, "type": type}
|
ps = {"id": valve, "type": type}
|
||||||
return set_valve(network, ChangeSet(ps))
|
return set_valve(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.post(
|
@router.patch(
|
||||||
"/setvalvesetting/",
|
"/valves/setting",
|
||||||
response_model=None,
|
response_model=None,
|
||||||
summary="设置阀门开度",
|
summary="设置阀门开度",
|
||||||
description="设置指定阀门的开度/设置值",
|
description="设置指定阀门的开度/设置值",
|
||||||
@@ -278,7 +278,7 @@ async def fastapi_set_valve_setting(
|
|||||||
return set_valve(network, ChangeSet(ps))
|
return set_valve(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getvalveproperties/",
|
"/valves/properties",
|
||||||
summary="获取阀门所有属性",
|
summary="获取阀门所有属性",
|
||||||
description="获取指定阀门的所有属性",
|
description="获取指定阀门的所有属性",
|
||||||
)
|
)
|
||||||
@@ -294,7 +294,7 @@ async def fastapi_get_valve_properties(
|
|||||||
return get_valve(network, valve)
|
return get_valve(network, valve)
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getallvalveproperties/",
|
"/valves",
|
||||||
summary="获取所有阀门属性",
|
summary="获取所有阀门属性",
|
||||||
description="获取指定水网中所有阀门的属性",
|
description="获取指定水网中所有阀门的属性",
|
||||||
)
|
)
|
||||||
@@ -311,8 +311,8 @@ async def fastapi_get_all_valve_properties(
|
|||||||
results = get_all_valves(network)
|
results = get_all_valves(network)
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@router.post(
|
@router.patch(
|
||||||
"/setvalveproperties/",
|
"/valves/properties",
|
||||||
response_model=None,
|
response_model=None,
|
||||||
summary="批量设置阀门属性",
|
summary="批量设置阀门属性",
|
||||||
description="批量设置指定阀门的多个属性",
|
description="批量设置指定阀门的多个属性",
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import json
|
import json
|
||||||
from fastapi import APIRouter, Request, HTTPException, Query, Path, Body, Depends
|
from fastapi import APIRouter, Request, HTTPException, Query, Path, Depends
|
||||||
from fastapi.responses import PlainTextResponse
|
from fastapi.responses import PlainTextResponse
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
from app.infra.db.metadb.repositories.metadata_repository import MetadataRepository
|
from app.infra.db.metadb.repositories.metadata_repository import MetadataRepository
|
||||||
from app.auth.project_dependencies import get_metadata_repository
|
from app.auth.project_dependencies import get_metadata_repository
|
||||||
|
from app.auth.permissions import (
|
||||||
|
ENVIRONMENT_MANAGE,
|
||||||
|
require_permission,
|
||||||
|
)
|
||||||
from app.domain.schemas.metadata import ProjectMetaResponse
|
from app.domain.schemas.metadata import ProjectMetaResponse
|
||||||
import app.services.project_info as project_info
|
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.postgresql.database import get_database_instance as get_pg_db
|
||||||
@@ -18,7 +22,6 @@ from app.services.tjnetwork import (
|
|||||||
open_project,
|
open_project,
|
||||||
close_project,
|
close_project,
|
||||||
copy_project,
|
copy_project,
|
||||||
import_inp,
|
|
||||||
export_inp,
|
export_inp,
|
||||||
read_inp,
|
read_inp,
|
||||||
dump_inp,
|
dump_inp,
|
||||||
@@ -42,8 +45,7 @@ inpDir = "data/" # Assuming data directory exists or is defined somewhere.
|
|||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
lockedPrjs: Dict[str, str] = {}
|
lockedPrjs: Dict[str, str] = {}
|
||||||
|
|
||||||
@router.get("/project-info", summary="获取项目信息", description="从数据库获取项目的详细信息,包括地图范围等。", response_model=ProjectMetaResponse)
|
@router.get("/projects/current", summary="获取项目信息", description="从数据库获取项目的详细信息,包括地图范围等。", response_model=ProjectMetaResponse)
|
||||||
@router.get("/project_info/", summary="获取项目信息(旧路径)", description="从数据库获取项目的详细信息,包括地图范围等。", response_model=ProjectMetaResponse, deprecated=True)
|
|
||||||
async def get_project_info_endpoint(
|
async def get_project_info_endpoint(
|
||||||
network: str = Query(..., description="管网名称(或项目代码)"),
|
network: str = Query(..., description="管网名称(或项目代码)"),
|
||||||
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
||||||
@@ -68,7 +70,7 @@ async def get_project_info_endpoint(
|
|||||||
project_role="viewer",
|
project_role="viewer",
|
||||||
)
|
)
|
||||||
|
|
||||||
@router.get("/listprojects/", summary="获取项目列表", description="获取服务器上所有可用的供水管网项目名称列表。")
|
@router.get("/project-codes", summary="获取项目列表", description="获取服务器上所有可用的供水管网项目名称列表。")
|
||||||
async def list_projects_endpoint() -> list[str]:
|
async def list_projects_endpoint() -> list[str]:
|
||||||
"""
|
"""
|
||||||
获取项目列表
|
获取项目列表
|
||||||
@@ -77,7 +79,7 @@ async def list_projects_endpoint() -> list[str]:
|
|||||||
"""
|
"""
|
||||||
return list_project()
|
return list_project()
|
||||||
|
|
||||||
@router.get("/haveproject/", summary="检查项目是否存在", description="检查指定名称的项目是否存在。")
|
@router.get("/projects/existence", summary="检查项目是否存在", description="检查指定名称的项目是否存在。")
|
||||||
async def have_project_endpoint(
|
async def have_project_endpoint(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||||
):
|
):
|
||||||
@@ -88,9 +90,10 @@ async def have_project_endpoint(
|
|||||||
"""
|
"""
|
||||||
return have_project(network)
|
return have_project(network)
|
||||||
|
|
||||||
@router.post("/createproject/", summary="创建新项目", description="创建一个新的供水管网项目。如果项目已存在,可能会覆盖或报错(取决于底层实现)。")
|
@router.post("/projects", summary="创建新项目", description="创建一个新的供水管网项目。如果项目已存在,可能会覆盖或报错(取决于底层实现)。")
|
||||||
async def create_project_endpoint(
|
async def create_project_endpoint(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
|
_=Depends(require_permission(ENVIRONMENT_MANAGE)),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
创建新项目
|
创建新项目
|
||||||
@@ -100,9 +103,10 @@ async def create_project_endpoint(
|
|||||||
create_project(network)
|
create_project(network)
|
||||||
return network
|
return network
|
||||||
|
|
||||||
@router.post("/deleteproject/", summary="删除项目", description="永久删除指定的供水管网项目。此操作不可恢复。")
|
@router.delete("/projects", summary="删除项目", description="永久删除指定的供水管网项目。此操作不可恢复。")
|
||||||
async def delete_project_endpoint(
|
async def delete_project_endpoint(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
|
_=Depends(require_permission(ENVIRONMENT_MANAGE)),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
删除项目
|
删除项目
|
||||||
@@ -112,7 +116,7 @@ async def delete_project_endpoint(
|
|||||||
delete_project(network)
|
delete_project(network)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@router.get("/isprojectopen/", summary="检查项目是否已打开", description="检查指定项目是否已被加载到内存中。")
|
@router.get("/projects/current/status", summary="检查项目是否已打开", description="检查指定项目是否已被加载到内存中。")
|
||||||
async def is_project_open_endpoint(
|
async def is_project_open_endpoint(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||||
):
|
):
|
||||||
@@ -123,8 +127,7 @@ async def is_project_open_endpoint(
|
|||||||
"""
|
"""
|
||||||
return is_project_open(network)
|
return is_project_open(network)
|
||||||
|
|
||||||
@router.post("/projects/open", summary="打开项目", description="将指定项目加载到内存中,并初始化数据库连接池。")
|
@router.post("/projects/current", summary="打开项目", description="将指定项目加载到内存中,并初始化数据库连接池。")
|
||||||
@router.post("/openproject/", summary="打开项目(旧路径)", description="将指定项目加载到内存中,并初始化数据库连接池。", deprecated=True)
|
|
||||||
async def open_project_endpoint(
|
async def open_project_endpoint(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||||
):
|
):
|
||||||
@@ -158,7 +161,7 @@ async def open_project_endpoint(
|
|||||||
|
|
||||||
return network
|
return network
|
||||||
|
|
||||||
@router.post("/closeproject/", summary="关闭项目", description="将指定项目从内存中卸载,释放资源。")
|
@router.delete("/projects/current", summary="关闭项目", description="将指定项目从内存中卸载,释放资源。")
|
||||||
async def close_project_endpoint(
|
async def close_project_endpoint(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||||
):
|
):
|
||||||
@@ -170,10 +173,11 @@ async def close_project_endpoint(
|
|||||||
close_project(network)
|
close_project(network)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@router.post("/copyproject/", summary="复制项目", description="将现有项目复制为新项目。")
|
@router.post("/project-copies", summary="复制项目", description="将现有项目复制为新项目。")
|
||||||
async def copy_project_endpoint(
|
async def copy_project_endpoint(
|
||||||
source: str = Query(..., description="管网名称(或数据库名称)"),
|
source: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
target: str = Query(..., description="管网名称(或数据库名称)")
|
target: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
|
_=Depends(require_permission(ENVIRONMENT_MANAGE)),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
复制项目
|
复制项目
|
||||||
@@ -184,25 +188,7 @@ async def copy_project_endpoint(
|
|||||||
copy_project(source, target)
|
copy_project(source, target)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@router.post("/importinp/", summary="导入 INP 文件内容", description="将 INP 格式的文本内容导入到指定项目中。")
|
@router.get("/projects/current/exports/change-set", response_model=None, summary="导出项目为 ChangeSet", description="导出项目的变更集 (ChangeSet),包含顶点、SCADA 元素、DMA、SA、VD 等信息。")
|
||||||
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(
|
async def export_inp_endpoint(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
version: str = Query(..., description="版本号 (通常用于增量更新)")
|
version: str = Query(..., description="版本号 (通常用于增量更新)")
|
||||||
@@ -235,7 +221,7 @@ async def export_inp_endpoint(
|
|||||||
|
|
||||||
return cs
|
return cs
|
||||||
|
|
||||||
@router.post("/readinp/", summary="读取 INP 文件到项目", description="从服务器文件系统中读取指定的 INP 文件并加载到项目中。")
|
@router.post("/projects/current/imports", summary="读取 INP 文件到项目", description="从服务器文件系统中读取指定的 INP 文件并加载到项目中。")
|
||||||
async def read_inp_endpoint(
|
async def read_inp_endpoint(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
inp: str = Query(..., description="INP 文件名 (不包含路径)")
|
inp: str = Query(..., description="INP 文件名 (不包含路径)")
|
||||||
@@ -249,7 +235,7 @@ async def read_inp_endpoint(
|
|||||||
read_inp(network, inp)
|
read_inp(network, inp)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@router.get("/dumpinp/", summary="导出项目到 INP 文件", description="将项目当前状态保存为 INP 文件到服务器文件系统。")
|
@router.post("/projects/current/exports/inp", summary="导出项目到 INP 文件", description="将项目当前状态保存为 INP 文件到服务器文件系统。")
|
||||||
async def dump_inp_endpoint(
|
async def dump_inp_endpoint(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
inp: str = Query(..., description="目标文件名")
|
inp: str = Query(..., description="目标文件名")
|
||||||
@@ -263,7 +249,7 @@ async def dump_inp_endpoint(
|
|||||||
dump_inp(network, inp)
|
dump_inp(network, inp)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@router.get("/isprojectlocked/", summary="检查项目是否被锁定", description="检查指定项目是否处于锁定状态。")
|
@router.get("/projects/current/lock", summary="检查项目是否被锁定", description="检查指定项目是否处于锁定状态。")
|
||||||
async def is_project_locked_endpoint(
|
async def is_project_locked_endpoint(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
req: Request = None
|
||||||
@@ -275,7 +261,7 @@ async def is_project_locked_endpoint(
|
|||||||
"""
|
"""
|
||||||
return network in lockedPrjs.keys()
|
return network in lockedPrjs.keys()
|
||||||
|
|
||||||
@router.get("/isprojectlockedbyme/", summary="检查项目是否被当前用户锁定", description="检查指定项目是否被当前客户端 (IP) 锁定。")
|
@router.get("/projects/current/lock/ownership", summary="检查项目是否被当前用户锁定", description="检查指定项目是否被当前访问地址 (IP) 锁定。")
|
||||||
async def is_project_locked_by_me_endpoint(
|
async def is_project_locked_by_me_endpoint(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
req: Request = None
|
||||||
@@ -291,7 +277,7 @@ async def is_project_locked_by_me_endpoint(
|
|||||||
# 0 successfully locked
|
# 0 successfully locked
|
||||||
# 1 already locked by you
|
# 1 already locked by you
|
||||||
# 2 locked by others
|
# 2 locked by others
|
||||||
@router.post("/lockproject/", summary="锁定项目", description="锁定指定项目以防止并发修改。")
|
@router.post("/projects/current/lock", summary="锁定项目", description="锁定指定项目以防止并发修改。")
|
||||||
async def lock_project_endpoint(
|
async def lock_project_endpoint(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
req: Request = None
|
||||||
@@ -314,7 +300,7 @@ async def lock_project_endpoint(
|
|||||||
else:
|
else:
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
@router.post("/unlockproject/", summary="解锁项目", description="释放对项目的锁定。")
|
@router.delete("/projects/current/lock", summary="解锁项目", description="释放对项目的锁定。")
|
||||||
def unlock_project_endpoint(
|
def unlock_project_endpoint(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
req: Request = None
|
||||||
@@ -332,27 +318,7 @@ def unlock_project_endpoint(
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# inp file operations
|
@router.get("/projects/current/files/inp", status_code=status.HTTP_200_OK, summary="下载 INP 文件", description="从服务器数据目录下载指定的 INP 文件。")
|
||||||
@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(
|
async def fastapi_download_inp(
|
||||||
name: str = Query(..., description="文件名"),
|
name: str = Query(..., description="文件名"),
|
||||||
response: Response = None
|
response: Response = None
|
||||||
@@ -372,7 +338,7 @@ async def fastapi_download_inp(
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
# DingZQ, 2024-12-28, convert v3 to v2
|
# 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 格式。")
|
@router.post("/project-conversions", response_model=None, summary="转换 INP V3 为 V2", description="将 EPANET 3.0 格式的 INP 内容转换为 2.x 格式。")
|
||||||
async def fastapi_convert_v3_to_v2(
|
async def fastapi_convert_v3_to_v2(
|
||||||
req: Request
|
req: Request
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
@@ -406,7 +372,6 @@ async def fastapi_convert_v3_to_v2(
|
|||||||
|
|
||||||
return cs
|
return cs
|
||||||
|
|
||||||
@router.post("/readinp/", summary="读取 INP 文件到项目", description="从服务器文件系统中读取指定的 INP 文件并加载到项目中。")
|
|
||||||
async def read_inp_endpoint(
|
async def read_inp_endpoint(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
inp: str = Query(..., description="INP 文件名 (不包含路径)")
|
inp: str = Query(..., description="INP 文件名 (不包含路径)")
|
||||||
@@ -420,7 +385,6 @@ async def read_inp_endpoint(
|
|||||||
read_inp(network, inp)
|
read_inp(network, inp)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@router.get("/dumpinp/", summary="导出项目到 INP 文件", description="将项目当前状态保存为 INP 文件到服务器文件系统。")
|
|
||||||
async def dump_inp_endpoint(
|
async def dump_inp_endpoint(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
inp: str = Query(..., description="目标文件名")
|
inp: str = Query(..., description="目标文件名")
|
||||||
@@ -434,7 +398,6 @@ async def dump_inp_endpoint(
|
|||||||
dump_inp(network, inp)
|
dump_inp(network, inp)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@router.get("/isprojectlocked/", summary="检查项目是否被锁定", description="检查指定项目是否处于锁定状态。")
|
|
||||||
async def is_project_locked_endpoint(
|
async def is_project_locked_endpoint(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
req: Request = None
|
||||||
@@ -446,7 +409,6 @@ async def is_project_locked_endpoint(
|
|||||||
"""
|
"""
|
||||||
return network in lockedPrjs.keys()
|
return network in lockedPrjs.keys()
|
||||||
|
|
||||||
@router.get("/isprojectlockedbyme/", summary="检查项目是否被当前用户锁定", description="检查指定项目是否被当前客户端 (IP) 锁定。")
|
|
||||||
async def is_project_locked_by_me_endpoint(
|
async def is_project_locked_by_me_endpoint(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
req: Request = None
|
||||||
@@ -462,7 +424,6 @@ async def is_project_locked_by_me_endpoint(
|
|||||||
# 0 successfully locked
|
# 0 successfully locked
|
||||||
# 1 already locked by you
|
# 1 already locked by you
|
||||||
# 2 locked by others
|
# 2 locked by others
|
||||||
@router.post("/lockproject/", summary="锁定项目", description="锁定指定项目以防止并发修改。")
|
|
||||||
async def lock_project_endpoint(
|
async def lock_project_endpoint(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
req: Request = None
|
||||||
@@ -485,7 +446,6 @@ async def lock_project_endpoint(
|
|||||||
else:
|
else:
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
@router.post("/unlockproject/", summary="解锁项目", description="释放对项目的锁定。")
|
|
||||||
def unlock_project_endpoint(
|
def unlock_project_endpoint(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
req: Request = None
|
||||||
@@ -503,27 +463,6 @@ def unlock_project_endpoint(
|
|||||||
|
|
||||||
return False
|
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(
|
async def fastapi_download_inp(
|
||||||
name: str = Query(..., description="文件名"),
|
name: str = Query(..., description="文件名"),
|
||||||
response: Response = None
|
response: Response = None
|
||||||
@@ -543,7 +482,6 @@ async def fastapi_download_inp(
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
# DingZQ, 2024-12-28, convert v3 to v2
|
# 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(
|
async def fastapi_convert_v3_to_v2(
|
||||||
req: Request
|
req: Request
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ async def get_database_connection(
|
|||||||
yield conn
|
yield conn
|
||||||
|
|
||||||
|
|
||||||
@router.get("/scada-info", summary="获取SCADA信息", description="使用连接池查询所有SCADA信息")
|
@router.get("/scada-info/database-view", summary="获取SCADA信息", description="使用连接池查询所有SCADA信息")
|
||||||
async def get_scada_info_with_connection(
|
async def get_scada_info_with_connection(
|
||||||
conn: AsyncConnection = Depends(get_database_connection),
|
conn: AsyncConnection = Depends(get_database_connection),
|
||||||
):
|
):
|
||||||
@@ -33,7 +33,7 @@ async def get_scada_info_with_connection(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/scheme-list", summary="获取方案列表", description="使用连接池查询所有方案信息")
|
@router.get("/schemes/list-with-connection", summary="获取方案列表", description="使用连接池查询所有方案信息")
|
||||||
async def get_scheme_list_with_connection(
|
async def get_scheme_list_with_connection(
|
||||||
conn: AsyncConnection = Depends(get_database_connection),
|
conn: AsyncConnection = Depends(get_database_connection),
|
||||||
):
|
):
|
||||||
@@ -49,7 +49,7 @@ async def get_scheme_list_with_connection(
|
|||||||
raise HTTPException(status_code=500, detail=f"查询方案信息时发生错误: {str(e)}")
|
raise HTTPException(status_code=500, detail=f"查询方案信息时发生错误: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/burst-locate-result", summary="获取爆管定位结果", description="使用连接池查询所有爆管定位结果")
|
@router.get("/burst-locations/database-view", summary="获取爆管定位结果", description="使用连接池查询所有爆管定位结果")
|
||||||
async def get_burst_locate_result_with_connection(
|
async def get_burst_locate_result_with_connection(
|
||||||
conn: AsyncConnection = Depends(get_database_connection),
|
conn: AsyncConnection = Depends(get_database_connection),
|
||||||
):
|
):
|
||||||
@@ -67,7 +67,7 @@ async def get_burst_locate_result_with_connection(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/burst-locate-result/{burst_incident}", summary="按事件查询爆管定位结果", description="根据爆管事件ID查询对应的爆管定位结果")
|
@router.get("/burst-locations/{burst_incident}", summary="按事件查询爆管定位结果", description="根据爆管事件ID查询对应的爆管定位结果")
|
||||||
async def get_burst_locate_result_by_incident(
|
async def get_burst_locate_result_by_incident(
|
||||||
burst_incident: str = Path(..., description="爆管事件ID"),
|
burst_incident: str = Path(..., description="爆管事件ID"),
|
||||||
conn: AsyncConnection = Depends(get_database_connection),
|
conn: AsyncConnection = Depends(get_database_connection),
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from app.services.tjnetwork import (
|
|||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getpiperiskprobabilitynow/",
|
"/pipes/risk-probability-now",
|
||||||
summary="获取管道当前风险概率",
|
summary="获取管道当前风险概率",
|
||||||
description="获取指定管道当前时刻的风险概率值"
|
description="获取指定管道当前时刻的风险概率值"
|
||||||
)
|
)
|
||||||
@@ -35,7 +35,7 @@ async def fastapi_get_pipe_risk_probability_now(
|
|||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getpiperiskprobability/",
|
"/pipes/risk-probability",
|
||||||
summary="获取管道风险概率历史",
|
summary="获取管道风险概率历史",
|
||||||
description="获取指定管道的风险概率历史数据"
|
description="获取指定管道的风险概率历史数据"
|
||||||
)
|
)
|
||||||
@@ -59,7 +59,7 @@ async def fastapi_get_pipe_risk_probability(
|
|||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getpipesriskprobability/",
|
"/pipes-risk-probabilities",
|
||||||
summary="批量获取多条管道风险概率",
|
summary="批量获取多条管道风险概率",
|
||||||
description="批量获取多条管道的风险概率值"
|
description="批量获取多条管道的风险概率值"
|
||||||
)
|
)
|
||||||
@@ -84,7 +84,7 @@ async def fastapi_get_pipes_risk_probability(
|
|||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getnetworkpiperiskprobabilitynow/",
|
"/network-pipe-risk-probability-nows",
|
||||||
summary="获取整个网络的管道风险概率",
|
summary="获取整个网络的管道风险概率",
|
||||||
description="获取指定网络中所有管道的当前风险概率值"
|
description="获取指定网络中所有管道的当前风险概率值"
|
||||||
)
|
)
|
||||||
@@ -106,7 +106,7 @@ async def fastapi_get_network_pipe_risk_probability_now(
|
|||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/getpiperiskprobabilitygeometries/",
|
"/pipes/risk-probability-geometries",
|
||||||
summary="获取管道风险几何信息",
|
summary="获取管道风险几何信息",
|
||||||
description="获取指定网络中管道的风险相关几何数据"
|
description="获取指定网络中管道的风险相关几何数据"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ from app.services.tjnetwork import (
|
|||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.get("/getscadaproperties/", summary="获取SCADA属性", tags=["SCADA基础"])
|
|
||||||
async def fast_get_scada_properties(
|
async def fast_get_scada_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
scada: str = Query(..., description="SCADA设备ID")
|
scada: str = Query(..., description="SCADA设备ID")
|
||||||
@@ -50,7 +49,6 @@ async def fast_get_scada_properties(
|
|||||||
"""
|
"""
|
||||||
return get_scada_info(network, scada)
|
return get_scada_info(network, scada)
|
||||||
|
|
||||||
@router.get("/getallscadaproperties/", summary="获取所有SCADA属性", tags=["SCADA基础"])
|
|
||||||
async def fast_get_all_scada_properties(
|
async def fast_get_all_scada_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
@@ -72,7 +70,7 @@ async def fast_get_all_scada_properties(
|
|||||||
# scada_device 设备管理
|
# scada_device 设备管理
|
||||||
############################################################
|
############################################################
|
||||||
|
|
||||||
@router.get("/getscadadeviceschema/", summary="获取SCADA设备架构", tags=["SCADA设备"])
|
@router.get("/network-schemas/scada-device", summary="获取SCADA设备架构", tags=["SCADA设备"])
|
||||||
async def fastapi_get_scada_device_schema(
|
async def fastapi_get_scada_device_schema(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||||
) -> dict[str, dict[str, Any]]:
|
) -> dict[str, dict[str, Any]]:
|
||||||
@@ -89,7 +87,7 @@ async def fastapi_get_scada_device_schema(
|
|||||||
"""
|
"""
|
||||||
return get_scada_device_schema(network)
|
return get_scada_device_schema(network)
|
||||||
|
|
||||||
@router.get("/getscadadevice/", summary="获取SCADA设备", tags=["SCADA设备"])
|
@router.get("/scada-devices/detail", summary="获取SCADA设备", tags=["SCADA设备"])
|
||||||
async def fastapi_get_scada_device(
|
async def fastapi_get_scada_device(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
id: str = Query(..., description="SCADA设备ID")
|
id: str = Query(..., description="SCADA设备ID")
|
||||||
@@ -108,7 +106,7 @@ async def fastapi_get_scada_device(
|
|||||||
"""
|
"""
|
||||||
return get_scada_device(network, id)
|
return get_scada_device(network, id)
|
||||||
|
|
||||||
@router.post("/setscadadevice/", response_model=None, summary="更新SCADA设备", tags=["SCADA设备"])
|
@router.patch("/scada-devices", response_model=None, summary="更新SCADA设备", tags=["SCADA设备"])
|
||||||
async def fastapi_set_scada_device(
|
async def fastapi_set_scada_device(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
req: Request = None
|
||||||
@@ -128,7 +126,7 @@ async def fastapi_set_scada_device(
|
|||||||
props = await req.json()
|
props = await req.json()
|
||||||
return set_scada_device(network, ChangeSet(props))
|
return set_scada_device(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.post("/addscadadevice/", response_model=None, summary="添加SCADA设备", tags=["SCADA设备"])
|
@router.post("/scada-devices", response_model=None, summary="添加SCADA设备", tags=["SCADA设备"])
|
||||||
async def fastapi_add_scada_device(
|
async def fastapi_add_scada_device(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
req: Request = None
|
||||||
@@ -148,7 +146,7 @@ async def fastapi_add_scada_device(
|
|||||||
props = await req.json()
|
props = await req.json()
|
||||||
return add_scada_device(network, ChangeSet(props))
|
return add_scada_device(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.post("/deletescadadevice/", response_model=None, summary="删除SCADA设备", tags=["SCADA设备"])
|
@router.delete("/scada-devices", response_model=None, summary="删除SCADA设备", tags=["SCADA设备"])
|
||||||
async def fastapi_delete_scada_device(
|
async def fastapi_delete_scada_device(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
req: Request = None
|
||||||
@@ -168,7 +166,7 @@ async def fastapi_delete_scada_device(
|
|||||||
props = await req.json()
|
props = await req.json()
|
||||||
return delete_scada_device(network, ChangeSet(props))
|
return delete_scada_device(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.post("/cleanscadadevice/", response_model=None, summary="清空SCADA设备表", tags=["SCADA设备"])
|
@router.post("/scada-device-cleaning-runs", response_model=None, summary="清空SCADA设备表", tags=["SCADA设备"])
|
||||||
async def fastapi_clean_scada_device(
|
async def fastapi_clean_scada_device(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
@@ -185,7 +183,7 @@ async def fastapi_clean_scada_device(
|
|||||||
"""
|
"""
|
||||||
return clean_scada_device(network)
|
return clean_scada_device(network)
|
||||||
|
|
||||||
@router.get("/getallscadadeviceids/", summary="获取所有SCADA设备ID", tags=["SCADA设备"])
|
@router.get("/scada-devices/ids", summary="获取所有SCADA设备ID", tags=["SCADA设备"])
|
||||||
async def fastapi_get_all_scada_device_ids(
|
async def fastapi_get_all_scada_device_ids(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
@@ -200,7 +198,7 @@ async def fastapi_get_all_scada_device_ids(
|
|||||||
"""
|
"""
|
||||||
return get_all_scada_device_ids(network)
|
return get_all_scada_device_ids(network)
|
||||||
|
|
||||||
@router.get("/getallscadadevices/", summary="获取所有SCADA设备", tags=["SCADA设备"])
|
@router.get("/scada-devices", summary="获取所有SCADA设备", tags=["SCADA设备"])
|
||||||
async def fastapi_get_all_scada_devices(
|
async def fastapi_get_all_scada_devices(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
@@ -220,7 +218,7 @@ async def fastapi_get_all_scada_devices(
|
|||||||
# scada_device_data 设备数据管理
|
# scada_device_data 设备数据管理
|
||||||
############################################################
|
############################################################
|
||||||
|
|
||||||
@router.get("/getscadadevicedataschema/", summary="获取SCADA设备数据架构", tags=["SCADA设备数据"])
|
@router.get("/network-schemas/scada-device-data", summary="获取SCADA设备数据架构", tags=["SCADA设备数据"])
|
||||||
async def fastapi_get_scada_device_data_schema(
|
async def fastapi_get_scada_device_data_schema(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
) -> dict[str, dict[str, Any]]:
|
) -> dict[str, dict[str, Any]]:
|
||||||
@@ -237,7 +235,7 @@ async def fastapi_get_scada_device_data_schema(
|
|||||||
"""
|
"""
|
||||||
return get_scada_device_data_schema(network)
|
return get_scada_device_data_schema(network)
|
||||||
|
|
||||||
@router.get("/getscadadevicedata/", summary="获取SCADA设备数据", tags=["SCADA设备数据"])
|
@router.get("/scada-device-datas/detail", summary="获取SCADA设备数据", tags=["SCADA设备数据"])
|
||||||
async def fastapi_get_scada_device_data(
|
async def fastapi_get_scada_device_data(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
device_id: str = Query(..., description="SCADA设备ID")
|
device_id: str = Query(..., description="SCADA设备ID")
|
||||||
@@ -256,7 +254,7 @@ async def fastapi_get_scada_device_data(
|
|||||||
"""
|
"""
|
||||||
return get_scada_device_data(network, device_id)
|
return get_scada_device_data(network, device_id)
|
||||||
|
|
||||||
@router.post("/setscadadevicedata/", response_model=None, summary="更新SCADA设备数据", tags=["SCADA设备数据"])
|
@router.patch("/scada-device-datas", response_model=None, summary="更新SCADA设备数据", tags=["SCADA设备数据"])
|
||||||
async def fastapi_set_scada_device_data(
|
async def fastapi_set_scada_device_data(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
req: Request = None
|
||||||
@@ -276,7 +274,7 @@ async def fastapi_set_scada_device_data(
|
|||||||
props = await req.json()
|
props = await req.json()
|
||||||
return set_scada_device_data(network, ChangeSet(props))
|
return set_scada_device_data(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.post("/addscadadevicedata/", response_model=None, summary="添加SCADA设备数据", tags=["SCADA设备数据"])
|
@router.post("/scada-device-datas", response_model=None, summary="添加SCADA设备数据", tags=["SCADA设备数据"])
|
||||||
async def fastapi_add_scada_device_data(
|
async def fastapi_add_scada_device_data(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
req: Request = None
|
||||||
@@ -296,7 +294,7 @@ async def fastapi_add_scada_device_data(
|
|||||||
props = await req.json()
|
props = await req.json()
|
||||||
return add_scada_device_data(network, ChangeSet(props))
|
return add_scada_device_data(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.post("/deletescadadevicedata/", response_model=None, summary="删除SCADA设备数据", tags=["SCADA设备数据"])
|
@router.delete("/scada-device-datas", response_model=None, summary="删除SCADA设备数据", tags=["SCADA设备数据"])
|
||||||
async def fastapi_delete_scada_device_data(
|
async def fastapi_delete_scada_device_data(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
req: Request = None
|
||||||
@@ -316,7 +314,7 @@ async def fastapi_delete_scada_device_data(
|
|||||||
props = await req.json()
|
props = await req.json()
|
||||||
return delete_scada_device_data(network, ChangeSet(props))
|
return delete_scada_device_data(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.post("/cleanscadadevicedata/", response_model=None, summary="清空SCADA设备数据表", tags=["SCADA设备数据"])
|
@router.post("/scada-device-data-cleaning-runs", response_model=None, summary="清空SCADA设备数据表", tags=["SCADA设备数据"])
|
||||||
async def fastapi_clean_scada_device_data(
|
async def fastapi_clean_scada_device_data(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
@@ -338,7 +336,7 @@ async def fastapi_clean_scada_device_data(
|
|||||||
# scada_element SCADA元素映射
|
# scada_element SCADA元素映射
|
||||||
############################################################
|
############################################################
|
||||||
|
|
||||||
@router.get("/getscadaelementschema/", summary="获取SCADA元素架构", tags=["SCADA元素映射"])
|
@router.get("/network-schemas/scada-element", summary="获取SCADA元素架构", tags=["SCADA元素映射"])
|
||||||
async def fastapi_get_scada_element_schema(
|
async def fastapi_get_scada_element_schema(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
) -> dict[str, dict[str, Any]]:
|
) -> dict[str, dict[str, Any]]:
|
||||||
@@ -355,7 +353,7 @@ async def fastapi_get_scada_element_schema(
|
|||||||
"""
|
"""
|
||||||
return get_scada_element_schema(network)
|
return get_scada_element_schema(network)
|
||||||
|
|
||||||
@router.get("/getscadaelements/", summary="获取所有SCADA元素映射", tags=["SCADA元素映射"])
|
@router.get("/scada-elements", summary="获取所有SCADA元素映射", tags=["SCADA元素映射"])
|
||||||
async def fastapi_get_scada_elements(
|
async def fastapi_get_scada_elements(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
@@ -372,7 +370,7 @@ async def fastapi_get_scada_elements(
|
|||||||
"""
|
"""
|
||||||
return get_all_scada_elements(network)
|
return get_all_scada_elements(network)
|
||||||
|
|
||||||
@router.get("/getscadaelement/", summary="获取单个SCADA元素映射", tags=["SCADA元素映射"])
|
@router.get("/scada-elements/detail", summary="获取单个SCADA元素映射", tags=["SCADA元素映射"])
|
||||||
async def fastapi_get_scada_element(
|
async def fastapi_get_scada_element(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
id: str = Query(..., description="SCADA元素映射ID")
|
id: str = Query(..., description="SCADA元素映射ID")
|
||||||
@@ -391,7 +389,7 @@ async def fastapi_get_scada_element(
|
|||||||
"""
|
"""
|
||||||
return get_scada_element(network, id)
|
return get_scada_element(network, id)
|
||||||
|
|
||||||
@router.post("/setscadaelement/", response_model=None, summary="更新SCADA元素映射", tags=["SCADA元素映射"])
|
@router.patch("/scada-elements", response_model=None, summary="更新SCADA元素映射", tags=["SCADA元素映射"])
|
||||||
async def fastapi_set_scada_element(
|
async def fastapi_set_scada_element(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
req: Request = None
|
||||||
@@ -411,7 +409,7 @@ async def fastapi_set_scada_element(
|
|||||||
props = await req.json()
|
props = await req.json()
|
||||||
return set_scada_element(network, ChangeSet(props))
|
return set_scada_element(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.post("/addscadaelement/", response_model=None, summary="添加SCADA元素映射", tags=["SCADA元素映射"])
|
@router.post("/scada-elements", response_model=None, summary="添加SCADA元素映射", tags=["SCADA元素映射"])
|
||||||
async def fastapi_add_scada_element(
|
async def fastapi_add_scada_element(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
req: Request = None
|
||||||
@@ -431,7 +429,7 @@ async def fastapi_add_scada_element(
|
|||||||
props = await req.json()
|
props = await req.json()
|
||||||
return add_scada_element(network, ChangeSet(props))
|
return add_scada_element(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.post("/deletescadaelement/", response_model=None, summary="删除SCADA元素映射", tags=["SCADA元素映射"])
|
@router.delete("/scada-elements", response_model=None, summary="删除SCADA元素映射", tags=["SCADA元素映射"])
|
||||||
async def fastapi_delete_scada_element(
|
async def fastapi_delete_scada_element(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
req: Request = None
|
||||||
@@ -451,7 +449,7 @@ async def fastapi_delete_scada_element(
|
|||||||
props = await req.json()
|
props = await req.json()
|
||||||
return delete_scada_element(network, ChangeSet(props))
|
return delete_scada_element(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.post("/cleanscadaelement/", response_model=None, summary="清空SCADA元素映射表", tags=["SCADA元素映射"])
|
@router.post("/scada-element-cleaning-runs", response_model=None, summary="清空SCADA元素映射表", tags=["SCADA元素映射"])
|
||||||
async def fastapi_clean_scada_element(
|
async def fastapi_clean_scada_element(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
@@ -473,7 +471,7 @@ async def fastapi_clean_scada_element(
|
|||||||
# scada_info SCADA信息
|
# scada_info SCADA信息
|
||||||
############################################################
|
############################################################
|
||||||
|
|
||||||
@router.get("/getscadainfoschema/", summary="获取SCADA信息架构", tags=["SCADA信息"])
|
@router.get("/scada-info-schemas", summary="获取SCADA信息架构", tags=["SCADA信息"])
|
||||||
async def fastapi_get_scada_info_schema(
|
async def fastapi_get_scada_info_schema(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||||
) -> dict[str, dict[str, Any]]:
|
) -> dict[str, dict[str, Any]]:
|
||||||
@@ -490,7 +488,7 @@ async def fastapi_get_scada_info_schema(
|
|||||||
"""
|
"""
|
||||||
return get_scada_info_schema(network)
|
return get_scada_info_schema(network)
|
||||||
|
|
||||||
@router.get("/getscadainfo/", summary="获取SCADA信息", tags=["SCADA信息"])
|
@router.get("/scada-info/detail", summary="获取SCADA信息", tags=["SCADA信息"])
|
||||||
async def fastapi_get_scada_info(
|
async def fastapi_get_scada_info(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
id: str = Query(..., description="SCADA信息ID")
|
id: str = Query(..., description="SCADA信息ID")
|
||||||
@@ -509,7 +507,7 @@ async def fastapi_get_scada_info(
|
|||||||
"""
|
"""
|
||||||
return get_scada_info(network, id)
|
return get_scada_info(network, id)
|
||||||
|
|
||||||
@router.get("/getallscadainfo/", summary="获取所有SCADA信息", tags=["SCADA信息"])
|
@router.get("/scada-info", summary="获取所有SCADA信息", tags=["SCADA信息"])
|
||||||
async def fastapi_get_all_scada_info(
|
async def fastapi_get_all_scada_info(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
from fastapi import APIRouter, Query
|
from datetime import datetime
|
||||||
from typing import Any, List, Dict
|
from fastapi import APIRouter, HTTPException, Path, Query
|
||||||
|
from typing import Any
|
||||||
from app.services.tjnetwork import get_scheme_schema, get_scheme, get_all_schemes
|
from app.services.tjnetwork import get_scheme_schema, get_scheme, get_all_schemes
|
||||||
|
from app.services.scheme_management import query_scheme_detail
|
||||||
|
from app.services.time_api import extract_date
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.get("/getschemeschema/", summary="获取方案模式", description="获取指定网络的方案模式定义")
|
@router.get("/network-schemas/scheme", summary="获取方案模式", description="获取指定网络的方案模式定义")
|
||||||
async def fastapi_get_scheme_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[Any, Any]]:
|
async def fastapi_get_scheme_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[Any, Any]]:
|
||||||
"""
|
"""
|
||||||
获取方案模式定义
|
获取方案模式定义
|
||||||
@@ -13,7 +16,7 @@ async def fastapi_get_scheme_schema(network: str = Query(..., description="管
|
|||||||
"""
|
"""
|
||||||
return get_scheme_schema(network)
|
return get_scheme_schema(network)
|
||||||
|
|
||||||
@router.get("/getscheme/", summary="获取单个方案", description="根据名称获取指定的方案信息")
|
@router.get("/schemes/detail", summary="获取单个方案", description="根据名称获取指定的方案信息")
|
||||||
async def fastapi_get_scheme(network: str = Query(..., description="管网名称(或数据库名称)"), schema_name: str = Query(..., description="方案名称")) -> dict[Any, Any]:
|
async def fastapi_get_scheme(network: str = Query(..., description="管网名称(或数据库名称)"), schema_name: str = Query(..., description="方案名称")) -> dict[Any, Any]:
|
||||||
"""
|
"""
|
||||||
获取单个方案详情
|
获取单个方案详情
|
||||||
@@ -23,11 +26,35 @@ async def fastapi_get_scheme(network: str = Query(..., description="管网名称
|
|||||||
return get_scheme(network, schema_name)
|
return get_scheme(network, schema_name)
|
||||||
|
|
||||||
@router.get("/schemes", summary="获取所有方案", description="获取指定网络的所有方案信息")
|
@router.get("/schemes", summary="获取所有方案", description="获取指定网络的所有方案信息")
|
||||||
@router.get("/getallschemes/", summary="获取所有方案(旧路径)", description="获取指定网络的所有方案信息", deprecated=True)
|
async def fastapi_get_all_schemes(
|
||||||
async def fastapi_get_all_schemes(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[dict[Any, Any]]:
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
|
scheme_type: str | None = Query(None, description="方案类型;为空时返回全部类型"),
|
||||||
|
query_date: datetime | None = Query(None, description="查询日期(可选)"),
|
||||||
|
) -> list[dict[Any, Any]]:
|
||||||
"""
|
"""
|
||||||
获取所有方案列表
|
获取所有方案列表
|
||||||
|
|
||||||
返回指定网络中所有可用的方案
|
返回指定网络中所有可用的方案
|
||||||
"""
|
"""
|
||||||
return get_all_schemes(network)
|
parsed_date = (
|
||||||
|
extract_date(query_date, field_name="query_date")
|
||||||
|
if query_date is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
return get_all_schemes(network, scheme_type=scheme_type, query_date=parsed_date)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/schemes/{scheme_name}", summary="获取方案详情", description="按方案类型获取指定方案详情")
|
||||||
|
async def fastapi_get_scheme_detail(
|
||||||
|
scheme_name: str = Path(..., description="方案名称"),
|
||||||
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
|
scheme_type: str | None = Query(None, description="方案类型;为空时返回通用方案详情"),
|
||||||
|
) -> dict[Any, Any]:
|
||||||
|
result = query_scheme_detail(
|
||||||
|
name=network,
|
||||||
|
scheme_name=scheme_name,
|
||||||
|
scheme_type=scheme_type,
|
||||||
|
)
|
||||||
|
if not result:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Scheme {scheme_name} not found")
|
||||||
|
return result
|
||||||
|
|||||||
@@ -0,0 +1,265 @@
|
|||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Path, 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 (
|
||||||
|
SensorPointResponse,
|
||||||
|
SensorPlacementExportRequest,
|
||||||
|
SensorPlacementOptimizeRequest,
|
||||||
|
SensorPlacementSchemeResponse,
|
||||||
|
SensorPlacementUpdateRequest,
|
||||||
|
)
|
||||||
|
from app.services.sensor_placement import (
|
||||||
|
SensorPlacementConflictError,
|
||||||
|
SensorPlacementNotFoundError,
|
||||||
|
SensorPlacementValidationError,
|
||||||
|
build_sensor_placement_workbook,
|
||||||
|
can_edit_sensor_placement,
|
||||||
|
get_sensor_placement_candidate,
|
||||||
|
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.get(
|
||||||
|
"/sensor-placement-candidates/{node_id}",
|
||||||
|
response_model=SensorPointResponse,
|
||||||
|
summary="获取监测点候选节点详情",
|
||||||
|
)
|
||||||
|
async def get_sensor_placement_candidate_detail(
|
||||||
|
node_id: str = Path(..., min_length=1, max_length=32),
|
||||||
|
project_context: ProjectContext = Depends(get_project_context),
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
try:
|
||||||
|
return await run_in_threadpool(
|
||||||
|
get_sensor_placement_candidate,
|
||||||
|
project_context.project_code,
|
||||||
|
node_id,
|
||||||
|
)
|
||||||
|
except SensorPlacementValidationError as exc:
|
||||||
|
raise _service_http_error(exc) from exc
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/sensor-placement-optimization-runs",
|
||||||
|
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}"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
from typing import Any, List, Optional
|
from typing import Any, List, Optional
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
import threading
|
import threading
|
||||||
from fastapi import APIRouter, Depends, HTTPException, File, UploadFile, Query, Path, Body
|
from fastapi import APIRouter, Depends, HTTPException, Query, Path, Body
|
||||||
from fastapi.responses import PlainTextResponse
|
from fastapi.responses import PlainTextResponse
|
||||||
from app.auth.keycloak_dependencies import get_current_keycloak_username
|
from app.auth.keycloak_dependencies import get_current_keycloak_username
|
||||||
import app.services.simulation as simulation
|
import app.services.simulation as simulation
|
||||||
@@ -29,7 +27,6 @@ from app.algorithms.sensor import (
|
|||||||
pressure_sensor_placement_kmeans,
|
pressure_sensor_placement_kmeans,
|
||||||
)
|
)
|
||||||
|
|
||||||
from app.services.network_import import network_update
|
|
||||||
from app.services.simulation_ops import (
|
from app.services.simulation_ops import (
|
||||||
project_management,
|
project_management,
|
||||||
scheduling_simulation,
|
scheduling_simulation,
|
||||||
@@ -142,7 +139,7 @@ def run_simulation_manually_by_date(
|
|||||||
|
|
||||||
|
|
||||||
# 必须用这个PlainTextResponse,不然每个key都有引号
|
# 必须用这个PlainTextResponse,不然每个key都有引号
|
||||||
@router.get("/runproject/", response_class=PlainTextResponse, summary="运行项目模拟", description="基于指定的管网项目运行标准水力模拟,返回纯文本格式的模拟报告。")
|
@router.post("/project-runs", response_class=PlainTextResponse, summary="运行项目模拟", description="基于指定的管网项目运行标准水力模拟,返回纯文本格式的模拟报告。")
|
||||||
async def run_project_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")) -> str:
|
async def run_project_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")) -> str:
|
||||||
"""
|
"""
|
||||||
运行项目模拟
|
运行项目模拟
|
||||||
@@ -158,7 +155,7 @@ async def run_project_endpoint(network: str = Query(..., description="管网名
|
|||||||
# output 和 report
|
# output 和 report
|
||||||
# output 是 json
|
# output 是 json
|
||||||
# report 是 text
|
# report 是 text
|
||||||
@router.get("/runprojectreturndict/", summary="运行项目模拟(返回字典)", description="基于指定的管网项目运行标准水力模拟,返回JSON格式的字典,包含输出数据和报告文本。")
|
@router.post("/project-return-dict-runs", summary="运行项目模拟(返回字典)", description="基于指定的管网项目运行标准水力模拟,返回JSON格式的字典,包含输出数据和报告文本。")
|
||||||
async def run_project_return_dict_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
|
async def run_project_return_dict_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
运行项目模拟(返回字典)
|
运行项目模拟(返回字典)
|
||||||
@@ -175,7 +172,7 @@ async def run_project_return_dict_endpoint(network: str = Query(..., description
|
|||||||
|
|
||||||
|
|
||||||
# put in inp folder, name without extension
|
# put in inp folder, name without extension
|
||||||
@router.get("/runinp/", summary="运行INP文件", description="运行指定INP文件格式的管网模型进行水力模拟。INP文件应该放在inp文件夹中,参数为文件名不含扩展名。")
|
@router.post("/inp-runs", summary="运行INP文件", description="运行指定INP文件格式的管网模型进行水力模拟。INP文件应该放在inp文件夹中,参数为文件名不含扩展名。")
|
||||||
async def run_inp_endpoint(network: str = Query(..., description="inp文件名(不含扩展名)")) -> str:
|
async def run_inp_endpoint(network: str = Query(..., description="inp文件名(不含扩展名)")) -> str:
|
||||||
"""
|
"""
|
||||||
运行INP文件
|
运行INP文件
|
||||||
@@ -188,7 +185,7 @@ async def run_inp_endpoint(network: str = Query(..., description="inp文件名
|
|||||||
|
|
||||||
|
|
||||||
# path is absolute path
|
# path is absolute path
|
||||||
@router.get("/dumpoutput/", summary="导出模拟输出", description="导出指定路径的模拟输出文件内容。参数应为绝对路径。")
|
@router.get("/outputs", summary="导出模拟输出", description="导出指定路径的模拟输出文件内容。参数应为绝对路径。")
|
||||||
async def dump_output_endpoint(output: str = Query(..., description="模拟输出文件的绝对路径")) -> str:
|
async def dump_output_endpoint(output: str = Query(..., description="模拟输出文件的绝对路径")) -> str:
|
||||||
"""
|
"""
|
||||||
导出模拟输出
|
导出模拟输出
|
||||||
@@ -201,8 +198,7 @@ async def dump_output_endpoint(output: str = Query(..., description="模拟输
|
|||||||
|
|
||||||
|
|
||||||
# Analysis Endpoints
|
# Analysis Endpoints
|
||||||
@router.get("/burst-analysis", summary="爆管分析(高级)", description="高级版本的爆管分析,支持在指定时间点修改泵控制模式和阀门开度,以分析这些改变对爆管影响的作用。支持固定泵和变速泵的独立控制。")
|
@router.post("/burst-analyses", summary="爆管分析(高级)", description="高级版本的爆管分析,支持在指定时间点修改泵控制模式和阀门开度,以分析这些改变对爆管影响的作用。支持固定泵和变速泵的独立控制。")
|
||||||
@router.get("/burst_analysis/", summary="爆管分析(高级,旧路径)", description="高级版本的爆管分析,支持在指定时间点修改泵控制模式和阀门开度,以分析这些改变对爆管影响的作用。支持固定泵和变速泵的独立控制。", deprecated=True)
|
|
||||||
async def fastapi_burst_analysis(
|
async def fastapi_burst_analysis(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
modify_pattern_start_time: str = Query(..., description="模式修改开始时间(ISO 8601格式)"),
|
modify_pattern_start_time: str = Query(..., description="模式修改开始时间(ISO 8601格式)"),
|
||||||
@@ -236,7 +232,7 @@ async def fastapi_burst_analysis(
|
|||||||
return "success"
|
return "success"
|
||||||
|
|
||||||
|
|
||||||
@router.get("/valve_close_analysis/", response_class=PlainTextResponse, summary="阀门关闭分析(高级)", description="高级版本的阀门关闭分析,支持同时关闭多个阀门,并在指定持续时间内进行模拟。返回纯文本格式的分析结果。")
|
@router.post("/valve-closure-analyses", response_class=PlainTextResponse, summary="阀门关闭分析(高级)", description="高级版本的阀门关闭分析,支持同时关闭多个阀门,并在指定持续时间内进行模拟。返回纯文本格式的分析结果。")
|
||||||
async def fastapi_valve_close_analysis(
|
async def fastapi_valve_close_analysis(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
start_time: str = Query(..., description="阀门关闭开始时间(ISO 8601格式)"),
|
start_time: str = Query(..., description="阀门关闭开始时间(ISO 8601格式)"),
|
||||||
@@ -265,8 +261,7 @@ async def fastapi_valve_close_analysis(
|
|||||||
return result or "success"
|
return result or "success"
|
||||||
|
|
||||||
|
|
||||||
@router.get("/valve-isolation-analysis", summary="阀门隔离分析", description="分析当发生突发事件时,通过关闭指定阀门进行隔离,确定哪些阀门必须关闭、哪些可选关闭,以及隔离的可行性。")
|
@router.post("/valve-isolation-analyses", summary="阀门隔离分析", description="分析当发生突发事件时,通过关闭指定阀门进行隔离,确定哪些阀门必须关闭、哪些可选关闭,以及隔离的可行性。")
|
||||||
@router.get("/valve_isolation_analysis/", summary="阀门隔离分析(旧路径)", description="分析当发生突发事件时,通过关闭指定阀门进行隔离,确定哪些阀门必须关闭、哪些可选关闭,以及隔离的可行性。", deprecated=True)
|
|
||||||
async def valve_isolation_endpoint(
|
async def valve_isolation_endpoint(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
accident_element: List[str] = Query(..., description="发生事故的管段/节点ID列表"),
|
accident_element: List[str] = Query(..., description="发生事故的管段/节点ID列表"),
|
||||||
@@ -282,7 +277,8 @@ async def valve_isolation_endpoint(
|
|||||||
返回隔离方案,包括:
|
返回隔离方案,包括:
|
||||||
- must_close_valves: 必须关闭的阀门列表
|
- must_close_valves: 必须关闭的阀门列表
|
||||||
- optional_valves: 可选关闭的阀门列表
|
- optional_valves: 可选关闭的阀门列表
|
||||||
- affected_nodes: 受影响的节点列表
|
- affected_nodes: 受影响的节点列表;不可隔离时为空列表
|
||||||
|
- affected_node_count: 受影响的节点总数
|
||||||
- isolatable: 是否可以有效隔离
|
- isolatable: 是否可以有效隔离
|
||||||
"""
|
"""
|
||||||
# result = {
|
# result = {
|
||||||
@@ -306,8 +302,7 @@ async def valve_isolation_endpoint(
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@router.get("/flushing-analysis", response_class=PlainTextResponse, summary="冲洗分析(高级)", description="高级版本的冲洗分析,支持同时开启多个阀门进行冲洗,指定排污节点,并设置固定的冲洗流量。返回纯文本格式的分析结果。")
|
@router.post("/flushing-analyses", response_class=PlainTextResponse, summary="冲洗分析(高级)", description="高级版本的冲洗分析,支持同时开启多个阀门进行冲洗,指定排污节点,并设置固定的冲洗流量。返回纯文本格式的分析结果。")
|
||||||
@router.get("/flushing_analysis/", response_class=PlainTextResponse, summary="冲洗分析(高级,旧路径)", description="高级版本的冲洗分析,支持同时开启多个阀门进行冲洗,指定排污节点,并设置固定的冲洗流量。返回纯文本格式的分析结果。", deprecated=True)
|
|
||||||
async def fastapi_flushing_analysis(
|
async def fastapi_flushing_analysis(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
start_time: str = Query(..., description="冲洗开始时间(ISO 8601格式)"),
|
start_time: str = Query(..., description="冲洗开始时间(ISO 8601格式)"),
|
||||||
@@ -349,8 +344,7 @@ async def fastapi_flushing_analysis(
|
|||||||
return result or "success"
|
return result or "success"
|
||||||
|
|
||||||
|
|
||||||
@router.get("/contaminant-simulation", response_class=PlainTextResponse, summary="污染物模拟", description="对管网中的污染物扩散进行模拟,评估污染源对管网的影响范围和浓度分布。支持指定污染源位置、污染浓度和扩散模式。")
|
@router.post("/contaminant-simulations", response_class=PlainTextResponse, summary="污染物模拟", description="对管网中的污染物扩散进行模拟,评估污染源对管网的影响范围和浓度分布。支持指定污染源位置、污染浓度和扩散模式。")
|
||||||
@router.get("/contaminant_simulation/", response_class=PlainTextResponse, summary="污染物模拟(旧路径)", description="对管网中的污染物扩散进行模拟,评估污染源对管网的影响范围和浓度分布。支持指定污染源位置、污染浓度和扩散模式。", deprecated=True)
|
|
||||||
async def fastapi_contaminant_simulation(
|
async def fastapi_contaminant_simulation(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
start_time: str = Query(..., description="污染开始时间(ISO 8601格式)"),
|
start_time: str = Query(..., description="污染开始时间(ISO 8601格式)"),
|
||||||
@@ -387,7 +381,7 @@ async def fastapi_contaminant_simulation(
|
|||||||
return result or "success"
|
return result or "success"
|
||||||
|
|
||||||
|
|
||||||
@router.get("/age_analysis/", response_class=PlainTextResponse, summary="水龄分析(高级)", description="高级版本的水龄分析,在指定时间点进行分析,支持自定义模拟持续时间。返回纯文本格式的分析结果。")
|
@router.post("/water-age-analyses", response_class=PlainTextResponse, summary="水龄分析(高级)", description="高级版本的水龄分析,在指定时间点进行分析,支持自定义模拟持续时间。返回纯文本格式的分析结果。")
|
||||||
async def fastapi_age_analysis(
|
async def fastapi_age_analysis(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
start_time: str = Query(..., description="分析开始时间(ISO 8601格式)"),
|
start_time: str = Query(..., description="分析开始时间(ISO 8601格式)"),
|
||||||
@@ -411,7 +405,7 @@ async def fastapi_age_analysis(
|
|||||||
# return scheduling_analysis(network)
|
# return scheduling_analysis(network)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/pressureregulation/", summary="压力调节(基础)", description="对管网的压力进行调节分析,通过控制泵的运行来维持目标节点的目标压力。此为基础版本。")
|
@router.post("/pressure-regulation-calculations", summary="压力调节(基础)", description="对管网的压力进行调节分析,通过控制泵的运行来维持目标节点的目标压力。此为基础版本。")
|
||||||
async def pressure_regulation_endpoint(
|
async def pressure_regulation_endpoint(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
target_node: str = Query(..., description="目标节点ID"),
|
target_node: str = Query(..., description="目标节点ID"),
|
||||||
@@ -429,7 +423,7 @@ async def pressure_regulation_endpoint(
|
|||||||
return pressure_regulation(network, target_node, target_pressure)
|
return pressure_regulation(network, target_node, target_pressure)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/pressure_regulation/", summary="压力调节(高级)", description="高级版本的压力调节分析,通过JSON请求体提供详细的控制参数,包括固定泵和变速泵的独立控制、水箱初始水位等。")
|
@router.post("/pressure-regulation-analyses", summary="压力调节(高级)", description="高级版本的压力调节分析,通过JSON请求体提供详细的控制参数,包括固定泵和变速泵的独立控制、水箱初始水位等。")
|
||||||
async def fastapi_pressure_regulation(data: PressureRegulation = Body(..., description="压力调节控制参数")) -> str:
|
async def fastapi_pressure_regulation(data: PressureRegulation = Body(..., description="压力调节控制参数")) -> str:
|
||||||
"""
|
"""
|
||||||
压力调节(高级版本)
|
压力调节(高级版本)
|
||||||
@@ -467,7 +461,7 @@ async def fastapi_pressure_regulation(data: PressureRegulation = Body(..., descr
|
|||||||
return "success"
|
return "success"
|
||||||
|
|
||||||
|
|
||||||
@router.post("/project_management/", summary="项目管理(高级)", description="高级版本的项目管理,通过JSON请求体提供详细的控制参数,包括泵控制策略、水箱初始水位和区域需水量控制。")
|
@router.post("/project-managements", summary="项目管理(高级)", description="高级版本的项目管理,通过JSON请求体提供详细的控制参数,包括泵控制策略、水箱初始水位和区域需水量控制。")
|
||||||
async def fastapi_project_management(data: ProjectManagement = Body(..., description="项目管理控制参数")) -> str:
|
async def fastapi_project_management(data: ProjectManagement = Body(..., description="项目管理控制参数")) -> str:
|
||||||
"""
|
"""
|
||||||
项目管理(高级版本)
|
项目管理(高级版本)
|
||||||
@@ -496,7 +490,7 @@ async def fastapi_project_management(data: ProjectManagement = Body(..., descrip
|
|||||||
# return daily_scheduling_analysis(network)
|
# return daily_scheduling_analysis(network)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/scheduling_analysis/", summary="排程分析", description="对管网的供水排程进行分析,优化泵的运行时间和出水流量,平衡水厂出水、水箱进出水,满足用户需求。")
|
@router.post("/scheduling-analyses", summary="排程分析", description="对管网的供水排程进行分析,优化泵的运行时间和出水流量,平衡水厂出水、水箱进出水,满足用户需求。")
|
||||||
async def fastapi_scheduling_analysis(data: SchedulingAnalysis = Body(..., description="排程分析参数")) -> str:
|
async def fastapi_scheduling_analysis(data: SchedulingAnalysis = Body(..., description="排程分析参数")) -> str:
|
||||||
"""
|
"""
|
||||||
排程分析
|
排程分析
|
||||||
@@ -522,7 +516,7 @@ async def fastapi_scheduling_analysis(data: SchedulingAnalysis = Body(..., descr
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/daily_scheduling_analysis/", summary="日排程分析", description="对管网的每日供水排程进行分析,优化水库、水厂、水箱和用户需求的协调,制定合理的每日排程方案。")
|
@router.post("/daily-scheduling-analyses", summary="日排程分析", description="对管网的每日供水排程进行分析,优化水库、水厂、水箱和用户需求的协调,制定合理的每日排程方案。")
|
||||||
async def fastapi_daily_scheduling_analysis(data: DailySchedulingAnalysis = Body(..., description="日排程分析参数")) -> str:
|
async def fastapi_daily_scheduling_analysis(data: DailySchedulingAnalysis = Body(..., description="日排程分析参数")) -> str:
|
||||||
"""
|
"""
|
||||||
日排程分析
|
日排程分析
|
||||||
@@ -549,52 +543,12 @@ async def fastapi_daily_scheduling_analysis(data: DailySchedulingAnalysis = Body
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/network_project/", summary="导入网络项目", description="通过上传INP格式的管网文件导入新的网络项目。系统将自动处理文件并执行模拟。")
|
|
||||||
async def fastapi_network_project(file: UploadFile = File(..., description="INP格式的管网文件")) -> str:
|
|
||||||
"""
|
|
||||||
导入网络项目
|
|
||||||
|
|
||||||
- **file**: 上传的INP格式管网文件
|
|
||||||
|
|
||||||
系统将上传的文件保存到inp文件夹并执行模拟。
|
|
||||||
"""
|
|
||||||
temp_file_dir = "./inp/"
|
|
||||||
if not os.path.exists(temp_file_dir):
|
|
||||||
os.mkdir(temp_file_dir)
|
|
||||||
temp_file_name = f'network_project_{datetime.now().strftime("%Y%m%d")}'
|
|
||||||
temp_file_path = f"{temp_file_dir}{temp_file_name}.inp"
|
|
||||||
with open(temp_file_path, "wb") as buffer:
|
|
||||||
shutil.copyfileobj(file.file, buffer)
|
|
||||||
return run_inp(temp_file_name)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/network_update/", summary="管网更新(高级)", description="通过上传更新文件对管网进行高级的更新操作。系统将处理更新文件并应用到数据库。")
|
|
||||||
async def fastapi_network_update(file: UploadFile = File(..., description="包含管网更新信息的文件")) -> str:
|
|
||||||
"""
|
|
||||||
管网更新(高级版本)
|
|
||||||
|
|
||||||
- **file**: 包含管网更新信息的文件
|
|
||||||
|
|
||||||
系统将处理上传的文件并应用管网更新。
|
|
||||||
"""
|
|
||||||
default_folder = "./"
|
|
||||||
temp_file_name = f'network_update_{datetime.now().strftime("%Y%m%d")}'
|
|
||||||
temp_file_path = os.path.join(default_folder, temp_file_name)
|
|
||||||
try:
|
|
||||||
with open(temp_file_path, "wb") as buffer:
|
|
||||||
shutil.copyfileobj(file.file, buffer)
|
|
||||||
network_update(temp_file_path)
|
|
||||||
return json.dumps({"message": "管网更新成功"})
|
|
||||||
except Exception as exc:
|
|
||||||
raise HTTPException(status_code=500, detail=f"数据库操作失败: {exc}")
|
|
||||||
|
|
||||||
|
|
||||||
# @router.get("/pumpfailure/")
|
# @router.get("/pumpfailure/")
|
||||||
# async def pump_failure_endpoint(network: str, pump_id: str, time: str):
|
# async def pump_failure_endpoint(network: str, pump_id: str, time: str):
|
||||||
# return pump_failure(network, pump_id, time)
|
# return pump_failure(network, pump_id, time)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/pump_failure/", summary="泵故障管理", description="记录和管理泵的故障状态,包括故障发生时间和受影响的泵列表。系统将记录故障日志并更新泵状态。")
|
@router.post("/pump-failure-events", summary="泵故障管理", description="记录和管理泵的故障状态,包括故障发生时间和受影响的泵列表。系统将记录故障日志并更新泵状态。")
|
||||||
async def fastapi_pump_failure(data: PumpFailureState = Body(..., description="泵故障状态信息")) -> str:
|
async def fastapi_pump_failure(data: PumpFailureState = Body(..., description="泵故障状态信息")) -> str:
|
||||||
"""
|
"""
|
||||||
泵故障管理
|
泵故障管理
|
||||||
@@ -638,7 +592,7 @@ async def fastapi_pump_failure(data: PumpFailureState = Body(..., description="
|
|||||||
return json.dumps("SUCCESS")
|
return json.dumps("SUCCESS")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/pressuresensorplacementsensitivity/", summary="压力传感器放置-灵敏度分析(基础)", description="基于灵敏度分析方法,为指定管网项目确定最优的压力传感器放置位置。此为基础版本。")
|
@router.post("/pressure-sensor-placement-sensitivity-calculations", summary="压力传感器放置-灵敏度分析(基础)", description="基于灵敏度分析方法,为指定管网项目确定最优的压力传感器放置位置。此为基础版本。")
|
||||||
async def pressure_sensor_placement_sensitivity_endpoint(
|
async def pressure_sensor_placement_sensitivity_endpoint(
|
||||||
name: str = Query(..., description="管网名称(或数据库名称)"),
|
name: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
scheme_name: str = Query(..., description="放置方案名称"),
|
scheme_name: str = Query(..., description="放置方案名称"),
|
||||||
@@ -662,7 +616,7 @@ async def pressure_sensor_placement_sensitivity_endpoint(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/pressure_sensor_placement_sensitivity/", summary="压力传感器放置-灵敏度分析(高级)", description="高级版本的压力传感器放置分析,通过JSON请求体提供详细参数。基于灵敏度分析方法确定最优放置位置。")
|
@router.post("/pressure-sensor-placement-sensitivities", summary="压力传感器放置-灵敏度分析(高级)", description="高级版本的压力传感器放置分析,通过JSON请求体提供详细参数。基于灵敏度分析方法确定最优放置位置。")
|
||||||
async def fastapi_pressure_sensor_placement_sensitivity(
|
async def fastapi_pressure_sensor_placement_sensitivity(
|
||||||
data: PressureSensorPlacement = Body(..., description="传感器放置分析参数"),
|
data: PressureSensorPlacement = Body(..., description="传感器放置分析参数"),
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -688,7 +642,7 @@ async def fastapi_pressure_sensor_placement_sensitivity(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/pressuresensorplacementkmeans/", summary="压力传感器放置-KMeans聚类分析(基础)", description="基于KMeans聚类算法,为指定管网项目确定压力传感器的最优放置位置。此为基础版本。")
|
@router.post("/pressure-sensor-placement-kmeans-calculations", summary="压力传感器放置-KMeans聚类分析(基础)", description="基于KMeans聚类算法,为指定管网项目确定压力传感器的最优放置位置。此为基础版本。")
|
||||||
async def pressure_sensor_placement_kmeans_endpoint(
|
async def pressure_sensor_placement_kmeans_endpoint(
|
||||||
name: str = Query(..., description="管网名称(或数据库名称)"),
|
name: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
scheme_name: str = Query(..., description="放置方案名称"),
|
scheme_name: str = Query(..., description="放置方案名称"),
|
||||||
@@ -712,7 +666,7 @@ async def pressure_sensor_placement_kmeans_endpoint(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/pressure_sensor_placement_kmeans/", summary="压力传感器放置-KMeans聚类分析(高级)", description="高级版本的压力传感器放置分析,通过JSON请求体提供详细参数。基于KMeans聚类算法确定最优放置位置。")
|
@router.post("/pressure-sensor-placement-kmeans", summary="压力传感器放置-KMeans聚类分析(高级)", description="高级版本的压力传感器放置分析,通过JSON请求体提供详细参数。基于KMeans聚类算法确定最优放置位置。")
|
||||||
async def fastapi_pressure_sensor_placement_kmeans(
|
async def fastapi_pressure_sensor_placement_kmeans(
|
||||||
data: PressureSensorPlacement = Body(..., description="传感器放置分析参数"),
|
data: PressureSensorPlacement = Body(..., description="传感器放置分析参数"),
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -739,7 +693,6 @@ async def fastapi_pressure_sensor_placement_kmeans(
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/sensor-placement-schemes", summary="传感器放置方案创建", description="创建新的传感器放置方案,支持灵敏度分析和KMeans聚类两种方法。根据指定的方法自动计算最优的传感器放置位置。")
|
@router.post("/sensor-placement-schemes", summary="传感器放置方案创建", description="创建新的传感器放置方案,支持灵敏度分析和KMeans聚类两种方法。根据指定的方法自动计算最优的传感器放置位置。")
|
||||||
@router.post("/sensorplacementscheme/create", summary="传感器放置方案创建(旧路径)", description="创建新的传感器放置方案,支持灵敏度分析和KMeans聚类两种方法。根据指定的方法自动计算最优的传感器放置位置。", deprecated=True)
|
|
||||||
async def fastapi_pressure_sensor_placement(
|
async def fastapi_pressure_sensor_placement(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
scheme_name: str = Query(..., description="放置方案名称"),
|
scheme_name: str = Query(..., description="放置方案名称"),
|
||||||
@@ -787,8 +740,7 @@ async def fastapi_pressure_sensor_placement(
|
|||||||
return "success"
|
return "success"
|
||||||
|
|
||||||
|
|
||||||
@router.post("/simulations/run-by-date", summary="手动运行日期指定模拟", description="根据指定的开始时间和持续时间,手动运行水力模拟。开始时间必须是显式带时区的 ISO 8601 / RFC3339 时间。")
|
@router.post("/simulation-runs", summary="手动运行日期指定模拟", description="根据指定的开始时间和持续时间,手动运行水力模拟。开始时间必须是显式带时区的 ISO 8601 / RFC3339 时间。")
|
||||||
@router.post("/runsimulationmanuallybydate/", summary="手动运行日期指定模拟(旧路径)", description="根据指定的开始时间和持续时间,手动运行水力模拟。开始时间必须是显式带时区的 ISO 8601 / RFC3339 时间。", deprecated=True)
|
|
||||||
async def fastapi_run_simulation_manually_by_date(
|
async def fastapi_run_simulation_manually_by_date(
|
||||||
data: RunSimulationManuallyByDate = Body(..., description="模拟运行参数"),
|
data: RunSimulationManuallyByDate = Body(..., description="模拟运行参数"),
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from fastapi import APIRouter, Request, Query
|
from fastapi import APIRouter, Depends, Request, Query
|
||||||
|
from app.auth.permissions import SIMULATION_RUN, require_permission
|
||||||
from app.services.tjnetwork import (
|
from app.services.tjnetwork import (
|
||||||
ChangeSet,
|
ChangeSet,
|
||||||
get_current_operation,
|
get_current_operation,
|
||||||
@@ -22,7 +23,7 @@ from app.services.tjnetwork import (
|
|||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.get("/getcurrentoperationid/", summary="获取当前操作ID", description="获取网络当前的操作ID")
|
@router.get("/current-operation-ids", summary="获取当前操作ID", description="获取网络当前的操作ID")
|
||||||
async def get_current_operation_id_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")) -> int:
|
async def get_current_operation_id_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")) -> int:
|
||||||
"""
|
"""
|
||||||
获取当前操作ID
|
获取当前操作ID
|
||||||
@@ -31,7 +32,7 @@ async def get_current_operation_id_endpoint(network: str = Query(..., descriptio
|
|||||||
"""
|
"""
|
||||||
return get_current_operation(network)
|
return get_current_operation(network)
|
||||||
|
|
||||||
@router.post("/undo/", summary="撤销操作", description="撤销网络上最后的一个操作")
|
@router.post("/undos", summary="撤销操作", description="撤销网络上最后的一个操作")
|
||||||
async def undo_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")):
|
async def undo_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")):
|
||||||
"""
|
"""
|
||||||
撤销操作
|
撤销操作
|
||||||
@@ -40,7 +41,7 @@ async def undo_endpoint(network: str = Query(..., description="管网名称(
|
|||||||
"""
|
"""
|
||||||
return execute_undo(network)
|
return execute_undo(network)
|
||||||
|
|
||||||
@router.post("/redo/", summary="重做操作", description="重做网络上被撤销的操作")
|
@router.post("/redos", summary="重做操作", description="重做网络上被撤销的操作")
|
||||||
async def redo_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")):
|
async def redo_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")):
|
||||||
"""
|
"""
|
||||||
重做操作
|
重做操作
|
||||||
@@ -49,7 +50,7 @@ async def redo_endpoint(network: str = Query(..., description="管网名称(
|
|||||||
"""
|
"""
|
||||||
return execute_redo(network)
|
return execute_redo(network)
|
||||||
|
|
||||||
@router.get("/getsnapshots/", summary="获取快照列表", description="获取网络中的所有快照")
|
@router.get("/snapshots", summary="获取快照列表", description="获取网络中的所有快照")
|
||||||
async def list_snapshot_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[tuple[int, str]]:
|
async def list_snapshot_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[tuple[int, str]]:
|
||||||
"""
|
"""
|
||||||
获取快照列表
|
获取快照列表
|
||||||
@@ -58,7 +59,7 @@ async def list_snapshot_endpoint(network: str = Query(..., description="管网
|
|||||||
"""
|
"""
|
||||||
return list_snapshot(network)
|
return list_snapshot(network)
|
||||||
|
|
||||||
@router.get("/havesnapshot/", summary="检查快照是否存在", description="检查指定标签的快照是否存在")
|
@router.get("/snapshots/existence", summary="检查快照是否存在", description="检查指定标签的快照是否存在")
|
||||||
async def have_snapshot_endpoint(network: str = Query(..., description="管网名称(或数据库名称)"), tag: str = Query(..., description="快照标签")) -> bool:
|
async def have_snapshot_endpoint(network: str = Query(..., description="管网名称(或数据库名称)"), tag: str = Query(..., description="快照标签")) -> bool:
|
||||||
"""
|
"""
|
||||||
检查快照是否存在
|
检查快照是否存在
|
||||||
@@ -67,7 +68,7 @@ async def have_snapshot_endpoint(network: str = Query(..., description="管网
|
|||||||
"""
|
"""
|
||||||
return have_snapshot(network, tag)
|
return have_snapshot(network, tag)
|
||||||
|
|
||||||
@router.get("/havesnapshotforoperation/", summary="检查操作快照是否存在", description="检查指定操作ID的快照是否存在")
|
@router.get("/snapshot-for-operations", summary="检查操作快照是否存在", description="检查指定操作ID的快照是否存在")
|
||||||
async def have_snapshot_for_operation_endpoint(network: str = Query(..., description="管网名称(或数据库名称)"), operation: int = Query(..., description="操作ID")) -> bool:
|
async def have_snapshot_for_operation_endpoint(network: str = Query(..., description="管网名称(或数据库名称)"), operation: int = Query(..., description="操作ID")) -> bool:
|
||||||
"""
|
"""
|
||||||
检查操作快照是否存在
|
检查操作快照是否存在
|
||||||
@@ -76,7 +77,7 @@ async def have_snapshot_for_operation_endpoint(network: str = Query(..., descrip
|
|||||||
"""
|
"""
|
||||||
return have_snapshot_for_operation(network, operation)
|
return have_snapshot_for_operation(network, operation)
|
||||||
|
|
||||||
@router.get("/havesnapshotforcurrentoperation/", summary="检查当前操作快照是否存在", description="检查当前操作的快照是否存在")
|
@router.get("/snapshot-for-current-operations", summary="检查当前操作快照是否存在", description="检查当前操作的快照是否存在")
|
||||||
async def have_snapshot_for_current_operation_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")) -> bool:
|
async def have_snapshot_for_current_operation_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")) -> bool:
|
||||||
"""
|
"""
|
||||||
检查当前操作快照是否存在
|
检查当前操作快照是否存在
|
||||||
@@ -85,7 +86,7 @@ async def have_snapshot_for_current_operation_endpoint(network: str = Query(...,
|
|||||||
"""
|
"""
|
||||||
return have_snapshot_for_current_operation(network)
|
return have_snapshot_for_current_operation(network)
|
||||||
|
|
||||||
@router.post("/takesnapshotforoperation/", summary="为操作创建快照", description="为指定的操作创建快照")
|
@router.post("/snapshot-for-operations", summary="为操作创建快照", description="为指定的操作创建快照")
|
||||||
async def take_snapshot_for_operation_endpoint(
|
async def take_snapshot_for_operation_endpoint(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
operation: int = Query(..., description="操作ID"),
|
operation: int = Query(..., description="操作ID"),
|
||||||
@@ -98,7 +99,7 @@ async def take_snapshot_for_operation_endpoint(
|
|||||||
"""
|
"""
|
||||||
return take_snapshot_for_operation(network, operation, tag)
|
return take_snapshot_for_operation(network, operation, tag)
|
||||||
|
|
||||||
@router.post("/takesnapshotforcurrentoperation", summary="为当前操作创建快照", description="为当前操作创建快照")
|
@router.post("/snapshot-for-current-operations", summary="为当前操作创建快照", description="为当前操作创建快照")
|
||||||
async def take_snapshot_for_current_operation_endpoint(network: str = Query(..., description="管网名称(或数据库名称)"), tag: str = Query(..., description="快照标签")) -> None:
|
async def take_snapshot_for_current_operation_endpoint(network: str = Query(..., description="管网名称(或数据库名称)"), tag: str = Query(..., description="快照标签")) -> None:
|
||||||
"""
|
"""
|
||||||
为当前操作创建快照
|
为当前操作创建快照
|
||||||
@@ -107,17 +108,7 @@ async def take_snapshot_for_current_operation_endpoint(network: str = Query(...,
|
|||||||
"""
|
"""
|
||||||
return take_snapshot_for_current_operation(network, tag)
|
return take_snapshot_for_current_operation(network, tag)
|
||||||
|
|
||||||
# 兼容旧拼写: takenapshotforcurrentoperation
|
@router.post("/snapshots", summary="创建快照", description="为网络创建一个快照")
|
||||||
@router.post("/takenapshotforcurrentoperation", summary="为当前操作创建快照(兼容模式)", description="为当前操作创建快照(兼容旧的API路径)")
|
|
||||||
async def take_snapshot_for_current_operation_legacy_endpoint(network: str = Query(..., description="管网名称(或数据库名称)"), tag: str = Query(..., description="快照标签")) -> None:
|
|
||||||
"""
|
|
||||||
为当前操作创建快照(兼容模式)
|
|
||||||
|
|
||||||
兼容旧的API路径,为网络当前操作创建一个快照
|
|
||||||
"""
|
|
||||||
return take_snapshot_for_current_operation(network, tag)
|
|
||||||
|
|
||||||
@router.post("/takesnapshot/", summary="创建快照", description="为网络创建一个快照")
|
|
||||||
async def take_snapshot_endpoint(network: str = Query(..., description="管网名称(或数据库名称)"), tag: str = Query(..., description="快照标签")) -> None:
|
async def take_snapshot_endpoint(network: str = Query(..., description="管网名称(或数据库名称)"), tag: str = Query(..., description="快照标签")) -> None:
|
||||||
"""
|
"""
|
||||||
创建快照
|
创建快照
|
||||||
@@ -126,7 +117,7 @@ async def take_snapshot_endpoint(network: str = Query(..., description="管网
|
|||||||
"""
|
"""
|
||||||
return take_snapshot(network, tag)
|
return take_snapshot(network, tag)
|
||||||
|
|
||||||
@router.post("/picksnapshot/", summary="选择快照", description="选择并恢复到指定的快照", response_model=None)
|
@router.patch("/snapshots", summary="选择快照", description="选择并恢复到指定的快照", response_model=None)
|
||||||
async def pick_snapshot_endpoint(network: str = Query(..., description="管网名称(或数据库名称)"), tag: str = Query(..., description="快照标签"), discard: bool = Query(False, description="是否丢弃当前更改")) -> ChangeSet:
|
async def pick_snapshot_endpoint(network: str = Query(..., description="管网名称(或数据库名称)"), tag: str = Query(..., description="快照标签"), discard: bool = Query(False, description="是否丢弃当前更改")) -> ChangeSet:
|
||||||
"""
|
"""
|
||||||
选择快照
|
选择快照
|
||||||
@@ -135,7 +126,7 @@ async def pick_snapshot_endpoint(network: str = Query(..., description="管网
|
|||||||
"""
|
"""
|
||||||
return pick_snapshot(network, tag, discard)
|
return pick_snapshot(network, tag, discard)
|
||||||
|
|
||||||
@router.post("/pickoperation/", summary="选择操作", description="选择并恢复到指定的操作", response_model=None)
|
@router.patch("/operations", summary="选择操作", description="选择并恢复到指定的操作", response_model=None)
|
||||||
async def pick_operation_endpoint(
|
async def pick_operation_endpoint(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
operation: int = Query(..., description="操作ID"),
|
operation: int = Query(..., description="操作ID"),
|
||||||
@@ -148,8 +139,12 @@ async def pick_operation_endpoint(
|
|||||||
"""
|
"""
|
||||||
return pick_operation(network, operation, discard)
|
return pick_operation(network, operation, discard)
|
||||||
|
|
||||||
@router.get("/syncwithserver/", summary="与服务器同步", description="将网络与服务器同步到指定操作", response_model=None)
|
@router.post("/with-servers", summary="与服务器同步", description="将网络与服务器同步到指定操作", response_model=None)
|
||||||
async def sync_with_server_endpoint(network: str = Query(..., description="管网名称(或数据库名称)"), operation: int = Query(..., description="目标操作ID")) -> ChangeSet:
|
async def sync_with_server_endpoint(
|
||||||
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
|
operation: int = Query(..., description="目标操作ID"),
|
||||||
|
_=Depends(require_permission(SIMULATION_RUN)),
|
||||||
|
) -> ChangeSet:
|
||||||
"""
|
"""
|
||||||
与服务器同步
|
与服务器同步
|
||||||
|
|
||||||
@@ -157,7 +152,7 @@ async def sync_with_server_endpoint(network: str = Query(..., description="管
|
|||||||
"""
|
"""
|
||||||
return sync_with_server(network, operation)
|
return sync_with_server(network, operation)
|
||||||
|
|
||||||
@router.post("/batch/", summary="执行批量命令", description="执行多个网络操作命令", response_model=None)
|
@router.post("/network-command-batches", summary="执行批量命令", description="执行多个网络操作命令", response_model=None)
|
||||||
async def execute_batch_commands_endpoint(network: str = Query(..., description="管网名称(或数据库名称)"), req: Request = None) -> ChangeSet:
|
async def execute_batch_commands_endpoint(network: str = Query(..., description="管网名称(或数据库名称)"), req: Request = None) -> ChangeSet:
|
||||||
"""
|
"""
|
||||||
执行批量命令
|
执行批量命令
|
||||||
@@ -170,7 +165,7 @@ async def execute_batch_commands_endpoint(network: str = Query(..., description=
|
|||||||
rcs = execute_batch_commands(network, cs)
|
rcs = execute_batch_commands(network, cs)
|
||||||
return rcs
|
return rcs
|
||||||
|
|
||||||
@router.post("/compressedbatch/", summary="执行压缩批量命令", description="执行压缩的批量命令", response_model=None)
|
@router.post("/network-command-batches/compressed", summary="执行压缩批量命令", description="执行压缩的批量命令", response_model=None)
|
||||||
async def execute_compressed_batch_commands_endpoint(
|
async def execute_compressed_batch_commands_endpoint(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
req: Request = None
|
||||||
@@ -185,7 +180,7 @@ async def execute_compressed_batch_commands_endpoint(
|
|||||||
cs.operations = jo_root["operations"]
|
cs.operations = jo_root["operations"]
|
||||||
return execute_batch_command(network, cs)
|
return execute_batch_command(network, cs)
|
||||||
|
|
||||||
@router.get("/getrestoreoperation/", summary="获取恢复操作ID", description="获取网络的恢复操作ID")
|
@router.get("/restore-operations", summary="获取恢复操作ID", description="获取网络的恢复操作ID")
|
||||||
async def get_restore_operation_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")) -> int:
|
async def get_restore_operation_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")) -> int:
|
||||||
"""
|
"""
|
||||||
获取恢复操作ID
|
获取恢复操作ID
|
||||||
@@ -194,7 +189,7 @@ async def get_restore_operation_endpoint(network: str = Query(..., description="
|
|||||||
"""
|
"""
|
||||||
return get_restore_operation(network)
|
return get_restore_operation(network)
|
||||||
|
|
||||||
@router.post("/setrestoreoperation/", summary="设置恢复操作ID", description="设置网络的恢复操作ID")
|
@router.patch("/restore-operations", summary="设置恢复操作ID", description="设置网络的恢复操作ID")
|
||||||
async def set_restore_operation_endpoint(network: str = Query(..., description="管网名称(或数据库名称)"), operation: int = Query(..., description="操作ID")) -> None:
|
async def set_restore_operation_endpoint(network: str = Query(..., description="管网名称(或数据库名称)"), operation: int = Query(..., description="操作ID")) -> None:
|
||||||
"""
|
"""
|
||||||
设置恢复操作ID
|
设置恢复操作ID
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from .dependencies import get_timescale_connection, get_postgres_connection
|
|||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/composite/scada-simulation", summary="获取SCADA关联的模拟数据")
|
@router.get("/timeseries/views/scada-simulations", summary="获取SCADA关联的模拟数据")
|
||||||
async def get_scada_associated_simulation_data(
|
async def get_scada_associated_simulation_data(
|
||||||
start_time: datetime = Query(..., description="查询开始时间"),
|
start_time: datetime = Query(..., description="查询开始时间"),
|
||||||
end_time: datetime = Query(..., description="查询结束时间"),
|
end_time: datetime = Query(..., description="查询结束时间"),
|
||||||
@@ -73,7 +73,7 @@ async def get_scada_associated_simulation_data(
|
|||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.get("/composite/element-simulation", summary="获取管网元素的模拟数据")
|
@router.get("/timeseries/views/element-simulations", summary="获取管网元素的模拟数据")
|
||||||
async def get_feature_simulation_data(
|
async def get_feature_simulation_data(
|
||||||
start_time: datetime = Query(..., description="查询开始时间"),
|
start_time: datetime = Query(..., description="查询开始时间"),
|
||||||
end_time: datetime = Query(..., description="查询结束时间"),
|
end_time: datetime = Query(..., description="查询结束时间"),
|
||||||
@@ -143,7 +143,7 @@ async def get_feature_simulation_data(
|
|||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.get("/composite/element-scada", summary="获取管网元素关联的SCADA监测数据")
|
@router.get("/timeseries/views/element-scada-readings", summary="获取管网元素关联的SCADA监测数据")
|
||||||
async def get_element_associated_scada_data(
|
async def get_element_associated_scada_data(
|
||||||
element_id: str = Query(..., description="管网元素ID(管道或节点)"),
|
element_id: str = Query(..., description="管网元素ID(管道或节点)"),
|
||||||
start_time: datetime = Query(..., description="查询开始时间"),
|
start_time: datetime = Query(..., description="查询开始时间"),
|
||||||
@@ -185,7 +185,7 @@ async def get_element_associated_scada_data(
|
|||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.post("/composite/clean-scada", summary="清洗SCADA监测数据")
|
@router.post("/timeseries/scada-cleaning-runs", summary="清洗SCADA监测数据")
|
||||||
async def clean_scada_data(
|
async def clean_scada_data(
|
||||||
device_ids: str = Query(..., description="设备ID列表或 'all' 表示清洗所有设备"),
|
device_ids: str = Query(..., description="设备ID列表或 'all' 表示清洗所有设备"),
|
||||||
start_time: datetime = Query(..., description="清洗数据的开始时间"),
|
start_time: datetime = Query(..., description="清洗数据的开始时间"),
|
||||||
@@ -228,7 +228,7 @@ async def clean_scada_data(
|
|||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.get("/composite/pipeline-health-prediction", summary="预测管道健康状况")
|
@router.get("/pipeline-health-predictions", summary="预测管道健康状况")
|
||||||
async def predict_pipeline_health(
|
async def predict_pipeline_health(
|
||||||
query_time: datetime = Query(..., description="查询时间"),
|
query_time: datetime = Query(..., description="查询时间"),
|
||||||
network_name: str = Query(..., description="管网名称(或数据库名称)"),
|
network_name: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ TIME_RANGE_START_DESC = f"时间范围开始时间。{TIME_WITH_TZ_DESC}"
|
|||||||
TIME_RANGE_END_DESC = f"时间范围结束时间。{TIME_WITH_TZ_DESC}"
|
TIME_RANGE_END_DESC = f"时间范围结束时间。{TIME_WITH_TZ_DESC}"
|
||||||
|
|
||||||
|
|
||||||
@router.post("/realtime/links/batch", status_code=201, summary="批量插入实时管道数据")
|
@router.post("/timeseries/realtime/links/batches", status_code=201, summary="批量插入实时管道数据")
|
||||||
async def insert_realtime_links(
|
async def insert_realtime_links(
|
||||||
data: List[dict] = Body(..., description="管道数据列表,每项包含管道ID、时间戳等信息"),
|
data: List[dict] = Body(..., description="管道数据列表,每项包含管道ID、时间戳等信息"),
|
||||||
conn: AsyncConnection = Depends(get_timescale_connection)
|
conn: AsyncConnection = Depends(get_timescale_connection)
|
||||||
@@ -34,7 +34,7 @@ async def insert_realtime_links(
|
|||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/realtime/links",
|
"/timeseries/realtime/links",
|
||||||
summary="查询实时管道数据",
|
summary="查询实时管道数据",
|
||||||
description="按时间范围查询实时管道数据。start_time 和 end_time 必须显式带时区;允许传 UTC+8,服务端会先归一化为 UTC 再执行查询。",
|
description="按时间范围查询实时管道数据。start_time 和 end_time 必须显式带时区;允许传 UTC+8,服务端会先归一化为 UTC 再执行查询。",
|
||||||
)
|
)
|
||||||
@@ -60,7 +60,7 @@ async def get_realtime_links(
|
|||||||
|
|
||||||
|
|
||||||
@router.delete(
|
@router.delete(
|
||||||
"/realtime/links",
|
"/timeseries/realtime/links",
|
||||||
summary="删除实时管道数据",
|
summary="删除实时管道数据",
|
||||||
description="按时间范围删除实时管道数据。start_time 和 end_time 必须显式带时区;允许传 UTC+8,服务端按请求中的绝对时间删除对应 UTC 数据。",
|
description="按时间范围删除实时管道数据。start_time 和 end_time 必须显式带时区;允许传 UTC+8,服务端按请求中的绝对时间删除对应 UTC 数据。",
|
||||||
)
|
)
|
||||||
@@ -85,7 +85,7 @@ async def delete_realtime_links(
|
|||||||
return {"message": "Deleted successfully"}
|
return {"message": "Deleted successfully"}
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/realtime/links/{link_id}/field", summary="更新实时管道字段")
|
@router.patch("/timeseries/realtime/links/{link_id}/field", summary="更新实时管道字段")
|
||||||
async def update_realtime_link_field(
|
async def update_realtime_link_field(
|
||||||
link_id: str = Path(..., description="管道ID"),
|
link_id: str = Path(..., description="管道ID"),
|
||||||
time: datetime = Query(..., description=f"要更新记录的时间戳。{TIME_WITH_TZ_DESC}"),
|
time: datetime = Query(..., description=f"要更新记录的时间戳。{TIME_WITH_TZ_DESC}"),
|
||||||
@@ -117,7 +117,7 @@ async def update_realtime_link_field(
|
|||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.post("/realtime/nodes/batch", status_code=201, summary="批量插入实时节点数据")
|
@router.post("/timeseries/realtime/nodes/batches", status_code=201, summary="批量插入实时节点数据")
|
||||||
async def insert_realtime_nodes(
|
async def insert_realtime_nodes(
|
||||||
data: List[dict] = Body(..., description="节点数据列表,每项包含节点ID、时间戳等信息"),
|
data: List[dict] = Body(..., description="节点数据列表,每项包含节点ID、时间戳等信息"),
|
||||||
conn: AsyncConnection = Depends(get_timescale_connection)
|
conn: AsyncConnection = Depends(get_timescale_connection)
|
||||||
@@ -138,7 +138,7 @@ async def insert_realtime_nodes(
|
|||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/realtime/nodes",
|
"/timeseries/realtime/nodes",
|
||||||
summary="查询实时节点数据",
|
summary="查询实时节点数据",
|
||||||
description="按时间范围查询实时节点数据。start_time 和 end_time 必须显式带时区;允许传 UTC+8,服务端会先归一化为 UTC 再执行查询。",
|
description="按时间范围查询实时节点数据。start_time 和 end_time 必须显式带时区;允许传 UTC+8,服务端会先归一化为 UTC 再执行查询。",
|
||||||
)
|
)
|
||||||
@@ -164,7 +164,7 @@ async def get_realtime_nodes(
|
|||||||
|
|
||||||
|
|
||||||
@router.delete(
|
@router.delete(
|
||||||
"/realtime/nodes",
|
"/timeseries/realtime/nodes",
|
||||||
summary="删除实时节点数据",
|
summary="删除实时节点数据",
|
||||||
description="按时间范围删除实时节点数据。start_time 和 end_time 必须显式带时区;允许传 UTC+8,服务端按请求中的绝对时间删除对应 UTC 数据。",
|
description="按时间范围删除实时节点数据。start_time 和 end_time 必须显式带时区;允许传 UTC+8,服务端按请求中的绝对时间删除对应 UTC 数据。",
|
||||||
)
|
)
|
||||||
@@ -191,7 +191,7 @@ async def delete_realtime_nodes(
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/realtime/simulation/store", status_code=201, summary="存储实时模拟结果")
|
@router.post("/timeseries/realtime/simulation-results", status_code=201, summary="存储实时模拟结果")
|
||||||
async def store_realtime_simulation_result(
|
async def store_realtime_simulation_result(
|
||||||
node_result_list: List[dict] = Body(..., description="节点模拟结果列表"),
|
node_result_list: List[dict] = Body(..., description="节点模拟结果列表"),
|
||||||
link_result_list: List[dict] = Body(..., description="管道模拟结果列表"),
|
link_result_list: List[dict] = Body(..., description="管道模拟结果列表"),
|
||||||
@@ -218,7 +218,7 @@ async def store_realtime_simulation_result(
|
|||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/realtime/query/by-time-property",
|
"/timeseries/realtime/records",
|
||||||
summary="按时间和属性查询实时数据",
|
summary="按时间和属性查询实时数据",
|
||||||
description="查询指定时间点的实时属性值。query_time 必须显式带时区;允许传 UTC+8,服务端会先归一化为 UTC 再执行查询。",
|
description="查询指定时间点的实时属性值。query_time 必须显式带时区;允许传 UTC+8,服务端会先归一化为 UTC 再执行查询。",
|
||||||
)
|
)
|
||||||
@@ -254,7 +254,7 @@ async def query_realtime_records_by_time_property(
|
|||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/realtime/query/by-id-time",
|
"/timeseries/realtime/simulation-results",
|
||||||
summary="按ID和时间查询实时模拟数据",
|
summary="按ID和时间查询实时模拟数据",
|
||||||
description="查询指定元素在某一时间点的实时模拟结果。query_time 必须显式带时区;允许传 UTC+8,服务端会先归一化为 UTC 再执行查询。",
|
description="查询指定元素在某一时间点的实时模拟结果。query_time 必须显式带时区;允许传 UTC+8,服务端会先归一化为 UTC 再执行查询。",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from .dependencies import get_timescale_connection
|
|||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.post("/scada/batch", status_code=201, summary="批量插入SCADA监测数据")
|
@router.post("/timeseries/scada-readings/batches", status_code=201, summary="批量插入SCADA监测数据")
|
||||||
async def insert_scada_data(
|
async def insert_scada_data(
|
||||||
data: List[dict] = Body(..., description="SCADA设备监测数据列表"),
|
data: List[dict] = Body(..., description="SCADA设备监测数据列表"),
|
||||||
conn: AsyncConnection = Depends(get_timescale_connection),
|
conn: AsyncConnection = Depends(get_timescale_connection),
|
||||||
@@ -29,7 +29,7 @@ async def insert_scada_data(
|
|||||||
return {"message": f"Inserted {len(data)} records"}
|
return {"message": f"Inserted {len(data)} records"}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/scada/by-ids-time-range", summary="按设备ID和时间范围查询SCADA数据")
|
@router.get("/timeseries/scada-readings", summary="按设备ID和时间范围查询SCADA数据")
|
||||||
async def get_scada_by_ids_time_range(
|
async def get_scada_by_ids_time_range(
|
||||||
start_time: datetime = Query(..., description="查询开始时间"),
|
start_time: datetime = Query(..., description="查询开始时间"),
|
||||||
end_time: datetime = Query(..., description="查询结束时间"),
|
end_time: datetime = Query(..., description="查询结束时间"),
|
||||||
@@ -60,7 +60,7 @@ async def get_scada_by_ids_time_range(
|
|||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/scada/by-ids-field-time-range", summary="按设备ID、字段和时间范围查询SCADA数据"
|
"/timeseries/scada-readings/fields", summary="按设备ID、字段和时间范围查询SCADA数据"
|
||||||
)
|
)
|
||||||
async def get_scada_field_by_ids_time_range(
|
async def get_scada_field_by_ids_time_range(
|
||||||
start_time: datetime = Query(..., description="查询开始时间"),
|
start_time: datetime = Query(..., description="查询开始时间"),
|
||||||
@@ -101,7 +101,7 @@ async def get_scada_field_by_ids_time_range(
|
|||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/scada/{device_id}/field", summary="更新SCADA设备字段")
|
@router.patch("/timeseries/scada-readings/{device_id}/field", summary="更新SCADA设备字段")
|
||||||
async def update_scada_field(
|
async def update_scada_field(
|
||||||
device_id: str = Path(..., description="设备ID"),
|
device_id: str = Path(..., description="设备ID"),
|
||||||
time: datetime = Query(..., description="更新数据的时间戳"),
|
time: datetime = Query(..., description="更新数据的时间戳"),
|
||||||
@@ -133,7 +133,7 @@ async def update_scada_field(
|
|||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/scada/by-id-time-range", summary="按设备ID和时间范围删除SCADA数据")
|
@router.delete("/timeseries/scada-readings", summary="按设备ID和时间范围删除SCADA数据")
|
||||||
async def delete_scada_data(
|
async def delete_scada_data(
|
||||||
device_id: str = Query(..., description="设备ID"),
|
device_id: str = Query(..., description="设备ID"),
|
||||||
start_time: datetime = Query(..., description="删除开始时间"),
|
start_time: datetime = Query(..., description="删除开始时间"),
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from .dependencies import get_timescale_connection
|
|||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.post("/scheme/links/batch", status_code=201, summary="批量插入方案管道数据")
|
@router.post("/timeseries/schemes/links/batches", status_code=201, summary="批量插入方案管道数据")
|
||||||
async def insert_scheme_links(
|
async def insert_scheme_links(
|
||||||
data: List[dict] = Body(..., description="方案管道数据列表"),
|
data: List[dict] = Body(..., description="方案管道数据列表"),
|
||||||
conn: AsyncConnection = Depends(get_timescale_connection),
|
conn: AsyncConnection = Depends(get_timescale_connection),
|
||||||
@@ -29,7 +29,7 @@ async def insert_scheme_links(
|
|||||||
return {"message": f"Inserted {len(data)} records"}
|
return {"message": f"Inserted {len(data)} records"}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/scheme/links", summary="查询方案管道数据")
|
@router.get("/timeseries/schemes/links", summary="查询方案管道数据")
|
||||||
async def get_scheme_links(
|
async def get_scheme_links(
|
||||||
scheme_type: str = Query(..., description="方案类型"),
|
scheme_type: str = Query(..., description="方案类型"),
|
||||||
scheme_name: str = Query(..., description="方案名称"),
|
scheme_name: str = Query(..., description="方案名称"),
|
||||||
@@ -56,7 +56,7 @@ async def get_scheme_links(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/scheme/links/{link_id}/field", summary="查询方案管道字段数据")
|
@router.get("/timeseries/schemes/links/{link_id}/field", summary="查询方案管道字段数据")
|
||||||
async def get_scheme_link_field(
|
async def get_scheme_link_field(
|
||||||
link_id: str = Path(..., description="管道ID"),
|
link_id: str = Path(..., description="管道ID"),
|
||||||
scheme_type: str = Query(..., description="方案类型"),
|
scheme_type: str = Query(..., description="方案类型"),
|
||||||
@@ -93,7 +93,7 @@ async def get_scheme_link_field(
|
|||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/scheme/links/{link_id}/field", summary="更新方案管道字段")
|
@router.patch("/timeseries/schemes/links/{link_id}/field", summary="更新方案管道字段")
|
||||||
async def update_scheme_link_field(
|
async def update_scheme_link_field(
|
||||||
link_id: str = Path(..., description="管道ID"),
|
link_id: str = Path(..., description="管道ID"),
|
||||||
scheme_type: str = Query(..., description="方案类型"),
|
scheme_type: str = Query(..., description="方案类型"),
|
||||||
@@ -131,7 +131,7 @@ async def update_scheme_link_field(
|
|||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/scheme/links", summary="删除方案管道数据")
|
@router.delete("/timeseries/schemes/links", summary="删除方案管道数据")
|
||||||
async def delete_scheme_links(
|
async def delete_scheme_links(
|
||||||
scheme_type: str = Query(..., description="方案类型"),
|
scheme_type: str = Query(..., description="方案类型"),
|
||||||
scheme_name: str = Query(..., description="方案名称"),
|
scheme_name: str = Query(..., description="方案名称"),
|
||||||
@@ -159,7 +159,7 @@ async def delete_scheme_links(
|
|||||||
return {"message": "Deleted successfully"}
|
return {"message": "Deleted successfully"}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/scheme/nodes/batch", status_code=201, summary="批量插入方案节点数据")
|
@router.post("/timeseries/schemes/nodes/batches", status_code=201, summary="批量插入方案节点数据")
|
||||||
async def insert_scheme_nodes(
|
async def insert_scheme_nodes(
|
||||||
data: List[dict] = Body(..., description="方案节点数据列表"),
|
data: List[dict] = Body(..., description="方案节点数据列表"),
|
||||||
conn: AsyncConnection = Depends(get_timescale_connection),
|
conn: AsyncConnection = Depends(get_timescale_connection),
|
||||||
@@ -179,7 +179,7 @@ async def insert_scheme_nodes(
|
|||||||
return {"message": f"Inserted {len(data)} records"}
|
return {"message": f"Inserted {len(data)} records"}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/scheme/nodes/{node_id}/field", summary="查询方案节点字段数据")
|
@router.get("/timeseries/schemes/nodes/{node_id}/field", summary="查询方案节点字段数据")
|
||||||
async def get_scheme_node_field(
|
async def get_scheme_node_field(
|
||||||
node_id: str = Path(..., description="节点ID"),
|
node_id: str = Path(..., description="节点ID"),
|
||||||
scheme_type: str = Query(..., description="方案类型"),
|
scheme_type: str = Query(..., description="方案类型"),
|
||||||
@@ -216,7 +216,7 @@ async def get_scheme_node_field(
|
|||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/scheme/nodes/{node_id}/field", summary="更新方案节点字段")
|
@router.patch("/timeseries/schemes/nodes/{node_id}/field", summary="更新方案节点字段")
|
||||||
async def update_scheme_node_field(
|
async def update_scheme_node_field(
|
||||||
node_id: str = Path(..., description="节点ID"),
|
node_id: str = Path(..., description="节点ID"),
|
||||||
scheme_type: str = Query(..., description="方案类型"),
|
scheme_type: str = Query(..., description="方案类型"),
|
||||||
@@ -254,7 +254,7 @@ async def update_scheme_node_field(
|
|||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/scheme/nodes", summary="删除方案节点数据")
|
@router.delete("/timeseries/schemes/nodes", summary="删除方案节点数据")
|
||||||
async def delete_scheme_nodes(
|
async def delete_scheme_nodes(
|
||||||
scheme_type: str = Query(..., description="方案类型"),
|
scheme_type: str = Query(..., description="方案类型"),
|
||||||
scheme_name: str = Query(..., description="方案名称"),
|
scheme_name: str = Query(..., description="方案名称"),
|
||||||
@@ -282,7 +282,7 @@ async def delete_scheme_nodes(
|
|||||||
return {"message": "Deleted successfully"}
|
return {"message": "Deleted successfully"}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/scheme/simulation/store", status_code=201, summary="存储方案模拟结果")
|
@router.post("/timeseries/schemes/simulation-results", status_code=201, summary="存储方案模拟结果")
|
||||||
async def store_scheme_simulation_result(
|
async def store_scheme_simulation_result(
|
||||||
scheme_type: str = Query(..., description="方案类型"),
|
scheme_type: str = Query(..., description="方案类型"),
|
||||||
scheme_name: str = Query(..., description="方案名称"),
|
scheme_name: str = Query(..., description="方案名称"),
|
||||||
@@ -318,7 +318,7 @@ async def store_scheme_simulation_result(
|
|||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/scheme/query/by-scheme-time-property", summary="按方案、时间和属性查询数据"
|
"/timeseries/schemes/records", summary="按方案、时间和属性查询数据"
|
||||||
)
|
)
|
||||||
async def query_scheme_records_by_scheme_time_property(
|
async def query_scheme_records_by_scheme_time_property(
|
||||||
scheme_type: str = Query(..., description="方案类型"),
|
scheme_type: str = Query(..., description="方案类型"),
|
||||||
@@ -355,7 +355,7 @@ async def query_scheme_records_by_scheme_time_property(
|
|||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.get("/scheme/query/by-id-time", summary="按ID和时间查询方案模拟数据")
|
@router.get("/timeseries/schemes/simulation-results", summary="按ID和时间查询方案模拟数据")
|
||||||
async def query_scheme_simulation_by_id_time(
|
async def query_scheme_simulation_by_id_time(
|
||||||
scheme_type: str = Query(..., description="方案类型"),
|
scheme_type: str = Query(..., description="方案类型"),
|
||||||
scheme_name: str = Query(..., description="方案名称"),
|
scheme_name: str = Query(..., description="方案名称"),
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
from fastapi import APIRouter, Request, Query
|
|
||||||
from typing import Any, List, Dict, Union
|
|
||||||
from app.services.tjnetwork import Any, get_all_users, get_user, get_user_schema
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
###########################################################
|
|
||||||
# user 39
|
|
||||||
###########################################################
|
|
||||||
|
|
||||||
@router.get("/getuserschema/", summary="获取用户模式", description="获取指定网络的用户模式定义")
|
|
||||||
async def fastapi_get_user_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[Any, Any]]:
|
|
||||||
"""
|
|
||||||
获取用户模式定义
|
|
||||||
|
|
||||||
返回指定网络的用户模式结构定义
|
|
||||||
"""
|
|
||||||
return get_user_schema(network)
|
|
||||||
|
|
||||||
@router.get("/getuser/", summary="获取单个用户", description="获取指定网络中的单个用户信息")
|
|
||||||
async def fastapi_get_user(network: str = Query(..., description="管网名称(或数据库名称)"), user_name: str = Query(..., description="用户名")) -> dict[Any, Any]:
|
|
||||||
"""
|
|
||||||
获取用户信息
|
|
||||||
|
|
||||||
返回指定网络中指定用户名的详细信息
|
|
||||||
"""
|
|
||||||
return get_user(network, user_name)
|
|
||||||
|
|
||||||
@router.get("/getallusers/", summary="获取所有用户", description="获取指定网络的所有用户列表")
|
|
||||||
async def fastapi_get_all_users(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[dict[Any, Any]]:
|
|
||||||
"""
|
|
||||||
获取所有用户列表
|
|
||||||
|
|
||||||
返回指定网络中所有用户的信息
|
|
||||||
"""
|
|
||||||
return get_all_users(network)
|
|
||||||
@@ -13,7 +13,7 @@ router = APIRouter()
|
|||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/web-search",
|
"/web-searches",
|
||||||
summary="Web Search",
|
summary="Web Search",
|
||||||
description="调用 Bocha Web Search API 获取实时网页搜索结果",
|
description="调用 Bocha Web Search API 获取实时网页搜索结果",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,391 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import inspect
|
||||||
|
import re
|
||||||
|
from collections.abc import Iterable
|
||||||
|
from copy import copy
|
||||||
|
from functools import wraps
|
||||||
|
from typing import Any, Generic, TypeVar, get_args, get_origin
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, Query
|
||||||
|
from fastapi.encoders import jsonable_encoder
|
||||||
|
from fastapi.routing import APIRoute
|
||||||
|
from pydantic import BaseModel, JsonValue, create_model
|
||||||
|
from starlette.responses import Response
|
||||||
|
|
||||||
|
from app.api.problem_details import ProblemDetails
|
||||||
|
from app.api.v1.router import api_router as handler_api_router
|
||||||
|
from app.auth.metadata_dependencies import get_current_metadata_user
|
||||||
|
from app.auth.project_dependencies import ProjectContext, get_project_context
|
||||||
|
|
||||||
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
|
class Page(BaseModel, Generic[T]):
|
||||||
|
items: list[T]
|
||||||
|
total: int
|
||||||
|
limit: int
|
||||||
|
offset: int
|
||||||
|
|
||||||
|
|
||||||
|
_NAME_IS_NETWORK = {
|
||||||
|
"pressure_sensor_placement_sensitivity_endpoint",
|
||||||
|
"pressure_sensor_placement_kmeans_endpoint",
|
||||||
|
}
|
||||||
|
_DERIVE_USERNAME = {
|
||||||
|
"pressure_sensor_placement_sensitivity_endpoint": "username",
|
||||||
|
"pressure_sensor_placement_kmeans_endpoint": "username",
|
||||||
|
"fastapi_pressure_sensor_placement": "user_name",
|
||||||
|
}
|
||||||
|
_PUBLIC_PARAMETER_RENAMES = {
|
||||||
|
"burst_ID": "burst_id",
|
||||||
|
"drainage_node_ID": "drainage_node_id",
|
||||||
|
}
|
||||||
|
_MODEL_NAME_IS_NETWORK = {"RunSimulationManuallyByDate"}
|
||||||
|
_MODEL_USERNAME_FROM_AUTH: set[str] = set()
|
||||||
|
|
||||||
|
|
||||||
|
def _clean_name(name: str) -> str:
|
||||||
|
for prefix in ("fastapi_", "fast_"):
|
||||||
|
if name.startswith(prefix):
|
||||||
|
name = name[len(prefix) :]
|
||||||
|
break
|
||||||
|
if name.endswith("_endpoint"):
|
||||||
|
name = name[: -len("_endpoint")]
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
def _rest_body_model(annotation):
|
||||||
|
if not inspect.isclass(annotation) or not issubclass(annotation, BaseModel):
|
||||||
|
return None
|
||||||
|
project_fields = {
|
||||||
|
name
|
||||||
|
for name in ("network", "network_name")
|
||||||
|
if name in annotation.model_fields
|
||||||
|
}
|
||||||
|
if annotation.__name__ in _MODEL_NAME_IS_NETWORK and "name" in annotation.model_fields:
|
||||||
|
project_fields.add("name")
|
||||||
|
username_fields = (
|
||||||
|
{
|
||||||
|
name
|
||||||
|
for name in ("username", "user_name")
|
||||||
|
if name in annotation.model_fields
|
||||||
|
}
|
||||||
|
if annotation.__name__ in _MODEL_USERNAME_FROM_AUTH
|
||||||
|
else set()
|
||||||
|
)
|
||||||
|
excluded_fields = project_fields | username_fields
|
||||||
|
if not excluded_fields:
|
||||||
|
return None
|
||||||
|
|
||||||
|
public_fields = {
|
||||||
|
name: (field.annotation, copy(field))
|
||||||
|
for name, field in annotation.model_fields.items()
|
||||||
|
if name not in excluded_fields
|
||||||
|
}
|
||||||
|
public_model = create_model(
|
||||||
|
f"{annotation.__name__}Rest",
|
||||||
|
__module__=annotation.__module__,
|
||||||
|
**public_fields,
|
||||||
|
)
|
||||||
|
return annotation, public_model, project_fields, username_fields
|
||||||
|
|
||||||
|
|
||||||
|
def _with_header_project_context(endpoint, route_name: str):
|
||||||
|
signature = inspect.signature(endpoint)
|
||||||
|
network_parameters = [
|
||||||
|
name for name in ("network", "network_name") if name in signature.parameters
|
||||||
|
]
|
||||||
|
if route_name in _NAME_IS_NETWORK and "name" in signature.parameters:
|
||||||
|
network_parameters.append("name")
|
||||||
|
username_parameter = _DERIVE_USERNAME.get(route_name)
|
||||||
|
parameter_renames = {
|
||||||
|
internal: public
|
||||||
|
for internal, public in _PUBLIC_PARAMETER_RENAMES.items()
|
||||||
|
if internal in signature.parameters
|
||||||
|
}
|
||||||
|
body_models = {
|
||||||
|
name: body_model
|
||||||
|
for name, parameter in signature.parameters.items()
|
||||||
|
if (body_model := _rest_body_model(parameter.annotation)) is not None
|
||||||
|
}
|
||||||
|
model_has_username = any(model[3] for model in body_models.values())
|
||||||
|
if (
|
||||||
|
not network_parameters
|
||||||
|
and not username_parameter
|
||||||
|
and not parameter_renames
|
||||||
|
and not body_models
|
||||||
|
):
|
||||||
|
return endpoint
|
||||||
|
|
||||||
|
existing_context_parameter = next(
|
||||||
|
(
|
||||||
|
name
|
||||||
|
for name, parameter in signature.parameters.items()
|
||||||
|
if parameter.annotation is ProjectContext
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
injected_context_name = existing_context_parameter or "_rest_project_context"
|
||||||
|
injected_user_name = "_rest_current_user"
|
||||||
|
|
||||||
|
@wraps(endpoint)
|
||||||
|
async def wrapper(*args, **kwargs):
|
||||||
|
project_context = kwargs.get(injected_context_name)
|
||||||
|
if not isinstance(project_context, ProjectContext):
|
||||||
|
raise RuntimeError("REST project context was not resolved")
|
||||||
|
if not existing_context_parameter:
|
||||||
|
kwargs.pop(injected_context_name, None)
|
||||||
|
for parameter_name in network_parameters:
|
||||||
|
kwargs[parameter_name] = project_context.project_code
|
||||||
|
if username_parameter:
|
||||||
|
kwargs[username_parameter] = kwargs[injected_user_name].username
|
||||||
|
kwargs.pop(injected_user_name, None)
|
||||||
|
for internal_name, public_name in parameter_renames.items():
|
||||||
|
kwargs[internal_name] = kwargs.pop(public_name)
|
||||||
|
for parameter_name, (
|
||||||
|
original_model,
|
||||||
|
_public_model,
|
||||||
|
project_fields,
|
||||||
|
username_fields,
|
||||||
|
) in body_models.items():
|
||||||
|
data = kwargs[parameter_name].model_dump()
|
||||||
|
data.update(
|
||||||
|
{field_name: project_context.project_code for field_name in project_fields}
|
||||||
|
)
|
||||||
|
if username_fields:
|
||||||
|
current_user = kwargs[injected_user_name]
|
||||||
|
data.update(
|
||||||
|
{field_name: current_user.username for field_name in username_fields}
|
||||||
|
)
|
||||||
|
kwargs[parameter_name] = original_model.model_validate(data)
|
||||||
|
if model_has_username:
|
||||||
|
kwargs.pop(injected_user_name, None)
|
||||||
|
result = endpoint(*args, **kwargs)
|
||||||
|
if inspect.isawaitable(result):
|
||||||
|
return await result
|
||||||
|
return result
|
||||||
|
|
||||||
|
parameters = []
|
||||||
|
for name, parameter in signature.parameters.items():
|
||||||
|
if name in network_parameters or name == username_parameter:
|
||||||
|
continue
|
||||||
|
public_name = parameter_renames.get(name, name)
|
||||||
|
if public_name != name:
|
||||||
|
default = copy(parameter.default)
|
||||||
|
default.alias = public_name
|
||||||
|
default.validation_alias = public_name
|
||||||
|
default.serialization_alias = public_name
|
||||||
|
parameter = parameter.replace(name=public_name, default=default)
|
||||||
|
if name in body_models:
|
||||||
|
parameter = parameter.replace(annotation=body_models[name][1])
|
||||||
|
parameters.append(parameter)
|
||||||
|
if not existing_context_parameter:
|
||||||
|
parameters.append(
|
||||||
|
inspect.Parameter(
|
||||||
|
injected_context_name,
|
||||||
|
kind=inspect.Parameter.KEYWORD_ONLY,
|
||||||
|
annotation=ProjectContext,
|
||||||
|
default=Depends(get_project_context),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if username_parameter or model_has_username:
|
||||||
|
parameters.append(
|
||||||
|
inspect.Parameter(
|
||||||
|
injected_user_name,
|
||||||
|
kind=inspect.Parameter.KEYWORD_ONLY,
|
||||||
|
default=Depends(get_current_metadata_user),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
wrapper.__signature__ = signature.replace(parameters=parameters)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def _with_pagination(endpoint):
|
||||||
|
signature = inspect.signature(endpoint)
|
||||||
|
handler_limit_parameter = "limit" if "limit" in signature.parameters else None
|
||||||
|
handler_offset_parameter = next(
|
||||||
|
(
|
||||||
|
parameter_name
|
||||||
|
for parameter_name in ("offset", "skip")
|
||||||
|
if parameter_name in signature.parameters
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
handler_handles_pagination = bool(
|
||||||
|
handler_limit_parameter or handler_offset_parameter
|
||||||
|
)
|
||||||
|
|
||||||
|
@wraps(endpoint)
|
||||||
|
async def wrapper(*args, **kwargs):
|
||||||
|
if handler_handles_pagination:
|
||||||
|
limit = kwargs.get(handler_limit_parameter, 0)
|
||||||
|
offset = kwargs.get(handler_offset_parameter, 0)
|
||||||
|
else:
|
||||||
|
limit = kwargs.pop("_rest_limit")
|
||||||
|
offset = kwargs.pop("_rest_offset")
|
||||||
|
result = endpoint(*args, **kwargs)
|
||||||
|
if inspect.isawaitable(result):
|
||||||
|
result = await result
|
||||||
|
if not isinstance(result, list):
|
||||||
|
return result
|
||||||
|
if handler_handles_pagination:
|
||||||
|
return Page(
|
||||||
|
items=result,
|
||||||
|
total=offset + len(result),
|
||||||
|
limit=limit or len(result),
|
||||||
|
offset=offset,
|
||||||
|
)
|
||||||
|
return Page(
|
||||||
|
items=result[offset : offset + limit],
|
||||||
|
total=len(result),
|
||||||
|
limit=limit,
|
||||||
|
offset=offset,
|
||||||
|
)
|
||||||
|
|
||||||
|
parameters = list(signature.parameters.values())
|
||||||
|
if not handler_handles_pagination:
|
||||||
|
parameters.extend(
|
||||||
|
[
|
||||||
|
inspect.Parameter(
|
||||||
|
"_rest_limit",
|
||||||
|
kind=inspect.Parameter.KEYWORD_ONLY,
|
||||||
|
annotation=int,
|
||||||
|
default=Query(100, ge=1, le=1000, alias="limit"),
|
||||||
|
),
|
||||||
|
inspect.Parameter(
|
||||||
|
"_rest_offset",
|
||||||
|
kind=inspect.Parameter.KEYWORD_ONLY,
|
||||||
|
annotation=int,
|
||||||
|
default=Query(0, ge=0, alias="offset"),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
wrapper.__signature__ = signature.replace(parameters=parameters)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def _with_jsonable_response(endpoint):
|
||||||
|
"""Normalize untyped handler results before JsonValue validation."""
|
||||||
|
|
||||||
|
@wraps(endpoint)
|
||||||
|
async def wrapper(*args, **kwargs):
|
||||||
|
result = endpoint(*args, **kwargs)
|
||||||
|
if inspect.isawaitable(result):
|
||||||
|
result = await result
|
||||||
|
if isinstance(result, Response):
|
||||||
|
return result
|
||||||
|
return jsonable_encoder(result)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def _adapt_route(route: APIRoute) -> APIRoute:
|
||||||
|
methods = route.methods or set()
|
||||||
|
if len(methods) != 1:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"REST route {route.name!r} must declare exactly one HTTP method"
|
||||||
|
)
|
||||||
|
method = next(iter(methods))
|
||||||
|
responses = dict(route.responses or {})
|
||||||
|
for status_code, description in (
|
||||||
|
(401, "Authentication required"),
|
||||||
|
(403, "Insufficient permission"),
|
||||||
|
(404, "Resource not found"),
|
||||||
|
(409, "Resource conflict"),
|
||||||
|
(422, "Validation error"),
|
||||||
|
(503, "Dependency unavailable"),
|
||||||
|
):
|
||||||
|
responses.setdefault(
|
||||||
|
status_code,
|
||||||
|
{"model": ProblemDetails, "description": description},
|
||||||
|
)
|
||||||
|
|
||||||
|
endpoint = _with_header_project_context(route.endpoint, route.name)
|
||||||
|
response_model = route.response_model
|
||||||
|
has_untyped_response = response_model is None
|
||||||
|
if has_untyped_response:
|
||||||
|
endpoint = _with_jsonable_response(endpoint)
|
||||||
|
if get_origin(response_model) is list:
|
||||||
|
item_type = get_args(response_model)[0] if get_args(response_model) else JsonValue
|
||||||
|
response_model = Page[item_type]
|
||||||
|
endpoint = _with_pagination(endpoint)
|
||||||
|
|
||||||
|
clean_name = _clean_name(route.name)
|
||||||
|
creates_resource = clean_name.startswith(
|
||||||
|
("add_", "create_", "copy_", "import_", "insert_", "store_", "take_", "upload_")
|
||||||
|
) or route.name == "fastapi_pressure_sensor_placement"
|
||||||
|
status_code = (
|
||||||
|
204
|
||||||
|
if method == "DELETE"
|
||||||
|
else 201
|
||||||
|
if method == "POST" and creates_resource
|
||||||
|
else route.status_code
|
||||||
|
)
|
||||||
|
if status_code == 204:
|
||||||
|
response_model = None
|
||||||
|
elif response_model is None:
|
||||||
|
response_model = JsonValue
|
||||||
|
|
||||||
|
return APIRoute(
|
||||||
|
path=route.path,
|
||||||
|
endpoint=endpoint,
|
||||||
|
response_model=response_model,
|
||||||
|
status_code=status_code,
|
||||||
|
tags=route.tags,
|
||||||
|
dependencies=route.dependencies,
|
||||||
|
summary=route.summary,
|
||||||
|
description=route.description,
|
||||||
|
response_description=route.response_description,
|
||||||
|
responses=responses,
|
||||||
|
deprecated=False,
|
||||||
|
name=route.name,
|
||||||
|
methods={method},
|
||||||
|
operation_id=f"{method.lower()}_{re.sub(r'[^a-z0-9]+', '_', route.path).strip('_')}",
|
||||||
|
response_model_include=route.response_model_include,
|
||||||
|
response_model_exclude=route.response_model_exclude,
|
||||||
|
response_model_by_alias=route.response_model_by_alias,
|
||||||
|
response_model_exclude_unset=route.response_model_exclude_unset,
|
||||||
|
response_model_exclude_defaults=route.response_model_exclude_defaults,
|
||||||
|
response_model_exclude_none=route.response_model_exclude_none,
|
||||||
|
include_in_schema=route.include_in_schema,
|
||||||
|
response_class=route.response_class,
|
||||||
|
callbacks=route.callbacks,
|
||||||
|
openapi_extra=route.openapi_extra,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_rest_router(routes: Iterable[Any]) -> APIRouter:
|
||||||
|
router = APIRouter()
|
||||||
|
seen: dict[tuple[str, str], APIRoute] = {}
|
||||||
|
operation_ids: set[str] = set()
|
||||||
|
|
||||||
|
for route in routes:
|
||||||
|
if not isinstance(route, APIRoute):
|
||||||
|
continue
|
||||||
|
|
||||||
|
methods = route.methods or set()
|
||||||
|
if len(methods) != 1:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"REST route {route.name!r} must declare exactly one HTTP method"
|
||||||
|
)
|
||||||
|
method = next(iter(methods))
|
||||||
|
key = (method, route.path)
|
||||||
|
if key in seen:
|
||||||
|
previous = seen[key]
|
||||||
|
raise RuntimeError(
|
||||||
|
"REST route collision for "
|
||||||
|
f"{method} {route.path}: {previous.name!r} and {route.name!r}."
|
||||||
|
)
|
||||||
|
|
||||||
|
adapted = _adapt_route(route)
|
||||||
|
if adapted.operation_id in operation_ids:
|
||||||
|
adapted.operation_id = f"{adapted.operation_id}_{route.name}"
|
||||||
|
seen[key] = route
|
||||||
|
operation_ids.add(adapted.operation_id or "")
|
||||||
|
router.routes.append(adapted)
|
||||||
|
|
||||||
|
return router
|
||||||
|
|
||||||
|
|
||||||
|
api_router = build_rest_router(handler_api_router.routes)
|
||||||
+202
-88
@@ -1,116 +1,230 @@
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
from app.api.v1.endpoints import (
|
from app.api.v1.endpoints import (
|
||||||
|
access,
|
||||||
admin_metadata,
|
admin_metadata,
|
||||||
agent_auth,
|
agent_auth,
|
||||||
project,
|
audit,
|
||||||
simulation,
|
|
||||||
scada,
|
|
||||||
extension,
|
|
||||||
snapshots,
|
|
||||||
users,
|
|
||||||
schemes,
|
|
||||||
misc,
|
|
||||||
risk,
|
|
||||||
cache,
|
|
||||||
leakage,
|
|
||||||
burst_detection,
|
burst_detection,
|
||||||
burst_location,
|
burst_location,
|
||||||
audit, # 新增:审计日志
|
cache,
|
||||||
meta,
|
extension,
|
||||||
web_search,
|
|
||||||
geocoding,
|
geocoding,
|
||||||
)
|
leakage,
|
||||||
from app.api.v1.endpoints.network import (
|
meta,
|
||||||
general,
|
misc,
|
||||||
junctions,
|
model_import,
|
||||||
reservoirs,
|
project,
|
||||||
tanks,
|
project_data,
|
||||||
pipes,
|
risk,
|
||||||
pumps,
|
scada,
|
||||||
valves,
|
schemes,
|
||||||
tags,
|
sensor_placement,
|
||||||
demands,
|
simulation,
|
||||||
geometry,
|
snapshots,
|
||||||
regions,
|
web_search,
|
||||||
)
|
)
|
||||||
from app.api.v1.endpoints.components import (
|
from app.api.v1.endpoints.components import (
|
||||||
curves,
|
|
||||||
patterns,
|
|
||||||
controls,
|
controls,
|
||||||
|
curves,
|
||||||
options,
|
options,
|
||||||
|
patterns,
|
||||||
quality,
|
quality,
|
||||||
visuals,
|
visuals,
|
||||||
)
|
)
|
||||||
|
from app.api.v1.endpoints.network import (
|
||||||
from app.api.v1.endpoints import project_data
|
demands,
|
||||||
|
general,
|
||||||
|
geometry,
|
||||||
|
junctions,
|
||||||
|
pipes,
|
||||||
|
pumps,
|
||||||
|
regions,
|
||||||
|
reservoirs,
|
||||||
|
tags,
|
||||||
|
tanks,
|
||||||
|
valves,
|
||||||
|
)
|
||||||
from app.api.v1.endpoints.timeseries import (
|
from app.api.v1.endpoints.timeseries import (
|
||||||
realtime as ts_realtime,
|
|
||||||
scheme as ts_scheme,
|
|
||||||
scada as ts_scada,
|
|
||||||
composite as ts_composite,
|
composite as ts_composite,
|
||||||
|
realtime as ts_realtime,
|
||||||
|
scada as ts_scada,
|
||||||
|
scheme as ts_scheme,
|
||||||
|
)
|
||||||
|
from app.auth.permissions import (
|
||||||
|
BURST_RUN,
|
||||||
|
OPTIMIZATION_RUN,
|
||||||
|
RISK_RUN,
|
||||||
|
SCADA_CLEAN,
|
||||||
|
SCADA_VIEW,
|
||||||
|
SIMULATION_RUN,
|
||||||
|
SIMULATION_VIEW,
|
||||||
|
WEBGIS_EDIT,
|
||||||
|
WEBGIS_VIEW,
|
||||||
|
require_method_permission,
|
||||||
|
require_permission,
|
||||||
)
|
)
|
||||||
|
|
||||||
api_router = APIRouter()
|
api_router = APIRouter()
|
||||||
|
|
||||||
# Core Services
|
webgis_access = Depends(
|
||||||
|
require_method_permission(
|
||||||
|
read_permission=WEBGIS_VIEW,
|
||||||
|
write_permission=WEBGIS_EDIT,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
scada_access = Depends(
|
||||||
|
require_method_permission(
|
||||||
|
read_permission=SCADA_VIEW,
|
||||||
|
write_permission=SCADA_CLEAN,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
simulation_access = Depends(
|
||||||
|
require_method_permission(
|
||||||
|
read_permission=SIMULATION_VIEW,
|
||||||
|
write_permission=SIMULATION_RUN,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
webgis_view_access = Depends(require_permission(WEBGIS_VIEW))
|
||||||
|
simulation_run_access = Depends(require_permission(SIMULATION_RUN))
|
||||||
|
burst_run_access = Depends(require_permission(BURST_RUN))
|
||||||
|
risk_run_access = Depends(require_permission(RISK_RUN))
|
||||||
|
optimization_run_access = Depends(require_permission(OPTIMIZATION_RUN))
|
||||||
|
|
||||||
|
# Core services
|
||||||
|
api_router.include_router(access.router, tags=["Access Control"])
|
||||||
api_router.include_router(agent_auth.router, tags=["Agent Auth"])
|
api_router.include_router(agent_auth.router, tags=["Agent Auth"])
|
||||||
api_router.include_router(
|
api_router.include_router(
|
||||||
admin_metadata.router, prefix="/admin", tags=["Metadata Admin"]
|
admin_metadata.router,
|
||||||
|
tags=["Metadata Admin"],
|
||||||
)
|
)
|
||||||
api_router.include_router(audit.router, prefix="/audit", tags=["Audit Logs"]) # 新增
|
api_router.include_router(model_import.router, tags=["Model Administration"])
|
||||||
|
api_router.include_router(audit.router, tags=["Audit Logs"])
|
||||||
api_router.include_router(meta.router, tags=["Metadata"])
|
api_router.include_router(meta.router, tags=["Metadata"])
|
||||||
api_router.include_router(project.router, tags=["Project"])
|
|
||||||
|
|
||||||
# Network Elements (Node/Link Types)
|
|
||||||
api_router.include_router(general.router, tags=["Network General"])
|
|
||||||
api_router.include_router(junctions.router, tags=["Junctions"])
|
|
||||||
api_router.include_router(reservoirs.router, tags=["Reservoirs"])
|
|
||||||
api_router.include_router(tanks.router, tags=["Tanks"])
|
|
||||||
api_router.include_router(pipes.router, tags=["Pipes"])
|
|
||||||
api_router.include_router(pumps.router, tags=["Pumps"])
|
|
||||||
api_router.include_router(valves.router, tags=["Valves"])
|
|
||||||
|
|
||||||
# Network Features
|
|
||||||
api_router.include_router(tags.router, tags=["Tags"])
|
|
||||||
api_router.include_router(demands.router, tags=["Demands"])
|
|
||||||
api_router.include_router(geometry.router, tags=["Geometry & Coordinates"])
|
|
||||||
api_router.include_router(regions.router, tags=["Regions & DMAs"])
|
|
||||||
|
|
||||||
# Components & Controls
|
|
||||||
api_router.include_router(curves.router, tags=["Curves"])
|
|
||||||
api_router.include_router(patterns.router, tags=["Patterns"])
|
|
||||||
api_router.include_router(controls.router, tags=["Controls & Rules"])
|
|
||||||
api_router.include_router(options.router, tags=["Options"])
|
|
||||||
api_router.include_router(quality.router, tags=["Quality"])
|
|
||||||
api_router.include_router(visuals.router, tags=["Visuals"])
|
|
||||||
|
|
||||||
# Simulation & Data
|
|
||||||
api_router.include_router(simulation.router, tags=["Simulation Control"])
|
|
||||||
api_router.include_router(scada.router)
|
|
||||||
api_router.include_router(snapshots.router, tags=["Snapshots"])
|
|
||||||
api_router.include_router(users.router, tags=["Users"])
|
|
||||||
api_router.include_router(schemes.router, tags=["Schemes"])
|
|
||||||
api_router.include_router(misc.router, tags=["Misc"])
|
|
||||||
api_router.include_router(risk.router, tags=["Risk"])
|
|
||||||
api_router.include_router(cache.router, tags=["Cache"])
|
|
||||||
api_router.include_router(web_search.router, tags=["Web Search"])
|
|
||||||
api_router.include_router(geocoding.router, tags=["Geocoding"])
|
|
||||||
api_router.include_router(leakage.router, prefix="/leakage", tags=["Leakage"])
|
|
||||||
api_router.include_router(
|
api_router.include_router(
|
||||||
burst_detection.router, prefix="/burst-detection", tags=["Burst Detection"]
|
project.router,
|
||||||
)
|
tags=["Project"],
|
||||||
api_router.include_router(
|
dependencies=[webgis_access],
|
||||||
burst_location.router, prefix="/burst-location", tags=["Burst Location"]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# TimescaleDB Data Access
|
# WebGIS data
|
||||||
api_router.include_router(ts_realtime.router, tags=["TimescaleDB - Realtime"])
|
for endpoint_router, tag in (
|
||||||
api_router.include_router(ts_scheme.router, tags=["TimescaleDB - Scheme"])
|
(general.router, "Network General"),
|
||||||
api_router.include_router(ts_scada.router, tags=["TimescaleDB - SCADA"])
|
(junctions.router, "Junctions"),
|
||||||
api_router.include_router(ts_composite.router, tags=["TimescaleDB - Composite"])
|
(reservoirs.router, "Reservoirs"),
|
||||||
|
(tanks.router, "Tanks"),
|
||||||
|
(pipes.router, "Pipes"),
|
||||||
|
(pumps.router, "Pumps"),
|
||||||
|
(valves.router, "Valves"),
|
||||||
|
(tags.router, "Tags"),
|
||||||
|
(demands.router, "Demands"),
|
||||||
|
(geometry.router, "Geometry & Coordinates"),
|
||||||
|
(regions.router, "Regions & DMAs"),
|
||||||
|
(curves.router, "Curves"),
|
||||||
|
(patterns.router, "Patterns"),
|
||||||
|
(controls.router, "Controls & Rules"),
|
||||||
|
(options.router, "Options"),
|
||||||
|
(quality.router, "Quality"),
|
||||||
|
(visuals.router, "Visuals"),
|
||||||
|
):
|
||||||
|
api_router.include_router(
|
||||||
|
endpoint_router,
|
||||||
|
tags=[tag],
|
||||||
|
dependencies=[webgis_access],
|
||||||
|
)
|
||||||
|
|
||||||
# Project Data (PostgreSQL)
|
# Simulation and analysis
|
||||||
api_router.include_router(project_data.router, tags=["Project Data"])
|
api_router.include_router(
|
||||||
|
simulation.router,
|
||||||
|
tags=["Simulation Control"],
|
||||||
|
dependencies=[simulation_run_access],
|
||||||
|
)
|
||||||
|
api_router.include_router(scada.router, dependencies=[scada_access])
|
||||||
|
api_router.include_router(
|
||||||
|
sensor_placement.router,
|
||||||
|
tags=["Sensor Placement"],
|
||||||
|
dependencies=[optimization_run_access],
|
||||||
|
)
|
||||||
|
api_router.include_router(
|
||||||
|
snapshots.router,
|
||||||
|
tags=["Snapshots"],
|
||||||
|
dependencies=[simulation_access],
|
||||||
|
)
|
||||||
|
api_router.include_router(
|
||||||
|
schemes.router,
|
||||||
|
tags=["Schemes"],
|
||||||
|
dependencies=[simulation_access],
|
||||||
|
)
|
||||||
|
api_router.include_router(
|
||||||
|
misc.router,
|
||||||
|
tags=["Misc"],
|
||||||
|
dependencies=[webgis_view_access],
|
||||||
|
)
|
||||||
|
api_router.include_router(
|
||||||
|
risk.router,
|
||||||
|
tags=["Risk"],
|
||||||
|
dependencies=[risk_run_access],
|
||||||
|
)
|
||||||
|
api_router.include_router(
|
||||||
|
cache.router,
|
||||||
|
tags=["Cache"],
|
||||||
|
dependencies=[simulation_run_access],
|
||||||
|
)
|
||||||
|
api_router.include_router(
|
||||||
|
web_search.router,
|
||||||
|
tags=["Web Search"],
|
||||||
|
dependencies=[webgis_view_access],
|
||||||
|
)
|
||||||
|
api_router.include_router(
|
||||||
|
geocoding.router,
|
||||||
|
tags=["Geocoding"],
|
||||||
|
dependencies=[webgis_view_access],
|
||||||
|
)
|
||||||
|
api_router.include_router(
|
||||||
|
leakage.router,
|
||||||
|
tags=["Leakage"],
|
||||||
|
dependencies=[burst_run_access],
|
||||||
|
)
|
||||||
|
api_router.include_router(
|
||||||
|
burst_detection.router,
|
||||||
|
tags=["Burst Detection"],
|
||||||
|
dependencies=[burst_run_access],
|
||||||
|
)
|
||||||
|
api_router.include_router(
|
||||||
|
burst_location.router,
|
||||||
|
tags=["Burst Location"],
|
||||||
|
dependencies=[burst_run_access],
|
||||||
|
)
|
||||||
|
|
||||||
# Extension
|
# TimescaleDB data
|
||||||
api_router.include_router(extension.router, tags=["Extension"])
|
for endpoint_router, tag in (
|
||||||
|
(ts_realtime.router, "TimescaleDB - Realtime"),
|
||||||
|
(ts_scheme.router, "TimescaleDB - Scheme"),
|
||||||
|
):
|
||||||
|
api_router.include_router(
|
||||||
|
endpoint_router,
|
||||||
|
tags=[tag],
|
||||||
|
dependencies=[simulation_access],
|
||||||
|
)
|
||||||
|
|
||||||
|
for endpoint_router, tag in (
|
||||||
|
(ts_scada.router, "TimescaleDB - SCADA"),
|
||||||
|
(ts_composite.router, "TimescaleDB - Composite"),
|
||||||
|
):
|
||||||
|
api_router.include_router(
|
||||||
|
endpoint_router,
|
||||||
|
tags=[tag],
|
||||||
|
dependencies=[scada_access],
|
||||||
|
)
|
||||||
|
|
||||||
|
api_router.include_router(
|
||||||
|
project_data.router,
|
||||||
|
tags=["Project Data"],
|
||||||
|
dependencies=[webgis_view_access],
|
||||||
|
)
|
||||||
|
api_router.include_router(
|
||||||
|
extension.router,
|
||||||
|
tags=["Extension"],
|
||||||
|
dependencies=[webgis_access],
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# import logging
|
import time
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from fastapi import Depends, HTTPException, status
|
from fastapi import Depends, HTTPException, status
|
||||||
@@ -23,12 +23,22 @@ def _decode_keycloak_token(token: str) -> dict:
|
|||||||
|
|
||||||
key = settings.KEYCLOAK_PUBLIC_KEY.replace("\\n", "\n")
|
key = settings.KEYCLOAK_PUBLIC_KEY.replace("\\n", "\n")
|
||||||
|
|
||||||
return jwt.decode(
|
payload = jwt.decode(
|
||||||
token,
|
token,
|
||||||
key,
|
key,
|
||||||
algorithms=[settings.KEYCLOAK_ALGORITHM],
|
algorithms=[settings.KEYCLOAK_ALGORITHM],
|
||||||
audience=settings.KEYCLOAK_AUDIENCE or None,
|
audience=settings.KEYCLOAK_AUDIENCE or None,
|
||||||
)
|
)
|
||||||
|
if settings.KEYCLOAK_ACCESS_TOKEN_MAX_AGE_SECONDS <= 0:
|
||||||
|
return payload
|
||||||
|
|
||||||
|
issued_at = payload.get("iat")
|
||||||
|
if not isinstance(issued_at, (int, float)) or (
|
||||||
|
time.time() >= issued_at + settings.KEYCLOAK_ACCESS_TOKEN_MAX_AGE_SECONDS
|
||||||
|
):
|
||||||
|
raise JWTError("Keycloak access token is older than the allowed maximum age")
|
||||||
|
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
async def get_current_keycloak_payload(
|
async def get_current_keycloak_payload(
|
||||||
@@ -73,14 +83,18 @@ async def get_current_keycloak_sub(
|
|||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
|
|
||||||
async def get_current_keycloak_username(
|
def get_keycloak_preferred_username(payload: dict) -> str:
|
||||||
payload: dict = Depends(get_current_keycloak_payload),
|
username = payload.get("preferred_username")
|
||||||
) -> str:
|
|
||||||
username = payload.get("preferred_username") or payload.get("username")
|
|
||||||
if not username:
|
if not username:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail="Missing username claim",
|
detail="Missing preferred_username claim",
|
||||||
headers={"WWW-Authenticate": "Bearer"},
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
)
|
)
|
||||||
return str(username)
|
return str(username)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_current_keycloak_username(
|
||||||
|
payload: dict = Depends(get_current_keycloak_payload),
|
||||||
|
) -> str:
|
||||||
|
return get_keycloak_preferred_username(payload)
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ from fastapi import Depends, HTTPException, status
|
|||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.auth.keycloak_dependencies import get_current_keycloak_payload
|
from app.auth.keycloak_dependencies import (
|
||||||
|
get_current_keycloak_payload,
|
||||||
|
get_keycloak_preferred_username,
|
||||||
|
)
|
||||||
from app.infra.db.metadb.database import get_metadata_session
|
from app.infra.db.metadb.database import get_metadata_session
|
||||||
from app.infra.db.metadb.repositories.metadata_repository import MetadataRepository
|
from app.infra.db.metadb.repositories.metadata_repository import MetadataRepository
|
||||||
|
|
||||||
@@ -38,11 +41,6 @@ def _keycloak_sub_from_payload(payload: dict) -> UUID:
|
|||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
|
|
||||||
def _username_from_payload(payload: dict) -> str | None:
|
|
||||||
username = payload.get("preferred_username") or payload.get("username")
|
|
||||||
return str(username) if username else None
|
|
||||||
|
|
||||||
|
|
||||||
def _email_from_payload(payload: dict) -> str | None:
|
def _email_from_payload(payload: dict) -> str | None:
|
||||||
email = payload.get("email")
|
email = payload.get("email")
|
||||||
return str(email) if email else None
|
return str(email) if email else None
|
||||||
@@ -53,6 +51,7 @@ async def get_current_metadata_user(
|
|||||||
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
||||||
):
|
):
|
||||||
keycloak_sub = _keycloak_sub_from_payload(keycloak_payload)
|
keycloak_sub = _keycloak_sub_from_payload(keycloak_payload)
|
||||||
|
username = get_keycloak_preferred_username(keycloak_payload)
|
||||||
try:
|
try:
|
||||||
user = await metadata_repo.get_user_by_keycloak_id(keycloak_sub)
|
user = await metadata_repo.get_user_by_keycloak_id(keycloak_sub)
|
||||||
except SQLAlchemyError as exc:
|
except SQLAlchemyError as exc:
|
||||||
@@ -62,7 +61,7 @@ async def get_current_metadata_user(
|
|||||||
)
|
)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
detail=f"Metadata database error: {exc}",
|
detail="Metadata database is unavailable",
|
||||||
) from exc
|
) from exc
|
||||||
if not user or not user.is_active:
|
if not user or not user.is_active:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -71,7 +70,7 @@ async def get_current_metadata_user(
|
|||||||
try:
|
try:
|
||||||
user = await metadata_repo.refresh_user_keycloak_snapshot(
|
user = await metadata_repo.refresh_user_keycloak_snapshot(
|
||||||
user,
|
user,
|
||||||
username=_username_from_payload(keycloak_payload),
|
username=username,
|
||||||
email=_email_from_payload(keycloak_payload),
|
email=_email_from_payload(keycloak_payload),
|
||||||
)
|
)
|
||||||
except SQLAlchemyError as exc:
|
except SQLAlchemyError as exc:
|
||||||
@@ -81,7 +80,7 @@ async def get_current_metadata_user(
|
|||||||
)
|
)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
detail=f"Metadata database error: {exc}",
|
detail="Metadata database is unavailable",
|
||||||
) from exc
|
) from exc
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
from collections.abc import Awaitable, Callable
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from fastapi import Depends, HTTPException, Request, status
|
||||||
|
|
||||||
|
from app.auth.project_dependencies import ProjectContext, get_project_context
|
||||||
|
|
||||||
|
WEBGIS_VIEW = "webgis.view"
|
||||||
|
WEBGIS_EDIT = "webgis.edit"
|
||||||
|
SCADA_VIEW = "scada.view"
|
||||||
|
SCADA_CLEAN = "scada.clean"
|
||||||
|
SIMULATION_VIEW = "simulation.view"
|
||||||
|
SIMULATION_RUN = "simulation.run"
|
||||||
|
BURST_VIEW = "burst.view"
|
||||||
|
BURST_RUN = "burst.run"
|
||||||
|
RISK_VIEW = "risk.view"
|
||||||
|
RISK_RUN = "risk.run"
|
||||||
|
OPTIMIZATION_VIEW = "optimization.view"
|
||||||
|
OPTIMIZATION_RUN = "optimization.run"
|
||||||
|
MODEL_IMPORT = "model.import"
|
||||||
|
AUDIT_VIEW = "audit.view"
|
||||||
|
ENVIRONMENT_MANAGE = "environment.manage"
|
||||||
|
MEMBERSHIP_MANAGE = "membership.manage"
|
||||||
|
|
||||||
|
PROJECT_MEMBER_PERMISSIONS = frozenset(
|
||||||
|
{
|
||||||
|
WEBGIS_VIEW,
|
||||||
|
WEBGIS_EDIT,
|
||||||
|
SCADA_VIEW,
|
||||||
|
SCADA_CLEAN,
|
||||||
|
SIMULATION_VIEW,
|
||||||
|
SIMULATION_RUN,
|
||||||
|
BURST_VIEW,
|
||||||
|
BURST_RUN,
|
||||||
|
RISK_VIEW,
|
||||||
|
RISK_RUN,
|
||||||
|
OPTIMIZATION_VIEW,
|
||||||
|
OPTIMIZATION_RUN,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
PROJECT_VIEWER_PERMISSIONS = frozenset(
|
||||||
|
{
|
||||||
|
WEBGIS_VIEW,
|
||||||
|
SCADA_VIEW,
|
||||||
|
SIMULATION_VIEW,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
SYSTEM_ADMIN_PERMISSIONS = frozenset(
|
||||||
|
{
|
||||||
|
MODEL_IMPORT,
|
||||||
|
AUDIT_VIEW,
|
||||||
|
ENVIRONMENT_MANAGE,
|
||||||
|
MEMBERSHIP_MANAGE,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
PROJECT_ROLE_PERMISSIONS: dict[str, frozenset[str]] = {
|
||||||
|
"member": PROJECT_MEMBER_PERMISSIONS,
|
||||||
|
"viewer": PROJECT_VIEWER_PERMISSIONS,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_permissions(
|
||||||
|
*,
|
||||||
|
project_role: str | None,
|
||||||
|
system_role: str,
|
||||||
|
is_superuser: bool,
|
||||||
|
) -> frozenset[str]:
|
||||||
|
permissions = set(PROJECT_ROLE_PERMISSIONS.get(project_role or "", frozenset()))
|
||||||
|
if is_superuser or system_role == "admin":
|
||||||
|
permissions.update(SYSTEM_ADMIN_PERMISSIONS)
|
||||||
|
return frozenset(permissions)
|
||||||
|
|
||||||
|
|
||||||
|
def permissions_for_context(ctx: ProjectContext) -> frozenset[str]:
|
||||||
|
return resolve_permissions(
|
||||||
|
project_role=ctx.project_role,
|
||||||
|
system_role=ctx.system_role,
|
||||||
|
is_superuser=ctx.is_superuser,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _permission_denied(permission: str) -> HTTPException:
|
||||||
|
return HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail={
|
||||||
|
"code": "permission_denied",
|
||||||
|
"permission": permission,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _enforce_project_scope(request: Request, ctx: ProjectContext) -> None:
|
||||||
|
requested_network = (
|
||||||
|
request.path_params.get("network")
|
||||||
|
or request.query_params.get("network")
|
||||||
|
)
|
||||||
|
if not requested_network:
|
||||||
|
content_type = request.headers.get("content-type", "")
|
||||||
|
if content_type.startswith("application/json"):
|
||||||
|
try:
|
||||||
|
payload = await request.json()
|
||||||
|
except (ValueError, RuntimeError):
|
||||||
|
payload = None
|
||||||
|
if isinstance(payload, dict):
|
||||||
|
requested_network = payload.get("network")
|
||||||
|
|
||||||
|
if requested_network and str(requested_network) != ctx.project_code:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail={
|
||||||
|
"code": "project_scope_denied",
|
||||||
|
"project_id": str(ctx.project_id),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def require_permission(
|
||||||
|
permission: str,
|
||||||
|
) -> Callable[..., Awaitable[ProjectContext]]:
|
||||||
|
async def dependency(
|
||||||
|
request: Request,
|
||||||
|
ctx: ProjectContext = Depends(get_project_context),
|
||||||
|
) -> ProjectContext:
|
||||||
|
if permission not in permissions_for_context(ctx):
|
||||||
|
raise _permission_denied(permission)
|
||||||
|
await _enforce_project_scope(request, ctx)
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
return dependency
|
||||||
|
|
||||||
|
|
||||||
|
def require_method_permission(
|
||||||
|
*,
|
||||||
|
read_permission: str,
|
||||||
|
write_permission: str,
|
||||||
|
) -> Callable[..., Awaitable[ProjectContext]]:
|
||||||
|
async def dependency(
|
||||||
|
request: Request,
|
||||||
|
ctx: ProjectContext = Depends(get_project_context),
|
||||||
|
) -> ProjectContext:
|
||||||
|
permission = (
|
||||||
|
read_permission
|
||||||
|
if request.method.upper() in {"GET", "HEAD", "OPTIONS"}
|
||||||
|
else write_permission
|
||||||
|
)
|
||||||
|
if permission not in permissions_for_context(ctx):
|
||||||
|
raise _permission_denied(permission)
|
||||||
|
await _enforce_project_scope(request, ctx)
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
return dependency
|
||||||
|
|
||||||
|
|
||||||
|
def has_permission(user: Any, project_role: str | None, permission: str) -> bool:
|
||||||
|
return permission in resolve_permissions(
|
||||||
|
project_role=project_role,
|
||||||
|
system_role=str(getattr(user, "role", "user")),
|
||||||
|
is_superuser=bool(getattr(user, "is_superuser", False)),
|
||||||
|
)
|
||||||
@@ -1,18 +1,21 @@
|
|||||||
|
import logging
|
||||||
|
from collections.abc import AsyncGenerator
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import AsyncGenerator
|
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
import logging
|
|
||||||
from fastapi import Depends, Header, HTTPException, status
|
from fastapi import Depends, Header, HTTPException, status
|
||||||
from psycopg import AsyncConnection
|
from psycopg import AsyncConnection
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.auth.keycloak_dependencies import get_current_keycloak_sub
|
from app.auth.metadata_dependencies import get_current_metadata_user
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.infra.db.dynamic_manager import project_connection_manager
|
from app.infra.db.dynamic_manager import project_connection_manager
|
||||||
from app.infra.db.metadb.database import get_metadata_session
|
from app.infra.db.metadb.database import get_metadata_session
|
||||||
from app.infra.db.metadb.repositories.metadata_repository import MetadataRepository
|
from app.infra.db.metadb.repositories.metadata_repository import (
|
||||||
|
MetadataRepository,
|
||||||
|
ProjectDbRouting,
|
||||||
|
)
|
||||||
|
|
||||||
DB_ROLE_BIZ_DATA = "biz_data"
|
DB_ROLE_BIZ_DATA = "biz_data"
|
||||||
DB_ROLE_IOT_DATA = "iot_data"
|
DB_ROLE_IOT_DATA = "iot_data"
|
||||||
@@ -28,6 +31,8 @@ class ProjectContext:
|
|||||||
project_code: str
|
project_code: str
|
||||||
user_id: UUID
|
user_id: UUID
|
||||||
project_role: str
|
project_role: str
|
||||||
|
system_role: str = "user"
|
||||||
|
is_superuser: bool = False
|
||||||
|
|
||||||
|
|
||||||
async def get_metadata_repository(
|
async def get_metadata_repository(
|
||||||
@@ -36,10 +41,10 @@ async def get_metadata_repository(
|
|||||||
return MetadataRepository(session)
|
return MetadataRepository(session)
|
||||||
|
|
||||||
|
|
||||||
async def get_project_context(
|
async def resolve_project_context(
|
||||||
x_project_id: str = Header(..., alias="X-Project-Id"),
|
x_project_id: str,
|
||||||
keycloak_sub: UUID = Depends(get_current_keycloak_sub),
|
current_user,
|
||||||
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
metadata_repo: MetadataRepository,
|
||||||
) -> ProjectContext:
|
) -> ProjectContext:
|
||||||
try:
|
try:
|
||||||
project_uuid = UUID(x_project_id)
|
project_uuid = UUID(x_project_id)
|
||||||
@@ -59,17 +64,9 @@ async def get_project_context(
|
|||||||
status_code=status.HTTP_403_FORBIDDEN, detail="Project is not active"
|
status_code=status.HTTP_403_FORBIDDEN, detail="Project is not active"
|
||||||
)
|
)
|
||||||
|
|
||||||
user = await metadata_repo.get_user_by_keycloak_id(keycloak_sub)
|
membership_role = await metadata_repo.get_membership_role(
|
||||||
if not user:
|
project_uuid, current_user.id
|
||||||
raise HTTPException(
|
)
|
||||||
status_code=status.HTTP_403_FORBIDDEN, detail="User not registered"
|
|
||||||
)
|
|
||||||
if not user.is_active:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_403_FORBIDDEN, detail="Inactive user"
|
|
||||||
)
|
|
||||||
|
|
||||||
membership_role = await metadata_repo.get_membership_role(project_uuid, user.id)
|
|
||||||
if not membership_role:
|
if not membership_role:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN, detail="No access to project"
|
status_code=status.HTTP_403_FORBIDDEN, detail="No access to project"
|
||||||
@@ -81,44 +78,71 @@ async def get_project_context(
|
|||||||
)
|
)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
detail=f"Metadata database error: {exc}",
|
detail="Metadata database is unavailable",
|
||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
return ProjectContext(
|
return ProjectContext(
|
||||||
project_id=project.id,
|
project_id=project.id,
|
||||||
project_code=project.code,
|
project_code=project.code,
|
||||||
user_id=user.id,
|
user_id=current_user.id,
|
||||||
project_role=membership_role,
|
project_role=membership_role,
|
||||||
|
system_role=current_user.role,
|
||||||
|
is_superuser=current_user.is_superuser,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_project_context(
|
||||||
|
x_project_id: str = Header(..., alias="X-Project-Id"),
|
||||||
|
current_user=Depends(get_current_metadata_user),
|
||||||
|
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
||||||
|
) -> ProjectContext:
|
||||||
|
return await resolve_project_context(x_project_id, current_user, metadata_repo)
|
||||||
|
|
||||||
|
|
||||||
|
async def _get_project_routing(
|
||||||
|
metadata_repo: MetadataRepository,
|
||||||
|
project_id: UUID,
|
||||||
|
db_role: str,
|
||||||
|
expected_db_type: str,
|
||||||
|
database_label: str,
|
||||||
|
) -> ProjectDbRouting:
|
||||||
|
try:
|
||||||
|
routing = await metadata_repo.get_project_db_routing(project_id, db_role)
|
||||||
|
except ValueError as exc:
|
||||||
|
logger.error(
|
||||||
|
"Invalid project %s routing DSN configuration",
|
||||||
|
database_label,
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
|
detail=f"Project {database_label} routing DSN is invalid: {exc}",
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
if not routing:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
|
detail=f"Project {database_label} not configured",
|
||||||
|
)
|
||||||
|
if routing.db_type != expected_db_type:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
|
detail=f"Project {database_label} type mismatch",
|
||||||
|
)
|
||||||
|
return routing
|
||||||
|
|
||||||
|
|
||||||
async def get_project_pg_session(
|
async def get_project_pg_session(
|
||||||
ctx: ProjectContext = Depends(get_project_context),
|
ctx: ProjectContext = Depends(get_project_context),
|
||||||
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
||||||
) -> AsyncGenerator[AsyncSession, None]:
|
) -> AsyncGenerator[AsyncSession, None]:
|
||||||
try:
|
routing = await _get_project_routing(
|
||||||
routing = await metadata_repo.get_project_db_routing(
|
metadata_repo,
|
||||||
ctx.project_id, DB_ROLE_BIZ_DATA
|
ctx.project_id,
|
||||||
)
|
DB_ROLE_BIZ_DATA,
|
||||||
except ValueError as exc:
|
DB_TYPE_POSTGRES,
|
||||||
logger.error(
|
"PostgreSQL",
|
||||||
"Invalid project PostgreSQL routing DSN configuration",
|
)
|
||||||
exc_info=True,
|
|
||||||
)
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
||||||
detail=f"Project PostgreSQL routing DSN is invalid: {exc}",
|
|
||||||
) from exc
|
|
||||||
if not routing:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
||||||
detail="Project PostgreSQL not configured",
|
|
||||||
)
|
|
||||||
if routing.db_type != DB_TYPE_POSTGRES:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
||||||
detail="Project PostgreSQL type mismatch",
|
|
||||||
)
|
|
||||||
|
|
||||||
pool_min_size = routing.pool_min_size or settings.PROJECT_PG_POOL_SIZE
|
pool_min_size = routing.pool_min_size or settings.PROJECT_PG_POOL_SIZE
|
||||||
pool_max_size = routing.pool_max_size or settings.PROJECT_PG_POOL_SIZE
|
pool_max_size = routing.pool_max_size or settings.PROJECT_PG_POOL_SIZE
|
||||||
@@ -137,29 +161,13 @@ async def get_project_pg_connection(
|
|||||||
ctx: ProjectContext = Depends(get_project_context),
|
ctx: ProjectContext = Depends(get_project_context),
|
||||||
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
||||||
) -> AsyncGenerator[AsyncConnection, None]:
|
) -> AsyncGenerator[AsyncConnection, None]:
|
||||||
try:
|
routing = await _get_project_routing(
|
||||||
routing = await metadata_repo.get_project_db_routing(
|
metadata_repo,
|
||||||
ctx.project_id, DB_ROLE_BIZ_DATA
|
ctx.project_id,
|
||||||
)
|
DB_ROLE_BIZ_DATA,
|
||||||
except ValueError as exc:
|
DB_TYPE_POSTGRES,
|
||||||
logger.error(
|
"PostgreSQL",
|
||||||
"Invalid project PostgreSQL routing DSN configuration",
|
)
|
||||||
exc_info=True,
|
|
||||||
)
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
||||||
detail=f"Project PostgreSQL routing DSN is invalid: {exc}",
|
|
||||||
) from exc
|
|
||||||
if not routing:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
||||||
detail="Project PostgreSQL not configured",
|
|
||||||
)
|
|
||||||
if routing.db_type != DB_TYPE_POSTGRES:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
||||||
detail="Project PostgreSQL type mismatch",
|
|
||||||
)
|
|
||||||
|
|
||||||
pool_min_size = routing.pool_min_size or settings.PROJECT_PG_POOL_SIZE
|
pool_min_size = routing.pool_min_size or settings.PROJECT_PG_POOL_SIZE
|
||||||
pool_max_size = routing.pool_max_size or settings.PROJECT_PG_POOL_SIZE
|
pool_max_size = routing.pool_max_size or settings.PROJECT_PG_POOL_SIZE
|
||||||
@@ -178,29 +186,13 @@ async def get_project_timescale_connection(
|
|||||||
ctx: ProjectContext = Depends(get_project_context),
|
ctx: ProjectContext = Depends(get_project_context),
|
||||||
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
||||||
) -> AsyncGenerator[AsyncConnection, None]:
|
) -> AsyncGenerator[AsyncConnection, None]:
|
||||||
try:
|
routing = await _get_project_routing(
|
||||||
routing = await metadata_repo.get_project_db_routing(
|
metadata_repo,
|
||||||
ctx.project_id, DB_ROLE_IOT_DATA
|
ctx.project_id,
|
||||||
)
|
DB_ROLE_IOT_DATA,
|
||||||
except ValueError as exc:
|
DB_TYPE_TIMESCALE,
|
||||||
logger.error(
|
"TimescaleDB",
|
||||||
"Invalid project TimescaleDB routing DSN configuration",
|
)
|
||||||
exc_info=True,
|
|
||||||
)
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
||||||
detail=f"Project TimescaleDB routing DSN is invalid: {exc}",
|
|
||||||
) from exc
|
|
||||||
if not routing:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
||||||
detail="Project TimescaleDB not configured",
|
|
||||||
)
|
|
||||||
if routing.db_type != DB_TYPE_TIMESCALE:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
||||||
detail="Project TimescaleDB type mismatch",
|
|
||||||
)
|
|
||||||
|
|
||||||
pool_min_size = routing.pool_min_size or settings.PROJECT_TS_POOL_MIN_SIZE
|
pool_min_size = routing.pool_min_size or settings.PROJECT_TS_POOL_MIN_SIZE
|
||||||
pool_max_size = routing.pool_max_size or settings.PROJECT_TS_POOL_MAX_SIZE
|
pool_max_size = routing.pool_max_size or settings.PROJECT_TS_POOL_MAX_SIZE
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,6 @@ class Settings(BaseSettings):
|
|||||||
PROJECT_NAME: str = "TJWater Server"
|
PROJECT_NAME: str = "TJWater Server"
|
||||||
ENVIRONMENT: str = "production"
|
ENVIRONMENT: str = "production"
|
||||||
API_V1_STR: str = "/api/v1"
|
API_V1_STR: str = "/api/v1"
|
||||||
|
|
||||||
NETWORK_NAME: str = "default_network"
|
NETWORK_NAME: str = "default_network"
|
||||||
|
|
||||||
# 敏感配置加密密钥 (Fernet)
|
# 敏感配置加密密钥 (Fernet)
|
||||||
@@ -49,6 +48,7 @@ class Settings(BaseSettings):
|
|||||||
KEYCLOAK_PUBLIC_KEY: str = ""
|
KEYCLOAK_PUBLIC_KEY: str = ""
|
||||||
KEYCLOAK_ALGORITHM: str = "RS256"
|
KEYCLOAK_ALGORITHM: str = "RS256"
|
||||||
KEYCLOAK_AUDIENCE: str = ""
|
KEYCLOAK_AUDIENCE: str = ""
|
||||||
|
KEYCLOAK_ACCESS_TOKEN_MAX_AGE_SECONDS: int = 900
|
||||||
|
|
||||||
# Bocha Web Search API
|
# Bocha Web Search API
|
||||||
BOCHA_API_KEY: str = ""
|
BOCHA_API_KEY: str = ""
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class AccessContextResponse(BaseModel):
|
||||||
|
user_id: UUID
|
||||||
|
username: str
|
||||||
|
system_role: str
|
||||||
|
is_system_admin: bool
|
||||||
|
project_id: UUID | None = None
|
||||||
|
project_role: str | None = None
|
||||||
|
permissions: list[str]
|
||||||
@@ -5,8 +5,8 @@ from uuid import UUID
|
|||||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||||
|
|
||||||
|
|
||||||
BusinessRole = Literal["admin", "user", "operator", "viewer"]
|
BusinessRole = Literal["admin", "user"]
|
||||||
ProjectRole = Literal["owner", "admin", "member", "viewer"]
|
ProjectRole = Literal["member", "viewer"]
|
||||||
ProjectStatus = Literal["active", "inactive", "archived"]
|
ProjectStatus = Literal["active", "inactive", "archived"]
|
||||||
ProjectDbRole = Literal["biz_data", "iot_data"]
|
ProjectDbRole = Literal["biz_data", "iot_data"]
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field, field_validator
|
||||||
|
|
||||||
|
|
||||||
|
AdjustmentStatus = Literal["current", "original", "added", "replaced"]
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_location_ids(value: list[str]) -> list[str]:
|
||||||
|
normalized = [str(item).strip() for item in value]
|
||||||
|
if any(not item for item in normalized):
|
||||||
|
raise ValueError("sensor locations cannot contain blank node IDs")
|
||||||
|
if len(set(normalized)) != len(normalized):
|
||||||
|
raise ValueError("sensor locations cannot contain duplicate node IDs")
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
|
class SensorPlacementOptimizeRequest(BaseModel):
|
||||||
|
network: str = Field(
|
||||||
|
...,
|
||||||
|
min_length=1,
|
||||||
|
max_length=63,
|
||||||
|
pattern=r"^[^/\\\x00]+$",
|
||||||
|
)
|
||||||
|
scheme_name: str = Field(..., min_length=1, max_length=32)
|
||||||
|
sensor_type: Literal["pressure"]
|
||||||
|
method: Literal["sensitivity", "kmeans"]
|
||||||
|
sensor_count: int = Field(..., gt=0, le=200)
|
||||||
|
min_diameter: int = Field(default=0, ge=0)
|
||||||
|
|
||||||
|
@field_validator("network")
|
||||||
|
@classmethod
|
||||||
|
def validate_network(cls, value: str) -> str:
|
||||||
|
normalized = value.strip()
|
||||||
|
if normalized in {".", ".."}:
|
||||||
|
raise ValueError("network must be a project identifier")
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
|
class SensorPlacementUpdateRequest(BaseModel):
|
||||||
|
expected_sensor_location: list[str] = Field(
|
||||||
|
...,
|
||||||
|
min_length=1,
|
||||||
|
max_length=200,
|
||||||
|
)
|
||||||
|
sensor_location: list[str] = Field(..., min_length=1, max_length=200)
|
||||||
|
|
||||||
|
@field_validator("expected_sensor_location", "sensor_location")
|
||||||
|
@classmethod
|
||||||
|
def validate_locations(cls, value: list[str]) -> list[str]:
|
||||||
|
return _normalize_location_ids(value)
|
||||||
|
|
||||||
|
|
||||||
|
class SensorPlacementExportRequest(BaseModel):
|
||||||
|
sensor_location: list[str] = Field(..., min_length=1, max_length=200)
|
||||||
|
adjustment_status: dict[str, AdjustmentStatus] = Field(
|
||||||
|
default_factory=dict,
|
||||||
|
max_length=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
@field_validator("sensor_location")
|
||||||
|
@classmethod
|
||||||
|
def validate_locations(cls, value: list[str]) -> list[str]:
|
||||||
|
return _normalize_location_ids(value)
|
||||||
|
|
||||||
|
|
||||||
|
class SensorPointResponse(BaseModel):
|
||||||
|
node_id: str
|
||||||
|
max_pipe_diameter: float | None = Field(
|
||||||
|
...,
|
||||||
|
description="节点关联管道的最大管径,单位:毫米",
|
||||||
|
)
|
||||||
|
project_x: float
|
||||||
|
project_y: float
|
||||||
|
map_x: float
|
||||||
|
map_y: float
|
||||||
|
longitude: float
|
||||||
|
latitude: float
|
||||||
|
elevation: float
|
||||||
|
|
||||||
|
|
||||||
|
class SensorPlacementSchemeResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
scheme_name: str
|
||||||
|
sensor_number: int
|
||||||
|
min_diameter: int
|
||||||
|
username: str
|
||||||
|
create_time: datetime
|
||||||
|
sensor_location: list[str]
|
||||||
|
sensor_points: list[SensorPointResponse]
|
||||||
|
can_edit: bool = False
|
||||||
@@ -58,6 +58,8 @@ class AuditMiddleware(BaseHTTPMiddleware):
|
|||||||
"/meta/projects",
|
"/meta/projects",
|
||||||
"/api/v1/openproject/",
|
"/api/v1/openproject/",
|
||||||
"/openproject/",
|
"/openproject/",
|
||||||
|
"/api/v1/audit/session-events",
|
||||||
|
"/audit/session-events",
|
||||||
}
|
}
|
||||||
EXCLUDED_PATH_PREFIXES = (
|
EXCLUDED_PATH_PREFIXES = (
|
||||||
)
|
)
|
||||||
@@ -80,27 +82,9 @@ class AuditMiddleware(BaseHTTPMiddleware):
|
|||||||
request_data = None
|
request_data = None
|
||||||
if should_capture_body:
|
if should_capture_body:
|
||||||
try:
|
try:
|
||||||
# 注意:读取 body 后需要重新设置,避免影响后续处理
|
|
||||||
original_receive = request._receive
|
|
||||||
body = await request.body()
|
body = await request.body()
|
||||||
if body:
|
if body:
|
||||||
request_data = json.loads(body.decode())
|
request_data = json.loads(body.decode())
|
||||||
|
|
||||||
# 重新构造请求以供后续使用:仅回放一次,后续回落原始 receive
|
|
||||||
body_sent = False
|
|
||||||
|
|
||||||
async def receive():
|
|
||||||
nonlocal body_sent
|
|
||||||
if not body_sent:
|
|
||||||
body_sent = True
|
|
||||||
return {
|
|
||||||
"type": "http.request",
|
|
||||||
"body": body,
|
|
||||||
"more_body": False,
|
|
||||||
}
|
|
||||||
return await original_receive()
|
|
||||||
|
|
||||||
request._receive = receive
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Failed to read request body for audit: {e}")
|
logger.warning(f"Failed to read request body for audit: {e}")
|
||||||
|
|
||||||
|
|||||||
@@ -211,6 +211,7 @@ class MetadataRepository:
|
|||||||
gs_workspace: str,
|
gs_workspace: str,
|
||||||
map_extent: dict | None,
|
map_extent: dict | None,
|
||||||
status: str,
|
status: str,
|
||||||
|
creator_user_id: UUID | None = None,
|
||||||
) -> models.Project:
|
) -> models.Project:
|
||||||
project = models.Project(
|
project = models.Project(
|
||||||
id=uuid4(),
|
id=uuid4(),
|
||||||
@@ -224,6 +225,15 @@ class MetadataRepository:
|
|||||||
updated_at=_utcnow(),
|
updated_at=_utcnow(),
|
||||||
)
|
)
|
||||||
self.session.add(project)
|
self.session.add(project)
|
||||||
|
if creator_user_id is not None:
|
||||||
|
self.session.add(
|
||||||
|
models.UserProjectMembership(
|
||||||
|
id=uuid4(),
|
||||||
|
user_id=creator_user_id,
|
||||||
|
project_id=project.id,
|
||||||
|
project_role="member",
|
||||||
|
)
|
||||||
|
)
|
||||||
await self.session.commit()
|
await self.session.commit()
|
||||||
await self.session.refresh(project)
|
await self.session.refresh(project)
|
||||||
return project
|
return project
|
||||||
@@ -483,7 +493,7 @@ class MetadataRepository:
|
|||||||
gs_workspace=project.gs_workspace,
|
gs_workspace=project.gs_workspace,
|
||||||
map_extent=project.map_extent,
|
map_extent=project.map_extent,
|
||||||
status=project.status,
|
status=project.status,
|
||||||
project_role="owner",
|
project_role="member",
|
||||||
)
|
)
|
||||||
for project in result.scalars().all()
|
for project in result.scalars().all()
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -169,6 +169,38 @@ class InternalQueries:
|
|||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def query_latest_scada_time(
|
||||||
|
device_ids: List[str],
|
||||||
|
before_time: str | datetime | None = None,
|
||||||
|
db_name: str = None,
|
||||||
|
max_retries: int = 3,
|
||||||
|
) -> datetime | None:
|
||||||
|
"""Return the latest SCADA timestamp for the selected devices."""
|
||||||
|
before_dt = (
|
||||||
|
parse_utc_time(before_time, field_name="before_time")
|
||||||
|
if before_time is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
for attempt in range(max_retries):
|
||||||
|
try:
|
||||||
|
conn_string = (
|
||||||
|
get_timescaledb_pgconn_string(db_name=db_name)
|
||||||
|
if db_name
|
||||||
|
else get_timescaledb_pgconn_string()
|
||||||
|
)
|
||||||
|
with psycopg.Connection.connect(conn_string) as conn:
|
||||||
|
return ScadaRepository.get_latest_scada_time_sync(
|
||||||
|
conn,
|
||||||
|
device_ids,
|
||||||
|
before_dt,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
if attempt < max_retries - 1:
|
||||||
|
time.sleep(1)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def query_realtime_simulation_by_ids_timerange(
|
def query_realtime_simulation_by_ids_timerange(
|
||||||
element_ids: List[str],
|
element_ids: List[str],
|
||||||
|
|||||||
@@ -54,6 +54,27 @@ class ScadaRepository:
|
|||||||
)
|
)
|
||||||
return cur.fetchall()
|
return cur.fetchall()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_latest_scada_time_sync(
|
||||||
|
conn: Connection,
|
||||||
|
device_ids: List[str],
|
||||||
|
before_time: datetime | None = None,
|
||||||
|
) -> datetime | None:
|
||||||
|
with conn.cursor(row_factory=dict_row) as cur:
|
||||||
|
if before_time is None:
|
||||||
|
cur.execute(
|
||||||
|
"SELECT max(time) AS time FROM scada.scada_data WHERE device_id = ANY(%s)",
|
||||||
|
(device_ids,),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
cur.execute(
|
||||||
|
"SELECT max(time) AS time FROM scada.scada_data "
|
||||||
|
"WHERE device_id = ANY(%s) AND time <= %s",
|
||||||
|
(device_ids, before_time),
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
return row["time"] if row else None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_scada_field_by_id_time_range(
|
async def get_scada_field_by_id_time_range(
|
||||||
conn: AsyncConnection,
|
conn: AsyncConnection,
|
||||||
|
|||||||
+4
-1
@@ -6,7 +6,8 @@ import logging
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import app.services.project_info as project_info
|
import app.services.project_info as project_info
|
||||||
from app.api.v1.router import api_router
|
from app.api.problem_details import install_problem_details_handlers
|
||||||
|
from app.api.v1.rest_router import api_router
|
||||||
from app.infra.db.timescaledb.database import db as tsdb
|
from app.infra.db.timescaledb.database import db as tsdb
|
||||||
from app.infra.db.postgresql.database import db as pgdb
|
from app.infra.db.postgresql.database import db as pgdb
|
||||||
from app.infra.db.dynamic_manager import project_connection_manager
|
from app.infra.db.dynamic_manager import project_connection_manager
|
||||||
@@ -64,11 +65,13 @@ app = FastAPI(
|
|||||||
docs_url=None if is_production else "/docs",
|
docs_url=None if is_production else "/docs",
|
||||||
redoc_url=None if is_production else "/redoc",
|
redoc_url=None if is_production else "/redoc",
|
||||||
openapi_url=None if is_production else "/openapi.json",
|
openapi_url=None if is_production else "/openapi.json",
|
||||||
|
redirect_slashes=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# Include Routers
|
# Include Routers
|
||||||
app.include_router(api_router, prefix="/api/v1")
|
app.include_router(api_router, prefix="/api/v1")
|
||||||
|
install_problem_details_handlers(app)
|
||||||
# Legcy Routers without version prefix
|
# Legcy Routers without version prefix
|
||||||
# app.include_router(api_router)
|
# app.include_router(api_router)
|
||||||
|
|
||||||
|
|||||||
@@ -320,7 +320,11 @@ from .s23_options_util import (
|
|||||||
from .s23_options_util import get_option_v3_schema, get_option_v3
|
from .s23_options_util import get_option_v3_schema, get_option_v3
|
||||||
from .batch_api import set_option_v3_ex
|
from .batch_api import set_option_v3_ex
|
||||||
|
|
||||||
from .s24_coordinates import get_node_coord, get_nodes_in_extent, get_links_in_extent
|
from .s24_coordinates import (
|
||||||
|
get_links_in_extent,
|
||||||
|
get_node_coord,
|
||||||
|
get_nodes_in_extent,
|
||||||
|
)
|
||||||
|
|
||||||
from .s25_vertices import (
|
from .s25_vertices import (
|
||||||
get_vertex_schema,
|
get_vertex_schema,
|
||||||
@@ -456,8 +460,6 @@ from .s36_wda_cal import (
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
from .s38_scada_info import get_scada_info_schema, get_scada_info, get_all_scada_info
|
from .s38_scada_info import get_scada_info_schema, get_scada_info, get_all_scada_info
|
||||||
|
|
||||||
from .s39_user import get_user_schema, get_user, get_all_users
|
|
||||||
|
|
||||||
from .s40_schema import get_scheme_schema, get_scheme, get_all_schemes
|
from .s40_schema import get_scheme_schema, get_scheme, get_all_schemes
|
||||||
|
|
||||||
from .s41_pipe_risk_probability import (
|
from .s41_pipe_risk_probability import (
|
||||||
@@ -468,6 +470,11 @@ from .s41_pipe_risk_probability import (
|
|||||||
get_pipe_risk_probability_geometries,
|
get_pipe_risk_probability_geometries,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .s42_sensor_placement import get_all_sensor_placements
|
from .s42_sensor_placement import (
|
||||||
|
get_all_sensor_placements,
|
||||||
|
get_sensor_placement,
|
||||||
|
get_sensor_placement_nodes,
|
||||||
|
update_sensor_placement,
|
||||||
|
)
|
||||||
|
|
||||||
from .s43_burst_locate_result import get_all_burst_locate_results
|
from .s43_burst_locate_result import get_all_burst_locate_results
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
from .database import *
|
|
||||||
from .s0_base import *
|
|
||||||
|
|
||||||
class User(object):
|
|
||||||
def __init__(self, input: dict[str, Any]) -> None:
|
|
||||||
self.type = 'user'
|
|
||||||
self.id = str(input['user_id'])
|
|
||||||
self.name = str(input['username'])
|
|
||||||
self.password = str(input['password'])
|
|
||||||
|
|
||||||
def as_dict(self) -> dict[str, Any]:
|
|
||||||
return { 'type': self.type, 'id': self.id, 'name': self.name, 'password': self.password }
|
|
||||||
|
|
||||||
def as_id_dict(self) -> dict[str, Any]:
|
|
||||||
return { 'type': self.type, 'id': self.id }
|
|
||||||
|
|
||||||
|
|
||||||
def get_user_schema(name: str) -> dict[str, dict[Any, Any]]:
|
|
||||||
return { 'id' : {'type': 'str' , 'optional': False , 'readonly': True },
|
|
||||||
'name' : {'type': 'str' , 'optional': False , 'readonly': False},
|
|
||||||
'password' : {'type': 'str' , 'optional': False , 'readonly': False} }
|
|
||||||
|
|
||||||
def get_user(name: str, user_name: str) -> dict[Any, Any]:
|
|
||||||
t = try_read(name, f"select * from users where username = '{user_name}'")
|
|
||||||
if t == None:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
d = {}
|
|
||||||
d['id'] = str(t['user_id'])
|
|
||||||
d['name'] = str(t['username'])
|
|
||||||
# d['password'] = str(t['password'])
|
|
||||||
|
|
||||||
return d
|
|
||||||
|
|
||||||
def get_all_users(name: str) -> list[dict[Any, Any]]:
|
|
||||||
return read_all(name, "select * from users")
|
|
||||||
|
|
||||||
@@ -1,7 +1,124 @@
|
|||||||
from .database import *
|
from typing import Any
|
||||||
from .s0_base import *
|
|
||||||
from .s42_sensor_placement import *
|
|
||||||
import json
|
|
||||||
|
|
||||||
def get_all_sensor_placements(name: str) -> list[dict[Any, Any]]:
|
from psycopg.rows import dict_row
|
||||||
return read_all(name, "select * from sensor_placement")
|
|
||||||
|
from .connection import project_connection
|
||||||
|
from .database import read_all
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_sensor_placements(name: str) -> list[dict[str, Any]]:
|
||||||
|
return read_all(name, "select * from sensor_placement")
|
||||||
|
|
||||||
|
|
||||||
|
def create_sensor_placement(
|
||||||
|
name: str,
|
||||||
|
*,
|
||||||
|
scheme_name: str,
|
||||||
|
min_diameter: int,
|
||||||
|
username: str,
|
||||||
|
sensor_location: list[str],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
with project_connection(name) as conn:
|
||||||
|
with conn.cursor(row_factory=dict_row) as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO sensor_placement (
|
||||||
|
scheme_name,
|
||||||
|
sensor_number,
|
||||||
|
min_diameter,
|
||||||
|
username,
|
||||||
|
sensor_location
|
||||||
|
)
|
||||||
|
VALUES (%s, %s, %s, %s, %s)
|
||||||
|
RETURNING *
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
scheme_name,
|
||||||
|
len(sensor_location),
|
||||||
|
min_diameter,
|
||||||
|
username,
|
||||||
|
sensor_location,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
created = cur.fetchone()
|
||||||
|
if created is None:
|
||||||
|
raise RuntimeError("监测点方案写入失败")
|
||||||
|
return dict(created)
|
||||||
|
|
||||||
|
|
||||||
|
def get_sensor_placement(name: str, scheme_id: int) -> dict[str, Any] | None:
|
||||||
|
with project_connection(name) as conn:
|
||||||
|
with conn.cursor(row_factory=dict_row) as cur:
|
||||||
|
cur.execute(
|
||||||
|
"SELECT * FROM sensor_placement WHERE id = %s",
|
||||||
|
(scheme_id,),
|
||||||
|
)
|
||||||
|
return cur.fetchone()
|
||||||
|
|
||||||
|
|
||||||
|
def get_sensor_placement_nodes(
|
||||||
|
name: str,
|
||||||
|
node_ids: list[str],
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
if not node_ids:
|
||||||
|
return []
|
||||||
|
with project_connection(name) as conn:
|
||||||
|
with conn.cursor(row_factory=dict_row) as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
WITH incident_pipe_diameters AS (
|
||||||
|
SELECT node_id, MAX(diameter) AS max_pipe_diameter
|
||||||
|
FROM (
|
||||||
|
SELECT node1 AS node_id, diameter
|
||||||
|
FROM pipes
|
||||||
|
WHERE node1 = ANY(%s)
|
||||||
|
UNION ALL
|
||||||
|
SELECT node2 AS node_id, diameter
|
||||||
|
FROM pipes
|
||||||
|
WHERE node2 = ANY(%s)
|
||||||
|
) AS incident_pipes
|
||||||
|
GROUP BY node_id
|
||||||
|
)
|
||||||
|
SELECT DISTINCT ON (gj.id)
|
||||||
|
gj.id AS node_id,
|
||||||
|
ipd.max_pipe_diameter,
|
||||||
|
gj.elevation,
|
||||||
|
ST_X(c.coord) AS project_x,
|
||||||
|
ST_Y(c.coord) AS project_y,
|
||||||
|
ST_X(gj.geom) AS map_x,
|
||||||
|
ST_Y(gj.geom) AS map_y
|
||||||
|
FROM geo_junctions_mat AS gj
|
||||||
|
JOIN coordinates AS c ON c.node = gj.id
|
||||||
|
LEFT JOIN incident_pipe_diameters AS ipd ON ipd.node_id = gj.id
|
||||||
|
WHERE gj.id = ANY(%s)
|
||||||
|
ORDER BY gj.id
|
||||||
|
""",
|
||||||
|
(node_ids, node_ids, node_ids),
|
||||||
|
)
|
||||||
|
return list(cur.fetchall())
|
||||||
|
|
||||||
|
|
||||||
|
def update_sensor_placement(
|
||||||
|
name: str,
|
||||||
|
scheme_id: int,
|
||||||
|
*,
|
||||||
|
expected_sensor_location: list[str],
|
||||||
|
sensor_location: list[str],
|
||||||
|
) -> dict[str, Any] | None:
|
||||||
|
with project_connection(name) as conn:
|
||||||
|
with conn.cursor(row_factory=dict_row) as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
UPDATE sensor_placement
|
||||||
|
SET sensor_location = %s, sensor_number = %s
|
||||||
|
WHERE id = %s AND sensor_location = %s
|
||||||
|
RETURNING *
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
sensor_location,
|
||||||
|
len(sensor_location),
|
||||||
|
scheme_id,
|
||||||
|
expected_sensor_location,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return cur.fetchone()
|
||||||
|
|||||||
+363
-29
@@ -1,8 +1,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime
|
from collections import Counter
|
||||||
|
from datetime import datetime, timedelta
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from app.algorithms.burst_detection.burst_detector import BurstDetector
|
from app.algorithms.burst_detection.burst_detector import BurstDetector
|
||||||
@@ -17,6 +19,15 @@ from app.services.tjnetwork import get_all_scada_info
|
|||||||
from app.services.time_api import extract_date, parse_utc_time, utc_now
|
from app.services.time_api import extract_date, parse_utc_time, utc_now
|
||||||
|
|
||||||
|
|
||||||
|
TARGET_DAY_COUNT = 15
|
||||||
|
DEFAULT_SAMPLE_INTERVAL_MINUTES = 15
|
||||||
|
TARGET_MU = 1
|
||||||
|
TARGET_N_ESTIMATORS = 50
|
||||||
|
TARGET_RANDOM_STATE = 42
|
||||||
|
TARGET_SCORE_THRESHOLD = -0.04
|
||||||
|
MIN_COMPLETE_SENSORS = 5
|
||||||
|
|
||||||
|
|
||||||
def run_burst_detection(
|
def run_burst_detection(
|
||||||
*,
|
*,
|
||||||
network: str,
|
network: str,
|
||||||
@@ -31,6 +42,8 @@ def run_burst_detection(
|
|||||||
points_per_day: int = 1440,
|
points_per_day: int = 1440,
|
||||||
mu: int = 100,
|
mu: int = 100,
|
||||||
iforest_params: dict[str, Any] | None = None,
|
iforest_params: dict[str, Any] | None = None,
|
||||||
|
target_time: datetime | str | None = None,
|
||||||
|
sampling_interval_minutes: int | None = None,
|
||||||
scada_start: datetime | str | None = None,
|
scada_start: datetime | str | None = None,
|
||||||
scada_end: datetime | str | None = None,
|
scada_end: datetime | str | None = None,
|
||||||
sensor_nodes: list[str] | None = None,
|
sensor_nodes: list[str] | None = None,
|
||||||
@@ -42,7 +55,8 @@ def run_burst_detection(
|
|||||||
"""
|
"""
|
||||||
运行爆管侦测服务入口。
|
运行爆管侦测服务入口。
|
||||||
|
|
||||||
调用方式二选一:
|
调用方式三选一:
|
||||||
|
- 不传数据时间窗,自动侦测最近完整时刻;可用 `target_time` 回放历史时刻
|
||||||
- 直接传 `observed_pressure_data`
|
- 直接传 `observed_pressure_data`
|
||||||
- 或传 `scada_start/scada_end` 让后端自动查询 SCADA 压力数据
|
- 或传 `scada_start/scada_end` 让后端自动查询 SCADA 压力数据
|
||||||
|
|
||||||
@@ -74,8 +88,65 @@ def run_burst_detection(
|
|||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
use_scada_source = scada_start is not None or scada_end is not None
|
use_scada_source = scada_start is not None or scada_end is not None
|
||||||
|
use_target_mode = (
|
||||||
|
observed_pressure_data is None
|
||||||
|
and not use_scada_source
|
||||||
|
and data_source != "simulation"
|
||||||
|
) or target_time is not None
|
||||||
|
|
||||||
if use_scada_source:
|
resolved_target_time: datetime | None = None
|
||||||
|
requested_target_time: datetime | None = None
|
||||||
|
excluded_sensors: list[dict[str, str]] = []
|
||||||
|
daily_times: list[datetime] | None = None
|
||||||
|
resolved_sampling_interval_minutes: int | None = None
|
||||||
|
|
||||||
|
if use_target_mode:
|
||||||
|
if observed_pressure_data is not None or use_scada_source:
|
||||||
|
raise ValueError(
|
||||||
|
"target_time 不能与 observed_pressure_data 或 scada_start/scada_end 同时使用。"
|
||||||
|
)
|
||||||
|
scada_sensor_nodes = (
|
||||||
|
selected_sensor_nodes
|
||||||
|
if selected_sensor_nodes is not None
|
||||||
|
else _get_pressure_sensor_nodes(network)
|
||||||
|
)
|
||||||
|
requested_target_time = (
|
||||||
|
_to_datetime(target_time) if target_time is not None else None
|
||||||
|
)
|
||||||
|
resolved_sampling_interval_minutes = _resolve_sampling_interval_minutes(
|
||||||
|
network=network,
|
||||||
|
sensor_nodes=scada_sensor_nodes,
|
||||||
|
requested_interval=sampling_interval_minutes,
|
||||||
|
)
|
||||||
|
target_points_per_day = 1440 // resolved_sampling_interval_minutes
|
||||||
|
(
|
||||||
|
observed_input,
|
||||||
|
resolved_target_time,
|
||||||
|
excluded_sensors,
|
||||||
|
) = _build_target_pressure_from_scada(
|
||||||
|
network=network,
|
||||||
|
sensor_nodes=scada_sensor_nodes,
|
||||||
|
requested_target_time=requested_target_time,
|
||||||
|
sampling_interval_minutes=resolved_sampling_interval_minutes,
|
||||||
|
points_per_day=target_points_per_day,
|
||||||
|
)
|
||||||
|
selected_sensor_nodes = list(observed_input.columns)
|
||||||
|
observed_source = (
|
||||||
|
"latest_monitoring" if target_time is None else "historical_monitoring"
|
||||||
|
)
|
||||||
|
points_per_day = target_points_per_day
|
||||||
|
mu = TARGET_MU
|
||||||
|
iforest_params = {
|
||||||
|
"n_estimators": TARGET_N_ESTIMATORS,
|
||||||
|
"random_state": TARGET_RANDOM_STATE,
|
||||||
|
"contamination": "auto",
|
||||||
|
}
|
||||||
|
daily_times = [
|
||||||
|
resolved_target_time - timedelta(days=offset)
|
||||||
|
for offset in range(TARGET_DAY_COUNT - 1, -1, -1)
|
||||||
|
]
|
||||||
|
|
||||||
|
elif use_scada_source:
|
||||||
scada_sensor_nodes = (
|
scada_sensor_nodes = (
|
||||||
selected_sensor_nodes
|
selected_sensor_nodes
|
||||||
if selected_sensor_nodes is not None
|
if selected_sensor_nodes is not None
|
||||||
@@ -121,7 +192,16 @@ def run_burst_detection(
|
|||||||
sensor_nodes=selected_sensor_nodes,
|
sensor_nodes=selected_sensor_nodes,
|
||||||
)
|
)
|
||||||
resolved_sensor_nodes = list(result_df.attrs.get("sensor_nodes", []))
|
resolved_sensor_nodes = list(result_df.attrs.get("sensor_nodes", []))
|
||||||
rows = _serialize_result_rows(result_df)
|
rows = _serialize_result_rows(
|
||||||
|
result_df,
|
||||||
|
daily_times=daily_times,
|
||||||
|
target_only=use_target_mode,
|
||||||
|
)
|
||||||
|
summary = _build_detection_summary(
|
||||||
|
result_df,
|
||||||
|
daily_times=daily_times,
|
||||||
|
target_only=use_target_mode,
|
||||||
|
)
|
||||||
payload: dict[str, Any] = {
|
payload: dict[str, Any] = {
|
||||||
"network": network,
|
"network": network,
|
||||||
"sensor_nodes": resolved_sensor_nodes,
|
"sensor_nodes": resolved_sensor_nodes,
|
||||||
@@ -130,7 +210,17 @@ def run_burst_detection(
|
|||||||
"points_per_day": int(result_df.attrs.get("points_per_day", points_per_day)),
|
"points_per_day": int(result_df.attrs.get("points_per_day", points_per_day)),
|
||||||
"day_count": int(result_df.attrs.get("day_count", len(result_df))),
|
"day_count": int(result_df.attrs.get("day_count", len(result_df))),
|
||||||
"rows": rows,
|
"rows": rows,
|
||||||
"summary": _build_detection_summary(result_df),
|
"summary": summary,
|
||||||
|
"algorithm_params": {
|
||||||
|
"mu": mu,
|
||||||
|
"points_per_day": points_per_day,
|
||||||
|
"iforest_params": detector.iforest_params,
|
||||||
|
**(
|
||||||
|
{"score_threshold": TARGET_SCORE_THRESHOLD}
|
||||||
|
if use_target_mode
|
||||||
|
else {}
|
||||||
|
),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
if data_source == "simulation":
|
if data_source == "simulation":
|
||||||
payload["data_source"] = "simulation"
|
payload["data_source"] = "simulation"
|
||||||
@@ -141,7 +231,50 @@ def run_burst_detection(
|
|||||||
else:
|
else:
|
||||||
payload["data_source"] = "monitoring"
|
payload["data_source"] = "monitoring"
|
||||||
|
|
||||||
if use_scada_source:
|
if (
|
||||||
|
use_target_mode
|
||||||
|
and resolved_target_time is not None
|
||||||
|
and resolved_sampling_interval_minutes is not None
|
||||||
|
):
|
||||||
|
sample_start = resolved_target_time - timedelta(
|
||||||
|
days=TARGET_DAY_COUNT,
|
||||||
|
minutes=-resolved_sampling_interval_minutes,
|
||||||
|
)
|
||||||
|
payload.update(
|
||||||
|
{
|
||||||
|
"requested_target_time": (
|
||||||
|
requested_target_time.isoformat()
|
||||||
|
if requested_target_time is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
"target_time": resolved_target_time.isoformat(),
|
||||||
|
"reference_window": {
|
||||||
|
"start": (resolved_target_time - timedelta(days=14)).isoformat(),
|
||||||
|
"end": (resolved_target_time - timedelta(days=1)).isoformat(),
|
||||||
|
"day_count": 14,
|
||||||
|
},
|
||||||
|
"sampling_interval_minutes": resolved_sampling_interval_minutes,
|
||||||
|
"daily_scores": [
|
||||||
|
{
|
||||||
|
"timestamp": row["Timestamp"],
|
||||||
|
"role": row["Role"],
|
||||||
|
"score": row["Score"],
|
||||||
|
"raw_prediction": row["Prediction"],
|
||||||
|
}
|
||||||
|
for row in rows
|
||||||
|
],
|
||||||
|
"data_quality": {
|
||||||
|
"included_sensors": resolved_sensor_nodes,
|
||||||
|
"excluded_sensors": excluded_sensors,
|
||||||
|
"minimum_required_sensors": MIN_COMPLETE_SENSORS,
|
||||||
|
},
|
||||||
|
"scada_window": {
|
||||||
|
"start": sample_start.isoformat(),
|
||||||
|
"end": resolved_target_time.isoformat(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
elif use_scada_source:
|
||||||
payload["scada_window"] = {
|
payload["scada_window"] = {
|
||||||
"start": _to_datetime(scada_start).isoformat(),
|
"start": _to_datetime(scada_start).isoformat(),
|
||||||
"end": _to_datetime(scada_end).isoformat(),
|
"end": _to_datetime(scada_end).isoformat(),
|
||||||
@@ -294,22 +427,49 @@ def _store_burst_detection_scheme(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _serialize_result_rows(result_df: pd.DataFrame) -> list[dict[str, Any]]:
|
def _serialize_result_rows(
|
||||||
|
result_df: pd.DataFrame,
|
||||||
|
*,
|
||||||
|
daily_times: list[datetime] | None = None,
|
||||||
|
target_only: bool = False,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
rows: list[dict[str, Any]] = []
|
rows: list[dict[str, Any]] = []
|
||||||
for row in result_df.to_dict(orient="records"):
|
raw_rows = result_df.to_dict(orient="records")
|
||||||
|
for index, row in enumerate(raw_rows):
|
||||||
|
is_target = index == len(raw_rows) - 1
|
||||||
|
is_burst = bool(row["IsBurst"])
|
||||||
|
if target_only:
|
||||||
|
is_burst = is_target and float(row["Score"]) <= TARGET_SCORE_THRESHOLD
|
||||||
rows.append(
|
rows.append(
|
||||||
{
|
{
|
||||||
"Day": int(row["Day"]),
|
"Day": int(row["Day"]),
|
||||||
"Score": float(row["Score"]),
|
"Score": float(row["Score"]),
|
||||||
"Prediction": int(row["Prediction"]),
|
"Prediction": int(row["Prediction"]),
|
||||||
"IsBurst": bool(row["IsBurst"]),
|
"IsBurst": is_burst,
|
||||||
|
**(
|
||||||
|
{
|
||||||
|
"Timestamp": daily_times[index].isoformat(),
|
||||||
|
"Role": "target" if is_target else "reference",
|
||||||
|
}
|
||||||
|
if daily_times is not None
|
||||||
|
else {}
|
||||||
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
|
|
||||||
def _build_detection_summary(result_df: pd.DataFrame) -> dict[str, Any]:
|
def _build_detection_summary(
|
||||||
rows = _serialize_result_rows(result_df)
|
result_df: pd.DataFrame,
|
||||||
|
*,
|
||||||
|
daily_times: list[datetime] | None = None,
|
||||||
|
target_only: bool = False,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
rows = _serialize_result_rows(
|
||||||
|
result_df,
|
||||||
|
daily_times=daily_times,
|
||||||
|
target_only=target_only,
|
||||||
|
)
|
||||||
if not rows:
|
if not rows:
|
||||||
raise ValueError("爆管侦测结果为空。")
|
raise ValueError("爆管侦测结果为空。")
|
||||||
|
|
||||||
@@ -318,7 +478,7 @@ def _build_detection_summary(result_df: pd.DataFrame) -> dict[str, Any]:
|
|||||||
latest_row = rows[-1]
|
latest_row = rows[-1]
|
||||||
anomaly_days = [row["Day"] for row in rows if row["IsBurst"]]
|
anomaly_days = [row["Day"] for row in rows if row["IsBurst"]]
|
||||||
|
|
||||||
return {
|
summary = {
|
||||||
"burst_detected": bool(latest_row["IsBurst"]),
|
"burst_detected": bool(latest_row["IsBurst"]),
|
||||||
"latest_day": latest_row,
|
"latest_day": latest_row,
|
||||||
"most_anomalous_day": int(result_df.iloc[most_anomalous_index]["Day"]),
|
"most_anomalous_day": int(result_df.iloc[most_anomalous_index]["Day"]),
|
||||||
@@ -326,6 +486,18 @@ def _build_detection_summary(result_df: pd.DataFrame) -> dict[str, Any]:
|
|||||||
"anomaly_day_count": len(anomaly_days),
|
"anomaly_day_count": len(anomaly_days),
|
||||||
"latest_sensor_rankings": _build_latest_sensor_rankings(result_df),
|
"latest_sensor_rankings": _build_latest_sensor_rankings(result_df),
|
||||||
}
|
}
|
||||||
|
if target_only:
|
||||||
|
target_score = float(latest_row["Score"])
|
||||||
|
summary.update(
|
||||||
|
{
|
||||||
|
"target_score": target_score,
|
||||||
|
"score_threshold": TARGET_SCORE_THRESHOLD,
|
||||||
|
"target_rank": int(result_df["Score"].rank(method="min").iloc[-1]),
|
||||||
|
"target_time": latest_row.get("Timestamp"),
|
||||||
|
"reference_day_count": TARGET_DAY_COUNT - 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return summary
|
||||||
|
|
||||||
|
|
||||||
def _build_latest_sensor_rankings(result_df: pd.DataFrame) -> list[dict[str, Any]]:
|
def _build_latest_sensor_rankings(result_df: pd.DataFrame) -> list[dict[str, Any]]:
|
||||||
@@ -334,20 +506,194 @@ def _build_latest_sensor_rankings(result_df: pd.DataFrame) -> list[dict[str, Any
|
|||||||
if feature_matrix is None or len(sensor_nodes) == 0:
|
if feature_matrix is None or len(sensor_nodes) == 0:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
latest_values = feature_matrix[-1]
|
latest_values = np.asarray(feature_matrix[-1], dtype=float)
|
||||||
|
history = np.asarray(feature_matrix[:-1], dtype=float)
|
||||||
|
history_means = history.mean(axis=0)
|
||||||
|
history_stds = history.std(axis=0)
|
||||||
|
safe_stds = np.where(history_stds > 1e-9, history_stds, 1e-9)
|
||||||
|
deviations = (latest_values - history_means) / safe_stds
|
||||||
ranking = sorted(
|
ranking = sorted(
|
||||||
zip(sensor_nodes, latest_values, strict=False),
|
zip(
|
||||||
key=lambda item: item[1],
|
sensor_nodes,
|
||||||
|
latest_values,
|
||||||
|
history_means,
|
||||||
|
history_stds,
|
||||||
|
deviations,
|
||||||
|
strict=False,
|
||||||
|
),
|
||||||
|
key=lambda item: item[4],
|
||||||
)
|
)
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
"sensor_node": sensor_id,
|
"sensor_node": sensor_id,
|
||||||
"latest_high_frequency_value": float(value),
|
"latest_high_frequency_value": float(value),
|
||||||
|
"historical_mean": float(history_mean),
|
||||||
|
"historical_std": float(history_std),
|
||||||
|
"standardized_deviation": float(deviation),
|
||||||
}
|
}
|
||||||
for sensor_id, value in ranking[: min(10, len(ranking))]
|
for sensor_id, value, history_mean, history_std, deviation in ranking[
|
||||||
|
: min(10, len(ranking))
|
||||||
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _build_target_pressure_from_scada(
|
||||||
|
*,
|
||||||
|
network: str,
|
||||||
|
sensor_nodes: list[str],
|
||||||
|
requested_target_time: datetime | None,
|
||||||
|
sampling_interval_minutes: int,
|
||||||
|
points_per_day: int,
|
||||||
|
) -> tuple[pd.DataFrame, datetime, list[dict[str, str]]]:
|
||||||
|
node_query_id = _get_pressure_sensor_mapping(network)
|
||||||
|
mapped_nodes = [node for node in sensor_nodes if node in node_query_id]
|
||||||
|
excluded_without_mapping = [
|
||||||
|
{"sensor_node": node, "reason": "missing_api_query_id"}
|
||||||
|
for node in sensor_nodes
|
||||||
|
if node not in node_query_id
|
||||||
|
]
|
||||||
|
if len(mapped_nodes) < MIN_COMPLETE_SENSORS:
|
||||||
|
raise ValueError(
|
||||||
|
f"可查询的压力测点少于 {MIN_COMPLETE_SENSORS} 个,无法执行爆管侦测。"
|
||||||
|
)
|
||||||
|
|
||||||
|
query_ids = [node_query_id[node] for node in mapped_nodes]
|
||||||
|
candidate_before = requested_target_time
|
||||||
|
last_excluded: list[dict[str, str]] = excluded_without_mapping
|
||||||
|
|
||||||
|
for _ in range(4):
|
||||||
|
resolved_target = InternalQueries.query_latest_scada_time(
|
||||||
|
db_name=network,
|
||||||
|
device_ids=query_ids,
|
||||||
|
before_time=candidate_before,
|
||||||
|
)
|
||||||
|
if resolved_target is None:
|
||||||
|
break
|
||||||
|
|
||||||
|
sample_start = resolved_target - timedelta(
|
||||||
|
days=TARGET_DAY_COUNT,
|
||||||
|
minutes=-sampling_interval_minutes,
|
||||||
|
)
|
||||||
|
expected_index = pd.date_range(
|
||||||
|
start=sample_start,
|
||||||
|
end=resolved_target,
|
||||||
|
freq=f"{sampling_interval_minutes}min",
|
||||||
|
)
|
||||||
|
scada_data = InternalQueries.query_scada_by_ids_timerange(
|
||||||
|
db_name=network,
|
||||||
|
device_ids=query_ids,
|
||||||
|
start_time=sample_start,
|
||||||
|
end_time=resolved_target,
|
||||||
|
)
|
||||||
|
|
||||||
|
complete_columns: dict[str, pd.Series] = {}
|
||||||
|
excluded = list(excluded_without_mapping)
|
||||||
|
for node_id in mapped_nodes:
|
||||||
|
query_id = node_query_id[node_id]
|
||||||
|
records = scada_data.get(query_id, [])
|
||||||
|
if not records:
|
||||||
|
excluded.append({"sensor_node": node_id, "reason": "no_data"})
|
||||||
|
continue
|
||||||
|
|
||||||
|
record_frame = pd.DataFrame.from_records(records)
|
||||||
|
record_frame["time"] = pd.to_datetime(record_frame["time"], utc=True)
|
||||||
|
record_frame["value"] = pd.to_numeric(
|
||||||
|
record_frame["value"], errors="coerce"
|
||||||
|
)
|
||||||
|
series = (
|
||||||
|
record_frame.drop_duplicates(subset="time", keep="last")
|
||||||
|
.set_index("time")["value"]
|
||||||
|
.reindex(expected_index)
|
||||||
|
)
|
||||||
|
if len(series) != TARGET_DAY_COUNT * points_per_day:
|
||||||
|
excluded.append(
|
||||||
|
{"sensor_node": node_id, "reason": "unexpected_sample_count"}
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
if series.isna().any():
|
||||||
|
excluded.append(
|
||||||
|
{"sensor_node": node_id, "reason": "missing_or_invalid_samples"}
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
complete_columns[node_id] = series
|
||||||
|
|
||||||
|
if len(complete_columns) >= MIN_COMPLETE_SENSORS:
|
||||||
|
observation_df = pd.DataFrame(complete_columns, index=expected_index)
|
||||||
|
return observation_df, resolved_target, excluded
|
||||||
|
|
||||||
|
last_excluded = excluded
|
||||||
|
candidate_before = resolved_target - timedelta(microseconds=1)
|
||||||
|
|
||||||
|
excluded_preview = ", ".join(
|
||||||
|
item["sensor_node"] for item in last_excluded[:10]
|
||||||
|
)
|
||||||
|
raise ValueError(
|
||||||
|
f"最近数据中完整压力测点少于 {MIN_COMPLETE_SENSORS} 个;"
|
||||||
|
f"请检查 15 天数据完整性。排除测点: {excluded_preview or '无'}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_sampling_interval_minutes(
|
||||||
|
*,
|
||||||
|
network: str,
|
||||||
|
sensor_nodes: list[str],
|
||||||
|
requested_interval: int | None,
|
||||||
|
) -> int:
|
||||||
|
if requested_interval is not None:
|
||||||
|
interval = int(requested_interval)
|
||||||
|
else:
|
||||||
|
selected_nodes = set(sensor_nodes)
|
||||||
|
inferred_intervals = [
|
||||||
|
parsed
|
||||||
|
for item in get_all_scada_info(network)
|
||||||
|
if str(item.get("type", "")).lower() == "pressure"
|
||||||
|
and str(item.get("associated_element_id", "")) in selected_nodes
|
||||||
|
and (
|
||||||
|
parsed := _parse_sampling_interval_minutes(
|
||||||
|
item.get("transmission_frequency")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
is not None
|
||||||
|
]
|
||||||
|
interval = (
|
||||||
|
Counter(inferred_intervals).most_common(1)[0][0]
|
||||||
|
if inferred_intervals
|
||||||
|
else DEFAULT_SAMPLE_INTERVAL_MINUTES
|
||||||
|
)
|
||||||
|
|
||||||
|
if interval <= 0 or 1440 % interval != 0:
|
||||||
|
raise ValueError("采样间隔必须是能整除 1440 分钟的正整数。")
|
||||||
|
return interval
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_sampling_interval_minutes(value: Any) -> int | None:
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
if isinstance(value, (int, float)):
|
||||||
|
minutes = float(value)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
minutes = pd.to_timedelta(str(value)).total_seconds() / 60
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
rounded = round(minutes)
|
||||||
|
if minutes <= 0 or abs(minutes - rounded) > 1e-6:
|
||||||
|
return None
|
||||||
|
return int(rounded)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_pressure_sensor_mapping(network: str) -> dict[str, str]:
|
||||||
|
node_query_id: dict[str, str] = {}
|
||||||
|
for item in get_all_scada_info(network):
|
||||||
|
if str(item.get("type", "")).lower() != "pressure":
|
||||||
|
continue
|
||||||
|
node_id = item.get("associated_element_id")
|
||||||
|
query_id = item.get("api_query_id")
|
||||||
|
if node_id and query_id is not None:
|
||||||
|
node_query_id[str(node_id)] = str(query_id)
|
||||||
|
return node_query_id
|
||||||
|
|
||||||
|
|
||||||
def _get_pressure_sensor_nodes(network: str) -> list[str]:
|
def _get_pressure_sensor_nodes(network: str) -> list[str]:
|
||||||
sensor_nodes: list[str] = []
|
sensor_nodes: list[str] = []
|
||||||
for item in get_all_scada_info(network):
|
for item in get_all_scada_info(network):
|
||||||
@@ -377,19 +723,7 @@ def _build_observed_pressure_from_scada(
|
|||||||
if start_dt >= end_dt:
|
if start_dt >= end_dt:
|
||||||
raise ValueError("SCADA 时间窗非法:scada_start 必须早于 scada_end。")
|
raise ValueError("SCADA 时间窗非法:scada_start 必须早于 scada_end。")
|
||||||
|
|
||||||
node_query_id: dict[str, str] = {}
|
node_query_id = _get_pressure_sensor_mapping(network)
|
||||||
for item in get_all_scada_info(network):
|
|
||||||
if str(item.get("type", "")).lower() != "pressure":
|
|
||||||
continue
|
|
||||||
node_id = item.get("associated_element_id")
|
|
||||||
query_id = item.get("api_query_id")
|
|
||||||
if (
|
|
||||||
isinstance(node_id, str)
|
|
||||||
and node_id
|
|
||||||
and isinstance(query_id, str)
|
|
||||||
and query_id
|
|
||||||
):
|
|
||||||
node_query_id[node_id] = query_id
|
|
||||||
|
|
||||||
missing_nodes = [node_id for node_id in sensor_nodes if node_id not in node_query_id]
|
missing_nodes = [node_id for node_id in sensor_nodes if node_id not in node_query_id]
|
||||||
if missing_nodes:
|
if missing_nodes:
|
||||||
|
|||||||
@@ -476,7 +476,7 @@ def _get_simulation_scheme_burst_ids(
|
|||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
if not scheme_name:
|
if not scheme_name:
|
||||||
return []
|
return []
|
||||||
rows = query_scheme_list(network) or []
|
rows = query_scheme_list(network, scheme_type=scheme_type) or []
|
||||||
for row in rows:
|
for row in rows:
|
||||||
if len(row) < 7:
|
if len(row) < 7:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -11,53 +11,6 @@ from app.core.config import get_pgconn_string
|
|||||||
from app.services.time_api import parse_utc_time
|
from app.services.time_api import parse_utc_time
|
||||||
|
|
||||||
|
|
||||||
# 2025/03/23
|
|
||||||
def create_user(name: str, username: str, password: str):
|
|
||||||
"""
|
|
||||||
创建用户
|
|
||||||
:param name: 数据库名称
|
|
||||||
:param username: 用户名
|
|
||||||
:param password: 密码
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# 动态替换数据库名称
|
|
||||||
conn_string = get_pgconn_string(db_name=name)
|
|
||||||
# 连接到 PostgreSQL 数据库(这里是数据库 "bb")
|
|
||||||
with psycopg.connect(conn_string) as conn:
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
cur.execute(
|
|
||||||
"INSERT INTO users (username, password) VALUES (%s, %s)",
|
|
||||||
(username, password),
|
|
||||||
)
|
|
||||||
# 提交事务
|
|
||||||
conn.commit()
|
|
||||||
print("新用户创建成功!")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"创建用户出错:{e}")
|
|
||||||
|
|
||||||
|
|
||||||
# 2025/03/23
|
|
||||||
def delete_user(name: str, username: str):
|
|
||||||
"""
|
|
||||||
删除用户
|
|
||||||
:param name: 数据库名称
|
|
||||||
:param username: 用户名
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# 动态替换数据库名称
|
|
||||||
conn_string = get_pgconn_string(db_name=name)
|
|
||||||
# 连接到 PostgreSQL 数据库(这里是数据库 "bb")
|
|
||||||
with psycopg.connect(conn_string) as conn:
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
cur.execute("DELETE FROM users WHERE username = %s", (username,))
|
|
||||||
conn.commit()
|
|
||||||
print(f"用户 {username} 删除成功!")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"删除用户出错:{e}")
|
|
||||||
|
|
||||||
|
|
||||||
# 2025/03/23
|
# 2025/03/23
|
||||||
def scheme_name_exists(name: str, scheme_name: str) -> bool:
|
def scheme_name_exists(name: str, scheme_name: str) -> bool:
|
||||||
"""
|
"""
|
||||||
@@ -98,8 +51,8 @@ def store_scheme_info(
|
|||||||
:param name: 数据库名称
|
:param name: 数据库名称
|
||||||
:param scheme_name: 方案名称
|
:param scheme_name: 方案名称
|
||||||
:param scheme_type: 方案类型
|
:param scheme_type: 方案类型
|
||||||
:param username: 用户名(需在 users 表中已存在)
|
:param username: MetaDB 中的用户名快照
|
||||||
:param scheme_start_time: 方案起始时间(字符串)
|
:param scheme_start_time: 带时区的方案起始时间;写入前统一转换为 UTC
|
||||||
:param scheme_detail: 方案详情(字典,会转换为 JSON)
|
:param scheme_detail: 方案详情(字典,会转换为 JSON)
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
@@ -154,10 +107,16 @@ def delete_scheme_info(name: str, scheme_name: str) -> None:
|
|||||||
|
|
||||||
|
|
||||||
# 2025/03/23
|
# 2025/03/23
|
||||||
def query_scheme_list(name: str) -> list:
|
def query_scheme_list(
|
||||||
|
name: str,
|
||||||
|
scheme_type: str | None = None,
|
||||||
|
query_date: date | None = None,
|
||||||
|
) -> list:
|
||||||
"""
|
"""
|
||||||
查询pg数据库中的scheme_list,按照 create_time 降序排列,离现在时间最近的记录排在最前面
|
查询pg数据库中的scheme_list,按照 create_time 降序排列,离现在时间最近的记录排在最前面
|
||||||
:param name: 项目名称(数据库名称)
|
:param name: 项目名称(数据库名称)
|
||||||
|
:param scheme_type: 方案类型;为空时返回全部类型
|
||||||
|
:param query_date: 查询日期;为空时不按日期过滤
|
||||||
:return: 返回查询结果的所有行
|
:return: 返回查询结果的所有行
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
@@ -166,8 +125,38 @@ def query_scheme_list(name: str) -> list:
|
|||||||
# 连接到 PostgreSQL 数据库(这里是数据库 "bb")
|
# 连接到 PostgreSQL 数据库(这里是数据库 "bb")
|
||||||
with psycopg.connect(conn_string) as conn:
|
with psycopg.connect(conn_string) as conn:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
# 按 create_time 降序排列
|
if scheme_type and query_date is not None:
|
||||||
cur.execute("SELECT * FROM scheme_list ORDER BY create_time DESC")
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT *
|
||||||
|
FROM scheme_list
|
||||||
|
WHERE scheme_type = %s AND DATE(create_time) = %s
|
||||||
|
ORDER BY create_time DESC
|
||||||
|
""",
|
||||||
|
(scheme_type, query_date),
|
||||||
|
)
|
||||||
|
elif scheme_type:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT *
|
||||||
|
FROM scheme_list
|
||||||
|
WHERE scheme_type = %s
|
||||||
|
ORDER BY create_time DESC
|
||||||
|
""",
|
||||||
|
(scheme_type,),
|
||||||
|
)
|
||||||
|
elif query_date is not None:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT *
|
||||||
|
FROM scheme_list
|
||||||
|
WHERE DATE(create_time) = %s
|
||||||
|
ORDER BY create_time DESC
|
||||||
|
""",
|
||||||
|
(query_date,),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
cur.execute("SELECT * FROM scheme_list ORDER BY create_time DESC")
|
||||||
rows = cur.fetchall()
|
rows = cur.fetchall()
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
@@ -175,6 +164,85 @@ def query_scheme_list(name: str) -> list:
|
|||||||
print(f"查询错误:{e}")
|
print(f"查询错误:{e}")
|
||||||
|
|
||||||
|
|
||||||
|
def _filter_scheme_detail_scope(
|
||||||
|
result: dict,
|
||||||
|
name: str,
|
||||||
|
scheme_type: str | None = None,
|
||||||
|
) -> dict:
|
||||||
|
if not result:
|
||||||
|
return {}
|
||||||
|
if scheme_type and result.get("scheme_type") != scheme_type:
|
||||||
|
return {}
|
||||||
|
network = result.get("network")
|
||||||
|
if network not in (None, name):
|
||||||
|
return {}
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def query_scheme_detail(
|
||||||
|
name: str,
|
||||||
|
scheme_name: str,
|
||||||
|
scheme_type: str | None = None,
|
||||||
|
) -> dict:
|
||||||
|
if scheme_type == "dma_leak_identification":
|
||||||
|
return _filter_scheme_detail_scope(
|
||||||
|
query_leakage_identify_scheme_detail(name, scheme_name),
|
||||||
|
name,
|
||||||
|
scheme_type,
|
||||||
|
)
|
||||||
|
if scheme_type == "burst_detection":
|
||||||
|
return _filter_scheme_detail_scope(
|
||||||
|
query_burst_detection_scheme_detail(name, scheme_name),
|
||||||
|
name,
|
||||||
|
scheme_type,
|
||||||
|
)
|
||||||
|
if scheme_type == "burst_location":
|
||||||
|
return _filter_scheme_detail_scope(
|
||||||
|
query_burst_location_scheme_detail(name, scheme_name),
|
||||||
|
name,
|
||||||
|
scheme_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
conn_string = get_pgconn_string(db_name=name)
|
||||||
|
with psycopg.connect(conn_string) as conn:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
if scheme_type:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT scheme_id, scheme_name, scheme_type, username, create_time, scheme_start_time, scheme_detail
|
||||||
|
FROM public.scheme_list
|
||||||
|
WHERE scheme_name = %s AND scheme_type = %s
|
||||||
|
LIMIT 1
|
||||||
|
""",
|
||||||
|
(scheme_name, scheme_type),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT scheme_id, scheme_name, scheme_type, username, create_time, scheme_start_time, scheme_detail
|
||||||
|
FROM public.scheme_list
|
||||||
|
WHERE scheme_name = %s
|
||||||
|
LIMIT 1
|
||||||
|
""",
|
||||||
|
(scheme_name,),
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
if row is None:
|
||||||
|
return {}
|
||||||
|
detail = row[6] if isinstance(row[6], dict) else {}
|
||||||
|
return _filter_scheme_detail_scope({
|
||||||
|
"scheme_id": row[0],
|
||||||
|
"scheme_name": row[1],
|
||||||
|
"scheme_type": row[2],
|
||||||
|
"username": row[3],
|
||||||
|
"create_time": row[4],
|
||||||
|
"scheme_start_time": row[5],
|
||||||
|
"scheme_detail": detail,
|
||||||
|
"network": detail.get("network"),
|
||||||
|
"result_payload": detail.get("result_payload", {}),
|
||||||
|
}, name, scheme_type)
|
||||||
|
|
||||||
|
|
||||||
def store_leakage_identify_result(
|
def store_leakage_identify_result(
|
||||||
name: str,
|
name: str,
|
||||||
scheme_name: str,
|
scheme_name: str,
|
||||||
|
|||||||
@@ -0,0 +1,271 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from io import BytesIO
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from openpyxl import Workbook
|
||||||
|
from openpyxl.styles import Alignment, Font, PatternFill
|
||||||
|
from openpyxl.worksheet.worksheet import Worksheet
|
||||||
|
from openpyxl.utils import get_column_letter
|
||||||
|
from pyproj import Transformer
|
||||||
|
|
||||||
|
from app.native import wndb
|
||||||
|
|
||||||
|
|
||||||
|
class SensorPlacementNotFoundError(LookupError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SensorPlacementValidationError(ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SensorPlacementConflictError(RuntimeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
_to_wgs84 = Transformer.from_crs("EPSG:3857", "EPSG:4326", always_xy=True)
|
||||||
|
_STATUS_LABELS = {
|
||||||
|
"current": "当前方案",
|
||||||
|
"original": "原方案",
|
||||||
|
"added": "新增",
|
||||||
|
"replaced": "替换",
|
||||||
|
}
|
||||||
|
_COORDINATE_DESCRIPTION = (
|
||||||
|
"工程 X/Y: 项目地方坐标系;地图 X/Y: EPSG:3857;经纬度: WGS84"
|
||||||
|
)
|
||||||
|
_LIST_HEADERS = (
|
||||||
|
"序号",
|
||||||
|
"节点 ID",
|
||||||
|
"经度",
|
||||||
|
"纬度",
|
||||||
|
"工程 X",
|
||||||
|
"工程 Y",
|
||||||
|
"地图 X",
|
||||||
|
"地图 Y",
|
||||||
|
"高程",
|
||||||
|
"调整状态",
|
||||||
|
)
|
||||||
|
_LIST_COLUMN_WIDTHS = (8, 20, 16, 16, 18, 18, 18, 18, 14, 14)
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_locations(sensor_location: list[str]) -> list[str]:
|
||||||
|
normalized = [str(node_id).strip() for node_id in sensor_location]
|
||||||
|
if not normalized or any(not node_id for node_id in normalized):
|
||||||
|
raise SensorPlacementValidationError("监测点列表不能为空")
|
||||||
|
if len(set(normalized)) != len(normalized):
|
||||||
|
raise SensorPlacementValidationError("监测点列表不能包含重复节点")
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
|
def _sensor_points(
|
||||||
|
network: str,
|
||||||
|
sensor_location: list[str],
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
nodes = wndb.get_sensor_placement_nodes(network, sensor_location)
|
||||||
|
by_id = {str(node["node_id"]): node for node in nodes}
|
||||||
|
missing = [node_id for node_id in sensor_location if node_id not in by_id]
|
||||||
|
if missing:
|
||||||
|
raise SensorPlacementValidationError(
|
||||||
|
f"以下节点不存在或不是 junction: {', '.join(missing)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
points: list[dict[str, Any]] = []
|
||||||
|
for node_id in sensor_location:
|
||||||
|
node = by_id[node_id]
|
||||||
|
project_x = float(node["project_x"])
|
||||||
|
project_y = float(node["project_y"])
|
||||||
|
map_x = float(node["map_x"])
|
||||||
|
map_y = float(node["map_y"])
|
||||||
|
longitude, latitude = _to_wgs84.transform(map_x, map_y)
|
||||||
|
points.append(
|
||||||
|
{
|
||||||
|
"node_id": node_id,
|
||||||
|
"max_pipe_diameter": (
|
||||||
|
float(node["max_pipe_diameter"])
|
||||||
|
if node["max_pipe_diameter"] is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
"project_x": project_x,
|
||||||
|
"project_y": project_y,
|
||||||
|
"map_x": map_x,
|
||||||
|
"map_y": map_y,
|
||||||
|
"longitude": float(longitude),
|
||||||
|
"latitude": float(latitude),
|
||||||
|
"elevation": float(node["elevation"]),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return points
|
||||||
|
|
||||||
|
|
||||||
|
def get_sensor_placement_candidate(
|
||||||
|
network: str,
|
||||||
|
node_id: str,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Return the authoritative editable point data for one junction."""
|
||||||
|
|
||||||
|
return _sensor_points(network, _normalize_locations([node_id]))[0]
|
||||||
|
|
||||||
|
|
||||||
|
def validate_sensor_placement_nodes(
|
||||||
|
network: str,
|
||||||
|
sensor_location: list[str],
|
||||||
|
) -> None:
|
||||||
|
_sensor_points(network, _normalize_locations(sensor_location))
|
||||||
|
|
||||||
|
|
||||||
|
def get_sensor_placement_scheme(network: str, scheme_id: int) -> dict[str, Any]:
|
||||||
|
scheme = wndb.get_sensor_placement(network, scheme_id)
|
||||||
|
if scheme is None:
|
||||||
|
raise SensorPlacementNotFoundError("监测点方案不存在")
|
||||||
|
|
||||||
|
locations = [str(item) for item in (scheme.get("sensor_location") or [])]
|
||||||
|
return {
|
||||||
|
**scheme,
|
||||||
|
"sensor_number": len(locations),
|
||||||
|
"sensor_location": locations,
|
||||||
|
"sensor_points": _sensor_points(network, locations),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def update_sensor_placement_scheme(
|
||||||
|
network: str,
|
||||||
|
scheme_id: int,
|
||||||
|
*,
|
||||||
|
expected_sensor_location: list[str],
|
||||||
|
sensor_location: list[str],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
expected = _normalize_locations(expected_sensor_location)
|
||||||
|
next_locations = _normalize_locations(sensor_location)
|
||||||
|
_sensor_points(network, next_locations)
|
||||||
|
|
||||||
|
updated = wndb.update_sensor_placement(
|
||||||
|
network,
|
||||||
|
scheme_id,
|
||||||
|
expected_sensor_location=expected,
|
||||||
|
sensor_location=next_locations,
|
||||||
|
)
|
||||||
|
if updated is None:
|
||||||
|
if wndb.get_sensor_placement(network, scheme_id) is None:
|
||||||
|
raise SensorPlacementNotFoundError("监测点方案不存在")
|
||||||
|
raise SensorPlacementConflictError("方案已被其他用户修改,请重新加载")
|
||||||
|
return get_sensor_placement_scheme(network, scheme_id)
|
||||||
|
|
||||||
|
|
||||||
|
def can_edit_sensor_placement(user: Any, scheme: dict[str, Any]) -> bool:
|
||||||
|
return bool(
|
||||||
|
getattr(user, "is_superuser", False)
|
||||||
|
or getattr(user, "role", None) == "admin"
|
||||||
|
or getattr(user, "username", None) == scheme.get("username")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_excel_text(value: Any) -> str:
|
||||||
|
text = "" if value is None else str(value)
|
||||||
|
if text.startswith(("=", "+", "-", "@")):
|
||||||
|
return f"'{text}"
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def _populate_info_sheet(
|
||||||
|
sheet: Worksheet,
|
||||||
|
*,
|
||||||
|
network: str,
|
||||||
|
scheme: dict[str, Any],
|
||||||
|
location_count: int,
|
||||||
|
is_draft: bool,
|
||||||
|
) -> None:
|
||||||
|
created_at = scheme["create_time"]
|
||||||
|
if isinstance(created_at, datetime):
|
||||||
|
created_at = created_at.isoformat(timespec="minutes")
|
||||||
|
|
||||||
|
rows = [
|
||||||
|
("项目", network),
|
||||||
|
("方案名称", scheme["scheme_name"]),
|
||||||
|
("监测点数量", location_count),
|
||||||
|
("最小管径", scheme["min_diameter"]),
|
||||||
|
("创建人", scheme["username"]),
|
||||||
|
("创建时间", created_at),
|
||||||
|
("导出时间", datetime.now().astimezone().isoformat(timespec="minutes")),
|
||||||
|
("文档状态", "未保存草稿" if is_draft else "当前方案"),
|
||||||
|
("坐标说明", _COORDINATE_DESCRIPTION),
|
||||||
|
]
|
||||||
|
for row_index, (label, value) in enumerate(rows, start=1):
|
||||||
|
sheet.cell(row=row_index, column=1, value=label)
|
||||||
|
safe_value = _safe_excel_text(value) if isinstance(value, str) else value
|
||||||
|
sheet.cell(row=row_index, column=2, value=safe_value)
|
||||||
|
sheet.column_dimensions["A"].width = 18
|
||||||
|
sheet.column_dimensions["B"].width = 64
|
||||||
|
|
||||||
|
|
||||||
|
def _populate_list_sheet(
|
||||||
|
sheet: Worksheet,
|
||||||
|
*,
|
||||||
|
points: list[dict[str, Any]],
|
||||||
|
adjustment_status: dict[str, str],
|
||||||
|
) -> None:
|
||||||
|
sheet.append(_LIST_HEADERS)
|
||||||
|
for index, point in enumerate(points, start=1):
|
||||||
|
status = adjustment_status.get(point["node_id"], "current")
|
||||||
|
sheet.append(
|
||||||
|
[
|
||||||
|
index,
|
||||||
|
_safe_excel_text(point["node_id"]),
|
||||||
|
point["longitude"],
|
||||||
|
point["latitude"],
|
||||||
|
point["project_x"],
|
||||||
|
point["project_y"],
|
||||||
|
point["map_x"],
|
||||||
|
point["map_y"],
|
||||||
|
point["elevation"],
|
||||||
|
_STATUS_LABELS.get(status, "当前方案"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
header_fill = PatternFill("solid", fgColor="257DD4")
|
||||||
|
for cell in sheet[1]:
|
||||||
|
cell.fill = header_fill
|
||||||
|
cell.font = Font(color="FFFFFF", bold=True)
|
||||||
|
cell.alignment = Alignment(horizontal="center", vertical="center")
|
||||||
|
sheet.freeze_panes = "A2"
|
||||||
|
sheet.auto_filter.ref = sheet.dimensions
|
||||||
|
for index, width in enumerate(_LIST_COLUMN_WIDTHS, start=1):
|
||||||
|
sheet.column_dimensions[get_column_letter(index)].width = width
|
||||||
|
for row in sheet.iter_rows(min_row=2):
|
||||||
|
row[0].alignment = Alignment(horizontal="center")
|
||||||
|
for cell in row[2:9]:
|
||||||
|
cell.number_format = "0.000000"
|
||||||
|
|
||||||
|
|
||||||
|
def build_sensor_placement_workbook(
|
||||||
|
*,
|
||||||
|
network: str,
|
||||||
|
scheme: dict[str, Any],
|
||||||
|
sensor_location: list[str],
|
||||||
|
adjustment_status: dict[str, str],
|
||||||
|
) -> BytesIO:
|
||||||
|
locations = _normalize_locations(sensor_location)
|
||||||
|
points = _sensor_points(network, locations)
|
||||||
|
is_draft = locations != list(scheme["sensor_location"])
|
||||||
|
|
||||||
|
workbook = Workbook()
|
||||||
|
info_sheet = workbook.active
|
||||||
|
info_sheet.title = "方案信息"
|
||||||
|
_populate_info_sheet(
|
||||||
|
info_sheet,
|
||||||
|
network=network,
|
||||||
|
scheme=scheme,
|
||||||
|
location_count=len(locations),
|
||||||
|
is_draft=is_draft,
|
||||||
|
)
|
||||||
|
|
||||||
|
list_sheet = workbook.create_sheet("监测点清单")
|
||||||
|
_populate_list_sheet(
|
||||||
|
list_sheet,
|
||||||
|
points=points,
|
||||||
|
adjustment_status=adjustment_status,
|
||||||
|
)
|
||||||
|
|
||||||
|
output = BytesIO()
|
||||||
|
workbook.save(output)
|
||||||
|
output.seek(0)
|
||||||
|
return output
|
||||||
+28
-18
@@ -1290,19 +1290,6 @@ def get_scada_info(name: str, id: str) -> dict[str, Any]:
|
|||||||
def get_all_scada_info(name: str) -> list[dict[str, Any]]:
|
def get_all_scada_info(name: str) -> list[dict[str, Any]]:
|
||||||
return api.get_all_scada_info(name)
|
return api.get_all_scada_info(name)
|
||||||
|
|
||||||
# DingZQ 2025-03-27
|
|
||||||
############################################################
|
|
||||||
# 39 users
|
|
||||||
############################################################
|
|
||||||
def get_user_schema(name: str) -> dict[str, dict[str, Any]]:
|
|
||||||
return api.get_user_schema(name)
|
|
||||||
|
|
||||||
def get_user(name: str, user_name: str) -> dict[str, Any]:
|
|
||||||
return api.get_user(name, user_name=user_name)
|
|
||||||
|
|
||||||
def get_all_users(name: str) -> list[dict[str, Any]]:
|
|
||||||
return api.get_all_users(name)
|
|
||||||
|
|
||||||
############################################################
|
############################################################
|
||||||
# scheme 40
|
# scheme 40
|
||||||
############################################################
|
############################################################
|
||||||
@@ -1312,8 +1299,34 @@ def get_scheme_schema(name: str) -> dict[str, dict[str, Any]]:
|
|||||||
def get_scheme(name: str, schema_name: str) -> dict[str, Any]:
|
def get_scheme(name: str, schema_name: str) -> dict[str, Any]:
|
||||||
return api.get_scheme(name, schema_name)
|
return api.get_scheme(name, schema_name)
|
||||||
|
|
||||||
def get_all_schemes(name: str) -> list[dict[str, Any]]:
|
def get_all_schemes(
|
||||||
return api.get_all_schemes(name)
|
name: str,
|
||||||
|
scheme_type: str | None = None,
|
||||||
|
query_date: Any | None = None,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
if scheme_type is None and query_date is None:
|
||||||
|
return api.get_all_schemes(name)
|
||||||
|
|
||||||
|
from app.services.scheme_management import query_scheme_list
|
||||||
|
|
||||||
|
rows = query_scheme_list(name, scheme_type=scheme_type, query_date=query_date) or []
|
||||||
|
columns = [
|
||||||
|
"scheme_id",
|
||||||
|
"scheme_name",
|
||||||
|
"scheme_type",
|
||||||
|
"username",
|
||||||
|
"create_time",
|
||||||
|
"scheme_start_time",
|
||||||
|
"scheme_detail",
|
||||||
|
]
|
||||||
|
result = []
|
||||||
|
for row in rows:
|
||||||
|
item = dict(zip(columns, row, strict=False))
|
||||||
|
detail = item.get("scheme_detail")
|
||||||
|
if isinstance(detail, dict) and detail.get("network") not in (None, name):
|
||||||
|
continue
|
||||||
|
result.append(item)
|
||||||
|
return result
|
||||||
|
|
||||||
############################################################
|
############################################################
|
||||||
# pipe_risk_probability 41
|
# pipe_risk_probability 41
|
||||||
@@ -1344,6 +1357,3 @@ def get_all_sensor_placements(name: str) -> list[dict[Any, Any]]:
|
|||||||
############################################################
|
############################################################
|
||||||
def get_all_burst_locate_results(name: str) -> list[dict[Any, Any]]:
|
def get_all_burst_locate_results(name: str) -> list[dict[Any, Any]]:
|
||||||
return api.get_all_burst_locate_results(name)
|
return api.get_all_burst_locate_results(name)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"contract_version": "1.0.0",
|
||||||
|
"contracts": {
|
||||||
|
"server": {
|
||||||
|
"file": "server-v1.openapi.json",
|
||||||
|
"sha256": "b003a57c9b8c1a041b644363ef58afb8bfcede1fe076ce48a190d2a1ac915e3f"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
|||||||
|
# TJWater REST API v1
|
||||||
|
|
||||||
|
This contract is the public API contract for coordinated TJWater
|
||||||
|
Server, Agent, Frontend, and CLI releases.
|
||||||
|
|
||||||
|
- Paths use lowercase kebab-case, have no trailing slash, and identify
|
||||||
|
resources rather than handler actions.
|
||||||
|
- JSON fields, query parameters, and path parameter names use snake_case.
|
||||||
|
- Project-scoped requests use `X-Project-Id`; `network` query parameters
|
||||||
|
are not part of the public contract.
|
||||||
|
- `GET` is read-only. Synchronous analysis and simulation requests use
|
||||||
|
`POST` and clients must not retry them automatically.
|
||||||
|
- JSON errors use `application/problem+json`.
|
||||||
|
- The static OpenAPI file and `contracts/manifest.json` are release
|
||||||
|
artifacts even when production runtime documentation is disabled.
|
||||||
|
|
||||||
|
Generate and validate the contract with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
conda run -n server python scripts/export_openapi.py
|
||||||
|
conda run -n server python scripts/check_openapi.py
|
||||||
|
```
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
# Keycloak 登录主题
|
||||||
|
|
||||||
|
`themes/tjwater` 是“供水管网水力模型及人工智能系统”的 Keycloak 登录主题。
|
||||||
|
主题继承 `keycloak.v2`,只覆盖样式、消息和本地 SVG 资源,不修改认证模板或
|
||||||
|
认证流程。
|
||||||
|
|
||||||
|
## 在管理控制台切换登录主题
|
||||||
|
|
||||||
|
确认 `themes/tjwater` 已挂载到容器的
|
||||||
|
`/opt/keycloak/themes/tjwater`,然后按以下步骤切换:
|
||||||
|
|
||||||
|
1. 打开 Keycloak 管理控制台:
|
||||||
|
`http://<Keycloak 地址>:<端口>/admin/`。
|
||||||
|
2. 使用管理员账号登录。
|
||||||
|
3. 在左上角选择需要应用主题的 realm,例如 `tjwater`。不要停留在
|
||||||
|
`master`,除非确实要修改 `master` realm。
|
||||||
|
4. 在左侧菜单进入 `Realm settings`,打开 `Themes` 标签页。
|
||||||
|
5. 在 `Login theme` 下拉框中选择 `tjwater`。
|
||||||
|
6. 点击 `Save` 保存。
|
||||||
|
7. 继续检查业务客户端是否单独指定了登录主题,再从业务前端重新进入登录页。
|
||||||
|
|
||||||
|
切回 Keycloak 默认登录页时,将 `Login theme` 改为 `keycloak` 并保存。
|
||||||
|
|
||||||
|
### 检查业务客户端的主题配置
|
||||||
|
|
||||||
|
Keycloak 的 realm 和 client 都可以设置登录主题。client 的配置优先于 realm。
|
||||||
|
因此,即使 `Realm settings > Themes > Login theme` 已选择 `tjwater`,业务
|
||||||
|
客户端如果仍指定 `keycloak`,从业务系统跳转后看到的还是默认登录页。
|
||||||
|
|
||||||
|
以授权地址中包含 `client_id=tjwater` 的业务系统为例:
|
||||||
|
|
||||||
|
1. 确认左上角当前 realm 是 `tjwater`。
|
||||||
|
2. 在左侧菜单进入 `Clients`。
|
||||||
|
3. 打开 `Client ID` 为 `tjwater` 的客户端。
|
||||||
|
4. 在 `Settings` 页面找到 `Login settings > Login theme`。
|
||||||
|
5. 将该字段设置为以下任一选项:
|
||||||
|
- `Choose...`:不在 client 层指定主题,继承 realm 的 `tjwater` 主题,
|
||||||
|
推荐使用此方式。
|
||||||
|
- `tjwater`:在 client 层明确指定 `tjwater` 主题。
|
||||||
|
6. 不要保留 `keycloak`,否则它会覆盖 realm 的主题。
|
||||||
|
7. 点击 `Save`,关闭旧登录页,再从业务前端重新发起一次登录。
|
||||||
|
|
||||||
|
`Choose...` 不是未配置完成,而是表示当前 client 继承 realm 配置。管理控制台
|
||||||
|
登录、账户中心和业务系统可能使用不同的 client。某一个入口已经显示
|
||||||
|
`tjwater` 主题,并不能证明业务 client 也已正确配置。
|
||||||
|
|
||||||
|
验证时以业务系统实际生成的 OpenID Connect 授权地址为准,并检查其中的
|
||||||
|
`client_id`。浏览器加载的主题资源路径应包含
|
||||||
|
`/resources/<版本>/login/tjwater/`;如果路径仍包含
|
||||||
|
`/resources/<版本>/login/keycloak/`,说明该 client 仍在使用默认主题。
|
||||||
|
|
||||||
|
如果 `Login theme` 下拉框中没有 `tjwater`,先检查容器内的主题文件:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose \
|
||||||
|
--env-file .env \
|
||||||
|
-f infra/docker/docker-compose.yml \
|
||||||
|
exec -T keycloak \
|
||||||
|
test -f /opt/keycloak/themes/tjwater/login/theme.properties
|
||||||
|
```
|
||||||
|
|
||||||
|
命令成功但控制台仍未显示主题时,重新创建 Keycloak 容器后再检查:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose \
|
||||||
|
--env-file .env \
|
||||||
|
-f infra/docker/docker-compose.yml \
|
||||||
|
up -d --force-recreate keycloak
|
||||||
|
```
|
||||||
|
|
||||||
|
主题名称已经正确,但页面仍显示旧样式时,也执行上述命令,并在容器启动后使用
|
||||||
|
`Ctrl+F5` 强制刷新登录页,避免继续使用浏览器缓存的 CSS。
|
||||||
|
|
||||||
|
## 启用
|
||||||
|
|
||||||
|
先启动 `infra/docker/docker-compose.yml` 中的 Keycloak,再从仓库根目录执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash infra/docker/keycloak/configure-theme.sh apply
|
||||||
|
```
|
||||||
|
|
||||||
|
脚本默认配置 `tjwater` realm、简体中文默认语言、中英文切换和
|
||||||
|
`供水管网水力模型及人工智能系统` 品牌名,并清除 `tjwater` client 对登录
|
||||||
|
主题的覆盖,使其继承 realm 主题。其他客户环境可临时覆盖:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TJWATER_KEYCLOAK_REALM=example \
|
||||||
|
TJWATER_KEYCLOAK_CLIENT_ID=example-web \
|
||||||
|
TJWATER_KEYCLOAK_DISPLAY_NAME="客户供水智能系统" \
|
||||||
|
bash infra/docker/keycloak/configure-theme.sh apply
|
||||||
|
```
|
||||||
|
|
||||||
|
管理员凭据继续使用 Compose 已注入的 `KC_BOOTSTRAP_ADMIN_USERNAME` /
|
||||||
|
`KC_BOOTSTRAP_ADMIN_PASSWORD`,并兼容现有的 `KEYCLOAK_ADMIN` /
|
||||||
|
`KEYCLOAK_ADMIN_PASSWORD`。
|
||||||
|
|
||||||
|
## 验证与回滚
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash infra/docker/keycloak/configure-theme.sh verify
|
||||||
|
bash infra/docker/keycloak/configure-theme.sh rollback
|
||||||
|
```
|
||||||
|
|
||||||
|
使用 `latest` 镜像时,每次重新拉取 Keycloak 后都应重新执行 `verify`,并在
|
||||||
|
1280px、375px 和 320px 视口检查登录、错误提示、忘记密码和 OTP 页面。
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
action="${1:-apply}"
|
||||||
|
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
repo_root="$(cd -- "${script_dir}/../../.." && pwd)"
|
||||||
|
compose_file="${repo_root}/infra/docker/docker-compose.yml"
|
||||||
|
realm="${TJWATER_KEYCLOAK_REALM:-tjwater}"
|
||||||
|
client_id="${TJWATER_KEYCLOAK_CLIENT_ID:-tjwater}"
|
||||||
|
display_name="${TJWATER_KEYCLOAK_DISPLAY_NAME:-供水管网水力模型及人工智能系统}"
|
||||||
|
|
||||||
|
case "${action}" in
|
||||||
|
apply|verify|rollback) ;;
|
||||||
|
*)
|
||||||
|
echo "用法: bash infra/docker/keycloak/configure-theme.sh [apply|verify|rollback]" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
compose_args=(docker compose)
|
||||||
|
if [[ -f "${repo_root}/.env" ]]; then
|
||||||
|
compose_args+=(--env-file "${repo_root}/.env")
|
||||||
|
fi
|
||||||
|
compose_args+=(-f "${compose_file}")
|
||||||
|
|
||||||
|
"${compose_args[@]}" exec -T \
|
||||||
|
-e TJWATER_KEYCLOAK_ACTION="${action}" \
|
||||||
|
-e TJWATER_KEYCLOAK_REALM="${realm}" \
|
||||||
|
-e TJWATER_KEYCLOAK_CLIENT_ID="${client_id}" \
|
||||||
|
-e TJWATER_KEYCLOAK_DISPLAY_NAME="${display_name}" \
|
||||||
|
keycloak sh -s <<'KEYCLOAK_SCRIPT'
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
action="${TJWATER_KEYCLOAK_ACTION}"
|
||||||
|
realm="${TJWATER_KEYCLOAK_REALM}"
|
||||||
|
client_id="${TJWATER_KEYCLOAK_CLIENT_ID}"
|
||||||
|
display_name="${TJWATER_KEYCLOAK_DISPLAY_NAME}"
|
||||||
|
server_url="${TJWATER_KEYCLOAK_SERVER_URL:-http://127.0.0.1:8080}"
|
||||||
|
admin_user="${KC_BOOTSTRAP_ADMIN_USERNAME:-${KEYCLOAK_ADMIN:-}}"
|
||||||
|
admin_password="${KC_BOOTSTRAP_ADMIN_PASSWORD:-${KEYCLOAK_ADMIN_PASSWORD:-}}"
|
||||||
|
config_file="/tmp/tjwater-kcadm-$$.config"
|
||||||
|
kcadm="/opt/keycloak/bin/kcadm.sh"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
rm -f "${config_file}"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
if [ -z "${admin_user}" ] || [ -z "${admin_password}" ]; then
|
||||||
|
echo "缺少 Keycloak 管理员用户名或密码环境变量。" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${action}" = "apply" ] && [ ! -f /opt/keycloak/themes/tjwater/login/theme.properties ]; then
|
||||||
|
echo "未找到 tjwater 登录主题,请检查主题目录挂载。" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
"${kcadm}" config credentials \
|
||||||
|
--config "${config_file}" \
|
||||||
|
--server "${server_url}" \
|
||||||
|
--realm master \
|
||||||
|
--user "${admin_user}" \
|
||||||
|
--password "${admin_password}" >/dev/null
|
||||||
|
|
||||||
|
client_uuid="$(
|
||||||
|
"${kcadm}" get clients \
|
||||||
|
--config "${config_file}" \
|
||||||
|
--target-realm "${realm}" \
|
||||||
|
--query "clientId=${client_id}" \
|
||||||
|
--fields id \
|
||||||
|
--format csv \
|
||||||
|
--noquotes |
|
||||||
|
sed -n '1p'
|
||||||
|
)"
|
||||||
|
|
||||||
|
if [ -z "${client_uuid}" ]; then
|
||||||
|
echo "realm ${realm} 中未找到 client ${client_id}。" >&2
|
||||||
|
echo "可通过 TJWATER_KEYCLOAK_CLIENT_ID 指定实际的 client ID。" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "${action}" in
|
||||||
|
apply)
|
||||||
|
"${kcadm}" update "realms/${realm}" \
|
||||||
|
--config "${config_file}" \
|
||||||
|
-s "displayName=${display_name}" \
|
||||||
|
-s "displayNameHtml=${display_name}" \
|
||||||
|
-s "loginTheme=tjwater" \
|
||||||
|
-s "internationalizationEnabled=true" \
|
||||||
|
-s 'supportedLocales=["zh-CN","en"]' \
|
||||||
|
-s "defaultLocale=zh-CN" >/dev/null
|
||||||
|
"${kcadm}" update "clients/${client_uuid}" \
|
||||||
|
--config "${config_file}" \
|
||||||
|
--target-realm "${realm}" \
|
||||||
|
--set attributes.login_theme= >/dev/null
|
||||||
|
echo "已为 realm ${realm} 启用 tjwater 登录主题。"
|
||||||
|
echo "client ${client_id} 已改为继承 realm 登录主题。"
|
||||||
|
;;
|
||||||
|
rollback)
|
||||||
|
"${kcadm}" update "realms/${realm}" \
|
||||||
|
--config "${config_file}" \
|
||||||
|
-s "loginTheme=keycloak" >/dev/null
|
||||||
|
echo "已将 realm ${realm} 恢复为 Keycloak 默认登录主题。"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
"${kcadm}" get "realms/${realm}" \
|
||||||
|
--config "${config_file}" \
|
||||||
|
--fields realm,displayName,loginTheme,internationalizationEnabled,supportedLocales,defaultLocale
|
||||||
|
"${kcadm}" get "clients/${client_uuid}" \
|
||||||
|
--config "${config_file}" \
|
||||||
|
--target-realm "${realm}" \
|
||||||
|
--fields 'clientId,attributes(login_theme)'
|
||||||
|
KEYCLOAK_SCRIPT
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<#import "template.ftl" as layout>
|
||||||
|
<#assign isLogout = message.summary == msg("successLogout")>
|
||||||
|
<@layout.registrationLayout displayMessage=false; section>
|
||||||
|
<#if section = "header">
|
||||||
|
<#if isLogout>
|
||||||
|
${kcSanitize(msg("tjwaterLogoutTitle"))?no_esc}
|
||||||
|
<#elseif messageHeader??>
|
||||||
|
${kcSanitize(msg("${messageHeader}"))?no_esc}
|
||||||
|
<#else>
|
||||||
|
${message.summary}
|
||||||
|
</#if>
|
||||||
|
<#elseif section = "form">
|
||||||
|
<#if isLogout>
|
||||||
|
<div id="kc-info-message" class="tjwater-logout-message" role="status">
|
||||||
|
<div class="tjwater-logout-mark" aria-hidden="true">
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
<div class="tjwater-logout-copy">
|
||||||
|
<p class="tjwater-logout-eyebrow">${kcSanitize(msg("tjwaterLogoutEyebrow"))?no_esc}</p>
|
||||||
|
<p class="tjwater-logout-description">${kcSanitize(msg("tjwaterLogoutDescription"))?no_esc}</p>
|
||||||
|
</div>
|
||||||
|
<#if pageRedirectUri?has_content>
|
||||||
|
<a class="pf-v5-c-button pf-m-primary tjwater-logout-action" href="${pageRedirectUri}">${kcSanitize(msg("tjwaterReturnToApplication"))?no_esc}</a>
|
||||||
|
<#elseif (client.baseUrl)?has_content>
|
||||||
|
<a class="pf-v5-c-button pf-m-primary tjwater-logout-action" href="${client.baseUrl}">${kcSanitize(msg("tjwaterReturnToApplication"))?no_esc}</a>
|
||||||
|
</#if>
|
||||||
|
</div>
|
||||||
|
<#else>
|
||||||
|
<div id="kc-info-message">
|
||||||
|
<p class="instruction">${message.summary}<#if requiredActions??><#list requiredActions>: <b><#items as reqActionItem>${kcSanitize(msg("requiredAction.${reqActionItem}"))?no_esc}<#sep>, </#items></b></#list><#else></#if></p>
|
||||||
|
<#if skipLink??>
|
||||||
|
<#else>
|
||||||
|
<#if pageRedirectUri?has_content>
|
||||||
|
<p><a href="${pageRedirectUri}">${kcSanitize(msg("backToApplication"))?no_esc}</a></p>
|
||||||
|
<#elseif actionUri?has_content>
|
||||||
|
<p><a href="${actionUri}">${kcSanitize(msg("proceedWithAction"))?no_esc}</a></p>
|
||||||
|
<#elseif (client.baseUrl)?has_content>
|
||||||
|
<p><a href="${client.baseUrl}">${kcSanitize(msg("backToApplication"))?no_esc}</a></p>
|
||||||
|
</#if>
|
||||||
|
</#if>
|
||||||
|
</div>
|
||||||
|
</#if>
|
||||||
|
</#if>
|
||||||
|
</@layout.registrationLayout>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
loginAccountTitle=Account sign in
|
||||||
|
doLogIn=Sign in
|
||||||
|
doForgotPassword=Forgot password
|
||||||
|
tjwaterLogoutTitle=Signed out securely
|
||||||
|
tjwaterLogoutEyebrow=Your session has ended
|
||||||
|
tjwaterLogoutDescription=Your platform and identity-provider sessions have been ended securely.
|
||||||
|
tjwaterReturnToApplication=Return to sign in
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
loginAccountTitle=账号登录
|
||||||
|
usernameOrEmail=用户名或邮箱
|
||||||
|
doLogIn=登录
|
||||||
|
doForgotPassword=忘记密码
|
||||||
|
rememberMe=记住我
|
||||||
|
invalidUserMessage=用户名或密码错误
|
||||||
|
invalidUsernameOrPasswordMessage=用户名或密码错误
|
||||||
|
expiredCodeMessage=登录已超时,请重新登录
|
||||||
|
loginTimeout=登录已超时,请重新开始登录
|
||||||
|
tjwaterLogoutTitle=已安全退出
|
||||||
|
tjwaterLogoutEyebrow=会话已结束
|
||||||
|
tjwaterLogoutDescription=您的平台与身份认证会话均已安全结束。
|
||||||
|
tjwaterReturnToApplication=返回登录页
|
||||||
@@ -0,0 +1,589 @@
|
|||||||
|
:root {
|
||||||
|
--tjwater-canvas: oklch(0.965 0.014 205);
|
||||||
|
--tjwater-surface: oklch(0.995 0.004 205);
|
||||||
|
--tjwater-surface-soft: oklch(0.982 0.008 205);
|
||||||
|
--tjwater-ink: oklch(0.3 0.055 215);
|
||||||
|
--tjwater-muted: oklch(0.52 0.035 215);
|
||||||
|
--tjwater-line: oklch(0.86 0.025 210);
|
||||||
|
--tjwater-blue: oklch(0.57 0.16 242);
|
||||||
|
--tjwater-blue-dark: oklch(0.49 0.15 242);
|
||||||
|
--tjwater-teal: oklch(0.58 0.12 180);
|
||||||
|
--tjwater-danger: oklch(0.55 0.19 27);
|
||||||
|
--tjwater-radius-sm: 6px;
|
||||||
|
--tjwater-radius-md: 12px;
|
||||||
|
--tjwater-radius-lg: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.login-pf {
|
||||||
|
height: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
background: var(--tjwater-canvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
body#keycloak-bg,
|
||||||
|
.login-pf body {
|
||||||
|
min-height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
color: var(--tjwater-ink);
|
||||||
|
background: var(--tjwater-canvas);
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI",
|
||||||
|
"PingFang SC", "Microsoft YaHei", "Noto Sans SC", sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-login,
|
||||||
|
.pf-v5-c-login * {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-login {
|
||||||
|
min-height: 100svh;
|
||||||
|
padding: 0;
|
||||||
|
background-color: var(--tjwater-canvas);
|
||||||
|
background-image:
|
||||||
|
linear-gradient(
|
||||||
|
90deg,
|
||||||
|
transparent 0%,
|
||||||
|
transparent 50%,
|
||||||
|
oklch(0.975 0.01 205 / 62%) 68%,
|
||||||
|
oklch(0.975 0.01 205 / 82%) 100%
|
||||||
|
),
|
||||||
|
url("../img/network-blueprint.svg");
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-login__container {
|
||||||
|
display: grid;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1720px;
|
||||||
|
min-height: 100svh;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: clamp(32px, 4.5vw, 76px) clamp(40px, 5vw, 88px);
|
||||||
|
grid-template-columns: minmax(360px, 1fr) minmax(400px, 460px);
|
||||||
|
grid-template-areas: "header main";
|
||||||
|
align-items: center;
|
||||||
|
gap: clamp(64px, 8vw, 152px);
|
||||||
|
}
|
||||||
|
|
||||||
|
#kc-header {
|
||||||
|
position: relative;
|
||||||
|
z-index: 0;
|
||||||
|
grid-area: header;
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 100%;
|
||||||
|
align-self: center;
|
||||||
|
justify-self: start;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
isolation: isolate;
|
||||||
|
animation: tjwater-enter 480ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
#kc-header::before {
|
||||||
|
position: absolute;
|
||||||
|
z-index: -1;
|
||||||
|
inset: -54px -72px;
|
||||||
|
background: radial-gradient(
|
||||||
|
ellipse at center,
|
||||||
|
oklch(0.925 0.018 205 / 98%) 0%,
|
||||||
|
oklch(0.925 0.018 205 / 94%) 48%,
|
||||||
|
oklch(0.925 0.018 205 / 62%) 65%,
|
||||||
|
transparent 82%
|
||||||
|
);
|
||||||
|
pointer-events: none;
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
#kc-header-wrapper {
|
||||||
|
display: flex;
|
||||||
|
max-width: 680px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
color: var(--tjwater-ink) !important;
|
||||||
|
font-size: clamp(34px, 3vw, 46px);
|
||||||
|
font-weight: 720;
|
||||||
|
line-height: 1.28;
|
||||||
|
letter-spacing: 0;
|
||||||
|
text-align: left;
|
||||||
|
text-transform: none;
|
||||||
|
text-wrap: balance;
|
||||||
|
}
|
||||||
|
|
||||||
|
#kc-header-wrapper::before {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
background: url("../img/logo-mark.svg") center / contain no-repeat;
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
#kc-header-wrapper::after {
|
||||||
|
width: 64px;
|
||||||
|
height: 3px;
|
||||||
|
margin-top: 26px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--tjwater-teal);
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-login__main {
|
||||||
|
grid-area: main;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 460px;
|
||||||
|
margin: 0;
|
||||||
|
align-self: center;
|
||||||
|
justify-self: stretch;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 0;
|
||||||
|
border-radius: var(--tjwater-radius-lg);
|
||||||
|
background: oklch(0.995 0.004 205 / 97%);
|
||||||
|
box-shadow:
|
||||||
|
0 32px 80px rgb(22 65 75 / 16%),
|
||||||
|
0 5px 18px rgb(22 65 75 / 9%);
|
||||||
|
animation: tjwater-enter 520ms 70ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-login__main-header {
|
||||||
|
display: grid;
|
||||||
|
margin: 0;
|
||||||
|
padding: 36px 36px 18px;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: center;
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#kc-page-title {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--tjwater-ink);
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 720;
|
||||||
|
line-height: 1.4;
|
||||||
|
letter-spacing: 0;
|
||||||
|
text-wrap: balance;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-login__main-header-utilities {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-login__main-body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 36px 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-form {
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-form__group {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-form__group-label {
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-form__label-text {
|
||||||
|
color: var(--tjwater-ink);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 650;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-form-control {
|
||||||
|
min-height: 48px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--tjwater-line);
|
||||||
|
border-radius: var(--tjwater-radius-sm);
|
||||||
|
background: var(--tjwater-surface-soft);
|
||||||
|
box-shadow: none;
|
||||||
|
transition-property: border-color, box-shadow, background-color;
|
||||||
|
transition-duration: 160ms;
|
||||||
|
transition-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-form-control::before,
|
||||||
|
.pf-v5-c-form-control::after {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-form-control:focus-within {
|
||||||
|
border-color: var(--tjwater-blue);
|
||||||
|
background: var(--tjwater-surface);
|
||||||
|
box-shadow: 0 0 0 3px oklch(0.78 0.1 235 / 28%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-form-control > input,
|
||||||
|
.pf-v5-c-form-control > select {
|
||||||
|
min-height: 46px;
|
||||||
|
padding-inline: 14px;
|
||||||
|
color: var(--tjwater-ink);
|
||||||
|
font-size: 16px;
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-login__main-header-utilities .pf-v5-c-form-control {
|
||||||
|
width: 116px;
|
||||||
|
min-height: 40px;
|
||||||
|
background: var(--tjwater-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
#login-select-toggle {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 38px;
|
||||||
|
padding-inline: 12px 32px;
|
||||||
|
color: var(--tjwater-muted);
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-form-control.pf-m-error {
|
||||||
|
border-color: var(--tjwater-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-input-group {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-input-group__item.pf-m-fill {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-button.pf-m-control {
|
||||||
|
min-width: 48px;
|
||||||
|
min-height: 48px;
|
||||||
|
border: 1px solid var(--tjwater-line);
|
||||||
|
border-radius: var(--tjwater-radius-sm);
|
||||||
|
color: var(--tjwater-muted);
|
||||||
|
background: var(--tjwater-surface-soft);
|
||||||
|
touch-action: manipulation;
|
||||||
|
transition-property: color, border-color, background-color, transform;
|
||||||
|
transition-duration: 160ms;
|
||||||
|
transition-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-button.pf-m-control:active {
|
||||||
|
transform: scale(0.96);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-button.pf-m-control:focus-visible,
|
||||||
|
.pf-v5-c-button.pf-m-primary:focus-visible,
|
||||||
|
.pf-v5-c-button.pf-m-secondary:focus-visible,
|
||||||
|
a:focus-visible {
|
||||||
|
outline: 3px solid oklch(0.74 0.12 235 / 60%);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-form__helper-text {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-helper-text {
|
||||||
|
min-height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-helper-text__item-text {
|
||||||
|
color: var(--tjwater-muted);
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-helper-text__item-text a,
|
||||||
|
#kc-registration a,
|
||||||
|
.pf-v5-c-login__main-footer a {
|
||||||
|
color: var(--tjwater-blue-dark);
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
text-underline-offset: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kc-feedback-text.pf-m-error,
|
||||||
|
.pf-v5-c-helper-text__item.pf-m-error .kc-feedback-text {
|
||||||
|
color: var(--tjwater-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-check__input {
|
||||||
|
accent-color: var(--tjwater-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-check__label {
|
||||||
|
color: var(--tjwater-muted);
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-form__actions {
|
||||||
|
padding-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-button.pf-m-primary {
|
||||||
|
min-height: 48px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: var(--tjwater-radius-md);
|
||||||
|
color: oklch(0.99 0.004 230);
|
||||||
|
background: var(--tjwater-blue);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
box-shadow: 0 8px 18px oklch(0.48 0.15 242 / 20%);
|
||||||
|
touch-action: manipulation;
|
||||||
|
transition-property: transform, background-color, box-shadow;
|
||||||
|
transition-duration: 160ms;
|
||||||
|
transition-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-button.pf-m-primary:active {
|
||||||
|
transform: scale(0.96);
|
||||||
|
background: var(--tjwater-blue-dark);
|
||||||
|
box-shadow: 0 4px 10px oklch(0.48 0.15 242 / 18%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-button.pf-m-secondary {
|
||||||
|
min-height: 44px;
|
||||||
|
border-radius: var(--tjwater-radius-md);
|
||||||
|
color: var(--tjwater-blue-dark);
|
||||||
|
border-color: var(--tjwater-line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-alert {
|
||||||
|
border-radius: var(--tjwater-radius-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-login__main-footer {
|
||||||
|
color: var(--tjwater-muted);
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-login__main-footer-band {
|
||||||
|
margin-top: 26px;
|
||||||
|
padding: 18px 0 0;
|
||||||
|
border-top: 1px solid var(--tjwater-line);
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tjwater-logout-message {
|
||||||
|
display: grid;
|
||||||
|
gap: 20px;
|
||||||
|
padding-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tjwater-logout-mark {
|
||||||
|
display: grid;
|
||||||
|
width: 52px;
|
||||||
|
height: 52px;
|
||||||
|
border: 1px solid oklch(0.76 0.09 184 / 52%);
|
||||||
|
border-radius: 50%;
|
||||||
|
background: oklch(0.92 0.04 184 / 58%);
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tjwater-logout-mark span {
|
||||||
|
width: 18px;
|
||||||
|
height: 10px;
|
||||||
|
border-bottom: 3px solid var(--tjwater-teal);
|
||||||
|
border-left: 3px solid var(--tjwater-teal);
|
||||||
|
transform: translateY(-2px) rotate(-45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tjwater-logout-copy {
|
||||||
|
display: grid;
|
||||||
|
gap: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tjwater-logout-eyebrow {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--tjwater-ink);
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tjwater-logout-description {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--tjwater-muted);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tjwater-logout-action {
|
||||||
|
display: inline-flex;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 48px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes tjwater-enter {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(12px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
.pf-v5-c-button.pf-m-primary:hover {
|
||||||
|
background: var(--tjwater-blue-dark);
|
||||||
|
box-shadow: 0 10px 22px oklch(0.48 0.15 242 / 25%);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-button.pf-m-control:hover {
|
||||||
|
color: var(--tjwater-blue-dark);
|
||||||
|
border-color: oklch(0.69 0.08 230);
|
||||||
|
background: var(--tjwater-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-helper-text__item-text a:hover,
|
||||||
|
#kc-registration a:hover,
|
||||||
|
.pf-v5-c-login__main-footer a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.pf-v5-c-login {
|
||||||
|
background-image:
|
||||||
|
linear-gradient(oklch(0.965 0.014 205 / 34%), oklch(0.965 0.014 205 / 34%)),
|
||||||
|
url("../img/network-blueprint.svg");
|
||||||
|
background-position: 34% center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-login__container {
|
||||||
|
max-width: 560px;
|
||||||
|
padding:
|
||||||
|
max(28px, env(safe-area-inset-top))
|
||||||
|
max(24px, env(safe-area-inset-right))
|
||||||
|
max(32px, env(safe-area-inset-bottom))
|
||||||
|
max(24px, env(safe-area-inset-left));
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
grid-template-areas:
|
||||||
|
"header"
|
||||||
|
"main";
|
||||||
|
align-content: center;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#kc-header-wrapper {
|
||||||
|
max-width: none;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
font-size: clamp(22px, 5vw, 28px);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
#kc-header::before {
|
||||||
|
inset: -24px -20px;
|
||||||
|
background: radial-gradient(
|
||||||
|
ellipse at center,
|
||||||
|
oklch(0.965 0.014 205 / 98%) 0%,
|
||||||
|
oklch(0.965 0.014 205 / 88%) 58%,
|
||||||
|
transparent 84%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#kc-header-wrapper::before {
|
||||||
|
width: 46px;
|
||||||
|
height: 46px;
|
||||||
|
margin: 0;
|
||||||
|
flex: 0 0 46px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#kc-header-wrapper::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-login__main {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 520px) {
|
||||||
|
.pf-v5-c-login__container {
|
||||||
|
gap: 18px;
|
||||||
|
padding-inline:
|
||||||
|
max(14px, env(safe-area-inset-left))
|
||||||
|
max(14px, env(safe-area-inset-right));
|
||||||
|
}
|
||||||
|
|
||||||
|
#kc-header-wrapper {
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 21px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#kc-header-wrapper::before {
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
flex-basis: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-login__main {
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-login__main-header {
|
||||||
|
gap: 12px;
|
||||||
|
padding: 26px 22px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#kc-page-title {
|
||||||
|
font-size: 23px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-login__main-header-utilities .pf-v5-c-form-control {
|
||||||
|
width: 108px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-login__main-body {
|
||||||
|
padding: 0 22px 28px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-height: 680px) and (min-width: 901px) {
|
||||||
|
.pf-v5-c-login__container {
|
||||||
|
padding-block: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#kc-header-wrapper::before {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#kc-header-wrapper::after {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-login__main-header {
|
||||||
|
padding-top: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-login__main-body {
|
||||||
|
padding-bottom: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
#kc-header,
|
||||||
|
.pf-v5-c-login__main {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-v5-c-button,
|
||||||
|
.pf-v5-c-form-control {
|
||||||
|
transition-duration: 0.01ms;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72" role="img" aria-labelledby="title">
|
||||||
|
<title id="title">TJWater</title>
|
||||||
|
<rect width="72" height="72" rx="18" fill="#1478d4"/>
|
||||||
|
<path d="M36 13c-7.8 11.1-16.1 19.4-16.1 29.3A16.1 16.1 0 0 0 36 58.4a16.1 16.1 0 0 0 16.1-16.1C52.1 32.4 43.8 24.1 36 13Z" fill="#f5fbfc"/>
|
||||||
|
<path d="M26.5 43.5h19M31 36.5l5 7 5-7" fill="none" stroke="#0b8f82" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/>
|
||||||
|
<circle cx="26.5" cy="43.5" r="2.7" fill="#0b8f82"/>
|
||||||
|
<circle cx="45.5" cy="43.5" r="2.7" fill="#0b8f82"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 585 B |
@@ -0,0 +1,39 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 900" preserveAspectRatio="xMidYMid slice">
|
||||||
|
<rect width="1440" height="900" fill="#edf5f5"/>
|
||||||
|
<path d="M0 0h790L650 900H0Z" fill="#dceced"/>
|
||||||
|
<path d="M0 182c187-78 303-73 451-16 153 59 288 47 409-25M0 552c193-45 327-19 461 74 128 90 266 100 410 47" fill="none" stroke="#c8dddd" stroke-width="2"/>
|
||||||
|
<g fill="none" stroke="#a9ccce" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M-32 725 178 608l142 51 155-190 184 67 176-205" stroke-width="5"/>
|
||||||
|
<path d="m178 608 25-222 151-92 121 175" stroke-width="3"/>
|
||||||
|
<path d="m203 386-92-95 55-161M354 294l92-141 152 58 98-106" stroke-width="3"/>
|
||||||
|
<path d="m320 659-4 132 171 81M659 536l88 116 124-42" stroke-width="3"/>
|
||||||
|
</g>
|
||||||
|
<g fill="#edf5f5" stroke="#1478d4" stroke-width="4">
|
||||||
|
<circle cx="178" cy="608" r="10"/>
|
||||||
|
<circle cx="203" cy="386" r="9"/>
|
||||||
|
<circle cx="354" cy="294" r="9"/>
|
||||||
|
<circle cx="475" cy="469" r="11"/>
|
||||||
|
<circle cx="659" cy="536" r="10"/>
|
||||||
|
<circle cx="747" cy="652" r="9"/>
|
||||||
|
<circle cx="320" cy="659" r="8"/>
|
||||||
|
</g>
|
||||||
|
<g fill="#0b8f82">
|
||||||
|
<circle cx="111" cy="291" r="6"/>
|
||||||
|
<circle cx="166" cy="130" r="6"/>
|
||||||
|
<circle cx="446" cy="153" r="7"/>
|
||||||
|
<circle cx="598" cy="211" r="6"/>
|
||||||
|
<circle cx="696" cy="105" r="6"/>
|
||||||
|
<circle cx="316" cy="791" r="6"/>
|
||||||
|
<circle cx="487" cy="872" r="6"/>
|
||||||
|
<circle cx="835" cy="331" r="7"/>
|
||||||
|
</g>
|
||||||
|
<g fill="none" stroke="#86b8bb" stroke-width="2" opacity=".72">
|
||||||
|
<circle cx="615" cy="448" r="222"/>
|
||||||
|
<circle cx="615" cy="448" r="276"/>
|
||||||
|
<circle cx="615" cy="448" r="334"/>
|
||||||
|
</g>
|
||||||
|
<g fill="#1478d4" opacity=".08">
|
||||||
|
<rect x="40" y="40" width="118" height="10" rx="5"/>
|
||||||
|
<rect x="40" y="62" width="72" height="6" rx="3"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,21 @@
|
|||||||
|
const localizeLocaleOptions = () => {
|
||||||
|
const localeSelect = document.querySelector("#login-select-toggle");
|
||||||
|
|
||||||
|
if (!(localeSelect instanceof HTMLSelectElement)) return;
|
||||||
|
|
||||||
|
for (const option of localeSelect.options) {
|
||||||
|
const optionUrl = new URL(option.value, window.location.origin);
|
||||||
|
const locale = optionUrl.searchParams.get("kc_locale");
|
||||||
|
|
||||||
|
if (locale === "zh-CN") option.textContent = "简体中文";
|
||||||
|
if (locale === "en") option.textContent = "English";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (document.readyState === "loading") {
|
||||||
|
document.addEventListener("DOMContentLoaded", localizeLocaleOptions, {
|
||||||
|
once: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
localizeLocaleOptions();
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
parent=keycloak.v2
|
||||||
|
import=common/keycloak
|
||||||
|
|
||||||
|
styles=css/styles.css css/tjwater-login.css
|
||||||
|
scripts=js/locale-labels.js
|
||||||
|
locales=zh-CN,en
|
||||||
|
darkMode=false
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
-- Metadata auth management schema patch.
|
|
||||||
-- Keycloak owns login credentials; TJWater stores only business identity and access.
|
|
||||||
|
|
||||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
|
||||||
|
|
||||||
DO $$
|
|
||||||
DECLARE
|
|
||||||
users_id_type text;
|
|
||||||
BEGIN
|
|
||||||
SELECT data_type INTO users_id_type
|
|
||||||
FROM information_schema.columns
|
|
||||||
WHERE table_schema = 'public'
|
|
||||||
AND table_name = 'users'
|
|
||||||
AND column_name = 'id';
|
|
||||||
|
|
||||||
IF users_id_type IS NULL THEN
|
|
||||||
CREATE TABLE users (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
keycloak_id UUID UNIQUE NOT NULL,
|
|
||||||
username VARCHAR(50) UNIQUE NOT NULL,
|
|
||||||
email VARCHAR(100) UNIQUE NOT NULL,
|
|
||||||
role VARCHAR(20) DEFAULT 'user' NOT NULL,
|
|
||||||
is_active BOOLEAN DEFAULT TRUE NOT NULL,
|
|
||||||
is_superuser BOOLEAN DEFAULT FALSE NOT NULL,
|
|
||||||
attributes JSONB,
|
|
||||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
|
||||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
|
||||||
last_login_at TIMESTAMP WITH TIME ZONE
|
|
||||||
);
|
|
||||||
ELSIF users_id_type <> 'uuid' THEN
|
|
||||||
RAISE EXCEPTION
|
|
||||||
'Existing public.users.id is %, not uuid. Export old local users, create Keycloak accounts, then migrate to metadata UUID users before applying this patch.',
|
|
||||||
users_id_type;
|
|
||||||
END IF;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
ALTER TABLE users
|
|
||||||
ADD COLUMN IF NOT EXISTS keycloak_id UUID,
|
|
||||||
ADD COLUMN IF NOT EXISTS attributes JSONB,
|
|
||||||
ADD COLUMN IF NOT EXISTS last_login_at TIMESTAMP WITH TIME ZONE;
|
|
||||||
|
|
||||||
ALTER TABLE users
|
|
||||||
ALTER COLUMN role SET DEFAULT 'user';
|
|
||||||
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_keycloak_id ON users(keycloak_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_users_role ON users(role);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_users_is_active ON users(is_active);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS user_project_membership (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
user_id UUID NOT NULL,
|
|
||||||
project_id UUID NOT NULL,
|
|
||||||
project_role VARCHAR(20) DEFAULT 'viewer' NOT NULL,
|
|
||||||
CONSTRAINT user_project_membership_role_check
|
|
||||||
CHECK (project_role IN ('owner', 'admin', 'member', 'viewer')),
|
|
||||||
CONSTRAINT user_project_membership_unique UNIQUE (user_id, project_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_user_project_membership_user_id
|
|
||||||
ON user_project_membership(user_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_user_project_membership_project_id
|
|
||||||
ON user_project_membership(project_id);
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
-- Metadata project configuration schema patch.
|
|
||||||
-- Admin APIs write these tables; operators should not hand-edit encrypted values.
|
|
||||||
|
|
||||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
|
||||||
RETURNS TRIGGER AS $$
|
|
||||||
BEGIN
|
|
||||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
|
||||||
RETURN NEW;
|
|
||||||
END;
|
|
||||||
$$ LANGUAGE plpgsql;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS projects (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
name VARCHAR(100) NOT NULL,
|
|
||||||
code VARCHAR(50) UNIQUE NOT NULL,
|
|
||||||
description TEXT,
|
|
||||||
gs_workspace VARCHAR(100) UNIQUE NOT NULL,
|
|
||||||
map_extent JSONB,
|
|
||||||
status VARCHAR(20) DEFAULT 'active' NOT NULL,
|
|
||||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
|
||||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
|
||||||
CONSTRAINT projects_status_check CHECK (status IN ('active', 'inactive', 'archived'))
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_projects_status ON projects(status);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_projects_code ON projects(code);
|
|
||||||
|
|
||||||
DROP TRIGGER IF EXISTS update_projects_updated_at ON projects;
|
|
||||||
CREATE TRIGGER update_projects_updated_at
|
|
||||||
BEFORE UPDATE ON projects
|
|
||||||
FOR EACH ROW
|
|
||||||
EXECUTE FUNCTION update_updated_at_column();
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS project_databases (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
||||||
db_role VARCHAR(20) NOT NULL,
|
|
||||||
db_type VARCHAR(20) NOT NULL,
|
|
||||||
dsn_encrypted TEXT NOT NULL,
|
|
||||||
pool_min_size INTEGER DEFAULT 2 NOT NULL,
|
|
||||||
pool_max_size INTEGER DEFAULT 10 NOT NULL,
|
|
||||||
CONSTRAINT project_databases_unique_role UNIQUE (project_id, db_role),
|
|
||||||
CONSTRAINT project_databases_role_check CHECK (db_role IN ('biz_data', 'iot_data')),
|
|
||||||
CONSTRAINT project_databases_type_check CHECK (db_type IN ('postgresql', 'timescaledb')),
|
|
||||||
CONSTRAINT project_databases_pool_check CHECK (
|
|
||||||
pool_min_size >= 1 AND pool_max_size >= pool_min_size
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_project_databases_project_id
|
|
||||||
ON project_databases(project_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_project_databases_role
|
|
||||||
ON project_databases(db_role);
|
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||||
|
|
||||||
|
HTTP_METHODS = {"get", "post", "put", "patch", "delete", "head", "options"}
|
||||||
|
KEBAB_SEGMENT = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
|
||||||
|
SNAKE_PARAMETER = re.compile(r"^[a-z][a-z0-9_]*$")
|
||||||
|
|
||||||
|
|
||||||
|
def canonical_json(document: dict[str, Any]) -> bytes:
|
||||||
|
return (
|
||||||
|
json.dumps(document, ensure_ascii=False, indent=2, sort_keys=True).encode("utf-8")
|
||||||
|
+ b"\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def current_contract_bytes() -> bytes:
|
||||||
|
os.environ.setdefault("ENVIRONMENT", "development")
|
||||||
|
from app.main import app
|
||||||
|
|
||||||
|
document = app.openapi()
|
||||||
|
document["info"]["version"] = "1.0.0"
|
||||||
|
return canonical_json(document)
|
||||||
|
|
||||||
|
|
||||||
|
def _iter_operations(document: dict[str, Any]):
|
||||||
|
for path, path_item in document.get("paths", {}).items():
|
||||||
|
for method, operation in path_item.items():
|
||||||
|
if method in HTTP_METHODS and isinstance(operation, dict):
|
||||||
|
yield path, method, operation
|
||||||
|
|
||||||
|
|
||||||
|
def validate(document: dict[str, Any]) -> list[str]:
|
||||||
|
errors: list[str] = []
|
||||||
|
operation_ids: set[str] = set()
|
||||||
|
|
||||||
|
for path, method, operation in _iter_operations(document):
|
||||||
|
if path != path.rstrip("/"):
|
||||||
|
errors.append(f"{method.upper()} {path}: trailing slash")
|
||||||
|
if "//" in path:
|
||||||
|
errors.append(f"{method.upper()} {path}: double slash")
|
||||||
|
for segment in path.split("/"):
|
||||||
|
if not segment or (segment.startswith("{") and segment.endswith("}")):
|
||||||
|
continue
|
||||||
|
if not KEBAB_SEGMENT.fullmatch(segment):
|
||||||
|
errors.append(f"{method.upper()} {path}: non-kebab segment {segment!r}")
|
||||||
|
|
||||||
|
operation_id = operation.get("operationId")
|
||||||
|
if not operation_id:
|
||||||
|
errors.append(f"{method.upper()} {path}: missing operationId")
|
||||||
|
elif operation_id in operation_ids:
|
||||||
|
errors.append(f"{method.upper()} {path}: duplicate operationId {operation_id}")
|
||||||
|
else:
|
||||||
|
operation_ids.add(operation_id)
|
||||||
|
|
||||||
|
if not operation.get("tags"):
|
||||||
|
errors.append(f"{method.upper()} {path}: missing tags")
|
||||||
|
if not operation.get("summary"):
|
||||||
|
errors.append(f"{method.upper()} {path}: missing summary")
|
||||||
|
for parameter in operation.get("parameters", []):
|
||||||
|
if (
|
||||||
|
parameter.get("in") in {"query", "path"}
|
||||||
|
and not SNAKE_PARAMETER.fullmatch(str(parameter.get("name", "")))
|
||||||
|
):
|
||||||
|
errors.append(
|
||||||
|
f"{method.upper()} {path}: non-snake parameter "
|
||||||
|
f"{parameter.get('name')!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
success_responses = [
|
||||||
|
(status, response)
|
||||||
|
for status, response in operation.get("responses", {}).items()
|
||||||
|
if str(status).startswith("2")
|
||||||
|
]
|
||||||
|
if not success_responses:
|
||||||
|
errors.append(f"{method.upper()} {path}: missing success response")
|
||||||
|
for status, response in success_responses:
|
||||||
|
if str(status) == "204":
|
||||||
|
continue
|
||||||
|
if "content" not in response:
|
||||||
|
errors.append(f"{method.upper()} {path}: success response has no content schema")
|
||||||
|
for media in response.get("content", {}).values():
|
||||||
|
if media.get("schema") == {}:
|
||||||
|
errors.append(f"{method.upper()} {path}: empty success schema")
|
||||||
|
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description="Validate TJWater REST OpenAPI invariants")
|
||||||
|
parser.add_argument(
|
||||||
|
"contract",
|
||||||
|
nargs="?",
|
||||||
|
type=Path,
|
||||||
|
default=Path("contracts/server-v1.openapi.json"),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--manifest",
|
||||||
|
type=Path,
|
||||||
|
default=Path("contracts/manifest.json"),
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
raw = args.contract.read_bytes()
|
||||||
|
document = json.loads(raw)
|
||||||
|
errors = validate(document)
|
||||||
|
|
||||||
|
manifest = json.loads(args.manifest.read_text(encoding="utf-8"))
|
||||||
|
expected_hash = manifest["contracts"]["server"]["sha256"]
|
||||||
|
actual_hash = hashlib.sha256(raw).hexdigest()
|
||||||
|
if expected_hash != actual_hash:
|
||||||
|
errors.append(
|
||||||
|
f"contract hash mismatch: manifest={expected_hash}, actual={actual_hash}"
|
||||||
|
)
|
||||||
|
current = current_contract_bytes()
|
||||||
|
if raw != current:
|
||||||
|
errors.append(
|
||||||
|
"contract is stale: run "
|
||||||
|
"`python scripts/export_openapi.py` and commit the regenerated files"
|
||||||
|
)
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
print("\n".join(f"- {error}" for error in errors))
|
||||||
|
return 1
|
||||||
|
print(
|
||||||
|
f"validated {len(document['paths'])} paths; "
|
||||||
|
f"sha256={actual_hash}; version={document['info']['version']}"
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def _canonical_json(document: dict[str, Any]) -> bytes:
|
||||||
|
return (
|
||||||
|
json.dumps(document, ensure_ascii=False, indent=2, sort_keys=True).encode("utf-8")
|
||||||
|
+ b"\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description="Export the TJWater REST v1 OpenAPI contract")
|
||||||
|
parser.add_argument(
|
||||||
|
"--output",
|
||||||
|
type=Path,
|
||||||
|
default=Path("contracts/server-v1.openapi.json"),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--manifest",
|
||||||
|
type=Path,
|
||||||
|
default=Path("contracts/manifest.json"),
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
os.environ.setdefault("ENVIRONMENT", "development")
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||||
|
|
||||||
|
from app.main import app
|
||||||
|
|
||||||
|
document = app.openapi()
|
||||||
|
document["info"]["version"] = "1.0.0"
|
||||||
|
payload = _canonical_json(document)
|
||||||
|
digest = hashlib.sha256(payload).hexdigest()
|
||||||
|
|
||||||
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
args.manifest.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
args.output.write_bytes(payload)
|
||||||
|
args.manifest.write_bytes(
|
||||||
|
_canonical_json(
|
||||||
|
{
|
||||||
|
"contract_version": "1.0.0",
|
||||||
|
"contracts": {
|
||||||
|
"server": {
|
||||||
|
"file": args.output.name,
|
||||||
|
"sha256": digest,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
print(f"exported {len(document['paths'])} paths to {args.output} ({digest})")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import argparse
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||||
|
from app.main import app
|
||||||
|
|
||||||
|
|
||||||
|
HTTP_METHODS = ("get", "post", "put", "patch", "delete", "options", "head")
|
||||||
|
|
||||||
|
|
||||||
|
def _parameter_line(parameter: dict) -> str:
|
||||||
|
required = "必填" if parameter.get("required") else "可选"
|
||||||
|
description = parameter.get("description")
|
||||||
|
suffix = f" {description}" if description else ""
|
||||||
|
return (
|
||||||
|
f" - `{parameter.get('name', '-')}` "
|
||||||
|
f"({parameter.get('in', '-')}, {required}){suffix}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_markdown() -> str:
|
||||||
|
schema = app.openapi()
|
||||||
|
info = schema.get("info", {})
|
||||||
|
lines = [
|
||||||
|
"# 后端 API 文档",
|
||||||
|
"",
|
||||||
|
"本文档根据后端服务 OpenAPI 配置生成,用于查看接口路径、方法和用途。",
|
||||||
|
"",
|
||||||
|
"## 基本信息",
|
||||||
|
"",
|
||||||
|
f"- 标题:{info.get('title', '')}",
|
||||||
|
f"- 版本:{info.get('version', '')}",
|
||||||
|
f"- 描述:{info.get('description', '')}",
|
||||||
|
"",
|
||||||
|
"## 接口列表",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
|
||||||
|
for path in sorted(schema.get("paths", {})):
|
||||||
|
path_item = schema["paths"][path]
|
||||||
|
for method in HTTP_METHODS:
|
||||||
|
operation = path_item.get(method)
|
||||||
|
if not operation:
|
||||||
|
continue
|
||||||
|
lines.extend(
|
||||||
|
[
|
||||||
|
f"### `{method.upper()} {path}`",
|
||||||
|
"",
|
||||||
|
f"- 分组:{', '.join(operation.get('tags', [])) or '-'}",
|
||||||
|
f"- 说明:{operation.get('summary', '')}",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
if operation.get("description"):
|
||||||
|
lines.append(f"- 详情:{operation['description'].strip()}")
|
||||||
|
parameters = operation.get("parameters", [])
|
||||||
|
if parameters:
|
||||||
|
lines.append("- 参数:")
|
||||||
|
lines.extend(_parameter_line(parameter) for parameter in parameters)
|
||||||
|
if operation.get("requestBody"):
|
||||||
|
lines.append("- 请求体:需要")
|
||||||
|
response_codes = ", ".join(operation.get("responses", {}).keys())
|
||||||
|
lines.extend([f"- 响应状态:{response_codes}", ""])
|
||||||
|
|
||||||
|
return "\n".join(lines).rstrip() + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("outputs", nargs="+", type=Path)
|
||||||
|
args = parser.parse_args()
|
||||||
|
content = build_markdown()
|
||||||
|
for output in args.outputs:
|
||||||
|
output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
output.write_text(content, encoding="utf-8")
|
||||||
|
print(output)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Build metadata user sync payloads from an old-user to Keycloak mapping CSV."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import csv
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
REQUIRED_COLUMNS = {"keycloak_id", "username", "email"}
|
|
||||||
|
|
||||||
|
|
||||||
def parse_bool(value: str | None) -> bool:
|
|
||||||
if value is None or value == "":
|
|
||||||
return True
|
|
||||||
return value.strip().lower() not in {"0", "false", "no", "n", "disabled"}
|
|
||||||
|
|
||||||
|
|
||||||
def build_payload(mapping_csv: Path) -> dict:
|
|
||||||
with mapping_csv.open(newline="", encoding="utf-8") as handle:
|
|
||||||
reader = csv.DictReader(handle)
|
|
||||||
missing = REQUIRED_COLUMNS.difference(reader.fieldnames or [])
|
|
||||||
if missing:
|
|
||||||
raise SystemExit(f"missing required CSV columns: {', '.join(sorted(missing))}")
|
|
||||||
|
|
||||||
users = []
|
|
||||||
for row in reader:
|
|
||||||
users.append(
|
|
||||||
{
|
|
||||||
"keycloak_id": row["keycloak_id"].strip(),
|
|
||||||
"username": row["username"].strip(),
|
|
||||||
"email": row["email"].strip(),
|
|
||||||
"role": (row.get("role") or "user").strip().lower(),
|
|
||||||
"is_active": parse_bool(row.get("is_active")),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return {"users": users}
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description=(
|
|
||||||
"Convert old local user mappings into a JSON body for "
|
|
||||||
"POST /api/v1/admin/users/sync/batch. Passwords are never migrated."
|
|
||||||
)
|
|
||||||
)
|
|
||||||
parser.add_argument("mapping_csv", type=Path)
|
|
||||||
parser.add_argument("-o", "--output", type=Path)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
payload = build_payload(args.mapping_csv)
|
|
||||||
content = json.dumps(payload, ensure_ascii=False, indent=2)
|
|
||||||
if args.output:
|
|
||||||
args.output.write_text(content + "\n", encoding="utf-8")
|
|
||||||
else:
|
|
||||||
print(content)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
from types import SimpleNamespace
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from app.api.v1.endpoints import access as access_endpoint
|
||||||
|
from app.auth.metadata_dependencies import (
|
||||||
|
get_current_metadata_user,
|
||||||
|
get_metadata_repository,
|
||||||
|
)
|
||||||
|
from tests.conftest import build_test_app
|
||||||
|
|
||||||
|
|
||||||
|
def _user(**overrides):
|
||||||
|
data = {
|
||||||
|
"id": uuid4(),
|
||||||
|
"username": "alice",
|
||||||
|
"role": "user",
|
||||||
|
"is_superuser": False,
|
||||||
|
}
|
||||||
|
data.update(overrides)
|
||||||
|
return SimpleNamespace(**data)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_client(user, repo) -> TestClient:
|
||||||
|
app = build_test_app(access_endpoint.router, "/api/v1")
|
||||||
|
app.dependency_overrides[get_current_metadata_user] = lambda: user
|
||||||
|
app.dependency_overrides[get_metadata_repository] = lambda: repo
|
||||||
|
return TestClient(app)
|
||||||
|
|
||||||
|
|
||||||
|
def test_access_context_returns_global_admin_permissions_without_project():
|
||||||
|
client = _build_client(_user(role="admin"), SimpleNamespace())
|
||||||
|
|
||||||
|
response = client.get("/api/v1/access-context")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
payload = response.json()
|
||||||
|
assert payload["is_system_admin"] is True
|
||||||
|
assert payload["project_id"] is None
|
||||||
|
assert "environment.manage" in payload["permissions"]
|
||||||
|
assert "webgis.view" not in payload["permissions"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_access_context_returns_project_member_permissions():
|
||||||
|
project_id = uuid4()
|
||||||
|
user = _user()
|
||||||
|
|
||||||
|
async def get_project_by_id(value):
|
||||||
|
assert value == project_id
|
||||||
|
return SimpleNamespace(id=project_id, code="demo", status="active")
|
||||||
|
|
||||||
|
async def get_membership_role(value, user_id):
|
||||||
|
assert value == project_id
|
||||||
|
assert user_id == user.id
|
||||||
|
return "member"
|
||||||
|
|
||||||
|
repo = SimpleNamespace(
|
||||||
|
get_project_by_id=get_project_by_id,
|
||||||
|
get_membership_role=get_membership_role,
|
||||||
|
)
|
||||||
|
client = _build_client(user, repo)
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
"/api/v1/access-context",
|
||||||
|
headers={"X-Project-Id": str(project_id)},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
payload = response.json()
|
||||||
|
assert payload["project_id"] == str(project_id)
|
||||||
|
assert payload["project_role"] == "member"
|
||||||
|
assert "scada.clean" in payload["permissions"]
|
||||||
|
assert "optimization.run" in payload["permissions"]
|
||||||
|
assert "model.import" not in payload["permissions"]
|
||||||
@@ -48,9 +48,9 @@ def test_router_configuration():
|
|||||||
routes = [r.path for r in api_router.routes if hasattr(r, "path")]
|
routes = [r.path for r in api_router.routes if hasattr(r, "path")]
|
||||||
|
|
||||||
# 验证基础路径是否存在
|
# 验证基础路径是否存在
|
||||||
assert any("/agent/auth/context" in r for r in routes), "缺少 Agent 认证上下文路由"
|
assert "/agent-auth-context" in routes, "缺少 Agent 认证上下文路由"
|
||||||
assert any("/admin/me" in r for r in routes), "缺少元数据管理认证路由"
|
assert "/admin/users/me" in routes, "缺少元数据管理认证路由"
|
||||||
assert any("/audit" in r for r in routes), "缺少审计日志路由 (/audit)"
|
assert "/audit-logs" in routes, "缺少审计日志路由"
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
pytest.fail(f"路由配置检查失败: {e}")
|
pytest.fail(f"路由配置检查失败: {e}")
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
from io import BytesIO
|
||||||
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.responses import StreamingResponse
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from app.infra.audit import middleware as audit_middleware
|
||||||
|
from app.infra.audit.middleware import AuditMiddleware
|
||||||
|
|
||||||
|
|
||||||
|
def test_post_streaming_response_survives_audit_body_capture(monkeypatch):
|
||||||
|
log_audit_event = AsyncMock()
|
||||||
|
monkeypatch.setattr(audit_middleware, "log_audit_event", log_audit_event)
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
app.add_middleware(AuditMiddleware)
|
||||||
|
|
||||||
|
@app.post("/exports")
|
||||||
|
async def export(payload: dict[str, str]) -> StreamingResponse:
|
||||||
|
assert payload == {"format": "xlsx"}
|
||||||
|
return StreamingResponse(
|
||||||
|
BytesIO(b"xlsx"),
|
||||||
|
media_type=(
|
||||||
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
response = TestClient(app, raise_server_exceptions=False).post(
|
||||||
|
"/exports",
|
||||||
|
json={"format": "xlsx"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.content == b"xlsx"
|
||||||
|
log_audit_event.assert_awaited_once()
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from app.api.v1.endpoints import burst_detection as burst_detection_endpoint
|
||||||
|
|
||||||
|
|
||||||
|
def _build_client() -> TestClient:
|
||||||
|
app = FastAPI()
|
||||||
|
app.include_router(burst_detection_endpoint.router, prefix="/api/v1")
|
||||||
|
app.dependency_overrides[
|
||||||
|
burst_detection_endpoint.get_current_keycloak_username
|
||||||
|
] = lambda: "tester"
|
||||||
|
return TestClient(app)
|
||||||
|
|
||||||
|
|
||||||
|
def test_detect_burst_forwards_target_mode_fields(monkeypatch):
|
||||||
|
captured = {}
|
||||||
|
|
||||||
|
def fake_run_burst_detection(**kwargs):
|
||||||
|
captured.update(kwargs)
|
||||||
|
return {
|
||||||
|
"network": kwargs["network"],
|
||||||
|
"sensor_nodes": [],
|
||||||
|
"observed_source": "historical_monitoring",
|
||||||
|
"sample_count": 0,
|
||||||
|
"points_per_day": 96,
|
||||||
|
"day_count": 0,
|
||||||
|
"rows": [],
|
||||||
|
"summary": {"burst_detected": False},
|
||||||
|
}
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
burst_detection_endpoint, "run_burst_detection", fake_run_burst_detection
|
||||||
|
)
|
||||||
|
|
||||||
|
response = _build_client().post(
|
||||||
|
"/api/v1/burst-detections",
|
||||||
|
json={
|
||||||
|
"network": "demo",
|
||||||
|
"scheme_name": "burst-history",
|
||||||
|
"target_time": "2026-06-20T13:30:00+08:00",
|
||||||
|
"sampling_interval_minutes": 15,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert captured["username"] == "tester"
|
||||||
|
assert captured["network"] == "demo"
|
||||||
|
assert captured["scheme_name"] == "burst-history"
|
||||||
|
assert captured["target_time"].isoformat() == "2026-06-20T13:30:00+08:00"
|
||||||
|
assert captured["sampling_interval_minutes"] == 15
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
from types import SimpleNamespace
|
|
||||||
|
|
||||||
from app.api.v1.endpoints import leakage as leakage_endpoint
|
from app.api.v1.endpoints import leakage as leakage_endpoint
|
||||||
|
|
||||||
|
|
||||||
def _build_client() -> TestClient:
|
def _build_client() -> TestClient:
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
app.include_router(leakage_endpoint.router, prefix="/api/v1/leakage")
|
app.include_router(leakage_endpoint.router, prefix="/api/v1")
|
||||||
app.dependency_overrides[leakage_endpoint.get_current_keycloak_username] = (
|
app.dependency_overrides[leakage_endpoint.get_current_keycloak_username] = (
|
||||||
lambda: "tester"
|
lambda: "tester"
|
||||||
)
|
)
|
||||||
@@ -25,7 +23,7 @@ def test_identify_leakage_success(monkeypatch):
|
|||||||
)
|
)
|
||||||
client = _build_client()
|
client = _build_client()
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/api/v1/leakage/identify/",
|
"/api/v1/leakage-identifications",
|
||||||
json={
|
json={
|
||||||
"network": "demo",
|
"network": "demo",
|
||||||
"scada_start": "2026-01-01T00:00:00+08:00",
|
"scada_start": "2026-01-01T00:00:00+08:00",
|
||||||
@@ -35,34 +33,3 @@ def test_identify_leakage_success(monkeypatch):
|
|||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.json()["area_count"] == 0
|
assert response.json()["area_count"] == 0
|
||||||
|
|
||||||
|
|
||||||
def test_query_leakage_schemes_success(monkeypatch):
|
|
||||||
monkeypatch.setattr(
|
|
||||||
leakage_endpoint,
|
|
||||||
"list_leakage_identify_schemes",
|
|
||||||
lambda network, query_date=None: [
|
|
||||||
{"scheme_name": "dma_001", "scheme_type": "dma_leak_identification"}
|
|
||||||
],
|
|
||||||
)
|
|
||||||
client = _build_client()
|
|
||||||
response = client.get("/api/v1/leakage/schemes/", params={"network": "demo"})
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert response.json()[0]["scheme_name"] == "dma_001"
|
|
||||||
|
|
||||||
|
|
||||||
def test_query_leakage_scheme_detail_success(monkeypatch):
|
|
||||||
monkeypatch.setattr(
|
|
||||||
leakage_endpoint,
|
|
||||||
"get_leakage_identify_scheme_detail",
|
|
||||||
lambda network, scheme_name: {
|
|
||||||
"scheme_name": scheme_name,
|
|
||||||
"rows": [{"Area": "1", "LeakageFlow_m3_per_s": 0.1}],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
client = _build_client()
|
|
||||||
response = client.get(
|
|
||||||
"/api/v1/leakage/schemes/dma_001", params={"network": "demo"}
|
|
||||||
)
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert response.json()["scheme_name"] == "dma_001"
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ async def test_meta_project_returns_map_extent(monkeypatch):
|
|||||||
response = await module.get_project_metadata(
|
response = await module.get_project_metadata(
|
||||||
ctx=SimpleNamespace(
|
ctx=SimpleNamespace(
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
project_role="editor",
|
project_role="member",
|
||||||
),
|
),
|
||||||
metadata_repo=repo,
|
metadata_repo=repo,
|
||||||
)
|
)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user