From a50e3f61c7b45cd9e11364519e83071b372c4b9d Mon Sep 17 00:00:00 2001
From: huminmin <mac@MacBook-Pro.local>
Date: 星期四, 20 八月 2026 09:13:40 +0800
Subject: [PATCH] 加班申请

---
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/salary/HrmSalaryStructureDO.java                     |   14 
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryStructureServiceImpl.java                   |   79 ++++
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/approval/HrmOvertimeApplicationDO.java               |   46 ++
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/salary/HrmSalaryStructureMapper.java                      |   13 
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryStructureService.java                       |   19 +
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/salary/HrmEmployeeSalaryMapper.java                       |    6 
 sql/mysql/hrm_overtime_application.sql                                                                                         |   64 +++
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/approval/HrmOvertimeApplicationMapper.java                |   38 ++
 yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtils.java                     |    6 
 yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/HrmOvertimeTypeEnum.java                                  |   31 +
 docs/projectSql/ruoyi-vue-pro.sql                                                                                              |    4 
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalaryStructureController.java           |   81 ++++
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmOvertimeApplicationServiceImpl.java             |  263 +++++++++++++
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryCalculationServiceImpl.java                 |   99 ++++
 sql/mysql/hrm_module_init.sql                                                                                                  |    6 
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmOvertimeApplicationRespVO.java      |   41 ++
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmSalaryStructureSaveReqVO.java         |   51 ++
 yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/ErrorCodeConstants.java                                   |   11 
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/HrmOvertimeApplicationController.java     |   71 +++
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmOvertimeApplicationSaveReqVO.java   |   36 +
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmSalaryStructureRespVO.java            |   30 +
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmOvertimeApplicationService.java                 |   26 +
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/redis/HrmNoRedisDAO.java                                        |    7 
 sql/mysql/hrm_salary_structure_overtime_ratio.sql                                                                              |    8 
 sql/mysql/hrm_salary_structure_menu.sql                                                                                        |   34 +
 yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtilsTest.java                 |   10 
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmOvertimeApplicationPageReqVO.java   |   18 
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/employee/HrmEmployeeMapper.java                           |    6 
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/listener/HrmOvertimeApplicationStatusListener.java |   20 +
 29 files changed, 1,113 insertions(+), 25 deletions(-)

