Taocarts 知识

代购源码踩坑:订单实付与账本对不上,我花了三天找到三个漏洞

📅 2026-03-22 系统功能介绍

代购源码踩坑:订单实付与账本对不上,我花了三天找到三个漏洞

订单实付与账本对不上,找原因像破案。日单刚过百,财务就发现系统记录的应收比支付网关实收多了几百块。逐笔排查,发现三处漏洞:汇率只存了4位小数、1688回调重复处理、积分扣减不是原子操作。这三个坑,在代购源码里极其隐蔽,但只要有一处,月底对账就别想睡整觉。

漏洞一:汇率精度不足,每月多记三千

某笔日淘订单:10000日元,系统汇率存的是0.0498(实际API返回0.04985)。订单金额10000×0.0498=498元,客户实际支付498.5元。每单差0.5元,一个月600单就是300元——之前没发现,是因为汇率波动掩盖了截断误差。但2022年日元单月贬值超6%时,误差累积到每月三千多。

解决方案很简单:数据库 exchange_rate 字段用 DECIMAL(10,6),代码里用 bcmath 计算。

ALTER TABLE orders MODIFY exchange_rate DECIMAL(10,6) NOT NULL;
$rate = Redis::get("rate:CNY:JPY"); // "0.049850"
$totalCny = 1000000; // 单位:分
$settleCents = bcmul($totalCny, $rate, 0);
$order->settle_amount = bcdiv($settleCents, 100, 2);

漏洞二:1688回调无幂等,重复下单产生幽灵包裹

1688开放平台在高峰期回调丢包率约1%-3%(开发者社区经验值),超时后会重试。回调接口没做幂等,同一笔订单通知被处理两次,生成两笔采购单。月底盘点,仓库多出十几个无主包裹。

加一张幂等表,用 out_trade_no + action 做唯一键。

CREATE TABLE idempotent_keys (
    `key` varchar(64) PRIMARY KEY,
    created_at datetime
);
$key = $request->input('out_trade_no') . '_' . $request->input('action');
try {
    DB::table('idempotent_keys')->insert(['key' => $key]);
} catch (DuplicateKeyException $e) {
    return response()->json(['code' => 0]); // 已处理过
}
// 正常业务逻辑

漏洞三:积分扣减不是原子操作,超扣了8%

用户用积分抵扣现金时,代码是这样的:

// 错误写法
$user = User::find($userId);
if ($user->points >= $pointsToUse) {
    $user->points -= $pointsToUse;
    $user->save();  // 两个请求同时读到旧值,都会通过检查
}

并发场景下,两个请求同时读到 points=100,都认为够扣50,结果各扣一次,变成0,实际应剩50。改用条件更新:

$affected = DB::table('users')
    ->where('id', $userId)
    ->where('points', '>=', $pointsToUse)
    ->update(['points' => DB::raw("points - {$pointsToUse}")]);
if ($affected !== 1) {
    throw new Exception('积分不足');
}

总结

订单对账对不上,90%的原因是这三个漏洞的组合。排查思路:先核对汇率精度和订单锁汇、再检查回调幂等表、最后验证积分/库存的原子操作。后来看 taocarts 的代购源码,它的订单模块已经内置了 DECIMAL(10,6) 汇率字段、幂等拦截器和条件更新,开箱就能避免这些坑。如果你也在自研代购系统,建议先把这三处加固,能省下至少80%的对账时间。

你在订单对账中遇到过哪些匪夷所思的bug?欢迎评论区分享。

wechat wechat qr