> **本文档属于 Cloud WAF — 企业 Web 防护管理平台**
> CLI 工具：`zcloud` · 5 大模块：guard / sys / analytics / cli_release / auth
> 完整 API 索引：[/api/openapi.json](/api/openapi.json) · 文档地图：[/sitemap.xml](/sitemap.xml) · AI 速读：[/llms.txt](/llms.txt)

---

# API 文档

> 商业级 REST API 文档 · 57 个 endpoint · 80+ 图表数据接口（chart-key）· 双通道鉴权
> 适用对象：客户对接工程师、SRE、SaaS 集成商、AI agent
> 阅读顺序：先看 [§0 总体约定](#0-总体约定) → [§1 鉴权](#1-鉴权先读) → 再按业务模块跳转

---

## 目录

**必读**

- [§0 总体约定](#0-总体约定) — 协议 / 信封 / HTTP 状态码 / 分页 / 三类读者画像
- [§1 鉴权（先读）](#1-鉴权先读) — Bearer Session + API Key 双通道 / scopes 收窄

**公开接口（无需鉴权）**

- [§2 CLI 发布](#2-cli-发布公开接口) — version · install.sh · download · checksums
- [§3 用户认证](#3-用户认证) — login · logout

**业务接口（双通道鉴权）**

- [§4 系统管理](#4-系统管理)
  - [4.1 用户管理](#41-用户管理) — `/api/sys/users`
  - [4.2 API Key 管理](#42-api-key-管理) — `/api/sys/apikeys`
  - [4.3 权限树](#43-权限树) — `/api/sys/permissions/tree`
- [§5 Guard 资源管理](#5-guard-资源管理)
  - [5.1 域名](#51-域名-apiguarddomains) — `/api/guard/domains`
  - [5.2 证书](#52-证书-apiguardcerts) — `/api/guard/certs`
  - [5.3 策略](#53-策略-apiguardpolicies) — `/api/guard/policies`
  - [5.4 CC 规则](#54-cc-规则-apiguardpoliciesidccrules) — `/api/guard/policies/{id}/cc/rules`
  - [5.5 ACL 规则](#55-acl-规则-apiguardpoliciesidaclrules) — `/api/guard/policies/{id}/acl/rules`
  - [5.6 黑白名单](#56-黑白名单-apiguardbwlist) — `/api/guard/bwlist`
  - [5.7 IP 转发](#57-ip-转发-apiguardforwards) — `/api/guard/forwards`
  - [5.8 调度管理](#58-调度管理-apiguardschedules) — `/api/guard/schedules`
  - [5.9 WAF 规则](#59-waf-规则-apiguardwafrules) — `/api/guard/waf/rules`

**套餐目录**

- [§7 套餐目录（Plan）](#7-套餐目录plan) — 只读对外：list · describe；admin 操作不在此范围

**节点运维**

- [§8 节点安装 / 升级（Node Install）](#8-节点安装--升级node-install) — artifacts · commands · upgrades · jobs · tokens + 安装机侧 script/package/env/report

**Analytics 统计分析（74 chart-key · 单一形状契约）**

- [§6.0 本章统一调用约定](#60-本章统一调用约定) — 5 字段契约 / render_hint 8 词白名单
- [§6.1 术语表](#61-术语表) · [§6.2 跨页 Batch](#62-跨页-batch) · [§6.3 单图 GET 调用入口](#63-单图-get-调用入口)
- [§6.4 总览页面 Overview](#64-总览页面overview) · 8 chart-key
- [§6.5 访问分析页面 Access](#65-访问分析页面access) · 10 chart-key
- [§6.6 防护分析页面 Protect](#66-防护分析页面protect) · 12 chart-key（WAF / CC / DDoS）
- [§6.7 AI 识别页面](#67-ai-识别页面ai) · 6 chart-key
- [§6.8 主动防护 / Bot 页面](#68-主动防护-bot-页面) · 10 chart-key
- [§6.9 告警统计页面 Alert](#69-告警统计页面alert) · 5 chart-key
- [§6.10 业务健康 Health](#610-业务健康health-phase-3) · 7 chart-key
- [§6.11 平台运维 Ops](#611-平台运维ops-phase-5) · 8 chart-key
- [§6.12 处置闭环 Closure](#612-处置闭环closure-phase-6) · 4 chart-key
- [§6.13 缓存收益 Cache](#613-缓存收益cache-phase-6) · 4 chart-key
- [§6.14 原始日志 Logs](#614-原始日志logs-phase-1) — 非 chart endpoint
- [§6.15 报表中心 Reports](#615-报表中心reports-phase-4) — 非 chart endpoint
- [§6.16 CLI 对应关系](#616-cli-对应关系) — 30 cobra 命令映射

**参考**

- [§A 通用查询参数](#a-analytics-通用查询参数) — window / stime / etime / site_id / domain_id / target_user_id / compare / top / order
- [§B 可视化建议总表](#b-可视化建议总表) — B.1 render_hint 速查 · B.2 数据形态速查
- [§C 相关文档](#c-相关文档)

> 完整 OpenAPI: [/api/openapi.json](/api/openapi.json) · AI 速读: [/llms.txt](/llms.txt) · 错误码: [/docs/errors](/docs/errors) · CLI: [/docs/cli](/docs/cli) · 权限: [/docs/permissions](/docs/permissions)

---

## §0 总体约定

Cloud WAF 后端基于 Gin 实现的 RESTful 服务。

### 0.1 基础协议

| 项目 | 值 |
|------|----|
| 协议 | HTTPS（推荐）/ HTTP |
| 数据格式 | 请求与响应均为 `application/json`（特例：`POST /api/guard/certs` 仍是 JSON，PEM 走字符串字段；导出/下载接口返回 `text/csv`、`application/pdf` 等） |
| 字符集 | UTF-8 |
| 路径前缀 | 所有业务 API 都挂在 `/api/` 下 |
| 时间格式 | Unix 毫秒时间戳（int64），不是 ISO 字符串 |
| 国际化 | `Accept-Language: zh-CN` 或 `en-US`，影响错误消息和权限名称 |

### 0.2 统一响应信封

任何 JSON 响应都遵循下面三段结构：

```json
{
  "code": 0,
  "message": "ok",
  "data": { /* 业务载荷，类型依接口而定 */ }
}
```

| 字段 | 类型 | 含义 |
|------|------|------|
| `code` | number | `0`=成功；非 0 = 业务错误码 |
| `message` | string | 错误描述（受 `Accept-Language` 影响） |
| `data` | any | 业务数据；列表接口为 `{ list, total, page, size }` |

> **特例**：导出文件下载接口（`POST /api/analytics/overview/export`、`GET /api/analytics/reports/:id/download`、`POST /api/analytics/logs/export`）直接返回二进制流或 CSV/JSON 原文，**不**包裹信封。

### 0.3 HTTP 状态码

| 状态码 | 何时出现 |
|--------|----------|
| 200 | 业务成功（仍需检查 `code`） |
| 201 | 资源创建成功 |
| 400 | 入参错误（参数缺失、格式非法、超出范围） |
| 401 | 未登录、token 过期、API Key 已吊销 |
| 403 | 已登录但权限不足 / 跨 OEM 越权（参见 [权限矩阵](/docs/permissions)） |
| 404 | 资源不存在或不在可见范围 |
| 429 | 限流（默认每 Key 每秒 100 请求） |
| 5xx | 服务端异常 |

### 0.4 列表接口分页约定

所有列表接口统一使用 `page` + `size`（**不是 `page_size`**）：

| 字段 | 类型 | 默认 | 范围 |
|------|------|------|------|
| `page` | int | `1` | ≥ 1 |
| `size` | int | `20` | 1 - 100 |

响应：

```json
{
  "code": 0,
  "data": {
    "list": [ /* ... */ ],
    "total": 42,
    "page": 1,
    "size": 20
  }
}
```

### 0.5 跨模块设计标记（D*）

文档中少量 `D*` 标记来自跨模块设计决策，用于提醒对接方不要使用不存在或语义错误的字段：

| 标记 | 含义 |
|------|------|
| D4 | percentile / p50 / p95 / p99 不作为通用字段暴露；仅时间窗 ≤ 24h 时走实时 ES percentile 计算 |
| D7 | 报表模板枚举为闭集，超出枚举的模板名不可调用 |
| D8 | 缓存价值字段以 `total_cache_*` 为准，**不存在** `cache_count` / `cache_bytes` / `cache_hit` 这类单字段 |
| D10 | 告警/风险处置闭环字段以 `process_uid` / `process_time` / `status` / `level` 为准，**不使用**旧字段 `handle_user` / `handle_time` / `risk_score` / `alert_status` |

### 0.6 三类读者的阅读路径

| 读者 | 入口 | 优先用 |
|------|------|--------|
| 人类对接工程师 | 本文档 + [快速上手](/docs/quickstart) | curl / Postman 调单接口 |
| 脚本 / CI / 第三方系统 | 本文档 + [API Key 管理](#42-api-key-管理) | API Key + 受限 scopes |
| 机器 / AI agent | `/api/openapi.json` / `/llms.txt` / `/llms-full.txt` | OpenAPI v3 schema |

---

## §1 鉴权（先读）

Cloud WAF 当前支持两条认证通道，**同一请求只能选其中一种**：

| 场景 | 请求头 | 适用对象 | 说明 |
|------|--------|----------|------|
| 人工登录 / Web 控制台 / CLI 交互登录 | `Authorization: Bearer <token>` | 人 | token 由 `POST /api/auth/login` 颁发，会过期，适合短期会话 |
| 脚本 / CI / 第三方系统集成 | `Authorization: ApiKey zck_<prefix>.<secret>` | 机器调用 | API Key 明文仅签发时返回一次，适合长期自动化对接 |

公开接口（无需鉴权）只有 4 个：

- `GET /api/cli/version`
- `GET /api/cli/install.sh`
- `GET /api/cli/download/{filename}`
- `GET /api/cli/checksums.txt`
- `POST /api/auth/login`

其它所有接口都必须携带上述任一认证头。下面各接口示例默认使用 `Bearer`；如改用 API Key，只需把请求头替换为 `Authorization: ApiKey zck_<prefix>.<secret>`，并确保该 Key 的 `scopes` 覆盖接口所需权限。

### 1.1 API Key 权限规则

```
effective_perms = user.RBAC ∩ key.scope
```

API Key 的权限不会超过签发用户当前的 RBAC；`scope` 只能收窄，不能放大。中间件认证通过后会注入与会话通道一致的 `user_id` / `role_id` 上下文，下游 RBAC/OEM 隔离保持一致。

**安全约束**：

- 明文 API Key 仅在签发响应里出现一次，落库零明文（`key_hash` 是 bcrypt 摘要）
- 用户被锁定或删除后，其名下 API Key 自动失效
- `scopes` 非空时，请求权限必须精确命中其中之一；为空表示完整继承当前 RBAC
- 跨 OEM 越权由中间件拦截：`api_keys.oem_id` 与请求 hostname 不一致时直接 403
- 可选 IP 白名单 `allowed_ip_cidrs`：非空时只放行命中 CIDR 的请求

> API Key 管理接口的完整说明见 [§4.2](#42-api-key-管理)。

---

## §2 CLI 发布（公开接口）

供 zcloud CLI 自更新与一键安装使用，无需鉴权。

### `GET /api/cli/version` — 查询 CLI 最新版本

**用途**：客户端启动时自检版本；安装脚本 `/api/cli/install.sh` 内部依赖此接口决定下载哪个二进制。

**鉴权**：无（公开）

**输入参数**：无

**输出字段**：

| 字段 | 类型 | 说明 |
|------|------|------|
| `data.version` | string | 形如 `v0.1.0-31`，对齐 git tag |
| `data.binaries[]` | array | 4 个 `os/arch` 组合的下载地址 |
| `data.binaries[].os` | string | `linux` / `darwin` |
| `data.binaries[].arch` | string | `amd64` / `arm64` |
| `data.binaries[].download_url` | string | 拼接服务地址即可下载 |

**可视化建议**：纯文本展示（版本徽章），不适合图表。前端可用作"系统设置 - CLI 版本"页面的 KPI 数字卡。

**示例响应**：

```json
{
  "code": 0,
  "message": "ok",
  "data": {
    "version": "v0.1.0-31",
    "binaries": [
      { "os": "linux",  "arch": "amd64", "download_url": "/api/cli/download/linux-amd64" },
      { "os": "linux",  "arch": "arm64", "download_url": "/api/cli/download/linux-arm64" },
      { "os": "darwin", "arch": "amd64", "download_url": "/api/cli/download/darwin-amd64" },
      { "os": "darwin", "arch": "arm64", "download_url": "/api/cli/download/darwin-arm64" }
    ]
  }
}
```

---

### `GET /api/cli/install.sh` — 一键安装脚本

**用途**：在 Linux/macOS 上一行命令完成 CLI 安装。返回 `text/x-shellscript`，可直接 `curl ... | sh`。

**鉴权**：无（公开）

**输入参数**：无

**输出字段**：纯 shell 脚本文本，**不**走 JSON 信封。

**可视化建议**：不适合图表，作为代码片段展示。

**示例**：

```bash
curl -fsSL https://waf.example.com/api/cli/install.sh | sh
```

---

### `GET /api/cli/download/{filename}` — 下载指定二进制

**用途**：拉取特定平台的 zcloud 二进制（已签名）。`filename` 取自 `/api/cli/version` 返回的 `download_url` 的最后一段，如 `linux-amd64`。

**鉴权**：无（公开）

**输入参数**：

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `filename` | path | 是 | `linux-amd64` / `linux-arm64` / `darwin-amd64` / `darwin-arm64` 其一 |

**输出字段**：二进制流（`application/octet-stream`）。

**可视化建议**：不适合图表。

---

### `GET /api/cli/checksums.txt` — 二进制校验和

**用途**：配合 `/api/cli/download/*` 做 SHA256 完整性校验，安装脚本会先 fetch 这个文件再下载二进制。

**鉴权**：无（公开）

**输入参数**：无

**输出字段**：纯文本 `text/plain`，每行一个 `<sha256>  <filename>`。

**可视化建议**：不适合图表。

---

## §3 用户认证

### `POST /api/auth/login` — 登录

**用途**：用用户名 + 密码换取一个会话 token。Web 控制台、CLI 交互式登录、移动端均走此接口。

**鉴权**：无（公开）

**输入参数**（请求体）：

| 字段 | 类型 | 必填 | 说明 |
|------|------|:---:|------|
| `username` | string | 是 | 用户名 |
| `password` | string | 是 | 密码（**前端 base64 编码后传入**，后端 decode 后再 bcrypt 比对，兼容老系统） |

**输出字段**：

| 字段 | 类型 | 说明 |
|------|------|------|
| `data.token` | string | 32 位会话 token，用于后续 `Authorization: Bearer <token>` |
| `data.user_id` | string | 用户唯一 ID |
| `data.user_name` | string | 用户名 |
| `data.nick_name` | string | 显示名 |
| `data.need_change_password` | bool | true 表示首次登录需强制改密码 |

**可视化建议**：登录响应不直接做图，但 `need_change_password=true` 时前端应跳转改密页。

**示例请求**：

```bash
curl -X POST https://waf.example.com/api/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":"'$(echo -n 'your_password' | base64)'"}'
```

**示例响应**：

```json
{
  "code": 0,
  "message": "ok",
  "data": {
    "token": "550e8400-e29b-41d4-a716-446655440000",
    "user_id": "u-admin",
    "user_name": "admin",
    "nick_name": "系统管理员",
    "need_change_password": false
  }
}
```

**常见误用**：
- 直接传明文密码 → 后端 base64 decode 失败，统一返回"用户名或密码错误"，无法定位真实原因
- token 在多端复用 → 注销时可能影响其它端，建议每端独立登录

---

### `POST /api/auth/logout` — 注销

**用途**：主动失效当前 `Bearer` token；再次请求该 token 返回 401。

**鉴权**：`Bearer <token>`（API Key 通道无 logout 概念，吊销走 `DELETE /api/sys/api-keys/:id`）

**输入参数**：无

**输出字段**：`data` 为 `null`。

**可视化建议**：不适合图表。

**示例请求**：

```bash
curl -X POST https://waf.example.com/api/auth/logout \
  -H "Authorization: Bearer $TOKEN"
```

---

## §4 系统管理

### 4.1 用户管理

> 适用场景：在"系统设置 - 用户管理"页面增删改查用户。所有用户接口都受 OEM 隔离，跨 OEM 操作会返回 403。

#### `GET /api/sys/users` — 用户列表（分页）

**用途**：在"用户管理"页面渲染用户表格，支持关键字模糊搜索 + 分页。

**鉴权**：`sys.user.list`

**输入参数**：

| 字段 | 类型 | 必填 | 取值/示例 | 说明 |
|------|------|:---:|----------|------|
| `page` | int | 否 | `1` | 页码，从 1 起 |
| `size` | int | 否 | `20` | 每页条数，1-100 |
| `keyword` | string | 否 | `admin` | 用户名/显示名模糊搜索 |

**输出字段**（`data.list[]`）：

| 字段 | 类型 | 说明 |
|------|------|------|
| `user_id` | string | 用户唯一 ID |
| `user_name` | string | 登录用户名 |
| `nick_name` | string | 显示名 |
| `email` | string | 邮箱 |
| `mobile` | string | 手机号 |
| `locked` | int | 0=正常，非 0=锁定 |
| `role_ids` | int64[] | 角色 ID 列表（可多角色） |
| `roles[]` | array | 角色摘要 `{role_id, name, level}` |
| `ctime` | int64 | 创建时间，Unix 毫秒 |

**可视化建议**：表格展示。`locked` 列建议用徽章（绿/红）；`roles` 用 chip 标签。

**示例请求**：

```bash
curl -H "Authorization: Bearer $TOKEN" \
  "https://waf.example.com/api/sys/users?page=1&size=20&keyword=admin"
```

**示例响应**：

```json
{
  "code": 0,
  "message": "ok",
  "data": {
    "list": [
      {
        "user_id": "u-001",
        "user_name": "admin",
        "nick_name": "系统管理员",
        "email": "admin@example.com",
        "mobile": "",
        "locked": 0,
        "role_ids": [1],
        "roles": [{ "role_id": 1, "name": "超级管理员", "level": 1 }],
        "ctime": 1714521600000
      }
    ],
    "total": 42,
    "page": 1,
    "size": 20
  }
}
```

**常见误用**：
- 把 `role_ids` 当成单值字段 → 用户可有多角色，必须按数组处理
- 用 `level` 做权限阈值判断 → `level` 是展示字段（业务层级），权限阈值用 `role_id`（参见 [pitfall_role_level_vs_role_id](/docs/permissions)）

---

#### `POST /api/sys/users` — 创建用户

**用途**：在"用户管理"页面提交"新建用户"表单。

**鉴权**：`sys.user.create`

**输入参数**（请求体）：

| 字段 | 类型 | 必填 | 取值/示例 | 说明 |
|------|------|:---:|----------|------|
| `user_name` | string | 是 | `u1` | 2-255 字符 |
| `password` | string | 是 | `InitPassw0rd!` | 6-72 字符（bcrypt 上限） |
| `nick_name` | string | 否 | `运维 A` | ≤ 100 字符 |
| `email` | string | 否 | `u1@x.com` | 标准邮箱格式 |
| `mobile` | string | 否 | `13800138000` | ≤ 20 字符 |
| `comment` | string | 否 | `值班同事` | 备注 |

**输出字段**：返回新建用户对象，结构同列表项。

**可视化建议**：不适合图表，是一次性写操作；前端应在成功后刷新用户列表。

**示例请求**：

```bash
curl -X POST https://waf.example.com/api/sys/users \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"user_name":"u1","password":"InitPassw0rd!","nick_name":"运维 A","email":"u1@example.com"}'
```

---

#### `DELETE /api/sys/users/{id}` — 删除用户

**用途**：用户管理页面"删除"按钮的后端接口。删除会级联清理会话、API Key、角色绑定。

**鉴权**：`sys.user.delete`

**输入参数**：

| 字段 | 类型 | 必填 | 说明 |
|------|------|:---:|------|
| `id` | path | 是 | 用户 `user_id` |

**输出字段**：`data` 为 `null`。

**可视化建议**：不适合图表。

---

#### `PUT /api/sys/users/{id}/password` — 重置密码

**用途**：管理员替用户重置密码。被重置用户下次登录后**强制**改密码。

**鉴权**：`sys.user.resetpwd`

**输入参数**：

| 字段 | 类型 | 必填 | 说明 |
|------|------|:---:|------|
| `id` | path | 是 | 用户 `user_id` |
| `password` | string | 是 | 新密码（明文，6-72 字符；后端自动 bcrypt） |

**输出字段**：`data` 为 `null`。

**可视化建议**：不适合图表。

**示例请求**：

```bash
curl -X PUT https://waf.example.com/api/sys/users/u-001/password \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"password":"NewPassw0rd!"}'
```

**常见误用**：
- 期望前端做 base64 编码 → 此接口直接传明文，与登录接口不同（登录接口 base64 是历史兼容）

---

### 4.2 API Key 管理

> 适用场景：在"系统设置 - API Key"页面发放/回收脚本调用凭证。配合 `/api/sys/api-keys/:id/logs|stats|audit-actions` 做调用审计。

#### `POST /api/sys/api-keys` — 签发新 API Key

**用途**：为脚本/CI/第三方系统签发一条机器调用凭证。明文 `api_key` 字段**仅此一次返回**，前端必须立即让用户复制保存。

**鉴权**：`sys.apikey.create`

**输入参数**（请求体）：

| 字段 | 类型 | 必填 | 取值/示例 | 说明 |
|------|------|:---:|----------|------|
| `name` | string | 是 | `生产对接` | ≤ 100 字符，用于审计识别 |
| `scopes` | string[] | 否 | `["guard.domain.list"]` | 权限 full key 列表；空数组 = 完整继承签发用户当前 RBAC |
| `expires_in_days` | int | 否 | `90` | 过期天数，默认 90，最大 365 |
| `allowed_ip_cidrs` | string[] | 否 | `["203.0.113.0/24"]` | E14 IP 白名单 CIDR；为空表示不限制来源 IP |

**输出字段**：

| 字段 | 类型 | 说明 |
|------|------|------|
| `data.key_id` | string | API Key 唯一 ID（吊销/查日志用此 ID） |
| `data.name` | string | 与请求一致 |
| `data.api_key` | string | **完整明文 `prefix.secret`，仅此一次返回** |
| `data.prefix` | string | 形如 `zck_abc12345`，可写入日志 |
| `data.last4` | string | secret 末 4 位，前端用于"我刚签的那条"识别 |
| `data.expires_at` | int64 | 过期时间，Unix 毫秒 |

**错误码**：
- `1052` scope 超出用户 RBAC 边界
- `1054` `expires_in_days > 365`
- `1055` `name` 缺失

**可视化建议**：签发响应是一次性写操作，建议前端用模态框 + 一次性复制按钮 + 遮码展示明文（参考行业惯例 GitHub/Stripe）。

**示例请求**：

```bash
curl -X POST https://waf.example.com/api/sys/api-keys \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"name":"生产对接","scopes":["guard.domain.list"],"expires_in_days":90}'
```

**示例响应**：

```json
{
  "code": 0,
  "data": {
    "key_id": "8f21c0c5-55ae-4cbd-a60a-8e64a6e2b1d0",
    "name": "生产对接",
    "api_key": "zck_abc12345.A1b2C3d4E5f6G7h8I9j0K1L2M3n4O5p6Q7r8S9t0",
    "prefix": "zck_abc12345",
    "last4": "5t0",
    "expires_at": 1732982400000
  }
}
```

**常见误用**：
- 把 `api_key` 字段存数据库 → 应只存 `key_id` + `prefix`，明文交给业务方一次性
- `scopes` 列空数组以为是无权限 → 空数组实际代表完整继承当前 RBAC，要明确收窄就传具体权限 key

---

#### `GET /api/sys/api-keys` — 查询 API Key 列表

**用途**：在 API Key 管理页面展示当前用户/OEM 内的 Key 清单与状态。

**鉴权**：`sys.apikey.list`

**输入参数**：

| 字段 | 类型 | 必填 | 说明 |
|------|------|:---:|------|
| `page` | int | 否 | 默认 1 |
| `size` | int | 否 | 默认 20，最大 100 |

**输出字段**（`data.list[]`）：

| 字段 | 类型 | 说明 |
|------|------|------|
| `key_id` | string | API Key 唯一 ID |
| `name` | string | 名称 |
| `prefix` | string | `zck_xxx` 前缀 |
| `last4` | string | secret 末 4 位 |
| `user_id` | string | 持有人 |
| `oem_id` | string | OEM 隔离边界 |
| `scopes` | string[] | 限定权限列表 |
| `allowed_ip_cidrs` | string[] | E14 IP 白名单 |
| `status` | int | `1`=active，`2`=revoked |
| `expires_at` | int64 | 过期时间 |
| `last_used_at` | int64 | 最近调用时间；从未用为 `0` |
| `last_used_ip` | string | 最近调用 IP；从未用为 `""` |
| `ctime` | int64 | 签发时间 |

**可见范围**：
- 普通用户（`role_id ≥ 10`）：只看自己签发的
- 平台级用户（`role_id < 10`：超管/运维/审计员）：看本 OEM 全部

**可视化建议**：表格展示。`status` 用徽章（绿=active/灰=revoked），`expires_at` 即将到期（< 7d）建议高亮。配合 `/stats` 接口可做柱状图"调用量 TOP 5 Key"。

**示例请求**：

```bash
curl -H "Authorization: Bearer $TOKEN" \
  "https://waf.example.com/api/sys/api-keys?page=1&size=20"
```

---

#### `DELETE /api/sys/api-keys/{id}` — 吊销 API Key

**用途**：软吊销（`status` → `revoked`），保留审计痕迹。中间件认证时 `status != active` 直接拒绝。

**鉴权**：`sys.apikey.delete`

**输入参数**：

| 字段 | 类型 | 必填 | 说明 |
|------|------|:---:|------|
| `id` | path | 是 | API Key `key_id` |

**输出字段**：`data` 为 `null`。

**幂等性**：重复吊销返回 `200`，前端反复操作不报错。

**可操作范围**：
- 普通用户：只能吊销自己签发的
- 平台级用户：可吊销本 OEM 内任意 key（管理止损用）

**错误码**：`1051` API Key 不存在或不在可见范围。

**可视化建议**：不适合图表；前端"吊销"按钮触发，建议加二次确认对话框。

**示例请求（用 API Key 调用）**：

```bash
curl -X DELETE https://waf.example.com/api/sys/api-keys/8f21c0c5-55ae-4cbd-a60a-8e64a6e2b1d0 \
  -H "Authorization: ApiKey zck_abc12345.A1b2C3d4..."
```

---

#### `GET /api/sys/api-keys/{id}/logs` — 查询某 Key 的调用流水（E12）

**用途**：审计某条 API Key 的调用历史。返回该 Key 在审计表 `api_key_audit_logs` 中的事件列表。

**鉴权**：`sys.apikey.logs`

**输入参数**：

| 字段 | 类型 | 必填 | 说明 |
|------|------|:---:|------|
| `id` | path | 是 | API Key `key_id` |
| `event` | string | 否 | `call`（默认，调用流水）/ `manage`（管理操作） |
| `page` | int | 否 | 默认 1 |
| `size` | int | 否 | 默认 20，最大 100 |

**输出字段**（每条记录）：

| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | int | 自增主键 |
| `event_type` | string | `call` / `manage` |
| `key_id` | string | 关联的 API Key ID |
| `user_id` | string | 操作主体（call=key 持有人；manage=操作人） |
| `auth_mode` | string | `apikey` / `session`，记录请求通过的认证通道 |
| `action` | string | call=`<METHOD> <PATH>`；manage=`create` / `revoke` / `renew` / `revoke-all` |
| `status_code` | int | HTTP 响应状态码（call 类型有效） |
| `biz_code` | int | 业务错码（0 = 成功；call 类型有效） |
| `client_ip` | string | 客户端 IP |
| `user_agent` | string | UA（≤ 255 字符，超长截断） |
| `extra` | string | JSON 字符串，承载续期 / 批量动作等结构化扩展字段 |
| `ctime` | int | Unix 毫秒时间戳 |

**可视化建议**：
- **推荐图表**：表格 + 时序折线图（按 ctime 聚合调用次数）
- **派生指标**：成功率柱状图（按 status_code 拆分 2xx/4xx/5xx）

**可见范围**：
- 普通用户：仅可查询自己签发的 Key
- 平台级用户：可查询本 OEM 内任意 Key（超管全库）

**错误码**：`1051` API Key 不存在或不在可见范围。

---

#### `GET /api/sys/api-keys/{id}/stats` — 查询某 Key 的聚合统计

**用途**：在 API Key 详情页展示该 Key 的调用聚合 KPI（总次数、成功率、QPS、TOP 接口）。仅基于 `event_type=call` 的记录。

**鉴权**：`sys.apikey.stats`

**输入参数**：

| 字段 | 类型 | 必填 | 说明 |
|------|------|:---:|------|
| `id` | path | 是 | API Key `key_id` |
| `since` | string/int | 否 | 聚合窗口起点；支持 `24h` / `7d` / `30m` 相对值，或纯整数 = 毫秒时间戳；留空 = 全量历史 |

**输出字段**：

| 字段 | 类型 | 说明 |
|------|------|------|
| `total_calls` | int | 窗口内总调用次数 |
| `success` | int | `200 ≤ status_code < 400` 的次数 |
| `client_err` | int | `400 ≤ status_code < 500` 的次数 |
| `server_err` | int | `status_code ≥ 500` 的次数 |
| `top_endpoints` | array | 调用次数 Top 5 的 action（`{action, count}`，按次数降序） |
| `last_1h_qps` | float | 最近 1 小时 QPS（次数 / 3600） |

**可视化建议**：
- **推荐图表**：KPI 数字卡（4 个：total_calls / 成功率 / QPS / 错误数）+ 横向 bar 图（top_endpoints）
- **派生指标**：饼图（success / client_err / server_err 三段占比）

**错误码**：`400` `since` 解析失败；`1051` API Key 不存在或不在可见范围。

**示例请求**：

```bash
curl -H "Authorization: Bearer $TOKEN" \
  "https://waf.example.com/api/sys/api-keys/8f21c0c5-55ae-4cbd-a60a-8e64a6e2b1d0/stats?since=24h"
```

---

#### `GET /api/sys/api-keys/audit-actions` — 查询管理操作流水（E13）

**用途**：跨 Key 的审计视图，返回 API Key 管理动作（创建 / 吊销 / 续期 / 批量吊销）的流水。

**鉴权**：`sys.apikey.audit`

**输入参数**：`page` / `size`，标准分页。

**输出字段**：与 `GET /api/sys/api-keys/{id}/logs` 相同；`event_type` 全部为 `manage`。

**可见范围**：
- 普通用户：只看自己作为操作人的记录（`user_id = currentUserID`）
- 平台级用户：本 OEM 全部（超管全库）

**可视化建议**：
- **推荐图表**：表格（按 ctime 倒序）
- **派生指标**：按 action 聚合的柱状图（create / revoke / renew / revoke-all 计数）

---

### 4.3 权限树

#### `GET /api/sys/permissions/tree` — 权限树

**用途**：返回当前 OEM 下的完整权限树（含模块/资源/动作三层 + i18n 名）。前端"角色权限"页用此渲染勾选树；签发 API Key 选 scope 时也用同一颗树。

**鉴权**：登录态（无具体权限要求）

**输入参数**：可附加 `Accept-Language: en-US` 切换权限名语言。

**输出字段**（节选，`data[]`）：

| 字段 | 类型 | 说明 |
|------|------|------|
| `module` | string | 模块名，如 `guard` / `sys` / `analytics` |
| `resources[]` | array | 该模块下的资源列表 |
| `resources[].prefix` | string | 资源前缀，如 `guard.domain` |
| `resources[].name` | string | 资源 i18n 显示名 |
| `resources[].actions[]` | array | 该资源的动作列表 |
| `resources[].actions[].key` | string | 动作短 key，如 `list` / `create` |
| `resources[].actions[].name` | string | 动作 i18n 显示名 |
| `resources[].actions[].full_key` | string | 完整权限 key，如 `guard.domain.list` |

**可视化建议**：
- **推荐图表**：三层树（n-tree / el-tree），module → resource → action
- **复用场景**：API Key scope 选择器、角色权限分配勾选器

**示例响应**（节选）：

```json
{
  "code": 0,
  "data": [
    {
      "module": "guard",
      "resources": [
        {
          "prefix": "guard.domain",
          "name": "域名",
          "actions": [
            { "key": "list",   "name": "列表",   "full_key": "guard.domain.list" },
            { "key": "view",   "name": "查看",   "full_key": "guard.domain.view" },
            { "key": "create", "name": "创建",   "full_key": "guard.domain.create" }
          ]
        }
      ]
    }
  ]
}
```

---

## §5 Guard 资源管理

> 📦 **Guard 资源管理** · 30 个 endpoint · 用于配置防护对象（域名/证书/策略/CC&ACL 规则/名单/转发/调度/WAF 规则），是 WAF 防护能力的"配置面"
> 完整 schema 见 `/api/openapi.json`，本节给出对接最关键的字段名、枚举与典型踩坑点。

### 5.1 域名 `/api/guard/domains`

域名是 Guard 的核心资源——所有防护策略、证书绑定、统计聚合都以 `domain_id` 为锚点。

#### `GET /api/guard/domains` — 域名列表

**用途**：在"防护管理 - 域名"页面渲染域名表格，按审核状态筛选 + 关键字搜索。

**鉴权**：`guard.domain.list`

**输入参数**：

| 字段 | 类型 | 必填 | 取值/示例 | 说明 |
|------|------|:---:|----------|------|
| `page` | int | 否 | `1` | 页码 |
| `size` | int | 否 | `20` | 每页条数，1-100（**不是 page_size**） |
| `keyword` | string | 否 | `api.example` | 模糊搜索 `domain` 或 `asset_name` |
| `audit_status` | int | 否 | `4` | `1`=未审核 `2`=审核中 `3`=未通过 `4`=通过 |

**输出字段**（`data.list[]` 即 `DomainVO`）：

| 字段 | 类型 | 说明 |
|------|------|------|
| `domain_id` | string | 域名唯一 ID |
| `domain` | string | 域名本身，如 `api.example.com` |
| `asset_name` | string | 资产备注名 |
| `user_id` | string | 归属用户 UUID（保留兼容老平台） |
| `user_name` | string | 归属用户名（**P1.3 新增**，来源 `cloud sys.users`） |
| `policy_id` | string | 关联策略 ID |
| `cname` | string | 后端为该域名分配的 CNAME |
| `auto_cert` | bool | 是否启用自动签发证书 |
| `mode` | int32 | 接入模式（`1`=反代等，参见运维文档） |
| `audit_status` | int32 | 1=未审核 2=审核中 3=未通过 4=通过 |
| `switches` | map<string,int32> | 保护开关，key 取自 `waf/cc/acl/bot/cache`，1=开 0=关 |
| `ctime` / `utime` | int64 | 创建/更新时间 |

**可视化建议**：
- **推荐图表**：表格（主图）+ 顶部 KPI 卡片（总数 / 已通过 / 待审核）+ `switches` 用 chip 方阵渲染开关状态
- **派生指标**：饼图（`audit_status` 4 状态占比）

**示例请求**：

```bash
curl -H "Authorization: ApiKey $ZCLOUD_API_KEY" \
  "https://waf.example.com/api/guard/domains?page=1&size=20&audit_status=4"
```

**示例响应**：

```json
{
  "code": 0,
  "data": {
    "list": [
      {
        "domain_id": "d_8a3b1c",
        "domain": "api.example.com",
        "asset_name": "线上 API 网关",
        "user_id": "u_abc",
        "policy_id": "p_default",
        "cname": "api.example.com.cname.zcloud.io",
        "auto_cert": false,
        "mode": 1,
        "audit_status": 4,
        "switches": { "waf": 1, "cc": 1, "acl": 1, "bot": 0, "cache": 1 },
        "ctime": 1714521600000,
        "utime": 1714608000000
      }
    ],
    "total": 8,
    "page": 1,
    "size": 20
  }
}
```

**常见误用**：
- 用 `page_size` 分页 → 实际是 `size`，传 `page_size` 会被忽略
- 期望返回 `protection_enabled` / `current_cert_id` / `origin_addr` → 这些字段不存在；保护开关在 `switches` map 里，证书绑定走 `/api/guard/certs/:id/domains` 反查

---

#### `POST /api/guard/domains` — 创建域名

**用途**：在"添加域名"表单提交后调用，创建一条新的防护域名。

**鉴权**：`guard.domain.create`

**输入参数**（请求体 `DomainCreateReq`）：

| 字段 | 类型 | 必填 | 说明 |
|------|------|:---:|------|
| `domain` | string | 是 | 域名本身，如 `api.example.com` |
| `asset_name` | string | 否 | 资产备注名 |
| `policy_id` | string | 否 | 绑定策略；不传走默认 |

> **重要**：`domain` 是唯一必填项；证书绑定走 `POST /api/guard/certs/:id/bind`，**不要**通过 create-domain 设置；`origin_addr` / `port` / `cert_id` 字段**不存在**。

**输出字段**：返回 `DomainVO`（结构同列表项），含分配的 `domain_id` 和 `cname`。

**可视化建议**：不适合图表。建议创建成功后立即跳转域名详情页或刷新列表。

---

#### `GET /api/guard/domains/{id}` — 域名详情

**用途**：进入域名详情页时拉单条详情。

**鉴权**：`guard.domain.view`

**输入参数**：path `id` = `domain_id`。

**输出字段**：`DomainVO`，与列表项完全一致。

**可视化建议**：表单展示。`switches` 渲染为开关组；`audit_status` 用徽章。

---

#### `PUT /api/guard/domains/{id}` — 更新域名

**用途**：编辑域名的资产名、策略绑定、自动证书等可变字段。

**鉴权**：`guard.domain.edit`

**输入参数**（请求体 `DomainUpdateReq`）：

| 字段 | 类型 | 必填 | 说明 |
|------|------|:---:|------|
| `asset_name` | string | 否 | 资产备注名 |
| `policy_id` | string | 否 | 切换策略 |
| `auto_cert` | bool | 否 | 切换自动签发开关 |
| `mode` | int32 | 否 | 接入模式 |

**输出字段**：返回更新后的 `DomainVO`。

**可视化建议**：不适合图表。

---

#### `DELETE /api/guard/domains/{id}` — 删除域名

**用途**：从防护列表中移除域名。删除会级联清理 settings、证书绑定、统计快照。

**鉴权**：`guard.domain.delete`

**输入参数**：path `id` = `domain_id`。

**输出字段**：`data` 为 `null`。

**可视化建议**：不适合图表。建议删除前二次确认，提示"会清理统计与绑定"。

---

#### `GET /api/guard/domains/{id}/settings` — 域名设置查询

**用途**：在域名详情页"高级设置"标签下展示当前生效的 settings（保护模块开关、缓存策略、CC 限速等）。

**鉴权**：`guard.domain.view`

**输入参数**：path `id` = `domain_id`。

**输出字段**：

| 字段 | 类型 | 说明 |
|------|------|------|
| `data.settings` | map<string,string> | key 取自后端 settings.* 字典，典型 `waf/cc/acl/bot/cache`，value 是 stringified 配置 JSON |

**可视化建议**：表单展示，每个 key 一行配置卡片。

---

#### `PUT /api/guard/domains/{id}/settings` — 域名设置更新

**用途**：修改域名的 settings map。

**鉴权**：`guard.domain.edit`

**输入参数**（请求体 `DomainSettingsUpdateReq`）：

| 字段 | 类型 | 必填 | 说明 |
|------|------|:---:|------|
| `settings` | map<string,string> | 是 | key 必须取自 `GET /settings` 返回的 settings.* 键名；非法 key 后端会拒绝 |

**输出字段**：`data` 为 `null`，调用方应紧接调用 GET 拉新值。

**可视化建议**：不适合图表。

**常见误用**：
- 把顶层布尔字段当 settings 传 → settings value 是 stringified JSON，不是 bool
- 自创不存在的 key → 后端按白名单校验，未知 key 返回 400

---

### 5.2 证书 `/api/guard/certs`

证书管理走"上传 PEM 文本 → 绑定域名"两步流程；**Content-Type 是 `application/json`，不是 multipart/form-data**。

#### `GET /api/guard/certs` — 证书列表

**用途**："防护管理 - 证书"页面表格。

**鉴权**：`guard.cert.list`

**输入参数**：`page` / `size` / `keyword`（搜索 name/common_name）。

**输出字段**（`data.list[]` = `CertVO`）：

| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | uint64 | 证书唯一 ID |
| `name` | string | 自定义名称 |
| `user_name` | string | 归属用户名（**P1.3 替换原 `user_id`**，来源 `cloud sys.users`） |
| `certificate_type` | int32 | 证书类型枚举 |
| `common_name` | string | 证书 CN |
| `issuer` | string | 颁发者 |
| `expired_at` | int64 | 到期时间，Unix 毫秒 |
| `auto_cert` | bool | 是否自动续期 |
| `ctime` / `utime` | int64 | 创建/更新时间 |

> **不存在** `bound_domains` 字段；要查证书绑定哪些域名，调 `GET /api/guard/certs/{id}/domains`。

**可视化建议**：
- **推荐图表**：表格 + 即将到期高亮（`expired_at - now < 30d` 染色）
- **派生指标**：KPI 卡（总数 / 30d 内到期数 / 已过期数）；饼图（`auto_cert` true/false 占比）

---

#### `POST /api/guard/certs` — 上传证书

**用途**：上传一条 PEM 证书。客户对接最容易踩的坑就是误用 multipart/form-data，请务必用 JSON。

**鉴权**：`guard.cert.create`

**安全提示**：直接集成本接口时，`cert` / `key` 仍按 JSON PEM 文本传入；但不要把私钥写入 AI 对话、工单、日志或可观测埋点。若通过 Aegeon Cloud 对话助手操作证书，优先使用其"安全证书附件"上传入口：浏览器将证书/私钥上传到 Aegeon 后只在对话中保留附件引用，私钥不会进入 AI 消息内容。

**输入参数**（请求体 `CertUploadReq`，**`application/json`**）：

| 字段 | 类型 | 必填 | 说明 |
|------|------|:---:|------|
| `name` | string | 是 | 证书名 |
| `cert` | string | 是 | PEM 文本字符串（含 `-----BEGIN CERTIFICATE-----` 头尾） |
| `key` | string | 是 | 私钥 PEM 文本字符串 |
| `sign_cert` | string | 否 | 国密签名证书 PEM（可选） |
| `sign_key` | string | 否 | 国密签名私钥 PEM（可选） |

**输出字段**：返回新建 `CertVO`。

**可视化建议**：不适合图表。前端上传组件应支持"粘贴 PEM 文本"和"读取本地文件"两种方式。

**示例请求**：

```bash
curl -X POST https://waf.example.com/api/guard/certs \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"name":"prod-2026","cert":"-----BEGIN CERTIFICATE-----\nMII...\n-----END CERTIFICATE-----","key":"-----BEGIN PRIVATE KEY-----\nMII...\n-----END PRIVATE KEY-----"}'
```

**常见误用**：
- 用 `multipart/form-data` 上传文件 → 后端不支持，返回 400
- `cert` / `key` 字段传文件路径 → 必须是 PEM 文本字符串本身
- 将私钥原文放进 AI 对话或日志 → 应使用安全附件/密钥托管路径，仅在服务端短暂解密后调用本接口

---

#### `GET /api/guard/certs/{id}` — 证书详情

**用途**：详情页展示，含完整 PEM 文本。

**鉴权**：`guard.cert.view`

**输入参数**：path `id`。

**输出字段**：`CertDetailVO` = `CertVO` + `cert` + `sign_cert`（PEM 文本，方便下载）。

**可视化建议**：表单展示。可加"下载证书"按钮（前端拼 PEM 触发下载）。

---

#### `PUT /api/guard/certs/{id}` — 更新证书

**用途**：直接替换 PEM 文本（无需先删后建）。

**鉴权**：`guard.cert.edit`

**输入参数**（请求体 `CertUpdateReq`，字段同 Upload 但全部可选）：`name` / `cert` / `key` / `sign_cert` / `sign_key`。

**输出字段**：返回更新后的 `CertVO`。

---

#### `DELETE /api/guard/certs/{id}` — 删除证书

**鉴权**：`guard.cert.delete`

**输入参数**：path `id`。

**输出字段**：`data` 为 `null`。

> **副作用**：删除前会自动解绑该证书绑定的所有域名。

---

#### `GET /api/guard/certs/{id}/domains` — 查询证书已绑定的域名

**用途**：在证书详情页展示"该证书正在保护哪些域名"。

**鉴权**：`guard.cert.view`

**输入参数**：path `id`。

**输出字段**（`data[]` = `CertDomainVO[]`）：

| 字段 | 类型 | 说明 |
|------|------|------|
| `domain_id` | string | 域名 ID |
| `domain` | string | 域名 |
| `cert_id` | uint64 | 证书 ID（即 path `id`） |
| `ctime` | int64 | 绑定时间 |

**可视化建议**：表格展示。

---

#### `POST /api/guard/certs/{id}/bind` — 绑定证书到域名

**用途**：把指定证书绑到一个域名上。

**鉴权**：`guard.cert.edit`

**输入参数**（请求体 `CertBindReq`）：

| 字段 | 类型 | 必填 | 说明 |
|------|------|:---:|------|
| `domain_id` | string | 是 | 目标域名 ID |

**输出字段**：`data` 为 `null`。

**可视化建议**：不适合图表。

---

#### `DELETE /api/guard/certs/{id}/bind/{domainId}` — 解绑证书与域名

**用途**：解除证书与某个域名的绑定关系。

**鉴权**：`guard.cert.edit`

**输入参数**：path `id` = 证书 ID；path `domainId` = 域名 ID。

**输出字段**：`data` 为 `null`。

> **路径**：是 `DELETE /bind/{domainId}`，**不是** `POST /unbind`。

---

### 5.3 策略 `/api/guard/policies`

策略是规则的容器：CC / ACL / 黑白名单 都挂在策略下。一个域名绑一个策略。

#### `GET /api/guard/policies` — 策略列表

**鉴权**：`guard.policy.list`

**输入参数**：`page` / `size` / `keyword`。

**输出字段**（`data.list[]` = `PolicyVO`）：

| 字段 | 类型 | 说明 |
|------|------|------|
| `policy_id` | string | 策略 ID |
| `name` | string | 策略名 |
| `comment` | string | 备注（**不是 `remark`**） |
| `user_id` | string | 归属用户 UUID（保留兼容老平台） |
| `user_name` | string | 归属用户名（**P1.3 新增**，来源 `cloud sys.users`） |
| `default_main_rule_version` | string | 默认主规则版本 |
| `is_default` | bool | 是否系统默认策略 |
| `schema_id` | int64 | schema 版本 |
| `cc_rule_count` | int32 | 该策略下的 CC 规则数 |
| `bwl_rule_count` | int32 | 黑白名单规则数 |
| `acl_rule_count` | int32 | ACL 规则数 |
| `ctime` / `utime` | int64 | 创建/更新时间 |

**可视化建议**：表格 + 三个 chip（cc/bwl/acl 数量）。

---

#### `POST /api/guard/policies` — 创建策略

**鉴权**：`guard.policy.create`

**输入参数**（`PolicyCreateReq`）：`name`（必填）/ `comment`。

**输出字段**：返回新建 `PolicyVO`。

---

#### `GET /api/guard/policies/{id}` — 策略详情

**鉴权**：`guard.policy.view`

**输入参数**：path `id`。

**输出字段**：`PolicyVO`。

---

#### `PUT /api/guard/policies/{id}` — 更新策略

**鉴权**：`guard.policy.edit`

**输入参数**（`PolicyUpdateReq`）：`name` / `comment`。

**输出字段**：返回更新后的 `PolicyVO`。

---

#### `DELETE /api/guard/policies/{id}` — 删除策略

**鉴权**：`guard.policy.delete`

**输入参数**：path `id`。

**输出字段**：`data` 为 `null`。删除前需保证该策略未被任何域名引用。

---

### 5.4 CC 规则 `/api/guard/policies/{id}/cc/rules`

> CC = HTTP 速率限制规则（Connection / Concurrency Control）。挂在策略下，按 policy_id 隔离。

#### `GET /api/guard/policies/{id}/cc/rules` — CC 规则列表

**鉴权**：`guard.cc.list`

**输入参数**：path `id` = `policy_id`；query `page` / `size`。

**输出字段**（`data.list[]` = `CcRuleVO`）：

| 字段 | 类型 | 说明 |
|------|------|------|
| `rule_id` | int64 | 规则 ID |
| `name` | string | 规则名 |
| `describe` | string | 描述 |
| `matches[]` | array | 匹配条件结构（路径/方法/头部等） |
| `stats` | object | 统计聚合维度 |
| `limit` | object | 速率限制阈值 |
| `action` | object | 命中动作（拦截/验证码/限速等） |
| `stime` / `etime` | int64 | 生效起止时间 |
| `status` | int32 | `1`=启用 `2`=禁用 |
| `ctime` / `utime` | int64 | 创建/更新时间 |

**可视化建议**：表格 + status 徽章。`matches` 太复杂建议折叠为"详情"按钮弹出。

---

#### `POST /api/guard/policies/{id}/cc/rules` — 创建 CC 规则

**鉴权**：`guard.cc.create`

**输入参数**（`CcRuleCreateReq`）：

| 字段 | 类型 | 必填 | 说明 |
|------|------|:---:|------|
| `name` | string | 是 | 规则名 |
| `describe` | string | 否 | 描述 |
| `matches[]` | array | 是 | 至少 1 项匹配条件 |
| `stats` | object | 否 | 统计维度（IP/URI/UA 等） |
| `limit` | object | 是 | 速率上限 |
| `action` | object | 是 | 命中动作 |
| `stime` / `etime` | int64 | 否 | 生效时间窗 |

> 复杂结构建议先调 list 取一条样本作为模板。

**输出字段**：返回新建 `CcRuleVO`。

---

#### `GET /api/guard/policies/{id}/cc/rules/{rid}` — CC 规则详情

**鉴权**：`guard.cc.view`

**输入参数**：path `id` = `policy_id`，`rid` = `rule_id`。会校验 `rule_id` 是否归属当前 `policy_id`，跨策略读取返回 `NotFound`。

**输出字段**：`CcRuleVO`。

---

#### `PUT /api/guard/policies/{id}/cc/rules/{rid}` — 更新 CC 规则

**鉴权**：`guard.cc.edit`

**输入参数**：path `id` + `rid`，body 同 Create。

**输出字段**：返回更新后的 `CcRuleVO`。

---

#### `DELETE /api/guard/policies/{id}/cc/rules/{rid}` — 删除 CC 规则

**鉴权**：`guard.cc.delete`

**输入参数**：path `id` + `rid`。

**输出字段**：`data` 为 `null`。

---

#### `PUT /api/guard/policies/{id}/cc/rules/{rid}/status` — 切换 CC 规则状态

**用途**：在列表页用开关组件启用/禁用规则。

**鉴权**：`guard.cc.edit`

**输入参数**（`CcRuleStatusReq`）：

| 字段 | 类型 | 必填 | 取值 |
|------|------|:---:|------|
| `status` | int32 | 是 | `1`=启用 `2`=禁用 |

> **重要**：`status` 是 **int32 数字**，不是 `"enabled"` / `"disabled"` 字符串。

**输出字段**：`data` 为 `null`。

**示例请求**：

```bash
curl -X PUT https://waf.example.com/api/guard/policies/p_default/cc/rules/12345/status \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"status":1}'
```

---

### 5.5 ACL 规则 `/api/guard/policies/{id}/acl/rules`

> ACL = 访问控制列表（基于 IP / Header / URI 的放行/拦截）。结构与 CC 规则相似但**没有** `priority` 字段。

#### `GET /api/guard/policies/{id}/acl/rules` — ACL 规则列表

**鉴权**：`guard.acl.list`

**输入参数**：path `id`，query `page` / `size`。

**输出字段**（`data.list[]` = `AclRuleVO`）：

| 字段 | 类型 | 说明 |
|------|------|------|
| `rule_id` | int64 | 规则 ID |
| `name` | string | 规则名 |
| `describe` | string | 描述 |
| `matches[]` | array | 匹配条件 |
| `action` | object | 命中动作（`block`/`page`/`pass`） |
| `stime` / `etime` | int64 | 生效起止时间 |
| `status` | int32 | `1`=启用 `2`=禁用 |
| `ctime` / `utime` | int64 | 创建/更新时间 |

**可视化建议**：表格 + status 徽章 + `action.type` chip 染色（block 红 / pass 绿 / page 蓝）。

---

#### `POST /api/guard/policies/{id}/acl/rules` — 创建 ACL 规则

**鉴权**：`guard.acl.create`

**输入参数**（`AclRuleCreateReq`）：`name` / `describe` / `matches[]`（≥1）/ `action` / `stime` / `etime`。

> `action.type` 为 `block` / `page` 时 `content` 字段服务端会 base64 编码存储；调用方传明文。

**输出字段**：返回新建 `AclRuleVO`。

---

#### `GET /api/guard/policies/{id}/acl/rules/{rid}` — ACL 规则详情

**鉴权**：`guard.acl.view`

**输入参数**：path `id` + `rid`。

**输出字段**：`AclRuleVO`。

---

#### `PUT /api/guard/policies/{id}/acl/rules/{rid}` — 更新 ACL 规则

**鉴权**：`guard.acl.edit`

**输入参数**：path `id` + `rid`，body 同 Create。

**输出字段**：返回更新后的 `AclRuleVO`。

---

#### `DELETE /api/guard/policies/{id}/acl/rules/{rid}` — 删除 ACL 规则

**鉴权**：`guard.acl.delete`

**输入参数**：path `id` + `rid`。

**输出字段**：`data` 为 `null`。

---

#### `PUT /api/guard/policies/{id}/acl/rules/{rid}/status` — 切换 ACL 规则状态

**鉴权**：`guard.acl.edit`

**输入参数**（`AclRuleStatusReq`）：`status` int32（1=启用 2=禁用，**数字**）。

**输出字段**：`data` 为 `null`。

---

### 5.6 黑白名单 `/api/guard/bwlist`

黑白名单分两层：**集合（set）** 是逻辑容器（黑/白/灰黑/灰白），**IP** 是集合内的具体条目。灰黑、灰白会在配置生成时分别按黑名单、白名单下发。

#### `GET /api/guard/bwlist/sets` — 名单集合列表

**鉴权**：`guard.bwlist.list`

**输入参数**：`page` / `size` / `keyword`。

**输出字段**（`data.list[]` = `IPSetVO`）：

| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | uint64 | 集合 ID |
| `user_name` | string | 归属用户名（**P1.3 替换原 `user_id`**，来源 `cloud sys.users`） |
| `name` | string | 集合名 |
| `policy_id` | string | 关联策略 |
| `ip_set_type` | int32 | **`1`=黑名单 `2`=白名单 `3`=灰黑名单 `4`=灰白名单**（数字枚举，**不是字符串**） |
| `status` | int32 | `1`=禁用 `2`=启用 |
| `count` | int64 | 总条目数 |
| `enable_count` | int64 | 启用条目数 |
| `unable_count` | int64 | 禁用条目数 |
| `describe` | string | 描述（**不是 `remark`**） |
| `is_default` | bool | 是否默认集合 |
| `is_private` | bool | 是否私有 |
| `private_domain_id` | string | 私有集合关联的域名 |
| `ctime` / `utime` | int64 | 时间戳 |

**可视化建议**：
- **推荐图表**：表格（主图）+ KPI 卡（黑名单总数 / 白名单总数 / 启用率）
- **派生指标**：饼图（`ip_set_type` 1/2/3/4 占比）

---

#### `POST /api/guard/bwlist/sets` — 创建名单集合

**鉴权**：`guard.bwlist.create`

**输入参数**（`BWListSetCreateReq`）：

| 字段 | 类型 | 必填 | 说明 |
|------|------|:---:|------|
| `name` | string | 是 | 集合名 |
| `ip_set_type` | int32 | 是 | `1`=黑名单 `2`=白名单 `3`=灰黑名单 `4`=灰白名单 |
| `policy_id` | string | 否 | 关联策略 |
| `status` | int32 | 否 | `1`=禁用 `2`=启用，默认 2 |
| `describe` | string | 否 | 描述 |

**输出字段**：返回新建 `IPSetVO`。

> 当 `policy_id` 非空时，后端会把集合同步到策略与该策略下域名的黑白名单配置；后续添加、批量添加或删除 IP 会触发配置下发。

---

#### `PUT /api/guard/bwlist/sets/{id}` — 更新名单集合

**鉴权**：`guard.bwlist.edit`

**输入参数**（`BWListSetUpdateReq`）：`name` / `status` / `describe`（全部可选）。

**输出字段**：返回更新后的 `IPSetVO`。

> 修改 `status` 会同步策略/域名配置并触发下发：启用集合进入黑/白名单，禁用集合进入禁用列表。

---

#### `DELETE /api/guard/bwlist/sets/{id}` — 删除名单集合

**鉴权**：`guard.bwlist.delete`

**输入参数**：path `id`。

**输出字段**：`data` 为 `null`。**会级联删除集合内所有 IP 条目**。

> 删除集合会同时从策略/域名黑白名单配置中解绑，并触发相关域名重新下发。

---

#### `GET /api/guard/bwlist/sets/{id}/ips` — 集合内 IP 列表

**鉴权**：`guard.bwlist.ip_list`

**输入参数**：path `id` = 集合 ID；query `page` / `size`。

**输出字段**（`data.list[]` = `IPVO`）：

| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | uint64 | 条目 ID |
| `ipset_id` | uint64 | 所属集合 |
| `ip_addr` | string | IP 或 CIDR（**不是 `ip`**） |
| `status` | bool | true=启用 false=禁用，默认 true |
| `ctime` / `utime` | int64 | 时间戳 |

> **不存在** `expires_at` / `remark` 字段。

**可视化建议**：表格。

---

#### `POST /api/guard/bwlist/sets/{id}/ips` — 单条添加 IP

**鉴权**：`guard.bwlist.ip_add`

**输入参数**（`BWListIPAddReq`）：

| 字段 | 类型 | 必填 | 说明 |
|------|------|:---:|------|
| `ip_addr` | string | 是 | IP 或 CIDR |
| `status` | bool | 否 | 默认 `true` |

**输出字段**：返回新建 `IPVO`。

---

#### `POST /api/guard/bwlist/sets/{id}/ips/batch` — 批量添加 IP

**用途**：一次导入数百条 IP（如威胁情报源），减少多次往返开销。

**鉴权**：`guard.bwlist.ip_add`

**输入参数**（`BWListIPBatchAddReq`）：

| 字段 | 类型 | 必填 | 说明 |
|------|------|:---:|------|
| `ips[]` | array | 是 | 至少 1 项；每项 `{ip_addr, status?}` |

**输出字段**：`data` 为 `null` 或返回新增 ID 列表（按实现）。

---

#### `POST /api/guard/bwlist/sets/{id}/ips/batch/delete` — 批量删除 IP

**用途**：一次删除集合下多条 IP（如清理过期封禁），一个事务原子完成，避免逐条删除的部分失败与审计刷屏。

**鉴权**：`guard.bwlist.ip_delete`

**输入参数**（`BWListIPBatchDeleteReq`）：

| 字段 | 类型 | 必填 | 说明 |
|------|------|:---:|------|
| `ip_ids[]` | array&lt;int64&gt; | 是 | 至少 1 项；要删除的 IP 条目 ID。删除范围限定在集合 `{id}` 内，不属于该集合的 ID 会被忽略 |

**输出字段**：`data.deleted` 为实际删除条数。集合内无任一匹配返回 `404`。

---

#### `DELETE /api/guard/bwlist/ips/{id}` — 删除单条 IP

> **重要**：删除路径是**顶级路径** `DELETE /api/guard/bwlist/ips/{id}`，**不是**嵌套在 sets 下的 `DELETE /sets/{sid}/ips/{id}`。

**鉴权**：`guard.bwlist.ip_delete`

**输入参数**：path `id` = IP 条目 ID。

**输出字段**：`data` 为 `null`。

---

### 5.7 IP 转发 `/api/guard/forwards`

> 这是 **TCP/UDP 端口转发**（4 层），**不是路径转发 / 反向代理**（7 层）。

#### `GET /api/guard/forwards` — IP 转发列表

**鉴权**：`guard.forward.list`

**输入参数**：

| 字段 | 类型 | 说明 |
|------|------|------|
| `page` / `size` | int | 标准分页 |
| `user_id` | string | 按归属用户过滤 |
| `domain_id` | string | 按域名过滤 |
| `status` | int32 | `1`=禁用 `2`=启用 |
| `keyword` | string | 搜索 `domain` / `describe` |

**输出字段**（`data.list[]` = `ForwardVO`）：

| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | uint64 | 转发 ID |
| `user_id` | string | 归属用户 UUID（保留兼容老平台） |
| `user_name` | string | 归属用户名（**P1.3 新增**，来源 `cloud sys.users`） |
| `domain` | string | 转发的域名/IP |
| `domain_id` | string | 关联域名 ID |
| `schema` | int32 | **`3`=TCP `4`=UDP**，默认 3 |
| `port` | int32 | 端口 1-65535 |
| `node_ipaddrs` | string | 源 IP 列表（逗号分隔） |
| `describe` | string | 描述 |
| `status` | int32 | `1`=禁用 `2`=启用 |
| `src_setting` | json | 源设置 raw JSON |
| `adv_settings` | json | 高级设置 raw JSON |
| `node_setting` | json | 节点设置 raw JSON |
| `dev_setting` | string | 设备设置 |
| `ctime` / `utime` | int64 | 时间戳 |

**可视化建议**：
- **推荐图表**：表格 + status / schema 双 chip
- **派生指标**：饼图（TCP/UDP 占比）

---

#### `POST /api/guard/forwards` — 创建 IP 转发

**鉴权**：`guard.forward.create`

**输入参数**（`ForwardCreateReq`）：

| 字段 | 类型 | 必填 | 取值 | 说明 |
|------|------|:---:|------|------|
| `domain` | string | 是 | `*.example.com` | 转发域名 |
| `domain_id` | string | 是 | `d_8a3b1c` | 关联域名 |
| `port` | int32 | 是 | `443` | 1-65535 |
| `schema` | int32 | 否 | `3` | 3=TCP（默认）/4=UDP |
| `node_ipaddrs` | string | 否 | `10.0.0.1,10.0.0.2` | 源 IP 列表 |
| `describe` | string | 否 | | 描述 |
| `status` | int32 | 否 | `2` | 1=禁 2=启用 |
| `src_setting` / `adv_settings` / `node_setting` | json | 否 | | raw JSON 配置 |
| `dev_setting` | string | 否 | | 设备配置 |

> **不存在** `source_path` / `target` 字段。

**输出字段**：返回新建 `ForwardVO`。

---

#### `GET /api/guard/forwards/{id}` — IP 转发详情

**鉴权**：`guard.forward.view`

**输入参数**：path `id`。

**输出字段**：`ForwardVO`。

---

#### `PUT /api/guard/forwards/{id}` — 更新 IP 转发

**鉴权**：`guard.forward.edit`

**输入参数**：path `id`，body 同 Create（字段全部可选）。

**输出字段**：返回更新后的 `ForwardVO`。

---

#### `DELETE /api/guard/forwards/{id}` — 删除 IP 转发

**鉴权**：`guard.forward.delete`

**输入参数**：path `id`。

**输出字段**：`data` 为 `null`。

---

### 5.8 调度管理 `/api/guard/schedules`

> **本模块只做 DNS 解析调度（域名解析模式切换 / 批量启停记录）。** 旧版 cron 定时任务接口已下架。
>
> ⚠️ **异步生效约定**：本组接口**不直接调用三方 DNS API**，所有写操作仅落库老平台 `guard_db.dns_records` + `guard_db.dns_affairs`，最终通过 NSQ topic=`dns` 通知 zdns 服务异步生效。**前端 / 调用方必须轮询 affairs 接口获取最终状态**（初始 `AffairsStatus_Start` → `AffairsStatus_Succeed` / `AffairsStatus_Faild`）。
>
> ⚠️ **数据真值源**：`dns_records.group_type` 是模式真相源，`guard_configs.parsing_state` 异步同步，均来自老平台 `guard_db`；存在最长 30s 不一致窗口。VO 同时返回两者，前端在不一致时显示"同步中" badge。

#### 5.8.1 域名调度

##### `GET /api/guard/schedules/domains` — 域名列表

**鉴权**：`guard.schedule.list`

**输入参数**：

| 字段 | 类型 | 说明 |
|------|------|------|
| `page` / `size` | int | 分页（size 上限 100） |
| `keyword` | string | 按域名模糊搜索 |
| `user_id` | string | 按归属用户过滤（仅超管/总代有效） |
| `mode` | int32 | `0`=全部 / `1`=源站(SRC) / `2`=节点(NODE) |

**输出字段**（`data` = `ScheduleDomainListResp`）：

| 字段 | 类型 | 说明 |
|------|------|------|
| `list[].domain_id` | string | 域名 ID（`guard_configs.domain_id`） |
| `list[].domain_name` | string | 域名 |
| `list[].user_id` / `user_name` | string | 归属用户 |
| `list[].parsing_state` | int32 | `guard_configs.parsing_state`（1=SRC / 2=NODE） |
| `list[].dns_group_type` | int32 | `dns_records.group_type` 多数票（真值源，1=SRC / 2=NODE） |
| `list[].src_count` | int | `group_type=1` 的记录数 |
| `list[].node_count` | int | `group_type=2` 的记录数 |
| `list[].src_records[]` | array | 源站解析摘要：`subdomain` / `record_type` / `record_line` / `value` / `status` |
| `list[].node_records[]` | array | 节点解析摘要：`subdomain` / `record_type` / `record_line` / `value` / `status` |
| `list[].last_affair_status` | string | 最近一条事务状态（`AffairsStatus_Start` / `AffairsStatus_Succeed` / `AffairsStatus_Faild`） |
| `list[].last_affair_ctime` | int64 | 最近一条事务创建时间（毫秒） |
| `list[].last_affair_message` | string | 最近一条事务消息（HTML 片段） |
| `total` | int64 | 总条数 |

---

##### `POST /api/guard/schedules/domains/{id}/switch-mode` — 切换源站/节点

**鉴权**：`guard.schedule.switch`

**输入参数**：

| 字段 | 类型 | 必填 | 说明 |
|------|------|:---:|------|
| path `id` | string | 是 | `domain_id` |
| body `target_mode` | int32 | 是 | `1`=源站(SRC) / `2`=节点(NODE) |
| body `comment` | string | 否 | 事务备注（写入 `dns_affairs.message`） |

**实现要点**：guard_db 单库事务内 `SELECT FOR UPDATE` 锁住该域名全部 dns_records → 更新 `group_type` 与 `switch_state=2`（切换中）→ INSERT `dns_affairs`（`status=AffairsStatus_Start`）→ 提交后 NSQ Cmd=0 通知。

**输出字段**：返回新建 `ScheduleAffairVO`，前端应将 `affairs_id` 写入轮询轮换。

---

##### `POST /api/guard/schedules/domains/{id}/init` — 初始化解析

**鉴权**：`guard.schedule.init`

**输入参数**：path `id`（`domain_id`）；body 可选 `comment`。

**实现要点**：读取 `guard_domain_settings` 的源站/调度配置与 `domain_node_ships` 节点绑定，锁住该域名旧 `dns_records` 后删除并重建源站/节点记录；随后 INSERT `dns_affairs`，以 NSQ Cmd=0 通知 zdns 同步一次。

**输出字段**：返回新建 `ScheduleAffairVO`。

---

##### `POST /api/guard/schedules/domains/{id}/reset` — 重置解析

**鉴权**：`guard.schedule.reset`

**输入参数**：path `id`（`domain_id`）；body 可选 `comment`。

**实现要点**：所有 `dns_records.switch_state` 归位为 `1`，`status` 回滚到 `last_status`；落事务后 NSQ Cmd=0 通知。

**输出字段**：返回新建 `ScheduleAffairVO`。

---

##### `GET /api/guard/schedules/domains/{id}/records` — 域名解析记录

**鉴权**：`guard.schedule.records`

**输入参数**：

| 字段 | 类型 | 说明 |
|------|------|------|
| path `id` | string | `domain_id` |
| `group_type` | int32 | `1`=SRC / `2`=NODE |
| `status` | int32 | `1`=禁用 / `2`=启用 |
| `page` / `size` | int | 分页 |

**输出字段**（`data` = `ScheduleRecordsResp`）：

| 字段 | 类型 | 说明 |
|------|------|------|
| `list[]` | `DnsRecordVO` | 解析记录视图 |
| `list[].record_id` | string | 主键 |
| `list[].associated_id` | string | DNS 服务商侧解析记录 ID，用于确认记录已在 ZDNS/服务商侧重建或关联 |
| `list[].domain` / `subdomain` / `value` | string | 域 / 子域 / 解析值 |
| `list[].record_type` | int32 | DNS 记录类型内部编码 |
| `list[].record_line` | int32 | 解析线路 |
| `list[].ttl` | int64 | TTL（秒） |
| `list[].status` / `last_status` | int32 | `1`=禁用 / `2`=启用 |
| `list[].group_type` | int32 | `1`=SRC / `2`=NODE |
| `list[].switch_state` | int32 | `1`=就绪 / `2`=切换中 |
| `list[].ctime` / `utime` | int64 | 时间戳（毫秒） |

> 直查 `zdns_db.dns_records`，只读，不落事务。

---

#### 5.8.2 解析调度

##### `POST /api/guard/schedules/records/batch-status` — 批量启停记录

**鉴权**：`guard.schedule.batch`

**输入参数**（`ScheduleBatchStatusReq`）：

| 字段 | 类型 | 必填 | 说明 |
|------|------|:---:|------|
| `record_ids` | string[] | 是 | 目标 `record_id` 集合 |
| `status` | int32 | 是 | `1`=禁用 / `2`=启用 |
| `comment` | string | 否 | 事务备注 |

**实现要点**：`UPDATE dns_records SET last_status=status, status=? WHERE record_id IN (?)`；落事务后 NSQ Cmd=0 通知。

**输出字段**：返回新建 `ScheduleAffairVO`。

---

#### 5.8.3 事务记录

##### `GET /api/guard/schedules/affairs` — 事务列表

**鉴权**：`guard.schedule.affairs`

**输入参数**：

| 字段 | 类型 | 说明 |
|------|------|------|
| `page` / `size` | int | 分页 |
| `user_id` | string | 按归属用户过滤 |
| `status` | string | `AffairsStatus_Start` / `AffairsStatus_Succeed` / `AffairsStatus_Faild` |
| `ctime_from` / `ctime_to` | int64 | 时间范围（毫秒） |
| `domain_id` | string | 按受影响 domain 过滤（实现走 `content LIKE`） |

**输出字段**（`data` = `ScheduleAffairListResp`）：见下方 `ScheduleAffairVO`。

---

##### `GET /api/guard/schedules/affairs/{id}` — 事务详情

**鉴权**：`guard.schedule.affairs`

**输入参数**：path `id`（`affairs_id`）。

**输出字段**（`data` = `ScheduleAffairVO`）：

| 字段 | 类型 | 说明 |
|------|------|------|
| `affairs_id` | string | 事务 ID，格式 `{ts}_{rand10}` |
| `user_id` / `user_name` | string | 操作人 |
| `status` | string | `AffairsStatus_Start` / `AffairsStatus_Succeed` / `AffairsStatus_Faild`（字符串枚举，对齐老平台 MarshalJSON 行为） |
| `message` | string | 事务消息，含 HTML 片段（如 `<br>`） |
| `content` | string | 受影响 `domain_id`，逗号分隔 |
| `json_content` | object | 扩展 JSON，含 outbox 重试计数等 |
| `affairs_oper` | string | `AffairsOperType_Page` / `AffairsOperType_Cron` / `AffairsOperType_Cli` |
| `ctime` / `utime` | int64 | 创建/更新时间（毫秒） |

> **轮询建议**：前端发起写操作后，每 2~5s 轮询一次本接口，直到 `status` 切换为 `Succeed` 或 `Faild`；若长时间停在 `Start`，说明 outbox 重试中，可在 UI 显示"同步中"。

---

### 5.9 WAF 规则 `/api/guard/waf/rules`

> WAF 规则组挂在策略（`policy_id`）下，一个规则组含若干子规则（`rules[]`），由网关侧匹配 `zone` / `pattern` 决定命中后 `action`（拦截 / 记录 / 验证码）。
>
> ⚠️ **状态约定**：本组接口的 `status` 字段统一约定 **`1`=禁用 `2`=启用**（S-6 修复后已与 `forwards` / `bwlist` / `schedules` 对齐）。`POST /status` 接口的 `oneof` 校验也是 `1|2`。
>
> ⚠️ **路径 ID 取 `tag`**：`PUT/DELETE /api/guard/waf/rules/{id}` 中 `{id}` 是规则组的 `tag`（uint64 主键），**不是** `rule_id`（业务编号）。前端从列表的 `tag` 字段取值。

#### `GET /api/guard/waf/rules` — WAF 规则组列表

**鉴权**：`guard.waf.list`

**输入参数**：

| 字段 | 类型 | 说明 |
|------|------|------|
| `page` / `size` | int | 标准分页 |
| `policy_id` | string | 按策略过滤（不传则返回当前用户可见的全部规则组） |

**输出字段**（`data.list[]` = `WafGroupVO`）：

| 字段 | 类型 | 说明 |
|------|------|------|
| `tag` | uint64 | 规则组主键（路径 `{id}` 用） |
| `rule_id` | int64 | 业务规则编号 |
| `name` | string | 规则组名称 |
| `describe` | string | 描述 |
| `waf_type` | int32 | WAF 类型内部编码 |
| `sub_rule_condition` | int32 | 子规则组合逻辑（`0`=AND / `1`=OR，按 model 实现为准） |
| `scope` | string | 作用域（domain / path / 留空 = 全部） |
| `action` | int32 | **`1`=block(拦截) `2`=log(记录) `3`=captcha(验证码)** |
| `status` | int32 | `1`=禁用 `2`=启用 |
| `policy_id` | string | 所属策略 ID |
| `rules[]` | `WafRuleVO` | 子规则列表（每条含 zone / pattern / pattern_type / is_not） |
| `ctime` / `utime` | int64 | 时间戳（毫秒） |

**`WafRuleVO`（子规则）字段**：

| 字段 | 类型 | 说明 |
|------|------|------|
| `tag` | uint64 | 子规则主键 |
| `rule_id` | int64 | 业务规则编号 |
| `group_id` | int64 | 所属规则组（关联 `WafGroupVO.tag` / `rule_id`） |
| `zone` | string | 匹配区域（如 `URL` / `ARGS` / `HEADER` / `BODY`） |
| `sub_field` | string | 区域内的子字段（如 header name） |
| `pattern_type` | int32 | 匹配类型（精确 / 正则 / 包含等内部编码） |
| `pattern` | string | 匹配表达式 |
| `describe` | string | 描述 |
| `is_not` | bool | true = 取反匹配 |
| `ctime` / `utime` | int64 | 时间戳（毫秒） |

**可视化建议**：
- **推荐图表**：表格 + `action` / `status` 双 chip + `rules.length` 数字徽章
- **派生指标**：按 `action` 聚合的饼图（block / log / captcha 占比）

---

#### `POST /api/guard/waf/rules` — 创建 WAF 规则组

**鉴权**：`guard.waf.create`

**输入参数**（`WafGroupCreateReq`）：

| 字段 | 类型 | 必填 | 取值 | 说明 |
|------|------|:---:|------|------|
| `name` | string | 是 | `"SQL 注入防护"` | 规则组名称 |
| `policy_id` | string | 是 | `"1"` | 挂载策略 |
| `rule_id` | int64 | 否 | | 业务编号，不传由后端生成 |
| `describe` | string | 否 | | 描述 |
| `waf_type` | int32 | 否 | | WAF 类型内部编码 |
| `sub_rule_condition` | int32 | 否 | | 子规则组合逻辑 |
| `scope` | string | 否 | `"*.example.com"` | 作用域 |
| `action` | int32 | 否 | `1` | 1=block / 2=log / 3=captcha |
| `status` | int32 | 否 | `2` | 1=禁用 2=启用 |
| `rules[]` | object[] | 否 | | 子规则列表（字段同 `WafRuleReq`，见下） |

**`WafRuleReq`（子规则请求体）**：

| 字段 | 类型 | 说明 |
|------|------|------|
| `rule_id` | int64 | 业务编号，不传由后端生成 |
| `zone` | string | 匹配区域 |
| `sub_field` | string | 区域内的子字段 |
| `pattern_type` | int32 | 匹配类型 |
| `pattern` | string | 匹配表达式 |
| `describe` | string | 描述 |
| `is_not` | bool | true = 取反 |

**示例**：

```bash
curl -X POST https://waf.example.com/api/guard/waf/rules \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "SQL 注入防护",
    "policy_id": "1",
    "describe": "OWASP Top 10 SQLi 黑名单",
    "action": 1,
    "status": 2,
    "rules": [
      { "zone": "ARGS", "pattern_type": 2, "pattern": "union\\s+select", "describe": "SQLi: union select" }
    ]
  }'
```

**输出字段**：返回新建 `WafGroupVO`（含 `tag`、`rules[]` 已落库的子规则）。

> **不存在**字段：`enabled` 布尔（用 `status` 整数）/ `description`（用 `describe`）/ `domains[]`（作用域用 `scope` 字符串）。

---

#### `PUT /api/guard/waf/rules/{id}` — 更新 WAF 规则组

**鉴权**：`guard.waf.edit`

**输入参数**（path `id` = 规则组 `tag`；body `WafGroupUpdateReq`，字段全部可选，按 flag 增量更新；`rules[]` 全量替换）：

| 字段 | 类型 | 说明 |
|------|------|------|
| `name` | string | 规则组名称 |
| `describe` | string | 描述 |
| `waf_type` | *int32 | 指针类型，未传不动 |
| `sub_rule_condition` | *int32 | 指针类型 |
| `scope` | string | 作用域 |
| `action` | *int32 | 指针类型 |
| `rules[]` | object[] | **全量替换**子规则列表 |

> **本接口不更新 `status`**——启停切换走独立的 `PUT /api/guard/waf/rules/{id}/status`。

**输出字段**：返回更新后的 `WafGroupVO`。

---

#### `DELETE /api/guard/waf/rules/{id}` — 删除 WAF 规则组

**鉴权**：`guard.waf.delete`

**输入参数**：path `id`（规则组 `tag`）。

**实现要点**：级联删除子规则（`waf_rules.group_id = tag`）。

**输出字段**：`data` 为 `null`。

---

#### `PUT /api/guard/waf/rules/{id}/status` — 切换 WAF 规则组启停

**鉴权**：`guard.waf.status`

**输入参数**（path `id` = 规则组 `tag`；body `WafStatusReq`）：

| 字段 | 类型 | 必填 | 取值 | 说明 |
|------|------|:---:|------|------|
| `status` | int32 | 是 | `1` / `2` | 1=禁用 2=启用，`oneof=1 2` 校验 |

**示例**：

```bash
curl -X PUT https://waf.example.com/api/guard/waf/rules/42/status \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "status": 1 }'
```

**输出字段**：`data` 为 `null`。

---

## §6 Analytics 统计分析

> 📊 **Analytics 统计分析** · 18 paths（含 80+ chart-key 单图接口）· 用于构建 WAF 监控大屏、运营报表、处置闭环
> 所有 `/api/analytics/*` 路径已对外，**只能增加字段或新增接口，不能修改或删除已发布路径**。
> 完整 chart-key → 数据形态映射见各小节"chart-key 索引表"，每行都明确推荐图表。

### 6.0 本章统一调用约定

Analytics 接口数量多但大多是同构的图表查询。本章用"统一约定 + 索引表 + 特殊接口展开"组织。

#### 鉴权与权限

**Header**：`Authorization: Bearer <token>` 或 `Authorization: ApiKey zck_...`，可附加 `Accept-Language: zh-CN` / `en-US`。

权限分三类：

| 类型 | 判断方式 | 示例 |
|------|----------|------|
| 页面级只读 | `analytics.<page>.view` | `GET /api/analytics/overview/kpi` 需要 `analytics.overview.view` |
| 特殊动作 | 表格或小节单独标出 | `POST /api/analytics/overview/export` 需要 `analytics.overview.export` |
| 登录态 | 只要求认证，不要求具体业务权限 | `GET /api/analytics/glossary` |

使用 API Key 时，Key 的 `scopes` 必须覆盖接口所需权限。例如调用 `GET /api/analytics/access/status`，Key 至少包含 `analytics.access.view`。

#### 单图 GET 调用模板

```bash
curl -sS 'https://waf.example.com/api/analytics/access/status?window=last_24h&site_id=site-001' \
  -H "Authorization: ApiKey $ZCLOUD_API_KEY" \
  -H 'Accept-Language: zh-CN'
```

返回统一 JSON 信封，图表 `data` 固定使用 **Chart 统一契约**（[`docs/specs/chart-contract.md`](https://gitea.com/y2026/cloud/src/branch/main/docs/specs/chart-contract.md)）。这是新系统对外唯一契约；老数据库表、聚合表和 ES 索引只作为内部数据源，不影响调用方入参或响应结构。

**图表 `data` 固定 5 字段，禁止 `series` / `totals` / `kpis` / `points` / `list` 作为对外顶层字段**：

| 字段 | 类型 | 说明 |
|------|------|------|
| `chart_key` | string | 与请求 `<chart>` 完全一致 |
| `render_hint` | enum | 8 词汇之一：`kpi` / `categorical_distribution` / `categorical_distribution_over_time` / `time_series_single` / `time_series_multi` / `topn` / `geo` / `table`，前端据此选渲染器 |
| `schema` | object | 列元信息 `{dimensions:[{name,type,unit?,values?}], measures:[{name,type,unit?,format?}]}` |
| `rows` | array | tidy 长表，一行一个观测（**禁止横铺**），空数据返回 `[]` |
| `meta` | object | 调试字段 `{source, cache, latency_ms, partial?, available?, ...}` |

> **window.granularity 不是入参**：客户传 `window=last_24h`，后端按时间窗大小**自动**选择 5m/1h/1d 聚合表，并在响应里回填实际命中的 `granularity`。

#### Batch 调用模板

适用于 `POST /api/analytics/batch` 与 `POST /api/analytics/<page>/batch`。

```bash
curl -sS -X POST https://waf.example.com/api/analytics/overview/batch \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
        "time_window": "last_24h",
        "site_id": "",
        "domain_id": "",
        "compare": false,
        "charts": [
          { "key": "kpi" },
          { "key": "bandwidth" }
        ]
      }'
```

Batch 响应的 `data.data` 以 chart-key 为键；`data.meta` 给出整体耗时、缓存命中率、失败图表数和实际时间窗。单个 chart 失败时优先查看该 chart 节点里的 `partial` / `error` 字段。

---

### 6.1 术语表

#### `GET /api/analytics/glossary` — 统计术语表

**用途**：返回统计分析术语表，前端 tooltip 用此渲染"什么是 QPS / 拦截率"等说明。

**鉴权**：登录态

**输入参数**：无。

**输出字段**：

| 字段 | 类型 | 说明 |
|------|------|------|
| `data.terms` | map<string,string> | key=术语短名，value=多语言解释 |

**可视化建议**：
- **推荐图表**：不直接做图；前端图表组件挂上 tooltip 时按 chart-key 在 terms map 里查询解释

**示例响应**：

```json
{
  "code": 0,
  "data": {
    "terms": {
      "qps": "每秒请求数",
      "block_rate": "拦截率"
    }
  }
}
```

---

### 6.2 跨页 Batch

#### `POST /api/analytics/batch` — 跨页通用批量查询

**用途**：在前端大屏页面初始化时一次性请求多个 chart-key，省去 N 次单图 GET 往返。后端并行执行各子查询并合并响应。

**鉴权**：按 `page` 字段映射到对应 `analytics.<page>.view`

**输入参数**（请求体）：

| 字段 | 类型 | 必填 | 取值/示例 | 说明 |
|------|------|:---:|----------|------|
| `page` | string | 是 | `overview` | 必须是当前已支持页面之一：`overview` / `access` / `protect` / `ai` / `bot` / `alert` / `health` / `ops` / `closure` / `cache` |
| `time_window` | string | 否 | `last_24h` | 同 `window` |
| `stime` / `etime` | int64 | 否 | `1746748800000` | 自定义时间戳 |
| `site_id` | string | 否 | | 站点过滤 |
| `domain_id` | string | 否 | | 域名过滤 |
| `target_user_id` | string | 否 | | 客户级切换被查看用户 |
| `compare` | bool | 否 | `false` | 是否启用上一周期对比 |
| `charts[]` | array | 是 | `[{key:"kpi"}]` | 至少 1 项 chart-key |

> `logs`（Phase 1 原始日志）和 `reports`（Phase 4 报表中心）是独立 group，**不走 batch 模式**。

**输出字段**：

| 字段 | 类型 | 说明 |
|------|------|------|
| `data.data` | map | key=chart-key；value 固定为 `{chart_key, render_hint, schema, rows, meta}` 5 字段 Chart 统一契约 |
| `data.meta.elapsed_ms` | int | 整体耗时 |
| `data.meta.cache_hit_ratio` | float | 缓存命中率（0-1） |
| `data.meta.total_charts` | int | 请求 chart 总数 |
| `data.meta.failed_charts` | int | 失败 chart 数 |
| `data.meta.window` | object | 实际命中时间窗 |

**可视化建议**：
- **推荐图表**：本接口本身不直接做图，是数据装载层；前端拿到响应后按 chart-key 分发到各图表组件
- **派生指标**：`data.meta.cache_hit_ratio` 可在 dev 环境做 KPI 数字卡监控大屏的缓存效率

**示例请求/响应**：

```json
// 请求
{
  "page": "overview",
  "time_window": "last_24h",
  "compare": false,
  "charts": [
    { "key": "kpi" },
    { "key": "bandwidth" }
  ]
}
```

```json
// 响应
{
  "code": 0,
  "data": {
    "data": {
      "kpi": {
        "chart_key": "overview/kpi",
        "render_hint": "kpi",
        "schema": {
          "dimensions": [],
          "measures": [
            { "name": "domain_count", "type": "integer", "unit": "" },
            { "name": "requests",     "type": "integer", "unit": "requests" },
            { "name": "blocked",      "type": "integer", "unit": "events" },
            { "name": "block_rate",   "type": "percent", "unit": "%" },
            { "name": "qps",          "type": "float",   "unit": "qps" },
            { "name": "ai_detect",    "type": "integer", "unit": "events" }
          ]
        },
        "rows": [
          { "domain_count": 8, "requests": 12345, "blocked": 678, "block_rate": 5.49, "qps": 0.143, "ai_detect": 0 }
        ],
        "meta": { "source": "postgres", "cache": "miss", "latency_ms": 10 }
      },
      "event-type": {
        "chart_key": "overview/event-type",
        "render_hint": "categorical_distribution",
        "schema": {
          "dimensions": [{ "name": "event_type", "type": "string" }],
          "measures": [{ "name": "count", "type": "integer", "unit": "events" }]
        },
        "rows": [
          { "event_type": "sql_injection", "count": 1234 },
          { "event_type": "xss", "count": 567 }
        ],
        "meta": { "source": "elasticsearch", "cache": "miss", "latency_ms": 15, "partial": false }
      }
    },
    "meta": {
      "elapsed_ms": 22,
      "cache_hit_ratio": 0,
      "total_charts": 2,
      "failed_charts": 0,
      "window": { "stime": 1746662400000, "etime": 1746748800000, "granularity": "1h" }
    }
  }
}
```

> **注意**：上例中 `kpi` 的 measure 名 `requests` / `blocked` 是对外契约真值名（与 `pkg/chart/contract` 真值结构体对齐）。底层数据库字段名不对外暴露，前端严格按 `schema.measures[].name` 取值。

---

#### `POST /api/analytics/{page}/batch` — 页面级 Batch

**用途**：与 `POST /api/analytics/batch` 等价，但 `page` 由 URL 决定（前端固定页面调用更直观）。

**鉴权**：根据 `{page}` 映射到对应 `analytics.<page>.view`。

**支持的 page 值**：

| URL | 权限 |
|-----|------|
| `POST /api/analytics/overview/batch` | `analytics.overview.view` |
| `POST /api/analytics/access/batch` | `analytics.access.view` |
| `POST /api/analytics/protect/batch` | `analytics.protect.view` |
| `POST /api/analytics/ai/batch` | `analytics.ai.view` |
| `POST /api/analytics/bot/batch` | `analytics.bot.view` |
| `POST /api/analytics/alert/batch` | `analytics.alert.view` |
| `POST /api/analytics/health/batch` | `analytics.health.view` |
| `POST /api/analytics/ops/batch` | `analytics.ops.view` |
| `POST /api/analytics/closure/batch` | `analytics.closure.view` |
| `POST /api/analytics/cache/batch` | `analytics.cache.view` |

**输入/输出**：与 `POST /api/analytics/batch` 完全一致；调用方不需要在请求体里再传 `page`，即使传了也以路径中的页面名为准。

**可视化建议**：同上。

---

### 6.3 单图 GET 调用入口

#### `GET /api/analytics/{page}/{chart}` — 单图通用入口

**用途**：拉取单个 chart-key 的数据。`{page}` 取值同 batch；`{chart}` 取值参见各页面小节的 chart-key 索引表。

**鉴权**：根据 `{page}` 映射到 `analytics.<page>.view`（少数特殊 chart 用独立权限，详见各小节）。

**输入参数**：见 [§A 通用查询参数](#a-analytics-通用查询参数)。

**输出字段**：统一信封 + `data`：

- `data` 固定为 Chart 统一契约：`{chart_key, render_hint, schema, rows, meta}`
- `schema` 声明 `rows` 的维度列与指标列；调用方不要从底层数据库字段名推断响应结构

**可视化建议**：前端按 `render_hint` 自动分发到对应图表组件；复杂图表的列定义以 `schema` 为准。

---

### 6.4 总览页面（Overview）

> 适用场景：WAF 防护监控大屏首页 KPI + 趋势 + 排行 + 地图。

下表所有 `GET` 接口都用 [§6.0 单图 GET 调用模板](#62-跨页-batch)、[§A 通用查询参数](#a-analytics-通用查询参数) 和 Chart 统一契约响应结构。

| API | chart-key | render_hint | 推荐图表 | 说明 |
|-----|-----------|---|----------|------|
| `GET /api/analytics/overview/kpi` | `kpi` | `kpi` | KpiGroupCard（6 measure） | 站点数 / requests / blocked / block_rate / qps / ai_detect |
| `GET /api/analytics/overview/bandwidth` | `bandwidth` | `time_series_multi` | 折线图（双 Y 轴） | 总带宽与回源带宽时序 |
| `GET /api/analytics/overview/request-attack` | `request-attack` | `time_series_multi` | 折线图（双系列） | 请求量 vs 攻击量对比 |
| `GET /api/analytics/overview/event-type` | `event-type` | `categorical_distribution` | 饼图 / 环形图 | 事件类型分布（dim=event_type, measure=count） |
| `GET /api/analytics/overview/waf-type` | `waf-type` | `categorical_distribution` | 饼图 / 环形图 | WAF 命中类型分布 |
| `GET /api/analytics/overview/geo` | `geo` | `geo` | 中国/世界地图热力 | 攻击来源地理分布 |
| `GET /api/analytics/overview/top-domains` | `top-domains` | `topn` | 横向 bar / 表格 | 被攻击域名 TOP 5 |
| `GET /api/analytics/overview/recent-events` | `recent-events` | `table` | 时间线 / 表格（50 条） | 最近 WAF 事件流 |

#### chart-key 数据形态详解

**`kpi`**（`render_hint = kpi`）

`schema.measures` 共 6 项：

| measure name | type | unit | 说明 |
|---|---|---|---|
| `domain_count` | integer | (空) | 站点数 |
| `requests` | integer | requests | 时间窗内总请求量 |
| `blocked` | integer | events | 时间窗内总拦截量 |
| `block_rate` | percent | % | 拦截率 = blocked / requests |
| `qps` | float | qps | QPS = requests / 窗口秒数 |
| `ai_detect` | integer | events | AI 识别数（当前固定 0，后续接 ES 实数） |

`rows` 单行：`[ { domain_count, requests, blocked, block_rate, qps, ai_detect } ]`。

> **对外字段名**：`requests` / `blocked` 是 API 契约字段名。底层数据库若仍使用 `request_today` / `attack_today` 等历史字段，由后端在服务层转换，不暴露给调用方。

**推荐图表**：`KpiGroupCard`（6 个 KPI 数字卡），`block_rate` 用百分比 + 进度条；`qps` 配迷你 sparkline。

---

**`bandwidth` / `request-attack`** — 时序双系列

输出 `[{ctime: int64ms, bandwidth: float, origin_bandwidth: float}, ...]` 或 `[{ctime, requests, attacks}, ...]`。

**推荐图表**：折线图，X 轴 `ctime`，Y 轴双系列。

---

**`event-type`**（`render_hint = categorical_distribution`）

| schema 部分 | 内容 |
|---|---|
| `dimensions` | `[{ name: "event_type", type: "string" }]` |
| `measures` | `[{ name: "count", type: "integer", unit: "events" }]` |

`rows` 长表：`[ { event_type: "sql_injection", count: 1234 }, { event_type: "xss", count: 567 }, ... ]`。

**推荐图表**：`PieCard`（≤8 类自动饼图）/ `BarCard`（>8 类自动横向 bar）；前端按 `categorical_distribution` 词汇分发。

---

**`waf-type`** — 维度分布

输出 `[{key: string, count: int}, ...]`。

**推荐图表**：饼图（≤ 8 类）或环形图。

---

**`geo`** — 地理热力

输出 `[{region: string, count: int}, ...]`，region 为国家/省份名。

**推荐图表**：地图热力（中国地图 + 世界地图叠加）。

---

**`top-domains`** — 域名排行

输出 `[{host: string, attack_count: int}, ...]`，按 attack_count DESC，最多 5 条。

**推荐图表**：横向 bar 图。

---

**`recent-events`** — 最近事件流

输出 `[{ctime, domain, attack_type, severity, ...}, ...]`，最多 50 条。

**推荐图表**：时间线 / 表格（按 ctime 倒序），可点击进入详情。

---

#### `POST /api/analytics/overview/export` — 总览页导出

**用途**：把 KPI 与图表快照导出为 CSV 或 JSON 文件，供线下分析或汇报。**返回原文文件流，不走信封**。

**鉴权**：`analytics.overview.export`

**输入参数**（请求体）：

| 字段 | 类型 | 必填 | 取值/示例 | 说明 |
|------|------|:---:|----------|------|
| `format` | string | 是 | `csv` / `json` | 导出格式 |
| `window` | string | 否 | `last_24h` | 时间窗 |
| `charts[]` | array | 是 | `[{key:"kpi"}]` | 要导出的 chart-key 列表 |

**输出**：直接返回文件流，`Content-Type: text/csv` 或 `application/json`，`Content-Disposition: attachment`。

**可视化建议**：不适合图表；触发后浏览器下载。

---

### 6.5 访问分析页面（Access）

> 适用场景：流量与质量分析大屏 — 看请求量、流量、缓存命中、状态码、耗时分布、运营商、TOP IP/URL、地域。

| API | chart-key | render_hint | 推荐图表 | 说明 |
|-----|-----------|---|----------|------|
| `GET /api/analytics/access/request-hm` | `request-hm` | `time_series_single`（无 compare）/ `time_series_multi`（compare） | LineCard | 请求量趋势；`compare=true` 走子形态 B（period 维度区分 current/previous） |
| `GET /api/analytics/access/flow-hm` | `flow-hm` | `time_series_multi` | LineCard 多线 | 5 measure：`total_bytes` / `request_bytes` / `response_bytes` / `upstream_send` / `upstream_receive` |
| `GET /api/analytics/access/cache-hm` | `cache-hm` | `time_series_multi` | 折线图（双 Y 轴） | 缓存命中次数 + 缓存字节趋势 |
| `GET /api/analytics/access/bandwidth` | `bandwidth` | `time_series_multi` | LineCard 多线 | 4 measure：`bandwidth` / `origin_bandwidth` / `up_bandwidth` / `down_bandwidth` |
| `GET /api/analytics/access/status` | `status` | `categorical_distribution_over_time` | StackedBarCard | 4 类 HTTP 状态码（dim=status_class enum["2xx","3xx","4xx","5xx"] + time）按时间堆叠 |
| `GET /api/analytics/access/flow-duration` | `flow-duration` | `time_series_multi` | 折线图（3 分位） | 请求耗时 P50/P95/P99（D4：仅时间窗 ≤ 24h 走实时计算） |
| `GET /api/analytics/access/isp` | `isp` | `categorical_distribution` | 饼图 | 运营商分布（移动/联通/电信/其它） |
| `GET /api/analytics/access/top-ip` | `top-ip` | `topn` | 表格 / 横向 bar（含地理） | 访问 IP TOP（默认 10，可调 `top`） |
| `GET /api/analytics/access/top-url` | `top-url` | `topn` | 表格 / 横向 bar | URL TOP，可按 `order=bytes_desc/cache_desc` 切换排序 |
| `GET /api/analytics/access/geo` | `geo` | `geo` | 中国/世界地图热力 | 访问来源地理分布 |

#### chart-key 数据形态详解

**`request-hm`**

无 compare（`render_hint = time_series_single`）：

| schema 部分 | 内容 |
|---|---|
| `dimensions` | `[{ name: "time", type: "timestamp", unit: "ms" }]` |
| `measures` | `[{ name: "requests", type: "integer", unit: "requests" }]` |

`rows`：`[ { time: 1746748800000, requests: 1234 }, ... ]`。

开 compare（`render_hint = time_series_multi` 子形态 B：`1 categorical + 1 ts + 1 measure`）：

| schema 部分 | 内容 |
|---|---|
| `dimensions` | `[{ name: "period", type: "enum", values: ["current","previous"] }, { name: "time", type: "timestamp", unit: "ms" }]` |
| `measures` | `[{ name: "requests", type: "integer", unit: "requests" }]` |

`rows`：`[ { period: "current", time: ..., requests: ... }, { period: "previous", time: ..., requests: ... }, ... ]` 长表，按 `period` pivot 成 2 条 series。

**推荐图表**：`LineCard`；compare 模式按 `period` pivot 双色实虚线，前端自动处理。

---

**`flow-hm`**（`render_hint = time_series_multi`）

| schema 部分 | 内容 |
|---|---|
| `dimensions` | `[{ name: "time", type: "timestamp", unit: "ms" }]` |
| `measures` | 5 项：`total_bytes` / `request_bytes` / `response_bytes` / `upstream_send` / `upstream_receive`（type=integer，unit=bytes，format=iec） |

`rows`：`[ { time: ..., total_bytes: ..., request_bytes: ..., response_bytes: ..., upstream_send: ..., upstream_receive: ... }, ... ]`。

**推荐图表**：`LineCard` 多线（5 条）/ 堆叠面积。

---

**`cache-hm`** — 缓存趋势

输出 `[{ctime, cache_count, cache_bytes, cache_response}, ...]`。

> **真值字段**（D8）：底层用 `total_cache_count` / `total_cache_bytes` / `total_cache_response_bytes`；返回时映射为 `cache_count` / `cache_bytes` / `cache_response`。

**推荐图表**：双 Y 轴折线（左轴次数，右轴字节）。

---

**`bandwidth`**（`render_hint = time_series_multi`）

| schema 部分 | 内容 |
|---|---|
| `dimensions` | `[{ name: "time", type: "timestamp", unit: "ms" }]` |
| `measures` | 4 项：`bandwidth` / `origin_bandwidth` / `up_bandwidth` / `down_bandwidth`（type=float，unit=bps） |

`rows`：`[ { time: ..., bandwidth: ..., origin_bandwidth: ..., up_bandwidth: ..., down_bandwidth: ... }, ... ]`。

**推荐图表**：`LineCard` 多线（4 条）。

---

**`status`**（`render_hint = categorical_distribution_over_time`）

| schema 部分 | 内容 |
|---|---|
| `dimensions` | `[{ name: "status_class", type: "enum", values: ["2xx","3xx","4xx","5xx"] }, { name: "time", type: "timestamp", unit: "ms" }]` |
| `measures` | `[{ name: "count", type: "integer", unit: "requests" }]` |

`rows` tidy 长表（**禁止横铺 c2xx/c3xx/c4xx/c5xx**）：

```json
[
  { "status_class": "2xx", "time": 1746748800000, "count": 1200 },
  { "status_class": "3xx", "time": 1746748800000, "count": 30 },
  { "status_class": "4xx", "time": 1746748800000, "count": 8 },
  { "status_class": "5xx", "time": 1746748800000, "count": 0 },
  { "status_class": "2xx", "time": 1746752400000, "count": 1340 },
  ...
]
```

每个时间点必须覆盖 4 个 status_class（缺则补 `count: 0`，前端堆叠柱图依赖完整网格）。

**推荐图表**：`StackedBarCard`（按 `status_class` 堆叠时序）。前端按 `categorical_distribution_over_time` 词汇分发。

---

**`flow-duration`** — 耗时分位

输出 `{p50, p95, p99}` 或时序数组。

> **真值边界**（D4）：百分位字段**仅时间窗 ≤ 24h** 时由 ES 实时计算；窗口更长时该接口返回 `available:false`。

**推荐图表**：折线图（3 分位曲线）或 KPI 数字卡（3 个）。

---

**`isp`** — 运营商分布

输出 `{mobile, unicom, telecom, other}`。

**推荐图表**：饼图（4 段）。

---

**`top-ip`** — IP TOP

输出 `[{remote_addr, count, country, region, isp}, ...]`，最多 `top` 条。

**推荐图表**：表格（含国旗 + 地区 + 运营商）；或横向 bar 图。

---

**`top-url`** — URL TOP

输出 `[{url, request_count, request_bytes, cache_bytes}, ...]`。

**推荐图表**：表格（默认）；或横向 bar（可按字节/缓存切换）。

---

**`geo`** — 地理热力

输出 `[{region, count}, ...]`。

**推荐图表**：地图热力。

---

### 6.6 防护分析页面（Protect）

> 适用场景：WAF / CC / DDoS 三大防护引擎的命中分析。

| API | chart-key | render_hint | 推荐图表 | 说明 |
|-----|-----------|---|----------|------|
| `GET /api/analytics/protect/overview` | `overview` | `kpi` | 多 KPI 卡 | WAF/CC/DDoS 总量汇总（时序由 statistics 系列独立 chart 提供） |
| `GET /api/analytics/protect/waf/statistics` | `waf/statistics` | `time_series_multi` | 折线图 | WAF 命中趋势（waf 命中数 + 总攻击数） |
| `GET /api/analytics/protect/waf/types` | `waf/types` | `categorical_distribution` | 饼图 / 横向 bar | WAF 命中类型分布（SQL 注入/XSS/扫描器等） |
| `GET /api/analytics/protect/waf/top-ip` | `waf/top-ip` | `topn` | RankingCard / 横向 bar | dim=ip(string), measure=count(events)，country/province 进 `meta.row_extras` |
| `GET /api/analytics/protect/waf/geo` | `waf/geo` | `geo` | GeoHeatmapCard | dim=country(geo), measure=count(events)；`provinces` 暂存 `meta.provinces` |
| `GET /api/analytics/protect/cc/statistics` | `cc/statistics` | `time_series_single` | 折线图 | CC 命中趋势 |
| `GET /api/analytics/protect/cc/top-ip` | `cc/top-ip` | `topn` | 表格 / 横向 bar | CC 攻击 IP TOP |
| `GET /api/analytics/protect/cc/geo` | `cc/geo` | `geo` | 地图热力 | CC 攻击地域 |
| `GET /api/analytics/protect/cc/top-url` | `cc/top-url` | `topn` | 表格 | CC 攻击 URL TOP |
| `GET /api/analytics/protect/ddos/statistics` | `ddos/statistics` | `time_series_multi` | 折线图（双 Y 轴） | DDoS 事件数 + 峰值带宽时序 |
| `GET /api/analytics/protect/ddos/types` | `ddos/types` | `categorical_distribution` | 饼图 / 横向 bar | DDoS 攻击类型分布（syn flood/udp flood 等） |
| `GET /api/analytics/protect/ddos/top-ip` | `ddos/top-ip` | `topn` | 表格 | DDoS 源 IP TOP（含峰值带宽） |

#### chart-key 数据形态详解（关键差异）

**`overview`**：`render_hint = kpi`，`rows = [{waf, cc, ddos_bytes}]`（单行汇总，dimensions=[]，measures=waf/cc/ddos_bytes）。推荐 3 KPI 卡；时序由 `protect/waf/statistics` 与 `protect/ddos/statistics` 独立 chart 提供。

**`waf/statistics`**：`render_hint = time_series_multi`，`rows = [{time, waf, attack_count}, ...]` tidy 长表（time=ms 时间戳，2 measure 双线）。推荐折线图（双系列）。

**`waf/types`** / **`ddos/types`**：`render_hint = categorical_distribution`，`rows = [{waf_type, count}, ...]` / `[{ddos_type, count}, ...]` tidy 长表，用饼图或横向 bar。

**`waf/top-ip`**（`render_hint = topn`）

| schema 部分 | 内容 |
|---|---|
| `dimensions` | `[{ name: "ip", type: "string" }]` |
| `measures` | `[{ name: "count", type: "integer", unit: "events" }]` |

`rows`：`[ { ip: "1.2.3.4", count: 1234 }, { ip: "5.6.7.8", count: 567 }, ... ]`，已按 count DESC 排序。country / province 等附加列进 `meta.row_extras`（前端按需取，不入 schema 列）。

**推荐图表**：`RankingCard` / 横向 bar 图。

---

**`ddos/top-ip`**：含地理的 IP TOP，推荐含国旗的表格。

---

**`waf/geo`**（`render_hint = geo`）

| schema 部分 | 内容 |
|---|---|
| `dimensions` | `[{ name: "country", type: "geo" }]` |
| `measures` | `[{ name: "count", type: "integer", unit: "events" }]` |

`rows`：`[ { country: "China", count: 1234 }, { country: "US", count: 567 }, ... ]`。

> **provinces 数据**暂存 `meta.provinces`（结构 `[{province, count}]`）。当前前端 GeoHeatmapCard 渲染主图用 `rows`，省级钻取用 `meta.provinces`；后续如需要省级独立图表，再新增 `protect/waf/geo-provinces` chart-key。

**推荐图表**：`GeoHeatmapCard`（中国 / 世界地图热力）。

---

**`cc/geo`**：`render_hint = geo`，`rows = [{country, count}, ...]`，地图热力。

**`ddos/statistics`**：`render_hint = time_series_multi`，`rows = [{time, events, bandwidth}, ...]` tidy 长表（events=integer events，bandwidth=float bytes/iec）。推荐双 Y 轴折线。

---

### 6.7 AI 识别页面（AI）

> 当前 AI 页面路径已发布。`/logs` 走独立 `analytics.ai.logs` 权限，其它走 `analytics.ai.view`。
> 第一版多数 chart 返回 BatchChartResult 占位（`rows: []`，`meta.available = false`，`meta.reason` 说明原因），路径与契约稳定，后端可在保持路径不变的前提下逐步升级真实数据。

| API | chart-key | 推荐图表 | 说明 |
|-----|-----------|----------|------|
| `GET /api/analytics/ai/attack-trend` | `attack-trend` | 折线图 | AI 攻击趋势 |
| `GET /api/analytics/ai/top-ip` | `top-ip` | 表格 / 横向 bar | AI 命中 IP TOP |
| `GET /api/analytics/ai/top-url` | `top-url` | 表格 | AI 命中 URL TOP |
| `GET /api/analytics/ai/detection` | `detection` | KPI 数字卡 / 雷达图 | AI 检测能力面板 |
| `GET /api/analytics/ai/test-results` | `test-results` | 表格 / 柱图 | AI 测试结果 |
| `GET /api/analytics/ai/logs` | `logs` | 表格（明细，分页） | AI 命中日志明细（独立权限 `analytics.ai.logs`） |

> **占位响应**：未升级的 chart 返回标准 BatchChartResult（5 字段齐全，`rows: []`，`meta.available = false`，`meta.reason` 说明原因），前端应渲染"暂无数据"占位卡片，不展示假数据。

---

### 6.8 主动防护 / Bot 页面

> 适用场景：识别和分析爬虫/Bot 流量。

| API | chart-key | 推荐图表 | 说明 |
|-----|-----------|----------|------|
| `GET /api/analytics/bot/statistics` | `statistics` | KPI（6 个） | Bot 请求/会话/IP/已知未知 Bot 总览（趋势线由独立 chart 提供，未拆时不展示） |
| `GET /api/analytics/bot/advance-warn` | `advance-warn` | 表格 | Bot 预警列表 |
| `GET /api/analytics/bot/browser` | `browser` | 饼图 | 浏览器分布（chrome/safari/firefox/edge/wechat/other） |
| `GET /api/analytics/bot/operating` | `operating` | 饼图 | 操作系统分布（android/ios/windows/mac/other） |
| `GET /api/analytics/bot/geo` | `geo` | 地图热力 | Bot 地域分布 |
| `GET /api/analytics/bot/top-agent` | `top-agent` | 表格 / 横向 bar | User-Agent TOP |
| `GET /api/analytics/bot/top-ip` | `top-ip` | 表格（含地理） | Bot IP TOP |
| `GET /api/analytics/bot/scatter` | `scatter` | 散点图 | Bot 预警散点（X=ctime / Y=top_visit_count，size=top_ip_count） |
| `GET /api/analytics/bot/sessions` | `sessions` | 表格（分页） | Bot 会话列表（独立权限 `analytics.bot.session`） |
| `GET /api/analytics/bot/sessions/{sid}` | - | 时间线 | 单个 Bot 会话时间线详情（独立权限 `analytics.bot.session`） |

#### chart-key 数据形态详解

**`statistics`**：`render_hint = kpi`，`rows = [{requests, sessions, ips, known_bot, unknown_bot, req_per_session}]`（单行汇总，6 measure）。推荐 6 个 KPI 卡；请求/会话趋势由独立 chart 提供（当前未拆，需要时新增 `bot/sessions-trend`）。

**`browser`**：`render_hint = categorical_distribution`，`rows = [{browser, count}, ...]`（6 行 enum：chrome/safari/firefox/edge/wechat/other），饼图。

**`operating`**：`render_hint = categorical_distribution`，`rows = [{os, count}, ...]`（5 行 enum：android/ios/windows/mac/other），饼图。

**`scatter`**：`render_hint = table`（无 scatter hint，table 兜底），`rows = [{time, session_id, top_visit_count, top_ip_count, top_ua_count}, ...]`，由前端解析 X/Y/size 渲染散点图。

**`sessions/{sid}`**：`render_hint = table`，`rows = [{time, uri, remote_addr, method, status}, ...]`（5 字段裁剪），是该 session 的全量访问记录时间线；未传 session_id（通过 query `order` 参数）时返空。

---

### 6.9 告警统计页面（Alert）

> 适用场景：告警总览 + 列表 + 单条详情 + 确认。

| API | 方法 | chart-key | 推荐图表 | 说明 |
|-----|------|-----------|----------|------|
| `GET /api/analytics/alert/total` | GET | `total` | KPI 数字卡 | 告警总数 |
| `GET /api/analytics/alert/hm` | GET | `hm` | 折线图 / 热力图 | 告警趋势（按 ctime 聚合 count） |
| `GET /api/analytics/alert/types` | GET | `types` | 饼图 | 告警类型分布（按 policy_type） |
| `GET /api/analytics/alert/domains` | GET | `domains` | 横向 bar / 表格 | 告警域名排行 |
| `GET /api/analytics/alert/list` | GET | `list` | 表格（分页） | 告警列表 |
| `GET /api/analytics/alert/{id}` | GET | - | 详情卡片 | 单条告警详情（含处置元信息） |
| `PATCH /api/analytics/alert/{id}/ack` | PATCH | - | 不适合图表 | 确认指定告警 |

#### `GET /api/analytics/alert/{id}` 输出字段

| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | int | 告警 ID |
| `uuid` | string | 告警 UUID |
| `policy_type` | string | 告警类型 |
| `title` / `body` | string | 告警标题 / 内容 |
| `domain` / `domain_id` | string | 关联域名 |
| `status` | int | 告警状态（0/1/2/3，参见 D10） |
| `ctime` | int64 | 创建时间 |
| `last_update_timestamp` | int64 | 最后更新时间 |
| `process_uid` | string | 处置人（D10 真值） |
| `process_time` | int64 | 处置时间（D10 真值） |
| `user_id` | string | 归属用户 |

> 非超管按 `user_id` 强制过滤；越权读取返回 404。所需权限 `analytics.alert.view`。

#### `PATCH /api/analytics/alert/{id}/ack` 请求

**鉴权**：`analytics.alert.ack`

**输入**：path `id`，body 可空。

**输出**：`data` 为 `null`。

---

### 6.10 业务健康（Health · Phase 3）

> 适用场景：分析业务可用性 + 源站质量 + 慢 URI + 地域质量。

| API | chart-key | 推荐图表 | 说明 |
|-----|-----------|----------|------|
| `GET /api/analytics/health/summary` | `summary` | KPI 数字卡（6 个） | c2xx/c3xx/c4xx/c5xx/n4xx/n5xx 总数 |
| `GET /api/analytics/health/status-breakdown` | `status-breakdown` | 堆叠柱图（时序） | 状态码三层 c/n/a 拆分（c=客户端、n=网关、a=应用） |
| `GET /api/analytics/health/origin-errors` | `origin-errors` | 表格 / 横向 bar | 源站异常排行（ES upstream_addr terms，仅 upstream_status >= 500） |
| `GET /api/analytics/health/origin-latency` | `origin-latency` | 折线图（3 系列） | 源站时延（移动/联通/电信 平均时延） |
| `GET /api/analytics/health/slow-uri` | `slow-uri` | 表格 | 慢 URI TOP（第一版按命中次数 TOP；慢请求排序后续按真实耗时数据接入） |
| `GET /api/analytics/health/availability` | `availability` | KPI / 多线折线 | HTTP/Ping/DNS/TCP/Page/IPv6 可用率 + 不可用时长 |
| `GET /api/analytics/health/geo-isp-quality` | `geo-isp-quality` | 表格 / 地图 | 地域/运营商质量 |

> **真值边界**：percentile / p50 / p95 / p99 在 chart 与 statistic 包**均无**字段；时间窗 ≤ 24h 才走 ES 实时 percentile（D4）。可用性表 `m_ava_domain.*AvailableDomain` 主键是 `domain_or_ip`（不是 `domain_id`）。

---

### 6.11 平台运维（Ops · Phase 5）

> 适用场景：平台级运营视角 — 看高流量/高错误用户和域名、源站异常、节点容量。

> **权限边界**：`analytics.ops.view` 只读全平台数据，**不允许**传 `target_user_id` 切换视角；`analytics.ops.admin` 才能切换。普通客户级账号即使有 view 也不能访问 ops。

| API | chart-key | 推荐图表 | 说明 |
|-----|-----------|----------|------|
| `GET /api/analytics/ops/summary` | `summary` | KPI 数字卡（6 个） | 全平台容量摘要（请求/字节/峰值带宽/源站带宽/活跃域名/活跃用户） |
| `GET /api/analytics/ops/traffic-users` | `traffic-users` | 横向 bar / 表格 | 高流量用户 TOP（按 total_bytes DESC） |
| `GET /api/analytics/ops/traffic-domains` | `traffic-domains` | 横向 bar / 表格 | 高流量域名 TOP |
| `GET /api/analytics/ops/error-users` | `error-users` | 表格 | 高错误用户 TOP（按 c5xx DESC，含 c4xx/n5xx） |
| `GET /api/analytics/ops/error-domains` | `error-domains` | 表格 | 高错误域名 TOP |
| `GET /api/analytics/ops/origin-errors` | `origin-errors` | 表格 | 源站异常排行 |
| `GET /api/analytics/ops/nodes` | `nodes` | 表格 / 拓扑图 | 节点/机房视图（RPC GetWafIpWithMachineRoom + ES `server_addr/bind_addr`） |
| `GET /api/analytics/ops/query-pressure` | `query-pressure` | 折线图（占位） | ES 查询压力（依赖埋点，第一版返回 `available:false`） |

> **真值边界**：节点系统指标（CPU/内存/磁盘）chart **不存**，需走外部 zabbix。本期不实现。`node_id` / `server_node` 是禁字段。

---

### 6.12 处置闭环（Closure · Phase 6）

> 适用场景：在一个页面同时处理告警和风险队列；支持批量确认。

> **真值字段**（D10）：`process_uid` / `process_time` / `status` / `level`。**禁字段**：`handle_user` / `handle_time` / `risk_score` / `alert_status`。AlertRecord.status (0/1/2/3) 与 RiskRecord.status (1/2) 语义不同，前端 i18n key 不可共用。

| API | 方法 | chart-key | 推荐图表 | 说明 |
|-----|------|-----------|----------|------|
| `GET /api/analytics/closure/summary` | GET | `summary` | KPI 数字卡（5 个） | 待处理告警/风险数 + 已处置数 + 平均处置时长（ms） |
| `GET /api/analytics/closure/alerts` | GET | `alerts` | 表格（分页） | 待处理告警队列（status=0） |
| `GET /api/analytics/closure/risks` | GET | `risks` | 表格（分页） | 待处理风险队列（后续接 RiskRecord 表；当前无数据时返回空列表） |
| `GET /api/analytics/closure/trend` | GET | `trend` | 折线图 / 堆叠柱 | 处置历史趋势（按 ctime + status 分组） |
| `POST /api/analytics/closure/alerts/confirm` | POST | - | 不适合图表 | 批量确认告警（代理 `/api/alert/records/confirm`） |
| `POST /api/analytics/closure/risks/confirm` | POST | - | 不适合图表 | 批量确认风险（代理 `/api/chart/risk/events/:event_id/confirm`） |

#### `summary` 输出字段

```json
{
  "alerts_pending": 12,
  "alerts_handled_today": 8,
  "risks_pending": 0,
  "risks_handled_today": 0,
  "avg_handle_time_ms": 0
}
```

#### confirm 接口请求体

```json
{
  "ids": ["a1", "a2", "a3"],
  "remark": "已加黑名单"
}
```

**鉴权**：`analytics.closure.confirm`

**输出**：`data` 为 `null`。

---

### 6.13 缓存收益（Cache · Phase 6）

> 适用场景：分析 CDN/边缘缓存对回源带宽的节省价值。

> **真值字段**（D8）：`total_cache_count` / `total_cache_bytes` / `total_cache_response_bytes`。**禁字段**：`cache_count` / `cache_bytes` / `cache_hit` 单字段命名（不存在）。

| API | chart-key | 推荐图表 | 说明 |
|-----|-----------|----------|------|
| `GET /api/analytics/cache/summary` | `summary` | KPI 数字卡（4 个） | 命中率 / 节省回源字节 / 命中次数 / 平均缓存对象大小 |
| `GET /api/analytics/cache/trend` | `trend` | 折线图（双 Y 轴） | 命中率趋势（请求数 vs 缓存命中数） |
| `GET /api/analytics/cache/top-uri` | `top-uri` | 表格 / 横向 bar | URI TOP（命中次数/命中率/节省字节） |
| `GET /api/analytics/cache/content-types` | `content-types` | 饼图 | 内容类型分布（response_content_type 聚合） |

#### `summary` 输出字段

| 字段 | 类型 | 说明 |
|------|------|------|
| `hit_rate` | float | 缓存命中率 = total_cache_count / request_count |
| `saved_response_bytes` | int | 节省回源带宽（源站本应承担但缓存挡住的字节） |
| `total_cache_count` | int | 命中次数 |
| `total_cache_bytes` | int | 命中字节 |
| `avg_object_bytes` | float | 平均缓存对象大小 = total_cache_bytes / total_cache_count |
| `request_count` | int | 总请求数 |

**业务化指标**：
- 缓存命中率 = `total_cache_count / request_count`
- 节省回源带宽 = `total_cache_response_bytes`
- 平均缓存对象大小 = `total_cache_bytes / total_cache_count`

---

### 6.14 原始日志（Logs · Phase 1）

> 原始日志是明细查询，不是图表单图接口；它使用统一鉴权方式，但请求/响应以列表、详情、导出为主。

| API | 方法 | 权限 | 推荐图表 | 说明 |
|-----|------|------|----------|------|
| `GET /api/analytics/logs` | GET | `analytics.logs.view` | 表格（分页 + 12 字段筛选） | 分页查询原始访问/攻击日志 |
| `GET /api/analytics/logs/{uuid}` | GET | `analytics.logs.view` | 详情卡片（7 区块） | 单条日志详情（basic/request/response/upstream/protection/waf_detail/ai_detail） |
| `POST /api/analytics/logs/export` | POST | `analytics.logs.export` | 不适合图表 | 字段白名单导出（csv/json，size ≤ 10000，超出走异步任务） |

#### `GET /api/analytics/logs` 筛选字段

12 字段筛选：`uuid` / `session_id` / `remote_addr` / `host` / `uri` / `method` / `status` / `z_final_action` / `z_final_type` / `z_final_mod` / `z_final_action_type` / `z_white`。

> **真值边界**：`z_final_action` 整数枚举 `0=放行 / 1=拦截 / 2=验证码`；`z_white` 是独立 bool 字段（白名单命中），**不参与** action 枚举。**禁字段**：`match_content` / `match_area` / `hit_rule` / `rule_desc`（chart 与 statistic 包均无）。

#### 第一版兜底响应

第一版 stub 返回：

```json
{ "available": false, "reason": "原始日志 ES 查询待接入..." }
```

列表/详情/导出契约稳定，后续接 ES `zcloud-access-*`。

---

### 6.15 报表中心（Reports · Phase 4）

> 报表中心是模板、生成、下载类接口，不使用单图响应结构。列表和详情仍使用统一 JSON 信封；下载接口返回文件流。

| API | 方法 | 权限 | 推荐图表 | 说明 |
|-----|------|------|----------|------|
| `GET /api/analytics/reports/templates` | GET | `analytics.reports.view` | 表格 / 卡片网格 | 模板列表（含 `platform_only` 标记） |
| `GET /api/analytics/reports` | GET | `analytics.reports.view` | 表格（分页） | 报表历史列表 |
| `GET /api/analytics/reports/{id}` | GET | `analytics.reports.view` | 详情卡片 | 单条报表详情（状态、参数、产物 URL） |
| `POST /api/analytics/reports/generate` | POST | `analytics.reports.generate` | 不适合图表 | 触发生成（`platform-summary` 模板需 `analytics.reports.platform`） |
| `GET /api/analytics/reports/{id}/download` | GET | `analytics.reports.download` | 不适合图表 | 下载产物（pdf/csv/json/html） |

**模板枚举**（D7）：`protection-value` / `asset-risk` / `attack-source` / `business-health` / `platform-summary`（**仅平台运维/超管**） / `raw-log-export`。

**异步阈值**：预估行数 ≤ 100k 走同步；超出强制异步返回 `task_id`，前端轮询 `/reports/:id` 获取状态 + 下载链接。第一版同步生成超时 30 秒，超时降级为异步。

#### generate 请求体

```json
{
  "template": "protection-value",
  "format": "pdf",
  "window": "last_30d",
  "stime": 1735660800000,
  "etime": 1738339200000,
  "filters": {}
}
```

| 字段 | 类型 | 必填 | 说明 |
|------|------|:---:|------|
| `template` | string | 是 | 模板名（D7 闭集） |
| `format` | string | 是 | `pdf` / `csv` / `json` / `html` |
| `window` | string | 否 | 时间窗别名 |
| `stime` / `etime` | int64 | 否 | 自定义时间戳 |
| `filters` | object | 否 | 模板专属筛选条件 |

---

### 6.16 CLI 对应关系

Analytics API 均由 `zcloud analytics` 命令适配：

```bash
# 原 6 page
zcloud analytics overview kpi --format json
zcloud analytics access status --window last_24h --format json
zcloud analytics protect waf/types --format json
zcloud analytics ai logs --page 1 --size 20 --format json
zcloud analytics bot session <session-id> --format json
zcloud analytics alert ack <alert-id>

# 2026-04-30 chart-rebuild 6 phase 扩展（13 条新命令）
zcloud analytics health summary --window last_24h --format json
zcloud analytics ops traffic-users --top 20 --format json
zcloud analytics closure summary --format json
zcloud analytics cache summary --format json
zcloud analytics logs list --window last_24h --status 403 --format json
zcloud analytics logs detail req-abc123 --format json
zcloud analytics logs export --format csv --fields ctime,uuid,host,uri,status > logs.csv
zcloud analytics closure alerts confirm --ids a1,a2,a3
zcloud analytics closure risks confirm --ids ev_001,ev_002
zcloud analytics reports templates --format json
zcloud analytics reports list --format json
zcloud analytics reports describe r-001 --format json
zcloud analytics reports generate --template protection-value --window last_30d --format pdf
zcloud analytics reports download r-001 --output report.pdf
```

后续如果新增 Analytics API，必须同步增加或确认已有 CLI 适配；如果只是新增 chart-key，至少要更新 CLI chart-key 清单与文档。

---

## §7 套餐目录（Plan）

> **对外开放范围说明**：仅以下两个只读接口对外开放，用于查询套餐目录。套餐的创建/编辑/删除、为用户开通、订阅查询，以及订单的**续费/变更/退款/审核**（`/api/plan/orders/*`），均属平台控制台管理操作，直接操作在线计费数据，**不在对外对接 API/CLI 范围**（仅平台运维经控制台 + RBAC 使用）。

### `GET /api/plan/plans` — 套餐列表（分页）

查询套餐目录，支持按产品类型和关键词过滤，分页返回。

**所需权限**：`plan.plan.list`

**Query 参数**

| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|:---:|--------|------|
| `page` | int | 否 | 1 | 页码（从 1 开始） |
| `size` | int | 否 | 20 | 每页条数 |
| `prod_type` | int | 否 | 0 (全部) | 产品类型过滤：2=WAF · 4=Monitor · 32=GFIP |
| `keyword` | string | 否 | — | 套餐名称关键词模糊搜索 |

**响应 `data` 字段**

```json
{
  "list": [ { "plan_id": "...", "name": "基础版", "prod_type": 2, "price": 99.00, "valid": 365, "level": 1, "open_status": true, ... } ],
  "total": 10,
  "page": 1,
  "size": 20
}
```

**示例**

```bash
# Bearer Session
curl -H "Authorization: Bearer $TOKEN" \
  "$API/api/plan/plans?prod_type=2&keyword=基础&page=1&size=20"

# API Key
curl -H "Authorization: ApiKey zck_prefix.secret" \
  "$API/api/plan/plans?prod_type=2"
```

---

### `GET /api/plan/plans/{id}` — 套餐详情

按套餐 ID 查询单个套餐的完整信息（含 `content` 配额 JSON）。

**所需权限**：`plan.plan.view`

**Path 参数**

| 参数 | 类型 | 必填 | 说明 |
|------|------|:---:|------|
| `id` | string | 是 | 套餐 ID（UUID） |

**响应 `data` 字段（`PlanVO`）**

| 字段 | 类型 | 说明 |
|------|------|------|
| `plan_id` | string | 套餐唯一 ID（UUID） |
| `name` | string | 套餐名称 |
| `content` | object | 套餐配额 JSON（各产品类型对应字段不同） |
| `price` | float | 套餐价格（保留 2 位小数） |
| `scene` | string | 适用场景描述 |
| `comment` | string | 备注 |
| `open_status` | bool | 是否公开售卖 |
| `valid` | int64 | 有效期（天数） |
| `effect` | int32 | 生效方式 |
| `level` | int64 | 套餐等级 |
| `creator_id` | string | 创建者 ID |
| `ctime` | int64 | 创建时间（Unix 毫秒） |
| `utime` | int64 | 更新时间（Unix 毫秒） |
| `version` | string | 套餐来源版本（cloud / zmod） |
| `prod_type` | int32 | 产品类型（2=WAF 4=Monitor 32=GFIP） |

**示例**

```bash
curl -H "Authorization: Bearer $TOKEN" \
  "$API/api/plan/plans/550e8400-e29b-41d4-a716-446655440000"
```

**CLI 等价命令**

```bash
zcloud plan list --prod-type 1 --keyword 基础版
zcloud plan describe <plan_id>
```

---

## §8 节点安装 / 升级（Node Install）

> 用途：在防护节点主机上一键安装 / 升级 skynet-node。链路分两侧：**管理面**（平台登录态 + RBAC）注册安装包、生成一次性命令、查询任务、撤销 token；**安装机侧**（仅认安装 token）拉脚本、下载包/env、回报结果。
> 后端实现：`src/backend/internal/node/{handler,service,repo}/install.go`、路由 `src/backend/internal/node/route.go`、回收任务 `src/backend/internal/app/install_reaper.go`。

### 8.0 鉴权与状态码

| 维度 | 管理面接口 | 安装机侧接口 |
|---|---|---|
| 路径 | `/install/artifacts` `/commands` `/upgrades` `/jobs` `/tokens/:id/revoke` | `/install/script` `/package` `/env` `/report` |
| 鉴权 | 平台统一登录态（Bearer Session / API Key）+ RBAC | **仅** `Authorization: Bearer <install_token>` |
| RBAC action（`perms.NodeNode`） | `artifact` / `install` / `upgrade` / `job` / `revoke` | 无（token 自鉴权） |
| 鉴权失败 | 401 / 403（平台统一信封） | **401**，并带 `WWW-Authenticate: Bearer realm="node-install"`（challenge） |

要点：
- 安装 token 永远只在 `Authorization` 头里传递，**绝不进 URL query**。
- 安装机侧四个接口鉴权失败统一返回 `401` + challenge 头；`/env` 在 token 有效但 env 载荷已不可用时返回 `403`；`/report` 的 `job_id` 与 token 绑定 job 不一致返回 `403`。
- cloud 自有表口径固定 `node_install_*`（`node_install_artifacts` / `node_install_tokens` / `node_install_jobs` / `node_install_reports`），仅落 `guard_local`，不写老库。

### 8.1 `POST /api/node/install/artifacts` — 注册本地安装包并预检

注册后端主机上已存在的安装包，扫描并计算 sha256、跑预检。

```bash
curl -sS -X POST https://<cloud>/api/node/install/artifacts \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"name":"skynet-node-1.0.0.tar.gz","version":"1.0.0","package_path":"/data/artifacts/skynet-node-1.0.0.tar.gz"}'
```

约束：
- `package_path` 必须是后端主机**绝对路径**，后缀 `.tar.gz` / `.tgz`，且必须位于**允许的 artifact 根目录**之内。
- 默认允许根目录为 `/data/artifacts,/opt/cloud/artifacts`（可用环境变量 `NODE_INSTALL_ARTIFACT_ROOTS` 覆盖，逗号分隔）。**默认不含 `/tmp`** —— `/tmp` 全局可写，允许它会让任意本地进程投放 tarball 走注册流程；测试需要临时目录时必须显式设置 `NODE_INSTALL_ARTIFACT_ROOTS`。
- 预检阻断项（`severity=error`）未过 → `status=failed`；`warning` / `info` 不阻断 → `status=ready`。阻断项含：`VERSIONS` 清单、`env.conf` 存在且无生产标记、必需 env key、必需 topic 提示、spoa `sig-*` 规则完整。`binary_version_drift`、`pulsar_config_risk` 为 `warning`，不阻断。

响应（节选，`package_path` 仅回**文件名**，不暴露后端绝对路径）：

```json
{"code":0,"data":{
  "artifact_id":"8f1c…","name":"skynet-node-1.0.0.tar.gz","version":"1.0.0",
  "sha256":"…64hex…","status":"ready",
  "precheck":{"passed":true,"checks":[
    {"key":"required_env","severity":"error","passed":true,"message":"required env keys found"},
    {"key":"binary_version_drift","severity":"warning","passed":true,"message":"…"}
  ]}
}}
```

`GET /api/node/install/artifacts` 列出已注册包；列表里的 `package_path` 同样只展示文件名，前端/示例**不要展示后端真实绝对路径**。

### 8.2 `POST /api/node/install/commands` · `POST /api/node/install/upgrades` · `POST /api/node/install/uninstalls` — 生成一次性命令

```bash
curl -sS -X POST https://<cloud>/api/node/install/commands \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{
        "artifact_id":"8f1c…",
        "node_id":"可选-目标节点UUID",
        "server_url":"https://<cloud>",
        "ttl_seconds":3600,
        "max_uses":20,
        "env":{
          "ZCLOUD_NGX_ACCESS_TOPIC":"cloud/ngx",
          "ZCLOUD_CC_TOPIC":"cc/sync",
          "ZCLOUD_SYNC_TOPIC":"zcloud-sync",
          "ZCLOUD_DELTA_TOPIC":"zcloud-delta",
          "ZCLOUD_BLOCK_TOPIC":"zcloud-block"
        }
      }'
```

升级命令用 `/upgrades`，等价于 `action=upgrade`，请求体相同。

卸载命令用 `/uninstalls`，等价于 `action=uninstall`，请求体相同（同 install/upgrade 的 `artifact_id` 必填、`server_url` 必填、`env`/`ttl_seconds`/`max_uses` 可选）。与升级一样**作用于已有节点，`node_id` 必填**（缺省 → 400「卸载必须指定目标节点」）。响应同样是一次性 `command`（`curl … | sudo bash` 一行，明文 token 只出现一次）。权限：`node.node.uninstall`。

> ⚠️ **破坏性、不可恢复**：引导脚本据 `GET /api/node/install/package` 返回的服务端权威响应头 `X-Install-Action`（值取自 token 绑定 job 的动作，此处为 `uninstall`）决定跑包内 **`uninstall.sh`** 而非 `install.sh`，并用 here-string 自动应答其交互式 `[y/N]` 确认。`uninstall.sh` 会**停止并移除该节点上的全部防护服务（nginx / agent / waf-spoa 等）及其数据目录**，操作不可恢复。
>
> **范围**：卸载只移除节点**主机上的服务**，**不删除云端节点列表里的节点记录**。如需同时清掉节点记录，运维另行调用 `DELETE /api/node/nodes/:id`。

响应：

```json
{"code":0,"data":{
  "job_id":"…","token_id":"…","token_prefix":"nit_xxxxxxx",
  "expires_at":1735900000000,
  "command":"curl -fsSL --connect-timeout 10 --max-time 60 -H 'Authorization: Bearer nit_…' 'https://<cloud>/api/node/install/script' | sudo bash -s -- --token 'nit_…' --server 'https://<cloud>'"
}}
```

约束 / 行为：
- **`server_url` 必须匹配 cloud 配置的对外 base URL**（scheme + host[:port]）。否则 400 —— 防止特权调用方把攻击者控制的主机塞进安装命令（安装机会用 token 信任该主机的 package/env/report）。本地测试例外：配置允许 localhost 时，loopback `server_url` 放行。
- `server_url` 必须是绝对 URL，**localhost 之外强制 https**；query / fragment 会被丢弃。
- `env` 是**注册时设定的节点变量**（env.conf 文本）；留空则默认取包内 `env.conf`。值做 shell 单引号转义后下发。
- **env key 采用 deny-list（非 allow-list）**：包是变量无关的，变量由上层在注册时给，故接受任意业务键（如 `REG_URL` / `AGENT_PORT` / `PULSAR_ADDR` 等）；仅当命中敏感词（`PASSWORD/PASSWD/SECRET/PRIVATE/CRED/APIKEY/API_KEY/AUTH_TOKEN/ACCESS_TOKEN/ACCESS_KEY/SECRET_KEY/SIGNING_KEY/BOOTSTRAP_TOKEN`，大小写不敏感）或疑似安装 token / Bearer 凭证时 → 400 且**不创建** token / job。env 文本持久化到 `node_install_jobs.env_payload`，以便后端重启 / 多副本时 `/install/env` 仍可重放。
- **`AGENT_PORT`（可选注册变量）**：设定后本次安装的 agent 以该端口监听 + 注册（默认 `33020`），用于与同机已有 agent 错开端口。端口由 cloud 在 sha256 校验后注入解压出的安装脚本，包文件不被改动。
- `ttl_seconds`：默认 `3600`，上限 `86400`（24h）。
- `max_uses`：**最低 5**（低于 5 自动抬到 5）、默认 `20`、上限 `100`。建议保留重试余量（脚本下载 package + env 至少各消耗 1 次，失败重试还会再消耗），**不要贴着最低值设**。
- `artifact.status != ready` → 400，不创建 token / job。
- **明文 token 只在 `command` 里出现一次**；库中 token 仅存 `sha256` 哈希 + 12 位 `prefix`。`node_install_jobs.command` 只存脱敏模板（`<redacted>`），不含明文 token 或 Bearer 头。
- `job.spec` 只保留白名单字段（`server_url` / `env_sha256` / `env_keys` / `artifact` / `token_prefix`），`env_payload` 在所有 job 响应里都被剥离。

### 8.3 安装机侧：script / package / env / report

`command` 拉取并执行的脚本（`GET /install/script`）会：`set -euo pipefail` + 退出清理临时目录 → 带 `--connect-timeout/--max-time/--retry` 下载 package、env → 校验 `X-Artifact-SHA256` 与本地 `sha256sum` 一致（不一致上报 `failed` 并退出）→ 用云端 env 覆盖包内 `env.conf` → **若注册变量含 `AGENT_PORT` 则注入解压出的 `install*.sh`**（把 agent 监听/注册端口从默认 `33020` 改为该值，仅改解压副本，不动已校验包文件）→ 执行包内 `install.sh` → 经 `/report` 回报 `running` / `success` / `failed`。

**token 配额语义（关键）**：

| 接口 | 鉴权方式 | 是否消耗 `max_uses`（`use_count`） | 计数 / 副作用 |
|---|---|---|---|
| `GET /install/script` | 校验 token 有效性 | **否**（`ValidateBearerNoUse`） | 不消耗，便于脚本可被重复拉取 |
| `GET /install/package` | 校验并占用 | **是**，`use_count+1` | 响应头 `X-Artifact-SHA256` / `X-Install-Job-ID`；job 置 `running` |
| `GET /install/env` | 校验并占用 | **是**，`use_count+1` | 返回 env 文本；env 载荷不可用时 403 |
| `POST /install/report` | `ValidateBearerForReport` | **否** | 独立 `report_count+1`；**不**消耗 `max_uses` |

- `report` 用独立 `report_count` 计数，安装机多次回报（running→success/failed）不会耗尽 `max_uses`。
- **`failed` / `running` 回报不关闭 token**；只有 `success` 才把 token 置 `used`（终态，后续不可用）。
- `failed` 回报**允许在已 `revoked` 的 token 上提交**，以便被运维撤销的安装运行仍能把 job 收敛到 `failed`。
- `report` 若带 `job_id`，必须与 token 绑定 job 一致，否则 403。job 已终态（`success`/`failed`）时滞后回报不覆盖终态。

安装机侧手动联调：

```bash
TOKEN=nit_xxx; BASE=https://<cloud>
curl -fsSL -H "Authorization: Bearer $TOKEN" "$BASE/api/node/install/script"
curl -fsSL -D - -o pkg.tar.gz -H "Authorization: Bearer $TOKEN" "$BASE/api/node/install/package"
curl -fsSL -H "Authorization: Bearer $TOKEN" "$BASE/api/node/install/env"
curl -fsS -X POST "$BASE/api/node/install/report" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"status":"success","message":"done","hostname":"node-1","node_version":"1.0.0"}'
```

### 8.4 `GET /api/node/install/jobs` · `/jobs/:id` — 查询任务

```bash
curl -sS -H "Authorization: Bearer $TOKEN" https://<cloud>/api/node/install/jobs        # 最近 50 条
curl -sS -H "Authorization: Bearer $TOKEN" https://<cloud>/api/node/install/jobs/<job_id>  # 含 reports[]
```

`status`：`pending` → `running` → `success` / `failed`。`started_at` / `finished_at` 为毫秒时间戳，`0` 表示未发生。详情接口附最近 `reports`（最多 20 条）与 `recent_report`。

### 8.5 `POST /api/node/install/tokens/:id/revoke` — 撤销 token

```bash
curl -sS -X POST -H "Authorization: Bearer $TOKEN" \
  https://<cloud>/api/node/install/tokens/<token_id>/revoke
```

- 仅能撤销仍 `active` 的 token；已 `used` / `revoked` 返回 400。
- 撤销后安装机侧 script/package/env 立即 401；`failed` 回报仍可提交以收敛 job。
- token 不落单独的 `expired` 状态，过期统一由 `expires_at` 派生；`status=active` 仅表示未撤销/未使用，不等于"仍可用"。

### 8.6 Stale job 回收（reaper）

后端单进程后台扫帚 `install_reaper.go`：启动时立即跑一次，之后每 `10m` 一次。把 `ctime` 早于 `now - 24h`（`StaleJobMaxAge`）且仍处于 `pending`/`running` 的 job 原子置为 `failed`（`finished_at=now`、`message="install job timed out without report"`），**并在同一事务内 `revoke` 指向这些 job 的仍 `active` 的 token** —— 防止被强杀的安装机事后再用 bearer token 复活已关闭的 job。终态 job 不会被改写；多副本下事务 WHERE 子句保证幂等（仅首个进程命中，其余 `RowsAffected=0` 静默退出）。

### 8.7 前端展示约定

- 生成命令的明文 token **只展示一次**；UI 须提示"仅显示一次、请立即复制保存"。
- 列表/详情只显示 `token_prefix`（如 `nit_xxxxxxx`）与状态，绝不回显明文 token。
- 复制命令后**清空剪贴板**（设定超时后清除），避免 token 长期驻留剪贴板。
- artifact 列表不展示后端真实 `package_path` 绝对路径（后端已只回文件名）。

### 8.8 `POST /api/node/reg` — 节点 agent 自注册

节点 agent 自注册端点。**公共端点**：不挂 RBAC、不需要 Bearer 鉴权，与 `POST /api/node/install/report` 同类（属 agent 基础设施，无 `zcloud` CLI 命令）。唯一访问门槛是请求体里的共享口令 `dummy_token`——必须等于 agent 端硬编码的固定常量值，不匹配即拒绝。

> 后端实现：`src/backend/internal/node/{handler,service}/reg.go`、路由 `src/backend/internal/node/route.go`（`rg.POST("/reg", h.RegNode)`）。

**请求体字段**（注意 `manger_addr` 是 agent 既有拼写，缺 `a`，不能改成 `manager_addr`）：

| 字段 | 类型 | 必填 | 说明 |
|---|---|:---:|---|
| `node_id` | string | 否 | 首次安装为空；非空表示 agent 已持有节点 ID（命中既有节点则复用，不新建） |
| `manger_addr` | string | **是** | 管理地址 `host:port`，agent 用 `ip route get` 自动探测后上报。端口缺失时回落默认端口 `33020` |
| `node_type` | string | 否 | proto 枚举名字符串，如 `"NODE_1_WAF"`。**仅支持 WAF 防护节点**（空 / `"NODE_1_WAF"` / `"waf"` / `"1"` 均映射为 WAF，其它值返回非零 `code`） |
| `extend_config` | string | 否 | url-escape 后的扩展配置，自注册暂不消费 |
| `dummy_token` | string | **是** | 共享口令，必须等于 agent 硬编码的固定常量值，否则拒绝 |
| `plugin` | string | 否 | 插件列表，如 `"waf,detect,agent,ebpf"` |
| `only_acl` | bool | 否 | 仅 ACL 模式标记，自注册节点不走该路径 |
| `ip` | string | 否 | `only_acl` 关联参数，标准注册为空 |
| `acl_tags` | string | 否 | `only_acl` 关联参数 |
| `ip_groups` | string | 否 | `only_acl` 关联参数 |

```bash
curl -sS -X POST https://<cloud>/api/node/reg \
  -H 'Content-Type: application/json' \
  -d '{
        "node_id":"",
        "manger_addr":"192.168.14.171:33020",
        "node_type":"NODE_1_WAF",
        "dummy_token":"<agent 硬编码共享口令>",
        "plugin":"waf,detect,agent,ebpf"
      }'
```

**响应**：标准信封 `{code, message, data}`，`data` 为 `RegNodeResponse`：

```json
{"code":0,"message":"success","data":{
  "node_id":"3f2c…",
  "listen_addr":":33020",
  "tls":false,
  "cert":"",
  "key":"",
  "settings":{},
  "plugin":{}
}}
```

| 响应字段 | 类型 | 说明 |
|---|---|---|
| `node_id` | string | 节点 ID。幂等命中既有节点时返回既有 ID；首次注册返回新建 ID |
| `listen_addr` | string | 监听地址，形如 `":33020"`，取自管理地址端口（缺失回落默认 `33020`） |
| `tls` | bool | 新平台（NSQ）固定 `false`（不再用 etcd 下发每节点证书） |
| `cert` | string | 新平台返回空串 |
| `key` | string | 新平台返回空串 |
| `settings` | object | 下发配置。新平台配置走 NSQ 发布订阅，固定返回空映射 `{}` |
| `plugin` | object | 插件配置。新平台固定返回空映射 `{}` |

**语义要点**：

- **幂等（按管理地址 upsert）**：同一 `manger_addr` 重复注册返回**相同** `node_id`，绝不新建重复节点；新老 agent 共存时**保留既有节点**，不覆盖其字段。
- **首次注册**：创建一条节点记录（`machine_room` 默认值为 `default`，运维可在节点列表里改归到真实机房），并以管理 IP 派生落一条 `ip_addr` 记录——于是节点**自动出现在节点列表**，**管理地址与业务 IP 都已自动填好**。落 `ip_addr` 为尽力而为：IP 已被占用等非致命错误不影响注册成功。
- **失败处理**：始终返回 HTTP `200`，业务错误通过响应体**非零 `code`** 表达。`dummy_token` 不匹配、`manger_addr` 缺失或非法、`node_type` 非 WAF 类型均返回非零 `code` 并附带错误消息。

---

## §A Analytics 通用查询参数

适用于所有单图 GET 接口（`GET /api/analytics/<page>/<chart>`）。未传参数时后端使用默认值；传入无权限的 `target_user_id` 或跨 OEM 资源时返回 403。

| 参数 | 类型 | 必填 | 取值/示例 | 说明 |
|------|------|:---:|----------|------|
| `window` | string | 否 | `last_1h` / `last_24h` / `last_7d` | 时间窗口别名；为空时后端默认 `last_24h` |
| `stime` | int64 | 否 | `1746748800000` | 自定义起始时间 Unix 毫秒（与 `etime` 配对，比 `window` 优先级高） |
| `etime` | int64 | 否 | `1746835200000` | 自定义结束时间 Unix 毫秒 |
| `site_id` | string | 否 | `site-001` | 站点过滤 |
| `domain_id` | string | 否 | `d_8a3b1c` | 域名过滤 |
| `target_user_id` | string | 否 | `u-tenant-001` | 客户级切换被查看用户；后端统一做越权校验 |
| `compare` | bool | 否 | `false` | 是否启用上一周期对比（仅部分 chart 支持） |
| `top` | int | 否 | `10` | TopN，默认 10，最大 100 |
| `order` | string | 否 | `bytes_desc` | 排序方式，具体含义由图表定义（如 `top-url` 支持 `request_count_desc`/`bytes_desc`/`cache_desc`） |
| `page` | int | 否 | `1` | 列表类图表分页 |
| `size` | int | 否 | `20` | 列表类分页每页条数，最大 100 |

**单图响应骨架**：

```json
{
  "code": 0,
  "message": "ok",
  "data": {
    "chart_key": "access/status",
    "render_hint": "categorical_distribution_over_time",
    "schema": {
      "dimensions": [
        { "name": "status_class", "type": "enum", "values": ["2xx", "3xx", "4xx", "5xx"] },
        { "name": "time", "type": "timestamp", "unit": "ms" }
      ],
      "measures": [
        { "name": "count", "type": "integer", "unit": "requests" }
      ]
    },
    "rows": [],
    "meta": {
      "cache": "miss",
      "source": "postgres",
      "latency_ms": 12,
      "window": {
        "stime": 1777526400000,
        "etime": 1777530000000,
        "granularity": "5m",
        "bucket_table": "tfs_flow_domains"
      }
    }
  }
}
```

> `window.granularity` 是**响应字段**，描述实际命中的聚合粒度（5m / 1h / 1d），**不是用户输入参数**。客户传 `window=last_24h`，后端按窗口大小自动选表。

---

## §B 可视化建议总表

### B.1 Chart 统一契约 — `render_hint` 速查

所有 chart-key 由前端按 `render_hint` 自动分发到 6 个 chart 组件之一。这是契约真值（[`docs/specs/chart-contract.md`](https://gitea.com/y2026/cloud/src/branch/main/docs/specs/chart-contract.md) §2），不可自创新词。

| `render_hint` | schema 形状 | 推荐前端组件 | 典型场景 |
|---|---|---|---|
| `kpi` | 0~1 dim + 1+ measure | `KpiGroupCard` | 多指标数字卡组（如 overview/kpi 的 6 测度） |
| `categorical_distribution` | 1 categorical dim + 1 measure | `PieCard`（≤8 类）/ `BarCard`（>8 类） | 一维占比（如 overview/event-type） |
| `categorical_distribution_over_time` | 1 categorical + 1 timestamp + 1 measure | `StackedBarCard` / `LineCard` 多 series | 多分类按时间堆叠（如 access/status） |
| `time_series_single` | 1 timestamp + 1 measure | `LineCard` | 单测度时序（如 access/request-hm 无 compare） |
| `time_series_multi` | 1 timestamp + ≥2 measures，**或** 1 categorical + 1 timestamp + 1 measure | `LineCard` 多线 | 多测度时序（如 access/flow-hm 5 测度）；compare 走子形态 B |
| `topn` | 1 string dim + 1 measure | `BarCard` 横向 / `RankingCard` | 已排序 TOP-N（如 protect/waf/top-ip） |
| `geo` | 1 geo dim + 1 measure | `GeoHeatmapCard` | 地理分布（如 protect/waf/geo） |
| `table` | 任意 | `TableCard` | 不适合可视化的兜底 |

> **新增 hint 词汇必须双方评审通过**（cloud + Aegeon），不可单边扩词汇表。

### B.2 数据形态速查

下表把"输出数据形态 → 推荐图表"的映射汇总在一起，对接前端时可作为速查表。实际响应仍以 Chart 统一契约的 `schema` + `rows` 为准。

| 数据形态 | 典型字段示例 | 推荐图表 | 不推荐 |
|---|---|---|---|
| 单值（标量） | `{count: 12345}` | KPI 数字卡 | 折线 / 饼图 |
| 多 KPI（4-6 个标量） | `{domain_count, requests, blocked, block_rate, qps}` | 多 KPI 卡阵 / 雷达图 | 单饼图 |
| 时序单系列 | `[{ctime, value}, ...]` | 折线图 / 面积图 | 饼图 |
| 时序多系列 | `[{ctime, requests, attacks}, ...]` | 多线折线 / 堆叠面积 | 饼图 |
| 时序对比（compare） | `{current:[...], previous:[...]}` | 双线对比折线（实虚线） | 单折线 |
| 维度分布（少类 ≤ 8） | `[{key, count}, ...]` | 饼图 / 环形图 | 表格 |
| 维度分布（多类 > 8） | `[{key, count}, ...]` | 横向 bar / 柱图 | 饼图（碎片化） |
| 地理分布 | `[{region, count}, ...]` | 中国/世界地图热力 | 表格 |
| 排行 TOP | `[{key, count, ...}, ...]` | 横向 bar / 表格（含明细列） | 折线 / 饼图 |
| 二维矩阵 | `[[v11, v12], [v21, v22]]` | 热力图 | 折线 |
| 散点 | `[{x, y, size, ...}, ...]` | 散点图 / 气泡图 | 饼图 |
| 列表分页 | `{list, total, page, size}` | 表格（分页） | 任何图表 |
| 时间线明细 | `{items: [{ctime, ...}]}` | 时间线（vertical timeline） | 饼图 |
| 占位 | `{available: false, ...}` | 不渲染图表，渲染 `n-empty` 占位卡片 | 假数据 |

**配色建议**：
- 防护类（攻击/拦截）：红色系（#ff4d4f / #ff7875）
- 流量类（请求/带宽）：蓝色系（#2563eb / #60a5fa）
- 缓存/AI 类（增益）：绿色系（#10b981 / #34d399）
- 中性指标：灰/紫色系（#6b7280 / #8b5cf6）

---

## §C 相关文档

- [认证说明](/docs/auth) — Bearer 会话、API Key 与 401 重试
- [示例代码](/docs/examples) — 三语言调用示例（curl / Python / Go）
- [权限矩阵](/docs/permissions) — 接口对应的权限 key 与 OEM 隔离规则
- [CLI 工具](/docs/cli) — 等价的 CLI 操作
- [错误码](/docs/errors) — 完整业务错误码表
- [快速上手](/docs/quickstart) — 5 分钟跑通第一次调用
- [/api/openapi.json](/api/openapi.json) — OpenAPI v3 完整 schema（机器读取）
- [/llms.txt](/llms.txt) / [/llms-full.txt](/llms-full.txt) — AI agent 速读入口

---

## 完整 API 索引

REST 全集导出为 OpenAPI v3：

```
GET /api/openapi.json
```

可直接导入 Postman / Insomnia / Swagger UI。所有路由含完整 schema、参数、响应示例与所需权限 key。

---

*Cloud WAF · REST API Documentation · 完整索引以 `/api/openapi.json` 为准*
