From 9697bac35fe2c74811afaf66f9cf28b758f3a386 Mon Sep 17 00:00:00 2001
From: liu <2021943741@qq.com>
Date: 星期五, 28 八月 2026 15:21:22 +0800
Subject: [PATCH] feat(hrm): 新增排班功能,薪酬核算按排班工时

---
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmShiftSaveReqVO.java            |   45 ++
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmShiftServiceImpl.java                      |  100 +++++
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeServiceImpl.java                     |   10 
 yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/ErrorCodeConstants.java                                |   10 
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmEmployeeScheduleController.java   |   46 ++
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmEmployeeScheduleRespVO.java    |   39 ++
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeePageReqVO.java           |    3 
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmShiftPageReqVO.java            |   21 +
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/attendance/HrmEmployeeScheduleDO.java             |   56 +++
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmEmployeeScheduleListReqVO.java |   21 +
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/salary/HrmSalaryCalculationDO.java                |    5 
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmShiftService.java                          |   47 ++
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/attendance/HrmShiftDO.java                        |   73 ++++
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmEmployeeScheduleServiceImpl.java           |  143 ++++++++
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmShiftController.java              |  100 +++++
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmEmployeeScheduleSaveReqVO.java |   27 +
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/attendance/HrmShiftMapper.java                         |   33 +
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmShiftRespVO.java               |   56 +++
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmSalaryCalculationRespVO.java       |    4 
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/employee/HrmEmployeeMapper.java                        |   37 ++
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmEmployeeScheduleService.java               |   26 +
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/attendance/HrmEmployeeScheduleMapper.java              |   51 +++
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryCalculationServiceImpl.java              |   50 ++
 23 files changed, 999 insertions(+), 4 deletions(-)

diff --git a/yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/ErrorCodeConstants.java b/yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/ErrorCodeConstants.java
index cd8b6d8..aeedd30 100644
--- a/yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/ErrorCodeConstants.java
+++ b/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, "钖叕缁撴瀯姝e湪琚憳宸ヤ娇鐢紝鏃犳硶鍒犻櫎");
 
+    // ========== 鐝绠$悊 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, "鎺掔彮寮曠敤鐨勫憳宸ヤ笉瀛樺湪");
+
 }
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmEmployeeScheduleController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmEmployeeScheduleController.java
new file mode 100644
index 0000000..d9c362a
--- /dev/null
+++ b/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);
+    }
+
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmShiftController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmShiftController.java
new file mode 100644
index 0000000..9d6e055
--- /dev/null
+++ b/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);
+    }
+
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmEmployeeScheduleListReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmEmployeeScheduleListReqVO.java
new file mode 100644
index 0000000..8653a66
--- /dev/null
+++ b/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;
+
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmEmployeeScheduleRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmEmployeeScheduleRespVO.java
new file mode 100644
index 0000000..c7a5458
--- /dev/null
+++ b/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;
+
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmEmployeeScheduleSaveReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmEmployeeScheduleSaveReqVO.java
new file mode 100644
index 0000000..feca57c
--- /dev/null
+++ b/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;
+
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmShiftPageReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmShiftPageReqVO.java
new file mode 100644
index 0000000..4506ffb
--- /dev/null
+++ b/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;
+
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmShiftRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmShiftRespVO.java
new file mode 100644
index 0000000..6117507
--- /dev/null
+++ b/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;
+
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmShiftSaveReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmShiftSaveReqVO.java
new file mode 100644
index 0000000..646e6c9
--- /dev/null
+++ b/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;
+
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeePageReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeePageReqVO.java
index 391bea8..7693ebf 100644
--- a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeePageReqVO.java
+++ b/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;
+
 }
\ No newline at end of file
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmSalaryCalculationRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmSalaryCalculationRespVO.java
index 945b210..16836b7 100644
--- a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmSalaryCalculationRespVO.java
+++ b/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")
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/attendance/HrmEmployeeScheduleDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/attendance/HrmEmployeeScheduleDO.java
new file mode 100644
index 0000000..01e84a1
--- /dev/null
+++ b/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;
+
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/attendance/HrmShiftDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/attendance/HrmShiftDO.java
new file mode 100644
index 0000000..be1ad37
--- /dev/null
+++ b/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;
+
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/salary/HrmSalaryCalculationDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/salary/HrmSalaryCalculationDO.java
index 187e15a..2f19cc4 100644
--- a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/salary/HrmSalaryCalculationDO.java
+++ b/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;
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/attendance/HrmEmployeeScheduleMapper.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/attendance/HrmEmployeeScheduleMapper.java
new file mode 100644
index 0000000..53b9b00
--- /dev/null
+++ b/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));
+    }
+
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/attendance/HrmShiftMapper.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/attendance/HrmShiftMapper.java
new file mode 100644
index 0000000..030d58f
--- /dev/null
+++ b/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));
+    }
+
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/employee/HrmEmployeeMapper.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/employee/HrmEmployeeMapper.java
index 19ab24b..0a48fa0 100644
--- a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/employee/HrmEmployeeMapper.java
+++ b/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) {
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmEmployeeScheduleService.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmEmployeeScheduleService.java
new file mode 100644
index 0000000..c948a29
--- /dev/null
+++ b/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);
+
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmEmployeeScheduleServiceImpl.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmEmployeeScheduleServiceImpl.java
new file mode 100644
index 0000000..36eb169
--- /dev/null
+++ b/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);
+                }
+            }
+        }
+    }
+
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmShiftService.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmShiftService.java
new file mode 100644
index 0000000..b7c0da4
--- /dev/null
+++ b/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();
+
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmShiftServiceImpl.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmShiftServiceImpl.java
new file mode 100644
index 0000000..ea66cc7
--- /dev/null
+++ b/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);
+    }
+
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeServiceImpl.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeServiceImpl.java
index c326a89..eefe7a7 100644
--- a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeServiceImpl.java
+++ b/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);
     }
 
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryCalculationServiceImpl.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryCalculationServiceImpl.java
index 8be05ab..6cf8b19 100644
--- a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryCalculationServiceImpl.java
+++ b/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) {

--
Gitblit v1.9.3