营销专家 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(同一套规范)