"""统一时间工具:所有对外展示时间按当前时区输出。""" from __future__ import annotations import os from datetime import datetime, time, timedelta from zoneinfo import ZoneInfo, ZoneInfoNotFoundError def get_timezone_name() -> str: return os.getenv("TZ") or os.getenv("APP_TIMEZONE") or "Asia/Shanghai" def get_timezone() -> ZoneInfo: name = get_timezone_name() try: return ZoneInfo(name) except ZoneInfoNotFoundError: return ZoneInfo("Asia/Shanghai") APP_TZ = get_timezone() def now_tz() -> datetime: """当前应用时区时间。""" return datetime.now(APP_TZ) def to_app_tz(value: datetime) -> datetime: """把 datetime 转为应用时区;无 tzinfo 的值按应用时区本地时间处理。""" if value.tzinfo is None: return value.replace(tzinfo=APP_TZ) return value.astimezone(APP_TZ) def format_dt(value: datetime | None) -> str | None: """面向 API/前端的统一时间格式,包含时区偏移。""" if value is None: return None return to_app_tz(value).isoformat(sep=" ", timespec="seconds") def from_timestamp(ts: float) -> datetime: return datetime.fromtimestamp(ts, APP_TZ) def local_day_range(day: str | None = None) -> tuple[str, datetime, datetime]: """返回本地日范围。为了兼容当前 ORM/数据库字段,过滤条件返回 naive datetime。""" if day: target = datetime.strptime(day, "%Y-%m-%d").date() else: target = now_tz().date() start = datetime.combine(target, time.min) end = start + timedelta(days=1) return target.strftime("%Y-%m-%d"), start, end