liu
12 小时以前 9697bac35fe2c74811afaf66f9cf28b758f3a386
feat(hrm): 新增排班功能,薪酬核算按排班工时

- 新增班次定义与员工排班模块及接口
- 薪酬核算按排班累计出勤天数与排班总工时,替代硬编码 22
- 员工查询支持选父部门连带子部门、按工号/姓名/手机号关键词搜索
已添加16个文件
已修改7个文件
1003 ■■■■■ 文件已修改
yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/ErrorCodeConstants.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmEmployeeScheduleController.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmShiftController.java 100 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmEmployeeScheduleListReqVO.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmEmployeeScheduleRespVO.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmEmployeeScheduleSaveReqVO.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmShiftPageReqVO.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmShiftRespVO.java 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmShiftSaveReqVO.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeePageReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmSalaryCalculationRespVO.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/attendance/HrmEmployeeScheduleDO.java 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/attendance/HrmShiftDO.java 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/salary/HrmSalaryCalculationDO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/attendance/HrmEmployeeScheduleMapper.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/attendance/HrmShiftMapper.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/employee/HrmEmployeeMapper.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmEmployeeScheduleService.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmEmployeeScheduleServiceImpl.java 143 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmShiftService.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmShiftServiceImpl.java 100 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeServiceImpl.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryCalculationServiceImpl.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/ErrorCodeConstants.java
@@ -119,4 +119,14 @@
    ErrorCode SALARY_STRUCTURE_NAME_EXISTS = new ErrorCode(1_030_005_025, "薪酬结构名称已存在");
    ErrorCode SALARY_STRUCTURE_IN_USE = new ErrorCode(1_030_005_026, "薪酬结构正在被员工使用,无法删除");
    // ========== ç­æ¬¡ç®¡ç† 1-030-009-000 ==========
    ErrorCode SHIFT_NOT_EXISTS = new ErrorCode(1_030_009_000, "班次不存在");
    ErrorCode SHIFT_NAME_EXISTS = new ErrorCode(1_030_009_001, "班次名称已存在");
    ErrorCode SHIFT_IN_USE = new ErrorCode(1_030_009_002, "班次已被排班使用,无法删除");
    // ========== æŽ’班管理 1-030-010-000 ==========
    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, "排班引用的员工不存在");
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmEmployeeScheduleController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,46 @@
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.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.service.attendance.HrmEmployeeScheduleService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - HRM å‘˜å·¥æŽ’班")
@RestController
@RequestMapping("/hrm/employee-schedule")
@Validated
public class HrmEmployeeScheduleController {
    @Resource
    private HrmEmployeeScheduleService employeeScheduleService;
    @GetMapping("/list")
    @Operation(summary = "获得某月员工排班列表")
    @PreAuthorize("@ss.hasPermission('hrm:employee-schedule:query')")
    public CommonResult<List<HrmEmployeeScheduleRespVO>> getEmployeeScheduleList(
            @Valid HrmEmployeeScheduleListReqVO reqVO) {
        return success(employeeScheduleService.getScheduleList(reqVO));
    }
    @PostMapping("/save")
    @Operation(summary = "批量保存员工排班(整月覆盖式)")
    @PreAuthorize("@ss.hasPermission('hrm:employee-schedule:save')")
    public CommonResult<Boolean> saveEmployeeSchedule(
            @Valid @RequestBody List<@Valid HrmEmployeeScheduleSaveReqVO> saveReqVOList) {
        employeeScheduleService.saveSchedule(saveReqVOList);
        return success(true);
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmShiftController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,100 @@
package cn.iocoder.yudao.module.hrm.controller.admin.attendance;
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
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.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmShiftPageReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmShiftRespVO;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmShiftSaveReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmShiftDO;
import cn.iocoder.yudao.module.hrm.service.attendance.HrmShiftService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
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.util.List;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - HRM ç­æ¬¡å®šä¹‰")
@RestController
@RequestMapping("/hrm/shift")
@Validated
public class HrmShiftController {
    @Resource
    private HrmShiftService shiftService;
    @PostMapping("/create")
    @Operation(summary = "新增班次")
    @PreAuthorize("@ss.hasPermission('hrm:shift:create')")
    public CommonResult<Long> createShift(@Valid @RequestBody HrmShiftSaveReqVO createReqVO) {
        return success(shiftService.createShift(createReqVO));
    }
    @PutMapping("/update")
    @Operation(summary = "更新班次")
    @PreAuthorize("@ss.hasPermission('hrm:shift:update')")
    public CommonResult<Boolean> updateShift(@Valid @RequestBody HrmShiftSaveReqVO updateReqVO) {
        shiftService.updateShift(updateReqVO);
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除班次")
    @Parameter(name = "id", description = "班次ID", required = true)
    @PreAuthorize("@ss.hasPermission('hrm:shift:delete')")
    public CommonResult<Boolean> deleteShift(@RequestParam("id") Long id) {
        shiftService.deleteShift(id);
        return success(true);
    }
    @GetMapping("/get")
    @Operation(summary = "获得班次")
    @Parameter(name = "id", description = "班次ID", required = true)
    @PreAuthorize("@ss.hasPermission('hrm:shift:query')")
    public CommonResult<HrmShiftRespVO> getShift(@RequestParam("id") Long id) {
        HrmShiftDO shift = shiftService.getShift(id);
        return success(BeanUtils.toBean(shift, HrmShiftRespVO.class));
    }
    @GetMapping("/page")
    @Operation(summary = "获得班次分页")
    @PreAuthorize("@ss.hasPermission('hrm:shift:query')")
    public CommonResult<PageResult<HrmShiftRespVO>> getShiftPage(@Valid HrmShiftPageReqVO pageReqVO) {
        PageResult<HrmShiftDO> pageResult = shiftService.getShiftPage(pageReqVO);
        return success(BeanUtils.toBean(pageResult, HrmShiftRespVO.class));
    }
    @GetMapping("/list")
    @Operation(summary = "获得班次列表(启用状态)")
    @PreAuthorize("@ss.hasPermission('hrm:shift:query')")
    public CommonResult<List<HrmShiftRespVO>> getShiftList() {
        List<HrmShiftDO> list = shiftService.getShiftList();
        return success(BeanUtils.toBean(list, HrmShiftRespVO.class));
    }
    @GetMapping("/export-excel")
    @Operation(summary = "导出班次 Excel")
    @PreAuthorize("@ss.hasPermission('hrm:shift:export')")
    @ApiAccessLog(operateType = EXPORT)
    public void exportShiftExcel(@Valid HrmShiftPageReqVO pageReqVO,
                                 HttpServletResponse response) throws IOException {
        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
        PageResult<HrmShiftDO> pageResult = shiftService.getShiftPage(pageReqVO);
        List<HrmShiftRespVO> data = BeanUtils.toBean(pageResult.getList(), HrmShiftRespVO.class);
        ExcelUtils.write(response, "班次.xls", "数据", HrmShiftRespVO.class, data);
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmEmployeeScheduleListReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Schema(description = "管理后台 - å‘˜å·¥æŽ’班查询 Request VO")
@Data
public class HrmEmployeeScheduleListReqVO {
    @Schema(description = "月份,格式:yyyy-MM", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-08")
    @NotBlank(message = "月份不能为空")
    private String yearMonth;
    @Schema(description = "部门ID", example = "1")
    private Long deptId;
    @Schema(description = "员工ID", example = "1")
    private Long employeeId;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmEmployeeScheduleRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,39 @@
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;
@Schema(description = "管理后台 - å‘˜å·¥æŽ’班 Response VO")
@Data
public class HrmEmployeeScheduleRespVO {
    @Schema(description = "排班ID")
    private Long id;
    @Schema(description = "员工ID")
    private Long employeeId;
    @Schema(description = "员工姓名")
    private String employeeName;
    @Schema(description = "员工工号")
    private String employeeNo;
    @Schema(description = "排班日期")
    private LocalDate date;
    @Schema(description = "班次ID,为空表示休息")
    private Long shiftId;
    @Schema(description = "班次名称")
    private String shiftName;
    @Schema(description = "班次颜色")
    private String shiftColor;
    @Schema(description = "备注")
    private String remark;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmEmployeeScheduleSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,27 @@
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;
@Schema(description = "管理后台 - å‘˜å·¥æŽ’班保存项 Request VO")
@Data
public class HrmEmployeeScheduleSaveReqVO {
    @Schema(description = "员工ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "员工不能为空")
    private Long employeeId;
    @Schema(description = "排班日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-08-01")
    @NotNull(message = "排班日期不能为空")
    private LocalDate date;
    @Schema(description = "班次ID,为空表示休息", example = "1")
    private Long shiftId;
    @Schema(description = "备注", example = "调休")
    private String remark;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmShiftPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@Schema(description = "管理后台 - ç­æ¬¡åˆ†é¡µ Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class HrmShiftPageReqVO extends PageParam {
    @Schema(description = "班次名称", example = "白班")
    private String name;
    @Schema(description = "状态:0-启用,1-禁用", example = "0")
    private Integer status;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmShiftRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,56 @@
package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.LocalTime;
@Schema(description = "管理后台 - ç­æ¬¡ Response VO")
@Data
@ExcelIgnoreUnannotated
public class HrmShiftRespVO {
    @Schema(description = "班次ID")
    @ExcelProperty("班次ID")
    private Long id;
    @Schema(description = "班次名称")
    @ExcelProperty("班次名称")
    private String name;
    @Schema(description = "上班时间")
    @ExcelProperty("上班时间")
    private LocalTime startTime;
    @Schema(description = "下班时间")
    @ExcelProperty("下班时间")
    private LocalTime endTime;
    @Schema(description = "标准工时(小时)")
    @ExcelProperty("标准工时")
    private BigDecimal workHours;
    @Schema(description = "是否跨天")
    @ExcelProperty("是否跨天")
    private Boolean crossDay;
    @Schema(description = "标签颜色(十六进制)")
    @ExcelProperty("标签颜色")
    private String color;
    @Schema(description = "状态:0-启用,1-禁用")
    private Integer status;
    @Schema(description = "备注")
    @ExcelProperty("备注")
    private String remark;
    @Schema(description = "创建时间")
    @ExcelProperty("创建时间")
    private LocalDateTime createTime;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmShiftSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,45 @@
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.math.BigDecimal;
import java.time.LocalTime;
@Schema(description = "管理后台 - ç­æ¬¡æ–°å¢ž/修改 Request VO")
@Data
public class HrmShiftSaveReqVO {
    @Schema(description = "班次ID(修改时必填)", example = "1")
    private Long id;
    @Schema(description = "班次名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "白班")
    @NotBlank(message = "班次名称不能为空")
    private String name;
    @Schema(description = "上班时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "08:00:00")
    @NotNull(message = "上班时间不能为空")
    private LocalTime startTime;
    @Schema(description = "下班时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "17:00:00")
    @NotNull(message = "下班时间不能为空")
    private LocalTime endTime;
    @Schema(description = "标准工时(小时)", example = "8")
    private BigDecimal workHours;
    @Schema(description = "是否跨天", example = "false")
    private Boolean crossDay;
    @Schema(description = "标签颜色(十六进制)", example = "#1677ff")
    private String color;
    @Schema(description = "状态:0-启用,1-禁用", example = "0")
    private Integer status;
    @Schema(description = "备注", example = "标准白班")
    private String remark;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeePageReqVO.java
@@ -24,4 +24,7 @@
    @Schema(description = "员工状态", example = "1")
    private Integer employeeStatus;
    @Schema(description = "关键词(模糊匹配员工编号/姓名/手机号)", example = "张三")
    private String keyword;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmSalaryCalculationRespVO.java
@@ -97,6 +97,10 @@
    @ExcelProperty("加班时长")
    private BigDecimal overtimeHours;
    @Schema(description = "排班总工时")
    @ExcelProperty("排班总工时")
    private BigDecimal totalWorkHours;
    @Schema(description = "状态")
    @ExcelProperty(value = "状态", converter = DictConvert.class)
    @DictFormat("hrm_salary_calculation_status")
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/attendance/HrmEmployeeScheduleDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,56 @@
package cn.iocoder.yudao.module.hrm.dal.dataobject.attendance;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.time.LocalDate;
/**
 * å‘˜å·¥æŽ’班明细 DO
 *
 * @author è¶…级管理员
 */
@TableName("hrm_employee_schedule")
@KeySequence("hrm_employee_schedule_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class HrmEmployeeScheduleDO extends BaseDO {
    /**
     * æŽ’班ID
     */
    @TableId
    private Long id;
    /**
     * å‘˜å·¥ID
     *
     * å…³è” HrmEmployeeDO#getId()
     */
    private Long employeeId;
    /**
     * æŽ’班日期
     */
    private LocalDate date;
    /**
     * ç­æ¬¡ID
     *
     * å…³è” HrmShiftDO#getId(),为空表示休息
     */
    private Long shiftId;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/attendance/HrmShiftDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,73 @@
package cn.iocoder.yudao.module.hrm.dal.dataobject.attendance;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.math.BigDecimal;
import java.time.LocalTime;
/**
 * ç­æ¬¡å®šä¹‰ DO
 *
 * @author è¶…级管理员
 */
@TableName("hrm_shift")
@KeySequence("hrm_shift_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class HrmShiftDO extends BaseDO {
    /**
     * ç­æ¬¡ID
     */
    @TableId
    private Long id;
    /**
     * ç­æ¬¡åç§°
     */
    private String name;
    /**
     * ä¸Šç­æ—¶é—´
     */
    private LocalTime startTime;
    /**
     * ä¸‹ç­æ—¶é—´
     */
    private LocalTime endTime;
    /**
     * æ ‡å‡†å·¥æ—¶ï¼ˆå°æ—¶ï¼‰
     */
    private BigDecimal workHours;
    /**
     * æ˜¯å¦è·¨å¤©
     */
    private Boolean crossDay;
    /**
     * æ ‡ç­¾é¢œè‰²ï¼ˆåå…­è¿›åˆ¶ï¼‰
     */
    private String color;
    /**
     * çŠ¶æ€ï¼š0-启用,1-禁用
     */
    private Integer status;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/salary/HrmSalaryCalculationDO.java
@@ -123,6 +123,11 @@
    private BigDecimal overtimeHours;
    /**
     * æŽ’班总工时(小时)
     */
    private BigDecimal totalWorkHours;
    /**
     * çŠ¶æ€ï¼š0-待确认,10-已确认,20-已发放
     */
    private Integer status;
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/attendance/HrmEmployeeScheduleMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,51 @@
package cn.iocoder.yudao.module.hrm.dal.mysql.attendance;
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.HrmEmployeeScheduleDO;
import org.apache.ibatis.annotations.Mapper;
import java.time.LocalDate;
import java.util.Collection;
import java.util.List;
/**
 * å‘˜å·¥æŽ’班明细 Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface HrmEmployeeScheduleMapper extends BaseMapperX<HrmEmployeeScheduleDO> {
    default HrmEmployeeScheduleDO selectByEmployeeIdAndDate(Long employeeId, LocalDate date) {
        return selectOne(new LambdaQueryWrapperX<HrmEmployeeScheduleDO>()
                .eq(HrmEmployeeScheduleDO::getEmployeeId, employeeId)
                .eq(HrmEmployeeScheduleDO::getDate, date));
    }
    default List<HrmEmployeeScheduleDO> selectListByPeriod(LocalDate startDate, LocalDate endDate) {
        return selectList(new LambdaQueryWrapperX<HrmEmployeeScheduleDO>()
                .between(HrmEmployeeScheduleDO::getDate, startDate, endDate)
                .orderByAsc(HrmEmployeeScheduleDO::getDate));
    }
    default List<HrmEmployeeScheduleDO> selectListByEmployeeIdsAndPeriod(Collection<Long> employeeIds, LocalDate startDate, LocalDate endDate) {
        return selectList(new LambdaQueryWrapperX<HrmEmployeeScheduleDO>()
                .in(HrmEmployeeScheduleDO::getEmployeeId, employeeIds)
                .between(HrmEmployeeScheduleDO::getDate, startDate, endDate)
                .orderByAsc(HrmEmployeeScheduleDO::getDate));
    }
    default List<HrmEmployeeScheduleDO> selectListByEmployeeIdAndPeriod(Long employeeId, LocalDate startDate, LocalDate endDate) {
        return selectList(new LambdaQueryWrapperX<HrmEmployeeScheduleDO>()
                .eq(HrmEmployeeScheduleDO::getEmployeeId, employeeId)
                .between(HrmEmployeeScheduleDO::getDate, startDate, endDate)
                .orderByAsc(HrmEmployeeScheduleDO::getDate));
    }
    default Long selectCountByShiftId(Long shiftId) {
        return selectCount(new LambdaQueryWrapperX<HrmEmployeeScheduleDO>()
                .eq(HrmEmployeeScheduleDO::getShiftId, shiftId));
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/attendance/HrmShiftMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
package cn.iocoder.yudao.module.hrm.dal.mysql.attendance;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmShiftPageReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmShiftDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
 * ç­æ¬¡å®šä¹‰ Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface HrmShiftMapper extends BaseMapperX<HrmShiftDO> {
    default PageResult<HrmShiftDO> selectPage(HrmShiftPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<HrmShiftDO>()
                .likeIfPresent(HrmShiftDO::getName, reqVO.getName())
                .eqIfPresent(HrmShiftDO::getStatus, reqVO.getStatus())
                .orderByAsc(HrmShiftDO::getStartTime));
    }
    default List<HrmShiftDO> selectListByStatus(Integer status) {
        return selectList(new LambdaQueryWrapperX<HrmShiftDO>()
                .eqIfPresent(HrmShiftDO::getStatus, status)
                .orderByAsc(HrmShiftDO::getStartTime));
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/employee/HrmEmployeeMapper.java
@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.hrm.dal.mysql.employee;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
@@ -7,6 +8,7 @@
import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
/**
@@ -46,12 +48,41 @@
    }
    default PageResult<HrmEmployeeDO> selectPage(HrmEmployeePageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<HrmEmployeeDO>()
        LambdaQueryWrapperX<HrmEmployeeDO> wrapper = new LambdaQueryWrapperX<HrmEmployeeDO>()
                .likeIfPresent(HrmEmployeeDO::getEmployeeNo, reqVO.getEmployeeNo())
                .likeIfPresent(HrmEmployeeDO::getName, reqVO.getName())
                .eqIfPresent(HrmEmployeeDO::getDeptId, reqVO.getDeptId())
                .eqIfPresent(HrmEmployeeDO::getEmployeeStatus, reqVO.getEmployeeStatus())
                .orderByDesc(HrmEmployeeDO::getId));
                .eqIfPresent(HrmEmployeeDO::getEmployeeStatus, reqVO.getEmployeeStatus());
        appendKeyword(wrapper, reqVO.getKeyword());
        wrapper.orderByDesc(HrmEmployeeDO::getId);
        return selectPage(reqVO, wrapper);
    }
    /**
     * æŒ‰éƒ¨é—¨é›†åˆï¼ˆå«å­éƒ¨é—¨ï¼‰åˆ†é¡µæŸ¥è¯¢å‘˜å·¥
     */
    default PageResult<HrmEmployeeDO> selectPageByDeptIds(HrmEmployeePageReqVO reqVO, Collection<Long> deptIds) {
        LambdaQueryWrapperX<HrmEmployeeDO> wrapper = new LambdaQueryWrapperX<HrmEmployeeDO>()
                .likeIfPresent(HrmEmployeeDO::getEmployeeNo, reqVO.getEmployeeNo())
                .likeIfPresent(HrmEmployeeDO::getName, reqVO.getName())
                .inIfPresent(HrmEmployeeDO::getDeptId, deptIds)
                .eqIfPresent(HrmEmployeeDO::getEmployeeStatus, reqVO.getEmployeeStatus());
        appendKeyword(wrapper, reqVO.getKeyword());
        wrapper.orderByDesc(HrmEmployeeDO::getId);
        return selectPage(reqVO, wrapper);
    }
    /**
     * å…³é”®è¯æ¨¡ç³ŠåŒ¹é…å‘˜å·¥ç¼–号 / å§“名 / æ‰‹æœºå·
     */
    private void appendKeyword(LambdaQueryWrapperX<HrmEmployeeDO> wrapper, String keyword) {
        if (StrUtil.isBlank(keyword)) {
            return;
        }
        String kw = keyword.trim();
        wrapper.and(w -> w.like(HrmEmployeeDO::getEmployeeNo, kw)
                .or().like(HrmEmployeeDO::getName, kw)
                .or().like(HrmEmployeeDO::getPhone, kw));
    }
    default List<HrmEmployeeDO> selectListByStatus(Integer employeeStatus) {
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmEmployeeScheduleService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,26 @@
package cn.iocoder.yudao.module.hrm.service.attendance;
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 java.util.List;
/**
 * å‘˜å·¥æŽ’班 Service æŽ¥å£
 *
 * @author è¶…级管理员
 */
public interface HrmEmployeeScheduleService {
    /**
     * æŸ¥è¯¢æŸæœˆæŽ’班列表
     */
    List<HrmEmployeeScheduleRespVO> getScheduleList(HrmEmployeeScheduleListReqVO reqVO);
    /**
     * æ‰¹é‡ä¿å­˜æŽ’班(整月覆盖式:有班次 upsert,无班次删除)
     */
    void saveSchedule(List<HrmEmployeeScheduleSaveReqVO> saveReqVOList);
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmEmployeeScheduleServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,143 @@
package cn.iocoder.yudao.module.hrm.service.attendance;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
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.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.attendance.HrmEmployeeScheduleMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.attendance.HrmShiftMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeMapper;
import jakarta.annotation.Resource;
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.util.function.Function;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants.*;
/**
 * å‘˜å·¥æŽ’班 Service å®žçŽ°ç±»
 *
 * @author è¶…级管理员
 */
@Service
@Validated
public class HrmEmployeeScheduleServiceImpl implements HrmEmployeeScheduleService {
    @Resource
    private HrmEmployeeScheduleMapper employeeScheduleMapper;
    @Resource
    private HrmEmployeeMapper employeeMapper;
    @Resource
    private HrmShiftMapper shiftMapper;
    @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) {
            HrmEmployeeScheduleDO existing = employeeScheduleMapper.selectByEmployeeIdAndDate(
                    reqVO.getEmployeeId(), reqVO.getDate());
            if (reqVO.getShiftId() == null) {
                if (existing != null) {
                    employeeScheduleMapper.deleteById(existing.getId());
                }
            } else {
                if (existing == null) {
                    employeeScheduleMapper.insert(BeanUtils.toBean(reqVO, HrmEmployeeScheduleDO.class));
                } else {
                    existing.setShiftId(reqVO.getShiftId());
                    existing.setRemark(reqVO.getRemark());
                    employeeScheduleMapper.updateById(existing);
                }
            }
        }
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmShiftService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,47 @@
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.HrmShiftPageReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmShiftSaveReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmShiftDO;
import java.util.List;
/**
 * ç­æ¬¡å®šä¹‰ Service æŽ¥å£
 *
 * @author è¶…级管理员
 */
public interface HrmShiftService {
    /**
     * æ–°å¢žç­æ¬¡
     */
    Long createShift(HrmShiftSaveReqVO createReqVO);
    /**
     * æ›´æ–°ç­æ¬¡
     */
    void updateShift(HrmShiftSaveReqVO updateReqVO);
    /**
     * åˆ é™¤ç­æ¬¡
     */
    void deleteShift(Long id);
    /**
     * èŽ·å¾—ç­æ¬¡
     */
    HrmShiftDO getShift(Long id);
    /**
     * èŽ·å¾—ç­æ¬¡åˆ†é¡µ
     */
    PageResult<HrmShiftDO> getShiftPage(HrmShiftPageReqVO pageReqVO);
    /**
     * èŽ·å¾—ç­æ¬¡åˆ—è¡¨ï¼ˆå¯ç”¨çŠ¶æ€ï¼‰
     */
    List<HrmShiftDO> getShiftList();
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmShiftServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,100 @@
package cn.iocoder.yudao.module.hrm.service.attendance;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmShiftPageReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmShiftSaveReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmShiftDO;
import cn.iocoder.yudao.module.hrm.dal.mysql.attendance.HrmEmployeeScheduleMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.attendance.HrmShiftMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants.*;
/**
 * ç­æ¬¡å®šä¹‰ Service å®žçŽ°ç±»
 *
 * @author è¶…级管理员
 */
@Service
@Validated
public class HrmShiftServiceImpl implements HrmShiftService {
    @Resource
    private HrmShiftMapper shiftMapper;
    @Resource
    private HrmEmployeeScheduleMapper employeeScheduleMapper;
    @Override
    public Long createShift(HrmShiftSaveReqVO createReqVO) {
        validateShiftNameUnique(null, createReqVO.getName());
        HrmShiftDO shift = BeanUtils.toBean(createReqVO, HrmShiftDO.class);
        if (shift.getStatus() == null) {
            shift.setStatus(0);
        }
        if (shift.getCrossDay() == null) {
            shift.setCrossDay(false);
        }
        shiftMapper.insert(shift);
        return shift.getId();
    }
    @Override
    public void updateShift(HrmShiftSaveReqVO updateReqVO) {
        validateShiftExists(updateReqVO.getId());
        validateShiftNameUnique(updateReqVO.getId(), updateReqVO.getName());
        HrmShiftDO updateObj = BeanUtils.toBean(updateReqVO, HrmShiftDO.class);
        if (updateObj.getCrossDay() == null) {
            updateObj.setCrossDay(false);
        }
        shiftMapper.updateById(updateObj);
    }
    @Override
    public void deleteShift(Long id) {
        validateShiftExists(id);
        // æ ¡éªŒç­æ¬¡æ˜¯å¦å·²è¢«æŽ’班使用
        Long usedCount = employeeScheduleMapper.selectCountByShiftId(id);
        if (usedCount != null && usedCount > 0) {
            throw exception(SHIFT_IN_USE);
        }
        shiftMapper.deleteById(id);
    }
    private void validateShiftExists(Long id) {
        if (shiftMapper.selectById(id) == null) {
            throw exception(SHIFT_NOT_EXISTS);
        }
    }
    private void validateShiftNameUnique(Long id, String name) {
        HrmShiftDO shift = shiftMapper.selectOne(HrmShiftDO::getName, name);
        if (shift == null) {
            return;
        }
        if (id == null || !shift.getId().equals(id)) {
            throw exception(SHIFT_NAME_EXISTS);
        }
    }
    @Override
    public HrmShiftDO getShift(Long id) {
        return shiftMapper.selectById(id);
    }
    @Override
    public PageResult<HrmShiftDO> getShiftPage(HrmShiftPageReqVO pageReqVO) {
        return shiftMapper.selectPage(pageReqVO);
    }
    @Override
    public List<HrmShiftDO> getShiftList() {
        return shiftMapper.selectListByStatus(0);
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeServiceImpl.java
@@ -325,6 +325,16 @@
    @Override
    public PageResult<HrmEmployeeDO> getEmployeePage(HrmEmployeePageReqVO pageReqVO) {
        if (pageReqVO.getDeptId() != null) {
            // é€‰ä¸­éƒ¨é—¨æ—¶ï¼Œè¿žå¸¦æŸ¥è¯¢å…¶æ‰€æœ‰å­éƒ¨é—¨çš„员工(含自身)
            List<DeptRespDTO> childDepts = deptApi.getChildDeptList(pageReqVO.getDeptId());
            Set<Long> deptIds = new LinkedHashSet<>();
            deptIds.add(pageReqVO.getDeptId());
            if (CollUtil.isNotEmpty(childDepts)) {
                childDepts.forEach(dept -> deptIds.add(dept.getId()));
            }
            return employeeMapper.selectPageByDeptIds(pageReqVO, deptIds);
        }
        return employeeMapper.selectPage(pageReqVO);
    }
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryCalculationServiceImpl.java
@@ -6,6 +6,8 @@
import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.HrmSalaryCalculationPageReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmLeaveApplicationDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmOvertimeApplicationDO;
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.dataobject.salary.HrmEmployeeSalaryDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmEmployeeSocialSecurityDO;
@@ -16,6 +18,8 @@
import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmSocialSecuritySchemeDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmSalaryStructureDO;
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.approval.HrmOvertimeApplicationMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmEmployeeSalaryMapper;
@@ -39,8 +43,11 @@
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -96,6 +103,10 @@
    private HrmSalaryPaymentMapper salaryPaymentMapper;
    @Resource
    private HrmSalaryPaymentDetailMapper salaryPaymentDetailMapper;
    @Resource
    private HrmEmployeeScheduleMapper employeeScheduleMapper;
    @Resource
    private HrmShiftMapper shiftMapper;
    @Override
    public PageResult<HrmSalaryCalculationDO> getSalaryCalculationPage(HrmSalaryCalculationPageReqVO pageReqVO) {
@@ -225,6 +236,11 @@
        OvertimeSalary overtimeSalary = calculateOvertimeSalary(employee, employeeSalary, period, baseSalary);
        BigDecimal overtimePay = overtimeSalary.pay();
        // æŒ‰æŽ’班累计出勤天数与排班总工时(无排班时出勤天数回退默认 22)
        ScheduleStat scheduleStat = calculateScheduleStat(employeeId, period);
        Integer workDays = scheduleStat.scheduledDays() > 0 ? scheduleStat.scheduledDays() : 22;
        BigDecimal totalWorkHours = scheduleStat.totalWorkHours();
        // åº”发工资总额
        BigDecimal totalIncome = baseSalary.add(performanceSalary).add(mealAllowance)
                .add(transportAllowance).add(otherAllowance).add(overtimePay);
@@ -295,8 +311,9 @@
                .otherDeduction(otherDeduction)
                .leaveDeduction(leaveDeduction)
                .actualSalary(actualSalary)
                .workDays(22) // é»˜è®¤å·¥ä½œæ—¥
                .workDays(workDays) // æŒ‰æŽ’班累计应出勤天数,无排班时回退 22
                .overtimeHours(overtimeSalary.hours())
                .totalWorkHours(totalWorkHours)
                .status(0) // å¾…确认
                .build();
    }
@@ -401,6 +418,37 @@
    private record OvertimeSalary(BigDecimal hours, BigDecimal pay) {}
    private record ScheduleStat(int scheduledDays, BigDecimal totalWorkHours) {}
    /**
     * æŒ‰æŽ’班表累计员工的出勤天数与排班总工时。
     * ä»…统计有班次的排班记录,班次标准工时之和即为排班工时。
     */
    private ScheduleStat calculateScheduleStat(Long employeeId, String period) {
        YearMonth yearMonth = YearMonth.parse(period);
        List<HrmEmployeeScheduleDO> schedules = employeeScheduleMapper.selectListByEmployeeIdAndPeriod(
                employeeId, yearMonth.atDay(1), yearMonth.atEndOfMonth());
        if (CollUtil.isEmpty(schedules)) {
            return new ScheduleStat(0, BigDecimal.ZERO);
        }
        Set<Long> shiftIds = schedules.stream().map(HrmEmployeeScheduleDO::getShiftId)
                .filter(Objects::nonNull).collect(Collectors.toSet());
        Map<Long, HrmShiftDO> shiftMap = CollUtil.isEmpty(shiftIds) ? Collections.emptyMap()
                : shiftMapper.selectBatchIds(shiftIds).stream()
                        .collect(Collectors.toMap(HrmShiftDO::getId, shift -> shift, (a, b) -> a));
        int scheduledDays = 0;
        BigDecimal totalWorkHours = BigDecimal.ZERO;
        for (HrmEmployeeScheduleDO schedule : schedules) {
            HrmShiftDO shift = schedule.getShiftId() == null ? null : shiftMap.get(schedule.getShiftId());
            if (shift == null) {
                continue;
            }
            scheduledDays++;
            totalWorkHours = totalWorkHours.add(shift.getWorkHours() != null ? shift.getWorkHours() : BigDecimal.ZERO);
        }
        return new ScheduleStat(scheduledDays, totalWorkHours.setScale(2, RoundingMode.HALF_UP));
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void confirmSalaryCalculation(List<Long> ids) {