# 营销专家 API · 优化定义（目标态 v2）

> 给 **Agent** 使用的营销专家 API 目标态定义。**v2 相比 v1 的关键改进**（来自实现回评）：
> ① 折扣收敛为带判别式的 `discount` 子对象；② **活动类型(场景) 与 优惠机制 解耦**（修 v1「类型既是场景又是机制」的自相矛盾）；③ `activity-types`+`member-tags` 合并为 `meta/capabilities`；④ 写操作补 `Idempotency-Key`；⑤ `available_actions` 补 `disabled_reason`。
> 设计目标：**Agent 能确定性把口语映射到端点+参数，prompt 尽量精简**（可枚举映射全下沉到参数/词库，不进 prompt）。
> 约定：前缀 `/pos/ai/v1`；时间 13 位 epoch ms（UTC+4）；金额 `Money` 对象。与现网契约 `2026-06-10-aigent-AI对接说明(2).md` 的差异见末尾「迁移对照」。

---

## 1. 端点总览（共享 3 + 活动 4 + 会员 3 = 10）

> **端点收敛原则**（本轮 review 核心）：端点选择 + 参数匹配由消费侧**确定性纯函数**完成 —— **不进 prompt、不靠 LLM 选**，所以端点数与 prompt 长度/准确性**解耦**（prompt 恒 0 端点）。因此「减端点」只合**真冗余**：所有危险写收成**一对共享 `marketing:preview`/`commit`**（`kind` 由消费侧确定性设置）；而 `list`/`detail`/`products` 等读端点形态各异，**不强合**（合了变多态汤、不增准确性）。
> 演进：v1 13 → v2 12（capabilities 合一）→ **v2.1 10**（危险写三对 preview/commit → 一对共享）。

### 共享（能力 + 危险写）

| 端点 | 用途 |
|---|---|
| `GET /marketing/capabilities` | 一次取全：活动场景 + 优惠机制 + 会员标签 + 限制（合法值不进 prompt） |
| `POST /marketing:preview` | **所有危险写预演**：`kind`(CAMPAIGN/MEMBER_TAG/MEMBER_DELETE) + payload → 配置卡 + preview_token |
| `POST /marketing:commit` | 确认执行：`preview_token` + `Idempotency-Key`；`kind` 由消费侧确定性设置（非 LLM 猜） |

### 活动域（读 + 生命周期）

| 端点 | 用途 |
|---|---|
| `GET /campaigns` | 活动列表（过滤/排序/投影，消歧用） |
| `GET /campaigns/{id}` | 活动详情（含复盘 + 可执行动作） |
| `POST /campaigns/{id}:action` | 生命周期合一（pause/resume/terminate/withdraw + Idempotency-Key + 状态机） |
| `GET /products` | 选品（秒杀建活动用） |

### 会员域（读 + 直接写）

| 端点 | 用途 |
|---|---|
| `POST /members:filter` | 4 维筛选 |
| `POST /members` | 新增会员（直接写，非两段确认） |
| `POST /members:import` | 批量导入（逐行部分成功，直接写） |

> **建活动 / 打移标签 / 删会员**的危险写统一走共享 `marketing:preview`→`commit`（`kind`=CAMPAIGN/MEMBER_TAG/MEMBER_DELETE）；**导出（数据侧）同样可挂 `kind`=EXPORT**，全平台危险写收成一对。生命周期 `:action` 是即时状态机、非两段确认，单列；新增/导入是直接写，不走 preview。

---

## 2. 公共约定

### 2.1 响应外壳
`{ api_version, code(0=成功), message, data, error }`；失败回真实 HTTP 状态 + `error{error_code,category,retriable,details}`。

### 2.2 金额 `Money`
`{ amount, currency:"AED", minor_unit:2 }`（裸值禁用）。

### 2.3 危险写 preview → commit + 幂等
`:preview` 出**配置卡 + preview_token**给用户确认 → `:commit` 执行。
- `preview_token` 一次性、10min；commit 校验门店绑定（跨店→`AIGENT_TOKEN_SCOPE_MISMATCH`）。
- **`commit`/`:action`/标签/删除等写操作带 `Idempotency-Key` 头**：网络重试/重复点不会重复发布/重复终止（同 key TTL 内返同一结果）。

