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