feat(wms/attendance): 新增考勤检查部门筛选功能,优化跨天排班逻辑与打卡记录批量查询
1. 前端新增部门筛选下拉框,支持按部门筛选员工并自动勾选,优化穿梭框数据映射逻辑 2. 后端实现跨天排班重叠检测机制,正向跨天夜班被反向跨天班覆盖时跳过下班打卡校验 3. 重构打卡记录查询为批量预取模式,通过单次SQL查询提升性能,支持按员工姓名集合和时间范围精确检索 4. 优化考勤检查记录构建逻辑,调整时段时间计算方式,完善全天缺勤状态判断规则
This commit is contained in:
@@ -403,10 +403,20 @@
|
|||||||
placeholder="请选择结束日期">
|
placeholder="请选择结束日期">
|
||||||
</el-date-picker>
|
</el-date-picker>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="选择部门" prop="dept">
|
||||||
|
<el-select v-model="checkDept" placeholder="请选择部门" @change="handleCheckDeptChange">
|
||||||
|
<el-option
|
||||||
|
v-for="item in departmentList"
|
||||||
|
:key="item.deptName"
|
||||||
|
:label="item.deptName + '(' + item.count + '人)'"
|
||||||
|
:value="item.deptName"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="选择员工" prop="userIds">
|
<el-form-item label="选择员工" prop="userIds">
|
||||||
<el-transfer
|
<el-transfer
|
||||||
v-model="selectedUserIds"
|
v-model="selectedUserIds"
|
||||||
:data="transferData"
|
:data="checkTransferData"
|
||||||
:titles="['待选员工', '已选员工']"
|
:titles="['待选员工', '已选员工']"
|
||||||
filterable
|
filterable
|
||||||
filter-placeholder="请输入员工姓名">
|
filter-placeholder="请输入员工姓名">
|
||||||
@@ -499,6 +509,7 @@ export default {
|
|||||||
departmentList: [],
|
departmentList: [],
|
||||||
selectedDept: '',
|
selectedDept: '',
|
||||||
currentDeptEmployeeIds: '',
|
currentDeptEmployeeIds: '',
|
||||||
|
checkDept: '',
|
||||||
selectedUserIds: [],
|
selectedUserIds: [],
|
||||||
|
|
||||||
detailRecordsLoading: false,
|
detailRecordsLoading: false,
|
||||||
@@ -508,9 +519,18 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
transferData() {
|
transferData() {
|
||||||
return this.allEmployees.map(emp => ({
|
return this.allEmployees.map(emp => ({
|
||||||
key: emp.infoId,
|
key: String(emp.infoId),
|
||||||
label: emp.name + '(' + emp.dept + ')'
|
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() {
|
created() {
|
||||||
@@ -548,15 +568,18 @@ export default {
|
|||||||
this.allEmployees.forEach(emp => {
|
this.allEmployees.forEach(emp => {
|
||||||
const deptName = emp.dept || '未分配部门'
|
const deptName = emp.dept || '未分配部门'
|
||||||
if (!deptMap[deptName]) {
|
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) {
|
if (this.departmentList.length > 0) {
|
||||||
this.selectedDept = this.departmentList[0].deptName
|
this.selectedDept = this.departmentList[0].deptName
|
||||||
this.currentDeptEmployeeIds = this.departmentList[0].empIds
|
this.currentDeptEmployeeIds = this.departmentList[0].empIds
|
||||||
@@ -841,23 +864,35 @@ export default {
|
|||||||
startDate: this.dateRangeParams.startDate,
|
startDate: this.dateRangeParams.startDate,
|
||||||
endDate: this.dateRangeParams.endDate
|
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() {
|
cancelCheck() {
|
||||||
this.checkOpen = false
|
this.checkOpen = false
|
||||||
this.checkForm = { startDate: undefined, endDate: undefined }
|
this.checkForm = { startDate: undefined, endDate: undefined }
|
||||||
this.selectedUserIds = []
|
this.selectedUserIds = []
|
||||||
|
this.checkDept = ''
|
||||||
this.$refs['checkForm'] && this.$refs['checkForm'].resetFields()
|
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() {
|
submitCheck() {
|
||||||
this.$refs["checkForm"].validate(valid => {
|
this.$refs["checkForm"].validate(valid => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
this.checkLoading = true
|
this.checkLoading = true
|
||||||
const params = {
|
const params = {
|
||||||
...this.checkForm,
|
...this.checkForm,
|
||||||
userIds: this.selectedUserIds.join(',')
|
userIds: this.selectedUserIds
|
||||||
}
|
}
|
||||||
generateAttendanceCheck(params).then(response => {
|
generateAttendanceCheck(params).then(response => {
|
||||||
this.$modal.msgSuccess("比对成功")
|
this.$modal.msgSuccess("比对成功")
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import com.klp.common.core.page.TableDataInfo;
|
|||||||
import com.klp.common.core.domain.PageQuery;
|
import com.klp.common.core.domain.PageQuery;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public interface IAttendanceRecordsService {
|
public interface IAttendanceRecordsService {
|
||||||
@@ -22,4 +23,9 @@ public interface IAttendanceRecordsService {
|
|||||||
Boolean updateByBo(AttendanceRecordsBo bo);
|
Boolean updateByBo(AttendanceRecordsBo bo);
|
||||||
|
|
||||||
Boolean deleteWithValidByIds(Collection<Integer> ids, Boolean isValid);
|
Boolean deleteWithValidByIds(Collection<Integer> ids, Boolean isValid);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按员工姓名集合 + 时间范围精确查询打卡记录(eq/in,走索引)
|
||||||
|
*/
|
||||||
|
List<AttendanceRecords> queryListByEnamesAndDateRange(List<String> enames, Date startTime, Date endTime);
|
||||||
}
|
}
|
||||||
@@ -15,6 +15,7 @@ import com.klp.domain.AttendanceRecords;
|
|||||||
import com.klp.mapper.AttendanceRecordsMapper;
|
import com.klp.mapper.AttendanceRecordsMapper;
|
||||||
import com.klp.service.IAttendanceRecordsService;
|
import com.klp.service.IAttendanceRecordsService;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
|
||||||
@@ -81,4 +82,13 @@ public class AttendanceRecordsServiceImpl implements IAttendanceRecordsService {
|
|||||||
}
|
}
|
||||||
return baseMapper.deleteBatchIds(ids) > 0;
|
return baseMapper.deleteBatchIds(ids) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AttendanceRecords> queryListByEnamesAndDateRange(List<String> enames, Date startTime, Date endTime) {
|
||||||
|
return baseMapper.selectList(Wrappers.<AttendanceRecords>lambdaQuery()
|
||||||
|
.select(AttendanceRecords::getId, AttendanceRecords::getEname, AttendanceRecords::getChecktime)
|
||||||
|
.in(AttendanceRecords::getEname, enames)
|
||||||
|
.ge(AttendanceRecords::getChecktime, startTime)
|
||||||
|
.le(AttendanceRecords::getChecktime, endTime));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -37,8 +37,11 @@ import java.util.Collection;
|
|||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -97,60 +100,114 @@ public class WmsAttendanceCheckServiceImpl implements IWmsAttendanceCheckService
|
|||||||
return lqw;
|
return lqw;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查员工考勤情况
|
||||||
|
*
|
||||||
|
* @param bo 考勤检查业务对象,包含开始日期、结束日期和用户ID等信息
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class) // 使用事务注解,确保方法内所有操作要么全部成功,要么全部回滚
|
||||||
public void checkAttendance(AttendanceCheckBo bo) {
|
public void checkAttendance(AttendanceCheckBo bo) {
|
||||||
|
// 创建考勤计划业务对象,并设置查询参数
|
||||||
WmsAttendanceScheduleBo scheduleBo = new WmsAttendanceScheduleBo();
|
WmsAttendanceScheduleBo scheduleBo = new WmsAttendanceScheduleBo();
|
||||||
scheduleBo.setStartDate(bo.getStartDate());
|
scheduleBo.setStartDate(bo.getStartDate()); // 设置开始日期
|
||||||
scheduleBo.setEndDate(bo.getEndDate());
|
scheduleBo.setEndDate(bo.getEndDate()); // 设置结束日期
|
||||||
scheduleBo.setUserIds(bo.getUserIds());
|
scheduleBo.setUserIds(bo.getUserIds()); // 设置用户ID列表
|
||||||
|
// 根据查询条件获取考勤计划列表
|
||||||
List<WmsAttendanceScheduleVo> schedules = scheduleService.queryList(scheduleBo);
|
List<WmsAttendanceScheduleVo> schedules = scheduleService.queryList(scheduleBo);
|
||||||
|
|
||||||
|
// 如果没有考勤计划,直接返回
|
||||||
if (schedules.isEmpty()) {
|
if (schedules.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取当前有效的考勤规则
|
||||||
WmsAttendanceRule rule = getActiveRule();
|
WmsAttendanceRule rule = getActiveRule();
|
||||||
|
|
||||||
|
// 将日期转换为LocalDate类型
|
||||||
LocalDate startLocal = toLocalDate(bo.getStartDate());
|
LocalDate startLocal = toLocalDate(bo.getStartDate());
|
||||||
LocalDate endLocal = toLocalDate(bo.getEndDate());
|
LocalDate endLocal = toLocalDate(bo.getEndDate());
|
||||||
|
|
||||||
|
// 创建待处理的考勤计划列表
|
||||||
List<WmsAttendanceScheduleVo> toProcess = new ArrayList<>();
|
List<WmsAttendanceScheduleVo> toProcess = new ArrayList<>();
|
||||||
|
// 遍历考勤计划,过滤掉无效数据
|
||||||
for (WmsAttendanceScheduleVo schedule : schedules) {
|
for (WmsAttendanceScheduleVo schedule : schedules) {
|
||||||
if (schedule.getEmployeeName() == null) {
|
if (schedule.getEmployeeName() == null) { // 跳过员工名为空的记录
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (schedule.getShiftStartTime() == null && schedule.getShiftEndTime() == null) {
|
if (schedule.getShiftStartTime() == null && schedule.getShiftEndTime() == null) { // 跳过上下班时间为空的记录
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
toProcess.add(schedule);
|
toProcess.add(schedule); // 添加到待处理列表
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果过滤后没有有效的考勤计划,直接返回
|
||||||
if (toProcess.isEmpty()) {
|
if (toProcess.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 倒班日重叠检测:若某天有正向跨天夜班(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());
|
||||||
|
|
||||||
|
// 按员工预取考勤记录
|
||||||
Map<String, List<AttendanceRecords>> recordsByEmployee = prefetchAttendanceRecords(toProcess);
|
Map<String, List<AttendanceRecords>> recordsByEmployee = prefetchAttendanceRecords(toProcess);
|
||||||
|
|
||||||
|
// 获取所有待处理考勤计划的ID列表
|
||||||
List<Long> scheduleIds = toProcess.stream()
|
List<Long> scheduleIds = toProcess.stream()
|
||||||
.map(WmsAttendanceScheduleVo::getScheduleId)
|
.map(WmsAttendanceScheduleVo::getScheduleId)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
// 批量删除这些考勤计划对应的考勤检查记录
|
||||||
batchDeleteByScheduleIds(scheduleIds);
|
batchDeleteByScheduleIds(scheduleIds);
|
||||||
|
|
||||||
|
// 创建要插入的考勤检查记录列表
|
||||||
List<WmsAttendanceCheck> checksToInsert = new ArrayList<>(toProcess.size());
|
List<WmsAttendanceCheck> checksToInsert = new ArrayList<>(toProcess.size());
|
||||||
|
// 遍历待处理的考勤计划,构建考勤检查记录
|
||||||
for (WmsAttendanceScheduleVo schedule : toProcess) {
|
for (WmsAttendanceScheduleVo schedule : toProcess) {
|
||||||
boolean crossDay = isCrossDayShift(schedule);
|
boolean crossDay = isCrossDayShift(schedule);
|
||||||
boolean backward = isBackwardCrossDay(schedule);
|
boolean backward = isBackwardCrossDay(schedule);
|
||||||
|
// 根据考勤计划和考勤记录,按天切片考勤记录
|
||||||
List<AttendanceRecords> records = sliceRecordsForDay(
|
List<AttendanceRecords> records = sliceRecordsForDay(
|
||||||
recordsByEmployee.get(schedule.getEmployeeName()),
|
recordsByEmployee.get(schedule.getEmployeeName()),
|
||||||
schedule.getWorkDate(),
|
schedule.getWorkDate(),
|
||||||
crossDay, backward);
|
crossDay, backward);
|
||||||
checksToInsert.add(buildCheck(schedule, rule, records, crossDay));
|
// 构建考勤检查记录并添加到插入列表
|
||||||
|
checksToInsert.add(buildCheck(schedule, rule, records, crossDay, endCheckOverridden));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 批量插入考勤检查记录
|
||||||
baseMapper.insertBatch(checksToInsert, BATCH_SIZE);
|
baseMapper.insertBatch(checksToInsert, BATCH_SIZE);
|
||||||
|
|
||||||
updateContinuousAbsent(startLocal, endLocal);
|
// 更新连续缺勤记录
|
||||||
|
updateContinuousAbsent(startLocal, endLocal, affectedUserIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isCrossDayShift(WmsAttendanceScheduleVo schedule) {
|
private boolean isCrossDayShift(WmsAttendanceScheduleVo schedule) {
|
||||||
@@ -182,7 +239,7 @@ public class WmsAttendanceCheckServiceImpl implements IWmsAttendanceCheckService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按员工姓名合并时间范围,每人只查一次打卡(与原先按排班逐条查询结果一致)。
|
* 一次 SQL 批量查询所有员工在日期范围内的打卡记录(用 IN/eq 精确匹配,走索引)。
|
||||||
*/
|
*/
|
||||||
private Map<String, List<AttendanceRecords>> prefetchAttendanceRecords(List<WmsAttendanceScheduleVo> toProcess) {
|
private Map<String, List<AttendanceRecords>> prefetchAttendanceRecords(List<WmsAttendanceScheduleVo> toProcess) {
|
||||||
Map<String, LocalDate> minByName = new HashMap<>();
|
Map<String, LocalDate> minByName = new HashMap<>();
|
||||||
@@ -193,37 +250,28 @@ public class WmsAttendanceCheckServiceImpl implements IWmsAttendanceCheckService
|
|||||||
minByName.merge(name, ld, (a, b) -> a.isBefore(b) ? a : b);
|
minByName.merge(name, ld, (a, b) -> a.isBefore(b) ? a : b);
|
||||||
maxByName.merge(name, ld, (a, b) -> a.isAfter(b) ? a : b);
|
maxByName.merge(name, ld, (a, b) -> a.isAfter(b) ? a : b);
|
||||||
}
|
}
|
||||||
Map<String, List<AttendanceRecords>> out = new HashMap<>(minByName.size() * 2);
|
if (minByName.isEmpty()) {
|
||||||
for (Map.Entry<String, LocalDate> e : minByName.entrySet()) {
|
return new HashMap<>();
|
||||||
String name = e.getKey();
|
}
|
||||||
LocalDate minLd = e.getValue();
|
LocalDate globalMin = minByName.values().stream().min(LocalDate::compareTo).get();
|
||||||
LocalDate maxLd = maxByName.get(name);
|
LocalDate globalMax = maxByName.values().stream().max(LocalDate::compareTo).get();
|
||||||
out.put(name, fetchRecordsForNameRange(name, minLd, maxLd));
|
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<>());
|
||||||
}
|
}
|
||||||
return out;
|
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) {
|
private List<AttendanceRecords> sliceRecordsForDay(List<AttendanceRecords> prefetched, Date workDate, boolean crossDay, boolean backward) {
|
||||||
if (prefetched == null || prefetched.isEmpty()) {
|
if (prefetched == null || prefetched.isEmpty()) {
|
||||||
return new ArrayList<>();
|
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,
|
private WmsAttendanceCheck buildCheck(WmsAttendanceScheduleVo schedule, WmsAttendanceRule rule,
|
||||||
List<AttendanceRecords> records, boolean crossDay) {
|
List<AttendanceRecords> records, boolean crossDay,
|
||||||
|
Set<Long> endCheckOverridden) {
|
||||||
|
// 创建考勤检查对象
|
||||||
WmsAttendanceCheck check = new WmsAttendanceCheck();
|
WmsAttendanceCheck check = new WmsAttendanceCheck();
|
||||||
|
// 设置排班基本信息
|
||||||
check.setScheduleId(schedule.getScheduleId());
|
check.setScheduleId(schedule.getScheduleId());
|
||||||
check.setUserId(schedule.getUserId());
|
check.setUserId(schedule.getUserId());
|
||||||
check.setEmployeeName(schedule.getEmployeeName());
|
check.setEmployeeName(schedule.getEmployeeName());
|
||||||
@@ -267,13 +328,22 @@ public class WmsAttendanceCheckServiceImpl implements IWmsAttendanceCheckService
|
|||||||
check.setShiftName(schedule.getShiftName());
|
check.setShiftName(schedule.getShiftName());
|
||||||
check.setShiftType(schedule.getShiftType());
|
check.setShiftType(schedule.getShiftType());
|
||||||
|
|
||||||
|
// 判断是否有第二时段
|
||||||
boolean hasPeriod2 = schedule.getShiftStartTime2() != null && schedule.getShiftEndTime2() != null;
|
boolean hasPeriod2 = schedule.getShiftStartTime2() != null && schedule.getShiftEndTime2() != null;
|
||||||
|
|
||||||
|
// 判断是否为向后跨天
|
||||||
boolean backward = isBackwardCrossDay(schedule);
|
boolean backward = isBackwardCrossDay(schedule);
|
||||||
|
|
||||||
|
// 处理时段设置
|
||||||
if (hasPeriod2) {
|
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.setP2StartTime(combineTime(schedule.getWorkDate(), schedule.getShiftStartTime2()));
|
||||||
check.setP2EndTime(combineTime(schedule.getWorkDate(), schedule.getShiftEndTime2()));
|
check.setP2EndTime(combineTime(schedule.getWorkDate(), schedule.getShiftEndTime2()));
|
||||||
} else {
|
} else {
|
||||||
@@ -313,12 +383,14 @@ public class WmsAttendanceCheckServiceImpl implements IWmsAttendanceCheckService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
checkPeriod(check, rule, 1, p1Records, schedule.getShiftStartTime(), schedule.getShiftEndTime());
|
checkPeriod(check, rule, 1, p1Records, check.getP1StartTime(), check.getP1EndTime());
|
||||||
checkPeriod(check, rule, 2, p2Records, schedule.getShiftStartTime2(), schedule.getShiftEndTime2());
|
checkPeriod(check, rule, 2, p2Records, check.getP2StartTime(), check.getP2EndTime());
|
||||||
} else {
|
} else {
|
||||||
List<AttendanceRecords> filtered = filterWindow(records, schedule.getWorkDate(),
|
List<AttendanceRecords> filtered = filterWindow(records, schedule.getWorkDate(),
|
||||||
schedule.getShiftStartTime(), schedule.getShiftEndTime(), crossDay);
|
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);
|
calculateOverall(check, rule);
|
||||||
@@ -326,7 +398,7 @@ public class WmsAttendanceCheckServiceImpl implements IWmsAttendanceCheckService
|
|||||||
}
|
}
|
||||||
|
|
||||||
private List<AttendanceRecords> filterWindow(List<AttendanceRecords> records, Date workDate,
|
private List<AttendanceRecords> filterWindow(List<AttendanceRecords> records, Date workDate,
|
||||||
Date expectedStart, Date expectedEnd, boolean crossDay) {
|
Date expectedStart, Date expectedEnd, boolean crossDay) {
|
||||||
LocalDate ld = toLocalDate(workDate);
|
LocalDate ld = toLocalDate(workDate);
|
||||||
LocalTime st = toLocalTime(expectedStart);
|
LocalTime st = toLocalTime(expectedStart);
|
||||||
LocalTime et = toLocalTime(expectedEnd);
|
LocalTime et = toLocalTime(expectedEnd);
|
||||||
@@ -366,7 +438,7 @@ public class WmsAttendanceCheckServiceImpl implements IWmsAttendanceCheckService
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void checkPeriod(WmsAttendanceCheck check, WmsAttendanceRule rule, int period,
|
private void checkPeriod(WmsAttendanceCheck check, WmsAttendanceRule rule, int period,
|
||||||
List<AttendanceRecords> periodRecords, Date expectedStart, Date expectedEnd) {
|
List<AttendanceRecords> periodRecords, Date expectedStart, Date expectedEnd) {
|
||||||
if (periodRecords.isEmpty()) {
|
if (periodRecords.isEmpty()) {
|
||||||
if (period == 1) {
|
if (period == 1) {
|
||||||
check.setP1Status("missed");
|
check.setP1Status("missed");
|
||||||
@@ -376,12 +448,12 @@ public class WmsAttendanceCheckServiceImpl implements IWmsAttendanceCheckService
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LocalTime expStart = toLocalTime(expectedStart);
|
LocalDateTime expStart = toLocalDateTime(expectedStart);
|
||||||
LocalTime expEnd = toLocalTime(expectedEnd);
|
LocalDateTime expEnd = toLocalDateTime(expectedEnd);
|
||||||
AttendanceRecords firstRec = periodRecords.get(0);
|
AttendanceRecords firstRec = periodRecords.get(0);
|
||||||
AttendanceRecords lastRec = periodRecords.get(periodRecords.size() - 1);
|
AttendanceRecords lastRec = periodRecords.get(periodRecords.size() - 1);
|
||||||
LocalTime firstCheck = toLocalDateTime(firstRec.getChecktime()).toLocalTime();
|
LocalDateTime firstCheck = toLocalDateTime(firstRec.getChecktime());
|
||||||
LocalTime lastCheck = toLocalDateTime(lastRec.getChecktime()).toLocalTime();
|
LocalDateTime lastCheck = toLocalDateTime(lastRec.getChecktime());
|
||||||
|
|
||||||
int lateMinutes = 0;
|
int lateMinutes = 0;
|
||||||
int earlyMinutes = 0;
|
int earlyMinutes = 0;
|
||||||
@@ -459,10 +531,18 @@ public class WmsAttendanceCheckServiceImpl implements IWmsAttendanceCheckService
|
|||||||
if ("absent_half".equals(check.getP1Status()) || "absent_half".equals(check.getP2Status())) {
|
if ("absent_half".equals(check.getP1Status()) || "absent_half".equals(check.getP2Status())) {
|
||||||
hasAbsentHalf = true;
|
hasAbsentHalf = true;
|
||||||
}
|
}
|
||||||
if ("missed".equals(check.getP1Status()) && check.getP2StartTime() != null && "missed".equals(check.getP2Status())) {
|
if ("missed".equals(check.getP1Status())) {
|
||||||
check.setAbsentType("full_day");
|
if (check.getP2StartTime() != null) {
|
||||||
check.setOverallStatus("absent_full");
|
if ("missed".equals(check.getP2Status())) {
|
||||||
return;
|
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) {
|
if (check.getP1StartTime() == null && check.getP2StartTime() == null) {
|
||||||
check.setAbsentType("full_day");
|
check.setAbsentType("full_day");
|
||||||
@@ -479,8 +559,12 @@ public class WmsAttendanceCheckServiceImpl implements IWmsAttendanceCheckService
|
|||||||
check.setOverallStatus(abnormal ? "abnormal" : "normal");
|
check.setOverallStatus(abnormal ? "abnormal" : "normal");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateContinuousAbsent(LocalDate startDate, LocalDate endDate) {
|
private void updateContinuousAbsent(LocalDate startDate, LocalDate endDate, Set<Long> affectedUserIds) {
|
||||||
|
if (affectedUserIds.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
List<WmsAttendanceCheck> checks = baseMapper.selectList(Wrappers.<WmsAttendanceCheck>lambdaQuery()
|
List<WmsAttendanceCheck> checks = baseMapper.selectList(Wrappers.<WmsAttendanceCheck>lambdaQuery()
|
||||||
|
.in(WmsAttendanceCheck::getUserId, affectedUserIds)
|
||||||
.ge(WmsAttendanceCheck::getWorkDate, toDate(startDate.atStartOfDay()))
|
.ge(WmsAttendanceCheck::getWorkDate, toDate(startDate.atStartOfDay()))
|
||||||
.le(WmsAttendanceCheck::getWorkDate, toDate(endDate.atTime(LocalTime.of(23, 59, 59))))
|
.le(WmsAttendanceCheck::getWorkDate, toDate(endDate.atTime(LocalTime.of(23, 59, 59))))
|
||||||
.eq(WmsAttendanceCheck::getDelFlag, 0));
|
.eq(WmsAttendanceCheck::getDelFlag, 0));
|
||||||
@@ -515,6 +599,8 @@ public class WmsAttendanceCheckServiceImpl implements IWmsAttendanceCheckService
|
|||||||
*/
|
*/
|
||||||
private Map<Long, Map<LocalDate, WmsAttendanceCheck>> loadAbsentHistoryByUser(List<Long> userIds, LocalDate endDate) {
|
private Map<Long, Map<LocalDate, WmsAttendanceCheck>> loadAbsentHistoryByUser(List<Long> userIds, LocalDate endDate) {
|
||||||
Map<Long, Map<LocalDate, WmsAttendanceCheck>> out = new HashMap<>();
|
Map<Long, Map<LocalDate, WmsAttendanceCheck>> out = new HashMap<>();
|
||||||
|
LocalDate startDate = endDate.minusDays(100);
|
||||||
|
Date start = toDate(startDate.atStartOfDay());
|
||||||
Date end = toDate(endDate.atTime(LocalTime.of(23, 59, 59)));
|
Date end = toDate(endDate.atTime(LocalTime.of(23, 59, 59)));
|
||||||
for (int i = 0; i < userIds.size(); i += BATCH_SIZE) {
|
for (int i = 0; i < userIds.size(); i += BATCH_SIZE) {
|
||||||
int to = Math.min(i + BATCH_SIZE, userIds.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,
|
.select(WmsAttendanceCheck::getUserId, WmsAttendanceCheck::getWorkDate, WmsAttendanceCheck::getAbsentType,
|
||||||
WmsAttendanceCheck::getCheckId, WmsAttendanceCheck::getDelFlag)
|
WmsAttendanceCheck::getCheckId, WmsAttendanceCheck::getDelFlag)
|
||||||
.in(WmsAttendanceCheck::getUserId, chunk)
|
.in(WmsAttendanceCheck::getUserId, chunk)
|
||||||
|
.ge(WmsAttendanceCheck::getWorkDate, start)
|
||||||
.le(WmsAttendanceCheck::getWorkDate, end)
|
.le(WmsAttendanceCheck::getWorkDate, end)
|
||||||
.eq(WmsAttendanceCheck::getDelFlag, 0));
|
.eq(WmsAttendanceCheck::getDelFlag, 0));
|
||||||
for (WmsAttendanceCheck row : rows) {
|
for (WmsAttendanceCheck row : rows) {
|
||||||
|
|||||||
Reference in New Issue
Block a user