feat: 足球 LLM 预测服务初始提交
Profeto — 给 LLM 提供数据,让 LLM 预测足球比分。 核心模块: - FastAPI 后端 + PostgreSQL (SQLAlchemy async) - 多 Agent LLM 预测 (5 专家 + 终裁) - 数据采集 (bzzoiro / understat / injuries) - React 前端 (Vite + Tailwind) 包含: - 数据源抽象 (DataSource 协议 + 注册表) - Alembic 数据库迁移 - Prompt 模板 (单/多 Agent) - 核心路径单元测试
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
"""比赛/联赛查询路由。"""
|
||||
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,
|
||||
)
|
||||
Reference in New Issue
Block a user