yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryCalculationServiceImpl.java
@@ -281,7 +281,7 @@
                .add(fullAttendanceBonus)
                .add(mealAllowance).add(transportAllowance).add(otherAllowance).add(overtimePay);
        // 获取社保基数:员工社保档案 > 员工基本工资
        // 社保基数:只取员工社保档案里配置的缴纳基数;未配置时用基本工资
        BigDecimal socialSecurityBase = (socialSecurity != null && socialSecurity.getSocialSecurityBase() != null)
                ? socialSecurity.getSocialSecurityBase() : baseSalary;
@@ -292,17 +292,31 @@
                ? scheme.getMedicalPersonalRatio() : DEFAULT_MEDICAL_PERSONAL_RATIO;
        BigDecimal unemploymentPersonalRatio = (scheme != null && scheme.getUnemploymentPersonalRatio() != null)
                ? scheme.getUnemploymentPersonalRatio() : DEFAULT_UNEMPLOYMENT_PERSONAL_RATIO;
        // 大病医疗 / 长期护理的个人比例:方案里可能为空,为空视为 0(不扣)
        BigDecimal seriousIllnessPersonalRatio = (scheme != null && scheme.getSeriousIllnessPersonalRatio() != null)
                ? scheme.getSeriousIllnessPersonalRatio() : BigDecimal.ZERO;
        BigDecimal longTermCarePersonalRatio = (scheme != null && scheme.getLongTermCarePersonalRatio() != null)
                ? scheme.getLongTermCarePersonalRatio() : BigDecimal.ZERO;
        // 公积金个人比例:只取方案配置值,不再回退默认比例。
        // 否则「公司没配公积金方案」时会被按默认 12% 扣款(这就是客户反馈的问题)。
        BigDecimal housingFundPersonalRatio = scheme != null ? scheme.getHousingFundPersonalRatio() : null;
        // 社保扣除 = 基数 × (养老个人比例 + 医疗个人比例 + 失业个人比例)
        BigDecimal socialSecurityDeduction = socialSecurityBase
                .multiply(pensionPersonalRatio)
                .add(socialSecurityBase.multiply(medicalPersonalRatio))
                .add(socialSecurityBase.multiply(unemploymentPersonalRatio))
                .setScale(2, RoundingMode.HALF_UP);
        // 社保扣除:必须「员工配置了社保档案」才扣款。
        // 之前无论有没有社保档案都按 基数 × 默认比例(0.08+0.02+0.005=10.5%) 扣款,
        // 导致 102 个没加社保的员工被扣了 204.75(= 基本工资 1950 × 10.5%)。
        // 个人应缴 = 养老 + 医疗 + 失业 + 大病医疗 + 长期护理。
        // 注意:原来漏算了后两项,使得「兵团五险」与「地方五险」两个方案算出来的扣款完全一样。
        BigDecimal socialSecurityDeduction = BigDecimal.ZERO;
        if (socialSecurity != null) {
            BigDecimal personalTotalRatio = pensionPersonalRatio
                    .add(medicalPersonalRatio)
                    .add(unemploymentPersonalRatio)
                    .add(seriousIllnessPersonalRatio)
                    .add(longTermCarePersonalRatio);
            socialSecurityDeduction = socialSecurityBase.multiply(personalTotalRatio)
                    .setScale(2, RoundingMode.HALF_UP);
        }
        // 公积金扣除:必须同时满足「方案配置了公积金比例 > 0」且「员工档案配置了公积金基数 > 0」才扣款,
        // 任一不满足都视为公司未缴纳公积金,扣款为 0。
@@ -314,9 +328,14 @@
                    .setScale(2, RoundingMode.HALF_UP);
        }
        // 计算请假扣款
        // 计算缺勤扣款(请假 + 旷工)
        BigDecimal leaveDeduction = calculateAttendanceDeduction(employee, period, baseSalary, workDays, actualDays);
        // 缺勤扣款(请假 + 旷工)
        // 计薪基数 = 基本工资 + 岗位工资 + 工龄工资 + 绩效工资(这些与出勤天数直接相关);
        // 不含加班费/各项补贴/全勤奖(这些不随出勤天数折算)。
        // 注意:不能用"基本工资"当基数——很多员工工资主体是岗位工资,否则实出 0 天时只扣掉基本工资,岗位工资照发。
        BigDecimal attendanceBaseSalary = baseSalary
                .add(postSalary).add(senioritySalary).add(performanceSalary);
        BigDecimal leaveDeduction = calculateAttendanceDeduction(
                employee, period, attendanceBaseSalary, workDays, actualDays);
        // 计算个税扣除
        // 应纳税所得额 = 应发工资 - 社保扣除 - 公积金扣除 - 起征点(5000)
