feat: 新增节假日缓存,仅工作日生成与推送

- daily/holiday.py: 抓取 xiaoai.me 全年节假日并缓存到 .cache/holidays-<year>.json,
  is_workday() 判定(法定节假日/周末休息,调休补班日上班),联网失败回退本地缓存
- scheduler: tick_once 前置工作日闸门,非工作日标记完成并跳过,判定失败降级为正常工作日避免漏跑
- config/.env.example: 新增 DAILY_WORKDAY_ONLY 开关(默认开)
- tests: test_holiday 8 例 + scheduler 非工作日闸门 2 例

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-23 17:51:09 +08:00
parent d66f2c716c
commit 36085f107d
6 changed files with 232 additions and 0 deletions

View File

@@ -20,7 +20,9 @@ from daily.config import (
schedule_generate_at,
schedule_push_at,
schedule_timezone_name,
workday_only,
)
from daily.holiday import is_workday, load_holidays, workday_name
logger = logging.getLogger(__name__)
@@ -200,6 +202,27 @@ def tick_once(
push_at = push_at or parse_hhmm(schedule_push_at())
today_str = now.astimezone(tz).date().isoformat()
# 仅工作日运行:非工作日(法定节假日/周末,调休补班日除外)跳过当日 generate/push。
# 将两者标记为已完成,避免 plan_next_action 在当天反复补跑。
if workday_only() and state.last_generate_date != today_str:
today = now.astimezone(tz).date()
try:
holidays = load_holidays(today.year)
if not is_workday(today, holidays):
name = workday_name(today, holidays) or "周末"
logger.info("今日 %s 非工作日(%s),跳过生成与推送", today_str, name)
state.last_generate_date = today_str
state.last_push_date = today_str
if not dry_run:
state.save()
if dry_run:
logger.info("[dry-run] 非工作日,将跳过")
return state
except Exception as exc:
# 节假日数据不可用时按常规工作日处理,避免因接口故障漏跑
logger.warning("工作日判定失败(%s),按正常工作日处理", exc)
planned = plan_next_action(
now=now,
tz=tz,