Node.js 支付差一分:我把订单金额改成不可变快照和双重校验
一次对账发现支付金额少一分,背后是优惠券叠加、浮点精度和下单锁价的问题。我把订单金额改成不可变快照,并在支付前做双重校验。

作者
ZHOUYI
上周对账时,我发现一笔微信小程序订单实付 39.99 元,订单表却是 40.00 元。差一分看似小,但财务没法平账,用户也可能看到不一致。最初我以为是支付回调丢字段,最后查下来,问题出在下单链路:前端传了金额,服务端又临时重算,优惠券叠加顺序一变,结果就漂了。
把金额从“临时算”改成“下单快照”
以前提交订单时,服务端会按当前请求再算一次总价,但优惠券、活动价、运费都可能变化。后来我改成下单成功创建时生成 priceSnapshot:原价、优惠明细、最终金额、币种、计算版本都写进 MongoDB。后续支付、回调、退款、客服查询都只认这个快照,不再现场重算。
优惠券叠加:统一用分,避免浮点
- 所有金额字段存整数分,展示时再转元,避免 0.1 + 0.2 的浮点误差。
- 优惠券叠加顺序固定:先商品折扣,再店铺券,最后平台券,防止顺序不同导致金额不同。
- 多商品分摊优惠时,先按比例取整,剩余分摊差额放到最后一个商品,保证总分一致。
- 券核销用 Redis 原子扣减,防止同一张券并发抵扣造成金额异常。
const toCents = (yuan) => Math.round(Number(yuan) * 100);
function applyCoupon(totalCents, couponCents) {
const finalCents = Math.max(totalCents - couponCents, 0);
return { totalCents, couponCents, finalCents };
}代码里我刻意不用浮点数做减法。哪怕前端展示用了元,后端也只接收展示用途,真正落库和支付都用分。对于优惠券满减、折扣率,也先转成整数规则再计算;如果遇到小数分摊,宁可把尾差放到固定商品,也不要让每个商品各自四舍五入。
支付前双重校验,锁住价格
提交支付前,我会做一次服务端重算,但不直接信任前端金额:第一重校验是重算最终分,第二重校验是对比订单快照与支付参数。如果价格、券状态、库存不一致,直接返回失效。同时用 Redis 写入锁价 key,五分钟内同一订单不允许改价。支付回调只校验订单号和快照金额,不再重新计算。
“支付金额这类问题,最怕“看起来能复现,但链路说不清”。把金额变成不可变快照,再加上支付前校验,排查时就能把责任边界切开。”