import json import time from typing import Any from loguru import logger from src.tasks import surugaya_auth from src.tasks.base import BasePurchaseTask from src.config.accounts import get_account_config from src.parsers.surugaya_news import collect_all_news from src.task_queue.redis_manager import get_redis_client # 「お知らせ一覧」页面地址。 NEWS_URL = "https://www.suruga-ya.jp/pcmypage/action_news" class SurugayaNewsMonitorTask(BasePurchaseTask): """ 骏河屋「お知らせ一覧」通知抓取任务。 入参约定: - topic: surugaya_news - purchase_account: 抓取通知所属账号 - payload: 无额外字段 行为:打开 action_news 页面(必要时自动登录),解析首页最新通知, 按 data_id 去重写入 Redis Hash(field=data_id),供中心 API 读取。 """ async def execute(self) -> dict[str, Any]: self.report_event = "surugaya_news" self.is_report = False logger.debug(f"[任务 {self.task_id}] 骏河屋通知抓取工作流已启动。") if not self.page: raise ValueError("页面对象未初始化,无法执行通知抓取任务。") page = self.page await page.goto(NEWS_URL, wait_until="domcontentloaded", timeout=30000) # 登录兜底:长时间运行登录态可能过期,未登录则自动登录后重新进入。 if not await self._ensure_login(page): raise RuntimeError(f"账号 '{self.account_id}' 登录失败,无法抓取通知。") news_items = await collect_all_news(page) logger.info(f"[任务 {self.task_id}] 账号 '{self.account_id}' 翻页解析到 {len(news_items)} 条通知。") written = await self._save_news_to_redis(news_items) return { "account_id": self.account_id, "parsed_count": len(news_items), "written_count": written, } async def _notify_task_failed(self) -> None: """通知抓取是每 5 分钟运行的常规轮询,单次失败下轮即重试,不发失败告警邮件, 避免登录抖动等偶发问题造成告警刷屏(失败现场截图仍由基类保留)。""" logger.warning( f"[任务 {self.task_id}] 通知抓取失败(不告警,等待下一轮重试): {self.error_message}" ) async def _ensure_login(self, page) -> bool: log_prefix = f"[任务 {self.task_id}] " if await surugaya_auth.check_login_status(page, log_prefix=log_prefix): return True logger.warning(f"{log_prefix}账号 '{self.account_id}' 未登录,执行登录流程。") account_config = get_account_config("surugaya", self.account_id) if not account_config: raise ValueError(f"未找到骏河屋账号配置: {self.account_id}") await surugaya_auth.surugaya_login(page, account_config, log_prefix=log_prefix) await page.goto(NEWS_URL, wait_until="domcontentloaded", timeout=30000) return await surugaya_auth.check_login_status(page, log_prefix=log_prefix) async def _save_news_to_redis(self, news_items: list[dict[str, Any]]) -> int: """按 data_id 作为 field 写入 Redis Hash;返回写入条数。""" if not news_items: return 0 redis_client = get_redis_client() hash_key = self.settings.redis_news_hash_key fetched_at = int(time.time()) mapping = { item["data_id"]: json.dumps( {**item, "account_id": self.account_id, "fetched_at": fetched_at}, ensure_ascii=False, ) for item in news_items } await redis_client.hset(hash_key, mapping=mapping) logger.info( f"[任务 {self.task_id}] 已将 {len(mapping)} 条通知写入 Redis Hash '{hash_key}'。" ) return len(mapping)