# -*- coding: utf-8 -*- """时区转换与日期范围工具。 默认时区:Asia/Shanghai(UTC+8),与业务数据库 timestamptz 对齐。 """ import calendar from datetime import datetime, date, timedelta from dateutil import tz SHANGHAI_TZ = tz.gettz("Asia/Shanghai") def now_shanghai() -> datetime: """获取上海时区当前时间。""" return datetime.now(SHANGHAI_TZ) def date_range(start: date, end: date) -> list[date]: """生成日期范围列表(含首尾)。 start > end 时返回空列表。 """ if start > end: return [] days = (end - start).days + 1 return [start + timedelta(days=i) for i in range(days)] # 营业日切点默认值(小时),与 .env BUSINESS_DAY_START_HOUR 对齐 DEFAULT_BUSINESS_DAY_START_HOUR = 8 def business_date(dt: datetime, day_start_hour: int = DEFAULT_BUSINESS_DAY_START_HOUR) -> date: """将时间戳归属到营业日。 day_start_hour 之前的记录归属前一天。 例:day_start_hour=8 时,07:59 归属前一天,08:00 归属当天。 """ if dt.hour < day_start_hour: return (dt - timedelta(days=1)).date() return dt.date() def business_month(dt: datetime, day_start_hour: int = DEFAULT_BUSINESS_DAY_START_HOUR) -> date: """将时间戳归属到营业月(返回该月第一天)。 当月1日 day_start_hour 之前的记录归属上月。 """ biz_d = business_date(dt, day_start_hour) return biz_d.replace(day=1) def business_week_monday(dt: datetime, day_start_hour: int = DEFAULT_BUSINESS_DAY_START_HOUR) -> date: """将时间戳归属到营业周(返回该周周一日期)。 周一 day_start_hour 之前的记录归属上周。 """ biz_d = business_date(dt, day_start_hour) return biz_d - timedelta(days=biz_d.weekday()) def biz_date_sql_expr(col: str, day_start_hour: int = DEFAULT_BUSINESS_DAY_START_HOUR) -> str: """生成 PostgreSQL 营业日归属 SQL 表达式。 返回形如 DATE(col - INTERVAL '8 hours') 的字符串, 用于替换原来的 DATE(col)。 """ return f"DATE({col} - INTERVAL '{day_start_hour} hours')" def business_day_range( biz_date: date, day_start_hour: int = DEFAULT_BUSINESS_DAY_START_HOUR ) -> tuple[datetime, datetime]: """返回给定营业日的精确时间戳范围 [start, end)。 start = biz_date 当天 day_start_hour:00(Asia/Shanghai) end = biz_date 次日 day_start_hour:00(Asia/Shanghai) """ start = datetime(biz_date.year, biz_date.month, biz_date.day, day_start_hour, 0, 0, tzinfo=SHANGHAI_TZ) end = start + timedelta(days=1) return (start, end) def business_week_range( week_monday: date, day_start_hour: int = DEFAULT_BUSINESS_DAY_START_HOUR ) -> tuple[datetime, datetime]: """返回给定营业周(周一)的精确时间戳范围 [start, end)。 start = week_monday 当天 day_start_hour:00(Asia/Shanghai) end = week_monday + 7天 day_start_hour:00(Asia/Shanghai) """ start = datetime(week_monday.year, week_monday.month, week_monday.day, day_start_hour, 0, 0, tzinfo=SHANGHAI_TZ) end = start + timedelta(days=7) return (start, end) def business_month_range( month_first: date, day_start_hour: int = DEFAULT_BUSINESS_DAY_START_HOUR ) -> tuple[datetime, datetime]: """返回给定营业月(首日)的精确时间戳范围 [start, end)。 start = month_first 当天 day_start_hour:00(Asia/Shanghai) end = 次月1日 day_start_hour:00(Asia/Shanghai) """ start = datetime(month_first.year, month_first.month, month_first.day, day_start_hour, 0, 0, tzinfo=SHANGHAI_TZ) # 计算次月1日 if month_first.month == 12: next_month_first = date(month_first.year + 1, 1, 1) else: next_month_first = date(month_first.year, month_first.month + 1, 1) end = datetime(next_month_first.year, next_month_first.month, next_month_first.day, day_start_hour, 0, 0, tzinfo=SHANGHAI_TZ) return (start, end)