3 天以前 b852254d90e7d1955db31db154193ec8ee84d8b3
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryCalculationServiceImpl.java
@@ -5,22 +5,30 @@
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.HrmSalaryCalculationPageReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmLeaveApplicationDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceSummaryDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.performanceappraise.HrmPerformanceAppraiseDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.performanceappraise.HrmPerformanceAppraiseResultDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmEmployeeSalaryDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmEmployeeSocialSecurityDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmLeaveTypeConfigDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmSalaryCalculationDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmSalaryPaymentDetailDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmSalaryPaymentDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmSalaryStructureDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmSocialSecuritySchemeDO;
import cn.iocoder.yudao.module.hrm.dal.mysql.approval.HrmLeaveApplicationMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.attendance.HrmAttendanceSummaryMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.performanceappraise.HrmPerformanceAppraiseMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.performanceappraise.HrmPerformanceAppraiseResultMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmEmployeeSalaryMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmEmployeeSocialSecurityMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmLeaveTypeConfigMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmSalaryCalculationMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmSalaryPaymentDetailMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmSalaryPaymentMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmSalaryStructureMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmSocialSecuritySchemeMapper;
import cn.iocoder.yudao.module.hrm.dal.redis.HrmNoRedisDAO;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
@@ -44,7 +52,7 @@
/**
 * 酬核算 Service 实现类
 *
 * @author 芋道源码
 * @author 超级管理员
 */
@Service
@Validated
@@ -62,6 +70,24 @@
    private static final BigDecimal DEFAULT_MEDICAL_PERSONAL_RATIO = new BigDecimal("0.02");
    private static final BigDecimal DEFAULT_UNEMPLOYMENT_PERSONAL_RATIO = new BigDecimal("0.005");
    private static final BigDecimal DEFAULT_HOUSING_FUND_PERSONAL_RATIO = new BigDecimal("0.12");
    /**
     * 加班工资倍率默认值(薪酬结构未配置时使用)
     */
    private static final BigDecimal DEFAULT_OVERTIME_PAY_RATIO = new BigDecimal("1.5");
    /**
     * 默认应出勤天数(无考勤汇总时的兜底值)
     */
    private static final int DEFAULT_WORK_DAYS = 22;
    /**
     * 绩效工资档级系数:1-优秀 2-良好 3-合格 4-待改进
     */
    private static final BigDecimal PERFORMANCE_GRADE_FACTOR_EXCELLENT = new BigDecimal("1.2");
    private static final BigDecimal PERFORMANCE_GRADE_FACTOR_GOOD = new BigDecimal("1.0");
    private static final BigDecimal PERFORMANCE_GRADE_FACTOR_PASS = new BigDecimal("0.8");
    private static final BigDecimal PERFORMANCE_GRADE_FACTOR_IMPROVE = new BigDecimal("0.6");
    @Resource
    private HrmSalaryCalculationMapper salaryCalculationMapper;
