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

---
 sql/20261019_staff_salary_detail_add_leave_attendance_deduct.sql                 |    8 +-
 src/main/java/com/ruoyi/staff/pojo/StaffSalaryDetail.java                        |    6 ++
 src/main/resources/mapper/staff/StaffSalaryDetailMapper.xml                      |    2 
 src/main/java/com/ruoyi/staff/service/impl/StaffSalaryMainServiceImpl.java       |    3 +
 src/main/java/com/ruoyi/staff/service/impl/SchemeApplicableStaffServiceImpl.java |    6 +
 src/main/java/com/ruoyi/staff/dto/SalaryStat.java                                |    5 +
 sql/product-inventory-management-new-pro.sql                                     |    2 
 src/main/java/com/ruoyi/staff/excel/StaffSalaryExportData.java                   |    6 ++
 src/main/java/com/ruoyi/staff/service/impl/SalaryStatServiceImpl.java            |  117 +++++++++++++++++++++++++++++++++++++-
 9 files changed, 145 insertions(+), 10 deletions(-)

diff --git a/sql/20261019_staff_salary_detail_add_leave_attendance_deduct.sql b/sql/20261019_staff_salary_detail_add_leave_attendance_deduct.sql
index 34bd4d8..70b8ca2 100644
--- a/sql/20261019_staff_salary_detail_add_leave_attendance_deduct.sql
+++ b/sql/20261019_staff_salary_detail_add_leave_attendance_deduct.sql
@@ -8,7 +8,7 @@
     ADD COLUMN `absence_count` int NULL DEFAULT 0 COMMENT '缂哄嫟娆℃暟' AFTER `leave_count`,
     ADD COLUMN `late_count` int NULL DEFAULT 0 COMMENT '杩熷埌娆℃暟' AFTER `absence_count`,
     ADD COLUMN `early_count` int NULL DEFAULT 0 COMMENT '鏃╅��娆℃暟' AFTER `late_count`,
-    ADD COLUMN `manual_deduct` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '搴旀墸宸ヨ祫锛堟墜鍔ㄥ~鍐欙級' AFTER `early_count`;
-
-ALTER TABLE staff_salary_detail
-MODIFY COLUMN remark text NULL COMMENT '澶囨敞';
+    ADD COLUMN `manual_deduct` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '搴旀墸宸ヨ祫锛堟墜鍔ㄥ~鍐欙級' AFTER `early_count`,
+    ADD COLUMN `overtime_hours` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '鍔犵彮鏃堕暱锛堝皬鏃讹級' AFTER `hourly_salary`,
+    ADD COLUMN `overtime_pay` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '鍔犵彮宸ヨ祫锛堟墜鍔ㄥ~鍐欙級' AFTER `overtime_hours`,
+    MODIFY COLUMN remark text NULL COMMENT '澶囨敞';
diff --git a/sql/product-inventory-management-new-pro.sql b/sql/product-inventory-management-new-pro.sql
index 5e24716..8dc9b70 100644
--- a/sql/product-inventory-management-new-pro.sql
+++ b/sql/product-inventory-management-new-pro.sql
@@ -4666,6 +4666,8 @@
   `basic_salary` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '鍩烘湰宸ヨ祫',
   `piece_salary` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '璁′欢宸ヨ祫',
   `hourly_salary` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '璁℃椂宸ヨ祫',
+  `overtime_hours` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '鍔犵彮鏃堕暱锛堝皬鏃讹級',
+  `overtime_pay` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '鍔犵彮宸ヨ祫锛堟墜鍔ㄥ~鍐欙級',
   `other_income` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '鍏朵粬鏀跺叆',
   `social_personal` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '绀句繚涓汉',
   `fund_personal` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '鍏Н閲戜釜浜�',
