# 营销专家 API · 优缺点分析与优化方案

> **对象**：现网契约 `2026-06-10-aigent-AI对接说明(2).md` 的**营销专家部分**（§2 会员 8 端点 + §3 活动 9 端点 = 17 端点）。
> **范围**：只针对**营销专家**场景（活动设计/生命周期/选品 + 会员筛选/标签/增删/导入），不含数据查询。
> **结论**：营销专家与数据专家有同样的共性病根（失败恒 200、错误码无 retriable、无版本、金额裸值），但**多一层写安全风险**——生命周期 pause/resume/terminate 缺状态冲突码 + 幂等 + 乐观并发。本文先评优缺点，再给一套**资源化 + 状态机 + 统一规范**的优化方案 + 迁移对照。

---

## 一、现状概览（营销专家 17 端点）

### 活动域（§3，9 端点）

| 端点 | 作用 | 危险写 |
|---|---|---|
| §3.0 `GET campaign/activity-types` | 允许创建的 4 类活动 | — |
| §3.1 `POST campaign/list` | 活动列表（消歧用） | — |
| §3.2 `GET campaign/detail` | 活动详情（含复盘 + availableActions） | — |
| §3.3 `POST campaign/preview` | 活动预演（建草稿 + 校验） | preview |
| §3.4 `POST campaign/commit` | 发布/存草稿（凭 previewToken） | commit |
| §3.5 `POST campaign/pause` · `resume` | 暂停 / 恢复 | 直接写 |
| §3.6 `POST campaign/terminate` | 终止（不可逆，confirmed=true） | 直接写 |
| §3.7 `POST campaign/product-picker` | 选品（秒杀用） | — |

### 会员域（§2，8 端点）

| 端点 | 作用 | 危险写 |
|---|---|---|
| §2.1 `POST member/filter` | 4 维筛选 | — |
| §2.2 `GET member/tag/list` | 标签库（系统标签不可增删） | — |
| §2.3/2.4 `member/tag/op/preview·commit` | 打/移标签 | preview→commit |
| §2.5 `POST member/create` | 新增会员 | 直接写 |
| §2.6/2.7 `member/delete/preview·commit` | 删除会员（confirmed=true，**真删未实现**） | preview→commit |
| §2.8 `POST member/import` | 批量导入（逐行部分成功） | 直接写 |

### 4 类活动（注意枚举与字面相反）

`FLASH_SALE`=秒杀 · `DIRECT_AMOUNT_OFF`=订单直减(Save AED 1-999) · `ORDER_DISCOUNT`=全单折扣(Pay X% 10-99) · `FIRST_ORDER_DISCOUNT`=首单优惠。

---

## 二、优点（值得保留）

| # | 优点 | 出处 |
|---|---|---|
| ✅1 | **preview→commit 统一危险写范式** + previewToken 一次性/10min TTL | §0.4 |
| ✅2 | **activity-types 防非法类型**：传清单外 activityType 被 preview 提前拒 `AIGENT_INVALID_ACTIVITY_TYPE` | §3.0 |
| ✅3 | **availableActions 按状态下发**：AI 据此判断能否 pause/resume/terminate | §3.2 |
| ✅4 | **下游 MKT_* 错误码透传**：`MKT_DISCOUNT_VALUE_INVALID` 等具体原因直接转述 | §3.3 |
| ✅5 | **terminate 二次确认**：`confirmed=true` + `AIGENT_CONFIRM_REQUIRED` | §3.6 |
| ✅6 | **选品冲突检测**：picker 返 `conflict`/`conflictCampaignId/Name` 防活动撞车 | §3.7 |
| ✅7 | **复盘统计完整**：detail 含 usedCount/totalPayAmount/refundOrderCount 等 | §3.2 |

> 优化方案保留并强化这些：沿用 preview→commit + activity-types 校验 + 冲突检测 + confirmed 二次确认。

---

## 三、缺点 / 问题

### P0 · 写安全（最该先改）

| # | 问题 | 出处 |
|---|---|---|
| **M1** | **生命周期写端点无状态冲突码 + 无幂等 + 无乐观并发**：`CampaignActionRequest` 只有 `id`。状态前置不满足（pause 非 ACTIVE）只「下游码透传」无结构化 `AIGENT_STATE_CONFLICT`；`availableActions` 是 detail 快照存在 **TOCTOU 竞态**；terminate `confirmed=true` 可无限重放，**重复终止是否二次归还库存未定义**——风险最高的操作防护最弱 | §3.5/3.6/3.2 |
| **M2** | **失败恒 HTTP 200 + 错误码无 retriable**：MKT_* 透传但无 `category/retriable`，可重试降级与永久参数错同被当终态 | §0.2/0.3 |
| **M3** | **commit 不校验 token 门店绑定**：门店信息来自 previewToken 快照、不传 braId，文档自陈「aigent 不校验越权，调用方负责」——门店 A 的 token 被 B 拿去 commit 会落到 A | §0.1/§7.4 |

