Spree 6.0 Store API 重大变更解析:购物车 404 语义、Webhook 事件切换与新增 Cart/Order 字段
2026/9/14 6:20:54 网站建设 项目流程

Spree 6.0 Store API 重大变更解析:购物车 404 语义、Webhook 事件切换与新增 Cart/Order 字段

【免费下载链接】spreeOpen Source eCommerce Platform for B2B, Marketplace, and Enterprise. REST API, TypeScript SDK, and production-ready Next.js storefront. Self-host it. Own your stack. No vendor lock-in. Zero platform fees.项目地址: https://gitcode.com/GitHub_Trending/sp/spree

Spree 6.0 对 Store API 做了一次面向前端集成(storefront integration)的破坏性升级,对应@spree/sdk的 major 版本变更,声明于 changeset 文件。本文以该 changeset 为主体,逐条讲清三个破坏性变更——已完成购物车从 cart 端点消失(404 语义)、DeliveryZone.members改为按需展开、下单 Webhook 事件切换为order.placed——并深入源码印证其行为,同时覆盖coupon_codecart_idcart.created/updated/deleted事件三类新增能力,帮助你在 6.0 上正确改造现有 Storefront 集成。

一、变更背景:6.0 的购物车/订单分离

理解这些变更的前提是 Spree 6.0 的一个核心数据模型调整:购物车(Spree::Cart)与订单(Spree::Order)被拆分为两种独立实体。从 Cart 模型 的注释可以看到:checkout 会把购物车“复制”为一个不可变的 Order,购物车本身保留并打上completed_at时间戳;Cart 没有 status 列,completed_at是它唯一的生命周期标记。

正是这个模型分离,决定了 Store API 的行为契约:cart 端点只服务“未完成”的购物车,完成后的结果一律走 orders 端点。下面逐条展开。

二、破坏性变更 1:已完成的购物车不再由 cart 端点返回,取之即 404

2.1 变更内容

changeset 原文:

Completed carts are no longer served by the cart endpoints — fetching one returns 404, the signal to drop stale cart state. The checkout outcome stays reachable throughorders.get(cartId)authorized by the cart token (the order inherits it).

含义分两层:

  1. 对已完成的购物车执行GET /api/v3/store/carts/:id等 cart 端点操作会返回 404。这个 404 不是错误,而是一个信号:前端收到后应当丢弃本地缓存的旧购物车状态(stale cart state)。
  2. 结账结果仍然可达——通过orders.get(cartId)获取,鉴权凭据就是购物车 token(因为 Order 继承了 Cart 的 token)。

2.2 源码印证:404 从何而来

Store API 的 cart 控制器统一通过 CartResolvable 关注点 解析购物车。其核心逻辑是:

# spree/api/app/controllers/concerns/spree/api/v3/cart_resolvable.rb def resolve_cart(include_completed:) cart_id = params[:cart_id] || params[:id] scope = current_store.carts scope = scope.incomplete unless include_completed scope.find_by_prefix_id!(cart_id) end

默认情况下查找范围被限定为incomplete,已完成的购物车在 scope 中查不到,find_by_prefix_id!抛出RecordNotFound,对外即 404。源码注释也点明了设计意图:“Completed carts are not carts anymore — they 404 unless the caller opts in(幂等完成、支付会话 confirm 竞争场景)”。

有例外的是 CartsController#complete:它以find_cart!(include_completed: true)查找,专门承接“网关竞态重试”——即 webhook 侧已经完成了购物车、客户端又拿原 ID 重发 complete 请求的场景。此时会走find_completed_result!在 orders/order_groups 范围中找回已完成的结果并按 cart token 授权后返回 Order(或拆分结账时的 OrderGroup),保证 complete 操作的幂等性。

2.3 Token 继承:为什么 orders.get(cartId) 能用 cart token 鉴权

在 Carts::Complete 工作流 的create_draft_order!方法中,新订单被显式创建为token: cart.token:

# spree/core/app/workflows/spree/carts/complete.rb order = cart.store.orders.new( cart: cart, status: 'draft', email: cart.email, ... token: cart.token, ... )

订单直接继承购物车 token,因此前端无需任何新的凭据交换,继续用x-spree-token请求头(cart token 即request.headers['x-spree-token'],见 cart_token 方法)就能读取结账结果。

