"""比赛/联赛查询路由。""" from __future__ import annotations from datetime import datetime, timedelta from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy import select from sqlalchemy.orm import selectinload from src.api.schemas import MatchListOut, MatchOut from src.db.base import AsyncSession, get_db_read from src.db.models import League, Match router = APIRouter(prefix="/api/v1", tags=["data"]) @router.get("/leagues", response_model=list[dict]) async def list_leagues(db: AsyncSession = Depends(get_db_read)): stmt = select(League).order_by(League.name) result = await db.execute(stmt) leagues = result.scalars().all() return [{"id": l.id, "code": l.code, "name": l.name, "country": l.country} for l in leagues] @router.get("/matches", response_model=MatchListOut) async def list_matches( league: str | None = None, status: str | None = None, date: str | None = None, cursor: str | None = None, limit: int = Query(50, ge=1, le=100), db: AsyncSession = Depends(get_db_read), ): """比赛列表(游标分页)。""" q = select(Match).options(selectinload(Match.league), selectinload(Match.home_team), selectinload(Match.away_team)) if cursor: try: # 用 | 分隔,避免 isoformat 含 _ 时解析失败 last_date_str, last_id_str = cursor.split("|", 1) last_date = datetime.fromisoformat(last_date_str) last_id = int(last_id_str) q = q.where( (Match.match_date < last_date) | ((Match.match_date == last_date) & (Match.id < last_id)) ) except (ValueError, AttributeError): pass if league: stmt = select(League.id).where(League.code == league) league_id = (await db.execute(stmt)).scalar_one_or_none() if league_id is None: return MatchListOut(items=[], next_cursor=None, has_more=False) q = q.where(Match.league_id == league_id) if status: q = q.where(Match.match_status == status) if date: try: d = datetime.strptime(date, "%Y-%m-%d") except ValueError: raise HTTPException(400, "date 格式应为 YYYY-MM-DD") q = q.where(Match.match_date >= d, Match.match_date < d + timedelta(days=1)) rows = (await db.execute(q.order_by(Match.match_date.desc(), Match.id.desc()).limit(limit + 1))).scalars().all() has_more = len(rows) > limit rows = rows[:limit] items = [] for m in rows: items.append(MatchOut( id=m.id, league_code=m.league.code if m.league else None, season=m.season, home_team=m.home_team.name if m.home_team else "?", away_team=m.away_team.name if m.away_team else "?", home_team_zh=m.home_team.name_zh if m.home_team else None, away_team_zh=m.away_team.name_zh if m.away_team else None, match_date=m.match_date, match_status=m.match_status, home_goals=m.home_goals, away_goals=m.away_goals, match_stage=m.match_stage, home_xg=m.stats.home_xg if m.stats else None, away_xg=m.stats.away_xg if m.stats else None, )) next_cursor = None if has_more and items: last = rows[-1] next_cursor = f"{last.match_date.isoformat()}|{last.id}" return MatchListOut(items=items, next_cursor=next_cursor, has_more=has_more) @router.get("/matches/{match_id}", response_model=MatchOut) async def get_match(match_id: int, db: AsyncSession = Depends(get_db_read)): stmt = ( select(Match) .options(selectinload(Match.league), selectinload(Match.home_team), selectinload(Match.away_team)) .where(Match.id == match_id) ) m = (await db.execute(stmt)).scalar_one_or_none() if m is None: raise HTTPException(404, "match not found") return MatchOut( id=m.id, league_code=m.league.code if m.league else None, season=m.season, home_team=m.home_team.name if m.home_team else "?", away_team=m.away_team.name if m.away_team else "?", home_team_zh=m.home_team.name_zh if m.home_team else None, away_team_zh=m.away_team.name_zh if m.away_team else None, match_date=m.match_date, match_status=m.match_status, home_goals=m.home_goals, away_goals=m.away_goals, match_stage=m.match_stage, home_xg=m.stats.home_xg if m.stats else None, away_xg=m.stats.away_xg if m.stats else None, )