package cn.iocoder.yudao.module.hrm.service.attendance; import cn.hutool.core.util.StrUtil; 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 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.LocalDate; 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 { @Resource private HrmAttendanceRuleMapper attendanceRuleMapper; @Override public Long createAttendanceRule(HrmAttendanceRuleSaveReqVO createReqVO) { validateLocation(createReqVO); // 校验规则名称是否重复(同一部门允许多条规则,不再限制部门唯一) validateAttendanceRuleNameUnique(null, createReqVO.getName()); validateEffectiveDateRange(createReqVO.getEffectiveStartDate(), createReqVO.getEffectiveEndDate()); 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) { HrmAttendanceRuleDO currentRule = attendanceRuleMapper.selectById(updateReqVO.getId()); if (currentRule == null) { throw exception(ATTENDANCE_RULE_NOT_EXISTS); } // 兼容旧管理端:没有提交地点字段时,不清空已配置的位置限制。 if (updateReqVO.getLocationRequired() == null) { updateReqVO.setLocationRequired(currentRule.getLocationRequired()); if (updateReqVO.getLocationName() == null) updateReqVO.setLocationName(currentRule.getLocationName()); if (updateReqVO.getLocationAddress() == null) updateReqVO.setLocationAddress(currentRule.getLocationAddress()); if (updateReqVO.getLongitude() == null) updateReqVO.setLongitude(currentRule.getLongitude()); if (updateReqVO.getLatitude() == null) updateReqVO.setLatitude(currentRule.getLatitude()); if (updateReqVO.getRadiusMeters() == null) updateReqVO.setRadiusMeters(currentRule.getRadiusMeters()); if (updateReqVO.getCoordinateSystem() == null) updateReqVO.setCoordinateSystem(currentRule.getCoordinateSystem()); } validateLocation(updateReqVO); // 校验规则名称是否重复 validateAttendanceRuleNameUnique(updateReqVO.getId(), updateReqVO.getName()); validateEffectiveDateRange(updateReqVO.getEffectiveStartDate(), updateReqVO.getEffectiveEndDate()); 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 validateLocation(HrmAttendanceRuleSaveReqVO reqVO) { if (reqVO.getLocationRequired() == null) { reqVO.setLocationRequired(false); } if (reqVO.getRadiusMeters() == null) { reqVO.setRadiusMeters(10); } if (StrUtil.isBlank(reqVO.getCoordinateSystem())) { reqVO.setCoordinateSystem("WGS84"); } else { reqVO.setCoordinateSystem(reqVO.getCoordinateSystem().trim().toUpperCase(java.util.Locale.ROOT)); } if (!"WGS84".equals(reqVO.getCoordinateSystem())) { throw exception(ATTENDANCE_LOCATION_SYSTEM_INVALID); } if (reqVO.getLongitude() != null && (reqVO.getLongitude().compareTo(BigDecimal.valueOf(-180)) < 0 || reqVO.getLongitude().compareTo(BigDecimal.valueOf(180)) > 0)) { throw exception(ATTENDANCE_LOCATION_COORDINATE_INVALID); } if (reqVO.getLatitude() != null && (reqVO.getLatitude().compareTo(BigDecimal.valueOf(-90)) < 0 || reqVO.getLatitude().compareTo(BigDecimal.valueOf(90)) > 0)) { throw exception(ATTENDANCE_LOCATION_COORDINATE_INVALID); } if (reqVO.getRadiusMeters() < 10 || reqVO.getRadiusMeters() > 5000) { throw exception(ATTENDANCE_LOCATION_RULE_INVALID); } if (Boolean.TRUE.equals(reqVO.getLocationRequired()) && (StrUtil.isBlank(reqVO.getLocationName()) || reqVO.getLongitude() == null || reqVO.getLatitude() == null)) { throw exception(ATTENDANCE_LOCATION_RULE_INVALID); } } 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); } } /** * 校验生效日期区间:开始日期不能晚于结束日期 */ private void validateEffectiveDateRange(LocalDate startDate, LocalDate endDate) { if (startDate != null && endDate != null && startDate.isAfter(endDate)) { throw exception(ATTENDANCE_RULE_EFFECTIVE_DATE_INVALID); } } @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, LocalDate date) { LocalDate targetDate = date != null ? date : LocalDate.now(); // 1. 先查找部门专属规则(覆盖目标日期的,生效开始日期最晚的优先) if (deptId != null) { List deptRules = selectEffectiveRules(deptId, targetDate); if (!deptRules.isEmpty()) { return deptRules.get(0); } } // 2. 查找全局规则(deptId 为 null) List globalRules = selectEffectiveRules(null, targetDate); if (!globalRules.isEmpty()) { return globalRules.get(0); } return null; } /** * 查询在指定日期生效的规则 * * @param deptId 部门ID,null 表示全局规则 * @param date 目标日期 */ private List selectEffectiveRules(Long deptId, LocalDate date) { LambdaQueryWrapperX wrapper = new LambdaQueryWrapperX<>(); if (deptId == null) { wrapper.isNull(HrmAttendanceRuleDO::getDeptId); } else { wrapper.eq(HrmAttendanceRuleDO::getDeptId, deptId); } wrapper.eq(HrmAttendanceRuleDO::getStatus, 0); // 生效开始日期为空(不限)或 <= 目标日期 wrapper.and(w -> w.isNull(HrmAttendanceRuleDO::getEffectiveStartDate) .or().le(HrmAttendanceRuleDO::getEffectiveStartDate, date)); // 生效结束日期为空(不限)或 >= 目标日期 wrapper.and(w -> w.isNull(HrmAttendanceRuleDO::getEffectiveEndDate) .or().ge(HrmAttendanceRuleDO::getEffectiveEndDate, date)); // 生效开始日期越晚优先级越高;同开始日期时取先创建的 wrapper.orderByDesc(HrmAttendanceRuleDO::getEffectiveStartDate); wrapper.orderByAsc(HrmAttendanceRuleDO::getId); return attendanceRuleMapper.selectList(wrapper); } }