From 9697bac35fe2c74811afaf66f9cf28b758f3a386 Mon Sep 17 00:00:00 2001
From: liu <2021943741@qq.com>
Date: 星期五, 28 八月 2026 15:21:22 +0800
Subject: [PATCH] feat(hrm): 新增排班功能,薪酬核算按排班工时

---
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryCalculationServiceImpl.java |  151 ++++++++++++++++++++++++++++++++++++++++++++------
 1 files changed, 133 insertions(+), 18 deletions(-)

diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryCalculationServiceImpl.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryCalculationServiceImpl.java
index ffea59e..6cf8b19 100644
--- a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryCalculationServiceImpl.java
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryCalculationServiceImpl.java
@@ -5,6 +5,9 @@
 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.approval.HrmOvertimeApplicationDO;
+import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmEmployeeScheduleDO;
+import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmShiftDO;
 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;
@@ -13,7 +16,11 @@
 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.dataobject.salary.HrmSalaryStructureDO;
 import cn.iocoder.yudao.module.hrm.dal.mysql.approval.HrmLeaveApplicationMapper;
+import cn.iocoder.yudao.module.hrm.dal.mysql.attendance.HrmEmployeeScheduleMapper;
+import cn.iocoder.yudao.module.hrm.dal.mysql.attendance.HrmShiftMapper;
+import cn.iocoder.yudao.module.hrm.dal.mysql.approval.HrmOvertimeApplicationMapper;
 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;
@@ -22,8 +29,10 @@
 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.mysql.salary.HrmSalaryStructureMapper;
 import cn.iocoder.yudao.module.hrm.dal.redis.HrmNoRedisDAO;
 import cn.iocoder.yudao.module.system.api.dept.DeptApi;
+import cn.iocoder.yudao.module.hrm.enums.HrmOvertimeTypeEnum;
 import jakarta.annotation.Resource;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -34,8 +43,11 @@
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.YearMonth;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
 import java.util.stream.Collectors;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -44,7 +56,7 @@
 /**
  * 閰牳绠� Service 瀹炵幇绫�
  *
- * @author 鑺嬮亾婧愮爜
+ * @author 瓒呯骇绠$悊鍛�
  */
 @Service
 @Validated
@@ -82,11 +94,19 @@
     @Resource
     private HrmLeaveApplicationMapper leaveApplicationMapper;
     @Resource
+    private HrmOvertimeApplicationMapper overtimeApplicationMapper;
+    @Resource
+    private HrmSalaryStructureMapper salaryStructureMapper;
+    @Resource
     private HrmLeaveTypeConfigMapper leaveTypeConfigMapper;
     @Resource
     private HrmSalaryPaymentMapper salaryPaymentMapper;
     @Resource
     private HrmSalaryPaymentDetailMapper salaryPaymentDetailMapper;
+    @Resource
+    private HrmEmployeeScheduleMapper employeeScheduleMapper;
+    @Resource
+    private HrmShiftMapper shiftMapper;
 
     @Override
     public PageResult<HrmSalaryCalculationDO> getSalaryCalculationPage(HrmSalaryCalculationPageReqVO pageReqVO) {
@@ -130,7 +150,10 @@
                 if (existCalculations.size() > 1) {
                     throw exception(SALARY_CALCULATION_DUPLICATE, employee.getName(), period);
                 }
-                continue; // 宸插瓨鍦ㄥ垯璺宠繃
+                // 宸茬‘璁ゆ垨宸插彂鏀剧殑璁板綍涓嶅彲鑷姩鏀瑰啓锛涘緟纭璁板綍鍏佽閲嶇畻锛屼互鍚告敹鍚庢潵瀹℃壒閫氳繃鐨勫姞鐝��
+                if (existCalculations.get(0).getStatus() != 0) {
+                    continue;
+                }
             }
 
             // 2.2 鑾峰彇鍛樺伐钖叕妗f锛堟寜鍛樺伐ID鏌ヨ锛�
@@ -151,15 +174,20 @@
             // 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();
+            // 2.6 淇濆瓨锛氶娆$敓鎴愬崟鍙凤紱宸叉湁寰呯‘璁よ褰曞垯淇濈暀鍗曞彿骞惰鐩栭噸绠楃粨鏋�
+            if (CollUtil.isNotEmpty(existCalculations)) {
+                HrmSalaryCalculationDO existing = existCalculations.get(0);
+                calculation.setId(existing.getId());
+                calculation.setNo(existing.getNo());
+                salaryCalculationMapper.updateById(calculation);
+            } else {
+                String no = noRedisDAO.generateSalaryCalculationNo();
+                while (salaryCalculationMapper.selectByNo(no) != null) {
+                    no = noRedisDAO.generateSalaryCalculationNo();
+                }
+                calculation.setNo(no);
+                salaryCalculationMapper.insert(calculation);
             }
-            calculation.setNo(no);
-
-            // 2.7 淇濆瓨鏍哥畻璁板綍
-            salaryCalculationMapper.insert(calculation);
         }
     }
 
