259 lines
9.4 KiB
Markdown
259 lines
9.4 KiB
Markdown
# 类目树查询 API
|
||
|
||
> 官方文档:https://docs.ozon.ru/api/seller/zh/#operation/DescriptionCategoryAPI_GetTree
|
||
|
||
---
|
||
|
||
## 接口信息
|
||
|
||
| 项 | 值 |
|
||
|---|---|
|
||
| 方法 | POST |
|
||
| 路径 | `/v1/description-category/tree` |
|
||
| 鉴权 | 需要 `Client-Id` + `Api-Key` |
|
||
| 用途 | 获取 Ozon 商品类目树(选择类目后才能发布商品) |
|
||
|
||
---
|
||
|
||
## 请求
|
||
|
||
### 请求体
|
||
|
||
```json
|
||
{
|
||
"language": "RU"
|
||
}
|
||
```
|
||
|
||
### 参数说明
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|---|---|---|---|
|
||
| language | string | 可选 | 语言代码:`DEFAULT`(默认俄文)、`RU`(俄文)、`EN`(英文)、`TR`(土耳其文)、`ZH_HANS`(简体中文) |
|
||
|
||
---
|
||
|
||
## 响应
|
||
|
||
### 成功响应(200)
|
||
|
||
```json
|
||
{
|
||
"result": [
|
||
{
|
||
"description_category_id": 17000001,
|
||
"category_name": "Дом и сад",
|
||
"type_id": 0,
|
||
"type_name": "",
|
||
"disabled": true,
|
||
"children": [
|
||
{
|
||
"description_category_id": 17000002,
|
||
"category_name": "Сувениры и подарки",
|
||
"type_id": 0,
|
||
"type_name": "",
|
||
"disabled": true,
|
||
"children": [
|
||
{
|
||
"description_category_id": 17032010,
|
||
"category_name": "Копилки",
|
||
"type_id": 93770,
|
||
"type_name": "Копилка",
|
||
"disabled": false,
|
||
"children": []
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
### 字段说明
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|---|---|---|
|
||
| description_category_id | integer | **类目 ID**(发布商品时必填) |
|
||
| category_name | string | 类目名称 |
|
||
| type_id | integer | **商品类型 ID**(发布商品时必填,与 category_id 配对) |
|
||
| type_name | string | 商品类型名称 |
|
||
| disabled | boolean | **是否可建品**。`true` = 当前节点不可用于建品;`false` = 可用于建品(通常是末级类目) |
|
||
| children | array | 子类目(递归结构) |
|
||
|
||
> **类型叶子例外**:Ozon 的最后一层可能只返回 `type_name`、`type_id`、`disabled` 和空 `children`,而不重复返回 `description_category_id`、`category_name`。此时它属于最近的父类目:缓存时应继承父类目的类目 ID,展示名称使用 `type_name`,发布时使用“父类目 `description_category_id` + 叶子 `type_id`”。
|
||
|
||
---
|
||
|
||
## 关键约束
|
||
|
||
1. **只有可发布的末级类目可建品**:选择结果必须是没有 `children` 且 `disabled=false` 的节点
|
||
2. **必须配对使用**:发布时需同时提供 `description_category_id` + `type_id`
|
||
3. **层级结构**:`children` 是递归结构,类目可有三级或更深层级;不能假定只有两层,也不能只展开一层
|
||
|
||
---
|
||
|
||
## 使用场景
|
||
|
||
### 场景 1:前端类目选择器
|
||
|
||
```
|
||
① 请求类目树(language=RU,给俄文用户看)
|
||
② 递归展开树形结构
|
||
③ 所有有子节点的目录都保持可展开;不要把目录节点映射为组件的 disabled
|
||
④ 仅允许选择没有子节点且 disabled=false 的节点
|
||
⑤ 选中后保存 description_category_id + type_id
|
||
```
|
||
|
||
### 场景 2:Atlas 平台级参考缓存(目标架构,未实施)
|
||
|
||
```
|
||
① 按平台的定时任务用 Atlas 专用参考源凭据拉取完整类目树(Ozon 首期 `ZH_HANS`)
|
||
② 原子更新该平台共享目录;同平台门店读取同一份树,不建门店覆盖层
|
||
③ 同步失败保留上次成功版本,由后台任务重试并告警
|
||
④ 用户选类目时只查 Atlas 缓存,不频繁调 Ozon API
|
||
```
|
||
|
||
---
|
||
|
||
## 示例代码
|
||
|
||
### Python(服务端缓存)
|
||
|
||
```python
|
||
import httpx
|
||
from typing import List, Dict, Any
|
||
|
||
async def fetch_category_tree(
|
||
client_id: str,
|
||
api_key: str,
|
||
language: str = "DEFAULT"
|
||
) -> List[Dict[str, Any]]:
|
||
"""拉取类目树并返回扁平化列表"""
|
||
headers = {
|
||
"Client-Id": client_id,
|
||
"Api-Key": api_key,
|
||
"Content-Type": "application/json"
|
||
}
|
||
|
||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||
resp = await client.post(
|
||
"https://api-seller.ozon.ru/v1/description-category/tree",
|
||
headers=headers,
|
||
json={"language": language}
|
||
)
|
||
resp.raise_for_status()
|
||
data = resp.json()
|
||
|
||
# 递归扁平化;不要限制层级数。
|
||
def flatten(nodes: List[Dict], level: int = 0, parent_id: int = 0):
|
||
flat = []
|
||
for node in nodes:
|
||
flat.append({
|
||
"description_category_id": node["description_category_id"],
|
||
"parent_id": parent_id,
|
||
"category_name": node["category_name"],
|
||
"type_id": node["type_id"],
|
||
"type_name": node["type_name"],
|
||
"disabled": node["disabled"],
|
||
"level": level,
|
||
"lang": language
|
||
})
|
||
if node.get("children"):
|
||
flat.extend(flatten(
|
||
node["children"],
|
||
level + 1,
|
||
node["description_category_id"]
|
||
))
|
||
return flat
|
||
|
||
return flatten(data.get("result", []))
|
||
```
|
||
|
||
### TypeScript(前端选择器)
|
||
|
||
> 以下嵌套 `CategoryNode` 和 `/api/categories/tree` 是早期资料中的概念示例,不是当前 Atlas 联调契约。当前商品编辑器从 `GET /api/product-platform-profiles/{profile_id}/category-options` 读取带 `parent_id` 的平台级扁平节点,再在前端重建树;请以[商品编辑与发布 API](../../api/product-editor-and-publishing.md)为准。
|
||
|
||
```typescript
|
||
interface CategoryNode {
|
||
description_category_id: number;
|
||
category_name: string;
|
||
type_id: number;
|
||
type_name: string;
|
||
disabled: boolean;
|
||
children: CategoryNode[];
|
||
}
|
||
|
||
async function fetchCategoryTree(language = 'RU'): Promise<CategoryNode[]> {
|
||
const resp = await fetch('/api/categories/tree?lang=' + language);
|
||
const data = await resp.json();
|
||
return data.result;
|
||
}
|
||
|
||
// 转为 antd Cascader 数据结构。目录永远不能设为 disabled,
|
||
// 否则用户无法继续展开到下一级。
|
||
function toTreeData(nodes: CategoryNode[]): any[] {
|
||
return nodes.map(node => {
|
||
const hasChildren = node.children.length > 0;
|
||
return {
|
||
value: `${node.description_category_id}-${node.type_id}`,
|
||
label: node.category_name,
|
||
isLeaf: !hasChildren,
|
||
// 仅不可发布的叶子不可选择;目录节点必须可展开。
|
||
disabled: !hasChildren && node.disabled,
|
||
children: hasChildren ? toTreeData(node.children) : undefined,
|
||
data: {
|
||
description_category_id: node.description_category_id,
|
||
type_id: node.type_id
|
||
}
|
||
};
|
||
});
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 缓存策略
|
||
|
||
Atlas 已按 [`platform-product-reference-catalog.md`](../../architecture/platform-product-reference-catalog.md) 为每个平台分别维护共享目录:定时自动同步与系统管理员手动触发共用异步任务,失败保留上次成功版本;商品编辑器只读 Atlas 平台缓存。同平台的门店不再各自复制类目树,本阶段也不维护门店差异层。
|
||
|
||
---
|
||
|
||
## 常见问题
|
||
|
||
### Q1: 类目树很大吗?
|
||
|
||
**A**: 完整树在一次响应中以递归 `children` 返回;具体节点数和响应大小以实际同步记录为准,后续从 Atlas 缓存读取。
|
||
|
||
### Q2: 多久更新一次?
|
||
|
||
**A**: Ozon 接口未规定 Atlas 的缓存 TTL。Atlas 固定在最近一次成功后 7 天自动同步完整树,并提供系统管理员手动排队同步入口;失败不会更新成功时间或清空旧树。
|
||
|
||
### Q3: 不同语言的类目树结构一样吗?
|
||
|
||
**A**: 接口支持语言参数,不应未经验证就假定各语言的 ID 和翻译映射完全相同。当前 Worker 使用 Ozon 平台参考源统一缓存 `ZH_HANS`,同平台所有门店共用。前端不能携带平台或门店密钥直连 Ozon。
|
||
|
||
### Q4: `type_id=0` 是什么意思?
|
||
|
||
**A**: 目录节点通常 `type_id=0`,但前端不能据此把所有目录设为 disabled,否则它们无法展开。是否允许提交必须同时判断:它是叶子节点、`disabled=false`,并且有可用的 `type_id`。
|
||
|
||
---
|
||
|
||
## Atlas 当前集成
|
||
|
||
- **Worker**:使用系统后台配置的平台参考源凭据调用 Ozon,递归持久化完整 `children` 树及父子关系;同一平台活动任务按作用域合并。
|
||
- **API**:按当前平台返回缓存的扁平节点(包含 `parent_id`);不会将 `Client-Id` 或 `Api-Key` 返回给浏览器。
|
||
- **前端**:根据 `parent_id` 重建任意深度的 Cascader;目录可展开,只有可发布的叶子能写入商品草稿。
|
||
- **调试**:若需要查看 Ozon 原始响应,应由受权限保护的服务端调试能力获取并脱敏展示;浏览器不得直接用门店密钥请求 Ozon。
|
||
- **同步维护**:完整树每 7 天自动同步,也可由系统管理员手动触发;树任务不自动展开为所有类型属性或字典任务。
|
||
- **发布边界**:当前发布预检校验所选本地类目仍活动,但尚未对树级新鲜度创建刷新任务;该边界需在真正发布 job 接入前确认或补齐。
|
||
|
||
---
|
||
|
||
## 相关文档
|
||
|
||
- [03-category-attributes.md](./03-category-attributes.md) —— 获取类目属性
|
||
- [平台商品参考目录](../../architecture/platform-product-reference-catalog.md) —— 平台级数据归属
|
||
- [平台目录缓存刷新方案](../../architecture/platform-catalog-cache-refresh-plan.md) —— 每周树同步和属性/字典按需刷新
|
||
- [商品编辑与发布 API](../../api/product-editor-and-publishing.md) —— 编辑器类目读取和发布预检
|