×

《电商API Mock与沙箱:九家开放平台测试环境对照与使用技巧》(附Python源码)

万邦科技Lex 万邦科技Lex 发表于2026-08-22 10:12:45 浏览29 评论0

抢沙发发表评论

结论先拍:九家平台里只有亚马逊SP-API有官方"真沙箱"(static/dynamic两种,按区域分端点);淘宝/1688有官方沙箱网关但返回Mock或空数据、需独立沙箱AppKey+SessionKey;京东有sandbox.jd.com数据隔离;拼多多、抖店、快手、微店、苏宁没有独立"沙箱网关",靠"测试店铺+测试AppKey+预发权限"或本地Mock Server替代。 大促前联调的正确姿势是"平台沙箱验签名/字段 → 本地Mock Server跑边界/异常/降级 → 测试店铺小流量验证 → 切生产",千万别拿真实店铺调未验证代码。


一、九家测试环境对照(2026现行)

平台
官方沙箱网关
数据性质
独立Key
限流
关键技巧
淘宝TOP
gw.api.tbsandbox.com/router/rest 授权oauth.tbsandbox.com
Mock/空/测试店铺
沙箱AppKey+SessionKey(控制台开)
宽松无计费
商品ID原样回显;订单需沙箱c/b账号;装Chrome插件
1688
gw.open.1688.com/openapi/sandbox/http/2/1
Mock商品/订单
同应用Key可识别沙箱
同生产签名逻辑
个人号可验签;真实订单403;高级实时库存买包不在沙箱
京东JOS
sandbox.jd.com(宙斯沙箱)
数据完全独立
沙箱AppKey
无调用量限制但大批量易超时
正式API全量部署;超时率高于生产
拼多多
无独立沙箱网关
测试AppKey(预发)
同生产
用"测试店铺+小流量";本地Mock替身
抖店
无独立沙箱网关
测试AppKey
同生产
云内测试店铺+消息订阅沙箱开关;本地Mock替身
快手
无独立沙箱
测试AppKey
同生产
同抖店思路
微店
无独立沙箱
测试应用
同生产
开测试微店+本地Mock
苏宁
开放平台测试工具(无公开沙箱域名)
Mock
测试AppKey
同生产
用官方在线调试+本地Mock
亚马逊SP-API
sandbox.sellingpartnerapi-{na/eu/fe}.amazon.com
Static固定Mock / Dynamic按参反应
沙箱ClientID/Secret/Refresh
5 QPS / burst 15
CreatedAfter=TEST_CASE_200触发固定订单;Dynamic可驱动订单状态机
关键认知:"有沙箱"≠"有真实数据"。淘宝/1688/京东沙箱返回的是模拟数据,只能验签名+字段解析+状态机;亚马逊Static沙箱连"状态推进"都要手调updateOrderStatus,Dynamic才像点样子。真正测业务闭环,必须本地Mock Server

二、各平台使用技巧(血泪版)

淘宝/1688

  • 沙箱AppKey绝不能用生产AppSecret,控制台"配置沙箱环境"页会给你独立secret

  • taobao.item.get传啥ID回显啥ID,别拿"返回空"当Bug

  • 订单类必须沙箱c/b账号(sandbox_c_1/sandbox_b_01,密码taobao1234)+ 沙箱SessionKey

  • 奇门QIMEN也有独立沙箱:qimenapi.tbsandbox.com/router/qimen/service

京东

  • 沙箱https://sandbox.jd.com数据完全独立,但大批量拉取超时率高于生产,别拿来压测

  • 沙箱不扣调用费,但签名/时间戳/版本号错法和生产一致

亚马逊SP-API(最规范)

  • Static沙箱:Swagger JSON里找x-amzn-api-sandbox.static,传它写的request.parameters才匹配Mock;Orders传CreatedAfter=TEST_CASE_200拿到902-1845936-5435065那条$11.01订单

  • Dynamic沙箱:Fulfillment Outbound可createOrder→updateOrderStatus(COMPLETE)驱动状态机;数量>10自动拆包(每10个一个package)

  • 沙箱5 QPS / burst 15,只测功能不测扩展性

  • LWA换token用沙箱ClientID/Secret,AWS SigV4用沙箱端点,别混生产region

拼多多/抖店/快手/微店/苏宁(无沙箱五家)

  • 申请测试AppKey+测试店铺(拼多多/抖店开放平台控制台可建)

  • 订单同步代码先用本地Mock跑通(见下文MockServer),再小流量打测试店铺

  • 抖店消息订阅有"沙箱推送开关",可让Webhook打本地ngrok


三、Python:UnifiedMockServer(九家方言归一+沙箱切换)

# unified_mock_server.py
"""
九家电商API Mock与沙箱统一层
- 真实沙箱网关直连(淘宝/1688/京东/亚马逊)
- 无沙箱平台(拼多多/抖店/快手/微店/苏宁)走本地Mock Server
- 同一Client切 sandbox/prod 不改业务代码
- Mock数据按平台方言返回,字段结构对齐真实响应
"""
import hashlib, time, json, threading
from typing import Dict, List, Optional
from dataclasses import dataclass
from http.server import HTTPServer, BaseHTTPRequestHandler

