This commit is contained in:
2026-07-06 13:39:08 +08:00
parent 16b3d223c1
commit c68215c177
33 changed files with 3250 additions and 732 deletions
+270
View File
@@ -0,0 +1,270 @@
from datetime import datetime, timedelta, timezone, date
from typing import Optional, Union, Tuple
from dateutil import parser
# ===================== 时区与格式化常量(PHP风格映射) =====================
# 北京时间 东八区 UTC+8
BEIJING_TZ = timezone(timedelta(hours=8))
UTC_TZ = timezone.utc
# Python strftime 标准格式
FORMAT_DATETIME = "%Y-%m-%d %H:%M:%S"
FORMAT_DATE = "%Y-%m-%d"
FORMAT_TIME = "%H:%M:%S"
# PHP格式 -> Python格式 映射,贴近PHP使用习惯
PHP_FMT_MAP = {
"Y-m-d": FORMAT_DATE,
"Y-m-d H:i:s": FORMAT_DATETIME,
"H:i:s": FORMAT_TIME
}
# ===================== 私有公共工具函数(内部复用) =====================
def _normalize_date_str(date_str: str) -> str:
"""
预处理中文格式日期字符串,统一转为横杠分隔标准格式
支持:2025年01月01日 12时30分00秒 / 20250101 等格式
"""
if not isinstance(date_str, str):
return ""
s = date_str.strip()
# 中文、全角符号替换清洗
s = s.replace("", ":") \
.replace("", "-") \
.replace("", "-") \
.replace("", " ") \
.replace("", ":") \
.replace("", ":") \
.replace("", "")
return s
def _get_python_fmt(fmt: str) -> str:
"""兼容PHP格式字符串,自动转为Python strftime格式"""
return PHP_FMT_MAP.get(fmt, fmt)
def _convert_ts_to_datetime(timestamp: Union[int, float], tz: timezone = BEIJING_TZ) -> datetime:
"""
统一处理 10位秒 / 13位毫秒 时间戳 -> 带时区datetime
"""
ts = float(timestamp)
# 毫秒时间戳兼容
if ts > 10 ** 12:
ts /= 1000
return datetime.fromtimestamp(ts, tz=tz)
def _parse_datetime_with_tz(dt_str: str, fmt: str, tz: timezone = BEIJING_TZ) -> Optional[datetime]:
"""
通用:固定格式日期字符串解析+绑定时区,支持中文日期预处理
仅支持指定fmt格式,不支持带时区后缀字符串
"""
try:
fmt = _get_python_fmt(fmt)
normalize_str = _normalize_date_str(dt_str)
dt = datetime.strptime(normalize_str, fmt)
return dt.replace(tzinfo=tz)
except (ValueError, TypeError):
return None
def _smart_parse_datetime(dt_str: str, base_dt: datetime, is_timezone: bool) -> Optional[datetime]:
"""
私有:dateutil智能解析日期(用于带时区、相对时间、中文复杂日期)
"""
clean_str = _normalize_date_str(dt_str)
try:
parsed_dt = parser.parse(clean_str, default=base_dt, tzinfos=None)
if is_timezone:
# 保留原始时区,无时区则兜底北京时间
if parsed_dt.tzinfo is None:
parsed_dt = parsed_dt.replace(tzinfo=BEIJING_TZ)
else:
# 普通日期强制绑定北京时间
parsed_dt = parsed_dt.replace(tzinfo=BEIJING_TZ)
return parsed_dt
except (parser.ParserError, ValueError, TypeError):
return None
# ===================== 对外工具方法(对标PHP + dateutil增强) =====================
def time() -> int:
"""
对标 PHP time()
获取当前北京时间 10位 秒级时间戳
"""
return int(datetime.now(tz=BEIJING_TZ).timestamp())
def microtime(get_as_float: bool = False) -> Union[float, str]:
"""
对标 PHP microtime()
:param get_as_float: True 返回浮点时间戳,False 返回 "微秒 秒" 字符串
"""
now = datetime.now(tz=BEIJING_TZ)
ts = now.timestamp()
if get_as_float:
return ts
sec = int(ts)
usec = int((ts - sec) * 1000000)
return f"{usec:06d} {sec}"
def strtotime(
time_str: str,
now: Optional[int] = None,
is_timezone: bool = False
) -> Optional[int]:
"""
【增强版 对标 PHP strtotime】依托 dateutil 支持相对时间、带时区时间解析
:param time_str: 待解析日期/相对时间字符串
:param now: 基准时间戳,默认当前北京时间
:param is_timezone: 是否为带时区格式的时间字符串(如 2026-07-03 10:01:29.621351+08
- True:优先使用字符串自带时区,无时区则兜底北京时间
- False:默认按北京时间解析普通日期字符串
:return: 秒级时间戳,解析失败返回 None
"""
if not isinstance(time_str, str) or not time_str.strip():
return None
# 基准时间:默认当前北京时间
base_dt = datetime.fromtimestamp(now, tz=BEIJING_TZ) if now else datetime.now(tz=BEIJING_TZ)
parsed_dt = _smart_parse_datetime(time_str, base_dt, is_timezone)
if not parsed_dt:
return None
return int(parsed_dt.timestamp())
def date(fmt: str, timestamp: Optional[int] = None) -> str:
"""
对标 PHP date()
时间戳格式化,默认北京时间
:param fmt: 支持 Y-m-d / Y-m-d H:i:s 或 %Y-%m-%d 原生格式
:param timestamp: 秒时间戳,None则取当前时间
"""
if timestamp is None:
dt = datetime.now(tz=BEIJING_TZ)
else:
dt = _convert_ts_to_datetime(timestamp)
python_fmt = _get_python_fmt(fmt)
return dt.strftime(python_fmt)
def timestamp_to_datetime(timestamp: Union[int, float], fmt: str = "Y-m-d H:i:s") -> str:
"""
时间戳(秒/毫秒)→ 北京时间格式化字符串
:param timestamp: 10位秒 /13位毫秒
:param fmt: PHP风格格式 Y-m-d / Y-m-d H:i:s
"""
dt = _convert_ts_to_datetime(timestamp)
python_fmt = _get_python_fmt(fmt)
return dt.strftime(python_fmt)
def datetime_to_timestamp(dt_str: str, fmt: str = "Y-m-d H:i:s") -> Optional[int]:
"""
日期字符串(含中文日期) → 北京时间秒时间戳
:param dt_str: 日期字符串
:param fmt: PHP风格格式化模板
:return: 秒时间戳,解析失败返回None
"""
dt = _parse_datetime_with_tz(dt_str, fmt)
if not dt:
return None
return int(dt.timestamp())
def get_today_start_timestamp() -> int:
"""获取今日 00:00:00 北京时间 秒时间戳"""
today: date = datetime.now(tz=BEIJING_TZ).date()
start_dt = datetime.combine(today, datetime.min.time(), tzinfo=BEIJING_TZ)
return int(start_dt.timestamp())
def get_today_end_timestamp() -> int:
"""获取今日 23:59:59.999999 北京时间 秒时间戳"""
today: date = datetime.now(tz=BEIJING_TZ).date()
end_dt = datetime.combine(today, datetime.max.time(), tzinfo=BEIJING_TZ)
return int(end_dt.timestamp())
def parse_date_range(date_list: list, fmt: str = "Y-m-d", is_timezone: bool = False) -> Optional[Tuple[datetime, datetime]]:
"""
时间范围解析:["2025-01-01","2026-01-01"] → (开始0点,结束23:59:59.999999) 带北京时间
:param date_list: 长度为2的日期字符串数组
:param fmt: 日期格式,默认Y-m-d
:param is_timezone: 是否为带时区格式的时间字符串(如 2026-07-03 10:01:29.621351+08
- True:使用dateutil智能解析,优先保留字符串自带时区,无时区兜底北京时间
- False:按指定fmt格式精准解析,强制北京时间
:return: (start_dt, end_dt) 格式非法返回None
"""
# 强参数校验
if not isinstance(date_list, list) or len(date_list) != 2:
return None
start_str, end_str = date_list[0].strip(), date_list[1].strip()
if not start_str or not end_str:
return None
base_now = datetime.now(tz=BEIJING_TZ)
if is_timezone:
# 带时区场景:智能解析,支持 2026-07-03 08:16:26.88303+08
start_dt = _smart_parse_datetime(start_str, base_now, is_timezone=True)
end_dt = _smart_parse_datetime(end_str, base_now, is_timezone=True)
else:
# 普通前端日期:固定格式解析
start_dt = _parse_datetime_with_tz(start_str, fmt, BEIJING_TZ)
end_dt = _parse_datetime_with_tz(end_str, fmt, BEIJING_TZ)
if not start_dt or not end_dt:
return None
# 结束时间补全到当日最后一毫秒
end_dt = end_dt.replace(hour=23, minute=59, second=59, microsecond=999999)
return start_dt, end_dt
def db_tz_str_to_datetime(db_datetime_str: str) -> Optional[datetime]:
"""
数据库timestamptz格式字符串(2026-07-03 08:16:26.88303+08)转为带时区datetime
"""
try:
dt = datetime.fromisoformat(db_datetime_str)
if dt.tzinfo is None:
dt = dt.replace(tzinfo=BEIJING_TZ)
return dt
except (ValueError, TypeError):
return None
def datetime_to_db_tz_str(dt: datetime) -> str:
"""
带时区datetime转为数据库timestamptz标准字符串,统一北京时间存储
"""
if dt.tzinfo is None:
dt = dt.replace(tzinfo=BEIJING_TZ)
else:
dt = dt.astimezone(BEIJING_TZ)
return dt.isoformat()
# 导出列表
__all__ = [
"BEIJING_TZ",
"UTC_TZ",
"FORMAT_DATETIME",
"FORMAT_DATE",
"FORMAT_TIME",
"time",
"microtime",
"strtotime",
"date",
"timestamp_to_datetime",
"datetime_to_timestamp",
"get_today_start_timestamp",
"get_today_end_timestamp",
"parse_date_range",
"db_tz_str_to_datetime",
"datetime_to_db_tz_str"
]