Files
Profeto/docs/03-api.md
T
WorkBuddy 983363dab7 feat(ingest): 采集任务状态跟踪(ingest_jobs),解决 fire-and-forget 不可观测
- 新表 ingest_jobs(迁移 0019): id UUID/task/params JSONB/
  status(pending|running|success|failed,CheckConstraint)/
  result JSONB(统计摘要)/error/created_at/started_at/finished_at
- POST /ingest/bzzoiro: 启动后台前创建 pending job,响应返回 job_id;
  仍 require_admin。后台 _run_bzzoiro 流转 running→success/failed,
  result 按子任务(events/standings/stats)记录摘要(errors 截断 10 条)
- _update_job 尽力而为: 状态更新失败只记日志,绝不拖垮采集主流程;
  与 IngestFailure 死信独立(行级 vs 任务级,可同时存在)
- 新增 admin 端点(挂 /api/v1/admin 路由,路由级 require_admin):
  GET /admin/ingest/jobs/{job_id} 与 GET /admin/ingest/jobs?limit&status
- 前端采集页: 提交后凭 job_id 3 秒轮询,终态展示结果摘要/失败原因;
  无 job_id 时回退旧的 30 秒盲等 + 系统日志提示
- 测试 11 项: 建 job+job_id 契约、非法 task 422、成功/失败/all 流转、
  update 失败不拖垮采集、部分失败仍 success、admin 端点 200/404/列表、
  结构守护(job 路由在 admin 路由且带 require_admin)
- 禁止项确认: 未动分批 UoW、BzzoiroSource、死信与 Bronze 写入;
  docs(01/03/05/07/README)同步 13 张表与端点说明
2026-09-21 21:19:21 +08:00

