mcpField
S
spree-decorators

spree-decorators

github.com/spree开发10225 次安装4 周前更新
代码仓库

关于 spree-decorators

面向 Rails 应用中扩展 Spree 类的开发技能,采用 Ruby 装饰器。内容说明模型与控制器装饰器的适用范围,提供生成器和实现模式,并指出工作流及 API 更改的替代方式。

主要功能

  1. 01生成模型和控制器装饰器
  2. 02添加模型关联和验证
  3. 03扩展作用域与辅助方法
  4. 04调整 API 控制器钩子
  5. 05允许属性并配置筛选
  6. 06识别应使用其他扩展点的场景

适用场景

  1. 01为 Spree 模型添加字段关联
  2. 02定义模型作用域或只读辅助方法
  3. 03缩小 API 端点返回的记录范围
  4. 04为业务行为选择工作流钩子而非装饰器

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/spree/agent-skills --skill spree-decorators

安装后在 Claude Code 中输入/spree-decorators确认已加载。

详细介绍

Spree 装饰器

命令使用 Spree CLI(spree …)。在没有 CLI 的传统 Rails 应用中,请在应用根目录运行 bin/rails g …。路径假定 Rails 应用位于 server/。

装饰器使用 Ruby 的 Module#prepend,按照 Spree 文件名约定并借助两个生成器,从应用中修改现有 Spree 类。这会让你依赖 Spree 内部实现——仅将其用于结构性更改(关联、验证、作用域、只读辅助方法、收窄控制器作用域),不要用于行为更改。

先选择合适的工具

使用场景使用此项,而不是装饰器
否决某项操作(添加到购物车限制、结账规则、取消/退货政策)工作流 validate 钩子 — spree-workflows
为税费 / 促销计算提供数据工作流上下文钩子 — spree-workflows
在购物车/订单/履约/支付操作的同时执行某项操作工作流生命周期钩子 — spree-workflows
保存后响应 / 同步到 ERP / 发送通知事件订阅器 — spree-events-webhooks
替换某个流程的计算方式Spree.dependencies — spree-dependencies
更改 API 响应结构通过 Spree.api.*_serializer 使用序列化器子类 — spree-dependencies
接受 API 写入中的额外属性Model.additional_permitted_attributes += [...](见下文)
让字段可通过 q[...] 筛选Spree.ransack.add_attribute(Spree::Product, :brand_id)
无需迁移即可存储额外值自定义字段(product.set_custom_field('ns.key', value))— spree-catalog
添加新端点新建 ResourceController 子类 — spree-resource
管理界面更改Dashboard 插件 / 插槽 — spree-dashboard-plugins
为模型添加关联、验证、作用域或辅助方法装饰器(此技能)
收窄现有端点的 scope,替换其 serializer_class控制器装饰器(此技能)

