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.framework.security.core.util.SecurityFrameworkUtils;
|
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmEmployeeScheduleBatchAssignReqVO;
|
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmEmployeeScheduleBatchAssignRespVO;
|
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmEmployeeScheduleLeaveRespVO;
|
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.approval.HrmLeaveApplicationDO;
|
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.dal.mysql.approval.HrmLeaveApplicationMapper;
|
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.hrm.enums.HrmEmployeeStatusEnum;
|
import cn.iocoder.yudao.module.hrm.enums.HrmLeaveTypeEnum;
|
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
|
import cn.iocoder.yudao.module.system.api.dept.PostApi;
|
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
|
import cn.iocoder.yudao.module.system.api.dept.dto.PostRespDTO;
|
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.time.LocalDate;
|
import java.time.YearMonth;
|
import java.time.temporal.ChronoUnit;
|
import java.util.ArrayList;
|
import java.util.Collection;
|
import java.util.Collections;
|
import java.util.HashMap;
|
import java.util.HashSet;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.Objects;
|
import java.util.Set;
|
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
|
@Slf4j
|
public class HrmEmployeeScheduleServiceImpl implements HrmEmployeeScheduleService {
|
|
/** 批量排班单次最多覆盖的天数,避免一次生成过多行拖垮请求 */
|
private static final int BATCH_MAX_DAYS = 62;
|
|
@Resource
|
private HrmEmployeeScheduleMapper employeeScheduleMapper;
|
@Resource
|
private HrmEmployeeMapper employeeMapper;
|
@Resource
|
private HrmShiftMapper shiftMapper;
|
@Resource
|
private HrmLeaveApplicationMapper leaveApplicationMapper;
|
@Resource
|
private HrmAttendanceHolidayService attendanceHolidayService;
|
@Resource
|
private DeptApi deptApi;
|
@Resource
|
private PostApi postApi;
|
|
@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) {
|
List<HrmEmployeeDO> employees = employeeMapper.selectByDeptId(reqVO.getDeptId());
|
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) {
|
if (reqVO.getShiftId() == null) {
|
clearShift(reqVO.getEmployeeId(), reqVO.getDate());
|
} else {
|
upsertShift(reqVO.getEmployeeId(), reqVO.getDate(), reqVO.getShiftId(), reqVO.getRemark());
|
}
|
}
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public HrmEmployeeScheduleBatchAssignRespVO batchAssign(HrmEmployeeScheduleBatchAssignReqVO reqVO) {
|
// 1. 校验日期区间
|
LocalDate startDate = reqVO.getStartDate();
|
LocalDate endDate = reqVO.getEndDate();
|
if (endDate.isBefore(startDate)) {
|
throw exception(SCHEDULE_BATCH_DATE_RANGE_INVALID, endDate, startDate);
|
}
|
long days = ChronoUnit.DAYS.between(startDate, endDate) + 1;
|
if (days > BATCH_MAX_DAYS) {
|
throw exception(SCHEDULE_BATCH_RANGE_TOO_LONG, startDate, endDate, days);
|
}
|
// 2. 校验班次存在
|
HrmShiftDO shift = shiftMapper.selectById(reqVO.getShiftId());
|
if (shift == null) {
|
throw exception(SCHEDULE_SHIFT_NOT_EXISTS);
|
}
|
// 3. 筛选员工:部门(含下级)+ 岗位 + 员工状态
|
Set<Long> deptIds = new HashSet<>();
|
deptIds.add(reqVO.getDeptId());
|
List<DeptRespDTO> childDepts = deptApi.getChildDeptList(reqVO.getDeptId());
|
if (CollUtil.isNotEmpty(childDepts)) {
|
childDepts.forEach(dept -> deptIds.add(dept.getId()));
|
}
|
List<HrmEmployeeDO> employees = employeeMapper.selectListByDeptIds(deptIds).stream()
|
.filter(employee -> reqVO.getEmployeeStatus() == null
|
|| Objects.equals(reqVO.getEmployeeStatus(), employee.getEmployeeStatus()))
|
.filter(employee -> matchesPost(employee, reqVO.getPostId()))
|
.collect(Collectors.toList());
|
if (CollUtil.isEmpty(employees)) {
|
throw exception(SCHEDULE_BATCH_NO_EMPLOYEE,
|
resolveDeptName(reqVO.getDeptId()),
|
resolvePostName(reqVO.getPostId()),
|
resolveStatusName(reqVO.getEmployeeStatus()));
|
}
|
// 4. 预加载已审批请假,按“员工 + 日期”标记需要跳过的单元格
|
Set<String> leaveKeys = buildApprovedLeaveKeys(employees, startDate, endDate);
|
// 5. 按日期逐天排班:工作日写班次、休息日清空、已审批请假跳过
|
int scheduledCount = 0;
|
int skippedLeaveCount = 0;
|
int clearedRestDayCount = 0;
|
for (LocalDate date = startDate; !date.isAfter(endDate); date = date.plusDays(1)) {
|
boolean workDay = attendanceHolidayService.isWorkDay(date);
|
for (HrmEmployeeDO employee : employees) {
|
if (leaveKeys.contains(buildKey(employee.getId(), date))) {
|
skippedLeaveCount++;
|
continue;
|
}
|
if (workDay) {
|
upsertShift(employee.getId(), date, reqVO.getShiftId(), reqVO.getRemark());
|
scheduledCount++;
|
} else if (clearShift(employee.getId(), date)) {
|
clearedRestDayCount++;
|
}
|
}
|
}
|
log.info("[batchAssign][部门({}) 岗位({}) 状态({}) {}~{} 班次({}) 命中员工 {} 人,写入 {} 条,跳过请假 {} 条,清空休息日 {} 条]",
|
reqVO.getDeptId(), reqVO.getPostId(), reqVO.getEmployeeStatus(), startDate, endDate,
|
reqVO.getShiftId(), employees.size(), scheduledCount, skippedLeaveCount, clearedRestDayCount);
|
return HrmEmployeeScheduleBatchAssignRespVO.builder()
|
.matchedEmployeeCount(employees.size())
|
.scheduledCount(scheduledCount)
|
.skippedLeaveCount(skippedLeaveCount)
|
.clearedRestDayCount(clearedRestDayCount)
|
.build();
|
}
|
|
@Override
|
public List<HrmEmployeeScheduleLeaveRespVO> getScheduleLeaveList(HrmEmployeeScheduleListReqVO reqVO) {
|
YearMonth yearMonth = YearMonth.parse(reqVO.getYearMonth());
|
LocalDate startDate = yearMonth.atDay(1);
|
LocalDate endDate = yearMonth.atEndOfMonth();
|
// 1. 确定需要标记的员工
|
List<HrmEmployeeDO> employees;
|
if (reqVO.getEmployeeId() != null) {
|
HrmEmployeeDO employee = employeeMapper.selectById(reqVO.getEmployeeId());
|
employees = employee == null ? Collections.emptyList() : List.of(employee);
|
} else if (reqVO.getDeptId() != null) {
|
employees = employeeMapper.selectByDeptId(reqVO.getDeptId());
|
} else {
|
employees = employeeMapper.selectList();
|
}
|
if (CollUtil.isEmpty(employees)) {
|
return Collections.emptyList();
|
}
|
// 2. 查询已审批通过的请假(按系统用户ID)
|
Map<Long, List<HrmEmployeeDO>> employeeByUserId = employees.stream()
|
.filter(employee -> employee.getUserId() != null)
|
.collect(Collectors.groupingBy(HrmEmployeeDO::getUserId));
|
if (employeeByUserId.isEmpty()) {
|
return Collections.emptyList();
|
}
|
List<HrmLeaveApplicationDO> leaves = leaveApplicationMapper.selectApprovedListByUserIdsAndTimeRange(
|
employeeByUserId.keySet(), startDate.atStartOfDay(), endDate.atTime(23, 59, 59));
|
if (CollUtil.isEmpty(leaves)) {
|
return Collections.emptyList();
|
}
|
// 3. 按天展开为排班页标记
|
List<HrmEmployeeScheduleLeaveRespVO> result = new ArrayList<>();
|
for (HrmLeaveApplicationDO leave : leaves) {
|
List<HrmEmployeeDO> matchedEmployees = employeeByUserId.get(leave.getUserId());
|
if (CollUtil.isEmpty(matchedEmployees)) {
|
continue;
|
}
|
LocalDate from = leave.getStartTime().toLocalDate().isBefore(startDate)
|
? startDate : leave.getStartTime().toLocalDate();
|
LocalDate to = leave.getEndTime().toLocalDate().isAfter(endDate)
|
? endDate : leave.getEndTime().toLocalDate();
|
String leaveTypeName = HrmLeaveTypeEnum.getNameByStatus(leave.getLeaveType());
|
for (LocalDate date = from; !date.isAfter(to); date = date.plusDays(1)) {
|
for (HrmEmployeeDO employee : matchedEmployees) {
|
result.add(new HrmEmployeeScheduleLeaveRespVO(employee.getId(), employee.getName(), date,
|
leave.getLeaveType(), leaveTypeName));
|
}
|
}
|
}
|
result.sort((a, b) -> {
|
int compare = a.getDate().compareTo(b.getDate());
|
return compare != 0 ? compare : a.getEmployeeId().compareTo(b.getEmployeeId());
|
});
|
return result;
|
}
|
|
/**
|
* 写入/覆盖班次;若该员工该日期存在逻辑删除的旧行,则先复活,规避唯一键冲突。
|
*/
|
private void upsertShift(Long employeeId, LocalDate date, Long shiftId, String remark) {
|
HrmEmployeeScheduleDO existing = employeeScheduleMapper.selectByEmployeeIdAndDateIncludeDeleted(employeeId, date);
|
if (existing == null) {
|
HrmEmployeeScheduleDO insert = new HrmEmployeeScheduleDO();
|
insert.setEmployeeId(employeeId);
|
insert.setDate(date);
|
insert.setShiftId(shiftId);
|
insert.setRemark(remark);
|
employeeScheduleMapper.insert(insert);
|
return;
|
}
|
if (Boolean.TRUE.equals(existing.getDeleted())) {
|
employeeScheduleMapper.reviveById(existing.getId(), shiftId, remark, currentUpdater());
|
return;
|
}
|
existing.setShiftId(shiftId);
|
existing.setRemark(remark);
|
employeeScheduleMapper.updateById(existing);
|
}
|
|
/**
|
* 清空班次(逻辑删除);返回本次是否真的清掉了一条非空班次。
|
*/
|
private boolean clearShift(Long employeeId, LocalDate date) {
|
HrmEmployeeScheduleDO existing = employeeScheduleMapper.selectByEmployeeIdAndDateIncludeDeleted(employeeId, date);
|
if (existing == null || Boolean.TRUE.equals(existing.getDeleted()) || existing.getShiftId() == null) {
|
return false;
|
}
|
employeeScheduleMapper.deleteById(existing.getId());
|
return true;
|
}
|
|
/**
|
* 构造“员工 + 日期”的已审批请假集合
|
*/
|
private Set<String> buildApprovedLeaveKeys(List<HrmEmployeeDO> employees, LocalDate startDate, LocalDate endDate) {
|
Map<Long, List<HrmEmployeeDO>> employeeByUserId = employees.stream()
|
.filter(employee -> employee.getUserId() != null)
|
.collect(Collectors.groupingBy(HrmEmployeeDO::getUserId));
|
if (employeeByUserId.isEmpty()) {
|
return Collections.emptySet();
|
}
|
List<HrmLeaveApplicationDO> leaves = leaveApplicationMapper.selectApprovedListByUserIdsAndTimeRange(
|
employeeByUserId.keySet(), startDate.atStartOfDay(), endDate.atTime(23, 59, 59));
|
if (CollUtil.isEmpty(leaves)) {
|
return Collections.emptySet();
|
}
|
Set<String> keys = new HashSet<>();
|
for (HrmLeaveApplicationDO leave : leaves) {
|
List<HrmEmployeeDO> matchedEmployees = employeeByUserId.get(leave.getUserId());
|
if (CollUtil.isEmpty(matchedEmployees)) {
|
continue;
|
}
|
LocalDate from = leave.getStartTime().toLocalDate().isBefore(startDate)
|
? startDate : leave.getStartTime().toLocalDate();
|
LocalDate to = leave.getEndTime().toLocalDate().isAfter(endDate)
|
? endDate : leave.getEndTime().toLocalDate();
|
for (LocalDate date = from; !date.isAfter(to); date = date.plusDays(1)) {
|
for (HrmEmployeeDO employee : matchedEmployees) {
|
keys.add(buildKey(employee.getId(), date));
|
}
|
}
|
}
|
return keys;
|
}
|
|
private static String buildKey(Long employeeId, LocalDate date) {
|
return employeeId + "_" + date;
|
}
|
|
private boolean matchesPost(HrmEmployeeDO employee, Long postId) {
|
if (postId == null) {
|
return true;
|
}
|
if (StrUtil.isBlank(employee.getPostIds())) {
|
return false;
|
}
|
try {
|
return JSONUtil.toList(employee.getPostIds(), Long.class).contains(postId);
|
} catch (Exception e) {
|
log.warn("[matchesPost][员工({}) 的 postIds({}) 不是合法 JSON,跳过岗位匹配]", employee.getId(), employee.getPostIds());
|
return false;
|
}
|
}
|
|
private String resolveDeptName(Long deptId) {
|
DeptRespDTO dept = deptId == null ? null : deptApi.getDept(deptId);
|
return dept == null ? "ID=" + deptId : dept.getName();
|
}
|
|
private String resolvePostName(Long postId) {
|
if (postId == null) {
|
return "不限";
|
}
|
List<PostRespDTO> posts = postApi.getPostList(Collections.singleton(postId));
|
return CollUtil.isEmpty(posts) ? "ID=" + postId : posts.get(0).getName();
|
}
|
|
private String resolveStatusName(Integer employeeStatus) {
|
if (employeeStatus == null) {
|
return "不限";
|
}
|
for (HrmEmployeeStatusEnum status : HrmEmployeeStatusEnum.values()) {
|
if (Objects.equals(status.getStatus(), employeeStatus)) {
|
return status.getName();
|
}
|
}
|
return "ID=" + employeeStatus;
|
}
|
|
private static String currentUpdater() {
|
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
return userId == null ? "" : String.valueOf(userId);
|
}
|
|
}
|