# 数据专家 API · 优缺点分析与优化方案

> **对象**：现网契约 `2026-06-10-aigent-AI对接说明(2).md` 的**数据专家部分**（§1.1–1.9，9 个端点）。
> **范围**：只针对**数据专家**场景（取数 / 趋势 / 环比 / 客单价 / 维度 / 导出），不含营销、会员操作。
> **结论**：现网取数侧"能跑通但偏碎"——5 条取数端点 + 3 套指标命名 + 3 种时间格式并存，失败语义弱，部分能力被错标"无数据源"。本文先评优缺点，再给一套**收敛为 2 个端点**的优化方案 + 迁移对照。
> 真实库口径以 ai-Recs 全表分析为准（`report_daily_statistics` 263 店 / `main_order` 全量）。

---

## 一、现状概览（数据专家 9 端点）

| 端点 | 作用 | 指标口径 | 时间载体 |
|---|---|---|---|
| §1.1 `GET metrics/capabilities` | 可答维度声明 | — | — |
| §1.2 `POST metrics/resolveDateRange` | 口语时间→区间 | — | 出 `yyyy-MM-dd HH:mm:ss` |
| §1.3 `POST metrics/query` | 查 **3 指标** | `netIncome/successAmount/...`（驼峰，5 字段 VO） | 入 `yyyy-MM-dd HH:mm:ss` |
| §1.4 `POST metrics/compare` | 净收入环比 | `netIncome` | 同上 |
| §1.5 `POST metrics/trend` | 近 7 天趋势 | `netIncome` | `yyyy-MM-dd` |
| §1.6 `POST metrics/report` | 经营报表（含客单价） | `revenue/orders/avgTicket`（第三套名） | `yyyy-MM-dd` |
| §1.7 `POST metrics/agent-bi` | 通用 BI（11 指标，取数/趋势/环比合一） | `income/success_amount/...`（蛇形，11 指标） | 13 位 ms |
| §1.8/1.9 `POST report/export/*` | 导出 preview→commit | — | — |

---

## 二、优点（值得保留的设计）

| # | 优点 | 说明 |
|---|---|---|
| ✅1 | **agent-bi 已合并取数/趋势/环比** | §1.7 用 `metrics+aggregation+timeGranularity+compare` 一个端点搞定三态，方向正确，是优化方案的基础 |
| ✅2 | **指标白名单显式 + 白名单外丢弃不报错** | §1.7 11 指标白名单，传非法指标自动忽略而非报错，容错好 |
| ✅3 | **`zero` 标志区分"零数据"与"错误"** | §1.3 query VO 有 `zero:bool`，避免把无数据当异常 |
| ✅4 | **比率口径明确** | §1.7 `refund_rate/success_rate` 注明"整段累加再除、分母 0 返 null"，口径清晰 |
| ✅5 | **环比无基线有判定** | `baselineExists=false` 表达上期无数据，避免 AI 编造环比（§1.4/§1.6） |
| ✅6 | **危险写 preview→commit + 一次性 token** | §0.4 导出走两步确认，token 10 分钟一次性，防重设计到位 |
| ✅7 | **键名大小写/连字符不敏感** | §1.7 `order-count=order_count=ORDER_COUNT`，调用方容错 |

> 优化方案**保留并强化**这些：以 agent-bi 为基础收口、沿用白名单 + zero + baselineExists + preview→commit。

---

## 三、缺点 / 问题（按影响排序）

### P0 · 结构性

| # | 问题 | 证据（原文） |
|---|---|---|
| **D1** | **取数端点 5 条冗余**：`query/compare/trend` 已被 `agent-bi` 完全覆盖 | §1.3–1.5 vs §1.7 |
| **D2** | **同一指标三套名**："净收入" = `netIncome`(§1.3) / `income`(§1.7) / `revenue`(§1.6)；驼峰蛇形混用 | §1.3/1.6/1.7 |
| **D3** | **失败恒 HTTP 200**：`R.code=500`、错误码埋 `data.errorCode`，连接器无法 `raise_for_status`、监控把失败当成功 | §0.2/0.3 |
| **D4** | **错误码无 `retriable` + 同码多义**：`AIGENT_RANGE_TOO_LARGE` 既表导出>90 天(§0.3)又表 agent-bi>400 天(§1.7)；`PRODUCT_SERVICE_DEGRADED` 走"其他"兜底 | §0.3/§1.7 |

