package cn.iocoder.yudao.module.hrm.service.attendance;
|
|
import cn.hutool.core.collection.CollUtil;
|
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceSummaryPageReqVO;
|
import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceRecordDO;
|
import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceRuleDO;
|
import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceSummaryDO;
|
import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO;
|
import cn.iocoder.yudao.module.hrm.dal.mysql.attendance.HrmAttendanceRecordMapper;
|
import cn.iocoder.yudao.module.hrm.dal.mysql.attendance.HrmAttendanceSummaryMapper;
|
import cn.iocoder.yudao.module.hrm.enums.HrmAttendanceClockTypeEnum;
|
import cn.iocoder.yudao.module.hrm.enums.HrmAuditStatusEnum;
|
import cn.iocoder.yudao.module.hrm.service.employee.HrmEmployeeService;
|
import jakarta.annotation.Resource;
|
import lombok.extern.slf4j.Slf4j;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.validation.annotation.Validated;
|
|
import java.math.BigDecimal;
|
import java.time.LocalDate;
|
import java.time.LocalDateTime;
|
import java.time.YearMonth;
|
import java.util.List;
|
|
/**
|
* 考勤统计汇总 Service 实现类
|
*
|
* @author 超级管理员
|
*/
|
@Service
|
@Validated
|
@Slf4j
|
public class HrmAttendanceSummaryServiceImpl implements HrmAttendanceSummaryService {
|
|
@Resource
|
private HrmAttendanceSummaryMapper attendanceSummaryMapper;
|
@Resource
|
private HrmAttendanceRecordMapper attendanceRecordMapper;
|
@Resource
|
private HrmEmployeeService employeeService;
|
@Resource
|
private HrmAttendanceHolidayService attendanceHolidayService;
|
@Resource
|
private cn.iocoder.yudao.module.hrm.dal.mysql.approval.HrmLeaveApplicationMapper leaveApplicationMapper;
|
@Resource
|
private HrmAttendanceRuleService attendanceRuleService;
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void generateMonthlySummary(Long userId, String yearMonth) {
|
// 1. 解析年月,获取该月起始日期
|
YearMonth ym = YearMonth.parse(yearMonth);
|
LocalDate startDate = ym.atDay(1);
|
LocalDate endDate = ym.atEndOfMonth();
|
|
// 2. 获取员工信息
|
HrmEmployeeDO employee = employeeService.getEmployee(userId);
|
if (employee == null) {
|
log.warn("[generateMonthlySummary] 员工不存在: {}", userId);
|
return;
|
}
|
|
// 3. 查询该月所有考勤记录
|
List<HrmAttendanceRecordDO> records = attendanceRecordMapper.selectListByUserIdAndDateBetween(userId, startDate, endDate);
|
|
// 3.1 查询该月所有审批通过的请假单
|
// 考勤记录只在「打卡」时才创建,请假当天没来上班就不会有记录,
|
// 所以必须单独查请假单,否则 leaveHours 会漏算。
|
List<cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmLeaveApplicationDO> monthLeaves =
|
leaveApplicationMapper.selectListByUserIdAndOverlapDate(
|
userId, HrmAuditStatusEnum.APPROVE.getStatus(),
|
startDate.atStartOfDay(), endDate.plusDays(1).atStartOfDay());
|
|
// 4. 计算应出勤天数(工作日数量)
|
int workDays = calculateWorkDays(startDate, endDate);
|
|
// 5. 统计各项数据
|
int lateCount = 0;
|
int earlyCount = 0;
|
int absentCount = 0;
|
BigDecimal overtimeHours = BigDecimal.ZERO;
|
BigDecimal leaveHours = BigDecimal.ZERO;
|
int actualDays = 0;
|
|
for (HrmAttendanceRecordDO record : records) {
|
// 请假标记:审批通过的请假单落库时已写入 record.leaveFlag
|
boolean onLeave = Boolean.TRUE.equals(record.getLeaveFlag());
|
// 累计请假工时
|
if (onLeave && record.getLeaveHours() != null) {
|
leaveHours = leaveHours.add(record.getLeaveHours());
|
}
|
// 请假当天不计迟到、早退、缺卡(请假类型本身是 LEAVE,不会命中下面的判断)
|
if (!onLeave) {
|
// 统计迟到
|
if (HrmAttendanceClockTypeEnum.LATE.getType().equals(record.getClockInType())) {
|
lateCount++;
|
}
|
// 统计早退
|
if (HrmAttendanceClockTypeEnum.EARLY.getType().equals(record.getClockOutType())) {
|
earlyCount++;
|
}
|
// 统计缺卡:请假当天没打卡属正常,不算缺卡
|
if (HrmAttendanceClockTypeEnum.MISSING.getType().equals(record.getClockInType())) {
|
absentCount++;
|
}
|
if (HrmAttendanceClockTypeEnum.MISSING.getType().equals(record.getClockOutType())) {
|
absentCount++;
|
}
|
}
|
// 累计加班时长
|
if (record.getOvertimeHours() != null) {
|
overtimeHours = overtimeHours.add(record.getOvertimeHours());
|
}
|
// 统计实际出勤天数(有打卡记录即算;纯请假未打卡不算出勤)
|
if (record.getClockInTime() != null || record.getClockOutTime() != null) {
|
actualDays++;
|
}
|
}
|
|
// 5.1 补算「有请假单但没有考勤记录」的日子
|
// 这类情况是请假当天完全没来打卡,不会生成 record,
|
// 上面循环覆盖不到,需要单独按请假单累计工时。
|
if (!monthLeaves.isEmpty()) {
|
java.util.Set<LocalDate> recordDates = records.stream()
|
.map(HrmAttendanceRecordDO::getDate).collect(java.util.stream.Collectors.toSet());
|
HrmAttendanceRuleDO rule = attendanceRuleService.getAttendanceRuleByDeptId(employee.getDeptId());
|
// 按天归组请假单
|
java.util.Map<LocalDate, List<cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmLeaveApplicationDO>> byDate
|
= new java.util.LinkedHashMap<>();
|
for (var leave : monthLeaves) {
|
if (leave.getStartTime() == null || leave.getEndTime() == null) {
|
continue;
|
}
|
LocalDate d = leave.getStartTime().toLocalDate();
|
LocalDate last = leave.getEndTime().toLocalDate();
|
if (last.isAfter(endDate)) {
|
last = endDate;
|
}
|
for (LocalDate cur = d; !cur.isAfter(last); cur = cur.plusDays(1)) {
|
if (cur.isBefore(startDate)) {
|
continue;
|
}
|
byDate.computeIfAbsent(cur, k -> new java.util.ArrayList<>()).add(leave);
|
}
|
}
|
for (var entry : byDate.entrySet()) {
|
// 已有考勤记录的日期,请假工时已在 record.leaveHours 里算过
|
if (recordDates.contains(entry.getKey())) {
|
continue;
|
}
|
leaveHours = leaveHours.add(calcLeaveHoursByDay(entry.getValue(), rule, entry.getKey()));
|
}
|
}
|
|
// 6. 保存或更新汇总记录
|
HrmAttendanceSummaryDO summary = attendanceSummaryMapper.selectByUserIdAndYearMonth(userId, yearMonth);
|
if (summary == null) {
|
summary = HrmAttendanceSummaryDO.builder()
|
.userId(userId)
|
.deptId(employee.getDeptId())
|
.yearMonth(yearMonth)
|
.workDays(workDays)
|
.actualDays(actualDays)
|
.lateCount(lateCount)
|
.earlyCount(earlyCount)
|
.absentCount(absentCount)
|
.overtimeHours(overtimeHours)
|
.leaveHours(leaveHours)
|
.build();
|
attendanceSummaryMapper.insert(summary);
|
} else {
|
summary.setWorkDays(workDays);
|
summary.setActualDays(actualDays);
|
summary.setLateCount(lateCount);
|
summary.setEarlyCount(earlyCount);
|
summary.setAbsentCount(absentCount);
|
summary.setOvertimeHours(overtimeHours);
|
summary.setLeaveHours(leaveHours); // 请假工时也要更新,否则重算时永远停在上一次
|
attendanceSummaryMapper.updateById(summary);
|
}
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void generateDeptMonthlySummary(Long deptId, String yearMonth) {
|
// 获取部门所有在职员工
|
List<HrmEmployeeDO> employees = employeeService.getEmployeeList(null);
|
if (CollUtil.isEmpty(employees)) {
|
return;
|
}
|
// 筛选部门员工
|
List<HrmEmployeeDO> deptEmployees = employees.stream()
|
.filter(e -> deptId.equals(e.getDeptId()))
|
.toList();
|
for (HrmEmployeeDO employee : deptEmployees) {
|
generateMonthlySummary(employee.getId(), yearMonth);
|
}
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void generateAllMonthlySummary(String yearMonth) {
|
// 获取所有在职员工
|
List<HrmEmployeeDO> employees = employeeService.getEmployeeList(null);
|
if (CollUtil.isEmpty(employees)) {
|
return;
|
}
|
for (HrmEmployeeDO employee : employees) {
|
generateMonthlySummary(employee.getId(), yearMonth);
|
}
|
}
|
|
/**
|
* 计算应出勤天数(工作日数量)
|
*/
|
/**
|
* 计算某一天的请假工时(与考勤记录里的算法保持一致)
|
*
|
* <p>取每张请假单与考勤规则工作时段的交集累加,精确到 0.01 小时。</p>
|
*/
|
private BigDecimal calcLeaveHoursByDay(
|
List<cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmLeaveApplicationDO> leaves,
|
HrmAttendanceRuleDO rule, LocalDate date) {
|
LocalDateTime workStart;
|
LocalDateTime workEnd;
|
if (rule != null && rule.getWorkStartTime() != null && rule.getWorkEndTime() != null) {
|
workStart = LocalDateTime.of(date, rule.getWorkStartTime());
|
workEnd = LocalDateTime.of(date, rule.getWorkEndTime());
|
if (!workEnd.isAfter(workStart)) {
|
workEnd = workEnd.plusDays(1);
|
}
|
} else {
|
workStart = date.atStartOfDay();
|
workEnd = date.plusDays(1).atStartOfDay();
|
}
|
BigDecimal total = BigDecimal.ZERO;
|
for (var leave : leaves) {
|
if (leave.getStartTime() == null || leave.getEndTime() == null) {
|
continue;
|
}
|
LocalDateTime from = leave.getStartTime().isAfter(workStart) ? leave.getStartTime() : workStart;
|
LocalDateTime to = leave.getEndTime().isBefore(workEnd) ? leave.getEndTime() : workEnd;
|
if (!to.isAfter(from)) {
|
continue;
|
}
|
long minutes = java.time.Duration.between(from, to).toMinutes();
|
total = total.add(BigDecimal.valueOf(minutes)
|
.divide(BigDecimal.valueOf(60), 2, java.math.RoundingMode.HALF_UP));
|
}
|
return total;
|
}
|
|
private int calculateWorkDays(LocalDate startDate, LocalDate endDate) {
|
int workDays = 0;
|
LocalDate current = startDate;
|
while (!current.isAfter(endDate)) {
|
if (attendanceHolidayService.isWorkDay(current)) {
|
workDays++;
|
}
|
current = current.plusDays(1);
|
}
|
return workDays;
|
}
|
|
@Override
|
public PageResult<HrmAttendanceSummaryDO> getAttendanceSummaryPage(HrmAttendanceSummaryPageReqVO pageReqVO) {
|
return attendanceSummaryMapper.selectPage(pageReqVO, new LambdaQueryWrapperX<HrmAttendanceSummaryDO>()
|
.eqIfPresent(HrmAttendanceSummaryDO::getUserId, pageReqVO.getUserId())
|
.eqIfPresent(HrmAttendanceSummaryDO::getDeptId, pageReqVO.getDeptId())
|
.eqIfPresent(HrmAttendanceSummaryDO::getYearMonth, pageReqVO.getYearMonth())
|
.orderByDesc(HrmAttendanceSummaryDO::getYearMonth)
|
.orderByAsc(HrmAttendanceSummaryDO::getDeptId));
|
}
|
|
@Override
|
public HrmAttendanceSummaryDO getMonthlySummary(Long userId, String yearMonth) {
|
return attendanceSummaryMapper.selectByUserIdAndYearMonth(userId, yearMonth);
|
}
|
|
@Override
|
public List<HrmAttendanceSummaryDO> getDeptMonthlySummaryList(Long deptId, String yearMonth) {
|
return attendanceSummaryMapper.selectListByDeptIdAndYearMonth(deptId, yearMonth);
|
}
|
|
}
|