案例 35:薪资补贴自动核算
本案例对应 JQuick-Java USE-CASES 场景 35(诉求 43:薪资补贴自动核算),属于人力资源/薪酬管理领域,决策内容是按员工月度考勤与绩效要素自动核算八项补贴分项、执行合规核减,并汇总补贴合计与应发工资。
企业每月薪酬核算要汇总多种政策性与考勤性补贴。入参模型 PayrollInput 承载工号、姓名、基本工资、本单位工龄、当月夜班次数、工资所属月份、高温作业天数及是否户外岗位、加班时长与加班小时基数、加班类型(WORKDAY 工作日 / WEEKEND 休息日 / HOLIDAY 法定节假日)、出勤餐补天数、是否边远地区岗位、是否全勤、月度绩效分等 14 个要素。
八项补贴各自有独立口径并全部外置在 XML 脚本中:工龄津贴按 20/10/5/1 年分四档(8%、5%、3%、1%);夜班补贴 50 元/班;高温津贴户外 30 元/天、室内 15 元/天;加班工资按工作日 1.5 倍、休息日 2 倍、法定节假日 3 倍;餐补 30 元/天;边远地区津贴为基本工资 10%;全勤奖 300 元;绩效奖金按 95/85/70/60 分四档(30%、20%、10%、5%)。编排层另设两道合规闸:高温津贴仅 6-9 月核发,其他月份申报直接清零并备注防错发;月加班超过法定 36 小时上限部分核减并备注,只按 36 小时计发。
决策结果由 PayrollDecision 承载:八个补贴分项金额、补贴合计 totalAllowance、应发工资 grossPay(基本工资加补贴),以及 notes 核算说明清单。所有金额在 Java 侧经 BigDecimal 统一保留两位小数、四舍五入,避免 double 直加产生分位误差。
- 补贴政策随地区与年度频繁调整:高温津贴标准、夜班单价、绩效档位系数、工龄比例各省与各年度口径不同,硬编码进薪资系统后每次调标都要赶在月末封账前发版。
- 八项补贴耦合在一个薪资公式里:任一单项标准变动都要回归整套核算,HR 与财务无法对单项口径独立调参、独立复核。
- 合规核减容易遗漏:高温津贴发放期、法定月加班 36 小时上限属于硬性合规要求,混在计算脚本里容易被申报数据绕过,产生错发与劳动合规风险。
- 金额精度与核算依据要求高:double 直接连加出现分位尾差,员工工资条上又需要每一分钱都有口径与核减说明可查。
- HR 系统老旧、信创环境难承载重型平台:大量薪酬系统仍运行在 JDK 8 与国产中间件上,不宜为补贴规则单独引入重量级 HR 规则平台。
本案例采用 JQuick-Java 标准六件套:入参模型、决策结果模型、Rules 规则接口、XML 规则脚本、Service 编排服务、JUnit 测试,全部代码在 Oracle JDK 1.8 下实测通过。
| 层次 | 职责 | 对应文件 |
|---|---|---|
| 领域模型 | 承载月度薪资核算要素与八项分项、合计决策 | PayrollInput.java、PayrollDecision.java |
| 规则层 | 八项补贴各一方法,比例/单价/档位全部外置 | SalaryAllowanceRules.java、salary-allowance.xml |
| 编排层 | 高温季防错发、加班 36 小时封顶核减、金额两位四舍五入与合计 | SalaryAllowanceService.java |
| 测试 | 6 组薪资核算真实形态 | SalaryAllowanceServiceTest.java |
规则方法拆分
Section titled “规则方法拆分”| 规则方法 | 口径 | 分值/输出 |
|---|---|---|
seniorityAllowance |
工龄 ≥20 年发基本工资 8%;≥10 年 5%;≥5 年 3%;满 1 年 1%;不满 1 年为 0 | 基本工资 × 0.08/0.05/0.03/0.01 |
nightShiftAllowance |
50 元/班 × 当月夜班次数 | shifts × 50(元) |
highTempAllowance |
户外岗位 30 元/天,室内岗位 15 元/天 | days × 30 或 days × 15(元) |
overtimePay |
法定节假日 3 倍、休息日 2 倍、工作日 1.5 倍:小时基数 × 小时数 × 倍率 | double 金额 |
mealAllowance |
30 元/天 × 出勤餐补天数 | days × 30(元) |
remoteAreaAllowance |
边远地区岗位发基本工资 10%,否则 0 | 基本工资 × 0.10 或 0 |
perfectAttendanceBonus |
全勤 300 元,否则 0 | 300 或 0(元) |
performanceBonus |
绩效 ≥95 发 30%;≥85 发 20%;≥70 发 10%;≥60 发 5%;其余 0 | 基本工资 × 0.30/0.20/0.10/0.05 |
代码实现(完整可运行)
Section titled “代码实现(完整可运行)”将两个模型类放入 src/test/java/com/github/paohaijiao/demo/model/,规则接口放入 demo/,Service 放入 demo/service/,XML 脚本放入 src/test/resources/demo/,测试类放入 demo/,包名保持源码原样。工程根目录执行 mvn test -Dtest=SalaryAllowanceServiceTest 即可复现全部 6 个用例。
PayrollInput.java
Section titled “PayrollInput.java”package com.github.paohaijiao.demo.model;
import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;
/** * 员工月度薪资补贴核算要素 */@Data@NoArgsConstructor@AllArgsConstructorpublic class PayrollInput {
/** * 工号 */ private String employeeId;
/** * 姓名 */ private String employeeName;
/** * 基本工资 */ private double baseSalary;
/** * 本单位工龄(年) */ private int yearsOfService;
/** * 当月夜班次数 */ private int nightShifts;
/** * 工资所属月份(1-12) */ private int payrollMonth;
/** * 高温作业天数 */ private int highTempDays;
/** * 是否户外高温岗位 */ private boolean highTempOutdoor;
/** * 加班时长(小时) */ private double overtimeHours;
/** * 加班小时基数(元/小时) */ private double overtimeHourlyBase;
/** * 加班类型:WORKDAY 工作日 / WEEKEND 休息日 / HOLIDAY 法定节假日 */ private String overtimeType;
/** * 出勤餐补天数 */ private int mealAllowanceDays;
/** * 是否边远地区岗位 */ private boolean remoteArea;
/** * 是否全勤 */ private boolean attendancePerfect;
/** * 月度绩效分(0-100) */ private int performanceScore;}PayrollDecision.java
Section titled “PayrollDecision.java”package com.github.paohaijiao.demo.model;
import lombok.Data;
import java.util.ArrayList;import java.util.List;
/** * 月度补贴分项与应发合计 */@Datapublic class PayrollDecision {
/** * 工号 */ private String employeeId;
private double seniorityAllowance; private double nightShiftAllowance; private double highTempAllowance; private double overtimePay; private double mealAllowance; private double remoteAreaAllowance; private double perfectAttendanceBonus; private double performanceBonus;
/** * 补贴合计 */ private double totalAllowance;
/** * 应发工资(基本工资 + 补贴) */ private double grossPay;
/** * 核算说明(防错发、超限提示) */ private List<String> notes = new ArrayList<String>();
public void addNote(String note) { this.notes.add(note); }}SalaryAllowanceRules.java
Section titled “SalaryAllowanceRules.java”package com.github.paohaijiao.demo;
import com.github.paohaijiao.xml.param.Param;
/** * 场景 35:薪资补贴各分项核算规则 */public interface SalaryAllowanceRules {
/** * 工龄津贴:20 年 8%、10 年 5%、5 年 3%、满 1 年 1% */ double seniorityAllowance(@Param("baseSalary") double baseSalary, @Param("years") int years);
/** * 夜班补贴:50 元/班 */ int nightShiftAllowance(@Param("shifts") int shifts);
/** * 高温津贴:户外 30 元/天,室内 15 元/天 */ int highTempAllowance(@Param("days") int days, @Param("outdoor") boolean outdoor);
/** * 加班工资:工作日 1.5 倍、休息日 2 倍、法定节假日 3 倍 */ double overtimePay(@Param("hourlyBase") double hourlyBase, @Param("hours") double hours, @Param("type") String type);
/** * 餐补:30 元/天 */ int mealAllowance(@Param("days") int days);
/** * 边远地区津贴:基本工资 10% */ double remoteAreaAllowance(@Param("baseSalary") double baseSalary, @Param("remote") boolean remote);
/** * 全勤奖:300 元 */ int perfectAttendanceBonus(@Param("perfect") boolean perfect);
/** * 绩效奖金:≥95 30%、≥85 20%、≥70 10%、≥60 5%,其余为 0 */ double performanceBonus(@Param("baseSalary") double baseSalary, @Param("score") int score);}salary-allowance.xml
Section titled “salary-allowance.xml”<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE javas PUBLIC "-//PAOHAIJIAO//DTD API JAVA 1.0//EN" "classpath:paohaijiao/dtd/Jquick-java.dtd"><javas namespace="com.github.paohaijiao.demo.SalaryAllowanceRules">
<java name="seniorityAllowance" returnClass="double"> <![CDATA[ double def seniorityAllowance(double:baseSalary, int:years) { if (years >= 20) { return baseSalary * 0.08; } else if (years >= 10) { return baseSalary * 0.05; } else if (years >= 5) { return baseSalary * 0.03; } else if (years >= 1) { return baseSalary * 0.01; } else { return 0.0; } } ]]> </java>
<java name="nightShiftAllowance" returnClass="int"> <![CDATA[ int def nightShiftAllowance(int:shifts) { return shifts * 50; } ]]> </java>
<java name="highTempAllowance" returnClass="int"> <![CDATA[ int def highTempAllowance(int:days, boolean:outdoor) { if (outdoor) { return days * 30; } else { return days * 15; } } ]]> </java>
<java name="overtimePay" returnClass="double"> <![CDATA[ double def overtimePay(double:hourlyBase, double:hours, str:type) { if (type == "HOLIDAY") { return hourlyBase * hours * 3.0; } else if (type == "WEEKEND") { return hourlyBase * hours * 2.0; } else { return hourlyBase * hours * 1.5; } } ]]> </java>
<java name="mealAllowance" returnClass="int"> <![CDATA[ int def mealAllowance(int:days) { return days * 30; } ]]> </java>
<java name="remoteAreaAllowance" returnClass="double"> <![CDATA[ double def remoteAreaAllowance(double:baseSalary, boolean:remote) { if (remote) { return baseSalary * 0.10; } else { return 0.0; } } ]]> </java>
<java name="perfectAttendanceBonus" returnClass="int"> <![CDATA[ int def perfectAttendanceBonus(boolean:perfect) { if (perfect) { return 300; } else { return 0; } } ]]> </java>
<java name="performanceBonus" returnClass="double"> <![CDATA[ double def performanceBonus(double:baseSalary, int:score) { if (score >= 95) { return baseSalary * 0.30; } else if (score >= 85) { return baseSalary * 0.20; } else if (score >= 70) { return baseSalary * 0.10; } else if (score >= 60) { return baseSalary * 0.05; } else { return 0.0; } } ]]> </java>
</javas>SalaryAllowanceService.java
Section titled “SalaryAllowanceService.java”package com.github.paohaijiao.demo.service;
import com.github.paohaijiao.demo.SalaryAllowanceRules;import com.github.paohaijiao.demo.model.PayrollDecision;import com.github.paohaijiao.demo.model.PayrollInput;
import java.math.BigDecimal;import java.math.RoundingMode;
/** * 场景 35:薪资补贴自动核算(工龄/夜班/高温/加班/餐补/边远/全勤/绩效) */public class SalaryAllowanceService {
/** * 法定月度加班时长上限(小时) */ private static final double MAX_LEGAL_OVERTIME_HOURS = 36.0;
private final SalaryAllowanceRules rules;
public SalaryAllowanceService(SalaryAllowanceRules rules) { this.rules = rules; }
public PayrollDecision calculate(PayrollInput input) { PayrollDecision d = new PayrollDecision(); d.setEmployeeId(input.getEmployeeId());
d.setSeniorityAllowance(money(rules.seniorityAllowance( input.getBaseSalary(), input.getYearsOfService()))); d.setNightShiftAllowance(rules.nightShiftAllowance(input.getNightShifts()));
if (input.getHighTempDays() > 0 && (input.getPayrollMonth() < 6 || input.getPayrollMonth() > 9)) { d.setHighTempAllowance(0.0); d.addNote("申报高温作业 " + input.getHighTempDays() + " 天,但 " + input.getPayrollMonth() + " 月不在高温津贴发放期(6-9 月),该补贴不予核发"); } else { d.setHighTempAllowance(rules.highTempAllowance( input.getHighTempDays(), input.isHighTempOutdoor())); }
double payableOvertimeHours = input.getOvertimeHours(); if (payableOvertimeHours > MAX_LEGAL_OVERTIME_HOURS) { d.addNote("申报加班 " + input.getOvertimeHours() + " 小时超出法定上限 36 小时,超出部分按 " + (payableOvertimeHours - MAX_LEGAL_OVERTIME_HOURS) + " 小时核减"); payableOvertimeHours = MAX_LEGAL_OVERTIME_HOURS; } d.setOvertimePay(money(rules.overtimePay(input.getOvertimeHourlyBase(), payableOvertimeHours, input.getOvertimeType()))); d.setMealAllowance(rules.mealAllowance(input.getMealAllowanceDays())); d.setRemoteAreaAllowance(money(rules.remoteAreaAllowance( input.getBaseSalary(), input.isRemoteArea()))); d.setPerfectAttendanceBonus(rules.perfectAttendanceBonus(input.isAttendancePerfect())); d.setPerformanceBonus(money(rules.performanceBonus( input.getBaseSalary(), input.getPerformanceScore())));
double total = money(d.getSeniorityAllowance() + d.getNightShiftAllowance() + d.getHighTempAllowance() + d.getOvertimePay() + d.getMealAllowance() + d.getRemoteAreaAllowance() + d.getPerfectAttendanceBonus() + d.getPerformanceBonus()); d.setTotalAllowance(total); d.setGrossPay(money(input.getBaseSalary() + total)); return d; }
private double money(double v) { return BigDecimal.valueOf(v).setScale(2, RoundingMode.HALF_UP).doubleValue(); }}SalaryAllowanceServiceTest.java
Section titled “SalaryAllowanceServiceTest.java”package com.github.paohaijiao.demo;
import com.github.paohaijiao.demo.model.PayrollDecision;import com.github.paohaijiao.demo.model.PayrollInput;import com.github.paohaijiao.demo.service.SalaryAllowanceService;import com.github.paohaijiao.xml.JQuickJava;import org.junit.Assert;import org.junit.Before;import org.junit.Test;
/** * 场景 35:薪资补贴自动核算 * <p> * 6 组形态:全补贴标杆、新人基础补贴、法定节假日三倍加班、 * 非高温季防错发、加班超 36 小时核减、绩效不达标与 60 分临界。 */public class SalaryAllowanceServiceTest {
private SalaryAllowanceService service;
@Before public void setUp() { SalaryAllowanceRules rules = JQuickJava.create() .createApi(SalaryAllowanceRules.class, "demo/salary-allowance.xml"); this.service = new SalaryAllowanceService(rules); }
/** * 12 年工龄、8 夜班、12 户外高温、10 小时平日加班、边远、全勤、90 分绩效 */ @Test public void shouldCalculateAllAllowanceItems() { PayrollInput i = base("E001", 10000, 7); i.setYearsOfService(12); i.setNightShifts(8); i.setHighTempDays(12); i.setHighTempOutdoor(true); i.setOvertimeHours(10); i.setOvertimeHourlyBase(60); i.setOvertimeType("WORKDAY"); i.setMealAllowanceDays(22); i.setRemoteArea(true); i.setAttendancePerfect(true); i.setPerformanceScore(90);
PayrollDecision d = service.calculate(i);
Assert.assertEquals(500.0, d.getSeniorityAllowance(), 0.0001); Assert.assertEquals(400.0, d.getNightShiftAllowance(), 0.0001); Assert.assertEquals(360.0, d.getHighTempAllowance(), 0.0001); Assert.assertEquals(900.0, d.getOvertimePay(), 0.0001); Assert.assertEquals(660.0, d.getMealAllowance(), 0.0001); Assert.assertEquals(1000.0, d.getRemoteAreaAllowance(), 0.0001); Assert.assertEquals(300.0, d.getPerfectAttendanceBonus(), 0.0001); Assert.assertEquals(2000.0, d.getPerformanceBonus(), 0.0001); Assert.assertEquals(6120.0, d.getTotalAllowance(), 0.0001); Assert.assertEquals(16120.0, d.getGrossPay(), 0.0001); }
/** * 新人无工龄/夜班/加班/全勤,仅餐补与 72 分绩效 */ @Test public void shouldCalculateFreshmanBasicAllowance() { PayrollInput i = base("E002", 6000, 6); i.setYearsOfService(0); i.setMealAllowanceDays(20); i.setPerformanceScore(72);
PayrollDecision d = service.calculate(i);
Assert.assertEquals(0.0, d.getSeniorityAllowance(), 0.0001); Assert.assertEquals(600.0, d.getMealAllowance(), 0.0001); Assert.assertEquals(600.0, d.getPerformanceBonus(), 0.0001); Assert.assertEquals(1200.0, d.getTotalAllowance(), 0.0001); Assert.assertEquals(7200.0, d.getGrossPay(), 0.0001); }
/** * 6 年工龄 + 4 夜班 + 法定节假日 8 小时三倍加班 + 85 分绩效 */ @Test public void shouldPayTripleOvertimeOnHoliday() { PayrollInput i = base("E003", 8000, 10); i.setYearsOfService(6); i.setNightShifts(4); i.setHighTempDays(0); i.setOvertimeHours(8); i.setOvertimeHourlyBase(50); i.setOvertimeType("HOLIDAY"); i.setMealAllowanceDays(0); i.setAttendancePerfect(true); i.setPerformanceScore(85);
PayrollDecision d = service.calculate(i);
Assert.assertEquals(240.0, d.getSeniorityAllowance(), 0.0001); Assert.assertEquals(200.0, d.getNightShiftAllowance(), 0.0001); Assert.assertEquals(1200.0, d.getOvertimePay(), 0.0001); Assert.assertEquals(300.0, d.getPerfectAttendanceBonus(), 0.0001); Assert.assertEquals(1600.0, d.getPerformanceBonus(), 0.0001); Assert.assertEquals(3540.0, d.getTotalAllowance(), 0.0001); }
/** * 10 月申报 10 天户外高温 → 非发放期防错发清零 */ @Test public void shouldRejectHighTempAllowanceOutsideSummer() { PayrollInput i = base("E004", 7000, 10); i.setYearsOfService(2); i.setHighTempDays(10); i.setHighTempOutdoor(true); i.setMealAllowanceDays(10); i.setPerformanceScore(60);
PayrollDecision d = service.calculate(i);
Assert.assertEquals(0.0, d.getHighTempAllowance(), 0.0001); Assert.assertEquals(70.0, d.getSeniorityAllowance(), 0.0001); Assert.assertEquals(300.0, d.getMealAllowance(), 0.0001); Assert.assertEquals(350.0, d.getPerformanceBonus(), 0.0001); Assert.assertEquals(720.0, d.getTotalAllowance(), 0.0001); Assert.assertTrue(d.getNotes().get(0).contains("不在高温津贴发放期(6-9 月)")); }
/** * 申报 40 小时工作日加班 → 仅核发 36 小时 */ @Test public void shouldCapOvertimeAtLegalLimit() { PayrollInput i = base("E005", 6000, 8); i.setYearsOfService(0); i.setOvertimeHours(40); i.setOvertimeHourlyBase(50); i.setOvertimeType("WORKDAY"); i.setMealAllowanceDays(0); i.setPerformanceScore(0);
PayrollDecision d = service.calculate(i);
Assert.assertEquals(2700.0, d.getOvertimePay(), 0.0001); Assert.assertEquals(2700.0, d.getTotalAllowance(), 0.0001); Assert.assertTrue(d.getNotes().get(0).contains("超出法定上限 36 小时,超出部分按 4.0 小时核减")); }
/** * 绩效 59 分无奖金;同条件 60 分临界拿 5% */ @Test public void shouldHandlePerformanceThresholdAtSixty() { PayrollInput low = base("E006", 9000, 9); low.setYearsOfService(10); low.setNightShifts(10); low.setMealAllowanceDays(21); low.setPerformanceScore(59); PayrollDecision d1 = service.calculate(low); Assert.assertEquals(450.0, d1.getSeniorityAllowance(), 0.0001); Assert.assertEquals(500.0, d1.getNightShiftAllowance(), 0.0001); Assert.assertEquals(630.0, d1.getMealAllowance(), 0.0001); Assert.assertEquals(0.0, d1.getPerformanceBonus(), 0.0001); Assert.assertEquals(1580.0, d1.getTotalAllowance(), 0.0001);
PayrollInput edge = base("E007", 9000, 9); edge.setYearsOfService(10); edge.setNightShifts(10); edge.setMealAllowanceDays(21); edge.setPerformanceScore(60); PayrollDecision d2 = service.calculate(edge); Assert.assertEquals(450.0, d2.getPerformanceBonus(), 0.0001); Assert.assertEquals(2030.0, d2.getTotalAllowance(), 0.0001); }
private PayrollInput base(String id, double baseSalary, int month) { PayrollInput i = new PayrollInput(); i.setEmployeeId(id); i.setEmployeeName("测试员工"); i.setBaseSalary(baseSalary); i.setYearsOfService(0); i.setNightShifts(0); i.setPayrollMonth(month); i.setHighTempDays(0); i.setHighTempOutdoor(false); i.setOvertimeHours(0); i.setOvertimeHourlyBase(0); i.setOvertimeType("WORKDAY"); i.setMealAllowanceDays(0); i.setRemoteArea(false); i.setAttendancePerfect(false); i.setPerformanceScore(0); return i; }}在工程根目录执行:
mvn test -Dtest=SalaryAllowanceServiceTest测试覆盖形态(6 个 @Test 方法,覆盖全分项标杆、新人、三倍加班、防错发、法定核减、绩效临界):
| 测试方法 | 业务形态 | 关键断言/期望决策 |
|---|---|---|
shouldCalculateAllAllowanceItems |
E001 基本工资 10000、7 月:12 年工龄、8 夜班、12 天户外高温、10 小时工作日加班(基数 60)、22 天餐补、边远、全勤、绩效 90 | 八项依次 500/400/360/900/660/1000/300/2000,补贴合计 6120.0,应发 16120.0 |
shouldCalculateFreshmanBasicAllowance |
E002 基本工资 6000、6 月:0 年工龄、无夜班加班、20 天餐补、绩效 72 | 工龄津贴 0、餐补 600、绩效 600(10% 档),合计 1200.0,应发 7200.0 |
shouldPayTripleOvertimeOnHoliday |
E003 基本工资 8000、10 月:6 年工龄、4 夜班、8 小时法定节假日加班(基数 50)、全勤、绩效 85 | 工龄 240、夜班 200、加班 1200(50×8×3)、全勤 300、绩效 1600,合计 3540.0 |
shouldRejectHighTempAllowanceOutsideSummer |
E004 基本工资 7000、10 月:2 年工龄、申报 10 天户外高温、10 天餐补、绩效 60 | 高温津贴 0.0,工龄 70、餐补 300、绩效 350(5% 档),合计 720.0,首条备注含“不在高温津贴发放期(6-9 月)” |
shouldCapOvertimeAtLegalLimit |
E005 基本工资 6000、8 月:申报 40 小时工作日加班(基数 50),其余为 0 | 仅按 36 小时计发:50×36×1.5=2700.0,合计 2700.0,备注含“超出法定上限 36 小时,超出部分按 4.0 小时核减” |
shouldHandlePerformanceThresholdAtSixty |
E006/E007 基本工资 9000、9 月:10 年工龄、10 夜班、21 天餐补,绩效分别 59 与 60 | 59 分绩效奖金 0,合计 1580.0;60 分临界拿 5%(450.0),合计 2030.0;两单工龄 450、夜班 500、餐补 630 一致 |
- 八项口径 XML 外置、政策调标免发版:工龄 8%/5%/3%/1%、夜班 50 元、高温 30/15 元、加班 1.5/2/3 倍、餐补 30 元、绩效 30%/20%/10%/5% 全部写在
salary-allowance.xml,地区或年度调标时改 XML 重建代理即可,月末封账不再依赖发版窗口。 - 一方法一补贴,独立调参与审计:每个补贴是一个独立规则方法,工资条上八个分项金额可逐一回溯到对应方法与档位,HR 复核与员工答疑都有明确口径。
- 合规闸在 Java 侧前置:高温津贴 6-9 月发放期校验直接清零并写防错发备注,月加班 36 小时法定上限在调用脚本前完成核减,申报数据再异常也绕不过硬性合规要求。
- BigDecimal 统一两位四舍五入:脚本只做纯数值计算,Service 用
BigDecimal HALF_UP对每个分项、合计、应发分别归整,规避 double 连加的分位尾差。 - 枚举型加班类型按名注入:
@Param("type")把 WORKDAY/WEEKEND/HOLIDAY 按名传入脚本,倍率分支正向书写、默认回落工作日 1.5 倍,无需在脚本中处理取反逻辑。 - ASM 动态代理、Java 8 无重型依赖:代理单例复用、无独立服务与额外中间件,可直接嵌入运行在 JDK 8 与信创 JDK 上的老旧 e-HR/薪酬系统。