# qzData API 接口文档

本文档对应当前 API 实现，覆盖前端控制台需要的执行观测、历史查询和 lane 触发能力。

本地联调默认地址：
`http://127.0.0.1:4556`

## 鉴权

认证覆盖范围：**仅 `/platforms` 系列接口**。

| 路径 | 需要认证 |
| --- | --- |
| `GET /health` | 否 |
| `GET /platforms` | 是 |
| `POST /platforms/trigger-batch` | 是 |
| `POST /platforms/{name}/trigger` | 是 |
| `GET /platforms/{name}/status` | 是 |
| `GET /platforms/{name}/history` | 是 |
| `POST /platforms/config/reload` | 是 |

> `platforms_router` 统一挂载在 `/platforms` 前缀下，并注入 `verify_emi_token` 依赖。
> 若未显式传 Bearer Token，且服务端已配置 EMI `username` / `password`，会自动登录。

有认证接口的请求头：

```http
Authorization: Bearer <EMI_TOKEN>
Content-Type: application/json
```

若未传 `Authorization` 头，且配置中填写了 EMI `username`/`password`，服务端会**自动用配置账号登录**，无需手动传 Token。

---

## 1. 平台摘要

### `GET /platforms`

返回所有平台的摘要状态，适合作为首页卡片或列表数据源。

> 需要认证。未传 Token 时若配置了 EMI 账号密码，服务端自动登录。

### 返回示例

```json
{
  "vivo": {
    "status": "idle",
    "running": false,
    "locked": false,
    "modules": {
      "account_profile": true,
      "account_balance": true,
      "plan": true,
      "creative": true
    },
    "module_semantics": {
      "account_profile": {
        "enabled": true,
        "lane": "account",
        "alias_of": "account",
        "implemented": true
      }
    },
    "lane_model": {
      "account_stage": true,
      "account_lane": {
        "modules": ["account_profile", "account_balance"]
      },
      "plan_lane": {
        "modules": ["plan"]
      },
      "creative_lane": {
        "modules": ["creative"],
        "implemented": true,
        "enabled": true
      }
    },
    "trigger_cooldown_remaining_s": 0,
    "last_run": {
      "platform": "vivo",
      "lane": "plan",
      "module": "plan",
      "status": "success",
      "duration_ms": 8421
    },
    "stats_20": {
      "run_count": 20,
      "success_count": 19,
      "failure_count": 1,
      "success_rate": 0.95,
      "avg_duration_ms": 9033
    },
    "lanes": {
      "plan": {
        "status": "success",
        "running": false,
        "last_started_at": "2026-04-14T02:10:01+00:00",
        "last_finished_at": "2026-04-14T02:10:09+00:00",
        "last_success_at": "2026-04-14T02:10:09+00:00",
        "last_failure_at": null,
        "last_duration_ms": 8421,
        "stats_20": {
          "run_count": 20,
          "success_count": 19,
          "failure_count": 1,
          "success_rate": 0.95,
          "avg_duration_ms": 9033
        }
      }
    },
    "account_counts": {
      "total": 126,
      "running": 0,
      "cooldown": 4,
      "failed_recently": 2
    },
    "execution": { ... },
    "execution_summary": { ... },
    "next_trigger_at": "2026-04-14T02:27:00+00:00"
  }
}
```

### 平台摘要额外字段

| 字段 | 说明 |
| --- | --- |
| `execution` | 执行上下文详情（调度器提供，可选） |
| `execution_summary` | 执行摘要（调度器提供，可选） |
| `next_trigger_at` | 下一次定时触发时间（ISO 8601） |

---

## 2. 单平台状态

### `GET /platforms/{name}/status`

返回平台级摘要与账号级状态。**需要认证**。

### Query 参数

| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `lane` | string | 否 | `account` / `plan` / `creative`，过滤响应中的 module_semantics / lane_model |
| `account` | string | 否 | 账号 scope 或 `account_key` 过滤 |

> `lane` 参数为查询参数，非路径参数。

### 返回字段

| 字段 | 说明 |
| --- | --- |
| `platform` | 平台名称 |
| `status` | 平台整体状态（`running` / `idle`） |
| `running` | 是否运行中 |
| `locked` | 是否被定时任务锁住 |
| `lanes` | 各 lane 的最近状态 |
| `summary` | 平台级最近执行摘要（来自调度器的增强格式） |
| `accounts` | 账号级状态数组（来自调度器的增强格式） |
| `module_semantics` | 模块语义元信息 |
| `lane_model` | lane 模型描述 |
| `execution` | 执行上下文详情 |
| `execution_summary` | 执行摘要 |
| `generated_at` | 响应生成时间 |

### `summary.account_counts`

