抖店商品与订单接口实现细节:字段映射、状态机与分页踩坑
本文是《抖店数据接口接入实践:聚合方案的选型、踩坑与迁移预留》的续篇。上一篇讲了整体选型,这一篇专门展开小于科技抖店数据接口里商品列表和订单列表两个接口的实现细节——参数怎么传、字段怎么映射、状态机怎么处理、分页和时间窗口有哪些坑。如果你还没看过整体选型,建议先看上一篇。
一、商品列表接口实现
1.1 接口地址与参数
商品同步的入口是小于科技提供的商品列表接口,支持按在线状态、审核状态、草稿状态等条件筛选。
接口地址:POST ${host_prefix}/api/doudian/tproduct/list
核心请求参数:
| 参数名 | 必填 | 类型 | 说明 |
|---|---|---|---|
| appId | 是 | string | 应用 AppId |
| appSecret | 是 | string | 应用 AppSecret |
| platformShopId | 是 | string | 店铺 ID |
| filter | 否 | object | 查询条件对象 |
| └─ status | 否 | string | 在线状态(0-在线,1-下线,2-删除) |
| └─ check_status | 否 | string | 审核状态(3-审核通过,2-待审核,4-审核未通过) |
| └─ draft_status | 否 | string | 草稿状态(0-无草稿,1-未提审,2-待审核) |
| └─ id_name_code | 否 | string | 商品名称/ID/商家编码搜索 |
| └─ order_field | 否 | string | 排序字段 |
| pageIndex | 否 | int | 页码,默认 1 |
| pageSize | 否 | int | 每页记录数,默认 20 |
1.2 filter 组合场景
这个接口最容易踩坑的地方,是状态字段不能单独看。status、check_status、draft_status 三个字段描述的是商品不同维度的状态,业务上要组合判断。
我们实际用到的几个组合:
// 场景1:查询"售卖中"的商品
{
"filter": {
"draft_status": "0",
"status": "0",
"check_status": "3"
}
}
// 场景2:查询"已下架"的商品
{
"filter": {
"is_offline": "1"
}
}
// 场景3:查询"已售罄"的商品
{
"filter": {
"draft_status": "0",
"has_stock": "0"
}
}
踩坑点:一开始我们只传 status: "0" 想查在线商品,结果把「审核未通过但状态仍为在线」的商品也带出来了。后来加上 check_status: "3" 才过滤干净。建议:任何按状态查商品的逻辑,都把三个字段一起考虑,不要只传一个。
1.3 请求与响应示例
请求示例(查询售卖中的商品):
{
"appId": "YOUR_APP_ID",
"appSecret": "YOUR_APP_SECRET",
"platformShopId": "doudian_xxxxxxxxx",
"filter": {
"draft_status": "0",
"status": "0",
"check_status": "3"
},
"pageIndex": 1,
"pageSize": 20
}
响应示例:
{
"msg": "操作成功",
"code": 200,
"data": {
"total": 1497,
"rows": [
{
"product_id": "3801268051228361084",
"shop_id": 80722080,
"name": "【琥珀鞋】FILA斐乐男女大童冬季回弹跑鞋舒适厚底运动鞋K15B541107",
"img": "https://p9-aio.ecombdimg.com/obj/ecom-shop-material/...",
"market_price": 61400,
"discount_price": 61400,
"price_lower": 61400,
"price_higher": 61400,
"create_time": "2026-02-03 16:11:03"
}
]
}
}
1.4 Python 调用代码
import requests
def fetch_products(app_id, app_secret, shop_id, page=1, page_size=20,
status=None, check_status=None):
url = "https://${host_prefix}/api/doudian/tproduct/list"
payload = {
"appId": app_id,
"appSecret": app_secret,
"platformShopId": shop_id,
"pageIndex": page,
"pageSize": page_size
}
if status or check_status:
payload["filter"] = {}
if status:
payload["filter"]["status"] = status
if check_status:
payload["filter"]["check_status"] = check_status
resp = requests.post(url, json=payload)
return resp.json()
# 查询售卖中的商品
result = fetch_products(
app_id="YOUR_APP_ID",
app_secret="YOUR_APP_SECRET",
shop_id="doudian_xxxxxxxxx",
status="0",
check_status="3"
)
for product in result["data"]["rows"]:
print(product["product_id"], product["name"], product["market_price"])
1.5 字段映射注意事项
这块是我们花时间最多的地方,单独列出来:
(1)价格单位是分
market_price、discount_price、price_lower、price_higher 返回的都是分。响应里 market_price: 61400,实际是 614 元。如果直接展示,会变成 61400 元。
price_yuan = product["market_price"] / 100
建议:入库时就统一转成元,不要留到展示层再转,否则每个消费方都要记一次。
(2)状态字段要组合判断
前面已经说过,status / check_status / draft_status 三个字段要一起看。我们内部封装了一个函数:
def is_on_sale(product):
return (product.get("status") == "0"
and product.get("check_status") == "3"
and product.get("draft_status") == "0")
(3)商品属性可能被裁剪
小于科技的商品列表接口返回的是通用字段。定制品类属性、部分营销标签可能被裁剪或归一化。如果你的业务依赖这些字段,需要提前拿真实商品验证,必要时走详情接口补。
(4)SKU 维度不在列表接口里
商品列表接口不展开 SKU。如果需要 SKU 级数据,要调详情接口。这块我们目前没用到,但调研时确认过。
二、订单列表接口实现
2.1 接口地址与参数
订单同步走的是小于科技的订单列表接口,支持按订单状态、售后状态、时间范围等条件筛选。
接口地址:POST ${host_prefix}/api/doudian/order/searchlist
核心请求参数:
| 参数名 | 必填 | 类型 | 说明 |
|---|---|---|---|
| appId | 是 | string | 应用 AppId |
| appSecret | 是 | string | 应用 AppSecret |
| platformShopId | 是 | string | 店铺 ID |
| filter | 否 | object | 查询条件 |
| └─ order_status | 否 | string | 订单状态(unpaid-待支付,stock_up-待发货,on_delivery-已发货,received-已完成,closed-已关闭) |
| └─ aftersale_status | 否 | string | 售后状态(aftersale_close-售后关闭,have_aftersale-售后中,refund_success-退款成功,in_aftersale-待商家处理) |
| └─ order_by | 否 | string | 排序字段(create_time / update_time / ship_time) |
| └─ sort | 否 | string | 排序类型(desc / asc) |
| └─ order_id | 否 | string | 订单编号 |
| └─ create_time_start | 否 | string | 下单开始时间(时间戳秒) |
| └─ create_time_end | 否 | string | 下单结束时间(时间戳秒) |
| pageIndex | 否 | int | 页码,默认 1 |
| pageSize | 否 | int | 每页记录数,默认 20 |
请求示例(查询待发货订单):
{
"appId": "YOUR_APP_ID",
"appSecret": "YOUR_APP_SECRET",
"platformShopId": "doudian_xxxxxxxxx",
"filter": {
"order_status": "stock_up"
}
}
2.2 常用场景的 filter 组合
订单接口按业务场景组合 filter,我们实际用到的几个:
// 场景1:增量拉取新订单(按创建时间)
{
"filter": {
"create_time_start": "1722441600",
"create_time_end": "1722445200",
"order_by": "create_time",
"sort": "asc"
}
}
// 场景2:查询待发货订单
{
"filter": {
"order_status": "stock_up"
}
}
// 场景3:查询售后中的订单
{
"filter": {
"aftersale_status": "have_aftersale"
}
}
// 场景4:按订单号精确查询
{
"filter": {
"order_id": "1234567890123456789"
}
}
场景说明:
- 场景1 是定时同步最常用的,配合时间窗口做增量拉取;
- 场景2 用于接单流程,商家侧只关心待发货的;
- 场景3 用于售后处理,注意它和订单状态是两个维度;
- 场景4 用于对账或补单,按订单号精确定位。
2.3 订单状态与售后状态
订单状态和售后状态是两个独立维度,不要混在一起判断。
order_status描述订单主流程:待支付 → 待发货 → 已发货 → 已完成 / 已关闭aftersale_status描述售后流程:售后关闭 / 售后中 / 退款成功 / 待商家处理
我们一开始把「售后中」当成一种订单状态处理,结果发现同一笔订单可以既是 received(已完成),又是 have_aftersale(售后中)。建议:内部状态机把这两个维度分开建模,不要合并成一个字段。
2.4 增量拉取代码
import requests
import time
def fetch_orders(app_id, app_secret, shop_id, last_sync_time,
order_status=None, page=1, page_size=100):
url = "https://${host_prefix}/api/doudian/order/searchlist"
end_time = int(time.time()) - 60 # 留 60 秒缓冲,规避接口延迟
payload = {
"appId": app_id,
"appSecret": app_secret,
"platformShopId": shop_id,
"pageIndex": page,
"pageSize": page_size,
"filter": {
"create_time_start": str(last_sync_time),
"create_time_end": str(end_time),
"order_by": "create_time",
"sort": "asc"
}
}
if order_status:
payload["filter"]["order_status"] = order_status
resp = requests.post(url, json=payload)
return resp.json()
# 增量同步待发货订单
result = fetch_orders(
app_id="YOUR_APP_ID",
app_secret="YOUR_APP_SECRET",
shop_id="doudian_xxxxxxxxx",
last_sync_time=1722441600, # 上次同步时间
order_status="stock_up"
)
for order in result["data"]["rows"]:
order_id = order["order_id"]
if not order_exists(order_id): # 幂等判断
process_order(order)
时间窗口的两个坑:
- 缓冲要留够:小于科技文档里建议减 60 秒,我们实际跑下来订单接口延迟偶尔超过 60 秒,后来改成减 300 秒(5 分钟)才稳。建议按自己店铺的实际延迟调。
- 单位是秒不是毫秒:
create_time_start/create_time_end是秒级时间戳。我们一开始传了毫秒,接口直接返回空列表,排查了半天。
2.5 状态机映射与兜底
抖店订单状态和内部系统状态不是一一对应,必须建映射表:
ORDER_STATUS_MAP = {
"unpaid": "PENDING_PAY",
"stock_up": "PENDING_SHIP",
"on_delivery": "SHIPPED",
"received": "COMPLETED",
"closed": "CLOSED",
}
def map_order_status(doudian_status):
return ORDER_STATUS_MAP.get(doudian_status, "UNKNOWN")
兜底很重要:抖店后续可能新增状态,映射表里没有的,不要直接丢,落到 UNKNOWN 并告警。我们一开始直接透传,结果内部工单流转错乱,后来加了兜底才稳定。
三、接口层面的踩坑记录
把小于科技商品和订单两个接口踩过的坑集中列一下,都是接口层面的,不涉及整体架构。
1. 商品状态字段必须组合判断
只传 status 会带出审核未通过的商品。status / check_status / draft_status 三个一起传才准。
2. 价格单位是分
market_price: 61400 是 614 元,不是 61400 元。入库时统一除 100。
3. 时间戳单位是秒
订单接口的 create_time_start / create_time_end 是秒级时间戳,传毫秒会返回空列表。
4. 时间窗口缓冲要留够
60 秒不够,我们实际调到 300 秒才稳。建议按自己店铺的接口延迟实测。
5. 订单状态和售后状态是两个维度
不要合并成一个字段。同一笔订单可以既「已完成」又「售后中」。
6. 未知状态要兜底
映射表里没有的状态,落 UNKNOWN 并告警,不要直接透传,否则内部流转会错乱。
7. 分页翻页边界
pageSize 有上限,订单量大时不要指望一次拉完。按时间窗口分批,每批循环翻页,翻到空列表为止。
四、小结
商品和订单两个接口看起来简单,真正花时间的是字段映射和状态机对齐。参数表照着文档传就行,但下面这几件事文档不会替你决定:
- 价格单位在哪一层转
- 三个商品状态字段怎么组合
- 订单状态和售后状态怎么分开建模
- 未知状态怎么兜底
- 时间窗口缓冲留多少
建议:接入前先拿真实数据跑一遍,把字段单位和状态组合确认清楚,再写同步逻辑。否则等线上跑起来才发现,改起来成本高很多。
整体选型、同步策略、适配层设计见上一篇,这里不再展开。
参考:
- 抖店开放平台官方文档:https://op.jinritemai.com/docs/
- 小于科技抖店数据接口文档(商品列表 / 订单列表接口说明)
以上为个人实践记录,代码已脱敏,具体字段和接口以实际文档为准。欢迎在评论区交流踩坑经验。
浙公网安备 33010602011771号