package com.klp.service.impl; 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.List; import java.util.stream.Collectors; @RequiredArgsConstructor @Service public class WmsAttendanceCheckServiceImpl implements IWmsAttendanceCheckService { 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; } 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.orderByDesc(WmsAttendanceCheck::getWorkDate); return lqw; } @Override @Transactional(rollbackFor = Exception.class) public void checkAttendance(AttendanceCheckBo bo) { WmsAttendanceScheduleBo scheduleBo = new WmsAttendanceScheduleBo(); scheduleBo.setStartDate(bo.getStartDate()); scheduleBo.setEndDate(bo.getEndDate()); List schedules = scheduleService.queryList(scheduleBo); if (schedules.isEmpty()) { return; } WmsAttendanceRule rule = getActiveRule(); LocalDate startLocal = toLocalDate(bo.getStartDate()); LocalDate endLocal = toLocalDate(bo.getEndDate()); for (WmsAttendanceScheduleVo schedule : schedules) { if (schedule.getEmployeeName() == null || schedule.getShiftStartTime() == 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); } 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); } 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; } private List getRecords(String employeeName, Date workDate, boolean crossDay) { 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)))); } List voList = attendanceRecordsService.queryList(recordsBo); return voList.stream() .map(v -> { AttendanceRecords r = new AttendanceRecords(); r.setId(v.getId()); r.setEname(v.getEname()); r.setChecktime(v.getChecktime()); return r; }) .sorted(Comparator.comparing(AttendanceRecords::getChecktime)) .collect(Collectors.toList()); } private WmsAttendanceCheck buildCheck(WmsAttendanceScheduleVo schedule, WmsAttendanceRule rule, List records) { 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 crossDay = isCrossDayShift(schedule); if (records.isEmpty()) { check.setOverallStatus("absent_full"); check.setAbsentType("full_day"); 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 { LocalTime p1End = toLocalTime(schedule.getShiftEndTime()); LocalTime p2Start = toLocalTime(schedule.getShiftStartTime2()); LocalTime split = LocalTime.of( (p1End.getHour() + p2Start.getHour()) / 2, (p1End.getMinute() + p2Start.getMinute()) / 2); List p1Records = new ArrayList<>(); List p2Records = new ArrayList<>(); for (AttendanceRecords r : records) { LocalTime t = toLocalDateTime(r.getChecktime()).toLocalTime(); if (t.isBefore(split)) { p1Records.add(r); } else { p2Records.add(r); } } check.setP1StartTime(schedule.getShiftStartTime()); check.setP1EndTime(schedule.getShiftEndTime()); checkPeriod(check, rule, 1, p1Records, schedule.getShiftStartTime(), schedule.getShiftEndTime()); check.setP2StartTime(schedule.getShiftStartTime2()); check.setP2EndTime(schedule.getShiftEndTime2()); checkPeriod(check, rule, 2, p2Records, schedule.getShiftStartTime2(), schedule.getShiftEndTime2()); } 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; if (crossDay) { windowStart = LocalDateTime.of(ld, st).minusHours(2); windowEnd = LocalDateTime.of(ld.plusDays(1), et).plusHours(2); } else { windowStart = LocalDateTime.of(ld, st).minusHours(2); windowEnd = LocalDateTime.of(ld, et).plusHours(2); } return records.stream() .filter(r -> { LocalDateTime ct = toLocalDateTime(r.getChecktime()); return !ct.isBefore(windowStart) && !ct.isAfter(windowEnd); }) .collect(Collectors.toList()); } private void checkPeriod(WmsAttendanceCheck check, WmsAttendanceRule rule, int period, List periodRecords, Date expectedStart, Date expectedEnd) { if (periodRecords.isEmpty()) { if (period == 1) { check.setP1Status("missed"); } else { check.setP2Status("missed"); } return; } LocalTime expStart = toLocalTime(expectedStart); LocalTime expEnd = toLocalTime(expectedEnd); AttendanceRecords firstRec = periodRecords.get(0); AttendanceRecords lastRec = periodRecords.get(periodRecords.size() - 1); LocalTime firstCheck = toLocalDateTime(firstRec.getChecktime()).toLocalTime(); LocalTime lastCheck = toLocalDateTime(lastRec.getChecktime()).toLocalTime(); int lateMinutes = 0; int earlyMinutes = 0; BigDecimal deduct = BigDecimal.ZERO; String status = "normal"; if (firstCheck.isAfter(expStart)) { lateMinutes = (int) Duration.between(expStart, firstCheck).toMinutes(); 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 (lastCheck.isBefore(expEnd)) { int min = (int) Duration.between(lastCheck, expEnd).toMinutes(); if (min > rule.getAbsentHalfDay()) { status = maxSeverity(status, "absent_half"); earlyMinutes = min; } else if (min > rule.getLateOne()) { status = maxSeverity(status, "early_two"); deduct = deduct.add(rule.getDeductTwo()); earlyMinutes = min; } else if (min > rule.getLateWarn()) { status = maxSeverity(status, "early_one"); deduct = deduct.add(rule.getDeductOne()); earlyMinutes = min; } else { if ("normal".equals(status)) { status = "early_warn"; } earlyMinutes = min; } } if (period == 1) { check.setP1FirstCheck(firstRec.getChecktime()); check.setP1LastCheck(lastRec.getChecktime()); check.setP1LateMinutes(lateMinutes); check.setP1EarlyMinutes(earlyMinutes); check.setP1Status(status); check.setP1Deduct(deduct); } else { check.setP2FirstCheck(firstRec.getChecktime()); check.setP2LastCheck(lastRec.getChecktime()); check.setP2LateMinutes(lateMinutes); check.setP2EarlyMinutes(earlyMinutes); check.setP2Status(status); check.setP2Deduct(deduct); } } 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)); } 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; } if ("missed".equals(check.getP1Status()) && check.getP2StartTime() != null && "missed".equals(check.getP2Status())) { check.setAbsentType("full_day"); check.setOverallStatus("absent_full"); 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) { List checks = baseMapper.selectList(Wrappers.lambdaQuery() .ge(WmsAttendanceCheck::getWorkDate, toDate(startDate.atStartOfDay())) .le(WmsAttendanceCheck::getWorkDate, toDate(endDate.atTime(LocalTime.of(23, 59, 59)))) .eq(WmsAttendanceCheck::getDelFlag, 0)); for (WmsAttendanceCheck check : checks) { if (check.getAbsentType() != null) { int continuous = countContinuousAbsent(check.getUserId(), toLocalDate(check.getWorkDate())); check.setContinuousAbsentDays(continuous); baseMapper.updateById(check); } } } private int countContinuousAbsent(Long userId, LocalDate workDate) { 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)); 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()); } }