fix(P1-A/P1-B): ingest 联赛级计数修正 + 非法 cursor 400

P1-A: ingest 联赛级 inserted/updated 读 r["leagues"][code] 而非顶层 r.get("inserted");
抽 _accumulate_ingest_result 纯函数 + 合约测试(4/4)。

P1-B: 非法 cursor 不再静默忽略,返回 400 + detail.code=INVALID_CURSOR;
抽 _parse_cursor 纯函数 + 测试(8/8)。

全量 307 通过。
This commit is contained in:
shangfangjian
2026-09-22 03:31:40 +08:00
parent 41cb2edd47
commit 5ff43d4984
2 changed files with 104 additions and 20 deletions
+29 -20
View File
@@ -14,6 +14,20 @@ from src.db.models import League, Match, MatchStats, Prediction, Standing, Team
router = APIRouter(prefix="/api/v1", tags=["data"])
def _parse_cursor(cursor: str) -> tuple[datetime, int]:
"""P1-B: 解析游标。非法格式 → HTTPException(400, code=INVALID_CURSOR)。"""
try:
last_date_str, last_id_str = cursor.split("|", 1)
last_date = datetime.fromisoformat(last_date_str)
last_id = int(last_id_str)
return last_date, last_id
except (ValueError, AttributeError) as e:
raise HTTPException(
status_code=400,
detail={"code": "INVALID_CURSOR", "message": f"非法游标格式: {cursor}(应为 date_iso|id)"},
) from e
def _stats_dict(stats) -> dict | None:
"""把 MatchStats ORM 对象序列化为前端可读的扁平 dict。"""
if stats is None:
@@ -65,26 +79,21 @@ async def list_matches(
)
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)
# 游标方向必须与排序方向一致:
# - scheduled(ASC):取「更大」的未开赛场次
# - 其它(DESC):取「更小」的已赛场次
if status == "scheduled":
q = q.where(
(Match.match_date > last_date) |
((Match.match_date == last_date) & (Match.id > last_id))
)
else:
q = q.where(
(Match.match_date < last_date) |
((Match.match_date == last_date) & (Match.id < last_id))
)
except (ValueError, AttributeError):
pass
# P1-B: 解析非法 → 400 + code=INVALID_CURSOR,而非静默忽略
last_date, last_id = _parse_cursor(cursor)
# 游标方向必须与排序方向一致:
# - scheduled(ASC):取「更大」的未开赛场次
# - 其它(DESC):取「更小」的已赛场次
if status == "scheduled":
q = q.where(
(Match.match_date > last_date) |
((Match.match_date == last_date) & (Match.id > last_id))
)
else:
q = q.where(
(Match.match_date < last_date) |
((Match.match_date == last_date) & (Match.id < last_id))
)
if league:
stmt = select(League.id).where(League.code == league)