558 lines
14 KiB
Markdown
558 lines
14 KiB
Markdown
# 商品信息查询 API
|
||
|
||
> 官方文档:https://docs.ozon.ru/api/seller/zh/#operation/ProductAPI_GetProductInfoListV3
|
||
|
||
---
|
||
|
||
## 接口信息
|
||
|
||
| 项 | 值 |
|
||
|---|---|
|
||
| 方法 | POST |
|
||
| 路径 | `/v3/product/info/list` |
|
||
| 鉴权 | 需要 `Client-Id` + `Api-Key` |
|
||
| 用途 | 查询商品详细信息(含审核状态、图片、属性、错误) |
|
||
|
||
---
|
||
|
||
## 1. 请求
|
||
|
||
### 请求体
|
||
|
||
```json
|
||
{
|
||
"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)
|
||
|
||
```json
|
||
{
|
||
"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
|
||
|
||
```python
|
||
# 导入后用 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:检查审核状态
|
||
|
||
```python
|
||
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:读取审核错误
|
||
|
||
```python
|
||
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. 错误处理
|
||
|
||
### 商品不存在
|
||
|
||
```json
|
||
{
|
||
"result": {
|
||
"items": []
|
||
}
|
||
}
|
||
```
|
||
|
||
返回空数组,非 404 错误。
|
||
|
||
### 部分成功
|
||
|
||
```json
|
||
{
|
||
"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(服务端)
|
||
|
||
```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(前端)
|
||
|
||
```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 层(待实现)
|
||
|
||
```python
|
||
# 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}
|
||
```
|
||
|
||
### 前端(商品详情页展示审核状态)
|
||
|
||
```tsx
|
||
// 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. 性能优化
|
||
|
||
### 批量查询
|
||
|
||
```python
|
||
# 一次查询多个商品(最多 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"
|
||
])
|
||
```
|
||
|
||
### 缓存策略
|
||
|
||
```python
|
||
# 商品详情变化不频繁,可短期缓存
|
||
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](./04-product-import.md) —— 创建/更新商品
|
||
- [06-product-list.md](./06-product-list.md) —— 商品列表分页
|
||
- [09-stocks.md](./09-stocks.md) —— 库存管理
|
||
- [docs/v2/ozon-publish.md](../v2/ozon-publish.md) —— 发布集成方案
|