2026-05-12 16:29:54 +08:00
|
|
|
|
package com.klp.service.impl;
|
|
|
|
|
|
|
2026-05-15 13:13:34 +08:00
|
|
|
|
import cn.hutool.core.bean.BeanUtil;
|
2026-05-12 16:29:54 +08:00
|
|
|
|
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;
|
2026-05-14 17:13:26 +08:00
|
|
|
|
import java.util.HashMap;
|
2026-05-27 14:16:34 +08:00
|
|
|
|
import java.util.HashSet;
|
2026-05-12 16:29:54 +08:00
|
|
|
|
import java.util.List;
|
2026-05-14 17:13:26 +08:00
|
|
|
|
import java.util.Map;
|
2026-05-27 14:16:34 +08:00
|
|
|
|
import java.util.Objects;
|
|
|
|
|
|
import java.util.Set;
|
2026-05-12 16:29:54 +08:00
|
|
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
|
|
|
|
|
|
@RequiredArgsConstructor
|
|
|
|
|
|
@Service
|
|
|
|
|
|
public class WmsAttendanceCheckServiceImpl implements IWmsAttendanceCheckService {
|
|
|
|
|
|
|
2026-05-14 17:13:26 +08:00
|
|
|
|
private static final List<String> STATUS_SEVERITY = java.util.Arrays.asList("normal", "late_warn", "early_warn",
|
2026-06-01 14:38:56 +08:00
|
|
|
|
"late_one", "early_one", "late_two", "early_two", "absent_half", "missed_start", "missed_end", "missed");
|
|
|
|
|
|
|
|
|
|
|
|
private static final long LATE_EARLY_MAX_SECONDS = 30 * 60L;
|
2026-05-14 17:13:26 +08:00
|
|
|
|
|
|
|
|
|
|
private static final int BATCH_SIZE = 500;
|
|
|
|
|
|
|
2026-05-12 16:29:54 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-15 13:13:34 +08:00
|
|
|
|
@Override
|
|
|
|
|
|
@Transactional(rollbackFor = Exception.class)
|
|
|
|
|
|
public Boolean updateByBo(WmsAttendanceCheckBo bo) {
|
|
|
|
|
|
WmsAttendanceCheck update = BeanUtil.toBean(bo, WmsAttendanceCheck.class);
|
|
|
|
|
|
return baseMapper.updateById(update) > 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 16:29:54 +08:00
|
|
|
|
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());
|
2026-05-26 17:01:43 +08:00
|
|
|
|
lqw.in(bo.getUserIds() != null, WmsAttendanceCheck::getUserId, bo.getUserIds());
|
2026-05-12 16:29:54 +08:00
|
|
|
|
lqw.orderByDesc(WmsAttendanceCheck::getWorkDate);
|
|
|
|
|
|
return lqw;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-27 14:16:34 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 检查员工考勤情况
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param bo 考勤检查业务对象,包含开始日期、结束日期和用户ID等信息
|
|
|
|
|
|
*/
|
2026-05-12 16:29:54 +08:00
|
|
|
|
@Override
|
2026-05-27 14:16:34 +08:00
|
|
|
|
@Transactional(rollbackFor = Exception.class) // 使用事务注解,确保方法内所有操作要么全部成功,要么全部回滚
|
2026-05-12 16:29:54 +08:00
|
|
|
|
public void checkAttendance(AttendanceCheckBo bo) {
|
2026-05-27 14:16:34 +08:00
|
|
|
|
// 创建考勤计划业务对象,并设置查询参数
|
2026-05-12 16:29:54 +08:00
|
|
|
|
WmsAttendanceScheduleBo scheduleBo = new WmsAttendanceScheduleBo();
|
2026-05-27 14:16:34 +08:00
|
|
|
|
scheduleBo.setStartDate(bo.getStartDate()); // 设置开始日期
|
|
|
|
|
|
scheduleBo.setEndDate(bo.getEndDate()); // 设置结束日期
|
|
|
|
|
|
scheduleBo.setUserIds(bo.getUserIds()); // 设置用户ID列表
|
|
|
|
|
|
// 根据查询条件获取考勤计划列表
|
2026-05-12 16:29:54 +08:00
|
|
|
|
List<WmsAttendanceScheduleVo> schedules = scheduleService.queryList(scheduleBo);
|
|
|
|
|
|
|
2026-05-27 14:16:34 +08:00
|
|
|
|
// 如果没有考勤计划,直接返回
|
2026-05-12 16:29:54 +08:00
|
|
|
|
if (schedules.isEmpty()) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-27 14:16:34 +08:00
|
|
|
|
// 获取当前有效的考勤规则
|
2026-05-12 16:29:54 +08:00
|
|
|
|
WmsAttendanceRule rule = getActiveRule();
|
|
|
|
|
|
|
2026-05-27 14:57:49 +08:00
|
|
|
|
// // 将日期转换为LocalDate类型
|
|
|
|
|
|
// LocalDate startLocal = toLocalDate(bo.getStartDate());
|
|
|
|
|
|
// LocalDate endLocal = toLocalDate(bo.getEndDate());
|
2026-05-12 16:29:54 +08:00
|
|
|
|
|
2026-05-27 14:16:34 +08:00
|
|
|
|
// 创建待处理的考勤计划列表
|
2026-05-14 17:13:26 +08:00
|
|
|
|
List<WmsAttendanceScheduleVo> toProcess = new ArrayList<>();
|
2026-05-27 14:16:34 +08:00
|
|
|
|
// 遍历考勤计划,过滤掉无效数据
|
2026-05-12 16:29:54 +08:00
|
|
|
|
for (WmsAttendanceScheduleVo schedule : schedules) {
|
2026-05-27 14:16:34 +08:00
|
|
|
|
if (schedule.getEmployeeName() == null) { // 跳过员工名为空的记录
|
2026-05-12 16:29:54 +08:00
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2026-05-27 14:16:34 +08:00
|
|
|
|
if (schedule.getShiftStartTime() == null && schedule.getShiftEndTime() == null) { // 跳过上下班时间为空的记录
|
2026-05-14 17:13:26 +08:00
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2026-05-27 14:16:34 +08:00
|
|
|
|
toProcess.add(schedule); // 添加到待处理列表
|
2026-05-14 17:13:26 +08:00
|
|
|
|
}
|
2026-05-12 16:29:54 +08:00
|
|
|
|
|
2026-05-27 14:16:34 +08:00
|
|
|
|
// 如果过滤后没有有效的考勤计划,直接返回
|
2026-05-14 17:13:26 +08:00
|
|
|
|
if (toProcess.isEmpty()) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-05-12 16:29:54 +08:00
|
|
|
|
|
2026-05-27 14:16:34 +08:00
|
|
|
|
// 倒班日重叠检测:若某天有正向跨天夜班(19:00→07:00),且次日存在同员工、同上班时间的反向跨天班(夜转白 19:00→13:00),
|
|
|
|
|
|
// 则正向跨天班的下班时间校验跳过(实际下班按反向跨天班 13:00 算),但上班打卡仍然校验
|
|
|
|
|
|
// 创建映射表,记录被反向跨天班覆盖的记录
|
|
|
|
|
|
Map<String, LocalTime> 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<Long> 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<Long> affectedUserIds = toProcess.stream()
|
|
|
|
|
|
.map(WmsAttendanceScheduleVo::getUserId)
|
|
|
|
|
|
.filter(Objects::nonNull)
|
|
|
|
|
|
.collect(Collectors.toSet());
|
|
|
|
|
|
|
|
|
|
|
|
// 按员工预取考勤记录
|
2026-05-14 17:13:26 +08:00
|
|
|
|
Map<String, List<AttendanceRecords>> recordsByEmployee = prefetchAttendanceRecords(toProcess);
|
2026-05-12 16:29:54 +08:00
|
|
|
|
|
2026-05-27 14:16:34 +08:00
|
|
|
|
// 获取所有待处理考勤计划的ID列表
|
2026-05-14 17:13:26 +08:00
|
|
|
|
List<Long> scheduleIds = toProcess.stream()
|
|
|
|
|
|
.map(WmsAttendanceScheduleVo::getScheduleId)
|
|
|
|
|
|
.collect(Collectors.toList());
|
2026-05-27 14:16:34 +08:00
|
|
|
|
// 批量删除这些考勤计划对应的考勤检查记录
|
2026-05-14 17:13:26 +08:00
|
|
|
|
batchDeleteByScheduleIds(scheduleIds);
|
2026-05-12 16:29:54 +08:00
|
|
|
|
|
2026-05-27 14:16:34 +08:00
|
|
|
|
// 创建要插入的考勤检查记录列表
|
2026-05-14 17:13:26 +08:00
|
|
|
|
List<WmsAttendanceCheck> checksToInsert = new ArrayList<>(toProcess.size());
|
2026-05-27 14:16:34 +08:00
|
|
|
|
// 遍历待处理的考勤计划,构建考勤检查记录
|
2026-05-14 17:13:26 +08:00
|
|
|
|
for (WmsAttendanceScheduleVo schedule : toProcess) {
|
|
|
|
|
|
boolean crossDay = isCrossDayShift(schedule);
|
2026-05-14 17:41:04 +08:00
|
|
|
|
boolean backward = isBackwardCrossDay(schedule);
|
2026-05-27 14:16:34 +08:00
|
|
|
|
// 根据考勤计划和考勤记录,按天切片考勤记录
|
2026-05-14 17:13:26 +08:00
|
|
|
|
List<AttendanceRecords> records = sliceRecordsForDay(
|
|
|
|
|
|
recordsByEmployee.get(schedule.getEmployeeName()),
|
|
|
|
|
|
schedule.getWorkDate(),
|
2026-05-14 17:41:04 +08:00
|
|
|
|
crossDay, backward);
|
2026-05-27 14:16:34 +08:00
|
|
|
|
// 构建考勤检查记录并添加到插入列表
|
|
|
|
|
|
checksToInsert.add(buildCheck(schedule, rule, records, crossDay, endCheckOverridden));
|
2026-05-12 16:29:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-27 14:16:34 +08:00
|
|
|
|
// 批量插入考勤检查记录
|
2026-05-14 17:13:26 +08:00
|
|
|
|
baseMapper.insertBatch(checksToInsert, BATCH_SIZE);
|
2026-05-27 14:57:49 +08:00
|
|
|
|
}
|
2026-05-14 17:13:26 +08:00
|
|
|
|
|
2026-05-27 14:57:49 +08:00
|
|
|
|
@Override
|
|
|
|
|
|
@Transactional(rollbackFor = Exception.class)
|
|
|
|
|
|
public void recalcContinuousAbsent(AttendanceCheckBo bo) {
|
|
|
|
|
|
LocalDate startLocal = toLocalDate(bo.getStartDate());
|
|
|
|
|
|
LocalDate endLocal = toLocalDate(bo.getEndDate());
|
|
|
|
|
|
Set<Long> affectedUserIds;
|
|
|
|
|
|
if (bo.getUserIds() != null && !bo.getUserIds().isEmpty()) {
|
|
|
|
|
|
affectedUserIds = new HashSet<>(bo.getUserIds());
|
|
|
|
|
|
} else {
|
|
|
|
|
|
List<WmsAttendanceCheck> checks = baseMapper.selectList(Wrappers.<WmsAttendanceCheck>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());
|
|
|
|
|
|
}
|
2026-05-27 14:16:34 +08:00
|
|
|
|
updateContinuousAbsent(startLocal, endLocal, affectedUserIds);
|
2026-05-12 16:29:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 16:18:05 +08:00
|
|
|
|
private boolean isCrossDayShift(WmsAttendanceScheduleVo schedule) {
|
2026-05-14 17:13:26 +08:00
|
|
|
|
return schedule.getShiftIsCrossDay() != null && schedule.getShiftIsCrossDay() == 1;
|
2026-05-13 16:18:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-14 17:41:04 +08:00
|
|
|
|
private boolean isBackwardCrossDay(WmsAttendanceScheduleVo schedule) {
|
|
|
|
|
|
if (!isCrossDayShift(schedule)) return false;
|
2026-05-14 18:12:45 +08:00
|
|
|
|
if (schedule.getShiftEndTime() == null) return false;
|
|
|
|
|
|
return toLocalTime(schedule.getShiftEndTime()).getHour() >= 12;
|
2026-05-14 17:41:04 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 16:29:54 +08:00
|
|
|
|
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);
|
2026-05-12 17:09:37 +08:00
|
|
|
|
rule.setLateOne(5L);
|
|
|
|
|
|
rule.setLateTwo(15L);
|
|
|
|
|
|
rule.setDeductOne(new BigDecimal("10"));
|
|
|
|
|
|
rule.setDeductTwo(new BigDecimal("30"));
|
2026-05-12 16:29:54 +08:00
|
|
|
|
rule.setAbsentHalfDay(15L);
|
|
|
|
|
|
rule.setContinuousAbsentDays(3L);
|
|
|
|
|
|
}
|
|
|
|
|
|
return rule;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-14 17:13:26 +08:00
|
|
|
|
/**
|
2026-05-27 14:16:34 +08:00
|
|
|
|
* 一次 SQL 批量查询所有员工在日期范围内的打卡记录(用 IN/eq 精确匹配,走索引)。
|
2026-05-14 17:13:26 +08:00
|
|
|
|
*/
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
2026-05-27 14:16:34 +08:00
|
|
|
|
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<String> allNames = new ArrayList<>(minByName.keySet());
|
|
|
|
|
|
List<AttendanceRecords> allRecords = attendanceRecordsService.queryListByEnamesAndDateRange(
|
|
|
|
|
|
allNames, rangeStart, rangeEnd);
|
|
|
|
|
|
|
|
|
|
|
|
Map<String, List<AttendanceRecords>> 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<>());
|
2026-05-14 17:13:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
return out;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-14 17:41:04 +08:00
|
|
|
|
private List<AttendanceRecords> sliceRecordsForDay(List<AttendanceRecords> prefetched, Date workDate, boolean crossDay, boolean backward) {
|
2026-05-14 17:13:26 +08:00
|
|
|
|
if (prefetched == null || prefetched.isEmpty()) {
|
|
|
|
|
|
return new ArrayList<>();
|
|
|
|
|
|
}
|
|
|
|
|
|
LocalDate ld = toLocalDate(workDate);
|
2026-05-15 18:04:40 +08:00
|
|
|
|
LocalDateTime rangeStart = backward
|
|
|
|
|
|
? ld.minusDays(1).atStartOfDay()
|
|
|
|
|
|
: ld.atStartOfDay();
|
2026-05-14 17:41:04 +08:00
|
|
|
|
LocalDateTime rangeEnd = crossDay && !backward
|
2026-05-14 17:13:26 +08:00
|
|
|
|
? 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));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-27 14:16:34 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 构建考勤检查对象,根据排班信息和考勤记录生成考勤结果
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param schedule 排班信息对象
|
|
|
|
|
|
* @param rule 考勤规则对象
|
|
|
|
|
|
* @param records 考勤记录列表
|
|
|
|
|
|
* @param crossDay 是否跨天
|
|
|
|
|
|
* @param endCheckOverridden 已覆盖的结束检查集合
|
|
|
|
|
|
* @return 构建好的考勤检查对象
|
|
|
|
|
|
*/
|
2026-05-13 16:18:05 +08:00
|
|
|
|
private WmsAttendanceCheck buildCheck(WmsAttendanceScheduleVo schedule, WmsAttendanceRule rule,
|
2026-05-27 14:16:34 +08:00
|
|
|
|
List<AttendanceRecords> records, boolean crossDay,
|
|
|
|
|
|
Set<Long> endCheckOverridden) {
|
|
|
|
|
|
// 创建考勤检查对象
|
2026-05-12 16:29:54 +08:00
|
|
|
|
WmsAttendanceCheck check = new WmsAttendanceCheck();
|
2026-05-27 14:16:34 +08:00
|
|
|
|
// 设置排班基本信息
|
2026-05-12 16:29:54 +08:00
|
|
|
|
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());
|
|
|
|
|
|
|
2026-05-27 14:16:34 +08:00
|
|
|
|
// 判断是否有第二时段
|
2026-05-12 16:29:54 +08:00
|
|
|
|
boolean hasPeriod2 = schedule.getShiftStartTime2() != null && schedule.getShiftEndTime2() != null;
|
|
|
|
|
|
|
2026-05-27 14:16:34 +08:00
|
|
|
|
// 判断是否为向后跨天
|
2026-05-15 17:05:10 +08:00
|
|
|
|
boolean backward = isBackwardCrossDay(schedule);
|
|
|
|
|
|
|
2026-05-27 14:16:34 +08:00
|
|
|
|
// 处理时段设置
|
2026-05-15 17:05:10 +08:00
|
|
|
|
if (hasPeriod2) {
|
2026-05-27 14:16:34 +08:00
|
|
|
|
// 设置第一时段开始时间,考虑跨天情况
|
|
|
|
|
|
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()));
|
2026-05-15 17:05:10 +08:00
|
|
|
|
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()));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 16:29:54 +08:00
|
|
|
|
if (records.isEmpty()) {
|
2026-05-16 09:13:26 +08:00
|
|
|
|
check.setP1Status("missed");
|
|
|
|
|
|
if (hasPeriod2) {
|
|
|
|
|
|
check.setP2Status("missed");
|
|
|
|
|
|
}
|
2026-05-12 16:29:54 +08:00
|
|
|
|
check.setOverallStatus("absent_full");
|
|
|
|
|
|
check.setAbsentType("full_day");
|
|
|
|
|
|
return check;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-14 17:13:26 +08:00
|
|
|
|
if (hasPeriod2) {
|
2026-05-12 16:29:54 +08:00
|
|
|
|
LocalTime p1End = toLocalTime(schedule.getShiftEndTime());
|
|
|
|
|
|
LocalTime p2Start = toLocalTime(schedule.getShiftStartTime2());
|
|
|
|
|
|
|
2026-05-30 17:38:13 +08:00
|
|
|
|
// 按理论时间范围分割:p1End之前、中间区间、p2Start之后
|
|
|
|
|
|
List<AttendanceRecords> beforeGap = new ArrayList<>();
|
|
|
|
|
|
List<AttendanceRecords> inGap = new ArrayList<>();
|
|
|
|
|
|
List<AttendanceRecords> afterGap = new ArrayList<>();
|
2026-05-13 16:18:05 +08:00
|
|
|
|
for (AttendanceRecords r : records) {
|
|
|
|
|
|
LocalTime t = toLocalDateTime(r.getChecktime()).toLocalTime();
|
2026-05-30 17:38:13 +08:00
|
|
|
|
if (!t.isAfter(p1End)) {
|
|
|
|
|
|
beforeGap.add(r);
|
|
|
|
|
|
} else if (!t.isBefore(p2Start)) {
|
|
|
|
|
|
afterGap.add(r);
|
2026-05-12 16:29:54 +08:00
|
|
|
|
} else {
|
2026-05-30 17:38:13 +08:00
|
|
|
|
inGap.add(r);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
List<AttendanceRecords> p1Records = new ArrayList<>();
|
|
|
|
|
|
List<AttendanceRecords> 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);
|
|
|
|
|
|
}
|
2026-05-12 16:29:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-27 14:16:34 +08:00
|
|
|
|
checkPeriod(check, rule, 1, p1Records, check.getP1StartTime(), check.getP1EndTime());
|
|
|
|
|
|
checkPeriod(check, rule, 2, p2Records, check.getP2StartTime(), check.getP2EndTime());
|
2026-05-14 17:13:26 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
List<AttendanceRecords> filtered = filterWindow(records, schedule.getWorkDate(),
|
|
|
|
|
|
schedule.getShiftStartTime(), schedule.getShiftEndTime(), crossDay);
|
2026-05-27 14:16:34 +08:00
|
|
|
|
Date actualEnd = endCheckOverridden.contains(schedule.getScheduleId())
|
|
|
|
|
|
? null : check.getP1EndTime();
|
|
|
|
|
|
checkPeriod(check, rule, 1, filtered, check.getP1StartTime(), actualEnd);
|
2026-05-12 16:29:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
calculateOverall(check, rule);
|
|
|
|
|
|
return check;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 16:18:05 +08:00
|
|
|
|
private List<AttendanceRecords> filterWindow(List<AttendanceRecords> records, Date workDate,
|
2026-05-27 14:16:34 +08:00
|
|
|
|
Date expectedStart, Date expectedEnd, boolean crossDay) {
|
2026-05-13 16:18:05 +08:00
|
|
|
|
LocalDate ld = toLocalDate(workDate);
|
|
|
|
|
|
LocalTime st = toLocalTime(expectedStart);
|
|
|
|
|
|
LocalTime et = toLocalTime(expectedEnd);
|
|
|
|
|
|
|
|
|
|
|
|
LocalDateTime windowStart;
|
|
|
|
|
|
LocalDateTime windowEnd;
|
|
|
|
|
|
|
2026-05-14 18:12:45 +08:00
|
|
|
|
boolean backward = crossDay && et != null && et.getHour() >= 12;
|
2026-05-14 17:41:04 +08:00
|
|
|
|
|
|
|
|
|
|
if (crossDay && backward) {
|
2026-05-15 18:04:40 +08:00
|
|
|
|
windowStart = st != null
|
|
|
|
|
|
? LocalDateTime.of(ld.minusDays(1), st).minusHours(2)
|
|
|
|
|
|
: LocalDateTime.of(ld, et).minusHours(2);
|
2026-05-14 17:41:04 +08:00
|
|
|
|
windowEnd = LocalDateTime.of(ld, et).plusHours(2);
|
|
|
|
|
|
} else if (crossDay) {
|
2026-05-13 16:18:05 +08:00
|
|
|
|
windowStart = LocalDateTime.of(ld, st).minusHours(2);
|
|
|
|
|
|
windowEnd = LocalDateTime.of(ld.plusDays(1), et).plusHours(2);
|
2026-05-14 17:13:26 +08:00
|
|
|
|
} else if (st != null && et != null) {
|
2026-05-13 16:18:05 +08:00
|
|
|
|
windowStart = LocalDateTime.of(ld, st).minusHours(2);
|
|
|
|
|
|
windowEnd = LocalDateTime.of(ld, et).plusHours(2);
|
2026-05-14 17:13:26 +08:00
|
|
|
|
} 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);
|
2026-05-13 16:18:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-14 17:13:26 +08:00
|
|
|
|
LocalDateTime finalStart = windowStart;
|
|
|
|
|
|
LocalDateTime finalEnd = windowEnd;
|
2026-05-13 16:18:05 +08:00
|
|
|
|
return records.stream()
|
|
|
|
|
|
.filter(r -> {
|
|
|
|
|
|
LocalDateTime ct = toLocalDateTime(r.getChecktime());
|
2026-05-14 17:13:26 +08:00
|
|
|
|
return !ct.isBefore(finalStart) && !ct.isAfter(finalEnd);
|
2026-05-13 16:18:05 +08:00
|
|
|
|
})
|
|
|
|
|
|
.collect(Collectors.toList());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 16:29:54 +08:00
|
|
|
|
private void checkPeriod(WmsAttendanceCheck check, WmsAttendanceRule rule, int period,
|
2026-05-27 14:16:34 +08:00
|
|
|
|
List<AttendanceRecords> periodRecords, Date expectedStart, Date expectedEnd) {
|
2026-06-01 14:38:56 +08:00
|
|
|
|
// 旷工:0条打卡记录
|
2026-05-13 16:18:05 +08:00
|
|
|
|
if (periodRecords.isEmpty()) {
|
2026-05-12 16:29:54 +08:00
|
|
|
|
if (period == 1) {
|
|
|
|
|
|
check.setP1Status("missed");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
check.setP2Status("missed");
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-27 14:16:34 +08:00
|
|
|
|
LocalDateTime expStart = toLocalDateTime(expectedStart);
|
|
|
|
|
|
LocalDateTime expEnd = toLocalDateTime(expectedEnd);
|
2026-05-13 16:18:05 +08:00
|
|
|
|
AttendanceRecords firstRec = periodRecords.get(0);
|
|
|
|
|
|
AttendanceRecords lastRec = periodRecords.get(periodRecords.size() - 1);
|
2026-05-27 14:16:34 +08:00
|
|
|
|
LocalDateTime firstCheck = toLocalDateTime(firstRec.getChecktime());
|
|
|
|
|
|
LocalDateTime lastCheck = toLocalDateTime(lastRec.getChecktime());
|
2026-05-12 16:29:54 +08:00
|
|
|
|
|
|
|
|
|
|
int lateMinutes = 0;
|
|
|
|
|
|
int earlyMinutes = 0;
|
|
|
|
|
|
BigDecimal deduct = BigDecimal.ZERO;
|
|
|
|
|
|
String status = "normal";
|
2026-06-01 14:38:56 +08:00
|
|
|
|
boolean startMissed = false;
|
|
|
|
|
|
boolean endMissed = false;
|
|
|
|
|
|
|
|
|
|
|
|
// 上班检测:晚于理论上班时间30分钟以上 → 漏打卡
|
|
|
|
|
|
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";
|
|
|
|
|
|
}
|
2026-05-12 16:29:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-01 14:38:56 +08:00
|
|
|
|
// 下班检测:早于理论下班时间30分钟以上 → 漏打卡
|
2026-05-14 17:13:26 +08:00
|
|
|
|
if (expEnd != null && lastCheck.isBefore(expEnd)) {
|
2026-06-01 14:38:56 +08:00
|
|
|
|
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";
|
|
|
|
|
|
}
|
2026-05-12 16:29:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-01 14:38:56 +08:00
|
|
|
|
// 上班漏打卡或下班漏打卡会覆盖原有状态
|
|
|
|
|
|
if (startMissed && endMissed) {
|
|
|
|
|
|
status = "missed";
|
|
|
|
|
|
} else if (startMissed) {
|
|
|
|
|
|
status = maxSeverity(status, "missed_start");
|
|
|
|
|
|
} else if (endMissed) {
|
|
|
|
|
|
status = maxSeverity(status, "missed_end");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 16:29:54 +08:00
|
|
|
|
if (period == 1) {
|
2026-06-01 14:38:56 +08:00
|
|
|
|
check.setP1FirstCheck(startMissed ? null : firstRec.getChecktime());
|
|
|
|
|
|
check.setP1LastCheck(endMissed ? null : lastRec.getChecktime());
|
2026-05-12 16:29:54 +08:00
|
|
|
|
check.setP1LateMinutes(lateMinutes);
|
|
|
|
|
|
check.setP1EarlyMinutes(earlyMinutes);
|
|
|
|
|
|
check.setP1Status(status);
|
|
|
|
|
|
check.setP1Deduct(deduct);
|
|
|
|
|
|
} else {
|
2026-06-01 14:38:56 +08:00
|
|
|
|
check.setP2FirstCheck(startMissed ? null : firstRec.getChecktime());
|
|
|
|
|
|
check.setP2LastCheck(endMissed ? null : lastRec.getChecktime());
|
2026-05-12 16:29:54 +08:00
|
|
|
|
check.setP2LateMinutes(lateMinutes);
|
|
|
|
|
|
check.setP2EarlyMinutes(earlyMinutes);
|
|
|
|
|
|
check.setP2Status(status);
|
|
|
|
|
|
check.setP2Deduct(deduct);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private String maxSeverity(String a, String b) {
|
2026-05-14 17:13:26 +08:00
|
|
|
|
int ai = STATUS_SEVERITY.indexOf(a);
|
|
|
|
|
|
int bi = STATUS_SEVERITY.indexOf(b);
|
|
|
|
|
|
return STATUS_SEVERITY.get(Math.max(ai, bi));
|
2026-05-12 16:29:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
2026-06-01 14:38:56 +08:00
|
|
|
|
|
|
|
|
|
|
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;
|
2026-05-27 14:16:34 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
check.setAbsentType("full_day");
|
|
|
|
|
|
check.setOverallStatus("absent_full");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-05-12 16:29:54 +08:00
|
|
|
|
}
|
2026-06-01 14:38:56 +08:00
|
|
|
|
if (p2Missed) {
|
|
|
|
|
|
check.setAbsentType("half_day");
|
|
|
|
|
|
check.setOverallStatus("absent_half");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-05-12 16:29:54 +08:00
|
|
|
|
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");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-27 14:16:34 +08:00
|
|
|
|
private void updateContinuousAbsent(LocalDate startDate, LocalDate endDate, Set<Long> affectedUserIds) {
|
|
|
|
|
|
if (affectedUserIds.isEmpty()) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-05-12 16:29:54 +08:00
|
|
|
|
List<WmsAttendanceCheck> checks = baseMapper.selectList(Wrappers.<WmsAttendanceCheck>lambdaQuery()
|
2026-05-27 14:16:34 +08:00
|
|
|
|
.in(WmsAttendanceCheck::getUserId, affectedUserIds)
|
2026-05-12 16:29:54 +08:00
|
|
|
|
.ge(WmsAttendanceCheck::getWorkDate, toDate(startDate.atStartOfDay()))
|
|
|
|
|
|
.le(WmsAttendanceCheck::getWorkDate, toDate(endDate.atTime(LocalTime.of(23, 59, 59))))
|
|
|
|
|
|
.eq(WmsAttendanceCheck::getDelFlag, 0));
|
|
|
|
|
|
|
2026-05-14 17:13:26 +08:00
|
|
|
|
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<>();
|
2026-05-12 16:29:54 +08:00
|
|
|
|
for (WmsAttendanceCheck check : checks) {
|
|
|
|
|
|
if (check.getAbsentType() != null) {
|
2026-05-14 17:13:26 +08:00
|
|
|
|
int continuous = countContinuousAbsentCached(
|
|
|
|
|
|
check.getUserId(), toLocalDate(check.getWorkDate()), historyByUserDate);
|
2026-05-12 16:29:54 +08:00
|
|
|
|
check.setContinuousAbsentDays(continuous);
|
2026-05-14 17:13:26 +08:00
|
|
|
|
toUpdate.add(check);
|
2026-05-12 16:29:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-14 17:13:26 +08:00
|
|
|
|
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<>();
|
2026-05-27 14:16:34 +08:00
|
|
|
|
LocalDate startDate = endDate.minusDays(100);
|
|
|
|
|
|
Date start = toDate(startDate.atStartOfDay());
|
2026-05-14 17:13:26 +08:00
|
|
|
|
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)
|
2026-05-27 14:16:34 +08:00
|
|
|
|
.ge(WmsAttendanceCheck::getWorkDate, start)
|
2026-05-14 17:13:26 +08:00
|
|
|
|
.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;
|
2026-05-12 16:29:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-14 17:13:26 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2026-05-12 16:29:54 +08:00
|
|
|
|
int count = 0;
|
|
|
|
|
|
LocalDate date = workDate.minusDays(1);
|
|
|
|
|
|
while (true) {
|
2026-05-14 17:13:26 +08:00
|
|
|
|
WmsAttendanceCheck prev = userMap.get(date);
|
2026-05-12 16:29:54 +08:00
|
|
|
|
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());
|
|
|
|
|
|
}
|
2026-05-15 17:05:10 +08:00
|
|
|
|
|
|
|
|
|
|
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));
|
|
|
|
|
|
}
|
2026-05-12 16:29:54 +08:00
|
|
|
|
}
|