×

《抖音订单API:/trade/order/detail 落地方案 —— 消息推送 + 补偿查询的架构设计》(附 Python 源码)

万邦科技Lex 万邦科技Lex 发表于2026-10-17 09:15:55 浏览31 评论0

抢沙发发表评论

《抖音订单API:/trade/order/detail 落地方案 —— 消息推送 + 补偿查询的架构设计》(附 Python 源码)

一、先拍结论

抖店订单同步的成熟架构只有一句话:
“消息/数据推送做触发器,/order/searchList 做时间窗兜底,/order/orderDetail 拉真相,本地状态机做仲裁。”
你写的 /trade/order/detail 在抖店开放平台里更接近“抖音开放平台担保支付/交易订单”语义;抖店(抖店开放平台)真实订单详情是 /order/orderDetail,列表是 /order/searchList。
官方自己都明说:
  • 订单详情接口存在部分数据返回延时,订单生成后延时 10s 再查;极端情况延迟 20 分钟,要用补偿机制再查一次

  • 数据推送(抖店云 RDS)按 B 端事件触发,不是底层表每行变更都推

  • 只靠消息推送 = 高峰丢包/重试失败/服务器慢响应 → 漏单

  • 正确姿势:消息推送 + 接口轮询补偿


二、抖店订单接口边界(先纠名)

接口
角色
说明
消息推送 doudian_trade_TradeCreate/Paid/...
触发器
只给 order_id / 事件类型
数据推送(抖店云 RDS)
准实时落库
推密文字段到绑定库,订单/售后/商品可配
/order/searchList
补偿/初始化
按创建时间或更新时间拉列表
/order/orderDetail
真相源
子单、优惠、收货密文、履约、售后态
/afterSale/List + /afterSale/Detail
售后真相
退款/退货/换货独立轴
规则:
消息 ≠ 订单数据,列表 ≠ 订单详情,数据推送 ≠ 业务已处理。

三、整体架构

┌──────────────────────────────┐
│ 抖店平台                      │
│  - 消息推送(TradeCreate等)  │
│  - 数据推送(RDS 密文)       │
└────────────┬─────────────────┘
             │
             ▼
┌──────────────────────────────┐
│ Push Ingest(2~5s 返回 success)│
│  - msg_id 去重                │
│  - 只入队 (shop_id, order_id) │
└────────────┬─────────────────┘
             ▼
     ┌─────────────────┐
     │  Order Detail    │
     │  Worker          │
     │ /order/orderDetail│
     │ +10s 延迟 + 20min │
     │   补偿            │
     └────────┬──────────┘
              ▼
     ┌─────────────────┐
     │ 状态机 + 幂等写   │
     │ version 仲裁     │
     └────────┬──────────┘
              ▼
     ┌─────────────────┐
     │ 补偿轮询 Cron     │
     │ /order/searchList│
     │ 更新时间窗 滚动   │
     └─────────────────┘
双通道:
  1. 推模型:快、省调用、但会丢/重/乱序

  2. 拉模型:慢、烧调用、但能兜底


四、为什么不能直接“消息来了就发货”

抖店订单详情有延迟:
  • 新单 10s 内可能拿不到完整数据

  • 达人信息要等联盟结算,最长延迟 1 小时

  • 数据推送的 version 不再是时间戳,不能拿来做时间排序

  • 翻页时订单状态变化 → 正向翻页跳单 → 必须倒序/时间窗重叠

所以消息只干一件事:
order_id 进队,标记 need_detail=true

五、Python:抖店订单 Client

# douyin/order_client.py
import time
import hashlib
import hmac
import requests


class DouyinOrderClient:
    GATEWAY = "https://openapi-fxg.jinritemai.com/"

    def __init__(self, app_key, app_secret, access_token, sign_method="hmac-sha256"):
        self.app_key = app_key
        self.app_secret = app_secret
        self.access_token = access_token
        self.sign_method = sign_method

    def _sign(self, params: dict) -> str:
        qs = "&".join(f"{k}={params[k]}" for k in sorted(params))
        if self.sign_method == "hmac-sha256":
            return hmac.new(self.app_secret.encode(), qs.encode(), hashlib.sha256).hexdigest()
        return hashlib.md5((self.app_secret + qs + self.app_secret).encode()).hexdigest().upper()
# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
    def _common(self, method: str):
        return {
            "method": method,
            "app_key": self.app_key,
            "access_token": self.access_token,
            "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
            "v": "2",
            "sign_method": self.sign_method,
            "format": "json",
        }

    def _post(self, method: str, biz: dict):
        params = self._common(method)
        params["param_json"] = str(biz).replace("'", '"')
        params["sign"] = self._sign(params)
        resp = requests.post(self.GATEWAY, data=params, timeout=5)
        return resp.json()

    def search_list(self, start_time, end_time, order_by=1, page=1, page_size=100, order_status=None):
        biz = {
            "start_time": start_time,
            "end_time": end_time,
            "order_by": order_by,       # 1=按更新时间倒序更稳
            "page": page,
            "page_size": page_size,
            "is_desc": 1,
        }
        if order_status is not None:
            biz["order_status"] = order_status
        return self._post("order.searchList", biz)

    def order_detail(self, order_id: str):
        return self._post("order.orderDetail", {"order_id": order_id})

六、Detail Worker:10s 延迟 + 20min 补偿

# douyin/order_detail_worker.py
import time


class OrderDetailWorker:
    def __init__(self, client, repo, dedup, bucket, queue):
        self.client = client
        self.repo = repo
        self.dedup = dedup
        self.bucket = bucket
        self.queue = queue

    def handle(self, shop_id: str, order_id: str, delay=10):
        # 第一次:延 10s 再查
        time.sleep(delay)
        snap = self._fetch(order_id)
        if not self._data_complete(snap):
            # 极端延迟:20 分钟后补偿
            self.queue.enqueue_delay(
                {"shop_id": shop_id, "order_id": order_id, "phase": "compensate"},
                delay=20 * 60,
            )
            return

        self._apply(shop_id, snap)

    def _fetch(self, order_id: str):
        self.bucket.acquire()
        resp = self.client.order_detail(order_id)
        return (resp.get("data") or {}).get("order_info") or resp
# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
    def _data_complete(self, snap) -> bool:
        if not snap:
            return False
        # 刚支付时可能缺收件/子单/优惠拆分
        if snap.get("pay_status") in (1, 2) and not snap.get("sku_order_list"):
            return False
        return True

    def _apply(self, shop_id, snap):
        event_key = f"detail:{snap.get('order_id')}:{snap.get('update_time')}"
        if not self.dedup.mark(shop_id, snap.get("order_id"), event_key):
            return
        self.repo.upsert_order(shop_id, snap)
        self.repo.emit_business_events(snap)

七、补偿轮询:时间窗前移 + 倒序 + 重叠

# douyin/order_compensator.py
from datetime import datetime, timedelta


class OrderCompensator:
    def __init__(self, client, repo, bucket):
        self.client = client
        self.repo = repo
        self.bucket = bucket

    def run(self, shop_id, now: datetime):
        # 结束时间前移 2~5 分钟,避开平台入库延迟
        end = now - timedelta(minutes=3)
        start = end - timedelta(minutes=30)

        page = 1
        min_update_seen = end

        while True:
            self.bucket.acquire()
            resp = self.client.search_list(
                start_time=int(start.timestamp()),
                end_time=int(end.timestamp()),
                order_by=1,     # 更新时间倒序
                page=page,
            )
            orders = self._extract(resp)
            if not orders:
                break

            for o in orders:
                self.repo.mark_need_detail(shop_id, o["order_id"], o.get("update_time"))
                u = int(o.get("update_time", 0))
                if u < min_update_seen.timestamp():
                    min_update_seen = datetime.fromtimestamp(u)

            if len(orders) < 100 or page >= 50:
                break
            page += 1

        # 下一轮从最小 update_time 往前,并留 1 分钟重叠防跳单
        return min_update_seen - timedelta(minutes=1)
规则(抖店专用):
  • 结束时间 前移 2~5 分钟

  • 按更新时间 倒序,从后往前翻

  • 下一轮起点 = 本批最小 update_time - 重叠窗口

  • 不深翻页,不靠 page=9999


八、幂等键与状态机

def order_version(snap) -> int:
    # 抖店 version 不是时间戳了,用 update_time 做业务版本
    return int(snap.get("update_time") or 0)


def apply_order(repo, shop_id, snap):
    cur = repo.get_order(shop_id, snap["order_id"])
    new_ver = order_version(snap)

    if cur is None:
        repo.insert_order(shop_id, snap)
        fire_events(None, snap)
        return

    if new_ver < cur.version:
        return  # 旧消息晚到
    if new_ver == cur.version and snap.get("order_status") == cur.order_status:
        return  # 完全幂等

    check_order_transition(cur.order_status, snap["order_status"])
    rows = repo.update_order_if_newer(shop_id, snap, cur.version)
    if rows == 1:
        fire_events(cur, snap)
