点狮HRM-企业薪酬核算系统架构与计算引擎设计

📖 目录

相关链接


在这里插入图片描述

一、业务背景与挑战

1.1 企业薪酬核算痛点

薪酬核算是HRM系统中最复杂、最敏感的功能模块,直接影响员工切身利益,容不得半点差错。在实际应用中面临诸多挑战:

计算规则复杂:基本工资、岗位工资、绩效工资、加班费、奖金、扣款等项目繁多,计算逻辑复杂。不同企业薪资结构差异巨大,难以通用化。

个税计算繁琐:累计预扣法计算个税,需要考虑专项附加扣除、全年一次性奖金计税等多种场景。税法政策频繁调整,系统需要及时适配。

数据来源分散:考勤数据、绩效数据、社保公积金、专项附加扣除等数据分散在不同系统,汇总核对工作量大。

核算效率低下:传统手工核算或Excel计算,500人企业需要2-3天完成月度核算,且容易出错。

数据安全要求高:薪资数据属于高度敏感信息,需要严格的权限控制、加密存储、审计日志。

系统性能瓶颈:月底集中核算时,大量计算任务导致系统响应缓慢,影响用户体验。

1.2 点狮薪酬核算解决方案

点狮薪酬核算系统基于规则引擎+分布式计算架构,提供完整的企业级薪酬解决方案:

核心功能

  • 薪资核算引擎:支持多种薪资方案自定义配置
  • 个税计算:自动计算累计预扣法个税、专项附加扣除
  • 社保公积金:自动计算个人及单位缴纳部分
  • 批量核算:支持5000人企业30分钟完成核算
  • 工资条发放:加密工资条、密码验证、防截屏保护

技术特色

  • 规则引擎驱动,零代码配置薪资方案
  • 分布式计算,支撑大规模薪酬核算
  • 数据一致性保障,支持回滚重算
  • 安全加密存储,完善的权限控制

二、薪酬核算系统架构设计

2.1 系统架构

┌──────────────────────────────────────────────────────────┐
│                    薪酬核算系统架构                           │
├──────────────────────────────────────────────────────────┤
│                                                                │
│  ┌──────────────────────────────────────────────────┐    │
│  │              薪酬计算引擎(Core)                │    │
│  │                                                  │    │
│  │  ┌─────────────┐  ┌─────────────┐  ┌───────────┐  │    │
│  │  │ 薪资项配置  │  │ 计算规则    │  │ 公式引擎  │  │    │
│  │  │ 模块        │  │ 引擎        │  │           │  │    │
│  │  └─────────────┘  └─────────────┘  └───────────┘  │    │
│  └──────────────────────────────────────────────────┘    │
│                                                                │
│  ┌──────────────────────────────────────────────────┐    │
│  │              数据采集层(Data)                   │    │
│  │                                                  │    │
│  │  ┌──────────┐  ┌──────────┐  ┌────────────┐    │    │
│  │  │考勤数据   │  │绩效数据   │  │员工档案     │    │    │
│  │  └──────────┘  └──────────┘  └────────────┘    │    │
│  └──────────────────────────────────────────────────┘    │
│                                                                │
│  ┌──────────────────────────────────────────────────┐    │
│  │              计算执行层(Compute)              │    │
│  │                                                  │    │
│  │  ┌────────────┐  ┌────────────┐  ┌───────────┐  │    │
│  │  │批量计算    │  │分布式计算  │  │异步处理   │  │    │
│  │  └────────────┘  └────────────┘  └───────────┘  │    │
│  └──────────────────────────────────────────────────┘    │
│                                                                │
└──────────────────────────────────────────────────────────┘

在这里插入图片描述

2.2 领域模型设计

薪酬核算聚合根
/**
 * 薪酬核算聚合根
 */
@Entity
@Table(name = "hrm_salary_calculation")
@AggregateRoot
public class SalaryCalculation {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    /**
     * 租户ID
     */
    @Column(name = "tenant_id", nullable = false)
    private Long tenantId;
    
    /**
     * 核算月份(格式:2024-01)
     */
    @Column(name = "calculation_month", nullable = false)
    private String calculationMonth;
    
    /**
     * 员工ID
     */
    @Column(name = "employee_id", nullable = false)
    private Long employeeId;
    
    /**
     * 薪资方案ID
     */
    @Column(name = "salary_template_id")
    private Long salaryTemplateId;
    
    /**
     * 核算状态:DRAFT草稿、CALCULATED已核算、CONFIRMED已确认
     */
    @Enumerated(EnumType.STRING)
    @Column(name = "status", nullable = false)
    private CalculationStatus status;
    
    /**
     * 应发工资
     */
    @Column(name = "gross_salary", precision = 10, scale = 2)
    private BigDecimal grossSalary;
    
    /**
     * 实发工资
     */
    @Column(name = "net_salary", precision = 10, scale = 2)
    private BigDecimal netSalary;
    
    /**
     * 工资项明细(JSON格式)
     */
    @Column(name = "salary_items", columnDefinition = "JSON")
    private String salaryItems;
    
    /**
     * 计算时间
     */
    @Column(name = "calc_time")
    private LocalDateTime calcTime;
    
    /**
     * 计算人ID
     */
    @Column(name = "calc_user_id")
    private Long calcUserId;
    
    /**
     * 领域方法:执行核算
     */
    public void calculate(CalculationCommand command) {
        // 1. 验证核算状态
        if (this.status != CalculationStatus.DRAFT) {
            throw new BusinessException("只有草稿状态的核算可以重新计算");
        }
        
        // 2. 获取薪资模板
        SalaryTemplate template = templateRepository.findById(this.salaryTemplateId);
        
        // 3. 构建计算上下文
        CalculationContext context = CalculationContext.builder()
            .tenantId(this.tenantId)
            .employeeId(this.employeeId)
            .calculationMonth(this.calculationMonth)
            .template(template)
            .employee(command.getEmployee())
            .attendanceData(command.getAttendanceData())
            .performanceData(command.getPerformanceData())
            .build();
        
        // 4. 执行薪资计算
        SalaryCalculationResult result = calculationEngine.calculate(context);
        
        // 5. 更新核算结果
        this.grossSalary = result.getGrossSalary();
        this.netSalary = result.getNetSalary();
        this.salaryItems = result.getSalaryItemsJson();
        this.calcTime = LocalDateTime.now();
        this.calcUserId = command.getCurrentUserId();
        this.status = CalculationStatus.CALCULATED;
        
        // 6. 发布核算完成事件
        DomainEventPublisher.publish(new SalaryCalculatedEvent(this.id, this.employeeId));
    }
    