diff --git a/docs/projectSql/ruoyi-vue-pro.sql b/docs/projectSql/ruoyi-vue-pro.sql
index baaafbc..ccf920f 100644
--- a/docs/projectSql/ruoyi-vue-pro.sql
+++ b/docs/projectSql/ruoyi-vue-pro.sql
@@ -4903,7 +4903,9 @@
   `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '缁撴瀯鍚嶇О',
   `base_salary` decimal(12, 2) NOT NULL COMMENT '鍩烘湰宸ヨ祫',
   `performance_ratio` decimal(4, 2) NULL DEFAULT 0.00 COMMENT '缁╂晥宸ヨ祫鍗犳瘮锛�%锛�',
-  `overtime_pay_ratio` decimal(4, 2) NULL DEFAULT 1.50 COMMENT '鍔犵彮宸ヨ祫鍊嶇巼',
+  `overtime_pay_ratio` decimal(4, 2) NULL DEFAULT 1.50 COMMENT '宸ヤ綔鏃ュ姞鐝伐璧勫�嶇巼',
+  `rest_day_overtime_pay_ratio` decimal(4, 2) NULL DEFAULT 2.00 COMMENT '浼戞伅鏃ュ姞鐝伐璧勫�嶇巼',
+  `legal_holiday_overtime_pay_ratio` decimal(4, 2) NULL DEFAULT 3.00 COMMENT '娉曞畾鑺傚亣鏃ュ姞鐝伐璧勫�嶇巼',
   `social_security_base` decimal(12, 2) NULL DEFAULT NULL COMMENT '绀句繚鍩烘暟',
   `housing_fund_base` decimal(12, 2) NULL DEFAULT NULL COMMENT '鍏Н閲戝熀鏁�',
   `status` tinyint NOT NULL DEFAULT 0 COMMENT '鐘舵�侊細0-鍚敤锛�1-绂佺敤',
diff --git a/sql/mysql/hrm_module_init.sql b/sql/mysql/hrm_module_init.sql
index 57c4d35..d1d46a3 100644
--- a/sql/mysql/hrm_module_init.sql
+++ b/sql/mysql/hrm_module_init.sql
@@ -274,7 +274,9 @@
     `name` varchar(100) NOT NULL COMMENT '缁撴瀯鍚嶇О',
     `base_salary` decimal(12,2) NOT NULL COMMENT '鍩烘湰宸ヨ祫',
     `performance_ratio` decimal(4,2) DEFAULT 0 COMMENT '缁╂晥宸ヨ祫鍗犳瘮锛�%锛�',
-    `overtime_pay_ratio` decimal(4,2) DEFAULT 1.5 COMMENT '鍔犵彮宸ヨ祫鍊嶇巼',
+    `overtime_pay_ratio` decimal(4,2) DEFAULT 1.5 COMMENT '宸ヤ綔鏃ュ姞鐝伐璧勫�嶇巼',
+    `rest_day_overtime_pay_ratio` decimal(4,2) DEFAULT 2.0 COMMENT '浼戞伅鏃ュ姞鐝伐璧勫�嶇巼',
+    `legal_holiday_overtime_pay_ratio` decimal(4,2) DEFAULT 3.0 COMMENT '娉曞畾鑺傚亣鏃ュ姞鐝伐璧勫�嶇巼',
     `social_security_base` decimal(12,2) DEFAULT NULL COMMENT '绀句繚鍩烘暟',
     `housing_fund_base` decimal(12,2) DEFAULT NULL COMMENT '鍏Н閲戝熀鏁�',
     `status` tinyint NOT NULL DEFAULT 0 COMMENT '鐘舵�侊細0-鍚敤锛�1-绂佺敤',
@@ -586,4 +588,4 @@
 -- =============================================
 -- 鎵ц瀹屾垚鎻愮ず
 -- =============================================
-SELECT 'HRM 浜哄姏璧勬簮妯″潡 SQL 鍒濆鍖栧畬鎴愶紒' AS result;
\ No newline at end of file
+SELECT 'HRM 浜哄姏璧勬簮妯″潡 SQL 鍒濆鍖栧畬鎴愶紒' AS result;
diff --git a/sql/mysql/hrm_overtime_application.sql b/sql/mysql/hrm_overtime_application.sql
new file mode 100644
index 0000000..7983a9b
--- /dev/null
+++ b/sql/mysql/hrm_overtime_application.sql
@@ -0,0 +1,64 @@
+-- HRM 鍔犵彮鐢宠锛氬崗鍚屽姙鍏鎵� + 钖叕鏍哥畻鏁版嵁婧�
+
+CREATE TABLE IF NOT EXISTS `hrm_overtime_application` (
+    `id` bigint NOT NULL AUTO_INCREMENT COMMENT '鐢宠ID',
+    `no` varchar(32) NOT NULL COMMENT '鐢宠鍗曞彿',
+    `user_id` bigint NOT NULL COMMENT '绯荤粺鐢ㄦ埛ID',
+    `employee_id` bigint NOT NULL COMMENT '鍛樺伐ID',
+    `dept_id` bigint NOT NULL COMMENT '閮ㄩ棬ID',
+    `overtime_date` date NOT NULL COMMENT '鍔犵彮鏃ユ湡',
+    `start_time` time NOT NULL COMMENT '寮�濮嬫椂闂�',
+    `end_time` time NOT NULL COMMENT '缁撴潫鏃堕棿',
+    `duration` decimal(6,1) NOT NULL COMMENT '鎵i櫎鐢ㄩ鍚庣殑鍔犵彮鏃堕暱锛�0.5灏忔椂鍙栨暣锛�',
+    `overtime_type` tinyint NOT NULL COMMENT '1-宸ヤ綔鏃ワ紝2-浼戞伅鏃ワ紝3-娉曞畾鑺傚亣鏃�',
+    `cross_day` bit(1) NOT NULL DEFAULT b'0' COMMENT '鏄惁璺ㄥぉ',
+    `meal_deduction` bit(1) NOT NULL DEFAULT b'0' COMMENT '鏄惁鎵i櫎鐢ㄩ鏃堕棿',
+    `meal_deduction_hours` decimal(4,1) NOT NULL DEFAULT 0 COMMENT '鐢ㄩ鎵i櫎灏忔椂鏁�',
+    `reason` varchar(500) NOT NULL COMMENT '鍔犵彮鍘熷洜',
+    `related_type` tinyint DEFAULT NULL COMMENT '鍏宠仈绫诲瀷锛�1-椤圭洰锛�2-宸ュ崟',
+    `related_code` varchar(100) DEFAULT NULL COMMENT '椤圭洰/宸ュ崟缂栧彿',
+    `work_content` varchar(1000) DEFAULT NULL COMMENT '宸ヤ綔鍐呭绠�杩�',
+    `status` tinyint NOT NULL DEFAULT 0 COMMENT '0-鑽夌锛�10-瀹℃壒涓紝20-閫氳繃锛�30-椹冲洖锛�40-鎾ら攢',
+    `process_instance_id` varchar(64) DEFAULT NULL COMMENT '鍗忓悓鍔炲叕娴佺▼瀹炰緥ID',
+    `approve_time` datetime DEFAULT NULL COMMENT '瀹℃壒瀹屾垚鏃堕棿',
+    `creator` varchar(64) DEFAULT '' COMMENT '鍒涘缓鑰�',
+    `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '鍒涘缓鏃堕棿',
+    `updater` varchar(64) DEFAULT '' COMMENT '鏇存柊鑰�',
+    `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '鏇存柊鏃堕棿',
+    `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '鏄惁鍒犻櫎',
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `uk_hrm_overtime_no` (`no`),
+    KEY `idx_hrm_overtime_user_date_status` (`user_id`, `overtime_date`, `status`),
+    KEY `idx_hrm_overtime_process_instance` (`process_instance_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='HRM鍔犵彮鐢宠琛�';
+
+-- 鍗忓悓鍔炲叕娴佺▼鍒嗙被宸插湪 bpm/manager/category 涓淮鎶わ紝鍒嗙被缂栫爜涓� overtime_approve銆�
+-- 浠嶉渶鍦ㄨ鍒嗙被涓嬪缓绔嬪苟鍙戝竷娴佺▼瀹氫箟 key=hrm_overtime銆�
+
+-- 浜轰簨瀹℃壒鑿滃崟鍙婃寜閽潈闄�
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
+SELECT 6007, '鍔犵彮鐢宠', '', 2, 4, 6002, 'overtime', 'ep:timer', 'hrm/overtime/index', 'HrmOvertimeApplication', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
+FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `system_menu` WHERE `id` = 6007);
+
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
+SELECT 60071, '鍔犵彮鐢宠鏌ヨ', 'hrm:overtime:query', 3, 1, 6007, '', '', '', NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0' FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `system_menu` WHERE `id` = 60071);
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
+SELECT 60072, '鍔犵彮鐢宠鏂板', 'hrm:overtime:create', 3, 2, 6007, '', '', '', NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0' FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `system_menu` WHERE `id` = 60072);
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
+SELECT 60073, '鍔犵彮鐢宠淇敼', 'hrm:overtime:update', 3, 3, 6007, '', '', '', NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0' FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `system_menu` WHERE `id` = 60073);
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
+SELECT 60074, '鍔犵彮鐢宠鍒犻櫎', 'hrm:overtime:delete', 3, 4, 6007, '', '', '', NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0' FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `system_menu` WHERE `id` = 60074);
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
+SELECT 60075, '鍔犵彮鐢宠鎻愪氦', 'hrm:overtime:submit', 3, 5, 6007, '', '', '', NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0' FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `system_menu` WHERE `id` = 60075);
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
+SELECT 60076, '鍔犵彮鐢宠鎾ら攢', 'hrm:overtime:cancel', 3, 6, 6007, '', '', '', NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0' FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `system_menu` WHERE `id` = 60076);
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
+SELECT 60077, '鍔犵彮鐢宠绠$悊', 'hrm:overtime:manage', 3, 7, 6007, '', '', '', NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0' FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `system_menu` WHERE `id` = 60077);
+
+INSERT INTO `system_role_menu` (`role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
+SELECT 1, menu_id, '1', NOW(), '1', NOW(), b'0' FROM (
+    SELECT 6007 menu_id UNION ALL SELECT 60071 UNION ALL SELECT 60072 UNION ALL
+    SELECT 60073 UNION ALL SELECT 60074 UNION ALL SELECT 60075 UNION ALL SELECT 60076 UNION ALL SELECT 60077
+) ids WHERE NOT EXISTS (SELECT 1 FROM `system_role_menu` rm WHERE rm.`role_id` = 1 AND rm.`menu_id` = ids.menu_id);
+
+SELECT 'HRM 鍔犵彮鐢宠鍒濆鍖栧畬鎴愶紱璇峰湪鍗忓悓鍔炲叕鍙戝竷 hrm_overtime 娴佺▼' AS result;
diff --git a/sql/mysql/hrm_salary_structure_menu.sql b/sql/mysql/hrm_salary_structure_menu.sql
new file mode 100644
index 0000000..5e03ee4
--- /dev/null
+++ b/sql/mysql/hrm_salary_structure_menu.sql
@@ -0,0 +1,34 @@
+-- HRM 钖叕缁撴瀯閰嶇疆鑿滃崟涓庢潈闄愶紙鍙噸澶嶆墽琛岋級
+
+INSERT INTO `system_menu`
+(`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`,
+ `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
+VALUES
+(6025, '钖叕缁撴瀯', '', 2, 3, 6020, 'structure', 'ep:set-up',
+ 'hrm/salary/structure/index', 'HrmSalaryStructure', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
+(60251, '钖叕缁撴瀯鏌ヨ', 'hrm:salary-structure:query', 3, 1, 6025, '', '', '', NULL,
+ 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
+(60252, '钖叕缁撴瀯鏂板', 'hrm:salary-structure:create', 3, 2, 6025, '', '', '', NULL,
+ 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
+(60253, '钖叕缁撴瀯淇敼', 'hrm:salary-structure:update', 3, 3, 6025, '', '', '', NULL,
+ 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
+(60254, '钖叕缁撴瀯鍒犻櫎', 'hrm:salary-structure:delete', 3, 4, 6025, '', '', '', NULL,
+ 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0')
+ON DUPLICATE KEY UPDATE
+`name` = VALUES(`name`), `permission` = VALUES(`permission`), `parent_id` = VALUES(`parent_id`),
+`path` = VALUES(`path`), `icon` = VALUES(`icon`), `component` = VALUES(`component`),
+`component_name` = VALUES(`component_name`), `status` = VALUES(`status`), `deleted` = b'0';
+
+INSERT INTO `system_role_menu`
+(`role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
+SELECT 1, menu_id, '1', NOW(), '1', NOW(), b'0'
+FROM (
+    SELECT 6025 AS menu_id UNION ALL SELECT 60251 UNION ALL SELECT 60252
+    UNION ALL SELECT 60253 UNION ALL SELECT 60254
+) menu_ids
+WHERE NOT EXISTS (
+    SELECT 1 FROM `system_role_menu` role_menu
+    WHERE role_menu.`role_id` = 1 AND role_menu.`menu_id` = menu_ids.menu_id AND role_menu.`deleted` = b'0'
+);
+
+SELECT 'HRM 钖叕缁撴瀯鑿滃崟鍒濆鍖栧畬鎴�' AS result;
diff --git a/sql/mysql/hrm_salary_structure_overtime_ratio.sql b/sql/mysql/hrm_salary_structure_overtime_ratio.sql
new file mode 100644
index 0000000..25e7fe4
--- /dev/null
+++ b/sql/mysql/hrm_salary_structure_overtime_ratio.sql
@@ -0,0 +1,8 @@
+-- 钖叕缁撴瀯澧炲姞浼戞伅鏃ャ�佹硶瀹氳妭鍋囨棩鍔犵彮宸ヨ祫鍊嶇巼
+-- 宸叉湁鐨勫伐浣滄棩鍊嶇巼缁х画浣跨敤 overtime_pay_ratio 瀛楁
+
+ALTER TABLE `hrm_salary_structure`
+    ADD COLUMN `rest_day_overtime_pay_ratio` decimal(4,2) DEFAULT 2.00
+        COMMENT '浼戞伅鏃ュ姞鐝伐璧勫�嶇巼' AFTER `overtime_pay_ratio`,
+    ADD COLUMN `legal_holiday_overtime_pay_ratio` decimal(4,2) DEFAULT 3.00
+        COMMENT '娉曞畾鑺傚亣鏃ュ姞鐝伐璧勫�嶇巼' AFTER `rest_day_overtime_pay_ratio`;
diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtils.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtils.java
index 3242dec..6dc6ae3 100644
--- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtils.java
+++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtils.java
@@ -27,6 +27,7 @@
 import org.flowable.task.api.TaskInfo;
 import tools.jackson.databind.JsonNode;
 
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -245,6 +246,11 @@
                 || !BpmModelFormTypeEnum.NORMAL.getType().equals(processDefinitionInfo.getFormType())) {
             return null;
         }
+        // 鍘嗗彶娴佺▼瀹氫箟銆佷笟鍔¤〃鍗曟祦绋嬪彲鑳芥病鏈夊啑浣� formFields銆傛鏃舵病鏈夊彲灞曠ず鐨勬憳瑕侊紝
+        // 涓嶈兘鍥犱负鍗曟潯娴佺▼鏁版嵁缂哄皯琛ㄥ崟瀛楁閰嶇疆瀵艰嚧鏁翠釜鈥滄垜鐨勬祦绋�/寰呭姙鈥濆垎椤垫煡璇㈠け璐ャ��
+        if (ObjectUtil.isEmpty(processDefinitionInfo.getFormFields())) {
+            return Collections.emptyList();
+        }
 
         // 瑙f瀽琛ㄥ崟閰嶇疆
         Map<String, BpmFormFieldVO> formFieldsMap = new LinkedHashMap<>();
diff --git a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtilsTest.java b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtilsTest.java
index f9fbf94..eea5ec0 100644
--- a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtilsTest.java
+++ b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtilsTest.java
@@ -92,6 +92,16 @@
     }
 
     @Test