### P1 · 契约语义

| # | 问题 | 出处 |
|---|---|---|
| **M4** | **`discountValue` 单字段多态**：语义由 `discountType`(PERCENTAGE/FIXED_AMOUNT) 重载；又另有 `directAmount` 表达减免——两套字段、易写反 | §3.1/3.2 |
| **M5** | **活动类型枚举与字面相反**：文档明确警告「`DIRECT_AMOUNT_OFF`=直减、`ORDER_DISCOUNT`=折扣，枚举与字面相反」，字面都含 AMOUNT/DISCOUNT，极易写反 | §3 开头 |
| **M6** | **金额全裸值无币种**：`directAmount/discountValue/flashPrice/minOrderAmount` 全裸 AED，多区域静默错算 | §3.0–3.3 |
| **M7** | **list 只下发 display* 预格式化串**：机读原始 `dateStart/dateEnd/timeStart/timeEnd` 只在 detail 有；`displayTimeSlot=null` 把「全天」压进缺省 | §3.1 vs §3.2 |
| **M8** | **availableActions 承诺超出可调端点**：列 EDIT/PUBLISH/WITHDRAW/DELETE/SHARE 但仅 pause/resume/terminate 有端点；补端点时语义从「展示占位」悄变「可调契约」（无版本号的破坏）；PENDING 的 WITHDRAW、DRAFT 的 EDIT/PUBLISH 是状态机死角 | §3.2 |
| **M9** | **product-picker 复用 CampaignListQuery**：`keyword` 语义漂移（list 匹配 activityName、picker 匹配 spuName）、忽略 activityType/status；price/originalPrice 与 preview 的 flashPrice 命名不对称 | §3.7/3.3 |
| **M10** | **list 查询能力弱**：status 单值、无时间区间过滤、返 `endedReason` 却不能按它过滤、无排序 → 分页不稳定 | §3.1 |
| **M11** | **会员域**：`affectedCount` 是「目标数、实际生效以执行为准」（预估≠实际）；delete/commit 真删未实现仍走 confirmed 流；系统标签不可增删但混在 tagCodes | §2.3/2.7/2.2 |

### P2 · 一致性

| # | 问题 | 出处 |
|---|---|---|
| **M12** | 秒杀进度字段冗余：flashSkuCount/flashTotalStock/flashRemainingStock/flashSoldCount/flashStockProgress 五个，progress≈sold/total 可能不自洽 | §3.2 |
| **M13** | 时间字段混用：`dateStart` 是 `yyyy-MM-dd`、`createTime` 是 datetime、display* 是 `dd/MM/yyyy` 串 | §3.1/3.2 |
| **M14** | 枚举（status/endedReason/discountType/activityType）未声明开放性，未约定未知值容错 | §3.x |
| **M15** | 缺批量（detail/pause 单 id）+ 缺字段投影（list 固定返十余复盘字段，消歧只需 id/name/status） | §3.1/3.2 |

---

## 四、优化方案

> 与数据专家优化同一套规范（统一信封/错误/Money/版本），再叠加营销专属的**资源化 + 状态机 + 幂等**。约定：前缀 `/pos/ai/v1`；时间 13 位 epoch ms；金额 `Money` 对象。

### 4.1 端点（资源化）

