From f81b963f9d1b7d4757446970830cb72264c6dc0c Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期三, 04 十一月 2026 15:18:57 +0800
Subject: [PATCH] 九鸿:工资表按客户口径重构 + 修复考勤汇总生成、新增节假日报错

---
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryCalculationServiceImpl.java |  393 ++++++++++++++++++++++++++++++++++++++++++++++++++-----
 1 files changed, 353 insertions(+), 40 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 ffea59e..edc86ab 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,10 @@
 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.attendance.HrmAttendanceRecordDO;
+import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmEmployeeScheduleDO;
+import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmShiftDO;
 import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO;
 import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmEmployeeSalaryDO;
 import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmEmployeeSocialSecurityDO;
@@ -13,7 +17,12 @@
 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.attendance.HrmEmployeeScheduleMapper;
+import cn.iocoder.yudao.module.hrm.dal.mysql.attendance.HrmShiftMapper;
+import cn.iocoder.yudao.module.hrm.dal.mysql.approval.HrmOvertimeApplicationMapper;
+import cn.iocoder.yudao.module.hrm.dal.mysql.attendance.HrmAttendanceRecordMapper;
 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 +31,11 @@
 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.HrmAttendanceClockTypeEnum;
+import cn.iocoder.yudao.module.hrm.enums.HrmOvertimeTypeEnum;
 import jakarta.annotation.Resource;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -34,8 +46,12 @@
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.YearMonth;
+import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
 import java.util.stream.Collectors;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -44,7 +60,7 @@
 /**
  * 閰牳绠� Service 瀹炵幇绫�
  *
- * @author 鑺嬮亾婧愮爜
+ * @author 瓒呯骇绠$悊鍛�
  */
 @Service
 @Validated
@@ -74,6 +90,8 @@
     @Resource
     private HrmSocialSecuritySchemeMapper socialSecuritySchemeMapper;
     @Resource
+    private HrmSocialSecuritySchemeService socialSecuritySchemeService;
+    @Resource
     private HrmNoRedisDAO noRedisDAO;
     @Resource
     private DeptApi deptApi;
@@ -82,11 +100,21 @@
     @Resource
     private HrmLeaveApplicationMapper leaveApplicationMapper;
     @Resource
+    private HrmOvertimeApplicationMapper overtimeApplicationMapper;
+    @Resource
+    private HrmSalaryStructureMapper salaryStructureMapper;
+    @Resource
     private HrmLeaveTypeConfigMapper leaveTypeConfigMapper;
     @Resource
     private HrmSalaryPaymentMapper salaryPaymentMapper;
     @Resource
     private HrmSalaryPaymentDetailMapper salaryPaymentDetailMapper;
