diff --git a/klp-ui/src/views/wms/hrm/attendance/attendanceCheck.vue b/klp-ui/src/views/wms/hrm/attendance/attendanceCheck.vue
index 9e8b4586..6c4e8004 100644
--- a/klp-ui/src/views/wms/hrm/attendance/attendanceCheck.vue
+++ b/klp-ui/src/views/wms/hrm/attendance/attendanceCheck.vue
@@ -403,10 +403,20 @@
placeholder="请选择结束日期">
+
+
+
+
+
@@ -499,6 +509,7 @@ export default {
departmentList: [],
selectedDept: '',
currentDeptEmployeeIds: '',
+ checkDept: '',
selectedUserIds: [],
detailRecordsLoading: false,
@@ -508,9 +519,18 @@ export default {
computed: {
transferData() {
return this.allEmployees.map(emp => ({
- key: emp.infoId,
+ key: String(emp.infoId),
label: emp.name + '(' + emp.dept + ')'
}))
+ },
+ checkTransferData() {
+ if (!this.checkDept) return this.transferData
+ return this.allEmployees
+ .filter(emp => (emp.dept || '未分配部门') === this.checkDept)
+ .map(emp => ({
+ key: String(emp.infoId),
+ label: emp.name + '(' + emp.dept + ')'
+ }))
}
},
created() {
@@ -548,15 +568,18 @@ export default {
this.allEmployees.forEach(emp => {
const deptName = emp.dept || '未分配部门'
if (!deptMap[deptName]) {
- deptMap[deptName] = []
+ deptMap[deptName] = new Set()
+ }
+ deptMap[deptName].add(String(emp.infoId))
+ })
+ this.departmentList = Object.keys(deptMap).map(deptName => {
+ const ids = [...deptMap[deptName]]
+ return {
+ deptName,
+ count: ids.length,
+ empIds: ids.join(',')
}
- deptMap[deptName].push(emp.infoId)
})
- this.departmentList = Object.keys(deptMap).map(deptName => ({
- deptName,
- count: deptMap[deptName].length,
- empIds: deptMap[deptName].join(',')
- }))
if (this.departmentList.length > 0) {
this.selectedDept = this.departmentList[0].deptName
this.currentDeptEmployeeIds = this.departmentList[0].empIds
@@ -841,23 +864,35 @@ export default {
startDate: this.dateRangeParams.startDate,
endDate: this.dateRangeParams.endDate
}
- this.selectedUserIds = []
+ this.checkDept = this.selectedDept || ''
+ if (this.checkDept) {
+ const dept = this.departmentList.find(d => d.deptName === this.checkDept)
+ this.selectedUserIds = dept ? dept.empIds.split(',') : []
+ } else {
+ this.selectedUserIds = []
+ }
},
cancelCheck() {
this.checkOpen = false
this.checkForm = { startDate: undefined, endDate: undefined }
this.selectedUserIds = []
+ this.checkDept = ''
this.$refs['checkForm'] && this.$refs['checkForm'].resetFields()
},
+ handleCheckDeptChange(deptName) {
+ const dept = this.departmentList.find(d => d.deptName === deptName)
+ this.selectedUserIds = dept ? dept.empIds.split(',') : []
+ },
+
submitCheck() {
this.$refs["checkForm"].validate(valid => {
if (valid) {
this.checkLoading = true
const params = {
...this.checkForm,
- userIds: this.selectedUserIds.join(',')
+ userIds: this.selectedUserIds
}
generateAttendanceCheck(params).then(response => {
this.$modal.msgSuccess("比对成功")
diff --git a/klp-wms/src/main/java/com/klp/service/IAttendanceRecordsService.java b/klp-wms/src/main/java/com/klp/service/IAttendanceRecordsService.java
index c9e9018e..c6028b50 100644
--- a/klp-wms/src/main/java/com/klp/service/IAttendanceRecordsService.java
+++ b/klp-wms/src/main/java/com/klp/service/IAttendanceRecordsService.java
@@ -7,6 +7,7 @@ import com.klp.common.core.page.TableDataInfo;
import com.klp.common.core.domain.PageQuery;
import java.util.Collection;
+import java.util.Date;
import java.util.List;
public interface IAttendanceRecordsService {
@@ -22,4 +23,9 @@ public interface IAttendanceRecordsService {
Boolean updateByBo(AttendanceRecordsBo bo);
Boolean deleteWithValidByIds(Collection ids, Boolean isValid);
+
+ /**
+ * 按员工姓名集合 + 时间范围精确查询打卡记录(eq/in,走索引)
+ */
+ List queryListByEnamesAndDateRange(List enames, Date startTime, Date endTime);
}
\ No newline at end of file
diff --git a/klp-wms/src/main/java/com/klp/service/impl/AttendanceRecordsServiceImpl.java b/klp-wms/src/main/java/com/klp/service/impl/AttendanceRecordsServiceImpl.java
index e6936583..31afd40a 100644
--- a/klp-wms/src/main/java/com/klp/service/impl/AttendanceRecordsServiceImpl.java
+++ b/klp-wms/src/main/java/com/klp/service/impl/AttendanceRecordsServiceImpl.java
@@ -15,6 +15,7 @@ import com.klp.domain.AttendanceRecords;
import com.klp.mapper.AttendanceRecordsMapper;
import com.klp.service.IAttendanceRecordsService;
+import java.util.Date;
import java.util.List;
import java.util.Collection;
@@ -81,4 +82,13 @@ public class AttendanceRecordsServiceImpl implements IAttendanceRecordsService {
}
return baseMapper.deleteBatchIds(ids) > 0;
}
+
+ @Override
+ public List queryListByEnamesAndDateRange(List enames, Date startTime, Date endTime) {
+ return baseMapper.selectList(Wrappers.lambdaQuery()
+ .select(AttendanceRecords::getId, AttendanceRecords::getEname, AttendanceRecords::getChecktime)
+ .in(AttendanceRecords::getEname, enames)
+ .ge(AttendanceRecords::getChecktime, startTime)
+ .le(AttendanceRecords::getChecktime, endTime));
+ }
}
\ No newline at end of file
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 71f0ed1e..6b26483a 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
@@ -37,8 +37,11 @@ import java.util.Collection;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
import java.util.stream.Collectors;
@RequiredArgsConstructor
@@ -97,60 +100,114 @@ public class WmsAttendanceCheckServiceImpl implements IWmsAttendanceCheckService
return lqw;
}
+ /**
+ * 检查员工考勤情况
+ *
+ * @param bo 考勤检查业务对象,包含开始日期、结束日期和用户ID等信息
+ */
@Override
- @Transactional(rollbackFor = Exception.class)
+ @Transactional(rollbackFor = Exception.class) // 使用事务注解,确保方法内所有操作要么全部成功,要么全部回滚
public void checkAttendance(AttendanceCheckBo bo) {
+ // 创建考勤计划业务对象,并设置查询参数
WmsAttendanceScheduleBo scheduleBo = new WmsAttendanceScheduleBo();
- scheduleBo.setStartDate(bo.getStartDate());
- scheduleBo.setEndDate(bo.getEndDate());
- scheduleBo.setUserIds(bo.getUserIds());
+ scheduleBo.setStartDate(bo.getStartDate()); // 设置开始日期
+ scheduleBo.setEndDate(bo.getEndDate()); // 设置结束日期
+ scheduleBo.setUserIds(bo.getUserIds()); // 设置用户ID列表
+ // 根据查询条件获取考勤计划列表
List schedules = scheduleService.queryList(scheduleBo);
+ // 如果没有考勤计划,直接返回
if (schedules.isEmpty()) {
return;
}
+ // 获取当前有效的考勤规则
WmsAttendanceRule rule = getActiveRule();
+ // 将日期转换为LocalDate类型
LocalDate startLocal = toLocalDate(bo.getStartDate());
LocalDate endLocal = toLocalDate(bo.getEndDate());
+ // 创建待处理的考勤计划列表
List toProcess = new ArrayList<>();
+ // 遍历考勤计划,过滤掉无效数据
for (WmsAttendanceScheduleVo schedule : schedules) {
- if (schedule.getEmployeeName() == null) {
+ if (schedule.getEmployeeName() == null) { // 跳过员工名为空的记录
continue;
}
- if (schedule.getShiftStartTime() == null && schedule.getShiftEndTime() == null) {
+ if (schedule.getShiftStartTime() == null && schedule.getShiftEndTime() == null) { // 跳过上下班时间为空的记录
continue;
}
- toProcess.add(schedule);
+ toProcess.add(schedule); // 添加到待处理列表
}
+ // 如果过滤后没有有效的考勤计划,直接返回
if (toProcess.isEmpty()) {
return;
}
+ // 倒班日重叠检测:若某天有正向跨天夜班(19:00→07:00),且次日存在同员工、同上班时间的反向跨天班(夜转白 19:00→13:00),
+ // 则正向跨天班的下班时间校验跳过(实际下班按反向跨天班 13:00 算),但上班打卡仍然校验
+ // 创建映射表,记录被反向跨天班覆盖的记录
+ Map 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 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 affectedUserIds = toProcess.stream()
+ .map(WmsAttendanceScheduleVo::getUserId)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toSet());
+
+ // 按员工预取考勤记录
Map> recordsByEmployee = prefetchAttendanceRecords(toProcess);
+ // 获取所有待处理考勤计划的ID列表
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);
boolean backward = isBackwardCrossDay(schedule);
+ // 根据考勤计划和考勤记录,按天切片考勤记录
List records = sliceRecordsForDay(
recordsByEmployee.get(schedule.getEmployeeName()),
schedule.getWorkDate(),
crossDay, backward);
- checksToInsert.add(buildCheck(schedule, rule, records, crossDay));
+ // 构建考勤检查记录并添加到插入列表
+ checksToInsert.add(buildCheck(schedule, rule, records, crossDay, endCheckOverridden));
}
+ // 批量插入考勤检查记录
baseMapper.insertBatch(checksToInsert, BATCH_SIZE);
- updateContinuousAbsent(startLocal, endLocal);
+ // 更新连续缺勤记录
+ updateContinuousAbsent(startLocal, endLocal, affectedUserIds);
}
private boolean isCrossDayShift(WmsAttendanceScheduleVo schedule) {
@@ -182,7 +239,7 @@ public class WmsAttendanceCheckServiceImpl implements IWmsAttendanceCheckService
}
/**
- * 按员工姓名合并时间范围,每人只查一次打卡(与原先按排班逐条查询结果一致)。
+ * 一次 SQL 批量查询所有员工在日期范围内的打卡记录(用 IN/eq 精确匹配,走索引)。
*/
private Map> prefetchAttendanceRecords(List toProcess) {
Map minByName = new HashMap<>();
@@ -193,37 +250,28 @@ public class WmsAttendanceCheckServiceImpl implements IWmsAttendanceCheckService
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));
+ 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 allNames = new ArrayList<>(minByName.keySet());
+ List allRecords = attendanceRecordsService.queryListByEnamesAndDateRange(
+ allNames, rangeStart, rangeEnd);
+
+ Map> 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<>());
}
return out;
}
- private List 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 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 sliceRecordsForDay(List prefetched, Date workDate, boolean crossDay, boolean backward) {
if (prefetched == null || prefetched.isEmpty()) {
return new ArrayList<>();
@@ -256,9 +304,22 @@ public class WmsAttendanceCheckServiceImpl implements IWmsAttendanceCheckService
}
}
+ /**
+ * 构建考勤检查对象,根据排班信息和考勤记录生成考勤结果
+ *
+ * @param schedule 排班信息对象
+ * @param rule 考勤规则对象
+ * @param records 考勤记录列表
+ * @param crossDay 是否跨天
+ * @param endCheckOverridden 已覆盖的结束检查集合
+ * @return 构建好的考勤检查对象
+ */
private WmsAttendanceCheck buildCheck(WmsAttendanceScheduleVo schedule, WmsAttendanceRule rule,
- List records, boolean crossDay) {
+ List records, boolean crossDay,
+ Set endCheckOverridden) {
+ // 创建考勤检查对象
WmsAttendanceCheck check = new WmsAttendanceCheck();
+ // 设置排班基本信息
check.setScheduleId(schedule.getScheduleId());
check.setUserId(schedule.getUserId());
check.setEmployeeName(schedule.getEmployeeName());
@@ -267,13 +328,22 @@ public class WmsAttendanceCheckServiceImpl implements IWmsAttendanceCheckService
check.setShiftName(schedule.getShiftName());
check.setShiftType(schedule.getShiftType());
+ // 判断是否有第二时段
boolean hasPeriod2 = schedule.getShiftStartTime2() != null && schedule.getShiftEndTime2() != null;
+ // 判断是否为向后跨天
boolean backward = isBackwardCrossDay(schedule);
+ // 处理时段设置
if (hasPeriod2) {
- check.setP1StartTime(combineTime(schedule.getWorkDate(), schedule.getShiftStartTime()));
- check.setP1EndTime(combineTime(schedule.getWorkDate(), schedule.getShiftEndTime()));
+ // 设置第一时段开始时间,考虑跨天情况
+ 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()));
check.setP2StartTime(combineTime(schedule.getWorkDate(), schedule.getShiftStartTime2()));
check.setP2EndTime(combineTime(schedule.getWorkDate(), schedule.getShiftEndTime2()));
} else {
@@ -313,12 +383,14 @@ public class WmsAttendanceCheckServiceImpl implements IWmsAttendanceCheckService
}
}
- checkPeriod(check, rule, 1, p1Records, schedule.getShiftStartTime(), schedule.getShiftEndTime());
- checkPeriod(check, rule, 2, p2Records, schedule.getShiftStartTime2(), schedule.getShiftEndTime2());
+ checkPeriod(check, rule, 1, p1Records, check.getP1StartTime(), check.getP1EndTime());
+ checkPeriod(check, rule, 2, p2Records, check.getP2StartTime(), check.getP2EndTime());
} else {
List filtered = filterWindow(records, schedule.getWorkDate(),
schedule.getShiftStartTime(), schedule.getShiftEndTime(), crossDay);
- checkPeriod(check, rule, 1, filtered, schedule.getShiftStartTime(), schedule.getShiftEndTime());
+ Date actualEnd = endCheckOverridden.contains(schedule.getScheduleId())
+ ? null : check.getP1EndTime();
+ checkPeriod(check, rule, 1, filtered, check.getP1StartTime(), actualEnd);
}
calculateOverall(check, rule);
@@ -326,7 +398,7 @@ public class WmsAttendanceCheckServiceImpl implements IWmsAttendanceCheckService
}
private List filterWindow(List records, Date workDate,
- Date expectedStart, Date expectedEnd, boolean crossDay) {
+ Date expectedStart, Date expectedEnd, boolean crossDay) {
LocalDate ld = toLocalDate(workDate);
LocalTime st = toLocalTime(expectedStart);
LocalTime et = toLocalTime(expectedEnd);
@@ -366,7 +438,7 @@ public class WmsAttendanceCheckServiceImpl implements IWmsAttendanceCheckService
}
private void checkPeriod(WmsAttendanceCheck check, WmsAttendanceRule rule, int period,
- List periodRecords, Date expectedStart, Date expectedEnd) {
+ List periodRecords, Date expectedStart, Date expectedEnd) {
if (periodRecords.isEmpty()) {
if (period == 1) {
check.setP1Status("missed");
@@ -376,12 +448,12 @@ public class WmsAttendanceCheckServiceImpl implements IWmsAttendanceCheckService
return;
}
- LocalTime expStart = toLocalTime(expectedStart);
- LocalTime expEnd = toLocalTime(expectedEnd);
+ LocalDateTime expStart = toLocalDateTime(expectedStart);
+ LocalDateTime expEnd = toLocalDateTime(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();
+ LocalDateTime firstCheck = toLocalDateTime(firstRec.getChecktime());
+ LocalDateTime lastCheck = toLocalDateTime(lastRec.getChecktime());
int lateMinutes = 0;
int earlyMinutes = 0;
@@ -459,10 +531,18 @@ public class WmsAttendanceCheckServiceImpl implements IWmsAttendanceCheckService
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 ("missed".equals(check.getP1Status())) {
+ if (check.getP2StartTime() != null) {
+ if ("missed".equals(check.getP2Status())) {
+ check.setAbsentType("full_day");
+ check.setOverallStatus("absent_full");
+ return;
+ }
+ } else {
+ check.setAbsentType("full_day");
+ check.setOverallStatus("absent_full");
+ return;
+ }
}
if (check.getP1StartTime() == null && check.getP2StartTime() == null) {
check.setAbsentType("full_day");
@@ -479,8 +559,12 @@ public class WmsAttendanceCheckServiceImpl implements IWmsAttendanceCheckService
check.setOverallStatus(abnormal ? "abnormal" : "normal");
}
- private void updateContinuousAbsent(LocalDate startDate, LocalDate endDate) {
+ private void updateContinuousAbsent(LocalDate startDate, LocalDate endDate, Set affectedUserIds) {
+ if (affectedUserIds.isEmpty()) {
+ return;
+ }
List checks = baseMapper.selectList(Wrappers.lambdaQuery()
+ .in(WmsAttendanceCheck::getUserId, affectedUserIds)
.ge(WmsAttendanceCheck::getWorkDate, toDate(startDate.atStartOfDay()))
.le(WmsAttendanceCheck::getWorkDate, toDate(endDate.atTime(LocalTime.of(23, 59, 59))))
.eq(WmsAttendanceCheck::getDelFlag, 0));
@@ -515,6 +599,8 @@ public class WmsAttendanceCheckServiceImpl implements IWmsAttendanceCheckService
*/
private Map> loadAbsentHistoryByUser(List userIds, LocalDate endDate) {
Map> out = new HashMap<>();
+ LocalDate startDate = endDate.minusDays(100);
+ Date start = toDate(startDate.atStartOfDay());
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());
@@ -523,6 +609,7 @@ public class WmsAttendanceCheckServiceImpl implements IWmsAttendanceCheckService
.select(WmsAttendanceCheck::getUserId, WmsAttendanceCheck::getWorkDate, WmsAttendanceCheck::getAbsentType,
WmsAttendanceCheck::getCheckId, WmsAttendanceCheck::getDelFlag)
.in(WmsAttendanceCheck::getUserId, chunk)
+ .ge(WmsAttendanceCheck::getWorkDate, start)
.le(WmsAttendanceCheck::getWorkDate, end)
.eq(WmsAttendanceCheck::getDelFlag, 0));
for (WmsAttendanceCheck row : rows) {