    /**
     * 领域方法:确认核算
     */
    public void confirm(ConfirmCommand command) {
        // 1. 验证核算状态
        if (this.status != CalculationStatus.CALCULATED) {
            throw new BusinessException("只有已核算状态的记录可以确认");
        }
        
        // 2. 更新状态
        this.status = CalculationStatus.CONFIRMED;
        
        // 3. 发布确认事件
        DomainEventPublisher.publish(new SalaryConfirmedEvent(this.id, this.employeeId));
    }
}

三、薪资计算引擎设计

3.1 计算引擎架构

/**
 * 薪资计算引擎接口
 */
public interface SalaryCalculationEngine {
    
    /**
     * 执行薪资计算
     */
    SalaryCalculationResult calculate(CalculationContext context);
}

/**
 * 抽象薪资计算引擎
 */
public abstract class AbstractSalaryCalculationEngine implements SalaryCalculationEngine {
    
    @Autowired
    protected SalaryItemConfigRepository itemConfigRepository;
    
    @Autowired
    protected AttendanceDataService attendanceDataService;
    
    @Autowired
    protected PerformanceDataService performanceDataService;
    
    /**
     * 模板方法:定义计算流程
     */
    @Override
    public final SalaryCalculationResult calculate(CalculationContext context) {
        
        // 1. 构建结果对象
        SalaryCalculationResult result = SalaryCalculationResult.builder()
            .tenantId(context.getTenantId())
            .employeeId(context.getEmployeeId())
            .calculationMonth(context.getCalculationMonth())
            .build();
        
        // 2. 获取薪资项配置
        List<SalaryItemConfig> itemConfigs = itemConfigRepository
            .findByTemplateId(context.getTemplate().getId());
        
        // 3. 按优先级排序薪资项(有些薪资项依赖其他薪资项)
        itemConfigs.sort(Comparator.comparingInt(SalaryItemConfig::getPriority));
        
        // 4. 依次计算各个薪资项
        Map<String, BigDecimal> calculatedValues = new HashMap<>();
        for (SalaryItemConfig itemConfig : itemConfigs) {
            
            // 判断是否启用
            if (!itemConfig.isEnabled()) {
                continue;
            }
            
            // 执行计算
            BigDecimal value = calculateItem(itemConfig, context, calculatedValues);
            
            // 保存计算结果
            calculatedValues.put(itemConfig.getItemCode(), value);
            result.addSalaryItem(itemConfig, value);
        }
        
        // 5. 汇总计算应发工资
        BigDecimal grossSalary = calculateGrossSalary(result);
        result.setGrossSalary(grossSalary);
        
        // 6. 计算扣款项(社保、公积金、个税)
        calculateDeductions(result, context);
        
        // 7. 计算实发工资
        BigDecimal netSalary = calculateNetSalary(result);
        result.setNetSalary(netSalary);
        
        return result;
    }
    
    /**
     * 计算单个薪资项(子类实现)
     */
    protected abstract BigDecimal calculateItem(
        SalaryItemConfig itemConfig,
        CalculationContext context,
        Map<String, BigDecimal> calculatedValues
    );
}

/**
 * 基本工资计算引擎
 */
@Component("baseSalaryEngine")
public class BaseSalaryEngine extends AbstractSalaryCalculationEngine {
    
    @Override
    protected BigDecimal calculateItem(
        SalaryItemConfig itemConfig,
        CalculationContext context,
        Map<String, BigDecimal> calculatedValues) {
        
        // 获取员工薪资标准
        EmployeeSalaryStandard standard = context.getStandard();
        
        // 计算月基本工资(日薪标准 × 出勤天数)
        BigDecimal dailySalary = standard.getBaseSalary().divide(
            BigDecimal.valueOf(21.75), 2, RoundingMode.HALF_UP
        );
        
        // 获取当月应出勤天数
        int expectedWorkDays = attendanceDataService.getExpectedWorkDays(
            context.getTenantId(), 
            context.getCalculationMonth()
        );
        
        // 获取当月实际出勤天数
        int actualWorkDays = attendanceDataService.getActualWorkDays(
            context.getEmployeeId(), 
            context.getCalculationMonth()
        );
        
        // 按实际出勤天数计算
        BigDecimal monthlySalary = dailySalary.multiply(BigDecimal.valueOf(actualWorkDays));
        
        return monthlySalary;
    }
}

/**
 * 绩效工资计算引擎
 */
@Component("performanceSalaryEngine")
public class PerformanceSalaryEngine extends AbstractSalaryCalculationEngine {
    
    @Override
    protected BigDecimal calculateItem(
        SalaryItemConfig itemConfig,
        CalculationContext context,
        Map<String, BigDecimal> calculatedValues) {
        
        // 1. 获取绩效数据
        PerformanceData performanceData = context.getPerformanceData();
        
        // 2. 获取绩效工资配置
        PerformanceSalaryConfig config = itemConfig.getPerformanceConfig();
        
        // 3. 计算绩效工资
        BigDecimal performanceSalary = BigDecimal.ZERO;
        
        if (config.isBasedOnScore()) {
            // 基于绩效得分计算
            BigDecimal score = performanceData.getScore();
            performanceSalary = calculateByScore(score, config);
        } else if (config.isBasedOnLevel()) {
            // 基于绩效等级计算
            String level = performanceData.getLevel();
            performanceSalary = calculateByLevel(level, config);
        }
        
        return performanceSalary;
    }
    
    /**
     * 基于绩效得分计算
     */
    private BigDecimal calculateByScore(BigDecimal score, PerformanceSalaryConfig config) {
        
        // 绩效得分转换为绩效系数
        BigDecimal coefficient = calculateCoefficient(score, config);
        
        // 绩效基数 × 绩效系数
        return config.getBaseAmount().multiply(coefficient).setScale(2, RoundingMode.HALF_UP);
    }
    
