跳转到内容

购物车同步

你的站点与 SDK 会读写同一个 Shopify 购物车:站点负责加购主商品、修改数量与删除商品;SDK 负责写入费用行与保障行。为了让双方状态始终一致,SDK 提供了两条方向固定的通知通道。

本页适用于所有适配模式,也适用于仅开启延保险(PP)的店铺。

谁修改了购物车通知方向你需要做什么
站点:加购、改数量、删除、清空、切换购物车站点 → SDK写入成功后调用 await syncCart(cart.id)
SDK:cart 模式下拨动 SP 开关、pp.addToCart()、pp.addProtection() 等SDK → 站点SDK 触发 onCartChange(cart),你在回调中刷新界面

只需记住一条核心原则:站点写车后调用 syncCart(cart.id);在 onCartChange 回调中只刷新站点界面。 首次绑定、换车与同车刷新都是同一个方法,SDK 自行判断属于哪一种。

站点完成购物车写入后,调用 syncCart(cart.id) 并等待其完成。购物车页与抽屉共用的忙碌状态必须覆盖站点写入和 SDK 同步两段;同步结束前禁用下一次改数量、删除、清空与结账操作,避免两方写请求重叠:

await addMainProduct()
await window.xcotton!.syncCart(cart.id)

syncCart 会按受影响的模块执行必要同步,并在完成后返回独立 Cart:

  1. 重新确认购物车事实(传入完整 Cart 时校验并采用该快照)。
  2. 同车同步时,若存在上一次已确认快照,只有 SP 输入(非保障行、购物车属性或折扣码)变化才重新计算 SP 报价并同步费用行;首次绑定与换车按首次同步规则重新整理。cart 模式下购物车为空或金额超出可保范围时自动移除费用行;checkout 和 checkout+ 模式把费用行留到结账时处理。仅追加或移除 PP 保障行不属于 SP 输入,不会触发 SP 全量重报价。
  3. 行、购物车属性或折扣变化时刷新 PP 的购物车补购报价(getCartQuotes() 的返回值);getRelations() 直接读取当前购物车快照,不依赖这份投影。
  4. 同步完成后触发一次 onCartChange,并返回本次确认的独立 Cart 快照。返回值是本次同步的结果,不保证后续外部操作不会使其过期。

syncCart 接受三种输入:无参(当前已绑定的购物车)、cartId 字符串、以及站点已从服务端取得的完整 Cart。无参调用在尚未绑车时会拒绝,错误以 [XC] syncCart requires a cartId before the first synchronization 开头;完整 Cart 必须字段完整、属于同一辆车且币种一致,否则拒绝,拒绝的输入不会成为有效快照,已知旧快照同时失效。

同步会校验购物车币种:cart.cost.totalAmount.currencyCode 必须与 window.xcottonConfig.currency 一致,否则同步直接 reject。站点切换市场时,应先准备属于新市场的购物车,并在新页面加载前声明新的 country / currency。运行中修改配置不会改变当前 SDK 会话。

init 是页面生命周期内的一次性入口。页面启动时调用一次,注册 adapter 与 onCartChange,之后所有刷新、首次绑定和换车都走 syncCart。进行中、成功后或失败后再次调用 init 都会被明确拒绝,不会共享进行中的 Promise 冒充成功,也不能替换首次登记的配置。

  • 环境失败(配置或装配失败,包括 PP 应启用但初始化失败):init 拒绝为 SDK_INIT_FAILED,SDK 不保留半初始化能力,syncCart 与依赖购物车的操作都报未初始化。修正原因后重新加载页面。
  • 环境就绪、首次同步失败:init({ cartId }) 拒绝为 SDK_INIT_SYNC_FAILED,错误携带 cartId、失败阶段,以及在能确认时的写入状态;商品级能力(如 PP 报价)仍可用。用 syncCart(cartId) 从购物车真实状态恢复,不重放此前已执行的写入,也不要再次 init。
  • 环境就绪、首次同步被在途操作占用:init({ cartId }) 拒绝为 OPERATION_IN_PROGRESS(operation=init),生命周期停在 busy;之后的重复 init 为 SDK_ALREADY_INITIALIZED(phase=busy)。等当前操作结束后用 syncCart(cartId) 恢复,不要再次 init。
  • 首次同步失败后,重复调用 init 仍会拒绝(phase=sync_failed,被在途操作拒绝时为 phase=busy);恢复完成后也不会让 init 重新可用。

