Files
ozon-seller-kit/docs/ozon-seller-api/06-product-list.md
T

478 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 商品列表查询 API
> 官方文档:https://docs.ozon.ru/api/seller/zh/#operation/ProductAPI_GetProductListV3
---
## 接口信息
| 项 | 值 |
|---|---|
| 方法 | POST |
| 路径 | `/v3/product/list` |
| 鉴权 | 需要 `Client-Id` + `Api-Key` |
| 用途 | 分页查询商品列表(支持筛选、排序) |
---
## 1. 请求
### 请求体
```json
{
"filter": {
"offer_id": ["MY-THERMOS-001"],
"product_id": [987654321],
"visibility": "ALL"
},
"last_id": "",
"limit": 100
}
```
### 参数说明
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| filter | object | 可选 | 筛选条件 |
| filter.offer_id | array | 可选 | 货号数组 |
| filter.product_id | array | 可选 | Ozon 商品 ID 数组 |
| filter.visibility | string | 可选 | 可见性:`ALL`(全部)/ `VISIBLE`(可见)/ `INVISIBLE`(不可见)/ `EMPTY_STOCK`(无库存)。默认 `ALL` |
| **last_id** | string | 可选 | **分页游标**(上一页最后一个商品的 ID),首页传空字符串 `""` |
| **limit** | integer | 可选 | 每页数量,最大 **1000**,默认 100 |
---
## 2. 响应
### 成功响应(200
```json
{
"result": {
"items": [
{
"product_id": 987654321,
"offer_id": "MY-THERMOS-001"
},
{
"product_id": 987654322,
"offer_id": "MY-THERMOS-002"
}
],
"total": 256,
"last_id": "bnVtYmVyMjo5ODc2NTQzMjI="
}
}
```
### 字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
| items | array | 商品列表(**仅基本字段**product_id + offer_id |
| total | integer | 商品总数 |
| **last_id** | string | **下一页游标**(Base64 编码,传给下次请求) |
⚠️ **注意**:此接口**仅返回 product_id 和 offer_id**,不返回名称/价格/图片等详情。要获取完整信息,需再调 `/v3/product/info/list`
---
## 3. 分页示例
### 游标分页(推荐)
```python
async def fetch_all_products(visibility="ALL"):
"""拉取所有商品(游标分页)"""
all_items = []
last_id = ""
while True:
resp = await ozon_client.get_product_list(
filter={"visibility": visibility},
last_id=last_id,
limit=1000 # 单次最多 1000
)
items = resp["result"]["items"]
all_items.extend(items)
# 无更多数据
if not resp["result"].get("last_id"):
break
last_id = resp["result"]["last_id"]
return all_items
```
### 分批处理
```python
async def process_products_in_batches(batch_size=100):
"""分批处理商品(避免一次拉全部)"""
last_id = ""
while True:
resp = await ozon_client.get_product_list(
last_id=last_id,
limit=batch_size
)
items = resp["result"]["items"]
if not items:
break
# 处理当前批次
await process_batch(items)
last_id = resp["result"].get("last_id")
if not last_id:
break
```
---
## 4. 获取完整信息
### 方法 A:批量查详情(推荐)
```python
async def fetch_products_with_detail(visibility="ALL"):
"""拉取商品列表 + 完整信息"""
# 1. 拉列表(仅 ID
list_resp = await ozon_client.get_product_list(
filter={"visibility": visibility},
limit=1000
)
items = list_resp["result"]["items"]
product_ids = [item["product_id"] for item in items]
# 2. 批量查详情(每次最多 100 个)
details = []
for i in range(0, len(product_ids), 100):
batch = product_ids[i:i+100]
detail_resp = await ozon_client.get_product_info(
product_id=batch
)
details.extend(detail_resp["result"]["items"])
return details
```
### 方法 B:按需查详情
```python
# 先列表,用户点击某个商品时再查详情
products = await ozon_client.get_product_list(limit=100)
# 用户点击 product_id=987654321
detail = await ozon_client.get_product_info(product_id=[987654321])
```
---
## 5. 筛选条件详解
### visibility 筛选
| 值 | 含义 | 使用场景 |
|---|---|---|
| **ALL** | 全部商品 | 管理后台(查看所有) |
| **VISIBLE** | 可见商品(上架) | 前台展示的商品 |
| **INVISIBLE** | 不可见商品(下架/草稿) | 待上架/审核失败/归档 |
| **EMPTY_STOCK** | 无库存商品 | 补货提醒 |
### 示例
```python
# 查询所有上架商品
visible = await ozon_client.get_product_list(
filter={"visibility": "VISIBLE"},
limit=1000
)
# 查询无库存商品(需补货)
empty_stock = await ozon_client.get_product_list(
filter={"visibility": "EMPTY_STOCK"},
limit=100
)
```
---
## 6. 性能对比
### `/v3/product/list` vs `/v3/product/info/list`
| 维度 | `/v3/product/list` | `/v3/product/info/list` |
|---|---|---|
| 返回字段 | 仅 product_id + offer_id | 完整字段(图片/属性/状态) |
| 单次数量 | 最多 **1000** | 最多 **100** |
| 响应速度 | 快(字段少) | 慢(字段多) |
| 适用场景 | 列表/分页/ID 收集 | 详情查询/更新前读取 |
**策略**
1. 先用 `/list` 拉 ID 列表(快)
2. 再用 `/info/list` 批量查详情(按需,100 个一批)
---
## 7. 与本地数据库同步
### 场景:回填 product_id
```python
async def sync_product_ids():
"""发布后回填 product_id(用 offer_id 匹配)"""
# 1. 从数据库取所有「已发布但无 product_id」的商品
local_products = await db.execute(
select(Product)
.where(
Product.stage == "published",
Product.ozon_product_id.is_(None),
Product.offer_id.isnot(None)
)
)
local_products = local_products.scalars().all()
if not local_products:
return
offer_ids = [p.offer_id for p in local_products]
# 2. 从 Ozon 查询这些 offer_id 的 product_id
ozon_items = []
for i in range(0, len(offer_ids), 100):
batch = offer_ids[i:i+100]
resp = await ozon_client.get_product_info(offer_id=batch)
ozon_items.extend(resp["result"]["items"])
# 3. 回填到数据库
ozon_map = {item["offer_id"]: item["id"] for item in ozon_items}
for p in local_products:
if p.offer_id in ozon_map:
p.ozon_product_id = ozon_map[p.offer_id]
await db.commit()
```
### 场景:定期同步状态
```python
async def sync_product_states():
"""定期同步商品状态(审核状态/库存/可见性)"""
# 1. 拉取所有 Ozon 商品 ID
ozon_resp = await ozon_client.get_product_list(limit=1000)
ozon_ids = [item["product_id"] for item in ozon_resp["result"]["items"]]
# 2. 批量查详情
details = []
for i in range(0, len(ozon_ids), 100):
batch = ozon_ids[i:i+100]
resp = await ozon_client.get_product_info(product_id=batch)
details.extend(resp["result"]["items"])
# 3. 更新本地数据库
for item in details:
await db.execute(
update(Product)
.where(Product.ozon_product_id == item["id"])
.values(
stage="published" if item["visible"] else "failed",
# 可同步更多字段:价格/库存/审核状态
)
)
await db.commit()
```
---
## 8. 常见问题
### Q1: 为什么 `/list` 只返回 ID
**A**: 性能考虑。商品列表可能有**数万条**,返回完整字段会很慢。设计思路:
1. 先快速拉 ID 列表(轻量)
2. 前端展示分页,只查当前页的详情
3. 或后台批量拉详情,按需处理
### Q2: 如何获取商品总数?
**A**: 响应的 `total` 字段。但注意:
- `total` 是当前筛选条件下的总数
- 不保证精确(可能略有延迟)
### Q3: 游标分页与偏移分页的区别?
**A**:
- **游标分页**(last_id):适合全量遍历,性能稳定
- **偏移分页**offset):Ozon 不支持(无 offset 参数)
### Q4: 多久同步一次?
**A**: 建议策略:
- 发布后立即查询(回填 product_id
- 定期同步(每天一次,更新状态/库存)
- 用户主动刷新(按需实时查询)
---
## 9. V2 项目集成
### API 层(待实现)
```python
# server/api/products.py
@router.get("/products/sync-from-ozon")
async def sync_from_ozon(
shop_id: str = Query(...),
db: AsyncSession = Depends(get_db)
):
"""从 Ozon 同步商品列表(回填 product_id + 状态)"""
shop = await get_shop(db, shop_id)
# 1. 拉取 Ozon 商品列表
ozon_items = []
last_id = ""
while True:
resp = await ozon_client.get_product_list(
shop.client_id_dec,
shop.api_key_dec,
last_id=last_id,
limit=1000
)
items = resp["result"]["items"]
ozon_items.extend(items)
last_id = resp["result"].get("last_id")
if not last_id:
break
# 2. 批量查详情
product_ids = [item["product_id"] for item in ozon_items]
details = []
for i in range(0, len(product_ids), 100):
batch = product_ids[i:i+100]
detail_resp = await ozon_client.get_product_info(
shop.client_id_dec,
shop.api_key_dec,
product_id=batch
)
details.extend(detail_resp["result"]["items"])
# 3. 更新本地数据库
updated = 0
for item in details:
result = await db.execute(
update(Product)
.where(Product.offer_id == item["offer_id"])
.values(
ozon_product_id=item["id"],
stage="published" if item["visible"] else "archived"
)
)
updated += result.rowcount
await db.commit()
return {
"synced": len(details),
"updated": updated
}
```
### 前端(待实现)
```tsx
// studio/src/pages/products/SyncButton.tsx
import { Button, message } from 'antd';
import { SyncOutlined } from '@ant-design/icons';
export function SyncFromOzonButton({ shopId }) {
const [syncing, setSyncing] = useState(false);
const handleSync = async () => {
setSyncing(true);
try {
const resp = await fetch(
`/api/products/sync-from-ozon?shop_id=${shopId}`
);
const data = await resp.json();
message.success(
`已同步 ${data.synced} 个商品,更新 ${data.updated} 条记录`
);
} catch (err) {
message.error(`同步失败: ${err.message}`);
} finally {
setSyncing(false);
}
};
return (
<Button
icon={<SyncOutlined />}
loading={syncing}
onClick={handleSync}
>
Ozon 同步
</Button>
);
}
```
---
## 10. 高级用法
### 增量同步
```python
async def incremental_sync(last_sync_time: datetime):
"""增量同步:只拉取最近更新的商品"""
# Ozon 的 /list 接口不支持按更新时间筛选
# 策略:全量拉 ID,对比本地 updated_at,只查变化的
ozon_items = await fetch_all_products()
ozon_ids = {item["product_id"] for item in ozon_items}
# 查本地已有的 product_id
local = await db.execute(
select(Product.ozon_product_id, Product.updated_at)
.where(Product.ozon_product_id.isnot(None))
)
local_map = {
row.ozon_product_id: row.updated_at
for row in local.fetchall()
}
# 找出新增的 ID
new_ids = ozon_ids - set(local_map.keys())
# 批量查详情(只查新增的)
if new_ids:
details = await batch_get_product_info(list(new_ids))
# 插入数据库
...
```
---
## 相关文档
- [04-product-import.md](./04-product-import.md) —— 创建/更新商品
- [05-product-info.md](./05-product-info.md) —— 查询商品详情
- [09-stocks.md](./09-stocks.md) —— 库存管理
- [docs/v2/database.md](../v2/database.md) —— products 表结构