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

12 KiB
Raw Blame History

商品列表查询 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 收集 详情查询/更新前读取

策略

  1. 先用 /list 拉 ID 列表(快)
  2. 再用 /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: 性能考虑。商品列表可能有数万条,返回完整字段会很慢。设计思路:

  1. 先快速拉 ID 列表(轻量)
  2. 前端展示分页,只查当前页的详情
  3. 或后台批量拉详情,按需处理

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))
        # 插入数据库
        ...

相关文档