| | |
| | | |
| | | 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 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; |
| | |
| | | /** |
| | | * 考勤规则配置 Service 实现类 |
| | | * |
| | | * @author 芋道源码 |
| | | * @author 超级管理员 |
| | | */ |
| | | @Service |
| | | @Validated |
| | | public class HrmAttendanceRuleServiceImpl implements HrmAttendanceRuleService { |
| | | |
| | | /** 规则状态:0-启用 */ |
| | | private static final Integer ENABLED_STATUS = 0; |
| | | |
| | | @Resource |
| | | private HrmAttendanceRuleMapper attendanceRuleMapper; |
| | |
| | | public Long createAttendanceRule(HrmAttendanceRuleSaveReqVO createReqVO) { |
| | | // 校验规则名称是否重复 |
| | | validateAttendanceRuleNameUnique(null, createReqVO.getName()); |
| | | // 校验部门是否已配置规则(如果指定了部门) |
| | | if (createReqVO.getDeptId() != null) { |
| | | validateAttendanceRuleDeptUnique(null, createReqVO.getDeptId()); |
| | | } |
| | | // 注:同一个部门允许配置多条规则(多班次),这里不再校验部门唯一 |
| | | HrmAttendanceRuleDO attendanceRule = BeanUtils.toBean(createReqVO, HrmAttendanceRuleDO.class); |
| | | // 设置默认值 |
| | | if (attendanceRule.getLateAllowMinutes() == null) { |
| | |
| | | validateAttendanceRuleExists(updateReqVO.getId()); |
| | | // 校验规则名称是否重复 |
| | | validateAttendanceRuleNameUnique(updateReqVO.getId(), updateReqVO.getName()); |
| | | // 校验部门是否已配置规则(如果指定了部门) |
| | | if (updateReqVO.getDeptId() != null) { |
| | | validateAttendanceRuleDeptUnique(updateReqVO.getId(), updateReqVO.getDeptId()); |
| | | } |
| | | // 注:同一个部门允许配置多条规则(多班次),这里不再校验部门唯一 |
| | | HrmAttendanceRuleDO updateObj = BeanUtils.toBean(updateReqVO, HrmAttendanceRuleDO.class); |
| | | attendanceRuleMapper.updateById(updateObj); |
| | | } |
| | |
| | | } |
| | | } |
| | | |
| | | private void validateAttendanceRuleDeptUnique(Long id, Long deptId) { |
| | | List<HrmAttendanceRuleDO> rules = attendanceRuleMapper.selectListByDeptId(deptId); |
| | | if (rules.isEmpty()) { |
| | | return; |
| | | } |
| | | for (HrmAttendanceRuleDO rule : rules) { |
| | | if (id == null || !rule.getId().equals(id)) { |
| | | throw exception(ATTENDANCE_RULE_DEPT_EXISTS); |
| | | } |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public HrmAttendanceRuleDO getAttendanceRule(Long id) { |
| | | return attendanceRuleMapper.selectById(id); |
| | |
| | | |
| | | @Override |
| | | public HrmAttendanceRuleDO getAttendanceRuleByDeptId(Long deptId) { |
| | | // 1. 先查找部门专属规则 |
| | | List<HrmAttendanceRuleDO> deptRules = attendanceRuleMapper.selectList(new cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX<HrmAttendanceRuleDO>() |
| | | return getAttendanceRuleByDeptId(deptId, null); |
| | | } |
| | | |
| | | @Override |
| | | public HrmAttendanceRuleDO getAttendanceRuleByDeptId(Long deptId, LocalDateTime clockTime) { |
| | | // 1. 先查找部门专属规则(同一个部门可以配置多条,用于多班次) |
| | | List<HrmAttendanceRuleDO> deptRules = attendanceRuleMapper.selectList(new LambdaQueryWrapperX<HrmAttendanceRuleDO>() |
| | | .eq(HrmAttendanceRuleDO::getDeptId, deptId) |
| | | .eq(HrmAttendanceRuleDO::getStatus, 0)); |
| | | .eq(HrmAttendanceRuleDO::getStatus, ENABLED_STATUS)); |
| | | if (!deptRules.isEmpty()) { |
| | | return deptRules.get(0); |
| | | return matchRuleByClockTime(deptRules, clockTime); |
| | | } |
| | | // 2. 查找全局规则(deptId 为 null) |
| | | List<HrmAttendanceRuleDO> globalRules = attendanceRuleMapper.selectList(new cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX<HrmAttendanceRuleDO>() |
| | | List<HrmAttendanceRuleDO> globalRules = attendanceRuleMapper.selectList(new LambdaQueryWrapperX<HrmAttendanceRuleDO>() |
| | | .isNull(HrmAttendanceRuleDO::getDeptId) |
| | | .eq(HrmAttendanceRuleDO::getStatus, 0)); |
| | | .eq(HrmAttendanceRuleDO::getStatus, ENABLED_STATUS)); |
| | | if (!globalRules.isEmpty()) { |
| | | return globalRules.get(0); |
| | | return matchRuleByClockTime(globalRules, clockTime); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | /** |
| | | * 多条规则(同一个部门多班次)时,按打卡时间选班次: |
| | | * 1. 打卡时间落在某个班次的 [上班时间, 下班时间] 内,就取这一条 |
| | | * 2. 都不在窗口内时,取班次时间窗口离打卡时间最近的一条 |
| | | * 3. 仍然并列时,按上班时间、编号排序,保证结果稳定(不会随数据库返回顺序漂移) |
| | | * 打卡时间为空(不关心班次)时,取上班时间最早的一条。 |
| | | */ |
| | | @VisibleForTesting |
| | | static HrmAttendanceRuleDO matchRuleByClockTime(List<HrmAttendanceRuleDO> 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.<LocalTime>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.<LocalTime>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); |
| | | } |
| | | |
| | | } |