From b852254d90e7d1955db31db154193ec8ee84d8b3 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期三, 09 九月 2026 10:57:59 +0800
Subject: [PATCH] feat(hrm): 新增员工用工性质、班组归属字段及考勤异常处理优化

---
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryCalculationServiceImpl.java |  109 ++++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 files changed, 105 insertions(+), 4 deletions(-)

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..b6b375f 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,22 +5,30 @@
 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.attendance.HrmAttendanceSummaryDO;
 import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO;
+import cn.iocoder.yudao.module.hrm.dal.dataobject.performanceappraise.HrmPerformanceAppraiseDO;
+import cn.iocoder.yudao.module.hrm.dal.dataobject.performanceappraise.HrmPerformanceAppraiseResultDO;
 import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmEmployeeSalaryDO;
 import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmEmployeeSocialSecurityDO;
 import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmLeaveTypeConfigDO;
 import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmSalaryCalculationDO;
 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.HrmSalaryStructureDO;
 import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmSocialSecuritySchemeDO;
 import cn.iocoder.yudao.module.hrm.dal.mysql.approval.HrmLeaveApplicationMapper;
+import cn.iocoder.yudao.module.hrm.dal.mysql.attendance.HrmAttendanceSummaryMapper;
 import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeMapper;
+import cn.iocoder.yudao.module.hrm.dal.mysql.performanceappraise.HrmPerformanceAppraiseMapper;
+import cn.iocoder.yudao.module.hrm.dal.mysql.performanceappraise.HrmPerformanceAppraiseResultMapper;
 import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmEmployeeSalaryMapper;
 import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmEmployeeSocialSecurityMapper;
 import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmLeaveTypeConfigMapper;
 import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmSalaryCalculationMapper;
 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.HrmSalaryStructureMapper;
 import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmSocialSecuritySchemeMapper;
 import cn.iocoder.yudao.module.hrm.dal.redis.HrmNoRedisDAO;
 import cn.iocoder.yudao.module.system.api.dept.DeptApi;
@@ -63,6 +71,24 @@
     private static final BigDecimal DEFAULT_UNEMPLOYMENT_PERSONAL_RATIO = new BigDecimal("0.005");
     private static final BigDecimal DEFAULT_HOUSING_FUND_PERSONAL_RATIO = new BigDecimal("0.12");
 
+    /**
+     * 鍔犵彮宸ヨ祫鍊嶇巼榛樿鍊硷紙钖叕缁撴瀯鏈厤缃椂浣跨敤锛�
+     */
+    private static final BigDecimal DEFAULT_OVERTIME_PAY_RATIO = new BigDecimal("1.5");
+
+    /**
+     * 榛樿搴斿嚭鍕ゅぉ鏁帮紙鏃犺�冨嫟姹囨�绘椂鐨勫厹搴曞�硷級
+     */
+    private static final int DEFAULT_WORK_DAYS = 22;
+
+    /**
+     * 缁╂晥宸ヨ祫妗g骇绯绘暟锛�1-浼樼 2-鑹ソ 3-鍚堟牸 4-寰呮敼杩�
+     */
+    private static final BigDecimal PERFORMANCE_GRADE_FACTOR_EXCELLENT = new BigDecimal("1.2");
+    private static final BigDecimal PERFORMANCE_GRADE_FACTOR_GOOD = new BigDecimal("1.0");
+    private static final BigDecimal PERFORMANCE_GRADE_FACTOR_PASS = new BigDecimal("0.8");
+    private static final BigDecimal PERFORMANCE_GRADE_FACTOR_IMPROVE = new BigDecimal("0.6");
+
     @Resource
     private HrmSalaryCalculationMapper salaryCalculationMapper;
     @Resource
@@ -87,6 +113,14 @@
     private HrmSalaryPaymentMapper salaryPaymentMapper;
     @Resource
     private HrmSalaryPaymentDetailMapper salaryPaymentDetailMapper;
