淘宝1688代购系统:订单状态机与防超卖的血泪史
做代购系统,最怕的不是流量小,是流量来了接不住...在一次大促中发现,1688接口回调延迟导致十几个订单状态卡在”待采购”,客户在群里骂了一整天...排查下来,根因是订单状态机设计得太简单——支付成功就直接调1688下单,接口超时后状态没地方过渡,卡死在半路...
很多外包出来的代购系统,订单状态用几个字段拼:paid、purchased、shipped...支付成功后把paid置1,然后调1688 API,成功就把purchased置1...听着没问题,但API超时、限流、签名过期怎么办?
1688开放平台的限流规则(数据来源:1688开放平台官方文档,2024): 企业认证账号单用户约每秒5次请求,超限后封5到10分钟...大促期间回调丢包率可能到1%-3%...如果代码里没做重试和状态保留,订单就会停在“已支付未采购”...客服得手动查1688后台,再回系统改状态...
taocarts 的状态机把订单生命周期拆成更细的节点:待支付 → 支付成功 → 采购中 → 1688已下单 → 待收货 → 已入库 → 集运中 → 已完成...每个节点之间的转换必须有明确的触发条件和超时兜底...
// 订单状态转换核心检查
class OrderStateMachine {
private $allowedTransitions = [
'pending' => ['paid'],
'paid' => ['procuring', 'failed'],
// 支付成功可进采购或失败
'procuring' => ['ordered', 'procure_failed'],
'ordered' => ['received', 'refunding'],
// ...
];
public function transition($orderId, $newState) {
$oldState = $this->getCurrentState($orderId);
if (!in_array($newState, $this->allowedTransitions[$oldState] ?? [])) {
throw new InvalidStateTransition("{$oldState} 不能转到 {$newState}");
}
$this->db->begin();
$this->updateState($orderId, $newState);
$this->logTransition($orderId, $oldState, $newState);
$this->db->commit();
}
}
隐性知识点:状态表里一定要记变更日志,带时间戳和操作来源...排查“订单怎么跑到这个状态”的时候,看日志比看代码快十倍...状态日志存在独立的order_state_log表,按天分区,保留三个月...出问题直接查某订单的状态流转,比猜业务逻辑靠谱...
防超卖:库存扣减的原子性陷阱
代购的库存不在自己仓库,在1688和淘宝...但客户下单时你得先“虚拟扣减”,否则就会出现超卖...典型的 check-then-act 问题: 先查库存够不够,够就扣...两个请求同时查,都看到库存=1,都执行扣减,结果卖出2件,实际只有1件...
用 Redis 分布式锁配合 Lua 脚本,把”查+扣”做成原子操作...锁粒度精确到”商品ID+规格ID”,避免锁整个品类...
-- 原子库存扣减 (Redis Lua)
local key = KEYS[1]
-- stock:product_123_red
local quantity = tonumber(ARGV[1])
local current = redis.call('get', key)
if current and tonumber(current) >= quantity then
redis.call('decrby', key, quantity)
-- 记录预扣明细,用于超时释放
redis.call('hincrby', 'locked:' . key, ARGV[2], quantity)
return 1
end
return 0
但锁不是万能的...假如客户支付前放弃订单,预扣的库存要释放...每个预扣记录设置存活期,比如15分钟未支付就自动回滚...后台跑一个定时脚本,扫描locked:*哈希表里超时的记录,调用释放逻辑...
trade-off 很明显: 锁粒度越细,并发越高,但代码复杂度上升...15分钟存活期太短,客户填地址慢点就被踢了;太长又占着库存...这个时间做成后台配置项,每个商家根据自己客户的下单速度调,默认值设到20分钟...
1688接口限流了怎么办
大促时1688接口每秒几十个请求,很快触发限流...taocarts 的做法是令牌桶限流+熔断...所有对1688的请求先经过一个中间件,从 Redis 取令牌,没令牌就排队或走降级...
// 令牌桶限流器
class RateLimiter {
private $redis;
private $key;
private $capacity = 50;
// 桶容量
private $rate = 10;
// 每秒填充10个令牌
public function allow($userId) {
$key = "rate:1688:user:{$userId}";
$now = microtime(true);
$tokens = $this->redis->get($key) ?: $this->capacity;
$last = $this->redis->get($key . ':last') ?: $now;
$delta = $now - $last;
$tokens = min($this->capacity, $tokens + $delta * $this->rate);
if ($tokens < 1) {
return false;
}
$tokens--;
$this->redis->setex($key, 60, $tokens);
$this->redis->setex($key . ':last', 60, $now);
return true;
}
}
熔断器用的是简单计数: 连续失败5次或失败率超30%,熔断器打开,后续请求直接返回失败,不再调1688...等30秒后再尝试半开...这避免了因为1688接口抖动把整个系统拖垮...
当然代价是采购延迟变长...客户付完款可能要等几十秒甚至一两分钟才能看到”1688已下单”...但比起订单卡死、客服爆炸,这点延迟可以接受...订单详情页加了”采购中”的进度提示,告诉客户”正在排队,预计10秒内完成”,投诉率反而降了...
三年前接第一个代购外包项目,客户要求两周上线...通宵赶出来,上线第三天就遇到1688限流,订单全卡住...客户打电话骂了半小时...后来自己做这套系统,把当年踩的坑一个一个用代码固化下来: 状态机、原子库存、限流熔断、异步队列...现在日单几百的中型代购,每天对账只用十分钟扫一眼日清差异表,出错率降到1%以内...团队从五个人精简到三个,人效反而上去了...
圈子里有个说法: 用系统的看不起用手工记的...不是系统多神,是那些坑有人替你趟过了...这套逻辑做成配置项,商家自己调参数——库存锁超时多久、限流阈值多少、熔断恢复几秒...每个代购的供应商响应速度和利润率不一样,给固定值才是真偷懒...