"""Scheduled-job handlers for the 动态广场 feed. Three handlers (all idempotent + safe to re-run): * ``feed.scan_today_celebrations`` — hourly. Walks active employees, upserts ``feed_celebrations`` + the per-person system post + the aggregate banner. UNIQUE constraints make the rerun cost ~0. * ``feed.notify_celebrants_and_leads`` — hourly (10-min offset). Reads today's celebrations and emits station + Feishu push to the celebrant, manager, and HRBP. Per-role idempotency lives in ``feed_celebrations.notify_state_json``. * ``feed.expire_pinned_announcements`` — every 10 minutes. Flips draft→published when ``publish_at`` has passed, and published→archived when ``expire_at`` has passed (also clears ``is_pinned``). All handlers raise on failure so the scheduler marks the run failed (see [[feedback_handler_status_must_raise]]). """ from __future__ import annotations import logging from typing import Any from sqlalchemy.ext.asyncio import AsyncSession from app.domains.feed.celebration_service import ( notify_today_celebrations, reconcile_announcement_publication, scan_today_celebrations, ) from app.scheduled_jobs.registry import register_scheduled_job logger = logging.getLogger(__name__) @register_scheduled_job( key="feed.scan_today_celebrations", display_name="动态广场 扫描今日生日与入职周年", description=( "每小时扫描活跃员工的本地时区今日,upsert feed_celebrations + " "对应 system_post + 当日聚合 banner。idempotent (UNIQUE 保护)。" ), ) async def run_scan_today_celebrations( db: AsyncSession, config: dict[str, Any], ) -> dict[str, Any]: try: result = await scan_today_celebrations(db) except Exception as exc: logger.exception("feed.scan_today_celebrations failed") raise RuntimeError(f"scan_today_celebrations: {exc}") from exc await db.commit() return result @register_scheduled_job( key="feed.notify_celebrants_and_leads", display_name="动态广场 推送祝福通知", description=( "紧随扫描任务,给寿星本人 / manager / HRBP 推送站内 + 飞书通知," "每个 (celebration, role) 仅推一次 (notify_state_json 持久化)。" ), ) async def run_notify_celebrants_and_leads( db: AsyncSession, config: dict[str, Any], ) -> dict[str, Any]: try: result = await notify_today_celebrations(db) except Exception as exc: logger.exception("feed.notify_celebrants_and_leads failed") raise RuntimeError(f"notify_celebrants_and_leads: {exc}") from exc await db.commit() return result @register_scheduled_job( key="feed.expire_pinned_announcements", display_name="动态广场 公告自动上下架", description=( "每 10 分钟扫描 feed_announcements,根据 publish_at / expire_at " "翻转 status (draft → published, published → archived)。" ), ) async def run_expire_pinned_announcements( db: AsyncSession, config: dict[str, Any], ) -> dict[str, Any]: try: result = await reconcile_announcement_publication(db) except Exception as exc: logger.exception("feed.expire_pinned_announcements failed") raise RuntimeError(f"expire_pinned_announcements: {exc}") from exc await db.commit() return result