From 5c4be6de6ec78f60e65b3fc0e21b094cb6b4787a Mon Sep 17 00:00:00 2001 From: Joshi <3040996759@qq.com> Date: Thu, 14 May 2026 17:13:26 +0800 Subject: [PATCH] =?UTF-8?q?perf(attendance):=20=E4=BC=98=E5=8C=96=E8=80=83?= =?UTF-8?q?=E5=8B=A4=E5=AF=B9=E6=AF=94=E9=80=BB=E8=BE=91=E4=BB=A5=E5=8F=8A?= =?UTF-8?q?=E6=80=A7=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现批量删除和插入操作,减少数据库交互次数 - 预加载员工打卡记录,避免按排班逐条查询的性能问题 - 添加跨天班次标识字段,简化跨天判断逻辑 - 使用缓存机制计算连续旷工天数,替代逐天查询 - 优化过滤窗口逻辑,提高数据处理效率 - 添加批处理大小限制,防止单次操作数据量过大 --- .../domain/vo/WmsAttendanceScheduleVo.java | 5 + .../impl/WmsAttendanceCheckServiceImpl.java | 217 +++++++++++++----- .../klp/WmsAttendanceScheduleMapper.xml | 3 +- 3 files changed, 172 insertions(+), 53 deletions(-) diff --git a/klp-wms/src/main/java/com/klp/domain/vo/WmsAttendanceScheduleVo.java b/klp-wms/src/main/java/com/klp/domain/vo/WmsAttendanceScheduleVo.java index 07c332d7..8b736838 100644 --- a/klp-wms/src/main/java/com/klp/domain/vo/WmsAttendanceScheduleVo.java +++ b/klp-wms/src/main/java/com/klp/domain/vo/WmsAttendanceScheduleVo.java @@ -121,4 +121,9 @@ public class WmsAttendanceScheduleVo { @ExcelProperty(value = "工时") private java.math.BigDecimal workHours; + /** + * 是否跨天 + */ + private Integer shiftIsCrossDay; + } diff --git a/klp-wms/src/main/java/com/klp/service/impl/WmsAttendanceCheckServiceImpl.java b/klp-wms/src/main/java/com/klp/service/impl/WmsAttendanceCheckServiceImpl.java index 5b0e29e9..45b4179d 100644 --- a/klp-wms/src/main/java/com/klp/service/impl/WmsAttendanceCheckServiceImpl.java +++ b/klp-wms/src/main/java/com/klp/service/impl/WmsAttendanceCheckServiceImpl.java @@ -35,13 +35,20 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.Date; +import java.util.HashMap; import java.util.List; +import java.util.Map; 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"); + + private static final int BATCH_SIZE = 500; + private final WmsAttendanceCheckMapper baseMapper; private final IWmsAttendanceScheduleService scheduleService; private final IAttendanceRecordsService attendanceRecordsService; @@ -98,36 +105,45 @@ public class WmsAttendanceCheckServiceImpl implements IWmsAttendanceCheckService LocalDate startLocal = toLocalDate(bo.getStartDate()); LocalDate endLocal = toLocalDate(bo.getEndDate()); + List toProcess = new ArrayList<>(); for (WmsAttendanceScheduleVo schedule : schedules) { - if (schedule.getEmployeeName() == null || schedule.getShiftStartTime() == null) { + if (schedule.getEmployeeName() == null) { continue; } - - boolean crossDay = isCrossDayShift(schedule); - List records = getRecords(schedule.getEmployeeName(), schedule.getWorkDate(), crossDay); - - WmsAttendanceCheck check = buildCheck(schedule, rule, records); - - baseMapper.delete(Wrappers.lambdaQuery() - .eq(WmsAttendanceCheck::getScheduleId, schedule.getScheduleId())); - - baseMapper.insert(check); + if (schedule.getShiftStartTime() == null && schedule.getShiftEndTime() == null) { + continue; + } + toProcess.add(schedule); } + if (toProcess.isEmpty()) { + return; + } + + Map> recordsByEmployee = prefetchAttendanceRecords(toProcess); + + 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); + List records = sliceRecordsForDay( + recordsByEmployee.get(schedule.getEmployeeName()), + schedule.getWorkDate(), + crossDay); + checksToInsert.add(buildCheck(schedule, rule, records)); + } + + baseMapper.insertBatch(checksToInsert, BATCH_SIZE); + updateContinuousAbsent(startLocal, endLocal); } private boolean isCrossDayShift(WmsAttendanceScheduleVo schedule) { - if (schedule.getShiftStartTime() == null || schedule.getShiftEndTime() == null) { - return false; - } - boolean hasPeriod2 = schedule.getShiftStartTime2() != null && schedule.getShiftEndTime2() != null; - if (hasPeriod2) { - return false; - } - LocalTime start = toLocalTime(schedule.getShiftStartTime()); - LocalTime end = toLocalTime(schedule.getShiftEndTime()); - return end.isBefore(start); + return schedule.getShiftIsCrossDay() != null && schedule.getShiftIsCrossDay() == 1; } private WmsAttendanceRule getActiveRule() { @@ -148,16 +164,33 @@ public class WmsAttendanceCheckServiceImpl implements IWmsAttendanceCheckService return rule; } - private List getRecords(String employeeName, Date workDate, boolean crossDay) { + /** + * 按员工姓名合并时间范围,每人只查一次打卡(与原先按排班逐条查询结果一致)。 + */ + 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); + } + Map> out = new HashMap<>(minByName.size() * 2); + for (Map.Entry e : minByName.entrySet()) { + String name = e.getKey(); + LocalDate minLd = e.getValue(); + LocalDate maxLd = maxByName.get(name); + out.put(name, fetchRecordsForNameRange(name, minLd, maxLd)); + } + return out; + } + + private List fetchRecordsForNameRange(String employeeName, LocalDate minLd, LocalDate maxLd) { AttendanceRecordsBo recordsBo = new AttendanceRecordsBo(); recordsBo.setEname(employeeName); - LocalDate ld = toLocalDate(workDate); - recordsBo.setChecktimeStart(toDate(ld.atStartOfDay())); - if (crossDay) { - recordsBo.setChecktimeEnd(toDate(ld.plusDays(1).atTime(LocalTime.of(23, 59, 59)))); - } else { - recordsBo.setChecktimeEnd(toDate(ld.atTime(LocalTime.of(23, 59, 59)))); - } + recordsBo.setChecktimeStart(toDate(minLd.atStartOfDay())); + recordsBo.setChecktimeEnd(toDate(maxLd.plusDays(1).atTime(LocalTime.of(23, 59, 59)))); List voList = attendanceRecordsService.queryList(recordsBo); return voList.stream() .map(v -> { @@ -171,6 +204,39 @@ public class WmsAttendanceCheckServiceImpl implements IWmsAttendanceCheckService .collect(Collectors.toList()); } + /** + * 与原先 {@code getRecords(ename, workDate, crossDay)} 的时间窗口一致。 + */ + private List sliceRecordsForDay(List prefetched, Date workDate, boolean crossDay) { + if (prefetched == null || prefetched.isEmpty()) { + return new ArrayList<>(); + } + LocalDate ld = toLocalDate(workDate); + LocalDateTime rangeStart = ld.atStartOfDay(); + LocalDateTime rangeEnd = crossDay + ? 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)); + } + } + private WmsAttendanceCheck buildCheck(WmsAttendanceScheduleVo schedule, WmsAttendanceRule rule, List records) { WmsAttendanceCheck check = new WmsAttendanceCheck(); @@ -191,13 +257,7 @@ public class WmsAttendanceCheckServiceImpl implements IWmsAttendanceCheckService return check; } - if (!hasPeriod2) { - List filtered = filterWindow(records, schedule.getWorkDate(), - schedule.getShiftStartTime(), schedule.getShiftEndTime(), crossDay); - check.setP1StartTime(schedule.getShiftStartTime()); - check.setP1EndTime(schedule.getShiftEndTime()); - checkPeriod(check, rule, 1, filtered, schedule.getShiftStartTime(), schedule.getShiftEndTime()); - } else { + if (hasPeriod2) { LocalTime p1End = toLocalTime(schedule.getShiftEndTime()); LocalTime p2Start = toLocalTime(schedule.getShiftStartTime2()); LocalTime split = LocalTime.of( @@ -222,6 +282,12 @@ public class WmsAttendanceCheckServiceImpl implements IWmsAttendanceCheckService check.setP2StartTime(schedule.getShiftStartTime2()); check.setP2EndTime(schedule.getShiftEndTime2()); checkPeriod(check, rule, 2, p2Records, schedule.getShiftStartTime2(), schedule.getShiftEndTime2()); + } else { + List filtered = filterWindow(records, schedule.getWorkDate(), + schedule.getShiftStartTime(), schedule.getShiftEndTime(), crossDay); + check.setP1StartTime(schedule.getShiftStartTime()); + check.setP1EndTime(schedule.getShiftEndTime()); + checkPeriod(check, rule, 1, filtered, schedule.getShiftStartTime(), schedule.getShiftEndTime()); } calculateOverall(check, rule); @@ -240,15 +306,23 @@ public class WmsAttendanceCheckServiceImpl implements IWmsAttendanceCheckService if (crossDay) { windowStart = LocalDateTime.of(ld, st).minusHours(2); windowEnd = LocalDateTime.of(ld.plusDays(1), et).plusHours(2); - } else { + } 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(windowStart) && !ct.isAfter(windowEnd); + return !ct.isBefore(finalStart) && !ct.isAfter(finalEnd); }) .collect(Collectors.toList()); } @@ -276,7 +350,7 @@ public class WmsAttendanceCheckServiceImpl implements IWmsAttendanceCheckService BigDecimal deduct = BigDecimal.ZERO; String status = "normal"; - if (firstCheck.isAfter(expStart)) { + if (expStart != null && firstCheck.isAfter(expStart)) { lateMinutes = (int) Duration.between(expStart, firstCheck).toMinutes(); if (lateMinutes > rule.getAbsentHalfDay()) { status = "absent_half"; @@ -291,7 +365,7 @@ public class WmsAttendanceCheckServiceImpl implements IWmsAttendanceCheckService } } - if (lastCheck.isBefore(expEnd)) { + if (expEnd != null && lastCheck.isBefore(expEnd)) { int min = (int) Duration.between(lastCheck, expEnd).toMinutes(); if (min > rule.getAbsentHalfDay()) { status = maxSeverity(status, "absent_half"); @@ -330,11 +404,9 @@ public class WmsAttendanceCheckServiceImpl implements IWmsAttendanceCheckService } private String maxSeverity(String a, String b) { - List severity = java.util.Arrays.asList("normal", "late_warn", "early_warn", - "late_one", "early_one", "late_two", "early_two", "absent_half"); - int ai = severity.indexOf(a); - int bi = severity.indexOf(b); - return severity.get(Math.max(ai, bi)); + 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) { @@ -375,23 +447,64 @@ public class WmsAttendanceCheckServiceImpl implements IWmsAttendanceCheckService .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 = countContinuousAbsent(check.getUserId(), toLocalDate(check.getWorkDate())); + int continuous = countContinuousAbsentCached( + check.getUserId(), toLocalDate(check.getWorkDate()), historyByUserDate); check.setContinuousAbsentDays(continuous); - baseMapper.updateById(check); + toUpdate.add(check); } } + if (!toUpdate.isEmpty()) { + baseMapper.updateBatchById(toUpdate, BATCH_SIZE); + } } - private int countContinuousAbsent(Long userId, LocalDate workDate) { + /** + * 加载各员工在 endDate 及之前的考勤结果,用于内存计算连续旷工(与按天 selectOne 结果一致)。 + */ + private Map> loadAbsentHistoryByUser(List userIds, LocalDate endDate) { + Map> out = new HashMap<>(); + 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) + .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 = baseMapper.selectOne(Wrappers.lambdaQuery() - .eq(WmsAttendanceCheck::getUserId, userId) - .eq(WmsAttendanceCheck::getWorkDate, toDate(date.atStartOfDay())) - .eq(WmsAttendanceCheck::getDelFlag, 0)); + WmsAttendanceCheck prev = userMap.get(date); if (prev != null && prev.getAbsentType() != null) { count++; date = date.minusDays(1); diff --git a/klp-wms/src/main/resources/mapper/klp/WmsAttendanceScheduleMapper.xml b/klp-wms/src/main/resources/mapper/klp/WmsAttendanceScheduleMapper.xml index af0145d2..2f25c226 100644 --- a/klp-wms/src/main/resources/mapper/klp/WmsAttendanceScheduleMapper.xml +++ b/klp-wms/src/main/resources/mapper/klp/WmsAttendanceScheduleMapper.xml @@ -36,12 +36,13 @@ + SELECT s.schedule_id, s.user_id, s.work_date, s.shift_id, s.shift_name, s.shift_group, s.remark, e.name as employee_name, e.dept as employee_dept, e.job_type as employee_job_type, - sh.shift_type, sh.start_time as shift_start_time, sh.end_time as shift_end_time, sh.start_time2 as shift_start_time2, sh.end_time2 as shift_end_time2, sh.work_hours + sh.shift_type, sh.start_time as shift_start_time, sh.end_time as shift_end_time, sh.start_time2 as shift_start_time2, sh.end_time2 as shift_end_time2, sh.work_hours, sh.is_cross_day as shift_is_cross_day FROM wms_attendance_schedule s LEFT JOIN wms_employee_info e ON s.user_id = e.info_id AND e.del_flag = 0 LEFT JOIN wms_attendance_shift sh ON s.shift_id = sh.shift_id AND sh.del_flag = 0