From ce5f8a87bb5d411bae67c39b269d879677bb98da Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期日, 08 十一月 2026 19:29:25 +0800
Subject: [PATCH] feat(hrm): 请假导入、加班代申请、批量排班与考勤应出勤重算
---
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmEmployeeScheduleServiceImpl.java | 294 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
1 files changed, 281 insertions(+), 13 deletions(-)
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmEmployeeScheduleServiceImpl.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmEmployeeScheduleServiceImpl.java
index 36eb169..3164235 100644
--- a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmEmployeeScheduleServiceImpl.java
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmEmployeeScheduleServiceImpl.java
@@ -1,24 +1,48 @@
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.util.*;
+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;
@@ -32,7 +56,11 @@
*/
@Service
@Validated
+@Slf4j
public class HrmEmployeeScheduleServiceImpl implements HrmEmployeeScheduleService {
+
+ /** 鎵归噺鎺掔彮鍗曟鏈�澶氳鐩栫殑澶╂暟锛岄伩鍏嶄竴娆$敓鎴愯繃澶氳鎷栧灝璇锋眰 */
+ private static final int BATCH_MAX_DAYS = 62;
@Resource
private HrmEmployeeScheduleMapper employeeScheduleMapper;
@@ -40,6 +68,14 @@
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) {
@@ -120,24 +156,256 @@
throw exception(SCHEDULE_SHIFT_NOT_EXISTS);
}
}
- // 3. 閫愭潯 upsert锛氭湁鐝鍒欎繚瀛橈紝鏃犵彮娆″垯鍒犻櫎
+ // 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());
- }
+ clearShift(reqVO.getEmployeeId(), reqVO.getDate());
} else {
- if (existing == null) {
- employeeScheduleMapper.insert(BeanUtils.toBean(reqVO, HrmEmployeeScheduleDO.class));
- } else {
- existing.setShiftId(reqVO.getShiftId());
- existing.setRemark(reqVO.getRemark());
- employeeScheduleMapper.updateById(existing);
+ 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. 鏌ヨ宸插鎵归�氳繃鐨勮鍋囷紙鎸夌郴缁熺敤鎴稩D锛�
+ 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);
}
}
--
Gitblit v1.9.3