2.4 前端集成应如何改

  • 在 cart 读取请求的 404 处理分支里,区分“从未存在/无权访问”与“已完成”:若此前持有过该 cart 且已发起过 complete,则将 404 解读为“购物车已转化为订单”,随后调用 orders 端点获取结果并清空本地 cart 状态。
  • orders.get(cartId)(携带原 cart token)作为 complete 之后获取结果的唯一路径,不再对 cart ID 发起二次读取。

三、破坏性变更 2:DeliveryZone.members 仅按需嵌入(类型变为可选)

3.1 变更内容

changeset 原文:

DeliveryZone.membersis only embedded when requested withexpand=members(the type is now optional).

即:查询交付区域时,成员列表(members,通常是区域包含的国家/地区)默认不再内联返回,必须在请求中显式加上expand=members才会嵌入;同时 SDK 中对应的 TypeScript 类型从必填变为可选。

3.2 源码印证

服务端,交付区域序列化器 中:

many :members, resource: proc { Spree.api.delivery_zone_member_serializer }, if: proc { expand?('members') }

members关联以条件式声明——只有当expand参数包含members时才序列化,这与 Spree API 一贯的按需展开(expandable associations)机制一致。

SDK 侧的类型同步更新,见 DeliveryZone 类型定义:

interface DeliveryZone { // ... members?: Array<DeliveryZoneMember>; }

members?可选标记;Zod schema 侧(zod/generated/DeliveryZone.ts)同样声明为z.array(DeliveryZoneMemberSchema).optional()

3.3 前端集成应如何改

  • 如果你的 Storefront 在结账页展示收货地区/国家选项,需要在拉取 DeliveryZone 的请求参数中追加expand=members,否则成员字段将缺席。
  • 处理响应时按“members可能为undefined”编写代码,不要再假设其必然存在——这正是类型变可选的直接后果。

四、破坏性变更 3:下单事件切换为 order.placed,order.completed 双发过渡至 6.1

4.1 变更内容

changeset 原文:

The placement webhook event isorder.placed;order.completedis still dual-emitted through 6.0 withdeprecated_alias_ofmetadata and drops in 6.1.

即:表示“订单已下单”的 Webhook 事件以order.placed为准;为了兼容仍监听旧事件的订阅方,6.0 会同时双发order.completed,并在事件元数据中携带deprecated_alias_of: 'order.placed'标记;到 6.1 别名事件将被彻底移除。

4.2 源码印证:双发在哪里发生

订单完成工作流 的publish_order_placed方法是这一行为的唯一落点:

# order.completed is a one-release alias for 5.x webhook consumers; # wildcard subscribers dedupe on the metadata marker. def publish_order_placed payload = order.event_payload.merge(notify_customer: order.notify_customer) order.publish_event('order.placed', payload) order.publish_event('order.completed', payload, { deprecated_alias_of: 'order.placed' }) end

两个事件 payload 完全一致,差别只在第三个参数:别名事件附带deprecated_alias_of元数据。源码注释还透露了一个细节——通配符(wildcard)订阅者会依据该元数据标记去重,避免同一次下单被重复消费。

补充一点上下文:该事件由Spree::Orders::Complete工作流统一发布,而 cart 侧的 Carts::Complete 在 FINALIZE 阶段正是委托给这个订单侧工作流,所以无论是 checkout 完成还是 B2B/后台的草稿订单完成,发布的事件语义都一致。

4.3 前端集成应如何改

  • 将监听order.completed的 webhook 处理器迁移到order.placed
  • 过渡期(6.0)你可以双发共存:若按事件名精确订阅则只会收到其一;若使用通配符订阅,应依据deprecated_alias_of元数据自行去重。
  • 在 6.1 升级前移除对order.completed的一切依赖——届时该别名不再发布。

五、新增能力:coupon_code、cart_id 与 cart.* 生命周期事件

5.1 Cart 与 Order 上的 coupon_code(含“待生效”语义)

changeset 原文:

Additions:coupon_codeon Cart and Order (with pending-code semantics — a real but not-yet-eligible code is kept and applies once the cart qualifies)

coupon_code现在同时出现在 Cart 和 Order 两个资源上,且具备待生效(pending-code)语义:一个真实存在、但当前条件尚不满足(如未达满减门槛)的优惠码会被保留在购物车上,等购物车满足条件后自动应用,而不是直接报错丢弃。

源码中有两处可以直接印证:

  1. Cart 模型 对字段做了归一化——存储前统一 strip + 小写,使优惠码查找保持大小写不敏感:
# Codes are stored stripped + lowercased so lookups stay case-insensitive normalizes :coupon_code, with: ->(code) { code.to_s.strip.downcase.presence }
  1. 订单侧同样带有该字段,SDK 生成的类型 Order.ts 与 Cart.ts 中均声明了coupon_code: string | null(zod schema 为z.string().nullable())。
  2. 完成结账时,购物车上的优惠码随草稿订单创建被复制过去,见 create_draft_order! 中的coupon_code: cart.read_attribute(:coupon_code);完成工作流末尾还会通过use_coupon_codes/mark_coupon_codes_used将对应Spree::CouponCode记录标记为已使用。

5.2 Order 上的 cart_id:关联购物车活动与转化

changeset 提及在 Order 上新增cart_id,用于把购物车侧的活动数据(浏览、加购、弃购等埋点)与最终转化匹配起来——分析工具可以用它回答“这个订单来自哪个购物车”。

该字段在 Carts::Complete 工作流 创建订单时以cart: cart建立外键,SDK 生成的 Order 类型 中声明为cart_id: string | null。注意它是可空的:并非所有订单都源自一条 Store API 购物车(如后台直接创建的订单),类型设计与之对应。

5.3 cart.created / cart.updated / cart.deleted 事件:弃购工具的信号源

changeset 原文:

cart.created/cart.updated/cart.deletedwebhook events for abandonment tooling.

这三个购物车生命周期事件专门为弃购(abandonment)工具提供信号。源码中,Cart 模型 通过publishes_lifecycle_events声明,注释解释得很清楚:

# cart.created / cart.updated / cart.deleted — the abandonment-tooling # signal (parity with 5.x, where incomplete orders emitted order.*). # Payload serializes through the V3 cart serializer by convention. publishes_lifecycle_events

即:5.x 时代由“未完成订单”发出的order.*生命周期信号,在 6.0 的模型分离后改由 Cart 发出,事件名也同步换成了cart.*。事件的 payload 约定上经由 V3 cart 序列化器序列化,因此与 Store API 返回的 cart 资源结构一致,可直接复用现有解析逻辑。

对做邮件/短信弃购召回的集成方,这三类事件覆盖了一个购物车从创建、每次变更(加购、改地址、用券)到删除(放弃或主动删除)的完整可观测面:其中cart.deleted尤其重要,它对应 CartsController#destroy 走的Spree.cart_destroy_service,是“用户明确放弃”的可靠信号,可与“长时间无cart.updated”的被动弃购判断互补。

六、升级检查清单

结合 changeset 与上述源码证据,升级到 Spree 6.0 Store API 时建议按以下清单核查:

#检查项处理方式
1cart 端点对已完成购物车的 404404 分支中识别“已转化订单”场景,改用orders.get(cartId)+ cart token 获取结果并清理本地 cart 状态
2complete 的幂等重试保留重发逻辑:服务端对网关竞态重试会经由find_completed_result!返回已完成的 Order/OrderGroup,见 cart_resolvable 与 complete 动作
3DeliveryZone 成员列表请求追加expand=members;类型处理按members可选编写
4下单 Webhook迁移至order.placed;通配符订阅按deprecated_alias_of去重;6.1 前移除对order.completed的依赖
5优惠码展示与状态利用 Cart/Order 的coupon_code字段;理解待生效语义——不满足条件的真实优惠码会被保留并自动应用
6转化归因用 Order 的cart_id关联购物车活动与成交(可空字段,需判空)
7弃购工具订阅cart.created/cart.updated/cart.deleted,payload 结构与 Store API cart 资源一致

七、小结

Spree 6.0 的 Store API 线变更,本质上是购物车与订单模型分离后 API 契约的一次收紧:cart 端点回归“只服务未完成购物车”的单一职责,404 成为状态转换的显式信号;资源嵌入(expand=members)与事件命名(order.placedcart.*)都朝更精确的方向演化。三个破坏性变更都有明确的过渡策略——cart token 继承保证结果可达、双发别名提供一整代(至 6.1)的迁移窗口——而coupon_codecart_id与 cart 生命周期事件则补齐了优惠归因与弃购工具两块此前缺失的集成能力。对照本文的检查清单逐项改造,即可平滑完成 6.0 升级。

【免费下载链接】spreeOpen Source eCommerce Platform for B2B, Marketplace, and Enterprise. REST API, TypeScript SDK, and production-ready Next.js storefront. Self-host it. Own your stack. No vendor lock-in. Zero platform fees.项目地址: https://gitcode.com/GitHub_Trending/sp/spree

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询