src/main/java/com/ruoyi/staff/service/impl/SchemeApplicableStaffServiceImpl.java
@@ -11,22 +11,22 @@
import com.ruoyi.project.system.domain.SysDept;
import com.ruoyi.project.system.domain.SysUser;
import com.ruoyi.project.system.mapper.SysDeptMapper;
import com.ruoyi.project.system.mapper.SysUserDeptMapper;
import com.ruoyi.project.system.mapper.SysUserMapper;
import com.ruoyi.staff.controller.TaxCalculator;
import com.ruoyi.staff.mapper.SchemeApplicableStaffMapper;
import com.ruoyi.staff.mapper.SchemeInsuranceDetailMapper;
import com.ruoyi.staff.mapper.StaffOnJobMapper;
import com.ruoyi.staff.pojo.SchemeApplicableStaff;
import com.ruoyi.staff.pojo.SchemeInsuranceDetail;
import com.ruoyi.staff.pojo.StaffOnJob;
import com.ruoyi.staff.mapper.*;
import com.ruoyi.staff.pojo.*;
import com.ruoyi.staff.service.SchemeApplicableStaffService;
import org.springframework.beans.factory.annotation.Autowired;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -41,28 +41,20 @@
 * @since 2026-03-05 11:50:17
 */
@Service
@RequiredArgsConstructor
public class SchemeApplicableStaffServiceImpl extends ServiceImpl<SchemeApplicableStaffMapper, SchemeApplicableStaff> implements SchemeApplicableStaffService {
    @Autowired
    private SchemeApplicableStaffMapper schemeApplicableStaffMapper;
    @Autowired
    private SchemeInsuranceDetailMapper schemeInsuranceDetailMapper;
    @Autowired
    private SysUserDeptMapper sysUserDeptMapper;
    @Autowired
    private SysUserMapper sysUserMapper;
    @Autowired
    private SysDeptMapper sysDeptMapper;
    @Autowired
    private StaffOnJobMapper staffOnJobMapper;
    @Autowired
    private SalesLedgerProductionAccountingService salesLedgerProductionAccountingService;
    private final SchemeApplicableStaffMapper schemeApplicableStaffMapper;
    private final SchemeInsuranceDetailMapper schemeInsuranceDetailMapper;
    private final SysUserMapper sysUserMapper;
    private final SysDeptMapper sysDeptMapper;
    private final StaffOnJobMapper staffOnJobMapper;
    private final PersonalShiftMapper personalShiftMapper;
    private final PersonalAttendanceLocationConfigMapper personalAttendanceLocationConfigMapper;
    private final SalesLedgerProductionAccountingService salesLedgerProductionAccountingService;
    private final SubsidyConfigurationMapper subsidyConfigurationMapper;
    private final StaffSalaryMainMapper staffSalaryMainMapper;
    private final StaffSalaryDetailMapper staffSalaryDetailMapper;
    @Override
@@ -73,6 +65,7 @@
                schemeApplicableStaffLambdaQueryWrapper.like(SchemeApplicableStaff::getTitle, schemeApplicableStaff.getTitle());
            }
        }
        schemeApplicableStaffLambdaQueryWrapper.orderByDesc(SchemeApplicableStaff::getId);
        Page<SchemeApplicableStaff> page1 = schemeApplicableStaffMapper.selectPage(page, schemeApplicableStaffLambdaQueryWrapper);
        List<Long> collect = page1.getRecords().stream().map(SchemeApplicableStaff::getId).collect(Collectors.toList());
        if(CollectionUtils.isEmpty(collect)){
@@ -179,6 +172,7 @@
        BigDecimal basicSalary = new BigDecimal("0.00");
        map.put("fundPersonal", fundPersonal); // 公积金
        map.put("socialPersonal", socialPersonal); // 社保金额
        map.put("socialSupplementAmount", BigDecimal.ZERO); // 社保补缴金额
        map.put("basicSalary", basicSalary); // 基本工资
        // 个税金额
        BigDecimal salaryTax = new BigDecimal("0.00");
@@ -204,15 +198,93 @@
        // 实发工资
        BigDecimal netSalary = new BigDecimal("0.00");
        map.put("netSalary", netSalary);
        // 调用基本工资
        // 根据排班数据计算白班天数和夜班天数
        BigDecimal dayDays = new BigDecimal("0.00");
        BigDecimal nightDays = new BigDecimal("0.00");
        // 查询当月排班记录
        List<PersonalShift> shiftList = personalShiftMapper.selectList(new LambdaQueryWrapper<PersonalShift>()
                .eq(PersonalShift::getStaffOnJobId, staffId)
                .like(PersonalShift::getWorkTime, date));
        if(!CollectionUtils.isEmpty(shiftList)){
            // 收集所有班次配置ID
            List<Integer> configIds = shiftList.stream()
                    .map(PersonalShift::getPersonalAttendanceLocationConfigId)
                    .filter(Objects::nonNull)
                    .collect(Collectors.toList());
            if(!CollectionUtils.isEmpty(configIds)){
                // 查询班次配置信息
                List<PersonalAttendanceLocationConfig> configList = personalAttendanceLocationConfigMapper.selectList(
                        new LambdaQueryWrapper<PersonalAttendanceLocationConfig>()
                                .in(PersonalAttendanceLocationConfig::getId, configIds));
                // 构建班次配置映射
                Map<Integer, String> configMap = configList.stream()
                        .collect(Collectors.toMap(
                                PersonalAttendanceLocationConfig::getId,
                                PersonalAttendanceLocationConfig::getShift,
                                (a, b) -> a));
                // 统计白班和夜班天数
                for (PersonalShift shift : shiftList) {
                    Integer configId = shift.getPersonalAttendanceLocationConfigId();
                    String shiftName = configMap.get(configId);
                    if(shiftName != null){
                        // 根据班次名称判断:包含"白"或"日"为白班,包含"夜"为夜班
                        if(shiftName.contains("白") || shiftName.contains("日")){
                            dayDays = dayDays.add(BigDecimal.ONE);
                        } else if(shiftName.contains("夜")){
                            nightDays = nightDays.add(BigDecimal.ONE);
                        }
                    }
                }
            }
        }
        map.put("dayDays", dayDays); // 白班天数
        map.put("nightDays", nightDays); // 夜班天数
        // 查询餐补和夜班补贴
        // 计算餐补(回族特有)和夜班补助
        BigDecimal mealAmount = new BigDecimal("0.00"); // 餐补
        BigDecimal nightAmount = new BigDecimal("0.00"); // 夜班补助
        // 获取餐补和夜班补贴标准(从补贴配置中获取)
        SubsidyConfiguration subsidyConfig = subsidyConfigurationMapper.selectOne(null);
        BigDecimal mealStandard = new BigDecimal("0.00"); // 餐补标准
        BigDecimal nightStandard = new BigDecimal("0.00"); // 夜班补贴标准
        if(subsidyConfig != null){
            mealStandard = subsidyConfig.getMealAmount() != null ? subsidyConfig.getMealAmount() : new BigDecimal("0.00");
            nightStandard = subsidyConfig.getNightAmount() != null ? subsidyConfig.getNightAmount() : new BigDecimal("0.00");
        }
        // 餐补:民族为回族特有,计算方式为(白班天数+夜班天数)*餐补标准
        StaffOnJob staffOnJobDto = staffOnJobMapper.selectById(staffId);
        if(staffOnJobDto == null){
            return;
        }
        String nation = staffOnJobDto.getNation();
        if("回族".equals(nation)){
            mealAmount = dayDays.add(nightDays).multiply(mealStandard);
        }
        // 夜班补助:夜班天数*夜班补贴标准
        nightAmount = nightDays.multiply(nightStandard);
        map.put("mealAmount", mealAmount); // 餐补
        map.put("nightAmount", nightAmount); // 夜班补助
        // 调用基本工资
        basicSalary = staffOnJobDto.getBasicSalary();
        map.put("basicSalary", basicSalary);
        // 应发工资
        map.put("grossSalary", basicSalary);
        // 应发工资(基本工资+餐补+夜班补助+其他收入)
        grossSalary = basicSalary.add(mealAmount).add(nightAmount).add(otherIncome);
        map.put("grossSalary", grossSalary);
        // 实发工资初始值
        netSalary = grossSalary;
        map.put("netSalary", netSalary);
        // 个税金额(无社保版)
        BigDecimal bigDecimal = TaxCalculator.calculateMonthlyTax(basicSalary, socialPersonal, fundPersonal);
        map.put("salaryTax", bigDecimal);
@@ -235,30 +307,47 @@
        if (CollectionUtils.isEmpty(schemeList)) {
            return; // 无匹配方案,返回空列表
        }
        // 3. 为每个方案关联明细列表
        for (SchemeApplicableStaff scheme : schemeList) {
            List<SchemeInsuranceDetail> detailList = schemeApplicableStaffMapper.selectDetailBySchemeId(scheme.getId());
            // 根据明细列表计算社保缴费金额
            if(CollectionUtils.isEmpty(detailList)){
                continue;
        YearMonth targetMonth = parseTargetMonth(date);
        SchemeApplicableStaff currentScheme = resolveCurrentScheme(schemeList, targetMonth);
        if (currentScheme == null) {
            return;
        }
        List<SchemeInsuranceDetail> currentDetailList = defaultIfNull(
                schemeApplicableStaffMapper.selectDetailBySchemeId(currentScheme.getId())
        );
        for (SchemeInsuranceDetail detail : currentDetailList) {
            BigDecimal amount = calculateInsuranceAmount(staffOnJobDto, detail);
            if ("公积金".equals(detail.getInsuranceType())) {
                fundPersonal = fundPersonal.add(amount);
            } else {
                socialPersonal = socialPersonal.add(amount);
            }
            for (SchemeInsuranceDetail detail : detailList) {
                if("公积金".equals(detail.getInsuranceType())){
                    fundPersonal = fundPersonal.add(calculateByEmployeeIdType(detail.getInsuranceType(),fundPersonal, staffOnJobDto, detail));
                }else{
                    socialPersonal = socialPersonal.add(calculateByEmployeeIdType(detail.getInsuranceType(),socialPersonal, staffOnJobDto, detail));
                }
            }
        }
        BigDecimal socialSupplementAmount = calculateSocialSupplementAmount(
                staffId,
                staffOnJobDto,
                targetMonth,
                currentScheme,
                currentDetailList,
                socialPersonal
        );
        if (socialSupplementAmount.compareTo(BigDecimal.ZERO) > 0) {
            socialPersonal = socialPersonal.add(socialSupplementAmount);
        }
        map.put("socialPersonal", socialPersonal);
        map.put("fundPersonal", fundPersonal);
        map.put("socialSupplementAmount", socialSupplementAmount);
        // 个税金额(社保版)
        bigDecimal = TaxCalculator.calculateMonthlyTax(basicSalary, socialPersonal, fundPersonal);
        map.put("salaryTax", bigDecimal);
        // 应扣工资
        map.put("deductSalary", bigDecimal.add(fundPersonal).add(socialPersonal));
        // 实发工资
        map.put("netSalary", basicSalary.subtract(bigDecimal).subtract(fundPersonal).subtract(socialPersonal));
        // 应扣工资 = 个税 + 公积金个人 + 社保个人 + 其他支出
        deductSalary = bigDecimal.add(fundPersonal).add(socialPersonal).add(otherDeduct);
        map.put("deductSalary", deductSalary);
        // 实发工资 = 应发工资 - 应扣工资
        netSalary = grossSalary.subtract(deductSalary);
        map.put("netSalary", netSalary);
    }
@@ -283,24 +372,181 @@
    }
    /**
     * 计算
     * @param type
     * @param bigDecimal
     * @param staffOnJobDto
     * @param detail
     * @return
     * 计算单项保险金额
     */
    public BigDecimal calculateByEmployeeIdType(String type,BigDecimal bigDecimal, StaffOnJob staffOnJobDto,SchemeInsuranceDetail detail) {
        // 判断是否调用基本工资
        if (detail.getUseBasicSalary() == 1) {
            BigDecimal divide = detail.getPaymentBase().multiply(detail.getPersonalRatio()).divide(new BigDecimal("100"), 2);
            bigDecimal = bigDecimal.add(divide);
        }else{
            // 调用基本工资
            BigDecimal multiply = staffOnJobDto.getBasicSalary().multiply(detail.getPersonalRatio().divide(new BigDecimal("100"), 2));
            bigDecimal = bigDecimal.add(multiply);
    public BigDecimal calculateInsuranceAmount(StaffOnJob staffOnJobDto, SchemeInsuranceDetail detail) {
        if (staffOnJobDto == null || detail == null) {
            return BigDecimal.ZERO;
        }
        bigDecimal = bigDecimal.add(detail.getPersonalFixed());
        return bigDecimal;
        BigDecimal ratio = detail.getPersonalRatio() == null ? BigDecimal.ZERO : detail.getPersonalRatio();
        BigDecimal fixed = detail.getPersonalFixed() == null ? BigDecimal.ZERO : detail.getPersonalFixed();
        BigDecimal baseAmount;
        if (detail.getUseBasicSalary() == 1) {
            baseAmount = detail.getPaymentBase() == null ? BigDecimal.ZERO : detail.getPaymentBase();
        } else {
            baseAmount = staffOnJobDto.getBasicSalary() == null ? BigDecimal.ZERO : staffOnJobDto.getBasicSalary();
        }
        BigDecimal ratioAmount = baseAmount.multiply(ratio).divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP);
        return ratioAmount.add(fixed);
    }
    private SchemeApplicableStaff resolveCurrentScheme(List<SchemeApplicableStaff> schemeList, YearMonth targetMonth) {
        if (CollectionUtils.isEmpty(schemeList)) {
            return null;
        }
        if (targetMonth == null) {
            return schemeList.get(0);
        }
        for (SchemeApplicableStaff scheme : schemeList) {
            if (scheme.getCreateTime() == null) {
                continue;
            }
            YearMonth schemeMonth = YearMonth.from(scheme.getCreateTime());
            if (!schemeMonth.isAfter(targetMonth)) {
                return scheme;
            }
        }
        return schemeList.get(schemeList.size() - 1);
    }
    /**
     * 计算社保补缴金额
     *
     * 业务逻辑:
     * 1. 只有在社保方案设置新增的当月才计算补缴金额,其余月份返回0
     * 2. 根据最新社保比例计算当月及以后应缴纳的金额标准
     * 3. 统计当年1月到当前月已缴纳的社保总额
     * 4. 用最新标准计算的期望总额与实际已缴纳金额的差额即为补缴金额
     *
     * @param staffId              员工ID
     * @param staffOnJobDto        员工在职信息(用于计算社保金额)
     * @param targetMonth          目标计算月份
     * @param currentScheme        当前社保方案
     * @param currentDetailList    当前社保方案明细列表
     * @param currentMonthSocialPersonal 当前月个人社保金额
     * @return 社保补缴金额(负数或0返回0)
     */
    private BigDecimal calculateSocialSupplementAmount(Long staffId,
                                                       StaffOnJob staffOnJobDto,
                                                       YearMonth targetMonth,
                                                       SchemeApplicableStaff currentScheme,
                                                       List<SchemeInsuranceDetail> currentDetailList,
                                                       BigDecimal currentMonthSocialPersonal) {
        // 1. 参数校验:任一必填参数为空,返回0
        if (staffId == null || staffOnJobDto == null || targetMonth == null || currentScheme == null || currentScheme.getCreateTime() == null) {
            return BigDecimal.ZERO;
        }
        // 2. 判断是否为社保方案新增当月
        //    只有新增社保设置的当月才计算补缴,其余月份返回0
        YearMonth schemeMonth = YearMonth.from(currentScheme.getCreateTime());
        if (!schemeMonth.equals(targetMonth)) {
            return BigDecimal.ZERO;
        }
        // 3. 计算最新月度社保个人金额(排除公积金)
        BigDecimal latestMonthlySocial = defaultIfNull(currentDetailList).stream()
                .filter(Objects::nonNull)
                .filter(detail -> !"公积金".equals(detail.getInsuranceType()))  // 排除公积金
                .map(detail -> calculateInsuranceAmount(staffOnJobDto, detail))   // 计算每项社保金额
                .reduce(BigDecimal.ZERO, BigDecimal::add);  // 累加求和
        // 如果最新月度社保金额为0或负数,无需补缴
        if (latestMonthlySocial.compareTo(BigDecimal.ZERO) <= 0) {
            return BigDecimal.ZERO;
        }
        // 4. 统计当年1月到当前月之前已缴纳的社保总额
        BigDecimal paidYtdSocial = sumPaidSocialPersonalBeforeMonth(staffId, targetMonth);
        // 5. 计算按最新标准应缴纳的年度总额
        //    期望总额 = 最新月度社保 × 当前月份(1-12)
        BigDecimal expectedYtdSocial = latestMonthlySocial.multiply(BigDecimal.valueOf(targetMonth.getMonthValue()));
        // 6. 计算补缴金额
        //    补缴金额 = 期望总额 - 已缴纳总额 - 当前月已计算的社保
        BigDecimal supplement = expectedYtdSocial.subtract(paidYtdSocial).subtract(defaultZero(currentMonthSocialPersonal));
        // 7. 返回结果:负数或0返回0,正数保留两位小数
        return supplement.compareTo(BigDecimal.ZERO) > 0 ? supplement.setScale(2, RoundingMode.HALF_UP) : BigDecimal.ZERO;
    }
    /**
     * 统计员工当年1月到目标月份之前已缴纳的社保个人部分总额
     *
     * 查询逻辑:
     * 1. 根据目标月份获取当年年份前缀(如 "2024-")
     * 2. 查询该年份下所有早于目标月份的薪资主表记录
     * 3. 根据薪资主表ID查询对应的薪资明细表
     * 4. 按员工ID筛选并累加社保个人部分金额
     *
     * @param staffId      员工ID
     * @param targetMonth  目标月份(统计截止到该月份之前)
     * @return 已缴纳的社保个人部分总额
     */
    private BigDecimal sumPaidSocialPersonalBeforeMonth(Long staffId, YearMonth targetMonth) {
        // 1. 参数校验
        if (staffId == null || targetMonth == null) {
            return BigDecimal.ZERO;
        }
        // 2. 构建查询条件
        String yearPrefix = targetMonth.getYear() + "-";  // 年份前缀,如 "2024-"
        String currentMonthStr = targetMonth.format(DateTimeFormatter.ofPattern("yyyy-MM"));  // 目标月份字符串,如 "2024-03"
        // 3. 查询当年1月到目标月份之前的薪资主表
        List<StaffSalaryMain> salaryMainList = staffSalaryMainMapper.selectList(
                new LambdaQueryWrapper<StaffSalaryMain>()
                        .likeRight(StaffSalaryMain::getSalaryMonth, yearPrefix)  // 匹配当年所有月份
                        .lt(StaffSalaryMain::getSalaryMonth, currentMonthStr)    // 小于目标月份
        );
        if (CollectionUtils.isEmpty(salaryMainList)) {
            return BigDecimal.ZERO;
        }
        // 4. 提取薪资主表ID列表
        List<Long> mainIds = salaryMainList.stream()
                .map(StaffSalaryMain::getId)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
        if (CollectionUtils.isEmpty(mainIds)) {
            return BigDecimal.ZERO;
        }
        // 5. 查询对应的薪资明细表
        List<StaffSalaryDetail> detailList = staffSalaryDetailMapper.selectList(
                new LambdaQueryWrapper<StaffSalaryDetail>()
                        .in(StaffSalaryDetail::getMainId, mainIds)        // 根据主表ID查询
                        .eq(StaffSalaryDetail::getStaffOnJobId, staffId)  // 按员工ID筛选
        );
        if (CollectionUtils.isEmpty(detailList)) {
            return BigDecimal.ZERO;
        }
        // 6. 累加社保个人部分金额
        return detailList.stream()
                .map(StaffSalaryDetail::getSocialPersonal)  // 获取社保个人部分
                .filter(Objects::nonNull)
                .reduce(BigDecimal.ZERO, BigDecimal::add);  // 累加求和
    }
    private YearMonth parseTargetMonth(String date) {
        if (StringUtils.isEmpty(date)) {
            return null;
        }
        String normalized = date.length() >= 7 ? date.substring(0, 7) : date;
        try {
            return YearMonth.parse(normalized, DateTimeFormatter.ofPattern("yyyy-MM"));
        } catch (DateTimeParseException ex) {
            return null;
        }
    }
    private <T> List<T> defaultIfNull(List<T> list) {
        return list == null ? Collections.emptyList() : list;
    }
    private BigDecimal defaultZero(BigDecimal value) {
        return value == null ? BigDecimal.ZERO : value;
    }
}