《1688物流API接口边界:logistics.trace.get 与 freight.template.list 的隐藏约束》(附Python源码)
1688 物流域有两个“看起来简单、实际上暗坑极多”的接口:alibaba.logistics.trace.get不是按运单号查世界的,而是“按订单/物流编号看平台已录入的轨迹”;freight.template.list(/delivery.template.list)不是用来算运费的,而是“读商家运费模板结构”的,真算钱要靠重量/件数/地区/子模板自己复刻平台规则。把“轨迹查询”当实时快递网关、把“模板列表”当运费计算器,是 ERP 接入 1688 物流最常见的两类幻觉。
一、alibaba.logistics.trace.get:轨迹接口的真实边界
1. 它到底吃什么参数
order_id:必须,交易订单号trade_source_type:必须,例如cbu-tradelogistics_id:可选,订单下的物流编号(如AL8234243)
trace_list[] ├─ logistics_id ├─ order_id ├─ logistics_bill_no # 真正运单号 └─ logistics_steps[] ├─ accept_time ├─ remark └─ trace_node_list[] ├─ accept_time ├─ action # TRANSPORT / SIGN / UNSIGN ├─ facility_name ├─ facility_type # 网点 / 分拨中心 ├─ area_code └─ remark
⚠️ 重点:不是company_code + waybill_no查全网快递;它是“1688 订单关联的那张物流单,平台现在知道哪些节点”。
2. 隐藏约束(文档不写但会咬人)
未发货查不到
订单没发货 → 返回成功但
trace_list空,或别处接口直接报500_2 订单尚未发货正确动作:监听“已发货消息”后再查,不要定时盲扫所有单
轨迹有录入延迟
快递公司已经派送了,1688 侧可能晚几分钟到几小时
不能拿
trace.get的“无 SIGN 节点”直接判“未签收”一个订单可能多物流单
拆单发货:A 仓发 2 件、B 仓发 3 件
只看第一个
trace_list[0]会丢货action 不是淘宝那套
1688:
TRANSPORT / SIGN / UNSIGN淘宝:
ARRIVE / SIGN / SENT_SCAN直接复用淘宝状态机 = 签收逻辑错乱
bill_no 可能后期才补
供应商“点了发货”但电子面单没回写 →
logistics_bill_no空这时候要:等 → 重推 → 再查,不是立刻报异常
QPS 不是无限
物流类也吃开放平台流控,盲轮询 500 单/30s 必被限流
二、freight.template.list / delivery.template.list:模板接口的真实边界
1. 它干什么
模板名
计费维度
是否包邮
子模板(快递 / 货运 / 系统模板)
地区费率
2. 计费维度藏在子模板里
DeliverySubTemplate chargeType: 0 = 按重量 1 = 按件数 2 = 按体积 serviceType: 0 = 快递 1 = 货运 2 = 货到付款 serviceChargeType: 0 = 卖家承担 1 = 买家承担 firstUnit # 首重(克) / 首件(件) / 首体积 firstUnitFee # 单位:分 nextUnit nextUnitFee leastExpenses # 最低一票 toAreaCodeText # 上海、福建、广东
重点:接口给你“规则”,不给你“这笔订单运费 = 12.5”。平台自己算,因为还要叠:订单总重、是否跨模板、是否满包邮、买家地区、是否货到付款。
3. 隐藏约束(成本算错就亏)
多商品多模板 ≠ 简单累加
老逻辑:各商品按自己模板算,再加总
1688 新逻辑(2024-10 后):同订单多模板时,用总重/总件数分别套每个模板,取对买家更低的那个
首重单位可能是克不是千克
firstUnit=1000是 1000 克你用 1.5 kg 直接塞进
firstUnit=1的模板 → 运费翻 1000 倍包邮门槛是“实付货值”还是“含运费”要分清
满 99 包邮:通常是货值满 99,不是货值+运费满 99
系统模板(isSysTemplate)不能当普通模板改
平台默认偏远地区/默认快递,覆盖逻辑要单独处理
模板列表 ≠ 商品当前生效模板
商品可能绑了模板 A,但商家刚改成模板 B
下单前要看商品详情里的
freightTemplateId,不是“商家最后一个模板”跨境/货到付款/大件货运是不同 serviceType
用快递模板算大件 = 血亏
三、生产向:轨迹归一化 + 运费预估(不是精算)
1. 轨迹客户端
# ali1688/logistics_trace.py
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class TraceNode:
accept_time: str
action: str # TRANSPORT / SIGN / UNSIGN
facility_name: str
area_code: str
remark: str
@dataclass
class LogisticsTrack:
logistics_id: str
bill_no: Optional[str]
nodes: list[TraceNode]
raw: dict = field(default_factory=dict)
@property
def signed(self) -> bool:
return any(n.action == "SIGN" for n in self.nodes)
@property
def latest_remark(self) -> str:
return self.nodes[-1].remark if self.nodes else ""
# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
class Ali1688TraceClient:
def __init__(self, top_client, qps_limiter):
self.top = top_client
self.limiter = qps_limiter
def get_track(self, order_id: int, logistics_id: str = None) -> Optional[LogisticsTrack]:
self.limiter.acquire("logistics.trace.get")
params = {
"order_id": order_id,
"trade_source_type": "cbu-trade",
}
if logistics_id:
params["logistics_id"] = logistics_id
# resp = self.top.execute("alibaba.logistics.trace.get", params)
resp = {"trace_list": []} # 伪响应
traces = []
for t in resp.get("trace_list", []):
nodes = [
TraceNode(
accept_time=n.get("accept_time", ""),
action=n.get("action", ""),
facility_name=n.get("facility_name", ""),
area_code=n.get("area_code", ""),
remark=n.get("remark", ""),
)
for n in t.get("trace_node_list", [])
]
traces.append(LogisticsTrack(
logistics_id=t.get("logistics_id", ""),
bill_no=t.get("logistics_bill_no") or None,
nodes=nodes,
raw=t,
))
# 多物流单:返回列表,不让上层只看第一单
return tracestracks = client.get_track(order_id)
if not tracks:
# 可能是未发货 / 延迟录入 → 进“待补查队列”,不报失败
return {"state": "no_trace_yet"}
all_signed = all(t.signed for t in tracks)
any_bill = [t.bill_no for t in tracks if t.bill_no]2. 运费模板“预估器”(只做采购成本下限)
# ali1688/freight_estimator.py from dataclasses import dataclass from enum import IntEnum class ChargeType(IntEnum): WEIGHT = 0 # 克 COUNT = 1 # 件 VOLUME = 2 # 立方厘米/方 @dataclass class FreightRule: charge_type: int first_unit: float first_fee_fen: int next_unit: float next_fee_fen: int least_fee_fen: int area_text: str buyer_pays: bool def estimate_freight(rule: FreightRule, qty: float) -> int: """ qty: - 重量场景 = 克 - 件数场景 = 件 - 体积场景 = 体积单位 """ if qty <= 0: return 0 if qty <= rule.first_unit: fee = rule.first_fee_fen else: extra = qty - rule.first_unit steps = (extra + rule.next_unit - 1) // rule.next_unit fee = rule.first_fee_fen + int(steps) * rule.next_fee_fen return max(fee, rule.least_fee_fen) # 多模板订单:平台新逻辑是“取低” def pick_1688_new_logic(rules: list[FreightRule], total_qty: float) -> int: fees = [estimate_freight(r, total_qty) for r in rules] return min(fees) if fees else 0
注意:这只是采购侧成本预估/异常拦截。真要对买家收银:还得叠地区匹配、包邮门槛、订单级多模板、货到付款加价——这些平台不给你算好,也不保证你复刻得和 1688 完全一样。
四、和前几篇的拼接
《1688 订单 API》:
trace.get是订单状态机的“物流子状态源”《1688 商品 API》:
freightTemplateId在商品里,模板规则在freight.template.list里《统一采购适配层》:运费要进“采购成本”,不是直接等于“买家付的运费”
《两套接口边界》:物流轨迹/运费模板是“供货侧元数据”,不能当成“闲鱼/Mercari 买家物流页的直接数据源”
五、上线前检查表
logistics.trace.get[ ] 只在“已发货”后查,不盲轮询
[ ] 处理多物流单(拆单)
[ ]
bill_no为空进补偿队列[ ] action 用 1688 枚举,不抄淘宝
[ ] 无 SIGN 不代表未签收(有延迟)
[ ] 物流类接口走令牌桶
freight.template.list[ ] 不把“模板列表”当“订单运费”
[ ] 重量用克、件数用件、体积用体积极
[ ] 多模板订单用“平台新逻辑取低”
[ ] 包邮门槛/地区/最低一票都生效
[ ] 商品绑哪个模板以商品详情为准
[ ] 运费只做成本预估,不对买家强制精算
六、一句话收口
logistics.trace.get是“平台知道到哪了”,不是“快递公司官网”;freight.template.list是“商家定了个规则”,不是“这笔单运费多少钱”。1688 物流对接的成熟度 =轨迹当“事件流”消费,运费当“成本模型”估算,绝不假装自己是 1688 计费内核。