From ef1052e7bf9bf1765d61776bc49ce69a23c6cdee Mon Sep 17 00:00:00 2001
From: yuan <123@>
Date: 星期二, 20 十月 2026 00:25:29 +0800
Subject: [PATCH] feat: 添加加班时长和加班工资字段到员工工资明细

---
 src/main/java/com/ruoyi/staff/service/impl/SalaryStatServiceImpl.java |  117 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 files changed, 112 insertions(+), 5 deletions(-)

diff --git a/src/main/java/com/ruoyi/staff/service/impl/SalaryStatServiceImpl.java b/src/main/java/com/ruoyi/staff/service/impl/SalaryStatServiceImpl.java
index 85df88c..a854fd8 100644
--- a/src/main/java/com/ruoyi/staff/service/impl/SalaryStatServiceImpl.java
+++ b/src/main/java/com/ruoyi/staff/service/impl/SalaryStatServiceImpl.java
@@ -10,20 +10,33 @@
 import com.ruoyi.project.system.domain.SysUser;
 import com.ruoyi.project.system.mapper.SysUserMapper;
 import com.ruoyi.staff.dto.SalaryStat;
+import com.ruoyi.staff.mapper.PersonalAttendanceLocationConfigMapper;
 import com.ruoyi.staff.mapper.PersonalAttendanceRecordsMapper;
+import com.ruoyi.staff.mapper.PersonalShiftMapper;
+import com.ruoyi.staff.pojo.PersonalAttendanceLocationConfig;
 import com.ruoyi.staff.pojo.PersonalAttendanceRecords;
+import com.ruoyi.staff.pojo.PersonalShift;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.Duration;
 import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
 import java.time.YearMonth;
 import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 /**
  * 鍛樺伐褰撴湀璇峰亣娆℃暟銆佺己鍕ゆ鏁般�佽繜鍒版鏁般�佹棭閫�娆℃暟缁熻
@@ -61,14 +74,20 @@
     private static final String[] IGNORE_LABEL_KEYWORDS = {"鏃堕棿", "鏃ユ湡", "鐢宠浜�", "閮ㄩ棬", "闄勪欢", "鍥剧墖", "涓婁紶"};
     /** 璇峰亣鍘熷洜瀛楁鍏抽敭瀛楋紝鍛戒腑鍒欎紭鍏堜綔涓鸿鍋囧師鍥� */
     private static final String[] REASON_LABEL_KEYWORDS = {"鍘熷洜", "浜嬬敱", "鐞嗙敱", "璇存槑"};
+    /** 鍔犵彮璧风畻鍒嗛挓鏁帮細鏃╁埌銆佹櫄璧板潎闇�婊� 60 鍒嗛挓鎵嶈鍏ュ姞鐝椂闀� */
+    private static final long OVERTIME_MIN_MINUTES = 60L;
+    /** 鍒嗛挓鎹㈢畻灏忔椂鐨勯櫎鏁� */
+    private static final BigDecimal MINUTES_PER_HOUR = BigDecimal.valueOf(60);
     private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
 
     private final PersonalAttendanceRecordsMapper personalAttendanceRecordsMapper;
