"""D - 联赛排名切片: 积分榜快照(支持 cutoff 的历史还原,standings)。""" from __future__ import annotations from typing import TYPE_CHECKING from sqlalchemy import select from sqlalchemy.orm import selectinload from src.db.base import AsyncSessionLocal from src.db.models import League, Standing from src.llm.slices.common import MatchHeader, SliceResult if TYPE_CHECKING: from sqlalchemy.ext.asyncio import AsyncSession async def standings_slice(header: MatchHeader, *, before=None, db: AsyncSession | None = None) -> SliceResult: """D - 联赛排名切片: 两队积分榜位置、积分、近期走势(form)、分区,评估整体实力差距。 P0-02: 支持 cutoff(before)。取 available_at<=cutoff 的每队最新快照 (DISTINCT ON team_id ORDER available_at DESC);before=None 时 cutoff=now()。 回测时可还原历史时刻榜单,不再只是"最新快照、忽略 cutoff"。 db: 可选共享 session(见 context_builder 模块 docstring)。 语义区分: - 两队都有积分榜行 → has_data=True(明确的排名信息) - 任一队缺失 → has_data=False(升班马/杯赛无榜,信息不完整时明确声明) """ # P0-02: before=None → cutoff=now()(取最新可用快照) if before is None: from datetime import datetime, timezone before = datetime.now(timezone.utc) if db is not None: league = (await db.execute(select(League).where(League.id == header.league_id))).scalar_one_or_none() # P0-02: DISTINCT ON (team_id) 取 available_at<=cutoff 的最新快照 rows = ( ( await db.execute( select(Standing) .options(selectinload(Standing.team)) .where(Standing.league_id == header.league_id) .where(Standing.available_at <= before) .distinct(Standing.team_id) .order_by(Standing.team_id, Standing.available_at.desc()) ) ) .scalars() .all() if league else [] ) else: async with AsyncSessionLocal() as new_db: return await standings_slice(header, before=before, db=new_db) lines = [f"── 联赛排名({header.league_name} 共 {len(rows)} 队) ──"] n_records = 0 def _fmt(row) -> str: zg = f" xG差 {row.xg_for - row.xg_against:+.1f}" if row.xg_for is not None and row.xg_against is not None else "" form = f" 近5场 {row.form}" if row.form else "" zone = f" [{row.zone}]" if row.zone else "" return ( f" 第 {row.position} 名: {row.points} 分 / {row.played} 场 " f"({row.won}胜{row.drawn}平{row.lost}负, 进{row.goals_for}失{row.goals_against} 净胜{row.goal_diff:+d}" f"{zg}){form}{zone}" ) for label, team_id in (("主队", header.home_team_id), ("客队", header.away_team_id)): row = next((r for r in rows if r.team_id == team_id), None) if row is None: lines.append(f" {label}: 暂无积分榜数据(可能杯赛/赛季未开始)") else: n_records += 1 lines.append(f" {label} {header.home_name if label == '主队' else header.away_name}:") lines.append(_fmt(row)) # 两队排名对比摘要 home_row = next((r for r in rows if r.team_id == header.home_team_id), None) away_row = next((r for r in rows if r.team_id == header.away_team_id), None) if home_row and away_row: diff = home_row.position - away_row.position # 正数=主队排名更靠前(名次更小) lead = f"主队排名高 {diff} 位" if diff > 0 else (f"客队排名高 {-diff} 位" if diff < 0 else "两队同排名结构") pts_diff = home_row.points - away_row.points lines.append(f" 排名对比: {lead}, 分差 {pts_diff:+d}") # has_data: 两队都有行才算完整;只有一队时仍有价值,但标记不完整 has_data = n_records >= 1 return SliceResult(text="\n".join(lines), has_data=has_data, n_records=n_records)