← 开放平台 · Developers

购物车一键导入 · 通用对接规范

版本 v1 · 2026-09-24 · 发起方:QCradar(qcradar.com)

一句话:用户在 QCradar(或任何实现了本规范的工具)把多件商品加入购物车,点一下「导入到 {你的平台}」,浏览器落到你们的购物车时商品已全部加好、规格已预选;用户在你们站内自己登录、自己结算。

你们只需要实现一个带签名的接口:建立导入会话,返回一条跳转链接。本规范已在生产环境与多家代购平台跑通,QCradar 侧客户端代码现成,你们按本文实现完成后,1~2 个工作日即可联调上线。

已有自己的购物车导入 / 开放接口?直接把文档发给我们(点击联系),我们按你们的契约适配。本文是我们建议的最省事方案。

为什么要做这件事、QCradar 怎样推荐代购平台,见致各代购平台的公开信。


0. 闭环模型(先看这个)

用户在 QCradar 购物车勾选商品,点「导入 {平台}」
   │
   ▼
QCradar 服务端
   │  1) 组装 items(货源平台 + 商品 ID + 规格 + 数量)
   │  2) 用 secretKey 做 HMAC-SHA256 签名
   │  3) POST {你们的网关}/open/cart/import-session/create
   ▼
你们的网关
   │  校验签名 → 把这批商品存成一个「导入会话」→ 生成不可猜的 cartToken
   │  返回 { code:200, data:{ sessionId, redirectUrl, expiresAt, itemCount } }
   ▼
QCradar 前端 window.open(redirectUrl)
   ▼
用户浏览器落在 https://{你们的域名}/cart?cartToken=xxxx
   │  你们用 cartToken 反查这批商品 → 合并进【当前登录用户】的购物车
   │  (未登录:先进游客购物车并提示登录,登录后并入账号)
   ▼
用户在你们站内自己结算支付 → 订单按 promotionCode 归属

三个要点:

要点说明
谁的车由你们自己的登录态决定。调用方传的 externalUserId 只作归属/分析,不做认证。
哪些货由 URL 里的 cartToken 决定。token 必须不可猜(≥128 bit 随机),绝不能是明文 ?userId=。
归属请求体带 promotionCode(你们分配给调用方的推广码/邀请码),写到这批购物车记录上,后续下单按它计佣。

1. 全局约定

项值
测试 / 生产 Base URL由你们分配
编码 / 请求体全程 UTF-8,application/json
响应必须是 JSON,Content-Type: application/json(调用方收到非 JSON 会判定为被拦截)
鉴权HMAC-SHA256 签名,4 个 X-* 请求头(第 3 节)
凭据交付线下交付即可:每个环境一套 clientId + secretKey(不需要做自助注册接口)
时间戳窗口与服务器时间偏差 ≤ 300 秒
Nonce 防重放同一 clientId 下 nonce 10 分钟内不可复用

测试与生产各一套独立凭据,调用方切换环境只换配置,不改代码。


2. 接口总览

只有一个必需接口:

接口方法签名说明
/open/cart/import-session/createPOST是建立导入会话,返回 redirectUrl

路径可以按你们的规范改,但请固定一个、大小写敏感、与签名行 2 完全一致,不要在网关层做大小写归一或重定向(签名里包含路径)。


3. 签名算法(核心,请逐字实现)

签名通过 4 个请求头传递:

请求头内容
X-API-KeyclientId
X-TimestampUnix 秒级时间戳(字符串)
X-Nonce每次请求唯一的随机串(UUID 去连字符,32 位 hex)
X-SignatureHMAC-SHA256 结果,小写 hex

3.1 拼待签名串(Canonical String)

6 个部分用 \n 连接,顺序固定:

行1: HTTP 方法            大写,固定 POST
行2: 请求路径              不含域名、不含 query,大小写敏感,与真实 URL 完全一致
行3: 排序后的 query 串      本接口无 query → 空字符串(但换行符仍在)
行4: 请求体 SHA256         对收到的【原始请求体字节】做 SHA256 → 小写 hex
行5: X-Timestamp           与请求头同值
行6: X-Nonce               与请求头同值

示例(注意第 3 行是空行):

POST
/open/cart/import-session/create

b43bd828c850c096d1872ed79b8e0d2ac087a27d767b2ba7b6bca2de2d2fb303
1760000000
0123456789abcdef0123456789abcdef

3.2 计算签名

X-Signature = HMAC_SHA256( key = secretKey, message = CanonicalString ) → 小写 hex

3.3 服务端校验流程

  1. 取 X-API-Key 查出 secretKey;查不到 → 401 Invalid app credentials。
  2. |now − X-Timestamp| > 300s → 403 Timestamp expired。
  3. X-Nonce 在 10 分钟内出现过 → 403 Duplicate request;否则记下。
  4. 对原始请求体字节(不要先 JSON 反序列化再重新序列化)做 SHA256,按 3.1 拼串,按 3.2 算签名,与 X-Signature 做常量时间比较;不一致 → 401 Invalid signature。

