# V2 Ozon 发布集成 > 状态:方案设计(待确认) > 上游:[V2 总览](./README.md) · [V2 架构](./architecture.md) · [数据库](./database.md) · [API](./api.md) > 官方文档:[Ozon Seller API(中文)](https://docs.ozon.ru/api/seller/zh/) --- ## 1. 鉴权与店铺凭证 Ozon Seller API 用 **两个请求头** 鉴权(不是 OAuth): ``` Client-Id: <你的 Client ID> Api-Key: <你的 API Key> ``` - 获取:Ozon 卖家后台 → 设置 → Seller API → 生成 Key(可选权限级别)。 - **凭证归属店铺**:V2 里每店铺一条 `shops` 记录,`client_id`/`api_key` 加密落库,调用时解密拼头。 - **连通性校验**:`POST /api/shops/:id/test` 调 [`/v1/roles`](https://docs.ozon.ru/api/seller/zh/#operation/AccessAPI_RolesByToken)(返回该 key 的角色与可用方法),既验证凭证又看权限范围,零成本。 服务端封装 `services/ozon_client.py`:统一 base URL(`https://api-seller.ozon.ru`)、拼头、超时、错误映射(400/403/409/500 → 语义化 detail)、限流退避。 --- ## 2. 核心接口(本项目用到) | 用途 | 方法 | 说明 | |---|---|---| | 类目树 | `POST /v1/description-category/tree` | 返回 `description_category_id / type_id / category_name / type_name / disabled / children`;**只有末级类目可建品** | | 类目属性 | `POST /v1/description-category/attribute` | 入参 `description_category_id + type_id`;返回属性含 `is_required / is_aspect / is_collection / dictionary_id / type / max_value_count` | | 属性值字典 | `POST /v1/description-category/attribute/values` | 入参 `attribute_id + category_id + type_id + limit(≤2000) + last_value_id`(分页) | | 属性值搜索 | `POST /v1/description-category/attribute/values/search` | 按 `value` 模糊匹配参考值(≥2 字符,limit≤100) | | **发布/更新商品** | `POST /v3/product/import` | 一次 ≤100 个 item;返回 `task_id` | | **发布状态** | `POST /v1/product/import/info` | 入参 `task_id`;返回 `items[{offer_id, product_id, status, errors[]}]` | | 商品列表/回填 | `POST /v3/product/list` | 用 `offer_id/product_id` 过滤取 `product_id`,或分页拉全部 | | 跟卖复制 PDP | `POST /v1/product/import-by-sku` | 入参 `sku + name + offer_id + price...`;返回 `task_id + unmatched_sku_list` | | 图片更新 | `POST /v1/product/pictures/import` | 按 `product_id` 覆盖 `images/images360/color_image` | | 商品详情(含图片/审核错误) | `POST /v3/product/info/list` | 回读已发布商品的图片/状态/错误 | --- ## 3. 类目与属性字典(采集属性 → Ozon 属性的关键) ### 3.1 数据流 ``` 类目树(全局缓存) └─ 用户选类目 → 得 description_category_id + type_id └─ 拉该类目属性(按 category+type 缓存) └─ 对每个「有字典」的属性,按需拉值(/values 或 /values/search) ``` - **类目树全局缓存**:与店铺无关(虽然接口要凭证),服务端拉一次存 `category_tree` 表 + 内存 LRU,TTL 24h。 - **属性按类目缓存**:`category_attributes` 表,按 `(category_id, type_id)` 缓存。 - **属性值按需拉取**:值目录可能非常大,只在用户映射到某个属性时才拉,且用 `/values/search`(按关键词搜)而非全量拉。 ### 3.2 属性映射(采集的 `raw.params` → Ozon `attributes[]`) 这是发布链路**最重的工作**。流程: ``` 采集 raw.params: [{key:"Материал", value:"Нержавеющая сталь"}, …] │ ① 自动匹配:key 与属性 name 模糊匹配(归一化 + 词干) │ ② 有字典的属性:value 去 /values/search 找 dictionary_value_id ▼ 属性映射 UI:自动匹配结果 + 人工确认未匹配项 + 必填项高亮 ▼ products.attributes = [{complex_id:0, id, values:[{dictionary_value_id, value}]}] ``` **必填项校验**:服务端在「ready 校验」和「发布前」两次校验:`category_attributes` 里 `is_required=true` 的属性必须已映射,否则阻断发布并指出缺哪些。 --- ## 4. 发布请求体组装(对齐 ImportProductsV3) `items[0]` 字段(已核对官方示例): | 字段 | 来源 | 说明 | |---|---|---| | offer_id | `products.offer_id` | **自己的货号**,跟卖不能用竞品的 | | name / description | `products.name/description`(俄文) | | | description_category_id / type_id | `products.*` | 从类目树选 | | price / old_price / currency_code / vat | `products.*` | currency 须与店铺设置一致(默认 RUB) | | depth/width/height/dimension_unit/weight/weight_unit | `products.*` | **必填且不能为 0**(官方硬约束) | | barcode | `products.barcode` | 可选 | | images | `products.images`(七牛 URL,≤15) | 顺序即展示顺序;首张为主图。**必须 https 直链**(实测 Ozon 不接受 http,见 §7) | | primary_image | `products.primary_image` | 用 primary_image 则 images ≤14 | | images360 / color_image | `products.*` | 可选 | | attributes | `products.attributes` | 映射结果 | | complex_attributes | `products.complex_attributes` | 视频/尺码表等 | | pdf_list / promotions | 可选 | 一般留空 | **跟卖场景可选优化**:若竞品允许复制 PDP,走 `/v1/product/import-by-sku`(只需 sku + 基本信息),更快且继承竞品详情——但受「卖家是否允许复制」限制,且不能更新,故作为**可选快捷路径**,主路径仍是 `import`。 --- ## 5. 发布状态机与轮询 `/v3/product/import` 是异步的,返回 `task_id`。流程: ``` POST /v3/product/import → { task_id } │ 建 publish_tasks(status=pending, ozon_task_id) ▼ 后台协程轮询 POST /v1/product/import/info { task_id } │ items[0].status ∈ imported | moderation | failed(+errors[]) ▼ imported → products.stage=published, ozon_product_id=items[0].product_id moderation → products.stage=publishing(继续轮询,通常 <1 天) failed → products.stage=failed, publish_tasks.errors=items[0].errors ``` - 轮询间隔:先 5s,退避到 30s;`moderation` 状态降低频率到分钟级。 - 用 `/v3/product/list`(filter by offer_id)回读 `product_id` 兜底(轮询遗漏时)。 - **上架还需设置库存**:`import` 成功后商品进入后台但不自动上架(`architecture.md` 与官方文档均明确「只有设置库存后才开售」)。V2 一期发布到「已创建/审核」即可,库存设置(`/v2/products/stocks`)作为二期可选,或提示用户去后台补库存。 --- ## 6. CSV 导出字段 服务端 `GET /api/export/products.csv`,带 BOM 的 UTF-8,Excel 直接打开不乱码。字段: ``` offer_id, product_id, name, description_category_id, type_id, price, old_price, currency_code, vat, weight, weight_unit, depth, width, height, dimension_unit, barcode, primary_image, images, source_platform, source_item_id, source_url, stage, published_at, created_at, updated_at ``` - `images` 用 `|` 拼接七牛 URL。 - 未发布商品 `product_id` 为空。 - 支持筛选 `stage`(collected/ready/published/failed/全部)与 `ids`(勾选导出)。 > 对齐 V1:v1 登记表导出的 `sku + 卢布预留价` 组合码,V2 里 `offer_id + price`(卢布)即等价物;若要完全兼容 v1 组合码,可加一列 `combo`(`offer_id + 卢布预留价`)。 --- ## 7. 错误处理与限流 | Ozon 错误 | 含义 | 处理 | |---|---|---| | 400 Invalid parameter | 参数错误 | 把 detail 透传前端,定位字段 | | 403 Access denied | 权限不足 | 提示检查 Api-Key 权限级别 | | 409 Request conflict | 冲突(如 offer_id 重复) | 提示改 offer_id 或走更新 | | 429 / 限流 | 频率超限 | 指数退避重试 | | `item_limit_exceeded` | 超过当日建/更新商品限额 | 提示限额,可查 `/v4/product/info/limit` | `publish_tasks.errors` 完整保存 Ozon 返回的 errors 数组,前端发布结果页展示中文解读。 --- ## 8. 店铺绑定交互 1. 店铺管理页「新增店铺」:填 `名称 + Client ID + API Key + 结算币种`。 2. 点「测试连接」→ `/api/shops/:id/test` → 调 `/v1/roles` → 显示 `ok` 与角色列表,或失败原因(凭证错/权限不足/网络)。 3. 保存后 `client_id` 只显示尾号打码(如 `…1234`),key 永不回显。 4. 发布时从店铺下拉选择目标店铺。 --- ## 9. 一期范围 vs 二期 | 能力 | 一期 | 二期 | |---|---|---| | 店铺绑定 + 连通性校验 | ✅ | | | 类目树 + 属性 + 值字典(缓存) | ✅ | | | 属性映射 UI(自动 + 人工) | ✅ 基础版 | 智能匹配优化 | | `/v3/product/import` 发布 + 轮询回填 | ✅ | | | `/v1/product/import-by-sku` 跟卖复制 | 🟡 可选 | | | 库存设置(上架) | ❌(提示去后台) | `/v2/products/stocks` | | 价格/库存批量更新 | ❌ | `/v1/product/import/prices`、`/v2/products/stocks` | | 图片更新(换图) | ❌ | `/v1/product/pictures/import` |