SQL 核心:
UPDATE douyin_order
SET order_status=#, pay_amount=#, post_addr=#,
    version=#, update_time=#
WHERE shop_id=# AND order_id=#
  AND version < #{new_version};

九、消息推送接入(只做触发器)

# douyin/push_webhook.py
def on_push(body: dict):
    # 2~5 秒内必须返回 success
    msg_id = body["msg_id"]
    if dedup.msg_seen(msg_id):
        return "success"

    tag = body["tag"]                 # TradeCreate / TradePaid ...
    order_id = body["data"]["order_id"]
    shop_id = body["shop_id"]

    queue.publish({"shop_id": shop_id, "order_id": order_id, "trigger": tag})
    dedup.mark_msg(msg_id)
    return "success"
消息类型(节选):
  • doudian_trade_TradeCreate:下单

  • doudian_trade_TradePaid:支付成功

  • doudian_trade_TradePartlySellerShip / TradeSellerShip:发货

  • doudian_trade_TradeSuccess:完成

  • doudian_trade_TradeCanceled:取消

  • doudian_trade_AuthorInfo:达人信息补推

消息只告诉你要“去看哪个 order_id”,别把消息体当订单真相。

十、数据推送(抖店云 RDS)怎么摆

抖店云数据推送:
  • 推到绑定 RDS,不是推到你的业务库

  • 订单/售后/商品可配

  • 历史 60 天、保留 60 天可配

  • version 不再当时间戳

生产摆法:
抖店云 RDS(密文)
   └─ CDC / 定时读
        └─ 业务库:order_raw(密文)
             └─ 解密服务(云内)
                  └─ order_biz(明文只在云内)
合规底线:
明文收货信息/手机号解密服务必须抖店云内;云外只持有 order_id / 状态 / 金额 / 商品,不持有明文。

十一、售后轴独立(和拼多多同思路)

def sync_aftersale(shop_id, start, end):
    while True:
        resp = client._post("afterSale.List", {
            "start_time": start, "end_time": end, "page": page, "page_size": 100
        })
        for a in extract(resp):
            detail = client._post("afterSale.Detail", {
                "order_id": a["order_id"], "after_sale_id": a["after_sale_id"]
            })
            apply_aftersale(shop_id, detail)
订单表只存:
after_sale_status = 最新一条售后单的投影
真实仲裁看 douyin_after_sale 表。

十二、调用配额与降本

  • 消息推送:不按 API 次数烧钱

  • 数据推送:走抖店云 RDS,不是 /order/orderDetail 轮询

  • /order/searchList:补偿用,别当实时主源

  • /order/orderDetail:按 order_id 触发,不批量盲拉

  • 新单:消息 → 10s 后 detail

  • 老单/漏单:30min 窗补偿

  • 大促:searchList 窗缩到 5~10min,detail 桶单独限流


十三、落地检查表

  • [ ] 消息 webhook 2~5s 返回 success,不写业务

  • [ ] msg_id 去重,order_id 进队

  • [ ] detail 延 10s 查,不完整就排 20min 补偿

  • [ ] searchList 结束时间前移 3min,倒序,重叠 1min

  • [ ] order_id 唯一键 + event_key 去重 + version(update_time) 仲裁

  • [ ] 订单轴 / 售后轴 / 达人信息轴分开

  • [ ] 达人信息用 doudian_trade_AuthorInfo 覆盖,别信创建时刻

  • [ ] 明文解密服务在抖店云内,云外只留密文/状态/金额

  • [ ] 数据推送 RDS 是“准实时源”,不是“业务已处理”

  • [ ] 大促补偿窗缩短、detail QPS 单独桶、余额守卫在 Client 层


十四、一句话收口

淘宝用 DSS 推、京东用 JMQ 推、拼多多靠增量轮询、抖店是“消息推送 + 抖店云数据推送 + searchList 补偿 + orderDetail 拉真相”四件套。
抖店订单最阴的三件事是:
新单详情延迟 10s~20min、version 不再是时间戳、达人信息要等联盟结算补推。
所以别把消息当数据、别把列表当详情、别把创建时刻的达人信息当最终口径。
消息负责“叫你起床”,searchList 负责“查谁没醒”,orderDetail 负责“看清楚这张脸”。


群贤毕至

访客