227 lines
15 KiB
Markdown
227 lines
15 KiB
Markdown
# Ozon 动态属性与 SKU 填报改造方案
|
||
|
||
> 状态:P0 已于 2026-09-18 实现,平台目录读取和首次发布预检已于 2026-09-20 迁移到平台级缓存;P1 与真正调用 Ozon 的发布 job 待实施。当前已完成必填展示、通用/变体属性分区、宽松草稿保存、服务端逐 SKU 校验和发布前定向刷新;新 SKU 首次保存时关联属性、结构化字典值、发布快照与 Ozon 错误回填仍以后续阶段为准。
|
||
|
||
## 1. 目标与原则
|
||
|
||
Ozon 商品的必填属性由 `description_category_id` 与 `type_id` 对应的类目 schema 决定;同一 Ozon 平台的所有门店共用该 schema。编辑器不得为儿童玩具、服装、食品等类目分别硬编码字段,而应使用 Ozon 返回的 `is_required`、`is_aspect`、`dictionary_id`、`is_collection`、`max_value_count`、`type` 与复杂属性信息驱动展示、填报和发布校验。唯一的编辑器业务例外是名为“商品颜色”的 SKU 字典属性:即使 schema 标记为单值或选填,也按多选控件、最多 100 个值和上架必填处理。当前编辑器和首次发布预检均读取平台级类目、属性和字典缓存;真正创建发布 job 并调用 Ozon 的闭环仍待后续实现。
|
||
|
||
页面继续以 Atlas 商品和 SKU 为编辑主体:主要信息保留稳定的商品级字段,Ozon 类目属性按通用属性和 SKU 变体属性拆分。保存草稿允许信息不完整;首次上架必须由服务端按当时有效的类目 schema 重新校验。
|
||
|
||
## 2. 页面字段归属
|
||
|
||
### 2.1 主要信息
|
||
|
||
以下字段必须继续放在「主要信息」区,不随动态属性区迁移:
|
||
|
||
- 上架店铺;
|
||
- 产品类目;
|
||
- 标题;
|
||
- 型号;
|
||
- 品牌;
|
||
- 默认包装重量与默认包装尺寸。
|
||
|
||
默认包装规格是父商品级默认值。每个 SKU 默认继承该值;当某个 SKU 的包装不同,可在 SKU 卡片中设置包装重量和包装尺寸覆盖值。SKU 未填写覆盖值不表示缺失,而是继续继承主要信息中的默认包装规格。包装重量和包装尺寸属于 Ozon 导入接口的固定物流字段,而不是由类目 schema 决定归属的动态属性:编辑器可以在重量、尺寸后分别提供“按 SKU 填写”的入口,但这只开启对应字段的 SKU 覆盖编辑,不应删除 SPU 默认值或把继承值冗余持久化到所有 SKU。
|
||
|
||
创建 Ozon 发布请求时,服务端必须为每个待发布 SKU 计算有效包装规格:SKU 覆盖值优先,否则取 SPU 默认值。每个 `/v3/product/import` 的 `items[]` 都要写入 `depth`(由 `length_mm` 映射)、`width`、`height`、`dimension_unit=mm`、`weight`(由 `shipping_weight_g` 映射)和 `weight_unit=g`;四个数值均必须存在且大于 0。该“逐 SKU 展开”发生在发布快照/payload 适配层,不要求草稿存储复制同一组 SPU 值。
|
||
|
||
品牌和型号既是主要信息,也是可供采集、AI 与多平台复用的 Atlas 标准字段。如果 Ozon 当前类目同时返回对应平台属性,发布适配器应建立显式映射并确保只有一个最终事实来源;字典型品牌必须由用户确认 Ozon 字典值,不能把自由文本直接冒充字典 ID。
|
||
|
||
### 2.2 产品属性
|
||
|
||
「产品属性」区展示当前 Ozon 类目中 `is_aspect=false` 的平台属性。展示规则如下:
|
||
|
||
- `is_required=true` 的属性默认展开并排在前面;选填属性可按分组折叠和搜索。
|
||
- 必填字段只使用表单标准样式:在 Label 前显示红色 `*`。不显示「Ozon 必填」标签,也不在区块标题展示完成数量。
|
||
- 属性名称使用 Ozon 返回的本地化名称,并展示 `description` 作为填写说明。
|
||
- 有字典的属性只能从 Ozon 字典值中选择;自由输入不能替代字典值。
|
||
- 多值属性限制为 `max_value_count`,单值属性只能提交一个值。
|
||
- 整数、十进制、布尔、文本和字典分别使用适合的输入控件。
|
||
- 复杂属性按 Ozon 的复杂属性结构分组展示,发布时进入 `complex_attributes`,不得扁平化为普通属性。
|
||
|
||
“商品净重”和“商品尺寸”不等同于 Ozon 的包装物流规格,也不能因为 SKU 存在而默认显示。它们只有在当前类目 schema 返回相应属性时才展示:`is_aspect=true` 放入 SKU,`is_aspect=false` 放入 SPU;是否为上架必填由 `is_required` 决定。Atlas 可继续保存实物规格供尺寸图等内部能力使用,但不能把它当作 Ozon 固定物流字段,也不得替代有效包装规格。
|
||
|
||
例如,儿童玩具类目把「最小儿童年龄」和「最大儿童年龄」标为必填时,两个字段应直接显示在产品属性区,并在各自 Label 前显示红色 `*`。如果它们是字典属性,必须选择 Ozon 返回的年龄值,而不是任意输入文本。
|
||
|
||
### 2.3 SKU 设置
|
||
|
||
Ozon schema 中 `is_aspect=true` 的属性放入每个 SKU 卡片的「Ozon 变体属性」区域,不再按 SKU 名称集中堆放在产品属性区。每张 SKU 卡片包含:
|
||
|
||
- SKU 名称与当前平台上架状态;
|
||
- 卖家货号 `offer_id`;
|
||
- SKU 条形码;
|
||
- Ozon 变体属性,例如颜色、尺码;
|
||
- 采购价、售价和划线价;
|
||
- SKU 图片;
|
||
- 当前 schema 要求或允许的 SKU 级商品净重/尺寸属性;
|
||
- 对父商品默认包装规格的可选覆盖。
|
||
|
||
SKU 中的 Ozon 必填变体属性同样只在 Label 前显示红色 `*`。父商品默认包装规格与 SKU 覆盖值应明确显示继承关系,例如「继承默认值:320 g」和「为此 SKU 单独设置」。
|
||
|
||
采集得到的 `specification_values` 仅作为跨平台的来源规格和预填建议,不直接作为 Ozon 发布属性。能可靠匹配时,可预填相应的 Ozon `is_aspect` 属性并标记为待确认;无法可靠匹配时保持为空。Ozon 变体属性是发布 payload 的唯一事实来源,避免 Atlas 规格轴和 Ozon 属性出现两套不一致值。
|
||
|
||
## 3. Schema 获取与展示状态
|
||
|
||
选择末级类目后,系统使用平台目录中的 `description_category_id + type_id` 获取属性 schema:
|
||
|
||
1. 优先读取当前平台共享的 Atlas schema 缓存;切换同平台目标门店不改变目录。
|
||
2. fresh 直接返回;stale(默认 14~21 天,含 21 天)返回旧缓存并创建/复用低优先级 Worker 任务。
|
||
3. expired(严格超过 21 天)或 missing 时创建/复用高优先级任务,API 最多等待 5 秒;仍未完成则返回 `202` 和 cache 元数据,前端轮询后重建表单。
|
||
4. 字典值使用 schema 返回的 Atlas `attribute_id` 精确限定平台、类型和属性作用域;部分字典的关键词未命中使用独立 `dictionary_search` 缓存状态。
|
||
5. 页面显示类目路径、商品类型和缓存状态,并提供失败后的重试入口。所有 Ozon 请求由 Worker 使用平台参考源凭据执行,浏览器和 API 进程不直连 Ozon。
|
||
|
||
完整规则见[平台目录缓存刷新方案](../architecture/platform-catalog-cache-refresh-plan.md)。当前属性 schema 契约为:
|
||
|
||
```ts
|
||
type ProductAttributeSchema = {
|
||
attribute_id: string;
|
||
platform_attribute_id: string;
|
||
complex_id: string;
|
||
name: string;
|
||
value_type: 'text' | 'integer' | 'decimal' | 'boolean' | 'dictionary';
|
||
is_required: boolean;
|
||
is_collection: boolean;
|
||
max_value_count: number;
|
||
dictionary_id: string | null;
|
||
is_aspect: boolean;
|
||
editor_field:
|
||
| 'category'
|
||
| 'brand'
|
||
| 'model'
|
||
| 'tags'
|
||
| 'rich_content'
|
||
| 'description'
|
||
| null;
|
||
};
|
||
```
|
||
|
||
响应外层同时返回 `cache.status`、`last_success_at`、`fresh_until`、`expires_at`、完整性、刷新任务和重试间隔。`editor_field` 只按 Ozon 属性 ID 绑定已有编辑器字段,不按名称或翻译文本猜测。
|
||
|
||
切换类目后,新 schema 中仍可匹配的值可以保留;不再匹配的旧属性进入未映射属性区,不能进入新的发布 payload,也不应仅由前端直接清空而丢失。
|
||
|
||
## 4. 属性值表达
|
||
|
||
草稿契约不再用同一个 `string[]` 同时表达自由文本和字典值,计划改为有判别字段的值结构:
|
||
|
||
```ts
|
||
type ProductAttributeValue =
|
||
| { kind: 'dictionary'; dictionary_value_id: string }
|
||
| { kind: 'scalar'; value: string };
|
||
```
|
||
|
||
服务端生成 Ozon payload 时遵守以下规则:
|
||
|
||
- 字典属性根据缓存中的 `dictionary_value_id` 解析对应显示值,不能信任客户端伪造的字典名称。
|
||
- 自由输入属性写入文本值,并按 Ozon 请求格式设置相应的空字典标识。
|
||
- 服务端校验字典值属于当前平台、当前类目类型和当前属性;门店不参与参考字典归属。
|
||
- 属性值数量、类型和复杂属性结构均由服务端复核。
|
||
|
||
## 5. 新增 SKU 与属性契约
|
||
|
||
当前属性草稿在顶层使用 `variant_id` 关联 SKU,新建或复制 SKU 在第一次保存前只有 `client_key`,因此无法在同一次编辑中稳定关联其 Ozon 变体属性。目标请求结构调整为:
|
||
|
||
```ts
|
||
type SaveEditorDraft = {
|
||
common_attributes: ProductAttributeDraft[];
|
||
variants: Array<{
|
||
id: string | null;
|
||
client_key: string | null;
|
||
offer_id: string | null;
|
||
barcode: string | null;
|
||
attributes: ProductAttributeDraft[];
|
||
// 其他 SKU 字段省略
|
||
}>;
|
||
};
|
||
```
|
||
|
||
这样新增 SKU 可以在一次原子保存中同时创建 SKU、保存条形码和 Ozon 变体属性,不需要先保存并刷新页面。该调整属于破坏性共享契约变更,实施时必须先更新 `docs/api/product-editor-and-publishing.md`,再同步修改 `@atlas/contracts`、API、数据库映射、Web 和测试。
|
||
|
||
SKU 条形码应保存到当前平台 listing 维度;现有商品级条形码可在迁移期作为默认值保留,但发布时必须解析出每个 SKU 的最终条形码。
|
||
|
||
## 6. 草稿校验与上架校验
|
||
|
||
### 6.1 保存草稿
|
||
|
||
保存草稿允许缺少 Ozon 必填属性、货号、价格、图片或完整包装规格。已经填写的值仍必须满足类型、数量、字典归属和 SKU 归属约束。页面可以提示缺失项,但不得因为动态必填属性为空而阻止用户保存进度。
|
||
|
||
因此,动态属性的红色 `*` 是上架要求提示,不直接等同于「保存草稿」按钮的阻断规则。未加载属性面板和已经加载属性面板时,保存行为必须一致。
|
||
|
||
### 6.2 首次上架
|
||
|
||
本地草稿就绪校验使用:
|
||
|
||
```text
|
||
GET /api/product-platform-profiles/{profile_id}/validation
|
||
```
|
||
|
||
该校验至少覆盖:
|
||
|
||
- 类目和 Ozon 商品类型有效;
|
||
- schema 已同步且未超过约定有效期;
|
||
- 所有通用必填属性有合法值;
|
||
- 每个待上架 SKU 的必填变体属性有合法值;
|
||
- 字典值、单/多值数量、数值类型与复杂属性结构有效;
|
||
- SKU 的 `offer_id`、条形码、价格、图片和有效包装规格符合发布要求;有效包装规格按“SKU 覆盖值 → SPU 默认值”解析,且在创建发布快照前必须可展开为该 SKU 的完整 Ozon 物流字段;
|
||
- 已上架 SKU 返回 `not_publishable_again`,不作为普通字段缺失处理。
|
||
|
||
`POST /publish-jobs` 必须调用同一套校验,不能信任浏览器校验结果。保存草稿和创建发布任务应继续保持各自的事务与幂等边界。
|
||
|
||
真正创建发布 job 前还必须调用已实现的:
|
||
|
||
```text
|
||
POST /api/product-platform-profiles/{profile_id}/publish-preflights
|
||
```
|
||
|
||
预检只处理用户选中的 `not_listed` / `failed` SKU,按当前平台类型 schema 和实际使用的字典执行 fresh/stale/expired/missing 判断:stale 使用旧缓存并低优先级刷新,expired/missing 高优先级刷新且所有任务共享一个 5 秒等待窗口。目录可用后再执行统一本地校验;返回 `ready` 才能进入未来的发布快照事务。当前预检尚未检查整棵类目树的新鲜度,只验证 profile 引用的本地类目仍活动;真正发布 job 接入前需要补齐或确认这一边界。
|
||
|
||
## 7. Ozon 发布错误回填
|
||
|
||
本地 schema 无法覆盖 Ozon 所有条件校验。Ozon 拒绝某个 import item 时,系统应:
|
||
|
||
- 通过 `offer_id` 定位 SKU;
|
||
- 在可能时通过 `attribute_id` 定位产品属性或 SKU 变体属性;
|
||
- 将错误展示在对应字段附近,并在发布任务中保存脱敏后的平台错误;
|
||
- 无法映射到字段的错误显示在对应 SKU 卡片顶部;
|
||
- 提供重新同步当前类目 schema 的操作。
|
||
|
||
发布任务需固化使用的类目、类型、schema 同步版本、属性值和最终 Ozon 请求摘要,保证失败排查和重试可审计。
|
||
|
||
## 8. 实施顺序与验收
|
||
|
||
### P0:展示与校验边界
|
||
|
||
- 必填属性默认展示,Label 前显示红色 `*`。
|
||
- 产品属性与 SKU 变体属性分区。
|
||
- 保存草稿允许不完整,发布校验阻断缺失项。
|
||
- 服务端补齐通用和逐 SKU 必填属性检查。
|
||
|
||
实现状态:已完成。编辑器会在选定类目后立即加载 schema,商品级必填属性始终显示在「产品属性」,SKU 级必填属性始终显示在对应 SKU 卡片;保存草稿不因缺少必填值失败,`validation` 接口按商品与 SKU 返回结构化缺失项。
|
||
|
||
类目 schema 中由编辑器已有控件承载的字段通过 `editor_field` 显式绑定,不在动态属性区重复展示,也不重复加载其字典。绑定只允许按 Ozon 属性 ID:品牌 `85`、型号名称 `9048`、类型 `8229`、主题标签 `23171`、JSON 富内容 `11254`、简介/产品描述 `4191`;禁止使用名称或翻译文本猜测。后续发布适配器必须据此生成 Ozon 所需字段,不能要求用户重复填写。
|
||
|
||
### P1:SKU 数据结构
|
||
|
||
- 属性草稿调整为 `common_attributes + variants[].attributes`。
|
||
- 新 SKU 使用 `client_key` 在首次保存时关联属性。
|
||
- 条形码迁到平台 SKU listing。
|
||
- Ozon 变体属性成为发布的唯一事实来源。
|
||
- 字典值改为结构化表达。
|
||
|
||
### P2:发布闭环
|
||
|
||
- 发布前定向刷新与 validation 复用已通过 `publish-preflights` 完成;真正发布接口必须调用同一服务。
|
||
- 按 SKU 生成 `/v3/product/import` 的 `items[]`。
|
||
- 正确生成普通属性与复杂属性。
|
||
- 固化发布快照并将 Ozon 错误回填到 SKU/属性。
|
||
- 增加 schema 过期、同步和重试状态。
|
||
|
||
验收至少覆盖以下场景:
|
||
|
||
1. 儿童玩具类目要求最小和最大儿童年龄时,两项均展示红色 `*`,缺少任一项不能上架。
|
||
2. 两个颜色 SKU 分别要求颜色,任一 SKU 缺值时只阻断该 SKU。
|
||
3. 新增或复制 SKU 无需先保存刷新,即可填写并保存其 Ozon 变体属性。
|
||
4. SKU 未覆盖包装规格时继承主要信息默认值,覆盖后只影响该 SKU。
|
||
5. 类目切换后旧属性不进入新 payload,同时保留为待处理的未映射数据。
|
||
6. 客户端伪造字典 ID 时服务端返回字段级 `422`。
|
||
7. 本地校验通过但 Ozon 拒绝时,错误能回填到对应 SKU 或属性。
|