+    private final PersonalShiftMapper personalShiftMapper;
+    private final PersonalAttendanceLocationConfigMapper personalAttendanceLocationConfigMapper;
     private final ApprovalInstanceMapper approvalInstanceMapper;
     private final SysUserMapper sysUserMapper;
 
     /**
-     * 缁熻鍛樺伐鎸囧畾鏈堜唤鐨勮鍋囨鏁般�佺己鍕ゆ鏁般�佽繜鍒版鏁般�佹棭閫�娆℃暟鍜岃鍋囧師鍥�
+     * 缁熻鍛樺伐鎸囧畾鏈堜唤鐨勮鍋囨鏁般�佺己鍕ゆ鏁般�佽繜鍒版鏁般�佹棭閫�娆℃暟銆佸姞鐝椂闀垮拰璇峰亣鍘熷洜
      *
      * @param staffOnJobId staff_on_job 琛ㄤ富閿�
      * @param staffName    鍛樺伐濮撳悕锛堢敤浜庡尮閰嶅鎵圭敵璇蜂汉锛�
@@ -81,12 +100,15 @@
         stat.setAbsenceCount(0);
         stat.setLateCount(0);
         stat.setEarlyCount(0);
+        stat.setOvertimeHours(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP));
         stat.setLeaveReason(null);
         YearMonth month = parseMonth(salaryMonth);
         if (staffOnJobId == null || month == null) {
             return stat;
         }
-        fillAttendanceStat(stat, staffOnJobId, month);
+        List<PersonalAttendanceRecords> records = listAttendanceRecords(staffOnJobId, month);
+        fillAttendanceStat(stat, records);
+        fillOvertimeStat(stat, staffOnJobId, month, records);
         List<ApprovalInstance> leaveList = listLeaveInstances(staffName, month);
         stat.setLeaveCount(leaveList.size());
         stat.setLeaveReason(joinLeaveReason(leaveList));
@@ -94,14 +116,20 @@
     }
 
     /**
-     * 鑰冨嫟缁熻锛氱己鍕ゆ鏁般�佽繜鍒版鏁般�佹棭閫�娆℃暟锛堣繜鍒版棭閫�鍚屾椂璁″叆涓よ竟锛�
+     * 鏌ヨ鍛樺伐鎸囧畾鏈堜唤鐨勬墦鍗¤褰�
      */