### 2.4 活动类型(场景) × 优惠机制（核心：v2 解耦）

> v1 把"机制"焊进类型名（FIXED_AMOUNT_OFF/WHOLE_ORDER_PERCENT），又让首单能 percent 或 amount，自相矛盾。v2 拆成两个正交维度：

**`activity_type`（场景 / 对谁）**

| code | 含义 |
|---|---|
| `WHOLE_ORDER` | 全场活动（所有顾客） |
| `FIRST_ORDER` | 首单优惠（新客，长期有效） |
| `FLASH` | 秒杀（限时低价，需选品） |

**`discount`（机制 / 怎么减，带 `model` 判别式）**

| model | 字段 | 含义 |
|---|---|---|
| `PERCENT` | `percent_to_pay`(10-99) | 付 X%（打折） |
| `AMOUNT_OFF` | `amount_off`(Money) + `min_order`(Money?) | 满减 / 立减 |
| `FLASH` | `flash_items[]` | 秒杀逐 SKU（见 §3.2） |

> 场景×机制自然组合：全场×PERCENT=全场折扣、全场×AMOUNT_OFF=满减、首单×PERCENT=首单折、首单×AMOUNT_OFF=首单减、FLASH 场景配 FLASH 机制。**枚举从「为每个组合起名」塌成 3 场景 + 3 机制**。

### 2.5 `schedule`（时间，子对象）

`schedule: { period: {start, end} | "LONG_TERM", daily_slot: {start, end} | "ALL_DAY" }`（epoch ms）。
- `period=LONG_TERM`：长期有效（首单常用）。
- `daily_slot`：每日时段（午市/晚市）；`ALL_DAY`=全天。
- 替代 v1 的 `date_start/end` + `time_start/end` + `is_long_term` + `is_all_day` 四散字段。

---

## 3. 端点详解

### 3.1 `POST /marketing:preview`（kind=CAMPAIGN）—— 建活动预演

| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `store_id` | string | 是 | 门店 |
| `activity_type` | enum | 是 | 场景 §2.4（非法→`AIGENT_INVALID_ACTIVITY_TYPE`） |
| `activity_name` | string | 是 | 活动名 |
| `discount` | object | 是 | 优惠机制 §2.4（带 `model` 判别式） |
| `schedule` | object | 条件 | 时间 §2.5（FIRST_ORDER 可省=LONG_TERM） |

**输出**：`{ preview_token, draft_id, card }`（card 含算好的 `display_summary`）。
**校验失败**：透传 `MKT_*` 码（如折扣超范围/秒杀价过高），Agent 转述让用户改。

**示例**
```jsonc
// 全场午市8折
{ "store_id":"B1", "activity_type":"WHOLE_ORDER", "activity_name":"午市8折",
  "discount": { "model":"PERCENT", "percent_to_pay":80 },
  "schedule": { "period":{"start":..,"end":..}, "daily_slot":{"start":..,"end":..} } }
// 满100减20
{ "activity_type":"WHOLE_ORDER", "discount":{ "model":"AMOUNT_OFF",
  "amount_off":{"amount":2000,"currency":"AED","minor_unit":2}, "min_order":{"amount":10000,...} } }
// 首单8折（长期）
{ "activity_type":"FIRST_ORDER", "discount":{"model":"PERCENT","percent_to_pay":80},
  "schedule":{"period":"LONG_TERM"} }
```

### 3.2 秒杀机制（`discount.model=FLASH`）
`flash_items: [{ sku_id, spu_id, flash_price:Money, flash_stock, per_order_limit }]`，`flash_price` < 原价；sku 从 `/products` 选。

### 3.3 `POST /marketing:commit`（kind=CAMPAIGN）
`preview_token` + `action`(`PUBLISH`/`DRAFT`) + **`Idempotency-Key` 头** → 活动详情；token 失效→`AIGENT_PREVIEW_EXPIRED`。同一端点凭 `preview_token` 里的 `kind` 分派（CAMPAIGN/MEMBER_TAG/MEMBER_DELETE/EXPORT）。

### 3.4 `POST /campaigns/{id}:action` —— 生命周期合一

