package cn.iocoder.yudao.module.hrm.service.attendance; import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceRuleDO; import org.junit.jupiter.api.Test; import java.math.BigDecimal; import java.time.LocalDateTime; import java.time.LocalTime; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; /** * {@link HrmAttendanceRuleServiceImpl} 的单元测试 * * 同一个部门允许配置多条考勤规则(多班次),这里覆盖「按打卡时间匹配班次」的选规则逻辑。 * 注意:断言匹配到的规则编号,而不是规则对象本身 —— 返回的是传入的同一个对象,比较编号更能说明选的是哪一条。 */ public class HrmAttendanceRuleServiceImplTest { private static HrmAttendanceRuleDO rule(Long id, String name, LocalTime start, LocalTime end) { return HrmAttendanceRuleDO.builder().id(id).name(name) .workStartTime(start).workEndTime(end) .lateAllowMinutes(0).earlyAllowMinutes(0) .overtimeThreshold(BigDecimal.ONE).status(0).build(); } /** 早班 08:00-16:00、晚班 16:00-24:00 —— 用来覆盖同一个部门多个班次 */ private static List dayAndNightRules() { return Stream.of( rule(1L, "早班", LocalTime.of(8, 0), LocalTime.of(16, 0)), rule(2L, "晚班", LocalTime.of(16, 0), LocalTime.of(23, 59))) .collect(Collectors.toList()); } @Test public void testMatchRuleByClockTime_singleRule() { // 只有一条规则时,无论打卡时间是多少都用它 List rules = Stream.of(rule(1L, "标准班", LocalTime.of(9, 0), LocalTime.of(18, 0))) .collect(Collectors.toList()); assertEquals(1L, HrmAttendanceRuleServiceImpl.matchRuleByClockTime(rules, LocalDateTime.of(2026, 10, 16, 2, 0)).getId()); } /** 打卡时间落在哪个班次的时间窗口内,就用哪个班次 —— 不能因为返回顺序变了就选错 */ @Test public void testMatchRuleByClockTime_clockTimeInWindow() { List rules = dayAndNightRules(); assertEquals(1L, HrmAttendanceRuleServiceImpl.matchRuleByClockTime(rules, LocalDateTime.of(2026, 10, 16, 8, 5)).getId()); assertEquals(2L, HrmAttendanceRuleServiceImpl.matchRuleByClockTime(rules, LocalDateTime.of(2026, 10, 16, 16, 5)).getId()); assertEquals(2L, HrmAttendanceRuleServiceImpl.matchRuleByClockTime(rules, LocalDateTime.of(2026, 10, 16, 23, 30)).getId()); } /** 班次顺序颠倒时结果不变(保证不随数据库返回顺序漂移) */ @Test public void testMatchRuleByClockTime_orderIndependent() { List reversed = Stream.of( rule(2L, "晚班", LocalTime.of(16, 0), LocalTime.of(23, 59)), rule(1L, "早班", LocalTime.of(8, 0), LocalTime.of(16, 0))) .collect(Collectors.toList()); assertEquals(1L, HrmAttendanceRuleServiceImpl.matchRuleByClockTime(reversed, LocalDateTime.of(2026, 10, 16, 7, 50)).getId()); } /** 打卡时间不在任何窗口内(例如提前很久到岗),取窗口离得最近的一条 */ @Test public void testMatchRuleByClockTime_outOfWindow() { List rules = dayAndNightRules(); assertEquals(1L, HrmAttendanceRuleServiceImpl.matchRuleByClockTime(rules, LocalDateTime.of(2026, 10, 16, 6, 0)).getId()); // 离早班 2 小时 assertEquals(2L, HrmAttendanceRuleServiceImpl.matchRuleByClockTime(rules, LocalDateTime.of(2026, 10, 17, 1, 0)).getId()); // 离晚班下班 1 小时 } /** 跨天夜班:22:00-06:00,凌晨的打卡属于这个班次 */ @Test public void testMatchRuleByClockTime_crossDay() { List rules = Stream.of( rule(1L, "白班", LocalTime.of(8, 0), LocalTime.of(16, 0)), rule(3L, "夜班", LocalTime.of(22, 0), LocalTime.of(6, 0))) .collect(Collectors.toList()); assertEquals(3L, HrmAttendanceRuleServiceImpl.matchRuleByClockTime(rules, LocalDateTime.of(2026, 10, 16, 22, 10)).getId()); assertEquals(3L, HrmAttendanceRuleServiceImpl.matchRuleByClockTime(rules, LocalDateTime.of(2026, 10, 17, 5, 40)).getId()); assertEquals(1L, HrmAttendanceRuleServiceImpl.matchRuleByClockTime(rules, LocalDateTime.of(2026, 10, 16, 9, 0)).getId()); } /** 不关心班次(打卡时间为空)时,取上班时间最早的一条,结果稳定 */ @Test public void testMatchRuleByClockTime_nullClockTime() { List rules = Stream.of( rule(2L, "晚班", LocalTime.of(16, 0), LocalTime.of(23, 59)), rule(1L, "早班", LocalTime.of(8, 0), LocalTime.of(16, 0))) .collect(Collectors.toList()); assertEquals(1L, HrmAttendanceRuleServiceImpl.matchRuleByClockTime(rules, null).getId()); } /** 班次时间没配好(为空)时不能抛异常,仍要能选出一条规则 */ @Test public void testMatchRuleByClockTime_ruleWithoutTime() { List rules = Stream.of( rule(9L, "未配置班次时间的规则", null, null), rule(1L, "早班", LocalTime.of(8, 0), LocalTime.of(16, 0))) .collect(Collectors.toList()); HrmAttendanceRuleDO matched = HrmAttendanceRuleServiceImpl.matchRuleByClockTime(rules, LocalDateTime.of(2026, 10, 16, 9, 0)); assertNotNull(matched); assertEquals(1L, matched.getId()); } }