gaoluyang
116 分钟以前 ef3cbc1c6882786e8218b08be3e52ea44587f03b
feat(hrm): 请假批量导入、加班代申请人、批量排班与节假日区间联动

- 请假申请支持 Excel 批量导入,按手机号匹配员工档案,逐行独立提示失败原因
- 加班申请改为选择申请人,自动带出工号/部门/岗位
- 排班管理新增按部门+岗位+在职状态批量排班
- 节假日配置按区间新增/编辑/删除,列表按连续日期合并展示
- 排班批量生成与自动排班以节假日为准:休息日强制休息、工作日强制排班
- 考勤统计查询时按最新节假日重算应出勤天数并回写

Co-Authored-By: Claude Code <noreply@anthropic.com>
已添加8个文件
已修改21个文件
995 ■■■■■ 文件已修改
yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/ErrorCodeConstants.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/HrmLeaveApplicationController.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/HrmOvertimeApplicationController.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmLeaveApplicationImportExcelVO.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmLeaveApplicationImportReqVO.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmLeaveApplicationImportRespVO.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmOvertimeApplicationSaveReqVO.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceHolidayController.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmEmployeeScheduleController.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceHolidayGroupRespVO.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceHolidayPageReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceHolidayRangeCreateReqVO.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceHolidayRangeUpdateReqVO.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmEmployeeScheduleBatchSaveReqVO.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmEmployeeScheduleBatchSaveRespVO.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeSimpleRespVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/attendance/HrmAttendanceHolidayMapper.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/attendance/HrmAttendanceSummaryMapper.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/employee/HrmEmployeeMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmLeaveApplicationService.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmLeaveApplicationServiceImpl.java 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmOvertimeApplicationService.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmOvertimeApplicationServiceImpl.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceHolidayService.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceHolidayServiceImpl.java 148 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceSummaryServiceImpl.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmEmployeeScheduleService.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmEmployeeScheduleServiceImpl.java 134 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeServiceImpl.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/ErrorCodeConstants.java
@@ -40,6 +40,12 @@
    ErrorCode LEAVE_APPLICATION_NO_EXISTS = new ErrorCode(1_030_001_004, "生成请假申请单号重复,请重试");
    ErrorCode LEAVE_APPLICATION_DURATION_EXCEED = new ErrorCode(1_030_001_005, "请假时长超出年假余额");
    ErrorCode LEAVE_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS = new ErrorCode(1_030_001_006, "请假审批流程不存在");
    ErrorCode LEAVE_APPLICATION_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_030_001_007, "导入请假数据不能为空");
    ErrorCode LEAVE_APPLICATION_IMPORT_FIELD_REQUIRED = new ErrorCode(1_030_001_008, "导入字段【{}】不能为空");
    ErrorCode LEAVE_APPLICATION_IMPORT_EMPLOYEE_NOT_EXISTS = new ErrorCode(1_030_001_009, "申请人手机号【{}】未找到员工档案,请先维护员工档案");
    ErrorCode LEAVE_APPLICATION_IMPORT_EMPLOYEE_NO_USER = new ErrorCode(1_030_001_010, "员工【{}】未关联系统账号,无法创建请假申请,请先在「系统管理-用户管理」中绑定账号");
    ErrorCode LEAVE_APPLICATION_IMPORT_TIME_INVALID = new ErrorCode(1_030_001_011, "开始时间【{}】必须早于结束时间【{}】");
    ErrorCode LEAVE_APPLICATION_IMPORT_FIELD_INVALID = new ErrorCode(1_030_001_012, "导入字段【{}】的值【{}】不合法,请填写:{}");
    // ========== è°ƒå²—申请 1-030-002-000 ==========
    ErrorCode TRANSFER_APPLICATION_NOT_EXISTS = new ErrorCode(1_030_002_000, "调岗申请不存在");
@@ -71,6 +77,8 @@
    ErrorCode OVERTIME_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS = new ErrorCode(1_030_008_004, "加班审批流程不存在,请先在协同办公中配置并发布流程");
    ErrorCode OVERTIME_APPLICATION_EMPLOYEE_NOT_EXISTS = new ErrorCode(1_030_008_005, "当前账号未关联员工档案");
    ErrorCode OVERTIME_APPLICATION_TIME_INVALID = new ErrorCode(1_030_008_006, "加班时段或扣除时长不合法");
    ErrorCode OVERTIME_APPLICATION_SELECTED_EMPLOYEE_NOT_EXISTS = new ErrorCode(1_030_008_007, "所选加班人员不存在或已被删除,请重新选择");
    ErrorCode OVERTIME_APPLICATION_SELECTED_EMPLOYEE_NO_USER = new ErrorCode(1_030_008_008, "所选员工【{}】未关联系统账号,无法提交加班申请,请先在「系统管理-用户管理」中绑定账号");
    // ========== ç”¨æˆ·æ•°æ®äº¤æŽ¥ 1-030-007-000 ==========
    ErrorCode USER_HANDOVER_NOT_EXISTS = new ErrorCode(1_030_007_000, "用户交接记录不存在");
@@ -89,6 +97,10 @@
    ErrorCode ATTENDANCE_RULE_DEPT_EXISTS = new ErrorCode(1_030_004_007, "该部门已配置考勤规则");
    ErrorCode ATTENDANCE_HOLIDAY_NOT_EXISTS = new ErrorCode(1_030_004_008, "节假日配置不存在");
    ErrorCode ATTENDANCE_HOLIDAY_DATE_EXISTS = new ErrorCode(1_030_004_009, "该日期节假日配置已存在");
    ErrorCode ATTENDANCE_HOLIDAY_DATE_RANGE_INVALID = new ErrorCode(1_030_004_012, "节假日结束日期不能早于开始日期");
    ErrorCode ATTENDANCE_HOLIDAY_RANGE_DATE_EXISTS = new ErrorCode(1_030_004_013, "日期【{}】已存在节假日配置【{}】,本次未保存,请先删除该日期配置或改为编辑");
    ErrorCode ATTENDANCE_HOLIDAY_RANGE_IDS_EMPTY = new ErrorCode(1_030_004_014, "请选择要编辑或删除的节假日区间");
    ErrorCode ATTENDANCE_HOLIDAY_RANGE_NOT_EXISTS = new ErrorCode(1_030_004_015, "部分节假日配置已不存在(可能已被删除),请刷新列表后重新操作");
    ErrorCode ATTENDANCE_CLOCK_FAIL_NO_RULE = new ErrorCode(1_030_004_010, "打卡失败,未找到考勤规则");
    ErrorCode ATTENDANCE_CLOCK_FAIL_HOLIDAY = new ErrorCode(1_030_004_011, "打卡失败,当日为休息日");
    ErrorCode ATTENDANCE_CLOCK_FAIL_NOT_WORKDAY = new ErrorCode(1_030_004_012, "打卡失败,当日不是工作日");
