《1688订单API落地方案:trade.order.get + 消息推送,正向/逆向订单的幂等与状态机》(附Python源码)
1688 订单域最容易被写崩的不是“查不到单”,而是:正向单和逆向单用同一张表瞎存、消息重复消费把“已退款”又算一遍、平台状态机和你自己 ERP 状态机对不齐、轮询把配额打光还丢更新。正确姿势是:消息推送做“触发器”,alibaba.trade.get.buyerView/alibaba.trade.order.get做“真相源”,本地用(order_id, status_version)做幂等,正向/逆向两套状态机解耦。
一、1688 订单真相源:别信自己缓存,信拉取结果
消息/推送:告诉你“有变化发生了”
alibaba.trade.get.buyerView/alibaba.trade.order.get:拿到当前订单/子单/物流/退款字段推送丢、重、乱序都正常 → 收到消息先入队,再按 order_id 去重拉详情
order_id 1688 采购单号 sub_order_id 子单号 status 正向状态:wait_pay / paid / shipped / signed / closed refund_status 子单/订单退款态 logistics 物流公司 + waybill_no + trace pay_amount 实付 supplier_id offer 对应的商家 outer_order_no 你 ERP 单号(下游平台单)
二、正向订单状态机(采购侧)
from enum import Enum, auto
class ForwardState(str, Enum):
UNKNOWN = "unknown"
WAIT_PAY = "wait_pay"
PAID = "paid"
SHIPPED = = "shipped"
SIGNED = "signed"
FINISHED = "finished"
CLOSED = "closed"
FORWARD_TRANSITIONS = {
ForwardState.UNKNOWN: {ForwardState.WAIT_PAY, ForwardState.CLOSED},
ForwardState.WAIT_PAY: {ForwardState.PAID, ForwardState.CLOSED},
ForwardState.PAID: {ForwardState.SHIPPED, ForwardState.CLOSED},
ForwardState.SHIPPED: {ForwardState.SIGNED, ForwardState.CLOSED}, # closed=退款关单
ForwardState.SIGNED: {ForwardState.FINISHED},
ForwardState.FINISHED: set(),
ForwardState.CLOSED: set(),
}WAIT_PAY → SHIPPED不允许,除非先经历PAIDFINISHED是终态,不再接受“已付款”老消息CLOSED可能是未付关闭,也可能是退款关单 → 必须看 refund 上下文
三、逆向订单状态机(售中/售后退款)
refundAction 很细:BUYER_APPLY_REFUND / SELLER_AGREE_REFUND / SELLER_REJECT_REFUND / BUYER_SEND_GOODS / SELLER_RECEIVE_GOODS / SYSTEM_AGREE_REFUND ...class RefundState(str, Enum):
NONE = "none"
BUYER_APPLIED = "buyer_applied"
SELLER_REJECTED = "seller_rejected"
WAIT_BUYER_RETURN = "wait_buyer_return"
BUYER_RETURNED = "buyer_returned"
SELLER_RECEIVED = "seller_received"
REFUNDING = "refunding"
REFUNDED = "refunded"
CLOSED = "closed"
REFUND_TRANSITIONS = {
RefundState.NONE: {RefundState.BUYER_APPLIED},
RefundState.BUYER_APPLIED: {
RefundState.SELLER_REJECTED,
RefundState.WAIT_BUYER_RETURN,
RefundState.REFUNDING,
RefundState.CLOSED,
},
RefundState.SELLER_REJECTED: {
RefundState.BUYER_APPLIED, RefundState.CLOSED
},
RefundState.WAIT_BUYER_RETURN: {
RefundState.BUYER_RETURNED, RefundState.CLOSED
},
RefundState.BUYER_RETURNED: {RefundState.SELLER_RECEIVED},
RefundState.SELLER_RECEIVED: {RefundState.REFUNDING},
RefundState.REFUNDING: {RefundState.REFUNDED, RefundState.CLOSED},
RefundState.REFUNDED: set(),
RefundState.CLOSED: set(),
}核心原则:REFUNDED 是钱相关终态,不能被“卖家拒绝”这种迟到消息打回去。状态只能前进/关闭,不能从终态逆推。
四、幂等模型:消息可能会推 3 次
msgId,用业务键:正向:order_id 逆向:refund_id(没有就 order_id + sku_id + refund_phase) 状态推进: (order_id, status, status_version)
CREATE TABLE ali1688_order_state ( order_id BIGINT PRIMARY KEY, sub_order_id BIGINT, forward_state VARCHAR(32), refund_state VARCHAR(32), refund_id VARCHAR(64), status_version BIGINT NOT NULL, last_platform_ts BIGINT, external_order_no VARCHAR(64), updated_at TIMESTAMP ); CREATE TABLE order_state_log ( id BIGSERIAL PRIMARY KEY, order_id BIGINT, from_state VARCHAR(32), to_state VARCHAR(32), event VARCHAR(64), platform_ts BIGINT, msg_dedup_key VARCHAR(128) UNIQUE, created_at TIMESTAMP DEFAULT now() );
def consume_order_event(evt):
dedup_key = evt.dedup_key() # order_id + event + platform_ts
if state_log.exists(dedup_key):
return "dedup"
detail = ali_client.get_order(evt.order_id) # 真相源
platform_ts = detail.modified_time
row = order_tbl.get_for_update(evt.order_id)
if row and platform_ts <= row.last_platform_ts:
state_log.insert(dedup_key, "ignore_old_ts")
return "stale"
new_forward = map_forward(detail.status)
new_refund = map_refund(detail)
if not can_transition(row.forward_state, new_forward, FORWARD_TRANSITIONS):
alarm("非法正向跃迁")
return "reject"
if not can_transition(row.refund_state, new_refund, REFUND_TRANSITIONS):
alarm("非法逆向跃迁")
return "reject"
order_tbl.update(
order_id=evt.order_id,
forward_state=new_forward,
refund_state=new_refund,
refund_id=detail.refund_id,
status_version=row.status_version + 1,
last_platform_ts=platform_ts,
)
state_log.insert(dedup_key, f"{row.forward_state}->{new_forward}")
emit_downstream(evt.order_id, new_forward, new_refund)
return "ok"五、消息推送接收器(轻量版)
ORDER_BUYER_VIEW_BUYER_MAKE下单ORDER_BUYER_VIEW_ORDER_SELLER_CLOSE关单ORDER_BUYER_VIEW_ORDER_COMFIRM_RECEIVEGOODS确认收货ORDER_BUYER_VIEW_ORDER_REFUND_AFTER_SALES售后退款
from fastapi import FastAPI, Request
import hmac, hashlib, json
app = FastAPI()
WEBHOOK_SECRET = "***"
# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
@app.post("/webhook/1688/order")
async def webhook(req: Request):
raw = await req.body()
sig = req.headers.get("x-1688-signature", "")
expect = hmac.new(WEBHOOK_SECRET.encode(), raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, expect):
return {"ok": False, "msg": "bad sig"}
msg = json.loads(raw)
# 只入队,不在这里查单/改单
mq.publish("ali1688.order.events", {
"order_id": msg.get("orderId"),
"topic": msg.get("topic"),
"current_status": msg.get("currentStatus"),
"msg_send_time": msg.get("msgSendTime"),
"dedup_key": f"{msg.get('orderId')}:{msg.get('topic')}:{msg.get('msgSendTime')}",
})
return {"ok": True}六、轮询兜底(消息丢了还能救)
def poll_incremental(last_modified_min: int): # 用 gmtModified 区间,不走“全量扫 24h” page = 1 while True: resp = ali_client.list_orders( modified_start=last_modified_min, page=page, page_size=50, ) for o in resp.orders: consume_order_event(OrderEvent.from_poll(o)) if not resp.has_more: break page += 1
不每 5 分钟全量拉全部订单
用
modified时间窗 + 游标轮询和推送走同一个
consume_order_event,幂等逻辑只有一份
七、正向/逆向怎么连到下游(闲鱼/Mercari)
1688 PAID → 下游:采购已付,等发货 1688 SHIPPED → 下游:回写物流(闲鱼 order.ship / Mercari 标记发货) 1688 SIGNED → 下游:交易完成 1688 refund → 下游:触发“买家退款 / 截单 / 退供 / 补发”
正向状态变更 → 动“销售单物流”
逆向状态变更 → 动“售后工单”,不直接改销售单金额
部分退款:子单维度处理,别用主单退款额覆盖全部 SKU
供应商发货后无单号 → 走
getLogisticsTraceInfo补偿,不是人工天天看后台
八、和前几篇收口
《1688 商品 API》:offer/sku/价格/库存是“能不能买”
本文:
trade.order.get+ 消息是“买完之后世界怎么变”《统一采购适配层》:
offerId+skuId → erp_order_id在这里被状态机保护《两套接口边界》:1688 订单是供货侧;闲鱼/Mercari 订单是销售侧;两边状态机不能合并成一张表
《六大坑》:这里新增第 8 坑——“平台退款消息重复/乱序,导致 ERP 退两次钱或关错单”
九、上线前检查表
[ ] 消息接收器只入队,不写业务状态
[ ] 所有状态变更都先
trade.get.buyerView拉真相[ ] 正向/逆向状态机分开,REFUNDED 不可回退
[ ]
(order_id, platform_ts, dedup_key)三层防重[ ] 状态日志不可改,只 append
[ ] 轮询用 modified 游标,不全量扫
[ ] 子单维度处理部分退款
[ ] 物流缺失有补偿任务,不靠人眼
[ ] 钱相关动作(退款/截单/补发)走人工卡点 or 双写对账
十、一句话收口
1688 订单对接的成熟度 =**消息只当“门铃”,trade.order.get才进屋看人;正向管货,逆向管钱;状态机不许回头,幂等键不许用 msgId。**能做到这三条,1688 采购单就不会在“已退款”和“已发货”之间精神分裂。