+    @Resource
+    private HrmAttendanceSummaryMapper attendanceSummaryMapper;
+    @Resource
+    private HrmPerformanceAppraiseMapper performanceAppraiseMapper;
+    @Resource
+    private HrmPerformanceAppraiseResultMapper performanceAppraiseResultMapper;
+    @Resource
+    private HrmSalaryStructureMapper salaryStructureMapper;
 
     @Override
     public PageResult<HrmSalaryCalculationDO> getSalaryCalculationPage(HrmSalaryCalculationPageReqVO pageReqVO) {
@@ -204,7 +238,21 @@
         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;
+        // ===== 鑰冨嫟閾捐矾锛氬簲鍑哄嫟澶╂暟 / 鍔犵彮鏃堕暱 / 缂哄嫟鎵f =====
+        HrmAttendanceSummaryDO attendanceSummary = attendanceSummaryMapper.selectByUserIdAndYearMonth(employeeId, period);
+        int workDays = attendanceSummary != null && attendanceSummary.getWorkDays() != null
+                ? attendanceSummary.getWorkDays() : DEFAULT_WORK_DAYS;
+        BigDecimal overtimeHours = attendanceSummary != null && attendanceSummary.getOvertimeHours() != null
+                ? attendanceSummary.getOvertimeHours() : BigDecimal.ZERO;
+        // ===== 鍔犵彮宸ヨ祫 = 鏃惰柂 脳 鍔犵彮鏃堕暱 脳 鍊嶇巼锛堟椂钖� = 鍩烘湰宸ヨ祫 梅 (搴斿嚭鍕ゅぉ 脳 8)锛� =====
         BigDecimal overtimePay = BigDecimal.ZERO;
+        if (overtimeHours.compareTo(BigDecimal.ZERO) > 0 && workDays > 0) {
+            BigDecimal hourlyRate = baseSalary.divide(BigDecimal.valueOf(workDays * 8L), 4, RoundingMode.HALF_UP);
+            overtimePay = hourlyRate.multiply(overtimeHours).multiply(getOvertimePayRatio(employeeSalary, employee))
+                    .setScale(2, RoundingMode.HALF_UP);
+        }
+        // ===== 缁╂晥宸ヨ祫锛氭寜褰撴湀缁╂晥缁撴灉妗g骇绯绘暟鎶樼畻锛堟棤缁撴灉鎸夊叏棰濓級 =====
+        performanceSalary = calculatePerformanceSalary(performanceSalary, employeeId, period);
 
         // 搴斿彂宸ヨ祫鎬婚
         BigDecimal totalIncome = baseSalary.add(performanceSalary).add(mealAllowance)
@@ -238,8 +286,8 @@
         // 鍏Н閲戞墸闄� = 鍩烘暟 脳 鍏Н閲戜釜浜烘瘮渚�
         BigDecimal housingFundDeduction = housingFundBase.multiply(housingFundPersonalRatio).setScale(2, RoundingMode.HALF_UP);
 
-        // 璁$畻璇峰亣鎵f
-        BigDecimal leaveDeduction = calculateLeaveDeduction(employee, period, baseSalary);
+        // 璁$畻缂哄嫟鎵f锛堣�冨嫟鍙e緞锛氱己鍕ゅぉ鏁� = 搴斿嚭鍕� 鈭� 瀹為檯鍑哄嫟锛涙棤鑰冨嫟姹囨�绘椂鍥為��鍒拌鍋囩敵璇峰彛寰勶級
+        BigDecimal leaveDeduction = calculateDeduction(attendanceSummary, baseSalary, workDays, employee, period);
 
         // 璁$畻涓◣鎵i櫎
         // 搴旂撼绋庢墍寰楅 = 搴斿彂宸ヨ祫 - 绀句繚鎵i櫎 - 鍏Н閲戞墸闄� - 璧峰緛鐐�(5000)
@@ -276,13 +324,66 @@
                 .otherDeduction(otherDeduction)
                 .leaveDeduction(leaveDeduction)
                 .actualSalary(actualSalary)
-                .workDays(22) // 榛樿宸ヤ綔鏃�
-                .overtimeHours(BigDecimal.ZERO)
+                .workDays(workDays) // 鍔ㄦ�佸簲鍑哄嫟澶╂暟锛堟潵鑷�冨嫟姹囨�伙級
+                .overtimeHours(overtimeHours) // 鍔犵彮鏃堕暱锛堟潵鑷�冨嫟姹囨�伙級
                 .status(0) // 寰呯‘璁�
                 .build();
     }
 
     /**
+     * 璁$畻缂哄嫟鎵f锛氫紭鍏堟寜鑰冨嫟姹囨�伙紙缂哄嫟澶╂暟 = 搴斿嚭鍕� 鈭� 瀹為檯鍑哄嫟锛夛紱鏃犳眹鎬绘椂鍥為��鍒拌鍋囩敵璇峰彛寰�
+     */
+    private BigDecimal calculateDeduction(HrmAttendanceSummaryDO attendanceSummary, BigDecimal baseSalary,
+                                          int workDays, HrmEmployeeDO employee, String period) {
+        if (attendanceSummary != null && workDays > 0 && attendanceSummary.getActualDays() != null) {
+            int absentDays = Math.max(0, workDays - attendanceSummary.getActualDays());
+            if (absentDays == 0) {
+                return BigDecimal.ZERO;
+            }
+            BigDecimal daySalary = baseSalary.divide(BigDecimal.valueOf(workDays), 4, RoundingMode.HALF_UP);
+            return daySalary.multiply(BigDecimal.valueOf(absentDays)).setScale(2, RoundingMode.HALF_UP);
+        }
+        return calculateLeaveDeduction(employee, period, baseSalary);
+    }
+
+    /**
+     * 璁$畻缁╂晥宸ヨ祫锛氭寜褰撴湀宸插畬鎴愪笖鍖归厤鏍哥畻鍛ㄦ湡鐨勬湀搴︾哗鏁堣�冩牳缁撴灉鐨勬。绾х郴鏁版姌绠�
+     */
+    private BigDecimal calculatePerformanceSalary(BigDecimal performanceSalary, Long employeeId, String period) {
+        HrmPerformanceAppraiseDO appraise = performanceAppraiseMapper.selectMonthlyCompletedByPeriod(period);
+        if (appraise == null) {
+            return performanceSalary;
+        }
+        HrmPerformanceAppraiseResultDO result = performanceAppraiseResultMapper
+                .selectByAppraiseIdAndEmployeeId(appraise.getId(), employeeId);
+        if (result == null || result.getGrade() == null) {
+            return performanceSalary;
+        }
+        BigDecimal factor = switch (result.getGrade()) {
+            case 1 -> PERFORMANCE_GRADE_FACTOR_EXCELLENT;
+            case 2 -> PERFORMANCE_GRADE_FACTOR_GOOD;
+            case 3 -> PERFORMANCE_GRADE_FACTOR_PASS;
+            case 4 -> PERFORMANCE_GRADE_FACTOR_IMPROVE;
+            default -> BigDecimal.ONE;
+        };
+        return performanceSalary.multiply(factor).setScale(2, RoundingMode.HALF_UP);
+    }
+
+    /**
+     * 鑾峰彇鍔犵彮宸ヨ祫鍊嶇巼锛堣柂閰粨鏋勬湭閰嶇疆鏃堕粯璁� 1.5锛�
+     */
+    private BigDecimal getOvertimePayRatio(HrmEmployeeSalaryDO employeeSalary, HrmEmployeeDO employee) {
+        Long structureId = employeeSalary.getSalaryStructureId() != null
+                ? employeeSalary.getSalaryStructureId() : employee.getSalaryStructureId();
+        if (structureId == null) {
+            return DEFAULT_OVERTIME_PAY_RATIO;
+        }
+        HrmSalaryStructureDO structure = salaryStructureMapper.selectById(structureId);
+        return structure != null && structure.getOvertimePayRatio() != null
+                ? structure.getOvertimePayRatio() : DEFAULT_OVERTIME_PAY_RATIO;
+    }
+
+    /**
      * 璁$畻璇峰亣鎵f
      *
      * @param employee   鍛樺伐淇℃伅

--
Gitblit v1.9.3