# 数据分析 API · 简版设计（2 端点）

> 一套给 **Agent** 使用的数据分析 API：Agent 把用户口语转成端点 + 参数，数据团队负责实现端点、从真实库取数。
> 本文只讲 **API 本身 + 每个参数 + Agent 如何匹配参数 + 数据团队如何实现**。
> 约定：前缀 `/pos/ai/v1`；时间用 13 位毫秒时间戳（UTC+4）；金额单位 AED。
> 设计沿用现网契约 `2026-06-10-aigent-AI对接说明(2).md` §1.7 `agent-bi` 的「一个接口搞定 取数/趋势/环比」思路，再把客单价、维度拆分也收进同一取数端点。

---

## 1. 端点总览（2 个）

| 端点 | 用途 | 谁调 |
|---|---|---|
| `POST /metrics:query` | **统一取数**：汇总 / 趋势 / 同比 / 客单价 / 维度拆分，全靠参数区分 | Agent 主力 |
| `GET /metrics/capabilities` | **能力发现**：这家店支持哪些指标/维度 | Agent 取数前先问 |

> 为什么是 2 个不是 1 个：取数类（原 query/report/dimensions）都是"给我数据"，可由参数合并；而 capabilities 是"问这家店有什么能力"，响应结构不同、且要在取数**前**调用做门控，性质不同，单独保留。

> **端点收敛原则**（与营销侧一致）：端点选择 + 参数匹配由消费侧**确定性纯函数**完成 —— **不进 prompt、不靠 LLM 选**，所以端点数与 prompt 长度/准确性**解耦**（实测数据专家 prompt 出现端点名 0 次）。只合**真冗余**（query/compare/trend/report/agent-bi → 一个 `metrics:query`），读端点性质不同的不强合。
>
> **危险写（导出报表）走平台共享写端点**：`POST /marketing:preview`(kind=EXPORT)→`commit`（与营销活动/会员的危险写同一对，`kind` 由消费侧确定性设置）。轻量取数无需 preview→commit，故本表不含。

---

## 2. 公共约定

### 2.1 响应外壳（两个端点统一）
```jsonc
{
  "code": 0,            // 0=成功，非 0 看 message
  "message": "ok",
  "data": { ... },      // 各端点的返回体
  "meta": {
    "currency": "AED",
    "data_basis": "DAILY_REPORT"   // 数据来源：DAILY_REPORT(日报直取) / ORDER_AGGREGATED(订单聚合) / NONE(无数据)
  }
}
```
> `data_basis=NONE` 表示这家店这段时间无数据 —— Agent 应如实告知"暂无数据"，**不要把空当 0 汇报**。

### 2.2 时间
- 一律 13 位毫秒时间戳，UTC+4。
- 区间 `date_from`/`date_to` **含头含尾**（如查 6 月，from=6/1 00:00，to=6/30 23:59:59）。

### 2.3 12 个指标（`metrics[]` 的取值）

| key | 含义 | 类型 |
|---|---|---|
| `income` | 净收入（= 营业额 − 退款） | 金额 |
| `success_amount` | 成功交易额（**= 营业额/流水**） | 金额 |
| `order_count` | 订单总数 | 笔数 |
| `success_count` | 成功笔数 | 笔数 |
| `refund_amount` | 退款额 | 金额 |
| `refund_count` | 退款笔数 | 笔数 |
| `fail_amount` | 失败额 | 金额 |
| `fail_count` | 失败笔数 | 笔数 |
| `settle_amount` | 总结算额 | 金额 |
| `refund_rate` | 退款率 | 比率(0–1) |
| `success_rate` | 成功率 | 比率(0–1) |
| `avg_ticket` | 客单价 | 金额 |

---

## 3. 端点详解

### 3.1 `POST /metrics:query` —— 统一取数

> 营业额、订单量、退款、客单价、趋势、同比、分渠道占比……**全用这一个端点**，靠参数区分要哪种数据。

**请求参数**

