8.9 KiB
8.9 KiB
类目树查询 API
官方文档:https://docs.ozon.ru/api/seller/zh/#operation/DescriptionCategoryAPI_GetTree
接口信息
| 项 | 值 |
|---|---|
| 方法 | POST |
| 路径 | /v1/description-category/tree |
| 鉴权 | 需要 Client-Id + Api-Key |
| 用途 | 获取 Ozon 商品类目树(选择类目后才能发布商品) |
请求
请求体
{
"language": "RU"
}
参数说明
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| language | string | 可选 | 语言代码,可选值:DEFAULT(英文)、RU(俄文)、EN(英文)、ZH_HANS(简体中文)。默认 DEFAULT |
响应
成功响应(200)
{
"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”。
关键约束
- 只有可发布的末级类目可建品:选择结果必须是没有
children且disabled=false的节点 - 必须配对使用:发布时需同时提供
description_category_id+type_id - 层级结构:
children是递归结构,类目可有三级或更深层级;不能假定只有两层,也不能只展开一层
使用场景
场景 1:前端类目选择器
① 请求类目树(language=RU,给俄文用户看)
② 递归展开树形结构
③ 所有有子节点的目录都保持可展开;不要把目录节点映射为组件的 disabled
④ 仅允许选择没有子节点且 disabled=false 的节点
⑤ 选中后保存 description_category_id + type_id
场景 2:服务端门店缓存
① 门店 `Client-Id` + `Api-Key` 验证成功后拉取完整类目树(Atlas 使用 `ZH_HANS`)
② 递归保存每一层及其父子关系,按门店存入本地类目缓存;不与其他门店混用
③ 同步失败由后台任务重试,已验证的门店凭据不因此改为无效
④ 用户选类目时直接查当前目标门店缓存,不频繁调 API
示例代码
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(前端选择器)
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 方案)
# 类目数据的读取凭证属于门店。即使结构通常相近,也必须按 store_id 隔离缓存。
# 验证成功或缓存失效时,由 Worker 用该门店凭证拉取完整树并递归写库。
# 前端只读取该门店的缓存,绝不携带或读取 Client-Id / Api-Key。
async def get_store_category_tree(store_id: str):
cached = await load_store_category_cache(store_id)
if cached and cached.is_valid:
return cached.tree
await enqueue_store_catalog_sync(store_id)
raise CatalogCachePreparingError(store_id)
按需更新
# 用户在门店管理中手动刷新时,API 同步请求 Ozon 并在成功后原子替换目标门店缓存。
# 请求或落库失败会保留旧缓存并向用户返回错误,不经 Worker 队列。
async def refresh_store_category_tree(store_id: str):
tree = await fetch_category_tree_for_store(store_id)
return await replace_store_category_cache_atomically(store_id, tree)
常见问题
Q1: 类目树很大吗?
A: 约 1-2 万个类目节点,JSON 约 3-5MB。完整树在一次响应中以递归 children 返回,首次拉取需几秒,后续从门店缓存读取。
Q2: 多久更新一次?
A: Ozon 不定期新增类目(月级别),建议 24h TTL + 手动刷新入口。
Q3: 不同语言的类目树结构一样吗?
A: 通常 description_category_id / type_id 相同,仅 category_name / type_name 翻译不同。Atlas 统一由 Worker 为每个门店缓存 ZH_HANS,前端直接读取该门店缓存,不能为展示语言绕过缓存直接携带门店密钥请求 Ozon。
Q4: type_id=0 是什么意思?
A: 目录节点通常 type_id=0,但前端不能据此把所有目录设为 disabled,否则它们无法展开。是否允许提交必须同时判断:它是叶子节点、disabled=false,并且有可用的 type_id。
Atlas 当前集成
- Worker:以门店凭证调用 Ozon,递归持久化完整
children树及父子关系;缓存命中时不重复调用 Ozon。 - API:按当前目标门店返回缓存的扁平节点(包含
parent_id);不会将Client-Id或Api-Key返回给浏览器。 - 前端:根据
parent_id重建任意深度的 Cascader;目录可展开,只有可发布的叶子能写入商品草稿。 - 调试:若需要查看 Ozon 原始响应,应由受权限保护的服务端调试能力获取并脱敏展示;浏览器不得直接用门店密钥请求 Ozon。
相关文档
- 03-category-attributes.md —— 获取类目属性
- docs/v2/database.md §2.7 ——
category_tree表结构 - docs/v2/ozon-publish.md §3 —— 类目字典缓存策略