"""Scheduled-job handler for 安全每日一题. * ``secquiz.assign_daily`` — daily. Pre-creates the question-of-the-day assignment for the HQ date so it's ready before the first player. Idempotent (UNIQUE(quiz_date) + get-or-create); the player path also lazily ensures the assignment, so a missed cron run never shows an empty state. Raises on failure so the scheduler marks the run failed (see [[feedback_handler_status_must_raise]]). """ 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.security_quiz.service import ensure_today_assignment from app.domains.security_quiz.team_battle import snapshot_last_week from app.scheduled_jobs.registry import register_scheduled_job logger = logging.getLogger(__name__) _HQ_TZ = ZoneInfo("Asia/Shanghai") @register_scheduled_job( key="secquiz.assign_daily", display_name="安全每日一题 指派今日题目", description=( "每日为 HQ 日期预指派今日安全题(轮换最久未用的在线题目)。" "idempotent(UNIQUE(quiz_date) + get-or-create);玩家路径亦懒加载兜底。" ), ) async def run_assign_daily( db: AsyncSession, config: dict[str, Any], ) -> dict[str, Any]: today = datetime.now(_HQ_TZ).date() try: assignment = await ensure_today_assignment(db, today) except Exception as exc: logger.exception("secquiz.assign_daily failed") raise RuntimeError(f"assign_daily: {exc}") from exc await db.commit() return { "quiz_date": today.isoformat(), "assigned": assignment is not None, "question_id": str(assignment.question_id) if assignment else None, } @register_scheduled_job( key="secquiz.team_battle_snapshot", display_name="安全段位擂台 周快照", description=( "每周一冻结上一个 ISO 周的部门擂台排名到 secquiz_team_snapshots" "(供页面展示上周冠军 + 周环比)。按 (week_start, department) 幂等。" ), ) async def run_team_battle_snapshot( db: AsyncSession, config: dict[str, Any], ) -> dict[str, Any]: try: summary = await snapshot_last_week(db) except Exception as exc: logger.exception("secquiz.team_battle_snapshot failed") raise RuntimeError(f"team_battle_snapshot: {exc}") from exc await db.commit() return summary __all__ = ["run_assign_daily", "run_team_battle_snapshot"]