mcpField
S
spree-b2b

spree-b2b

github.com/spree开发10102 次安装4 周前更新
代码仓库

关于 spree-b2b

用于配置 Spree 6 批发店铺的技能,涵盖公司结构、目录定价、数量规则和访问限制,并着重说明 B2B 设置中的规则与注意事项。

主要功能

  1. 01公司层级与成员关系
  2. 02公司目录及专属定价
  3. 03数量规则与订单最低额
  4. 04批发店铺访问限制

适用场景

  1. 01规划 Spree B2B 店铺设置
  2. 02配置公司买家及其成员关系
  3. 03设置批发价格和数量阶梯
  4. 04限制目录或价格的访客访问

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/spree/agent-skills --skill spree-b2b

安装后在 Claude Code 中输入/spree-b2b确认已加载。

详细介绍

Spree B2B / 批发

在 Spree 6 中,B2B 由四个开源构建模块组成。它们都通过配置实现,一个店面和一个后台即可同时服务零售和贸易业务:

模块说明
Company(Spree::Company)谁在购买:组织及其成员、地址和税务身份
Catalog(Spree::Catalog)交易约定:该受众能看到什么、支付多少、最低订购量是多少
Channel 门控(storefront_access)访客是否能看到目录或价格
Freight(配送规则加上 Freight 费率提供程序)批发货物如何运输:纸箱、托盘、集装箱,或审核后报价

端到端操作指南是 Build a B2B Store(node_modules/@spree/docs/dist/developer/how-to/build-a-b2b-store.md)——请按其说明了解设置顺序和仪表板界面;本文介绍其背后的规则和注意事项。

每个商店都会预置: 一个 wholesale 渠道(需要登录,关闭访客结账)、一个 Wholesale 客户组(作为批发门户的审批标记),以及一个绑定到批发渠道的可发布密钥。示例数据(spree sample-data)会添加一个演示买家 [email protected] / spree123——这些凭据是公开已知的,因此切勿将示例数据加载到生产环境(此任务没有环境保护机制)——一个带有数量阶梯价的批发价目表、两级公司 Acme Industrial → Acme EMEA,以及一个分配给 Acme Industrial 的未启用 Wholesale Assortment 目录。

仅限 Spree Enterprise:公司角色和权限(OSS 成员资格的 role 只是外观标签,每位成员都能在其职权范围内执行所有操作)、订单审批(approval_required)、支出限额、治理审计历史记录,以及公司入驻/审批流程。付款条款、净额开票和报价功能已列入 Enterprise 路线图。Enterprise 治理通过相同的端点实施,因此启用后,OSS 店面无需更改即可继续运行。

公司

  • 一个最多有 5 层的树(Spree::Company::MAX_DEPTH)。根节点必须是 kind: 'company'。kind 可以是 company(可持有税务登记的法律实体)或 division(不具备此资格的组织单位)。前缀:comp_。
  • 成员资格涵盖整个子树。“Acme Europe”的成员可以代表其下的每个节点行事。授权始终会检查“该客户是否在此节点或任一祖先节点具有成员资格?”成员资格(cmem_)始终处于有效状态,并且始终关联真实客户。
  • 通过电子邮件添加人员。通过店面(client.companies.members.create(companyId, { customer_email }) → POST /api/v3/store/companies/:id/members)添加时,始终会创建邀请(cinv_),即使对方是现有客户也一样——输入电子邮件地址并不能证明你要找的就是这个人,而且响应不会透露该电子邮件是否已关联账户。通过员工端(管理后台 / POST /api/v3/admin/companies/:id/memberships,服务 Spree::Companies::AddMember)添加时,现有客户会立即成为成员(cmem_),未知电子邮件则会收到邀请——请检查 ID 前缀。邀请在 30 天后过期。受邀者无需登录即可调用 companyInvitations.lookup(token) / accept(token, { first_name, last_name, password })。
  • **每家公司都有自己的地址簿。**每个地址都有标签,并可设为默认收货地址或账单地址。配送地点属于地址,而非节点,因此十个仓库不需要创建十家公司。
  • 税务信息通过最近的 company 祖先节点解析(company.legal_entity)。遍历过程会在第一个 company 节点处停止,即使该节点没有登记信息也一样,因此子公司不会借用母公司的 VAT 号码。税务识别号和免税证明仅保存在法律实体上。请读取证明的 active,而非其状态,因为 active 还会考虑有效期。请参阅 spree-taxes。
  • 删除节点会移除其整个子树;如果该节点下存在任何订单,Spree 会拒绝删除。更新 parent_id 可移动分支。
  • 店面不支持公司自助注册:Store API 仅在 /store/companies 上提供 show / update。员工可在管理后台创建公司,或通过 POST /api/v3/admin/companies 创建。