+    public void testGetSummary_normalFormWithoutFields_returnEmpty() {
+        // 鍑嗗鍙傛暟锛氫笟鍔¤〃鍗曟垨鍘嗗彶娴佺▼瀹氫箟鍙兘娌℃湁鍐椾綑 formFields銆�
+        BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionInfo(null, null);
+
+        // 璋冪敤 & 鏂█锛氱己灏戞憳瑕佸瓧娈垫椂杩斿洖绌烘憳瑕侊紝涓嶈兘褰卞搷鏁翠釜鍒嗛〉鎺ュ彛銆�
+        assertEquals(Collections.emptyList(),
+                FlowableUtils.getSummary(processDefinitionInfo, Collections.emptyMap()));
+    }
+
+    @Test
     public void testGetSummary_notNormalForm_returnNull() {
         // 鍑嗗鍙傛暟
         BpmProcessDefinitionInfoDO processDefinitionInfo = BpmProcessDefinitionInfoDO.builder()
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 dd2eb29..c845909 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
@@ -55,6 +55,15 @@
     ErrorCode RESIGNATION_APPLICATION_EXISTS = new ErrorCode(1_030_003_005, "璇ュ憳宸ュ凡瀛樺湪绂昏亴鐢宠锛岃鍕块噸澶嶆彁浜�");
     ErrorCode RESIGNATION_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS = new ErrorCode(1_030_003_006, "绂昏亴瀹℃壒娴佺▼涓嶅瓨鍦�");
 
+    // ========== 鍔犵彮鐢宠 1-030-008-000 ==========
+    ErrorCode OVERTIME_APPLICATION_NOT_EXISTS = new ErrorCode(1_030_008_000, "鍔犵彮鐢宠涓嶅瓨鍦�");
+    ErrorCode OVERTIME_APPLICATION_UPDATE_FAIL_NOT_DRAFT = new ErrorCode(1_030_008_001, "鍔犵彮鐢宠鏇存柊澶辫触锛屽師鍥狅細涓嶆槸鑽夌鐘舵��");
+    ErrorCode OVERTIME_APPLICATION_SUBMIT_FAIL_NOT_DRAFT = new ErrorCode(1_030_008_002, "鍔犵彮鐢宠鎻愪氦澶辫触锛屽師鍥狅細涓嶆槸鑽夌鐘舵��");
+    ErrorCode OVERTIME_APPLICATION_UPDATE_AUDIT_STATUS_FAIL_NOT_PROCESS = new ErrorCode(1_030_008_003, "鏇存柊鍔犵彮鐢宠瀹℃壒鐘舵�佸け璐ワ紝鍘熷洜锛氫笉鏄鎵逛腑鐘舵��");
+    ErrorCode OVERTIME_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS = new ErrorCode(1_030_008_004, "鍔犵彮瀹℃壒娴佺▼涓嶅瓨鍦紝璇峰厛鍦ㄥ崗鍚屽姙鍏腑閰嶇疆骞跺彂甯冩祦绋�");
+    ErrorCode OVERTIME_APPLICATION_EMPLOYEE_NOT_EXISTS = new ErrorCode(1_030_008_005, "褰撳墠璐﹀彿鏈叧鑱斿憳宸ユ。妗�");
+    ErrorCode OVERTIME_APPLICATION_TIME_INVALID = new ErrorCode(1_030_008_006, "鍔犵彮鏃舵鎴栨墸闄ゆ椂闀夸笉鍚堟硶");
+
     // ========== 鐢ㄦ埛鏁版嵁浜ゆ帴 1-030-007-000 ==========
     ErrorCode USER_HANDOVER_NOT_EXISTS = new ErrorCode(1_030_007_000, "鐢ㄦ埛浜ゆ帴璁板綍涓嶅瓨鍦�");
     ErrorCode USER_HANDOVER_ALREADY_EXISTS = new ErrorCode(1_030_007_001, "璇ョ鑱岀敵璇峰凡瀛樺湪浜ゆ帴璁板綍");
@@ -106,5 +115,7 @@
     ErrorCode SALARY_PAYMENT_REVOKE_FAIL = new ErrorCode(1_030_005_022, "鎾ら攢鍙戞斁澶辫触锛屽師鍥狅細璇ユ槑缁嗘湭鍙戞斁");
     ErrorCode SALARY_PAYMENT_DELETE_FAIL = new ErrorCode(1_030_005_023, "鍒犻櫎澶辫触锛屽師鍥狅細浠呭緟鍙戞斁鐘舵�佸彲鍒犻櫎锛岃鍏堟挙閿�宸插彂鏀炬槑缁�");
     ErrorCode SALARY_PAYMENT_HAS_PAID_DETAIL = new ErrorCode(1_030_005_024, "鍒犻櫎澶辫触锛屽師鍥狅細瀛樺湪宸插彂鏀炬槑缁嗭紝璇峰厛鎾ら攢");
+    ErrorCode SALARY_STRUCTURE_NAME_EXISTS = new ErrorCode(1_030_005_025, "钖叕缁撴瀯鍚嶇О宸插瓨鍦�");
+    ErrorCode SALARY_STRUCTURE_IN_USE = new ErrorCode(1_030_005_026, "钖叕缁撴瀯姝e湪琚憳宸ヤ娇鐢紝鏃犳硶鍒犻櫎");
 
 }