| 参数 | 类型 | 必填 | 说明 | 取值 |
|---|---|---|---|---|
| `store_id` | string | 是 | 门店 ID | — |
| `metrics` | string[] | 是 | 要查哪些指标，可多选 | 见 §2.3（含 `avg_ticket`） |
| `aggregation` | string | 否 | 区间内怎么合并，默认 `SUM` | `SUM` 求和 / `AVG` 日均 / `MAX` 单日最高 / `MIN` 单日最低 |
| `time_granularity` | string | 否 | 出数颗粒，不填=整段汇总成 1 行 | `DAY` 按天 / `MONTH` 按月 |
| `date_from` | number | 是 | 区间起（毫秒） | — |
| `date_to` | number | 是 | 区间止（毫秒） | — |
| `compare` | string | 否 | 是否要同比，默认 `NONE` | `NONE` / `WOW` 周环比 / `MOM` 月环比 / `YOY` 年同比 |
| `dimension` | string | 否 | 是否按维度拆分，不填=不拆 | `CHANNEL` 渠道 / `PAYMENT` 支付 / `ORDER_SOURCE` 来源 / `MEMBER` 会员 |

**参数怎么组合 = 拿到哪种数据**（这就是合并的核心）

| 你要的 | metrics | time_granularity | compare | dimension |
|---|---|---|---|---|
| 上周营业额（一个数） | `["success_amount"]` | 不填 | `NONE` | 不填 |
| 近 7 天每天营业额（趋势） | `["success_amount"]` | `DAY` | `NONE` | 不填 |
| 本周营业额 + 比上周（同比） | `["success_amount"]` | 不填 | `WOW` | 不填 |
| 客单价 | `["avg_ticket"]` | 不填 | `NONE` | 不填 |
| 分渠道营业额占比 | `["success_amount"]` | 不填 | `NONE` | `CHANNEL` |

**响应 `data`**

**① 不带 `dimension`（普通取数/趋势）**

| 字段 | 类型 | 说明 |
|---|---|---|
| `series` | array | 数据行；`time_granularity` 不填时 1 行，填了多行 |
| `series[].period` | string | 周期标签，如 `2026-06-10`（汇总行可为空） |
| `series[].values` | object | 每个指标一个值 `{value, prev, pct}` |
| `series[].values.<m>.value` | number | 本期值 |
| `series[].values.<m>.prev` | number\|null | 对比期值（不带 compare 时 null） |
| `series[].values.<m>.pct` | number\|null | 变化率（不带 compare 时 null） |

**② 带 `dimension`（维度拆分）**

| 字段 | 类型 | 说明 |
|---|---|---|
| `breakdown` | array | 各维度切片 |
| `breakdown[].dimension_value` | string | 维度值，如 `DINING_ROOM`/`PAY_BY_CARD` |
| `breakdown[].value` | number | 该切片的指标值 |
| `breakdown[].share` | number | 占比 0–1 |

**示例 A · 「上周每天的营业额」**
```jsonc
// 请求
{ "store_id":"B177", "metrics":["success_amount"],
  "time_granularity":"DAY", "date_from":1717977600000, "date_to":1718582399000 }
// 响应 data
{ "series":[
  {"period":"2026-06-09","values":{"success_amount":{"value":2167,"prev":null,"pct":null}}},
  {"period":"2026-06-10","values":{"success_amount":{"value":0,"prev":null,"pct":null}}}
  /* …共 7 天… */
]}
```

**示例 B · 「分渠道看营业额」**
```jsonc
// 请求
{ "store_id":"B177", "metrics":["success_amount"], "dimension":"CHANNEL",
  "date_from":1717977600000, "date_to":1718582399000 }
// 响应 data
{ "breakdown":[
  {"dimension_value":"DINING_ROOM","value":1860,"share":0.86},
  {"dimension_value":"STORE_DELIVERY","value":307,"share":0.14}
]}
```

> **三点约束**：
> 1. `avg_ticket`（客单价）只有 263 家接入日报的门店有；其它店该指标 `available:false`，**不要用 income÷订单数现算**。
> 2. `dimension=ORDER_SOURCE`（来源）只有笔数没有金额，传金额类 `metric` 时按笔数口径返回。
> 3. `dimension` 和 `time_granularity` 一般不同时用（要么看趋势、要么看拆分）。

---

### 3.2 `GET /metrics/capabilities` —— 能力发现

> Agent 取数前先问：这家店支持哪些指标/维度，避免对没数据的店瞎查。

**请求参数**：`store_id`（query 串）

**响应 `data`**

| 字段 | 类型 | 说明 |
|---|---|---|
| `metrics` | array | 支持的指标 `{key, label, available}` |
| `dimensions` | array | 支持的维度 `{dimension, available}` |
| `limits` | object | `{query_max_days:400}` 区间上限 |
| `currency` | string | `AED` |

---

## 4. Agent 如何调用与匹配参数

Agent 把用户一句话拆成几件事，分别填到 `/metrics:query` 的参数里：

