# -*- coding: utf-8 -*- """ 有效期轮询器(Task Expiry Checker) 每小时运行一次,检查 expires_at 不为 NULL 且已过期的 active 任务, 将其标记为 inactive 并记录 history。 由 trigger_jobs 中的 task_expiry_check 配置驱动。 """ import json import logging logger = logging.getLogger(__name__) def _get_connection(): """延迟导入 get_connection,避免模块级导入失败。""" from app.database import get_connection return get_connection() def _insert_history( cur, task_id: int, action: str, old_status: str | None = None, new_status: str | None = None, old_task_type: str | None = None, new_task_type: str | None = None, detail: dict | None = None, ) -> None: """在 coach_task_history 中记录变更。""" cur.execute( """ INSERT INTO biz.coach_task_history (task_id, action, old_status, new_status, old_task_type, new_task_type, detail) VALUES (%s, %s, %s, %s, %s, %s, %s) """, ( task_id, action, old_status, new_status, old_task_type, new_task_type, json.dumps(detail) if detail else None, ), ) def run() -> dict: """ 有效期轮询主流程。 1. SELECT id, task_type FROM biz.coach_tasks WHERE expires_at IS NOT NULL AND expires_at < NOW() AND status = 'active' 2. 逐条 UPDATE status = 'inactive' 3. INSERT coach_task_history (action='expired') 每条过期任务独立事务,失败不影响其他。 返回: {"expired_count": int} """ expired_count = 0 conn = _get_connection() try: # 查询所有已过期的 active 任务 with conn.cursor() as cur: cur.execute( """ SELECT id, task_type FROM biz.coach_tasks WHERE expires_at IS NOT NULL AND expires_at < NOW() AND status = 'active' """ ) expired_tasks = cur.fetchall() conn.commit() # 逐条处理,每条独立事务 for task_id, task_type in expired_tasks: try: with conn.cursor() as cur: cur.execute("BEGIN") cur.execute( """ UPDATE biz.coach_tasks SET status = 'inactive', updated_at = NOW() WHERE id = %s AND status = 'active' """, (task_id,), ) _insert_history( cur, task_id, action="expired", old_status="active", new_status="inactive", old_task_type=task_type, new_task_type=task_type, ) conn.commit() expired_count += 1 except Exception: logger.exception("处理过期任务失败: task_id=%s", task_id) conn.rollback() finally: conn.close() logger.info("有效期轮询完成: expired_count=%d", expired_count) return {"expired_count": expired_count}