diff --git a/yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/HrmOvertimeTypeEnum.java b/yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/HrmOvertimeTypeEnum.java
new file mode 100644
index 0000000..8886910
--- /dev/null
+++ b/yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/HrmOvertimeTypeEnum.java
@@ -0,0 +1,31 @@
+package cn.iocoder.yudao.module.hrm.enums;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.math.BigDecimal;
+import java.util.Arrays;
+
+/** 鍔犵彮绫诲瀷鍙婃硶瀹氬伐璧勫�嶇巼銆� */
+@Getter
+@RequiredArgsConstructor
+public enum HrmOvertimeTypeEnum {
+
+    WORKDAY(1, "宸ヤ綔鏃ュ姞鐝�", new BigDecimal("1.5")),
+    REST_DAY(2, "浼戞伅鏃ュ姞鐝�", new BigDecimal("2.0")),
+    LEGAL_HOLIDAY(3, "娉曞畾鑺傚亣鏃ュ姞鐝�", new BigDecimal("3.0"));
+
+    private final Integer type;
+    private final String name;
+    private final BigDecimal salaryMultiplier;
+
+    public static HrmOvertimeTypeEnum valueOfType(Integer type) {
+        return Arrays.stream(values()).filter(item -> item.type.equals(type)).findFirst().orElse(null);
+    }
+
+    public static String getName(Integer type) {
+        HrmOvertimeTypeEnum value = valueOfType(type);
+        return value == null ? "" : value.name;
+    }
+
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/HrmOvertimeApplicationController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/HrmOvertimeApplicationController.java
new file mode 100644
index 0000000..5d616ee
--- /dev/null
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/HrmOvertimeApplicationController.java
@@ -0,0 +1,71 @@
+package cn.iocoder.yudao.module.hrm.controller.admin.approval;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
+import cn.iocoder.yudao.framework.security.core.service.SecurityFrameworkService;
+import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.*;
+import cn.iocoder.yudao.module.hrm.service.approval.HrmOvertimeApplicationService;
+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 java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "绠$悊鍚庡彴 - HRM 鍔犵彮鐢宠")
+@RestController
+@RequestMapping("/hrm/overtime")
+@Validated
+public class HrmOvertimeApplicationController {
+    @Resource private HrmOvertimeApplicationService service;
+    @Resource private SecurityFrameworkService securityFrameworkService;
+
+    @PostMapping("/create") @PreAuthorize("@ss.hasPermission('hrm:overtime:create')")
+    public CommonResult<Long> create(@Valid @RequestBody HrmOvertimeApplicationSaveReqVO reqVO) {
+        return success(service.create(reqVO, SecurityFrameworkUtils.getLoginUserId()));
+    }
+    @PutMapping("/update") @PreAuthorize("@ss.hasPermission('hrm:overtime:update')")
+    public CommonResult<Boolean> update(@Valid @RequestBody HrmOvertimeApplicationSaveReqVO reqVO) {
+        service.update(reqVO, SecurityFrameworkUtils.getLoginUserId()); return success(true);
+    }
+    @DeleteMapping("/delete") @PreAuthorize("@ss.hasPermission('hrm:overtime:delete')")
+    public CommonResult<Boolean> delete(@RequestParam Long id) {
+        service.delete(id, SecurityFrameworkUtils.getLoginUserId()); return success(true);
+    }
+    @GetMapping("/get") @PreAuthorize("@ss.hasAnyPermissions('hrm:overtime:query', 'bpm:task:query')")
+    public CommonResult<HrmOvertimeApplicationRespVO> get(@RequestParam Long id) {
+        return success(service.getDetail(id, SecurityFrameworkUtils.getLoginUserId(), canManage()));
+    }
+    @GetMapping("/current-applicant") @PreAuthorize("@ss.hasPermission('hrm:overtime:create')")
+    public CommonResult<HrmOvertimeApplicationRespVO> currentApplicant() {
+        return success(service.getCurrentApplicant(SecurityFrameworkUtils.getLoginUserId()));
+    }
+    @GetMapping("/page") @PreAuthorize("@ss.hasPermission('hrm:overtime:query')")
+    public CommonResult<PageResult<HrmOvertimeApplicationRespVO>> page(@Valid HrmOvertimeApplicationPageReqVO reqVO) {
+        if (!canManage()) {
+            reqVO.setUserId(SecurityFrameworkUtils.getLoginUserId());
+        }
+        return success(service.getPage(reqVO));
+    }
+    @PostMapping("/submit") @PreAuthorize("@ss.hasPermission('hrm:overtime:submit')")
+    public CommonResult<Boolean> submit(@RequestParam Long id, @RequestParam String processDefinitionKey) {
+        service.submit(id, processDefinitionKey, SecurityFrameworkUtils.getLoginUserId()); return success(true);
+    }
+    @PostMapping("/cancel") @PreAuthorize("@ss.hasPermission('hrm:overtime:cancel')")
+    public CommonResult<Boolean> cancel(@RequestParam Long id) {
+        service.cancel(id, SecurityFrameworkUtils.getLoginUserId()); return success(true);
+    }
+    @GetMapping("/approve-process-definition-list") @PreAuthorize("@ss.hasPermission('hrm:overtime:query')")
+    public CommonResult<List<Map<String, Object>>> processes() { return success(service.getApproveProcessDefinitionList()); }
+
+    private boolean canManage() {
+        return securityFrameworkService.hasPermission("hrm:overtime:manage");
+    }
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmOvertimeApplicationPageReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmOvertimeApplicationPageReqVO.java
new file mode 100644
index 0000000..0a1912d
--- /dev/null
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmOvertimeApplicationPageReqVO.java
@@ -0,0 +1,18 @@
+package cn.iocoder.yudao.module.hrm.controller.admin.approval.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDate;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class HrmOvertimeApplicationPageReqVO extends PageParam {
+    private String no;
+    private Long userId;
+    private Long deptId;
+    private LocalDate overtimeDate;
+    private Integer overtimeType;
+    private Integer status;
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmOvertimeApplicationRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmOvertimeApplicationRespVO.java
new file mode 100644
index 0000000..01fc937
--- /dev/null
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmOvertimeApplicationRespVO.java
@@ -0,0 +1,41 @@
+package cn.iocoder.yudao.module.hrm.controller.admin.approval.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+
+@Schema(description = "绠$悊鍚庡彴 - 鍔犵彮鐢宠 Response VO")
+@Data
+public class HrmOvertimeApplicationRespVO {
+    private Long id;
+    private String no;
+    private Long userId;
+    private Long employeeId;
+    private String employeeName;
+    private String employeeNo;
+    private Long deptId;
+    private String deptName;
+    private String postName;
+    private LocalDate overtimeDate;
+    private LocalTime startTime;
+    private LocalTime endTime;
+    private BigDecimal duration;
+    private Integer overtimeType;
+    private String overtimeTypeName;
+    private Boolean crossDay;
+    private Boolean mealDeduction;
+    private BigDecimal mealDeductionHours;
+    private String reason;
+    private Integer relatedType;
+    private String relatedCode;
+    private String workContent;
+    private Integer status;
+    private String statusName;
+    private String processInstanceId;
+    private LocalDateTime approveTime;
+    private LocalDateTime createTime;
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmOvertimeApplicationSaveReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmOvertimeApplicationSaveReqVO.java
new file mode 100644
index 0000000..ffc71d5
--- /dev/null
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmOvertimeApplicationSaveReqVO.java
@@ -0,0 +1,36 @@
+package cn.iocoder.yudao.module.hrm.controller.admin.approval.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.DecimalMin;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalTime;
+
+@Schema(description = "绠$悊鍚庡彴 - 鍔犵彮鐢宠鏂板/淇敼 Request VO")
+@Data
+public class HrmOvertimeApplicationSaveReqVO {
+    private Long id;
+    @NotNull(message = "鍔犵彮鏃ユ湡涓嶈兘涓虹┖")
+    private LocalDate overtimeDate;
+    @NotNull(message = "寮�濮嬫椂闂翠笉鑳戒负绌�")
+    private LocalTime startTime;
+    @NotNull(message = "缁撴潫鏃堕棿涓嶈兘涓虹┖")
+    private LocalTime endTime;
+    @NotNull(message = "鍔犵彮鏃堕暱涓嶈兘涓虹┖")
+    @DecimalMin(value = "0.5", message = "鍔犵彮鏃堕暱涓嶈兘灏忎簬 0.5 灏忔椂")
+    private BigDecimal duration;
+    @NotNull(message = "鍔犵彮绫诲瀷涓嶈兘涓虹┖")
+    private Integer overtimeType;
+    private Boolean crossDay;
+    private Boolean mealDeduction;
+    private BigDecimal mealDeductionHours;
+    @NotBlank(message = "鍔犵彮鍘熷洜涓嶈兘涓虹┖")
+    private String reason;
+    private Integer relatedType;
+    private String relatedCode;
+    private String workContent;
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalaryStructureController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalaryStructureController.java
new file mode 100644
index 0000000..9f230b6
--- /dev/null
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalaryStructureController.java
@@ -0,0 +1,81 @@
+package cn.iocoder.yudao.module.hrm.controller.admin.salary;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.HrmSalaryStructureRespVO;
+import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.HrmSalaryStructureSaveReqVO;
+import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmSalaryStructureDO;
+import cn.iocoder.yudao.module.hrm.service.salary.HrmSalaryStructureService;
+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.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "绠$悊鍚庡彴 - HRM 钖叕缁撴瀯")
+@RestController
+@RequestMapping("/hrm/salary/structure")
+@Validated
+public class HrmSalaryStructureController {
+
+    @Resource
+    private HrmSalaryStructureService salaryStructureService;
+
+    @PostMapping("/create")
+    @Operation(summary = "鍒涘缓钖叕缁撴瀯")
+    @PreAuthorize("@ss.hasPermission('hrm:salary-structure:create')")
+    public CommonResult<Long> createSalaryStructure(@Valid @RequestBody HrmSalaryStructureSaveReqVO reqVO) {
+        return success(salaryStructureService.createSalaryStructure(
+                BeanUtils.toBean(reqVO, HrmSalaryStructureDO.class)));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "鏇存柊钖叕缁撴瀯")
+    @PreAuthorize("@ss.hasPermission('hrm:salary-structure:update')")
+    public CommonResult<Boolean> updateSalaryStructure(@Valid @RequestBody HrmSalaryStructureSaveReqVO reqVO) {
+        salaryStructureService.updateSalaryStructure(BeanUtils.toBean(reqVO, HrmSalaryStructureDO.class));
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "鍒犻櫎钖叕缁撴瀯")
+    @Parameter(name = "id", description = "缁撴瀯缂栧彿", required = true)
+    @PreAuthorize("@ss.hasPermission('hrm:salary-structure:delete')")
+    public CommonResult<Boolean> deleteSalaryStructure(@RequestParam("id") Long id) {
+        salaryStructureService.deleteSalaryStructure(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "鑾峰緱钖叕缁撴瀯")
+    @PreAuthorize("@ss.hasPermission('hrm:salary-structure:query')")
+    public CommonResult<HrmSalaryStructureRespVO> getSalaryStructure(@RequestParam("id") Long id) {
+        return success(BeanUtils.toBean(salaryStructureService.getSalaryStructure(id),
+                HrmSalaryStructureRespVO.class));
+    }
+
+    @GetMapping("/list")
+    @Operation(summary = "鑾峰緱钖叕缁撴瀯鍒楄〃")
+    @PreAuthorize("@ss.hasPermission('hrm:salary-structure:query')")
+    public CommonResult<List<HrmSalaryStructureRespVO>> getSalaryStructureList(
+            @RequestParam(value = "name", required = false) String name,
+            @RequestParam(value = "status", required = false) Integer status) {
+        return success(BeanUtils.toBean(salaryStructureService.getSalaryStructureList(name, status),
+                HrmSalaryStructureRespVO.class));
+    }
+
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmSalaryStructureRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmSalaryStructureRespVO.java
new file mode 100644
index 0000000..6ea2dba
--- /dev/null
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmSalaryStructureRespVO.java
@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Schema(description = "绠$悊鍚庡彴 - 钖叕缁撴瀯 Response VO")
+@Data
+public class HrmSalaryStructureRespVO {
+
+    @Schema(description = "缁撴瀯缂栧彿")
+    private Long id;
+    @Schema(description = "缁撴瀯鍚嶇О")
+    private String name;
+    @Schema(description = "宸ヤ綔鏃ュ姞鐝伐璧勫�嶇巼")
+    private BigDecimal overtimePayRatio;
+    @Schema(description = "浼戞伅鏃ュ姞鐝伐璧勫�嶇巼")
+    private BigDecimal restDayOvertimePayRatio;
+    @Schema(description = "娉曞畾鑺傚亣鏃ュ姞鐝伐璧勫�嶇巼")
+    private BigDecimal legalHolidayOvertimePayRatio;
+    @Schema(description = "鐘舵�侊細0-鍚敤锛�1-绂佺敤")
+    private Integer status;
+    @Schema(description = "澶囨敞")
+    private String remark;
+    @Schema(description = "鍒涘缓鏃堕棿")
+    private LocalDateTime createTime;
+
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmSalaryStructureSaveReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmSalaryStructureSaveReqVO.java
new file mode 100644
index 0000000..aad0b4c
--- /dev/null
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmSalaryStructureSaveReqVO.java
@@ -0,0 +1,51 @@
+package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.DecimalMax;
+import jakarta.validation.constraints.DecimalMin;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Schema(description = "绠$悊鍚庡彴 - 钖叕缁撴瀯鏂板/淇敼 Request VO")
+@Data
+public class HrmSalaryStructureSaveReqVO {
+
+    @Schema(description = "缁撴瀯缂栧彿", example = "1")
+    private Long id;
+
+    @Schema(description = "缁撴瀯鍚嶇О", requiredMode = Schema.RequiredMode.REQUIRED, example = "鏍囧噯鍛樺伐钖叕")
+    @NotBlank(message = "缁撴瀯鍚嶇О涓嶈兘涓虹┖")
+    @Size(max = 100, message = "缁撴瀯鍚嶇О涓嶈兘瓒呰繃 100 涓瓧绗�")
+    private String name;
+
+    @Schema(description = "宸ヤ綔鏃ュ姞鐝伐璧勫�嶇巼锛�0 琛ㄧず涓嶈鍔犵彮宸ヨ祫", example = "1.50")
+    @NotNull(message = "宸ヤ綔鏃ュ姞鐝�嶇巼涓嶈兘涓虹┖")
+    @DecimalMin(value = "0", message = "宸ヤ綔鏃ュ姞鐝�嶇巼涓嶈兘灏忎簬 0")
+    @DecimalMax(value = "10", message = "宸ヤ綔鏃ュ姞鐝�嶇巼涓嶈兘澶т簬 10")
+    private BigDecimal overtimePayRatio;
+
+    @Schema(description = "浼戞伅鏃ュ姞鐝伐璧勫�嶇巼锛�0 琛ㄧず涓嶈璇ョ被鍨嬪姞鐝伐璧�", example = "2.00")
+    @NotNull(message = "浼戞伅鏃ュ姞鐝�嶇巼涓嶈兘涓虹┖")
+    @DecimalMin(value = "0", message = "浼戞伅鏃ュ姞鐝�嶇巼涓嶈兘灏忎簬 0")
+    @DecimalMax(value = "10", message = "浼戞伅鏃ュ姞鐝�嶇巼涓嶈兘澶т簬 10")
+    private BigDecimal restDayOvertimePayRatio;
+
+    @Schema(description = "娉曞畾鑺傚亣鏃ュ姞鐝伐璧勫�嶇巼锛�0 琛ㄧず涓嶈璇ョ被鍨嬪姞鐝伐璧�", example = "3.00")
+    @NotNull(message = "娉曞畾鑺傚亣鏃ュ姞鐝�嶇巼涓嶈兘涓虹┖")
+    @DecimalMin(value = "0", message = "娉曞畾鑺傚亣鏃ュ姞鐝�嶇巼涓嶈兘灏忎簬 0")
+    @DecimalMax(value = "10", message = "娉曞畾鑺傚亣鏃ュ姞鐝�嶇巼涓嶈兘澶т簬 10")
+    private BigDecimal legalHolidayOvertimePayRatio;
+
+    @Schema(description = "鐘舵�侊細0-鍚敤锛�1-绂佺敤", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
+    @NotNull(message = "鐘舵�佷笉鑳戒负绌�")
+    private Integer status;
+
+    @Schema(description = "澶囨敞")
+    @Size(max = 500, message = "澶囨敞涓嶈兘瓒呰繃 500 涓瓧绗�")
+    private String remark;
+
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/approval/HrmOvertimeApplicationDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/approval/HrmOvertimeApplicationDO.java
new file mode 100644
index 0000000..3bf0e62
--- /dev/null
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/approval/HrmOvertimeApplicationDO.java
@@ -0,0 +1,46 @@
+package cn.iocoder.yudao.module.hrm.dal.dataobject.approval;
+
+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.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+
+@TableName("hrm_overtime_application")
+@KeySequence("hrm_overtime_application_seq")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class HrmOvertimeApplicationDO extends BaseDO {
+    @TableId
+    private Long id;
+    private String no;
+    /** 绯荤粺鐢ㄦ埛 ID */
+    private Long userId;
+    private Long deptId;
+    private Long employeeId;
+    private LocalDate overtimeDate;
+    private LocalTime startTime;
+    private LocalTime endTime;
+    private BigDecimal duration;
+    private Integer overtimeType;
+    private Boolean crossDay;
+    private Boolean mealDeduction;
+    private BigDecimal mealDeductionHours;
+    private String reason;
+    /** 1-椤圭洰锛�2-宸ュ崟 */
+    private Integer relatedType;
+    private String relatedCode;
+    private String workContent;
+    private Integer status;
+    private String processInstanceId;
+    private LocalDateTime approveTime;
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/salary/HrmSalaryStructureDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/salary/HrmSalaryStructureDO.java
index b467169..9c43e3d 100644
--- a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/salary/HrmSalaryStructureDO.java
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/salary/HrmSalaryStructureDO.java
@@ -45,9 +45,19 @@
     private BigDecimal performanceRatio;
 
     /**
-     * 鍔犵彮宸ヨ祫鍊嶇巼
+     * 宸ヤ綔鏃ュ姞鐝伐璧勫�嶇巼
      */
     private BigDecimal overtimePayRatio;
+
+    /**
+     * 浼戞伅鏃ュ姞鐝伐璧勫�嶇巼
+     */
+    private BigDecimal restDayOvertimePayRatio;
+
+    /**
+     * 娉曞畾鑺傚亣鏃ュ姞鐝伐璧勫�嶇巼
+     */
+    private BigDecimal legalHolidayOvertimePayRatio;
 
     /**
      * 绀句繚鍩烘暟
@@ -69,4 +79,4 @@
      */
     private String remark;
 
-}
\ No newline at end of file
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/approval/HrmOvertimeApplicationMapper.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/approval/HrmOvertimeApplicationMapper.java
new file mode 100644
index 0000000..cfcb138
--- /dev/null
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/approval/HrmOvertimeApplicationMapper.java
@@ -0,0 +1,38 @@
+package cn.iocoder.yudao.module.hrm.dal.mysql.approval;
+
+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.approval.vo.HrmOvertimeApplicationPageReqVO;
+import cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmOvertimeApplicationDO;
+import cn.iocoder.yudao.module.hrm.enums.HrmAuditStatusEnum;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.time.LocalDate;
+import java.util.List;
+
+@Mapper
+public interface HrmOvertimeApplicationMapper extends BaseMapperX<HrmOvertimeApplicationDO> {
+
+    default PageResult<HrmOvertimeApplicationDO> selectPage(HrmOvertimeApplicationPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<HrmOvertimeApplicationDO>()
+                .likeIfPresent(HrmOvertimeApplicationDO::getNo, reqVO.getNo())
+                .eqIfPresent(HrmOvertimeApplicationDO::getUserId, reqVO.getUserId())
+                .eqIfPresent(HrmOvertimeApplicationDO::getDeptId, reqVO.getDeptId())
+                .eqIfPresent(HrmOvertimeApplicationDO::getOvertimeDate, reqVO.getOvertimeDate())
+                .eqIfPresent(HrmOvertimeApplicationDO::getOvertimeType, reqVO.getOvertimeType())
+                .eqIfPresent(HrmOvertimeApplicationDO::getStatus, reqVO.getStatus())
+                .orderByDesc(HrmOvertimeApplicationDO::getId));
+    }
+
+    default HrmOvertimeApplicationDO selectByProcessInstanceId(String processInstanceId) {
+        return selectOne(HrmOvertimeApplicationDO::getProcessInstanceId, processInstanceId);
+    }
+
+    default List<HrmOvertimeApplicationDO> selectApprovedByUserAndDateRange(Long userId, LocalDate start, LocalDate end) {
+        return selectList(new LambdaQueryWrapperX<HrmOvertimeApplicationDO>()
+                .eq(HrmOvertimeApplicationDO::getUserId, userId)
+                .eq(HrmOvertimeApplicationDO::getStatus, HrmAuditStatusEnum.APPROVE.getStatus())
+                .between(HrmOvertimeApplicationDO::getOvertimeDate, start, end));
+    }
+}
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 1e992a2..6265746 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
@@ -52,4 +52,8 @@
                 .orderByAsc(HrmEmployeeDO::getName));
     }
 
-}
\ No newline at end of file
+    default Long selectCountBySalaryStructureId(Long salaryStructureId) {
+        return selectCount(HrmEmployeeDO::getSalaryStructureId, salaryStructureId);
+    }
+
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/salary/HrmEmployeeSalaryMapper.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/salary/HrmEmployeeSalaryMapper.java
index d05bee2..4eee062 100644
--- a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/salary/HrmEmployeeSalaryMapper.java
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/salary/HrmEmployeeSalaryMapper.java
@@ -36,4 +36,8 @@
                 .orderByDesc(HrmEmployeeSalaryDO::getEffectiveDate));
     }
 
-}
\ No newline at end of file
+    default Long selectCountBySalaryStructureId(Long salaryStructureId) {
+        return selectCount(HrmEmployeeSalaryDO::getSalaryStructureId, salaryStructureId);
+    }
+
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/salary/HrmSalaryStructureMapper.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/salary/HrmSalaryStructureMapper.java
index 4759301..72edf01 100644
--- a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/salary/HrmSalaryStructureMapper.java
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/salary/HrmSalaryStructureMapper.java
@@ -15,10 +15,19 @@
 @Mapper
 public interface HrmSalaryStructureMapper extends BaseMapperX<HrmSalaryStructureDO> {
 
-    default List<HrmSalaryStructureDO> selectListByStatus(Integer status) {
+    default List<HrmSalaryStructureDO> selectListByCondition(String name, Integer status) {
         return selectList(new LambdaQueryWrapperX<HrmSalaryStructureDO>()
+                .likeIfPresent(HrmSalaryStructureDO::getName, name)
                 .eqIfPresent(HrmSalaryStructureDO::getStatus, status)
                 .orderByAsc(HrmSalaryStructureDO::getName));
     }
 
-}
\ No newline at end of file
+    default List<HrmSalaryStructureDO> selectListByStatus(Integer status) {
+        return selectListByCondition(null, status);
+    }
+
+    default HrmSalaryStructureDO selectByName(String name) {
+        return selectOne(HrmSalaryStructureDO::getName, name);
+    }
+
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/redis/HrmNoRedisDAO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/redis/HrmNoRedisDAO.java
index 44009fd..7f05f3d 100644
--- a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/redis/HrmNoRedisDAO.java
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/redis/HrmNoRedisDAO.java
@@ -52,6 +52,9 @@
      */
     public static final String RESIGNATION_APPLICATION_NO_PREFIX = "RA";
 
+    /** 鍔犵彮鐢宠缂栧彿鍓嶇紑 */
+    public static final String OVERTIME_APPLICATION_NO_PREFIX = "OT";
+
     /**
      * 鍛樺伐鍚堝悓缂栧彿鍓嶇紑
      */
@@ -100,6 +103,10 @@
         return generate(RESIGNATION_APPLICATION_NO_PREFIX, 4);
     }
 
+    public String generateOvertimeApplicationNo() {
+        return generate(OVERTIME_APPLICATION_NO_PREFIX, 4);
+    }
+
     /**
      * 鐢熸垚鍛樺伐鍚堝悓缂栧彿锛屾牸寮忥細HT + yyyyMMdd + 4浣嶈嚜澧�
      * 渚嬪锛欻T202401010001
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmOvertimeApplicationService.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmOvertimeApplicationService.java
new file mode 100644
index 0000000..4cdebad
--- /dev/null
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmOvertimeApplicationService.java
@@ -0,0 +1,26 @@
+package cn.iocoder.yudao.module.hrm.service.approval;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmOvertimeApplicationPageReqVO;
+import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmOvertimeApplicationRespVO;
+import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmOvertimeApplicationSaveReqVO;
+import jakarta.validation.Valid;
+
+import java.util.List;
+import java.util.Map;
+
+public interface HrmOvertimeApplicationService {
+    String BPM_PROCESS_DEFINITION_KEY = "hrm_overtime";
+    String APPROVE_CATEGORY_CODE = "overtime_approve";
+
+    Long create(@Valid HrmOvertimeApplicationSaveReqVO reqVO, Long loginUserId);
+    void update(@Valid HrmOvertimeApplicationSaveReqVO reqVO, Long loginUserId);
+    void delete(Long id, Long loginUserId);
+    HrmOvertimeApplicationRespVO getDetail(Long id, Long loginUserId, boolean canManage);
+    HrmOvertimeApplicationRespVO getCurrentApplicant(Long loginUserId);
+    PageResult<HrmOvertimeApplicationRespVO> getPage(HrmOvertimeApplicationPageReqVO reqVO);
+    void submit(Long id, String processDefinitionKey, Long loginUserId);
+    void cancel(Long id, Long loginUserId);
+    List<Map<String, Object>> getApproveProcessDefinitionList();
+    void updateAuditStatus(Long id, Integer status);
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmOvertimeApplicationServiceImpl.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmOvertimeApplicationServiceImpl.java
new file mode 100644
index 0000000..aa9449d
--- /dev/null
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmOvertimeApplicationServiceImpl.java
@@ -0,0 +1,263 @@
+package cn.iocoder.yudao.module.hrm.service.approval;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.NumberUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi;
+import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
+import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCancelReqVO;
+import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
+import cn.iocoder.yudao.module.bpm.service.definition.BpmCategoryService;
+import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
+import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
+import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService;
+import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.*;
+import cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmOvertimeApplicationDO;
+import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO;
+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.redis.HrmNoRedisDAO;
+import cn.iocoder.yudao.module.hrm.enums.HrmAuditStatusEnum;
+import cn.iocoder.yudao.module.hrm.enums.HrmOvertimeTypeEnum;
+import cn.iocoder.yudao.module.system.api.dept.DeptApi;
+import cn.iocoder.yudao.module.system.api.dept.PostApi;
+import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
+import cn.iocoder.yudao.module.system.api.dept.dto.PostRespDTO;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.flowable.engine.repository.ProcessDefinition;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.stream.Collectors;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants.*;
+
+@Service
+@Slf4j
+public class HrmOvertimeApplicationServiceImpl implements HrmOvertimeApplicationService {
+    @Resource private HrmOvertimeApplicationMapper mapper;
+    @Resource private HrmEmployeeMapper employeeMapper;
+    @Resource private HrmNoRedisDAO noRedisDAO;
+    @Resource private DeptApi deptApi;
+    @Resource private PostApi postApi;
+    @Resource private BpmProcessInstanceApi bpmProcessInstanceApi;
+    @Resource private BpmProcessInstanceService bpmProcessInstanceService;
+    @Resource private BpmTaskService bpmTaskService;
+    @Resource private BpmCategoryService bpmCategoryService;
+    @Resource private BpmProcessDefinitionService bpmProcessDefinitionService;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long create(HrmOvertimeApplicationSaveReqVO reqVO, Long loginUserId) {
+        HrmEmployeeDO employee = requireEmployee(loginUserId);
+        HrmOvertimeApplicationDO application = BeanUtils.toBean(reqVO, HrmOvertimeApplicationDO.class);
+        applyApplicantAndCalculatedTime(application, employee);
+        application.setNo(noRedisDAO.generateOvertimeApplicationNo());
+        application.setStatus(HrmAuditStatusEnum.DRAFT.getStatus());
+        mapper.insert(application);
+        return application.getId();
+    }
+
+    @Override
+    public void update(HrmOvertimeApplicationSaveReqVO reqVO, Long loginUserId) {
+        HrmOvertimeApplicationDO existing = requireOwnedDraft(reqVO.getId(), loginUserId);
+        HrmOvertimeApplicationDO update = BeanUtils.toBean(reqVO, HrmOvertimeApplicationDO.class);
+        applyApplicantAndCalculatedTime(update, requireEmployee(loginUserId));
+        update.setId(existing.getId());
+        mapper.updateById(update);
+    }
+
+    @Override
+    public void delete(Long id, Long loginUserId) {
+        HrmOvertimeApplicationDO application = require(id);
+        boolean deletable = Objects.equals(application.getStatus(), HrmAuditStatusEnum.DRAFT.getStatus())
+                || Objects.equals(application.getStatus(), HrmAuditStatusEnum.CANCEL.getStatus());
+        if (!Objects.equals(application.getUserId(), loginUserId) || !deletable) {
+            throw exception(OVERTIME_APPLICATION_UPDATE_FAIL_NOT_DRAFT);
+        }
+        mapper.deleteById(id);
+    }
+
+    @Override
+    public HrmOvertimeApplicationRespVO getDetail(Long id, Long loginUserId, boolean canManage) {
+        HrmOvertimeApplicationDO application = mapper.selectById(id);
+        if (application != null && !canManage && !Objects.equals(application.getUserId(), loginUserId)
+                && !isProcessParticipant(application.getProcessInstanceId(), loginUserId)) {
+            return null;
+        }
+        return application == null ? null : buildResp(application);
+    }
+
+    @Override
+    public HrmOvertimeApplicationRespVO getCurrentApplicant(Long loginUserId) {
+        HrmEmployeeDO employee = requireEmployee(loginUserId);
+        HrmOvertimeApplicationRespVO resp = new HrmOvertimeApplicationRespVO();
+        fillEmployee(resp, employee);
+        return resp;
+    }
+
+    @Override
+    public PageResult<HrmOvertimeApplicationRespVO> getPage(HrmOvertimeApplicationPageReqVO reqVO) {
+        PageResult<HrmOvertimeApplicationDO> page = mapper.selectPage(reqVO);
+        return new PageResult<>(page.getList().stream().map(this::buildResp).toList(), page.getTotal());
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void submit(Long id, String processDefinitionKey, Long loginUserId) {
+        HrmOvertimeApplicationDO application = requireOwnedDraft(id, loginUserId);
+        validateProcessDefinitions();
+        ProcessDefinition definition = bpmProcessDefinitionService.getActiveProcessDefinition(processDefinitionKey);
+        if (definition == null || !BPM_PROCESS_DEFINITION_KEY.equals(definition.getKey())) {
+            throw exception(OVERTIME_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS);
+        }
+        String processInstanceId = bpmProcessInstanceApi.createProcessInstance(loginUserId,
+                new BpmProcessInstanceCreateReqDTO().setProcessDefinitionKey(processDefinitionKey)
+                        .setBusinessKey(String.valueOf(id)));
+        mapper.updateById(HrmOvertimeApplicationDO.builder().id(id).processInstanceId(processInstanceId)
+                .status(HrmAuditStatusEnum.PROCESS.getStatus()).build());
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void cancel(Long id, Long loginUserId) {
+        HrmOvertimeApplicationDO application = require(id);
+        if (!Objects.equals(application.getUserId(), loginUserId)
+                || !Objects.equals(application.getStatus(), HrmAuditStatusEnum.PROCESS.getStatus())) {
+            throw exception(OVERTIME_APPLICATION_UPDATE_AUDIT_STATUS_FAIL_NOT_PROCESS);
+        }
+        bpmProcessInstanceService.cancelProcessInstanceByStartUser(loginUserId,
+                new BpmProcessInstanceCancelReqVO().setId(application.getProcessInstanceId()).setReason("鐢宠浜烘挙閿�鍔犵彮鐢宠"));
+    }
+
+    @Override
+    public List<Map<String, Object>> getApproveProcessDefinitionList() {
+        validateProcessDefinitions();
+        List<BpmProcessDefinitionInfoDO> infos = bpmProcessDefinitionService
+                .getProcessDefinitionInfoListByCategory(APPROVE_CATEGORY_CODE);
+        Set<String> ids = convertSet(infos, BpmProcessDefinitionInfoDO::getProcessDefinitionId);
+        Map<String, ProcessDefinition> latest = new HashMap<>();
+        for (ProcessDefinition definition : bpmProcessDefinitionService.getProcessDefinitionList(ids)) {
+            if (definition.isSuspended()) continue;
+            latest.compute(definition.getKey(), (key, old) -> old == null || definition.getVersion() > old.getVersion() ? definition : old);
+        }
+        return latest.values().stream().map(item -> {
+            Map<String, Object> value = new HashMap<>();
+            value.put("id", item.getId()); value.put("key", item.getKey()); value.put("name", item.getName());
+            value.put("version", item.getVersion()); return value;
+        }).collect(Collectors.toList());
+    }
+
+    @Override
+    public void updateAuditStatus(Long id, Integer status) {
+        HrmOvertimeApplicationDO application = require(id);
+        if (!Objects.equals(application.getStatus(), HrmAuditStatusEnum.PROCESS.getStatus())) {
+            throw exception(OVERTIME_APPLICATION_UPDATE_AUDIT_STATUS_FAIL_NOT_PROCESS);
+        }
+        mapper.updateById(HrmOvertimeApplicationDO.builder().id(id).status(status)
+                .approveTime(LocalDateTime.now()).build());
+    }
+
+    private void validateProcessDefinitions() {
+        if (!bpmCategoryService.getCategoryMap(Collections.singleton(APPROVE_CATEGORY_CODE)).containsKey(APPROVE_CATEGORY_CODE)) {
+            throw exception(OVERTIME_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS);
+        }
+        List<BpmProcessDefinitionInfoDO> infos = bpmProcessDefinitionService.getProcessDefinitionInfoListByCategory(APPROVE_CATEGORY_CODE);
+        if (CollUtil.isEmpty(infos)) throw exception(OVERTIME_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS);
+        Set<String> ids = convertSet(infos, BpmProcessDefinitionInfoDO::getProcessDefinitionId);
+        if (bpmProcessDefinitionService.getProcessDefinitionList(ids).stream().noneMatch(item -> !item.isSuspended())) {
+            throw exception(OVERTIME_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS);
+        }
+    }
+
+    private void applyApplicantAndCalculatedTime(HrmOvertimeApplicationDO application, HrmEmployeeDO employee) {
+        if (HrmOvertimeTypeEnum.valueOfType(application.getOvertimeType()) == null) {
+            throw exception(OVERTIME_APPLICATION_TIME_INVALID);
+        }
+        application.setUserId(employee.getUserId());
+        application.setEmployeeId(employee.getId());
+        application.setDeptId(employee.getDeptId());
+        long minutes = Duration.between(application.getStartTime(), application.getEndTime()).toMinutes();
+        boolean crossDay = minutes <= 0;
+        if (crossDay) minutes += 24 * 60;
+        BigDecimal mealHours = Boolean.TRUE.equals(application.getMealDeduction())
+                ? Optional.ofNullable(application.getMealDeductionHours()).orElse(BigDecimal.ONE) : BigDecimal.ZERO;
+        BigDecimal rawHours = BigDecimal.valueOf(minutes).divide(BigDecimal.valueOf(60), 2, RoundingMode.HALF_UP);
+        if (Boolean.TRUE.equals(application.getMealDeduction())
+                && (mealHours.compareTo(new BigDecimal("0.5")) < 0
+                || mealHours.compareTo(new BigDecimal("4")) > 0
+                || mealHours.compareTo(rawHours) >= 0)) {
+            throw exception(OVERTIME_APPLICATION_TIME_INVALID);
+        }
+        BigDecimal hours = rawHours
+                .subtract(mealHours).multiply(BigDecimal.valueOf(2)).setScale(0, RoundingMode.HALF_UP)
+                .divide(BigDecimal.valueOf(2), 1, RoundingMode.UNNECESSARY);
+        if (hours.compareTo(BigDecimal.ZERO) <= 0) throw exception(OVERTIME_APPLICATION_TIME_INVALID);
+        application.setCrossDay(crossDay);
+        application.setMealDeductionHours(mealHours);
+        application.setDuration(hours);
+    }
+
+    private HrmOvertimeApplicationDO require(Long id) {
+        HrmOvertimeApplicationDO value = mapper.selectById(id);
+        if (value == null) throw exception(OVERTIME_APPLICATION_NOT_EXISTS);
+        return value;
+    }
+
+    private HrmOvertimeApplicationDO requireOwnedDraft(Long id, Long userId) {
+        HrmOvertimeApplicationDO value = require(id);
+        if (!Objects.equals(value.getUserId(), userId)
+                || !Objects.equals(value.getStatus(), HrmAuditStatusEnum.DRAFT.getStatus())) {
+            throw exception(OVERTIME_APPLICATION_UPDATE_FAIL_NOT_DRAFT);
+        }
+        return value;
+    }
+
+    private HrmEmployeeDO requireEmployee(Long userId) {
+        HrmEmployeeDO employee = employeeMapper.selectByUserId(userId);
+        if (employee == null) throw exception(OVERTIME_APPLICATION_EMPLOYEE_NOT_EXISTS);
+        return employee;
+    }
+
+    /** 褰撳墠寰呭姙瀹℃壒浜烘垨鏇惧鐞嗚繃璇ユ祦绋嬬殑瀹℃壒浜哄彲鏌ョ湅涓氬姟璇︽儏銆� */
+    private boolean isProcessParticipant(String processInstanceId, Long userId) {
+        if (processInstanceId == null || userId == null) {
+            return false;
+        }
+        if (bpmTaskService.getTodoTask(userId, null, processInstanceId) != null) {
+            return true;
+        }
+        return bpmTaskService.getTaskListByProcessInstanceId(processInstanceId, true).stream()
+                .anyMatch(task -> Objects.equals(NumberUtil.parseLong(task.getAssignee(), null), userId)
+                        || Objects.equals(NumberUtil.parseLong(task.getOwner(), null), userId));
+    }
+
+    private HrmOvertimeApplicationRespVO buildResp(HrmOvertimeApplicationDO application) {
+        HrmOvertimeApplicationRespVO resp = BeanUtils.toBean(application, HrmOvertimeApplicationRespVO.class);
+        HrmEmployeeDO employee = employeeMapper.selectById(application.getEmployeeId());
+        if (employee != null) fillEmployee(resp, employee);
+        resp.setOvertimeTypeName(HrmOvertimeTypeEnum.getName(application.getOvertimeType()));
+        resp.setStatusName(HrmAuditStatusEnum.getNameByStatus(application.getStatus()));
+        return resp;
+    }
+
+    private void fillEmployee(HrmOvertimeApplicationRespVO resp, HrmEmployeeDO employee) {
+        resp.setUserId(employee.getUserId()); resp.setEmployeeId(employee.getId());
+        resp.setEmployeeName(employee.getName()); resp.setEmployeeNo(employee.getEmployeeNo());
+        resp.setDeptId(employee.getDeptId());
+        DeptRespDTO dept = employee.getDeptId() == null ? null : deptApi.getDept(employee.getDeptId());
+        PostRespDTO post = employee.getPostId() == null ? null
+                : postApi.getPostMap(Collections.singleton(employee.getPostId())).get(employee.getPostId());
+        resp.setDeptName(dept == null ? null : dept.getName());
+        resp.setPostName(post == null ? null : post.getName());
+    }
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/listener/HrmOvertimeApplicationStatusListener.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/listener/HrmOvertimeApplicationStatusListener.java
new file mode 100644
index 0000000..cabb0c6
--- /dev/null
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/listener/HrmOvertimeApplicationStatusListener.java
@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.module.hrm.service.approval.listener;
+
+import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent;
+import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEventListener;
+import cn.iocoder.yudao.module.hrm.enums.HrmAuditStatusEnum;
+import cn.iocoder.yudao.module.hrm.service.approval.HrmOvertimeApplicationService;
+import cn.iocoder.yudao.module.hrm.util.HrmAuditStatusUtils;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Component;
+
+@Component
+public class HrmOvertimeApplicationStatusListener extends BpmProcessInstanceStatusEventListener {
+    @Resource private HrmOvertimeApplicationService service;
+    @Override protected String getProcessDefinitionKey() { return HrmOvertimeApplicationService.BPM_PROCESS_DEFINITION_KEY; }
+    @Override protected void onEvent(BpmProcessInstanceStatusEvent event) {
+        Integer status = HrmAuditStatusUtils.convertBpmResultToAuditStatus(event.getStatus());
+        if (status == null) status = HrmAuditStatusEnum.PROCESS.getStatus();
+        service.updateAuditStatus(Long.valueOf(event.getBusinessKey()), status);
+    }
+}
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 8279b4e..8be05ab 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
@@ -5,6 +5,7 @@
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 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.employee.HrmEmployeeDO;
 import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmEmployeeSalaryDO;
 import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmEmployeeSocialSecurityDO;
@@ -13,7 +14,9 @@
 import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmSalaryPaymentDetailDO;
 import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmSalaryPaymentDO;
 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.approval.HrmOvertimeApplicationMapper;
 import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeMapper;
 import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmEmployeeSalaryMapper;
 import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmEmployeeSocialSecurityMapper;
@@ -22,8 +25,10 @@
 import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmSalaryPaymentDetailMapper;
 import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmSalaryPaymentMapper;
 import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmSocialSecuritySchemeMapper;
+import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmSalaryStructureMapper;
 import cn.iocoder.yudao.module.hrm.dal.redis.HrmNoRedisDAO;
 import cn.iocoder.yudao.module.system.api.dept.DeptApi;
+import cn.iocoder.yudao.module.hrm.enums.HrmOvertimeTypeEnum;
 import jakarta.annotation.Resource;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -82,6 +87,10 @@
     @Resource
     private HrmLeaveApplicationMapper leaveApplicationMapper;
     @Resource
+    private HrmOvertimeApplicationMapper overtimeApplicationMapper;
+    @Resource
+    private HrmSalaryStructureMapper salaryStructureMapper;
+    @Resource
     private HrmLeaveTypeConfigMapper leaveTypeConfigMapper;
     @Resource
     private HrmSalaryPaymentMapper salaryPaymentMapper;
@@ -130,7 +139,10 @@
                 if (existCalculations.size() > 1) {
                     throw exception(SALARY_CALCULATION_DUPLICATE, employee.getName(), period);
                 }
-                continue; // 宸插瓨鍦ㄥ垯璺宠繃
+                // 宸茬‘璁ゆ垨宸插彂鏀剧殑璁板綍涓嶅彲鑷姩鏀瑰啓锛涘緟纭璁板綍鍏佽閲嶇畻锛屼互鍚告敹鍚庢潵瀹℃壒閫氳繃鐨勫姞鐝��
+                if (existCalculations.get(0).getStatus() != 0) {
+                    continue;
+                }
             }
 
             // 2.2 鑾峰彇鍛樺伐钖叕妗f锛堟寜鍛樺伐ID鏌ヨ锛�
@@ -151,15 +163,20 @@
             // 2.5 鏋勫缓鏍哥畻璁板綍锛堝寘鍚墸闄よ绠楋紝閫氳繃鏂规鑾峰彇姣斾緥锛�
             HrmSalaryCalculationDO calculation = buildSalaryCalculation(period, employeeId, employee, employeeSalary, socialSecurity, scheme, effectiveDate);
 
-            // 2.6 鐢熸垚鏍哥畻鍗曞彿
-            String no = noRedisDAO.generateSalaryCalculationNo();
-            while (salaryCalculationMapper.selectByNo(no) != null) {
-                no = noRedisDAO.generateSalaryCalculationNo();
+            // 2.6 淇濆瓨锛氶娆$敓鎴愬崟鍙凤紱宸叉湁寰呯‘璁よ褰曞垯淇濈暀鍗曞彿骞惰鐩栭噸绠楃粨鏋�
+            if (CollUtil.isNotEmpty(existCalculations)) {
+                HrmSalaryCalculationDO existing = existCalculations.get(0);
+                calculation.setId(existing.getId());
+                calculation.setNo(existing.getNo());
+                salaryCalculationMapper.updateById(calculation);
+            } else {
+                String no = noRedisDAO.generateSalaryCalculationNo();
+                while (salaryCalculationMapper.selectByNo(no) != null) {
+                    no = noRedisDAO.generateSalaryCalculationNo();
+                }
+                calculation.setNo(no);
+                salaryCalculationMapper.insert(calculation);
             }
-            calculation.setNo(no);
-
-            // 2.7 淇濆瓨鏍哥畻璁板綍
-            salaryCalculationMapper.insert(calculation);
         }
     }
 
