
Spree 6.0 投递区设计解析DeliveryZone 如何取代 Spree::Zone 并支持邮编区间配送【免费下载链接】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-zones.md解析 Spree 6.0 中投递区Delivery Zone体系的完整设计如何用Spree::DeliveryZoneSpree::DeliveryZoneMember取代通用的Spree::Zone如何以“国家 / 州省 / 邮编前缀 / 邮编区间”三类成员表达地理规则以及如何通过数据迁移任务将存量 Zone 平滑转换为新模型。读完后你将理解投递区在配送计价链路DeliveryMethod → Estimator中的位置、邮编匹配与校验规则的实现细节以及 5.6 → 6.0 升级时的迁移步骤。背景为什么 6.0 要重写投递区模型Spree 旧版使用通用的Spree::Zone配合Spree::ZoneMember的多态zoneable指向 Country/State来同时服务税率与配送两个场景。这个模型在配送侧有一个结构性缺陷Zone 只能表达“国家 州省”粒度的成员集合无法表达邮编级别的规则。规划文档中给出的目标场景都很具体“本地邮编 10001–10099 免运费”、“华沙市中心特快配送”、“岛屿不加价的区域”——这类规则是欧洲本地配送和快递业务的标配也是托管型平台普遍提供、Spree 旧模型缺失的能力。因此 6.0 的决定是用带邮编区间成员的Spree::DeliveryZone专门服务配送地理规则税率侧改用TaxRate.for_address按国家/地址匹配由6.0-tax-provider.mdPhase 5 负责已于 2026-08-07 完成解耦在 6.0 结束前彻底删除Spree::Zone/Spree::ZoneMember表、模型、Country#zones/State#zones、管理端 CRUD、工厂全部移除。6.0 是唯一的破坏性变更窗口不做向后兼容等待。文档还特别说明早期版本曾将此计划降级约 183 处非测试引用无人认领而这份计划文档本身就是该清理工作的“所有者”——所有 Zone 触点都被列出了归属方见下文“消费者清单”。总体设计DeliveryZone 与带类型的成员核心数据模型由两个类组成前缀 ID 分别为dz_和dzm_Spree::DeliveryZone目的地集合挂在一组类型化成员members之上Spree::DeliveryZoneMember单条地理规则由member_type区分类型——country国家、state州省、postal_code邮编规则。一个 Zone 可以自由混合多种成员类型。两条关键修订Amendment改变了最终落地形态阅读规划文档时要特别注意2026-08-096.0-delivery-profiles.mdDeliveryZone 改为归属于一个配送档案delivery_profile_id且配送方法最多绑定一个Zonedelivery_zone_id单值外键方法↔Zone 的多对多连接表被移除。Zone 成员机制国家/州/邮编区间本身不变。2026-08-076.0-drop-country-state-models.md成员上的 Country/State 外键转换为ISO 字符串列——country/postal 成员带country_codestate 成员带country_codestate_code二元组因为州省代码在全局并不唯一只有配对才能唯一定位。从 DeliveryZone 模型源码 可以看到最终结构class Spree::DeliveryZone Spree.base_class has_prefix_id :dz belongs_to :store, class_name: Spree::Store belongs_to :delivery_profile, class_name: Spree::DeliveryProfile, inverse_of: :delivery_zones belongs_to :delivery_origin_group, class_name: Spree::DeliveryOriginGroup, inverse_of: :delivery_zones has_many :members, class_name: Spree::DeliveryZoneMember, dependent: :destroy, inverse_of: :delivery_zone has_many :delivery_methods, class_name: Spree::DeliveryMethod, dependent: :destroy, inverse_of: :delivery_zone def include?(address) return false if address.nil? members.any? { |member| member.match?(address) } end end几个值得注意的实现细节均出自 delivery_zone.rbZone 必须归属某个 Storevalidates :store, presence: true名称在同一 Store 内唯一默认归属兜底创建时若未显式指定 profile会自动挂到店铺的默认配送档案assign_default_delivery_profile并落到该档案的默认发货组assign_default_origin_grouporigin_group_must_belong_to_profile校验保证发货组与档案不跨配删除级联是 destroy 而非 nullify源码注释解释得很清楚——delivery_zone_id为 nil 的配送方法语义是“无目的地限制全球报价”如果删除 Zone 时把方法置空它们会被静默“升级”为全球配送因此删 Zone 连带删除其下的方法名称检索走 Ransack 白名单whitelisted_ransackable_attributes %w[name]。数据表结构对应迁移 20260728000002_create_spree_delivery_zones.rb表关键列说明spree_delivery_zonesstore_id非空、name非空、description、metadatajsonb/json唯一索引[:store_id, :name]spree_delivery_zone_membersdelivery_zone_id非空、member_type非空、country_id/state_id可空、postal_code_prefix、postal_code_from、postal_code_to类型化成员metadata列是“单一合并 metadata JSON 列”设计的一部分只写不读的开发逃生舱。另外spree_delivery_method_zones连接表在 6.0 中被单值外键spree_delivery_methods.delivery_zone_id取代——这是 Amendment 的直接结果可从 DeliveryMethod 模型 的belongs_to :delivery_zone, class_name: Spree::DeliveryZone, optional: true得到印证。成员匹配规则country / state / postal_codeDeliveryZoneMember 模型 是地理匹配的核心match?按类型分派def match?(address) case member_type when country then same_country?(address) when state then same_country?(address) address.state_code.present? address.state_code state_code when postal_code then same_country?(address) postal_match?(address.normalized_postal_code) else false end end三条规则要点一切邮编匹配都以国家为作用域same_country?先比对country_code。state 成员同样先比对国家——源码注释明确说明“子区域代码只在所属国家内唯一单独的 CA 会同时命中加利福尼亚和其他地方”。邮编匹配是前缀或区间二选一postal_match?有postal_code_prefix时用zipcode.start_with?(prefix)否则用区间比较zipcode from zipcode to。区间成员受“数字邮编国家白名单”门控。模型中内置了一份range_capable_country_codesUS、DE、PL、FR、JP 等 50 个邮编为纯数字格式的国家允许通过 class_attribute 扩展Spree::DeliveryZoneMember.range_capable_country_codes %w[XY]。前缀成员则对所有国家开放。设计动机来自规划文档的 Resolved QuestionsUK、加拿大、荷兰、爱尔兰这类非纯数字邮编格式如SW1A 1AA、M5V 2T6天然呈“前缀形态”——商户在这些国家实际就是这么思考的UK 的 outward code、加拿大的 FSA因此启动时只支持前缀列表不做区间同时刻意不引入 per-country 匹配器策略和通配符匹配逻辑保持为“一个 Ruby 方法 归一化集中在一个地方”未来出现真实需求时可以零 schema 变更内部升级。邮编归一化normalize_postal_code区间比较是字典序的前提是邮编已归一化。归一化统一收口在 Address 模型def self.normalize_postal_code(value) value.to_s.gsub(/[\s-]/, ).upcase end即去除空格与连字符、转大写10001、sw1这类输入可安全比较。成员侧的三个邮编列在保存时也走同一归一化normalizes :postal_code_prefix, :postal_code_from, :postal_code_to。规划文档把“地址归一化必须走这一处禁止到处散落邮编处理”列为对当前开发的硬约束。注意 6.0 中旧的normalize_zipcode/normalized_zipcode已标记为 deprecated6.1 移除文档中的旧名以现名normalize_postal_code为准。校验规则一条邮编成员到底要写什么postal_definition自定义校验delivery_zone_member.rb明确了成员配置的合法形态场景结果前缀与区间同时提供报错二者互斥delivery_zone_prefix_and_range_exclusive区间但from/to缺一报错必须成对提供delivery_zone_postal_definition_required区间但国家不在数字格式白名单报错该国家不支持区间delivery_zone_range_not_supportedto from报错区间反转delivery_zone_range_invertedmember_type state但缺state_code常规 presence 校验拦截另外resolve_geography回调会用Spree::IsoData.subdivision_code把已废弃的子区域代码解析为其继任者——因为normalizes按单列运行、看不到 countrystate 的组合所以这部分保留为回调。配送链路集成include? 与 Estimator规划文档要求“DeliveryMethod#include?迭代 delivery zones检查点原地不动留在 Estimator 的配送方法过滤中”。最终实现比文档描述更进一步——配合 delivery-profiles 修订方法直接绑定单个 Zonedelivery_method.rbdef include?(address) return true unless requires_zone_check? return false unless address return true if delivery_zone.nil? delivery_zone.include?(address) end # Zones describe the customers destination address, so only methods # that deliver to one are zone-checked. def requires_zone_check? requires_address? end语义链完整可读不需要收货地址的方法如自提/数字商品不做地理检查需要地址但delivery_zone为 nil 的方法 全球可投延续了旧版无 Zone 方法的语义规划文档明确“保留不变”有 Zone 的方法委托给DeliveryZone#include?即任一成员命中即通过。该检查在运费估算链路中被调用Stock::Estimator 在过滤配送方法时执行delivery_method.include?(order.ship_address)。配送方法还带有跨档案一致性校验delivery_zone_must_belong_to_profile与 origin group 归属校验delivery_method.rb保证 Zone、发货组、配送档案三者同属一个档案。店铺级统计也基于新模型重写Store#countries_with_shipping_coveragestore.rb先检查是否存在无 Zone 的方法存在则视为覆盖全国否则从DeliveryZoneMember的country_code聚合得出实际覆盖国家集合。新建店铺时还有默认数据兜底Stores::ProvisionDefaults 会创建Domestic与International两个默认投递区并为其预置配送方法即开箱即用的地理基线。5.6 → 6.0 数据迁移spree:migrate_zones_to_delivery_zones迁移任务实现在 delivery_migration.rake并注册进 5.6→6.0 升级清单db:migrate之后执行。核心逻辑确定待迁移集合取所有被配送方法引用的delivery_zone_id此时新 ID 尚未产生旧 ID 无歧义减去已通过metadata[migrated_from_zone_id]标记完成的 Zone——这保证任务幂等中断后可重跑按档案拆分旧 Zone 可被不同 store/profile 下的方法共享而新 Zone 归属档案因此任务按[store_id, delivery_profile_id]分组一个旧 Zone 可能拆成多个 DeliveryZone重名时自动追加(profile_id)后缀成员拷贝直接查spree_countries/spree_states表此时 Country/State 模型已移除按 ID 读表把Spree::Country成员转成member_type: country, country_code: isoSpree::State成员转成member_type: state, state_code: abbr, country_code: iso二元组回写引用update_all(delivery_zone_id: 新ID)把配送方法的指针改过来跳过与报告ID 冲突的 Zone 打印SKIP ... resolve manually仅被税率引用kind tax 场景的 Zone 留给税率迁移任务处理无任何引用的 shipping Zone 仅打印 SKIP 日志。规划文档中的 Phase B 描述“为每个被配送方法引用的 Zone 创建同名 DeliveryZone 并拷贝成员纯税率 Zone 归税率计划处理无人引用的记日志跳过幂等、分批”与该实现一一对应。升级顺序上有一个易踩的坑任务注释强调“立即在 db:migrate 之后运行——在新 DeliveryZone 被创建之前——这样未迁移的 ID 才是无歧义的”。Zone 消费者清单与清理进度规划文档最有价值的部分是那张消费者归属表约 183 处非 spec 引用core/app 109、core/lib 37、admin/app 37它把 Zone 的每个触点都指派给了具体计划触点归属状态TaxRate 的 Zone 外键、TaxRate.match、税率展示读取器6.0-tax-provider.mdPhase 5改写为TaxRate.for_address/Spree::Current.tax_country2026-08-07 完成Spree::Pricing::Context的 zone 维度、Spree::Current.zone同上因Order#tax_zone移除而被强制提前已随税率计划发货Spree::Zone.global工厂猴补丁、zone_factory、shipping_method_factory本计划改为默认全球的无 Zone 方法 显式:delivery_zone工厂见下方测试支持说明权限集default_customer、configuration_management本计划Phase C 余项Country#zones/State#zones/Store#checkout_zone/Seeds::Zones/ 权限目录条目本计划Phase D 已删2026-08-14管理端zones_controller 视图、导航/表格注册项本计划由 DeliveryZones CRUD 选择器替代旧 Rails 管理端随 6.0 删除不重建已替代几个值得开发者知道的收尾状态来自文档“Implementation status”与 decisions.md 记录Zone#default_tax成为可写空操作税率侧解耦后无人读取该标记但旧模型仍强制执行“单一默认 Zone”不变量且允许列表与文案还在——商家可以反复切换一个没有效果的开关计划决定随模型一并删列升级任务必须在模型删除后仍能运行spree:migrate_tax_zones与spree:migrate_zones_to_delivery_zones在db:migrate之后执行Phase D 删除Spree::Zone常量前两个任务要先改为匿名 AR 类读表——先例是Spree::ReturnsMigrator它正是为同一原因这样写的。当前 delivery_migration.rake 中读spree_state_changes已采用该模式Class.new(ActiveRecord::Base) { self.table_name ... }Phase D 基本完成Zone/ZoneMember降级为仅迁移用的空壳裸 AR country_list遗留到 6.1 的只是删除空壳与spree_zones/spree_zone_members两张表PriceRules::ZoneRule退役2026-08-14spree:migrate_tax_zones会把每个规则转换为MarketRule当 Zone 国家集合恰好匹配某个市场或停用价格列表并给出逐列表报告——没有替代性的地理类MarketRule 就是最终形态。测试支持Zone.global 猴补丁的退场文档把“Zone.global工厂模式退役”列为关键决策旧测试里几乎所有配送方法都靠zones { [Spree::Zone.global] }挂一个全球 Zone新方案是无 Zone 即全球绝大多数 spec 不再关心地理少数需要的用显式工厂。仓库中对应的 delivery_zone 工厂 展示了新写法factory :delivery_zone, class: Spree::DeliveryZone do sequence(:name) { |n| Delivery Zone ##{n} } store { Spree::Store.find_by(default: true) || association(:store) } delivery_profile { store.default_delivery_profile || association(:delivery_profile, store: store) } factory :delivery_zone_with_country do transient { country { create(:country) } } after(:create) do |zone, evaluator| create(:delivery_zone_member, delivery_zone: zone, member_type: country, country: evaluator.country) end end end注意工厂把country/state作为 transient 值对象接收、落库为country_code/state_code字符串——这与模型“存 ISO 字符串、州省必须带国家”的设计一致。模型自身的行为由 delivery_zone_member_spec 与 delivery_zone_spec 覆盖迁移任务由 six_zero_data_migrations_spec 验证。约束与后续规划文档给出的“对当前工作的约束”在实现后依然成立可作为二次开发守则不得新增任何Spree::Zone引用。新的地理资格逻辑走 DeliveryZone配送或TaxRate.for_address税率不要基于 Zone 管理端构建 UI——它正在被替代邮编归一化统一走Address#normalize_postal_code禁止在别处散落邮编处理逻辑。遗留工作文档明确留给 6.1只剩删表收尾migrate_tax_zones、migrate_zones_to_delivery_zones与backfill_order_markets中的读者切换为匿名 AR 类后即可删除 Zone/ZoneMember 空壳与spree_zones/spree_zone_members表。整体迁移路径按四个阶段推进Phase A建模型与表已发货→Phase B数据迁移任务已发货→Phase C消费方切换API、Dashboard、匹配、覆盖统计、工厂已完成权限集、种子与Zone.global测试补丁收尾中→Phase D删除基本完成2026-08-14。从源码结构看这份规划文档的价值不仅在于新增的邮编区间能力更在于它展示了一次“破坏性版本窗口如何被系统性执行”的完整案例每个触点有归属、每个迁移步骤可重跑、每个语义变化空 Zone 全球、删除级联策略、州省二元组都有明确的理由记录在案——这些细节都沉淀在 delivery_zone.rb、delivery_zone_member.rb 的注释与 delivery_migration.rake 的任务说明里值得在升级或二次开发时对照阅读。【免费下载链接】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),仅供参考