@@ -376,18 +395,18 @@
     *     <li>扣款总额封顶为基本工资,避免出现负工资。</li>
     * </ol>
     *
     * @param employee   员工信息
     * @param period     核算周期(如 2026-02)
     * @param baseSalary 基本工资
     * @param workDays   应出天数(排班累计,无排班时传 22)
     * @param actualDays 实出天数(取自考勤汇总;null 表示该月未生成汇总)
     * @return 缺勤扣款金额
     * @param employee       员工信息
     * @param period         核算周期(如 2026-02)
     * @param attendanceBase 计薪基数 = 基本 + 岗位 + 工龄 + 绩效(与出勤直接相关的固定工资)
     * @param workDays       应出天数(排班累计,无排班时传 22)
     * @param actualDays     实出天数(取自考勤汇总;null 表示该月未生成汇总)
     * @return 缺勤扣款金额(封顶为计薪基数)
     */
    private BigDecimal calculateAttendanceDeduction(HrmEmployeeDO employee, String period,
                                                    BigDecimal baseSalary, Integer workDays, Integer actualDays) {
                                                    BigDecimal attendanceBase, Integer workDays, Integer actualDays) {
        int shouldDays = (workDays != null && workDays > 0) ? workDays : 22;
        // 日工资 = 基本工资 / 应出天数(保留 4 位,最后统一保留 2 位)
        BigDecimal dailySalary = baseSalary.divide(new BigDecimal(shouldDays), 4, RoundingMode.HALF_UP);
        // 日工资 = 计薪基数 / 应出天数(保留 4 位,最后统一保留 2 位)
        BigDecimal dailySalary = attendanceBase.divide(new BigDecimal(shouldDays), 4, RoundingMode.HALF_UP);
        BigDecimal totalDeduction = BigDecimal.ZERO;
        BigDecimal leaveDays = BigDecimal.ZERO;
@@ -428,7 +447,7 @@
        // 3. 封顶:扣款不超过基本工资
        BigDecimal result = totalDeduction.setScale(2, RoundingMode.HALF_UP);
        BigDecimal cap = baseSalary.setScale(2, RoundingMode.HALF_UP);
        BigDecimal cap = attendanceBase.setScale(2, RoundingMode.HALF_UP);
        return result.compareTo(cap) > 0 ? cap : result;
    }
@@ -766,16 +785,22 @@
     * 重算「缺勤扣款」与「实发工资」
     *
     * <p>核算单 userId 存的是员工ID(hrm_employee.id)。按「应出天数 / 实出天数 + 请假单」重算
     * 缺勤扣款(请假按配置比例扣、旷工按 100% 扣、封顶基本工资),再重算实发工资(不可为负)。
     * 查不到员工或核算周期时,保留原缺勤扣款,只重算实发。
     * 缺勤扣款(请假按配置比例扣、旷工按 100% 扣、封顶计薪基数),再重算实发工资(不可为负)。
     *
     * <p>计薪基数 = 基本 + 岗位 + 工龄 + 绩效(与出勤直接相关的固定工资部分),
     * 不能用基本工资单独封顶,否则工资主体为岗位工资的员工实出 0 天时仍会发放大部分工资。
     */
    private void recalcAttendanceDeduction(HrmSalaryCalculationDO calculation) {
        HrmEmployeeDO employee = calculation.getUserId() != null
                ? employeeMapper.selectById(calculation.getUserId()) : null;
        if (employee != null && calculation.getPeriod() != null) {
            BigDecimal attendanceBaseSalary = nvl(calculation.getBaseSalary())
                    .add(nvl(calculation.getPostSalary()))
                    .add(nvl(calculation.getSenioritySalary()))
                    .add(nvl(calculation.getPerformanceSalary()));
            BigDecimal deduction = calculateAttendanceDeduction(
                    employee, calculation.getPeriod(),
                    nvl(calculation.getBaseSalary()),
                    attendanceBaseSalary,
                    calculation.getWorkDays(),
                    calculation.getActualDays());
            calculation.setLeaveDeduction(deduction);