    /**
     * 计算绩效系数
     */
    private BigDecimal calculateCoefficient(BigDecimal score, PerformanceSalaryConfig config) {
        
        // 示例:60分以下系数0.8,60-80分系数1.0,80-100分系数1.2
        if (score.compareTo(BigDecimal.valueOf(60)) < 0) {
            return BigDecimal.valueOf(0.8);
        } else if (score.compareTo(BigDecimal.valueOf(80)) < 0) {
            return BigDecimal.valueOf(1.0);
        } else {
            return BigDecimal.valueOf(1.2);
        }
    }
}

/**
 * 加班费计算引擎
 */
@Component("overtimePayEngine")
public class OvertimePayEngine extends AbstractSalaryCalculationEngine {
    
    @Override
    protected BigDecimal calculateItem(
        SalaryItemConfig itemConfig,
        CalculationContext context,
        Map<String, BigDecimal> calculatedValues) {
        
        // 1. 获取考勤数据
        AttendanceData attendanceData = context.getAttendanceData();
        
        // 2. 获取员工薪资标准(用于计算时薪)
        EmployeeSalaryStandard standard = context.getStandard();
        BigDecimal hourlyRate = standard.getBaseSalary().divide(
            BigDecimal.valueOf(21.75 * 8), 2, RoundingMode.HALF_UP
        );
        
        // 3. 计算工作日加班费
        double weekdayOvertimeHours = attendanceData.getWeekdayOvertimeHours();
        BigDecimal weekdayOvertimePay = hourlyRate
            .multiply(BigDecimal.valueOf(weekdayOvertimeHours))
            .multiply(BigDecimal.valueOf(1.5)) // 工作日加班1.5倍
            .setScale(2, RoundingMode.HALF_UP);
        
        // 4. 计算周末加班费
        double weekendOvertimeHours = attendanceData.getWeekendOvertimeHours();
        BigDecimal weekendOvertimePay = hourlyRate
            .multiply(BigDecimal.valueOf(weekendOvertimeHours))
            .multiply(BigDecimal.valueOf(2.0)) // 周末加班2倍
            .setScale(2, RoundingMode.HALF_UP);
        
        // 5. 计算法定节假日加班费
        double holidayOvertimeHours = attendanceData.getHolidayOvertimeHours();
        BigDecimal holidayOvertimePay = hourlyRate
            .multiply(BigDecimal.valueOf(holidayOvertimeHours))
            .multiply(BigDecimal.valueOf(3.0)) // 法定节假日加班3倍
            .setScale(2, RoundingMode.HALF_UP);
        
        // 6. 汇总加班费
        return weekdayOvertimePay
            .add(weekendOvertimePay)
            .add(holidayOvertimePay);
    }
}

/**
 * 奖金扣款计算引擎
 */
@Component("deductionEngine")
public class DeductionEngine extends AbstractSalaryCalculationEngine {
    
    @Override
    protected BigDecimal calculateItem(
        SalaryItemConfig itemConfig,
        CalculationContext context,
        Map<String, BigDecimal> calculatedValues) {
        
        // 1. 获取扣款配置
        DeductionConfig config = itemConfig.getDeductionConfig();
        
        // 2. 根据扣款类型计算
        switch (config.getDeductionType()) {
            case ABSENTEEISM:
                // 旷工扣款
                return calculateAbsenteeismDeduction(config, context);
            case LATE:
                // 迟到扣款
                return calculateLateDeduction(config, context);
            case EARLY:
                // 早退扣款
                return calculateEarlyDeduction(config, context);
            case LEAVE_WITHOUT_PAY:
                // 无薪假扣款
                return calculateLeaveDeduction(config, context);
            default:
                return BigDecimal.ZERO;
        }
    }
    
    /**
     * 计算旷工扣款
     */
    private BigDecimal calculateAbsenteeismDeduction(
        DeductionConfig config,
        CalculationContext context
    ) {
        // 获取旷工天数
        int absentDays = attendanceDataService.getAbsentDays(
            context.getEmployeeId(),
            context.getCalculationMonth()
        );
        
        // 日薪标准 × 旷工天数 × 扣款系数
        BigDecimal dailySalary = context.getStandard().getBaseSalary()
            .divide(BigDecimal.valueOf(21.75), 2, RoundingMode.HALF_UP);
            
        return dailySalary
            .multiply(BigDecimal.valueOf(absentDays))
            .multiply(config.getDeductionRate())
            .setScale(2, RoundingMode.HALF_UP);
    }
    
    /**
     * 计算迟到扣款
     */
    private BigDecimal calculateLateDeduction(
        DeductionConfig config,
        CalculationContext context
    ) {
        // 获取迟到记录
        List<LateRecord> lateRecords = attendanceDataService.getLateRecords(
            context.getEmployeeId(),
            context.getCalculationMonth()
        );
        
        // 计算扣款金额
        BigDecimal deduction = BigDecimal.ZERO;
        for (LateRecord record : lateRecords) {
            
            // 判断迟到时长对应的扣款标准
            BigDecimal deductionPerTime = getDeductionByLateMinutes(
                record.getLateMinutes(),
                config
            );
            
            deduction = deduction.add(deductionPerTime);
        }
        
        return deduction;
    }
}

3.2 公式解析引擎

/**
 * 公式解析引擎
 */
@Component
public class FormulaParserEngine {
    
    /**
     * 解析并计算公式
     */
    public BigDecimal parseAndCalculate(String formula, CalculationContext context) {
        
        // 1. 预处理公式(替换变量)
        String processedFormula = preprocessFormula(formula, context);
        
        // 2. 词法分析
        List<Token> tokens = tokenize(processedFormula);
        
        // 3. 语法分析
        ASTNode ast = parse(tokens);
        
        // 4. 计算AST
        return calculateAST(ast, context);
    }
    
    /**
     * 预处理公式:替换变量为实际值
     */
    private String preprocessFormula(String formula, CalculationContext context) {
        
        // 1. 替换系统变量
        String result = formula;
        result = result.replace("${baseSalary}", context.getStandard().getBaseSalary().toString());
        result = result.replace("${attendanceDays}", String.valueOf(context.getAttendanceDays()));
        result = result.replace("${performanceScore}", context.getPerformanceData().getScore().toString());
        
        // 2. 替换自定义变量
        Map<String, Object> variables = context.getVariables();
        for (Map.Entry<String, Object> entry : variables.entrySet()) {
            result = result.replace("${" + entry.getKey() + "}", String.valueOf(entry.getValue()));
        }
        
        return result;
    }
    