diff --git a/src/main/java/com/ruoyi/staff/dto/SalaryStat.java b/src/main/java/com/ruoyi/staff/dto/SalaryStat.java
index f107a17..9f5f728 100644
--- a/src/main/java/com/ruoyi/staff/dto/SalaryStat.java
+++ b/src/main/java/com/ruoyi/staff/dto/SalaryStat.java
@@ -3,6 +3,8 @@
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
+import java.math.BigDecimal;
+
 /**
  * 鍛樺伐褰撴湀璇峰亣銆佽�冨嫟缁熻缁撴灉
  */
@@ -21,6 +23,9 @@
     @Schema(description = "鏃╅��娆℃暟")
     private Integer earlyCount;
 
+    @Schema(description = "鍔犵彮鏃堕暱锛堝皬鏃讹級")
+    private BigDecimal overtimeHours;
+
     @Schema(description = "璇峰亣鍘熷洜锛屽娆¤鍋囩敤锛涙嫾鎺�")
     private String leaveReason;
 }
diff --git a/src/main/java/com/ruoyi/staff/excel/StaffSalaryExportData.java b/src/main/java/com/ruoyi/staff/excel/StaffSalaryExportData.java
index 67a1d1b..b2986a2 100644
--- a/src/main/java/com/ruoyi/staff/excel/StaffSalaryExportData.java
+++ b/src/main/java/com/ruoyi/staff/excel/StaffSalaryExportData.java
@@ -32,6 +32,12 @@
     @Excel(name = "璁℃椂宸ヨ祫")
     private BigDecimal hourlySalary;
 
+    @Excel(name = "鍔犵彮鏃堕暱")
+    private BigDecimal overtimeHours;
+
+    @Excel(name = "鍔犵彮宸ヨ祫")
+    private BigDecimal overtimePay;
+
     @Excel(name = "鍏朵粬鏀跺叆")
     private BigDecimal otherIncome;
 
diff --git a/src/main/java/com/ruoyi/staff/pojo/StaffSalaryDetail.java b/src/main/java/com/ruoyi/staff/pojo/StaffSalaryDetail.java
index b9cd992..96709bf 100644
--- a/src/main/java/com/ruoyi/staff/pojo/StaffSalaryDetail.java
+++ b/src/main/java/com/ruoyi/staff/pojo/StaffSalaryDetail.java
@@ -56,6 +56,12 @@
     @Schema(description = "璁℃椂宸ヨ祫")
     private BigDecimal hourlySalary;
 
+    @Schema(description = "鍔犵彮鏃堕暱锛堝皬鏃讹級")
+    private BigDecimal overtimeHours;
+
+    @Schema(description = "鍔犵彮宸ヨ祫锛堟墜鍔ㄥ~鍐欙級")
+    private BigDecimal overtimePay;
+
     @Schema(description = "鍏朵粬鏀跺叆")
     private BigDecimal otherIncome;
 
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>
      * 鍏堟寜鐢宠浜烘煡鍑烘墍鏈夊凡閫氳繃鐨勮鍋囷紙璇峰亣绠$悊銆佽鍋囧鎵逛袱绉嶄笟鍔$被鍨嬮兘鍏煎锛夛紝
diff --git a/src/main/java/com/ruoyi/staff/service/impl/SchemeApplicableStaffServiceImpl.java b/src/main/java/com/ruoyi/staff/service/impl/SchemeApplicableStaffServiceImpl.java
index fe84134..c7ad83a 100644
--- a/src/main/java/com/ruoyi/staff/service/impl/SchemeApplicableStaffServiceImpl.java
+++ b/src/main/java/com/ruoyi/staff/service/impl/SchemeApplicableStaffServiceImpl.java
@@ -174,6 +174,8 @@
                 : calculateLatestDeptSocialSupplementAmount(staffOnJobDto);
         BigDecimal pieceSalary = normalizeMoney(getBigDecimal(map, "pieceSalary"));
         BigDecimal hourlySalary = normalizeMoney(getBigDecimal(map, "hourlySalary"));
