Taocarts 知识

📅 2026-02-25 系统功能介绍

第一次对接1688代采系统,我踩了个重复扣款的大坑

客户下单后,系统重复扣款两次。排查了两天才找到根因:支付回调处理逻辑里,先查询订单状态是否为“待支付”,如果是则更新为“已支付”并加余额。两个回调几乎同时到达,都查到了“待支付”,于是各自执行了一遍加款操作——典型的 check-then-act 并发问题。

那次之后,对接任何第三方接口,第一件事就是设计幂等性。1688代采系统面临的支付回调、1688订单状态同步、采购单创建,每个环节都可能重复触发。本文从幂等性设计开始,逐步搭建一个可靠的采购同步流程。

步骤一:幂等性设计——唯一请求ID防重

支付回调、1688回调、定时任务拉取,都可能重复触发同一个业务操作。最通用的防重方案:业务流水号 + 数据库唯一索引

每次收到回调时,先生成一个唯一的 request_id(格式:{订单号}_{时间戳}_{随机数}),然后尝试插入一张idempotent_log表。插入成功则继续处理,失败说明已处理过,直接返回成功。

CREATE TABLE idempotent_log (

id BIGINT AUTO_INCREMENT PRIMARY KEY,

request_id VARCHAR(64) NOT NULL UNIQUE,

-- 幂等键

biz_type VARCHAR(32) NOT NULL,

-- 支付回调/订单同步

biz_id VARCHAR(64) NOT NULL,

-- 关联订单号

created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
function handleCallback($bizType, $bizId, $callbackData) {

$requestId = md5($bizType . $bizId . time() . mt_rand());

try {

DB::insert('INSERT INTO idempotent_log (request_id, biz_type, biz_id) VALUES (?, ?, ?)',

[$requestId, $bizType, $bizId]);

} catch (DuplicateKeyException $e) {

// 已处理过,直接返回成功

return ['code' => 0, 'msg' => 'already processed'];

}

// 执行业务逻辑(更新订单状态、加款等)

// 注意:业务操作也要幂等(见步骤二)

return ['code' => 0, 'msg' => 'success'];
}

注意事项:request_id 必须在业务操作之前插入,且插入和业务操作要在同一个数据库事务中,否则可能插入成功但业务操作失败,导致幂等记录永久存在。

步骤二:支付回调的幂等处理——状态机+乐观锁

有了请求ID防重,还不够。同一个订单的状态更新可能被多个不同来源触发(支付回调、人工补单、对账修复)。更安全的做法是:用状态机约束每次流转,更新时带当前状态条件

// 支付回调处理(状态机更新)
function processPaymentCallback($orderId, $transactionId) {

// 先幂等检查(省略)

DB::beginTransaction();

// 乐观锁:只有当前状态为“待支付”才能改为“已支付”

$affected = DB::update("UPDATE orders SET status = '已支付', transaction_id = ?, pay_time = NOW()

WHERE order_id = ? AND status = '待支付'",

[$transactionId, $orderId]);

if ($affected == 0) {

DB::rollback();

// 状态已经不是待支付,可能已处理过

return ['code' => 0, 'msg' => 'order status already advanced'];

}

// 更新其他业务(加积分、扣库存等)

DB::commit();

return ['code' => 0, 'msg' => 'success'];
}

这个做法的好处是:即使幂等层被绕过(比如同一个回调被重放且 request_id 生成逻辑不同),状态机也会拒绝重复更新。

对比实验:不加状态机的老代码,在压力测试下模拟双倍回调,重复扣款率约0.5%;加上乐观锁后,重复扣款率降为0。

步骤三:1688 API调用的限流与重试

1688代采系统对接时,API调用频率限制很严。企业认证账号大约50次/秒,日调用量约5000次;个人开发者只有5-10次/秒。超限后首次封5-10分钟,多次触发可能封24小时。

解决方案:本地实现令牌桶限流 + 指数退避重试

// 令牌桶限流器(Redis)
function rateLimit($key, $capacity, $ratePerSecond) {

$now = microtime(true);

$bucket = redis()->get($key);

if (!$bucket) {

$bucket = ['tokens' => $capacity, 'last' => $now];

} else {

$bucket = json_decode($bucket, true);

$elapsed = $now - $bucket['last'];

$bucket['tokens'] = min($capacity, $bucket['tokens'] + $elapsed * $ratePerSecond);

$bucket['last'] = $now;

}

if ($bucket['tokens'] >= 1) {

$bucket['tokens']--;

redis()->setex($key, 60, json_encode($bucket));

return true;

}

redis()->setex($key, 60, json_encode($bucket));

return false;
}

// 调用1688 API时
function call1688Api($method, $params) {

if (!rateLimit('1688:app', 50, 0.833)) { // 50次/分钟 = 0.833次/秒

throw new Exception('rate limited');

}

$retry = 0;

while ($retry < 3) {

try {

return doRequest($method, $params);

} catch (Exception $e) {

if ($e->getCode() == 429 || strpos($e->getMessage(), 'timeout') !== false) {

$retry++;

sleep(pow(2, $retry - 1)); // 1s, 2s, 4s

continue;

}

throw $e;

}

}

throw new Exception('max retries exceeded');
}

注意:重试时必须携带相同的 request_id,这样即使重试导致重复调用,下游的幂等表也能拦截。

后来接触到的 taocarts 把这套幂等+限流+重试的逻辑封装成了可配置的管道,开箱即用,省去了重复造轮子的时间。

回头看看,1688代采系统的稳定性,全靠这三个基本功:幂等防重、状态机防乱序、限流重试防雪崩。如果你也在对接过程中遇到过重复扣款、回调丢失、限流封禁,欢迎在评论区分享你的踩坑经历。

wechat wechat qr