package cn.iocoder.yudao.module.hrm.service.salary; import cn.hutool.core.collection.CollUtil; 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; import cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmLeaveApplicationDO; import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO; 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.HrmSocialSecuritySchemeDO; import cn.iocoder.yudao.module.hrm.dal.mysql.approval.HrmLeaveApplicationMapper; import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeMapper; 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.HrmSocialSecuritySchemeMapper; import cn.iocoder.yudao.module.hrm.dal.redis.HrmNoRedisDAO; import cn.iocoder.yudao.module.system.api.dept.DeptApi; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.YearMonth; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants.*; /** * 酬核算 Service 实现类 * * @author 芋道源码 */ @Service @Validated public class HrmSalaryCalculationServiceImpl implements HrmSalaryCalculationService { /** * 个税起征点(每月5000元) */ private static final BigDecimal TAX_THRESHOLD = new BigDecimal("5000"); /** * 默认缴纳比例(当员工社保档案中没有配置或方案中没有配置时使用) */ private static final BigDecimal DEFAULT_PENSION_PERSONAL_RATIO = new BigDecimal("0.08"); 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"); @Resource private HrmSalaryCalculationMapper salaryCalculationMapper; @Resource private HrmEmployeeSalaryMapper employeeSalaryMapper; @Resource private HrmEmployeeMapper employeeMapper; @Resource private HrmEmployeeSocialSecurityMapper employeeSocialSecurityMapper; @Resource private HrmSocialSecuritySchemeMapper socialSecuritySchemeMapper; @Resource private HrmNoRedisDAO noRedisDAO; @Resource private DeptApi deptApi; @Resource private HrmDeductionService deductionService; @Resource private HrmLeaveApplicationMapper leaveApplicationMapper; @Resource private HrmLeaveTypeConfigMapper leaveTypeConfigMapper; @Resource private HrmSalaryPaymentMapper salaryPaymentMapper; @Resource private HrmSalaryPaymentDetailMapper salaryPaymentDetailMapper; @Override public PageResult getSalaryCalculationPage(HrmSalaryCalculationPageReqVO pageReqVO) { return salaryCalculationMapper.selectPage(pageReqVO); } @Override public HrmSalaryCalculationDO getSalaryCalculation(Long id) { return salaryCalculationMapper.selectById(id); } @Override @Transactional(rollbackFor = Exception.class) public void calculateSalary(String period) { calculateSalary(period, null); } @Override @Transactional(rollbackFor = Exception.class) public void calculateSalary(String period, Long deptId) { // 解析核算周期的日期 LocalDate effectiveDate = parsePeriodToDate(period); // 1. 获取所有在职员工(员工状态:1-在职,2-试用) List employees = employeeMapper.selectListByStatus(null); // 过滤在职和试用期员工 employees = employees.stream() .filter(e -> e.getEmployeeStatus() != null && (e.getEmployeeStatus() == 1 || e.getEmployeeStatus() == 2)) .filter(e -> deptId == null || deptId.equals(e.getDeptId())) .toList(); // 2. 遍历员工进行薪酬核算 for (HrmEmployeeDO employee : employees) { // employeeId 是员工ID,用于关联薪酬档案、社保档案、核算记录 Long employeeId = employee.getId(); // 2.1 检查该员工当月是否已存在核算记录(按员工ID查询) List existCalculations = salaryCalculationMapper.selectListByUserIdAndPeriod(employeeId, period); if (CollUtil.isNotEmpty(existCalculations)) { // 如果存在多条记录,提示异常数据 if (existCalculations.size() > 1) { throw exception(SALARY_CALCULATION_DUPLICATE, employee.getName(), period); } continue; // 已存在则跳过 } // 2.2 获取员工薪酬档案(按员工ID查询) HrmEmployeeSalaryDO employeeSalary = employeeSalaryMapper.selectLatestByUserId(employeeId); if (employeeSalary == null) { // 如果没有薪酬档案,使用员工表中的基本工资创建默认数据 employeeSalary = buildDefaultEmployeeSalary(employeeId, employee); } // 2.3 获取员工社保公积金档案(按员工ID查询,取最新生效的) HrmEmployeeSocialSecurityDO socialSecurity = employeeSocialSecurityMapper.selectLatestByUserId(employeeId); // 2.4 如果有社保档案,获取绑定的方案 HrmSocialSecuritySchemeDO scheme = null; if (socialSecurity != null && socialSecurity.getSocialSecuritySchemeId() != null) { scheme = socialSecuritySchemeMapper.selectById(socialSecurity.getSocialSecuritySchemeId()); } // 2.5 构建核算记录(包含扣除计算,通过方案获取比例) HrmSalaryCalculationDO calculation = buildSalaryCalculation(period, employeeId, employee, employeeSalary, socialSecurity, scheme, effectiveDate); // 2.6 生成核算单号 String no = noRedisDAO.generateSalaryCalculationNo(); while (salaryCalculationMapper.selectByNo(no) != null) { no = noRedisDAO.generateSalaryCalculationNo(); } calculation.setNo(no); // 2.7 保存核算记录 salaryCalculationMapper.insert(calculation); } } /** * 解析核算周期为日期(如 2024-01 -> 2024-01-01) */ private LocalDate parsePeriodToDate(String period) { String[] parts = period.split("-"); int year = Integer.parseInt(parts[0]); int month = Integer.parseInt(parts[1]); return LocalDate.of(year, month, 1); } /** * 构建默认的员工薪酬档案(基于员工表的基本工资) */ private HrmEmployeeSalaryDO buildDefaultEmployeeSalary(Long employeeId, HrmEmployeeDO employee) { return HrmEmployeeSalaryDO.builder() .userId(employeeId) // 使用员工ID .salaryStructureId(employee.getSalaryStructureId()) .baseSalary(employee.getBaseSalary() != null ? employee.getBaseSalary() : BigDecimal.ZERO) .performanceSalary(BigDecimal.ZERO) .mealAllowance(BigDecimal.ZERO) .transportAllowance(BigDecimal.ZERO) .otherAllowance(BigDecimal.ZERO) .status(0) .build(); } /** * 构建薪酬核算记录(包含社保、公积金、个税、请假扣除计算) * 通过员工社保档案绑定的方案获取缴纳比例 */ private HrmSalaryCalculationDO buildSalaryCalculation(String period, Long employeeId, HrmEmployeeDO employee, HrmEmployeeSalaryDO employeeSalary, HrmEmployeeSocialSecurityDO socialSecurity, HrmSocialSecuritySchemeDO scheme, LocalDate effectiveDate) { // 计算应发薪资 BigDecimal baseSalary = employeeSalary.getBaseSalary() != null ? employeeSalary.getBaseSalary() : BigDecimal.ZERO; BigDecimal performanceSalary = employeeSalary.getPerformanceSalary() != null ? employeeSalary.getPerformanceSalary() : BigDecimal.ZERO; 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; BigDecimal overtimePay = BigDecimal.ZERO; // 应发工资总额 BigDecimal totalIncome = baseSalary.add(performanceSalary).add(mealAllowance) .add(transportAllowance).add(otherAllowance).add(overtimePay); // 获取社保基数(员工档案中的个性化基数) BigDecimal socialSecurityBase = (socialSecurity != null && socialSecurity.getSocialSecurityBase() != null) ? socialSecurity.getSocialSecurityBase() : baseSalary; // 获取公积金基数(员工档案中的个性化基数) BigDecimal housingFundBase = (socialSecurity != null && socialSecurity.getHousingFundBase() != null) ? socialSecurity.getHousingFundBase() : baseSalary; // 从方案中获取缴纳比例(优先使用方案中的比例,否则使用默认比例) BigDecimal pensionPersonalRatio = (scheme != null && scheme.getPensionPersonalRatio() != null) ? scheme.getPensionPersonalRatio() : DEFAULT_PENSION_PERSONAL_RATIO; BigDecimal medicalPersonalRatio = (scheme != null && scheme.getMedicalPersonalRatio() != null) ? scheme.getMedicalPersonalRatio() : DEFAULT_MEDICAL_PERSONAL_RATIO; BigDecimal unemploymentPersonalRatio = (scheme != null && scheme.getUnemploymentPersonalRatio() != null) ? scheme.getUnemploymentPersonalRatio() : DEFAULT_UNEMPLOYMENT_PERSONAL_RATIO; BigDecimal housingFundPersonalRatio = (scheme != null && scheme.getHousingFundPersonalRatio() != null) ? scheme.getHousingFundPersonalRatio() : DEFAULT_HOUSING_FUND_PERSONAL_RATIO; // 社保扣除 = 基数 × (养老个人比例 + 医疗个人比例 + 失业个人比例) BigDecimal socialSecurityDeduction = socialSecurityBase .multiply(pensionPersonalRatio) .add(socialSecurityBase.multiply(medicalPersonalRatio)) .add(socialSecurityBase.multiply(unemploymentPersonalRatio)) .setScale(2, RoundingMode.HALF_UP); // 公积金扣除 = 基数 × 公积金个人比例 BigDecimal housingFundDeduction = housingFundBase.multiply(housingFundPersonalRatio).setScale(2, RoundingMode.HALF_UP); // 计算请假扣款 BigDecimal leaveDeduction = calculateLeaveDeduction(employee, period, baseSalary); // 计算个税扣除 // 应纳税所得额 = 应发工资 - 社保扣除 - 公积金扣除 - 起征点(5000) BigDecimal taxableIncome = totalIncome .subtract(socialSecurityDeduction) .subtract(housingFundDeduction) .subtract(TAX_THRESHOLD); // 如果应纳税所得额小于0,则不缴纳个税 if (taxableIncome.compareTo(BigDecimal.ZERO) < 0) { taxableIncome = BigDecimal.ZERO; } BigDecimal taxDeduction = deductionService.calculateTaxDeduction(taxableIncome, effectiveDate); BigDecimal otherDeduction = BigDecimal.ZERO; // 计算实发工资 = 应发 - 扣除 BigDecimal totalDeduction = socialSecurityDeduction.add(housingFundDeduction) .add(taxDeduction).add(otherDeduction).add(leaveDeduction); BigDecimal actualSalary = totalIncome.subtract(totalDeduction); return HrmSalaryCalculationDO.builder() .period(period) .userId(employeeId) // 使用员工ID .deptId(employee.getDeptId()) .baseSalary(baseSalary) .performanceSalary(performanceSalary) .overtimePay(overtimePay) .mealAllowance(mealAllowance) .transportAllowance(transportAllowance) .otherAllowance(otherAllowance) .socialSecurityDeduction(socialSecurityDeduction) .housingFundDeduction(housingFundDeduction) .taxDeduction(taxDeduction) .otherDeduction(otherDeduction) .leaveDeduction(leaveDeduction) .actualSalary(actualSalary) .workDays(22) // 默认工作日 .overtimeHours(BigDecimal.ZERO) .status(0) // 待确认 .build(); } /** * 计算请假扣款 * * @param employee 员工信息 * @param period 核算周期(如2026-02) * @param baseSalary 基本工资 * @return 请假扣款金额 */ private BigDecimal calculateLeaveDeduction(HrmEmployeeDO employee, String period, BigDecimal baseSalary) { // 解析周期获取月份开始和结束时间 YearMonth yearMonth = YearMonth.parse(period); LocalDateTime monthStart = yearMonth.atDay(1).atStartOfDay(); LocalDateTime monthEnd = yearMonth.atEndOfMonth().atTime(23, 59, 59); // 查询员工在该月已审批通过的请假记录 // 注意:请假申请的 userId 是系统用户ID,需要通过员工表获取 Long systemUserId = employee.getUserId(); if (systemUserId == null) { return BigDecimal.ZERO; } List leaveApplications = leaveApplicationMapper .selectApprovedListByUserIdAndTimeRange(systemUserId, monthStart, monthEnd); if (leaveApplications.isEmpty()) { return BigDecimal.ZERO; } // 获取所有请假类型配置 List configList = leaveTypeConfigMapper.selectList(); Map configMap = configList.stream() .filter(c -> c.getStatus() == 0) // 只取启用的配置 .collect(Collectors.toMap(HrmLeaveTypeConfigDO::getLeaveType, HrmLeaveTypeConfigDO::getDeductionRatio)); // 计算日工资(基本工资 / 21.75,按国家规定的工作日计算) BigDecimal dailySalary = baseSalary.divide(new BigDecimal("21.75"), 2, RoundingMode.HALF_UP); // 累计请假扣款 BigDecimal totalLeaveDeduction = BigDecimal.ZERO; for (HrmLeaveApplicationDO application : leaveApplications) { Integer leaveType = application.getLeaveType(); BigDecimal duration = application.getDuration() != null ? application.getDuration() : BigDecimal.ZERO; BigDecimal deductionRatio = configMap.getOrDefault(leaveType, BigDecimal.ZERO); // 请假扣款 = 日工资 × 请假天数 × 扣款比例 BigDecimal deduction = dailySalary.multiply(duration).multiply(deductionRatio) .setScale(2, RoundingMode.HALF_UP); totalLeaveDeduction = totalLeaveDeduction.add(deduction); } return totalLeaveDeduction; } @Override @Transactional(rollbackFor = Exception.class) public void confirmSalaryCalculation(List ids) { if (ids == null || ids.isEmpty()) { return; } LocalDateTime confirmTime = LocalDateTime.now(); String period = null; for (Long id : ids) { HrmSalaryCalculationDO salaryCalculation = salaryCalculationMapper.selectById(id); if (salaryCalculation == null) { throw exception(SALARY_CALCULATION_NOT_EXISTS); } if (salaryCalculation.getStatus() != 0) { throw exception(SALARY_CALCULATION_CONFIRM_FAIL); } salaryCalculation.setStatus(10); // 已确认 salaryCalculation.setConfirmTime(confirmTime); salaryCalculationMapper.updateById(salaryCalculation); // 记录周期 period = salaryCalculation.getPeriod(); } // 检查该周期是否所有核算记录都已确认,自动生成发放台账 if (period != null) { autoGeneratePaymentIfAllConfirmed(period); } } /** * 检查该周期是否所有核算记录都已确认,如果全部确认则自动生成发放台账 */ private void autoGeneratePaymentIfAllConfirmed(String period) { // 查询该周期所有核算记录 List allCalculations = salaryCalculationMapper.selectListByPeriod(period); if (CollUtil.isEmpty(allCalculations)) { return; } // 检查是否全部已确认(状态=10) boolean allConfirmed = allCalculations.stream() .allMatch(c -> c.getStatus() == 10); if (!allConfirmed) { return; } // 检查是否已存在发放台账 HrmSalaryPaymentDO existPayment = salaryPaymentMapper.selectByPeriod(period); if (existPayment != null) { return; } // 自动生成发放台账 generatePaymentInternal(period, allCalculations); } private HrmSalaryCalculationDO validateSalaryCalculationExists(Long id) { HrmSalaryCalculationDO salaryCalculation = salaryCalculationMapper.selectById(id); if (salaryCalculation == null) { throw exception(SALARY_CALCULATION_NOT_EXISTS); } return salaryCalculation; } @Override public List getSalaryCalculationListByPeriod(String period) { return salaryCalculationMapper.selectListByPeriod(period); } @Override public List previewSalaryCalculation(String period, Long deptId) { // 解析核算周期的日期 LocalDate effectiveDate = parsePeriodToDate(period); // 1. 获取所有在职员工(员工状态:1-在职,2-试用) List employees = employeeMapper.selectListByStatus(null); // 过滤在职和试用期员工 employees = employees.stream() .filter(e -> e.getEmployeeStatus() != null && (e.getEmployeeStatus() == 1 || e.getEmployeeStatus() == 2)) .filter(e -> deptId == null || deptId.equals(e.getDeptId())) .toList(); // 2. 遍历员工实时计算薪酬 List resultList = new java.util.ArrayList<>(); for (HrmEmployeeDO employee : employees) { Long employeeId = employee.getId(); // 2.1 获取员工薪酬档案(按员工ID查询) HrmEmployeeSalaryDO employeeSalary = employeeSalaryMapper.selectLatestByUserId(employeeId); if (employeeSalary == null) { // 如果没有薪酬档案,使用员工表中的基本工资创建默认数据 employeeSalary = buildDefaultEmployeeSalary(employeeId, employee); } // 2.2 获取员工社保公积金档案(按员工ID查询,取最新生效的) HrmEmployeeSocialSecurityDO socialSecurity = employeeSocialSecurityMapper.selectLatestByUserId(employeeId); // 2.3 如果有社保档案,获取绑定的方案 HrmSocialSecuritySchemeDO scheme = null; if (socialSecurity != null && socialSecurity.getSocialSecuritySchemeId() != null) { scheme = socialSecuritySchemeMapper.selectById(socialSecurity.getSocialSecuritySchemeId()); } // 2.4 构建核算记录(实时计算,不存储) HrmSalaryCalculationDO calculation = buildSalaryCalculation(period, employeeId, employee, employeeSalary, socialSecurity, scheme, effectiveDate); resultList.add(calculation); } return resultList; } @Override @Transactional(rollbackFor = Exception.class) public void saveSalaryCalculationList(List calculationList) { for (HrmSalaryCalculationDO calculation : calculationList) { if (calculation.getId() == null) { // 新增记录,生成核算单号 String no = noRedisDAO.generateSalaryCalculationNo(); while (salaryCalculationMapper.selectByNo(no) != null) { no = noRedisDAO.generateSalaryCalculationNo(); } calculation.setNo(no); calculation.setStatus(0); // 待确认 salaryCalculationMapper.insert(calculation); } else { // 更新已有记录 salaryCalculationMapper.updateById(calculation); } } } /** * 内部生成发放台账方法(供自动生成调用) */ private void generatePaymentInternal(String period, List calculationList) { // 生成发放台账 String no = generatePaymentNo(); BigDecimal totalAmount = calculationList.stream() .map(c -> c.getActualSalary() != null ? c.getActualSalary() : BigDecimal.ZERO) .reduce(BigDecimal.ZERO, BigDecimal::add); Integer totalCount = calculationList.size(); HrmSalaryPaymentDO payment = HrmSalaryPaymentDO.builder() .no(no) .period(period) .totalAmount(totalAmount) .totalCount(totalCount) .status(0) // 待发放 .build(); salaryPaymentMapper.insert(payment); // 生成发放明细(不更新核算状态) for (HrmSalaryCalculationDO calculation : calculationList) { HrmSalaryPaymentDetailDO detail = HrmSalaryPaymentDetailDO.builder() .paymentId(payment.getId()) .calculationId(calculation.getId()) .userId(calculation.getUserId()) .deptId(calculation.getDeptId()) .actualSalary(calculation.getActualSalary()) .status(0) // 待发放 .build(); salaryPaymentDetailMapper.insert(detail); } } private String generatePaymentNo() { String no = noRedisDAO.generateSalaryPaymentNo(); while (salaryPaymentMapper.selectByNo(no) != null) { no = noRedisDAO.generateSalaryPaymentNo(); } return no; } @Override @Transactional(rollbackFor = Exception.class) public void revokeSalaryCalculation(List ids) { if (ids == null || ids.isEmpty()) { return; } for (Long id : ids) { HrmSalaryCalculationDO calculation = salaryCalculationMapper.selectById(id); if (calculation == null) { throw exception(SALARY_CALCULATION_NOT_EXISTS); } // 校验状态必须是已确认(10) if (calculation.getStatus() != 10) { throw exception(SALARY_CALCULATION_REVOKE_FAIL); } // 检查是否已发放(状态=20) if (calculation.getStatus() == 20) { throw exception(SALARY_CALCULATION_ALREADY_PAY); } // 删除关联的发放明细 HrmSalaryPaymentDetailDO detail = salaryPaymentDetailMapper.selectByCalculationId(id); if (detail != null) { // 如果明细已发放,不允许撤销 if (detail.getStatus() == 10) { throw exception(SALARY_CALCULATION_ALREADY_PAY); } salaryPaymentDetailMapper.deleteById(detail.getId()); // 更新发放台账的总金额和总人数 updatePaymentStatistics(detail.getPaymentId()); } // 恢复核算状态为待确认 calculation.setStatus(0); calculation.setConfirmTime(null); salaryCalculationMapper.updateById(calculation); } } /** * 更新发放台账的统计信息 */ private void updatePaymentStatistics(Long paymentId) { HrmSalaryPaymentDO payment = salaryPaymentMapper.selectById(paymentId); if (payment == null) { return; } // 重新计算总金额和总人数 List details = salaryPaymentDetailMapper.selectListByPaymentId(paymentId); if (CollUtil.isEmpty(details)) { // 如果没有明细了,删除发放台账 salaryPaymentMapper.deleteById(paymentId); return; } BigDecimal totalAmount = details.stream() .map(d -> d.getActualSalary() != null ? d.getActualSalary() : BigDecimal.ZERO) .reduce(BigDecimal.ZERO, BigDecimal::add); payment.setTotalAmount(totalAmount); payment.setTotalCount(details.size()); salaryPaymentMapper.updateById(payment); } @Override @Transactional(rollbackFor = Exception.class) public void deleteSalaryCalculation(Long id) { HrmSalaryCalculationDO calculation = salaryCalculationMapper.selectById(id); if (calculation == null) { throw exception(SALARY_CALCULATION_NOT_EXISTS); } // 仅待确认状态可删除 if (calculation.getStatus() != 0) { throw exception(SALARY_CALCULATION_DELETE_FAIL); } salaryCalculationMapper.deleteById(id); } }