gaoluyang
6 小时以前 5db13c2ad6678cf4614b81c95df035c6f47d313d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
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<HrmAttendanceRuleDO> 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<HrmAttendanceRuleDO> 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<HrmAttendanceRuleDO> 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<HrmAttendanceRuleDO> 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<HrmAttendanceRuleDO> 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<HrmAttendanceRuleDO> 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<HrmAttendanceRuleDO> 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<HrmAttendanceRuleDO> 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());
    }
 
}