代表公司购买
// Store SDK: which companies can the signed-in customer act for?
const { data: memberships } = await client.account.companies()   // m.company.ancestors = path above it

await client.carts.update(cartId, { company_id: 'comp_xxx' })     // must be a node they have standing on
const { data: orders } = await client.companies.orders.list('comp_xxx') // whole-subtree order history
// other members' orders come with gift card codes masked and no digital download links — those stay with the buyer
await client.companies.addresses.create('comp_xxx', { label: 'Northern Warehouse', /* … */ default_shipping: true })

如果买家只有一个成员资格,就无需选择,因为公司会自动解析。购物车上的 company_id 会在完成结账时固定到订单上,地址和价格也会一并固定。购物车会公开 company_id、company_name 和 po_number_required。在后端,你的代码应读取 Spree::Purchase::Company concern 中的 cart.b2b? / cart.resolved_company。

目录:约定

目录(cat_)包含商品组合(CatalogProduct)、可选的自有价目表、分配对象(cata_,可为 customer_group 或 company)以及商业条款。

  • 商品组合为空时,目录是定价叠加层:受众可以看到所有商品,并按该目录的价格购买。商品组合非空时,目录会限制商品范围:受众只能看到其中的商品。添加第一个商品后,目录便会从“所有商品”切换为“仅此商品”。
  • 价目表仅由一个目录拥有,自身不设受众规则。它会在目录生效时应用,并且不参与常规规则匹配。移除价目表(price_list: null)或删除目录都会对价目表执行软删除。Spree 不会释放该价目表,因为没有所有者且没有规则的价目表会为所有购物者定价。已停用目录的价目表会进入休眠状态。
  • 定价通过每个变体的 prices 和/或 price_adjustment_percentage(例如 -15)明确设置,还可选配数量区间 price_adjustment_tiers。百分比模式仅适用于目录自有价目表。
  • 目录创建时处于未激活状态。 使用 adminClient.catalogs.activate(id)(PATCH /admin/catalogs/:id/activate)使其生效。激活是一项工作流(Spree::Catalogs::Activate),而非写入列值;未分配任何受众的目录无法激活(错误 no_audience),除非它是某个渠道的 default_catalog_id。停用目录会保留所有内容以便日后使用;删除目录也会删除其自有价目表。
const catalog = await adminClient.catalogs.create({
  name: 'Acme wholesale',
  price_list: { name: 'Acme pricing', price_adjustment_percentage: -15 },
  assignments: [{ assignable_type: 'company', assignable_id: 'comp_xxx' }],
  minimum_order_quantity: 6, order_multiple: 6,          // catalog-wide defaults
  order_minimums: [{ currency: 'EUR', amount: 500 }],    // per currency, never converted
})
await adminClient.catalogs.products.create(catalog.id, ['prod_a', 'prod_b'])  // → restricting mode
await adminClient.catalogs.activate(catalog.id)
解析:谁能看到什么,以及他们支付的价格
  1. 确定受众。第一个能找到任何目录的来源胜出,各来源不会合并。 顺序为:公司节点及其祖先节点,然后是客户所属的群组,最后是渠道的 default_catalog_id。
  2. 可见性是这些目录商品组合的并集。如果任何适用目录是叠加层(商品组合为空),则不限制可见商品。
  3. 价格:先考虑适用目录自有的价目表,按节点从近到远。在同一个节点内,若适用多个目录,则按购买数量比较并选出最低价。接着考虑规则匹配的常规价目表,最后采用基础价格。较近节点的协议优先于上层节点中更低的价格。

只要公司链中分配了任何目录,公司买家就不会获得客户群组目录。 将贸易等级建模为公司分配(根公司的群组级目录,以及成员公司或部门的等级目录)。仅对非代表公司购买的买家使用客户群组(cg_),例如零售忠诚度等级员工或经批准的批发个人。新建配置时,优先使用目录,而非 CustomerGroupRule 价目表。

下文所述的店面访问限制会在目录解析之前检查。