# ==================== 沙箱端点表 ====================
SANDBOX_ENDPOINTS = {
    "taobao":  "https://gw.api.tbsandbox.com/router/rest",
    "1688":    "https://gw.open.1688.com/openapi/sandbox/http/2/1",
    "jd":      "https://api.sandbox.jd.com/routerjson",
    "amazon_na": "https://sandbox.sellingpartnerapi-na.amazon.com",
    "amazon_eu": "https://sandbox.sellingpartnerapi-eu.amazon.com",
    "amazon_fe": "https://sandbox.sellingpartnerapi-fe.amazon.com",
}
PROD_ENDPOINTS = {
    "taobao":  "https://gw.api.taobao.com/router/rest",
    "1688":    "https://gw.open.1688.com/openapi/param2/1/",
    "jd":      "https://api.jd.com/routerjson",
    "amazon_na": "https://sellingpartnerapi-na.amazon.com",
    "amazon_eu": "https://sellingpartnerapi-eu.amazon.com",
    "amazon_fe": "https://sellingpartnerapi-fe.amazon.com",
}
# 无官方沙箱的五家:本地Mock
NO_SANDBOX = {"pdd", "douyin", "kuaishou", "weidian", "suning"}

# ==================== 统一Client ====================
class UnifiedApiClient:
    def __init__(self, platform: str, app_key: str, app_secret: str,
                 session: str = "", region: str = "na", sandbox: bool = True):
        self.platform = platform
        self.app_key = app_key
        self.app_secret = app_secret
        self.session = session
        self.region = region
        self.sandbox = sandbox

    def _endpoint(self) -> Optional[str]:
        if self.platform in NO_SANDBOX:
            return None  # 走本地Mock
        if self.platform == "amazon":
            key = f"amazon_{self.region}"
        else:
            key = self.platform
        table = SANDBOX_ENDPOINTS if self.sandbox else PROD_ENDPOINTS
        return table.get(key)

    # ---- 淘宝/1688 MD5签名 ----
    def _sign_md5(self, params: Dict) -> str:
        s = self.app_secret + "".join(
            f"{k}{params[k]}" for k in sorted(params) if params[k] is not None
        ) + self.app_secret
        return hashlib.md5(s.encode()).hexdigest().upper()

    def call(self, method: str, biz: Dict) -> Dict:
        ep = self._endpoint()
        # 无沙箱平台 → 本地Mock
        if ep is None:
            return LocalMockStore().respond(self.platform, method, biz, sandbox=self.sandbox)

        if self.platform in ("taobao", "1688"):
            params = {
                "method": method, "app_key": self.app_key,
                "session": self.session,
                "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
                "format": "json", "v": "2.0", **biz
            }
            params["sign"] = self._sign_md5(params)
            # 演示:不真发请求,返回"会发到哪里"
            return {"_mock": "would_post", "url": ep, "params": params}

        if self.platform == "amazon":
            # 沙箱Static触发参数
            if self.sandbox and method == "getOrders":
                biz = {**biz, "CreatedAfter": "TEST_CASE_200"}
            return {"_mock": "would_get", "url": ep + "/orders/v0/orders", "params": biz}

        if self.platform == "jd":
            return {"_mock": "would_post", "url": ep, "params": {"method": method, **biz}}

        return {"_mock": "unsupported"}

# ==================== 本地Mock Server(无沙箱五家 + 全平台单测)====================
class LocalMockStore:
    """内存Mock:按平台返回贴近真实的字段结构"""

    def respond(self, platform: str, method: str, biz: Dict, sandbox: bool) -> Dict:
        if platform == "pdd":
            if method == "pdd.order.information.get":
                return {"order_sn": biz.get("order_sn", "pdd_mock_001"),
                        "order_status": "1", "pay_amount": "9900",
                        "receiver_name": "Mock李四",
                        "receiver_phone": "13900000000",
                        "item_list": [{"sku_id": "sku_1", "count": 1, "price": "9900"}]}
            if method == "pdd.goods.list.get":
                return {"goods_list": [{"goods_id": 123, "goods_name": "Mock商品",
                                        "price": "1990", "quantity": 50}]}
        if platform == "douyin":
            if method == "order.listQuery":
                return {"data": {"order_list": [{
                    "order_id": "dy_mock_001", "order_status": "1",
                    "pay_amount": 9900, "receiver_name": "Mock王五"}]}}
            if method == "sku.stockNum.update":
                return {"data": {"success": True}}
        if platform == "kuaishou":
            if method == "item.list":
                return {"items": [{"item_id": 999, "title": "Mock快手脚手"}]}
        if platform == "weidian":
            if method == "vdian.order.list":
                return {"orders": [{"order_id": "vd_mock_1", "status": "paid",
                                    "total": "88.00"}]}
        if platform == "suning":
            if method == "suning.api.order.query":
                return {"orderList": [{"orderId": "sn_mock_1", "status": "UNSHIPPED"}]}
        # 兜底
        return {"mock": True, "platform": platform, "method": method,
                "echo": biz, "sandbox": sandbox}

