gaoluyang
昨天 8c65f898ed6e5b0c7a94c792c4562f6ec831dccd
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryCalculationServiceImpl.java
@@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.hrm.service.salary;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.HrmSalaryCalculationPageReqVO;
@@ -31,6 +32,8 @@
import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmSocialSecuritySchemeMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmSalaryStructureMapper;
import cn.iocoder.yudao.module.hrm.dal.redis.HrmNoRedisDAO;
import cn.iocoder.yudao.module.mes.api.feedback.MesProFeedbackWageApi;
import cn.iocoder.yudao.module.mes.api.feedback.dto.MesProFeedbackWageDetailRespDTO;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.hrm.enums.HrmOvertimeTypeEnum;
import jakarta.annotation.Resource;
@@ -44,6 +47,7 @@
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -107,6 +111,8 @@
    private HrmEmployeeScheduleMapper employeeScheduleMapper;
    @Resource
    private HrmShiftMapper shiftMapper;
    @Resource
    private MesProFeedbackWageApi feedbackWageApi;
    @Override
    public PageResult<HrmSalaryCalculationDO> getSalaryCalculationPage(HrmSalaryCalculationPageReqVO pageReqVO) {
@@ -139,6 +145,11 @@
                .filter(e -> deptId == null || deptId.equals(e.getDeptId()))
                .toList();
        // 1.1 一次汇总所有员工当月的生产报工工资,避免逐人查询
        Map<Long, BigDecimal> workWageMap = loadWorkWageMap(employees, period);
        // 1.2 一次加载涉及部门的最新社保公积金档案,避免逐人查询
        Map<Long, HrmEmployeeSocialSecurityDO> deptSocialSecurityMap = loadDeptSocialSecurityMap(employees);
        // 2. 遍历员工进行薪酬核算
        for (HrmEmployeeDO employee : employees) {
            // employeeId 是员工ID,用于关联薪酬档案、社保档案、核算记录
@@ -163,8 +174,9 @@
                employeeSalary = buildDefaultEmployeeSalary(employeeId, employee);
            }
            // 2.3 获取员工社保公积金档案(按员工ID查询,取最新生效的)
            HrmEmployeeSocialSecurityDO socialSecurity = employeeSocialSecurityMapper.selectLatestByUserId(employeeId);
            // 2.3 获取员工所在部门的社保公积金档案(按部门取最新生效的)
            HrmEmployeeSocialSecurityDO socialSecurity = employee.getDeptId() == null
                    ? null : deptSocialSecurityMap.get(employee.getDeptId());
            // 2.4 如果有社保档案,获取绑定的方案
            HrmSocialSecuritySchemeDO scheme = null;
            if (socialSecurity != null && socialSecurity.getSocialSecuritySchemeId() != null) {
@@ -172,7 +184,8 @@
            }
            // 2.5 构建核算记录(包含扣除计算,通过方案获取比例)
            HrmSalaryCalculationDO calculation = buildSalaryCalculation(period, employeeId, employee, employeeSalary, socialSecurity, scheme, effectiveDate);
            HrmSalaryCalculationDO calculation = buildSalaryCalculation(period, employeeId, employee, employeeSalary,
                    socialSecurity, scheme, effectiveDate, workWageMap);
            // 2.6 保存:首次生成单号;已有待确认记录则保留单号并覆盖重算结果
            if (CollUtil.isNotEmpty(existCalculations)) {
@@ -202,6 +215,46 @@
    }
    /**
     * 汇总核算周期内所有员工的生产报工工资。
     *
     * <p>报工单记录的是报工人(系统用户ID),薪酬核算记录的是员工ID,两者需通过
     * {@link HrmEmployeeDO#getUserId()} 映射;没有绑定系统账号的员工不产生报工工资。
     */
    private Map<Long, BigDecimal> loadWorkWageMap(List<HrmEmployeeDO> employees, String period) {
        Set<Long> userIds = employees.stream()
                .map(HrmEmployeeDO::getUserId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        if (CollUtil.isEmpty(userIds)) {
            return Collections.emptyMap();
        }
        YearMonth month = YearMonth.parse(period);
        return feedbackWageApi.getWageAmountMap(userIds, month.atDay(1).atStartOfDay(),
                month.atEndOfMonth().atTime(23, 59, 59));
    }
    /**
     * 加载涉及部门各自最新生效的社保公积金档案。
     *
     * <p>档案按部门维护,部门下所有员工共用同一份档案;查询结果已按生效日期倒序,
     * 每个部门取第一条即可。
     */
    private Map<Long, HrmEmployeeSocialSecurityDO> loadDeptSocialSecurityMap(List<HrmEmployeeDO> employees) {
        Set<Long> deptIds = employees.stream()
                .map(HrmEmployeeDO::getDeptId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        if (CollUtil.isEmpty(deptIds)) {
            return Collections.emptyMap();
        }
        Map<Long, HrmEmployeeSocialSecurityDO> result = new HashMap<>();
        for (HrmEmployeeSocialSecurityDO socialSecurity : employeeSocialSecurityMapper.selectListByDeptIds(deptIds)) {
            result.putIfAbsent(socialSecurity.getDeptId(), socialSecurity);
        }
        return result;
    }
    /**
     * 构建默认的员工薪酬档案。基本工资统一取员工档案,绩效工资默认不自动生成。
     */
    private HrmEmployeeSalaryDO buildDefaultEmployeeSalary(Long employeeId, HrmEmployeeDO employee) {
@@ -226,7 +279,8 @@
                                                          HrmEmployeeSalaryDO employeeSalary,
                                                          HrmEmployeeSocialSecurityDO socialSecurity,
                                                          HrmSocialSecuritySchemeDO scheme,
                                                          LocalDate effectiveDate) {
                                                          LocalDate effectiveDate,
                                                          Map<Long, BigDecimal> workWageMap) {
        // 计算应发薪资
        BigDecimal baseSalary = employeeSalary.getBaseSalary() != null ? employeeSalary.getBaseSalary() : BigDecimal.ZERO;
        BigDecimal performanceSalary = employeeSalary.getPerformanceSalary() != null ? employeeSalary.getPerformanceSalary() : BigDecimal.ZERO;
@@ -236,14 +290,19 @@
        OvertimeSalary overtimeSalary = calculateOvertimeSalary(employee, employeeSalary, period, baseSalary);
        BigDecimal overtimePay = overtimeSalary.pay();
        // 报工工资:报工人(系统用户ID)在核算周期内已审批通过的报工工资合计
        BigDecimal workWage = employee.getUserId() != null
                ? workWageMap.getOrDefault(employee.getUserId(), BigDecimal.ZERO)
                : BigDecimal.ZERO;
        // 按排班累计出勤天数与排班总工时(无排班时出勤天数回退默认 22)
        ScheduleStat scheduleStat = calculateScheduleStat(employeeId, period);
        Integer workDays = scheduleStat.scheduledDays() > 0 ? scheduleStat.scheduledDays() : 22;
        BigDecimal totalWorkHours = scheduleStat.totalWorkHours();
        // 应发工资总额
        // 应发工资总额(含报工工资)
        BigDecimal totalIncome = baseSalary.add(performanceSalary).add(mealAllowance)
                .add(transportAllowance).add(otherAllowance).add(overtimePay);
                .add(transportAllowance).add(otherAllowance).add(overtimePay).add(workWage);
        // 获取社保基数:员工社保档案 > 员工基本工资
        BigDecimal socialSecurityBase = (socialSecurity != null && socialSecurity.getSocialSecurityBase() != null)
@@ -305,6 +364,7 @@
                .mealAllowance(mealAllowance)
                .transportAllowance(transportAllowance)
                .otherAllowance(otherAllowance)
                .workWage(workWage)
                .socialSecurityDeduction(socialSecurityDeduction)
                .housingFundDeduction(housingFundDeduction)
                .taxDeduction(taxDeduction)
@@ -529,7 +589,12 @@
                .filter(e -> deptId == null || deptId.equals(e.getDeptId()))
                .toList();
        // 2. 遍历员工实时计算薪酬
        // 2. 一次汇总所有员工当月的生产报工工资
        Map<Long, BigDecimal> workWageMap = loadWorkWageMap(employees, period);
        // 2.1 一次加载涉及部门的最新社保公积金档案
        Map<Long, HrmEmployeeSocialSecurityDO> deptSocialSecurityMap = loadDeptSocialSecurityMap(employees);
        // 3. 遍历员工实时计算薪酬
        List<HrmSalaryCalculationDO> resultList = new java.util.ArrayList<>();
        for (HrmEmployeeDO employee : employees) {
            Long employeeId = employee.getId();
@@ -541,8 +606,9 @@
                employeeSalary = buildDefaultEmployeeSalary(employeeId, employee);
            }
            // 2.2 获取员工社保公积金档案(按员工ID查询,取最新生效的)
            HrmEmployeeSocialSecurityDO socialSecurity = employeeSocialSecurityMapper.selectLatestByUserId(employeeId);
            // 2.2 获取员工所在部门的社保公积金档案(按部门取最新生效的)
            HrmEmployeeSocialSecurityDO socialSecurity = employee.getDeptId() == null
                    ? null : deptSocialSecurityMap.get(employee.getDeptId());
            // 2.3 如果有社保档案,获取绑定的方案
            HrmSocialSecuritySchemeDO scheme = null;
            if (socialSecurity != null && socialSecurity.getSocialSecuritySchemeId() != null) {
@@ -550,7 +616,8 @@
            }
            // 2.4 构建核算记录(实时计算,不存储)
            HrmSalaryCalculationDO calculation = buildSalaryCalculation(period, employeeId, employee, employeeSalary, socialSecurity, scheme, effectiveDate);
            HrmSalaryCalculationDO calculation = buildSalaryCalculation(period, employeeId, employee, employeeSalary,
                    socialSecurity, scheme, effectiveDate, workWageMap);
            resultList.add(calculation);
        }
        return resultList;
@@ -576,6 +643,20 @@
        }
    }
    @Override
    public List<MesProFeedbackWageDetailRespDTO> getWorkWageDetailList(String period, Long employeeId) {
        if (employeeId == null || StrUtil.isBlank(period)) {
            return Collections.emptyList();
        }
        HrmEmployeeDO employee = employeeMapper.selectById(employeeId);
        if (employee == null || employee.getUserId() == null) {
            return Collections.emptyList();
        }
        YearMonth month = YearMonth.parse(period);
        return feedbackWageApi.getWageDetailList(employee.getUserId(),
                month.atDay(1).atStartOfDay(), month.atEndOfMonth().atTime(23, 59, 59));
    }
    /**
     * 内部生成发放台账方法(供自动生成调用)
     */