Files
ozon-seller-kit/docs/ozon-seller-api/09-stocks.md
T

14 KiB
Raw Blame History

库存管理 API

官方文档:https://docs.ozon.ru/api/seller/zh/#operation/ProductAPI_ProductsStocksV2


接口信息

方法 POST
路径 /v2/products/stocks
鉴权 需要 Client-Id + Api-Key
用途 设置/更新商品库存(必须操作,否则商品不可购买)

1. 重要约束

⚠️ 商品导入成功后不会自动上架,必须设置库存才能开售:

POST /v3/product/import → status=imported(商品已创建)
    ↓
POST /v2/products/stocks → 设置库存(商品可购买)

未设置库存的商品:

  • 在卖家后台可见
  • 前台不展示
  • 无法购买

2. 请求

请求体

{
  "stocks": [
    {
      "offer_id": "MY-THERMOS-001",
      "product_id": 987654321,
      "stock": 100,
      "warehouse_id": 12345678
    },
    {
      "offer_id": "MY-THERMOS-002",
      "stock": 50,
      "warehouse_id": 12345678
    }
  ]
}

参数说明

字段 类型 必填 说明
stocks array 库存数组(最多 100 个)
stocks[].offer_id string 条件 自己的货号(与 product_id 二选一)
stocks[].product_id integer 条件 Ozon 商品 ID(与 offer_id 二选一)
stocks[].stock integer 库存数量。0 = 无库存(下架)
stocks[].warehouse_id integer 仓库 ID(见下节)

⚠️ 必须提供 offer_id 或 product_id(建议用 offer_id,更稳定)。


3. 仓库 IDwarehouse_id

获取仓库 ID

接口POST /v1/warehouse/list

{}

响应

{
  "result": [
    {
      "warehouse_id": 12345678,
      "name": "FBS 仓库-莫斯科",
      "can_print_act_in_advance": true,
      "is_rfbs": false,
      "has_postings_limit": false,
      "postings_limit": 0,
      "status": "working"
    }
  ]
}
字段 说明
warehouse_id 仓库 ID(设置库存时用)
name 仓库名称
is_rfbs 是否 rFBS 仓库(Ozon 代发货)
status 状态:working(运行中)/ disabled(禁用)

仓库类型