@@ -87,6 +113,14 @@
    private HrmSalaryPaymentMapper salaryPaymentMapper;
    @Resource
    private HrmSalaryPaymentDetailMapper salaryPaymentDetailMapper;
    @Resource
    private HrmAttendanceSummaryMapper attendanceSummaryMapper;
    @Resource
    private HrmPerformanceAppraiseMapper performanceAppraiseMapper;
    @Resource
    private HrmPerformanceAppraiseResultMapper performanceAppraiseResultMapper;
    @Resource
    private HrmSalaryStructureMapper salaryStructureMapper;
    @Override
    public PageResult<HrmSalaryCalculationDO> getSalaryCalculationPage(HrmSalaryCalculationPageReqVO pageReqVO) {
@@ -204,7 +238,21 @@
        BigDecimal mealAllowance = employeeSalary.getMealAllowance() != null ? employeeSalary.getMealAllowance() : BigDecimal.ZERO;
        BigDecimal transportAllowance = employeeSalary.getTransportAllowance() != null ? employeeSalary.getTransportAllowance() : BigDecimal.ZERO;
        BigDecimal otherAllowance = employeeSalary.getOtherAllowance() != null ? employeeSalary.getOtherAllowance() : BigDecimal.ZERO;
        // ===== 考勤链路:应出勤天数 / 加班时长 / 缺勤扣款 =====
        HrmAttendanceSummaryDO attendanceSummary = attendanceSummaryMapper.selectByUserIdAndYearMonth(employeeId, period);
        int workDays = attendanceSummary != null && attendanceSummary.getWorkDays() != null
                ? attendanceSummary.getWorkDays() : DEFAULT_WORK_DAYS;
        BigDecimal overtimeHours = attendanceSummary != null && attendanceSummary.getOvertimeHours() != null
                ? attendanceSummary.getOvertimeHours() : BigDecimal.ZERO;
        // ===== 加班工资 = 时薪 × 加班时长 × 倍率(时薪 = 基本工资 ÷ (应出勤天 × 8)) =====
        BigDecimal overtimePay = BigDecimal.ZERO;
        if (overtimeHours.compareTo(BigDecimal.ZERO) > 0 && workDays > 0) {
            BigDecimal hourlyRate = baseSalary.divide(BigDecimal.valueOf(workDays * 8L), 4, RoundingMode.HALF_UP);
            overtimePay = hourlyRate.multiply(overtimeHours).multiply(getOvertimePayRatio(employeeSalary, employee))
                    .setScale(2, RoundingMode.HALF_UP);
        }
        // ===== 绩效工资:按当月绩效结果档级系数折算(无结果按全额) =====
        performanceSalary = calculatePerformanceSalary(performanceSalary, employeeId, period);
        // 应发工资总额
        BigDecimal totalIncome = baseSalary.add(performanceSalary).add(mealAllowance)
@@ -238,8 +286,8 @@
        // 公积金扣除 = 基数 × 公积金个人比例
        BigDecimal housingFundDeduction = housingFundBase.multiply(housingFundPersonalRatio).setScale(2, RoundingMode.HALF_UP);
        // 计算请假扣款
        BigDecimal leaveDeduction = calculateLeaveDeduction(employee, period, baseSalary);
        // 计算缺勤扣款(考勤口径:缺勤天数 = 应出勤 − 实际出勤;无考勤汇总时回退到请假申请口径)
        BigDecimal leaveDeduction = calculateDeduction(attendanceSummary, baseSalary, workDays, employee, period);
        // 计算个税扣除
        // 应纳税所得额 = 应发工资 - 社保扣除 - 公积金扣除 - 起征点(5000)
@@ -276,13 +324,66 @@
                .otherDeduction(otherDeduction)
                .leaveDeduction(leaveDeduction)
                .actualSalary(actualSalary)
                .workDays(22) // 默认工作日
                .overtimeHours(BigDecimal.ZERO)
                .workDays(workDays) // 动态应出勤天数(来自考勤汇总)
                .overtimeHours(overtimeHours) // 加班时长(来自考勤汇总)
                .status(0) // 待确认
                .build();
    }
    /**
     * 计算缺勤扣款:优先按考勤汇总(缺勤天数 = 应出勤 − 实际出勤);无汇总时回退到请假申请口径
     */
    private BigDecimal calculateDeduction(HrmAttendanceSummaryDO attendanceSummary, BigDecimal baseSalary,
                                          int workDays, HrmEmployeeDO employee, String period) {
        if (attendanceSummary != null && workDays > 0 && attendanceSummary.getActualDays() != null) {
            int absentDays = Math.max(0, workDays - attendanceSummary.getActualDays());
            if (absentDays == 0) {
                return BigDecimal.ZERO;
            }
            BigDecimal daySalary = baseSalary.divide(BigDecimal.valueOf(workDays), 4, RoundingMode.HALF_UP);
            return daySalary.multiply(BigDecimal.valueOf(absentDays)).setScale(2, RoundingMode.HALF_UP);
        }
        return calculateLeaveDeduction(employee, period, baseSalary);
    }
    /**
     * 计算绩效工资:按当月已完成且匹配核算周期的月度绩效考核结果的档级系数折算
     */
    private BigDecimal calculatePerformanceSalary(BigDecimal performanceSalary, Long employeeId, String period) {
        HrmPerformanceAppraiseDO appraise = performanceAppraiseMapper.selectMonthlyCompletedByPeriod(period);
        if (appraise == null) {
            return performanceSalary;
        }
        HrmPerformanceAppraiseResultDO result = performanceAppraiseResultMapper
                .selectByAppraiseIdAndEmployeeId(appraise.getId(), employeeId);
        if (result == null || result.getGrade() == null) {
            return performanceSalary;
        }
        BigDecimal factor = switch (result.getGrade()) {
            case 1 -> PERFORMANCE_GRADE_FACTOR_EXCELLENT;
            case 2 -> PERFORMANCE_GRADE_FACTOR_GOOD;
            case 3 -> PERFORMANCE_GRADE_FACTOR_PASS;
            case 4 -> PERFORMANCE_GRADE_FACTOR_IMPROVE;
            default -> BigDecimal.ONE;
        };
        return performanceSalary.multiply(factor).setScale(2, RoundingMode.HALF_UP);
    }
    /**
     * 获取加班工资倍率(薪酬结构未配置时默认 1.5)
     */
    private BigDecimal getOvertimePayRatio(HrmEmployeeSalaryDO employeeSalary, HrmEmployeeDO employee) {
        Long structureId = employeeSalary.getSalaryStructureId() != null
                ? employeeSalary.getSalaryStructureId() : employee.getSalaryStructureId();
        if (structureId == null) {
            return DEFAULT_OVERTIME_PAY_RATIO;
        }
        HrmSalaryStructureDO structure = salaryStructureMapper.selectById(structureId);
        return structure != null && structure.getOvertimePayRatio() != null
                ? structure.getOvertimePayRatio() : DEFAULT_OVERTIME_PAY_RATIO;
    }
    /**
     * 计算请假扣款
     *
     * @param employee   员工信息