478 lines
12 KiB
Markdown
478 lines
12 KiB
Markdown
# 商品列表查询 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 表结构
|