"""通知(お知らせ)路由:从 Redis 读取 worker 抓取的账号通知。 worker(pruchase_jhw)定时抓取骏河屋 action_news 页面,把每条通知按 data_id 作为 field 写入 Redis Hash(key 见 redis_keys.news_hash)。本路由读取该 Hash, 按 data_id 倒序(新→旧)返回,只读不删除。Redis 读取沿用 session_store 复用的连接, 与 scrape 路由保持一致。 """ from __future__ import annotations import json import logging from typing import Any from fastapi import APIRouter, Depends, Query from app.api.dependencies import get_container, require_bearer_token from app.core.container import ServiceContainer from app.models.scrape import ApiResponse, NewsItemData from surugaya_common import redis_keys logger = logging.getLogger(__name__) router = APIRouter(prefix="/api", tags=["news"]) def _news_sort_key(item: dict[str, Any]) -> tuple[int, int]: """排序键:优先按 data_id 数值倒序,非数值退化到最小,保证排序稳定。""" raw = str(item.get("data_id", "")).strip() if raw.isdigit(): return (1, int(raw)) return (0, 0) @router.get( "/news", response_model=ApiResponse[list[NewsItemData]], dependencies=[Depends(require_bearer_token)], ) async def list_news( limit: int | None = Query(default=None, ge=1, le=500, description="返回条数上限,省略则返回全部"), container: ServiceContainer = Depends(get_container), ) -> ApiResponse[list[NewsItemData]]: """读取通知列表(按 data_id 倒序,新→旧),只读不删除。""" if container.session_store._redis is None: return ApiResponse[list[NewsItemData]]( success=False, msg="Redis 未配置", data=None, code=500, ) # dev/prod 由本服务 app_env 推导,需与 worker 写入端(APP_ENV)保持一致。 is_dev = "1" if container.settings.app_env == "dev" else "0" raw_map = await container.session_store._redis.hgetall(redis_keys.news_hash(is_dev)) items: list[dict[str, Any]] = [] for data_id, raw_value in raw_map.items(): try: items.append(json.loads(raw_value)) except (json.JSONDecodeError, TypeError): logger.warning("通知 data_id=%s 的 JSON 解析失败,已跳过。", data_id) items.sort(key=_news_sort_key, reverse=True) if limit is not None and limit > 0: items = items[:limit] return ApiResponse[list[NewsItemData]]( success=True, msg="success", data=[NewsItemData(**item) for item in items], code=0, )