Files
klp-oa/klp-wms/src/main/java/com/klp/service/impl/WmsAttendanceCheckServiceImpl.java

547 lines
23 KiB
Java
Raw Normal View History

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.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<String> 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;
private final WmsAttendanceRuleMapper ruleMapper;
@Override
public WmsAttendanceCheckVo queryById(Long checkId) {
return baseMapper.selectVoById(checkId);
}
@Override
public TableDataInfo<WmsAttendanceCheckVo> queryPageList(WmsAttendanceCheckBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<WmsAttendanceCheck> lqw = buildQueryWrapper(bo);
Page<WmsAttendanceCheckVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
return TableDataInfo.build(result);
}
@Override
public List<WmsAttendanceCheckVo> queryList(WmsAttendanceCheckBo bo) {
LambdaQueryWrapper<WmsAttendanceCheck> lqw = buildQueryWrapper(bo);
return baseMapper.selectVoList(lqw);
}
@Override
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
return baseMapper.deleteBatchIds(ids) > 0;
}
private LambdaQueryWrapper<WmsAttendanceCheck> buildQueryWrapper(WmsAttendanceCheckBo bo) {
LambdaQueryWrapper<WmsAttendanceCheck> 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<WmsAttendanceScheduleVo> schedules = scheduleService.queryList(scheduleBo);
if (schedules.isEmpty()) {
return;
}
WmsAttendanceRule rule = getActiveRule();
LocalDate startLocal = toLocalDate(bo.getStartDate());
LocalDate endLocal = toLocalDate(bo.getEndDate());
List<WmsAttendanceScheduleVo> 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;
}
Map<String, List<AttendanceRecords>> recordsByEmployee = prefetchAttendanceRecords(toProcess);
List<Long> scheduleIds = toProcess.stream()
.map(WmsAttendanceScheduleVo::getScheduleId)
.collect(Collectors.toList());
batchDeleteByScheduleIds(scheduleIds);
List<WmsAttendanceCheck> checksToInsert = new ArrayList<>(toProcess.size());
for (WmsAttendanceScheduleVo schedule : toProcess) {
boolean crossDay = isCrossDayShift(schedule);
boolean backward = isBackwardCrossDay(schedule);
List<AttendanceRecords> records = sliceRecordsForDay(
recordsByEmployee.get(schedule.getEmployeeName()),
schedule.getWorkDate(),
crossDay, backward);
checksToInsert.add(buildCheck(schedule, rule, records, crossDay));
}
baseMapper.insertBatch(checksToInsert, BATCH_SIZE);
updateContinuousAbsent(startLocal, endLocal);
}
private boolean isCrossDayShift(WmsAttendanceScheduleVo schedule) {
return schedule.getShiftIsCrossDay() != null && schedule.getShiftIsCrossDay() == 1;
}
private boolean isBackwardCrossDay(WmsAttendanceScheduleVo schedule) {
if (!isCrossDayShift(schedule)) return false;
if (schedule.getShiftStartTime() == null || schedule.getShiftEndTime() == null) return false;
return toLocalTime(schedule.getShiftStartTime()).getHour() >= 16;
}
private WmsAttendanceRule getActiveRule() {
LambdaQueryWrapper<WmsAttendanceRule> 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 Map<String, List<AttendanceRecords>> prefetchAttendanceRecords(List<WmsAttendanceScheduleVo> toProcess) {
Map<String, LocalDate> minByName = new HashMap<>();
Map<String, LocalDate> 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<String, List<AttendanceRecords>> out = new HashMap<>(minByName.size() * 2);
for (Map.Entry<String, LocalDate> 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<AttendanceRecords> fetchRecordsForNameRange(String employeeName, LocalDate minLd, LocalDate maxLd) {
AttendanceRecordsBo recordsBo = new AttendanceRecordsBo();
recordsBo.setEname(employeeName);
recordsBo.setChecktimeStart(toDate(minLd.atStartOfDay()));
recordsBo.setChecktimeEnd(toDate(maxLd.plusDays(1).atTime(LocalTime.of(23, 59, 59))));
List<AttendanceRecordsVo> 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());
}
/**
* 与原先 {@code getRecords(ename, workDate, crossDay)} 的时间窗口一致
*/
private List<AttendanceRecords> sliceRecordsForDay(List<AttendanceRecords> prefetched, Date workDate, boolean crossDay, boolean backward) {
if (prefetched == null || prefetched.isEmpty()) {
return new ArrayList<>();
}
LocalDate ld = toLocalDate(workDate);
LocalDateTime rangeStart = 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<Long> 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<Long> chunk = scheduleIds.subList(i, to);
baseMapper.delete(Wrappers.<WmsAttendanceCheck>lambdaQuery()
.in(WmsAttendanceCheck::getScheduleId, chunk));
}
}
private WmsAttendanceCheck buildCheck(WmsAttendanceScheduleVo schedule, WmsAttendanceRule rule,
List<AttendanceRecords> records, boolean crossDay) {
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;
if (records.isEmpty()) {
check.setOverallStatus("absent_full");
check.setAbsentType("full_day");
return check;
}
if (hasPeriod2) {
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<AttendanceRecords> p1Records = new ArrayList<>();
List<AttendanceRecords> 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());
} else {
List<AttendanceRecords> 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);
return check;
}
private List<AttendanceRecords> filterWindow(List<AttendanceRecords> 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 = st != null && et != null && st.getHour() >= 16 && crossDay;
if (crossDay && backward) {
windowStart = 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<AttendanceRecords> 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 (expStart != null && 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 (expEnd != null && 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) {
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;
}
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<WmsAttendanceCheck> checks = baseMapper.selectList(Wrappers.<WmsAttendanceCheck>lambdaQuery()
.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<Long> userIds = checks.stream()
.map(WmsAttendanceCheck::getUserId)
.distinct()
.collect(Collectors.toList());
Map<Long, Map<LocalDate, WmsAttendanceCheck>> historyByUserDate = loadAbsentHistoryByUser(userIds, endDate);
List<WmsAttendanceCheck> 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<Long, Map<LocalDate, WmsAttendanceCheck>> loadAbsentHistoryByUser(List<Long> userIds, LocalDate endDate) {
Map<Long, Map<LocalDate, WmsAttendanceCheck>> 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<Long> chunk = userIds.subList(i, to);
List<WmsAttendanceCheck> rows = baseMapper.selectList(Wrappers.<WmsAttendanceCheck>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<Long, Map<LocalDate, WmsAttendanceCheck>> historyByUserDate) {
Map<LocalDate, WmsAttendanceCheck> 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());
}
}