### P1 · 口径一致 + 能力解锁

| # | 问题 | 证据 |
|---|---|---|
| **D5** | **客单价自相矛盾**：§1.7 说"客单价 minipos 没数据源"，§1.6 又给 `avgTicket=revenue/orders`（现算，orders=0→0） | §1.6/1.7 |
| **D6** | **分渠道/客流/会员错标"无数据源"**：§1.6 `customers/breakdown` 恒 null、§1.7 说分渠道没源，但真实日报表这些列**都在** | §1.6/1.7 vs 真实表 |
| **D7** | **时间载体三种混用**：`yyyy-MM-dd HH:mm:ss`(§0.5/1.3) / `yyyy-MM-dd`(§1.5/1.6) / 13 位 ms(§1.7) | §0.5/1.x |
| **D8** | **覆盖度不声明**：全篇不提 263/5001 店、日报直取 vs 订单聚合、"无数据源"vs"真 0" | 通篇 |

### P2 · 规范细节

| # | 问题 | 证据 |
|---|---|---|
| **D9** | 无版本号（`R<T>` 无 `apiVersion`、URL 无 `/v1`） | §0.2 |
| **D10** | `timeGranularity=YEAR` 真实性存疑（真实网关/mock 行为不一致） | §1.7 |
| **D11** | `dimensions` 是语义漂移的兼容字段（填 `trend`/`time_range` 等价 `timeGranularity=DAY`） | §1.7 |
| **D12** | `resolveDateRange` 单独一跳：取数前先调一次解析时间，多一次往返 | §1.2 |
| **D13** | 比率护栏与能力冲突：§1.7 提供比率，又注"对话侧护栏禁止算比率，自行斟酌" | §1.7 |

---

## 四、优化方案：数据专家收敛为 2 个端点

> 核心思路：以 §1.7 `agent-bi` 为基础，**①把 `query/compare/trend`(旧 3 指标链) 和 `report`(客单价) 也收进来；②加 `dimension` 解锁分渠道；③统一时间/命名/错误/覆盖度**。导出（§1.8/1.9）单列，下文「4.5」给精简版。

约定：前缀 `/pos/ai/v1`；时间一律 13 位 epoch ms（UTC+4，含头含尾）；金额 AED。

### 4.1 端点总览

| 端点 | 用途 | 替代现网 |
|---|---|---|
| `POST /metrics:query` | **统一取数**：汇总/趋势/同比/客单价/维度，靠参数区分 | query+compare+trend+report+agent-bi |
| `GET /metrics/capabilities` | 能力发现：逐店支持的指标/维度 | capabilities（增强） |
| （导出见 4.5，沿用 preview→commit） | | export/preview·commit |

### 4.2 `POST /metrics:query` 请求参数

| 参数 | 类型 | 必填 | 说明 | 取值 |
|---|---|---|---|---|
| `store_id` | string | 是 | 门店 ID | — |
| `metrics` | string[] | 是 | 查哪些指标，可多选（白名单外忽略不报错） | §4.4（含 `avg_ticket`） |
| `aggregation` | string | 否 | 区间内怎么合并，默认 `SUM` | `SUM`/`AVG` 日均/`MAX` 单日最高/`MIN` |
| `time_granularity` | string | 否 | 出数颗粒，不填=整段 1 行 | `DAY`/`MONTH`（移除 YEAR，见 D10） |
| `date_from` | number | 是 | 区间起（13 位 ms） | — |
| `date_to` | number | 是 | 区间止（13 位 ms） | — |
| `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 |

### 4.3 `POST /metrics:query` 响应

**响应外壳**（失败回真实 HTTP 状态，纠正 D3）
```jsonc
{ "api_version":"v1", "code":0, "message":"ok",
  "data": { ... },
  "meta": { "currency":"AED",
            "data_basis":"DAILY_REPORT",          // 三态：DAILY_REPORT / ORDER_AGGREGATED / NONE（纠正 D8）
            "metric_basis": {"income":"DAILY_REPORT","fail_count":"ORDER_AGGREGATED"} },  // 逐指标底座
  "error": null }                                  // 失败：{error_code,category,retriable,details}
