Files
Profeto/src/api/routes/matches.py
T
shangfangjian 0a27b18c27 feat: 足球 LLM 预测服务初始提交
Profeto — 给 LLM 提供数据,让 LLM 预测足球比分。

核心模块:
- FastAPI 后端 + PostgreSQL (SQLAlchemy async)
- 多 Agent LLM 预测 (5 专家 + 终裁)
- 数据采集 (bzzoiro / understat / injuries)
- React 前端 (Vite + Tailwind)

包含:
- 数据源抽象 (DataSource 协议 + 注册表)
- Alembic 数据库迁移
- Prompt 模板 (单/多 Agent)
- 核心路径单元测试
2026-09-09 02:10:47 +08:00

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,
)