数量规则和订单最低限额
  • 按变体设置的 minimum_order_quantity 和 order_multiple(Variant 列)按字段解析:依次采用变体基础值、目录默认值,再到目录和商品的覆盖值(Spree::Catalogs::ResolveQuantityRules,结果为 Spree::QuantityRule)——以声明该规则的最具体层级为准。数量从最低限额开始按倍数递增:MOQ 为 48 / 倍数为 24 时,允许 48、72、96,拒绝 50;MOQ 为 50 / 倍数为 24 时,允许 50、74、98。无限制的变体读取值为 1 / 1。
  • 按币种设置订单最低限额:adminClient.catalogs.orderMinimums.create(id, { currency: 'USD', amount: '600' })(也可以在创建时传入 order_minimums)。使用 catalogs.products.list(id, { expand: ['quantity_rule'] }) 读取覆盖值。
  • 使用 adminClient.catalogs.quantityRules.upsert(id, { terms: { prod_x: { minimum_order_quantity: 48, order_multiple: 24 } } }) 设置整组商品覆盖规则。为不在商品组合中的商品添加条款,会将其加入商品组合,从而使覆盖目录进入限制模式。
  • 添加商品时(Carts::AddItem、UpsertItems)以及完成订单时,都会执行规则检查(要求代码 quantity_rule_violated)。订单最低限额属于建议性要求(order_minimum_not_met),未达标会阻止订单完成。购物车会公开 order_minimum、order_minimum_shortfall 和 below_order_minimum,变体会公开 minimum_order_quantity、order_multiple 和 purchase_unit,以便步进控件按倍数调整数量。
  • 以员工密钥创建的购买不受此规则限制(管理后台草稿、带有 created_by 的购物车),因此可以记录协商后的例外情况。条款会实时读取,绝不会存储。已下单订单不会重新验证。
目录价目表:规则
  • 可组合使用以下模式:price_adjustment_percentage(相对于店铺价格的折扣)、price_adjustment_tiers([{ min_quantity, percentage }])、显式 prices,以及带有 min_quantity 的 prices(数量阶梯价)。
  • 数量按行计算——同一 SKU 购买十件即可达到十件阶梯;两个 SKU 各买五件则不行。
  • 带有阶梯价的变体仅按自身阶梯价定价——目录百分比不会叠加。
  • 某一阶梯价不得高于其下方数量对应的价格(保存时会拒绝);一个价目表中,每个变体每种币种最多可设置十个阶梯(Spree::Price::MAXIMUM_BREAKS_PER_VARIANT)。
  • 价格不会在币种之间转换——应以买家交易所用的每种币种分别注明协议。
  • CustomerGroupRule / UserRule 价格规则在已使用的场景中仍可继续使用,但新价目表不再提供这些规则——通过分配目录来指定目标受众。

门控渠道与批发门户

Spree::Channel#storefront_access 的值为 public | prices_hidden | login_required。未设置时,它继承商店的 preferred_storefront_access,默认为 public。Store API 会强制执行此设置,因此店面无法放宽限制:

  • login_required 会对访客读取请求返回 401(身份验证、密码重置和参考数据除外)。
  • prices_hidden 会向访客返回商品目录,所有金额字段均为 null。
  • guest_checkout 是单独的渠道设置。

已登录客户不会受到 storefront_access 限制。是否批准买家由你的应用决定,例如通过 customers/me → customer_groups 检查其是否属于“Wholesale”客户组。

请求通过 X-Spree-Channel 标头选择渠道,可通过 SDK 的 channel 选项或 client.setChannel('wholesale') 设置,也可使用绑定到渠道的可发布密钥。Spree 会预置一个 wholesale 渠道(login_required,且关闭访客结账)及一个绑定到该渠道的可发布密钥。

Next.js 店面提供可选启用的 /wholesale 门户。设置 SPREE_WHOLESALE_CHANNEL=wholesale 即可启用;未设置时,所有 /wholesale 路由都会返回 404。SPREE_WHOLESALE_PUBLISHABLE_KEY 为可选项。它会为每个界面分别使用 SDK 客户端、购物车 Cookie 和缓存键,同时共用一个客户 JWT。可将其作为参考实现进行调整,例如在面向 B2B 商店的主渠道上启用登录优先门控。请参阅 spree-storefront。

PO 编号

  • po_number 是 Cart 和 Order 上的普通索引列(Spree::Purchase::PurchaseOrder,并非 Spree::PurchaseOrder;后者是前缀为 po_ 的供应商采购模型)。订单完成时,该值会复制到订单上。
  • 为公司启用 Spree::Company#po_number_required。在设置 PO 编号之前,结账会发出要求 { step: 'address', field: 'po_number', code: 'po_number_required' }:client.carts.update(id, { po_number: 'PO-4471' })。使用员工密钥创建的订单不受此限制。
  • 可选的PO 文档位于 POST/GET/DELETE /api/v3/store/carts/:id/po_document。它接受最大 10 MB 的 PDF、图片或 Word 文件,会检查文件内容、私密存储并复制到订单。管理员可通过 GET /admin/orders/:id/po_document 读取。

赊账订单

