关于 spree-customization
spree-customization 是面向 Spree 6 开发者的技能,帮助选择扩展点而不分叉核心代码。它将常见改动对应到设置、钩子、事件、提供程序、API 资源等定制方式。
主要功能
- 01为改动选择扩展点
- 02用工作流钩子校验或调整流程
- 03通过订阅器或 Webhook 响应事件
- 04配置提供程序和认证策略
- 05扩展资源、仪表盘界面和模型
适用场景
- 01在结账时执行策略检查
- 02将订单事件发送到外部系统
- 03添加 API 资源或仪表盘功能
- 04用自定义行为替换核心工作流
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ spree/ agent-skills --skill spree-customization安装后在 Claude Code 中输入/spree-customization确认已加载。
详细介绍
Spree 自定义——我的代码该放在哪里?
Spree 6 的设计允许你在不分叉代码库的情况下扩展功能。难点在于选择合适的模式。按照表格从上往下查看:越靠前的选项越偏向数据或配置,编写成本更低,也不会在升级时受到影响;越靠后的选项与 Spree 内部实现的耦合越紧密。
命令使用
spreeCLI 格式。在经典 Rails 应用中,请在应用根目录运行bin/rails …,并去掉server/路径前缀——参见spree-project。
决策树
| 你想…… | 使用 | 技能 |
|---|---|---|
| 更改货币、市场、语言、配送区域/方式、税率、支付方式、订单编号 | 商店设置 — 仪表盘 → Settings,或 Admin API。数据更改,无需部署 | spree-dashboard、spree-api-v3 |
| 调整全站安装级别的限制(密码长度、JWT 过期时间、速率限制、购物车过期时间、Webhook 开关) | SPREE_* 环境变量(启动时进行类型检查和验证);Spree.config { |c| … } 在 config/initializers/spree.rb 中仍然有效 | —(文档:customization/configuration) |
为某种记录类型设置专属的类型化设置(例如 Brand 的“featured”标志、计算器的费率) | 模型偏好设置 — 在带有 preferences 列的模型上使用 preference :featured, :boolean, default: false | spree-resource |
| 否决某项操作(购买限制、地区政策、B2B 资格) | 工作流 validate 钩子 — Spree.hooks.register('carts.add_item.validate', 'MyApp::CheckLimit'),处理器调用 workflow.reject! | spree-workflows |
| 在核心流程中运行代码(完成后、取消前,提供定价/服务商上下文) | 工作流生命周期/上下文钩子 — carts.complete.after_finalize、orders.cancel.before_cancel、set_*_context | spree-workflows |
| 在自有应用中某件事发生后作出响应(索引、同步、发送邮件、审计) | 事件订阅者 — 监听 order.placed、product.updated 等事件的 Spree::Subscriber | spree-events-webhooks |
| 无需 Ruby 代码即可告知外部系统发生了某件事 | Webhook 端点 — 仪表盘 → Settings → Webhooks,或 Admin API | spree-events-webhooks |
| 要求结账完成前提供某些信息(PO 编号、电话号码、年龄验证),或添加结账步骤 | Spree::Checkout::Registry.add_requirement / register_step — 会显示在 Cart API 的 requirements[] 中 | spree-checkout |
| 替换税费、配送费率、履约、搜索、定价、库存、付款结算、数字商品配送或支付的工作方式 | 服务商 — 注册一个类(Spree.tax_providers、Spree.delivery_rate_providers、Spree.search_provider = …、Spree.payment_methods 等),并在商店/市场/方式上选择该类 | spree-providers、spree-taxes、spree-payments |
| 使用自有 IdP 或令牌为顾客/员工登录 | 身份验证策略 — Spree.store_authentication_strategies.add(:idp, MyApp::Auth::IdpStrategy)(也适用于 admin_/seller_) | spree-auth-permissions |
| 存储面向商家的额外数据(材质、护理说明、礼品留言) | 自定义字段 — CustomFieldDefinition + record.set_custom_field('custom.material', 'Cotton');类型化、可筛选,并支持 storefront_visible | spree-catalog |
| 存储集成/机器数据(ERP ID、UTM 来源、同步状态) | metadata — 无模式 JSON,采用合并语义;Store API 中只写入,Admin API 中可读取 | spree-api-v3 |
使某个属性可通过 q[...] 筛选/排序 | Spree.ransack.add_attribute / add_association / add_scope | spree-api-v3 |
| 添加或移除地址规则(PO 盒、电话号码格式) | Spree.validators.addresses.register / unregister(在 config.to_prepare 中调用) | spree-customization(见下文) |
| 允许通过 Admin API 写入新列 | Spree::Product.additional_permitted_attributes += [:brand_id] | spree-resource |
| 将新资源限制为特定员工角色/API 密钥权限范围可用 | Spree.permissions.register_scope(:brands, group: :catalog, resources: -> { [Spree::Brand] }) → read_brands / write_brands | spree-auth-permissions |
| 添加全新的模型和 Admin/Store 端点 | spree generate api_resource Brand … | spree-resource |
| 为核心模型添加状态 | Spree::Return.add_status('on_hold', after: 'approved') + 自有工作流,用于将记录转换为该状态 | spree-workflows |
| 从根本上更改核心流程的行为 | 依赖项替换 — Spree::Dependencies.cart_add_item_workflow = 'MyApp::Carts::AddItem' | spree-dependencies |
| 为核心模型添加关联、作用域或辅助方法 | 装饰器(spree generate model_decorator Spree::Product)— 最后手段 | spree-decorators |
| 添加管理界面、字段、列、导航项 | 在 apps/dashboard/src/plugins.ts 中使用 Dashboard 插件(defineDashboardPlugin) | spree-dashboard、spree-dashboard-plugins |
| 在多个应用之间共享上述任意扩展 | 扩展 gem(Rails 引擎)+ 可选的 Dashboard 插件(spree plugin new) | spree-extensions |
Hook、事件还是依赖项?
- 必须阻止流程或改变结果吗? → 工作流 Hook。只有
validate(和before_cancel)Hook 可以停止流程,而且它们会在资金流动前运行。 - 只需要知道某件事已经发生吗? → 事件订阅器(应用内)或 Webhook(外部)。彼此解耦,默认异步处理,升级时无需改动。
- 流程必须发生根本性变化吗? → 替换依赖项,并接受升级时你需要自行维护这一类代码。
实例
阻止特定地区的结账 — validate Hook
# server/config/initializers/spree.rb
Spree.hooks.register('carts.complete.validate', 'MyApp::RegionPolicy')
# server/app/services/my_app/region_policy.rb
module MyApp
class RegionPolicy
def call(workflow)
return unless workflow.cart.ship_address&.country_code == 'KP'
workflow.reject!('We cannot deliver to this region')
end
end
end
启动时会验证 Hook 键(拼写错误会触发 Spree::Hooks::UnknownHookError)。处理器以类名字符串的形式存储,因此注册可安全用于重新加载。多个处理器可以共享一个键——第一个调用 reject! 的处理器生效。
将已下单订单推送到 ERP — 事件订阅器
spree generate subscriber ErpOrderSync order.placed # creates the class AND registers it
# server/app/subscribers/erp_order_sync_subscriber.rb
class ErpOrderSyncSubscriber < Spree::Subscriber
subscribes_to 'order.placed'
def handle(event)
order = Spree::Order.find_by_prefix_id(event.payload['id'])
MyApp::Erp.push(order) if order
end
end
订阅器不会自动发现——如果不使用生成器,请自行添加 Spree.subscribers << ErpOrderSyncSubscriber。
要求 B2B 购物车填写 PO 编号 — 结账要求
Spree::Checkout::Registry.add_requirement(
step: :payment,
field: :po_number,
message: 'PO number is required',
satisfied: ->(cart) { cart.metadata['po_number'].present? },
applicable: ->(cart) { cart.company.present? }
)
此要求会显示在 Cart API 的 requirements 数组中,因此店面无需重复规则即可将其呈现出来,而 Carts::Complete 会在要求得到满足前拒绝完成结账。
为商品添加 brand_id — 资源 + 允许的属性 + 装饰器
spree generate api_resource Brand name:string
spree generate migration AddBrandIdToSpreeProducts brand_id:bigint:index
spree migrate
# server/config/initializers/spree.rb
Rails.application.config.to_prepare do
Spree::Product.additional_permitted_attributes += [:brand_id] # += — the default is frozen
Spree::Product.storefront_ransackable_associations |= %w[brand] # Store API filters follow only this list
end
Spree.ransack.add_association(Spree::Product, :brand) # Admin API filters
Spree.permissions.register_scope(:brands, group: :catalog, resources: -> { [Spree::Brand] })
# server/app/models/spree/product_decorator.rb — structural addition, the one legit decorator job
module Spree
module ProductDecorator
def self.prepended(base)
base.belongs_to :brand, class_name: 'Spree::Brand', optional: true
end
end
Product.prepend(ProductDecorator)
end
如果数据可由商家编辑,且不需要单独的表,那么使用自定义字段比上述所有做法都省事。
放宽已发布的地址规则 — 验证器注册表
# server/config/initializers/spree.rb
Rails.application.config.to_prepare do
Spree.validators.addresses.register(MyApp::PostBoxValidator) # an ActiveModel::Validator
Spree.validators.addresses.unregister(Spree::Addresses::PhoneValidator)
end
有几条规则也是普通的商店偏好设置(address_requires_phone、company_field_enabled、disable_sku_validation)——请先检查这些设置。其他模型没有通用的验证器注册表:通过装饰器添加模型验证;使用 validate Hook 否决操作。
替换购物车添加商品逻辑 — 替换依赖项
Spree::Dependencies.cart_add_item_workflow = 'MyApp::Carts::AddItem' # subclass Spree::Carts::AddItem
工作流的键以 _workflow 结尾(carts_complete_workflow、order_cancel_workflow、cart_recalculate_totals_workflow、fulfillment_update_workflow,……);普通服务的键以 _service 结尾(order_update_statuses_service)。如果只需添加行为,优先使用 Hook。
反模式
- 在装饰器中重写
finalize!、after_finalize、取消操作或其他生命周期方法 → 注册 Hook(carts.complete.after_finalize、orders.cancel.after_cancel)。已没有可挂接的状态机——state_machine块、after_transition和checkout_flow均已不存在。 - 为副作用向 Spree 模型添加
after_save/after_commit回调 → 使用事件订阅器。 clear_validators!→ 清除模型上的所有验证,包括其他扩展添加的验证。请使用商店偏好设置、Spree.validators.addresses.unregister或针对性装饰器。Spree::Product.additional_permitted_attributes << :brand_id→ 会引发FrozenError。始终使用+=。- 写入旧版
*_service依赖键(cart_add_item_service、carts_complete_service)→ 写入会被暂存并忽略,同时显示警告;请使用*_workflow键。 - 直接设置
payment_status/fulfillment_status→ 这些状态由Spree::Orders::UpdateStatuses派生;请改为操作付款/履行记录。 - 为新状态字段使用枚举或状态机 →
include Spree::HasStatus; has_status :a, :b, default: :a,并在工作流中定义状态转换。 - 通过 Rails 视图/局部模板修改管理后台 → 管理后台是 React 仪表板;请使用仪表板插件。
- 为单个应用创建私有扩展 gem → 将代码放在
server/app/;gem 用于在多个应用之间共享。 - Fork Spree → 先查阅此表;几乎可以肯定已有合适的扩展点。
延伸阅读
node_modules/@spree/docs/dist/developer/customization/quickstart.md— 官方推荐的规范阅读顺序…/customization/{configuration,workflows,dependencies,decorators,validations,metadata,model-preferences,permissions,checkout,api}.md…/providers/overview.md和每种提供方契约对应的…/how-to/custom-*.md…/dashboard/customization/quickstart.md— 管理界面- 在线文档:https://spreecommerce.org/docs/developer/customization/quickstart