| 字段 | 说明 |
| --- | --- |
| `total` | 已被观测到的账号总数 |
| `running` | 当前有运行中 lane 的账号数 |
| `cooldown` | 当前命中冷却的账号数 |
| `failed_recently` | 最近一次账号 lane 状态为失败的账号数 |

### `accounts[].lanes.{lane}`

| 字段 | 说明 |
| --- | --- |
| `status` | 当前 lane 快照状态（`running` / `success` / `failed` / `cooldown`） |
| `running` | 是否运行中 |
| `last_started_at` | 最近开始时间 |
| `last_finished_at` | 最近结束时间 |
| `last_success_at` | 最近成功时间 |
| `last_failure_at` | 最近失败时间 |
| `last_duration_ms` | 最近一次耗时 |
| `stats_20` | 最近 20 次统计 |
| `cooldown` | 冷却详情（若存在） |
| `last_run` | 最近一次 run 记录 |

### 返回示例

```json
{
  "platform": "honor",
  "status": "idle",
  "running": false,
  "locked": false,
  "summary": {
    "platform": "honor",
    "last_run": {
      "platform": "honor",
      "scope_type": "account",
      "scope_key": "acct_10001",
      "lane": "plan",
      "module": "plan",
      "status": "success",
      "duration_ms": 5320
    },
    "stats_20": {
      "run_count": 20,
      "success_count": 18,
      "failure_count": 2,
      "success_rate": 0.9,
      "avg_duration_ms": 6130
    },
    "lanes": {
      "plan": {
        "status": "success",
        "running": false
      }
    },
    "account_counts": {
      "total": 42,
      "running": 0,
      "cooldown": 3,
      "failed_recently": 1
    }
  },
  "accounts": [
    {
      "account_key": "acct_10001",
      "account_refs": {
        "customerId": "10001",
        "accountId": "10001"
      },
      "display_name": "10001",
      "updated_at": "2026-04-14T02:10:09+00:00",
      "lanes": {
        "plan": {
          "status": "success",
          "running": false,
          "last_success_at": "2026-04-14T02:10:09+00:00",
          "last_duration_ms": 5320,
          "stats_20": {
            "run_count": 8,
            "success_count": 7,
            "failure_count": 1,
            "success_rate": 0.875,
            "avg_duration_ms": 4988
          }
        }
      },
      "cooldowns": []
    }
  ],
  "generated_at": "2026-04-14T02:12:00+00:00"
}
```

---

## 3. 单平台历史

### `GET /platforms/{name}/history`

返回平台执行历史，可按 lane、account、date 过滤。**需要认证**。

### Query 参数

| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `lane` | string | 否 | `account` / `plan` / `creative` |
| `account` | string | 否 | `account_key` 或原始 scope |
| `date` | string | 否 | 业务日期，`YYYY-MM-DD` |
| `limit` | int | 否 | 1 到 20，默认 20 |

### 返回示例

```json
{
  "platform": "vivo",
  "lane": "plan",
  "account": "acct_1",
  "date": "2026-04-14",
  "limit": 20,
  "total": 2,
  "records": [
    {
      "run_id": "vivo:account:acct_1:plan:2026-04-14T02:10:01+00:00",
      "platform": "vivo",
      "scope_type": "account",
      "scope_key": "acct_1",
      "account_key": "acct_1",
      "lane": "plan",
      "module": "plan",
      "status": "success",
      "started_at": "2026-04-14T02:10:01+00:00",
      "finished_at": "2026-04-14T02:10:09+00:00",
      "duration_ms": 8000,
      "resolved_date": "2026-04-14",
      "updated_at": "2026-04-14T02:10:09+00:00"
    }
  ]
}
```

---

## 4. 单平台触发

### `POST /platforms/{name}/trigger`

单平台触发支持两种模式：传 `modules` 或传 `lane`，二者互斥。**需要认证**。

### 请求体

```json
{
  "date": "2026-04-14",
  "lane": "plan",
  "refresh": false
}
```

或（modules 模式）：

```json
{
  "date": "2026-04-14",
  "modules": ["plan", "metrics"],
  "refresh": true
}
```

### `lane -> modules` 语义

| lane | 触发模块 |
| --- | --- |
| `account` | 优先 `["account"]`；若平台关闭 `account` 但启用 `account_profile` / `account_balance`，则触发所有启用的 account-lane 模块 |
| `plan` | `["plan"]` |
| `creative` | `["creative"]` |

### 成功响应

```json
{
  "status": "triggered",
  "lane": "plan"
}
```

### 错误状态

| HTTP 状态码 | 说明 |
| --- | --- |
| `400` | 参数非法、`lane` 不支持、`lane` 与 `modules` 同时传、模块未启用 |
| `404` | 平台不存在 |
| `409` | 平台当前有任务运行，命中平台级锁 |
| `429` | 命中手动触发冷却 |

