Spree 6.0 Store-Scoped Configuration把商业行为配置从全局 Spree::Config 迁往 Store 偏好的完整实战指南【免费下载链接】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/spreeSpree 6.0 将八项「商业行为」全局配置扣款时机、库存跟踪、价格历史等从Spree::Config迁移到Spree::Store偏好之上并废弃了一批无人读取的死设置。本文基于 docs/plans/6.0-store-scoped-configuration.md 的设计决策结合仓库源码StorePreferencesconcern、CaptureMethodconcern、store_settings.rake回填任务等逐层拆解迁移动机、核心取舍与升级路径帮助你在多店铺multi-store场景下正确使用新的 Store 级偏好并安全完成 5.6 → 6.0 的升级迁移。背景为什么 Spree 6.0 要重构配置体系Spree 的配置历史上集中在Spree::Config其底层实现在 spree/core/lib/spree/core/configuration.rb中。2026-08-04 对全部 68 个偏好做了一次带对抗性验证的完整使用审计结论是38 个真实活跃、6 个只有 6.0 已替换掉的旧子系统还在读、7 个全仓库零读取。更关键的发现是这些活跃设置天然分成两类应用配置application configuration——安全、限额、后台任务、URL 等描述的是「这套安装」的属性。例如密码长度它保护的是应用而不是某个店铺。商业行为配置commerce behavior——扣款时机、库存跟踪、目录可见性等描述的是「某个店铺怎么卖货」的属性。在 6.0 之前Product、Promotion、PaymentMethod、StockLocation 已经陆续变成单店single-store资源见 6.0-channels-catalogs-b2b.md此时再让「按店而异的商业行为」由全局标志控制就成了架构上的坏味道。此前company→company_field_enabled、allow_guest_checkout→guest_checkout已经开了头本次计划把剩下的八项商业行为全局配置一并迁到Spree::Store偏好并淘汰掉那些死设置。核心问题两个真相来源必然产生漂移整个计划由一个具体 bug 触发default_stock_reservation_ttl_minutes这个全局配置只在 Store 对应偏好为空时才会被读取。但Store#stock_reservation_ttl_minutes声明了default: 10且带greater_than: 0校验所以它永远不会为空——全局配置实际上对所有带 store 的订单都静默失效了。由于两处默认值恰好都是 10没有任何人察觉。这就是「同一行为存在两个真相来源」的后果一个悄悄生效另一个成为文档里的谎言。同样的形状也在别处出现——auto_capture既存在于全局又以列的形式存在于每个支付方式上而 Dashboard 只暴露列。分类测试一个设置该留在全局还是迁到 Store判断标准非常简单一句话同一套安装上的第二个店铺是否会合理地想要不同的值会→ 应该是 Store 偏好。例如欧盟店铺需要track_price_history满足欧盟 Omnibus 指令其非欧盟姊妹店铺不需要。不会→ 留在Spree::Config。例如密码长度保护的是应用本身而不是某个店铺的销售方式。八个迁移项一览下表来自计划文档名称与默认值在 Store 上保持不变唯二例外是合并后的两个 capture 布尔见下文全局配置今日Store 偏好默认值auto_captureauto_capture_on_dispatchpreferred_capture_methodcheckouttrack_inventory_levelspreferred_track_inventory_levelstruestock_reservations_enabledpreferred_stock_reservations_enabledtruetrack_price_historypreferred_track_price_historytrueshow_products_without_pricepreferred_show_products_without_pricefalseaddress_requires_phonepreferred_address_requires_phonefalsedisable_sku_validationpreferred_disable_sku_validationfalse这些偏好都已在 spree/core/app/models/spree/store.rb 中声明例如preference :track_inventory_levels, :boolean, default: truestore.rb第 118 行、preference :capture_method, :string, default: Spree::CaptureMethod::DEFAULT_CAPTURE_METHOD第 102 行、preference :disable_sku_validation, :boolean, default: false第 123 行等并有validates :preferred_capture_method, inclusion: { in: Spree::CaptureMethod::CAPTURE_METHODS }第 336 行这类取值校验。关键决策与设计取舍1.capture_method两个布尔合并为一个三值字符串最初计划按「一全局一偏好」逐个搬运但对 capture 这一对做了例外2026-08-13 决定。原因在于auto_captureauto_capture_on_dispatch两个布尔用四种组合编码三种真实行为其中一种组合自相矛盾两者都开结账时钱已经扣走发货时的 capture 是死操作no-op更糟的是第三种真实行为——结账时仅授权、由员工稍后手动收款——只能表达为「两个都关」没有任何商家能自己发现这个用法。于是合并为单一字符串偏好词汇表是checkout | on_dispatch | manual定义在 spree/core/app/models/concerns/spree/capture_method.rbCAPTURE_METHODS %w[checkout on_dispatch manual]默认checkout与 Shopify 的三选项支付扣款设置一致。这样自相矛盾的组合变得不可表达manual 模式则变得显式可见。checkout下单即扣款on_dispatch结账时仅授权发货时扣款manual结账时仅授权留给员工手动收款。对应的语义提问方法也在该 concern 中capture_at_checkout?、capture_on_dispatch?、capture_manually?。2. PaymentMethod 上是列不是偏好auto_capture原本是支付方式表里的布尔列与active、position并列替换它的capture_method也保持为列而不是偏好。理由写在迁移文件 spree/core/db/migrate/20260813130001_add_capture_method_to_payment_methods.rb 中支付方式上的 preferences 哈希存放的是各家网关的凭证把核心设置放进去会把它渲染成 provider 配置表单里的凭证字段还不得不在PreferenceSchema里加排除清单——需要逃生舱本身就说明存储位置错了。列还可以被 SQL 查询逐支付的 dispatch 检查正需要null则天然承载「继承 store 选择」的语义与Spree::Channel::Gating的 nullable 继承如出一辙。在 Store 上它仍是偏好因为 Store 的所有设置都以偏好形式存储。解析链spree/core/app/models/spree/payment_method.rb 的resolved_capture_method第 239-244 行def resolved_capture_method return capture_method if capture_method.present? # 1. 方法列优先 return checkout if auto_capture # 2. 旧布尔列兼容 store_preference(:capture_method).presence || Spree::CaptureMethod::DEFAULT_CAPTURE_METHOD end # 3. store 偏好 → 声明默认值auto_capture?保留为不告警的弃用提问方法它跑在每一笔支付上告警会刷爆日志。3. dispatch 扣款按支付方式逐笔判定而非店铺级一刀切spree/core/app/models/spree/fulfillment.rb 的payments_to_capture_on_dispatch第 484-488 行只挑出「支付方式解析为on_dispatch」的待处理付款def payments_to_capture_on_dispatch pool grouped_owner? ? owner.settlement_payments.pending : Array(owner.pending_payments) Array(pool).select { |payment| payment.payment_method.capture_on_dispatch? } end随后process_order_payments第 490-514 行按未扣金额从大到小排序、按发货价逐笔 capture。选择manual的支付方式永远不会被发货动作扫走——这正是商家选择 manual 的全部意义。发货就绪判定同理当某个支付方式故意延迟收款时已授权未扣款的订单可以放行发货而不是被一个店铺级布尔拦住。4. Store 偏好是唯一权威不做运行时回退核心代码只读 store 偏好 其声明默认值绝不回退读旧全局。回退链会重新引入让default_stock_reservation_ttl_minutes不可达的那类漂移。company先例已经如此工作。5. 无 store 的读取Spree::Current.store 声明默认值有两个读取者没有记录级 store 可问Address无 store 关联和 Product 可用性 scope类级。它们都通过Spree::Current.store解析。已接受的权衡是在请求之外运行的校验或目录查询控制台、seed、忘记设置 store 的后台任务会静默使用默认值而非该 store 的值。因此计划明确要求任何校验地址或查询目录的后台任务都必须设置Spree::Current.store。6. 其余关键决策摘要default_stock_reservation_ttl_minutes是弃用而非迁移Store#stock_reservation_ttl_minutes早已持有该值StockReservation.ttl_for去掉全局读取对无 store 场景保留硬性的 10 分钟下限。track_price_history6.0 先落在 StoreMarket 记为未来精化项欧盟 Omnibus 是分国立法Market 已拥有return_window_days等法律类设置但价格目前未按 Market 划分留待价格/Market 作用域设计时再议。看似行为型但留在全局的credit_to_new_allocation账本形态约定按店差异会让一套安装的记账口径不一致、non_expiring_credit_types参考数据已在 store credit 分类移除时整体废弃、geocode_addresses地理编码是基础设施依赖安装的 provider 凭证与配额与店铺怎么卖无关。allow_checkout_on_gateway_error直接丢弃而非迁移Spree 6 中没有任何代码读取它且Carts::Complete#process_payments与Orders::Complete#process_payments已改为检查支付是否覆盖总额Orders::Complete还会在 gateway 错误时向errors追加消息并失败——开着它订单也完不成放进 Dashboard 只会是个死开关。address_requires_state直接丢弃而非迁移它已被废弃且只是国家自身states_required标志的重复开关。Address#state_validate现在只读国家标志把该设置设为false而国家要求州名的店铺其地址将从迁移后开始校验失败——应该去改国家标志。七个零读取的死设置在 6.0 加了弃用壳products_per_page、alternative_shipping_phone、show_variant_full_price、reserve_stock_on、storefront_products_path、storefront_taxons_path、storefront_pages_path6.1 删除。壳只用于让「在 initializer 里设置了它们的」安装在升级中途不因启动崩溃。新行为标志从 6.0 起一律从 Store或按区域从 Market/Channel出生绝不进入Spree.config。源码级原理Spree::StorePreferences读取器迁移后所有重新指向的读取点都经由 spree/core/app/models/concerns/spree/store_preferences.rb 统一读取。include 该 concern 的模型通过定义preference_store说明自己如何到达一个 storeVariant 经由其 productPrice 经由 variant → productFulfillment 经由其 ownerpreference_store默认实现是「有store关联就返回它否则 nil」。def store_preference(name) Spree::StorePreferences.read(preference_store, name) end def preference_store respond_to?(:store) ? store : nil end class self def read(store, name) return store.get_preference(name) if store Spree::Store.new.preference_default(name) # 无 store → 声明默认值 end def current(name) read(Spree::Current.store, name) # 解析环境 store end end两种解析方式有细微且重要的差异计划文档明确强调Spree::Current.store自身会回退到Spree::Store.default所以.current(name)返回的是默认 store 的配置值只有当整套安装没有任何默认 store 时才会落到声明默认值.read(nil, name)则始终返回声明默认值。因此环境 store 就是正确答案时用.current某个具体记录的 store 才是正确答案时用.read(record_store, name)。这在地址校验和商品可用性上会产生行为差异。测试用例 spree/core/spec/models/concerns/spree/store_preferences_spec.rb 明确覆盖了「无 store 时回退到声明默认值」和「跟随被覆盖的preference_store」两种行为。顺带修的两个 bugAddress#show_company_address_field?原来会在无 store 读取时抛异常Spree::Store.current.prefers_…商品可用性 scope 原来从Spree::Store.default取回退货币而非环境 store导致多店铺安装按错误店铺的货币过滤目录。读取点重新指向一览计划文档记录了所有读取点迁移后的到达路径均已落库设置读取点Store 到达方式capture_methodpayment_method.rbresolved_capture_method、fulfillment.rbprocess_order_payments、fulfillments/fulfill.rbPaymentMethod 为 store 所有fulfillment → order/cart → storetrack_inventory_levelsvariant.rb、product.rbvariant → product → storestock_reservations_enabledstock_reservations/reserve.rb、stock_reservations/extend.rb、stock/quantifier.rbcart/order → storestock_item → stock_location → storetrack_price_historyprice.rbprice → variant → product → storeshow_products_without_priceproduct_scopes.rbSpree::Current.store类级 scopeaddress_requires_phoneaddress.rb、addresses/phone_validator.rbSpree::Current.store无 store 关联disable_sku_validationvariant.rbvariant → product → storeProduct.store是optional: true见product/channels.rb所以每个被重指re-pointed的读取者都以偏好默认值作为「无 store」时的兜底。附带修复迁移之外的顺手清理coupon_codes_total_limit在类加载时被插值进Promotion的数值校验promotion.rb在 initializer 里晚于加载设置它不会生效——校验选项应改为 lambda。disable_sku_validation的定义注释configuration.rb「when turned off disables」写反了——true才是禁用校验。storefront_products_path不只是无人读base_helper.rb和 Google feed presenter 硬编码/products/覆盖它今天会静默产生损坏的 feed URL删除它反而让问题显式化。迁移路径Phase 1 / 2 / 3Phase 16.0移动 弃用已上线 2026-08-12在Spree::Store上新增八个偏好默认值与全局一致按上表重指读取点Address与 product scope 读Spree::Current.store在spree/core/lib/spree/core/configuration.rb中把八个全局配置与死设置标记为deprecated:访问时经Spree::Deprecation告警交付spree:store_settings:backfill_from_config并加入 5.6→6.0 升级清单与sanitize_rich_text同机制StockReservation.ttl_for去掉全局读取。回填任务backfill的实现与坑任务实现在 spree/core/lib/tasks/store_settings.rake。MOVED_SETTINGS哈希把全局名映射到 store 偏好名与默认值注意两个改名项company→company_field_enabled、default_stock_reservation_ttl_minutes→stock_reservation_ttl_minutes任务按名映射而非假设同名。执行逻辑task backfill_from_config: :environment do changed MOVED_SETTINGS.reject do |name, config| Spree::Config.send(name) config[:default] # 仍为默认值的全局不拷贝 end # ...逐 store 拷贝跳过已定制项记录元数据标记 end两个 capture 布尔不是按名拷贝而是由CAPTURE_METHOD_FROM_CONFIGlambda 一起推导auto_capture开 →checkout否则auto_capture_on_dispatch开 →on_dispatch两者都关 →manual。若推导结果等于默认checkout则不写。关键坑store 实例化的瞬间会把每个声明默认值写进自己的 preferences 哈希因此 key 永远存在无法用preferences.key?或与preference_default比较来区分「商家选的值」与「种子默认值」——没有「显式设置」标志可查。所以任务在 store 的 metadata 里记录store_settings_backfilled_from_config标记每个 store 至多访问一次残余风险是单向且很小的单次运行时某个值恰好等于默认值的 store 会采用全局的值此后标记保护所有后续变更。任务幂等且逐 store 打印它写入的每个设置。配套迁移spree:migrate_capture_methods把auto_capture: true的行复制为checkoutfalse的行留空——布尔只记录了「不在结账时扣」区分不了 dispatch 与 manual所以这些行继续继承 store 的选择。测试策略stub 而非写入规格通过Spree::TestingSupport::Preferences#stub_store_preferences驱动——stub 而不是写库因为默认 store 是全测试套件共享的一个例子持久化设置会污染下一个。stub 以 store id 为键被测代码经关联或 reload 拿到的是同一行的不同实例其他 store 与未命名偏好照常自行回答。Phase 26.0暴露Admin API v3 store serializer 与 permitted params 加入八个偏好重新生成类型Dashboard 设置页在自然分区payments、inventory、catalog、checkout浮现新开关文档将八个设置移入 store-settings 文档当前仓库对应页面为 docs/developer/customization/configuration.mdx保持 commerce / application 两组结构。Phase 36.1删除删除八个已弃用全局、死设置及文中列出的遗留壳。对当前开发工作的硬约束计划文档明确了迁移期间的编码纪律不要新增对八个迁移项的Spree::Config读取——它们已弃用、core 不再读。一律经Spree::StorePreferencesinclude concern 并定义preference_store或调用.read/.current新行为标志从出生就放 Store——绝不把商业行为偏好加进Spree.config不要基于auto_capture/auto_capture_on_dispatch构建 UI 或代码——Store 与 PaymentMethod 上均已弃用。读resolved_capture_method或capture_at_checkout?/capture_on_dispatch?/capture_manually?写capture_method。任何表达「何时动钱」的新设置都必须放进Spree::CaptureMethod词汇表而不是在旁边再加布尔校验地址或查询目录的后台任务必须设置Spree::Current.store——否则静默使用默认值。开放问题与后续方向Market 级track_price_history留待价格按 Market 划分设计完成后再议购物车过期设置guest_cart_expiry_days等仍是应用级维护事项按店留存期延迟到有人提出需求。延伸阅读配置使用审计2026-08-04 多 Agent 追踪 对抗性验证51 个非弃用偏好中 38 活跃 / 6 遗留 / 7 未用先例company→Store#company_field_enabledconfiguration.rb:47、allow_guest_checkout→Store#guest_checkout、Store#stock_reservation_ttl_minutesstore.rb 第 98 行5.6-6.0-single-store-promotions-payment-methods.md——本次复用的回填 弃用桥模式6.0-tax-provider.md——负责tax_using_ship_address的退役6.0-returns-exchanges-claims.md——负责退货相关设置的退役6.0-stock-reservations.md——承载reserve_stock_on与 TTL 全局的取代说明文档页docs/developer/customization/configuration.mdx。【免费下载链接】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),仅供参考