    /**
     * 词法分析
     */
    private List<Token> tokenize(String formula) {
        
        List<Token> tokens = new ArrayList<>();
        Tokenizer tokenizer = new Tokenizer(new StringReader(formula));
        
        while (tokenizer.hasNext()) {
            tokens.add(tokenizer.next());
        }
        
        return tokens;
    }
    
    /**
     * 语法分析(递归下降分析法)
     */
    private ASTNode parse(List<Token> tokens) {
        
        return new Parser(tokens).parse();
    }
    
    /**
     * 计算AST
     */
    private BigDecimal calculateAST(ASTNode ast, CalculationContext context) {
        
        return ast.evaluate(context);
    }
}

/**
 * 抽象语法树节点
 */
public abstract class ASTNode {
    
    /**
     * 计算节点值
     */
    public abstract BigDecimal evaluate(CalculationContext context);
}

/**
 * 数字常量节点
 */
public class NumberNode extends ASTNode {
    
    private final BigDecimal value;
    
    public NumberNode(BigDecimal value) {
        this.value = value;
    }
    
    @Override
    public BigDecimal evaluate(CalculationContext context) {
        return value;
    }
}

/**
 * 二元运算节点
 */
public class BinaryOpNode extends ASTNode {
    
    private final ASTNode left;
    private final ASTNode right;
    private final BinaryOperator operator;
    
    public BinaryOpNode(ASTNode left, BinaryOperator operator, ASTNode right) {
        this.left = left;
        this.operator = operator;
        this.right = right;
    }
    
    @Override
    public BigDecimal evaluate(CalculationContext context) {
        
        BigDecimal leftValue = left.evaluate(context);
        BigDecimal rightValue = right.evaluate(context);
        
        switch (operator) {
            case ADD:
                return leftValue.add(rightValue);
            case SUBTRACT:
                return leftValue.subtract(rightValue);
            case MULTIPLY:
                return leftValue.multiply(rightValue);
            case DIVIDE:
                return leftValue.divide(rightValue, 2, RoundingMode.HALF_UP);
            case MODULO:
                return leftValue.remainder(rightValue);
            case POWER:
                return leftValue.pow(rightValue.intValue());
            default:
                throw new UnsupportedOperationException("Unknown operator: " + operator);
        }
    }
}

/**
 * 函数调用节点
 */
public class FunctionCallNode extends ASTNode {
    
    private final String functionName;
    private final List<ASTNode> arguments;
    
    public FunctionCallNode(String functionName, List<ASTNode> arguments) {
        this.functionName = functionName;
        this.arguments = arguments;
    }
    
    @Override
    public BigDecimal evaluate(CalculationContext context) {
        
        // 获取函数参数值
        List<BigDecimal> argValues = new ArrayList<>();
        for (ASTNode arg : arguments) {
            argValues.add(arg.evaluate(context));
        }
        
        // 根据函数名调用对应函数
        BuiltinFunction function = BuiltinFunction.getFunction(functionName);
        return function.apply(argValues);
    }
}

/**
 * 内置函数
 */
public enum BuiltinFunction {
    
    SUM("sum", args -> args.stream().reduce(BigDecimal.ZERO, BigDecimal::add)),
    AVG("avg", args -> {
        if (args.isEmpty()) return BigDecimal.ZERO;
        return args.stream().reduce(BigDecimal.ZERO, BigDecimal::add)
            .divide(BigDecimal.valueOf(args.size()), 2, RoundingMode.HALF_UP);
    }),
    MAX("max", args -> args.stream().max(BigDecimal::compareTo).orElse(BigDecimal.ZERO)),
    MIN("min", args -> args.stream().min(BigDecimal::compareTo).orElse(BigDecimal.ZERO)),
    ROUND("round", args -> args.get(0).setScale(args.get(1).intValue(), RoundingMode.HALF_UP)),
    IF("if", args -> {
        // IF(condition, trueValue, falseValue)
        if (args.get(0).compareTo(BigDecimal.ZERO) != 0) {
            return args.get(1);
        } else {
            return args.get(2);
        }
    });
    
    private final String name;
    private final Function<List<BigDecimal>, BigDecimal> function;
    
    BuiltinFunction(String name, Function<List<BigDecimal>, BigDecimal> function) {
        this.name = name;
        this.function = function;
    }
    
    public static BuiltinFunction getFunction(String name) {
        return Arrays.stream(values())
            .filter(f -> f.name.equals(name))
            .findFirst()
            .orElseThrow(() -> new UnsupportedOperationException("Unknown function: " + name));
    }
    
    public BigDecimal apply(List<BigDecimal> args) {
        return function.apply(args);
    }
}

四、个税计算引擎实现

4.1 累计预扣法个税计算

/**
 * 个税计算引擎
 */
@Component
public class PersonalTaxCalculator {
    
    /**
     * 2024年个人所得税税率表
     */
    private static final TaxBracket[] TAX_BRACKETS = {
        new TaxBracket(0, 3000, 0.03, 0),
        new TaxBracket(3000, 12000, 0.10, 210),
        new TaxBracket(12000, 25000, 0.20, 1410),
        new TaxBracket(25000, 35000, 0.25, 2660),
        new TaxBracket(35000, 55000, 0.30, 4410),
        new TaxBracket(55000, 80000, 0.35, 7160),
        new TaxBracket(80000, Double.MAX_VALUE, 0.45, 15160)
    };
    