syncCart 是购物车身份的唯一入口(支持无参、cartId 与完整 Cart),以下场景都调用它:

  • 首次同步:页面首次获取到购物车 ID 时调用。你也可以在初始化时直接传入 init({ cartId }),成功路径相同;失败时 init 以 SDK_INIT_SYNC_FAILED 拒绝(首次同步被在途操作占用时为 OPERATION_IN_PROGRESS),而 syncCart(cartId) 只拒绝本次同步。
  • 切换购物车:当顾客切换到另一个购物车时(例如登录后合并购物车),传入新的购物车 ID 调用。SDK 会按 首次同步购物车的规则 重新整理费用行。
  • 同车刷新:传入与当前绑定相同的 ID 时,SDK 重新读取并同步该购物车,不会重新做首次整理。

站点不需要记录「上次绑定的 ID」,也不需要在调用前排队:快速开始 的 syncXcotton(cartId) 先等待初始化完成,再把这次同步交给 SDK。启动组件观察 cartId 属性,身份异步恢复或变化时调用 syncXcotton(cartId),同值重渲染不重复请求。这条身份订阅独立于站点写车事件,查询恢复也能完成绑定。

公开的 syncCart 调用按顺序执行;相邻且入参相同的并发调用会合并为一次同步并共享结果。若它与面板开关等不经公开入口的内部写车重叠,syncCart 会以 OPERATION_IN_PROGRESS 拒绝:context 中给出 activeOperation 与 requestedOperation,recovery.action 为 WAIT_FOR_OPERATION。同一时间只允许一个购物车组合操作(同步、SP/PP 写入与换车共用同一协调)。同车同步被拒会使已知旧快照失效。发起同车同步即表示当前购物车可能已被外部修改。当前进行中的旧流程不会再依据旧快照发起后续写入,但已经发出的请求仍按服务端真实结果确认。换车请求被拒只拒绝该请求。当前绑定和旧快照保持不变,目标车没有被接受不能证明当前车已变化。等待当前操作结束后再重新调用 syncCart。

SDK 内部的同步步骤(读车、SP/PP 同步与写入)共享本次操作权限并按依赖顺序执行,不会互相排队或重入公开锁;SP 与 PP 也不需要各自维护队列。SDK 只能协调本实例知晓的操作,无法阻止其他标签页或绕过 SDK 的直接请求。

在 syncCart 完成之前,请勿读取 getCartQuotes(),也不要启用结账按钮。启用购物车补购按钮时,此时 getCartQuotes() 会以 PP_CART_QUOTES_NOT_READY(未同步、同步中或快照失效)、PP_CART_QUOTES_SYNC_FAILED(上一次同步失败)或 CART_NOT_BOUND 拒绝,不会返回旧候选;window.xcotton.sp 也可能尚未就绪。

SDK 修改购物车后会调用你在 init 中注册的 onCartChange(cart)。该回调具有以下行为特征:

  • 触发时机:SDK 确认购物车事实后触发,参数是已确认的 Cart 快照。它不保证 PP 补购报价已就绪,getCartQuotes() 仍可能以 PP_CART_QUOTES_NOT_READY 或 PP_CART_QUOTES_SYNC_FAILED 拒绝;收到回调也不代表同步完成、本次业务成功或可以放行结账。
  • 触发次数:单次 SDK 操作最多触发一次通知,并传入最终确认的购物车快照。若写入中途失败,但 SDK 随后的补读成功,同样会触发一次通知以同步购物车的真实状态。
  • 静默操作:只读操作不会触发回调;购物车属性(attributes)写入确认后同样会触发一次回调。syncCart 确认购物车后触发一次通知,组合流程内的多次内部写入合并为一次通知;读车失败等没有确认快照的情况不通知,也不会用旧 Cart 代替。
  • 不等待返回值:SDK 不会等待回调返回的 Promise。回调抛出异常或 reject 不会影响 SDK 操作本身的结果,请在回调内部自行捕获并处理异步错误。
  • 回调参数:参数 cart 仅包含 SDK 所需的字段子集,详见 Cart。商品图片、checkoutUrl 等界面展示字段不在其中,如需使用请在回调内发起站点自身的查询。

