huminmin
4 天以前 a50e3f61c7b45cd9e11364519e83071b372c4b9d
加班申请
已修改11个文件
已添加18个文件
1138 ■■■■■ 文件已修改
docs/projectSql/ruoyi-vue-pro.sql 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/hrm_module_init.sql 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/hrm_overtime_application.sql 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/hrm_salary_structure_menu.sql 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/hrm_salary_structure_overtime_ratio.sql 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtils.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtilsTest.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/ErrorCodeConstants.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/HrmOvertimeTypeEnum.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/HrmOvertimeApplicationController.java 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmOvertimeApplicationPageReqVO.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmOvertimeApplicationRespVO.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmOvertimeApplicationSaveReqVO.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalaryStructureController.java 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmSalaryStructureRespVO.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmSalaryStructureSaveReqVO.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/approval/HrmOvertimeApplicationDO.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/salary/HrmSalaryStructureDO.java 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/approval/HrmOvertimeApplicationMapper.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/employee/HrmEmployeeMapper.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/salary/HrmEmployeeSalaryMapper.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/salary/HrmSalaryStructureMapper.java 13 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/redis/HrmNoRedisDAO.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmOvertimeApplicationService.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmOvertimeApplicationServiceImpl.java 263 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/listener/HrmOvertimeApplicationStatusListener.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryCalculationServiceImpl.java 99 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryStructureService.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryStructureServiceImpl.java 79 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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-禁用',
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;
SELECT 'HRM äººåŠ›èµ„æºæ¨¡å— SQL åˆå§‹åŒ–完成!' AS result;
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 '扣除用餐后的加班时长(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 '是否扣除用餐时间',
    `meal_deduction_hours` decimal(4,1) NOT NULL DEFAULT 0 COMMENT '用餐扣除小时数',
    `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;
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;
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`;
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();
        }
        // è§£æžè¡¨å•配置
        Map<String, BpmFormFieldVO> formFieldsMap = new LinkedHashMap<>();
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()
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, "薪酬结构正在被员工使用,无法删除");
}
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;
    }
}
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");
    }
}
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;
}
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;
}
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;
}
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));
    }
}
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;
}
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;
}
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;
}
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;
}
}
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));
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/employee/HrmEmployeeMapper.java
@@ -52,4 +52,8 @@
                .orderByAsc(HrmEmployeeDO::getName));
    }
}
    default Long selectCountBySalaryStructureId(Long salaryStructureId) {
        return selectCount(HrmEmployeeDO::getSalaryStructureId, salaryStructureId);
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/salary/HrmEmployeeSalaryMapper.java
@@ -36,4 +36,8 @@
                .orderByDesc(HrmEmployeeSalaryDO::getEffectiveDate));
    }
}
    default Long selectCountBySalaryStructureId(Long salaryStructureId) {
        return selectCount(HrmEmployeeSalaryDO::getSalaryStructureId, salaryStructureId);
    }
}
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));
    }
}
    default List<HrmSalaryStructureDO> selectListByStatus(Integer status) {
        return selectListByCondition(null, status);
    }
    default HrmSalaryStructureDO selectByName(String name) {
        return selectOne(HrmSalaryStructureDO::getName, name);
    }
}
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位自增
     * ä¾‹å¦‚:HT202401010001
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);
}
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());
    }
}
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);
    }
}
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 èŽ·å–å‘˜å·¥è–ªé…¬æ¡£æ¡ˆï¼ˆæŒ‰å‘˜å·¥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;
        // èŽ·å–å…¬ç§¯é‡‘åŸºæ•°ï¼ˆå‘˜å·¥æ¡£æ¡ˆä¸­çš„ä¸ªæ€§åŒ–åŸºæ•°ï¼‰
        // èŽ·å–å…¬ç§¯é‡‘åŸºæ•°ï¼šå‘˜å·¥ç¤¾ä¿æ¡£æ¡ˆ > å‘˜å·¥åŸºæœ¬å·¥èµ„
        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);
    }
}
}
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);
}
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);
        }
    }
}