《淘宝订单API落地方案:taobao.trade.fullinfo.get 与 消息推送 vs 轮询 的成本/可靠性》(附Python源码)
一、先拍结论
taobao.trade.fullinfo.get永远不是“同步方式”,它是“真相源拉取动作”。真正二选一的是:
消息推送(TMC / 订单变更消息 / 聚石塔 DSS) → 低延迟、省轮询、但会丢/会重/会延迟
轮询(
trades.sold.increment.get+fullinfo.get) → 稳、能兜底、但烧 API 额度、有延迟生产级淘宝订单同步只有一种正确架构:消息做触发器,增量轮询做兜底,fullinfo 做真相源,tid+status+modified 做幂等键。
二、三种同步方式定位
方式 | 核心接口 | 延迟 | 成本 | 可靠性 | 适用 |
|---|---|---|---|---|---|
全量初始化 | trades.sold.get → trade.fullinfo.get | 小时级 | 高(历史单多) | 中 | 首次接入/换库 |
增量轮询 | trades.sold.increment.get → fullinfo.get | 30s~10min | 中(按调用次数) | 高(自己可控) | 中小卖家、兜底 |
消息推送 | TMC / 订单消息 → fullinfo.get | 秒级 | 低调用费,DSS 按单计费 | 中(要自己补) | 中大型卖家 |
聚石塔 DSS | 平台推到塔内 RDS / MQ | 秒级 | ¥0.12/百单 + 套餐 | 高(但仍是最终一致) | 大促/几十万单/天 |
淘宝官方口径:
trades.sold.get:初始化用,不该拿来拿增量
trades.sold.increment.get:增量用,不该拿来拉三个月历史
trade.fullinfo.get:列表只给半截,详情才给子单/优惠/物流/退款
三、消息推送的隐藏约束(不写代码也会漏单)
1. 消息不是真相
trade.fullinfo.get 拉详情。2. 消息会丢
回调地址超时(有资料提到默认约 5s)→ 平台判失败
重试 3 次(10s/30s/60s)还失败 → 不再推
大促淘宝内部延迟 → 消息晚 1 分钟以上
Webhook 验签错 → 直接丢弃
3. 消息会重
tid 的状态变更可能推 2 次。4. 消息不保证顺序
WAIT_SELLER_SEND_GOODS 可能比 TRADE_PAID 先到。四、轮询的隐藏约束(不写代码也会烧钱)
1. 增量窗口前移
start_modified = last_sync - 10min # 平时 start_modified = last_sync - 30min # 双11/大促
2. 倒序分页必须从后往前翻
increment.get 按 modified 倒序返回。3. type 不传全 = 漏单
guarantee_trade/auto_delivery/ec/cod/step。4. 时间跨度
5. 成本模型
塔内
fullinfo.get≈ ¥0.02 / 百次塔外 ≈ ¥0.20 / 百次(10 倍)
DSS ≈ ¥0.12 / 百单(不是按次,是按新增订单)
轮询 1000 个变更 tid → 1000 次 fullinfo塔外:¥2.0 / 轮塔内:¥0.2 / 轮每秒轮询?一个月能烧出一台服务器钱。
五、生产架构:消息 + 轮询 双通道
┌─────────────┐ │ 淘宝订单事件 │ TMC / DSS / Webhook └──────┬──────┘ │ msgId 去重 ▼ InboundMessage(msg_id, tid, event_time) │ │ 只入队,不业务处理 ▼ Queue: order_change │ ├──► Worker: trade.fullinfo.get(tid) ──► OrderSnapshot ──► 状态机 │ ┌──────▼──────┐ │ 兜底定时器 │ trades.sold.increment.get(modified_start-10min, now) └──────┬──────┘ ▼ fullinfo.get 拉漏的 tid
消息来了:入队 + 异步拉
轮询来了:补可能漏的 tid
两者都写同一张
order_snapshot表谁后到、谁版本新,谁赢
六、Python:增量轮询器(官方坑全处理)
# top/order_incremental.py
from datetime import datetime, timedelta
import time
# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
class IncrementalOrderSyncer:
def __init__(self, top_client, store, slide_back_sec=600):
self.top = top_client
self.store = store # 本地 last_modified 存储
self.slide_back = slide_back_sec # 平时10min,大促30min
def _all_trade_types(self):
# 不传全 = 漏单
return [
"fixed","auction","guarantee_trade","step","independent_simple_trade",
"independent_shop_trade","au_trade","netcn_trade","external_trade",
"instant_trade","b2c_cod","hotel_trade","super_market_trade",
"super_market_cod_trade","taohua","waimai","o2o_offlinetrade",
"nopaid","eticket","tmall_i18n","insurance_plus","finance",
"pre_auth_type","lazada",
]
def run_once(self, page_size=100):
last = self.store.get_last_modified() or datetime.now() - timedelta(days=1)
start = last - timedelta(seconds=self.slide_back)
end = datetime.now()
params = {
"start_modified": start.strftime("%Y-%m-%d %H:%M:%S"),
"end_modified": end.strftime("%Y-%m-%d %H:%M:%S"),
"type": ",".join(self._all_trade_types()),
"use_has_next": True, # 避免 count(*)
"page_no": 1,
"page_size": page_size,
}
tids = []
# 倒序返回:从后往前翻
page = 1
while True:
params["page_no"] = page
resp = self.top.execute("taobao.trades.sold.increment.get", params)
body = resp.get("trades_sold_increment_get_response", {})
result = body.get("trades", {})
trades = result.get("trade", [])
if not trades:
break
for t in trades:
tids.append(str(t["tid"]))
# 记录最大 modified 作为下次起点
self.store.maybe_update_last_modified(t["modified"])
if not result.get("has_next"):
break
page += 1
# 再拉详情
for tid in tids:
self.pull_fullinfo(tid)
def pull_fullinfo(self, tid: str):
resp = self.top.execute("taobao.trade.fullinfo.get", {
"tid": tid,
"fields": (
"tid,status,payment,modified,buyer_nick,receiver,"
"orders,logistics,refund_infos,oaid,credit_card_fee"
),
})
trade = resp.get("trade_fullinfo_get_response", {}).get("trade")
if trade:
apply_order_snapshot(trade)七、Python:消息接收端(快返回 + 去重)
# top/webhook_order.py
import time
from fastapi import FastAPI, Request, Response
app = FastAPI()
# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
@app.post("/webhook/taobao/order")
async def order_webhook(req: Request):
raw = await req.body()
# 1) 验签(TOP:md5(secret + k+v 排序 + secret) 或 top-sig,按你接入方式)
if not verify_top_sign(raw, req.headers):
return Response("invalid", status_code=401)
payload = parse_top_message(raw)
msg_id = payload.get("msgId") or payload.get("id")
tid = str(payload.get("tid") or payload.get("orderId") or "")
# 2) msgId 去重
if inbound.exists(msg_id):
return Response("ok", status_code=200)
try:
inbound.insert(msg_id, tid, payload, received_at=time.time())
except DuplicateKeyError:
return Response("ok", status_code=200)
# 3) 只入队,不同步拉接口
order_change_queue.publish({"tid": tid, "msg_id": msg_id})
# 4) 必须快返回,别在 HTTP 线程里调 fullinfo.get
return Response("ok", status_code=200)def consume_order_change(): while True: job = order_change_queue.pop() try: pull_fullinfo(job["tid"]) # 消息只触发,fullinfo 才是真相 except RetryableError: order_change_queue.requeue(job, backoff=2 ** job["retry"]) except FatalError as e: dead_letter.save(job, str(e))
八、状态机:消息和轮询写同一张表
TERMINAL = {"TRADE_CLOSED", "TRADE_CLOSED_BY_TAOBAO", "TRADE_FINISHED"}
def apply_order_snapshot(trade: dict):
tid = str(trade["tid"])
cur = order_repo.get(tid)
incoming_modified = parse_time(trade["modified"])
incoming_version = int(incoming_modified.timestamp() * 1000)
if cur is None:
order_repo.insert(trade)
emit_order_event(trade)
return
if incoming_version < cur.version:
return # 旧消息晚到,丢弃
if incoming_version == cur.version and trade["status"] == cur.status:
return # 幂等
if cur.status in TERMINAL and trade["status"] not in TERMINAL_TRANSITIONS[cur.status]:
log.warn("rollback blocked", tid, cur.status, trade["status"])
return
order_repo.update(trade, version=incoming_version)
emit_order_event(trade)九、成本对比(日 3 万单变更)
方案 | 日调 fullinfo | 塔内费用 | 塔外费用 | 实时性 | 漏单风险 |
|---|---|---|---|---|---|
每 30s 轮询 | ~86400 次 | ¥17.28 | ¥172.8 | 30s | 低(前移不够仍漏) |
每 5min 轮询 | ~8640 次 | ¥1.73 | ¥17.28 | 5min | 中 |
消息 + 5min 兜底 | ~3000 次 | ¥0.6 | ¥6.0 | 秒级 | 低 |
DSS 推送 + 不轮询 | 0 次 API | ¥36(3万单×0.12/百) | 不可用 | 秒级 | 低(DSS 自身延迟) |
结论:单少 → 轮询最省心;单多 → 消息+DSS 更便宜;纯消息 → 大促必漏;纯轮询 → 塔外必破产。
十、大促额外坑
trade.fullinfo.get大促前后可能延时,付款/物流信息晚 1 分钟commission_fee大促期间可能不返回order.refund_id/special_refund_type大促期间可能不返回增量窗口必须前移 30 分钟
消息重试失败 → 必须有 5min 增量兜底
十一、落地检查表
[ ] 首次:
trades.sold.get分段拉 3 个月 + fullinfo[ ] 日常:TMC/DSS 消息 → 入队 → fullinfo
[ ] 兜底:increment.get 每 1~5 min,窗口前移 10min(大促 30min)
[ ] increment 分页从后往前,type 传全
[ ] msgId 去重,tid+modified 推进状态
[ ] fullinfo 是真相源,消息/列表都不是
[ ] 塔外部署订单类接口 = 烧钱;敏感字段(姓名/手机/地址)走 OAID/密文
[ ] 大促前压测消息消费者,不是压测轮询
十二、一句话收口
淘宝订单同步的成熟度和“你会不会调 fullinfo”无关,和这三件事有关:消息只当闹钟,轮询只当保险,fullinfo 才当法官。消息丢了轮询补,轮询慢了消息催,两者都落到tid+status+modified上求最终一致——这才是淘宝 TOP 订单域的生产解法。