399 lines
16 KiB
Java
399 lines
16 KiB
Java
|
|
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<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());
|
||
|
|
|
||
|
|
for (WmsAttendanceScheduleVo schedule : schedules) {
|
||
|
|
if (schedule.getEmployeeName() == null || schedule.getShiftStartTime() == null) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 获取该员工该日期的所有打卡记录
|
||
|
|
List<AttendanceRecords> records = getRecords(schedule.getEmployeeName(), schedule.getWorkDate());
|
||
|
|
|
||
|
|
WmsAttendanceCheck check = buildCheck(schedule, rule, records);
|
||
|
|
|
||
|
|
// 删除已有的比对结果
|
||
|
|
baseMapper.delete(Wrappers.<WmsAttendanceCheck>lambdaQuery()
|
||
|
|
.eq(WmsAttendanceCheck::getScheduleId, schedule.getScheduleId()));
|
||
|
|
|
||
|
|
baseMapper.insert(check);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 更新连续旷工天数
|
||
|
|
updateContinuousAbsent(startLocal, endLocal);
|
||
|
|
}
|
||
|
|
|
||
|
|
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.setLate1(5L);
|
||
|
|
rule.setLate2(15L);
|
||
|
|
rule.setDeduct1(new BigDecimal("10"));
|
||
|
|
rule.setDeduct2(new BigDecimal("30"));
|
||
|
|
rule.setAbsentHalfDay(15L);
|
||
|
|
rule.setContinuousAbsentDays(3L);
|
||
|
|
}
|
||
|
|
return rule;
|
||
|
|
}
|
||
|
|
|
||
|
|
private List<AttendanceRecords> getRecords(String employeeName, Date workDate) {
|
||
|
|
AttendanceRecordsBo recordsBo = new AttendanceRecordsBo();
|
||
|
|
recordsBo.setEname(employeeName);
|
||
|
|
LocalDate ld = toLocalDate(workDate);
|
||
|
|
recordsBo.setChecktimeStart(toDate(ld.atStartOfDay()));
|
||
|
|
recordsBo.setChecktimeEnd(toDate(ld.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());
|
||
|
|
}
|
||
|
|
|
||
|
|
private WmsAttendanceCheck buildCheck(WmsAttendanceScheduleVo schedule, WmsAttendanceRule rule, List<AttendanceRecords> 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;
|
||
|
|
|
||
|
|
if (records.isEmpty()) {
|
||
|
|
check.setOverallStatus("absent_full");
|
||
|
|
check.setAbsentType("full_day");
|
||
|
|
return check;
|
||
|
|
}
|
||
|
|
|
||
|
|
List<LocalTime> checkTimes = records.stream()
|
||
|
|
.map(r -> toLocalDateTime(r.getChecktime()).toLocalTime())
|
||
|
|
.collect(Collectors.toList());
|
||
|
|
|
||
|
|
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<LocalTime> period1Times = new ArrayList<>();
|
||
|
|
List<LocalTime> period2Times = new ArrayList<>();
|
||
|
|
for (LocalTime t : checkTimes) {
|
||
|
|
if (t.isBefore(split)) {
|
||
|
|
period1Times.add(t);
|
||
|
|
} else {
|
||
|
|
period2Times.add(t);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
check.setP1StartTime(schedule.getShiftStartTime());
|
||
|
|
check.setP1EndTime(schedule.getShiftEndTime());
|
||
|
|
checkPeriod(check, rule, 1, period1Times, schedule.getShiftStartTime(), schedule.getShiftEndTime(), records);
|
||
|
|
|
||
|
|
check.setP2StartTime(schedule.getShiftStartTime2());
|
||
|
|
check.setP2EndTime(schedule.getShiftEndTime2());
|
||
|
|
checkPeriod(check, rule, 2, period2Times, schedule.getShiftStartTime2(), schedule.getShiftEndTime2(), records);
|
||
|
|
} else {
|
||
|
|
// 倒班:全天一个时段
|
||
|
|
check.setP1StartTime(schedule.getShiftStartTime());
|
||
|
|
check.setP1EndTime(schedule.getShiftEndTime());
|
||
|
|
checkPeriod(check, rule, 1, checkTimes, schedule.getShiftStartTime(), schedule.getShiftEndTime(), records);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 总体判定
|
||
|
|
calculateOverall(check, rule);
|
||
|
|
return check;
|
||
|
|
}
|
||
|
|
|
||
|
|
private void checkPeriod(WmsAttendanceCheck check, WmsAttendanceRule rule, int period,
|
||
|
|
List<LocalTime> periodTimes, Date expectedStart, Date expectedEnd,
|
||
|
|
List<AttendanceRecords> allRecords) {
|
||
|
|
if (periodTimes.isEmpty()) {
|
||
|
|
if (period == 1) {
|
||
|
|
check.setP1Status("missed");
|
||
|
|
} else {
|
||
|
|
check.setP2Status("missed");
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
LocalTime expStart = toLocalTime(expectedStart);
|
||
|
|
LocalTime expEnd = toLocalTime(expectedEnd);
|
||
|
|
LocalTime firstCheck = periodTimes.get(0);
|
||
|
|
LocalTime lastCheck = periodTimes.get(periodTimes.size() - 1);
|
||
|
|
|
||
|
|
int lateMinutes = 0;
|
||
|
|
int earlyMinutes = 0;
|
||
|
|
BigDecimal deduct = BigDecimal.ZERO;
|
||
|
|
String status = "normal";
|
||
|
|
|
||
|
|
// 迟到判定:最早打卡 vs 理论上班时间
|
||
|
|
if (firstCheck.isAfter(expStart)) {
|
||
|
|
lateMinutes = (int) Duration.between(expStart, firstCheck).toMinutes();
|
||
|
|
if (lateMinutes > rule.getAbsentHalfDay()) {
|
||
|
|
status = "absent_half";
|
||
|
|
} else if (lateMinutes > rule.getLate2()) {
|
||
|
|
status = "absent_half";
|
||
|
|
} else if (lateMinutes > rule.getLate1()) {
|
||
|
|
status = "late_2";
|
||
|
|
deduct = deduct.add(rule.getDeduct2());
|
||
|
|
} else if (lateMinutes > rule.getLateWarn()) {
|
||
|
|
status = "late_1";
|
||
|
|
deduct = deduct.add(rule.getDeduct1());
|
||
|
|
} else {
|
||
|
|
status = "late_warn";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 早退判定:最晚打卡 vs 理论下班时间
|
||
|
|
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.getLate2()) {
|
||
|
|
status = maxSeverity(status, "absent_half");
|
||
|
|
earlyMinutes = min;
|
||
|
|
} else if (min > rule.getLate1()) {
|
||
|
|
status = maxSeverity(status, "early_2");
|
||
|
|
deduct = deduct.add(rule.getDeduct2());
|
||
|
|
earlyMinutes = min;
|
||
|
|
} else if (min > rule.getLateWarn()) {
|
||
|
|
status = maxSeverity(status, "early_1");
|
||
|
|
deduct = deduct.add(rule.getDeduct1());
|
||
|
|
earlyMinutes = min;
|
||
|
|
} else {
|
||
|
|
if ("normal".equals(status)) {
|
||
|
|
status = "early_warn";
|
||
|
|
}
|
||
|
|
earlyMinutes = min;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 找到对应的打卡时间
|
||
|
|
for (AttendanceRecords r : allRecords) {
|
||
|
|
LocalTime ct = toLocalDateTime(r.getChecktime()).toLocalTime();
|
||
|
|
if (ct.equals(firstCheck)) {
|
||
|
|
if (period == 1) check.setP1FirstCheck(r.getChecktime());
|
||
|
|
else check.setP2FirstCheck(r.getChecktime());
|
||
|
|
}
|
||
|
|
if (ct.equals(lastCheck)) {
|
||
|
|
if (period == 1) check.setP1LastCheck(r.getChecktime());
|
||
|
|
else check.setP2LastCheck(r.getChecktime());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (period == 1) {
|
||
|
|
check.setP1LateMinutes(lateMinutes);
|
||
|
|
check.setP1EarlyMinutes(earlyMinutes);
|
||
|
|
check.setP1Status(status);
|
||
|
|
check.setP1Deduct(deduct);
|
||
|
|
} else {
|
||
|
|
check.setP2LateMinutes(lateMinutes);
|
||
|
|
check.setP2EarlyMinutes(earlyMinutes);
|
||
|
|
check.setP2Status(status);
|
||
|
|
check.setP2Deduct(deduct);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private String maxSeverity(String a, String b) {
|
||
|
|
List<String> severity = java.util.Arrays.asList("normal", "late_warn", "early_warn", "late_1", "early_1", "late_2", "early_2", "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<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));
|
||
|
|
|
||
|
|
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.<WmsAttendanceCheck>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());
|
||
|
|
}
|
||
|
|
}
|