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.module.hrm.controller.admin.attendance.vo.HrmAttendanceMyStatusRespVO;
|
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRecordPageReqVO;
|
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRecordRespVO;
|
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRuleRespVO;
|
import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceHolidayDO;
|
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.HrmAttendanceExceptionDO;
|
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.enums.HrmAttendanceClockTypeEnum;
|
import cn.iocoder.yudao.module.hrm.enums.HrmAttendanceDataSourceEnum;
|
import cn.iocoder.yudao.module.hrm.enums.HrmAttendanceExceptionTypeEnum;
|
import cn.iocoder.yudao.module.hrm.enums.HrmAttendanceHolidayTypeEnum;
|
import cn.iocoder.yudao.module.hrm.service.employee.HrmEmployeeService;
|
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
|
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
|
import jakarta.annotation.Resource;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.validation.annotation.Validated;
|
|
import java.math.BigDecimal;
|
import java.math.RoundingMode;
|
import java.time.DayOfWeek;
|
import java.time.Duration;
|
import java.time.LocalDate;
|
import java.time.LocalDateTime;
|
import java.time.LocalTime;
|
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 HrmAttendanceRecordServiceImpl implements HrmAttendanceRecordService {
|
|
@Resource
|
private HrmAttendanceRecordMapper attendanceRecordMapper;
|
@Resource
|
private HrmEmployeeService employeeService;
|
@Resource
|
private HrmAttendanceRuleService attendanceRuleService;
|
@Resource
|
private HrmAttendanceHolidayService attendanceHolidayService;
|
@Resource
|
private DeptApi deptApi;
|
|
@Override
|
public PageResult<HrmAttendanceRecordDO> getAttendanceRecordPage(HrmAttendanceRecordPageReqVO pageReqVO) {
|
return attendanceRecordMapper.selectPage(pageReqVO);
|
}
|
|
@Override
|
public HrmAttendanceRecordDO getAttendanceRecord(Long id) {
|
return attendanceRecordMapper.selectById(id);
|
}
|
|
@Override
|
public List<HrmAttendanceRecordDO> getAttendanceRecordListByUserIdAndDateBetween(Long userId, String startDate, String endDate) {
|
return attendanceRecordMapper.selectListByUserIdAndDateBetween(userId, LocalDate.parse(startDate), LocalDate.parse(endDate));
|
}
|
|
@Override
|
public HrmAttendanceRecordDO getAttendanceRecordByUserIdAndDate(Long userId, LocalDate date) {
|
return attendanceRecordMapper.selectByUserIdAndDate(userId, date);
|
}
|
|
@Override
|
public HrmAttendanceMyStatusRespVO getMyStatus(Long loginUserId, LocalDate date) {
|
LocalDate queryDate = date != null ? date : LocalDate.now();
|
// 1. 登录用户 -> 员工档案
|
HrmEmployeeDO employee = employeeService.getEmployeeByUserId(loginUserId);
|
if (employee == null) {
|
throw exception(EMPLOYEE_NOT_EXISTS,
|
"当前登录账号未关联员工档案,无法打卡。请联系管理员在【员工管理】中将该登录账号绑定到员工。");
|
}
|
HrmAttendanceMyStatusRespVO respVO = new HrmAttendanceMyStatusRespVO();
|
respVO.setUserId(loginUserId);
|
respVO.setEmployeeId(employee.getId());
|
respVO.setEmployeeName(employee.getName());
|
respVO.setDeptId(employee.getDeptId());
|
if (employee.getDeptId() != null) {
|
DeptRespDTO dept = deptApi.getDept(employee.getDeptId());
|
if (dept != null) {
|
respVO.setDeptName(dept.getName());
|
}
|
}
|
respVO.setDate(queryDate);
|
respVO.setServerTime(LocalDateTime.now());
|
|
// 2. 是否工作日 + 休息原因
|
boolean workDay = attendanceHolidayService.isWorkDay(queryDate);
|
respVO.setWorkDay(workDay);
|
if (!workDay) {
|
respVO.setRestDayReason(getRestDayReason(queryDate));
|
}
|
|
// 3. 当前生效规则(部门规则优先,其次全局规则)
|
HrmAttendanceRuleDO rule = attendanceRuleService.getAttendanceRuleByDeptId(employee.getDeptId());
|
if (rule != null) {
|
HrmAttendanceRuleRespVO ruleRespVO = BeanUtils.toBean(rule, HrmAttendanceRuleRespVO.class);
|
if (rule.getDeptId() != null) {
|
DeptRespDTO ruleDept = deptApi.getDept(rule.getDeptId());
|
if (ruleDept != null) {
|
ruleRespVO.setDeptName(ruleDept.getName());
|
}
|
}
|
respVO.setRule(ruleRespVO);
|
}
|
|
// 4. 当日考勤记录
|
HrmAttendanceRecordDO record = attendanceRecordMapper.selectByUserIdAndDate(employee.getId(), queryDate);
|
if (record != null) {
|
respVO.setRecord(buildRecordRespVO(record, employee));
|
}
|
|
// 5. 当前可打卡状态
|
respVO.setClockable(buildClockable(workDay, rule, record, queryDate));
|
return respVO;
|
}
|
|
/**
|
* 休息日原因:优先取节假日配置名称,未配置时按星期给出“周六/周日”
|
*/
|
private String getRestDayReason(LocalDate date) {
|
List<HrmAttendanceHolidayDO> holidays = attendanceHolidayService.getAttendanceHolidayListByDateRange(date, date);
|
for (HrmAttendanceHolidayDO holiday : holidays) {
|
if (HrmAttendanceHolidayTypeEnum.REST_DAY.getType().equals(holiday.getType())) {
|
return holiday.getName();
|
}
|
}
|
DayOfWeek dayOfWeek = date.getDayOfWeek();
|
if (dayOfWeek == DayOfWeek.SATURDAY) {
|
return "周六";
|
}
|
if (dayOfWeek == DayOfWeek.SUNDAY) {
|
return "周日";
|
}
|
return null;
|
}
|
|
/**
|
* 构建打卡记录返回 VO,补齐打卡类型名称、员工与部门信息
|
*/
|
private HrmAttendanceRecordRespVO buildRecordRespVO(HrmAttendanceRecordDO record, HrmEmployeeDO employee) {
|
HrmAttendanceRecordRespVO recordRespVO = BeanUtils.toBean(record, HrmAttendanceRecordRespVO.class);
|
recordRespVO.setClockInTypeName(HrmAttendanceClockTypeEnum.getNameByType(record.getClockInType()));
|
recordRespVO.setClockOutTypeName(HrmAttendanceClockTypeEnum.getNameByType(record.getClockOutType()));
|
if (employee != null) {
|
recordRespVO.setUserName(employee.getName());
|
recordRespVO.setPhone(employee.getPhone());
|
}
|
if (record.getDeptId() != null) {
|
DeptRespDTO dept = deptApi.getDept(record.getDeptId());
|
if (dept != null) {
|
recordRespVO.setDeptName(dept.getName());
|
}
|
}
|
return recordRespVO;
|
}
|
|
/**
|
* 根据工作日、考勤规则与当日记录计算可打卡状态
|
*/
|
private HrmAttendanceMyStatusRespVO.Clockable buildClockable(boolean workDay, HrmAttendanceRuleDO rule,
|
HrmAttendanceRecordDO record, LocalDate date) {
|
HrmAttendanceMyStatusRespVO.Clockable clockable = new HrmAttendanceMyStatusRespVO.Clockable();
|
if (!workDay) {
|
clockable.setClockIn(false);
|
clockable.setClockOut(false);
|
clockable.setNextAction("none");
|
clockable.setMessage("今日休息,无需打卡");
|
return clockable;
|
}
|
if (rule == null) {
|
clockable.setClockIn(false);
|
clockable.setClockOut(false);
|
clockable.setNextAction("none");
|
clockable.setMessage("未配置考勤规则,请联系管理员");
|
return clockable;
|
}
|
// 只有下班卡、没有上班卡:当日已结束,不再允许继续打卡
|
if (record != null && record.getClockInTime() == null && record.getClockOutTime() != null) {
|
clockable.setClockIn(false);
|
clockable.setClockOut(false);
|
clockable.setNextAction("none");
|
clockable.setMessage("今日缺上班卡,已完成下班打卡");
|
return clockable;
|
}
|
// 今天还没打上班卡
|
if (record == null || record.getClockInTime() == null) {
|
clockable.setClockIn(true);
|
clockable.setClockOut(false);
|
clockable.setNextAction("in");
|
clockable.setMessage("可上班打卡");
|
// 仅当天查询时提示已迟到时长
|
LocalDateTime now = LocalDateTime.now();
|
if (rule.getWorkStartTime() != null && date.equals(now.toLocalDate())) {
|
LocalDateTime workStart = LocalDateTime.of(date, rule.getWorkStartTime());
|
int lateAllowMinutes = rule.getLateAllowMinutes() != null ? rule.getLateAllowMinutes() : 0;
|
if (now.isAfter(workStart.plusMinutes(lateAllowMinutes))) {
|
long lateMinutes = Duration.between(workStart, now).toMinutes();
|
clockable.setMessage("已超过上班时间 " + lateMinutes + " 分钟");
|
}
|
}
|
return clockable;
|
}
|
// 已打上班卡、未打下班卡
|
if (record.getClockOutTime() == null) {
|
clockable.setClockIn(false);
|
clockable.setClockOut(true);
|
clockable.setNextAction("out");
|
clockable.setMessage("已上班打卡,可下班打卡");
|
return clockable;
|
}
|
// 上下班卡都已打
|
clockable.setClockIn(false);
|
clockable.setClockOut(false);
|
clockable.setNextAction("none");
|
clockable.setMessage("今日已完成打卡");
|
return clockable;
|
}
|
|
@Override
|
public void createImportedRecord(HrmAttendanceRecordDO record) {
|
// 按考勤规则重算打卡类型与时长,不信任 Excel 填写的值
|
fillClockTypes(record);
|
fillCalculatedHours(record);
|
attendanceRecordMapper.insert(record);
|
}
|
|
@Override
|
public void updateImportedRecord(HrmAttendanceRecordDO record) {
|
fillClockTypes(record);
|
fillCalculatedHours(record);
|
attendanceRecordMapper.updateById(record);
|
}
|
|
/**
|
* 按考勤规则重算上下班打卡类型(正常/迟到/早退),与打卡逻辑保持一致
|
*/
|
private void fillClockTypes(HrmAttendanceRecordDO record) {
|
if (record.getDeptId() == null) {
|
return;
|
}
|
HrmAttendanceRuleDO rule = attendanceRuleService.getAttendanceRuleByDeptId(record.getDeptId());
|
if (rule == null) {
|
return;
|
}
|
// 上班打卡类型:晚于 上班时间+迟到允许 记为迟到
|
if (record.getClockInTime() != null && rule.getWorkStartTime() != null) {
|
int lateAllowMinutes = rule.getLateAllowMinutes() != null ? rule.getLateAllowMinutes() : 0;
|
LocalTime lateThreshold = rule.getWorkStartTime().plusMinutes(lateAllowMinutes);
|
LocalTime clockInOnly = record.getClockInTime().toLocalTime();
|
record.setClockInType(clockInOnly.isAfter(lateThreshold)
|
? HrmAttendanceClockTypeEnum.LATE.getType()
|
: HrmAttendanceClockTypeEnum.NORMAL.getType());
|
}
|
// 下班打卡类型:早于 下班时间-早退允许 记为早退
|
if (record.getClockOutTime() != null && rule.getWorkEndTime() != null) {
|
int earlyAllowMinutes = rule.getEarlyAllowMinutes() != null ? rule.getEarlyAllowMinutes() : 0;
|
LocalTime earlyThreshold = rule.getWorkEndTime().minusMinutes(earlyAllowMinutes);
|
LocalTime clockOutOnly = record.getClockOutTime().toLocalTime();
|
record.setClockOutType(clockOutOnly.isBefore(earlyThreshold)
|
? HrmAttendanceClockTypeEnum.EARLY.getType()
|
: HrmAttendanceClockTypeEnum.NORMAL.getType());
|
}
|
}
|
|
/**
|
* 根据上下班打卡时间和考勤规则,计算工作时长与加班时长
|
*/
|
private void fillCalculatedHours(HrmAttendanceRecordDO record) {
|
// 工作时长 = 下班打卡时间 - 上班打卡时间(与打卡逻辑一致)
|
if (record.getClockInTime() != null && record.getClockOutTime() != null) {
|
Duration duration = Duration.between(record.getClockInTime(), record.getClockOutTime());
|
BigDecimal workHours = BigDecimal.valueOf(duration.toMinutes())
|
.divide(BigDecimal.valueOf(60), 2, RoundingMode.HALF_UP);
|
record.setWorkHours(workHours);
|
}
|
// 加班时长:按考勤规则的标准下班时间计算超额部分
|
if (record.getClockOutTime() != null && record.getDeptId() != null) {
|
HrmAttendanceRuleDO rule = attendanceRuleService.getAttendanceRuleByDeptId(record.getDeptId());
|
if (rule != null && rule.getWorkEndTime() != null) {
|
LocalTime workEnd = rule.getWorkEndTime();
|
LocalDate outDate = record.getClockOutTime().toLocalDate();
|
LocalDateTime workEndDateTime = LocalDateTime.of(outDate, workEnd);
|
if (record.getClockOutTime().isAfter(workEndDateTime)) {
|
Duration overtimeDuration = Duration.between(workEndDateTime, record.getClockOutTime());
|
BigDecimal overtimeThreshold = rule.getOvertimeThreshold() != null
|
? rule.getOvertimeThreshold() : BigDecimal.ZERO;
|
BigDecimal overtimeHours = BigDecimal.valueOf(overtimeDuration.toMinutes())
|
.divide(BigDecimal.valueOf(60), 2, RoundingMode.HALF_UP);
|
record.setOvertimeHours(overtimeHours.compareTo(overtimeThreshold) > 0 ? overtimeHours : BigDecimal.ZERO);
|
} else {
|
record.setOvertimeHours(BigDecimal.ZERO);
|
}
|
} else {
|
record.setOvertimeHours(BigDecimal.ZERO);
|
}
|
}
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public HrmAttendanceRecordDO clockIn(Long userId, LocalDateTime clockTime, String location) {
|
LocalDate clockDate = clockTime.toLocalDate();
|
// 1. 校验是否工作日
|
if (!attendanceHolidayService.isWorkDay(clockDate)) {
|
throw exception(ATTENDANCE_CLOCK_FAIL_NOT_WORKDAY);
|
}
|
// 2. 获取员工信息和考勤规则
|
HrmEmployeeDO employee = employeeService.getEmployee(userId);
|
if (employee == null) {
|
throw exception(EMPLOYEE_NOT_EXISTS);
|
}
|
HrmAttendanceRuleDO rule = attendanceRuleService.getAttendanceRuleByDeptId(employee.getDeptId());
|
if (rule == null) {
|
throw exception(ATTENDANCE_CLOCK_FAIL_NO_RULE);
|
}
|
// 3. 查询当日考勤记录
|
HrmAttendanceRecordDO record = attendanceRecordMapper.selectByUserIdAndDate(userId, clockDate);
|
if (record != null && record.getClockInTime() != null) {
|
throw exception(ATTENDANCE_CLOCK_ALREADY_CLOCKED);
|
}
|
// 4. 判断打卡类型
|
LocalTime workStartTime = rule.getWorkStartTime();
|
int lateAllowMinutes = rule.getLateAllowMinutes() != null ? rule.getLateAllowMinutes() : 0;
|
LocalTime lateThreshold = workStartTime.plusMinutes(lateAllowMinutes);
|
LocalTime clockTimeOnly = clockTime.toLocalTime();
|
Integer clockInType = clockTimeOnly.isBefore(lateThreshold) || clockTimeOnly.equals(lateThreshold)
|
? HrmAttendanceClockTypeEnum.NORMAL.getType()
|
: HrmAttendanceClockTypeEnum.LATE.getType();
|
// 5. 创建或更新考勤记录
|
if (record == null) {
|
record = HrmAttendanceRecordDO.builder()
|
.userId(userId)
|
.deptId(employee.getDeptId())
|
.date(clockDate)
|
.clockInTime(clockTime)
|
.clockInType(clockInType)
|
.location(location)
|
.dataSource(HrmAttendanceDataSourceEnum.CLOCK.getType())
|
.build();
|
attendanceRecordMapper.insert(record);
|
} else {
|
record.setClockInTime(clockTime);
|
record.setClockInType(clockInType);
|
if (location != null) {
|
record.setLocation(location);
|
}
|
attendanceRecordMapper.updateById(record);
|
}
|
return record;
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public HrmAttendanceRecordDO clockOut(Long userId, LocalDateTime clockTime, String location) {
|
LocalDate clockDate = clockTime.toLocalDate();
|
// 1. 校验是否工作日
|
if (!attendanceHolidayService.isWorkDay(clockDate)) {
|
throw exception(ATTENDANCE_CLOCK_FAIL_NOT_WORKDAY);
|
}
|
// 2. 获取员工信息和考勤规则
|
HrmEmployeeDO employee = employeeService.getEmployee(userId);
|
if (employee == null) {
|
throw exception(EMPLOYEE_NOT_EXISTS);
|
}
|
HrmAttendanceRuleDO rule = attendanceRuleService.getAttendanceRuleByDeptId(employee.getDeptId());
|
if (rule == null) {
|
throw exception(ATTENDANCE_CLOCK_FAIL_NO_RULE);
|
}
|
// 3. 查询当日考勤记录
|
HrmAttendanceRecordDO record = attendanceRecordMapper.selectByUserIdAndDate(userId, clockDate);
|
if (record == null) {
|
// 如果没有上班打卡记录,创建一条缺卡的记录
|
record = HrmAttendanceRecordDO.builder()
|
.userId(userId)
|
.deptId(employee.getDeptId())
|
.date(clockDate)
|
.clockInType(HrmAttendanceClockTypeEnum.MISSING.getType())
|
.dataSource(HrmAttendanceDataSourceEnum.CLOCK.getType())
|
.build();
|
attendanceRecordMapper.insert(record);
|
}
|
if (record.getClockOutTime() != null) {
|
throw exception(ATTENDANCE_CLOCK_ALREADY_CLOCKED);
|
}
|
// 4. 判断打卡类型
|
LocalTime workEndTime = rule.getWorkEndTime();
|
int earlyAllowMinutes = rule.getEarlyAllowMinutes() != null ? rule.getEarlyAllowMinutes() : 0;
|
LocalTime earlyThreshold = workEndTime.minusMinutes(earlyAllowMinutes);
|
LocalTime clockTimeOnly = clockTime.toLocalTime();
|
Integer clockOutType = clockTimeOnly.isAfter(earlyThreshold) || clockTimeOnly.equals(earlyThreshold)
|
? HrmAttendanceClockTypeEnum.NORMAL.getType()
|
: HrmAttendanceClockTypeEnum.EARLY.getType();
|
// 5. 更新考勤记录
|
record.setClockOutTime(clockTime);
|
record.setClockOutType(clockOutType);
|
if (location != null) {
|
record.setLocation(location);
|
}
|
// 6. 计算工作时长
|
if (record.getClockInTime() != null) {
|
Duration duration = Duration.between(record.getClockInTime(), clockTime);
|
BigDecimal workHours = BigDecimal.valueOf(duration.toMinutes())
|
.divide(BigDecimal.valueOf(60), 2, RoundingMode.HALF_UP);
|
record.setWorkHours(workHours);
|
}
|
attendanceRecordMapper.updateById(record);
|
return record;
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void supplementClockIn(HrmAttendanceExceptionDO exception) {
|
// 仅处理补卡类型
|
if (!HrmAttendanceExceptionTypeEnum.SUPPLEMENT.getType().equals(exception.getType())) {
|
return;
|
}
|
LocalDate exceptionDate = exception.getExceptionDate();
|
LocalDateTime exceptionTime = exception.getExceptionTime();
|
Long userId = exception.getUserId();
|
// 查询考勤记录
|
HrmAttendanceRecordDO record = attendanceRecordMapper.selectByUserIdAndDate(userId, exceptionDate);
|
if (record == null) {
|
// 如果没有记录,创建一条
|
HrmEmployeeDO employee = employeeService.getEmployee(userId);
|
record = HrmAttendanceRecordDO.builder()
|
.userId(userId)
|
.deptId(employee != null ? employee.getDeptId() : exception.getDeptId())
|
.date(exceptionDate)
|
.dataSource(HrmAttendanceDataSourceEnum.SUPPLEMENT.getType())
|
.build();
|
attendanceRecordMapper.insert(record);
|
}
|
// 根据异常时间判断是上班还是下班补卡
|
LocalTime time = exceptionTime.toLocalTime();
|
if (time.isBefore(LocalTime.of(12, 0))) {
|
// 上班补卡
|
record.setClockInTime(exceptionTime);
|
record.setClockInType(HrmAttendanceClockTypeEnum.NORMAL.getType());
|
} else {
|
// 下班补卡
|
record.setClockOutTime(exceptionTime);
|
record.setClockOutType(HrmAttendanceClockTypeEnum.NORMAL.getType());
|
}
|
// 更新数据来源为补卡审批
|
record.setDataSource(HrmAttendanceDataSourceEnum.SUPPLEMENT.getType());
|
// 计算工作时长
|
if (record.getClockInTime() != null && record.getClockOutTime() != null) {
|
Duration duration = Duration.between(record.getClockInTime(), record.getClockOutTime());
|
BigDecimal workHours = BigDecimal.valueOf(duration.toMinutes())
|
.divide(BigDecimal.valueOf(60), 2, RoundingMode.HALF_UP);
|
record.setWorkHours(workHours);
|
}
|
attendanceRecordMapper.updateById(record);
|
}
|
|
}
|