---

## 5. 多平台单 Lane 触发

### `POST /platforms/trigger-batch`

按单一 lane 对多个平台批量触发，接口内逐平台尝试触发，并立即返回每个平台的即时结果。**需要认证**。

### 请求体

```json
{
  "platforms": ["vivo", "honor", "xiaomi"],
  "lane": "account",
  "date": "2026-04-14",
  "refresh": false
}
```

### 成功响应

```json
{
  "lane": "account",
  "requested_platforms": ["vivo", "honor", "xiaomi"],
  "results": [
    {
      "platform": "vivo",
      "status": "triggered",
      "detail": "triggered"
    },
    {
      "platform": "honor",
      "status": "locked",
      "detail": "定时任务正在运行中，请稍后再试"
    },
    {
      "platform": "xiaomi",
      "status": "cooldown",
      "detail": "触发冷却中，请 9 秒后重试",
      "cooldown_remaining_s": 9
    }
  ]
}
```

### `results[].status` 枚举

| 状态 | 说明 |
| --- | --- |
| `triggered` | 成功下发触发 |
| `locked` | 平台当前有任务在运行，未触发 |
| `cooldown` | 命中手动触发冷却，未触发 |
| `unsupported_lane` | 平台不支持该 lane |
| `disabled_lane` | 该 lane 已配置禁用 |
| `not_found` | 平台不存在 |
| `invalid_request` | 请求参数非法 |
| `failed` | 触发过程中出现未预期异常 |

---

## 6. 配置热重载

### `POST /platforms/config/reload`

重载配置并刷新调度器 schedule。**需要认证**。

### 响应

```json
{
  "status": "reloaded"
}
```

---

## 7. 健康检查

### `GET /health`

无需认证。返回服务健康状态。

```json
{
  "status": "ok"
}
```

---

## 8. 一键触发全部平台

如果前端需要“点一个按钮，触发全部平台的账户余额 / 计划 / 创意”，推荐两种实现：

### 方案 A：计划 / 创意走批量接口

- 计划：`POST /platforms/trigger-batch`，请求体 `{"platforms":[...],"lane":"plan","refresh":false}`
- 创意：`POST /platforms/trigger-batch`，请求体 `{"platforms":[...],"lane":"creative","refresh":false}`

### 方案 B：账户余额走逐平台接口

批量接口只接受 `lane`，不能直接传 `modules=["account_balance"]`。

因此“账户余额”按钮建议按平台逐个调用：

1. 先 `GET /platforms` 读取各平台 `module_semantics`
2. 若平台启用了 `account_balance`，调用：

```json
POST /platforms/{name}/trigger
{
  "modules": ["account_balance"],
  "refresh": true
}
```

3. 若平台未单独实现 `account_balance`，则回退为：

```json
POST /platforms/{name}/trigger
{
  "lane": "account",
  "refresh": true
}
```

> 当前 `vivo`、`honor`、`xiaomi`、`oppo` 支持单独触发 `account_balance`。
> 其它平台应回退到 `lane=account`。

### 现成页面

`doc/app` 目录已提供独立页面：

- `/app/batch-trigger.html`

页面内可直接点击按钮触发：

- 全平台账户余额
- 全平台计划
- 全平台创意

并展示逐平台返回结果。

---

## 接口路径速查

| 方法 | 路径 | 认证 | 说明 |
| --- | --- | --- | --- |
| `GET` | `/health` | 否 | 健康检查 |
| `GET` | `/platforms` | **是** | 平台摘要 |
| `POST` | `/platforms/trigger-batch` | **是** | 多平台批量触发 |
| `GET` | `/platforms/{name}/status` | **是** | 单平台状态 |
| `GET` | `/platforms/{name}/history` | **是** | 单平台历史 |
| `POST` | `/platforms/{name}/trigger` | **是** | 单平台触发 |
| `POST` | `/platforms/config/reload` | **是** | 配置热重载 |

---

## 前端接入建议

- 前端看板（/platforms 系列）需携带 Bearer Token；未配置时服务端自动登录。
- 批量触发（`/platforms/trigger-batch`）和单平台触发（`/platforms/{name}/trigger`）都走 `/platforms` 认证体系。
- 首页先请求 `GET /platforms`（需认证），展示平台摘要、lane 摘要与账号数。
- 点击平台进入详情后，请求 `GET /platforms/{name}/status`。
- 历史抽屉或明细表请求 `GET /platforms/{name}/history`。
- 手动执行入口优先传 `lane`，仅在需要兼容旧逻辑时传 `modules`。
- “账户余额”若要精确到 `account_balance`，应逐平台判断能力后调用单平台 trigger，不建议直接用 batch lane 代替。
- 批量触发后，前端应按 `results[].status` 做逐平台反馈，不要假设全部成功。