@@ -174,13 +202,14 @@
     }
 
     /**
-     * 鏋勫缓榛樿鐨勫憳宸ヨ柂閰。妗堬紙鍩轰簬鍛樺伐琛ㄧ殑鍩烘湰宸ヨ祫锛�
+     * 鏋勫缓榛樿鐨勫憳宸ヨ柂閰。妗堛�傚熀鏈伐璧勭粺涓�鍙栧憳宸ユ。妗堬紝缁╂晥宸ヨ祫榛樿涓嶈嚜鍔ㄧ敓鎴愩��
      */
     private HrmEmployeeSalaryDO buildDefaultEmployeeSalary(Long employeeId, HrmEmployeeDO employee) {
+        BigDecimal baseSalary = employee.getBaseSalary() != null ? employee.getBaseSalary() : BigDecimal.ZERO;
         return HrmEmployeeSalaryDO.builder()
                 .userId(employeeId) // 浣跨敤鍛樺伐ID
                 .salaryStructureId(employee.getSalaryStructureId())
-                .baseSalary(employee.getBaseSalary() != null ? employee.getBaseSalary() : BigDecimal.ZERO)
+                .baseSalary(baseSalary)
                 .performanceSalary(BigDecimal.ZERO)
                 .mealAllowance(BigDecimal.ZERO)
                 .transportAllowance(BigDecimal.ZERO)
@@ -204,17 +233,23 @@
         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;
+        OvertimeSalary overtimeSalary = calculateOvertimeSalary(employee, employeeSalary, period, baseSalary);
+        BigDecimal overtimePay = overtimeSalary.pay();
+
+        // 鎸夋帓鐝疮璁″嚭鍕ゅぉ鏁颁笌鎺掔彮鎬诲伐鏃讹紙鏃犳帓鐝椂鍑哄嫟澶╂暟鍥為��榛樿 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);
 
-        // 鑾峰彇绀句繚鍩烘暟锛堝憳宸ユ。妗堜腑鐨勪釜鎬у寲鍩烘暟锛�
+        // 鑾峰彇绀句繚鍩烘暟锛氬憳宸ョぞ淇濇。妗� > 鍛樺伐鍩烘湰宸ヨ祫
         BigDecimal socialSecurityBase = (socialSecurity != null && socialSecurity.getSocialSecurityBase() != null)
                 ? socialSecurity.getSocialSecurityBase() : baseSalary;
 
-        // 鑾峰彇鍏Н閲戝熀鏁帮紙鍛樺伐妗f涓殑涓�у寲鍩烘暟锛�
+        // 鑾峰彇鍏Н閲戝熀鏁帮細鍛樺伐绀句繚妗f > 鍛樺伐鍩烘湰宸ヨ祫
         BigDecimal housingFundBase = (socialSecurity != null && socialSecurity.getHousingFundBase() != null)
                 ? socialSecurity.getHousingFundBase() : baseSalary;
 
@@ -276,8 +311,9 @@
                 .otherDeduction(otherDeduction)
                 .leaveDeduction(leaveDeduction)
                 .actualSalary(actualSalary)
-                .workDays(22) // 榛樿宸ヤ綔鏃�
-                .overtimeHours(BigDecimal.ZERO)
+                .workDays(workDays) // 鎸夋帓鐝疮璁″簲鍑哄嫟澶╂暟锛屾棤鎺掔彮鏃跺洖閫� 22
+                .overtimeHours(overtimeSalary.hours())
+                .totalWorkHours(totalWorkHours)
                 .status(0) // 寰呯‘璁�
                 .build();
     }
@@ -332,6 +368,85 @@
         }
 
         return totalLeaveDeduction;
