背景痛点:淘宝客服机器人到底难在哪?
第一次把扣子智能客服接到淘宝,我以为只是“拿到 AppKey → 调个接口 → 回条消息”这么简单。结果踩坑踩到怀疑人生。总结下来,拦路虎就三只:
OAuth2.0 鉴权链路长
淘宝开放平台(TOP)用 OAuth2.0 授权,scope 必须带item_*,trade_*,user_*,否则后续拿不到订单数据。最坑的是 refresh_token 只有 7 天有效期,忘记刷新就直接 401。消息加密 & 签名验证
淘宝把请求头里的X-TOP-Sign拆成 MD5+RSA 双签名,任何字段顺序写错就报“sign check fail”。官方文档在「TOP 技术文档 / 签名机制」章节,但示例是 Java,Python 党只能自己啃。订单状态同步
客服里一句“帮我查订单”就要实时拿到物流、退款、维权状态。TOP 的trade.fullinfo.get接口限流 100 次/60s,高峰期直接被打爆,用户页面一直转菊花。
技术方案:Webhook vs 长连接怎么选?
先给结论:淘宝只认 HTTP 回调(Webhook),长连接 SDK(MQ 模式)2022 年就下线了,所以不用纠结。但扣子内部为了“及时”与“稳定”兼顾,可以自己做一层 Webhook → 消息队列 → 消费进程的解耦。
适用场景对比:
Webhook(官方模式)
优点:接入简单,淘宝负责重试。
缺点:公网域名必须备案 + HTTPS,高峰时 20 次/秒重试能把 4 核 8 G 机器打满。自建长连接(非官方,可选)
用 RabbitMQ/Kafka 做缓冲,本地消费者按店铺维度分队列,再回包到淘宝。
优点:削峰填谷,可灰度。
缺点:多一次组件运维,消息顺序要自己保证。
下文默认“官方 Webhook + 本地队列”混合模式,既能过审,又能抗量。
核心实现:Python 3.8+ 代码实战
1. 签名生成(带幂等键)
淘宝要求把所有业务参数按 key 升序拼成k1v1k2v2...的字符串,再拼上secret,先 MD5 再 RSA。以下函数直接返回X-TOP-Sign头,同时把幂等键idempotency_key也带进去,防止重复发货通知。
import hashlib, hmac, time, uuid from typing import Dict from urllib.parse import quote_plus def build_top_sign(params: Dict[str, str], secret: str) -> str: """生成淘宝 TOP 签名,返回 X-TOP-Sign 头值""" # 1. 升序排序 sorted_str = ''.join(f'{k}{params[k]}' for k in sorted(params)) # 2. 拼 secret raw = sorted_str + secret # 3. MD5 md5 = hashlib.md5(raw.encode()).hexdigest() # 4. RSA 私钥签名(示例用伪代码,生产请用 rsa 库) # sign = rsa.sign(md5.encode(), private_key, 'SHA-1') # 这里返回 hex 方便演示 return md5.upper()幂等键生成:
def make_idempotent_key(user_id: str, item_id: str) -> str: return f'{user_id}_{item_id}_{int(time.time())}_{uuid.uuid4().hex[:8]}'2. SDK 初始化(带类型注解)
from typing import Optional import httpx class TopClient: def __init__(self, appkey: str, secret: str, sandbox: bool = True): self.appkey = appkey self.secret = secret self.base = 'https://gw.api.tbsandbox.com' if sandbox else 'https://eco.taobao.com' self.client = httpx.Client(timeout=10) def request(self, method: str, params: Dict) -> Dict: params.setdefault('app_key', self.appkey) params.setdefault('timestamp', int(time.time())) params.setdefault('v', '2.0') params.setdefault('sign_method', 'md5') params['sign'] = build_top_sign(params, self.secret) r = self.client.post(self.base + '/router/rest', data=params) r.raise_for_status() return r.json()3. 消息处理状态机(文字流程图)
淘宝 Webhook 每推送一条消息,本地维护一个有限状态机:
[Receive] → 验签 → 解析消息类型 ↓ 订单查询 ──→ 缓存命中 → 直接回复 ↓ 未命中 调用 TOP 接口 → 组装答案 → 回包淘宝 ↓ 记录幂等键 → 结束状态说明:
- Receive:Nginx 层限流 1k QPS,失败直接 503,淘宝会重试。
- 验签:失败直接 400,不消耗业务资源。
- 缓存命中:用 Redis 存
user_id+item_id→ 答案,TTL 300 s,减少 60% 接口调用。 - 回包:淘宝要求 1500 ms 内返回,否则算超时;本地用 asyncio + 连接池保证。
生产考量:压测与会话保持
压测指标
目标:日常 500 QPS,大促峰值 2 k QPS,P99 延迟 < 800 ms。
工具:locust + 自建淘宝模拟网关(把 Webhook 请求格式录下来回放)。
结果调优过程:
- 单进程同步模型只能到 350 QPS,CPU 占满。
- 换成
uvicorn+FastAPI+async httpx,4 进程 8 线程,压到 1 k QPS,CPU 70%。 - 再加一层 Redis 缓存,500 QPS 时平均延迟从 220 ms 降到 90 ms。
- 最终 2 k QPS 时,MySQL 连接池打到 80%,线程池满;把订单查询改走只读实例,峰值平稳。
分布式会话保持
淘宝 同一买家连续问 3 句,必须保证落到同一逻辑节点,否则上下文丢失。方案:
- 网关层按
user_id做一致性哈希,后端无状态。 - 若节点挂掉,一致性哈希环漂移 < 1/32,影响可接受。
- 上下文存 Redis Hash,key=
session:{user_id},TTL 15 min,客服端定期续期。
避坑指南:频控 & 合规
1. 规避淘宝 API 频控
- 提前读「TOP 技术文档 / 流量限制」章节,记录所有接口的 60 s 窗口次数。
- 本地令牌桶限速:把 100 次/60 s 换算成 1.67 次/s,桶容量 5,突发也能抗。
- 对“查订单”做批量聚合:同一秒内有 10 人查同一订单,只调 1 次接口,结果缓存 5 s。
2. 敏感数据脱敏
- 手机号、收货地址只存后 4 位,前面打
*。 - 日志打印用
__str__重写,默认脱敏;打开 debug 才输出完整 JSON,且需配置白名单 IP。 - 数据库字段用 AES-256-GCM 加密,key 放 KMS,定期轮转。
- 隐私字段不回流淘宝,只本地渲染,减少合规风险。
延伸思考题
- 如果公司同时运营 100 个店铺,如何做到 AppKey、证书、账单完全隔离,又能共享同一套客服代码?
- 当淘宝把“延迟回答”从 1500 ms 降到 500 ms 时,你的缓存与异步流程要怎么改?
- 假设用户在微信小程序里下单,却跑到淘宝聊天里咨询,如何把两边的会话 ID 映射到同一上下文?
把这三个问题想透,基本就能从“能跑”进化到“好跑、稳跑、快跑”。祝各位少踩坑,早上线。