Spree 5.5→6.0 重构指南:display_on 三态字段收敛为 storefront_visible 布尔值
【免费下载链接】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
在 Spree(面向 B2B、Marketplace 与多租户场景的开源电商平台)中,支付与配送方式长期用一个三态字符串字段display_on(both/front_end/back_end)控制可见性。本文基于仓库内的规划文档 5.5-6.0-display-on-to-boolean.md,完整梳理这次已落地(Status: Implemented, 2026-08-19)的重构:为什么三态收敛为布尔值storefront_visible、5.5 版本的 API 桥接层如何设计、6.0 的数据库迁移如何原子化完成,以及模型、Admin API v3、TypeScript SDK 与 Admin SPA 各层的具体实现证据。读完后你能掌握 Spree "先加别名、后改 schema" 的字段重命名迁移模式,并能在自己的扩展中正确使用新字段。
背景:display_on 三态字段的设计缺陷
在 5.4 时代的 Spree 中,Spree::PaymentMethod和Spree::ShippingMethod(6.0 更名为DeliveryMethod)的表结构里都有一个display_on字符串列,取值域为'both' | 'front_end' | 'back_end',并共同 include 一个名为Spree::DisplayOn的 concern,提供:
scope :available(仅both)scope :available_on_front_end(front_end+both)scope :available_on_back_end(back_end+both)validates :display_on, presence: true, inclusion: { in: DISPLAY.map(&:to_s) }available_on_front_end?实例谓词
这次重构的动机来自一个业务判断:front_end(仅前台可见、后台不可见)这个状态并不对应任何真实工作流——后台运营人员必须能看到每一种支付/配送方式,才能查询历史交易、处理退款、编辑订单。既然front_end是死状态,剩下的both与back_end两个状态就可以无损地坍缩成一个布尔值:
back_end→storefront_visible: falseboth(以及遗留的front_end)→storefront_visible: true
规划文档的关键决策(原文 Key Decisions 章节)包括:
- 用布尔值而非三态:与 custom-fields 重命名计划(5.4-6.0-custom-fields-rename.md)采用同一套推理。
- 命名
storefront_visible而非available_on_storefront:整个产品对同一概念使用同一个词,与 custom fields 计划保持一致。 - 默认值为
true:大多数支付/配送方式是面向顾客的;back_end只是少数例外(手动支票录入、内部电汇、仅退款用途的方式)。 front_end边缘数据一律映射为true,与 custom-fields 计划的数据迁移规则一致。- 5.5 的迁移是非破坏性的:不做数据迁移、不删列;读
display_on的代码继续工作,读storefront_visible的代码拿到布尔值。
两阶段落地路线
重构分两个版本交付,每个版本的目标不同:
阶段一(5.5):API 桥接 +alias_attributeshim,无 schema 变更
- 在两个模型上新增
storefront_visible读写访问器,代理到既有的display_on列; - Admin API v3 的读与写都以
storefront_visible为规范字段(写请求同时容忍display_on,保证树外集成在 5.5 周期内继续工作); - 遗留 Rails 后台引擎(
spree/admin)继续通过表单辅助方法写display_on,该路径到 6.0 才迁移; - SDK 只发布
storefront_visible;Admin SPA 使用<StorefrontVisibleSwitch>组件; - 数据库零改动。
规划文档给出的 5.5 桥接实现是扩展Spree::DisplayOnconcern(当时三个调用方PaymentMethod、ShippingMethod、MetafieldDefinition都 include 它,加在 concern 里可让三个模型自动继承):
# spree/core/app/models/concerns/spree/display_on.rb (5.5) module Spree module DisplayOn extend ActiveSupport::Concern DISPLAY = [:both, :front_end, :back_end] included do scope :available, -> { where(display_on: [:both]) } scope :available_on_front_end, -> { where(display_on: [:front_end, :both]) } scope :available_on_back_end, -> { where(display_on: [:back_end, :both]) } # New canonical scopes (5.5 → 6.0). scope :storefront_visible, -> { where.not(display_on: 'back_end') } scope :admin_only, -> { where(display_on: 'back_end') } validates :display_on, presence: true, inclusion: { in: DISPLAY.map(&:to_s) } def available_on_front_end? display_on == 'front_end' || display_on == 'both' end # 5.5 bridge — `storefront_visible` is the canonical wire field. # `back_end` → false; everything else → true. def storefront_visible display_on != 'back_end' end def storefront_visible=(value) self.display_on = ActiveModel::Type::Boolean.new.cast(value) ? 'both' : 'back_end' end end end end往返语义清晰:读时back_end→false,其余(both或遗留的front_end)→true;写时true→both,false→back_end,round-trip 无损。Spree::MetafieldDefinition原本按 custom-fields 计划单独定义了同名访问器,concern 提供后这些副本可以删除(该协调通过对应计划的 checklist 完成)。
同期,序列化器侧把线上字段从字符串换成布尔:
# Before typelize display_on: :string attributes :display_on # After (5.5) — wire field is the boolean only typelize storefront_visible: :boolean attributes :storefront_visibledisplay_on从此不再出现在 API 响应中;DB 列仍保留,只是 API 表面变了。允许参数方面,payment_method_attributes与shipping_method_attributes在 5.5 周期同时保留:display_on与:storefront_visible,concern 上的访问器把两者收敛到同一底层列。文档明确警告:客户端应提交其一,同时提交两者是未定义行为(取决于 JSON 键顺序),storefront_visible是规范名,display_on只是 6.0 即删的兼容 shim。
阶段二(6.0):schema 变更 + 删除 concern
6.0 把display_on字符串列替换为storefront_visible布尔列(default: true, null: false),删除Spree::DisplayOnconcern(三个调用方至此全部迁完,是干净的一次性清除),并移除display_onAPI 别名。这一阶段属于 6.0 的模型重命名波次,与Shipment → Fulfillment、ShippingMethod → DeliveryMethod、Metafield → CustomField同期推进(配送侧上下文见 6.0-fulfillment-and-delivery.md)。
6.0 迁移实现:数据转换内嵌于 migration
以 PaymentMethod 实际交付的迁移 20260819000001_replace_payment_method_display_on_with_storefront_visible.rb 为例:
class ReplacePaymentMethodDisplayOnWithStorefrontVisible < ActiveRecord::Migration[8.1] # Payment methods are the last host of the tri-state display_on column # (docs/plans/5.5-6.0-display-on-to-boolean.md). Only back_end ever meant # "hide from the storefront"; both and the legacy front_end-only value # collapse to true. def up add_column :spree_payment_methods, :storefront_visible, :boolean, default: true, null: false execute(<<~SQL.squish) UPDATE spree_payment_methods SET storefront_visible = #{connection.quoted_false} WHERE display_on = 'back_end' SQL remove_column :spree_payment_methods, :display_on end def down add_column :spree_payment_methods, :display_on, :string, default: 'both' execute(<<~SQL.squish) UPDATE spree_payment_methods SET display_on = 'back_end' WHERE storefront_visible = #{connection.quoted_false} SQL remove_column :spree_payment_methods, :storefront_visible end end迁移注释与规划文档给出了一个反直觉但重要的工程决策:数据转换写在 migration 里而不是 rake task。惯例上"数据转换放 rake task"的前提是任务执行时源列还在;而这里的 migration 在同一条语句里删掉了display_on,后续任务将无列可读。又因为除back_end外所有值都保留列默认值true,所以一条窄UPDATE即可,且down方向对称可逆。
三个模型的实际切换时间线(见文档 Status 行):
- DeliveryMethod:2026-08-05,先切——它的三态
DISPLAY_ON_*费率过滤常量改成了符号DeliveryMethod::STOREFRONT/BACKOFFICE,migration 只加列,回填留给spree:migrate_shipping_to_delivery(转换后清空display_on,重跑安全); - CustomFieldDefinition:2026-08-09,作为 metafields 重命名的一部分;
- PaymentMethod:2026-08-19,即上面这条 migration。
当前源码中的模型层实现
PaymentMethod:真实布尔列 + 规范 scopes
spree/core/app/models/spree/payment_method.rb 展示了 6.0 的最终形态:
scope :active, -> { where(active: true).order(position: :asc) } # Every tri-state display_on value passed the old filter, so availability # only ever meant "active". scope :available, -> { active } ... # Customer-facing methods vs backoffice-only ones (manual check entry, # internal wire transfers). The backoffice always sees every method. scope :storefront_visible, -> { where(storefront_visible: true) } scope :admin_only, -> { where(storefront_visible: false) } # Real column, so admin clients filter it directly — no ransacker needed. self.whitelisted_ransackable_attributes = %w[storefront_visible] ... validates :storefront_visible, inclusion: { in: [true, false] }几个值得注意的实现细节:
storefront_visible是真实列,因此管理端客户端可以直接按列过滤,whitelisted_ransackable_attributes白名单里注册了它,不需要写 ransacker 方法。- 旧
availablescope 的语义修正:源码头注释说明,旧三态值都能通过available过滤,所以该 scope 实际只等价于active——布尔化之后这个历史含糊被显式记录。 - 校验改为布尔包含式:
inclusion: { in: [true, false] },配合列的null: false, default: true,杜绝了旧presence+ 枚举字符串校验。
同时模型保留了 6.1 才移除的弃用入口(payment_method.rb#L291-L307):
# @deprecated Use {#storefront_visible?}; removed in 6.1. def available_on_front_end? Spree::Deprecation.warn('Spree::PaymentMethod#available_on_front_end? is deprecated and will be removed in Spree 6.1. Use #storefront_visible? instead.') storefront_visible? end # @deprecated Use {#storefront_visible}; removed in 6.1. def display_on Spree::Deprecation.warn('Spree::PaymentMethod#display_on is deprecated and will be removed in Spree 6.1. Use #storefront_visible instead.') storefront_visible? ? 'both' : 'back_end' end # @deprecated Use {#storefront_visible=}; removed in 6.1. def display_on=(value) Spree::Deprecation.warn('Spree::PaymentMethod#display_on= is deprecated and will be removed in Spree 6.1. Use #storefront_visible= instead.') self.storefront_visible = value.to_s != 'back_end' end这与规划文档"每个模型保留display_on作为弃用读写入口直到 6.1"的约束一致:读时反向翻译回旧字符串词表,写时把任意非back_end值折成true。
DeliveryMethod:受众过滤从整数常量改为符号
spree/core/app/models/spree/delivery_method.rb 中可以看到费率报价的受众常量已完成切换:
# Audience a rate refresh is quoting for: the storefront sees only # customer-facing methods, the backoffice sees every method. STOREFRONT = :storefront BACKOFFICE = :backoffice旧的整数常量DISPLAY_ON_FRONT_END = 1/DISPLAY_ON_BACK_END = 2保留为弃用别名(delivery_method.rb#L28-L31),而 normalize_audience 负责把旧调用方安全地映射到新词汇表:
def self.normalize_audience(audience) case audience when STOREFRONT, BACKOFFICE then audience when DISPLAY_ON_FRONT_END, DISPLAY_ON_BACK_END Spree::Deprecation.warn("...Use #{STOREFRONT.inspect} / #{BACKOFFICE.inspect} instead.") audience == DISPLAY_ON_BACK_END ? BACKOFFICE : STOREFRONT else raise ArgumentError, "unknown delivery audience #{audience.inspect} ..." end end注意这里的防御性设计:未知受众直接raise,而不是悄悄按前台口径收窄报价集——"typo must not narrow the offer set unnoticed"。配套的实例方法 available_to? 则体现了规划文档中的核心约束"后台看到一切":
def available_to?(audience) case self.class.normalize_audience(audience) when BACKOFFICE then true else storefront_visible? end endDeliveryMethod 同样拥有布尔属性与规范 scopes(delivery_method.rb#L67、L91-L92、L118):
attribute :storefront_visible, :boolean, default: true ... scope :storefront_visible, -> { where(storefront_visible: true) } scope :admin_only, -> { where(storefront_visible: false) } ... self.whitelisted_ransackable_attributes = %w[storefront_visible available_to_sellers seller_id]其弃用壳display_on/display_on=(delivery_method.rb#L453-L463)与available_to_display?的处置方式与 PaymentMethod 完全对称。
API 层:序列化器与允许参数
Admin API v3 的序列化器只发布布尔字段。以 Admin::PaymentMethodSerializer 为例:
typelize active: :boolean, ... storefront_visible: :boolean, ... attributes :metadata, :active, :auto_capture, :capture_method, :resolved_capture_method, :storefront_visible, :position, created_at: :iso8601, updated_at: :iso8601display_on已彻底不在 wire 上。控制器侧的允许参数只暴露新字段,如 admin/payment_methods_controller.rb#L55 中的:name, :description, :active, :storefront_visible, :auto_capture, :capture_method, :position, ...,配送方式控制器(admin/delivery_methods_controller.rb#L131)同样在:pickup_point_provider, :rate_provider, :storefront_visible, ...中注册了该字段。Store API 侧则从未把display_on作为线上字段暴露过(规划文档 Resolved Questions 一节确认:Store 的配送方式接口只是服务端按它过滤,因此不需要 Store API 桥接层)。
SDK 与 Admin SPA 的配套变更
TypeScript 侧按规划一次性删除旧类型而非渐进弃用(admin SDK 在 5.5 周期仍是nexttag、pre-1.0,树外 TS 消费者极少,"删除优于弃用"):
// Before export type DisplayOnValue = 'both' | 'front_end' | 'back_end' export interface PaymentMethod { display_on: DisplayOnValue } // After (5.5) — boolean only; legacy types removed export interface PaymentMethod { storefront_visible: boolean }DisplayOnValue等类型被整体移除;create/update 参数形状只暴露storefront_visible?: boolean。服务器写请求仍接受display_on,但 SDK 不再为其建模,需要发送遗留字段的 TS 调用方可自行断言。
Admin SPA 侧提供了两个配套 UI 组件。表单控件是 StorefrontVisibleSwitch——一个带标签的Switch(默认文案 "Visible on storefront",带说明文本),组件 JSDoc 明确标注它是 "Canonical 5.5+ 'Visible on storefront' control",供所有在 API 上暴露storefront_visible的资源使用;接入方式与其他表单项一致:
<StorefrontVisibleSwitch control={form.control} name="storefront_visible" />表格单元则复用既有的 Active 列视觉语言,按规划文档建议渲染为:
<ActiveBadge active={pm.storefront_visible} activeLabel="Visible" inactiveLabel="Admin only" />旧的三态<DisplayOnSelect>不存在于 6.0 代码库——它是在引入 Switch 的同一变更中被删除的。
迁移路径与调用方行动清单
对商户 / 托管方(5.5 → 6.0)
- 升级到 5.5:无需任何操作。
display_on继续可用,storefront_visible开始可用; - 升级到 6.0:运行
bin/rails db:migrate。支付方式与自定义字段定义在各自 migration 内原子转换,不存在"一半转换"的中间窗口,也没有额外的 rake 任务;配送方式的转换在spree:migrate_shipping_to_delivery中完成,已包含在 5.6 → 6.0 升级清单里。
对消费 Admin / Store API 的集成方
- 5.5 起:改为读布尔字段
storefront_visible(两个字段曾短暂并行出现); - 5.5 起:create/update 调用改提交
storefront_visible; - 6.0 起:
display_on从响应与参数形状中消失,删除所有读写它的代码。
对扩展 / 插件开发者
- 直接读
payment_method.display_on的扩展代码,改为payment_method.storefront_visible(6.0 中旧入口只发弃用告警); where(display_on: 'back_end')这类查询,改用新 scopeadmin_only或where(storefront_visible: false);- 6.0 中列已删除,任何残留的
display_on数据库级引用都会直接失败。
约束与边界决策
规划文档对后续开发立下了三条约束(Constraints on Current Work),这些约束决定了 Spree 可见性系统的长期形态:
storefront_visible是唯一规范名。display_on仅作为树外代码的弃用读写入口存在于各模型上,Spree 内部代码不得再调用它;- 新模型不得引入可见性 concern。从源码结构看,
PaymentMethod与DeliveryMethod各自内联了两行 scope 加校验——"四行代码的三份拷贝从未证明过这层间接抽象的必要性",这正是 6.0 选择删除 concern 而非改写的理由;只有当第四个调用方出现时才重新讨论; - 后台永远看全部。
storefront_visible只过滤顾客端表面,管理端列表不得应用该过滤;旧的available_on_back_endscope(返回back_end+both)没有布尔等价物,被直接丢弃而不是强行翻译——这也解释了为何 PaymentMethod 的availablescope 只剩active语义。
规划文档的 Resolved Questions 一节还澄清了两点:available_on_front_end?谓词以弃用壳形式保留在 PaymentMethod 上(委托storefront_visible?,6.1 移除),而 DeliveryMethod 与 CustomFieldDefinition 从未继承该谓词;Store API 不需要ShippingMethod的display_on桥接,因为它从来不是线上字段。可见性系统简化相关的后续决策沉淀在 docs/plans/decisions.md 中。
延伸阅读
- 本次重构的完整规划与决策记录:docs/plans/5.5-6.0-display-on-to-boolean.md
- 同一桥接模式的先行者(Metafield → CustomField):docs/plans/5.4-6.0-custom-fields-rename.md
- ShippingMethod → DeliveryMethod 重命名背景:docs/plans/6.0-fulfillment-and-delivery.md
- 核心模型实现:PaymentMethod、DeliveryMethod
- 数据迁移:ReplacePaymentMethodDisplayOnWithStorefrontVisible
- 序列化器与控制器:Admin::PaymentMethodSerializer、admin payment methods controller
- Admin SPA 组件:StorefrontVisibleSwitch
【免费下载链接】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),仅供参考