推荐做法是让购物车页与购物车抽屉共用同一份站点状态,并在 onCartChange 中统一更新这份状态。这样两处视图会同时刷新,无需额外派发自定义事件:

await window.xcotton!.init({
onCartChange: () => {
void reloadCartView().catch(showCartError)
},
})

写入请求发出后,失败不一定代表购物车没有被修改。SDK 按证据区分四种写入状态(未发送、明确拒绝且未生效、已确认部分写入、结果未知),并保留在错误的 context.writeOutcome 中。处理原则:

  • 写入已发出但响应丢失或超时:先重新读取购物车确认事实,不要重放同一个写请求;超时只表示 SDK 停止等待,不代表服务端未执行。
  • 写入被确认但完整购物车读取失败:SDK 只做一次只读补读来恢复事实,不重新发送 mutation;补读成功也不会把原失败改成成功。
  • 默认 adapter 读取时出现分页游标、车身份、重复行或跨页车级事实异常:丢弃本轮聚合结果,从第一页做有限次数的纯读恢复,不重放写入。自定义 adapter 的跨页一致性由 adapter 自己保证。这只降低混合快照风险,不代表跨页读取具备事务一致性。
  • 公开操作失败时,已确认的购物车事实仍可能通过 onCartChange 通知(例如主商品已写入、保障失败);通知只用于刷新界面,业务是否完成仍以对应 API 的 Promise 为准。
错误写法导致的后果正确做法
在 onCartChange 中调用 syncCart同步完成后再次触发 onCartChange,形成死循环,不断发出购物车与报价请求回调内只刷新站点界面
将 SDK 的写入也作为站点写车事件转发给 syncCart与上一条相同,形成无限循环站点写车事件仅在站点自身发起写入时派发
站点修改购物车后未调用 syncCart报价与费用行停留在旧购物车状态(例如清空购物车后费用行仍残留)站点每次写车成功后都调用 syncCart(cart.id)
切换到另一个购物车后仍用旧 ID 同步SDK 继续读取旧购物车换车时传入新的购物车 ID
自己记录「已绑定购物车 ID」并据此跳过同步站点与服务端状态不一致时漏同步,且多出一份需要维护的身份每次都调用 syncCart(cart.id),由 SDK 判断是否需要重读
只刷新触发点击的单个组件购物车页与购物车抽屉展示的数据不一致所有购物车视图订阅同一份状态源
从回调参数 cart 中读取商品图片或 checkoutUrl字段不存在(返回 undefined)在回调中执行站点自身的购物车查询
回调中用 void 丢弃 Promise 且未加 catch刷新失败时产生未处理的 Promise Rejection,界面停留在过期状态在回调内部捕获并处理异步错误
写入失败后自动重试同一个写请求失败的请求可能已在服务端部分生效,自动重试会导致重复加购重新读取购物车状态,交由顾客决定下一步操作
未等上一次 syncCart 完成就继续发起站点写车站点写车可能基于旧快照;后到同步会排队,与内部写车冲突时可能报 OPERATION_IN_PROGRESS等当前同步结束并确认最新状态后再写车;syncCart 本身可连续调用,SDK 会排队或合并
自定义 adapter 吞掉限流错误后返回 nullSDK 会误判为购物车不存在,且不会进入冷却,后续请求继续被限流reject 一个至少带 status: 429 或 code: 'THROTTLED' 的 Error,可附 retryAfterMs