Profeto — 给 LLM 提供数据,让 LLM 预测足球比分。 核心模块: - FastAPI 后端 + PostgreSQL (SQLAlchemy async) - 多 Agent LLM 预测 (5 专家 + 终裁) - 数据采集 (bzzoiro / understat / injuries) - React 前端 (Vite + Tailwind) 包含: - 数据源抽象 (DataSource 协议 + 注册表) - Alembic 数据库迁移 - Prompt 模板 (单/多 Agent) - 核心路径单元测试
124 lines
4.5 KiB
Python
124 lines
4.5 KiB
Python
"""比赛/联赛查询路由。"""
|
|
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,
|
|
)
|