From 9e9f0e2d9f6fcce7dcea88b181b3c7e6069e8a1b Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期一, 02 十一月 2026 16:51:55 +0800
Subject: [PATCH] fix(hrm): 请假跨月按天分摊扣款,加班工资按薪酬结构倍率计算

---
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryCalculationServiceImpl.java |   87 ++++++++++++++++++++++++++++++++++++++++++-
 1 files changed, 85 insertions(+), 2 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 8be05ab..5e24cc8 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
@@ -6,6 +6,8 @@
 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;
@@ -16,6 +18,8 @@
 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;
@@ -39,8 +43,12 @@
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.YearMonth;
+import java.time.temporal.ChronoUnit;
+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;
@@ -96,6 +104,10 @@
     private HrmSalaryPaymentMapper salaryPaymentMapper;
     @Resource
     private HrmSalaryPaymentDetailMapper salaryPaymentDetailMapper;
+    @Resource
+    private HrmEmployeeScheduleMapper employeeScheduleMapper;
+    @Resource
+    private HrmShiftMapper shiftMapper;
 
     @Override
     public PageResult<HrmSalaryCalculationDO> getSalaryCalculationPage(HrmSalaryCalculationPageReqVO pageReqVO) {
@@ -225,6 +237,11 @@
         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);
@@ -295,8 +312,9 @@
                 .otherDeduction(otherDeduction)
                 .leaveDeduction(leaveDeduction)
                 .actualSalary(actualSalary)
-                .workDays(22) // 榛樿宸ヤ綔鏃�
+                .workDays(workDays) // 鎸夋帓鐝疮璁″簲鍑哄嫟澶╂暟锛屾棤鎺掔彮鏃跺洖閫� 22
                 .overtimeHours(overtimeSalary.hours())
+                .totalWorkHours(totalWorkHours)
                 .status(0) // 寰呯‘璁�
                 .build();
     }
@@ -341,7 +359,8 @@
         BigDecimal totalLeaveDeduction = BigDecimal.ZERO;
         for (HrmLeaveApplicationDO application : leaveApplications) {
             Integer leaveType = application.getLeaveType();
-            BigDecimal duration = application.getDuration() != null ? application.getDuration() : BigDecimal.ZERO;
+            // 璺ㄦ湀璇峰亣鎸夎惤鍦ㄦ湰鏍哥畻鏈堝唴鐨勫ぉ鏁版姌绠楋紝閬垮厤鏁存潯璇峰亣琚烦杩囨垨琚噸澶嶈鍏�
+            BigDecimal duration = resolveDurationInMonth(application, monthStart, monthEnd);
             BigDecimal deductionRatio = configMap.getOrDefault(leaveType, BigDecimal.ZERO);
 
             // 璇峰亣鎵f = 鏃ュ伐璧� 脳 璇峰亣澶╂暟 脳 鎵f姣斾緥
@@ -351,6 +370,39 @@
         }
 
         return totalLeaveDeduction;
+    }
+
+    /**
+     * 璁$畻涓�绗旇鍋囪惤鍦ㄦ湰鏍哥畻鏈堝唴鐨勮鍋囨椂闀�
+     *
+     * 1锛夎鍋囨暣娈甸兘鍦ㄦ湰鏈堝唴锛氱洿鎺ュ彇璇峰亣鍗曚笂鐨勬椂闀匡紱
+     * 2锛夎鍋囪法鏈堬細鎸夈�屾湰鏈堝唴鍗犵敤鐨勮嚜鐒舵棩 / 璇峰亣鎬昏嚜鐒舵棩銆嶆姌绠楋紝渚嬪 10-28 ~ 11-03 鍏� 7 澶╋紝
+     *    鍏朵腑 10 鏈堝崰 4 澶╋紝鍒� 10 鏈堟牳绠楁寜 鏃堕暱 脳 4 / 7 璁★紝11 鏈堟寜 鏃堕暱 脳 3 / 7 璁★紝閬垮厤閲嶅鎴栨紡绠椼��
+     */
+    private BigDecimal resolveDurationInMonth(HrmLeaveApplicationDO application,
+                                             LocalDateTime monthStart, LocalDateTime monthEnd) {
+        BigDecimal duration = application.getDuration() != null ? application.getDuration() : BigDecimal.ZERO;
+        LocalDateTime start = application.getStartTime();
+        LocalDateTime end = application.getEndTime();
+        if (start == null || end == null) {
+            return duration;
+        }
+        // 鏁存閮藉湪鏈湀鍐�
+        if (!start.isBefore(monthStart) && !end.isAfter(monthEnd)) {
+            return duration;
+        }
+        long totalDays = ChronoUnit.DAYS.between(start.toLocalDate(), end.toLocalDate()) + 1;
+        if (totalDays <= 0) {
+            return duration;
+        }
+        LocalDateTime overlapStart = start.isBefore(monthStart) ? monthStart : start;
+        LocalDateTime overlapEnd = end.isAfter(monthEnd) ? monthEnd : end;
+        if (overlapStart.isAfter(overlapEnd)) {
+            return BigDecimal.ZERO;
+        }
+        long overlapDays = ChronoUnit.DAYS.between(overlapStart.toLocalDate(), overlapEnd.toLocalDate()) + 1;
+        return duration.multiply(BigDecimal.valueOf(overlapDays))
+                .divide(BigDecimal.valueOf(totalDays), 4, RoundingMode.HALF_UP);
     }
 
     /**
@@ -401,6 +453,37 @@
 
     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
     @Transactional(rollbackFor = Exception.class)
     public void confirmSalaryCalculation(List<Long> ids) {

--
Gitblit v1.9.3