mcpField
S
spree-catalog

spree-catalog

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

关于 spree-catalog

spree-catalog 是介绍 Spree 6 电商目录数据建模与管理的参考技能,面向处理商品、变体、商品策划、自定义字段、媒体和店面搜索的开发者。

主要功能

  1. 01管理商品及可购买变体
  2. 02用类别和集合组织目录
  3. 03定义类型化自定义字段
  4. 04管理商品媒体与渠道可见性
  5. 05配置目录搜索和筛选

适用场景

  1. 01创建带变体和价格的商品
  2. 02按导航或商品策划分组
  3. 03添加商家维护的商品属性
  4. 04排查商品未在渠道展示的原因

安装

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

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

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

详细介绍

Spree 目录

目录涵盖所有可售内容:产品(商品列表)、变体(实际购买的对象——SKU、价格、库存)、选项类型/值(区分变体的属性)、类别和合集(产品的分组方式)、自定义字段、媒体,以及让商品可被搜索到的搜索提供方。

从 Spree 5.x(主变体、taxons、metafields、presentation)迁移而来?请查看 spree-upgrade-5-to-6 技能了解重命名映射。此技能介绍 Spree 6 的工作方式。

目录关系图

Store ─┬─ Product  (prod_, belongs_to :store — single owner; status draft/active/archived)
       │    ├── default_variant  (real FK default_variant_id — no master variant)
       │    ├── Variant × n  (variant_) — sku, barcode, weight…, Price per currency, StockLevel per location
       │    │     └── OptionValue × n (optval_)  via option_value_variants
       │    ├── ProductOptionType → OptionType (opt_) — name, label, kind (dropdown|color_swatch|buttons)
       │    ├── ProductCategory → Category (ctg_) — nested tree, permalink "clothing/shirts"
       │    ├── ProductCollection → Collection (coll_) — flat; manual or automatic (rules)
       │    ├── ProductPublication → Channel — visibility per channel, optional time window
       │    ├── Media (media_) — product gallery; primary_media = thumbnail
       │    ├── CustomField (cf_) → CustomFieldDefinition (cfdef_, store-owned)
       │    ├── ProductType (pt_) — template it was created from (optional)
       │    ├── DeliveryProfile — how it ships (required; auto-assigned)
       │    └── Seller — marketplace owner (nil = the operator's own catalog)
       └── Catalog (cat_) — audience-specific assortment + price list (see spree-pricing / spree-b2b)

产品

属性说明
statusdraft(默认)、active、archived。只有 active 状态的产品会在店面显示。Marketplace 还会添加 proposed / rejected(卖家审核——请参阅 spree-marketplace)。
name、slug、description、meta_*可翻译(Mobility)。description 是富文本;API 返回 description(纯文本)和 description_html。
available_on、discontinue_on销售时间范围,叠加于 status 之上。
storebelongs_to :store——每个产品恰好归属于一家店铺(Product.for_store(store))。默认值为 Spree::Current.store / Spree::Store.default。
delivery_profile_id必填。创建时从产品类型获取,否则使用店铺的默认配置。
product_type_id、tax_category_id、seller_id、tags、metadata运营字段(仅限 Admin API)。

状态使用普通字符串列(Spree::HasStatus),不是状态机——product.active?、Product.published(即 status: 'active')、Product.not_archived。

创建产品

Admin API 是规范的写入路径(会运行产品工作流并解码带前缀的 ID):

import { createAdminClient } from '@spree/admin-sdk'
const admin = createAdminClient({ baseUrl: 'https://store.example.com', secretKey: 'sk_xxx' })

// Single-variant product: prices go straight on the product (forwarded to its one variant)
await admin.products.create({
  name: 'The Spree Handbook',
  status: 'active',
  prices: [{ currency: 'USD', amount: '19.99' }],
})

// Product with options: send variants; options are matched by name/value (no ID lookups)
await admin.products.create({
  name: 'Premium T-Shirt',
  status: 'draft',
  variants: [{
    sku: 'TEE-S-NAVY',
    options: [{ name: 'size', value: 'Small' }, { name: 'color', value: 'navy' }],
    prices: [{ currency: 'USD', amount: '29.99' }],
    stock_levels: [{ stock_location_id: 'sloc_xxx', count_on_hand: 50 }],
  }],
  product_publications: [{ channel_id: 'ch_xxx' }], // Admin API does NOT auto-publish
})
spree api post /products -d '{"name":"The Spree Handbook","status":"active","prices":[{"currency":"USD","amount":"19.99"}]}'
spree api post /products/prod_xxx/clone          # duplicate as a new draft
spree api delete /products/prod_xxx              # soft delete

不要同时发送顶层 prices 和 variants。提供了批量端点(bulkStatusUpdate、bulkAddToCategories、bulkAddTags、bulkDestroy)。

在 Ruby 中(种子数据、任务、规格测试):

Spree::Current.store = Spree::Store.default   # outside a request — Store.default can be nil in a fresh DB
product = Spree::Product.create!(name: 'Tote', status: 'active', sku: 'TOTE-1')  # sku= builds the default variant
product.default_variant.set_price('USD', 19.99)     # prices are per currency — see spree-pricing
product.set_custom_field('properties.material', 'Canvas')

变体和默认变体

每个产品至少有一个变体,且所有变体都是真实可购买的——不存在隐藏的主变体。没有选项的书籍也有一个变体,用于承载其 SKU、价格和库存。

  • product.default_variant——持久化外键 default_variant_id;代表产品的“门面”(列表价格、选择前的加入购物车)。除非另行设置,否则它是第一个变体;如果被移除,系统会自动提升另一个变体。
  • 产品级的 sku、barcode、weight、price_in、amount_in、track_inventory……都委托给默认变体(设置器会按需创建默认变体)。
  • product.variants 包含所有变体(按位置排序)。product.has_multiple_variants? 使用 variant_count 计数缓存。
  • buy_box_variant_id 是 Marketplace 在多个卖家提供同一商品时主推的变体。
await admin.products.variants.create('prod_xxx', {
  sku: 'TEE-L-R',
  options: [{ name: 'size', value: 'Large' }, { name: 'color', value: 'Red' }],
  prices: [{ currency: 'USD', amount: '24.99' }],
})

选项类型和值

size  = Spree::OptionType.create!(name: 'size',  label: 'Size',  kind: 'buttons')
color = Spree::OptionType.create!(name: 'color', label: 'Color', kind: 'color_swatch')
color.option_values.create!(name: 'navy', label: 'Navy', color_code: '#1f2a44')

color.color_swatch?        # kind == 'color_swatch'
variant.options_text       # "Size: Large, Color: Red"
variant.option_value('size') # "Large" (the value's label)
  • 展示字段是 label(可翻译),不是 presentation。使用 where(label: ...) 查询;API 筛选器为 q[label_cont]。
  • kind ∈ dropdown、color_swatch、buttons(Spree::OptionType::KINDS)。
  • Admin API:在选项类型上发送 option_values 会替换整个集合——请包含所有要保留的值。
  • 选项类型归店铺所有:产品只能使用所属店铺的选项类型(由 ProductOptionType 验证),变体中的 options: [{ name:, value: }] 也只会匹配或创建产品所属店铺的选项类型。Spree::OptionValue.for_store(store) 会通过选项类型限定值的范围。

产品类型(模板)

Spree::ProductType(pt_)描述了“每款鞋都需要尺码 + 颜色,归属鞋类,按标准配置发货,并需要填写材质”:

部分行为
选项类型、分类、配送配置创建时写入产品,此后相互独立。播种只会追加,绝不会移除。
自定义字段定义通过引用保持实时关联——产品表单始终反映当前类型。required 仅供参考(写入时不会强制执行)。
const shoes = await admin.productTypes.create({
  name: 'Footwear',
  option_type_ids: ['opt_size', 'opt_color'],
  category_ids: ['ctg_footwear'],
  delivery_profile_id: 'fp_standard',
  custom_field_definitions: [{ id: 'cfdef_material', required: true, sort_order: 0 }],
})
await admin.products.create({ name: 'Trail Runner', product_type_id: shoes.id })
await admin.productTypes.applyToProducts(shoes.id)   // explicit, background, additive re-seed

编辑类型不会改写现有产品。已有产品的类型无法删除。

分类与合集

Spree::Category(ctg_)Spree::Collection(coll_)
结构嵌套树(parent、descendants),由店铺拥有扁平结构,由店铺拥有
成员关系手动分配(product.category_ids = [...])手动分配,或根据规则自动分配(available_on、sale、tag 规则类型;匹配方式为 all/any)
URLpermalink = 完整路径,例如 clothing/shirtspermalink
用途导航商品营销:“2026 夏季”“低于 50 美元”“畅销商品”
shirts = Spree::Category.for_store(store).find_by!(permalink: 'clothing/shirts')
shirts.active_products_with_descendants
Spree::Product.in_category(shirts)            # includes descendants
Spree::Product.in_collection(collection)
  • category_ids= / collection_ids= 只接受来自产品所属店铺的 ID(外部 ID 会被静默丢弃)。collection_ids= 只管理手动成员关系——自动成员关系由规则生成并会保留。
  • 品牌:如果品牌需要出现在导航中,可将其建模为分类;如果需要作为落地页,则可建模为合集;也可以另外添加 brand 自定义字段。只有在品牌需要除此之外的专属数据时,才应创建 Spree::Brand 模型(spree-resource)。
  • Spree::Taxon / Taxonomy 和 product.taxons 是已弃用的别名(将在 6.1 中移除)。

Store API:client.categories.get('clothing/shirts')、client.categories.products.list(...)、client.collections.products.list('summer-2026')。促销活动可通过 category 规则指定分类(参见 spree-promotions)。

自定义字段

由商家整理的类型化数据——无需迁移,也无需装饰器。定义(Spree::CustomFieldDefinition,由店铺拥有)声明数据结构;自定义字段(Spree::CustomField)是某条记录的值。

const def = await admin.customFieldDefinitions.create({
  resource_type: 'Spree::Product',
  namespace: 'properties', key: 'material', label: 'Material',
  field_type: 'short_text',          // short_text | long_text | rich_text | number | boolean | json
  storefront_visible: true,          // false = absent from the Store API entirely
  searchable: true,                  // also: sortable — both make it filterable
})
await admin.products.customFields.create('prod_xxx', { custom_field_definition_id: def.id, value: '100% Cotton' })
product.set_custom_field('properties.material', '100% Cotton')   # blank value deletes the row
product.get_custom_field('properties.material')&.value
product.storefront_custom_fields                                  # storefront_visible only
store.custom_field_definitions                                    # read definitions through the store

Store API:?expand=custom_fields。使用 cf_ 键进行筛选和排序:q[cf_properties_material_i_cont]=wool、sort=-cf_properties_weight。自定义字段供人使用;metadata(JSON)供机器使用,且永远不会进入 Store API。

媒体

Spree::Media(media_,原名 Spree::Asset)由店铺拥有,存放在媒体库中——文件可以先上传但不关联任何对象,然后再附加到产品、变体、分类和合集。重复使用会共享 blob,而非记录。

  • media_type ∈ image、video(上传的 MP4/WebM/QuickTime,可选 poster_signed_id)、external_video(YouTube/Vimeo 的 external_video_url;video_embed_url 会在保存时生成)。
  • product.primary_media / variant.primary_media——缩略图(第一张可渲染的图片)。product.gallery_media 在没有产品图片时会回退到变体图片。Store API 的 thumbnail_url 始终存在——列表页面不要使用 expand=media。
  • focal_point_x/y 用于适配裁切的缩略图;通过 Active Storage 变体使用具名尺寸。
  • 删除正在使用的媒体文件会返回 422,并附带其使用位置(admin.media.usage(id))。
await admin.products.media.create('prod_xxx', { signed_id, alt: 'Front' })  // after a direct upload
await admin.products.media.create('prod_xxx', { media_type: 'external_video', external_video_url: 'https://youtu.be/…' })

渠道可见性(“我的商品没有显示”)

只有通过 ProductPublication 关联的商品才属于某个渠道(可选的 published_at/unpublished_at 时间范围)。管理后台会自动在默认渠道发布新商品;Admin API 不会。使用 PATCH /products/:id { product_publications: [...] } 写入完整集合(未包含的渠道会解除关联),也可以通过 POST /channels/:id/add_products / remove_products 批量操作。

请逐项检查:

  1. status == 'active'(外层门槛),且当前时间在 available_on / discontinue_on 范围内?
  2. 属于此店铺?Spree::Product.for_store(store).exists?(product.id)
  3. 已在请求所用的渠道发布?product.publication_for(channel) / Spree::Product.for_channel(channel);发布窗口已开启?
  4. 请求货币中有基础价格?product.default_variant.price_in('EUR').persisted? — 当前货币下没有价格的商品会被隐藏。
  5. B2B:买家的商品目录可能会限制可选商品(Spree.products_for_context_service)— 参见 spree-b2b。同一可见商品集合也会限制所有从购物者处获取变体的操作(例如愿望清单项目):列表中不会显示的变体也会被拒绝。
  6. Meilisearch 索引是否过期?spree rake spree:search:reindex。

搜索

Spree.search_provider(类名字符串)负责 Store API 商品列表的文本搜索、筛选、分面、排序和分页;控制器会先构建安全性/可见性范围,再交由搜索提供程序处理。

提供程序设置
Spree::SearchProvider::Database(默认)无需设置。使用 ILIKE + Ransack;适用于小型商品目录。不建立索引。
SpreeMeilisearch::SearchProvider添加 gem 'spree_meilisearch' + spree bundle install,设置 MEILISEARCH_URL(生产环境中还需设置 MEILISEARCH_API_KEY),在 config/initializers/spree.rb 中设置 Spree.search_provider = 'SpreeMeilisearch::SearchProvider',然后重新建立索引。支持拼写容错、相关性排序和快速分面。
自定义提供程序继承 Spree::SearchProvider::Base;实现 search_and_filter(scope:, query:, filters:, sort:, page:, limit:)(返回 SearchResult)、self.indexing_required?、index、remove、remove_by_id、index_batch、reindex。
spree rake spree:search:reindex        # per store; no-op work for the Database provider
  • 索引文档由 Spree::Dependencies.search_product_presenter 定义(该 gem 将其设为 SpreeMeilisearch::ProductPresenter;核心代码未提供默认实现)。替换它以添加字段;product.search_presentation 可预览文档(每种语言环境 × 货币各一份)。
  • 使用带前缀的 ID(prod_…、ctg_…)建立索引,切勿使用原始 ID。
  • 批量导入、切换提供程序或更改自定义字段的 searchable/sortable 标志后,重新建立索引。
  • Ruby 作用域:Product.search(query)、.with_option_value(option, value)、.with_option_value_ids、.in_category、.in_categories、.in_collection、.price_between、.in_stock。使用普通 scope 定义自己的作用域(没有 add_search_scope)。

数字资产

当变体的交付配置文件为数字类型时,该变体即为数字商品;它通过 Spree::DigitalAsset 携带文件(dig_,属于某个变体,文件存储在私有存储中 — 请求直接上传时使用 private: true)。下单后,每件资产的每个数量单位都会生成一个 Spree::DigitalLink(令牌,authorized_clicks 默认值为 5,authorized_days 默认值为 7)。事件:digital_asset.*、digital_link.downloaded。自定义交付内容(许可证密钥)属于数字资产提供程序 — 参见 spree-providers 和 docs/developer/how-to/custom-digital-asset-provider。

导入/导出

CSV 导入和导出通过 Admin API 进行(admin.imports、admin.exports、type: 'products' 等)。自定义字段列使用 custom_field.<namespace>.<key> 前缀。按类型下载模板以获取预期列。参见 docs/developer/core-concepts/imports-exports。

注意事项

  • variant.price / product.price= 不存在——价格按币种设置(price_in、set_price)。参见 spree-pricing。
  • Product#default_image、#featured_image、#primary_image 已移除 → primary_media。OptionType#color? → color_swatch?。
  • 使用 Spree::Product.for_store(store),而不是 Spree::Product.all——在作业/测试中,绝不要假设 Spree::Store.default 存在;请设置 Spree::Current.store。
  • 添加自定义产品属性:优先使用自定义字段。如果确实需要添加列,请添加该列,并通过 Spree::Product.additional_permitted_attributes += [:brand_id] 扩展许可参数(绝不要使用 <<——该数组已被冻结)。
  • Admin API 只通过带前缀的 ID 解析资源;Store API 接受 slug 或 ID。旧 slug 保存在 friendly_id_slugs 中,但 Store API 会对其返回 404——请在店面中设置重定向。
  • 卖家提交的产品:卖家无法写入状态;请使用提交/批准/拒绝流程——参见 spree-marketplace。

延伸阅读

  • node_modules/@spree/docs/dist/developer/core-concepts/products.md
  • node_modules/@spree/docs/dist/developer/core-concepts/media.md
  • node_modules/@spree/docs/dist/developer/core-concepts/metafields.md(自定义字段)
  • node_modules/@spree/docs/dist/developer/core-concepts/search-filtering.md、.../slugs.md、.../imports-exports.md
  • node_modules/@spree/docs/dist/developer/how-to/custom-search-provider.md
  • 相关技能:spree-pricing、spree-inventory、spree-b2b、spree-marketplace、spree-i18n、spree-typescript-sdk

评论

支持 Markdown