| 用户说的 | 匹配到的参数 | 规则 |
|---|---|---|
| 「上周」「6 月」「近 7 天」 | `date_from` / `date_to` | 把时间词解析成两个毫秒时间戳 |
| 「**日**营业额」「每天」「趋势」 | `time_granularity=DAY`；「按月」→ MONTH；不说 → 不填 | 要不要逐日/逐月 |
| 「营业额」「订单量」「退款率」「客单价」 | `metrics[]` | 见下面"模糊词"表 |
| 「日均」「最高」 | `aggregation=AVG/MAX` | 不说 → SUM |
| 「比上周」「同比」 | `compare=WOW/YOY` | 不说 → NONE |
| 「分渠道」「各支付方式」 | `dimension=CHANNEL/PAYMENT` | 出现维度词就填这个参数 |

**模糊词 → 指标（关键）**：「营业额」这种词不强行只给一个数 —— Agent 取一个**主指标**先回答，同时在数据卡列出相关指标。

| 用户词 | 主指标 |
|---|---|
| 营业额 / 销售额 / 成交额 / 流水 | `success_amount` |
| 净收入 / 利润 | `income` |
| 营收 / 收入 / 赚了多少（模糊） | `success_amount`（商家口语"收入"多指流水） |
| 订单 / 单量 | `order_count` |
| 客单价 | `avg_ticket` |

**完整例子**：用户问「上周的日营业额」
1. 「上周」→ `date_from/to` = 上周一~周日
2. 「日…」→ `time_granularity=DAY`
3. 「营业额」→ 主指标 `success_amount`
4. 无"日均/比较/分渠道" → `aggregation=SUM`、`compare=NONE`、`dimension` 不填
→ 调 `POST /metrics:query`，得到 7 天逐日营业额。

---

## 5. 数据团队如何实现

合并成一个取数端点后，数据团队在 **`/metrics:query` 的实现里按参数分支**取数，对应真实库表（以 ai-Recs 全表分析为准）：

| 参数情形 / 指标 | 真实来源 | 实现口径 |
|---|---|---|
| `success_amount` | `report_daily_statistics.turnover` | 直取（营业额=流水） |
| `income` | `turnover − refund` | 派生（首选；如确认有 `actual_income_no_tips` 列再切换） |
| `order_count` / `success_count` | `report_daily_statistics.order_quantity` | 直取（近似） |
| `refund_amount` / `refund_rate` | `refund` / `refund÷turnover` | 直取 / 派生 |
| `refund_count` / `fail_count` / `success_rate` | `main_order.order_status`（refunded/blankout/cancel/payed） | **订单表聚合**（日报无笔数级） |
| `avg_ticket` | `report_daily_statistics.average_order_price` | 直取（仅 263 日报店） |
| `time_granularity=DAY/MONTH` | 按日/月分组 | 多行序列；不填则整段聚合 1 行 |
| `compare` | 再拉一份上期同口径 | 每个指标补 `prev`/`pct` |
| `dimension=CHANNEL/PAYMENT/MEMBER` | `dining_room`/`togo`/`store_delivery`、`pay_by_*`、`member_*` 列 | 预聚合列直取，算占比 |
| `dimension=ORDER_SOURCE` | `order_source_by_near/h5/shop` | 只有笔数 |

**实现要点**：
1. **三端点收一端**：现网 `agent-bi`(§1.7) 已合并取数/趋势/环比；本方案再把 **客单价**（源 `average_order_price`）和**维度拆分**（源分渠道/支付列）接进同一端点，即可让 `/metrics:query` 一口气覆盖原 query/report/dimensions。这些源 ai-Recs 已确认在真实日报表里存在。
2. **覆盖度**：日报只覆盖 263/5001 店。日报店走 `report_daily_statistics`（`data_basis=DAILY_REPORT`）；其余店从 `main_order`/`daily_record` 按日聚合（`ORDER_AGGREGATED`）；都没有 → `NONE`，**不要返回 0**。
3. **capabilities 要如实**：哪些指标/维度这家店真有数据，就在 `capabilities` 里标 `available`，让 Agent 据此决定查不查。

---

## 6. 待数据团队确认（实现前先核实）

| 列（`report_daily_statistics`） | 用途 | 未确认前回退 |
|---|---|---|
| `actual_income_no_tips` / `tips` / `vat` | income 精确口径（是否扣小费/税） | income 用 `turnover − refund` |
| `customer_number` 是否有/填充率 | 客流量指标 | 暂不提供该指标 |
| `member_*` 各列填充率 | 会员维拆分 | 填充率低 → `available:false` |
