资讯动态

Spree 6.0 配送方式规则(Delivery Method Rules):把运费资格判定从计价计算器中剥离出来

发布时间:2026/9/14 18:59:40 来源:尧图企业网站定制
Spree 6.0 配送方式规则Delivery Method Rules把运费资格判定从计价计算器中剥离出来【免费下载链接】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 6.0 的设计规划文档 6.0-delivery-method-rules.md完整讲解 Spree 6.0 如何将配送方式的“资格限制”最低/最高商品总额、最低/最高重量、排除商品等从运费计价计算器中抽离落成独立的Spree::DeliveryMethodRuleSTI 记录并在Stock::Estimator的单一过滤缝中统一执行。读完本文你将理解 Spree 6.0 配送资格模型的设计动机、三类内置规则的实现细节、Admin API v3 的管理接口、FlatRate 旧配置的数据迁移路径以及如何以插件形式扩展新的规则类型。一、问题背景资格判定为什么会“长”在计价器里在 6.0 之前Spree 的配送方式资格限制eligibility bounds与计价逻辑pricing存在双重纠缠规划文档将其归纳为四个具体问题限制属性只存在于 FlatRate 计算器上。minimum_item_total/maximum_item_total/minimum_weight/maximum_weight是Calculator::Shipping::FlatRate的 preference而 FlexiRate、PerItem、PriceSack、FlatPercentItemTotal 等其他运费计算器根本不提供这些限制——也就是说一个配送方式能不能用取决于你当初选了哪种计价公式。在当前仓库的 flat_rate.rb 中可以看到这四个 preference 已被标注为deprecated废弃信息明确写着“改用配送方式上的Spree::DeliveryMethodRules规则将在 Spree 6.1 移除”。“不合格”被编码成“无价格”。旧实现靠compute_package返回nil来表示“此包裹不符合资格”见 flat_rate.rb 的 compute_package四个return nil if ...守卫。而 Estimator 真正会去询问的ShippingCalculator#available?(package)钩子FlatRate 从未使用。配送服务商rate provider绕过了限制。按照 6.0-delivery-rate-provider.md 的设计由 EasyPost/Shippo 等服务商报价的配送方式会绕过计算器直接向服务商询价——承载资格限制的计算器对象被整体跳过限制于是“静默失效”。管理台Dashboard呈现错位。这些限制属性渲染在“运费计算器”的配置表单里商户看到的是一堆“计价配置”读不出“资格限制”的语义。规划文档还引用了平台调研结论Shopify 在 zone rate 上直接放重量/价格条件Saleor 在方式上放列外加按渠道的价格边界Medusa v2 使用类型化的ShippingOptionRule记录Vendure 为 ShippingMethod 提供与计算器平行的ShippingEligibilityChecker策略——没有任何主流平台把资格判定埋进计价对象内部。这正是本次重构的方向。二、核心设计决策复刻“房屋规则模式”House Rule PatternSpree::DeliveryMethodRule是 Spree 内部“规则模式”的第五个实例前四个是PromotionRule、PriceRule、OrderRoutingRule、PaymentMethodRule。规划文档在 Key Decisions 一节列出了不可随意偏离的决策清单以下是其中最关键的部分镜像Spree::PaymentMethodRule不另造框架。STI 基类落在spree_delivery_method_rules表上子类放在app/models/spree/delivery_method_rules/目录下配置由 preference schema 驱动type字段经过注册表registry校验。凡是与 6.0-payment-method-rules.md 重叠的部分基类形状、AND 语义、空 preference 失败开放、注册表、仅 Dashboard 管理以支付规则规划文档的措辞为权威——两套规则保持对称。用规则而不是方式上的列。Saleor 式的列方案能覆盖第一阶段Phase 1但会堵死支付侧已承诺的渠道/市场/客户组channel/market/customer group扩展轴。一种模式服务两套方式。无规则 处处可用No rules eligible everywhere。在数据迁移任务运行之前升级 6.0 不产生任何行为变化。AND 语义每个配送方式每种规则类型只允许一个实例唯一性约束在[:type, :delivery_method_id]上。唯一的执行缝是 Estimator 的方法过滤链。Stock::Estimator获得delivery_method.eligible_for_package?(package)与calculator.available?处于同一过滤链中。三个入口Fulfillment#refresh_rates、OrderRouting::Strategy::Rules、Carts::EstimateShippingRates因此免费获得统一执行。与支付规则不同这里没有“管理端旁路”概念Estimator 是唯一的费率来源而重量/总额边界是物流约束不是商店前台的门禁。规则只评估包裹及其所有者package.weight、package.item_total以及后续规则用到的package.owner.channel_id绝不读取Spree::Current这类全局当前值。WeightRule 按商店隐含的重量单位比较原始数字与旧 FlatRate 边界行为一致Dashboard 标签会展示商店的unit_system。带单位感知的重量值是另一个未来项目。规则只作用于配送方式作用域method-scoped永远不带 zone 上下文。想按地区使用不同边界按“区域化方式”regional methods决策拆成两个配送方式。Estimator 的 zone 过滤独立运行。管理仅限 DashboardAdmin API v3 嵌套 CRUD 配送方式编辑页上独立的“Conditions”卡片与计算器的计价 preference 分开呈现。三、基类与规则模型源码级实现基类Spree::DeliveryMethodRule规划文档给出的设计代码与仓库中的实际实现 delivery_method_rule.rb 高度一致。核心结构如下取自当前源码# spree/core/app/models/spree/delivery_method_rule.rb module Spree class DeliveryMethodRule Spree.base_class include Spree::PreferenceSchema has_prefix_id :dmrule # API 前置 ID 前缀如 dmrule_1 belongs_to :delivery_method, class_name: Spree::DeliveryMethod, inverse_of: :delivery_method_rules, touch: true delegate :store, to: :delivery_method attribute :active, :boolean, default: true validates :type, presence: true # 每种规则类型每个方式仅一个实例——AND 语义下重复项要么无效、要么自相矛盾 validates :type, uniqueness: { scope: [:delivery_method_id, *spree_base_uniqueness_scope] } validate :type_must_be_registered scope :active, - { where(active: true) } registers_subclasses_via { Spree.delivery_method_rules } # param package [Spree::Stock::Package] def eligible?(_package) raise NotImplementedError, ... end end end值得注意的实现细节has_prefix_id :dmrule规则实体使用带前缀的 API ID如dmrule_xxx与 Spree 其他实体一致touch: true规则变化会刷新所属配送方式的updated_atregisters_subclasses_via { Spree.delivery_method_rules }子类发现STI 注册表指向引擎配置数组插件可以扩展type_must_be_registered校验器第 48-53 行会在type不在Spree.delivery_method_rules注册表中时拒绝保存错误消息为:invalid_delivery_method_rule类方法human_name/human_description从delivery_method_rule_types.api_type.name/description翻译键取本地化文案供管理端选择器展示。ItemTotalRule 与 WeightRule两个边界规则这两个规则是第一阶段的核心对应今天已存在的两个约束维度实现完全按规划文档中的形状落地# spree/core/app/models/spree/delivery_method_rules/item_total_rule.rb class Spree::DeliveryMethodRules::ItemTotalRule Spree::DeliveryMethodRule preference :minimum_amount, :decimal, default: nil, nullable: true preference :maximum_amount, :decimal, default: nil, nullable: true def eligible?(package) total package.item_total return false if preferred_minimum_amount.present? total preferred_minimum_amount return false if preferred_maximum_amount.present? total preferred_maximum_amount true end endweight_rule.rb 形状完全相同只是把比较对象换成package.weightpreference 为minimum_weight/maximum_weight同样是:decimal、nullable: true、默认nil。语义要点规划文档“Boundary semantics”一节边界语义为“含边界的 min / 含边界的 max”total min判不合格total max判不合格等于边界值判合格旧 FlatRate 的守卫在最小值一侧是开区间源码为preferred_minimum_item_total package.item_total时返回 nil即恰好等于最小值也不合格数据迁移任务保留商户已存的数字接受这个“差一分钱”的边界变化——规划文档明确将其定性为bug 修复而非回归空 preference 失败开放fail-open只配置了minimum_amount而未配置maximum_amount的规则只对最小值做判定。这与PromotionRule/PriceRule的惯例一致见 item_total_rule.rb 的注释“半配置的规则按每规则失败开放”金额比较使用包裹所有者的货币即“currency-aware via the owners currency”规则本身不携带货币设置。ExcludedProductsRule取代产品侧 JSON 列2026-08-06 加入的决策废弃spree_products.excluded_delivery_method_ids列该列由履约重构 Phase 1b 加入从未通过任何 API 或 UI 暴露仅控制台可写改为“脆弱商品不能走快递”式的方式侧排除规则。规划文档给出的选型理由非常具体JSON 里的 ID 列表在三种受支持的数据库上无法被索引或可移植地查询配送方式删除后会留下陈旧 IDAPI 边界上被迫引入 raw-ID / 前置-ID 的映射垫片方法侧排除正是 Saleor 的模型ShippingMethodType.excludedProductsshippingPriceExcludeProducts并且把资格判定保持在 Estimator 这一道缝里而不是回到Stock::Package#eligible_delivery_methods内被废弃的“第二道缝”。当前源码 excluded_products_rule.rb 的关键实现class Spree::DeliveryMethodRules::ExcludedProductsRule Spree::DeliveryMethodRule has_many :delivery_method_rule_products, class_name: Spree::DeliveryMethodRuleProduct, foreign_key: :delivery_method_rule_id, dependent: :destroy, inverse_of: :delivery_method_rule has_many :products, class_name: Spree::Product, through: :delivery_method_rule_products self.additional_permitted_attributes [product_ids: []] def eligible?(package) product_ids package.contents.map { |item| item.variant.product_id }.uniq return true if product_ids.empty? # target 覆盖“已分配但尚未保存”的关联行管理控制器先赋产品再存规则 return false if delivery_method_rule_products.target.any? { |link| link.new_record? product_ids.include?(link.product_id) } return true if new_record? !delivery_method_rule_products.exists?(product_id: product_ids) end end实现上有三个值得一提的细节判问方向反过来了不是把整个排除清单加载进内存再比对而是用包裹内商品 ID 去问中间表“有没有交集”——中间表 spree_delivery_method_rule_products 的复合索引[delivery_method_rule_id, product_id]唯一可以无行传输地回答未保存记录的内存态处理delivery_method_rule_products.target读取的是“已在内存中的目标”不会触发数据库加载因此能覆盖“管理控制器先给规则赋产品、规则还没保存”这个窗口期软删除商品的读取面product_prefixed_ids通过products关联 pluck 再排序编码为前置 ID软删除的商品自动从返回列表中消失——客户端重新提交时不会被告知一个它从未选择过的商品“不可达”第 16-25 行。中间模型 delivery_method_rule_product.rb 镜像Spree::ProductPromotionRule双向belongs_to并在[delivery_method_rule_id]作用域内对product_id唯一。语义边界包裹内含有任意一个被排除商品即不合格没有关联商品的规则放行fail-open与空 preference 惯例一致。由于该列从未通过受支持的 API 或 Dashboard 可写不存在数据迁移——直接迁移到规则即可曾在 6.0 预发布期用 Rails 控制台手工写入该列的人需要把值重建为各配送方式上的excluded_products_rule。四、执行点Estimator 方法过滤链整个运行时改动只有一行——把eligible_for_package?放进 Estimator 现有的过滤链。当前仓库 estimator.rb 的 filter_delivery_methodsdef filter_delivery_methods(package, audience) methods package.eligible_delivery_methods methods methods.merge(order.store.delivery_methods) if order.store # ... 市场多卖家场景下 package 由货位的卖家报价 ... methods.select do |delivery_method| offered_to_seller?(delivery_method, package_seller_id) delivery_method.available_to?(audience) delivery_method.include?(order.ship_address) # zone 过滤独立运行 delivery_method.serves_location?(package.stock_location) delivery_method.eligible_for_package?(package) # ← 规则执行缝 calculator_offers?(delivery_method, package) end end而DeliveryMethod侧的实现就是规划文档中的 AND 聚合无规则 全部通过# AND over active rules; no rules eligible. def eligible_for_package?(package) delivery_method_rules.select(:active).all? { |rule| rule.eligible?(package) } end这样做的直接收益无论配送方式由计算器定价还是由服务商EasyPost/Shippo 等定价资格判定都发生在同一处——服务商报价路径不再绕过限制这正是规划文档要堵住的“provider bypass”。同时 Estimator 中存在一条向后兼容的delivery_methods/shipping_methods派发逻辑第 163-175 行宿主应用若覆写了旧缝shipping_methods会被自动识别并沿用其定义保证 6.1 之前旧代码不会静默漏掉本应过滤的方式。五、注册表类型校验与插件扩展规则类型通过引擎配置数组注册。当前 engine.rb 中的注册内容是# Delivery-method eligibility rule kinds (docs/plans/6.0-delivery-method-rules.md). Rails.application.config.spree.delivery_method_rules.concat [ Spree::DeliveryMethodRules::ItemTotalRule, Spree::DeliveryMethodRules::WeightRule, Spree::DeliveryMethodRules::ExcludedProductsRule, Spree::DeliveryMethodRules::ChannelRule, Spree::DeliveryMethodRules::VolumeRule, Spree::DeliveryMethodRules::CompanyRule ]配置项由 core.rb 的Spree.delivery_method_rules/Spree.delivery_method_rules读写形状与Spree.payment_method_rules/Spree.order_routing.rules相同——插件只需向这个数组 concat 自己的规则类基类的type_must_be_registered、registers_subclasses_via从而find_by_api_type按Spree.delivery_method_rules解析与 API 的 types 发现端点都会自动带上它无需改动任何控制器代码。从当前源码结构看注册表已不止规划文档第一阶段所列的三类ChannelRule、VolumeRule、CompanyRule对应app/models/spree/delivery_method_rules/下的 channel_rule.rb、volume_rule.rb、company_rule.rb已经就位。规划文档原本的安排是“Channel / Market / CustomerGroup 跟随支付规则的节奏一起落地使两组规则在 Dashboard 中同时出现”仓库现状表明这条扩展路线正在兑现。六、管理接口Admin API v3 与 Dashboard端点GET/POST/PATCH/DELETE /api/v3/admin/delivery_methods/:id/rules——嵌套 CRUD扁平参数type的 wire 简写 preferencesGET /api/v3/admin/delivery_method_rules/types——发现端点返回每个已注册规则类型的type、name、description、preference_schema以及本次新增的association_fields。发现端点的实现见 delivery_method_rules_controller.rbdef types authorize! :create, Spree::DeliveryMethodRule data Spree.delivery_method_rules.map do |klass| { type: klass.api_type, name: klass.human_name, description: klass.human_description, preference_schema: klass.serialized_preference_schema, # 规则在 preferences 之外接受的关联型配置如 product_ids # 让管理端 UI 无需硬编码规则类型即可渲染正确的编辑器 association_fields: association_fields_for(klass) } end render json: { data: data } endassociation_fields直接派生自各规则类的additional_permitted_attributesExcludedProductsRule声明了product_ids: []对应 第 14 行。这解决了规划文档指出的一个前后端不对称问题没有它后端是注册表驱动的而客户端不是——插件带来的关联型规则会被 API 接受却在 Dashboard 里渲染成空白的 preferences 表单提交时 ID 被丢弃。有了该字段Dashboard 的 Conditions 卡片对任何声明了product_ids的规则都渲染商品选择器而不是PreferencesForm。规划文档同时说明完整的方案是 Dashboard 侧的“slot registry”推广编辑器模式约 150 行新结构被刻意推迟——触发条件是“第二个需要非 preference 配置类型的规则”或“第一个插件自带的规则”出现。rules扁平载荷写入器2026-08-06 的决策推翻了最初“每条规则一个立即写入的 Save 按钮”的做法一个编辑页上出现两个 Save 按钮且与所有兄弟编辑器不一致改为规则随方式一起保存。DeliveryMethod#rules是一个基于Spree::TypedAssociations的扁平载荷写入器——与Promotion#rules、PriceList#rules同一机制。一次 POST/PATCH 携带rules: [...]即完成对账reconcile已有行按id更新新行经find_by_api_type构建载荷中省略的行被销毁。嵌套的/delivery_methods/:id/rules独立端点保留给程序化使用。product_ids的解析策略关联型规则的关联写入以前置 ID 参数product_ids承载控制器必须同时经过商店作用域和调用方能力accessible_by(current_ability, :show)解析——与DeliveryMethodsController解析税类别/区域/自提点的做法一致模型写入器本身不做这两层校验。解析失败的 ID 被从选择集中丢弃而不是让保存 404一个不可达的商品 ID 无论如何都成不了排除项而失败会困住那个“产品被删除时表单还开着”的商户与价格列表过滤成员的策略一致。序列化器以同名product_ids回显保证读写对称。七、迁移路径与弃用桥规划文档的 Migration Path 共七步当前仓库能看到前六步的落地证据表 基类 规则 注册表纯增量——20260729130001_create_spree_delivery_method_rules.rbEstimator 过滤行 eligible_for_package?——见上文第四节FlatRate 四个边界 preference 的弃用警告——flat_rate.rb 中四个 preference 已带deprecated:元数据其compute_package的 nil 守卫作为弃用桥保留至 6.1Admin API Dashboard Conditions 卡片 SDK OpenAPI——控制器、序列化器admin/delivery_method_rule_serializer.rb均已就位数据任务spree:migrate_calculator_bounds_to_delivery_method_rules——完整实现见 delivery_method_rules_migration.rake行为要点遍历所有挂在Spree::DeliveryMethod/ 旧Spree::ShippingMethod上的 FlatRate 计算器若设置了minimum/maximum_item_total且该方式尚无ItemTotalRule创建规则并携带数值重量同理已存在同类型规则则跳过幂等用update_columns直接清掉计算器上的四个旧边界避免旧 nil 守卫与新规则双重执行该任务挂在 5.6→6.0 升级清单 manifest.yml 中migrate_shipping_to_delivery之后执行ExcludedProductsRule——20260806000001_create_spree_delivery_method_rule_products.rb 建中间表产品侧列删除规则注册、product_ids参数与 Dashboard 商品选择器6.1删除四个 preference 与 FlatRate 的 nil 守卫尚未执行。测试侧可参考 delivery_method_rule_spec.rb模型层注册校验、唯一性、各规则 eligible? 语义与 delivery_method_rules_controller_spec.rbAPI 层嵌套 CRUD、types 发现、product_ids权限过滤。八、对现有工作的约束与扩展指南规划文档“Constraints on Current Work”一节给出三条硬约束对贡献者和插件作者仍然有效不要再给任何计算器添加资格类 preference——新约束一律等规则不要基于 FlatRate 边界构建商店前台功能——它们是弃用桥6.1 即拆除Estimator 过滤链是资格判定的唯一挂载点——控制器/服务层里不得出现逐调用点的资格检查。扩展一个新规则类型的最小路径以插件为例在app/models/spree/delivery_method_rules/下新建 STI 子类声明preference或如ExcludedProductsRule那样声明additional_permitted_attributes实现eligible?(package)然后向Rails.application.config.spree.delivery_method_rules注册。此后类型校验、API 发现端点、find_by_api_type解析、Dashboard 编辑器选择全部自动生效——这正是“注册表驱动”这套模式的复利所在。小结Spree 6.0 的 Delivery Method Rules 用一个五度复用内部规则模式的机制回答了“配送方式在什么情况下可用”这个此前被埋在计价公式里的问题STI 基类 preference 配置 注册表校验AND 语义且无规则即全通执行点唯一收敛在Stock::Estimator的过滤链上使计算器定价与服务商定价的方式服从同一套资格ItemTotalRule/WeightRule承接存量约束并通过幂等 rake 任务完成 5.6→6.0 数据搬迁ExcludedProductsRule则以具体外键中间表取代了不可查询、留陈旧数据的 JSON ID 列管理面由 Admin API v3 嵌套 CRUD 与“types 发现 association_fields”驱动 Dashboard 的 Conditions 卡片插件扩展规则类型无需触碰 API 层。规划文档标注 Phase 1 已实现、ExcludedProductsRule决策 2026-08-06PR #14399在评审中而当前仓库源码显示其模型、迁移、控制器与注册表均已落地后续 6.1 将完成 FlatRate 旧 preference 的拆除。【免费下载链接】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),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价