⚠️ 最常见的失败原因就是第 4 步:框架先把 body 解析成对象、再序列化一遍去算哈希,字段顺序/空格一变,哈希就不一样。请在过滤器/中间件层拿到原始字节再校验。

3.4 测试向量(可离线核对你们的实现)

用下面这组固定输入,你们的校验代码应当算出同样的签名:

项值
secretKeydemo_secret_0123456789abcdef0123
clientId(X-API-Key)ak_demo0123456789
X-Timestamp1760000000
X-Nonce0123456789abcdef0123456789abcdef
路径/open/cart/import-session/create

请求体(一行、无空格、无换行,共 600 字节):

{"idempotencyKey":"qcr_cart_user_12345_9f86d081884c_1760000000","externalUserId":"user_12345","returnUrl":"https://qcradar.com/cart","promotionCode":"DEMO_PROMO_CODE","items":[{"platform":"weidian","itemId":"7412345678","proUrl":"https://weidian.com/item.html?itemID=7412345678","proName":"Demo Hoodie Black","proPrice":128,"quantity":2,"selectedSku":{"skuId":"4501234567","skuText":"Black / L","imgUrl":"https://cdn.example.com/sku.jpg"}},{"platform":"taobao","itemId":"650123456789","proUrl":"https://item.taobao.com/item.htm?id=650123456789","proName":"Demo Sneaker","proPrice":299,"quantity":1}]}

期望结果:

项值
请求体 SHA256b43bd828c850c096d1872ed79b8e0d2ac087a27d767b2ba7b6bca2de2d2fb303
空请求体 SHA256(备查)e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
X-Signature61fc4fc001802b5257a7f80e7f4ef5fb28f9a77a6c6053e36e0aab0fa55d13b8

3.5 参考实现 · 服务端校验(Node.js)

const crypto = require('node:crypto');
const sha256Hex = (buf) => crypto.createHash('sha256').update(buf).digest('hex');

// rawBody: Buffer,必须是收到的原始字节
function verify({ method, path, rawBody, headers, secretKey }) {
  const canonical = [
    method.toUpperCase(),
    path,
    '',                       // 无 query
    sha256Hex(rawBody),
    headers['x-timestamp'],
    headers['x-nonce'],
  ].join('\n');
  const expected = crypto.createHmac('sha256', secretKey).update(canonical, 'utf8').digest('hex');
  const given = String(headers['x-signature'] ?? '').toLowerCase();
  return expected.length === given.length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(given));
}

3.6 参考实现 · 服务端校验(Java)

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.nio.charset.StandardCharsets;

static String hex(byte[] b) { StringBuilder s = new StringBuilder(); for (byte x : b) s.append(String.format("%02x", x)); return s.toString(); }