-    private void fillAttendanceStat(SalaryStat stat, Long staffOnJobId, YearMonth month) {
-        List<PersonalAttendanceRecords> records = personalAttendanceRecordsMapper.selectList(
+    private List<PersonalAttendanceRecords> listAttendanceRecords(Long staffOnJobId, YearMonth month) {
+        return personalAttendanceRecordsMapper.selectList(
                 new LambdaQueryWrapper<PersonalAttendanceRecords>()
                         .eq(PersonalAttendanceRecords::getStaffOnJobId, staffOnJobId)
                         .ge(PersonalAttendanceRecords::getDate, month.atDay(1))
                         .le(PersonalAttendanceRecords::getDate, month.atEndOfMonth()));
+    }
+
+    /**
+     * 鑰冨嫟缁熻锛氱己鍕ゆ鏁般�佽繜鍒版鏁般�佹棭閫�娆℃暟锛堣繜鍒版棭閫�鍚屾椂璁″叆涓よ竟锛�
+     */
+    private void fillAttendanceStat(SalaryStat stat, List<PersonalAttendanceRecords> records) {
         int absenceCount = 0;
         int lateCount = 0;
         int earlyCount = 0;
@@ -127,6 +155,85 @@
     }
 
     /**
+     * 鍔犵彮鏃堕暱缁熻锛�
+     * 鎸夊憳宸ュ綋鏈堟瘡澶╂帓鐝搴旂彮娆$殑涓婁笅鐝椂闂达紝涓庡綋澶╃殑鎵撳崱鏃堕棿姣旇緝锛�
+     * 鏃╁埌锛堟帓鐝笂鐝椂闂� - 瀹為檯鎵撳崱涓婄彮鏃堕棿锛夋弧 60 鍒嗛挓鎵嶈鍏ワ紝
+     * 鏅氳蛋锛堝疄闄呮墦鍗′笅鐝椂闂� - 鎺掔彮涓嬬彮鏃堕棿锛夋弧 60 鍒嗛挓鎵嶈鍏ワ紝
+     * 涓ゆ鍒嗛挓鏁扮疮鍔犲悗鎹㈢畻鎴愬皬鏃讹紝淇濈暀涓や綅灏忔暟銆�
+     */
+    private void fillOvertimeStat(SalaryStat stat, Long staffOnJobId, YearMonth month,
+                                  List<PersonalAttendanceRecords> records) {
+        stat.setOvertimeHours(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP));
+        if (records.isEmpty()) {
+            return;
+        }
+        List<PersonalShift> shifts = personalShiftMapper.selectList(
+                new LambdaQueryWrapper<PersonalShift>()
+                        .eq(PersonalShift::getStaffOnJobId, staffOnJobId)
+                        .isNotNull(PersonalShift::getPersonalAttendanceLocationConfigId)
+                        .ge(PersonalShift::getWorkTime, month.atDay(1).atStartOfDay())
+                        .lt(PersonalShift::getWorkTime, month.plusMonths(1).atDay(1).atStartOfDay()));
+        if (shifts.isEmpty()) {
+            return;
+        }
+        Set<Integer> configIds = shifts.stream()
+                .map(PersonalShift::getPersonalAttendanceLocationConfigId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+        if (configIds.isEmpty()) {
+            return;
+        }
+        Map<Integer, PersonalAttendanceLocationConfig> configMap = personalAttendanceLocationConfigMapper
+                .selectBatchIds(configIds).stream()
+                .collect(Collectors.toMap(PersonalAttendanceLocationConfig::getId, config -> config,
+                        (first, second) -> first));
+        Map<LocalDate, PersonalAttendanceRecords> recordMap = records.stream()
+                .filter(record -> record.getDate() != null)
+                .collect(Collectors.toMap(PersonalAttendanceRecords::getDate, record -> record,
+                        (first, second) -> first));
+        Map<LocalDate, PersonalShift> shiftMap = new HashMap<>();
+        for (PersonalShift shift : shifts) {
+            if (shift.getWorkTime() != null) {
+                shiftMap.putIfAbsent(shift.getWorkTime().toLocalDate(), shift);
+            }
+        }
+        long overtimeMinutes = 0L;
+        for (Map.Entry<LocalDate, PersonalShift> entry : shiftMap.entrySet()) {
+            LocalDate workDate = entry.getKey();
+            PersonalAttendanceRecords record = recordMap.get(workDate);
+            PersonalAttendanceLocationConfig config =
+                    configMap.get(entry.getValue().getPersonalAttendanceLocationConfigId());
+            if (record == null || config == null) {
+                continue;
+            }
+            LocalTime startAt = config.getStartAt();
+            LocalTime endAt = config.getEndAt();
+            if (startAt != null && record.getWorkStartAt() != null) {
+                long earlyMinutes = Duration.between(record.getWorkStartAt(),
+                        LocalDateTime.of(workDate, startAt)).toMinutes();
+                if (earlyMinutes >= OVERTIME_MIN_MINUTES) {
+                    overtimeMinutes += earlyMinutes;
+                }
+            }
+            if (endAt != null && record.getWorkEndAt() != null) {
+                LocalDateTime scheduledEnd = LocalDateTime.of(workDate, endAt);
+                // 璺ㄥぉ鐝锛堝 22:00-娆℃棩 06:00锛変笅鐝椂闂存寜娆℃棩璁$畻
+                if (startAt == null || !endAt.isAfter(startAt)) {
+                    scheduledEnd = scheduledEnd.plusDays(1);
+                }
+                long lateMinutes = Duration.between(scheduledEnd, record.getWorkEndAt()).toMinutes();
+                if (lateMinutes >= OVERTIME_MIN_MINUTES) {
+                    overtimeMinutes += lateMinutes;
+                }
+            }
+        }
+        if (overtimeMinutes > 0) {
+            stat.setOvertimeHours(BigDecimal.valueOf(overtimeMinutes)
+                    .divide(MINUTES_PER_HOUR, 2, RoundingMode.HALF_UP));
+        }
+    }
+
+    /**
      * 鏌ヨ鍛樺伐褰掑睘褰撴湀鐨勫凡閫氳繃璇峰亣鐢宠锛屾寜璇峰亣鏃堕棿鍗囧簭
      * <p>
      * 鍏堟寜鐢宠浜烘煡鍑烘墍鏈夊凡閫氳繃鐨勮鍋囷紙璇峰亣绠$悊銆佽鍋囧鎵逛袱绉嶄笟鍔$被鍨嬮兘鍏煎锛夛紝

--
Gitblit v1.9.3