```

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

| 字段 | 类型 | 说明 |
|---|---|---|
| `series` | array | 数据行；`time_granularity` 不填 1 行，填了多行 |
| `series[].period` | string | 周期标签 `2026-06-10`（汇总行可空） |
| `series[].values.<m>.value` | number | 本期值 |
| `series[].values.<m>.prev` | number\|null | 对比期值（不带 compare 为 null） |
| `series[].values.<m>.pct` | number\|null | 变化率（不带 compare 为 null） |
| `series[].values.<m>.baseline_exists` | bool | 上期是否有基线（false=上期无数据，prev/pct 为 null，沿用 ✅5） |

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

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

### 4.4 指标白名单（12 项，统一蛇形，纠正 D2/D5）

| key | 含义 | 类型 | 真实来源列 | data_basis |
|---|---|---|---|---|
| `income` | 净收入 | 金额 | `turnover − refund`（待确认 `actual_income_no_tips` 后切换） | DAILY_REPORT |
| `success_amount` | 成功交易额（营业额/流水） | 金额 | `turnover` 直取 | DAILY_REPORT |
| `order_count` | 订单总数 | 笔 | `order_quantity` 直取 | DAILY_REPORT |
| `success_count` | 成功笔数 | 笔 | `order_quantity` 近似直取 | DAILY_REPORT |
| `refund_amount` | 退款额 | 金额 | `refund` 直取 | DAILY_REPORT |
| `refund_count` | 退款笔数 | 笔 | `main_order.order_status=refunded` 聚合 | ORDER_AGGREGATED |
| `fail_amount` | 失败额 | 金额 | `blankout/cancel` 聚合 | ORDER_AGGREGATED |
| `fail_count` | 失败笔数 | 笔 | `blankout+cancel` 聚合 | ORDER_AGGREGATED |
| `settle_amount` | 总结算额 | 金额 | `turnover − refund`（待 minipos 对齐） | DAILY_REPORT |
| `refund_rate` | 退款率 | 比率 | `refund / turnover` | DAILY_REPORT |
| `success_rate` | 成功率 | 比率 | 成功/(成功+失败) | ORDER_AGGREGATED |
| `avg_ticket` | 客单价 | 金额 | **`average_order_price` 直取**（解 D5，仅 263 日报店，**不现算**） | DAILY_REPORT |

> 笔数级（refund_count/fail/success_rate）日报无、走订单聚合（与 `真实库表校验确认.md` 一致）。比率口径沿用 ✅4「整段累加再除、分母 0 返 null」。

### 4.5 导出（沿用 preview→commit，规范化）

`POST /report:export-preview`（>90 天→`AIGENT_EXPORT_RANGE_TOO_LARGE`）→ 出确认卡 → `POST /report:export-commit`（凭 `previewToken`，幂等）。沿用 §0.4 两步式 + 一次性 token；仅把错误码与时间格式对齐到下方统一规范。

### 4.6 统一错误模型（纠正 D3/D4）

| 项 | 优化 |
|---|---|
| HTTP 状态 | 失败回真实状态（400/409/410/422/429/503），不再恒 200 |
| 结构 | `error{error_code, category, retriable, details}` |
| category | `validation/conflict/degraded/not_found/rate_limited/unsupported` |
| 拆同码多义 | `AIGENT_QUERY_RANGE_TOO_LARGE`(400 天) / `AIGENT_EXPORT_RANGE_TOO_LARGE`(90 天)，`details{max_days,requested_days}` |
| 可重试 | `*_DEGRADED`/限流 `retriable=true`+`retry_after_ms`；校验类 `retriable=false` |

### 4.7 capabilities 增强（纠正 D8）

```jsonc
{ "contract_version":"1.0.0", "currency":"AED", "tz_offset":"+04:00",
  "metrics":[ {"key":"avg_ticket","label":"客单价","available":true,"scope":["query"]}, ... ],
  "dimensions":[ {"dimension":"CHANNEL","available":true},
                 {"dimension":"ORDER_SOURCE","measure":"COUNT_ONLY"} ],   // 来源只有笔数
  "limits":{ "query_max_days":400, "export_max_days":90 } }               // 拆全两个上限
