gaoluyang
10 小时以前 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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
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<HrmAttendanceRuleDO> getAttendanceRulePage(HrmAttendanceRulePageReqVO pageReqVO) {
        return attendanceRuleMapper.selectPage(pageReqVO);
    }
 
    @Override
    public List<HrmAttendanceRuleDO> 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<HrmAttendanceRuleDO> deptRules = attendanceRuleMapper.selectList(new LambdaQueryWrapperX<HrmAttendanceRuleDO>()
                .eq(HrmAttendanceRuleDO::getDeptId, deptId)
                .eq(HrmAttendanceRuleDO::getStatus, ENABLED_STATUS));
        if (!deptRules.isEmpty()) {
            return matchRuleByClockTime(deptRules, clockTime);
        }
        // 2. 查找全局规则(deptId 为 null)
        List<HrmAttendanceRuleDO> globalRules = attendanceRuleMapper.selectList(new LambdaQueryWrapperX<HrmAttendanceRuleDO>()
                .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<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);
    }
 
}