16 KiB
类目属性与字典值 API
官方文档:https://docs.ozon.ru/api/seller/zh/#operation/DescriptionCategoryAPI_GetAttributes
1. 获取类目属性
接口信息
| 项 | 值 |
|---|---|
| 方法 | POST |
| 路径 | /v1/description-category/attribute |
| 鉴权 | 需要 Client-Id + Api-Key |
| 用途 | 获取指定类目的所有属性(发布商品时需填写) |
请求
{
"description_category_id": 17033876,
"type_id": 97114,
"language": "RU"
}
参数说明
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| description_category_id | integer | ✅ 是 | 类目 ID(从类目树获取) |
| type_id | integer | ✅ 是 | 商品类型 ID(从类目树获取,与 category_id 配对) |
| language | string | 可选 | 语言代码:DEFAULT / RU / EN / ZH_HANS。默认 DEFAULT |
响应
{
"result": [
{
"id": 85,
"name": "Бренд",
"description": "Укажите бренд товара",
"type": "String",
"is_collection": false,
"is_required": true,
"is_aspect": false,
"max_value_count": 1,
"dictionary_id": 28732,
"category_dependent": false,
"group_id": 0,
"group_name": ""
},
{
"id": 8229,
"name": "Цвет товара",
"description": "",
"type": "String",
"is_collection": false,
"is_required": false,
"is_aspect": true,
"max_value_count": 1,
"dictionary_id": 61405,
"category_dependent": false,
"group_id": 1,
"group_name": "Варианты"
},
{
"id": 9048,
"name": "Объем",
"description": "Укажите объем в миллилитрах",
"type": "Integer",
"is_collection": false,
"is_required": true,
"is_aspect": false,
"max_value_count": 1,
"dictionary_id": 0,
"category_dependent": false,
"group_id": 2,
"group_name": "Основные"
}
]
}
字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
| id | integer | 属性 ID(发布时填 attributes[].id) |
| name | string | 属性名称(如"品牌"、"颜色") |
| description | string | 属性说明(填写提示) |
| type | string | 值类型:String / Integer / Decimal / Boolean / URL |
| is_required | boolean | 是否必填。true = 必须填写,否则发布失败 |
| is_aspect | boolean | 是否变体属性(如颜色/尺码)。true = 该属性用于区分 SKU 变体 |
| is_collection | boolean | 是否多值。true = 可填多个值(如"适用场景:家用,办公") |
| max_value_count | integer | 最多值数量(is_collection=true 时有效) |
| dictionary_id | integer | 字典 ID。> 0 = 有预设值字典(需调字典值接口),0 = 自由输入 |
| category_dependent | boolean | 字典值是否依赖类目(true = 不同类目的字典值不同) |
| group_id | integer | 属性分组 ID |
| group_name | string | 属性分组名(如"基本信息"、"变体") |
关键字段组合
| 组合 | 含义 | 示例 | 填写方式 |
|---|---|---|---|
is_required=true |
必填 | 品牌、尺寸、重量 | 必须有值,否则发布失败 |
dictionary_id > 0 |
有字典 | 品牌、颜色、材质 | 值必须从字典选(dictionary_value_id) |
dictionary_id = 0 |
自由输入 | 商品名、描述、数值 | 直接填文本/数字 |
is_aspect=true |
变体属性 | 颜色、尺码 | 用于区分 SKU(不同颜色 = 不同 SKU) |
is_collection=true |
多值 | 适用场景、材质组成 | 可传数组 ["值1", "值2"] |
2. 获取属性值字典
接口信息
| 项 | 值 |
|---|---|
| 方法 | POST |
| 路径 | /v1/description-category/attribute/values |
| 鉴权 | 需要 Client-Id + Api-Key |
| 用途 | 获取属性的预设值字典(dictionary_id > 0 的属性) |
请求
{
"attribute_id": 85,
"description_category_id": 17033876,
"type_id": 97114,
"language": "RU",
"limit": 1000,
"last_value_id": 0
}
参数说明
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| attribute_id | integer | ✅ 是 | 属性 ID |
| description_category_id | integer | ✅ 是 | 类目 ID |
| type_id | integer | ✅ 是 | 商品类型 ID |
| language | string | 可选 | 语言代码 |
| limit | integer | ✅ 是 | 每页数量,范围 1–2000;Atlas 当前首批请求使用 1000 |
| last_value_id | integer | 可选 | 分页游标(上一页最后一个值的 id),首页传 0 |
响应
{
"result": [
{
"id": 971082156,
"value": "Thermos",
"info": "",
"picture": ""
},
{
"id": 971317107,
"value": "Stanley",
"info": "",
"picture": ""
}
],
"has_next": true
}
字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
| id | integer | 字典值 ID(发布时填 attributes[].values[].dictionary_value_id) |
| value | string | 字典值文本(如品牌名"Thermos") |
| info | string | 补充说明 |
| picture | string | 值配图 URL(部分属性有,如颜色) |
| has_next | boolean | 是否有下一页(true = 用最后一个 id 继续分页) |
分页示例
async def fetch_all_values(attribute_id, category_id, type_id):
all_values = []
last_id = 0
while True:
resp = await client.post(
"https://api-seller.ozon.ru/v1/description-category/attribute/values",
json={
"attribute_id": attribute_id,
"description_category_id": category_id,
"type_id": type_id,
"language": "RU",
"limit": 2000,
"last_value_id": last_id
}
)
data = resp.json()
values = data.get("result", [])
all_values.extend(values)
if not data.get("has_next") or not values:
break
last_id = values[-1]["id"]
return all_values
3. 按关键词搜索属性值
接口信息
| 项 | 值 |
|---|---|
| 方法 | POST |
| 路径 | /v1/description-category/attribute/values/search |
| 鉴权 | 需要 Client-Id + Api-Key |
| 用途 | 模糊搜索字典值(避免拉全量字典) |
请求
{
"attribute_id": 85,
"description_category_id": 17033876,
"type_id": 97114,
"language": "RU",
"value": "Ther",
"limit": 100
}
参数说明
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| attribute_id | integer | ✅ 是 | 属性 ID |
| description_category_id | integer | ✅ 是 | 类目 ID |
| type_id | integer | ✅ 是 | 商品类型 ID |
| language | string | 可选 | 语言代码 |
| value | string | ✅ 是 | 搜索关键词(≥2 个字符) |
| limit | integer | 可选 | 返回数量,最大 100,默认 50 |
响应
{
"result": [
{
"id": 971082156,
"value": "Thermos",
"info": "",
"picture": ""
},
{
"id": 971982345,
"value": "Thermocafe",
"info": "",
"picture": ""
}
]
}
4. 属性映射工作流
场景:采集来的参数 → Ozon 属性
采集原文(raw.params):
[
{ "key": "Материал", "value": "Нержавеющая сталь" },
{ "key": "Объем", "value": "500 мл" },
{ "key": "Бренд", "value": "Thermos" }
]
↓ ① 自动匹配属性名
attributes = [
{ id: 8505, name: "Материал" }, // 材质
{ id: 9048, name: "Объем" }, // 容量
{ id: 85, name: "Бренд" } // 品牌
]
↓ ② 对有字典的属性(dictionary_id > 0),搜索字典值
POST /attribute/values/search {
attribute_id: 85, // 品牌
value: "Thermos"
}
→ { id: 971082156, value: "Thermos" }
↓ ③ 组装最终 attributes
products.attributes = [
{
"complex_id": 0,
"id": 8505,
"values": [{ "value": "Нержавеющая сталь" }] // 材质无字典,直接填
},
{
"complex_id": 0,
"id": 9048,
"values": [{ "value": "500" }] // 容量是数值,提取数字
},
{
"complex_id": 0,
"id": 85,
"values": [{ "dictionary_value_id": 971082156, "value": "Thermos" }] // 品牌有字典
}
]
5. 自动匹配策略
策略 A:归一化 + 模糊匹配
import re
from difflib import SequenceMatcher
def normalize(text: str) -> str:
"""归一化:小写 + 去标点 + 词干"""
text = text.lower().strip()
text = re.sub(r'[^\w\s]', '', text)
# 俄文词干化(需 pymorphy2 库)
# text = morph.parse(text)[0].normal_form
return text
def fuzzy_match(采集key: str, 属性列表: list, threshold=0.8):
"""模糊匹配:相似度 > 0.8 即认为匹配"""
norm_key = normalize(采集key)
best = None
best_score = 0
for attr in 属性列表:
norm_name = normalize(attr["name"])
score = SequenceMatcher(None, norm_key, norm_name).ratio()
if score > best_score:
best = attr
best_score = score
return best if best_score >= threshold else None
策略 B:关键词映射表
# 预定义常见映射(中文采集 key → Ozon 属性 name)
KEYWORD_MAP = {
"品牌": ["Бренд", "Brand"],
"材质": ["Материал", "Material"],
"重量": ["Вес", "Weight"],
"尺寸": ["Размер", "Size"],
"颜色": ["Цвет", "Color"],
# ... 补充更多
}
def keyword_match(采集key: str, 属性列表: list):
for cn_key, ru_keys in KEYWORD_MAP.items():
if cn_key in 采集key:
for attr in 属性列表:
if any(rk in attr["name"] for rk in ru_keys):
return attr
return None
6. 必填项校验
发布前校验
async def validate_required_attributes(
category_id: int,
type_id: int,
attributes: list
) -> list[str]:
"""返回缺失的必填属性名列表"""
# 获取该类目的所有属性
attrs = await fetch_category_attributes(category_id, type_id)
# 提取必填属性
required = [a for a in attrs if a["is_required"]]
# 已填写的属性 ID
filled_ids = {a["id"] for a in attributes}
# 找出缺失的
missing = [a["name"] for a in required if a["id"] not in filled_ids]
return missing
# 使用
missing = await validate_required_attributes(17033876, 97114, product.attributes)
if missing:
raise ValueError(f"缺少必填属性:{', '.join(missing)}")
7. 缓存策略
Ozon 只定义查询参数和分页规则,不规定 Atlas 的 TTL。Atlas 已按 2026-09-18 的架构决策将属性定义和字典候选值迁移为平台级参考数据:同一平台所有门店共用,本阶段不维护门店差异层。商品编辑器、草稿校验和发布预检都从 Atlas 平台缓存读取,Ozon 请求统一由 Worker 使用平台参考源凭据执行。
类型属性和字典默认 14 天内 fresh,14~21 天(含 21 天)stale 时旧缓存可用并低优先级刷新,严格超过 21 天或从未同步时必须高优先级刷新成功后才能作为当前 schema、候选值或发布依据。系统不周期性扫描属性和字典;刷新由管理员批次/单字典操作、普通用户访问或发布预检触发。
字典候选值至少以平台、类目类型、Atlas 属性 UUID、外部值 ID 和语言限定作用域。category_dependent=false 可以作为进一步去重的依据,但在跨账号/跨类目验证前不能只以 dictionary_id 合并。小字典可完整分页并标记 complete;大字典保存已同步页与搜索命中并明确标记 partial。搜索结果不是全集,不能据此清理未命中的旧值;partial 字典的规范化关键词有独立缓存状态。成功后原子更新缓存,失败保留上次成功值。
8. 前端交互设计
属性映射 UI(推荐)
┌─────────────────────────────────────────────────┐
│ 类目属性映射 │
├─────────────────────────────────────────────────┤
│ 采集属性 → Ozon 属性 │
├─────────────────────────────────────────────────┤
│ ✅ Материал (材质) → [自动] Материал (8505) │
│ 值:Нержавеющая сталь │
├─────────────────────────────────────────────────┤
│ ✅ Бренд (品牌) → [自动] Бренд (85) ⚠️必填 │
│ 值:Thermos → 字典值:[选择 ▼] │
│ ├ Thermos ✅ │
│ ├ Stanley │
│ └ ... │
├─────────────────────────────────────────────────┤
│ ⚠️ 未匹配:包装重量 │
│ → [手动选择属性 ▼] │
├─────────────────────────────────────────────────┤
│ ❌ 缺少必填属性: │
│ - Объем (容量) [+添加] │
│ - Цвет (颜色) [+添加] │
└─────────────────────────────────────────────────┘
9. 常见问题
Q1: 属性太多怎么办?
A: 一个类目可能有 50-100+ 属性,但常用的只有 10-20 个。策略:
- 必填属性置顶 + 高亮
- 已匹配属性展开,未匹配折叠
- 提供搜索/筛选
Q2: 字典值有多大?
A:
- 小字典(颜色/材质):几十到几百条
- 大字典(品牌):数万条(如品牌字典 > 50,000)
- 策略:优先用
/values/search,避免拉全量
Q3: 自由输入的属性怎么填?
A: dictionary_id=0 的属性直接填 value,无需 dictionary_value_id:
{
"id": 9048,
"values": [{ "value": "500" }] // 容量,数值类型
}
Q4: 如何处理多值属性?
A: is_collection=true 时传数组:
{
"id": 10096,
"values": [
{ "value": "家用" },
{ "value": "办公" }
]
}
10. Atlas 当前集成
- 商品编辑器通过
GET /api/product-platform-profiles/{profile_id}/attribute-schema读取当前平台类型属性,响应包含 Atlasattribute_id和统一cache元数据。 - 字典通过
GET /api/product-platform-profiles/{profile_id}/attribute-dictionaries/{attribute_id}读取;attribute_id是平台属性 UUID,不以门店或单独dictionary_id定位。 - fresh/stale 立即返回;expired/missing 创建或复用高优先级任务并最多等待 5 秒,仍未完成时返回
202。浏览器不会拿到平台凭据或未落库的 Ozon 临时结果。 - 发布前使用
POST /api/product-platform-profiles/{profile_id}/publish-preflights,只刷新当前类型 schema 和所选 SKU 实际使用的字典,再执行必填、scope、字典有效性及其他首次上架校验。 - 管理后台支持三种类型属性批次、单字典同步、缓存策略弹窗和最近任务观察;类目树固定每 7 天同步,属性与字典不做周期性全量扫描。
相关文档
- 02-category-tree.md —— 获取类目树
- 04-product-import.md —— 发布商品(使用属性)
- 平台目录缓存刷新方案 —— 属性和字典缓存状态机
- 商品编辑与发布 API —— 当前 Atlas 端点与发布预检