    /**
     * 计算月度个税(累计预扣法)
     */
    public MonthlyTaxResult calculateMonthlyTax(MonthlyTaxCalculationRequest request) {
        
        // 1. 获取本年累计已预缴税额
        BigDecimal cumulativeTaxPaid = request.getCumulativeTaxPaid();
        
        // 2. 计算本月应纳税所得额
        BigDecimal taxableIncome = calculateTaxableIncome(request);
        
        // 3. 计算本年累计应纳税所得额
        BigDecimal cumulativeTaxableIncome = request.getCumulativeTaxableIncome()
            .add(taxableIncome);
        
        // 4. 计算本年累计应纳税额
        BigDecimal cumulativeTax = calculateCumulativeTax(cumulativeTaxableIncome);
        
        // 5. 计算本月应补(退)税额
        BigDecimal monthlyTax = cumulativeTax.subtract(cumulativeTaxPaid);
        
        // 如果本月应补税额为负,说明多缴了,本月不退税,结转下月
        if (monthlyTax.compareTo(BigDecimal.ZERO) < 0) {
            monthlyTax = BigDecimal.ZERO;
        }
        
        return MonthlyTaxResult.builder()
            .taxableIncome(taxableIncome)
            .cumulativeTaxableIncome(cumulativeTaxableIncome)
            .cumulativeTax(cumulativeTax)
            .cumulativeTaxPaid(cumulativeTaxPaid)
            .monthlyTax(monthlyTax)
            .build();
    }
    
    /**
     * 计算应纳税所得额
     */
    private BigDecimal calculateTaxableIncome(MonthlyTaxCalculationRequest request) {
        
        // 1. 收入减去免税收入
        BigDecimal income = request.getGrossSalary()
            .subtract(request.getExemptIncome());
        
        // 2. 减去社保公积金个人缴纳部分
        income = income.subtract(request.getPersonalSocial());
        income = income.subtract(request.getHousingFund());
        
        // 3. 减去专项附加扣除
        income = income.subtract(request.getSpecialDeduction());
        
        // 4. 减去起征点(5000元/月)
        income = income.subtract(BigDecimal.valueOf(5000));
        
        // 5. 如果小于0,不用纳税
        return income.max(BigDecimal.ZERO);
    }
    
    /**
     * 计算累计应纳税额(使用速算扣除数法)
     */
    private BigDecimal calculateCumulativeTax(BigDecimal cumulativeTaxableIncome) {
        
        // 1. 找到适用税率级数
        for (TaxBracket bracket : TAX_BRACKETS) {
            if (cumulativeTaxableIncome.compareTo(bracket.getUpperBound()) <= 0) {
                // 找到适用税率,计算税额
                return cumulativeTaxableIncome.multiply(bracket.getRate())
                    .subtract(bracket.getQuickDeduction());
            }
        }
        
        return BigDecimal.ZERO;
    }
    
    /**
     * 税率表数据结构
     */
    @Data
    @AllArgsConstructor
    private static class TaxBracket {
        /**
         * 下限
         */
        private final double lowerBound;
        
        /**
         * 上限
         */
        private final double upperBound;
        
        /**
         * 税率
         */
        private final double rate;
        
        /**
         * 速算扣除数
         */
        private final int quickDeduction;
    }
}

4.2 专项附加扣除计算

/**
 * 专项附加扣除服务
 */
@Service
public class SpecialDeductionService {
    
    /**
     * 专项附加扣除项目
     */
    private static final String[] DEDUCTION_ITEMS = {
        "子女教育",
        "继续教育",
        "大病医疗",
        "住房贷款利息",
        "住房租金",
        "赡养老人",
        "婴幼儿照护"
    };
    
    /**
     * 计算专项附加扣除总额
     */
    public BigDecimal calculateTotalDeduction(Long employeeId, String month) {
        
        BigDecimal total = BigDecimal.ZERO;
        
        // 1. 获取员工专项附加扣除配置
        SpecialDeductionConfig config = configRepository.findByEmployeeId(employeeId);
        
        // 2. 累加各项扣除金额
        for (String item : DEDUCTION_ITEMS) {
            BigDecimal deduction = config.getDeduction(item, month);
            total = total.add(deduction);
        }
        
        // 3. 验证扣除上限
        total = total.min(BigDecimal.valueOf(1000)); // 每月上限1000元
        
        return total;
    }
    
    /**
     * 子女教育专项扣除
     */
    public BigDecimal calculateChildrenEducation(Long employeeId, String month) {
        
        // 1. 获取子女教育扣除配置
        ChildrenEducationConfig config = configRepository
            .findChildrenEducationConfig(employeeId);
        
        if (config == null) {
            return BigDecimal.ZERO;
        }
        
        // 2. 计算扣除金额
        // 每个子女每月1000元(2000元/月/每孩),最多扣3个子女
        int childCount = config.getChildrenCount();
        childCount = Math.min(childCount, 3); // 最多3个
        
        BigDecimal deduction = BigDecimal.valueOf(childCount * 1000);
        
        // 3. 判断是按月扣除还是按年扣除
        if (config.isYearlyDeduction()) {
            // 按年扣除,每月扣除1/12
            deduction = deduction.divide(BigDecimal.valueOf(12), 2, RoundingMode.HALF_UP);
        }
        
        return deduction;
    }
    
    /**
     * 住房贷款利息专项扣除
     */
    public BigDecimal calculateHousingLoanInterest(Long employeeId, String month) {
        
        // 1. 获取住房贷款扣除配置
        HousingLoanConfig config = configRepository
            .findHousingLoanConfig(employeeId);
        
        if (config == null || !config.isJointDeduction()) {
            return BigDecimal.ZERO;
        }
        
        // 2. 获取当月还款金额
        BigDecimal monthlyPayment = housingLoanService.getMonthlyPayment(
            config.getLoanContractNo(), 
            month
        );
        
        // 3. 计算利息部分(假设利息占还款额的50%)
        // 实际应该根据贷款合同计算
        BigDecimal interest = monthlyPayment.multiply(BigDecimal.valueOf(0.5));
        
        // 4. 每月扣除上限1000元
        return interest.min(BigDecimal.valueOf(1000));
    }
}

4.3 年终奖个税计算

/**
 * 年终奖个税计算服务
 */
@Service
public class YearEndBonusTaxService {
    
