diff --git a/.env.example b/.env.example index 6376b4b..9b3b90c 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,7 @@ # TJWater Server 环境变量配置模板 # 复制此文件为 .env 并填写实际值 -ENVIRONMENT="local" +# CI/CD: 将生产 .env 的完整内容保存为 Gitea 仓库密钥 TJWATER_SERVER_ENV。 +ENVIRONMENT="production" NETWORK_NAME="tjwater" # ============================================ # 安全配置 (必填) @@ -49,6 +50,7 @@ KEYCLOAK_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----" KEYCLOAK_ALGORITHM=RS256 KEYCLOAK_AUDIENCE="account" + # ============================================ # Bocha Web Search API # ============================================ diff --git a/app/api/v1/endpoints/misc.py b/app/api/v1/endpoints/misc.py index 1ebb083..248032f 100644 --- a/app/api/v1/endpoints/misc.py +++ b/app/api/v1/endpoints/misc.py @@ -1,5 +1,4 @@ from typing import Any -import random from fastapi import APIRouter, Query from fastapi.responses import JSONResponse from fastapi import status @@ -63,24 +62,3 @@ async def fastapi_test_dict(data: Item) -> dict[str, str]: """ item = data.dict() return item - -@router.get("/getrealtimedata/", summary="获取实时数据", description="获取实时监测数据") -async def fastapi_get_realtimedata(): - """ - 获取实时数据 - - 返回随机生成的实时监测数据示例 - """ - data = [random.randint(0, 100) for _ in range(100)] - return data - - -@router.get("/getsimulationresult/", summary="获取模拟结果", description="获取仿真计算结果") -async def fastapi_get_simulationresult(): - """ - 获取仿真结果 - - 返回随机生成的仿真计算结果示例 - """ - data = [random.randint(0, 100) for _ in range(100)] - return data diff --git a/app/api/v1/router.py b/app/api/v1/router.py index 75d2f38..52c5431 100644 --- a/app/api/v1/router.py +++ b/app/api/v1/router.py @@ -6,6 +6,7 @@ from app.api.v1.endpoints import ( scada, extension, snapshots, + # data_query, users, schemes, misc, @@ -54,7 +55,9 @@ api_router = APIRouter() # Core Services api_router.include_router(auth.router, prefix="/auth", tags=["Auth"]) -api_router.include_router(user_management.router, prefix="/users", tags=["User Management"]) # 新增 +api_router.include_router( + user_management.router, prefix="/users", tags=["User Management"] +) # 新增 api_router.include_router(audit.router, prefix="/audit", tags=["Audit Logs"]) # 新增 api_router.include_router(meta.router, tags=["Metadata"]) api_router.include_router(project.router, tags=["Project"]) @@ -84,7 +87,8 @@ 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, tags=["SCADA"]) +# api_router.include_router(data_query.router, tags=["Data Query & InfluxDB"]) +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"]) diff --git a/app/core/config.py b/app/core/config.py index dd9ad77..bc51014 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -6,9 +6,11 @@ from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): PROJECT_NAME: str = "TJWater Server" - ENVIRONMENT: str = "local" + ENVIRONMENT: str = "production" API_V1_STR: str = "/api/v1" + NETWORK_NAME: str = "default_network" + # JWT 配置 SECRET_KEY: str = ( "your-secret-key-here-change-in-production-use-openssl-rand-hex-32" diff --git a/app/services/burst_detection.py b/app/services/burst_detection.py index 59baf32..9665934 100644 --- a/app/services/burst_detection.py +++ b/app/services/burst_detection.py @@ -14,6 +14,7 @@ from app.services.scheme_management import ( store_scheme_info, ) from app.services.tjnetwork import get_all_scada_info +from app.services.time_api import extract_date, parse_utc_time, utc_now def run_burst_detection( @@ -241,7 +242,7 @@ def list_burst_detection_schemes( network: str, query_date: datetime | str | None = None, ) -> list[dict[str, Any]]: - parsed_date = _to_datetime(query_date).date() if query_date is not None else None + parsed_date = extract_date(query_date, field_name="query_date") if query_date is not None else None return query_burst_detection_schemes( name=network, network=network, @@ -269,7 +270,7 @@ def _store_burst_detection_scheme( if scheme_name_exists(network, scheme_name): raise ValueError(f"方案名称已存在: {scheme_name}") - now_iso = datetime.now().isoformat() + now_iso = utc_now().isoformat() scheme_detail = { "network": network, "sensor_nodes": payload.get("sensor_nodes", []), @@ -426,6 +427,4 @@ def _build_observed_pressure_from_scada( def _to_datetime(value: datetime | str) -> datetime: - if isinstance(value, datetime): - return value - return datetime.fromisoformat(value) + return parse_utc_time(value) diff --git a/app/services/burst_location.py b/app/services/burst_location.py index 3892ca2..5a6b52b 100644 --- a/app/services/burst_location.py +++ b/app/services/burst_location.py @@ -15,6 +15,7 @@ from app.services.scheme_management import ( store_scheme_info, ) from app.services.tjnetwork import dump_inp, get_all_scada_info +from app.services.time_api import extract_date, parse_utc_time, utc_now SeriesInput = pd.Series | dict[str, Any] | list[dict[str, Any]] FLOW_SCADA_TYPES = {"pipe_flow", "flow", "demand"} @@ -301,7 +302,7 @@ def run_burst_location_by_network( def list_burst_location_schemes( network: str, query_date: datetime | str | None = None ) -> list[dict[str, Any]]: - parsed_date = _to_datetime(query_date).date() if query_date is not None else None + parsed_date = extract_date(query_date, field_name="query_date") if query_date is not None else None return query_burst_location_schemes( name=network, network=network, query_date=parsed_date ) @@ -327,7 +328,7 @@ def _store_burst_scheme( if scheme_name_exists(network, scheme_name): raise ValueError(f"方案名称已存在: {scheme_name}") - now_iso = datetime.now().isoformat() + now_iso = utc_now().isoformat() scheme_detail = { "network": network, "pressure_scada_ids": payload.get("pressure_scada_ids", []), @@ -641,9 +642,7 @@ def _dedupe_ids(ids: list[str] | None) -> list[str]: def _to_datetime(value: datetime | str) -> datetime: - if isinstance(value, datetime): - return value - return datetime.fromisoformat(value) + return parse_utc_time(value) def _prepare_burst_inp(network: str) -> str: diff --git a/app/services/leakage_identifier.py b/app/services/leakage_identifier.py index e90cb24..a85d653 100644 --- a/app/services/leakage_identifier.py +++ b/app/services/leakage_identifier.py @@ -23,6 +23,7 @@ from app.services.tjnetwork import ( get_network_link_nodes, get_network_node_coords, ) +from app.services.time_api import extract_date, parse_utc_time, utc_now DEFAULT_N_WORKERS = max(1, min((os.cpu_count() or 1) - 1, 4)) @@ -119,7 +120,7 @@ def run_leakage_identification( scheme_start_time = ( _to_datetime(scada_start).isoformat() if scada_start is not None - else datetime.now().isoformat() + else utc_now().isoformat() ) scheme_detail = { "network": network, @@ -177,7 +178,7 @@ def run_leakage_identification( def list_leakage_identify_schemes( network: str, query_date: datetime | str | None = None ) -> list[dict[str, Any]]: - parsed_date = _to_datetime(query_date).date() if query_date is not None else None + parsed_date = extract_date(query_date, field_name="query_date") if query_date is not None else None return query_leakage_identify_schemes( name=network, network=network, query_date=parsed_date ) @@ -509,9 +510,7 @@ def _build_observed_pressure_from_scada( def _to_datetime(value: datetime | str) -> datetime: - if isinstance(value, datetime): - return value - return datetime.fromisoformat(value) + return parse_utc_time(value) def _prepare_leakage_inp(network: str) -> str: diff --git a/app/services/project_info.py b/app/services/project_info.py index 0a38481..19ebf46 100644 --- a/app/services/project_info.py +++ b/app/services/project_info.py @@ -1,4 +1,3 @@ -import os +from app.core.config import settings -# 从环境变量 NETWORK_NAME 读取 -name = os.getenv("NETWORK_NAME") +name = settings.NETWORK_NAME diff --git a/app/services/scheme_management.py b/app/services/scheme_management.py index a86a9bd..0bb1f11 100644 --- a/app/services/scheme_management.py +++ b/app/services/scheme_management.py @@ -1,6 +1,6 @@ import ast import json -from datetime import date +from datetime import date, datetime import geopandas as gpd import pandas as pd @@ -8,6 +8,7 @@ import psycopg from sqlalchemy import create_engine from app.core.config import get_pgconn_string +from app.services.time_api import parse_utc_time # 2025/03/23 @@ -89,7 +90,7 @@ def store_scheme_info( scheme_name: str, scheme_type: str, username: str, - scheme_start_time: str, + scheme_start_time: datetime | str, scheme_detail: dict, ): """ @@ -112,13 +113,16 @@ def store_scheme_info( """ # 将字典转换为 JSON 字符串 scheme_detail_json = json.dumps(scheme_detail) + normalized_scheme_start_time = parse_utc_time( + scheme_start_time, field_name="scheme_start_time" + ) cur.execute( sql, ( scheme_name, scheme_type, username, - scheme_start_time, + normalized_scheme_start_time, scheme_detail_json, ), ) diff --git a/infra/docker/docker-compose.yml b/infra/docker/docker-compose.yml index 6b938fa..138b374 100644 --- a/infra/docker/docker-compose.yml +++ b/infra/docker/docker-compose.yml @@ -3,6 +3,7 @@ services: # Core API Service # ========================================== api: + image: ${TJWATER_SERVER_IMAGE:-tjwater-server:local} build: context: ../.. dockerfile: Dockerfile