From 27ac1e4e823b0bdf414e5adb0a1c043cbcb93ef9 Mon Sep 17 00:00:00 2001 From: yueli Date: Sat, 3 Oct 2026 09:48:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(scheduler):=20=E6=96=B0=E5=A2=9E=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E6=97=A5=E5=88=A4=E5=AE=9A=E5=B7=A5=E5=85=B7=EF=BC=88?= =?UTF-8?q?=E6=B3=95=E5=AE=9A=E8=8A=82=E5=81=87=E6=97=A5=E4=B8=8E=E8=B0=83?= =?UTF-8?q?=E4=BC=91=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 依赖 chinesecalendar(按年收录,1.11.0 覆盖 2004-2026)。口径:非法定节假日、非周末、非调休放假;补班日(调休上班的周六周日)算工作日,纯 weekday 判断做不到。 库缺失或年份未收录(实测 2027 起抛 NotImplementedError)时降级为周一至周五判断并打 [Workday] 警告,宁可偶尔在节假日多发也不让日报停发。库按年发布,次年安排发布后需升级并重建镜像。 --- inventory-backend/app/utils/workday.py | 64 ++++++++++++++++++++++++++ inventory-backend/requirements.txt | 6 ++- 2 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 inventory-backend/app/utils/workday.py diff --git a/inventory-backend/app/utils/workday.py b/inventory-backend/app/utils/workday.py new file mode 100644 index 0000000..e1dc410 --- /dev/null +++ b/inventory-backend/app/utils/workday.py @@ -0,0 +1,64 @@ +""" +工作日判定 —— 供定时任务决定「今天要不要发邮件」。 + +口径 +---- +「工作日」= 非法定节假日、非周末、且非法定调休放假的日子; +「补班日」(为凑长假而调休上班的周六/周日)**算工作日**。 + +★ 这个口径**无法用规则算出来**。中国的放假/调休安排由国务院逐年发布, + 纯按 weekday() 判断必然错判:国庆前后的补班周末会被漏发,长假中间的 + 工作日会被误发 —— 两种都是「该发的不发 / 不该发的发了」的实际故障。 + 故依赖 chinese-calendar,它内置国务院公布的节假日与调休数据。 + +★ 但它**按年收录**(1.11.0 覆盖 2004–2026),超出范围会抛 + NotImplementedError。因此这里必须兜底:库缺失或该年份尚未收录时, + 退化为「周一至周五即工作日」并打警告 —— + 宁可偶尔在节假日多发一封,也绝不能让日报因第三方库没更新而**整个停发**: + 收不到报表是比多收一封更难排查的静默故障(发件侧一切正常,没人知道 + 为什么报表没了)。 +""" +import logging +from datetime import date, datetime + +logger = logging.getLogger(__name__) + +try: + from chinese_calendar import is_workday as _cc_is_workday + _CC_AVAILABLE = True +except ImportError: # 依赖未装(例如镜像未随 requirements 重建) + _cc_is_workday = None + _CC_AVAILABLE = False + + +def is_workday(day=None): + """ + 判断某天是否为工作日。 + + day:date 或 datetime,缺省取今天(**调用方若需北京时间口径,必须自己 + 传入 `datetime.now(beijing_tz).date()`** —— 本函数不做任何时区换算, + 容器时区多为 UTC,直接依赖缺省值会在凌晨前后算错一天)。 + """ + if day is None: + day = date.today() + if isinstance(day, datetime): + day = day.date() + + if _CC_AVAILABLE: + try: + return bool(_cc_is_workday(day)) + except NotImplementedError: + # 该年份尚未被 chinese-calendar 收录(通常是次年安排还没发布) + logger.warning( + "[Workday] chinese-calendar 未收录 %s 的放假安排," + "本次退化为「仅按周末判断」;请升级 chinesecalendar 包", day + ) + except Exception as e: # noqa: BLE001 —— 第三方库任何异常都不该阻断日报 + logger.warning( + f"[Workday] chinese-calendar 判定 {day} 出错,退化为周末判断: {e}" + ) + else: + logger.warning("[Workday] 未安装 chinese-calendar,退化为「仅按周末判断」") + + # 兜底:周一至周五算工作日(无节假日感知,可能误发) + return day.weekday() < 5 diff --git a/inventory-backend/requirements.txt b/inventory-backend/requirements.txt index f528fd0..132498d 100644 --- a/inventory-backend/requirements.txt +++ b/inventory-backend/requirements.txt @@ -35,4 +35,8 @@ tqdm>=4.66.0 # [新增] pgvector 向量数据库支持(以图搜图 / 实时向量提取) pgvector>=0.2.0 # [新增] HTTP 客户端(Track 系统 Webhook 异步通知) -httpx>=0.24.0 \ No newline at end of file +httpx>=0.24.0 +# [新增] 中国法定节假日/调休数据(日报仅工作日发送)。 +# 按年收录(1.11.0 覆盖 2004–2026),次年数据发布后需 pip install -U chinesecalendar; +# 代码侧已兜底(见 app/utils/workday.py),未升级不会导致日报停发。 +chinesecalendar>=1.11.0 \ No newline at end of file