@@ -174,13 +191,14 @@
     }
 
     /**
-     * 鏋勫缓榛樿鐨勫憳宸ヨ柂閰。妗堬紙鍩轰簬鍛樺伐琛ㄧ殑鍩烘湰宸ヨ祫锛�
+     * 鏋勫缓榛樿鐨勫憳宸ヨ柂閰。妗堛�傚熀鏈伐璧勭粺涓�鍙栧憳宸ユ。妗堬紝缁╂晥宸ヨ祫榛樿涓嶈嚜鍔ㄧ敓鎴愩��
      */
     private HrmEmployeeSalaryDO buildDefaultEmployeeSalary(Long employeeId, HrmEmployeeDO employee) {
+        BigDecimal baseSalary = employee.getBaseSalary() != null ? employee.getBaseSalary() : BigDecimal.ZERO;
         return HrmEmployeeSalaryDO.builder()
                 .userId(employeeId) // 浣跨敤鍛樺伐ID
                 .salaryStructureId(employee.getSalaryStructureId())
-                .baseSalary(employee.getBaseSalary() != null ? employee.getBaseSalary() : BigDecimal.ZERO)
+                .baseSalary(baseSalary)
                 .performanceSalary(BigDecimal.ZERO)
                 .mealAllowance(BigDecimal.ZERO)
                 .transportAllowance(BigDecimal.ZERO)
@@ -204,17 +222,18 @@
         BigDecimal mealAllowance = employeeSalary.getMealAllowance() != null ? employeeSalary.getMealAllowance() : BigDecimal.ZERO;
         BigDecimal transportAllowance = employeeSalary.getTransportAllowance() != null ? employeeSalary.getTransportAllowance() : BigDecimal.ZERO;
         BigDecimal otherAllowance = employeeSalary.getOtherAllowance() != null ? employeeSalary.getOtherAllowance() : BigDecimal.ZERO;
-        BigDecimal overtimePay = BigDecimal.ZERO;
+        OvertimeSalary overtimeSalary = calculateOvertimeSalary(employee, employeeSalary, period, baseSalary);
+        BigDecimal overtimePay = overtimeSalary.pay();
 
         // 搴斿彂宸ヨ祫鎬婚
         BigDecimal totalIncome = baseSalary.add(performanceSalary).add(mealAllowance)
                 .add(transportAllowance).add(otherAllowance).add(overtimePay);
 
-        // 鑾峰彇绀句繚鍩烘暟锛堝憳宸ユ。妗堜腑鐨勪釜鎬у寲鍩烘暟锛�
+        // 鑾峰彇绀句繚鍩烘暟锛氬憳宸ョぞ淇濇。妗� > 鍛樺伐鍩烘湰宸ヨ祫
         BigDecimal socialSecurityBase = (socialSecurity != null && socialSecurity.getSocialSecurityBase() != null)
                 ? socialSecurity.getSocialSecurityBase() : baseSalary;
 
-        // 鑾峰彇鍏Н閲戝熀鏁帮紙鍛樺伐妗f涓殑涓�у寲鍩烘暟锛�
+        // 鑾峰彇鍏Н閲戝熀鏁帮細鍛樺伐绀句繚妗f > 鍛樺伐鍩烘湰宸ヨ祫
         BigDecimal housingFundBase = (socialSecurity != null && socialSecurity.getHousingFundBase() != null)
                 ? socialSecurity.getHousingFundBase() : baseSalary;
 
@@ -277,7 +296,7 @@
                 .leaveDeduction(leaveDeduction)
                 .actualSalary(actualSalary)
                 .workDays(22) // 榛樿宸ヤ綔鏃�
-                .overtimeHours(BigDecimal.ZERO)
+                .overtimeHours(overtimeSalary.hours())
                 .status(0) // 寰呯‘璁�
                 .build();
     }