    /**
     * 计算年终奖个税
     */
    public YearEndBonusTaxResult calculateBonusTax(YearEndBonusRequest request) {
        
        BigDecimal bonusAmount = request.getBonusAmount();
        
        // 1. 将年终奖金额除以12,按月换算薪资
        BigDecimal monthlyEquivalent = bonusAmount.divide(
            BigDecimal.valueOf(12),
            2,
            RoundingMode.HALF_UP
        );
        
        // 2. 扣除起征点
        BigDecimal taxableAmount = monthlyEquivalent.subtract(BigDecimal.valueOf(5000));
        
        // 3. 如果小于等于0,不纳税
        if (taxableAmount.compareTo(BigDecimal.ZERO) <= 0) {
            return YearEndBonusTaxResult.builder()
                .taxAmount(BigDecimal.ZERO)
                .effectiveTaxRate(0.0)
                .build();
        }
        
        // 4. 查找适用税率
        TaxBracket bracket = findTaxBracket(taxableAmount);
        
        // 5. 计算个税
        BigDecimal taxAmount = taxableAmount.multiply(bracket.getRate())
            .subtract(bracket.getQuickDeduction());
        
        // 6. 乘以12还原为全年个税
        taxAmount = taxAmount.multiply(BigDecimal.valueOf(12));
        
        return YearEndBonusTaxResult.builder()
            .taxAmount(taxAmount.setScale(2, RoundingMode.HALF_UP))
            .effectiveTaxRate(bracket.getRate())
            .build();
    }
    
    /**
     * 查找适用税率级数
     */
    private TaxBracket findTaxBracket(BigDecimal amount) {
        
        for (TaxBracket bracket : TAX_BRACKETS) {
            if (amount.compareTo(bracket.getUpperBound()) <= 0) {
                return bracket;
            }
        }
        
        return TAX_BRACKETS[TAX_BRACKETS.length - 1];
    }
}

五、社保公积金计算

5.1 社保公积金计算

/**
 * 社保公积金计算服务
 */
@Service
public class SocialSecurityCalculator {
    
    /**
     * 计算个人社保
     */
    public SocialSecurityResult calculatePersonalSocial(
        PersonalSocialCalculationRequest request
    ) {
        
        // 1. 获取社保配置
        SocialSecurityConfig config = configRepository.getConfig(request.getTenantId());
        
        // 2. 计算各项社保
        BigDecimal pension = calculatePension(request.getBaseSalary(), config);
        BigDecimal medical = calculateMedical(request.getBaseSalary(), config);
        BigDecimal unemployment = calculateUnemployment(request.getBaseSalary(), config);
        
        // 3. 计算社保总额
        BigDecimal total = pension.add(medical).add(unemployment);
        
        return SocialSecurityResult.builder()
            .pension(pension)
            .medical(medical)
            .unemployment(unemployment)
            .total(total)
            .build();
    }
    
    /**
     * 计算养老保险
     */
    private BigDecimal calculatePension(BigDecimal baseSalary, SocialSecurityConfig config) {
        // 个人缴费比例:8%
        return baseSalary.multiply(config.getPensionPersonalRate())
            .setScale(2, RoundingMode.HALF_UP);
    }
    
    /**
     * 计算医疗保险
     */
    private BigDecimal calculateMedical(BigDecimal baseSalary, SocialSecurityConfig config) {
        // 个人缴费比例:2%
        return baseSalary.multiply(config.getMedicalPersonalRate())
            .setScale(2, RoundingMode.HALF_UP);
    }
    
    /**
     * 计算失业保险
     */
    private BigDecimal calculateUnemployment(BigDecimal baseSalary, SocialSecurityConfig config) {
        // 个人缴费比例:1%
        return baseSalary.multiply(config.getUnemploymentPersonalRate())
            .setScale(2, RoundingMode.HALF_UP);
    }
    
    /**
     * 计算住房公积金
     */
    public HousingFundResult calculateHousingFund(
        HousingFundCalculationRequest request
    ) {
        
        // 1. 获取公积金配置
        HousingFundConfig config = configRepository.getConfig(request.getTenantId());
        
        // 2. 获取缴费基数
        BigDecimal base = request.getBaseSalary();
        
        // 3. 确保缴费基数在上下限之间
        BigDecimal minBase = config.getMinBase();
        BigDecimal maxBase = config.getMaxBase();
        base = base.max(minBase).min(maxBase);
        
        // 4. 计算个人公积金(个人缴费比例:5%-12%)
        BigDecimal personalRate = config.getPersonalRate();
        BigDecimal personalHousingFund = base.multiply(personalRate)
            .setScale(2, RoundingMode.HALF_UP);
        
        // 5. 计算单位公积金(单位缴费比例:5%-12%)
        BigDecimal companyRate = config.getCompanyRate();
        BigDecimal companyHousingFund = base.multiply(companyRate)
            .setScale(2, RoundingMode.HALF_UP);
        
        return HousingFundResult.builder()
            .base(base)
            .personalRate(personalRate)
            .personalAmount(personalHousingFund)
            .companyRate(companyRate)
            .companyAmount(companyHousingFund)
            .totalAmount(personalHousingFund.add(companyHousingFund))
            .build();
    }
}

六、批量计算优化方案

6.1 分布式计算

/**
 * 分布式薪酬核算服务
 */
@Service
public class DistributedSalaryCalculationService {
    
    @Autowired
    private RocketMQTemplate rocketMQTemplate;
    
    /**
     * 批量薪酬核算(异步)
     */
    public void batchCalculateAsync(BatchCalculationRequest request) {
        
        // 1. 获取需要核算的员工列表
        List<Employee> employees = employeeService.findEmployeesByCondition(
            request.getTenantId(),
            request.getDepartmentId(),
            request.getQueryCondition()
        );
        
        // 2. 按批次分组(每批500人)
        List<List<Employee>> batches = Lists.partition(employees, 500);
        
        // 3. 发送计算任务到消息队列
        for (int i = 0; i < batches.size(); i++) {
            List<Employee> batch = batches.get(i);
            
            BatchCalculationTask task = BatchCalculationTask.builder()
                .tenantId(request.getTenantId())
                .calculationMonth(request.getCalculationMonth())
                .batchNo(i + 1)
                .totalBatches(batches.size())
                .employeeIds(batch.stream().map(Employee::getId).collect(Collectors.toList()))
                .build();
                
            rocketMQTemplate.asyncSend("salary-calculation-topic", task);
        }
        
        log.info("批量薪酬核算任务已提交,员工总数={}, 批次数={}", 
            employees.size(), 
            batches.size()
        );
    }
}

/**
 * 批量计算任务消费者
 */
@RocketMQMessageListener(
    topic = "salary-calculation-topic",
    consumerGroup = "salary-calculation-group",
    consumeThreadNumber = 20 // 并发20个线程
)
public class BatchCalculationTaskConsumer implements RocketMQListener<BatchCalculationTask> {
    