| 优化后 | 替代现网 | 标注 |
|---|---|---|
| `GET /meta/campaign-activity-types` | activity-types（元数据上移） | 规范化 |
| `GET /campaigns`（list） | campaign/list | 补排序/过滤/投影 |
| `GET /campaigns/{id}` | campaign/detail | 补机读原始字段 |
| `POST /campaigns:preview` → `:commit` | campaign/preview·commit | 沿用 + token 门店校验 |
| `POST /campaigns/{id}:pause` · `:resume` · `:terminate` · **`:withdraw`** | campaign/pause·resume·terminate（+ 补 withdraw） | **状态机 + 幂等 + CAS** |
| `GET /products`（选品） | campaign/product-picker | 拆 DTO 重定位 |
| 会员域：`POST /member:filter`、`/member/tags:preview·commit`、`/member`、`/member/{id}:delete-preview·commit`、`/member:import` | member/* | 规范化 |

### 4.2 生命周期写（解 M1，核心）

**请求**：`POST /campaigns/{id}:pause` body `{ expected_status? }`（CAS 乐观并发）。
**响应**：`{ campaign_id, status }`。
**关键约束**：

| 约束 | 行为 |
|---|---|
| 状态前置不满足（pause 非 ACTIVE） | `AIGENT_STATE_CONFLICT`(409, `details{current_status, allowed_actions}`) |
| 目标态已达成（已 PAUSED 再 pause） | **幂等成功**，返当前状态（不报错） |
| terminate 重复调 | **库存归还恰好一次**（幂等键去重） |
| `expected_status` 与实际不符 | CAS 失败 → `AIGENT_STATE_CONFLICT`，杜绝 TOCTOU |

### 4.3 优惠字段（解 M4/M5/M6）

| 现网 | 优化后 |
|---|---|
| `discountValue`（多态，由 discountType 重载）+ `directAmount` | 拆 `percent_to_pay`(10-99) / `amount_off`(Money) / `flash_price`(Money)，非适用置 null |
| 类型 `DIRECT_AMOUNT_OFF`/`ORDER_DISCOUNT`（与字面相反） | 重命名 `FIXED_AMOUNT_OFF` / `WHOLE_ORDER_PERCENT`，带 `legacy_code` 过渡 |
| 裸 AED 金额 | 全换 `Money{amount,currency,minor_unit}` |

### 4.4 list / detail（解 M7/M10/M15）

- list 行补机读原始：`date_start/end`、`time_start/end`(epoch) + `is_long_term`/`is_all_day` 布尔；`display_*` 标 **presentation-only 勿解析**。
- 加 `statuses[]`（多值）、`created_from/to`、`ended_reason` 过滤 + `sort:[{field,order}]`（默 createTime DESC）+ `view(SLIM|FULL)` 投影。
- list/detail 收敛为「detail = list ∪ 额外明细」超集，后续纯加法。

### 4.5 availableActions（解 M8）

升级为机读对象：`available_actions: [{ action, endpoint, available, status }]` 或拆 `enabled_actions`/`all_actions`；补 PENDING/DRAFT 的 `:withdraw` 消除状态机死角。

### 4.6 选品（解 M9）

拆独立 `ProductPickerQuery{ spu_keyword, page, page_size }`，重定位为 `GET /products`；价格命名对齐（picker `price/original_price` ↔ preview `flash_price`，给映射示例）。

### 4.7 统一错误模型 + 版本（解 M2/M3/M14）

- 失败回真实 HTTP 状态 + `error{error_code, category, retriable, details}`；补全 MKT_* 的 category/retriable。
- commit 校验「鉴权门店==token 门店」→ `AIGENT_TOKEN_SCOPE_MISMATCH`(403)。
- `/pos/ai/v1` + `api_version`；每枚举标 OPEN/CLOSED，OPEN 者要求消费者遇未知值走默认分支。

---

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

| 现网 | 优化后 | 处置 |
|---|---|---|
| `campaign/pause`(body id) | `:pause`(路径 id + expected_status) | 加状态冲突码 + 幂等 |
| `campaign/terminate`(confirmed) | `:terminate`(幂等，库存归还一次) | 解重放 |
| `discountValue`+`directAmount` | `percent_to_pay`/`amount_off`/`flash_price` | 拆多态 |
| `DIRECT_AMOUNT_OFF`/`ORDER_DISCOUNT` | `FIXED_AMOUNT_OFF`/`WHOLE_ORDER_PERCENT`(+legacy_code) | 重命名 |
| 裸金额 | `Money` 对象 | — |
| list 仅 display* | + date/time 原始字段 + is_long_term/is_all_day | 补机读 |
| `availableActions: string[]` | `[{action,endpoint,available,status}]` + withdraw | 升级 |
| `product-picker`(复用 CampaignListQuery) | `GET /products`(ProductPickerQuery) | 拆 DTO |
| 失败 200 + data.errorCode | 真实 HTTP + `error{category,retriable}` | — |

---

## 六、待确认 / 受限项

| 项 | 现状 | 处置 |
|---|---|---|
| `member/delete/commit` 真删 | 未实现（受理即返回） | 声明 `AIGENT_DOWNSTREAM_UNSUPPORTED` 非重试且**不消耗 token** |
| 触达发送 / 设计专家 | 本期未实现 | `suspended[]` 声明，不设计端点 |
| `affectedCount` 预估 vs 实际 | 目标数≠实际生效 | 字段标 `is_estimate:true`，commit 后返实际 |

---

## 附：相关文档
- 现网契约：`2026-06-10-aigent-AI对接说明(2).md`（§2 会员、§3 活动）
- 既有建议：`营销活动API-优化建议（给数据团队）.md`（P0×3/P1×14/P2×12 多视角合并版）
- 对照：`数据专家API-优缺点与优化方案（针对2026-06-10契约）.md`（同一套规范）