254 lines
8.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 03 · API 参考
Base URL: `http://localhost:8000` · 交互式文档: `/docs`(Swagger)与 `/redoc`
所有数据端点返回 JSON。错误统一为 `{"detail": "<message>"}` + 对应 HTTP 状态码。
## 鉴权模型
| 级别 | 端点 | 说明 |
|---|---|---|
| **公开只读** | `GET /leagues``GET /matches``GET /matches/{id}``GET /matches/{id}/context``GET /standings``GET /health*` | 无需任何凭据;公开站直接调用 |
| **公开 + 限流** | `POST /predict` | 内存滑动窗口限流(10 次/分钟/IP) |
| **需管理员** | `GET /predictions*``POST /ingest/bzzoiro``/eval/*``POST /backtest``/admin/**` | Cookie 会话(`POST /auth/login` 颁发)或 `X-API-Key` 头 |
管理端点在生产环境未配置鉴权时 fail-closed(503),不会静默放行。
---
## 数据查询(公开只读)
### `GET /api/v1/leagues`
列出已入库联赛(P1-3: 公开站联赛筛选动态加载来源)。
```json
[{"id": 1, "code": "E0", "name": "Premier League", "country": "England"}]
```
仅返回 `id/code/name/country` 四个展示字段,不含任何配置或密钥信息。
### `GET /api/v1/matches`
比赛列表,游标分页。
| 参数 | 说明 |
|---|---|
| `league` | 联赛代码,如 `E0` / `SP1` / `D1` / `I1` / `F1` |
| `status` | `scheduled` / `finished`(不传 = 全部) |
| `date` | `YYYY-MM-DD`,当天比赛 |
| `cursor` | 上一页返回的 `next_cursor` |
| `limit` | 1100,默认 50 |
响应(倒序,含双方中文名与 xG):
```json
{
"items": [
{"id": 42, "league_code": "E0", "season": "2026-2027",
"home_team": "Arsenal", "away_team": "Manchester United",
"home_team_zh": null, "away_team_zh": null,
"match_date": "2026-09-15T19:00:00Z", "match_status": "scheduled",
"home_goals": null, "away_goals": null,
"match_stage": "第 5 轮", "home_xg": null, "away_xg": null}
],
"next_cursor": "2026-09-15T19:00:00+00:00|41",
"has_more": true
}
```
### `GET /api/v1/matches/{id}`
单场比赛详情,字段同上,另含 `stats`(统计)与 `recent_predictions`(最近 5 条预测摘要)。
### `GET /api/v1/matches/{id}/context`
比赛上下文(公开只读,P1-2: 公开站详情页「近况/交锋」数据来源;不触发 LLM):
- `home_recent`: 主队最近 5 场已完赛
- `away_recent`: 客队最近 5 场已完赛
- `h2h`: 双方最近 5 次交手
```json
{
"home_recent": [
{"match_date": "2026-09-12T14:00:00+00:00", "home_team": "阿森纳",
"away_team": "切尔西", "home_goals": 2, "away_goals": 1}
],
"away_recent": [],
"h2h": []
}
```
数据不足时对应列表为空(前端展示空态)。比赛不存在返回 404。
### `GET /api/v1/standings`
联赛积分榜(公开只读)。参数:`league`(联赛代码,空 = 全部)、`season`(空 = 各联赛最新赛季)。
```json
{"leagues": [
{"league_code": "E0", "league_name": "Premier League", "season": "2026-2027",
"retrieved_at": "2026-09-20T08:00:00+00:00",
"rows": [{"position": 1, "team": "阿森纳", "team_en": "Arsenal",
"played": 5, "won": 4, "drawn": 1, "lost": 0,
"goals_for": 11, "goals_against": 3, "goal_diff": 8,
"points": 13, "xg_for": 9.8, "xg_against": 3.9,
"form": "WWWDW", "zone": "UEFA Champions League"}]}
]}
```
---
## 预测
### `POST /api/v1/predict`
对一场比赛做 LLM 预测。**核心端点。**
请求:
```json
{
"match_id": 42,
"mode": "multi",
"model": "gpt-4o",
"prompt_version": "v1"
}
```
| 字段 | 默认 | 说明 |
|---|---|---|
| `match_id` | 必填 | 比赛 ID |
| `mode` | `multi` | `multi` = 5 专家 + 终裁;`single` = 单次调用 |
| `model` | 配置值 | 覆盖本次模型(single 模式下生效) |
| `prompt_version` | `v1` | prompt 版本(multi 模式即 agent prompt 版本) |
响应(multi 模式):
```json
{
"prediction_id": 7,
"provider": "openai",
"model": "gpt-4o",
"prompt_version": "multi_v1",
"mode": "multi",
"pred_home_goals": 2.1,
"pred_away_goals": 1.0,
"pred_1x2": "1",
"subjective_confidence": 0.68,
"reasoning": "综合 xg 报告的进球期望 2.1-1.0 与 form 报告的三连胜势头……",
"agent_outputs": [
{"agent": "h2h", "status": "ok", "data_sufficiency": "medium",
"analysis": "近 5 次交锋主队 3 胜……", "home_edge": 0.4,
"subjective_confidence": 0.7, "key_evidence": ["近5次交锋主队3胜", "主场交锋3连胜"],
"exp_home_goals": null, "exp_away_goals": null, "probable_score": null,
"model": "gpt-4o-mini", "latency_ms": 2100,
"prompt_tokens": 380, "completion_tokens": 120},
{"agent": "standings", "status": "no_data", "data_sufficiency": "none",
"analysis": "该维度无数据,跳过分析。", "home_edge": null, "subjective_confidence": null,
"key_evidence": [], "exp_home_goals": null, "exp_away_goals": null,
"probable_score": null, "model": "", "latency_ms": null,
"prompt_tokens": null, "completion_tokens": null}
],
"agent_weights": {"form": 0.9, "stats": 0.8, "home_away": 0.7, "standings": 0.8, "h2h": 0.8},
"context": "[5 份报告的 JSON 串]",
"latency_ms": 9800
}
```
`agent.status` 取值:`ok` / `no_data`(维度无数据,已跳过 LLM)/ `error`(调用失败,fail-open 不阻断)/ `parse_error`
错误:404 比赛不存在;502 LLM 调用失败(终裁失败时整体失败,专家失败不会)。
### `GET /api/v1/predictions?match_id=&limit=`(需管理员)
预测历史(倒序),含 `settled` 与实际比分回填状态。
### `GET /api/v1/predictions/{id}`(需管理员)
单条预测详情(含完整 `agent_outputs`)。
---
## 数据采集
### `POST /api/v1/ingest/bzzoiro`(需管理员)
从 bzzoiro(唯一数据源)采集数据并入库(幂等,重复跑安全)。任务在后台异步执行,请求立即返回。
```json
{"task": "all", "leagues": ["E0", "SP1"], "date_from": "2025-08-01", "date_to": "2026-09-08", "status": "finished"}
```
| 字段 | 默认 | 说明 |
|---|---|---|
| `task` | `events` | 采集任务:`events`(比赛)/ `standings`(积分榜)/ `stats`(统计回填,含 xG)/ `all` |
| `leagues` | 全部已知联赛 | 联赛代码列表,如 `["E0", "SP1"]` |
| `date_from` / `date_to` | 空 | 日期范围(`YYYY-MM-DD`) |
| `status` | 空(两者都采) | `finished`(已完赛)/ `scheduled`(未来赛程) |
| `season` | 当前赛季 | standings 赛季,如 `"2026-2027"` |
| `limit` | 100 | stats 回填单次最大比赛数(1500) |
- 响应:`{"ok": true, "job_id": "<uuid>", "message": "……"}`,`job_id` 用于查询任务状态
- `task=stats` 只补空字段、不创建比赛(xG/射门/控球等统计回填)
### `GET /api/v1/admin/ingest/jobs/{job_id}`(需管理员)
查询一次采集任务的状态(`ingest_jobs` 表,任务级可观测性):
```json
{"id": "…", "task": "standings", "params": {"leagues": ["E0"]},
"status": "success", "result": {"total_upserted": 20, "errors": []},
"error": null, "created_at": "…", "started_at": "…", "finished_at": "…"}
```
- `status` 取值:`pending`(已创建未开始)/ `running` / `success` / `failed`
- 管理端采集页提交后凭 `job_id` 轮询本端点直至终态
-`ingest_failures` 死信独立:死信记录单条管线抓取失败(行级),job 记录整次任务结果
### `GET /api/v1/admin/ingest/jobs?limit=20&status=`(需管理员)
列出最近采集任务(最新在前),可按 `status` 过滤,`limit` 1100。
> 历史版本曾有独立的 understat(xG)与 injuries(伤停)采集端点,
> 已随数据源收敛为 bzzoiro 唯一来源而移除。
---
## 评估
### `POST /api/v1/eval/settle`(需管理员)
赛后回填实际比分:
```json
{"prediction_id": 7, "home_goals": 2, "away_goals": 1}
```
### `GET /api/v1/eval/summary`(需管理员)
`provider × model` 聚合已结算预测:
```json
{"summary": [
{"provider": "openai", "model": "gpt-4o", "total": 12,
"accuracy_1x2": 58.3, "avg_score_rmse": 1.21, "avg_subjective_confidence": 0.65}
]}
```
> 提示:multi 模式存的 `model` 是终裁模型、`prompt_version` 是 `multi_v1`,
> 因此 summary 里天然可对比 multi vs single、以及不同 prompt 版本的效果。
---
## 基础
| 端点 | 权限 | 说明 |
|---|---|---|
| `GET /health` | 公开 | 存活检查 |
| `GET /docs``GET /redoc` | 公开 | Swagger UI / ReDoc |
| `POST /auth/login``POST /auth/logout``GET /auth/me` | 公开 | 管理员 Cookie 会话登录/登出/当前用户 |
| `POST /auth/change-password` | 需管理员 | 修改管理员密码 |
| `POST /backtest` | 需管理员 | 历史回测(对已完赛比赛批量预测并评估) |