类型 说明 warehouse_id
FBS 自发货(Fulfillment by Seller /v1/warehouse/list 获取
FBO Ozon 发货(Fulfillment by Ozon /v1/warehouse/list 获取
rFBS Ozon 代发货(类似 FBO,但库存在卖家处) is_rfbs=true

推荐:新商户优先用 FBS(自发货),灵活且门槛低。


4. 响应

成功响应(200

{
  "result": [
    {
      "errors": [],
      "offer_id": "MY-THERMOS-001",
      "product_id": 987654321,
      "updated": true,
      "warehouse_id": 12345678
    },
    {
      "errors": [
        {
          "code": "PRODUCT_NOT_FOUND",
          "message": "Product not found"
        }
      ],
      "offer_id": "MY-THERMOS-999",
      "product_id": 0,
      "updated": false,
      "warehouse_id": 12345678
    }
  ]
}

字段说明

字段 类型 说明
updated boolean 是否更新成功
errors array 错误数组(失败时)
offer_id string 货号(回显)
product_id integer Ozon 商品 ID(回显)
warehouse_id integer 仓库 ID(回显)

5. 常见错误

code message 原因 解决
PRODUCT_NOT_FOUND 商品不存在 offer_id/product_id 错误或商品已删除 检查 ID 是否正确
WAREHOUSE_NOT_FOUND 仓库不存在 warehouse_id 错误 /v1/warehouse/list 获取正确 ID
INVALID_STOCK 库存值错误 stock < 0 库存必须 ≥ 0
PRODUCT_ARCHIVED 商品已归档 商品处于归档状态 先恢复商品再设库存

6. 使用场景

场景 1:发布后设置初始库存

async def publish_and_set_stock(product: Product, shop: Shop):
    """发布商品 + 设置库存(完整流程)"""
    # 1. 导入商品
    resp = await ozon_client.import_products(
        shop.client_id_dec,
        shop.api_key_dec,
        [build_import_item(product)]
    )
    task_id = resp["result"]["task_id"]
    
    # 2. 轮询直到成功
    result = await wait_for_import(task_id)
    if not result["success"]:
        raise Exception(f"发布失败: {result['errors']}")
    
    product_id = result["product_id"]
    
    # 3. 获取仓库 ID
    warehouses = await ozon_client.get_warehouses(
        shop.client_id_dec,
        shop.api_key_dec
    )
    warehouse_id = warehouses[0]["warehouse_id"]  # 取第一个
    
    # 4. 设置库存
    stock_resp = await ozon_client.update_stocks(
        shop.client_id_dec,
        shop.api_key_dec,
        [{
            "product_id": product_id,
            "stock": 100,  # 初始库存
            "warehouse_id": warehouse_id
        }]
    )
    
    return stock_resp

场景 2:批量更新库存

async def batch_update_stocks(updates: list[dict]):
    """批量更新库存(最多 100 个)"""
    # updates = [
    #   {"offer_id": "PROD-001", "stock": 50},
    #   {"offer_id": "PROD-002", "stock": 0},  # 0 = 下架
    # ]
    
    warehouse_id = await get_default_warehouse_id()
    
    stocks = [
        {
            "offer_id": u["offer_id"],
            "stock": u["stock"],
            "warehouse_id": warehouse_id
        }
        for u in updates
    ]
    
    resp = await ozon_client.update_stocks(
        client_id, api_key, stocks
    )
    
    # 检查失败项
    failed = [
        item for item in resp["result"]
        if not item["updated"]
    ]
    
    return {
        "success": len(resp["result"]) - len(failed),
        "failed": failed
    }

场景 3:库存为 0 时下架

async def out_of_stock(offer_id: str):
    """库存售罄,设为 0(自动下架)"""
    await ozon_client.update_stocks(
        client_id, api_key,
        [{
            "offer_id": offer_id,
            "stock": 0,  # 库存为 0 → 前台不展示
            "warehouse_id": warehouse_id
        }]
    )

场景 4:补货后上架

async def restock(offer_id: str, quantity: int):
    """补货后重新上架"""
    await ozon_client.update_stocks(
        client_id, api_key,
        [{
            "offer_id": offer_id,
            "stock": quantity,  # 设置新库存 → 自动上架
            "warehouse_id": warehouse_id
        }]
    )

7. 查询当前库存

接口信息

方法 POST
路径 /v3/product/info/stocks
用途 查询商品当前库存

请求

{
  "filter": {
    "offer_id": ["MY-THERMOS-001"],
    "product_id": [987654321],
    "visibility": "ALL"
  },
  "last_id": "",
  "limit": 100
}

响应

{
  "result": {
    "items": [
      {
        "offer_id": "MY-THERMOS-001",
        "product_id": 987654321,
        "stocks": [
          {
            "type": "fbs",
            "present": 100,
            "reserved": 5,
            "warehouse_id": 12345678,
            "warehouse_name": "FBS 仓库-莫斯科"
          }
        ]
      }
    ],
    "last_id": "",
    "total": 1
  }
}
字段 说明
stocks[].present 可用库存
stocks[].reserved 已预订数量(订单未完成)
stocks[].type 仓库类型:fbs / fbo / rfbs

8. 库存同步策略

策略 A:实时同步(推荐)

# 本地库存变化时立即更新 Ozon
async def on_local_stock_change(product_id: str, new_stock: int):
    product = await get_product(db, product_id)
    
    if not product.ozon_product_id:
        return  # 未发布到 Ozon
    
    shop = await get_default_shop(db)
    warehouse_id = await get_default_warehouse_id()
    
    await ozon_client.update_stocks(
        shop.client_id_dec,
        shop.api_key_dec,
        [{
            "offer_id": product.offer_id,
            "stock": new_stock,
            "warehouse_id": warehouse_id
        }]
    )

策略 B:定时同步

# 每天凌晨同步一次(防止偏差累积)
async def daily_sync_stocks():
    """定时任务:同步本地库存到 Ozon"""
    products = await db.execute(
        select(Product)
        .where(
            Product.stage == "published",
            Product.ozon_product_id.isnot(None)
        )
    )
    products = products.scalars().all()
    
    warehouse_id = await get_default_warehouse_id()
    
    # 批量更新(100 个一批)
    for i in range(0, len(products), 100):
        batch = products[i:i+100]
        stocks = [
            {
                "offer_id": p.offer_id,
                "stock": p.local_stock,  # 假设有 local_stock 字段
                "warehouse_id": warehouse_id
            }
            for p in batch
        ]
        
        await ozon_client.update_stocks(
            client_id, api_key, stocks
        )

策略 C:反向同步(从 Ozon 读回)

# 定期从 Ozon 读回库存(多渠道销售时需要)
async def sync_stocks_from_ozon():
    """从 Ozon 同步库存到本地"""
    resp = await ozon_client.get_product_stocks(
        filter={"visibility": "VISIBLE"},
        limit=1000
    )
    
    for item in resp["result"]["items"]:
        offer_id = item["offer_id"]
        ozon_stock = item["stocks"][0]["present"]
        
        # 更新本地库存
        await db.execute(
            update(Product)
            .where(Product.offer_id == offer_id)
            .values(local_stock=ozon_stock)
        )
    
    await db.commit()

9. V2 项目集成

API 层(待实现)

# server/api/products.py

@router.post("/products/{product_id}/set-stock")
async def set_stock(
    product_id: str,
    stock: int = Body(..., ge=0),
    shop_id: str = Body(...),
    db: AsyncSession = Depends(get_db)
):
    """设置商品库存"""
    product = await get_product(db, product_id)
    
    if not product.ozon_product_id and not product.offer_id:
        raise HTTPException(400, "商品尚未发布到 Ozon")
    
    shop = await get_shop(db, shop_id)
    
    # 获取仓库 ID(缓存)
    warehouse_id = await get_or_cache_warehouse_id(shop)
    
    # 调用 Ozon API
    resp = await ozon_client.update_stocks(
        shop.client_id_dec,
        shop.api_key_dec,
        [{
            "offer_id": product.offer_id,
            "stock": stock,
            "warehouse_id": warehouse_id
        }]
    )
    
    result = resp["result"][0]
    
    if not result["updated"]:
        raise HTTPException(500, f"更新失败: {result['errors']}")
    
    # 更新本地记录
    product.local_stock = stock
    await db.commit()
    
    return {"success": True, "stock": stock}


@router.get("/shops/{shop_id}/warehouses")
async def get_warehouses(
    shop_id: str,
    db: AsyncSession = Depends(get_db)
):
    """获取店铺的仓库列表"""
    shop = await get_shop(db, shop_id)
    
    warehouses = await ozon_client.get_warehouses(
        shop.client_id_dec,
        shop.api_key_dec
    )
    
    return {"result": warehouses}

前端(待实现)

// studio/src/pages/product/components/StockPanel.tsx

import { InputNumber, Button, message } from 'antd';

export function StockPanel({ productId, shopId }) {
  const [stock, setStock] = useState(0);
  const [saving, setSaving] = useState(false);
  
  const handleSave = async () => {
    setSaving(true);
    try {
      await fetch(`/api/products/${productId}/set-stock`, {
        method: 'POST',
        body: JSON.stringify({ stock, shop_id: shopId })
      });
      
      message.success(`库存已设置为 ${stock}`);
    } catch (err) {
      message.error(`设置失败: ${err.message}`);
    } finally {
      setSaving(false);
    }
  };
  
  return (
    <div>
      <InputNumber
        min={0}
        value={stock}
        onChange={setStock}
        placeholder="库存数量"
      />
      <Button
        type="primary"
        loading={saving}
        onClick={handleSave}
      >
        设置库存
      </Button>
      <div style={{ marginTop: 8, fontSize: 12, color: '#888' }}>
        💡 库存为 0 时商品自动下架
      </div>
    </div>
  );
}

10. 最佳实践

1. 发布流程中必须设库存

✅ 正确:
POST /v3/product/import → 轮询成功 → POST /v2/products/stocks

❌ 错误:
POST /v3/product/import → 轮询成功 → 结束(商品不可购买)

2. 缓存仓库 ID

# 仓库 ID 不常变,启动时拉取并缓存
_warehouse_cache = {}

async def get_warehouse_id(shop_id: str):
    if shop_id not in _warehouse_cache:
        warehouses = await ozon_client.get_warehouses(...)
        _warehouse_cache[shop_id] = warehouses[0]["warehouse_id"]
    
    return _warehouse_cache[shop_id]

3. 库存为 0 的处理

# 库存为 0 → 自动下架,但商品仍在后台
# 补货后再设置库存 → 自动上架
# 不需要删除商品,只需更新库存

4. 批量操作

# 单次最多 100 个,超过需分批
async def update_large_batch(stocks: list):
    results = []
    for i in range(0, len(stocks), 100):
        batch = stocks[i:i+100]
        resp = await ozon_client.update_stocks(client_id, api_key, batch)
        results.extend(resp["result"])
    
    return results

相关文档