12 KiB
12 KiB
商品列表查询 API
官方文档:https://docs.ozon.ru/api/seller/zh/#operation/ProductAPI_GetProductListV3
接口信息
| 项 | 值 |
|---|---|
| 方法 | POST |
| 路径 | /v3/product/list |
| 鉴权 | 需要 Client-Id + Api-Key |
| 用途 | 分页查询商品列表(支持筛选、排序) |
1. 请求
请求体
{
"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)
{
"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. 分页示例
游标分页(推荐)
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
分批处理
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:批量查详情(推荐)
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:按需查详情
# 先列表,用户点击某个商品时再查详情
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 | 无库存商品 | 补货提醒 |
示例
# 查询所有上架商品
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 收集 | 详情查询/更新前读取 |
策略:
- 先用
/list拉 ID 列表(快) - 再用
/info/list批量查详情(按需,100 个一批)
7. 与本地数据库同步
场景:回填 product_id
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()
场景:定期同步状态
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: 性能考虑。商品列表可能有数万条,返回完整字段会很慢。设计思路:
- 先快速拉 ID 列表(轻量)
- 前端展示分页,只查当前页的详情
- 或后台批量拉详情,按需处理
Q2: 如何获取商品总数?
A: 响应的 total 字段。但注意:
total是当前筛选条件下的总数- 不保证精确(可能略有延迟)
Q3: 游标分页与偏移分页的区别?
A:
- 游标分页(last_id):适合全量遍历,性能稳定
- 偏移分页(offset):Ozon 不支持(无 offset 参数)
Q4: 多久同步一次?
A: 建议策略:
- 发布后立即查询(回填 product_id)
- 定期同步(每天一次,更新状态/库存)
- 用户主动刷新(按需实时查询)
9. V2 项目集成
API 层(待实现)
# 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
}
前端(待实现)
// 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. 高级用法
增量同步
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 —— 创建/更新商品
- 05-product-info.md —— 查询商品详情
- 09-stocks.md —— 库存管理
- docs/v2/database.md —— products 表结构