@@ -138,5 +150,7 @@
    ErrorCode SCHEDULE_ITEM_LIST_EMPTY = new ErrorCode(1_030_010_000, "排班数据不能为空");
    ErrorCode SCHEDULE_SHIFT_NOT_EXISTS = new ErrorCode(1_030_010_001, "排班引用的班次不存在");
    ErrorCode SCHEDULE_EMPLOYEE_NOT_EXISTS = new ErrorCode(1_030_010_002, "排班引用的员工不存在");
    ErrorCode SCHEDULE_BATCH_DATE_RANGE_INVALID = new ErrorCode(1_030_010_003, "批量排班的结束日期不能早于开始日期");
    ErrorCode SCHEDULE_BATCH_EMPLOYEE_EMPTY = new ErrorCode(1_030_010_004, "所选部门【{}】下没有符合条件的员工,请检查部门、岗位和员工状态");
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/HrmLeaveApplicationController.java
@@ -6,6 +6,9 @@
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmLeaveApplicationImportExcelVO;
import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmLeaveApplicationImportReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmLeaveApplicationImportRespVO;
import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmLeaveApplicationPageReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmLeaveApplicationRespVO;
import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmLeaveApplicationSaveReqVO;
@@ -21,6 +24,8 @@
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
@@ -94,6 +99,35 @@
        return success(leaveApplicationService.getApproveProcessDefinitionList());
    }
    @GetMapping("/get-import-template")
    @Operation(summary = "下载请假申请导入模板")
    @PreAuthorize("@ss.hasPermission('hrm:leave:import')")
    public void getImportTemplate(HttpServletResponse response) throws IOException {
        List<HrmLeaveApplicationImportExcelVO> list = List.of(
                HrmLeaveApplicationImportExcelVO.builder()
                        .phone("13800138000")
                        .leaveType("年假")
                        .startTime(LocalDateTime.of(2026, 10, 8, 9, 0, 0))
                        .endTime(LocalDateTime.of(2026, 10, 8, 18, 0, 0))
                        .duration(new BigDecimal("1"))
                        .reason("家中有事")
                        .remark("示例行,导入前请删除")
                        .build()
        );
        ExcelUtils.write(response, "请假申请导入模板.xls", "请假申请列表",
                HrmLeaveApplicationImportExcelVO.class, list);
    }
    @PostMapping("/import")
    @Operation(summary = "批量导入请假申请")
    @PreAuthorize("@ss.hasPermission('hrm:leave:import')")
    public CommonResult<HrmLeaveApplicationImportRespVO> importLeaveApplication(
            @Valid HrmLeaveApplicationImportReqVO importReqVO) throws IOException {
        List<HrmLeaveApplicationImportExcelVO> list = ExcelUtils.read(
                importReqVO.getFile(), HrmLeaveApplicationImportExcelVO.class);
        return success(leaveApplicationService.importLeaveApplicationList(list));
    }
    @GetMapping("/export-excel")
    @Operation(summary = "导出请假申请 Excel")
    @PreAuthorize("@ss.hasPermission('hrm:leave:export')")
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/HrmOvertimeApplicationController.java
@@ -33,11 +33,11 @@
    }
    @PutMapping("/update") @PreAuthorize("@ss.hasPermission('hrm:overtime:update')")
    public CommonResult<Boolean> update(@Valid @RequestBody HrmOvertimeApplicationSaveReqVO reqVO) {
        service.update(reqVO, SecurityFrameworkUtils.getLoginUserId()); return success(true);
        service.update(reqVO, SecurityFrameworkUtils.getLoginUserId(), canManage()); return success(true);
    }
    @DeleteMapping("/delete") @PreAuthorize("@ss.hasPermission('hrm:overtime:delete')")
    public CommonResult<Boolean> delete(@RequestParam Long id) {
        service.delete(id, SecurityFrameworkUtils.getLoginUserId()); return success(true);
        service.delete(id, SecurityFrameworkUtils.getLoginUserId(), canManage()); return success(true);
    }
    @GetMapping("/get") @PreAuthorize("@ss.hasAnyPermissions('hrm:overtime:query', 'bpm:task:query')")
    public CommonResult<HrmOvertimeApplicationRespVO> get(@RequestParam Long id) {
@@ -56,11 +56,11 @@
    }
    @PostMapping("/submit") @PreAuthorize("@ss.hasPermission('hrm:overtime:submit')")
    public CommonResult<Boolean> submit(@RequestParam Long id, @RequestParam String processDefinitionKey) {
        service.submit(id, processDefinitionKey, SecurityFrameworkUtils.getLoginUserId()); return success(true);
        service.submit(id, processDefinitionKey, SecurityFrameworkUtils.getLoginUserId(), canManage()); return success(true);
    }
    @PostMapping("/cancel") @PreAuthorize("@ss.hasPermission('hrm:overtime:cancel')")
    public CommonResult<Boolean> cancel(@RequestParam Long id) {
        service.cancel(id, SecurityFrameworkUtils.getLoginUserId()); return success(true);
        service.cancel(id, SecurityFrameworkUtils.getLoginUserId(), canManage()); return success(true);
    }
    @GetMapping("/approve-process-definition-list") @PreAuthorize("@ss.hasPermission('hrm:overtime:query')")
    public CommonResult<List<Map<String, Object>>> processes() { return success(service.getApproveProcessDefinitionList()); }
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmLeaveApplicationImportExcelVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,45 @@
package cn.iocoder.yudao.module.hrm.controller.admin.approval.vo;
import cn.idev.excel.annotation.ExcelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * è¯·å‡ç”³è¯· Excel å¯¼å…¥ VO
 *
 * è¯·å‡ç±»åž‹æŒ‰åç§°å¡«å†™ï¼ˆå¦‚「年假」「事假」),也兼容填写字典值(1~6),
 * è¿™æ ·å¡«å†™é”™è¯¯æ—¶èƒ½ç»™å‡ºã€Œä½ å¡«çš„值 + å¯å¡«å€¼æ¸…单」的精确提示。
 */
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class HrmLeaveApplicationImportExcelVO {
    @ExcelProperty("申请人手机号")
    private String phone;
    @ExcelProperty("请假类型")
    private String leaveType;
    @ExcelProperty("开始时间")
    private LocalDateTime startTime;
    @ExcelProperty("结束时间")
    private LocalDateTime endTime;
    @ExcelProperty("请假时长(天)")
    private BigDecimal duration;
    @ExcelProperty("请假原因")
    private String reason;
    @ExcelProperty("备注")
    private String remark;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmLeaveApplicationImportReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
package cn.iocoder.yudao.module.hrm.controller.admin.approval.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.springframework.web.multipart.MultipartFile;
@Schema(description = "管理后台 - è¯·å‡ç”³è¯·å¯¼å…¥ Request VO")
@Data
public class HrmLeaveApplicationImportReqVO {
    @Schema(description = "Excel æ–‡ä»¶", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "Excel æ–‡ä»¶ä¸èƒ½ä¸ºç©º")
    private MultipartFile file;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmLeaveApplicationImportRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
package cn.iocoder.yudao.module.hrm.controller.admin.approval.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
import java.util.List;
import java.util.Map;
@Schema(description = "管理后台 - è¯·å‡ç”³è¯·å¯¼å…¥ Response VO")
@Data
@Builder
public class HrmLeaveApplicationImportRespVO {
    @Schema(description = "创建成功的申请人及日期数组", requiredMode = Schema.RequiredMode.REQUIRED)
    private List<String> createNames;
    @Schema(description = "导入失败的集合,key ä¸ºè¡Œå·ï¼Œvalue ä¸ºå¤±è´¥åŽŸå› ", requiredMode = Schema.RequiredMode.REQUIRED)
    private Map<String, String> failureNames;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmOvertimeApplicationSaveReqVO.java
@@ -14,6 +14,8 @@
@Data
public class HrmOvertimeApplicationSaveReqVO {
    private Long id;
    @Schema(description = "加班人员ID(选择申请人为空时按当前登录账号关联的员工)", example = "66")
    private Long employeeId;
    @NotNull(message = "加班日期不能为空")
    private LocalDate overtimeDate;
    @NotNull(message = "开始时间不能为空")
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceHolidayController.java
@@ -6,7 +6,10 @@
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceHolidayGroupRespVO;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceHolidayPageReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceHolidayRangeCreateReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceHolidayRangeUpdateReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceHolidayRespVO;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceHolidaySaveReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceHolidayDO;
@@ -17,11 +20,13 @@
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.time.LocalDate;
import java.util.List;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
@@ -43,11 +48,28 @@
        return success(attendanceHolidayService.createAttendanceHoliday(createReqVO));
    }
    @PostMapping("/create-range")
    @Operation(summary = "按日期范围新增节假日配置(含中间每一天)")
    @PreAuthorize("@ss.hasPermission('hrm:attendance:create')")
    public CommonResult<Integer> createAttendanceHolidayRange(
            @Valid @RequestBody HrmAttendanceHolidayRangeCreateReqVO createReqVO) {
        return success(attendanceHolidayService.createAttendanceHolidayRange(createReqVO));
    }
    @PutMapping("/update")
    @Operation(summary = "更新节假日配置")
    @PreAuthorize("@ss.hasPermission('hrm:attendance:update')")
    public CommonResult<Boolean> updateAttendanceHoliday(@Valid @RequestBody HrmAttendanceHolidaySaveReqVO updateReqVO) {
        attendanceHolidayService.updateAttendanceHoliday(updateReqVO);
        return success(true);
    }
    @PutMapping("/update-range")
    @Operation(summary = "按日期区间更新节假日配置(删除原区间记录后按新起止日期重写)")
    @PreAuthorize("@ss.hasPermission('hrm:attendance:update')")
    public CommonResult<Boolean> updateAttendanceHolidayRange(
            @Valid @RequestBody HrmAttendanceHolidayRangeUpdateReqVO updateReqVO) {
        attendanceHolidayService.updateAttendanceHolidayRange(updateReqVO);
        return success(true);
    }
@@ -57,6 +79,15 @@
    @PreAuthorize("@ss.hasPermission('hrm:attendance:delete')")
    public CommonResult<Boolean> deleteAttendanceHoliday(@RequestParam("id") Long id) {
        attendanceHolidayService.deleteAttendanceHoliday(id);
        return success(true);
    }
    @DeleteMapping("/delete-range")
    @Operation(summary = "按日期区间删除节假日配置(删除区间内全部日期)")
    @Parameter(name = "ids", description = "区间内节假日ID列表", required = true)
    @PreAuthorize("@ss.hasPermission('hrm:attendance:delete')")
    public CommonResult<Boolean> deleteAttendanceHolidayRange(@RequestParam("ids") List<Long> ids) {
        attendanceHolidayService.deleteAttendanceHolidayRange(ids);
        return success(true);
    }
@@ -77,6 +108,26 @@
        return success(BeanUtils.toBean(pageResult, HrmAttendanceHolidayRespVO.class));
    }
    @GetMapping("/page-grouped")
    @Operation(summary = "获得节假日配置分页(按连续日期区间合并展示)")
    @PreAuthorize("@ss.hasPermission('hrm:attendance:query')")
    public CommonResult<PageResult<HrmAttendanceHolidayGroupRespVO>> getAttendanceHolidayGroupPage(
            @Valid HrmAttendanceHolidayPageReqVO pageReqVO) {
        return success(attendanceHolidayService.getAttendanceHolidayGroupPage(pageReqVO));
    }
    @GetMapping("/list")
    @Operation(summary = "获得指定日期范围的节假日列表(逐日,排班日历使用)")
    @Parameter(name = "startDate", description = "开始日期", required = true)
    @Parameter(name = "endDate", description = "结束日期", required = true)
    @PreAuthorize("@ss.hasPermission('hrm:attendance:query')")
    public CommonResult<List<HrmAttendanceHolidayRespVO>> getAttendanceHolidayList(
            @RequestParam("startDate") @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate startDate,
            @RequestParam("endDate") @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate endDate) {
        List<HrmAttendanceHolidayDO> list = attendanceHolidayService.getAttendanceHolidayListByDateRange(startDate, endDate);
        return success(BeanUtils.toBean(list, HrmAttendanceHolidayRespVO.class));
    }
    @GetMapping("/export-excel")
    @Operation(summary = "导出节假日配置 Excel")
    @PreAuthorize("@ss.hasPermission('hrm:attendance:export')")
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmEmployeeScheduleController.java
@@ -1,6 +1,8 @@
package cn.iocoder.yudao.module.hrm.controller.admin.attendance;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
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;
@@ -43,4 +45,12 @@
        return success(true);
    }
    @PostMapping("/batch-save")
    @Operation(summary = "按部门+岗位批量排班(一类人一次排完)")
    @PreAuthorize("@ss.hasPermission('hrm:employee-schedule:save')")
    public CommonResult<HrmEmployeeScheduleBatchSaveRespVO> batchSaveEmployeeSchedule(
            @Valid @RequestBody HrmEmployeeScheduleBatchSaveReqVO reqVO) {
        return success(employeeScheduleService.batchSaveSchedule(reqVO));
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceHolidayGroupRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,50 @@
package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
/**
 * ç®¡ç†åŽå° - è€ƒå‹¤èŠ‚å‡æ—¥æŒ‰åŒºé—´åˆ†ç»„åŽçš„ Response VO
 *
 * åº“里按自然日存储,列表按「连续日期 + åç§°/类型/是否法定/备注一致」合并成一行展示,
 * ä¾‹å¦‚ 2026-10-01 ~ 2026-10-03 çš„国庆节在列表中只占一行。
 */
@Schema(description = "管理后台 - è€ƒå‹¤èŠ‚å‡æ—¥åˆ†ç»„ Response VO")
@Data
public class HrmAttendanceHolidayGroupRespVO {
    @Schema(description = "起始日期对应的记录ID(行 key)", example = "1")
    private Long id;
    @Schema(description = "区间内全部记录ID,用于区间编辑/删除")
    private List<Long> ids;
    @Schema(description = "节假日名称", example = "国庆节")
    private String name;
    @Schema(description = "开始日期", example = "2026-10-01")
    private LocalDate date;
    @Schema(description = "结束日期", example = "2026-10-03")
    private LocalDate endDate;
    @Schema(description = "区间天数", example = "3")
    private Integer dayCount;
    @Schema(description = "类型:1-休息日,2-工作日", example = "1")
    private Integer type;
    @Schema(description = "是否法定节假日:0-否,1-是", example = "1")
    private Integer isLegal;
    @Schema(description = "备注", example = "国庆节法定假期")
    private String remark;
    @Schema(description = "创建时间")
    private LocalDateTime createTime;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceHolidayPageReqVO.java
@@ -20,6 +20,9 @@
    @Schema(description = "类型:1-休息日,2-工作日", example = "1")
    private Integer type;
    @Schema(description = "是否法定节假日:0-否,1-是", example = "1")
    private Integer isLegal;
    @Schema(description = "开始日期", example = "2024-01-01")
    private LocalDate startDate;
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceHolidayRangeCreateReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,41 @@
package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.time.LocalDate;
/**
 * ç®¡ç†åŽå° - æŒ‰æ—¥æœŸèŒƒå›´æ–°å¢žèŠ‚å‡æ—¥é…ç½® Request VO
 *
 * å‰ç«¯é€‰æ‹©ã€Œ10月1日 ~ 10月3日」时,后端一次性写入 3 å¤©ï¼Œé¿å…åªå†™èµ·æ­¢ä¸¤å¤©ã€‚
 */
@Schema(description = "管理后台 - æŒ‰æ—¥æœŸèŒƒå›´æ–°å¢žèŠ‚å‡æ—¥é…ç½® Request VO")
@Data
public class HrmAttendanceHolidayRangeCreateReqVO {
    @Schema(description = "节假日名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "国庆节")
    @NotBlank(message = "节假日名称不能为空")
    private String name;
    @Schema(description = "开始日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-10-01")
    @NotNull(message = "开始日期不能为空")
    private LocalDate startDate;
    @Schema(description = "结束日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-10-03")
    @NotNull(message = "结束日期不能为空")
    private LocalDate endDate;
    @Schema(description = "类型:1-休息日,2-工作日", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "类型不能为空")
    private Integer type;
    @Schema(description = "是否法定节假日:false-否,true-是", example = "true")
    private Boolean isLegal;
    @Schema(description = "备注", example = "国庆节法定假期")
    private String remark;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceHolidayRangeUpdateReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,48 @@
package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.time.LocalDate;
import java.util.List;
/**
 * ç®¡ç†åŽå° - æŒ‰æ—¥æœŸåŒºé—´æ›´æ–°èŠ‚å‡æ—¥é…ç½® Request VO
 *
 * åˆ—表按连续区间合并展示,编辑时把该区间内的全部记录 ID ä¸€èµ·æäº¤ï¼Œ
 * åŽç«¯å…ˆåˆ é™¤æ—§è®°å½•再按新的起止日期重新写入,避免只改到某一天导致区间断裂。
 */
@Schema(description = "管理后台 - æŒ‰æ—¥æœŸåŒºé—´æ›´æ–°èŠ‚å‡æ—¥é…ç½® Request VO")
@Data
public class HrmAttendanceHolidayRangeUpdateReqVO {
    @Schema(description = "区间内原有记录ID列表", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotEmpty(message = "区间记录ID不能为空")
    private List<Long> ids;
    @Schema(description = "节假日名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "国庆节")
    @NotBlank(message = "节假日名称不能为空")
    private String name;
    @Schema(description = "开始日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-10-01")
    @NotNull(message = "开始日期不能为空")
    private LocalDate startDate;
    @Schema(description = "结束日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-10-03")
    @NotNull(message = "结束日期不能为空")
    private LocalDate endDate;
    @Schema(description = "类型:1-休息日,2-工作日", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "类型不能为空")
    private Integer type;
    @Schema(description = "是否法定节假日:false-否,true-是", example = "true")
    private Boolean isLegal;
    @Schema(description = "备注", example = "国庆节法定假期")
    private String remark;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmEmployeeScheduleBatchSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,43 @@
package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.time.LocalDate;
/**
 * ç®¡ç†åŽå° - å‘˜å·¥æŽ’班按群体批量保存 Request VO
 *
 * ä¸€æ¬¡ä¸ºã€Œéƒ¨é—¨ + å²—位」筛选出的一类员工,按日期范围统一排班。
 */
@Schema(description = "管理后台 - å‘˜å·¥æŽ’班按群体批量保存 Request VO")
@Data
public class HrmEmployeeScheduleBatchSaveReqVO {
    @Schema(description = "部门ID(含下级部门)", requiredMode = Schema.RequiredMode.REQUIRED, example = "119")
    @NotNull(message = "部门不能为空")
    private Long deptId;
    @Schema(description = "岗位ID,为空表示不限岗位", example = "48")
    private Long postId;
    @Schema(description = "员工状态:1-在职,2-试用,3-离职;为空表示在职及试用", example = "1")
    private Integer employeeStatus;
    @Schema(description = "排班开始日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-10-01")
    @NotNull(message = "排班开始日期不能为空")
    private LocalDate startDate;
    @Schema(description = "排班结束日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-10-31")
    @NotNull(message = "排班结束日期不能为空")
    private LocalDate endDate;
    @Schema(description = "班次ID,为空表示休息(清除排班)", example = "1")
    private Long shiftId;
    @Schema(description = "休息模式:none-不休息,single-周日休息,double-周六日休息;为空按不休息处理",
            example = "double")
    private String restMode;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmEmployeeScheduleBatchSaveRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - å‘˜å·¥æŽ’班按群体批量保存 Response VO")
@Data
public class HrmEmployeeScheduleBatchSaveRespVO {
    @Schema(description = "本次排班覆盖的员工数量", example = "50")
    private Integer employeeCount;
    @Schema(description = "本次排班写入/清除的排班条目数量", example = "1550")
    private Integer itemCount;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeSimpleRespVO.java
@@ -19,6 +19,9 @@
    @Schema(description = "部门名称")
    private String deptName;
    @Schema(description = "岗位名称(多个用逗号分隔)")
    private String postName;
    @Schema(description = "员工状态")
    private Integer employeeStatus;
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/attendance/HrmAttendanceHolidayMapper.java
@@ -5,7 +5,9 @@
import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceHolidayDO;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceHolidayPageReqVO;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.time.LocalDate;
import java.util.List;
@@ -22,8 +24,21 @@
        return selectPage(reqVO, new LambdaQueryWrapperX<HrmAttendanceHolidayDO>()
                .likeIfPresent(HrmAttendanceHolidayDO::getName, reqVO.getName())
                .eqIfPresent(HrmAttendanceHolidayDO::getType, reqVO.getType())
                .eqIfPresent(HrmAttendanceHolidayDO::getIsLegal, reqVO.getIsLegal())
                .betweenIfPresent(HrmAttendanceHolidayDO::getDate, reqVO.getStartDate(), reqVO.getEndDate())
                .orderByDesc(HrmAttendanceHolidayDO::getDate));
    }
    /**
     * æŒ‰æŸ¥è¯¢æ¡ä»¶å–全部节假日(升序),用于列表按连续区间合并展示
     */
    default List<HrmAttendanceHolidayDO> selectListByCondition(HrmAttendanceHolidayPageReqVO reqVO) {
        return selectList(new LambdaQueryWrapperX<HrmAttendanceHolidayDO>()
                .likeIfPresent(HrmAttendanceHolidayDO::getName, reqVO.getName())
                .eqIfPresent(HrmAttendanceHolidayDO::getType, reqVO.getType())
                .eqIfPresent(HrmAttendanceHolidayDO::getIsLegal, reqVO.getIsLegal())
                .betweenIfPresent(HrmAttendanceHolidayDO::getDate, reqVO.getStartDate(), reqVO.getEndDate())
                .orderByAsc(HrmAttendanceHolidayDO::getDate));
    }
    default HrmAttendanceHolidayDO selectByDate(LocalDate date) {
@@ -37,4 +52,14 @@
                .orderByAsc(HrmAttendanceHolidayDO::getDate));
    }
    /**
     * ç‰©ç†åˆ é™¤èŠ‚å‡æ—¥é…ç½®
     *
     * å”¯ä¸€ç´¢å¼•为 (date, deleted),逻辑删除两次同一天会产生 deleted=1 çš„主键冲突,
     * å› æ­¤èŠ‚å‡æ—¥é…ç½®çš„åˆ é™¤ç»Ÿä¸€èµ°ç‰©ç†åˆ é™¤ã€‚
     */
    @Delete("<script>DELETE FROM hrm_attendance_holiday WHERE id IN " +
            "<foreach collection='ids' item='id' open='(' separator=',' close=')'>#{id}</foreach></script>")
    int physicalDeleteByIds(@Param("ids") List<Long> ids);
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/attendance/HrmAttendanceSummaryMapper.java
@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.hrm.dal.mysql.attendance;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceSummaryDO;
@@ -35,4 +36,15 @@
                .orderByAsc(HrmAttendanceSummaryDO::getUserId));
    }
    /**
     * æŒ‰å¹´æœˆæ‰¹é‡åˆ·æ–°åº”出勤天数(节假日配置变化后调用)
     *
     * @return æ›´æ–°çš„记录数
     */
    default int updateWorkDaysByYearMonth(String yearMonth, Integer workDays) {
        return update(new HrmAttendanceSummaryDO(), new LambdaUpdateWrapper<HrmAttendanceSummaryDO>()
                .set(HrmAttendanceSummaryDO::getWorkDays, workDays)
                .eq(HrmAttendanceSummaryDO::getYearMonth, yearMonth));
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/employee/HrmEmployeeMapper.java
@@ -43,6 +43,15 @@
        return selectList(HrmEmployeeDO::getDeptId, deptId);
    }
    default List<HrmEmployeeDO> selectListByDeptIds(Collection<Long> deptIds) {
        if (deptIds == null || deptIds.isEmpty()) {
            return List.of();
        }
        return selectList(new LambdaQueryWrapperX<HrmEmployeeDO>()
                .in(HrmEmployeeDO::getDeptId, deptIds)
                .orderByAsc(HrmEmployeeDO::getId));
    }
    default List<HrmEmployeeDO> selectByEmployeeStatus(Integer employeeStatus) {
        return selectList(HrmEmployeeDO::getEmployeeStatus, employeeStatus);
    }
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmLeaveApplicationService.java
@@ -1,6 +1,8 @@
package cn.iocoder.yudao.module.hrm.service.approval;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmLeaveApplicationImportExcelVO;
import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmLeaveApplicationImportRespVO;
import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmLeaveApplicationPageReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmLeaveApplicationRespVO;
import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmLeaveApplicationSaveReqVO;
@@ -105,4 +107,12 @@
     */
    HrmLeaveApplicationDO getLeaveApplicationByProcessInstanceId(String processInstanceId);
    /**
     * æ‰¹é‡å¯¼å…¥è¯·å‡ç”³è¯·ï¼ˆå¯¼å…¥åŽä¸ºè‰ç¨¿çŠ¶æ€ï¼Œéœ€åœ¨é¡µé¢æäº¤å®¡æ‰¹ï¼‰
     *
     * @param importList å¯¼å…¥æ•°æ®
     * @return å¯¼å…¥ç»“æžœ
     */
    HrmLeaveApplicationImportRespVO importLeaveApplicationList(List<HrmLeaveApplicationImportExcelVO> importList);
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmLeaveApplicationServiceImpl.java
@@ -2,6 +2,8 @@
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
@@ -10,11 +12,15 @@
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
import cn.iocoder.yudao.module.bpm.service.definition.BpmCategoryService;
import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmLeaveApplicationImportExcelVO;
import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmLeaveApplicationImportRespVO;
import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmLeaveApplicationPageReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmLeaveApplicationRespVO;
import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmLeaveApplicationSaveReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmLeaveApplicationDO;
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.employee.HrmEmployeeMapper;
import cn.iocoder.yudao.module.hrm.dal.redis.HrmNoRedisDAO;
import cn.iocoder.yudao.module.hrm.enums.HrmAuditStatusEnum;
import cn.iocoder.yudao.module.hrm.enums.HrmLeaveTypeEnum;
@@ -31,8 +37,10 @@
import org.springframework.validation.annotation.Validated;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -41,6 +49,7 @@
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList;
import static cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants.*;
/**
@@ -69,6 +78,8 @@
    private StorageAttachmentApi storageAttachmentApi;
    @Resource
    private BpmProcessDefinitionService bpmProcessDefinitionService;
    @Resource
    private HrmEmployeeMapper employeeMapper;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -332,4 +343,89 @@
        return leaveApplicationMapper.selectByProcessInstanceId(processInstanceId);
    }
    @Override
    public HrmLeaveApplicationImportRespVO importLeaveApplicationList(List<HrmLeaveApplicationImportExcelVO> importList) {
        // 1. è¿‡æ»¤ç©ºè¡Œï¼ˆæ‰‹æœºå·ä¸ºç©ºçš„ Excel è¡Œè§†ä¸ºç©ºè¡Œï¼‰
        importList = filterList(importList, item -> StrUtil.isNotBlank(item.getPhone()));
        if (CollUtil.isEmpty(importList)) {
            throw exception(LEAVE_APPLICATION_IMPORT_LIST_IS_EMPTY);
        }
        // 2. é€è¡Œå¯¼å…¥ï¼Œå•行失败不影响其他行
        HrmLeaveApplicationImportRespVO respVO = HrmLeaveApplicationImportRespVO.builder()
                .createNames(new ArrayList<>())
                .failureNames(new LinkedHashMap<>())
                .build();
        for (int i = 0; i < importList.size(); i++) {
            HrmLeaveApplicationImportExcelVO row = importList.get(i);
            // Excel ç¬¬ 1 è¡Œæ˜¯è¡¨å¤´ï¼Œæ•°æ®ä»Žç¬¬ 2 è¡Œå¼€å§‹
            String rowKey = "第" + (i + 2) + "行";
            try {
                HrmLeaveApplicationSaveReqVO saveReqVO = buildImportSaveReqVO(row);
                createLeaveApplication(saveReqVO);
                respVO.getCreateNames().add(row.getPhone() + " " + row.getStartTime().toLocalDate());
            } catch (ServiceException ex) {
                respVO.getFailureNames().put(rowKey, ex.getMessage());
            } catch (Exception ex) {
                respVO.getFailureNames().put(rowKey, "导入失败:" + ex.getMessage());
            }
        }
        return respVO;
    }
    private HrmLeaveApplicationSaveReqVO buildImportSaveReqVO(HrmLeaveApplicationImportExcelVO row) {
        String phone = row.getPhone().trim();
        HrmEmployeeDO employee = employeeMapper.selectByPhone(phone);
        if (employee == null) {
            throw exception(LEAVE_APPLICATION_IMPORT_EMPLOYEE_NOT_EXISTS, phone);
        }
        if (employee.getUserId() == null) {
            throw exception(LEAVE_APPLICATION_IMPORT_EMPLOYEE_NO_USER, employee.getName());
        }
        // è¯·å‡ç±»åž‹ï¼šæŒ‰åç§°åŒ¹é…ï¼ˆå…¼å®¹ç›´æŽ¥å¡«å­—典值),匹配不到时列出可填值
        List<String> leaveTypeNames = Arrays.stream(HrmLeaveTypeEnum.values())
                .map(HrmLeaveTypeEnum::getName).toList();
        String leaveTypeText = StrUtil.trim(row.getLeaveType());
        if (StrUtil.isBlank(leaveTypeText)) {
            throw exception(LEAVE_APPLICATION_IMPORT_FIELD_REQUIRED,
                    "请假类型(可选:" + String.join("、", leaveTypeNames) + ")");
        }
        Integer leaveType = Arrays.stream(HrmLeaveTypeEnum.values())
                .filter(item -> item.getName().equals(leaveTypeText)
                        || String.valueOf(item.getType()).equals(leaveTypeText))
                .map(HrmLeaveTypeEnum::getType)
                .findFirst().orElse(null);
        if (leaveType == null) {
            throw exception(LEAVE_APPLICATION_IMPORT_FIELD_INVALID,
                    "请假类型", leaveTypeText, String.join("、", leaveTypeNames));
        }
        if (row.getStartTime() == null) {
            throw exception(LEAVE_APPLICATION_IMPORT_FIELD_REQUIRED, "开始时间");
        }
        if (row.getEndTime() == null) {
            throw exception(LEAVE_APPLICATION_IMPORT_FIELD_REQUIRED, "结束时间");
        }
        if (!row.getStartTime().isBefore(row.getEndTime())) {
            throw exception(LEAVE_APPLICATION_IMPORT_TIME_INVALID,
                    row.getStartTime(), row.getEndTime());
        }
        if (row.getDuration() == null || row.getDuration().signum() <= 0) {
            throw exception(LEAVE_APPLICATION_IMPORT_FIELD_REQUIRED, "请假时长(天,必须大于 0)");
        }
        if (StrUtil.isBlank(row.getReason())) {
            throw exception(LEAVE_APPLICATION_IMPORT_FIELD_REQUIRED, "请假原因");
        }
        HrmLeaveApplicationSaveReqVO saveReqVO = new HrmLeaveApplicationSaveReqVO();
        saveReqVO.setUserId(employee.getUserId());
        saveReqVO.setDeptId(employee.getDeptId());
        saveReqVO.setLeaveType(leaveType);
        saveReqVO.setStartTime(row.getStartTime());
        saveReqVO.setEndTime(row.getEndTime());
        saveReqVO.setDuration(row.getDuration());
        saveReqVO.setReason(row.getReason().trim());
        saveReqVO.setRemark(row.getRemark());
        return saveReqVO;
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmOvertimeApplicationService.java
@@ -14,13 +14,13 @@
    String APPROVE_CATEGORY_CODE = "overtime_approve";
    Long create(@Valid HrmOvertimeApplicationSaveReqVO reqVO, Long loginUserId);
    void update(@Valid HrmOvertimeApplicationSaveReqVO reqVO, Long loginUserId);
    void delete(Long id, Long loginUserId);
    void update(@Valid HrmOvertimeApplicationSaveReqVO reqVO, Long loginUserId, boolean canManage);
    void delete(Long id, Long loginUserId, boolean canManage);
    HrmOvertimeApplicationRespVO getDetail(Long id, Long loginUserId, boolean canManage);
    HrmOvertimeApplicationRespVO getCurrentApplicant(Long loginUserId);
    PageResult<HrmOvertimeApplicationRespVO> getPage(HrmOvertimeApplicationPageReqVO reqVO);
    void submit(Long id, String processDefinitionKey, Long loginUserId);
    void cancel(Long id, Long loginUserId);
    void submit(Long id, String processDefinitionKey, Long loginUserId, boolean canManage);
    void cancel(Long id, Long loginUserId, boolean canManage);
    List<Map<String, Object>> getApproveProcessDefinitionList();
    void updateAuditStatus(Long id, Integer status);
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmOvertimeApplicationServiceImpl.java
@@ -60,7 +60,7 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long create(HrmOvertimeApplicationSaveReqVO reqVO, Long loginUserId) {
        HrmEmployeeDO employee = requireEmployee(loginUserId);
        HrmEmployeeDO employee = resolveEmployee(reqVO.getEmployeeId(), loginUserId);
        HrmOvertimeApplicationDO application = BeanUtils.toBean(reqVO, HrmOvertimeApplicationDO.class);
        applyApplicantAndCalculatedTime(application, employee);
        application.setNo(noRedisDAO.generateOvertimeApplicationNo());
@@ -70,20 +70,20 @@
    }
    @Override
    public void update(HrmOvertimeApplicationSaveReqVO reqVO, Long loginUserId) {
        HrmOvertimeApplicationDO existing = requireOwnedDraft(reqVO.getId(), loginUserId);
    public void update(HrmOvertimeApplicationSaveReqVO reqVO, Long loginUserId, boolean canManage) {
        HrmOvertimeApplicationDO existing = requireManageableDraft(reqVO.getId(), loginUserId, canManage);
        HrmOvertimeApplicationDO update = BeanUtils.toBean(reqVO, HrmOvertimeApplicationDO.class);
        applyApplicantAndCalculatedTime(update, requireEmployee(loginUserId));
        applyApplicantAndCalculatedTime(update, resolveEmployee(reqVO.getEmployeeId(), loginUserId));
        update.setId(existing.getId());
        mapper.updateById(update);
    }
    @Override
    public void delete(Long id, Long loginUserId) {
    public void delete(Long id, Long loginUserId, boolean canManage) {
        HrmOvertimeApplicationDO application = require(id);
        boolean deletable = Objects.equals(application.getStatus(), HrmAuditStatusEnum.DRAFT.getStatus())
                || Objects.equals(application.getStatus(), HrmAuditStatusEnum.CANCEL.getStatus());
        if (!Objects.equals(application.getUserId(), loginUserId) || !deletable) {
        if ((!canManage && !Objects.equals(application.getUserId(), loginUserId)) || !deletable) {
            throw exception(OVERTIME_APPLICATION_UPDATE_FAIL_NOT_DRAFT);
        }
        mapper.deleteById(id);
@@ -115,14 +115,15 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void submit(Long id, String processDefinitionKey, Long loginUserId) {
        HrmOvertimeApplicationDO application = requireOwnedDraft(id, loginUserId);
    public void submit(Long id, String processDefinitionKey, Long loginUserId, boolean canManage) {
        HrmOvertimeApplicationDO application = requireManageableDraft(id, loginUserId, canManage);
        validateProcessDefinitions();
        ProcessDefinition definition = bpmProcessDefinitionService.getActiveProcessDefinition(processDefinitionKey);
        if (definition == null || !BPM_PROCESS_DEFINITION_KEY.equals(definition.getKey())) {
            throw exception(OVERTIME_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS);
        }
        String processInstanceId = bpmProcessInstanceApi.createProcessInstance(loginUserId,
        // ä»£å‘˜å·¥æäº¤æ—¶ï¼Œæµç¨‹å‘起人仍应为申请表上的申请人
        String processInstanceId = bpmProcessInstanceApi.createProcessInstance(application.getUserId(),
                new BpmProcessInstanceCreateReqDTO().setProcessDefinitionKey(processDefinitionKey)
                        .setBusinessKey(String.valueOf(id)));
        mapper.updateById(HrmOvertimeApplicationDO.builder().id(id).processInstanceId(processInstanceId)
@@ -131,13 +132,13 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void cancel(Long id, Long loginUserId) {
    public void cancel(Long id, Long loginUserId, boolean canManage) {
        HrmOvertimeApplicationDO application = require(id);
        if (!Objects.equals(application.getUserId(), loginUserId)
        if ((!canManage && !Objects.equals(application.getUserId(), loginUserId))
                || !Objects.equals(application.getStatus(), HrmAuditStatusEnum.PROCESS.getStatus())) {
            throw exception(OVERTIME_APPLICATION_UPDATE_AUDIT_STATUS_FAIL_NOT_PROCESS);
        }
        bpmProcessInstanceService.cancelProcessInstanceByStartUser(loginUserId,
        bpmProcessInstanceService.cancelProcessInstanceByStartUser(application.getUserId(),
                new BpmProcessInstanceCancelReqVO().setId(application.getProcessInstanceId()).setReason("申请人撤销加班申请"));
    }
@@ -215,9 +216,9 @@
        return value;
    }
    private HrmOvertimeApplicationDO requireOwnedDraft(Long id, Long userId) {
    private HrmOvertimeApplicationDO requireManageableDraft(Long id, Long userId, boolean canManage) {
        HrmOvertimeApplicationDO value = require(id);
        if (!Objects.equals(value.getUserId(), userId)
        if ((!canManage && !Objects.equals(value.getUserId(), userId))
                || !Objects.equals(value.getStatus(), HrmAuditStatusEnum.DRAFT.getStatus())) {
            throw exception(OVERTIME_APPLICATION_UPDATE_FAIL_NOT_DRAFT);
        }
@@ -230,6 +231,23 @@
        return employee;
    }
    /**
     * ç”³è¯·äººä¸ºç©ºæ—¶å›žé€€åˆ°å½“前登录账号关联的员工;选择他人时按员工ID加载
     */
    private HrmEmployeeDO resolveEmployee(Long employeeId, Long loginUserId) {
        if (employeeId == null) {
            return requireEmployee(loginUserId);
        }
        HrmEmployeeDO employee = employeeMapper.selectById(employeeId);
        if (employee == null) {
            throw exception(OVERTIME_APPLICATION_SELECTED_EMPLOYEE_NOT_EXISTS);
        }
        if (employee.getUserId() == null) {
            throw exception(OVERTIME_APPLICATION_SELECTED_EMPLOYEE_NO_USER, employee.getName());
        }
        return employee;
    }
    /** å½“前待办审批人或曾处理过该流程的审批人可查看业务详情。 */
    private boolean isProcessParticipant(String processInstanceId, Long userId) {
        if (processInstanceId == null || userId == null) {
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceHolidayService.java
@@ -1,7 +1,10 @@
package cn.iocoder.yudao.module.hrm.service.attendance;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceHolidayGroupRespVO;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceHolidayPageReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceHolidayRangeCreateReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceHolidayRangeUpdateReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceHolidaySaveReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceHolidayDO;
import jakarta.validation.Valid;
@@ -25,11 +28,33 @@
    Long createAttendanceHoliday(@Valid HrmAttendanceHolidaySaveReqVO createReqVO);
    /**
     * æŒ‰æ—¥æœŸèŒƒå›´æ–°å¢žèŠ‚å‡æ—¥é…ç½®ï¼ˆå«èµ·æ­¢æ—¥æœŸä¹‹é—´çš„æ¯ä¸€å¤©ï¼‰
     *
     * @param createReqVO æ–°å¢žä¿¡æ¯
     * @return æ–°å¢žçš„节假日数量
     */
    Integer createAttendanceHolidayRange(@Valid HrmAttendanceHolidayRangeCreateReqVO createReqVO);
    /**
     * æ›´æ–°èŠ‚å‡æ—¥é…ç½®
     *
     * @param updateReqVO æ›´æ–°ä¿¡æ¯
     */
    void updateAttendanceHoliday(@Valid HrmAttendanceHolidaySaveReqVO updateReqVO);
    /**
     * æŒ‰æ—¥æœŸåŒºé—´æ›´æ–°èŠ‚å‡æ—¥é…ç½®ï¼šåˆ é™¤åŒºé—´å†…åŽŸè®°å½•ï¼Œå†æŒ‰æ–°èµ·æ­¢æ—¥æœŸé‡æ–°å†™å…¥æ¯ä¸€å¤©
     *
     * @param updateReqVO æ›´æ–°ä¿¡æ¯
     */
    void updateAttendanceHolidayRange(@Valid HrmAttendanceHolidayRangeUpdateReqVO updateReqVO);
    /**
     * æŒ‰æ—¥æœŸåŒºé—´åˆ é™¤èŠ‚å‡æ—¥é…ç½®ï¼ˆåˆ é™¤åŒºé—´å†…å…¨éƒ¨è®°å½•ï¼‰
     *
     * @param ids åŒºé—´å†…记录ID列表
     */
    void deleteAttendanceHolidayRange(List<Long> ids);
    /**
     * åˆ é™¤èŠ‚å‡æ—¥é…ç½®
@@ -55,6 +80,16 @@
    PageResult<HrmAttendanceHolidayDO> getAttendanceHolidayPage(HrmAttendanceHolidayPageReqVO pageReqVO);
    /**
     * èŽ·å¾—æŒ‰è¿žç»­æ—¥æœŸåŒºé—´åˆå¹¶åŽçš„èŠ‚å‡æ—¥åˆ†é¡µ
     *
     * åº“里按自然日存储,列表侧按「日期连续 + åç§°/类型/是否法定/备注一致」合并成一行展示。
     *
     * @param pageReqVO åˆ†é¡µæŸ¥è¯¢å‚æ•°
     * @return åˆå¹¶åŽçš„节假日分页
     */
    PageResult<HrmAttendanceHolidayGroupRespVO> getAttendanceHolidayGroupPage(HrmAttendanceHolidayPageReqVO pageReqVO);
    /**
     * èŽ·å¾—æŒ‡å®šæ—¥æœŸèŒƒå›´çš„èŠ‚å‡æ—¥åˆ—è¡¨
     *
     * @param startDate å¼€å§‹æ—¥æœŸ
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceHolidayServiceImpl.java
@@ -1,19 +1,29 @@
package cn.iocoder.yudao.module.hrm.service.attendance;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
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.HrmAttendanceHolidayGroupRespVO;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceHolidayPageReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceHolidayRangeCreateReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceHolidayRangeUpdateReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceHolidaySaveReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceHolidayDO;
import cn.iocoder.yudao.module.hrm.dal.mysql.attendance.HrmAttendanceHolidayMapper;
import cn.iocoder.yudao.module.hrm.enums.HrmAttendanceHolidayTypeEnum;
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.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants.*;
@@ -42,6 +52,42 @@
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Integer createAttendanceHolidayRange(HrmAttendanceHolidayRangeCreateReqVO createReqVO) {
        if (createReqVO.getEndDate().isBefore(createReqVO.getStartDate())) {
            throw exception(ATTENDANCE_HOLIDAY_DATE_RANGE_INVALID);
        }
        return insertDays(createReqVO.getName(), createReqVO.getStartDate(), createReqVO.getEndDate(),
                createReqVO.getType(), createReqVO.getIsLegal(), createReqVO.getRemark());
    }
    /**
     * æŒ‰å¤©å†™å…¥èŠ‚å‡æ—¥ï¼›å†™å…¥å‰æ•´ä½“æŸ¥é‡ï¼Œä»»ä¸€æ—¥æœŸå·²å­˜åœ¨åˆ™æ•´ä½“ä¸å†™å…¥
     */
    private int insertDays(String name, LocalDate startDate, LocalDate endDate,
                           Integer type, Boolean isLegal, String remark) {
        // å…ˆæ•´ä½“校验,区间内已存在任一日期时不做部分写入
        List<HrmAttendanceHolidayDO> existingList = attendanceHolidayMapper.selectListByDateBetween(startDate, endDate);
        if (!existingList.isEmpty()) {
            HrmAttendanceHolidayDO existing = existingList.get(0);
            throw exception(ATTENDANCE_HOLIDAY_RANGE_DATE_EXISTS, existing.getDate(), existing.getName());
        }
        int count = 0;
        for (LocalDate date = startDate; !date.isAfter(endDate); date = date.plusDays(1)) {
            HrmAttendanceHolidayDO attendanceHoliday = HrmAttendanceHolidayDO.builder()
                    .name(name)
                    .date(date)
                    .type(type)
                    .isLegal(Boolean.TRUE.equals(isLegal) ? 1 : 0)
                    .remark(remark)
                    .build();
            attendanceHolidayMapper.insert(attendanceHoliday);
            count++;
        }
        return count;
    }
    @Override
    public void updateAttendanceHoliday(HrmAttendanceHolidaySaveReqVO updateReqVO) {
        validateAttendanceHolidayExists(updateReqVO.getId());
        // å¦‚果日期变更,校验新日期是否已存在配置
@@ -55,9 +101,41 @@
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateAttendanceHolidayRange(HrmAttendanceHolidayRangeUpdateReqVO updateReqVO) {
        // 1. æ ¡éªŒæ—¶é—´èŒƒå›´ä¸ŽåŒºé—´è®°å½•
        if (updateReqVO.getEndDate().isBefore(updateReqVO.getStartDate())) {
            throw exception(ATTENDANCE_HOLIDAY_DATE_RANGE_INVALID);
        }
        List<Long> ids = updateReqVO.getIds().stream().filter(Objects::nonNull).distinct().toList();
        if (ids.isEmpty()) {
            throw exception(ATTENDANCE_HOLIDAY_RANGE_IDS_EMPTY);
        }
        List<HrmAttendanceHolidayDO> oldList = attendanceHolidayMapper.selectBatchIds(ids);
        if (oldList.size() != ids.size()) {
            throw exception(ATTENDANCE_HOLIDAY_RANGE_NOT_EXISTS);
        }
        // 2. åˆ é™¤åŒºé—´å†…原记录,再按新的起止日期整体重写(同一事务,异常会回滚)
        attendanceHolidayMapper.physicalDeleteByIds(ids);
        insertDays(updateReqVO.getName(), updateReqVO.getStartDate(), updateReqVO.getEndDate(),
                updateReqVO.getType(), updateReqVO.getIsLegal(), updateReqVO.getRemark());
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteAttendanceHolidayRange(List<Long> ids) {
        List<Long> cleanIds = ids == null ? Collections.emptyList()
                : ids.stream().filter(Objects::nonNull).distinct().toList();
        if (cleanIds.isEmpty()) {
            throw exception(ATTENDANCE_HOLIDAY_RANGE_IDS_EMPTY);
        }
        attendanceHolidayMapper.physicalDeleteByIds(cleanIds);
    }
    @Override
    public void deleteAttendanceHoliday(Long id) {
        validateAttendanceHolidayExists(id);
        attendanceHolidayMapper.deleteById(id);
        attendanceHolidayMapper.physicalDeleteByIds(Collections.singletonList(id));
    }
    private void validateAttendanceHolidayExists(Long id) {
@@ -83,6 +161,72 @@
    }
    @Override
    public PageResult<HrmAttendanceHolidayGroupRespVO> getAttendanceHolidayGroupPage(
            HrmAttendanceHolidayPageReqVO pageReqVO) {
        List<HrmAttendanceHolidayDO> list = attendanceHolidayMapper.selectListByCondition(pageReqVO);
        if (list.isEmpty()) {
            return new PageResult<>(Collections.emptyList(), 0L);
        }
        // 1. è¿žç»­æ—¥æœŸ + åç§°/类型/是否法定/备注一致的记录合并为一行
        List<HrmAttendanceHolidayGroupRespVO> groups = new ArrayList<>();
        HrmAttendanceHolidayDO previous = null;
        for (HrmAttendanceHolidayDO current : list) {
            HrmAttendanceHolidayGroupRespVO last = groups.isEmpty() ? null : groups.get(groups.size() - 1);
            boolean sameGroup = last != null && previous != null
                    && previous.getDate().plusDays(1).equals(current.getDate())
                    && sameHoliday(previous, current);
            if (sameGroup) {
                last.setEndDate(current.getDate());
                last.getIds().add(current.getId());
                last.setDayCount(last.getDayCount() + 1);
            } else {
                HrmAttendanceHolidayGroupRespVO group = new HrmAttendanceHolidayGroupRespVO();
                group.setId(current.getId());
                List<Long> ids = new ArrayList<>();
                ids.add(current.getId());
                group.setIds(ids);
                group.setName(current.getName());
                group.setDate(current.getDate());
                group.setEndDate(current.getDate());
                group.setDayCount(1);
                group.setType(current.getType());
                group.setIsLegal(current.getIsLegal());
                group.setRemark(current.getRemark());
                group.setCreateTime(current.getCreateTime());
                groups.add(group);
            }
            previous = current;
        }
        // 2. æŒ‰æ—¥æœŸå€’序,与逐日列表保持一致
        groups.sort(Comparator.comparing(HrmAttendanceHolidayGroupRespVO::getDate).reversed());
        // 3. åˆ†ç»„后内存分页
        return pageGroups(groups, pageReqVO);
    }
    private boolean sameHoliday(HrmAttendanceHolidayDO a, HrmAttendanceHolidayDO b) {
        return StrUtil.equals(a.getName(), b.getName())
                && Objects.equals(a.getType(), b.getType())
                && Objects.equals(a.getIsLegal(), b.getIsLegal())
                && StrUtil.equals(a.getRemark(), b.getRemark());
    }
    private PageResult<HrmAttendanceHolidayGroupRespVO> pageGroups(
            List<HrmAttendanceHolidayGroupRespVO> groups, HrmAttendanceHolidayPageReqVO pageReqVO) {
        long total = groups.size();
        if (Objects.equals(PageParam.PAGE_SIZE_NONE, pageReqVO.getPageSize())) {
            return new PageResult<>(groups, total);
        }
        int pageNo = pageReqVO.getPageNo() == null ? 1 : pageReqVO.getPageNo();
        int pageSize = pageReqVO.getPageSize() == null ? 10 : pageReqVO.getPageSize();
        int from = Math.max(0, (pageNo - 1) * pageSize);
        if (from >= groups.size()) {
            return new PageResult<>(Collections.emptyList(), total);
        }
        int to = Math.min(groups.size(), from + pageSize);
        return new PageResult<>(new ArrayList<>(groups.subList(from, to)), total);
    }
    @Override
    public List<HrmAttendanceHolidayDO> getAttendanceHolidayListByDateRange(LocalDate startDate, LocalDate endDate) {
        return attendanceHolidayMapper.selectListByDateBetween(startDate, endDate);
    }
@@ -100,4 +244,4 @@
        return dayOfWeek != DayOfWeek.SATURDAY && dayOfWeek != DayOfWeek.SUNDAY;
    }
}
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceSummaryServiceImpl.java
@@ -1,15 +1,18 @@
package cn.iocoder.yudao.module.hrm.service.attendance;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
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.HrmAttendanceHolidayDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceRecordDO;
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.HrmAttendanceHolidayTypeEnum;
import cn.iocoder.yudao.module.hrm.service.employee.HrmEmployeeService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
@@ -18,9 +21,12 @@
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.YearMonth;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
 * è€ƒå‹¤ç»Ÿè®¡æ±‡æ€» Service å®žçŽ°ç±»
@@ -154,12 +160,28 @@
    /**
     * è®¡ç®—应出勤天数(工作日数量)
     *
     * ä¸€æ¬¡å–出区间内节假日配置,避免逐日查询。
     */
    private int calculateWorkDays(LocalDate startDate, LocalDate endDate) {
        Map<LocalDate, Integer> holidayTypeMap = attendanceHolidayService
                .getAttendanceHolidayListByDateRange(startDate, endDate)
                .stream()
                .collect(Collectors.toMap(HrmAttendanceHolidayDO::getDate, HrmAttendanceHolidayDO::getType,
                        (a, b) -> a));
        int workDays = 0;
        LocalDate current = startDate;
        while (!current.isAfter(endDate)) {
            if (attendanceHolidayService.isWorkDay(current)) {
            Integer holidayType = holidayTypeMap.get(current);
            boolean work;
            if (holidayType != null) {
                // æœ‰èŠ‚å‡æ—¥é…ç½®æ—¶æŒ‰é…ç½®åˆ¤æ–­ï¼šä¼‘æ¯æ—¥ä¸ç®—åº”å‡ºå‹¤ï¼Œå·¥ä½œæ—¥è®¡å…¥ï¼ˆå«è°ƒä¼‘ä¸Šç­çš„å‘¨æœ«ï¼‰
                work = HrmAttendanceHolidayTypeEnum.WORK_DAY.getType().equals(holidayType);
            } else {
                DayOfWeek dayOfWeek = current.getDayOfWeek();
                work = dayOfWeek != DayOfWeek.SATURDAY && dayOfWeek != DayOfWeek.SUNDAY;
            }
            if (work) {
                workDays++;
            }
            current = current.plusDays(1);
@@ -167,8 +189,28 @@
        return workDays;
    }
    /**
     * èŠ‚å‡æ—¥é…ç½®å˜æ›´åŽï¼Œå·²æœ‰æ±‡æ€»çš„åº”å‡ºå‹¤å¤©æ•°ä¼šä¸Žæœ€æ–°é…ç½®ä¸ä¸€è‡´ã€‚
     * æŸ¥è¯¢æ±‡æ€»æ—¶æŒ‰æœ€æ–°èŠ‚å‡æ—¥é‡ç®—ä¸€æ¬¡å¹¶å›žå†™ï¼Œä¿è¯ã€Œåº”å‡ºå‹¤å¤©æ•°ã€å§‹ç»ˆå…³è”èŠ‚å‡æ—¥é…ç½®ã€‚
     */
    private void refreshWorkDaysOfMonth(String yearMonth) {
        if (StrUtil.isBlank(yearMonth)) {
            return;
        }
        try {
            YearMonth ym = YearMonth.parse(yearMonth);
            attendanceSummaryMapper.updateWorkDaysByYearMonth(yearMonth,
                    calculateWorkDays(ym.atDay(1), ym.atEndOfMonth()));
        } catch (Exception ex) {
            // åˆ·æ–°å¤±è´¥ä¸å½±å“æŸ¥è¯¢æœ¬èº«ï¼Œè®°å½•日志即可
            log.warn("[refreshWorkDaysOfMonth][yearMonth({}) åˆ·æ–°åº”出勤天数失败]", yearMonth, ex);
        }
    }
    @Override
    public PageResult<HrmAttendanceSummaryDO> getAttendanceSummaryPage(HrmAttendanceSummaryPageReqVO pageReqVO) {
        // æŸ¥è¯¢å‰æŒ‰æœ€æ–°èŠ‚å‡æ—¥é‡ç®—è¯¥æœˆåº”å‡ºå‹¤å¤©æ•°
        refreshWorkDaysOfMonth(pageReqVO.getYearMonth());
        return attendanceSummaryMapper.selectPage(pageReqVO, new LambdaQueryWrapperX<HrmAttendanceSummaryDO>()
                .eqIfPresent(HrmAttendanceSummaryDO::getUserId, pageReqVO.getUserId())
                .eqIfPresent(HrmAttendanceSummaryDO::getDeptId, pageReqVO.getDeptId())
@@ -184,6 +226,8 @@
    @Override
    public List<HrmAttendanceSummaryDO> getDeptMonthlySummaryList(Long deptId, String yearMonth) {
        // æŸ¥è¯¢å‰æŒ‰æœ€æ–°èŠ‚å‡æ—¥é‡ç®—è¯¥æœˆåº”å‡ºå‹¤å¤©æ•°
        refreshWorkDaysOfMonth(yearMonth);
        return attendanceSummaryMapper.selectListByDeptIdAndYearMonth(deptId, yearMonth);
    }
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmEmployeeScheduleService.java
@@ -1,5 +1,7 @@
package cn.iocoder.yudao.module.hrm.service.attendance;
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;
@@ -23,4 +25,9 @@
     */
    void saveSchedule(List<HrmEmployeeScheduleSaveReqVO> saveReqVOList);
    /**
     * æŒ‰ã€Œéƒ¨é—¨ + å²—位」批量排班:为筛选出的一类员工,按日期范围统一设置班次或休息日
     */
    HrmEmployeeScheduleBatchSaveRespVO batchSaveSchedule(HrmEmployeeScheduleBatchSaveReqVO reqVO);
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmEmployeeScheduleServiceImpl.java
@@ -1,21 +1,30 @@
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.*;
@@ -40,6 +49,10 @@
    private HrmEmployeeMapper employeeMapper;
    @Resource
    private HrmShiftMapper shiftMapper;
    @Resource
    private DeptApi deptApi;
    @Resource
    private HrmAttendanceHolidayService attendanceHolidayService;
    @Override
    public List<HrmEmployeeScheduleRespVO> getScheduleList(HrmEmployeeScheduleListReqVO reqVO) {
@@ -52,7 +65,9 @@
        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> 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);
@@ -140,4 +155,121 @@
        }
    }
    @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;
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeServiceImpl.java
@@ -360,6 +360,14 @@
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(deptIds);
        // å¡«å……岗位名称
        Set<Long> postIds = list.stream()
                .map(HrmEmployeeDO::getPostIds)
                .filter(StrUtil::isNotBlank)
                .flatMap(json -> JSONUtil.toList(json, Long.class).stream())
                .collect(Collectors.toSet());
        Map<Long, PostRespDTO> postMap = CollUtil.isEmpty(postIds)
                ? Collections.emptyMap() : postApi.getPostMap(postIds);
        return list.stream().map(emp -> {
            HrmEmployeeSimpleRespVO vo = new HrmEmployeeSimpleRespVO();
            vo.setId(emp.getId());
@@ -369,6 +377,12 @@
            if (emp.getDeptId() != null && deptMap.containsKey(emp.getDeptId())) {
                vo.setDeptName(deptMap.get(emp.getDeptId()).getName());
            }
            if (StrUtil.isNotBlank(emp.getPostIds())) {
                vo.setPostName(JSONUtil.toList(emp.getPostIds(), Long.class).stream()
                        .filter(postMap::containsKey)
                        .map(id -> postMap.get(id).getName())
                        .collect(Collectors.joining(",")));
            }
            return vo;
        }).collect(Collectors.toList());
    }