176 lines
8.9 KiB
Markdown
176 lines
8.9 KiB
Markdown
# 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` |
|