+    }
+
+    /**
+     * 姹囨�诲綋鏈堝鎵归�氳繃鐨勫姞鐝敵璇枫�傚皬鏃跺伐璧勬寜鍩烘湰宸ヨ祫 / 21.75 / 8 璁$畻锛�
+     * 宸ヤ綔鏃ラ粯璁� 1.5 鍊嶃�佷紤鎭棩榛樿 2 鍊嶃�佹硶瀹氳妭鍋囨棩榛樿 3 鍊嶏紝鍧囧彲鐢辫柂閰粨鏋勮鐩栥��
+     */
+    private OvertimeSalary calculateOvertimeSalary(HrmEmployeeDO employee, HrmEmployeeSalaryDO employeeSalary,
+                                                    String period, BigDecimal baseSalary) {
+        if (employee.getUserId() == null) {
+            return new OvertimeSalary(BigDecimal.ZERO, BigDecimal.ZERO);
+        }
+        YearMonth month = YearMonth.parse(period);
+        List<HrmOvertimeApplicationDO> applications = overtimeApplicationMapper.selectApprovedByUserAndDateRange(
+                employee.getUserId(), month.atDay(1), month.atEndOfMonth());
+        if (CollUtil.isEmpty(applications)) {
+            return new OvertimeSalary(BigDecimal.ZERO, BigDecimal.ZERO);
+        }
+        Long structureId = employeeSalary.getSalaryStructureId() != null
+                ? employeeSalary.getSalaryStructureId() : employee.getSalaryStructureId();
+        HrmSalaryStructureDO structure = structureId == null ? null : salaryStructureMapper.selectById(structureId);
+        BigDecimal workdayMultiplier = structure != null && structure.getOvertimePayRatio() != null
+                ? structure.getOvertimePayRatio() : HrmOvertimeTypeEnum.WORKDAY.getSalaryMultiplier();
+        BigDecimal restDayMultiplier = structure != null && structure.getRestDayOvertimePayRatio() != null
+                ? structure.getRestDayOvertimePayRatio() : HrmOvertimeTypeEnum.REST_DAY.getSalaryMultiplier();
+        BigDecimal legalHolidayMultiplier = structure != null && structure.getLegalHolidayOvertimePayRatio() != null
+                ? structure.getLegalHolidayOvertimePayRatio() : HrmOvertimeTypeEnum.LEGAL_HOLIDAY.getSalaryMultiplier();
+        BigDecimal hourlySalary = baseSalary.divide(new BigDecimal("21.75"), 8, RoundingMode.HALF_UP)
+                .divide(new BigDecimal("8"), 8, RoundingMode.HALF_UP);
+        BigDecimal totalHours = BigDecimal.ZERO;
+        BigDecimal totalPay = BigDecimal.ZERO;
+        for (HrmOvertimeApplicationDO application : applications) {
+            BigDecimal hours = application.getDuration() == null ? BigDecimal.ZERO : application.getDuration();
+            HrmOvertimeTypeEnum type = HrmOvertimeTypeEnum.valueOfType(application.getOvertimeType());
+            if (type == null || hours.signum() <= 0) {
+                continue;
+            }
+            BigDecimal multiplier = switch (type) {
+                case WORKDAY -> workdayMultiplier;
+                case REST_DAY -> restDayMultiplier;
+                case LEGAL_HOLIDAY -> legalHolidayMultiplier;
+            };
+            totalHours = totalHours.add(hours);
+            totalPay = totalPay.add(hourlySalary.multiply(hours).multiply(multiplier));
+        }
+        return new OvertimeSalary(totalHours.setScale(1, RoundingMode.HALF_UP),
+                totalPay.setScale(2, RoundingMode.HALF_UP));
+    }
+
+    private record OvertimeSalary(BigDecimal hours, BigDecimal pay) {}
+
+    private record ScheduleStat(int scheduledDays, BigDecimal totalWorkHours) {}
+
+    /**
+     * 鎸夋帓鐝〃绱鍛樺伐鐨勫嚭鍕ゅぉ鏁颁笌鎺掔彮鎬诲伐鏃躲��
+     * 浠呯粺璁℃湁鐝鐨勬帓鐝褰曪紝鐝鏍囧噯宸ユ椂涔嬪拰鍗充负鎺掔彮宸ユ椂銆�
+     */
+    private ScheduleStat calculateScheduleStat(Long employeeId, String period) {
+        YearMonth yearMonth = YearMonth.parse(period);
+        List<HrmEmployeeScheduleDO> schedules = employeeScheduleMapper.selectListByEmployeeIdAndPeriod(
+                employeeId, yearMonth.atDay(1), yearMonth.atEndOfMonth());
+        if (CollUtil.isEmpty(schedules)) {
+            return new ScheduleStat(0, BigDecimal.ZERO);
+        }
+        Set<Long> shiftIds = schedules.stream().map(HrmEmployeeScheduleDO::getShiftId)
+                .filter(Objects::nonNull).collect(Collectors.toSet());
+        Map<Long, HrmShiftDO> shiftMap = CollUtil.isEmpty(shiftIds) ? Collections.emptyMap()
+                : shiftMapper.selectBatchIds(shiftIds).stream()
+                        .collect(Collectors.toMap(HrmShiftDO::getId, shift -> shift, (a, b) -> a));
+        int scheduledDays = 0;
+        BigDecimal totalWorkHours = BigDecimal.ZERO;
+        for (HrmEmployeeScheduleDO schedule : schedules) {
+            HrmShiftDO shift = schedule.getShiftId() == null ? null : shiftMap.get(schedule.getShiftId());
+            if (shift == null) {
+                continue;
+            }
+            scheduledDays++;
+            totalWorkHours = totalWorkHours.add(shift.getWorkHours() != null ? shift.getWorkHours() : BigDecimal.ZERO);
+        }
+        return new ScheduleStat(scheduledDays, totalWorkHours.setScale(2, RoundingMode.HALF_UP));
     }
 
     @Override
@@ -577,4 +692,4 @@
         salaryCalculationMapper.deleteById(id);
     }
 
-}
\ No newline at end of file
+}

--
Gitblit v1.9.3