scheduler.py 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. from __future__ import annotations
  2. import asyncio
  3. import logging
  4. import os
  5. from datetime import datetime, timedelta, timezone
  6. from apscheduler.schedulers.asyncio import AsyncIOScheduler
  7. from apscheduler.triggers.interval import IntervalTrigger
  8. from apscheduler.triggers.cron import CronTrigger
  9. from app.db import get_pool
  10. from app.services.scraper import ScraperService
  11. from app.services.key_pool_pusher import push_all_groups_to_pool
  12. logger = logging.getLogger(__name__)
  13. _scheduler = AsyncIOScheduler()
  14. _scraper = ScraperService()
  15. JOB_ID = "auto_scrape"
  16. JOB_ID_POOL_PUSH = "pool_push"
  17. async def _run_auto_scrape() -> None:
  18. """爬取所有已注册模型。"""
  19. pool = get_pool()
  20. async with pool.acquire() as conn:
  21. cfg = await conn.fetchrow("SELECT * FROM scrape_schedule WHERE id = 1")
  22. if not cfg or not cfg["enabled"]:
  23. return
  24. async with pool.acquire() as conn:
  25. rows = await conn.fetch("SELECT url FROM models ORDER BY created_at")
  26. urls = [r["url"] for r in rows]
  27. if not urls:
  28. logger.info("[scheduler] 没有已注册的模型,跳过")
  29. return
  30. logger.info(f"[scheduler] 开始自动爬取 {len(urls)} 个模型")
  31. job_id_row = await pool.fetchrow(
  32. "INSERT INTO scrape_jobs (urls, status) VALUES ($1, 'pending') RETURNING id",
  33. urls,
  34. )
  35. asyncio.create_task(_scraper.run_job(str(job_id_row["id"]), urls, pool))
  36. def _reschedule(interval_days: int, start_hour: int) -> None:
  37. if _scheduler.get_job(JOB_ID):
  38. _scheduler.remove_job(JOB_ID)
  39. # start_hour 是用户输入的本地时间(UTC+8),转换为 UTC
  40. tz_offset = int(os.environ.get("TZ_OFFSET_HOURS", "8"))
  41. start_hour_utc = (start_hour - tz_offset) % 24
  42. now = datetime.now(timezone.utc)
  43. next_run = now.replace(hour=start_hour_utc, minute=0, second=0, microsecond=0)
  44. if next_run <= now:
  45. next_run += timedelta(days=1)
  46. _scheduler.add_job(
  47. _run_auto_scrape,
  48. trigger=IntervalTrigger(days=interval_days, start_date=next_run),
  49. id=JOB_ID,
  50. replace_existing=True,
  51. )
  52. logger.info(f"[scheduler] 已设置每 {interval_days} 天本地 {start_hour}:00 (UTC {start_hour_utc}:00) 执行,下次: {next_run}")
  53. async def start_scheduler() -> None:
  54. pool = get_pool()
  55. async with pool.acquire() as conn:
  56. cfg = await conn.fetchrow("SELECT * FROM scrape_schedule WHERE id = 1")
  57. interval = cfg["interval_days"] if cfg else 1
  58. start_hour = cfg["start_hour"] if cfg else 2
  59. _reschedule(interval, start_hour)
  60. # 添加号池推送定时任务(每天凌晨 3 点推送一次)
  61. tz_offset = int(os.environ.get("TZ_OFFSET_HOURS", "8"))
  62. start_hour_utc = (3 - tz_offset) % 24
  63. _scheduler.add_job(
  64. _run_pool_push,
  65. trigger=CronTrigger(hour=start_hour_utc, minute=0),
  66. id=JOB_ID_POOL_PUSH,
  67. replace_existing=True,
  68. )
  69. _scheduler.start()
  70. logger.info(f"[scheduler] 调度器已启动(号池推送每天凌晨 3:00 本地时间 / UTC {start_hour_utc}:00)")
  71. async def _run_pool_push() -> None:
  72. """定时推送号池数据"""
  73. pool = get_pool()
  74. logger.info("[scheduler] 开始推送号池数据到company_base_pool")
  75. await push_all_groups_to_pool(pool)
  76. async def stop_scheduler() -> None:
  77. if _scheduler.running:
  78. _scheduler.shutdown(wait=False)
  79. def get_scheduler() -> AsyncIOScheduler:
  80. return _scheduler
  81. def reschedule(interval_days: int, start_hour: int) -> None:
  82. _reschedule(interval_days, start_hour)