×

《1688订单API落地方案:trade.order.get + 消息推送,正向/逆向订单的幂等与状态机》(附Python源码)

万邦科技Lex 万邦科技Lex 发表于2026-09-29 10:09:48 浏览31 评论0

抢沙发发表评论

《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 不允许,除非先经历 PAID

  • FINISHED 是终态,不再接受“已付款”老消息

  • CLOSED 可能是未付关闭,也可能是退款关单 → 必须看 refund 上下文


三、逆向订单状态机(售中/售后退款)

1688 退款消息里的 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"

五、消息推送接收器(轻量版)

1688 开放平台有订单/退款类消息主题,例如:
  • 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}

六、轮询兜底(消息丢了还能救)

消息不是 100% 可达,所以要“推送为主 + 增量轮询兜底”:
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 采购单就不会在“已退款”和“已发货”之间精神分裂。

群贤毕至

访客