package com.ruoyi.inspect.service.impl; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.util.ObjectUtil; import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.ruoyi.common.core.domain.Result; import com.ruoyi.common.enums.ClockInState; import com.ruoyi.common.enums.EnterOrExitType; import com.ruoyi.common.enums.SyncStatus; import com.ruoyi.common.utils.api.icc.IccApiUtil; import com.ruoyi.common.utils.api.icc.model.GetResultPageRequest; import com.ruoyi.common.utils.api.icc.model.GetResultPageResponse; import com.ruoyi.inspect.dto.StaffAttendanceDTO; import com.ruoyi.inspect.mapper.StaffAttendanceTrackingRecordMapper; import com.ruoyi.inspect.pojo.StaffAttendanceTrackingRecord; import com.ruoyi.inspect.service.StaffAttendanceTrackingRecordService; import com.ruoyi.inspect.util.HourDiffCalculator; import com.ruoyi.inspect.vo.StaffAttendanceVO; import com.ruoyi.inspect.vo.StaffClockInVO; import com.ruoyi.performance.dto.PerformanceShiftMapDto; import com.ruoyi.performance.mapper.PerformanceShiftMapper; import com.ruoyi.performance.mapper.ShiftTimeMapper; import com.ruoyi.performance.pojo.ShiftTime; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.stream.Collectors; /** * @author 27233 * @description 针对表【staff_attendance_tracking_record(人员考勤-考勤记录)】的数据库操作Service实现 * @createDate 2026-03-09 17:42:25 */ @Slf4j @Service public class StaffAttendanceTrackingRecordServiceImpl extends ServiceImpl implements StaffAttendanceTrackingRecordService { @Autowired private IccApiUtil iccApiUtil; @Autowired private PerformanceShiftMapper performanceShiftMapper; @Autowired private ShiftTimeMapper shiftTimeMapper; private DateTimeFormatter yyyMMdd = DateTimeFormatter.ofPattern("yyyy-MM-dd"); private DateTimeFormatter HHmm = DateTimeFormatter.ofPattern("HH:mm"); private DateTimeFormatter yyyMMddHHmmss = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); /** 质量部id */ private final static String deptIds = "6"; // 固定前缀 private static final String PREFIX = "ZT-"; // 数字部分固定长度 private static final int DIGIT_LENGTH = 6; /** * 同步的门禁设备列表 * channel_name device_code * 10.100.22.2_门禁通道_1 1001538 * 10.100.22.3_门禁通道_1 1001539 * 10.100.22.4_门禁通道_1 1001540 * 10.100.22.5_门禁通道_1 1001541 * 10.100.22.12_门禁通道_1 1001626 * 10.100.22.13_门禁通道_1 1001627 * 10.100.22.14_门禁通道_1 1001628 * 10.100.22.15_门禁通道_1 1001629 */ private final static List syncDeviceCode = Arrays.asList("1001538", "1001539", "1001540", "1001541", "1001626", "1001627", "1001628", "1001629"); @Override @Transactional(rollbackFor = Exception.class) public boolean syncAttendanceRecord(LocalDateTime startDate, LocalDateTime endDate) { if (ObjectUtil.isAllEmpty(startDate, endDate)) { throw new RuntimeException("同步日期不能为空"); } // 查询已同步的记录 List trackingRecordIccIdList = baseMapper.selectIccIdList(); try { // 查询icc开放平台的考勤记录 GetResultPageRequest getResultPageRequest = new GetResultPageRequest(); getResultPageRequest.setPageNum(1); getResultPageRequest.setPageSize(9999); getResultPageRequest.setDeptIds(deptIds); getResultPageRequest.setStartSwingTime(startDate.format(yyyMMddHHmmss)); getResultPageRequest.setEndSwingTime(endDate.format(yyyMMddHHmmss)); GetResultPageResponse trackingRecordResponse = iccApiUtil.getAttendanceResultPage(getResultPageRequest); if (trackingRecordResponse.isSuccess()) { if (ObjectUtil.isNotNull(trackingRecordResponse.getData()) && !trackingRecordResponse.getData().getPageData().isEmpty()) { List recordList = trackingRecordResponse.getData().getPageData() .stream() .filter(f -> !trackingRecordIccIdList.contains(f.getId()) && syncDeviceCode.contains(f.getDeviceCode())) .map(result -> { StaffAttendanceTrackingRecord trackingRecord = new StaffAttendanceTrackingRecord(); BeanUtil.copyProperties(result, trackingRecord); trackingRecord.setIccId(result.getId()); trackingRecord.setPersonCode(restorePersonCode(result.getPersonCode())); trackingRecord.setId(null); return trackingRecord; }).collect(Collectors.toList()); if (!recordList.isEmpty()) this.saveBatch(recordList); log.info("同步ICC考勤记录条数->,{}", recordList.size()); } } else { log.error("同步ICC开放平台考勤记录错误,{}", trackingRecordResponse.getErrMsg()); } } catch (Exception e) { throw new RuntimeException(e); } return true; } @Override public IPage pageAttendanceRecord(Page page, StaffAttendanceDTO staffAttendanceDTO) { // 查询打卡记录 System.out.println(staffAttendanceDTO.getStartDate()); System.out.println(staffAttendanceDTO.getEndDate()); System.out.println(ObjectUtils.allNotNull(staffAttendanceDTO.getStartDate(),staffAttendanceDTO.getEndDate())); Wrapper queryWrapper = Wrappers.lambdaQuery() .eq(StaffAttendanceTrackingRecord::getEnableReport, Boolean.TRUE) .between(ObjectUtils.allNotNull(staffAttendanceDTO.getStartDate(),staffAttendanceDTO.getEndDate()), StaffAttendanceTrackingRecord::getSwingTime, staffAttendanceDTO.getStartDate(),ObjectUtils.isNotEmpty(staffAttendanceDTO.getEndDate())?staffAttendanceDTO.getEndDate().plusDays(1L):null) .and(StringUtils.isNotEmpty(staffAttendanceDTO.getKeyword()), i -> i.like(StaffAttendanceTrackingRecord::getPersonCode, staffAttendanceDTO.getKeyword()) .or() .like(StaffAttendanceTrackingRecord::getPersonName, staffAttendanceDTO.getKeyword())); List recordList = baseMapper.selectList(queryWrapper); // 查询班次 List performanceShifts = performanceShiftMapper.selectListByWorkTime( staffAttendanceDTO.getStartDate(), staffAttendanceDTO.getEndDate(), staffAttendanceDTO.getKeyword()); // 组装数据 List resultList = new ArrayList<>(); for (int i = 0; i < performanceShifts.size(); i++) { PerformanceShiftMapDto p = performanceShifts.get(i); StaffAttendanceVO vo = new StaffAttendanceVO(); // 获取对应班次小时数 if (ObjectUtil.isAllNotEmpty(p.getStartTime(), p.getEndTime()) && !recordList.isEmpty()) { double hourDiff = HourDiffCalculator.getHourDiff(p.getStartTime(), p.getEndTime()); /* * 上班时间取值: * 正常:当前班次开始前最后一次进门时间 * 异常(迟到):无当前班次开始前进门记录,取当前班次开始后第一次进门时间 * 下班时间取值: * 正常:当前班次结束后第一次出门时间 * 异常(早退):无当前班次结束到下一班次开始前的出门记录并且最后一次出门在当前班次时间范围内,取当前班次最后一次出门时间 */ // 当前班次开始天0点时间 LocalDateTime startDateTime = LocalDateTime.of(p.getWorkTime().toLocalDate(), LocalTime.MIN); // 当前班次结束天24点时间 LocalDateTime endDateTime = LocalDateTime.of(p.getWorkTime().toLocalDate(), LocalTime.MAX); // 当前班次开始时间 LocalTime currentShiftStartTime = LocalTime.parse(p.getStartTime(), HHmm); LocalDateTime currentShiftStartDateTime = LocalDateTime.of(p.getWorkTime().toLocalDate(), currentShiftStartTime); // 当前班次结束时间 LocalTime currentShiftEndTime = LocalTime.parse(p.getEndTime(), HHmm); LocalDateTime currentShiftEndDateTime = LocalDateTime.of(endDateTime.toLocalDate(), currentShiftEndTime); // 下一班次开始时间 LocalDateTime nextShiftStartDateTime = getShiftStartDateTime(i + 1, performanceShifts, startDateTime.plusDays(1L)); if (Double.compare(hourDiff, 0) == -1) { // 如果小时差为负数,表示跨天,结束时间需加一 endDateTime = endDateTime.plusDays(1L); } // 过滤出当前人员当前班次的进/出记录 LocalDateTime workDateTime = null; LocalDateTime offWorkDateTime = null; List enterRecords = filterAttendanceRecord(p.getPersonCode(), EnterOrExitType.ENTER.getValue(), startDateTime, endDateTime, recordList); if (!enterRecords.isEmpty()) { // 上班时间和状态 StaffAttendanceTrackingRecord enterRecord = enterRecords.stream() .filter(s -> !s.getSwingTime().isAfter(currentShiftStartDateTime)) .max(Comparator.comparing(StaffAttendanceTrackingRecord::getSwingTime)) .orElse(new StaffAttendanceTrackingRecord()); if (BeanUtil.isEmpty(enterRecord)) { enterRecord = enterRecords.stream() .filter(s -> (s.getSwingTime().isAfter(currentShiftStartDateTime) && s.getSwingTime().isBefore(currentShiftEndDateTime))) .min(Comparator.comparing(StaffAttendanceTrackingRecord::getSwingTime)) .orElse(new StaffAttendanceTrackingRecord()); workDateTime = enterRecord.getSwingTime(); vo.setWorkClockInState(ClockInState.ABNORMAL.getValue()); vo.setWorkDataId(enterRecord.getId()); } else { workDateTime = enterRecord.getSwingTime(); vo.setWorkClockInState(ClockInState.NORMAL.getValue()); vo.setWorkDataId(enterRecord.getId()); } } List exitRecords = filterAttendanceRecord(p.getPersonCode(), EnterOrExitType.EXIT.getValue(), startDateTime, endDateTime, recordList); if (!exitRecords.isEmpty()) { // 下班时间和状态 StaffAttendanceTrackingRecord exitRecord = exitRecords.stream() .filter(s -> !s.getSwingTime().isBefore(currentShiftEndDateTime) && s.getSwingTime().isBefore(nextShiftStartDateTime)) .min(Comparator.comparing(StaffAttendanceTrackingRecord::getSwingTime)) .orElse(new StaffAttendanceTrackingRecord()); if (BeanUtil.isEmpty(exitRecord)) { exitRecord = exitRecords.stream() .filter(s -> (s.getSwingTime().isAfter(currentShiftStartDateTime) && s.getSwingTime().isBefore(currentShiftEndDateTime))) .max(Comparator.comparing(StaffAttendanceTrackingRecord::getSwingTime)) .orElse(new StaffAttendanceTrackingRecord()); offWorkDateTime = exitRecord.getSwingTime(); vo.setOffClockInState(ClockInState.ABNORMAL.getValue()); vo.setOffWorkDataId(exitRecord.getId()); } else { offWorkDateTime = exitRecord.getSwingTime(); vo.setOffClockInState(ClockInState.NORMAL.getValue()); vo.setOffWorkDataId(exitRecord.getId()); } } if (ObjectUtils.allNotNull(workDateTime, offWorkDateTime)) { vo.setActualWorkHours(HourDiffCalculator.getHourDiff(workDateTime.toLocalTime().format(HHmm), offWorkDateTime.toLocalTime().format(HHmm))); } // 赋值 vo.setShiftId(p.getShift()); vo.setPersonCode(p.getPersonCode()); vo.setPersonName(p.getUserName()); vo.setPlannedWorkHours(hourDiff); vo.setSwingDate(startDateTime); vo.setWorkDateTime(workDateTime); vo.setOffWorkDateTime(offWorkDateTime); vo.setDeptName(recordList.get(0).getDeptName()); vo.setIsSync(recordList.get(0).getIsSync()); vo.setCreateUser(recordList.get(0).getCreateUser()); vo.setCreateTime(recordList.get(0).getCreateTime()); vo.setUpdateUser(recordList.get(0).getUpdateUser()); vo.setUpdateTime(recordList.get(0).getUpdateTime()); vo.setResult(ClockInState.ABNORMAL.getValue()); if(ObjectUtils.allNotNull(vo.getWorkClockInState(),vo.getOffClockInState())){ vo.setResult(Integer.min(vo.getWorkClockInState(),vo.getOffClockInState())); } // 计算缺勤时长 if (ObjectUtils.allNotNull(vo.getActualWorkHours(), vo.getPlannedWorkHours())) { double absenceWorkHours = BigDecimal.valueOf(vo.getPlannedWorkHours()) .subtract(BigDecimal.valueOf(vo.getActualWorkHours())).setScale(2, RoundingMode.HALF_EVEN) .doubleValue(); if (Double.compare(absenceWorkHours, 0) > 0) { vo.setAbsenceWorkHours(absenceWorkHours); } } if (!enterRecords.isEmpty() || !exitRecords.isEmpty()) resultList.add(vo); } } return limitPages(page, resultList); } @Override public List getClockInRecord(StaffAttendanceDTO staffAttendanceDTO) { ShiftTime shiftTime = shiftTimeMapper.selectOne(Wrappers.lambdaQuery().eq(ShiftTime::getShift, staffAttendanceDTO.getShiftId())); if(ObjectUtils.isEmpty(shiftTime)){ throw new RuntimeException("未查询到当前班次的时间配置"); } LocalDateTime startDateTime = LocalDateTime.of(staffAttendanceDTO.getSwingDate(),LocalTime.MIN); LocalDateTime endDateTime = LocalDateTime.of(staffAttendanceDTO.getSwingDate(),LocalTime.MAX); //判断当前班次是否要跨天 double hourDiff = HourDiffCalculator.getHourDiff(shiftTime.getStartTime(), shiftTime.getEndTime()); if(Double.compare(hourDiff,0)<0){ endDateTime = endDateTime.plusDays(1L); } return baseMapper.selectList(Wrappers.lambdaQuery() .eq(StaffAttendanceTrackingRecord::getPersonCode,staffAttendanceDTO.getPersonCode()) .between(ObjectUtil.isAllNotEmpty(startDateTime, endDateTime),StaffAttendanceTrackingRecord::getSwingTime, startDateTime,endDateTime) .orderByAsc(StaffAttendanceTrackingRecord::getSwingTime) ); } @Override public PerformanceShiftMapDto checkDutyDate(StaffAttendanceDTO staffAttendanceDTO) { if(ObjectUtils.isEmpty(staffAttendanceDTO.getSwingDate())){ throw new RuntimeException("考勤日期不能为空"); } LocalDateTime startDateTime = LocalDateTime.of(staffAttendanceDTO.getSwingDate(),LocalTime.MIN); LocalDateTime endDateTime = LocalDateTime.of(staffAttendanceDTO.getSwingDate(),LocalTime.MAX); Long count = baseMapper.selectCount(Wrappers.lambdaQuery() .eq(StaffAttendanceTrackingRecord::getPersonCode, staffAttendanceDTO.getPersonCode()) .between(StaffAttendanceTrackingRecord::getSwingTime, startDateTime, endDateTime)); if(count>0){ throw new RuntimeException("所选日期已存在考勤记录!"); } //查询人员当天班次配置 List shiftMapDtos = performanceShiftMapper.selectListByWorkTime(startDateTime, endDateTime, staffAttendanceDTO.getPersonCode()); if(shiftMapDtos.isEmpty()){ throw new RuntimeException("未找到人员所选考勤时间的班次配置,请先配置班次"); } return shiftMapDtos.get(0); } @Override @Transactional(rollbackFor = Exception.class) public boolean saveOrUpdateRecord(StaffAttendanceDTO staffAttendanceDTO) { if(ObjectUtils.isEmpty(staffAttendanceDTO)){ throw new RuntimeException("传参不能为空"); } LocalDateTime workDateTime = LocalDateTime.of(staffAttendanceDTO.getSwingDate(),staffAttendanceDTO.getWorkDateTime()); LocalDateTime offWorkDateTime = LocalDateTime.of(staffAttendanceDTO.getSwingDate(),staffAttendanceDTO.getOffWorkDateTime()); //校验上下班时间是否跨天 double hourDiff = HourDiffCalculator.getHourDiff(staffAttendanceDTO.getWorkDateTime().format(HHmm), staffAttendanceDTO.getOffWorkDateTime().format(HHmm)); if(Double.compare(hourDiff,0)<0){ offWorkDateTime = offWorkDateTime.plusDays(1L); } //组装上/下班考勤记录 StaffAttendanceTrackingRecord workRecord = new StaffAttendanceTrackingRecord( staffAttendanceDTO.getWorkDataId(), workDateTime, staffAttendanceDTO.getPersonCode(), staffAttendanceDTO.getPersonName(), staffAttendanceDTO.getDeptName(), staffAttendanceDTO.getResult(), EnterOrExitType.ENTER.getValue(), SyncStatus.INERT.getValue()); StaffAttendanceTrackingRecord offWorkRecord = new StaffAttendanceTrackingRecord( staffAttendanceDTO.getOffWorkDataId(), offWorkDateTime, staffAttendanceDTO.getPersonCode(), staffAttendanceDTO.getPersonName(), staffAttendanceDTO.getDeptName(), staffAttendanceDTO.getResult(), EnterOrExitType.EXIT.getValue(), SyncStatus.INERT.getValue()); List records = Arrays.asList(workRecord, offWorkRecord); return this.saveOrUpdateBatch(records); } /** * 自定义分页方法 * * @param page 分页对象 * @param resultList 数据列表 * @return */ private IPage limitPages(Page page, List resultList) { IPage resultPage = new Page<>(); long current = page.getCurrent(); long size = page.getSize(); if (current < 1) current = 1; long total = resultList.size(); long pages = getPages(size,total); int startIndex = Math.toIntExact((current - 1) * size >= total ? (pages - 1) * size : (current - 1) * size); int endIndex = Math.toIntExact(Math.min(current * size, total)); List records = resultList.subList(startIndex, endIndex); resultPage.setRecords(records); resultPage.setTotal(total); resultPage.setSize(size); resultPage.setCurrent(current); if(current>=pages)resultPage.setCurrent(pages); return resultPage; } /** * 当前分页总页数 */ private long getPages(long size,long total) { if (size == 0) { return 0L; } long pages = total / size; if (total % size != 0) { pages++; } return pages; } /** * 获取指定下标的班次开始时间 * * @param index * @param dtoList * @return */ private LocalDateTime getShiftStartDateTime(int index, List dtoList, LocalDateTime nextShiftTime) { if (dtoList.isEmpty() || index >= dtoList.size()) { return LocalDateTime.of(nextShiftTime.toLocalDate(), LocalTime.MAX); } LocalTime localTime = ObjectUtil.isNull(dtoList.get(index).getStartTime()) ? LocalTime.MAX : LocalTime.parse(dtoList.get(index).getStartTime(), HHmm); return LocalDateTime.of(nextShiftTime.toLocalDate(), localTime); } /** * 过滤指定时间范围的进出记录 * * @param personCode 人员编号 * @param enterOrExit 进门/出门 * @param startDateTime 开始时间 * @param endDateTime 结束时间 * @param recordList 进出记录列表 * @return */ public List filterAttendanceRecord(String personCode, Integer enterOrExit, LocalDateTime startDateTime, LocalDateTime endDateTime, List recordList) { if (recordList.isEmpty()) { return Collections.emptyList(); } return recordList.stream() .filter(s -> ObjectUtil.equal(s.getEnterOrExit(), enterOrExit)) .filter(s -> (s.getSwingTime().isAfter(startDateTime) && s.getSwingTime().isBefore(endDateTime)) && StringUtils.equals(s.getPersonCode(), personCode)) .collect(Collectors.toList()); } /** * 将纯数字复原为标准员工编号 * * @param number 传入的数字(员工编号去除前缀和前置0后的数字,支持int/long/String类型) * @return 标准格式员工编号(如输入123 → ZT-000123) * @throws IllegalArgumentException 传入非数字/负数时抛出异常 */ public static String restorePersonCode(Object number) { // 1. 空值校验 if (number == null) { throw new IllegalArgumentException("传入数字不能为空"); } // 2. 统一转换为字符串并去除首尾空格 String numStr = number.toString().trim(); // 3. 校验是否为纯数字(排除负数、非数字字符) if (!numStr.matches("\\d+")) { throw new IllegalArgumentException("传入的不是有效正整数:" + numStr); } // 4. 补前置0到指定长度(6位),超出则保留原数字 String paddedNum = String.format("%0" + DIGIT_LENGTH + "d", Long.parseLong(numStr)); // 5. 拼接前缀返回 return PREFIX + paddedNum; } }