关于 spree-catalog
spree-catalog 是介绍 Spree 6 电商目录数据建模与管理的参考技能,面向处理商品、变体、商品策划、自定义字段、媒体和店面搜索的开发者。
主要功能
- 01管理商品及可购买变体
- 02用类别和集合组织目录
- 03定义类型化自定义字段
- 04管理商品媒体与渠道可见性
- 05配置目录搜索和筛选
适用场景
- 01创建带变体和价格的商品
- 02按导航或商品策划分组
- 03添加商家维护的商品属性
- 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)
产品
| 属性 | 说明 |
|---|---|
status | draft(默认)、active、archived。只有 active 状态的产品会在店面显示。Marketplace 还会添加 proposed / rejected(卖家审核——请参阅 spree-marketplace)。 |
name、slug、description、meta_* | 可翻译(Mobility)。description 是富文本;API 返回 description(纯文本)和 description_html。 |
available_on、discontinue_on | 销售时间范围,叠加于 status 之上。 |
store | belongs_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) |
| URL | permalink = 完整路径,例如 clothing/shirts | permalink |
| 用途 | 导航 | 商品营销:“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 批量操作。
请逐项检查:
status == 'active'(外层门槛),且当前时间在available_on/discontinue_on范围内?- 属于此店铺?
Spree::Product.for_store(store).exists?(product.id) - 已在请求所用的渠道发布?
product.publication_for(channel)/Spree::Product.for_channel(channel);发布窗口已开启? - 请求货币中有基础价格?
product.default_variant.price_in('EUR').persisted?— 当前货币下没有价格的商品会被隐藏。 - B2B:买家的商品目录可能会限制可选商品(
Spree.products_for_context_service)— 参见spree-b2b。同一可见商品集合也会限制所有从购物者处获取变体的操作(例如愿望清单项目):列表中不会显示的变体也会被拒绝。 - 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.mdnode_modules/@spree/docs/dist/developer/core-concepts/media.mdnode_modules/@spree/docs/dist/developer/core-concepts/metafields.md(自定义字段)node_modules/@spree/docs/dist/developer/core-concepts/search-filtering.md、.../slugs.md、.../imports-exports.mdnode_modules/@spree/docs/dist/developer/how-to/custom-search-provider.md- 相关技能:
spree-pricing、spree-inventory、spree-b2b、spree-marketplace、spree-i18n、spree-typescript-sdk