"""Scheduled-job handler for the 年度报告 (Helix Wrapped) year-end push. ``wrapped.year_end_push`` — fires once on Dec 31 (HQ date). Sends the opted-in active employees a single Feishu card linking to their year-in-review. All guardrails live in ``run_year_end_push`` (once/year idempotency marker, opt-out preference, per-run cap, per-person isolation) — this handler just resolves the year and forwards the config cap. """ from __future__ import annotations import logging from datetime import datetime from typing import Any from zoneinfo import ZoneInfo from sqlalchemy.ext.asyncio import AsyncSession from app.domains.me_universe.wrapped_push import run_year_end_push from app.scheduled_jobs.registry import register_scheduled_job logger = logging.getLogger(__name__) _HQ_TZ = ZoneInfo("Asia/Shanghai") @register_scheduled_job( key="wrapped.year_end_push", display_name="年度报告 年底推送", description=( "每年 12/31 给已选择接收的在职员工推送一张飞书卡片,链接到其年度报告。" "护栏:一年一次(幂等标记)+ 设置可 opt-out + 每轮上限 + 逐人隔离。" ), # #2243: enqueue-then-drain over up to ``max_recipients`` (5000) # people runs 飞书 sends on the handler's POOLED session for minutes # — exactly the registry's own criterion for ``heavy`` ("holds a DB # pool connection by the minute"). Unmarked it defaulted to # ``light``, so it never took a slot in the heavy-4 sub-gate # (#2006) while holding a connection the whole time. weight="heavy", ) async def run_wrapped_year_end_push( db: AsyncSession, config: dict[str, Any], ) -> dict[str, Any]: year = datetime.now(_HQ_TZ).year cap = int(config.get("max_recipients", 5000)) if config else 5000 try: summary = await run_year_end_push(db, year=year, max_recipients=cap) except Exception as exc: logger.exception("wrapped.year_end_push failed") raise RuntimeError(f"wrapped.year_end_push: {exc}") from exc await db.commit() return summary __all__ = ["run_wrapped_year_end_push"]