+    @Resource
+    private HrmEmployeeScheduleMapper employeeScheduleMapper;
+    @Resource
+    private HrmShiftMapper shiftMapper;
+    @Resource
+    private HrmAttendanceRecordMapper attendanceRecordMapper;
 
     @Override
     public PageResult<HrmSalaryCalculationDO> getSalaryCalculationPage(HrmSalaryCalculationPageReqVO pageReqVO) {
@@ -120,6 +148,8 @@
                 .toList();
 
         // 2. 閬嶅巻鍛樺伐杩涜钖叕鏍哥畻
+        // 閮ㄩ棬 -> 鏂规 鐨勮В鏋愮粨鏋滃湪鏈鏍哥畻鍐呯紦瀛橈紝閬垮厤閫愬憳宸ラ噸澶嶆煡搴�
+        Map<Long, HrmSocialSecuritySchemeDO> deptSchemeCache = new HashMap<>();
         for (HrmEmployeeDO employee : employees) {
             // employeeId 鏄憳宸D锛岀敤浜庡叧鑱旇柂閰。妗堛�佺ぞ淇濇。妗堛�佹牳绠楄褰�
             Long employeeId = employee.getId();
@@ -130,7 +160,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鏌ヨ锛�
@@ -142,24 +175,26 @@
 
             // 2.3 鑾峰彇鍛樺伐绀句繚鍏Н閲戞。妗堬紙鎸夊憳宸D鏌ヨ锛屽彇鏈�鏂扮敓鏁堢殑锛�
             HrmEmployeeSocialSecurityDO socialSecurity = employeeSocialSecurityMapper.selectLatestByUserId(employeeId);
-            // 2.4 濡傛灉鏈夌ぞ淇濇。妗堬紝鑾峰彇缁戝畾鐨勬柟妗�
-            HrmSocialSecuritySchemeDO scheme = null;
-            if (socialSecurity != null && socialSecurity.getSocialSecuritySchemeId() != null) {
-                scheme = socialSecuritySchemeMapper.selectById(socialSecurity.getSocialSecuritySchemeId());
-            }
+            // 2.4 鑾峰彇鏂规锛氬憳宸ユ。妗堟寚瀹� > 鎵�鍦ㄩ儴闂ㄧ粦瀹� > 鍏ㄥ眬榛樿
+            HrmSocialSecuritySchemeDO scheme = resolveScheme(socialSecurity, employee, effectiveDate, deptSchemeCache);
 
             // 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 +209,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,19 +240,82 @@
         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;
 
-        // 搴斿彂宸ヨ祫鎬婚
-        BigDecimal totalIncome = baseSalary.add(performanceSalary).add(mealAllowance)
-                .add(transportAllowance).add(otherAllowance).add(overtimePay);
+        // ========== 涔濇硴宸ヨ祫琛細鏍囧噯椤瑰彇鑷憳宸ユ。妗� ==========
+        BigDecimal standardSalary = nvl(employee.getStandardSalary());
+        BigDecimal postSalary = nvl(employee.getPostSalary());
+        BigDecimal overtimeHourlyRate = employee.getOvertimeHourlyRate();
+        BigDecimal safetyAwardStandard = nvl(employee.getSafetyAwardStandard());
+        BigDecimal performanceStandard = nvl(employee.getPerformanceStandard());
+        // 鏍囧噯宸ヨ祫鏈崟鐙淮鎶ゆ椂锛屾寜銆屽熀鏈伐璧� + 宀椾綅宸ヨ祫銆嶅悎鎴�
+        if (standardSalary.signum() == 0 && (baseSalary.signum() > 0 || postSalary.signum() > 0)) {
+            standardSalary = baseSalary.add(postSalary);
+        }
 
-        // 鑾峰彇绀句繚鍩烘暟锛堝憳宸ユ。妗堜腑鐨勪釜鎬у寲鍩烘暟锛�
+        // 鎸夋帓鐝疮璁″簲鍑哄嫟澶╂暟涓庢爣鍑嗗伐鏃讹紙鏃犳帓鐝椂鍥為�� 22 澶� 脳 8 灏忔椂锛�
+        ScheduleStat scheduleStat = calculateScheduleStat(employeeId, period);
+        Integer workDays = scheduleStat.scheduledDays() > 0 ? scheduleStat.scheduledDays() : 22;
+        BigDecimal totalWorkHours = scheduleStat.totalWorkHours();
+        BigDecimal standardWorkHours = scheduleStat.scheduledDays() > 0
+                ? scheduleStat.totalWorkHours()
+                : BigDecimal.valueOf(workDays).multiply(BigDecimal.valueOf(8));
+
+        // 璁¤柂鍑哄嫟 = 鏍囧噯鍑哄嫟澶╂暟 - 缂哄嫟澶╂暟锛堣�冨嫟璁板綍涓己鍗$殑澶╂暟锛屾寜澶╁幓閲嶏級
+        int absentDays = countAbsentDays(employeeId, period);
+        BigDecimal paidAttendanceDays = BigDecimal.valueOf(Math.max(workDays - absentDays, 0));
+
+        // 鍔犵彮宸ヨ祫 / 娉曞畾鍋囧伐璧勶紙鍔犵彮宸ヨ祫鏍囧噯涓虹┖鏃跺洖閫�鍒� 鍩烘湰宸ヨ祫 / 21.75 / 8 脳 鍊嶇巼锛�
+        OvertimeSalary overtimeSalary = calculateOvertimeSalary(employee, employeeSalary, period, baseSalary, overtimeHourlyRate);
+        BigDecimal overtimeHours = overtimeSalary.hours();
+        BigDecimal overtimePay = overtimeSalary.pay();
+        BigDecimal legalHolidayHours = overtimeSalary.legalHolidayHours();
+        BigDecimal legalHolidayPay = overtimeSalary.legalHolidayPay();
+
+        // 鍑哄嫟鎶樼畻姣斾緥锛岀敤浜庢寜瀹為檯璁¤柂鍑哄嫟鎶樼畻鏍囧噯椤�
+        BigDecimal attendanceRatio = workDays > 0
+                ? paidAttendanceDays.divide(BigDecimal.valueOf(workDays), 6, RoundingMode.HALF_UP)
+                : BigDecimal.ZERO;
+        BigDecimal standardAttendancePay = standardSalary.multiply(attendanceRatio).setScale(2, RoundingMode.HALF_UP);
+        BigDecimal safetyAward = safetyAwardStandard.multiply(attendanceRatio).setScale(2, RoundingMode.HALF_UP);
+        BigDecimal floatingPerformancePay = performanceStandard.multiply(attendanceRatio).setScale(2, RoundingMode.HALF_UP);
+
+        // 搴斾粯宸ヨ祫 = 鏍囧噯鍑哄嫟宸ヨ祫 + 鍔犵彮宸ヨ祫 + 娉曞畾鍋囧伐璧� + 瀹夊叏鐜繚濂� + 娴姩缁╂晥宸ヨ祫
+        BigDecimal payableSalary = standardAttendancePay.add(overtimePay).add(legalHolidayPay)
+                .add(safetyAward).add(floatingPerformancePay);
+
+        // 鍔犻」锛堝伐榫勫伐璧�/澶滅彮琛ュ姪/瀛﹀巻琛ヨ创/浣撴/浼樼鍛樺伐濂�/鍏朵粬琛ュ姪/閮ㄩ棬缁╂晥锛変笌鎵i」
+        // 锛堟紡鎵撳崱/杩熷埌鏃╅��/鍏朵粬鎵f锛夌敓鎴愭椂榛樿涓� 0锛屾牳绠楅〉鍙墜宸ヨ皟鏁村悗淇濆瓨
+        BigDecimal seniorityPay = BigDecimal.ZERO;
+        BigDecimal nightShiftAllowance = BigDecimal.ZERO;
+        BigDecimal educationAllowance = BigDecimal.ZERO;
+        BigDecimal healthCheckAllowance = BigDecimal.ZERO;
+        BigDecimal excellentEmployeeAward = BigDecimal.ZERO;
+        BigDecimal otherSubsidy = BigDecimal.ZERO;
+        BigDecimal deptPerformance = BigDecimal.ZERO;
+        BigDecimal additionTotal = BigDecimal.ZERO;
+        BigDecimal missedPunchDeduction = BigDecimal.ZERO;
+        BigDecimal lateEarlyDeduction = BigDecimal.ZERO;
+        BigDecimal deductionTotal = BigDecimal.ZERO;
+
+        // 璁$◣鍙e緞锛氬簲浠樺伐璧� + 鍔犻」鍚堣 - 鎵f鍚堣
+        BigDecimal totalIncome = payableSalary.add(additionTotal).subtract(deductionTotal);
+
+        // 鑾峰彇绀句繚鍩烘暟锛氬憳宸ョぞ淇濇。妗� > 鏂规榛樿缂磋垂鍩烘暟 > 鍛樺伐鍩烘湰宸ヨ祫
         BigDecimal socialSecurityBase = (socialSecurity != null && socialSecurity.getSocialSecurityBase() != null)
-                ? socialSecurity.getSocialSecurityBase() : baseSalary;
+                ? socialSecurity.getSocialSecurityBase()
+                : (scheme != null && scheme.getDefaultSocialSecurityBase() != null
+                        ? scheme.getDefaultSocialSecurityBase() : baseSalary);
 
-        // 鑾峰彇鍏Н閲戝熀鏁帮紙鍛樺伐妗f涓殑涓�у寲鍩烘暟锛�
-        BigDecimal housingFundBase = (socialSecurity != null && socialSecurity.getHousingFundBase() != null)
-                ? socialSecurity.getHousingFundBase() : baseSalary;
+        // 鑾峰彇鍏Н閲戝熀鏁帮細鍛樺伐绀句繚妗f > 鏂规榛樿鍏Н閲戝熀鏁帮紙澶т簬 0 鏃讹級> 绀句繚鍩烘暟
+        BigDecimal housingFundBase;
+        if (socialSecurity != null && socialSecurity.getHousingFundBase() != null) {
+            housingFundBase = socialSecurity.getHousingFundBase();
+        } else if (scheme != null && scheme.getDefaultHousingFundBase() != null
+                && scheme.getDefaultHousingFundBase().compareTo(BigDecimal.ZERO) > 0) {
+            housingFundBase = scheme.getDefaultHousingFundBase();
+        } else {
+            housingFundBase = socialSecurityBase;
+        }
 
         // 浠庢柟妗堜腑鑾峰彇缂寸撼姣斾緥锛堜紭鍏堜娇鐢ㄦ柟妗堜腑鐨勬瘮渚嬶紝鍚﹀垯浣跨敤榛樿姣斾緥锛�
         BigDecimal pensionPersonalRatio = (scheme != null && scheme.getPensionPersonalRatio() != null)
@@ -238,8 +337,8 @@
         // 鍏Н閲戞墸闄� = 鍩烘暟 脳 鍏Н閲戜釜浜烘瘮渚�
         BigDecimal housingFundDeduction = housingFundBase.multiply(housingFundPersonalRatio).setScale(2, RoundingMode.HALF_UP);
 
-        // 璁$畻璇峰亣鎵f
-        BigDecimal leaveDeduction = calculateLeaveDeduction(employee, period, baseSalary);
+        // 璇峰亣鎵f锛氭柊鐗堝伐璧勮〃鎸夈�岃钖嚭鍕ゃ�嶆姌鍑忔爣鍑嗗嚭鍕ゅ伐璧勶紝涓嶅啀鍗曠嫭閲嶅鎵d竴娆★紝淇濈暀瀛楁鍏煎鏃ф暟鎹�
+        BigDecimal leaveDeduction = BigDecimal.ZERO;
 
         // 璁$畻涓◣鎵i櫎
         // 搴旂撼绋庢墍寰楅 = 搴斿彂宸ヨ祫 - 绀句繚鎵i櫎 - 鍏Н閲戞墸闄� - 璧峰緛鐐�(5000)
@@ -255,15 +354,43 @@
 
         BigDecimal otherDeduction = BigDecimal.ZERO;
 
-        // 璁$畻瀹炲彂宸ヨ祫 = 搴斿彂 - 鎵i櫎
-        BigDecimal totalDeduction = socialSecurityDeduction.add(housingFundDeduction)
-                .add(taxDeduction).add(otherDeduction).add(leaveDeduction);
-        BigDecimal actualSalary = totalIncome.subtract(totalDeduction);
+        // 绋庡悗宸ヨ祫 = 搴斾粯宸ヨ祫 + 鍔犻」鍚堣 - 鎵f鍚堣 - 绀句繚 - 鍏Н閲� - 涓◣
+        BigDecimal actualSalary = payableSalary.add(additionTotal).subtract(deductionTotal)
+                .subtract(socialSecurityDeduction).subtract(housingFundDeduction)
+                .subtract(taxDeduction);
 
         return HrmSalaryCalculationDO.builder()
                 .period(period)
                 .userId(employeeId) // 浣跨敤鍛樺伐ID
                 .deptId(employee.getDeptId())
+                // 涔濇硴宸ヨ祫琛細鏍囧噯椤�
+                .standardSalary(standardSalary)
+                .postSalary(postSalary)
+                .overtimeHourlyRate(overtimeHourlyRate)
+                .safetyAwardStandard(safetyAwardStandard)
+                .performanceStandard(performanceStandard)
+                // 涔濇硴宸ヨ祫琛細鍑哄嫟涓庤钖�
+                .paidAttendanceDays(paidAttendanceDays)
+                .standardWorkHours(standardWorkHours)
+                .legalHolidayHours(legalHolidayHours)
+                .standardAttendancePay(standardAttendancePay)
+                .legalHolidayPay(legalHolidayPay)
+                .safetyAward(safetyAward)
+                .floatingPerformancePay(floatingPerformancePay)
+                .payableSalary(payableSalary)
+                // 涔濇硴宸ヨ祫琛細鍔犻」 / 鎵i」
+                .seniorityPay(seniorityPay)
+                .nightShiftAllowance(nightShiftAllowance)
+                .educationAllowance(educationAllowance)
+                .healthCheckAllowance(healthCheckAllowance)
+                .excellentEmployeeAward(excellentEmployeeAward)
+                .otherSubsidy(otherSubsidy)
+                .deptPerformance(deptPerformance)
+                .additionTotal(additionTotal)
+                .missedPunchDeduction(missedPunchDeduction)
+                .lateEarlyDeduction(lateEarlyDeduction)
+                .deductionTotal(deductionTotal)
+                // 鍏煎鏃у瓧娈�
                 .baseSalary(baseSalary)
                 .performanceSalary(performanceSalary)
                 .overtimePay(overtimePay)
@@ -276,10 +403,35 @@
                 .otherDeduction(otherDeduction)
                 .leaveDeduction(leaveDeduction)
                 .actualSalary(actualSalary)
-                .workDays(22) // 榛樿宸ヤ綔鏃�
-                .overtimeHours(BigDecimal.ZERO)
+                .workDays(workDays) // 鎸夋帓鐝疮璁″簲鍑哄嫟澶╂暟锛屾棤鎺掔彮鏃跺洖閫� 22
+                .overtimeHours(overtimeHours)
+                .totalWorkHours(totalWorkHours)
                 .status(0) // 寰呯‘璁�
                 .build();
+    }
+
+    /**
+     * 瑙f瀽鍛樺伐鏍哥畻鐢ㄧ殑绀句繚鍏Н閲戞柟妗�
+     *
+     * 浼樺厛绾э細鍛樺伐绀句繚妗f鎸囧畾鐨勬柟妗堬紙涓埆瑕嗙洊锛�> 鍛樺伐鎵�鍦ㄩ儴闂ㄧ粦瀹氱殑鏂规 > 鍏ㄥ眬榛樿鏂规銆�
+     * 閮藉彇涓嶅埌鏃惰繑鍥� null锛岀敱 {@link #buildSalaryCalculation} 浣跨敤榛樿姣斾緥甯搁噺銆�
+     *
+     * @param deptSchemeCache 鏈鏍哥畻鍐呫�岄儴闂� -> 鏂规銆嶇殑缂撳瓨锛堝厑璁� null 鍊硷紝鐢ㄤ簬缂撳瓨"璇ラ儴闂ㄦ棤鏂规"锛�
+     */
+    private HrmSocialSecuritySchemeDO resolveScheme(HrmEmployeeSocialSecurityDO socialSecurity, HrmEmployeeDO employee,
+                                                    LocalDate periodDate,
+                                                    Map<Long, HrmSocialSecuritySchemeDO> deptSchemeCache) {
+        if (socialSecurity != null && socialSecurity.getSocialSecuritySchemeId() != null) {
+            HrmSocialSecuritySchemeDO scheme = socialSecuritySchemeMapper.selectById(socialSecurity.getSocialSecuritySchemeId());
+            if (scheme != null) {
+                return scheme;
+            }
+        }
+        Long deptId = employee.getDeptId();
+        if (!deptSchemeCache.containsKey(deptId)) {
+            deptSchemeCache.put(deptId, socialSecuritySchemeService.resolveSchemeByDeptId(deptId, periodDate));
+        }
+        return deptSchemeCache.get(deptId);
     }
 
     /**
@@ -332,6 +484,123 @@
         }
 
         return totalLeaveDeduction;
+    }
+
+    /**
+     * 姹囨�诲綋鏈堝鎵归�氳繃鐨勫姞鐝敵璇枫��
+     *
+     * 鍛樺伐妗f缁存姢浜嗐�屽姞鐝伐璧勬爣鍑嗭紙鍏�/灏忔椂锛夈�嶆椂锛氬姞鐝伐璧� = 鍔犵彮鏃堕暱 脳 鍗曚环锛�
+     * 娉曞畾鍋囧伐璧� = 娉曞畾鍋囨棩鏃堕暱 脳 鍗曚环 脳 娉曞畾鑺傚亣鏃ュ�嶇巼锛堥粯璁� 3锛夈��
+     * 鏈淮鎶ゅ崟浠锋椂鍥為��鏃у彛寰勶細鍩烘湰宸ヨ祫 / 21.75 / 8 脳 鍚勭被鍨嬪�嶇巼銆�
+     *
+     * 娉曞畾鑺傚亣鏃ュ姞鐝崟鐙粺璁★紝涓嶈鍏ャ�屽姞鐝椂闀裤�嶅垪锛岄伩鍏嶄笌銆屾硶瀹氬亣宸ヨ祫銆嶉噸澶嶈钖��
+     */
+    private OvertimeSalary calculateOvertimeSalary(HrmEmployeeDO employee, HrmEmployeeSalaryDO employeeSalary,
+                                                    String period, BigDecimal baseSalary, BigDecimal overtimeHourlyRate) {
+        if (employee.getUserId() == null) {
+            return OvertimeSalary.EMPTY;
+        }
+        YearMonth month = YearMonth.parse(period);
+        List<HrmOvertimeApplicationDO> applications = overtimeApplicationMapper.selectApprovedByUserAndDateRange(
+                employee.getUserId(), month.atDay(1), month.atEndOfMonth());
+        if (CollUtil.isEmpty(applications)) {
+            return OvertimeSalary.EMPTY;
+        }
+        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 = overtimeHourlyRate != null && overtimeHourlyRate.signum() > 0
+                ? overtimeHourlyRate
+                : 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;
+        BigDecimal legalHolidayHours = BigDecimal.ZERO;
+        BigDecimal legalHolidayPay = 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;
+            }
+            if (type == HrmOvertimeTypeEnum.LEGAL_HOLIDAY) {
+                legalHolidayHours = legalHolidayHours.add(hours);
+                legalHolidayPay = legalHolidayPay.add(hourlySalary.multiply(hours).multiply(legalHolidayMultiplier));
+                continue;
+            }
+            BigDecimal multiplier = type == HrmOvertimeTypeEnum.WORKDAY ? workdayMultiplier : restDayMultiplier;
+            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),
+                legalHolidayHours.setScale(1, RoundingMode.HALF_UP),
+                legalHolidayPay.setScale(2, RoundingMode.HALF_UP));
+    }
+
+    /**
+     * 缁熻褰撴湀鑰冨嫟璁板綍涓己鍗$殑澶╂暟锛堝悓涓�鏃ユ湡澶氭缂哄崱鍙畻涓�澶╋級
+     */
+    private int countAbsentDays(Long employeeId, String period) {
+        YearMonth month = YearMonth.parse(period);
+        List<HrmAttendanceRecordDO> records = attendanceRecordMapper.selectListByUserIdAndDateBetween(
+                employeeId, month.atDay(1), month.atEndOfMonth());
+        if (CollUtil.isEmpty(records)) {
+            return 0;
+        }
+        return (int) records.stream()
+                .filter(record -> HrmAttendanceClockTypeEnum.MISSING.getType().equals(record.getClockInType())
+                        || HrmAttendanceClockTypeEnum.MISSING.getType().equals(record.getClockOutType()))
+                .map(HrmAttendanceRecordDO::getDate)
+                .distinct()
+                .count();
+    }
+
+    private static BigDecimal nvl(BigDecimal value) {
+        return value == null ? BigDecimal.ZERO : value;
+    }
+
+    private record OvertimeSalary(BigDecimal hours, BigDecimal pay,
+                                  BigDecimal legalHolidayHours, BigDecimal legalHolidayPay) {
+        private static final OvertimeSalary EMPTY = new OvertimeSalary(
+                BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO);
+    }
+
+    private record ScheduleStat(int scheduledDays, BigDecimal totalWorkHours) {}
+
+    /**
+     * 鎸夋帓鐝〃绱鍛樺伐鐨勫嚭鍕ゅぉ鏁颁笌鎺掔彮鎬诲伐鏃躲��
+     * 浠呯粺璁℃湁鐝鐨勬帓鐝褰曪紝鐝鏍囧噯宸ユ椂涔嬪拰鍗充负鎺掔彮宸ユ椂銆�
+     */
+    private ScheduleStat calculateScheduleStat(Long employeeId, String period) {
+        YearMonth yearMonth = YearMonth.parse(period);
+        List<HrmEmployeeScheduleDO> schedules = employeeScheduleMapper.selectListByEmployeeIdAndPeriod(
+                employeeId, yearMonth.atDay(1), yearMonth.atEndOfMonth());
+        if (CollUtil.isEmpty(schedules)) {
+            return new ScheduleStat(0, BigDecimal.ZERO);
+        }
+        Set<Long> shiftIds = schedules.stream().map(HrmEmployeeScheduleDO::getShiftId)
+                .filter(Objects::nonNull).collect(Collectors.toSet());
+        Map<Long, HrmShiftDO> shiftMap = CollUtil.isEmpty(shiftIds) ? Collections.emptyMap()
+                : shiftMapper.selectBatchIds(shiftIds).stream()
+                        .collect(Collectors.toMap(HrmShiftDO::getId, shift -> shift, (a, b) -> a));
+        int scheduledDays = 0;
+        BigDecimal totalWorkHours = BigDecimal.ZERO;
+        for (HrmEmployeeScheduleDO schedule : schedules) {
+            HrmShiftDO shift = schedule.getShiftId() == null ? null : shiftMap.get(schedule.getShiftId());
+            if (shift == null) {
+                continue;
+            }
+            scheduledDays++;
+            totalWorkHours = totalWorkHours.add(shift.getWorkHours() != null ? shift.getWorkHours() : BigDecimal.ZERO);
+        }
+        return new ScheduleStat(scheduledDays, totalWorkHours.setScale(2, RoundingMode.HALF_UP));
     }
 
     @Override
