package cn.iocoder.yudao.module.hrm.service.attendance; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import com.google.common.annotations.VisibleForTesting; import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRulePageReqVO; import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRuleSaveReqVO; import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceRuleDO; import cn.iocoder.yudao.module.hrm.dal.mysql.attendance.HrmAttendanceRuleMapper; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import java.math.BigDecimal; import java.time.Duration; import java.time.LocalDateTime; import java.time.LocalTime; import java.util.Comparator; import java.util.List; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants.*; /** * 考勤规则配置 Service 实现类 * * @author 超级管理员 */ @Service @Validated public class HrmAttendanceRuleServiceImpl implements HrmAttendanceRuleService { /** 规则状态:0-启用 */ private static final Integer ENABLED_STATUS = 0; @Resource private HrmAttendanceRuleMapper attendanceRuleMapper; @Override public Long createAttendanceRule(HrmAttendanceRuleSaveReqVO createReqVO) { // 校验规则名称是否重复 validateAttendanceRuleNameUnique(null, createReqVO.getName()); // 注:同一个部门允许配置多条规则(多班次),这里不再校验部门唯一 HrmAttendanceRuleDO attendanceRule = BeanUtils.toBean(createReqVO, HrmAttendanceRuleDO.class); // 设置默认值 if (attendanceRule.getLateAllowMinutes() == null) { attendanceRule.setLateAllowMinutes(0); } if (attendanceRule.getEarlyAllowMinutes() == null) { attendanceRule.setEarlyAllowMinutes(0); } if (attendanceRule.getOvertimeThreshold() == null) { attendanceRule.setOvertimeThreshold(BigDecimal.ONE); } if (attendanceRule.getStatus() == null) { attendanceRule.setStatus(0); } attendanceRuleMapper.insert(attendanceRule); return attendanceRule.getId(); } @Override public void updateAttendanceRule(HrmAttendanceRuleSaveReqVO updateReqVO) { validateAttendanceRuleExists(updateReqVO.getId()); // 校验规则名称是否重复 validateAttendanceRuleNameUnique(updateReqVO.getId(), updateReqVO.getName()); // 注:同一个部门允许配置多条规则(多班次),这里不再校验部门唯一 HrmAttendanceRuleDO updateObj = BeanUtils.toBean(updateReqVO, HrmAttendanceRuleDO.class); attendanceRuleMapper.updateById(updateObj); } @Override public void deleteAttendanceRule(Long id) { validateAttendanceRuleExists(id); attendanceRuleMapper.deleteById(id); } private void validateAttendanceRuleExists(Long id) { if (attendanceRuleMapper.selectById(id) == null) { throw exception(ATTENDANCE_RULE_NOT_EXISTS); } } private void validateAttendanceRuleNameUnique(Long id, String name) { HrmAttendanceRuleDO rule = attendanceRuleMapper.selectOne(HrmAttendanceRuleDO::getName, name); if (rule == null) { return; } if (id == null) { throw exception(ATTENDANCE_RULE_NAME_EXISTS); } if (!rule.getId().equals(id)) { throw exception(ATTENDANCE_RULE_NAME_EXISTS); } } @Override public HrmAttendanceRuleDO getAttendanceRule(Long id) { return attendanceRuleMapper.selectById(id); } @Override public PageResult getAttendanceRulePage(HrmAttendanceRulePageReqVO pageReqVO) { return attendanceRuleMapper.selectPage(pageReqVO); } @Override public List getAttendanceRuleList() { return attendanceRuleMapper.selectListByStatus(0); } @Override public HrmAttendanceRuleDO getAttendanceRuleByDeptId(Long deptId) { return getAttendanceRuleByDeptId(deptId, null); } @Override public HrmAttendanceRuleDO getAttendanceRuleByDeptId(Long deptId, LocalDateTime clockTime) { // 1. 先查找部门专属规则(同一个部门可以配置多条,用于多班次) List deptRules = attendanceRuleMapper.selectList(new LambdaQueryWrapperX() .eq(HrmAttendanceRuleDO::getDeptId, deptId) .eq(HrmAttendanceRuleDO::getStatus, ENABLED_STATUS)); if (!deptRules.isEmpty()) { return matchRuleByClockTime(deptRules, clockTime); } // 2. 查找全局规则(deptId 为 null) List globalRules = attendanceRuleMapper.selectList(new LambdaQueryWrapperX() .isNull(HrmAttendanceRuleDO::getDeptId) .eq(HrmAttendanceRuleDO::getStatus, ENABLED_STATUS)); if (!globalRules.isEmpty()) { return matchRuleByClockTime(globalRules, clockTime); } return null; } /** * 多条规则(同一个部门多班次)时,按打卡时间选班次: * 1. 打卡时间落在某个班次的 [上班时间, 下班时间] 内,就取这一条 * 2. 都不在窗口内时,取班次时间窗口离打卡时间最近的一条 * 3. 仍然并列时,按上班时间、编号排序,保证结果稳定(不会随数据库返回顺序漂移) * 打卡时间为空(不关心班次)时,取上班时间最早的一条。 */ @VisibleForTesting static HrmAttendanceRuleDO matchRuleByClockTime(List rules, LocalDateTime clockTime) { if (rules.size() == 1) { return rules.get(0); } if (clockTime == null) { return rules.stream().min(Comparator .comparing(HrmAttendanceRuleDO::getWorkStartTime, Comparator.nullsLast(Comparator.naturalOrder())) .thenComparing(HrmAttendanceRuleDO::getId)) .orElse(rules.get(0)); } LocalTime time = clockTime.toLocalTime(); return rules.stream().min(Comparator .comparingLong((HrmAttendanceRuleDO rule) -> minutesToRuleWindow(rule, time)) .thenComparing(HrmAttendanceRuleDO::getWorkStartTime, Comparator.nullsLast(Comparator.naturalOrder())) .thenComparing(HrmAttendanceRuleDO::getId)) .orElse(rules.get(0)); } /** * 打卡时间到规则班次窗口的距离(分钟):落在窗口内为 0,班次时间未配置时记为最大。 * 支持跨天班次(如 22:00-06:00,只要 end <= start 就按跨天处理)。 */ private static long minutesToRuleWindow(HrmAttendanceRuleDO rule, LocalTime time) { LocalTime start = rule.getWorkStartTime(); LocalTime end = rule.getWorkEndTime(); if (start == null || end == null) { return Long.MAX_VALUE; } boolean crossDay = !end.isAfter(start); boolean inWindow = crossDay ? !time.isBefore(start) || !time.isAfter(end) : !time.isBefore(start) && !time.isAfter(end); if (inWindow) { return 0; } return Math.min(minutesBetween(time, start), minutesBetween(time, end)); } /** 两个时刻的分钟差,按一天 24 小时取最近的环回距离(23:50 与 00:10 只差 20 分钟) */ private static long minutesBetween(LocalTime one, LocalTime other) { long minutes = Math.abs(Duration.between(one, other).toMinutes()); return Math.min(minutes, 24 * 60 - minutes); } }