现可通过两种方式先接受交易订单、之后再收款——开源版没有付款期限引擎(净 30 天账期、信用额度、定金均在 Enterprise 路线图中):

  • 结账时选择发票付款。 内置的 Spree::PaymentMethod::Check 会记录付款,但不会联系支付服务商;订单完成时付款会成功,并保持 pending 状态,等待员工捕获付款。每种已启用且对店面可见的付款方式都会提供给所有订单,因此若要将发票付款条件限制为公司采购,请继承该类:
    # server/app/models/spree/payment_method/invoice.rb
    class Spree::PaymentMethod::Invoice < Spree::PaymentMethod::Check
      def available_for_order?(order) = super && order.b2b?
    end
    # server/config/initializers/spree.rb
    Rails.application.config.after_initialize { Spree.payment_methods << Spree::PaymentMethod::Invoice }
    
  • 使用员工密钥创建草稿订单。 adminClient.orders.create({ customer_id, company_id, channel_id, currency, po_number, use_customer_default_address: true, items: [{ variant_id, quantity, price: '8.75' }] })。设置 company_id(创建或更新时)会使订单成为公司采购,并采用该公司的商品目录价格和税费。商品项上的 price 是协商后的行价格(price_source: 'manual',不会重新定价;price: null 会恢复为商品目录定价)——仅限下单前设置;下单后请通过费用或折扣调整金额。随后调用 adminClient.orders.complete(id, { payment_pending: true }),即可在不处理付款的情况下下单(分配订单号、分配库存、触发 order.placed,payment_status 保持为 none)。发票付清后:调用 orders.payments.create(orderId, { payment_method_id, amount }) + orders.payments.capture(orderId, paymentId)。控制面板中的 Complete 操作会收取付款;payment_pending 仅适用于 API。

运费

  • 变体上的纸箱链:carton_package_type_id(kind: 'carton' 的 PackageType)、units_per_carton、carton_weight、cartons_per_pallet 和 purchase_unit(unit 或 carton,只影响店面如何呈现数量)。存储的数量始终以件为单位。跨越纸箱边界的 order_multiple 会被拒绝。
  • 购物车和运费率会提供 freight_summary(total_units、total_cartons、total_pallets、以 m³ 为单位的 total_volume、以 kg 为单位的 total_weight,以及 complete)。当 complete: false 时,部分商品使用了单件尺寸作为后备值,因此这些数据应视为最低值。
  • 运费档位是普通配送方式,使用带规则的 rate_provider: 'Spree::DeliveryRateProvider::Freight'。volume_rule 接受以 m³ 为单位的 minimum_volume / maximum_volume。company_rule 接受 company_orders_only:true 表示仅限公司订单,false 表示仅限非公司订单。该提供程序会返回未定价的费率(unpriced: true,显示为“审核后报价”,排序靠后,绝不会预选)。结账仍可完成,报价后再添加运费。参见 spree-fulfillment。
  • 处理费或附加费等额外费用是 Spree::Fee 行(始终为正数,默认应税)。参见 spree-order-totals。

注意事项

  • 向覆盖型目录添加一个商品会隐藏其他所有商品。为系列之外的商品设置数量条款也会如此。
  • 将档位目录分配给公司买家的客户群组不起作用,因为公司链会优先响应。请改为分配给公司。
  • 忘记 activate:新目录处于非活动状态,不会产生任何效果。
  • 分离目录的价格表会将其软删除。Spree 永远不会释放它以供规则匹配。
  • **认为部门会继承其上级的 VAT ID。**系统使用最近的公司节点,而该节点可能没有 VAT ID。
  • **认为 prices_hidden 会对已登录但尚未获批的买家隐藏价格。**它只限制访客,因此是否批准由你的应用负责。
  • **只在 UI 中验证 MOQ。**API 仍会强制执行,员工操作路径则按设计跳过验证。
  • 公司自主注册、approval_required 门控和公司角色不属于 OSS。不要围绕这些功能构建。

延伸阅读

  • node_modules/@spree/docs/dist/developer/how-to/build-a-b2b-store.md — 端到端指南(https://spreecommerce.org/docs/developer/how-to/build-a-b2b-store)
  • node_modules/@spree/docs/dist/developer/core-concepts/{companies,catalogs,channels,freight,fees,customers}.md
  • node_modules/@spree/docs/dist/developer/storefront/nextjs/wholesale.md
  • node_modules/@spree/docs/dist/user/how-to/selling-to-businesses.md:为一位贸易客户进行端到端设置
  • https://spreecommerce.org/docs/developer/core-concepts/companies
  • 相关技能:spree-pricing(价格表、数量规则)、spree-checkout(需求注册表)、spree-taxes、spree-fulfillment、spree-storefront、spree-auth-permissions

评论

支持 Markdown