@@ -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 < WmsAttendanceScheduleVo > schedules = scheduleService . queryList ( scheduleBo ) ;
// 如果没有考勤计划,直接返回
if ( schedules . isEmpty ( ) ) {
return ;
}
// 获取当前有效的考勤规则
WmsAttendanceRule rule = getActiveRule ( ) ;
// 将日期转换为LocalDate类型
LocalDate startLocal = toLocalDate ( bo . getStartDate ( ) ) ;
LocalDate endLocal = toLocalDate ( bo . getEndDate ( ) ) ;
// 创建待处理的考勤计划列表
List < WmsAttendanceScheduleVo > 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 < 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 ) ;
// 获取所有待处理考勤计划的ID列表
List < Long > scheduleIds = toProcess . stream ( )
. map ( WmsAttendanceScheduleVo : : getScheduleId )
. collect ( Collectors . toList ( ) ) ;
// 批量删除这些考勤计划对应的考勤检查记录
batchDeleteByScheduleIds ( scheduleIds ) ;
// 创建要插入的考勤检查记录列表
List < WmsAttendanceCheck > checksToInsert = new ArrayList < > ( toProcess . size ( ) ) ;
// 遍历待处理的考勤计划,构建考勤检查记录
for ( WmsAttendanceScheduleVo schedule : toProcess ) {
boolean crossDay = isCrossDayShift ( schedule ) ;
boolean backward = isBackwardCrossDay ( schedule ) ;
// 根据考勤计划和考勤记录,按天切片考勤记录
List < AttendanceRecords > records = sliceRecordsForDay (
recordsByEmployee . get ( schedule . getEmployeeName ( ) ) ,
schedule . getWorkDate ( ) ,
crossDay , backward ) ;
checksToInsert . add ( buildCheck ( schedule , rule , records , crossDay ) ) ;
// 构建考勤检查记录并添加到插入列表
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 < String , List < AttendanceRecords > > prefetchAttendanceRecords ( List < WmsAttendanceScheduleVo > toProcess ) {
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 ) ;
maxByName . merge ( name , ld , ( a , b ) - > a . isAfter ( b ) ? a : b ) ;
}
Map < String , List < AttendanceRecords > > out = new HashMap < > ( minByName . size ( ) * 2 ) ;
for ( Map . Entry < String , LocalDate > e : minByName . entrySet ( ) ) {
String name = e . getKey ( ) ;
LocalDate minLd = e . getValue ( ) ;
LocalDate maxLd = maxByName . get (name ) ;
out . put ( name , fetchRecordsForNameRange ( name , minLd , maxLd ) ) ;
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 < > ( ) ) ;
}
return out ;
}
private List < AttendanceRecords > fetchRecordsForNameRange ( String employeeName , LocalDate minLd , LocalDate maxLd ) {
AttendanceRecordsBo recordsBo = new AttendanceRecordsBo ( ) ;
recordsBo . setEname ( employeeName ) ;
recordsBo . setChecktimeStart ( toDate ( minLd . atStartOfDay ( ) ) ) ;
recordsBo . setChecktimeEnd ( toDate ( maxLd . plusDays ( 1 ) . atTime ( LocalTime . of ( 23 , 59 , 59 ) ) ) ) ;
List < AttendanceRecordsVo > voList = attendanceRecordsService . queryList ( recordsBo ) ;
return voList . stream ( )
. map ( v - > {
AttendanceRecords r = new AttendanceRecords ( ) ;
r . setId ( v . getId ( ) ) ;
r . setEname ( v . getEname ( ) ) ;
r . setChecktime ( v . getChecktime ( ) ) ;
return r ;
} )
. sorted ( Comparator . comparing ( AttendanceRecords : : getChecktime ) )
. collect ( Collectors . toList ( ) ) ;
}
/**
* 与原先 {@code getRecords(ename, workDate, crossDay)} 的时间窗口一致。
*/
private List < AttendanceRecords > sliceRecordsForDay ( List < AttendanceRecords > prefetched , Date workDate , boolean crossDay , boolean backward ) {
if ( prefetched = = null | | prefetched . isEmpty ( ) ) {
return new ArrayList < > ( ) ;
@@ -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 < AttendanceRecords > records , boolean crossDay ) {
List < AttendanceRecords > records , boolean crossDay ,
Set < Long > 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 . setP1End Time ( combineTime ( schedule . getWorkDate ( ) , schedule . getShiftEndTime ( ) ) ) ;
// 设置第一时段开始时间,考虑跨天情况
check . setP1Start Time ( 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 , s chedule . getShift StartTime ( ) , s chedule . getShift EndTime ( ) ) ;
checkPeriod ( check , rule , 2 , p2Records , s chedule . getShift StartTime2 ( ) , s chedule . getShift EndTime2 ( ) ) ;
checkPeriod ( check , rule , 1 , p1Records , check . getP1 StartTime ( ) , check . getP1 EndTime ( ) ) ;
checkPeriod ( check , rule , 2 , p2Records , check . getP2 StartTime ( ) , check . getP2 EndTime ( ) ) ;
} else {
List < AttendanceRecords > 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 < AttendanceRecords > filterWindow ( List < AttendanceRecords > 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 < AttendanceRecords > periodRecords , Date expectedStart , Date expectedEnd ) {
List < AttendanceRecords > 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 ) ;
LocalDate Time expStart = toLocalDate Time ( expectedStart ) ;
LocalDate Time expEnd = toLocalDate Time ( 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 ( ) ;
LocalDate Time firstCheck = toLocalDateTime ( firstRec . getChecktime ( ) ) ;
LocalDate Time 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 . s etAbsentType ( " full_day " ) ;
check . s etOverall Status( " absent_full " ) ;
return ;
if ( " missed " . equals ( check . getP1Status ( ) ) ) {
if ( check . g etP2StartTime ( ) ! = null ) {
if ( " missed " . equals ( check . g etP2 Status( ) ) ) {
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 < Long > affectedUserIds ) {
if ( affectedUserIds . isEmpty ( ) ) {
return ;
}
List < WmsAttendanceCheck > checks = baseMapper . selectList ( Wrappers . < WmsAttendanceCheck > 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 < Long , Map < LocalDate , WmsAttendanceCheck > > loadAbsentHistoryByUser ( List < Long > userIds , LocalDate endDate ) {
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 ) ) ) ;
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 ) {