# ==================== 本地HTTP Mock Server(给SDK/Webhook用)====================
MOCK_ROUTES = {
    "/pdd/order/information/get": lambda b: LocalMockStore().respond("pdd", "pdd.order.information.get", b),
    "/douyin/order/listQuery": lambda b: LocalMockStore().respond("douyin", "order.listQuery", b),
    "/kuaishou/item/list": lambda b: LocalMockStore().respond("kuaishou", "item.list", b),
    "/weidian/order/list": lambda b: LocalMockStore().respond("weidian", "vdian.order.list", b),
    "/suning/order/query": lambda b: LocalMockStore().respond("suning", "suning.api.order.query", b),
}

class _Handler(BaseHTTPRequestHandler):
    def do_POST(self):
        length = int(self.headers.get("Content-Length", 0))
        body = json.loads(self.rfile.read(length) or b"{}")
        fn = MOCK_ROUTES.get(self.path)
        if fn:
            resp = fn(body)
            self.send_response(200)
            self.send_header("Content-Type", "application/json")
            self.end_headers()
            self.wfile.write(json.dumps(resp).encode())
        else:
            self.send_response(404)
            self.end_headers()

def start_mock_server(port=8899):
    srv = HTTPServer(("127.0.0.1", port), _Handler)
    t = threading.Thread(target=srv.serve_forever, daemon=True)
    t.start()
    return srv

# ==================== 演示 ====================
if __name__ == "__main__":
    # 1. 淘宝沙箱(验签名/网关切换)
    tb = UnifiedApiClient("taobao", "tb_appkey", "tb_secret",
                         session="sandbox_session_xxx", sandbox=True)
    print("淘宝沙箱调用:", tb.call("taobao.item.get", {"num_iid": "12345"}))

    # 2. 亚马逊NA沙箱(Static触发)
    amz = UnifiedApiClient("amazon", "cid", "csec", region="na", sandbox=True)
    print("亚马逊沙箱Orders:", amz.call("getOrders", {"MarketplaceIds": "ATVPDKIKX0DER"}))

    # 3. 拼多多无沙箱 → 本地Mock
    pdd = UnifiedApiClient("pdd", "pdd_key", "pdd_sec", sandbox=True)
    print("拼多多本地Mock订单:", pdd.call("pdd.order.information.get", {"order_sn": "123"}))

    # 4. 抖店无沙箱 → 本地Mock
    dy = UnifiedApiClient("douyin", "dy_key", "dy_sec", sandbox=True)
    print("抖店本地Mock订单:", dy.call("order.listQuery", {}))

    # 5. 启动HTTP Mock Server供SDK对接
    srv = start_mock_server(8899)
    print("本地Mock Server: http://127.0.0.1:8899/pdd/order/information/get")
跑出来关键输出:
淘宝沙箱调用: {'_mock': 'would_post', 'url': 'https://gw.api.tbsandbox.com/router/rest', 'params': {...签名已算...}}
亚马逊沙箱Orders: {'_mock': 'would_get', 'url': 'https://sandbox.sellingpartnerapi-na.amazon.com/orders/v0/orders', 'params': {'MarketplaceIds': 'ATVPDKIKX0DER', 'CreatedAfter': 'TEST_CASE_200'}}
拼多多本地Mock订单: {'order_sn': '123', 'order_status': '1', 'pay_amount': '9900', ...}
抖店本地Mock订单: {'data': {'order_list': [{'order_id': 'dy_mock_001', 'order_status': '1', ...}]}}

四、联调四步法(大促前必做)

  1. 平台沙箱验签名:淘宝/1688/京东/亚马逊先沙箱跑通MD5/HMAC/LWA+SigV4,签名错立刻暴露,不碰真实数据

  2. 本地Mock跑边界:用LocalMockStore造"订单状态机全分支/库存负数/地址解密失败/分页断页/429/5xx",覆盖单元测试

  3. HTTP Mock Server对接SDK:无沙箱五家把SDK的gateway_url指到127.0.0.1:8899,Webhook同理(ngrok暴露本地)

  4. 测试店铺小流量:拼多多/抖店/快手开测试店铺,正式AppKey切"测试模式"打5~50单,核对落库字段,再切生产


五、和前几篇的衔接

把本篇 UnifiedApiClient 嵌进前篇 ApiGateway
  • gateway_urlclient._endpoint() 决定,sandbox开关在配置中心热切

  • LocalMockStore 作为无沙箱平台的FallbackAdapter,CI流水线默认走Mock不调外网

  • 大促压测时禁止调任何沙箱(亚马逊沙箱5QPS、淘宝沙箱不稳、京东沙箱易超时),压测只能用生产预发+本地Mock放大
    "沙箱验签 + Mock验业务 + 测试店铺验端到端" 三段式,是九家统一的可观测联调基线。

要不要我把 unified_mock_server.py 扩成 pytest fixtures(每家平台mock数据快照)+ 沙箱/生产双跑对比断言 + OpenAPI Schema校验(响应字段自动diff生产),直接进你commerce-meshtests/目录?


群贤毕至

访客