Files
atlas-seller-kit/docs/product/ozon-dynamic-attributes-and-sku-form-plan.md
T
2026-09-23 17:18:09 +08:00

227 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 卡片展开后的「SKU 信息」区域,与包装和文案字段共用布局,不单独显示「Ozon 变体属性」标题。每张 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 展开后的「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 或属性。