切勿装饰业务动词(Order#finalize!、Order#cancel、Payment#capture!、Fulfillment#finalize!):它们是已弃用的空壳,会委托给工作流,而实际流程(Spree::Carts::Complete、Spree::Orders::Cancel、Spree::Payments::Capture)不会调用它们。状态转换也没有可供挂钩的状态机回调——请使用工作流钩子。

模式

# server/app/models/spree/product_decorator.rb
module Spree
  module ProductDecorator
    def self.prepended(base)
      # class-level: associations, validations, scopes, class_attributes — called on base
    end

    # instance methods here; call super to reach the original
  end

  Product.prepend(ProductDecorator)
end

装饰器由 config/application.rb 中的 config.to_prepare 块加载(由 Spree 安装程序添加),该块会对 app/**/*_decorator*.rb 执行 glob 匹配——因此 prepend 会在启动时以及每次开发环境重新加载后运行。如果应用中没有此块,请添加它:Zeitwerk 不会自动加载未被引用的装饰器,因此装饰器会静默地不生效。

生成器

spree generate model_decorator Spree::Product
# → server/app/models/spree/product_decorator.rb

spree generate controller_decorator Spree::Api::V3::Admin::ProductsController
# → server/app/controllers/spree/api/v3/admin/products_controller_decorator.rb

(不使用 CLI 时,使用 bin/rails g spree:model_decorator …。)模型生成器会对名称执行 classify,该操作会将名称变为单数形式——对于名称为复数的类(Spree::Exports::Products),请手动编写文件。

模型装饰器模式

添加关联

先创建迁移——Spree 风格是不使用外键约束:

spree generate migration AddBrandIdToSpreeProducts brand_id:bigint:index
class AddBrandIdToSpreeProducts < ActiveRecord::Migration[8.1]
  def change
    add_column :spree_products, :brand_id, :bigint
    add_index :spree_products, :brand_id
  end
end
module Spree
  module ProductDecorator
    def self.prepended(base)
      base.belongs_to :brand, class_name: 'Spree::Brand', optional: true
      base.has_many :videos, class_name: 'Spree::Video', dependent: :destroy
      base.additional_permitted_attributes += [:brand_id]   # writable via Admin API (see below)
    end
  end

  Product.prepend(ProductDecorator)
end

在 Spree 模型中,belongs_to 默认是必需的——添加 optional: true,否则每个现有产品都会报错“Brand must exist”。始终传入 class_name(字符串——延迟解析,可避免加载顺序问题),并在 has_many 上设置 dependent:。

验证、作用域、类方法
module Spree
  module ProductDecorator
    def self.prepended(base)
      base.validates :external_id, uniqueness: { scope: :store_id }, allow_nil: true   # plus a DB index
      base.scope :recently_added, -> { where(created_at: 30.days.ago..) }
      base.scope :tagged_featured, -> { where("metadata->>'featured' = ?", 'true') }  # jsonb on PostgreSQL
      base.extend ClassMethods
    end

    module ClassMethods
      def search_by_name(query)
        where('LOWER(name) LIKE ?', "%#{sanitize_sql_like(query.downcase)}%")
      end
    end
  end

  Product.prepend(ProductDecorator)
end

带有元数据的模型有一个 metadata JSON 列(public_metadata / private_metadata 已不再存在;private_metadata 是已弃用的 Ruby 别名)。metadata 是内部数据,绝不会通过 Store API 暴露;对于店面可见或可筛选的数据,优先使用真实列或自定义字段。JSON 运算符(->>)是 PostgreSQL 语法;MySQL/SQLite 需要使用不同的 SQL。

操作级规则(“每笔订单最多 10 件”“仅限 B2B 客户”)不是模型验证;它们应放在工作流的 validate 钩子中,这样数据迁移和员工修正才不会受阻。不要通过 clear_validators! 放宽核心规则;请参阅 node_modules/@spree/docs/dist/developer/customization/validations.md。

覆盖方法(调用 super)
module Spree
  module ProductDecorator
    def purchasable?
      return false if metadata[:embargoed]
      super
    end
  end

  Product.prepend(ProductDecorator)
end

省略 super 会完全替换原方法。覆盖范围应限于只读或派生方法;任何会写入数据或转移资金的操作都应放在工作流中。

Cart 和 Order:方法位于 Spree::Purchase::* concerns 中

Spree::Cart(结账前)和 Spree::Order(已下单,不可变)通过 concerns 共享行为,包括 Spree::Purchase::Totals、::Addresses、::Taxation、::PaymentProcessing、::CheckoutSteps(仅 Cart)、::Validations 等(spree_core 中的 app/models/concerns/spree/purchase/)。装饰之前,先找到方法的定义位置(Spree::Cart.instance_method(:amount).owner)。

  • 只需更改单一一侧时,装饰 Spree::Cart 或 Spree::Order(前置的模块位于 concern 之前,super 会调用它)。
  • 两侧都要更改时,装饰两个类(或直接前置到 concern:Spree::Purchase::Totals.prepend(MyTotals)——Ruby 3 会将其传播到已包含该 concern 的类)。
  • 大多数你可能想更改的 cart/order 逻辑(添加商品、总计、完成订单)都在工作流中,而非模型中——请使用钩子。
允许写入新的属性
Spree::Product.additional_permitted_attributes += [:brand_id]
Spree::LineItem.additional_permitted_attributes += [:gift_note]
  • 始终使用 +=(或 |=)。不要使用 <<——默认值是冻结数组(会导致 FrozenError);也不要使用 =,否则会覆盖其他扩展添加的内容。
  • 将其放在模型装饰器的 prepended 中(如上所示),或放在 Rails.application.config.to_prepare 块中,以确保引擎模型在开发环境重新加载后仍然有效。
  • API v3 控制器会将此列表与自身列表合并(resource_permitted_attributes);Spree::PermittedAttributes 已不再存在。
  • 行项目:Spree::LineItem.additional_permitted_attributes 是通过加入购物车到达记录的唯一内容——可在 POST /carts/:id/items 请求中作为顶层参数发送,也可放在 options 中(Spree::Carts::AddItem)。options 中的其他内容都会被丢弃;即使将 id、variant_id 和 quantity 加入许可列表,也永远无法通过这种方式设置(这些值已完成校验和定价)。
使其可筛选
# config/initializers/spree.rb
Spree.ransack.add_attribute(Spree::Product, :brand_id)
Spree.ransack.add_association(Spree::Product, :brand)
Spree.ransack.add_scope(Spree::Product, :recently_added)

如果不这样做,q[brand_id_eq]=… 会被静默丢弃(返回 200,但未筛选)。add_association 只会扩展 Admin API:Store API 仅遵循 storefront_ransackable_associations(如果购物者应能按品牌名称筛选,请在同一初始化文件中添加 Spree::Product.storefront_ransackable_associations |= %w[brand]),Seller API 则不遵循任何关联。使用 private_ransackable_attributes({ store: [...], seller: [...] })可对购物者和卖家隐藏仅供员工使用的属性。请参阅 spree-api-v3。

控制器装饰器模式(API v3)

Spree 唯一提供的控制器是 API 控制器——Spree::Api::V3::Store::*、Spree::Api::V3::Admin::*(以及 ::Seller::*)。没有可供装饰的 Rails 管理后台或 Rails 店面。请覆盖 ResourceController 提供的具名钩子——scope、serializer_class、collection_includes、scope_includes、resource_permitted_attributes、model_class——而不是 action。

缩小端点的返回范围
# server/app/controllers/spree/api/v3/admin/products_controller_decorator.rb
module Spree::Api::V3::Admin
  module ProductsControllerDecorator
    protected

    # Chain onto super — it is already store-scoped (and ability-scoped on the
    # Admin API). Replacing it leaks other stores' records.
    # current_user is nil for secret-key (sk_) requests — guard with &.
    def scope
      return super if current_user&.spree_admin?

      super.where(discontinue_on: nil)
    end
  end
end

Spree::Api::V3::Admin::ProductsController.prepend(Spree::Api::V3::Admin::ProductsControllerDecorator)

在 Store API 中,current_user 是已登录的客户(访客为 nil);在 Admin API 中,它是员工(使用密钥请求时为 nil)。基类中的钩子方法为 protected;装饰器中也应保持 protected。

为单个端点更换序列化器
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)