@@ -333,6 +352,54 @@
 
         return totalLeaveDeduction;
     }
+
+    /**
+     * 姹囨�诲綋鏈堝鎵归�氳繃鐨勫姞鐝敵璇枫�傚皬鏃跺伐璧勬寜鍩烘湰宸ヨ祫 / 21.75 / 8 璁$畻锛�
+     * 宸ヤ綔鏃ラ粯璁� 1.5 鍊嶃�佷紤鎭棩榛樿 2 鍊嶃�佹硶瀹氳妭鍋囨棩榛樿 3 鍊嶏紝鍧囧彲鐢辫柂閰粨鏋勮鐩栥��
+     */
+    private OvertimeSalary calculateOvertimeSalary(HrmEmployeeDO employee, HrmEmployeeSalaryDO employeeSalary,
+                                                    String period, BigDecimal baseSalary) {
+        if (employee.getUserId() == null) {
+            return new OvertimeSalary(BigDecimal.ZERO, BigDecimal.ZERO);
+        }
+        YearMonth month = YearMonth.parse(period);
+        List<HrmOvertimeApplicationDO> applications = overtimeApplicationMapper.selectApprovedByUserAndDateRange(
+                employee.getUserId(), month.atDay(1), month.atEndOfMonth());
+        if (CollUtil.isEmpty(applications)) {
+            return new OvertimeSalary(BigDecimal.ZERO, BigDecimal.ZERO);
+        }
+        Long structureId = employeeSalary.getSalaryStructureId() != null
+                ? employeeSalary.getSalaryStructureId() : employee.getSalaryStructureId();
+        HrmSalaryStructureDO structure = structureId == null ? null : salaryStructureMapper.selectById(structureId);
+        BigDecimal workdayMultiplier = structure != null && structure.getOvertimePayRatio() != null
+                ? structure.getOvertimePayRatio() : HrmOvertimeTypeEnum.WORKDAY.getSalaryMultiplier();
+        BigDecimal restDayMultiplier = structure != null && structure.getRestDayOvertimePayRatio() != null
+                ? structure.getRestDayOvertimePayRatio() : HrmOvertimeTypeEnum.REST_DAY.getSalaryMultiplier();
+        BigDecimal legalHolidayMultiplier = structure != null && structure.getLegalHolidayOvertimePayRatio() != null
+                ? structure.getLegalHolidayOvertimePayRatio() : HrmOvertimeTypeEnum.LEGAL_HOLIDAY.getSalaryMultiplier();
+        BigDecimal hourlySalary = baseSalary.divide(new BigDecimal("21.75"), 8, RoundingMode.HALF_UP)
+                .divide(new BigDecimal("8"), 8, RoundingMode.HALF_UP);
+        BigDecimal totalHours = BigDecimal.ZERO;
+        BigDecimal totalPay = BigDecimal.ZERO;
+        for (HrmOvertimeApplicationDO application : applications) {
+            BigDecimal hours = application.getDuration() == null ? BigDecimal.ZERO : application.getDuration();
+            HrmOvertimeTypeEnum type = HrmOvertimeTypeEnum.valueOfType(application.getOvertimeType());
+            if (type == null || hours.signum() <= 0) {
+                continue;
+            }
+            BigDecimal multiplier = switch (type) {
+                case WORKDAY -> workdayMultiplier;
+                case REST_DAY -> restDayMultiplier;
+                case LEGAL_HOLIDAY -> legalHolidayMultiplier;
+            };
+            totalHours = totalHours.add(hours);
+            totalPay = totalPay.add(hourlySalary.multiply(hours).multiply(multiplier));
+        }
+        return new OvertimeSalary(totalHours.setScale(1, RoundingMode.HALF_UP),
+                totalPay.setScale(2, RoundingMode.HALF_UP));
+    }
+
+    private record OvertimeSalary(BigDecimal hours, BigDecimal pay) {}
 
     @Override
     @Transactional(rollbackFor = Exception.class)
