把购物接口拆成授权、幂等、责任三层,各自留一份服务端记录

一、开放的不是查询接口,是一条会写账的链路

据公开报道,有零售平台推出了面向 AI 助手的网关,允许第三方助手连接其购物系统,完成商品搜索与交易。来源没有给接口字段、鉴权细节与结算方式,本篇不猜这些;值得单独拎出来讨论的是它带来的一个结构变化:下单这个动作的发起方,从坐在页面前的用户,变成了一个会重试、会忘事、会自己组织语言的程序。

一旦这样看,事情就从”加一层 API”变成了另一件事:这是一个有副作用的分布式事务,智能体只是发起方。前端页面时代,这三件事被界面顺手解决了——按钮点下去变灰防止重复提交,价格变化时弹一个确认框,订单状态挂在页面上由用户自己刷新。界面消失之后,这三件事没有任何一件是自动继承的,全部要在服务端重新长出来,而且长得要能被一个没有记忆的调用方正确使用。

我们的分法是把最敏感的三件事切开:授权、幂等、责任。切不开的典型症状是”用同一个令牌既表达用户同意、又表达这次请求是新的”,出事时两边都说不清。下面按这三层各给字段和判据。

二、授权要分两条轨:当场确认与预授权额度

同样是”用户同意了”,这两种的工程含义完全不同,混用是纠纷的主要来源。

当场确认:每一次下单前,用户在一个明确的界面上对一份明确的内容点了确认。它的服务端表达应当是一个一次性的确认凭据,绑定到具体的商品集合与金额快照,用完即失效。失效条件要写死:金额或商品集合变了就作废;超过时限就作废;同一凭据第二次使用直接拒绝。撤销入口是”这次确认”级别的,用户撤回一条不影响别的。

预授权额度:用户事先给出一个范围,范围内的动作不再逐次问。它本质是一条有边界的委托,服务端要存的不是凭据而是额度账:授权上限、已消耗、剩余、可用于哪类动作、有效期。它的失效条件是额度耗尽或到期,撤销入口必须是”整条授权”级别的一键关停,而且要立即生效在正在排队的请求上,而不是等下一个会话开始。

字段 当场确认 预授权额度
授权类型标识 逐次型 额度型
绑定的内容 金额与商品快照的摘要 范围、上限、适用动作类别
一次性凭据 必须有,用一次即废 无,改为消耗记录
失效条件 内容变化、超时、重复使用 额度耗尽、到期、被撤销
撤销入口 单次确认粒度 整条授权粒度,立即生效
服务端拒绝码 待重新确认 超出额度

判据很朴素:**任何一条下单记录,事后都要能回答”它是被哪一次确认覆盖的”**。答不出,就是授权设计有洞。

三、幂等键里要装用户确认的序号

会话 ID 不能当幂等键。一个会话里用户可以合法地买同样的东西两次,按会话去重的结果是第二次被”合并”掉,用户以为买成功了。反过来,助手因为超时重发同一个请求,如果键里没有任何去重信息,就会出现两笔真实订单。两种方向的错都来自同一件事:**键没有表达”这是同一个意图,还是另一个意图”**。

可用的构造是分层标识:调用方标识、会话标识、用户显式确认的序号、内容摘要。前两层定位来源,第三层区分意图——每次用户对一份内容点确认,都由服务端发一个递增的确认序号,这个序号不是客户端自己生成的;最后一层用于校验”同一个序号对应的内容确实没变”。

重复提交的合并语义也要事先定义,而不是靠实现顺手写:

  1. 键完全相同、内容摘要相同:不新建订单,返回第一次的结果,并带上一个”本次为合并响应”的标记,让调用方知道自己没有产生新订单。
  2. 键相同、内容摘要不同:拒绝,返回待确认,绝不能用旧内容成交。
  3. 键不同但业务上疑似重复(同一 SKU、同一地址、短时间窗内):不自动合并,转成一条人工可见的提示,交给用户或客服判断。

第三条要单独强调:自动合并是危险动作,因为它猜的是用户意图。猜错了会漏买,漏买比多买更难被发现。