要在所有地方更换,请改用 Spree.api.product_serializer = …(spree-dependencies)。

新操作应使用新控制器

不要通过装饰器向 Spree 的控制器添加操作。为资源控制器创建子类——你将继承分页、Ransack 筛选、带前缀的 ID、错误封装和授权:

# server/app/controllers/spree/api/v3/admin/product_audits_controller.rb
module Spree::Api::V3::Admin
  class ProductAuditsController < ResourceController
    scoped_resource :products   # API-key scope: read_products / write_products (required on Admin controllers)

    protected

    def model_class = MyStore::ProductAudit
    def serializer_class = MyStore::ProductAuditSerializer
    def resource_permitted_attributes = [:note]
  end
end
# config/routes.rb
Spree::Core::Engine.add_routes do
  namespace :api, defaults: { format: 'json' } do
    namespace :v3 do
      namespace :admin do
        resources :product_audits
      end
    end
  end
end

spree generate api_resource 会搭建模型、序列化器、控制器和路由,详见 spree-resource。

API 装饰器必须渲染 JSON(render_error / 标准错误封装),绝不能使用 flash 或重定向。

常见问题

  • 忘记调用 super——会悄无声息地替换 Spree 的逻辑。
  • 在 prepended 中使用实例变量(@setting = true)——它们属于模块,而非实例;请使用 base.class_attribute :setting, default: true。
  • 在启动时引用常量——has_many :variants 若未指定 class_name: 'Spree::Variant',可能会遇到加载顺序错误。
  • 文件与模块不匹配——Spree::ProductDecorator 必须放在 app/models/spree/product_decorator.rb 中。
  • 为结账行为装饰 Spree::Order——结账前的状态属于 Spree::Cart;订单只会在 Spree::Carts::Complete 之后创建。
  • belongs_to 未设置 optional: true——现有行可能会出现“必须存在”错误。
  • Spree.user_class——已弃用;请装饰 Spree.customer_class(默认值为 Spree::Customer)或 Spree.admin_user_class(Spree::AdminUser)。

组织多个装饰器

server/app/models/spree/
├── product_decorator.rb        # the only file that calls prepend
└── product/
    ├── brand_decorator.rb      # plain modules
    └── seo_decorator.rb
module Spree
  module ProductDecorator
    include Product::BrandDecorator
    include Product::SeoDecorator
  end

  Product.prepend(ProductDecorator)
end

Zeitwerk 会根据路径解析嵌套模块——无需使用 require / require_dependency(Zeitwerk 模式下已移除)。请注意,嵌套文件也符合 *_decorator*.rb glob,因此它们不能自行调用 prepend。

迁移回调装饰器

添加 after_save :sync_to_erp 的装饰器应改为订阅者:

# server/app/subscribers/product_sync_subscriber.rb   (spree generate subscriber ProductSync)
class ProductSyncSubscriber < Spree::Subscriber
  subscribes_to 'product.updated'

  def handle(event)
    ExternalSyncJob.perform_later(event.payload['id'])
  end
end

载荷是序列化后的记录(带前缀的 id),而不是变更集。注册和异步行为详见 spree-events-webhooks。在操作中触及金额、库存或外部系统的回调,应改用工作流钩子。

延伸阅读

  • node_modules/@spree/docs/dist/developer/customization/decorators.md(https://spreecommerce.org/docs/developer/customization/decorators)
  • node_modules/@spree/docs/dist/developer/customization/validations.md — 钩子、装饰器验证与注册表的比较
  • node_modules/@spree/docs/dist/developer/customization/api.md — 自定义 API 端点
  • node_modules/@spree/docs/dist/developer/tutorial/model-and-api.md — 产品品牌配置演练
  • 相关技能:spree-workflows、spree-dependencies、spree-events-webhooks、spree-resource、spree-customization

评论

支持 Markdown