@@ -577,4 +644,4 @@
         salaryCalculationMapper.deleteById(id);
     }
 
-}
\ No newline at end of file
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryStructureService.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryStructureService.java
new file mode 100644
index 0000000..d8a848e
--- /dev/null
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryStructureService.java
@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.module.hrm.service.salary;
+
+import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmSalaryStructureDO;
+
+import java.util.List;
+
+public interface HrmSalaryStructureService {
+
+    Long createSalaryStructure(HrmSalaryStructureDO structure);
+
+    void updateSalaryStructure(HrmSalaryStructureDO structure);
+
+    void deleteSalaryStructure(Long id);
+
+    HrmSalaryStructureDO getSalaryStructure(Long id);
+
+    List<HrmSalaryStructureDO> getSalaryStructureList(String name, Integer status);
+
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryStructureServiceImpl.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryStructureServiceImpl.java
new file mode 100644
index 0000000..5659c58
--- /dev/null
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryStructureServiceImpl.java
@@ -0,0 +1,79 @@
+package cn.iocoder.yudao.module.hrm.service.salary;
+
+import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmSalaryStructureDO;
+import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeMapper;
+import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmEmployeeSalaryMapper;
+import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmSalaryStructureMapper;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants.SALARY_STRUCTURE_IN_USE;
+import static cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants.SALARY_STRUCTURE_NAME_EXISTS;
+import static cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants.SALARY_STRUCTURE_NOT_EXISTS;
+
+@Service
+@Validated
+public class HrmSalaryStructureServiceImpl implements HrmSalaryStructureService {
+
+    @Resource
+    private HrmSalaryStructureMapper salaryStructureMapper;
+    @Resource
+    private HrmEmployeeMapper employeeMapper;
+    @Resource
+    private HrmEmployeeSalaryMapper employeeSalaryMapper;
+
+    @Override
+    public Long createSalaryStructure(HrmSalaryStructureDO structure) {
+        validateNameUnique(null, structure.getName());
+        // 鍩烘湰宸ヨ祫鐢卞憳宸ユ。妗堢淮鎶ゃ�傛暟鎹簱鍘嗗彶瀛楁闈炵┖锛屾柊寤虹粨鏋勭粺涓�鍐欏叆鍗犱綅鍊� 0銆�
+        structure.setBaseSalary(BigDecimal.ZERO);
+        salaryStructureMapper.insert(structure);
+        return structure.getId();
+    }
+
+    @Override
+    public void updateSalaryStructure(HrmSalaryStructureDO structure) {
+        validateExists(structure.getId());
+        validateNameUnique(structure.getId(), structure.getName());
+        salaryStructureMapper.updateById(structure);
+    }
+
+    @Override
+    public void deleteSalaryStructure(Long id) {
+        validateExists(id);
+        if (employeeMapper.selectCountBySalaryStructureId(id) > 0
+                || employeeSalaryMapper.selectCountBySalaryStructureId(id) > 0) {
+            throw exception(SALARY_STRUCTURE_IN_USE);
+        }
+        salaryStructureMapper.deleteById(id);
+    }
+
+    @Override
+    public HrmSalaryStructureDO getSalaryStructure(Long id) {
+        return salaryStructureMapper.selectById(id);
+    }
+
+    @Override
+    public List<HrmSalaryStructureDO> getSalaryStructureList(String name, Integer status) {
+        return salaryStructureMapper.selectListByCondition(name, status);
+    }
+
+    private void validateExists(Long id) {
+        if (id == null || salaryStructureMapper.selectById(id) == null) {
+            throw exception(SALARY_STRUCTURE_NOT_EXISTS);
+        }
+    }
+
+    private void validateNameUnique(Long id, String name) {
+        HrmSalaryStructureDO structure = salaryStructureMapper.selectByName(name);
+        if (structure != null && !structure.getId().equals(id)) {
+            throw exception(SALARY_STRUCTURE_NAME_EXISTS);
+        }
+    }
+
+}

--
Gitblit v1.9.3