用户退了一件,这件该退多少没数
三件商品一张 50 元券,50 除以 3 除不尽,明细加起来不是 50。
我跑了 20 万单:每单 2~5 件,49065 单分摊之和不等于优惠总额,占 24.53%。
一天一千单,每天两百多笔。不报错。退款那天一起爆。
用户在小程序上先看到一个数。申请退款页把整单实付除以件数,再乘要退的件数。
三件价格不一样,这个数一开始就不是「这一件该退多少」。
后端退的是明细上那一列实付。列对了,两边还可能对不上;
列不对,小程序展示、退款单、商户结算,三个数各说各的。
订单总价始终是对的。用户付的钱也是对的。错的是没人日常会看的那一列。
申请页除一下,测试再除一下
申请页那段可以写成这样:
let unit = pay / qty;
let show = (unit * n).toFixed(2);pay 是整单实付,qty 是整单件数。
100 块和 50 块两件,用了 30 块券,实付 120。
退那件 100 的,页面显示 60。
后端如果按明细实付退,这一件不该是 60。
两边差的不是分,是「这一件到底摊了多少券」有没有存下来。
测试也爱用整除。两件各 100,券 50,每件摊 25,assert 通过。
20 万单里对不上的 24.53%,用的是 2~5 件随机金额。
整除的用例证明不了除不尽。
一张券为什么必须摊到行上
真实系统里一张券的旋钮不少:平台还是商家发、满减还是折扣、
固定有效期还是领取后 N 天、能不能跨店。
光发行方 × 六种类别 × 满减/折扣,就是 24 种。
叠加要回答哪些能一起用、先算谁。
跨店券是那个逼你必须分摊的旋钮。
一张券摊到不同商户的商品上,结算是分开的。
订单上只记一个「优惠总额」,月底两家商户都说这 50 块是自己出的。
复杂度不在算术,在组合。算术错了,组合会把错放大到每一行。
定死三条,其余都好办
一、先算门槛,再算优惠
满 100 减 20,两张,商品 110:
- 按原价:两张都能用,减 40
- 按减完再判:第一张减完剩 90,第二张作废,只减 20
同样两张券,差 20 块。
我选按原价判断门槛,并写进规则。
用户预期通常是「这单 110,两张满 100 的都能用」。
更重要的不是选哪个,是选了就写下来,别变成隐式行为。
二、折扣和满减同时在,先算折扣
100 的商品,8 折,再满 50 减 10:
- 先折后减:80 再减 10 = 70
- 先减后折:90 再 × 0.8 = 72
差 2 块。一天一万单就是两万。
先折扣后满减。 折扣改「这件值多少钱」,满减改「这单少付多少」。
不是同一个动作,顺序不能靠手感。
三、优惠摊到明细,除不尽给最后一件
订单表记一个「优惠总额」不够。每一笔优惠都要落到商品行。
按金额占比摊。前 n-1 行向下取整,最后一行吃掉差额:
import static java.math.BigDecimal.ZERO;
import static java.math.RoundingMode.DOWN;
static void split(
BigDecimal coupon,
BigDecimal[] amt) {
BigDecimal tot = ZERO;
for (BigDecimal a : amt) {
tot = tot.add(a);
}
BigDecimal used = ZERO;
int n = amt.length;
for (int i = 0; i < n; i++) {
BigDecimal share;
if (i == n - 1) {
share = coupon.subtract(used);
} else {
share = coupon
.multiply(amt[i])
.divide(tot, 2, DOWN);
used = used.add(share);
}
amt[i] = share;
}
}别用四舍五入然后祈祷加起来正好。
不做兜底时,20 万单跑下来:
模拟订单: 200000 单
分摊对不上: 49065 单
占比: 24.53%
最大偏差: 2 分
最坏的一单: 券 269.76 分摊到 5 件,
合计 269.78,差 2 分接近四分之一的订单会对不上。 不是极端情况,是日常。
平时看不出来。出事在退款:按明细退,跟当初优惠对不上。
一天两百多笔,谁也查不动。
券、活动、会员折扣,每一种单独分摊、单独记一列。
不要合并成一个「优惠总额」——退款时要知道哪部分是平台的、哪部分是商户的。
申请页那个 pay / qty,在分摊列写上之后可以改成读这一列。
展示和结算用同一个数。 两套算法并存,客服会对着两个屏幕吵架。
退多少、券退不退,写代码之前定死
用户用 100 元券买了 200 的东西,实付 100。全额退,退多少?
退 100,券不退。 —— 用户会说券没了。
退 100,券退回账户。 —— 常见,但要处理券已过期。
退 200。 —— 你亏 100,别这么干。
没有标准答案,但必须在设计阶段定死,写进注释。
运行时再讨论,讨论的就不是「该退多少」,是「已经退错的这几百笔怎么办」。
常见问题对照表
| 现象 | 真正的原因 | 解法 |
|---|---|---|
| 小程序显示的退款额和实际到账不一致 | 申请页用整单实付÷件数 | 展示改读明细上的分摊列 |
| 部分退款金额算不出来 | 优惠没摊到明细,只记了订单总优惠 | 每笔优惠分摊到商品行,单独记列 |
| 分摊之和比优惠总额差一分 | 每行四舍五入(实测 24.53% 会中) | 前 n-1 行向下取整,最后一行兜底 |
| 单测绿、线上退一件就错 | 用例金额都能整除 | 用 3 件 × 除不尽的券额做用例 |
| 同样两张券,不同订单减的钱不一样 | 门槛按原价还是按剩余,实现不一致 | 定死一种,写进文档和注释 |
| 折扣和满减一起用,金额有争议 | 计算顺序没定义 | 先折扣后满减,并明确写出来 |
| 跨店券结算时算不清谁出的钱 | 没按发行方拆分优惠来源 | 分摊时区分平台补贴和商户补贴 |
| 券在有效期内却提示不可用 | 固定时间段和领取后 N 天漏判一种 | 两条分支都覆盖,写测试 |
| 用券订单退款后,券的状态没人管 | 退款只处理了钱 | 券的回退纳入退款处理单元 |
| 限量券超发 | 剩余数量的扣减没做并发保护 | 用带条件的 UPDATE,不要先查后改 |
这条链路上的其他几篇