| 参数 | 类型 | 说明 |
|---|---|---|
| `action` | enum | `PAUSE`/`RESUME`/`TERMINATE`/`WITHDRAW` |
| `confirmed` | bool | TERMINATE 必须 true（归还库存恰好一次） |
| `expected_status` | enum | CAS 乐观并发，防 TOCTOU |
| `Idempotency-Key`（头） | — | 幂等去重 |

状态前置不满足→`AIGENT_STATE_CONFLICT`(409, `details{current_status, allowed_actions}`)；目标态已达成→幂等成功。

### 3.5 `GET /campaigns` —— 列表
入参：`store_id`、`statuses[]`、`activity_type`、`keyword`、`created_from/to`、`ended_reason`、`sort:[{field,order}]`(默 created DESC)、`view(SLIM|FULL)`、分页。
出参：行含机读原始 `schedule`(period/daily_slot) + `discount`(model+值)；`display_*` 标 presentation-only。

### 3.6 `GET /campaigns/{id}` —— 详情
列表行超集 + 复盘统计 + `available_actions:[{action, endpoint, available, status, disabled_reason?}]`（不可用时带机读原因如 `STATE_NOT_ACTIVE`）+ 秒杀 `flash_items[]`。

### 3.7 `GET /products` —— 选品
`store_id`、`spu_keyword`、分页 → `items[]{ spu_id, spu_name, sku_list:[{sku_id, spec, price:Money, original_price:Money, stock, conflict, conflict_campaign}] }`。

### 3.8 `GET /marketing/capabilities`
```jsonc
{ "contract_version":"2.0.0",
  "activity_types":[{"code":"WHOLE_ORDER","label":"全场活动"}, {"code":"FIRST_ORDER",...}, {"code":"FLASH",...}],
  "discount_models":[{"model":"PERCENT","range":[10,99]}, {"model":"AMOUNT_OFF"}, {"model":"FLASH"}],
  "member_tags":[{"code":"HIGH_VALUE","label":"高价值","is_system":true,"editable":false}, ...],
  "limits":{ "flash_max_sku":50, "campaign_name_max":30 } }
```
> Agent 建活动/打标签前一次取全合法场景/机制/标签 —— **合法值不进 prompt**。

### 3.9 会员域（参数从简）
- `members:filter`：`keyword`(昵称/手机)·`min_net_consumption`·`tags[]` → 分页会员。
- **打/移标签**（危险写）→ `marketing:preview`(kind=MEMBER_TAG, payload`{op:ASSIGN/UNASSIGN, member_ids[], tag_codes[]}`)→`commit`；`affected_count` 标 `is_estimate:true`，commit 后返实际。系统标签(`HIGH_VALUE`/`AT_RISK`)只筛不可改。
- `members`：新增（直接写）`phone`(+`country_code`/`nickname`/`tag_codes`)。
- **删除会员**（危险写）→ `marketing:preview`(kind=MEMBER_DELETE, payload`{member_id}`)→`commit`(`confirmed=true`)；下游未实现→`AIGENT_DOWNSTREAM_UNSUPPORTED`(非重试、不消耗 token)。
- `members:import`：直接写，`items[]` 逐行部分成功，返 `inserted/skipped/format_errors`。

---

## 4. Agent 如何匹配口语 → 端点 + 参数（确定性，少 prompt）

> 可枚举映射在词库/纯函数；**prompt 只留语气 + 安全 + 澄清**。

### 4.1 端点路由

| 用户口语 | 端点 | 出参 |
|---|---|---|
| 建/做活动/促销/折扣 | `campaigns:preview` | 见 4.2 |
| 暂停/恢复/终止/撤回 | `campaigns/{id}:action` | `action=PAUSE/RESUME/TERMINATE/WITHDRAW` |
| 看/查活动、进行中的 | `campaigns` | `statuses=[...]` |
| 选品/加商品 | `products` | `spu_keyword` |
| 筛会员/打标签/删会员/导入 | `members:*` / `member-tags:*` | — |

### 4.2 口语 → 场景 + 机制（确定性解析，**不靠 prompt 算**）

