关于 spree-dependencies
介绍通过依赖注入替换 Spree 核心类的开发技能,面向 Spree 应用和扩展开发者。内容涵盖何时使用钩子或类替换,以及工作流、服务和 API 序列化器的约定。
主要功能
- 01判断何时适合替换类
- 02配置工作流和服务替换
- 03通过依赖项扩展 API 序列化器
- 04处理旧版依赖键
- 05通过 Spree 访问器调用以保留覆盖
适用场景
- 01实现自定义购物车或订单工作流
- 02为 Store 或 Admin API 序列化器添加字段
- 03将旧服务覆盖迁移到工作流
- 04让扩展代码遵循宿主应用的覆盖
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ spree/ agent-skills --skill spree-dependencies安装后在 Claude Code 中输入/spree-dependencies确认已加载。
详细介绍
Spree 依赖项(替换整个类)
命令使用 Spree CLI(
spree …)。如果是没有 CLI 的经典 Rails 应用,请从应用根目录运行bin/rails …/bundle exec rake …。路径假设 Rails 应用位于server/。
Spree.dependencies 可让你用自己的类替换核心类,例如服务、工作流、策略或序列化器。Spree 代码调用的是 Spree.cart_add_item_workflow.call(...),而不是 Spree::Carts::AddItem.call(...),因此你的替换类会在所有地方运行:Store API、Admin API、仪表板、作业和导入流程。
首先:你真的需要替换这个类吗?
替换类意味着你需要自行维护一份副本,并在每次 Spree 发布时重新同步。大多数替换类的原因都可以通过工作流钩子解决,而且这种做法能经受版本升级:
| 你的需求是…… | 用以下方式代替替换类 |
|---|---|
| 阻止加入购物车 / 结账 / 取消 / 退货 | validate 钩子 — Spree.hooks.register('carts.add_item.validate', 'MyStore::Rule') |
| 为税费或促销计算添加数据 | 上下文钩子(carts.recalculate_totals.set_tax_line_context、carts.recalculate.set_promotion_context) |
| 在变更时原子性地写入相关行 | 生命周期钩子(carts.add_item.after_item_added、carts.complete.after_finalize) |
| 与 ERP 同步 / 发送邮件 / 重新索引 | 事件订阅器(spree-events-webhooks) |
| 添加结账要求 | Spree::Checkout::Registry.add_requirement(spree-checkout) |
| 为 API 响应添加字段 | 子类化序列化器(见下文)——这是一种小范围且合理的替换方式 |
请参阅 spree-workflows 技能,查看钩子目录。只有在确实需要替换某项功能的计算方式时,才替换类(例如使用不同的合并策略、自定义购买框排序、B2B 访问策略或佣金模型)。
各扩展接口
Spree::Core::Dependencies::INJECTION_POINTS_WITH_DEFAULTS(位于 spree_core 的 lib/spree/core/dependencies.rb)定义了 167 个核心注入点;其中 106 个是 *_workflow 键。Spree::Api::ApiDependencies::INJECTION_POINTS_WITH_DEFAULTS(spree_api 的 lib/spree/api/dependencies.rb)包含 272 个 API 注入点,且仅限序列化器:Store 有 91 个(product_serializer),Admin 有 139 个(admin_product_serializer),Seller 有 42 个(seller_*_serializer)。(统计数据来自 6.0 源码;运行 spree rake spree:dependencies:list 查看你所用版本的数据。)
命名会告诉你约定:
| 后缀 | 基类 | 需要重写 |
|---|---|---|
*_workflow | Spree::Workflow 子类 | #perform(**keywords) — 绝不能用 #call |
*_service | 使用 prepend Spree::ServiceModule::Base 的普通类 | #call(**keywords) |
*_class、*_finder、*_handler 等 | 各不相同 — 阅读默认实现 | 按默认实现中的说明 |
常见的替换项:
| 键 | 默认值 | 说明 |
|---|---|---|
cart_add_item_workflow | Spree::Carts::AddItem | perform(variant:, cart:, quantity:, metadata:, options:, price:) |
cart_upsert_items_workflow | Spree::Carts::UpsertItems | 设置数量、移除或批量操作 |
cart_recalculate_workflow | Spree::Carts::Recalculate | 完整重新计价(配送、促销、总计) |
cart_recalculate_totals_workflow | Spree::Carts::RecalculateTotals | 唯一的总计扩展接口(折扣/费用/税费行 → 汇总) |
order_recalculate_totals_workflow | Spree::Orders::RecalculateTotals | 订单提交后的重新汇总对应项 |
cart_merge_workflow | Spree::Carts::Merge | 合并访客与客户购物车 |
carts_complete_workflow | Spree::Carts::Complete | 结账完成 — 优先使用 carts.complete.* 钩子 |
order_complete_workflow / order_cancel_workflow | Spree::Orders::Complete / ::Cancel | |
order_update_statuses_service | Spree::Orders::UpdateStatuses | payment_status / fulfillment_status 的唯一写入者 |
fulfillment_create_workflow / fulfillment_update_workflow | Spree::Fulfillments::Create / ::Update | |
payment_process_workflow、payment_capture_workflow、payment_void_workflow、refund_create_workflow | Spree::Payments::*、Spree::Refunds::Create | |
return_create_workflow … claim_*_workflow | Spree::Returns::*、Exchanges::*、Claims::* | |
storefront_access_policy_class | Spree::Storefront::AccessPolicy | Store API 记录可见性(所有权 + 访客令牌);B2B 扩展接口 |
products_for_context_service | Spree::Products::ForContext | 为买家缩小商品目录范围 |
product_buy_box_service | Spree::Products::SelectBuyBox | Marketplace:决定哪个变体作为主推项 |
purchase_amount_due_at_checkout_service | Spree::Purchases::AmountDueAtCheckout | 定金/净额账期 |
tax_resolve_exemptions_service | Spree::Tax::ResolveExemptions | 提供给税务服务商的免税证明 |
reporting_adapter | Spree::Reporting::Adapters::Live | |
search_product_presenter | nil(由搜索 gem 设置) |
ability_class(Spree::Ability)负责构建所有员工 CanCanCan 权限,包括 Admin API JWT 员工、Seller API 和导入/导出功能。继承 Spree::Ability,保留 initialize(user, options = {})(options[:store]、options[:resource]),调用 super,然后添加规则。它受权限键门控保护,而机密 API 密钥(Spree::ApiKeyAbility)不会查询它。请参阅 spree-auth-permissions。
替换工作流
继承随附的工作流,并使用相同的关键字参数重写 perform。perform 的签名就是契约(不匹配时 Ruby 会引发 ArgumentError);不带参数的 super 会运行整个父级流程并返回其 Result。
# server/app/workflows/my_store/carts/add_item.rb
module MyStore
module Carts
class AddItem < Spree::Carts::AddItem
# REQUIRED: without this the subclass dispatches 'my_store.carts.add_item.*'
# and every handler registered on 'carts.add_item.*' silently stops firing.
workflow_key 'carts.add_item'
def perform(variant:, cart: nil, **rest)
result = super # full Spree flow; a failure raises through, so code below runs only on success
external_step :notify_recommendations # network I/O — outside any transaction
result # Result#value is the line item
end
private
def notify_recommendations
MyStore::Recommendations.item_added(cart_id: cart.prefixed_id, sku: line_item.sku)
end
end
end
end
# server/config/initializers/spree.rb
Spree.cart_add_item_workflow = MyStore::Carts::AddItem
替换工作流的规则:
- 重写
perform,绝不要重写call。Spree::Workflow#call负责埋点、处理failure/halt!以及补偿。 - 保留
workflow_key,使其指向父级的键(见上文)。这是最常见的静默故障。 - 若要更改其中一个步骤,可重写私有步骤方法(例如
add_to_line_item)并调用super——但步骤名称属于内部实现,可能会在不同版本之间变化;使用钩子更安全。 - 返回
Spree::ServiceModule::Result(super的返回值,或success(value));非Result返回值会被包装为success。 Spree.cart_add_item_workflow的调用方预期父级的Result#value类型(此处为行项目)。不要更改它。- 工作流通过其他衔接点协作(
step :recalculate, with: -> { Spree.cart_recalculate_workflow }),因此替换一个衔接点会影响所有委托给它的流程。
替换普通服务
采用相同模式,重写 call:
module MyStore
class CartUpdate < Spree::Carts::Update
def call(cart:, params:)
result = super
MyStore::ErpSync.enqueue(result.value.prefixed_id) if result.success?
result
end
end
end
Spree.carts_update_service = MyStore::CartUpdate
从默认实现继承,以保留 Spree::ServiceModule::Base 的连接机制(类级 .call、success/failure)。始终返回 Result。
旧版 *_service 键会被暂存,而不会生效
已改为工作流的 Seam 仍接受旧名称,以确保应用能够启动,但赋值不会产生任何效果。值会被存储,并显示一条弃用警告:“Spree 不再读取此值——未应用此覆盖”,Spree 仍会调用工作流。按照旧服务契约(call(order:, …) 和 run 步骤)编写的类,不能与工作流契约互换。请进行迁移:继承工作流,将逻辑移至 perform/钩子中,并赋值给新键。这些名称将在 6.1 中移除。
| 旧键(仅暂存) | 改为赋值给 | 默认值 |
|---|---|---|
cart_add_item_service | cart_add_item_workflow | Spree::Carts::AddItem |
cart_recalculate_service | cart_recalculate_workflow | Spree::Carts::Recalculate |
cart_merge_strategy | cart_merge_workflow | Spree::Carts::Merge |
carts_complete_service | carts_complete_workflow | Spree::Carts::Complete |
carts_upsert_items_service | cart_upsert_items_workflow | Spree::Carts::UpsertItems |
order_complete_service | order_complete_workflow | Spree::Orders::Complete |
order_cancel_service | order_cancel_workflow | Spree::Orders::Cancel |
fulfillment_create_service | fulfillment_create_workflow | Spree::Fulfillments::Create |
shipment_update_service、fulfillment_update_service | fulfillment_update_workflow | Spree::Fulfillments::Update |
payments_handle_webhook_service | payments_handle_webhook_workflow | Spree::Payments::HandleWebhook |
gift_card_apply/remove/redeem_service | gift_card_apply/remove/redeem_workflow | Spree::GiftCards::* |
纯重命名(会生效,并显示警告):checkout_add_store_credit_service → store_credit_apply_service、checkout_remove_store_credit_service → store_credit_remove_service;API 的 stock_item_serializer → stock_level_serializer、digital_serializer → digital_asset_serializer(以及对应的 admin_ 版本)。
已移除的键(赋值时会在启动时引发 NoMethodError):carts_validate_service(完成验证由 Spree::Checkout::Requirements + carts.complete.validate 处理)、checkout_next_service、checkout_update_service、checkout_complete_service、checkout_select_shipping_method_service、checkout_get_shipping_rates_service、cart_create_service、cart_update_service、cart_estimate_shipping_rates_service、cart_change_currency_service、line_item_*_service、shipment_*_service、account_*_service,以及旧版查找器/排序器(products_finder、taxon_finder、variant_finder,等等)。购物车通过 carts_create_service / carts_update_service 创建和更新;结账流程推进使用 checkout_advance_service。从 5.x 升级?请参阅 spree-upgrade-5-to-6。
API 序列化器
API 依赖项仅限序列化器——没有按 API 区分的服务键;每个端点都会直接调用核心 Spree.<key> Seam。
# server/app/serializers/my_store/product_serializer.rb
module MyStore
class ProductSerializer < Spree::Api::V3::ProductSerializer # Alba resource
typelize brand_name: [:string, nullable: true]
attribute :brand_name do |product|
product.brand&.name
end
end
end
# config/initializers/spree.rb
Spree.api.product_serializer = 'MyStore::ProductSerializer' # Store API
Spree.api.admin_product_serializer = 'MyStore::Admin::ProductSerializer' # Admin API — separate key
- Store 和 Admin 的键彼此独立。
Spree::Api::V3::Admin::ProductSerializer通过常量继承随附的Spree::Api::V3::ProductSerializer,因此替换product_serializer不会改变 Admin 的输出——如果两个端点都需要该字段,请创建子类并同时替换两者。 - 嵌套关联通常通过访问器解析(
one :product, resource: proc { Spree.api.product_serializer }),因此这里也会使用你的类。 - 为
@spree/sdk类型添加带类型的字段:使用typelize(参见spree-typescript-sdk)。
按控制器覆盖
若只需更改一个端点,请装饰 API 控制器的具名钩子(serializer_class、scope、collection_includes、model_class、resource_permitted_attributes):
# server/app/controllers/spree/api/v3/store/products_controller_decorator.rb
module Spree::Api::V3::Store
module ProductsControllerDecorator
protected
def serializer_class
MyStore::DetailedProductSerializer
end
end
end
Spree::Api::V3::Store::ProductsController.prepend(Spree::Api::V3::Store::ProductsControllerDecorator)
没有按控制器区分的 *_service 钩子——控制器会调用全局 Seam。请参阅 spree-decorators。
使用自有代码中的接缝
Spree.cart_add_item_workflow.call(cart: cart, variant: variant, quantity: 1) # resolves overrides
Spree.api.cart_serializer.new(cart, params: { store: current_store }).to_h
Spree::Carts::AddItem.call(...) # ❌ bypasses any override
扩展尤其必须通过访问器进行配置,才能与宿主应用的覆盖项组合使用。
语法:Spree.<key> = MyClass(推荐——常量拼写错误时会立即报错),多个配置可使用 Spree.dependencies { |d| d.<key> = MyClass },也可使用旧版 Spree::Dependencies.<key> = 'MyClass'(字符串;延迟解析,仍受支持)。
检查覆盖项
spree rake spree:dependencies:list # every seam with its current value, [OVERRIDDEN] marked
spree rake spree:dependencies:list | grep workflow
spree rake spree:dependencies:overrides # only overrides: default -> current (file:line that set it)
spree rake spree:dependencies:validate # constantize every seam; exits 1 on NameError — good CI check
在继承项目中,首先要运行 overrides。对应的编程接口包括:Spree::Dependencies.current_values、.overridden?(:cart_add_item_workflow)、.override_info(:cart_add_item_workflow)、.validate!;API 侧则使用 Spree::Api::Dependencies。仅写入旧版暂存区的内容不会显示为覆盖项——升级后,请在初始化文件中搜索 _service =。
注意事项
- 加载顺序: 在
config/initializers/spree.rb中设置覆盖项。如果两个 gem 设置了同一个键,则最后一次赋值生效;可通过spree:dependencies:overrides检查。 - 替换后的工作流失去了钩子 → 缺少
workflow_key(见上文)。 - 升级后覆盖项“没有效果” → 你赋值的是旧版
*_service键;请在启动日志中查找“NO LONGER CONSULTED”弃用提示。 - 丢失行为: 如果替换实现跳过
super,就必须重新实现标准的预留、定价提供方、可用性检查、重新计算和事件。请先完整阅读父级的perform。 - 调用时出现
ArgumentError→ 你的perform签名不接受调用方传入的关键字参数。请接受**rest并转发它。
延伸阅读
node_modules/@spree/docs/dist/developer/customization/dependencies.md(https://spreecommerce.org/docs/developer/customization/dependencies)——注意:已发布页面的部分内容仍显示 API v2 的storefront_*键;应以源代码中的常量为准。node_modules/@spree/docs/dist/developer/customization/workflows.md——钩子,是首选替代方案- 源码:
spree_core的lib/spree/core/dependencies.rb(INJECTION_POINTS_WITH_DEFAULTS、LEGACY_WORKFLOW_KEYS、LEGACY_SERVICE_KEYS、RENAMED_SERVICE_KEYS)、spree_api的lib/spree/api/dependencies.rb、lib/tasks/dependencies.rake - 相关技能:
spree-workflows、spree-customization、spree-decorators、spree-extensions