ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Spree 6.0 Cart/Order 拆分实战:copy-on-completion、Carts::Complete 三阶段管线与订单状态机的移除

Spree 6.0 Cart/Order 拆分实战:copy-on-completion、Carts::Complete 三阶段管线与订单状态机的移除 Spree 6.0 Cart/Order 拆分实战copy-on-completion、Carts::Complete 三阶段管线与订单状态机的移除【免费下载链接】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 最上游的重构计划 Cart/Order Split 展开购物车Spree::Cart与订单Spree::Order彻底拆分为两个独立模型结算完成时通过Spree::Carts::Complete三阶段管线把购物车复制为不可变订单订单状态机state列、next!、checkout_flow随之删除。读完本文你将掌握这套拆分背后的九个关键决策、完成管线的每步幂等设计、checkout 需求Requirements体系如何取代状态机以及payment_status/fulfillment_status的先推导后持久化状态重算机制并能在仓库源码中定位到每一处实现。一、设计总览为什么把 Cart 和 Order 拆成两个模型该计划docs/plans/6.0-cart-order-split.md状态为已实现Spree::Cart端到端拥有购物/结算阶段订单状态机与spree_orders.state列已删除Carts::Complete三阶段管线上线Orders::UpdateStatuses是状态列的唯一写入者仅剩 Phase 6 的清理工作划归 6.1移除弃用别名与迁移任务、给spree_orders.market_id/channel_id加null: false。核心思路一句话概括购物车拥有未完成的一生订单拥有已成交的一生两者通过完成时的整份拷贝连接互不共享任何行。计划引用了 2026-07-27 的行业评审结论——独立 cart/order 加 copy-on-completion 是主流模式某开源平台 A 的 Cart 模块与 Order 模块用并行表、cart 保留并打completed_at平台 B 通过checkoutComplete以checkout_token幂等托管市场头部产品甚至在 2025 年整体删除了中间层 Checkout API。唯一的单实体派平台 C靠active标志、orderPlacedAt快照、重开已下单订单的 Modifying 状态等补丁维持反过来印证了拆分的必要性。Spree 的拆分不是从零画线而是固化代码库中已经存在的接缝v3 Store API 本来就是 cart 形状——Spree::Carts::{Create,Update,UpsertItems,Complete}已存在路由为/store/carts/...加只读的/store/orders/:idAPI 契约是步骤形状而非状态形状——CartSerializer只暴露current_step、completed_steps、requirements、warnings从不暴露原始stateOrderSerializer暴露payment_status/fulfillment_status/completed_at无 state、无 steps查询层早已以completed_at为界——Order.complete/.incomplete主作用域都基于completed_atOrder#statusdraft/placed/canceled在拆分前就已存在生命周期列活下来结算state列死了。该计划同时是四个下游计划的门禁gatestyped-line 所属者6.0-6.1-split-adjustments.md、履约/费率所属者6.0-fulfillment-and-delivery.md、多卖家市场 Phase 3拆分在Carts::Complete内部触发、state 归一化6.0-normalize-state-to-status.md。二、Spree::Cart模型购物车侧的数据形状关键决策 1 规定Spree::Cart前缀cart_表spree_carts拥有完成前阶段——store/market/channel/currency/locale/customer 绑定统一了旧代码中三条不一致的创建路径、email、收货/账单地址引用、accept_marketing、特殊说明、自己的has_secure_token、metadata、completed_at与完成锁completing_at。购物车没有 status 列——completed_at是唯一的生命周期标记。从源码看这一决策在 Cart 模型 的类注释里被逐字落实# The pre-completion (shopping checkout) phase of a purchase. Completing # checkout copies a cart into an immutable {Spree::Order}; the cart is # retained with completed_at set. Carts have no status column — # completed_at is the only lifecycle marker. class Cart Spree.base_class模型层面的几个值得注意的实现细节完成即只读readonly?在completed_at已持久化时返回 truecart.rb#L131-L137把已完成的购物车挡在 save/update_columns/touch/destroy 之外——完成写操作本身之所以能通过是因为它在completed_at仍为 nil 时触发。完成锁与 TTLCOMPLETING_CLAIM_TTL 5.minutescart.rb#L139-L155completion_claimed?判断一次结算尝试是否在持锁——超过 5 分钟的锁视为陈旧、可被接管崩溃的进程不会把购物车锁死。生命周期事件publishes_lifecycle_events发出cart.created/cart.updated/cart.deleted恢复了 5.x 时代未完成订单发order.*事件所提供的弃购信号。作用域complete/incomplete全部基于completed_atcart.rb#L102-L103。写触发重算recalculate_for_address_change!cart.rb#L238-L257在一次 provider 往返里重定价、重计税、重建配送提案是transition 触发重算的替代品——旧行为是税费只在状态迁移时重跑在payment阶段就地改地址永远不会重新计税。幂等的履约提案重建rebuild_fulfillments!取代旧的破坏性create_proposed_shipments完成的购物车其履约是冻结副本永不重建。三、Owner 模式双具体外键cart_id/order_id恰好其一不是多态关键决策 2 推翻了早期草稿中的多态owner方案6.0 的设计语言是具体外键胜过多态类型字符串split-adjustments 删除了adjustable_typetyped-stock-movements 删除了originator_typeLineItem 不应重新引入这个版本要移除的模式。因此所有cart 或 order 拥有的表采用同一形状可空cart_id 可空order_id恰好其一校验#owner作为方法order || cart。覆盖范围包括spree_line_items只新增cart_id——order_id已存在且永久保留这是成本最低的迁移、typed 金额行TaxLine/Discount/Fee与Fulfillment。DeliveryRate则不携带任何 owner 列费率属于其 fulfillmentowner 经由它推导——spree_shipping_rates今天就没有这两个外键复制这对列只会在每次完成重新指向时引入漂移风险。溯源provenance不需要行上第二个外键Order#cart_id唯一——即幂等键把每个订单连回其购物车保留的购物车带着自己那批行作为结算时刻的冻结快照这单从哪个 cart 来经由 order 回答即可下单后的追加行order 有而 cart 没有的行也天然可区分。两列同时设置被评估后否决order 侧副本携带cart_id会在每个cart.line_items式关联中与购物车自己的行冲突。在 Cart 模型 中可以看到 owner 模式落地后的关联形态line_items、tax_lines、discounts、fees、fulfillments、payments、payment_sessions、stock_reservations全部以 cart 侧挂接has_one :order与has_one :order_group二者恒有且仅有一个被设置多卖家购物车完成为订单组。LineItem 原有的 22 处内部 14 处外部order读取定价上下文、税区、库存、促销分摊、guest-token 权限检查迁移到owner订单拥有的行继续原生回答line_item.order。四、Copy-on-Completion复制一切不共享任何东西关键决策 3 规定完成时的拷贝契约订单收到副本——行项目带新 IDtyped 金额行按 split-adjustments 的拷贝契约重新指向、fulfillments 已选配送费率、以及地址绝不共享行——平台 B 拷贝地址正是为了让之后对 cart/客户档案的编辑无法污染订单历史。购物车被保留带completed_at 订单链接平台 A 的选择而非托管头部产品的删除路线——保留正是幂等重放得以自然成立的原因也保全了弃购分析。guest token 携带到订单上确认页因此能用客人已持有的凭证工作。Complete 工作流 的create_draft_order!就是拷贝契约的源码级呈现整个拷贝在一个事务内完成创建status: draft的订单携带 email/currency/locale/market/channel/customer/token、accept_marketing、coupon_code、preferred_stock_location、B2B 字段po_number、po_document、地址快照cart.ship_address.snapshot——见下文快照规则copy_line_items!逐行拷贝skip_tax_estimation truecart 的税行几行之后会原样拷过来对每行再估算一次——对外部引擎而言是锁内的可计费远程调用copy_fulfillments!fulfillment 已选selected_delivery_rate fulfillment_items按line_item_map/fulfillment_map重指 IDcopy_typed_lines!按Fee → Discount → TaxLine顺序拷贝费用在税行之先因为费用是计税对象先拷税行会指向马上被销毁的 cart 侧费用行copy_promotions!、copy_tax_identifier!把买方的税务登记冻结到订单上标注来源链override/company/customer、copy_po_document!同一 blob 挂到两个记录上不重复上传repoint_money_records!complete.rb#L385-L391钱相关记录不是拷贝而是重新指向——payments、payment_sessions、stock_reservations、CouponCode 从cart_id改指order_id因为外部交易引用绝不能分叉最后用update_columns把 P3 算出的全部金额列item_total 到 total、payment_total盖到订单上。地址快照还有一条更细的纪律计划的 Constraints 部分2026-08-31 定案订单侧地址副本必须用Spree::Address#snapshot而非dup——副本记录的是包裹去了哪里、属于订单不携带 owner 和 labeldup保留了 owner会导致每下一单就在买家地址簿里多出两条他已存地址的新副本。地址拷贝与地址相等性是两件事各有唯一答案#snapshot负责造副本#/value_attributes/EXCLUDED_KEYS_FOR_COMPARISON负责判定两行是否同一地点任何造副本的代码都不得读取相等性语义。五、幂等完成旧 stub 最大的缺口关键决策 4 指出每个竞品都把幂等当承重结构。Spree::Carts::Complete的四道保险spree_orders.cart_id带唯一索引——比平台 B 的filter().first()在真实并发下更强入口处若该 cart 已有订单直接把它作为成功返回——双击 Place Order 或丢失响应后的重试必须拿到订单而不是报错cart.with_lockcompleting_at守卫并行尝试订单先于支付捕获创建平台 A 的原始理由最小化已授权/已捕获支付必须回滚的窗口支付后失败走补偿作废。在 complete.rb#L33-L70 中可以看到这些保险的精确落位replay_completedP1 重放在流程最前cart.with_lock块内是guard_concurrent_completion409completion_in_progress、recalculate_in_lock、verify_expected_total409cart_changed、validate_cart、mark_completing、create_draft_order最外层rescue ActiveRecord::RecordNotUnique——并发下输掉唯一索引竞争的一方直接重入由重放步骤返回赢家的订单。finalize pending 的标记是结构性的不需要单独的完成状态列order.status: draftorders.cart_id存在 cart.completed_at: nil三者合起来就是待 finalize。重试的Complete调用走重放找到 draft 订单 → 跳过 PREPARE、重新核验支付覆盖Y、重跑 FINALIZE每步自守卫。事件只在把draft → placed的那个事务的after_commit中触发回滚的 finalize 永远不泄漏order.completed事件或确认邮件。补偿边界同样精确rollback_draft_ordercomplete.rb#L177-L198只有捕获前的失败才走 Y3 补偿销毁 draft 订单 副本、清空completing_at购物车保持可用一旦订单有有效 completed 支付或支付已被覆盖绝不销毁——捕获后的失败只能靠恢复 sweeper 重试 FINALIZE持久失败告警操作员。每步幂等的守卫一览继承自计划全部有源码对应步骤守卫创建订单orders.cart_id唯一数据库约束不是查询支付处理前检查支付记录跳过已覆盖总额的已完成支付库存 finalizeunit 上的pending标志只 unstock pending unit优惠券/礼品卡/促销计数器每 cart 持久化标志check-then-set数字商品链接按行项目按数量find_or_create修复旧的每行一次非幂等回调事件/邮件只在draft → placed翻转时触发confirmation_delivered?守卫保留支付覆盖的判定也值得单独拎出来complete.rb#L393-L399# Completion gates on a valid payment exists covering what is owed at # checkout, never on paid? — a net-terms order completes with a # pending payment. def payment_covered?(order) order.payments.valid.where(status: %w[pending processing completed]).sum(:amount) order.amount_due_at_checkout end六、三阶段完成管线PREPARE / PAYMENT / FINALIZE完成是一个带显式事务边界的三阶段管线外部支付 I/O 永不运行在数据库事务内成功扣款之后的一切都是小的、幂等的、可恢复的。Complete 工作流 类注释里的管线摘要即权威版本以下是计划的完整阶段规格PHASE P — PREPARE一个事务 P1. 重放检查completion_result(cart) → 返回 success(order|order_group) P2. cart.with_lockcompleting_at 新鲜则拒绝409 completion_in_progress 超过 TTL默认 5 分钟视为陈旧 → 接管 P3. 锁内重算价格、促销、税估算——即将收取的金额在这里计算 不信任更早请求的结果 P4. 漂移守卫客户端发来 expected_total 且 ≠ cart.total → 409 cart_changed { current_total }客户端重渲染、确认、重试 P5. 全量校验电池结构化错误见 API 契约 P6. 对照库存预留做最终可用性检查hold 延长到完成为止 P7. 创建 status: draft 的订单 全部副本行项目 owner 重指、typed 金额行、 fulfillments 已选费率、地址副本、email/currency/locale/market/channel、 携带 guest token、number R…写 orders.cart_id唯一盖 completing_at ——对一切界面不可见店面列表用 placed_orders管理端 Drafts 列表 限定 cart_id: nil后台 draft 订单没有 cart 所以完成中的 draft 订单永远不会出现在那里 PHASE Y — PAYMENT事务边界取决于支付方式 Y1. payment_required? 为假时跳过零总额或 store credit gift card 完全覆盖——覆盖路径绝不早于此刻自动完成 Y2. 对 DRAFT ORDER 的总额逐支付方式处理 • 线下Check、挂账/账期、货到付款、银行转账、人工记账无外部 I/O 按 auto_capture? 落 pending/completed整个阶段收进 FINALIZE 事务 • 外部网关支付会话、redirect/3DS、卡捕获无事务失败走补偿 Y3. 外部失败 → 补偿销毁 draft 订单 副本清 completing_at 返回 422 payment_failed购物车保持可用外部什么都没发生 PHASE F — FINALIZE一个事务每步幂等 F1. 库存finalize unitspending: false、unstock、释放库存预留 唯一入口——修复了今天管理端完成路径从不释放预留的不对称 F2. 持久化标志背后的用量计数器优惠券码、礼品卡兑换、促销额度 F3. 多卖家拆分钩子分区 → 子订单 OrderGroup completion_result 变成该 group F4. order.status: draft → placed盖 cart.completed_at 清 completing_atUpdateStatuses F5. after_commit发 order.placed / order.completed遵守 notify_customer、数字履约经 FulfillmentProvider::Digital、 用户账户创建 newsletter 订阅非致命边作业源码中每个 P 步骤与命名 step 一一对应replay_completedP1/P2、recalculate_in_lockP3且外部定价在锁外先经confirm_prices确认——网络调用不占完成锁、verify_expected_totalP4、validate_cartP5调用Spree::Checkout::Requirements#call(completion: true)、mark_completingP2 盖戳、create_draft_orderP7。多卖家交互幂等查询是completion_result(cart)——cart.order单分区或cart.order_group多分区cart_id在 group 上、子订单经它连接二者恰有一个。拆分运行在 FINALIZEF3即支付之后客户对一个篮子授权了一个金额钱先收、簿记随后。中途崩溃的拆分像任何 finalize 失败一样恢复重放返回 group。源码里split_by_sellercomplete.rb#L428-L453的实现细节单分区时不建 group只把seller_id校正为卖方多分区时 PREPARE 建的 draft 订单成为 group 的第一个子订单而非被替换——它是支付所针对的记录、是携带唯一cart_id使重放成立的行。complete_orders逐个 child 走Spree::Orders::Complete第一个 child 携带确认邮件、其余静默下单commit_tax对每个已下单订单向税务引擎提交每个子订单是一笔独立的应税销售。七、移除状态机三个替代品不是一个关键决策 5 拆成三条线索每条有唯一归属地(a) 结算推进 → Requirements。current_step/completed_steps/requirements由Checkout::Requirements对着购物车数据计算。Requirements 实现 聚合三处来源——内置检查DefaultRequirements、Registry注册的步骤ordered_steps中适用且未满足者、Registry.add_requirement注册的附加要求——输出统一形状{ step, field, code, message }。call(completion: true)就是完成的答案Carts::Complete直接调用它Carts::Validate外壳在变成一行调用后被删除因此 API 的requirements数组带稳定code与完成闸门永不失配。Checkout 教条2026-08-01 定案后端只强制一个硬闸门——Carts::Complete——其余一切皆咨询性。没有服务端步骤排序客户端可以任意顺序写任何结算字段步骤只是需求的分组标签前端哪页展示该错误加序列化器糖current_checkout_step 第一个未满足需求无未决时回退到complete前一步——只有已完成的购物车才报complete。排序是前端的设计内职责5.x 在后端流程定义与前端页面之间来回跳的痛苦正源于后端拥有序列。后果各归一处一个完成事实所有检查声明在Checkout::DefaultRequirements仅完成的检查——逐项库存、已停售、guest 策略——藏在completion: true之后保持 cart 读取时的咨询 feed 便宜或经Registry.add_requirement注册一个步骤词汇表Registry.base_steps有序{name applicability-proc}哈希是内置步骤的唯一声明安装方通过改base_steps换谓词、删confirm或注册步骤来定制——无常量、无子类化定制面额外需求 一行Registry.add_requirement同时出现在 API feed 里并阻挡完成渲染 feed 的前端零改动步骤之间逻辑刻意没有后端归属——副作用挂在数据写入workflow 钩子Carts::AddItem#after_item_added、Carts::Complete#before_finalize与事件cart.updated、order.placed上一个 finalize 归属订单侧完成必要时处理支付、履约 finalize、下单、副作用、自动履约、状态、order.placed是Spree::Orders::Completeworkflow——命名步骤同时服务 checkoutCarts::Complete委托其 FINALIZE 阶段与 admin/B2B draftOrder#finalize!是标记 6.1 移除的弃用外壳。(b) 迁移触发重算 → 写触发重算。地址/market 变更 → 重定价、重计税、重建配送提案Carts::Update在地址变更后调recalculate_for_address_change!行项目变更 → 总额 预留Carts::Update中现有的 Release/Reserve/Extend 分派沿用。旧的memoizedtax_zone陈旧隐患随 memoized 订单上下文一起消亡。(c) 状态 → 先推导后持久化。平台 A 在读取时纯派生付出过公开 bug 的代价管理端订单列表不能按派生状态过滤/排序全额退货后状态卡死托管头部产品有类似的状态过滤索引 bug。Spree 管理端重度使用 Ransack——纯派生会让订单列表过滤在第一天就坏。所以payment_status与fulfillment_status是存储且建索引的字符串列由单一服务重算下一节。订单生命周期留在 Order 上、但没有机器关键决策 6status只有draft/placed/canceledpartially_canceled变成对子取消的派生谓词completed?≡completed_at.present?Cancel/Resume/Approve 保持为服务Orders::Cancel/Resume/Approve翻status并原样运行今天的副作用。事件命名在拆分后定案购物车发自己的cart.created/updated/deleted下单事件是order.placed6.0 双发旧名并带deprecated_alias_of元数据6.1 删除order.created担不起这个角色——它对每个订单行都会触发包括 admin draft 和Carts::Complete内部支付前的 draft 副本支付失败还会为它发order.deleted。Spree::StateChange及Spree::LogEntry于 2026-08-07 整体删除生命周期事件是唯一审计来源。价格冻结保证平台 C 的ArrangingPayment锁所提供的由completing_at拒绝支付在途期间的购物车变更 完成订单不可变split-adjustments 的订单级冻结共同承担完成时点的可审计价格也同时服务欧盟 Omnibus 指令。八、状态重算Orders::UpdateStatuses是唯一写入者Spree::Orders::UpdateStatuses.call(order:) # 两列的唯一写入者 # payment_status: quantized(captured charged) vs quantized(total − granted_refunds) # → none/authorized/partially_paid/paid/partially_refunded/refunded/ # overcharged/voided # fulfillment_status: 聚合自 fulfillment 记录含 backorder # 该值域归履约计划所有由 payment/refund/fulfillment/return 事件的订阅者触发——永不内联自 controller。实现 里能看到竞品血泪学来的规则逐条落实金额先量化到币种精度再比较quantize到currency.exponent比较前从总额中减去已批准/待处理退款target quantize(total) − refunded支付值域包含overcharged与voidedvoided的判定已取消订单、净捕获 ≤ 0、但曾发生过捕获;退货交互被显式定义有退款且净捕获 ≤ 0 →refunded有退款且净捕获 0 →partially_refundedfulfillment_status_for把履约记录卷成一个词含 backorder →backordercanceled 履约被忽略除非全部被召回混合含已交付 →partial全部包裹到达才delivered拆分订单的金额不来自订单自身——money_for中 group 内订单的金额取自其payment_splits份额份额被存储而非重算一旦一个卖家已发货并被捕获而另一个还没有group 总额的任何比例都无法描述双方。九、支付边缘情况外部网关与离线支付方法异步成功3DS / redirect / webhook两个写者调用同一个服务——客户返回页的POST /complete与Payments::HandleWebhook。先跑者创建订单后跑者重放它。webhook 保留其order.with_lock 重放检查但安全性来自唯一索引——当前的 30 秒 job 延迟变成延迟优化而非正确性机制。Webhook 报失败把支付会话标记失败仅当存在 draft 订单且无其他有效支付时才按 Y3 补偿。购物车保持可用以待再试。支付已捕获、finalize 持续失败如数据库分区订单以 draft 成功支付的形式存在。绝不对瞬时失败自动退款——完成恢复 sweeper 重试有支付覆盖的陈旧 draft 订单持久失败告警。这正是订单先于支付排序所最小化的精确场景。部分 store credit 外部支付store-credit 支付在 P3 重算recalculate_store_credit_payment行为沿用外部会话覆盖余额Y2 两者都处理store credit 在补偿时最后作废。零总额购物车100% 促销、礼品卡Y 整体跳过F 直接跑。地址更新时绝不自动完成规则留在Carts::Update#try_advance——只有显式Complete调用到达此管线。购物车过期后的迟到 webhook结构性不可能——reaper 从不删除带 authorized/pending 支付会话的购物车资金安全排除项。并非每种支付方式都是外部网关已决问题 11PaymentMethod::Check、B2B 挂账/账期、货到付款、银行转账、人工 API 记账的支付走同一个process_payments!接缝但不做任何外部 I/O——方法返回本地成功响应auto_capture?决定支付落pending已授权发票结清后由 admin 捕获还是completed。对管线的后果线下支付把 PAYMENT 收进 FINALIZE——没有网络调用就没有可补偿物与部分失败窗口整个完成是单事务payment_required?不等于必须现在付钱账期订单以payment_total: 0pending支付完成——那是一个成功完成的未付款订单而非失败完成必须门槛在存在覆盖总额的有效支付永不在paid?UpdateStatuses报payment_status: none/authorized催款与捕获是完成后 AR 的事store credit 与礼品卡按同一规则也是离线的线下 外部混合卡上订金、余额账期任何外部成分强制走外部路径离线成分在同阶段处理并在补偿时一并作废;B2B/后台订单绕过 checkout 人机工学但必须复用同一服务幂等、库存 finalize、预留释放留在一处allow_checkout_on_gateway_error只对外部方法保留其含义——它不得掩盖离线失败那意味着 bug 或规则违例而不是网络抖动。漂移与变更守卫completing_at在支付在途期间以 409cart_locked拒绝购物车变更items、地址、折扣——价格冻结保证TTL 陈旧锁可回收。重算发生在锁内P3过期促销掉出、价目表变更生效、税重新估算expected_total守卫把静默漂移变成显式的客户端确认循环而不是向客户收取他从未见过的金额。库存侧预留 hold/extend 贯穿完成P6 检查捕获无预留的安装可 backorder 的变体以 backordered unit 完成backorder 是履约状态不是完成失败。乐观并发state_lock_version更名为lock_version、语义不变自增-on-write陈旧客户端在变更时拿 409stale_cartComplete本身依赖行锁。API 契约POST /store/carts/:id/complete结果响应完成含已完成购物车的重放200{ order }或{ order_group }校验失败422{ errors: [{ step, field, code, message }] }—— 与requirements属性同形支付被拒/捕获前失败422{ error: { code: payment_failed, … } }总额自客户端渲染以来变化409{ error: { code: cart_changed, current_total } }另一次完成在途409{ error: { code: completion_in_progress } }—— 客户端轮询/重试完成后重放返回订单锁定的购物车被变更409cart_locked在变更端点上而非 complete 上十、迁移路径六个阶段计划给出的六阶段迁移Wave 15 已落地Phase 6 划归 6.1Phase 1 — schema纯增量spree_cartsspree_line_items加可空cart_idorder_id保留存在性校验放宽为恰好其一spree_orders.cart_id唯一按 normalize 计划建存储状态列。spree_orders.state原样存活到 Phase 4。Phase 2 — 数据rake 任务既有未完成订单转换为 Cart 行它们就是今天的购物车每个未完成订单一行 Cart携带 token/绑定/地址行项目改由 cart 拥有设cart_id、清order_id空壳订单行删除。支付进行中的未完成订单最后转换并重新链接会话。已完成订单除cart_id: nil外不动。分批、幂等、可恢复。Phase 3 — 模型 服务Cart 模型 写触发重算Carts::Complete重写Registry 步骤列表所有权旧Cart::*服务删除/改址current_cartmerge 策略reaper谓词改写。Phase 4 — 移除状态机删Order::Checkout、state 列、checkout_flowAPI、订单state_changes写入Ransack 白名单替换UpdateStatuses 订阅者上线六处带外state写者地址服务等变成 cart 重算触发器。Phase 5 — API/SDK/dashboard/文档契约已是步骤形状此阶段很薄carts/fulfillments_controller的cart.next循环 → requirements 驱动序列化器形状不变typelizer/Zod/OpenAPI 再生order_walkthrough/factories扩展迁移指南checkout_flow→ Registry邮件支付链接 URL。Phase 6 — 清理6.1移除弃用别名与迁移任务spree_line_items.order_id不删——它是永久双 FK owner 模式的一半另在此阶段给spree_orders.market_id/channel_id加null: false升级清单的spree:backfill_order_markets步骤保证届时每个安装都有解析值spree_carts从创建起就在 DB 层强制两者。十一、购物车卫生与遗留服务清理拆分让购物车卫生从无所谓变成必需关键决策 7、8分层过期 reaper今天不存在guest 购物车 30 天、客户购物车 90 天、空购物车 48 小时——可配置、基于索引updated_at计量、批处理。带资金的购物车authorized/pending 支付会话或交易永不收割——绝不在授权中途做垃圾回收。购物车合并成为可替换策略Spree::Dependencies.cart_merge_strategy默认保留现有OrderMerger行为但去掉其 bug——不再静默丢弃不同币种的行而不提示不再对 10 的 cart 溢出只做一行日志就硬删。源码中Cart#merge!即委托cart_merge_workflowcart.rb#L265-L270。旧Spree::Cart::*服务清理14 个中 8 个是死重DI 注册、零调用方直接删除6 个有真实调用方的改址到 Cart 模型/Carts::*命名空间。Carts::{Create,Update,UpsertItems}获得 DI 注册controller_helpers/order.rb的current_order变为经Carts::Create构建的current_cart修复其缺失的 market/channel/locale 绑定。破坏性表面关键决策 9直言不讳order.state、checkout 状态机、checkout_flow/insert_checkout_step/remove_checkout_step、next!/advance、can_go_to_state?全部删除扩展作者迁移到Checkout::Registry requirementscheckout_flow硬删、不留 shim——翻译 shim 是泄漏的机器迁移没有 registry 等价物。Ransack 上state离开白名单status/payment_status/fulfillment_status成为可过滤存储列——替代品严格更有用。OrderLock的state_lock_version更名lock_version且语义保存。emails gem 基本不动仅支付链接邮件中的checkout_state_url(token, :payment)变成 cart checkout URL。十二、面向开发者的现行约束计划对正在写的代码立下数条纪律值得摘录为团队规范任何触碰行项目的新代码用line_item.owner而非line_item.order——owner是方法order || cart不要新增 checkout 状态、迁移或checkout_flow步骤——扩展Checkout::Registry新代码中不要读取或按order.state过滤——用completed_at/status谓词与 requirements API新的金额/履约表出生时自带 cart/order owner XOR不要在任何地方基于state_changes行构建——该模型已删除订阅生命周期事件订单地址副本一律Spree::Address#snapshot永不用dup不要调用已弃用的Address#clone6.1 起回归标准Object#clone。十三、完成副作用对齐清单与关联计划计划末尾的 parity checklist 确保旧finalize!after_transition to: :complete链条做的每件事都有去向调整冻结→ split-adjustments 订单级冻结、支付/发货状态更新→ UpdateStatuses、status: placed、completed_at、update 钩子、风险检查considered_riskyorder.updated、order.completed事件、优惠券使用、礼品卡兑换、newsletter 订阅、用户账户创建、数字链接→FulfillmentProvider::Digital幂等 按数量、确认邮件本就事件驱动不变。关联计划均位于 docs/plans/6.0-6.1-split-adjustments.md — typed 金额行所属者与订单级冻结的拷贝契约6.0-fulfillment-and-delivery.md —Fulfillment双 FK 与fulfillment_status值域含backorder6.0-multi-vendor-marketplace.md Phase 3 — 在Carts::CompleteFINALIZE 内触发的卖家拆分6.0-returns-exchanges-claims.md —awaiting_return/returned归属其一等 Return 模型而非订单状态6.0-normalize-state-to-status.md —Order.state的删除与状态列命名6.0-stock-reservations.md — 贯穿完成的预留 hold/extend 语义。核心源码入口一览Spree::Cart 模型、Carts::Complete 工作流、Checkout::Requirements、Checkout 目录DefaultRequirements/Registry/Step、Orders::UpdateStatuses、Carts 服务目录Create/Update/PartitionBySeller/SplitBySeller 等、完成管线的规格测试 complete_spec.rb 与拆分规格 complete_split_spec.rb。【免费下载链接】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),仅供参考
返回列表