| 用户口语 | activity_type | discount |
|---|---|---|
| 全场9折 / 打9折 | `WHOLE_ORDER` | `{model:PERCENT, percent_to_pay:90}` |
| 8.5折 / 85折 | `WHOLE_ORDER` | `{model:PERCENT, percent_to_pay:85}` |
| 满100减20 | `WHOLE_ORDER` | `{model:AMOUNT_OFF, amount_off:20, min_order:100}` |
| 立减30 | `WHOLE_ORDER` | `{model:AMOUNT_OFF, amount_off:30}` |
| 新客首单8折 | `FIRST_ORDER` | `{model:PERCENT, percent_to_pay:80}` |
| 秒杀 / 限时特价 | `FLASH` | `{model:FLASH, flash_items:[...]}`（走选品） |

> 解析规则（消费侧纯函数）：`X折→percent`(9折→90,85折→85)；`满X减Y→amount_off+min_order`；`立减/直减Y→amount_off`；`付X%→percent`。命中即填，未命中回退 LLM。**场景词（全场/首单/秒杀）+ 机制由这套解析确定，prompt 不写换算规则。**

### 4.3 类型/标签靠 capabilities
模糊「做个活动」→ 出 `pick_type` 引导卡；合法场景/机制/标签从 `capabilities` 取。**清单不进 prompt。**

### 4.4 Prompt 只留 4 件事
语气专业简洁 · 危险写必走 preview→commit · 终止/删除要 confirmed · 模糊出 pick_type / 拿不准就澄清。

---

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

| 优化点 | 实现要点 |
|---|---|
| **discount 子对象** | 按 `discount.model` 分支校验/落库；映射现网 `discountValue`/`directAmount`（带 legacy_code），消除字段-类型耦合 |
| **场景×机制解耦** | `activity_type` 只判场景；机制走 `discount.model`；首单×percent / 首单×amount 自然支持 |
| **schedule 子对象** | 落库映射 `dateStart/end`+`timeStart/end`；`LONG_TERM`/`ALL_DAY` 用显式枚举 |
| **:action 合一** | 端点内分派 + `AIGENT_STATE_CONFLICT` + Idempotency-Key 去重（终止库存归还一次） |
| **capabilities 合一** | 一个端点返活动/机制/标签/限制，由白名单同源生成 |
| **token 门店校验** | commit 校验鉴权门店==token 门店 |

---

## 6. 迁移对照

### v1 → v2

| v1 | v2 | 原因 |
|---|---|---|
| `percent_to_pay`/`amount_off`/`flash_price` 顶层 | `discount{model, ...}` 子对象 | 消除字段-类型耦合，校验按 model 分支 |
| 类型 `FIXED_AMOUNT_OFF`/`WHOLE_ORDER_PERCENT` | `activity_type`(场景) × `discount.model`(机制) | 修「类型既场景又机制」自相矛盾 |
| `date/time/is_long_term/is_all_day` 四散 | `schedule{period, daily_slot}` | 收敛，显式枚举 |
| `activity-types` + `member-tags` 两端点 | `marketing/capabilities` 一端点 | 一次取全合法值 + 门控 |
| 活动/标签/删会员 三对 preview/commit（6 端点） | `marketing:preview` + `marketing:commit` 一对（kind 判别） | 全平台危险写收一对（导出同挂 kind=EXPORT）；消费侧本就一个 commit，无损准确性 |
| 仅 export 提幂等 | `:commit`/`:action`/标签/删除 加 `Idempotency-Key` | 写安全 |
| `available_actions` 无原因 | 补 `disabled_reason` | 可解释 |

### 现网 → v2（要点）

| 现网 | v2 |
|---|---|
| `campaign/pause`+`resume`+`terminate` | `campaigns/{id}:action` |
| `discountValue`(多态)+`directAmount` | `discount{model,...}` |
| `DIRECT_AMOUNT_OFF`/`ORDER_DISCOUNT` | `activity_type`×`discount.model`（legacy_code 过渡） |
| 失败 200 + data.errorCode | 真实 HTTP + `error{category,retriable}` |

---

## 附：相关文档
- 评审依据：`营销专家API-优缺点与优化方案（针对2026-06-10契约）.md`
- 现网契约：`2026-06-10-aigent-AI对接说明(2).md`
- 对照（数据侧）：`数据分析API-简版（Agent调用+数据团队实现）.md`
