后端统一时区为 UTC

This commit is contained in:
2026-04-14 14:46:51 +08:00
parent 51b481d174
commit bf2aaa5ff7
16 changed files with 263 additions and 252 deletions
+4 -5
View File
@@ -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)
+4 -5
View File
@@ -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:
+4 -5
View File
@@ -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:
+7 -3
View File
@@ -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,
),
)
+60 -49
View File
@@ -1,5 +1,6 @@
from datetime import datetime, timezone, timedelta
from dateutil import parser, tz
from datetime import date, datetime, time, timedelta, timezone
from dateutil import parser, tz
'''
2025-02-09T15:45:00+00:00 采用的是 ISO 8601 国际标准日期时间格式,具体特点如下:
@@ -13,57 +14,67 @@ from dateutil import parser, tz
2025-02-09T15:45:00+08:00
'''
BG_TZ = tz.gettz('Asia/Shanghai')
UTC_TZ = tz.gettz('UTC')
BG_TZ = tz.gettz("Asia/Shanghai")
UTC_TZ = timezone.utc
def parse_utc_time(query_time: str) -> datetime:
'''
接受 任意格式的字符串,如果解析出来不带时区,则用 replace 添加 +00:00 时区
如果解析出来已经有时区,则用 astimezone 转换成UTC时间
'''
TIMEZONE_REQUIRED_MESSAGE = (
"Datetime values must include an explicit timezone offset, for example "
"'2025-02-09T15:45:00Z' or '2025-02-09T23:45:00+08:00'."
)
# 解析时间字符串
dt: datetime = parser.parse(query_time)
def parse_aware_time(query_time: datetime | str, field_name: str = "datetime") -> datetime:
"""
解析时间并确保结果带有时区信息。
"""
dt = parser.parse(query_time) if isinstance(query_time, str) else query_time
if dt.tzinfo is None:
dt = dt.replace(tzinfo=UTC_TZ)
else:
dt = dt.astimezone(UTC_TZ)
raise ValueError(f"{field_name} is missing timezone information. {TIMEZONE_REQUIRED_MESSAGE}")
return dt
def extract_date(value: date | datetime | str, field_name: str = "date") -> date:
"""
提取日期部分,但保留调用方原始时区语义,不强制转换到 UTC。
"""
if isinstance(value, date) and not isinstance(value, datetime):
return value
return parse_aware_time(value, field_name=field_name).date()
def utc_now() -> datetime:
"""
返回带 UTC 时区的当前时间。
"""
return datetime.now(UTC_TZ)
def parse_utc_time(query_time: datetime | str, field_name: str = "datetime") -> datetime:
'''
接受带时区的时间字符串/对象,并统一转换成 UTC 时间。
'''
return parse_aware_time(query_time, field_name=field_name).astimezone(UTC_TZ)
def parse_beijing_time(query_time: str) -> datetime:
def parse_beijing_time(query_time: datetime | str, field_name: str = "datetime") -> datetime:
'''
接受 任意格式的字符串,如果解析出来不带时区,则用 replace 添加 +08:00 时区
如果解析出来已经有时区,则用 astimezone 转换成北京时间
也就是任意合法的时间字符串,最后都解析成 北京 时间
接受带时区的时间字符串/对象,并统一转换成北京时间。
'''
# 解析时间字符串
dt: datetime = parser.parse(query_time)
if dt.tzinfo is None:
dt = dt.replace(tzinfo=BG_TZ)
else:
dt = dt.astimezone(tz=BG_TZ)
return dt
return parse_aware_time(query_time, field_name=field_name).astimezone(tz=BG_TZ)
def to_utc_time(dt: datetime) -> datetime:
def to_utc_time(dt: datetime | str, field_name: str = "datetime") -> datetime:
'''
将一个北京时间的时间点,转换成utc
将一个带时区的时间点,转换成 UTC。
'''
utc_time = dt.astimezone(UTC_TZ)
return utc_time
return parse_aware_time(dt, field_name=field_name).astimezone(UTC_TZ)
def to_beijing_time(dt: datetime) -> datetime:
def to_beijing_time(dt: datetime | str, field_name: str = "datetime") -> datetime:
'''
将一个 utc 的时间点,转换成北京时间
将一个带时区的时间点,转换成北京时间
'''
beijing_time = dt.astimezone(tz=BG_TZ)
return beijing_time
return parse_aware_time(dt, field_name=field_name).astimezone(tz=BG_TZ)
def to_time_range(dt: datetime, delta: float) -> tuple[datetime, datetime]:
@@ -83,7 +94,8 @@ def parse_beijing_date_range(query_date: str) -> tuple[datetime, datetime]:
将一个日期字符串,转换成 start/end 时间段,传进来的日期被认为是北京时间
日期字符串格式:YYYY-MM-DD
'''
start_time = parse_beijing_time(query_date)
target_date = date.fromisoformat(query_date)
start_time = datetime.combine(target_date, time.min, BG_TZ)
end_time = start_time + timedelta(days=1)
return (start_time, end_time)
@@ -108,7 +120,7 @@ def get_date_from_time(time: str) -> str:
'''
将一个时间点,转换成日期
'''
dt = parse_beijing_time(time)
dt = parse_beijing_time(time, field_name="time")
return str(dt.date())
@@ -116,28 +128,27 @@ def is_today(query_date: str) -> bool:
'''
判断一个日期是否是今天
'''
dt = parse_beijing_time(query_date)
return dt.date() == datetime.now().date()
dt = parse_beijing_time(query_date, field_name="query_date")
return dt.date() == datetime.now(BG_TZ).date()
def is_yesterday(query_date: str) -> bool:
'''
判断一个日期是否是昨天
'''
dt = parse_beijing_time(query_date)
return dt.date() == (datetime.now().date() - timedelta(days=1))
dt = parse_beijing_time(query_date, field_name="query_date")
return dt.date() == (datetime.now(BG_TZ).date() - timedelta(days=1))
def is_tomorrow(query_date: str) -> bool:
'''
判断一个日期是否是明天
'''
dt = parse_beijing_time(query_date)
return dt.date() == (datetime.now().date() + timedelta(days=1))
dt = parse_beijing_time(query_date, field_name="query_date")
return dt.date() == (datetime.now(BG_TZ).date() + timedelta(days=1))
def is_today_or_future(query_date: str) -> bool:
'''
判断一个日期是否是今天或未来
'''
dt = parse_beijing_time(query_date)
return dt.date() >= datetime.now().date()
dt = parse_beijing_time(query_date, field_name="query_date")
return dt.date() >= datetime.now(BG_TZ).date()