14 KiB
14 KiB
商品信息查询 API
官方文档:https://docs.ozon.ru/api/seller/zh/#operation/ProductAPI_GetProductInfoListV3
接口信息
| 项 | 值 |
|---|---|
| 方法 | POST |
| 路径 | /v3/product/info/list |
| 鉴权 | 需要 Client-Id + Api-Key |
| 用途 | 查询商品详细信息(含审核状态、图片、属性、错误) |
1. 请求
请求体
{
"offer_id": ["MY-THERMOS-001", "MY-THERMOS-002"],
"product_id": [987654321],
"sku": [123456789]
}
参数说明
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| offer_id | array | 可选 | 自己的货号数组(最多 100 个) |
| product_id | array | 可选 | Ozon 商品 ID 数组(最多 100 个) |
| sku | array | 可选 | Ozon SKU 数组(最多 100 个) |
⚠️ 至少提供一个筛选条件(offer_id / product_id / sku)。
2. 响应
成功响应(200)
{
"result": {
"items": [
{
"id": 987654321,
"name": "Термокружка Thermos из нержавеющей стали 500 мл",
"offer_id": "MY-THERMOS-001",
"barcode": "",
"buybox_price": "2990.00",
"category_id": 17033876,
"created_at": "2024-08-10T10:30:00Z",
"images": [
{
"file_name": "thermos-main-1.jpg",
"default": true,
"index": 0
}
],
"marketing_price": "2990.00",
"min_price": "2690.00",
"old_price": "3490.00",
"premium_price": "2790.00",
"price": "2990.00",
"recommended_price": "2990.00",
"sources": [
{
"is_enabled": true,
"sku": 123456789,
"source": "fbs"
}
],
"state": "processed",
"stocks": {
"coming": 0,
"present": 100,
"reserved": 5
},
"errors": [],
"vat": "0.00",
"visible": true,
"visibility_details": {
"has_price": true,
"has_stock": true,
"active_product": true
},
"price_index": "5.0",
"images360": [],
"color_image": "",
"primary_image": "",
"status": {
"state": "processed",
"state_failed": "",
"moderate_status": "approved",
"decline_reasons": [],
"validation_state": "success",
"state_name": "Processed",
"state_description": "Product is processed",
"is_failed": false,
"is_created": true,
"state_tooltip": ""
},
"description_category_id": 17033876,
"type_id": 97114,
"width": 80,
"height": 200,
"depth": 80,
"dimension_unit": "mm",
"weight": 320,
"weight_unit": "g",
"attributes": [
{
"attribute_id": 85,
"complex_id": 0,
"values": [
{
"dictionary_value_id": 971082156,
"value": "Thermos"
}
]
}
]
}
]
}
}
3. 核心字段说明
基本信息
| 字段 | 类型 | 说明 |
|---|---|---|
| id | integer | Ozon 商品 ID(product_id) |
| name | string | 商品名称 |
| offer_id | string | 自己的货号 |
| barcode | string | 条形码 |
| created_at | string | 创建时间(ISO 8601) |
价格
| 字段 | 类型 | 说明 |
|---|---|---|
| price | string | 当前售价 |
| old_price | string | 划线价(原价) |
| marketing_price | string | 营销价 |
| buybox_price | string | BuyBox 价格(赢得购物车的价格) |
| recommended_price | string | 平台推荐价 |
| min_price | string | 允许的最低价(低于此价需申请) |
| premium_price | string | Premium 会员价 |
状态
| 字段 | 类型 | 说明 |
|---|---|---|
| state | string | 商品状态(见下表) |
| status | object | 状态详情(含审核状态、错误原因) |
| visible | boolean | 是否可见(上架) |
| visibility_details | object | 可见性详情(是否有价格/库存/激活) |
state 状态值
| state | 含义 | 说明 |
|---|---|---|
| processed | ✅ 已处理 | 商品创建成功,可正常展示 |
| processing | ⏳ 处理中 | 正在处理(刚导入) |
| moderating | ⏳ 审核中 | 平台审核中 |
| failed | ❌ 失败 | 创建/审核失败,查看 errors |
| archived | 📦 已归档 | 商品已下架归档 |
status.moderate_status 审核状态
| moderate_status | 含义 |
|---|---|
| approved | ✅ 审核通过 |
| pending | ⏳ 待审核 |
| declined | ❌ 审核拒绝 |
库存
| 字段 | 类型 | 说明 |
|---|---|---|
| stocks.present | integer | 当前库存 |
| stocks.reserved | integer | 已预订数量 |
| stocks.coming | integer | 即将到货数量 |
图片
| 字段 | 类型 | 说明 |
|---|---|---|
| images | array | 图片数组 |
| images[].file_name | string | 图片文件名 |
| images[].default | boolean | 是否主图 |
| images[].index | integer | 顺序 |
| primary_image | string | 主图 URL |
| images360 | array | 360° 图 |
| color_image | string | 营销色图 |
尺寸与属性
| 字段 | 类型 | 说明 |
|---|---|---|
| description_category_id | integer | 类目 ID |
| type_id | integer | 商品类型 ID |
| width / height / depth | number | 尺寸 |
| dimension_unit | string | 尺寸单位 |
| weight | number | 重量 |
| weight_unit | string | 重量单位 |
| attributes | array | 属性数组(结构同导入) |
错误信息
| 字段 | 类型 | 说明 |
|---|---|---|
| errors | array | 错误数组(审核失败原因、字段错误等) |
| status.decline_reasons | array | 审核拒绝原因 |
| status.validation_state | string | 校验状态:success / failed |
4. 使用场景
场景 1:发布后回查 product_id
# 导入后用 offer_id 查询,获取 product_id
async def get_product_id_by_offer(offer_id: str):
resp = await ozon_client.get_product_info(
offer_id=[offer_id]
)
items = resp["result"]["items"]
if items:
return items[0]["id"]
return None
场景 2:检查审核状态
async def check_moderation_status(product_id: int):
resp = await ozon_client.get_product_info(
product_id=[product_id]
)
item = resp["result"]["items"][0]
status = item["status"]
return {
"state": status["state"],
"moderate_status": status["moderate_status"],
"is_approved": status["moderate_status"] == "approved",
"decline_reasons": status["decline_reasons"]
}
场景 3:读取审核错误
async def get_product_errors(offer_id: str):
resp = await ozon_client.get_product_info(offer_id=[offer_id])
item = resp["result"]["items"][0]
errors = []
# 字段错误
if item.get("errors"):
errors.extend(item["errors"])
# 审核拒绝原因
if item["status"].get("decline_reasons"):
errors.extend(item["status"]["decline_reasons"])
return errors
5. 错误处理
商品不存在
{
"result": {
"items": []
}
}
返回空数组,非 404 错误。
部分成功
{
"result": {
"items": [
{
"id": 987654321,
"offer_id": "EXISTING-001",
/* ... 完整信息 */
}
]
}
}
请求 3 个 offer_id,只有 1 个存在 → 只返回 1 个 item。
6. 与其他接口的关系
与 /v3/product/import 的配合
① POST /v3/product/import → task_id
② POST /v1/product/import/info → status=imported, product_id=X
③ POST /v3/product/info/list (product_id=X) → 读取完整信息(含图片/审核状态)
用途:导入后可能需要:
- 确认图片上传成功
- 检查审核状态
- 读取 Ozon 生成的 SKU
- 查看价格索引(
price_index,影响排名)
与 /v3/product/list 的区别
| 接口 | 用途 | 返回字段 |
|---|---|---|
/v3/product/info/list |
详情查询 | 完整字段(图片/属性/状态/错误) |
/v3/product/list |
列表分页 | 基本字段(id/name/price/state),支持筛选/排序 |
选择建议:
- 已知 offer_id/product_id,要完整信息 → 用
info/list - 分页浏览所有商品、筛选状态 → 用
list
7. 示例代码
Python(服务端)
async def fetch_product_detail(
client_id: str,
api_key: str,
offer_id: str = None,
product_id: int = None
):
"""查询商品详情"""
headers = {
"Client-Id": client_id,
"Api-Key": api_key,
"Content-Type": "application/json"
}
payload = {}
if offer_id:
payload["offer_id"] = [offer_id]
if product_id:
payload["product_id"] = [product_id]
async with httpx.AsyncClient(timeout=30.0) as client:
resp = await client.post(
"https://api-seller.ozon.ru/v3/product/info/list",
headers=headers,
json=payload
)
resp.raise_for_status()
data = resp.json()
items = data.get("result", {}).get("items", [])
return items[0] if items else None
TypeScript(前端)
async function getProductDetail(
productId: string,
by: 'offer_id' | 'product_id' = 'offer_id'
) {
const resp = await fetch('/api/products/detail', {
method: 'POST',
body: JSON.stringify({
[by]: [productId]
})
});
const data = await resp.json();
return data.result.items[0];
}
// 使用
const detail = await getProductDetail('MY-THERMOS-001', 'offer_id');
console.log('审核状态:', detail.status.moderate_status);
console.log('库存:', detail.stocks.present);
8. 审核拒绝原因解读
常见拒绝原因
| decline_reason | 含义 | 解决 |
|---|---|---|
| 图片不符合要求 | 图片非白底/有水印/模糊 | 重新上传符合规范的图片 |
| 标题含禁用词 | 标题有夸大宣传/品牌侵权词 | 修改标题,去除违规词 |
| 描述不完整 | 描述过短或缺少关键信息 | 补充完整商品描述 |
| 类目错误 | 商品与类目不匹配 | 重新选择正确类目 |
| 属性缺失 | 缺少必填属性 | 补充必填属性 |
| 品牌未授权 | 品牌需授权认证 | 提供品牌授权书或改用无品牌 |
处理流程
① 读取 status.decline_reasons
② 根据原因修改商品(改图/改文案/改属性)
③ 重新调用 /v3/product/import(同 offer_id = 更新)
④ 再次审核
9. V2 项目集成
API 层(待实现)
# server/api/products.py
@router.get("/products/{product_id}/ozon-detail")
async def get_ozon_detail(
product_id: str,
db: AsyncSession = Depends(get_db)
):
"""查询商品在 Ozon 的详情(审核状态/库存/图片)"""
product = await get_product(db, product_id)
if not product.ozon_product_id and not product.offer_id:
raise HTTPException(404, "商品尚未发布到 Ozon")
# 获取店铺凭证(从发布记录找)
task = await db.execute(
select(PublishTask)
.where(PublishTask.product_id == product_id)
.order_by(PublishTask.created_at.desc())
.limit(1)
)
task = task.scalar_one_or_none()
if not task:
raise HTTPException(404, "未找到发布记录")
shop = await get_shop(db, task.shop_id)
# 调用 Ozon API
detail = await ozon_client.get_product_info(
shop.client_id_dec,
shop.api_key_dec,
offer_id=[product.offer_id] if product.offer_id else None,
product_id=[product.ozon_product_id] if product.ozon_product_id else None
)
return {"result": detail}
前端(商品详情页展示审核状态)
// studio/src/pages/product/components/OzonStatusBadge.tsx
import { Badge, Tooltip } from 'antd';
export function OzonStatusBadge({ productId }) {
const [status, setStatus] = useState(null);
useEffect(() => {
fetch(`/api/products/${productId}/ozon-detail`)
.then(r => r.json())
.then(data => {
const item = data.result.items[0];
setStatus(item.status);
});
}, [productId]);
if (!status) return null;
const statusMap = {
approved: { color: 'success', text: '审核通过' },
pending: { color: 'processing', text: '审核中' },
declined: { color: 'error', text: '审核拒绝' }
};
const config = statusMap[status.moderate_status] || {};
return (
<Tooltip title={status.decline_reasons?.join(', ')}>
<Badge status={config.color} text={config.text} />
</Tooltip>
);
}
10. 性能优化
批量查询
# 一次查询多个商品(最多 100 个)
async def batch_get_products(offer_ids: list[str]):
resp = await ozon_client.get_product_info(offer_id=offer_ids)
return {
item["offer_id"]: item
for item in resp["result"]["items"]
}
# 使用
details = await batch_get_products([
"PROD-001", "PROD-002", "PROD-003"
])
缓存策略
# 商品详情变化不频繁,可短期缓存
from functools import lru_cache
@lru_cache(maxsize=1000)
async def get_product_info_cached(offer_id: str, ttl=300):
# TTL 5 分钟
detail = await ozon_client.get_product_info(offer_id=[offer_id])
return detail["result"]["items"][0] if detail["result"]["items"] else None
# 审核状态变化时清缓存
get_product_info_cached.cache_clear()
相关文档
- 04-product-import.md —— 创建/更新商品
- 06-product-list.md —— 商品列表分页
- 09-stocks.md —— 库存管理
- docs/v2/ozon-publish.md —— 发布集成方案