@@ -416,6 +685,8 @@
 
         // 2. 閬嶅巻鍛樺伐瀹炴椂璁$畻钖叕
         List<HrmSalaryCalculationDO> resultList = new java.util.ArrayList<>();
+        // 閮ㄩ棬 -> 鏂规 鐨勮В鏋愮粨鏋滃湪鏈棰勮鍐呯紦瀛橈紝閬垮厤閫愬憳宸ラ噸澶嶆煡搴�
+        Map<Long, HrmSocialSecuritySchemeDO> deptSchemeCache = new HashMap<>();
         for (HrmEmployeeDO employee : employees) {
             Long employeeId = employee.getId();
 
@@ -428,11 +699,8 @@
 
             // 2.2 鑾峰彇鍛樺伐绀句繚鍏Н閲戞。妗堬紙鎸夊憳宸D鏌ヨ锛屽彇鏈�鏂扮敓鏁堢殑锛�
             HrmEmployeeSocialSecurityDO socialSecurity = employeeSocialSecurityMapper.selectLatestByUserId(employeeId);
-            // 2.3 濡傛灉鏈夌ぞ淇濇。妗堬紝鑾峰彇缁戝畾鐨勬柟妗�
-            HrmSocialSecuritySchemeDO scheme = null;
-            if (socialSecurity != null && socialSecurity.getSocialSecuritySchemeId() != null) {
-                scheme = socialSecuritySchemeMapper.selectById(socialSecurity.getSocialSecuritySchemeId());
-            }
+            // 2.3 鑾峰彇鏂规锛氬憳宸ユ。妗堟寚瀹� > 鎵�鍦ㄩ儴闂ㄧ粦瀹� > 鍏ㄥ眬榛樿
+            HrmSocialSecuritySchemeDO scheme = resolveScheme(socialSecurity, employee, effectiveDate, deptSchemeCache);
 
             // 2.4 鏋勫缓鏍哥畻璁板綍锛堝疄鏃惰绠楋紝涓嶅瓨鍌級
             HrmSalaryCalculationDO calculation = buildSalaryCalculation(period, employeeId, employee, employeeSalary, socialSecurity, scheme, effectiveDate);
@@ -445,6 +713,7 @@
     @Transactional(rollbackFor = Exception.class)
     public void saveSalaryCalculationList(List<HrmSalaryCalculationDO> calculationList) {
         for (HrmSalaryCalculationDO calculation : calculationList) {
+            normalizeSalaryTotals(calculation);
             if (calculation.getId() == null) {
                 // 鏂板璁板綍锛岀敓鎴愭牳绠楀崟鍙�
                 String no = noRedisDAO.generateSalaryCalculationNo();
@@ -459,6 +728,50 @@
                 salaryCalculationMapper.updateById(calculation);
             }
         }
+    }
+
+    /**
+     * 淇濆瓨鍓嶆寜涔濇硴宸ヨ祫琛ㄥ彛寰勯噸绠楀悎璁★紝閬垮厤鍓嶇婕忕畻鎴栨棫瀹㈡埛绔紶鍏ヤ笉涓�鑷寸殑鍚堣鍊笺��
+     *
+     * 鍔犻」鍚堣 = 宸ラ緞宸ヨ祫 + 澶滅彮琛ュ姪 + 瀛﹀巻琛ヨ创 + 浣撴 + 浼樼鍛樺伐濂� + 鍏朵粬琛ュ姪 + 閮ㄩ棬缁╂晥
+     * 鎵f鍚堣 = 婕忔墦鍗� + 杩熷埌鏃╅�� + 鍏朵粬鎵f
+     * 绋庡悗宸ヨ祫 = 搴斾粯宸ヨ祫 + 鍔犻」鍚堣 - 鎵f鍚堣 - 绀句繚 - 鍏Н閲� - 涓◣
+     *
+     * 浠呭綋鏈璁板綍甯︽湁銆屽簲浠樺伐璧勩�嶆椂閲嶇畻锛涘彧鏈夋棫瀛楁鐨勮�佹暟鎹繚鎸佸師鏍凤紝閬垮厤鎶婃湭缁存姢鐨勬柊瀛楁娓呮垚 0銆�
+     */
+    private void normalizeSalaryTotals(HrmSalaryCalculationDO calculation) {
+        if (calculation.getPayableSalary() == null) {
+            return;
+        }
+        // 搴斾粯宸ヨ祫 = 鏍囧噯鍑哄嫟宸ヨ祫 + 鍔犵彮宸ヨ祫 + 娉曞畾鍋囧伐璧� + 瀹夊叏鐜繚濂� + 娴姩缁╂晥宸ヨ祫
+        // 浠呭綋鏈璁板綍甯︽柊鐗堟爣鍑嗛」鏃堕噸绠楋紝閬垮厤鏃ф暟鎹紙鏃犺繖浜涘垪锛夎娓呴浂
+        if (calculation.getStandardAttendancePay() != null) {
+            calculation.setPayableSalary(nvl(calculation.getStandardAttendancePay())
+                    .add(nvl(calculation.getOvertimePay()))
+                    .add(nvl(calculation.getLegalHolidayPay()))
+                    .add(nvl(calculation.getSafetyAward()))
+                    .add(nvl(calculation.getFloatingPerformancePay()))
+                    .setScale(2, RoundingMode.HALF_UP));
+        }
+        BigDecimal additionTotal = nvl(calculation.getSeniorityPay())
+                .add(nvl(calculation.getNightShiftAllowance()))
+                .add(nvl(calculation.getEducationAllowance()))
+                .add(nvl(calculation.getHealthCheckAllowance()))
+                .add(nvl(calculation.getExcellentEmployeeAward()))
+                .add(nvl(calculation.getOtherSubsidy()))
+                .add(nvl(calculation.getDeptPerformance()));
+        BigDecimal deductionTotal = nvl(calculation.getMissedPunchDeduction())
+                .add(nvl(calculation.getLateEarlyDeduction()))
+                .add(nvl(calculation.getOtherDeduction()));
+        calculation.setAdditionTotal(additionTotal.setScale(2, RoundingMode.HALF_UP));
+        calculation.setDeductionTotal(deductionTotal.setScale(2, RoundingMode.HALF_UP));
+        calculation.setActualSalary(calculation.getPayableSalary()
+                .add(calculation.getAdditionTotal())
+                .subtract(calculation.getDeductionTotal())
+                .subtract(nvl(calculation.getSocialSecurityDeduction()))
+                .subtract(nvl(calculation.getHousingFundDeduction()))
+                .subtract(nvl(calculation.getTaxDeduction()))
+                .setScale(2, RoundingMode.HALF_UP));
     }
 
     /**
@@ -577,4 +890,4 @@
         salaryCalculationMapper.deleteById(id);
     }
 
-}
\ No newline at end of file
+}

--
Gitblit v1.9.3