代购源码踩坑:订单实付与账本对不上,我花了三天找到三个漏洞
代购源码踩坑:订单实付与账本对不上,我花了三天找到三个漏洞
订单实付与账本对不上,找原因像破案。日单刚过百,财务就发现系统记录的应收比支付网关实收多了几百块。逐笔排查,发现三处漏洞:汇率只存了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?欢迎评论区分享。