ForcePilot/src/utils/datetime_utils.py
Wenjie Zhang b36f6e78f3 refactor(time): 重构应用程序中的日期时间处理
- 新增工具模块 `datetime_utils.py`,用于统一处理UTC和上海时区
- 在 `manager.py`、`models.py` 等后端文件中,将直接操作日期时间的代码替换为工具函数
- 更新前端组件,使用 `dayjs` 进行时区感知的日期格式化和解析
- 移除冗余的日期格式化逻辑,将其集中到工具函数中
- 通过确保所有时间戳均以亚洲/上海时区显示,提升用户体验
2025-10-13 15:08:54 +08:00

126 lines
3.6 KiB
Python

"""
Datetime helper utilities for consistent timezone handling.
The backend stores timestamps in UTC and exposes ISO 8601 strings with an
explicit timezone designator. For user-facing displays we typically convert to
Asia/Shanghai.
"""
from __future__ import annotations
import datetime as dt
from collections.abc import Iterable
from zoneinfo import ZoneInfo
UTC = dt.UTC
SHANGHAI_TZ = ZoneInfo("Asia/Shanghai")
_ISO_Z_SUFFIX = "+00:00"
def utc_now() -> dt.datetime:
"""Return the current UTC time as an aware datetime."""
return dt.datetime.now(UTC)
def shanghai_now() -> dt.datetime:
"""Return the current Asia/Shanghai time as an aware datetime."""
return utc_now().astimezone(SHANGHAI_TZ)
def ensure_utc(value: dt.datetime) -> dt.datetime:
"""
Convert a datetime to UTC.
Naive values are assumed to be in Asia/Shanghai to preserve legacy data.
"""
if value.tzinfo is None:
value = value.replace(tzinfo=SHANGHAI_TZ)
return value.astimezone(UTC)
def ensure_shanghai(value: dt.datetime) -> dt.datetime:
"""
Convert a datetime to Asia/Shanghai.
Naive values are assumed to be in Asia/Shanghai (legacy behaviour).
"""
if value.tzinfo is None:
value = value.replace(tzinfo=SHANGHAI_TZ)
return value.astimezone(SHANGHAI_TZ)
def utc_isoformat(value: dt.datetime | None = None) -> str:
"""Return an ISO 8601 string in UTC with a trailing Z suffix."""
value = ensure_utc(value or utc_now())
iso_string = value.isoformat()
if iso_string.endswith(_ISO_Z_SUFFIX):
return iso_string.replace(_ISO_Z_SUFFIX, "Z")
return iso_string
def shanghai_isoformat(value: dt.datetime | None = None) -> str:
"""Return an ISO 8601 string in Asia/Shanghai timezone."""
value = ensure_shanghai(value or shanghai_now())
return value.isoformat()
def coerce_datetime(value: dt.datetime | None) -> dt.datetime | None:
"""Normalize persisted datetimes to UTC, handling nulls gracefully."""
if value is None:
return None
return ensure_utc(value)
def coerce_any_to_utc_datetime(value: dt.datetime | int | float | str | None) -> dt.datetime | None:
"""
Convert heterogeneous timestamp representations to an aware UTC datetime.
Supports:
* aware or naive datetime objects
* unix timestamps (seconds) as int/float
* ISO 8601 strings
"""
if value is None:
return None
if isinstance(value, dt.datetime):
return ensure_utc(value)
if isinstance(value, (int, float)):
return dt.datetime.fromtimestamp(value, tz=UTC)
if isinstance(value, str):
# Attempt to parse ISO 8601 strings.
try:
parsed = dt.datetime.fromisoformat(value.replace("Z", _ISO_Z_SUFFIX))
return ensure_utc(parsed)
except ValueError:
# Attempt fallback to numeric string
try:
as_number = float(value)
return dt.datetime.fromtimestamp(as_number, tz=UTC)
except ValueError:
raise ValueError(f"Unsupported datetime string format: {value!r}") from None
raise TypeError(f"Unsupported datetime value: {value!r}")
def normalize_iterable_to_utc(values: Iterable[dt.datetime | None]) -> list[dt.datetime | None]:
"""Normalize each datetime in iterable to UTC."""
return [coerce_datetime(item) if isinstance(item, dt.datetime) else None for item in values]
__all__ = [
"UTC",
"SHANGHAI_TZ",
"utc_now",
"shanghai_now",
"ensure_utc",
"ensure_shanghai",
"utc_isoformat",
"shanghai_isoformat",
"coerce_datetime",
"coerce_any_to_utc_datetime",
"normalize_iterable_to_utc",
]