    @Autowired
    private SalaryCalculationEngine calculationEngine;
    
    @Autowired
    private SalaryCalculationRepository calculationRepository;
    
    @Override
    public void onMessage(BatchCalculationTask task) {
        
        log.info("开始执行批量薪酬核算,batchNo={}/{}, employeeCount={}", 
            task.getBatchNo(),
            task.getTotalBatches(),
            task.getEmployeeIds().size()
        );
        
        try {
            // 1. 为每个员工执行核算
            for (Long employeeId : task.getEmployeeIds()) {
                
                try {
                    // 执行核算
                    SalaryCalculationResult result = calculationEngine.calculate(
                        CalculationContext.builder()
                            .tenantId(task.getTenantId())
                            .employeeId(employeeId)
                            .calculationMonth(task.getCalculationMonth())
                            .build()
                    );
                    
                    // 保存核算结果
                    saveCalculationResult(result);
                    
                } catch (Exception e) {
                    // 单个员工核算失败,记录日志但继续处理其他员工
                    log.error("员工薪酬核算失败,employeeId={}", employeeId, e);
                    saveCalculationError(employeeId, e);
                }
            }
            
            log.info("批量薪酬核算完成,batchNo={}", task.getBatchNo());
            
        } catch (Exception e) {
            log.error("批量薪酬核算失败,batchNo={}", task.getBatchNo(), e);
            throw e;
        }
    }
    
    /**
     * 保存核算结果
     */
    private void saveCalculationResult(SalaryCalculationResult result) {
        
        SalaryCalculation calculation = SalaryCalculation.builder()
            .tenantId(result.getTenantId())
            .employeeId(result.getEmployeeId())
            .calculationMonth(result.getCalculationMonth())
            .grossSalary(result.getGrossSalary())
            .netSalary(result.getNetSalary())
            .salaryItems(result.getSalaryItemsJson())
            .status(CalculationStatus.CALCULATED)
            .calcTime(LocalDateTime.now())
            .build();
            
        calculationRepository.save(calculation);
    }
}

6.2 核算进度跟踪

/**
 * 核算进度跟踪服务
 */
@Service
public class CalculationProgressService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    /**
     * 更新核算进度
     */
    public void updateProgress(String batchNo, int currentCount, int totalCount) {
        
        // 计算进度百分比
        double progress = (double) currentCount / totalCount * 100;
        
        // 更新Redis
        String key = buildProgressKey(batchNo);
        CalculationProgress progress = CalculationProgress.builder()
            .batchNo(batchNo)
            .totalCount(totalCount)
            .currentCount(currentCount)
            .progress(progress)
            .updateTime(LocalDateTime.now())
            .build();
            
        redisTemplate.opsForValue().set(key, progress, 1, TimeUnit.HOURS);
    }
    
    /**
     * 获取核算进度
     */
    public CalculationProgress getProgress(String batchNo) {
        
        String key = buildProgressKey(batchNo);
        CalculationProgress progress = (CalculationProgress) redisTemplate.opsForValue().get(key);
        
        return progress;
    }
    
    /**
     * 构建进度Key
     */
    private String buildProgressKey(String batchNo) {
        return "salary:calculation:progress:" + batchNo;
    }
}

七、工资条安全发放

7.1 加密工资条

/**
 * 工资条加密服务
 */
@Service
public class SalarySlipEncryptionService {
    
    @Value("${salary.slip.encrypt.key}")
    private String encryptKey;
    
    /**
     * 生成加密工资条
     */
    public EncryptedSalarySlip generateEncryptedSlip(SalaryCalculation calculation) {
        
        // 1. 构建工资条数据
        SalarySlip salarySlip = SalarySlip.builder()
            .employeeId(calculation.getEmployeeId())
            .employeeName(calculation.getEmployeeName())
            .calculationMonth(calculation.getCalculationMonth())
            .grossSalary(calculation.getGrossSalary())
            .netSalary(calculation.getNetSalary())
            .items(parseSalaryItems(calculation.getSalaryItems()))
            .build();
        
        // 2. 转换为JSON
        String json = JsonUtils.toJson(salarySlip);
        
        // 3. AES加密
        String encryptedData = encrypt(json);
        
        // 4. 生成访问码
        String accessCode = generateAccessCode();
        
        // 5. 保存加密工资条
        EncryptedSalarySlip encryptedSlip = EncryptedSalarySlip.builder()
            .tenantId(calculation.getTenantId())
            .employeeId(calculation.getEmployeeId())
            .calculationMonth(calculation.getCalculationMonth())
            .encryptedData(encryptedData)
            .accessCode(accessCode)
            .createTime(LocalDateTime.now())
            .expireTime(LocalDateTime.now().plusDays(90)) // 90天后过期
            .build();
            
        encryptedSlipRepository.save(encryptedSlip);
        
        return encryptedSlip;
    }
    
    /**
     * AES加密
     */
    private String encrypt(String plainText) {
        
        try {
            // 1. 生成随机IV
            byte[] iv = generateIV();
            
            // 2. 构建密钥
            SecretKeySpec keySpec = new SecretKeySpec(
                encryptKey.getBytes(StandardCharsets.UTF_8),
                "AES"
            );
            
            // 3. 加密
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            cipher.init(Cipher.ENCRYPT_MODE, keySpec, new IvParameterSpec(iv));
            byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
            
            // 4. 组合IV和密文
            byte[] combined = new byte[iv.length + encrypted.length];
            System.arraycopy(iv, 0, combined, 0, iv.length);
            System.arraycopy(encrypted, 0, combined, iv.length, encrypted.length);
            
            // 5. Base64编码
            return Base64.getEncoder().encodeToString(combined);
            
        } catch (Exception e) {
            throw new BusinessException("工资条加密失败", e);
        }
    }
    
    /**
     * 生成随机IV
     */
    private byte[] generateIV() {
        byte[] iv = new byte[16];
        new SecureRandom().nextBytes(iv);
        return iv;
    }
    
    /**
     * 生成访问码
     */
    private String generateAccessCode() {
        RandomString randomString = new RandomStringGenerator.Builder()
            .length(8)
            .build();
        return randomString.generate();
    }
}

