Files
atlas-seller-kit/docs/product/ozon-dynamic-attributes-and-sku-form-plan.md
T

209 lines
12 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 实现,P1/P2 待实施。当前已完成必填展示、通用/变体属性分区、宽松草稿保存及服务端逐 SKU 校验;新 SKU 首次保存时关联属性、结构化字典值与发布闭环仍以后续阶段为准。
## 1. 目标与原则
Ozon 商品的必填属性由 `description_category_id` 与 `type_id` 对应的类目 schema 决定;同一 Ozon 平台的所有门店共用该 schema。编辑器不得为儿童玩具、服装、食品等类目分别硬编码字段,而应使用 Ozon 返回的 `is_required`、`is_aspect`、`dictionary_id`、`is_collection`、`max_value_count`、`type` 与复杂属性信息驱动展示、填报和发布校验。当前编辑器已经读取平台级类目、属性和字典缓存;发布链路迁移仍待后续步骤。
页面继续以 Atlas 商品和 SKU 为编辑主体:主要信息保留稳定的商品级字段,Ozon 类目属性按通用属性和 SKU 变体属性拆分。保存草稿允许信息不完整;首次上架必须由服务端按当时有效的类目 schema 重新校验。
## 2. 页面字段归属
### 2.1 主要信息
以下字段必须继续放在「主要信息」区,不随动态属性区迁移:
- 上架店铺;
- 产品类目;
- 标题;
- 型号;
- 品牌;
- 默认包装重量与默认包装尺寸。
默认包装规格是父商品级默认值。每个 SKU 默认继承该值;当某个 SKU 的包装不同,可在 SKU 卡片中设置包装重量和包装尺寸覆盖值。SKU 未填写覆盖值不表示缺失,而是继续继承主要信息中的默认包装规格。
品牌和型号既是主要信息,也是可供采集、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`,不得扁平化为普通属性。
例如,儿童玩具类目把「最小儿童年龄」和「最大儿童年龄」标为必填时,两个字段应直接显示在产品属性区,并在各自 Label 前显示红色 `*`。如果它们是字典属性,必须选择 Ozon 返回的年龄值,而不是任意输入文本。
### 2.3 SKU 设置
Ozon schema 中 `is_aspect=true` 的属性放入每个 SKU 卡片的「Ozon 变体属性」区域,不再按 SKU 名称集中堆放在产品属性区。每张 SKU 卡片包含:
- SKU 名称与当前平台上架状态;
- 卖家货号 `offer_id`;
- SKU 条形码;
- Ozon 变体属性,例如颜色、尺码;
- 采购价、售价和划线价;
- SKU 图片;
- 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. 优先读取当前门店的本地 schema 缓存。
2. 缓存不存在或超过约定有效期时,创建异步同步任务并显示「正在更新 Ozon 类目要求」。
3. 同步成功后重新构建动态属性表单。
4. 页面显示类目路径、商品类型和最后同步时间,并提供失败后的重试入口。
以上描述当前门店级 schema 的获取流程。迁移到平台级参考目录后,直接读取当前平台共享 schema,不叠加门店差异;普通用户只请求 Atlas,Atlas 在缓存 stale、expired 或 missing 时创建/复用统一 Worker 任务,不由 API 进程直连 Ozon。14~21 天的旧缓存立即返回并低优先级刷新,严格超过 21 天或未缓存时等待高优先级任务;完整规则见[平台目录缓存刷新方案](../architecture/platform-catalog-cache-refresh-plan.md)。接口字段及草稿外键在 `docs/api/` 更新前不改变。
属性 schema 响应除现有字段外,计划补充:
```ts
type ProductAttributeSchema = {
platform_attribute_id: string;
complex_id: string;
name: string;
description: string | null;
group_id: string | null;
group_name: string | null;
value_type: 'text' | 'integer' | 'decimal' | 'boolean' | 'dictionary';
is_required: boolean;
is_collection: boolean;
max_value_count: number;
dictionary_id: string | null;
is_aspect: boolean;
synced_at: string;
};
```
切换类目后,新 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 返回 `not_publishable_again`,不作为普通字段缺失处理。
`POST /publish-jobs` 必须调用同一套校验,不能信任浏览器校验结果。保存草稿和创建发布任务应继续保持各自的事务与幂等边界。
## 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 校验结果。
- 按 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 或属性。