130 万回款,绩效只奖励 800。
这谁看了不气?评论区还有更狠的:2500 万回款,最后人直接被开了,奖金一分没拿到。

钱该不该多发,不能只看“回款金额”一个数,得结合岗位、利润、提成规则和劳动合同。可如果公司事前把激励讲得很漂亮,等钱回来再换一套算法,员工不爽再正常不过。
做开发这些年,我对这种“月底突然换口径”的东西也特别敏感。业务系统里最容易扯皮的,恰好也是钱:销售说回了 130 万,财务说只有 118 万,绩效系统最后算出 112 万。
类似场景我真处理过。先别管公式好不好看,第一步一定是把“回款到底怎么算”钉死。
先查为什么130万进系统只剩112万
假设绩效系统收到一条销售汇总:
sales_id = 7318
month = 2026-07
contract_amount = 1,560,000
received_amount = 1,300,000
财务库里却跑出来:
绩效有效回款:1,126,400
第一反应不要改绩效公式。
先拆链路:
银行流水
↓
payment_record
↓
合同认领
↓
退款/冲销
↓
销售归属
↓
绩效快照
↓
bonus_calculate
我一般直接从明细往上查。
SELECT
p.id,
p.contract_id,
p.sales_id,
p.amount,
p.pay_time,
p.status,
p.refund_amount
FROM payment_record p
WHERE p.sales_id = 7318
AND p.pay_time >= '2026-07-01'
AND p.pay_time < '2026-08-01'
ORDER BY p.pay_time;
假设结果是:
合同A 480000 SUCCESS refund=0
合同B 320000 SUCCESS refund=0
合同C 260000 SUCCESS refund=60000
合同D 240000 SUCCESS refund=0
-------------------------------
到账 1300000
退款 60000
那有效回款至少应该还有 124 万。
112.64 万又是哪里来的?
继续查绩效代码:
BigDecimal validAmount = payments.stream()
.filter(Payment::isSettled)
.map(Payment::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal bonusBase = validAmount
.multiply(new BigDecimal("0.92"));
看到这个 0.92 就得停一下。
这 8% 是什么?
如果回答是“历史上一直这么算”,这段代码线上我不太敢留。
业务比例必须有名字:
BigDecimal validAmount = receivedAmount
.subtract(refundAmount)
.subtract(reversedAmount);
BigDecimal bonusBase = validAmount
.multiply(rule.getSettlementFactor());
至少日志也得把过程打出来:
log.info(
"bonus base calculated, salesId={}, received={}, refund={}, reversed={}, factor={}, base={}",
salesId,
receivedAmount,
refundAmount,
reversedAmount,
rule.getSettlementFactor(),
bonusBase
);
这样 112.64 万就能解释:
received = 1,300,000
refund = 60,000
reversed = 16,000
valid = 1,224,000
factor = 0.92
bonusBase = 1,126,080
数字对不上不可怕。
最麻烦的是数字对不上,还没人知道中间减了什么。
更坑的是:规则被人改了,历史绩效跟着变
不少绩效系统会这么设计:
bonus_rule
id
level
min_amount
max_amount
rate
计算的时候直接查当前规则:
BonusRule rule = ruleRepository.findMatchedRule(amount);
return amount.multiply(rule.getRate());
这代码很短,但有个坑。
7 月份回款完成时,规则是:
0 - 50万 0.05%
50 - 100万 0.08%
100万以上 0.12%
8 月份公司把 100 万以上改成:
100万以上 0.06%
然后财务 8 月重新跑 7 月绩效。
130 万直接按新规则算了。
这不是计算错误,是规则没有版本化。
我更愿意把规则表设计成:
CREATE TABLE bonus_rule (
id BIGINT PRIMARY KEY,
rule_version VARCHAR(32) NOT NULL,
min_amount DECIMAL(18,2) NOT NULL,
max_amount DECIMAL(18,2),
rate DECIMAL(10,6) NOT NULL,
effective_from DATETIME NOT NULL,
effective_to DATETIME,
created_at DATETIME NOT NULL
);
计算时不能只传金额:
BonusRule rule = ruleService.match(
performanceDate,
receivedAmount
);
SQL 也必须带生效时间:
SELECT id, rule_version, rate
FROM bonus_rule
WHERE min_amount <= ?
AND (max_amount IS NULL OR max_amount > ?)
AND effective_from <= ?
AND (effective_to IS NULL OR effective_to > ?)
ORDER BY effective_from DESC
LIMIT 1;
还不够。
最终使用的规则必须跟绩效结果一起冻结。
performance_result
sales_id
period
received_amount
valid_amount
rule_version
rule_rate
bonus_amount
calculated_at
比如:
7318
2026-07
1300000.00
1240000.00
BONUS_2026_V3
0.001200
1488.00
2026-08-03 02:10:18
以后哪怕 BONUS_2026_V4 上线,也不能偷偷改变这 1488。
重新计算必须明确指定:
BonusResult recalculate(
Long salesId,
YearMonth period,
String ruleVersion
)
而不是:
recalculate(salesId);
后面这种接口看着省事,出问题的时候根本说不清它到底用了哪套规则。
钱的计算,别再用“查出来直接乘”
还有一种代码我见一次嫌弃一次:
double bonus = received * rate;
金额直接上 BigDecimal:
BigDecimal bonus = validAmount
.multiply(rate)
.setScale(2, RoundingMode.HALF_UP);
但真正容易出错的是阶梯计算。
130 万不一定是:
130万 × 0.12%
规则可能要求分段:
前50万 0.05%
50~100万 0.08%
100万以上 0.12%
那就别写一堆 if-else:
record BonusTier(
BigDecimal start,
BigDecimal end,
BigDecimal rate){
}
BigDecimal calculateTierBonus(
BigDecimal amount,
List<BonusTier> tiers){
BigDecimal total = BigDecimal.ZERO;
for (BonusTier tier : tiers) {
if (amount.compareTo(tier.start()) <= 0) {
continue;
}
BigDecimal upper = tier.end() == null
? amount
: amount.min(tier.end());
BigDecimal tierAmount = upper.subtract(tier.start());
total = total.add(
tierAmount.multiply(tier.rate())
);
}
return total.setScale(2, RoundingMode.HALF_UP);
}
直接拿 130 万压一下:
500000 × 0.0005 = 250
500000 × 0.0008 = 400
300000 × 0.0012 = 360
bonus = 1010
再补几个 边界测试:
assertBonus("0", "0.00");
assertBonus("500000", "250.00");
assertBonus("500001", "250.00");
assertBonus("1000000", "650.00");
assertBonus("1300000", "1010.00");
退款也必须单独测:
到账:1,300,000
退款:200,000
有效回款:1,100,000
预期:
500000 × 0.0005
500000 × 0.0008
100000 × 0.0012
= 770
最后我还会加一张计算明细表:
bonus_calculation_detail
performance_id
tier_start
tier_end
base_amount
rate
bonus_amount
rule_version
前端不要只显示:
本月绩效:800元
而应该能展开:
到账金额 1,300,000
退款金额 -60,000
冲销金额 -16,000
有效回款 1,224,000
规则版本 BONUS_2026_V3
0~50万 250.00
50~100万 400.00
100万以上 268.80
-----------------------
绩效合计 918.80
这样员工觉得少,可以争规则;财务觉得算错,可以查数据;开发被叫起来,也知道应该查哪一层。
130 万最后奖励 800,到底合理不合理,单看这两个数字很难判断,因为缺合同和绩效规则。
但系统如果连“130 万怎么一步步算成 800”都解释不了,那就不只是奖金让人气笑了,开发看到这个账也得跟着笑一下。类似系统设计问题,在 云栈社区 也常被讨论。