This commit is contained in:
2026-06-13 20:00:45 +08:00
parent 09f12c6c47
commit 2bcaab1626
6 changed files with 167 additions and 2 deletions
+72
View File
@@ -0,0 +1,72 @@
"""通知(お知らせ)路由:从 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,
)
+2
View File
@@ -18,6 +18,7 @@ from starlette.exceptions import HTTPException as StarletteHTTPException
from app.api.routes.health import router as health_router
from app.api.routes.scrape import router as scrape_router
from app.api.routes.news import router as news_router
from app.core.config import get_settings
from app.core.container import ServiceContainer
from app.core.errors import AppError
@@ -90,6 +91,7 @@ def create_app() -> FastAPI:
app = FastAPI(title="Surugaya Scraper Service", lifespan=lifespan)
app.include_router(health_router)
app.include_router(scrape_router)
app.include_router(news_router)
@app.exception_handler(AppError)
async def app_error_handler(_: Request, exc: AppError) -> JSONResponse:
+14
View File
@@ -207,3 +207,17 @@ class CheckTaskCallbackUrlData(BaseModel):
"""测试回调地址响应数据"""
response_status: int
response_body: Any
class NewsItemData(BaseModel):
"""单条「お知らせ」通知,由 worker 解析 action_news 页面后写入 Redis。"""
model_config = ConfigDict(extra="allow")
data_id: str # 站点侧通知唯一标识(单调递增)
date: str = "" # 通知日期,如 2026/06/12
message: str = "" # 通知文案,如 【販売】(取引番号:S2606115133)発送準備中です。
data_type: str = "" # 站点 data-type
data_url: str | None = None # 站点 data-url
trade_code: str | None = None # 从 data-url 解析的交易单号(非交易通知为 null)
account_id: str = "" # 该通知所属抓取账号
fetched_at: int = 0 # worker 抓取时间(epoch 秒)