```

---

## 五、Agent 如何调用与匹配参数（消费侧，不进 API 契约）

一句话拆成几件事填到 `/metrics:query`：时间词→`date_from/to`；「日/每天/趋势」→`time_granularity`；指标词→`metrics[]`；「日均/最高」→`aggregation`；「比上周/同比」→`compare`；「分渠道/各支付」→`dimension`。

**模糊词→指标**：营业额/销售额/流水→`success_amount`；净收入/利润→`income`；营收/收入（模糊）→`success_amount`（商家口语优先流水）；订单/单量→`order_count`；客单价→`avg_ticket`。

**例**「上周的日营业额」→ `date_from/to`=上周一~日 + `time_granularity=DAY` + `metrics=[success_amount]` + 其余默认 → 7 天逐日营业额。

> 时间解析建议挪到 Agent 侧（省去 §1.2 `resolveDateRange` 一跳，D12）；若 Agent 不自算，可保留该端点作回退。

---

## 六、数据团队如何实现

在 `/metrics:query` 实现里**按参数分支**取真实表（§4.4 已列来源列）：

1. **三链收一端**：现网 agent-bi 已合并取数/趋势/环比；再把 **客单价**（源 `average_order_price`）和**维度拆分**（源 `dining_room/pay_by_*/member_*` 列）接进同一端点，即可让 `query/compare/trend/report` 一并退役。**这些源 ai-Recs 已确认在 `report_daily_statistics` 里存在**（解 D6）。
2. **覆盖度三态**：日报 263 店走 `report_daily_statistics`（`data_basis=DAILY_REPORT`）；其余店 `main_order/daily_record` 按日聚合（`ORDER_AGGREGATED`）；都没有→`NONE`，**不返 0**（解 D8）。
3. **性能**：日报表本就是 per 店/日预聚合，直取即可，避免逐日拉下游（缓解 §1.7 自陈的性能问题）。
4. **capabilities 如实**：哪些指标/维度这家店真有数据就标 `available`，让 Agent 据此门控。

---

## 七、迁移对照（现网 → 优化后）

| 现网 | 优化后 | 处置 |
|---|---|---|
| `metrics/query`(3 指标) | `metrics:query` `metrics=[...]` | Deprecated，字段 1:1 映射 |
| `metrics/compare` | `metrics:query` `compare=wow/mom/yoy` | Deprecated |
| `metrics/trend` | `metrics:query` `time_granularity=DAY` | Deprecated |
| `metrics/report` 的 `avgTicket` | `metrics:query` `metrics=[avg_ticket]` | 并入，源改直取 |
| `metrics/report` 的 `customers/breakdown`(null) | `metrics:query` `dimension=...` | 解锁（真实列存在） |
| `metrics/agent-bi` | `metrics:query` | 重命名 + 加 dimension/avg_ticket |
| `netIncome/revenue/income` | 统一 `income` | 蛇形单一字典 |
| `yyyy-MM-dd HH:mm:ss` / ms 混用 | 统一 13 位 ms | — |
| 失败 200 + data.errorCode | 真实 HTTP + `error{}` | — |
| `AIGENT_RANGE_TOO_LARGE`(两义) | 拆 query/export 两码 | — |

---

## 八、待数据团队确认（实现前先核实）

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

---

## 附：相关文档
- 现网契约：`2026-06-10-aigent-AI对接说明(2).md`（§1.1–1.9 数据专家）
- 真实库校验：`数据报表API-真实库表校验确认.md`（11 指标逐列、avg_ticket 源确认）
- 简版对外：`数据分析API-简版（Agent调用+数据团队实现）.md`
