package cn.iocoder.yudao.module.hrm.service.attendance;
|
|
import cn.hutool.core.collection.CollUtil;
|
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.json.JSONUtil;
|
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmEmployeeScheduleBatchSaveReqVO;
|
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmEmployeeScheduleBatchSaveRespVO;
|
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmEmployeeScheduleListReqVO;
|
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmEmployeeScheduleRespVO;
|
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmEmployeeScheduleSaveReqVO;
|
import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceHolidayDO;
|
import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmEmployeeScheduleDO;
|
import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmShiftDO;
|
import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO;
|
import cn.iocoder.yudao.module.hrm.enums.HrmAttendanceHolidayTypeEnum;
|
import cn.iocoder.yudao.module.hrm.dal.mysql.attendance.HrmEmployeeScheduleMapper;
|
import cn.iocoder.yudao.module.hrm.dal.mysql.attendance.HrmShiftMapper;
|
import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeMapper;
|
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.time.DayOfWeek;
|
import java.time.LocalDate;
|
import java.time.YearMonth;
|
import java.util.*;
|
import java.util.function.Function;
|
import java.util.stream.Collectors;
|
|
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 HrmEmployeeScheduleServiceImpl implements HrmEmployeeScheduleService {
|
|
@Resource
|
private HrmEmployeeScheduleMapper employeeScheduleMapper;
|
@Resource
|
private HrmEmployeeMapper employeeMapper;
|
@Resource
|
private HrmShiftMapper shiftMapper;
|
@Resource
|
private DeptApi deptApi;
|
@Resource
|
private HrmAttendanceHolidayService attendanceHolidayService;
|
|
@Override
|
public List<HrmEmployeeScheduleRespVO> getScheduleList(HrmEmployeeScheduleListReqVO reqVO) {
|
YearMonth yearMonth = YearMonth.parse(reqVO.getYearMonth());
|
LocalDate startDate = yearMonth.atDay(1);
|
LocalDate endDate = yearMonth.atEndOfMonth();
|
|
// 1. 按条件查询排班记录
|
List<HrmEmployeeScheduleDO> list;
|
if (reqVO.getEmployeeId() != null) {
|
list = employeeScheduleMapper.selectListByEmployeeIdAndPeriod(reqVO.getEmployeeId(), startDate, endDate);
|
} else if (reqVO.getDeptId() != null) {
|
// 与员工分页保持一致:选中部门时连带查询所有下级部门的员工
|
Set<Long> deptIds = getSelfAndChildDeptIds(reqVO.getDeptId());
|
List<HrmEmployeeDO> employees = employeeMapper.selectListByDeptIds(deptIds);
|
Set<Long> employeeIds = employees.stream().map(HrmEmployeeDO::getId).collect(Collectors.toSet());
|
list = CollUtil.isEmpty(employeeIds) ? Collections.emptyList()
|
: employeeScheduleMapper.selectListByEmployeeIdsAndPeriod(employeeIds, startDate, endDate);
|
} else {
|
list = employeeScheduleMapper.selectListByPeriod(startDate, endDate);
|
}
|
if (CollUtil.isEmpty(list)) {
|
return Collections.emptyList();
|
}
|
|
// 2. 拼接员工信息与班次信息
|
Set<Long> employeeIds = list.stream().map(HrmEmployeeScheduleDO::getEmployeeId)
|
.filter(Objects::nonNull).collect(Collectors.toSet());
|
Set<Long> shiftIds = list.stream().map(HrmEmployeeScheduleDO::getShiftId)
|
.filter(Objects::nonNull).collect(Collectors.toSet());
|
Map<Long, HrmEmployeeDO> employeeMap = CollUtil.isEmpty(employeeIds) ? Collections.emptyMap()
|
: employeeMapper.selectBatchIds(employeeIds).stream()
|
.collect(Collectors.toMap(HrmEmployeeDO::getId, Function.identity(), (a, b) -> a));
|
Map<Long, HrmShiftDO> shiftMap = CollUtil.isEmpty(shiftIds) ? Collections.emptyMap()
|
: shiftMapper.selectBatchIds(shiftIds).stream()
|
.collect(Collectors.toMap(HrmShiftDO::getId, Function.identity(), (a, b) -> a));
|
|
return list.stream().map(schedule -> {
|
HrmEmployeeScheduleRespVO respVO = new HrmEmployeeScheduleRespVO();
|
respVO.setId(schedule.getId());
|
respVO.setEmployeeId(schedule.getEmployeeId());
|
HrmEmployeeDO employee = schedule.getEmployeeId() == null ? null : employeeMap.get(schedule.getEmployeeId());
|
if (employee != null) {
|
respVO.setEmployeeName(employee.getName());
|
respVO.setEmployeeNo(employee.getEmployeeNo());
|
}
|
respVO.setDate(schedule.getDate());
|
respVO.setShiftId(schedule.getShiftId());
|
HrmShiftDO shift = schedule.getShiftId() == null ? null : shiftMap.get(schedule.getShiftId());
|
if (shift != null) {
|
respVO.setShiftName(shift.getName());
|
respVO.setShiftColor(shift.getColor());
|
}
|
respVO.setRemark(schedule.getRemark());
|
return respVO;
|
}).collect(Collectors.toList());
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void saveSchedule(List<HrmEmployeeScheduleSaveReqVO> saveReqVOList) {
|
if (CollUtil.isEmpty(saveReqVOList)) {
|
throw exception(SCHEDULE_ITEM_LIST_EMPTY);
|
}
|
// 1. 校验员工存在
|
Set<Long> employeeIds = saveReqVOList.stream().map(HrmEmployeeScheduleSaveReqVO::getEmployeeId)
|
.collect(Collectors.toSet());
|
if (CollUtil.isNotEmpty(employeeIds)) {
|
List<HrmEmployeeDO> employees = employeeMapper.selectBatchIds(employeeIds);
|
if (employees.size() != employeeIds.size()) {
|
throw exception(SCHEDULE_EMPLOYEE_NOT_EXISTS);
|
}
|
}
|
// 2. 校验班次存在
|
Set<Long> shiftIds = saveReqVOList.stream().map(HrmEmployeeScheduleSaveReqVO::getShiftId)
|
.filter(Objects::nonNull).collect(Collectors.toSet());
|
if (CollUtil.isNotEmpty(shiftIds)) {
|
List<HrmShiftDO> shifts = shiftMapper.selectBatchIds(shiftIds);
|
if (shifts.size() != shiftIds.size()) {
|
throw exception(SCHEDULE_SHIFT_NOT_EXISTS);
|
}
|
}
|
// 3. 逐条 upsert:有班次则保存,无班次则删除
|
for (HrmEmployeeScheduleSaveReqVO reqVO : saveReqVOList) {
|
HrmEmployeeScheduleDO existing = employeeScheduleMapper.selectByEmployeeIdAndDate(
|
reqVO.getEmployeeId(), reqVO.getDate());
|
if (reqVO.getShiftId() == null) {
|
if (existing != null) {
|
employeeScheduleMapper.deleteById(existing.getId());
|
}
|
} else {
|
if (existing == null) {
|
employeeScheduleMapper.insert(BeanUtils.toBean(reqVO, HrmEmployeeScheduleDO.class));
|
} else {
|
existing.setShiftId(reqVO.getShiftId());
|
existing.setRemark(reqVO.getRemark());
|
employeeScheduleMapper.updateById(existing);
|
}
|
}
|
}
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public HrmEmployeeScheduleBatchSaveRespVO batchSaveSchedule(HrmEmployeeScheduleBatchSaveReqVO reqVO) {
|
// 1. 校验日期范围和班次
|
if (reqVO.getEndDate().isBefore(reqVO.getStartDate())) {
|
throw exception(SCHEDULE_BATCH_DATE_RANGE_INVALID);
|
}
|
if (reqVO.getShiftId() != null && shiftMapper.selectById(reqVO.getShiftId()) == null) {
|
throw exception(SCHEDULE_SHIFT_NOT_EXISTS);
|
}
|
// 2. 按部门(含下级)+ 岗位 + 员工状态筛选目标员工
|
Set<Long> deptIds = getSelfAndChildDeptIds(reqVO.getDeptId());
|
List<HrmEmployeeDO> employees = employeeMapper.selectListByDeptIds(deptIds).stream()
|
.filter(employee -> matchEmployeeStatus(employee, reqVO.getEmployeeStatus()))
|
.filter(employee -> matchPost(employee, reqVO.getPostId()))
|
.collect(Collectors.toList());
|
if (CollUtil.isEmpty(employees)) {
|
DeptRespDTO dept = deptApi.getDept(reqVO.getDeptId());
|
throw exception(SCHEDULE_BATCH_EMPLOYEE_EMPTY, dept == null ? String.valueOf(reqVO.getDeptId()) : dept.getName());
|
}
|
|
// 3. 预加载范围内已有排班,避免逐条查询
|
Set<Long> employeeIds = employees.stream().map(HrmEmployeeDO::getId).collect(Collectors.toSet());
|
Map<String, HrmEmployeeScheduleDO> existingMap = employeeScheduleMapper
|
.selectListByEmployeeIdsAndPeriod(employeeIds, reqVO.getStartDate(), reqVO.getEndDate())
|
.stream()
|
.collect(Collectors.toMap(item -> scheduleKey(item.getEmployeeId(), item.getDate()),
|
Function.identity(), (a, b) -> a));
|
|
// 4. 预加载区间内节假日配置:配置为休息日的当天强制休息,配置为工作日的当天强制排班(可覆盖周末)
|
Map<LocalDate, Integer> holidayTypeMap = attendanceHolidayService
|
.getAttendanceHolidayListByDateRange(reqVO.getStartDate(), reqVO.getEndDate())
|
.stream()
|
.collect(Collectors.toMap(HrmAttendanceHolidayDO::getDate, HrmAttendanceHolidayDO::getType,
|
(a, b) -> a));
|
|
// 5. 按日期逐天生成排班:休息日删除原班次,工作日 upsert
|
int itemCount = 0;
|
for (LocalDate date = reqVO.getStartDate(); !date.isAfter(reqVO.getEndDate()); date = date.plusDays(1)) {
|
boolean rest = isRestDay(date, reqVO.getRestMode(), holidayTypeMap.get(date))
|
|| reqVO.getShiftId() == null;
|
for (HrmEmployeeDO employee : employees) {
|
HrmEmployeeScheduleDO existing = existingMap.get(scheduleKey(employee.getId(), date));
|
if (rest) {
|
if (existing != null) {
|
employeeScheduleMapper.deleteById(existing.getId());
|
itemCount++;
|
}
|
continue;
|
}
|
if (existing == null) {
|
HrmEmployeeScheduleDO schedule = new HrmEmployeeScheduleDO();
|
schedule.setEmployeeId(employee.getId());
|
schedule.setDate(date);
|
schedule.setShiftId(reqVO.getShiftId());
|
employeeScheduleMapper.insert(schedule);
|
itemCount++;
|
} else if (!Objects.equals(existing.getShiftId(), reqVO.getShiftId())) {
|
existing.setShiftId(reqVO.getShiftId());
|
employeeScheduleMapper.updateById(existing);
|
itemCount++;
|
}
|
}
|
}
|
|
HrmEmployeeScheduleBatchSaveRespVO respVO = new HrmEmployeeScheduleBatchSaveRespVO();
|
respVO.setEmployeeCount(employees.size());
|
respVO.setItemCount(itemCount);
|
return respVO;
|
}
|
|
private Set<Long> getSelfAndChildDeptIds(Long deptId) {
|
Set<Long> deptIds = new LinkedHashSet<>();
|
deptIds.add(deptId);
|
List<DeptRespDTO> childDepts = deptApi.getChildDeptList(deptId);
|
if (CollUtil.isNotEmpty(childDepts)) {
|
childDepts.forEach(dept -> deptIds.add(dept.getId()));
|
}
|
return deptIds;
|
}
|
|
/** employeeStatus 为空时包含在职和试用,排除离职 */
|
private boolean matchEmployeeStatus(HrmEmployeeDO employee, Integer employeeStatus) {
|
if (employeeStatus != null) {
|
return Objects.equals(employee.getEmployeeStatus(), employeeStatus);
|
}
|
return !Objects.equals(employee.getEmployeeStatus(), 3);
|
}
|
|
private boolean matchPost(HrmEmployeeDO employee, Long postId) {
|
if (postId == null) {
|
return true;
|
}
|
if (StrUtil.isBlank(employee.getPostIds())) {
|
return false;
|
}
|
return JSONUtil.toList(employee.getPostIds(), Long.class).contains(postId);
|
}
|
|
private boolean isRestDay(LocalDate date, String restMode, Integer holidayType) {
|
// 节假日配置优先:休息日强制休息,工作日强制上班(含调休上班的周末)
|
if (holidayType != null) {
|
return HrmAttendanceHolidayTypeEnum.REST_DAY.getType().equals(holidayType);
|
}
|
if ("double".equals(restMode)) {
|
return date.getDayOfWeek() == DayOfWeek.SATURDAY || date.getDayOfWeek() == DayOfWeek.SUNDAY;
|
}
|
if ("single".equals(restMode)) {
|
return date.getDayOfWeek() == DayOfWeek.SUNDAY;
|
}
|
return false;
|
}
|
|
private String scheduleKey(Long employeeId, LocalDate date) {
|
return employeeId + "_" + date;
|
}
|
|
}
|