四、会话中途价格或库存变了,返回语义不能留给模型判断

对话式购物最容易被忽视的一段是时间差。用户从提出需求到确认下单,中间可能隔了几轮对话,价格和可售状态在服务端是随时会动的。如果接口在成交时才返回一个不同的价格,让调用方自己决定”差一点就算了”,等于把成交判据交给了一个语言模型——它会倾向于把事情做完。

规则应当是:服务端把变更当成一种显式结果返回,而不是悄悄按新条件成交,也不是简单报错。

  • 返回结构里要区分”价格变了””库存不够了””这个地址不配送了”这三类,各自是独立的错误码,不能都塞进一个”下单失败”。
  • 任何影响成交条件的变更,一律作废当前确认序号并要求一次新的用户确认,不设”差不多可以过”的容忍区间。金额与数量差多少算差,不由模型判断,写死在服务端配置里。
  • 库存不足时返回语义要能表达”部分可成交还是整体不可”,并把可选方案作为结构化数据给出,而不是让助手用一句话转述。转述会丢失”这是替代方案而不是原方案”这个关键区分。
  • 响应里带上服务端成交判定的时间戳与所依据的快照标识,纠纷时才能还原当时到底是哪个价。

五、异步状态要回写给一个会忘记上下文的调用方

下单只是链路的起点,退款、取消、部分发货这些都在之后发生,而调用方(那个助手)很可能已经不持有这段会话的上下文了。把状态回写做成”只推一次通知”,等于假设模型永远不会丢消息。

可用的结构是推送加拉取双轨:状态变化时推送一个事件,事件里只带标识、状态、版本号和变化时间,不带大段自然语言;同时提供一个按订单号拉取当前完整状态的接口,任何时刻调用方都能重新取回。两轨要满足:同一状态的重复推送不产生副作用(推送本身也要幂等);状态带单调版本,旧版本到达时能被识别丢弃;终态要定义清楚哪些状态之后不再变化,避免”已取消”和”退款中”在两边各自被解释。

另外把责任分层说白:发货、退款、售后的状态机主权在服务端,调用方拿到的只是可读视图。如果允许调用方通过接口推进一个业务状态(比如”助手替用户点确认收货”),那它和点击页面的按钮是同一个动作,必须回到第二节的当场确认轨上去,不能因为它来自接口就免掉确认。

六、对账按调用方维度记账,纠纷时能还原模型看到了什么

限流这一段最容易出事。智能体重试的方式和人不一样:人在页面卡住会等一下再看,模型会因为”没拿到结果”而立刻换一种说法再问一遍,同一个意图在短时间被重放多次是常态。服务端要按调用方维度设配额、返回明确的”稍后重试”语义并附带重试窗口,同时对超限重试本身做惩罚性限流——否则一次上游故障会演变成对下单链路的自压。退避策略要写在接口约定里让调用方遵守,而不是指望它自觉。

对账必须按调用方维度记账,一条账至少能回答四个问题:谁发起的(哪个调用方、哪个用户授权)、哪个会话、哪一次确认、下单时模型看到的快照是什么。最后一项最容易被省掉,但它是纠纷时唯一有用的东西——用户会说”我要买的是另一个规格”,能不能还原助手当时收到的商品表述,决定了这句话有没有办法核对。

这就是三件事切开之后各自要交付的东西:

层 服务端必须有的字段 验收判据
授权 授权类型、确认序号或额度账、失效条件、撤销记录 任一笔订单能指到一次具体确认或一条额度消耗记录;撤销后新请求立即被拒
幂等 分层幂等键、内容摘要、合并标记、去重窗口 重放同一请求不产生第二笔订单;改内容后同键请求被拒而不是成交
责任 调用方标识、会话标识、快照标识与时间戳、状态版本、对账记录 客服能在不联系调用方的情况下还原一单的全部判据;状态推两次不产生副作用

给准备接入这类接口的同行一句话:先把这张表填完再谈联调。填不满的那一格,就是出纠纷时你唯一说不清的地方。洹拓内部评审时把这件事当作硬性门槛:三列里有一列拿不出字段,接口方案退回重写,而不是”先上线再补对账”。