/**
 * 工资条查看服务
 */
@Service
public class SalarySlipViewService {
    
    @Autowired
    private SalarySlipEncryptionService encryptionService;
    
    /**
     * 查看工资条(需密码验证)
     */
    public SalarySlip viewSalarySlip(String accessCode, String password) {
        
        // 1. 验证访问码
        EncryptedSalarySlip encryptedSlip = encryptedSlipRepository.findByAccessCode(accessCode);
        if (encryptedSlip == null) {
            throw new BusinessException("工资条不存在或已过期");
        }
        
        // 2. 验证是否过期
        if (encryptedSlip.getExpireTime().isBefore(LocalDateTime.now())) {
            throw new BusinessException("工资条已过期");
        }
        
        // 3. 验证密码
        if (!validatePassword(encryptedSlip.getEmployeeId(), password)) {
            // 记录失败尝试
            recordFailedAttempt(encryptedSlip.getEmployeeId(), accessCode);
            throw new BusinessException("密码错误");
        }
        
        // 4. 解密工资条
        SalarySlip salarySlip = decryptSalarySlip(encryptedSlip);
        
        // 5. 记录查看日志
        recordViewLog(encryptedSlip.getEmployeeId(), accessCode);
        
        // 6. 检查查看次数限制
        checkViewLimit(encryptedSlip);
        
        return salarySlip;
    }
    
    /**
     * 解密工资条
     */
    private SalarySlip decryptSalarySlip(EncryptedSalarySlip encryptedSlip) {
        
        try {
            // 1. Base64解码
            byte[] combined = Base64.getDecoder().decode(encryptedSlip.getEncryptedData());
            
            // 2. 分离IV和密文
            byte[] iv = new byte[16];
            byte[] encrypted = new byte[combined.length - 16];
            System.arraycopy(combined, 0, iv, 0, iv.length);
            System.arraycopy(combined, iv.length, encrypted, 0, encrypted.length);
            
            // 3. 解密
            SecretKeySpec keySpec = new SecretKeySpec(
                encryptKey.getBytes(StandardCharsets.UTF_8),
                "AES"
            );
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            cipher.init(Cipher.DECRYPT_MODE, keySpec, new IvParameterSpec(iv));
            byte[] decrypted = cipher.doFinal(encrypted);
            
            // 4. 转换为对象
            return JsonUtils.fromJson(new String(decrypted, StandardCharsets.UTF_8), SalarySlip.class);
            
        } catch (Exception e) {
            throw new BusinessException("工资条解密失败", e);
        }
    }
    
    /**
     * 检查查看次数限制
     */
    private void checkViewLimit(EncryptedSalarySlip encryptedSlip) {
        
        // 获取查看次数
        int viewCount = encryptedSlip.getViewCount() + 1;
        
        // 每月最多查看3次
        if (viewCount > 3) {
            throw new BusinessException("本月查看次数已用完,每月最多查看3次");
        }
        
        // 更新查看次数
        encryptedSlip.setViewCount(viewCount);
        encryptedSlipRepository.save(encryptedSlip);
    }
}

八、系统监控与数据一致性

8.1 数据一致性保障

/**
 * 薪酬核算数据一致性服务
 */
@Service
public class SalaryDataConsistencyService {
    
    /**
     * 校验数据一致性
     */
    public ConsistencyCheckResult checkConsistency(ConsistencyCheckRequest request) {
        
        List<ConsistencyIssue> issues = new ArrayList<>();
        
        // 1. 校验应发工资一致性
        issues.addAll(checkGrossSalaryConsistency(request));
        
        // 2. 校验个税计算一致性
        issues.addAll(checkPersonalTaxConsistency(request));
        
        // 3. 校验社保公积金一致性
        issues.addAll(checkSocialSecurityConsistency(request));
        
        // 4. 校验实发工资一致性
        issues.addAll(checkNetSalaryConsistency(request));
        
        return ConsistencyCheckResult.builder()
            .issues(issues)
            .passed(issues.isEmpty())
            .build();
    }
    
    /**
     * 校验应发工资一致性
     */
    private List<ConsistencyIssue> checkGrossSalaryConsistency(ConsistencyCheckRequest request) {
        
        List<ConsistencyIssue> issues = new ArrayList<>();
        
        // 获取所有核算记录
        List<SalaryCalculation> calculations = calculationRepository
            .findByTenantAndMonth(request.getTenantId(), request.getCalculationMonth());
        
        for (SalaryCalculation calculation : calculations) {
            
            // 1. 重新计算应发工资
            SalaryCalculationResult recalculated = calculationEngine.recalculate(
                CalculationContext.builder()
                    .tenantId(calculation.getTenantId())
                    .employeeId(calculation.getEmployeeId())
                    .calculationMonth(calculation.getCalculationMonth())
                    .build()
            );
            
            // 2. 对比金额
            if (recalculated.getGrossSalary().compareTo(calculation.getGrossSalary()) != 0) {
                issues.add(ConsistencyIssue.builder()
                    .type(ConsistencyIssueType.GROSS_SALARY_MISMATCH)
                    .employeeId(calculation.getEmployeeId())
                    .expectedValue(recalculated.getGrossSalary())
                    .actualValue(calculation.getGrossSalary())
                    .message("应发工资不一致")
                    .build());
            }
        }
        
        return issues;
    }
}

九、总结与最佳实践

核心技术总结

技术点实现方案效果
薪资计算引擎规则引擎+公式解析零代码配置薪资方案
个税计算累计预扣法+速算扣除数符合最新税法
批量核算分布式计算+消息队列5000人/30分钟
数据加密AES加密+密码验证保障薪资数据安全
一致性保障校验机制+回滚重算数据准确率>99.9%

最佳实践建议

  1. 规则驱动:薪资项规则化配置,无需修改代码即可适配不同企业需求
  2. 异步处理:批量核算异步化处理,提升用户体验
  3. 数据安全:敏感数据加密存储,完善审计日志
  4. 性能优化:分布式计算,支持大规模薪酬核算
  5. 可追溯性:完整记录核算过程,支持数据回溯

参考资料

  1. 《个人所得税法》及其实施条例
  2. 《社会保险法》
  3. 《住房公积金管理条例》
  4. Spring Batch文档

相关链接


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值