package com.klp.service.impl; import cn.hutool.core.bean.BeanUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.klp.common.core.domain.PageQuery; import com.klp.common.core.page.TableDataInfo; import com.klp.common.utils.StringUtils; import com.klp.domain.AttendanceRecords; import com.klp.domain.WmsAttendanceCheck; import com.klp.domain.WmsAttendanceRule; import com.klp.domain.bo.AttendanceCheckBo; import com.klp.domain.bo.AttendanceRecordsBo; import com.klp.domain.bo.WmsAttendanceCheckBo; import com.klp.domain.bo.WmsAttendanceScheduleBo; import com.klp.domain.vo.AttendanceRecordsVo; import com.klp.domain.vo.WmsAttendanceCheckVo; import com.klp.domain.vo.WmsAttendanceScheduleVo; import com.klp.mapper.WmsAttendanceCheckMapper; import com.klp.mapper.WmsAttendanceRuleMapper; import com.klp.service.IAttendanceRecordsService; import com.klp.service.IWmsAttendanceCheckService; import com.klp.service.IWmsAttendanceScheduleService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.time.Duration; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.ZoneId; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @RequiredArgsConstructor @Service public class WmsAttendanceCheckServiceImpl implements IWmsAttendanceCheckService { private static final List STATUS_SEVERITY = java.util.Arrays.asList("normal", "late_warn", "early_warn", "late_one", "early_one", "late_two", "early_two", "absent_half", "missed_start", "missed_end", "missed"); private static final long LATE_EARLY_MAX_SECONDS = 120 * 60L; private static final int BATCH_SIZE = 500; private final WmsAttendanceCheckMapper baseMapper; private final IWmsAttendanceScheduleService scheduleService; private final IAttendanceRecordsService attendanceRecordsService; private final WmsAttendanceRuleMapper ruleMapper; @Override public WmsAttendanceCheckVo queryById(Long checkId) { return baseMapper.selectVoById(checkId); } @Override public TableDataInfo queryPageList(WmsAttendanceCheckBo bo, PageQuery pageQuery) { LambdaQueryWrapper lqw = buildQueryWrapper(bo); Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); return TableDataInfo.build(result); } @Override public List queryList(WmsAttendanceCheckBo bo) { LambdaQueryWrapper lqw = buildQueryWrapper(bo); return baseMapper.selectVoList(lqw); } @Override public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { return baseMapper.deleteBatchIds(ids) > 0; } @Override @Transactional(rollbackFor = Exception.class) public Boolean updateByBo(WmsAttendanceCheckBo bo) { WmsAttendanceCheck update = BeanUtil.toBean(bo, WmsAttendanceCheck.class); return baseMapper.updateById(update) > 0; } private LambdaQueryWrapper buildQueryWrapper(WmsAttendanceCheckBo bo) { LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); lqw.eq(bo.getUserId() != null, WmsAttendanceCheck::getUserId, bo.getUserId()); lqw.like(StringUtils.isNotBlank(bo.getEmployeeName()), WmsAttendanceCheck::getEmployeeName, bo.getEmployeeName()); lqw.eq(bo.getShiftId() != null, WmsAttendanceCheck::getShiftId, bo.getShiftId()); lqw.ge(bo.getStartDate() != null, WmsAttendanceCheck::getWorkDate, bo.getStartDate()); lqw.le(bo.getEndDate() != null, WmsAttendanceCheck::getWorkDate, bo.getEndDate()); lqw.in(bo.getUserIds() != null, WmsAttendanceCheck::getUserId, bo.getUserIds()); if (Boolean.TRUE.equals(bo.getAbnormal())){ lqw.ne(WmsAttendanceCheck::getOverallStatus, "normal"); } lqw.orderByDesc(WmsAttendanceCheck::getWorkDate); return lqw; } /** * 检查员工考勤情况 * * @param bo 考勤检查业务对象,包含开始日期、结束日期和用户ID等信息 */ @Override @Transactional(rollbackFor = Exception.class) // 使用事务注解,确保方法内所有操作要么全部成功,要么全部回滚 public void checkAttendance(AttendanceCheckBo bo) { // 创建考勤计划业务对象,并设置查询参数 WmsAttendanceScheduleBo scheduleBo = new WmsAttendanceScheduleBo(); scheduleBo.setStartDate(bo.getStartDate()); // 设置开始日期 scheduleBo.setEndDate(bo.getEndDate()); // 设置结束日期 scheduleBo.setUserIds(bo.getUserIds()); // 设置用户ID列表 // 根据查询条件获取考勤计划列表 List schedules = scheduleService.queryList(scheduleBo); // 如果没有考勤计划,直接返回 if (schedules.isEmpty()) { return; } // 获取当前有效的考勤规则 WmsAttendanceRule rule = getActiveRule(); // // 将日期转换为LocalDate类型 // LocalDate startLocal = toLocalDate(bo.getStartDate()); // LocalDate endLocal = toLocalDate(bo.getEndDate()); // 创建待处理的考勤计划列表 List toProcess = new ArrayList<>(); // 遍历考勤计划,过滤掉无效数据 for (WmsAttendanceScheduleVo schedule : schedules) { if (schedule.getEmployeeName() == null) { // 跳过员工名为空的记录 continue; } if (schedule.getShiftStartTime() == null && schedule.getShiftEndTime() == null) { // 跳过上下班时间为空的记录 continue; } toProcess.add(schedule); // 添加到待处理列表 } // 如果过滤后没有有效的考勤计划,直接返回 if (toProcess.isEmpty()) { return; } // 倒班日重叠检测:若某天有正向跨天夜班(19:00→07:00),且次日存在同员工、同上班时间的反向跨天班(夜转白 19:00→13:00), // 则正向跨天班的下班时间校验跳过(实际下班按反向跨天班 13:00 算),但上班打卡仍然校验 // 创建映射表,记录被反向跨天班覆盖的记录 Map backwardCoveredByEmployee = new HashMap<>(); for (WmsAttendanceScheduleVo s : toProcess) { // 如果是跨天班且是反向跨天班,记录该员工在特定日期的上班时间 if (isCrossDayShift(s) && isBackwardCrossDay(s) && s.getEmployeeName() != null) { LocalDate workLd = toLocalDate(s.getWorkDate()); String key = s.getEmployeeName() + "_" + workLd.minusDays(1); backwardCoveredByEmployee.put(key, toLocalTime(s.getShiftStartTime())); } } // 记录需要覆盖下班检查的考勤计划ID Set endCheckOverridden = new HashSet<>(); for (WmsAttendanceScheduleVo s : toProcess) { // 如果是正向跨天班,检查是否有对应的反向跨天班覆盖 if (isCrossDayShift(s) && !isBackwardCrossDay(s) && s.getEmployeeName() != null && s.getShiftStartTime() != null) { LocalDate workLd = toLocalDate(s.getWorkDate()); String key = s.getEmployeeName() + "_" + workLd; LocalTime backwardStart = backwardCoveredByEmployee.get(key); if (backwardStart != null && backwardStart.equals(toLocalTime(s.getShiftStartTime()))) { endCheckOverridden.add(s.getScheduleId()); } } } // 获取受影响的用户ID集合 Set affectedUserIds = toProcess.stream() .map(WmsAttendanceScheduleVo::getUserId) .filter(Objects::nonNull) .collect(Collectors.toSet()); // 按员工预取考勤记录 Map> recordsByEmployee = prefetchAttendanceRecords(toProcess); // 获取所有待处理考勤计划的ID列表 List scheduleIds = toProcess.stream() .map(WmsAttendanceScheduleVo::getScheduleId) .collect(Collectors.toList()); // 批量删除这些考勤计划对应的考勤检查记录 batchDeleteByScheduleIds(scheduleIds); // 创建要插入的考勤检查记录列表 List checksToInsert = new ArrayList<>(toProcess.size()); // 遍历待处理的考勤计划,构建考勤检查记录 for (WmsAttendanceScheduleVo schedule : toProcess) { boolean crossDay = isCrossDayShift(schedule); boolean backward = isBackwardCrossDay(schedule); // 根据考勤计划和考勤记录,按天切片考勤记录 List records = sliceRecordsForDay( recordsByEmployee.get(schedule.getEmployeeName()), schedule.getWorkDate(), crossDay, backward); // 构建考勤检查记录并添加到插入列表 checksToInsert.add(buildCheck(schedule, rule, records, crossDay, endCheckOverridden)); } // 批量插入考勤检查记录 baseMapper.insertBatch(checksToInsert, BATCH_SIZE); } @Override @Transactional(rollbackFor = Exception.class) public void recalcContinuousAbsent(AttendanceCheckBo bo) { LocalDate startLocal = toLocalDate(bo.getStartDate()); LocalDate endLocal = toLocalDate(bo.getEndDate()); Set affectedUserIds; if (bo.getUserIds() != null && !bo.getUserIds().isEmpty()) { affectedUserIds = new HashSet<>(bo.getUserIds()); } else { List checks = baseMapper.selectList(Wrappers.lambdaQuery() .select(WmsAttendanceCheck::getUserId) .ge(WmsAttendanceCheck::getWorkDate, toDate(startLocal.atStartOfDay())) .le(WmsAttendanceCheck::getWorkDate, toDate(endLocal.atTime(LocalTime.of(23, 59, 59)))) .eq(WmsAttendanceCheck::getDelFlag, 0)); affectedUserIds = checks.stream() .map(WmsAttendanceCheck::getUserId) .collect(Collectors.toSet()); } updateContinuousAbsent(startLocal, endLocal, affectedUserIds); } private boolean isCrossDayShift(WmsAttendanceScheduleVo schedule) { return schedule.getShiftIsCrossDay() != null && schedule.getShiftIsCrossDay() == 1; } private boolean isBackwardCrossDay(WmsAttendanceScheduleVo schedule) { if (!isCrossDayShift(schedule)) return false; if (schedule.getShiftEndTime() == null) return false; return toLocalTime(schedule.getShiftEndTime()).getHour() >= 12; } private WmsAttendanceRule getActiveRule() { LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); wrapper.eq(WmsAttendanceRule::getDelFlag, 0); wrapper.last("LIMIT 1"); WmsAttendanceRule rule = ruleMapper.selectOne(wrapper); if (rule == null) { rule = new WmsAttendanceRule(); rule.setLateWarn(3L); rule.setLateOne(5L); rule.setLateTwo(15L); rule.setDeductOne(new BigDecimal("10")); rule.setDeductTwo(new BigDecimal("30")); rule.setAbsentHalfDay(15L); rule.setContinuousAbsentDays(3L); } return rule; } /** * 一次 SQL 批量查询所有员工在日期范围内的打卡记录(用 IN/eq 精确匹配,走索引)。 */ private Map> prefetchAttendanceRecords(List toProcess) { Map minByName = new HashMap<>(); Map maxByName = new HashMap<>(); for (WmsAttendanceScheduleVo s : toProcess) { String name = s.getEmployeeName(); LocalDate ld = toLocalDate(s.getWorkDate()); minByName.merge(name, ld, (a, b) -> a.isBefore(b) ? a : b); maxByName.merge(name, ld, (a, b) -> a.isAfter(b) ? a : b); } if (minByName.isEmpty()) { return new HashMap<>(); } LocalDate globalMin = minByName.values().stream().min(LocalDate::compareTo).get(); LocalDate globalMax = maxByName.values().stream().max(LocalDate::compareTo).get(); Date rangeStart = toDate(globalMin.atStartOfDay()); Date rangeEnd = toDate(globalMax.plusDays(1).atTime(LocalTime.of(23, 59, 59))); List allNames = new ArrayList<>(minByName.keySet()); List allRecords = attendanceRecordsService.queryListByEnamesAndDateRange( allNames, rangeStart, rangeEnd); Map> out = new HashMap<>(); for (AttendanceRecords r : allRecords) { out.computeIfAbsent(r.getEname(), k -> new ArrayList<>()).add(r); } for (String name : allNames) { out.computeIfAbsent(name, k -> new ArrayList<>()); } return out; } private List sliceRecordsForDay(List prefetched, Date workDate, boolean crossDay, boolean backward) { if (prefetched == null || prefetched.isEmpty()) { return new ArrayList<>(); } LocalDate ld = toLocalDate(workDate); LocalDateTime rangeStart = backward ? ld.minusDays(1).atStartOfDay() : ld.atStartOfDay(); LocalDateTime rangeEnd = crossDay && !backward ? ld.plusDays(1).atTime(LocalTime.of(23, 59, 59)) : ld.atTime(LocalTime.of(23, 59, 59)); return prefetched.stream() .filter(r -> { LocalDateTime ct = toLocalDateTime(r.getChecktime()); return ct != null && !ct.isBefore(rangeStart) && !ct.isAfter(rangeEnd); }) .sorted(Comparator.comparing(AttendanceRecords::getChecktime)) .collect(Collectors.toList()); } private void batchDeleteByScheduleIds(List scheduleIds) { if (scheduleIds.isEmpty()) { return; } for (int i = 0; i < scheduleIds.size(); i += BATCH_SIZE) { int to = Math.min(i + BATCH_SIZE, scheduleIds.size()); List chunk = scheduleIds.subList(i, to); baseMapper.delete(Wrappers.lambdaQuery() .in(WmsAttendanceCheck::getScheduleId, chunk)); } } /** * 构建考勤检查对象,根据排班信息和考勤记录生成考勤结果 * * @param schedule 排班信息对象 * @param rule 考勤规则对象 * @param records 考勤记录列表 * @param crossDay 是否跨天 * @param endCheckOverridden 已覆盖的结束检查集合 * @return 构建好的考勤检查对象 */ private WmsAttendanceCheck buildCheck(WmsAttendanceScheduleVo schedule, WmsAttendanceRule rule, List records, boolean crossDay, Set endCheckOverridden) { // 创建考勤检查对象 WmsAttendanceCheck check = new WmsAttendanceCheck(); // 设置排班基本信息 check.setScheduleId(schedule.getScheduleId()); check.setUserId(schedule.getUserId()); check.setEmployeeName(schedule.getEmployeeName()); check.setWorkDate(schedule.getWorkDate()); check.setShiftId(schedule.getShiftId()); check.setShiftName(schedule.getShiftName()); check.setShiftType(schedule.getShiftType()); // 判断是否有第二时段 boolean hasPeriod2 = schedule.getShiftStartTime2() != null && schedule.getShiftEndTime2() != null; // 判断是否为向后跨天 boolean backward = isBackwardCrossDay(schedule); // 处理时段设置 if (hasPeriod2) { // 设置第一时段开始时间,考虑跨天情况 check.setP1StartTime(crossDay && backward ? combinePrevDay(schedule.getWorkDate(), schedule.getShiftStartTime()) : combineTime(schedule.getWorkDate(), schedule.getShiftStartTime())); // 设置第一时段结束时间,考虑跨天情况 check.setP1EndTime(crossDay && !backward ? combineNextDay(schedule.getWorkDate(), schedule.getShiftEndTime()) : combineTime(schedule.getWorkDate(), schedule.getShiftEndTime())); check.setP2StartTime(combineTime(schedule.getWorkDate(), schedule.getShiftStartTime2())); check.setP2EndTime(combineTime(schedule.getWorkDate(), schedule.getShiftEndTime2())); } else { check.setP1StartTime(crossDay && backward ? combinePrevDay(schedule.getWorkDate(), schedule.getShiftStartTime()) : combineTime(schedule.getWorkDate(), schedule.getShiftStartTime())); check.setP1EndTime(crossDay && !backward ? combineNextDay(schedule.getWorkDate(), schedule.getShiftEndTime()) : combineTime(schedule.getWorkDate(), schedule.getShiftEndTime())); } if (records.isEmpty()) { check.setP1Status("missed"); if (hasPeriod2) { check.setP2Status("missed"); } check.setOverallStatus("absent_full"); check.setAbsentType("full_day"); return check; } if (hasPeriod2) { LocalTime p1End = toLocalTime(schedule.getShiftEndTime()); LocalTime p2Start = toLocalTime(schedule.getShiftStartTime2()); // 按理论时间范围分割:p1End之前、中间区间、p2Start之后 List beforeGap = new ArrayList<>(); List inGap = new ArrayList<>(); List afterGap = new ArrayList<>(); for (AttendanceRecords r : records) { LocalTime t = toLocalDateTime(r.getChecktime()).toLocalTime(); if (!t.isAfter(p1End)) { beforeGap.add(r); } else if (!t.isBefore(p2Start)) { afterGap.add(r); } else { inGap.add(r); } } List p1Records = new ArrayList<>(); List p2Records = new ArrayList<>(); boolean lunchPatternFound = false; // 检测[p1End, p2Start]区间内是否有午休打卡(取最远两条判断>10分钟,按最大连续间隔切开) if (inGap.size() >= 2) { LocalDateTime firstInGap = toLocalDateTime(inGap.get(0).getChecktime()); LocalDateTime lastInGap = toLocalDateTime(inGap.get(inGap.size() - 1).getChecktime()); if (Duration.between(firstInGap, lastInGap).getSeconds() > 600) { int splitIdx = 0; long maxGap = 0; for (int i = 0; i < inGap.size() - 1; i++) { LocalDateTime t1 = toLocalDateTime(inGap.get(i).getChecktime()); LocalDateTime t2 = toLocalDateTime(inGap.get(i + 1).getChecktime()); long gap = Duration.between(t1, t2).getSeconds(); if (gap > maxGap) { maxGap = gap; splitIdx = i; } if (gap > 600) { break; } } p1Records = new ArrayList<>(beforeGap); p1Records.addAll(inGap.subList(0, splitIdx + 1)); p2Records = new ArrayList<>(inGap.subList(splitIdx + 1, inGap.size())); p2Records.addAll(afterGap); lunchPatternFound = true; } } // 检测边界:beforeGap最后一条 与 inGap第一条 间隔>10分钟(提前下班+午饭回来) if (!lunchPatternFound && !beforeGap.isEmpty() && !inGap.isEmpty()) { LocalDateTime lastBefore = toLocalDateTime(beforeGap.get(beforeGap.size() - 1).getChecktime()); LocalDateTime firstIn = toLocalDateTime(inGap.get(0).getChecktime()); if (Duration.between(lastBefore, firstIn).getSeconds() > 600) { p1Records = new ArrayList<>(beforeGap); p2Records = new ArrayList<>(inGap); p2Records.addAll(afterGap); lunchPatternFound = true; } } // 检测边界:inGap最后一条 与 afterGap第一条 间隔>10分钟(午休晚归) if (!lunchPatternFound && !inGap.isEmpty() && !afterGap.isEmpty()) { LocalDateTime lastIn = toLocalDateTime(inGap.get(inGap.size() - 1).getChecktime()); LocalDateTime firstAfter = toLocalDateTime(afterGap.get(0).getChecktime()); if (Duration.between(lastIn, firstAfter).getSeconds() > 600) { p1Records = new ArrayList<>(beforeGap); p1Records.addAll(inGap); p2Records = new ArrayList<>(afterGap); lunchPatternFound = true; } } if (!lunchPatternFound) { // 没有午休打卡模式,回退原中值法 LocalTime split = LocalTime.of( (p1End.getHour() + p2Start.getHour()) / 2, (p1End.getMinute() + p2Start.getMinute()) / 2); p1Records = new ArrayList<>(); p2Records = new ArrayList<>(); for (AttendanceRecords r : records) { LocalTime t = toLocalDateTime(r.getChecktime()).toLocalTime(); if (t.isBefore(split)) { p1Records.add(r); } else { p2Records.add(r); } } } checkPeriod(check, rule, 1, p1Records, check.getP1StartTime(), check.getP1EndTime()); checkPeriod(check, rule, 2, p2Records, check.getP2StartTime(), check.getP2EndTime()); } else { List filtered = filterWindow(records, schedule.getWorkDate(), schedule.getShiftStartTime(), schedule.getShiftEndTime(), crossDay); Date actualEnd = endCheckOverridden.contains(schedule.getScheduleId()) ? null : check.getP1EndTime(); checkPeriod(check, rule, 1, filtered, check.getP1StartTime(), actualEnd); } calculateOverall(check, rule); return check; } private List filterWindow(List records, Date workDate, Date expectedStart, Date expectedEnd, boolean crossDay) { LocalDate ld = toLocalDate(workDate); LocalTime st = toLocalTime(expectedStart); LocalTime et = toLocalTime(expectedEnd); LocalDateTime windowStart; LocalDateTime windowEnd; boolean backward = crossDay && et != null && et.getHour() >= 12; if (crossDay && backward) { windowStart = st != null ? LocalDateTime.of(ld.minusDays(1), st).minusHours(2) : LocalDateTime.of(ld, et).minusHours(2); windowEnd = LocalDateTime.of(ld, et).plusHours(2); } else if (crossDay) { windowStart = LocalDateTime.of(ld, st).minusHours(2); windowEnd = LocalDateTime.of(ld.plusDays(1), et).plusHours(2); } else if (st != null && et != null) { windowStart = LocalDateTime.of(ld, st).minusHours(2); windowEnd = LocalDateTime.of(ld, et).plusHours(2); } else if (st != null) { windowStart = LocalDateTime.of(ld, st).minusHours(2); windowEnd = LocalDateTime.of(ld, st).plusHours(2); } else { windowStart = LocalDateTime.of(ld, et).minusHours(2); windowEnd = LocalDateTime.of(ld, et).plusHours(2); } LocalDateTime finalStart = windowStart; LocalDateTime finalEnd = windowEnd; return records.stream() .filter(r -> { LocalDateTime ct = toLocalDateTime(r.getChecktime()); return !ct.isBefore(finalStart) && !ct.isAfter(finalEnd); }) .collect(Collectors.toList()); } private void checkPeriod(WmsAttendanceCheck check, WmsAttendanceRule rule, int period, List periodRecords, Date expectedStart, Date expectedEnd) { // 旷工:0条打卡记录 if (periodRecords.isEmpty()) { if (period == 1) { check.setP1Status("missed"); } else { check.setP2Status("missed"); } return; } LocalDateTime expStart = toLocalDateTime(expectedStart); LocalDateTime expEnd = toLocalDateTime(expectedEnd); AttendanceRecords firstRec = periodRecords.get(0); AttendanceRecords lastRec = periodRecords.get(periodRecords.size() - 1); LocalDateTime firstCheck = toLocalDateTime(firstRec.getChecktime()); LocalDateTime lastCheck = toLocalDateTime(lastRec.getChecktime()); int lateMinutes = 0; int earlyMinutes = 0; BigDecimal deduct = BigDecimal.ZERO; String status = "normal"; boolean startMissed = false; boolean endMissed = false; // 上班检测:晚于理论上班时间俩小时以上 → 漏打卡 if (expStart != null) { long lateSecs = Duration.between(expStart, firstCheck).getSeconds(); if (lateSecs > LATE_EARLY_MAX_SECONDS) { startMissed = true; } else if (lateSecs > 0) { lateMinutes = (int) (lateSecs / 60); if (lateMinutes > rule.getAbsentHalfDay()) { status = "absent_half"; } else if (lateMinutes > rule.getLateOne()) { status = "late_two"; deduct = deduct.add(rule.getDeductTwo()); } else if (lateMinutes > rule.getLateWarn()) { status = "late_one"; deduct = deduct.add(rule.getDeductOne()); } else { status = "late_warn"; } } } // 下班检测:早于理论下班时间俩小时以上 → 漏打卡 if (expEnd != null && lastCheck.isBefore(expEnd)) { long earlySecs = Duration.between(lastCheck, expEnd).getSeconds(); if (earlySecs > LATE_EARLY_MAX_SECONDS) { endMissed = true; } else if (earlySecs > 0) { earlyMinutes = (int) (earlySecs / 60); if (earlyMinutes > rule.getAbsentHalfDay()) { status = maxSeverity(status, "absent_half"); } else if (earlyMinutes > rule.getLateOne()) { status = maxSeverity(status, "early_two"); deduct = deduct.add(rule.getDeductTwo()); } else if (earlyMinutes > rule.getLateWarn()) { status = maxSeverity(status, "early_one"); deduct = deduct.add(rule.getDeductOne()); } else { if ("normal".equals(status)) { status = "early_warn"; } } } } // 上班漏打卡或下班漏打卡会覆盖原有状态 if (startMissed && endMissed) { status = "missed"; } else if (startMissed) { status = maxSeverity(status, "missed_start"); } else if (endMissed) { status = maxSeverity(status, "missed_end"); } if (period == 1) { check.setP1FirstCheck(startMissed ? null : firstRec.getChecktime()); check.setP1LastCheck(endMissed ? null : lastRec.getChecktime()); check.setP1LateMinutes(lateMinutes); check.setP1EarlyMinutes(earlyMinutes); check.setP1Status(status); check.setP1Deduct(deduct); } else { check.setP2FirstCheck(startMissed ? null : firstRec.getChecktime()); check.setP2LastCheck(endMissed ? null : lastRec.getChecktime()); check.setP2LateMinutes(lateMinutes); check.setP2EarlyMinutes(earlyMinutes); check.setP2Status(status); check.setP2Deduct(deduct); } } private String maxSeverity(String a, String b) { int ai = STATUS_SEVERITY.indexOf(a); int bi = STATUS_SEVERITY.indexOf(b); return STATUS_SEVERITY.get(Math.max(ai, bi)); } private void calculateOverall(WmsAttendanceCheck check, WmsAttendanceRule rule) { BigDecimal total = BigDecimal.ZERO; boolean hasAbsentHalf = false; if (check.getP1Deduct() != null) total = total.add(check.getP1Deduct()); if (check.getP2Deduct() != null) total = total.add(check.getP2Deduct()); check.setTotalDeduct(total); if ("absent_half".equals(check.getP1Status()) || "absent_half".equals(check.getP2Status())) { hasAbsentHalf = true; } boolean p1Missed = "missed".equals(check.getP1Status()); boolean p2Missed = check.getP2Status() != null && "missed".equals(check.getP2Status()); boolean hasP2 = check.getP2StartTime() != null; if (p1Missed) { if (hasP2 && !p2Missed) { check.setAbsentType("half_day"); check.setOverallStatus("absent_half"); return; } else { check.setAbsentType("full_day"); check.setOverallStatus("absent_full"); return; } } if (p2Missed) { check.setAbsentType("half_day"); check.setOverallStatus("absent_half"); return; } if (check.getP1StartTime() == null && check.getP2StartTime() == null) { check.setAbsentType("full_day"); check.setOverallStatus("absent_full"); return; } if (hasAbsentHalf) { check.setAbsentType("half_day"); check.setOverallStatus("absent_half"); return; } boolean abnormal = !"normal".equals(check.getP1Status()) || (check.getP2Status() != null && !"normal".equals(check.getP2Status())); check.setOverallStatus(abnormal ? "abnormal" : "normal"); } private void updateContinuousAbsent(LocalDate startDate, LocalDate endDate, Set affectedUserIds) { if (affectedUserIds.isEmpty()) { return; } List checks = baseMapper.selectList(Wrappers.lambdaQuery() .in(WmsAttendanceCheck::getUserId, affectedUserIds) .ge(WmsAttendanceCheck::getWorkDate, toDate(startDate.atStartOfDay())) .le(WmsAttendanceCheck::getWorkDate, toDate(endDate.atTime(LocalTime.of(23, 59, 59)))) .eq(WmsAttendanceCheck::getDelFlag, 0)); if (checks.isEmpty()) { return; } List userIds = checks.stream() .map(WmsAttendanceCheck::getUserId) .distinct() .collect(Collectors.toList()); Map> historyByUserDate = loadAbsentHistoryByUser(userIds, endDate); List toUpdate = new ArrayList<>(); for (WmsAttendanceCheck check : checks) { if (check.getAbsentType() != null) { int continuous = countContinuousAbsentCached( check.getUserId(), toLocalDate(check.getWorkDate()), historyByUserDate); check.setContinuousAbsentDays(continuous); toUpdate.add(check); } } if (!toUpdate.isEmpty()) { baseMapper.updateBatchById(toUpdate, BATCH_SIZE); } } /** * 加载各员工在 endDate 及之前的考勤结果,用于内存计算连续旷工(与按天 selectOne 结果一致)。 */ private Map> loadAbsentHistoryByUser(List userIds, LocalDate endDate) { Map> out = new HashMap<>(); LocalDate startDate = endDate.minusDays(100); Date start = toDate(startDate.atStartOfDay()); Date end = toDate(endDate.atTime(LocalTime.of(23, 59, 59))); for (int i = 0; i < userIds.size(); i += BATCH_SIZE) { int to = Math.min(i + BATCH_SIZE, userIds.size()); List chunk = userIds.subList(i, to); List rows = baseMapper.selectList(Wrappers.lambdaQuery() .select(WmsAttendanceCheck::getUserId, WmsAttendanceCheck::getWorkDate, WmsAttendanceCheck::getAbsentType, WmsAttendanceCheck::getCheckId, WmsAttendanceCheck::getDelFlag) .in(WmsAttendanceCheck::getUserId, chunk) .ge(WmsAttendanceCheck::getWorkDate, start) .le(WmsAttendanceCheck::getWorkDate, end) .eq(WmsAttendanceCheck::getDelFlag, 0)); for (WmsAttendanceCheck row : rows) { LocalDate wd = toLocalDate(row.getWorkDate()); out.computeIfAbsent(row.getUserId(), k -> new HashMap<>()).put(wd, row); } } return out; } private int countContinuousAbsentCached(Long userId, LocalDate workDate, Map> historyByUserDate) { Map userMap = historyByUserDate.get(userId); if (userMap == null) { return 0; } int count = 0; LocalDate date = workDate.minusDays(1); while (true) { WmsAttendanceCheck prev = userMap.get(date); if (prev != null && prev.getAbsentType() != null) { count++; date = date.minusDays(1); } else { break; } } return count; } private static LocalDate toLocalDate(Date date) { return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); } private static LocalTime toLocalTime(Date date) { if (date == null) return null; return date.toInstant().atZone(ZoneId.systemDefault()).toLocalTime(); } private static LocalDateTime toLocalDateTime(Date date) { if (date == null) return null; return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime(); } private static Date toDate(LocalDateTime ldt) { return Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant()); } private static Date combineTime(Date workDate, Date shiftTime) { if (workDate == null || shiftTime == null) return null; LocalDate ld = toLocalDate(workDate); LocalTime lt = toLocalTime(shiftTime); return toDate(LocalDateTime.of(ld, lt)); } private static Date combineNextDay(Date workDate, Date shiftTime) { if (workDate == null || shiftTime == null) return null; LocalDate ld = toLocalDate(workDate).plusDays(1); LocalTime lt = toLocalTime(shiftTime); return toDate(LocalDateTime.of(ld, lt)); } private static Date combinePrevDay(Date workDate, Date shiftTime) { if (workDate == null || shiftTime == null) return null; LocalDate ld = toLocalDate(workDate).minusDays(1); LocalTime lt = toLocalTime(shiftTime); return toDate(LocalDateTime.of(ld, lt)); } }