关于 spree-providers
面向 Spree 6 开发者的外部系统集成指南,介绍如何通过 Provider 合约和注册表扩展电商功能,并涵盖配置、凭据管理与实现要求。
主要功能
- 01各类电商功能的 Provider 合约
- 02注册表配置与时机说明
- 03按店铺管理集成凭据
- 04实现接口参考
- 05常见集成问题
适用场景
- 01添加税务或支付 Provider
- 02接入配送报价或履约服务
- 03集成外部搜索或卖家结算
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ spree/ agent-skills --skill spree-providers安装后在 Claude Code 中输入/spree-providers确认已加载。
详细介绍
Spree 提供方 — 接入外部系统
Spree 6 通过提供方契约连接外部系统:继承基类、向注册表添加实现,再由商家(或配置)选择提供方。每种契约都有一个使用 Spree 自有数据的内置默认实现,因此不接入任何系统也不会改变现有行为。你无需分叉或装饰核心流程。
以下路径使用
create-spree-app项目的server/前缀;在传统 Rails 应用中请去掉该前缀。
一览
| 关注点 | 基类 | 注册 | 选择方式 | 默认值 |
|---|---|---|---|---|
| 税费 | Spree::TaxProvider::Base | Spree.tax_providers << MyTax | market.tax_provider,否则设置 Spree.default_tax_provider = | Spree::TaxProvider::Internal |
| 配送费率 | Spree::DeliveryRateProvider::Base | Spree.delivery_rate_providers << MyRates | delivery_method.rate_provider | …::Internal(计算器)、…::Freight |
| 履约 / 标签 / WMS | Spree::FulfillmentProvider::Base | Spree.fulfillment_providers << MyWms ⚠ after_initialize | delivery_method.fulfillment_provider | …::Manual、Digital、Pickup、PickupPoint |
| 付款 | Spree::PaymentMethod(或 Spree::Gateway)+ Spree::PaymentSession STI | Spree.payment_methods << MyGateway ⚠ after_initialize | 付款方式记录(设置 → 付款) | Check、StoreCredit、Bogus |
| 搜索 | Spree::SearchProvider::Base | Spree.search_provider = 'My::Provider'(全局唯一) | — | Spree::SearchProvider::Database |
| 卖家结算 | Spree::PayoutProvider::Base | Spree.payout_providers << MyPayouts | 商店偏好设置 payout_provider,否则使用 Spree.default_payout_provider | Spree::PayoutProvider::System |
| 数字交付物 | Spree::DigitalAssetProvider::Base | Spree.digital_asset_providers << 'My::Provider' | digital_asset.provider_type | …::File |
| 身份验证策略(SSO、外部 JWT) | Spree::Authentication::Strategies::BaseStrategy / OidcStrategy.configure(...) | Spree.{store,admin,seller}_authentication_strategies.add(:key, klass) ⚠ after_initialize | 登录请求(provider 参数) | :email(EmailPasswordStrategy) |
| 上述各项的凭据 | Spree::Integration | Spree.integrations << 'My::Integration' ⚠ after_initialize | 每个商店单独设置,设置 → 集成 | — |
| 可观测性 | —(gem) | gem 'spree_opentelemetry' + OTEL_* 环境变量 | — | 未启用 |
交易邮件不是提供方。 Spree 通过纯 SMTP 发送交易邮件,SMTP 使用整个部署环境的 SMTP_* 环境变量配置,而不是在管理后台按商店配置;也无需安装提供方 gem。任何 SMTP 服务都可使用(Resend、Postmark、SendGrid、Mailgun 和 Amazon SES 均提供设置指南)。请使用 STARTTLS 端口(587);隐式 TLS 端口(如 465)无法连接。Spree 不会处理退信或投诉,因此请通过提供方自有的 Webhook 处理。参见 spree-deployment。
相关扩展点虽不是“提供方”,但使用方式相同:Spree.order_routing.rules / .strategies(基类为 Spree::OrderRoutingRule、Spree::OrderRouting::Strategy::Base)、Spree.stock_splitters ⚠、Spree.number_generators[:order] = 'My::Numbers'、Spree.promotions.rules / .actions ⚠、Spree.adjusters ⚠、Spree.tracking_carriers['my_courier'] = { name:, url: } ⚠。参见 spree-fulfillment、spree-promotions、spree-order-totals。
即将推出(路线图,接口可能变更): ERP 库存同步、PIM 定价数据源、外部托管的 DAM 媒体。核心代码已包含 Spree::PricingProvider::Base / Spree::InventoryProvider::Base,以及 Spree.pricing_providers / Spree.inventory_providers 和商店偏好设置 pricing_provider / inventory_provider(默认值为 'internal'),但文档将它们标记为路线图内容——在查看当前源码前,请勿基于它们构建生产集成。
⚠ 注册时机
Core 会在其自身的 config.after_initialize 中赋值 payment_methods、fulfillment_providers、integrations、stock_splitters、adjusters、promotions.actions、tracking_carriers 以及三个身份验证注册表。在此之前追加的任何内容——无论是在 config/initializers/spree.rb 顶部,还是在 to_prepare 中(启动时它会先于 after_initialize 运行)——都会被静默清除。其他提供方注册表会较早初始化并进行拼接,因此可在任意时机注册。统一规则:在 after_initialize 中注册每个提供方。
# server/config/initializers/spree.rb
Rails.application.config.after_initialize do
Spree.integrations << 'MyApp::AcmeIntegration'
Spree.delivery_rate_providers << MyApp::AcmeRates
Spree.fulfillment_providers << MyApp::AcmeFulfillment
Spree.payment_methods << MyApp::AcmeGateway
end
Spree.search_provider = 'MyApp::SearchProvider::Typesense' # plain assignment, any time
在 gem 中,应在引擎内使用 config.after_initialize(参见 spree-extensions)。配置错误的表现:控制面板选择器中找不到该提供方,或保存失败并出现 "must be registered"/包含性错误——注册表也兼作验证允许列表(delivery_method.rate_provider、market.tax_provider、digital_asset.provider_type、integration.type 都会根据这些注册表进行验证)。
凭据:Spree::Integration
每个店铺的凭据绝不能放在 ENV 中——多店铺或市场应用的每个店铺都有不同的账户。应提供一个集成:
module MyApp
class AcmeIntegration < Spree::Integration
preference :api_key, :password # :password = masked on read, guarded on write
preference :test_mode, :boolean, default: true
def self.integration_group = 'shipping' # gallery grouping
# Called when the merchant activates it; false blocks activation with the message.
def can_connect?
client.ping
true
rescue StandardError => e
self.connection_error_message = e.message
false
end
def client = @client ||= Acme::Client.new(api_key: preferred_api_key)
end
end
提供方通过 def self.integration_class = 'MyApp::AcmeIntegration' 与其关联(配送费率、履约、定价、库存提供方包含 Spree::IntegrationBackedProvider):每个店铺是否可用由此决定,而 integration 会返回该店铺的活动记录。税务和付款提供方会自行重写 self.available_for_store?(store)。
合约速查表
税务(spree-taxes)— estimate(owner, items = nil, tax_date:, tax_identifier:, exemptions:, context:) 会以逐项全量替换的语义写入 Spree::TaxLine 记录;commit(order)、void(order)、refund(order, return_items, amount:, tax_date:) 除非接入远程账本,否则均为空操作。类级方法包括 display_name、available_for_store?、unsupported_capabilities。提供方为无状态对象,无需参数即可创建。
配送费率(spree-fulfillment)— estimates(package) → Spree::DeliveryRateProvider::Estimate.new(cost:, currency:, carrier:, service_level:, estimated_delivery_date:, metadata:) 对象数组,或返回单个报价的 estimate(package)。nil/[] 会隐藏该配送方式。绝不要抛出异常——捕获异常,调用 Rails.error.report,并返回 []。cost 为税前金额。使用 Spree::Current.provider_cache[key] ||= 按请求缓存。可选的 self.service_catalog(integration) → ServiceCatalog.listing/none/unavailable。book/release 已存在,但 Core 尚未调用。
履约 — 类谓词:generates_labels?、digital?、pickup?;实例方法:can_fulfill?、auto_fulfill?、requires_address?、serves_location?(delivery_method, stock_location)、purchase_label(owner)、refund_label(label)、create_fulfillment(fulfillment)(幂等)、cancel_fulfillment、tracking_url、documents。生成面单的提供方会在履约标记为已发货之前购买面单。参考实现:spree_easypost(SpreeEasyPost::DeliveryRateProvider、SpreeEasyPost::FulfillmentProvider、SpreeEasyPost::Integration)。
支付(spree-payments)— 继承 Spree::PaymentMethod,使用匹配的 Spree::PaymentSession STI 子类(也可选用 Spree::PaymentSetupSession)实现支付会话方法,并处理 Webhook。参考实现:SpreeStripe::Gateway。
搜索 — search_and_filter(scope:, query:, filters:, sort:, page:, limit:)、filters(...)、index(product)、remove(product)、remove_by_id(prefixed_id)、index_batch(documents)、reindex(scope)、self.indexing_required?。文档结构由 Spree::Dependencies.search_product_presenter 提供(spree_meilisearch gem 会注册 SpreeMeilisearch::ProductPresenter)。Meilisearch:gem 'spree_meilisearch'、MEILISEARCH_URL / MEILISEARCH_API_KEY、Spree.search_provider = 'SpreeMeilisearch::SearchProvider',然后运行 spree task search:reindex。
付款(spree-marketplace)——transfer!(seller_transfer)、pay!(seller_payout)(将你的 ID 存储在 reference 中,不要标记为已完成——通过 Webhook 和 Spree.seller_payout_complete_workflow 确认)、reverse!(seller_transfer);入驻:onboarding_url、onboarded?、onboarding_state、onboarding_message;类方法:display_name、reference_system、requires_payout_account?、available_for_store?。不要覆盖 provider_key。超时时抛出 Spree::Core::AmbiguousGatewayError,明确失败时抛出 Spree::Core::GatewayError。参考:SpreeStripe::PayoutProvider(Stripe Connect)。
数字资产——deliver(digital_link, expires_in:) 返回 Spree::DigitalDelivery.new(redirect_url:) 或 .new(inline_value:, content_type:);返回空值表示拒绝交付,且不消耗客户的下载额度。通过 setting :pool_name, :string 设置每项资产的配置(会显示为控制面板表单);对于生成的交付内容,self.requires_attachment? 为 false。
单点登录 / 外部身份(spree-auth-permissions)——OpenID Connect 已内置于核心中:
Rails.application.config.after_initialize do
Spree.admin_authentication_strategies.add(
:entra,
Spree::Authentication::Strategies::OidcStrategy.configure(
issuer: 'https://login.microsoftonline.com/<tenant>/v2.0',
client_id: ENV['ENTRA_CLIENT_ID'],
client_secret: ENV['ENTRA_CLIENT_SECRET'],
redirect_uri: 'https://shop.example.com/api/v3/admin/auth/callback/entra',
label: 'Microsoft Entra ID'
)
)
end
适用于任何 OIDC 发行方(Okta、Google Workspace、Keycloak、Auth0)。使用 .remove(:email) 移除密码登录。对于非 OIDC JWT 发行方,可继承 BaseStrategy(authenticate、provider、success(user)、failure(msg)、find_or_create_user_from_oauth)。
可观测性(spree-deployment、spree-performance)——gem 'spree_opentelemetry',设置 OTEL_SERVICE_NAME + OTEL_EXPORTER_OTLP_ENDPOINT;OTEL_SDK_DISABLED=true 可将其关闭。为每次工作流运行 / 步骤 / 钩子、事件分发、Webhook 投递和网关调用生成 Span。使用 SpreeOpenTelemetry.configure { |c| c.use/skip/with_sdk } 调整配置。Sentry 可用作导出器(OTEL_TRACES_EXPORTER=none)。
注意事项
- 费率和履约服务提供方绝不能导致结账失败。 它们的调用在工作流中运行——应捕获供应商错误并降级处理(隐藏选项、回退),而不是抛出错误,除非契约要求抛出网关错误。
- 税务服务提供方失败时应采取封闭策略。 如果
estimate无法计算完整的Spree::TaxLine行,就抛出错误——重新计算(或完成操作)会中止,而不会在缺少税额的情况下创建订单。只有在回退方案(例如Spree::TaxProvider::Internal)会写入完整且可审计的税额行时,才回退。 - 无状态服务提供方。 它们不带参数实例化(配送费率服务提供方会接收
delivery_method);任何请求特定的数据都作为参数传入。不要在类上缓存每个店铺的数据。 - 注册表中的类名在开发环境重新加载后仍有效——核心代码会在重新加载时重新解析
delivery_rate_providers/fulfillment_providers;注册表接受字符串条目时('My::Provider'),这是最能适应重新加载的形式。 - 移除 gem 会留下指向未注册类的记录。这些记录会保留(验证只在变更时运行),而支付服务提供方消失的店铺会回退到内置服务提供方——但应清理引用它的配送方式/市场。
- 传入的服务提供方 Webhook 会在记录所属的店铺中运行。
POST /api/v3/webhooks/{payments,payouts}/:payment_method_id和/webhooks/fulfillments/:integration_id仅通过带前缀的 ID 查找支付方式/集成(不需要 API 密钥或店铺标头),并将Spree::Current.store设为其所属店铺;Spree::Payments::HandleWebhookJob也会执行相同操作。针对该记录自身密钥的签名校验是唯一的身份验证方式——绝不能跳过;未配置密钥时应抛出错误,而不是直接返回。还必须在此处执行服务提供方的时效规则(带签名的时间戳 + 容差,例如 Stripe 在construct_event中的 5 分钟窗口);核心支付处理对支付本身是幂等的,但你编写的任何处理程序都必须根据服务提供方的稳定键(事件或对象 ID)保持幂等,以便重复投递时不产生任何操作。 - 税务服务提供方 ≠ 税率。 内置服务提供方使用区域/税率;外部引擎会替换每个市场的计算方式。已创建的订单金额会被冻结——服务提供方的
commit/refund会处理远程账本。
延伸阅读
- 概览:
node_modules/@spree/docs/dist/developer/providers/overview.md - 操作指南:
node_modules/@spree/docs/dist/developer/how-to/{custom-delivery-rate-provider,custom-payment-method,custom-search-provider,custom-digital-asset-provider,custom-api-authentication,custom-order-routing,custom-stock-splitter,custom-document-numbers}.md - 服务提供方:
node_modules/@spree/docs/dist/developer/providers/{fulfillment,payouts,sso,observability,erp,pim,dam}.md - 集成目录:https://spreecommerce.org/docs/integrations(支付:Stripe、Adyen、PayPal、Razorpay;市场平台:Stripe Connect;配送:EasyPost;税务:Avalara;搜索:Meilisearch;营销/分析:Klaviyo、GA、GTM)——设置指南位于
node_modules/@spree/docs/dist/integrations/<category>/<name>.md - 相关技能:
spree-extensions、spree-taxes、spree-fulfillment、spree-payments、spree-marketplace、spree-auth-permissions、spree-customization