static String sign(String secretKey, String path, byte[] rawBody, String ts, String nonce) throws Exception {
    String bodyHash = hex(MessageDigest.getInstance("SHA-256").digest(rawBody));
    String canonical = String.join("\n", "POST", path, "", bodyHash, ts, nonce);
    Mac mac = Mac.getInstance("HmacSHA256");
    mac.init(new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
    return hex(mac.doFinal(canonical.getBytes(StandardCharsets.UTF_8)));   // 与 X-Signature 比较(小写)
}

3.7 调用方(QCradar)的行为(便于你们对照)

  • 请求头:Content-Type: application/json、Accept: application/json、Origin: https://qcradar.com、浏览器样式的 User-Agent,加上 4 个签名头。
  • 请求体只序列化一次,签名和发送用同一个字符串。
  • 单次超时 12 秒,最多重试 4 次,总预算 20 秒;重试时 idempotencyKey 和请求体不变,X-Timestamp / X-Nonce 换新并重新签名。
  • 同时在途请求 ≤ 8 个;超过则调用方自行快速失败,不会压到你们。
  • 出口 IP 固定,需要的话可以提供给你们加白名单(见 §8)。

4. 接口:建立导入会话

POST {BASE}/open/cart/import-session/create
Content-Type: application/json
Origin: https://qcradar.com
X-API-Key / X-Timestamp / X-Nonce / X-Signature

4.1 请求体

{
  "idempotencyKey": "qcr_cart_user_12345_9f86d081884c_1760000000",
  "externalUserId": "user_12345",
  "returnUrl": "https://qcradar.com/cart",
  "promotionCode": "你们分配给调用方的推广码",
  "clientType": "H5",
  "items": [
    {
      "platform": "weidian",
      "itemId": "7412345678",
      "proUrl": "https://weidian.com/item.html?itemID=7412345678",
      "proName": "示例卫衣 黑色",
      "proPrice": 128.00,
      "quantity": 2,
      "selectedSku": { "skuId": "4501234567", "skuText": "黑色 / L", "imgUrl": "https://.../sku.jpg" }
    },
    {
      "platform": "taobao",
      "itemId": "650123456789",
      "proUrl": "https://item.taobao.com/item.htm?id=650123456789",
      "proName": "示例运动鞋",
      "proPrice": 299.00,
      "quantity": 1
    }
  ]
}

顶层字段:

字段类型必填说明
idempotencyKeystring是幂等键,≤128 字符。同 key + 同内容在有效期内重复请求 → 返回同一个会话(同一 redirectUrl);同 key 不同内容 → 409。用于调用方重试时不重复建会话。
externalUserIdstring否调用方站内的用户标识(登录用户 user_<id>;游客不传)。只作映射/分析,不做认证。
returnUrlstring否用户结算后可返回的调用方地址。可在购物车/订单完成页放一个「返回」入口;不做也不影响闭环。
promotionCodestring否(要计佣必填)你们分配给调用方的推广码/邀请码。整批商品生效,写入每条购物车记录。
clientTypestring否PC / H5。QCradar 目前恒传 H5,可忽略。
itemsarray是商品明细,1~50 项。

items[] 字段:

字段类型必填说明
platformstring是货源平台:weidian / taobao / 1688(小写字符串)。微店占 QCradar 货源的大头,请务必支持。
itemIdstring是该平台商品 ID(纯数字字符串)。
proUrlstring是源站商品链接,按平台固定模板拼(见 4.2)。
proNamestring是商品名(中文优先),仅作展示。
proPricenumber是人民币元,参考价。请以你们抓取到的实时价格为准,不要把它当成结算价。
quantityint是数量,1~99。
selectedSkuobject否预选规格 { skuId, skuText, imgUrl }。skuId 是该平台的真实 SKU ID(微店 skuId / 淘宝、1688 的 sku_id),skuText 为展示文本,imgUrl 可能为空串。不传或空对象 → 商品整款加入,用户落地后自己选规格。

✅ 最小可跑通字段集:platform / itemId / proUrl / proName / proPrice / quantity。 其它顶层字段和 selectedSku 都可以先不处理。请忽略未知字段,不要因为多了字段而报错,方便双方后续各自演进。

4.2 proUrl 模板

platformproUrl
weidianhttps://weidian.com/item.html?itemID={itemId}
taobaohttps://item.taobao.com/item.htm?id={itemId}
1688https://detail.1688.com/offer/{itemId}.html

4.3 关于规格(SKU):请按这个语义实现

  • 调用方能给出真实 skuId 的比例:微店款几乎全部;淘宝/1688 款约 90%。剩下的一定会有不带 selectedSku 的多规格商品。
  • 因此请采用「没传规格就整款加入、用户落地后再选」的语义,不要做成「多规格商品未传 skuId 就整批失败」。那会让一部分购物车永远导不进去,而且失败发生在用户跳转之后,调用方收不到任何信号。
  • 1688 的规格有两套标识(数字 sku_id 与 32 位 spec_id),调用方传的是数字 sku_id,请在你们侧兼容。
  • 传了 skuId 但在你们侧解析不到(例如上游已改规格):按整款加入并提示用户选规格,同样不要整批失败。

4.4 成功响应

{
  "code": 200,
  "msg": "success",
  "data": {
    "sessionId": "CIS_20260924_xxxxxxxx",
    "redirectUrl": "https://{你们的域名}/cart?cartToken=8f3a2b1c9d0e4f5a6b7c8d9e0f1a2b3c",
    "expiresAt": "2026-09-24T12:30:00Z",
    "itemCount": 2
  }
}
字段说明
code业务码,成功固定 200
data.sessionId会话 ID,调用方留存做对账/排错
data.redirectUrl核心。调用方前端直接跳转;调用方会校验它必须是 https 且域名属于你们(防开放重定向),所以请把测试与生产会用到的购物车域名都告诉我们
data.expiresAt会话过期时间,ISO 8601(带时区)
data.itemCount实际纳入会话的商品数

4.5 失败响应

顶层 code != 200,HTTP 状态码可与业务码一致(推荐),也可保持 200;但响应体必须是 JSON:

{ "code": 401, "msg": "Invalid signature", "data": null }
codemsg(示例)含义调用方处理
400Invalid request / Too many items / Invalid quantity参数错误报错给用户,不重试
401Invalid app credentials / Invalid signature凭据或签名错报错,不重试
403Timestamp expired / Duplicate request时间戳超窗 / nonce 重放换新 nonce 重试
409Idempotency key conflict同幂等键不同内容报错,不重试
429Too many requests限流退避后重试
110Item fetch failed, please retry商品抓取瞬时失败(可重试)换新 nonce 立即重试
500Internal server error内部错误保持幂等键重试

code 110 请专门留给「重试大概率就能成功」的瞬时失败;其它业务失败别用它,否则调用方会白重试。


5. 用户落地行为(你们侧,建议)

场景建议行为
用户已登录把会话里的商品合并进该用户购物车(同商品同规格叠加数量),然后展示购物车。
用户未登录先进游客购物车并提示登录/注册,登录后游客车并入账号;或者先登录、登录后自动认领。二选一即可,但不要要求用户在调用方站点做任何授权:游客也能用,转化最好。
同一 cartToken 被打开多次(刷新、回退)按 sessionId 去重,不重复加购。
会话过期后打开提示「链接已过期,请返回重新导入」。建议有效期 30~60 分钟。
某一项商品解析失败跳过该项、导入其余商品,并在购物车页提示哪几项没成功。不要整批失败。
购物车容量不足能加多少加多少并提示,或整批拒绝并明确提示;二者任选,但请提示用户。
佣金归属promotionCode 写入每条购物车记录;用户后续从这些记录下单,订单归属调用方。用户自己删掉再手动加的商品不归属,这是正常的。

6. 幂等、限流、容量

  • 幂等:clientId + idempotencyKey 为键。同键同内容 → 返回原会话(不延长过期时间);同键不同内容 → 409。已过期会话按你们方便:返回原会话或新建都可以,调用方每次用户点击都会生成新键。
  • 限流(建议值):同一 clientId 60 次/分钟;同一 IP 30 次/分钟。超限返回 429 JSON 即可。
  • 容量:单次 items ≤ 50 项。

7. 安全要求

  • secretKey 只在双方服务端保存,不进代码仓库、不进日志、不下发浏览器。
  • 签名一律服务端校验;cartToken 用密码学随机数生成(≥128 bit),不可枚举、不可从用户 ID 推导。
  • redirectUrl 是临时敏感凭证:不要写进可公开的日志,不要长期有效。
  • 服务器校时(NTP),否则时间戳窗口会误拒。
  • 不要用 externalUserId 做任何鉴权或账号绑定,它是调用方站内的标识,可被伪造。

8. 网关 / 反爬层请注意

这几条是在真实对接中踩过的坑,提前说:

  1. 开放接口路径请不要挂 Cloudflare 托管挑战 / JS 挑战。服务端到服务端的调用过不了浏览器挑战,表现为 403 + 一段 HTML,而不是 JSON。如果网关前必须有 WAF,请按 X-API-Key 请求头或调用方出口 IP 放行这条路径。
  2. WAF 规则如果是按路径放行,请确认放行的路径与真实路径大小写完全一致。
  3. 调用方请求会带浏览器样式 User-Agent 和 Origin,便于你们做白名单。
  4. 返回任何情况都请给 JSON(含 4xx/5xx/429),不要返回 HTML 错误页。

9. 对接时需要你们提供的信息

#项说明
1测试环境 Base URL + clientId / secretKey线下安全渠道交付
2生产环境 Base URL + clientId / secretKey联调通过后
3推广码 promotionCode每个环境一个;确认测试环境是否认生产码
4redirectUrl 会落到的域名测试与生产各自的购物车域名(调用方要加进跳转白名单)
5支持的货源平台确认 weidian / taobao / 1688 三个都支持
6会话有效期分钟数
7是否需要出口 IP 白名单需要则调用方提供 IP
8对账方式后台能按推广码查到订单/佣金即可;有查询接口更好,没有也不阻塞上线
9联调对接人技术 + 商务各一位

10. 联调步骤(建议顺序)

  1. 按第 3 节实现签名校验,先用 3.4 的测试向量离线核对,通过后交付测试凭据。
  2. 调用方指向测试环境,发一个单商品、只含必填字段的请求 → 期望 code 200 + redirectUrl。
    • 签名错 → 双方对照 canonical 逐行比;最常见是第 4 行(原始字节 vs 重新序列化)。
  3. 浏览器打开 redirectUrl:未登录 / 已登录两种状态各验一次,确认商品与数量落进购物车。
  4. 扩到多商品 + selectedSku 预选规格 + promotionCode;验证同一链接刷新不重复加购;验证一项解析失败时其余照常导入。
  5. 下一笔测试订单,确认后台归属到调用方的推广码。
  6. 切生产凭据,把第 2~5 步各复测一遍。
  7. 调用方上线导入入口。

11. 联系方式

技术与商务对接统一走站内反馈系统:点击联系我们,打开后已预选「联系 → 合作」,写明平台名称和对接人即可。提交需要先登录 QCradar 账号,之后的往来都在同一个会话线程里。


本文所有域名、凭据、推广码均为示例。希望改路径、字段名或签名头名称的,提前告知即可,调用方适配成本很低。