| | |
| | | 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; |
| | | |
| | | /** |
| | | * 员工当月请假次数、缺勤次数、迟到次数、早退次数统计 |
| | |
| | | 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 员工姓名(用于匹配审批申请人) |
| | |
| | | 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)); |
| | |
| | | } |
| | | |
| | | /** |
| | | * 考勤统计:缺勤次数、迟到次数、早退次数(迟到早退同时计入两边) |
| | | * 查询员工指定月份的打卡记录 |
| | | */ |
| | | 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; |
| | |
| | | } |
| | | |
| | | /** |
| | | * 加班时长统计: |
| | | * 按员工当月每天排班对应班次的上下班时间,与当天的打卡时间比较; |
| | | * 早到(排班上班时间 - 实际打卡上班时间)满 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> |
| | | * 先按申请人查出所有已通过的请假(请假管理、请假审批两种业务类型都兼容), |