+        // 鍔犵彮宸ヨ祫锛氭墜宸ュ~鍐欙紝涓嶆寜鍊嶇巼鑷姩璁$畻
+        BigDecimal overtimePay = normalizeMoney(getBigDecimal(map, "overtimePay"));
         BigDecimal socialPersonal = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
         BigDecimal fundPersonal = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
 
@@ -204,7 +206,7 @@
             }
         }
 
-        BigDecimal grossSalary = basicSalary.add(pieceSalary).add(hourlySalary).add(otherIncome);
+        BigDecimal grossSalary = basicSalary.add(pieceSalary).add(hourlySalary).add(overtimePay).add(otherIncome);
         BigDecimal salaryTax = TaxCalculator.calculateMonthlyTax(
                 grossSalary,
                 socialPersonal.add(socialSupplementAmount),
@@ -231,6 +233,8 @@
         map.put("basicSalary", basicSalary);
         map.put("pieceSalary", pieceSalary);
         map.put("hourlySalary", hourlySalary);
+        map.put("overtimeHours", salaryStat.getOvertimeHours());
+        map.put("overtimePay", overtimePay);
         map.put("otherIncome", otherIncome);
         map.put("socialPersonal", socialPersonal);
         map.put("fundPersonal", fundPersonal);
diff --git a/src/main/java/com/ruoyi/staff/service/impl/StaffSalaryMainServiceImpl.java b/src/main/java/com/ruoyi/staff/service/impl/StaffSalaryMainServiceImpl.java
index f15b9b9..f3974d6 100644
--- a/src/main/java/com/ruoyi/staff/service/impl/StaffSalaryMainServiceImpl.java
+++ b/src/main/java/com/ruoyi/staff/service/impl/StaffSalaryMainServiceImpl.java
@@ -210,6 +210,7 @@
         }
         map.put("pieceSalary", staffSalaryCalculateDto.getPieceSalary());
         map.put("hourlySalary", staffSalaryCalculateDto.getHourlySalary());
+        map.put("overtimePay", staffSalaryCalculateDto.getOvertimePay());
         map.put("otherIncome", staffSalaryCalculateDto.getOtherIncome());
         map.put("socialPersonal", staffSalaryCalculateDto.getSocialPersonal());
         map.put("fundPersonal", staffSalaryCalculateDto.getFundPersonal());
@@ -256,6 +257,8 @@
                 exportData.setBasicSalary(detail.getBasicSalary());
                 exportData.setPieceSalary(detail.getPieceSalary());
                 exportData.setHourlySalary(detail.getHourlySalary());
+                exportData.setOvertimeHours(detail.getOvertimeHours() == null ? BigDecimal.ZERO : detail.getOvertimeHours());
+                exportData.setOvertimePay(detail.getOvertimePay() == null ? BigDecimal.ZERO : detail.getOvertimePay());
                 exportData.setOtherIncome(detail.getOtherIncome());
                 exportData.setSocialPersonal(detail.getSocialPersonal());
                 exportData.setFundPersonal(detail.getFundPersonal());
diff --git a/src/main/resources/mapper/staff/StaffSalaryDetailMapper.xml b/src/main/resources/mapper/staff/StaffSalaryDetailMapper.xml
index f029b09..f3647df 100644
--- a/src/main/resources/mapper/staff/StaffSalaryDetailMapper.xml
+++ b/src/main/resources/mapper/staff/StaffSalaryDetailMapper.xml
@@ -13,6 +13,8 @@
         <result column="basic_salary" property="basicSalary" />
         <result column="piece_salary" property="pieceSalary" />
         <result column="hourly_salary" property="hourlySalary" />
+        <result column="overtime_hours" property="overtimeHours" />
+        <result column="overtime_pay" property="overtimePay" />
         <result column="other_income" property="otherIncome" />
         <result column="social_personal" property="socialPersonal" />
         <result column="fund_personal" property="fundPersonal" />

--
Gitblit v1.9.3