From fdbc5af6474abb39120b771af87005aeb6186ae5 Mon Sep 17 00:00:00 2001
From: tuqin <tuqin@tuqin.cn>
Date: 星期一, 14 九月 2026 15:28:28 +0800
Subject: [PATCH] feat: 售后工单按条绑定部门与原因类型;新增加班工资管理;员工附件留档

---
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryCalculationServiceImpl.java |  185 ++++++++++++++++++++++++++++++++++++++++------
 1 files changed, 160 insertions(+), 25 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 8be05ab..5f0019e 100644
--- a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryCalculationServiceImpl.java
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryCalculationServiceImpl.java
@@ -6,6 +6,8 @@
 import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.HrmSalaryCalculationPageReqVO;
 import cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmLeaveApplicationDO;
 import cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmOvertimeApplicationDO;
+import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmEmployeeScheduleDO;
+import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmShiftDO;
 import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO;
 import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmEmployeeSalaryDO;
 import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmEmployeeSocialSecurityDO;
@@ -16,6 +18,8 @@
 import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmSocialSecuritySchemeDO;
 import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmSalaryStructureDO;
 import cn.iocoder.yudao.module.hrm.dal.mysql.approval.HrmLeaveApplicationMapper;
+import cn.iocoder.yudao.module.hrm.dal.mysql.attendance.HrmEmployeeScheduleMapper;
+import cn.iocoder.yudao.module.hrm.dal.mysql.attendance.HrmShiftMapper;
 import cn.iocoder.yudao.module.hrm.dal.mysql.approval.HrmOvertimeApplicationMapper;
 import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeMapper;
 import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmEmployeeSalaryMapper;
@@ -28,6 +32,11 @@
 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.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONUtil;
+import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmOvertimeWageConfigDO;
+import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmOvertimeWageConfigMapper;
+import cn.iocoder.yudao.module.hrm.enums.HrmEmployeeCategoryEnum;
 import cn.iocoder.yudao.module.hrm.enums.HrmOvertimeTypeEnum;
 import jakarta.annotation.Resource;
 import org.springframework.stereotype.Service;
@@ -39,8 +48,11 @@
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.YearMonth;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
 import java.util.stream.Collectors;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -91,11 +103,17 @@
     @Resource
     private HrmSalaryStructureMapper salaryStructureMapper;
     @Resource
+    private HrmOvertimeWageConfigMapper overtimeWageConfigMapper;
+    @Resource
     private HrmLeaveTypeConfigMapper leaveTypeConfigMapper;
     @Resource
     private HrmSalaryPaymentMapper salaryPaymentMapper;
     @Resource
     private HrmSalaryPaymentDetailMapper salaryPaymentDetailMapper;
+    @Resource
+    private HrmEmployeeScheduleMapper employeeScheduleMapper;
+    @Resource
+    private HrmShiftMapper shiftMapper;
 
     @Override
     public PageResult<HrmSalaryCalculationDO> getSalaryCalculationPage(HrmSalaryCalculationPageReqVO pageReqVO) {
@@ -225,6 +243,11 @@
         OvertimeSalary overtimeSalary = calculateOvertimeSalary(employee, employeeSalary, period, baseSalary);
         BigDecimal overtimePay = overtimeSalary.pay();
 
+        // 鎸夋帓鐝疮璁″嚭鍕ゅぉ鏁颁笌鎺掔彮鎬诲伐鏃讹紙鏃犳帓鐝椂鍑哄嫟澶╂暟鍥為��榛樿 22锛�
+        ScheduleStat scheduleStat = calculateScheduleStat(employeeId, period);
+        Integer workDays = scheduleStat.scheduledDays() > 0 ? scheduleStat.scheduledDays() : 22;
+        BigDecimal totalWorkHours = scheduleStat.totalWorkHours();
+
         // 搴斿彂宸ヨ祫鎬婚
         BigDecimal totalIncome = baseSalary.add(performanceSalary).add(mealAllowance)
                 .add(transportAllowance).add(otherAllowance).add(overtimePay);
@@ -295,8 +318,9 @@
                 .otherDeduction(otherDeduction)
                 .leaveDeduction(leaveDeduction)
                 .actualSalary(actualSalary)
-                .workDays(22) // 榛樿宸ヤ綔鏃�
+                .workDays(workDays) // 鎸夋帓鐝疮璁″簲鍑哄嫟澶╂暟锛屾棤鎺掔彮鏃跺洖閫� 22
                 .overtimeHours(overtimeSalary.hours())
+                .totalWorkHours(totalWorkHours)
                 .status(0) // 寰呯‘璁�
                 .build();
     }
@@ -354,8 +378,16 @@
     }
 
     /**
-     * 姹囨�诲綋鏈堝鎵归�氳繃鐨勫姞鐝敵璇枫�傚皬鏃跺伐璧勬寜鍩烘湰宸ヨ祫 / 21.75 / 8 璁$畻锛�
-     * 宸ヤ綔鏃ラ粯璁� 1.5 鍊嶃�佷紤鎭棩榛樿 2 鍊嶃�佹硶瀹氳妭鍋囨棩榛樿 3 鍊嶏紝鍧囧彲鐢辫柂閰粨鏋勮鐩栥��
+     * 姹囨�诲綋鏈堝鎵归�氳繃鐨勫姞鐝敵璇凤紝璁$畻鍔犵彮宸ヨ祫涓庡姞鐝椂闀裤��
+     *
+     * <p>鎸夈�屽憳宸ョ被鍒�嶅樊寮傚寲鏍哥畻锛�
+     * <ul>
+     *     <li>鎶�宸ワ細鍔犵彮钖祫鎸� 1:1 鏍哥畻锛屼笌骞虫椂鏃惰柂涓�鑷达紙鍩烘湰宸ヨ祫 / 21.75 / 8锛夛紱</li>
+     *     <li>鏅�氬憳宸ワ細鎸夊矖浣嶉厤缃殑鍥哄畾鍔犵彮鏃惰柂锛堥粯璁� 20 鍏�/灏忔椂锛夋牳绠楋紱</li>
+     *     <li>绠$悊锛氫笉璁″姞鐝伐璧勶紱</li>
+     * </ul>
+     * 鍛戒腑宀椾綅閰嶇疆鏃讹紝鎸夐厤缃殑鏈�澶у姞鐝椂闀夸笂闄愬鍔犵彮鏃堕暱灏侀《锛�
+     * 鏈懡涓换浣曞矖浣嶉厤缃笖闈炴妧宸�/绠$悊鏃讹紝鍥為��鏃ч�昏緫锛堣柂閰粨鏋勫�嶇巼 / 鍔犵彮绫诲瀷榛樿鍊嶇巼锛夈��
      */
     private OvertimeSalary calculateOvertimeSalary(HrmEmployeeDO employee, HrmEmployeeSalaryDO employeeSalary,
                                                     String period, BigDecimal baseSalary) {
@@ -368,39 +400,142 @@
         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)
+        // 绠$悊绫诲埆涓嶈鍔犵彮宸ヨ祫
+        HrmEmployeeCategoryEnum category = HrmEmployeeCategoryEnum.valueOfCategory(employee.getEmployeeCategory());
+        if (category == HrmEmployeeCategoryEnum.MANAGER) {
+            return new OvertimeSalary(BigDecimal.ZERO, BigDecimal.ZERO);
+        }
+        // 鍛樺伐宀椾綅鍛戒腑鐨勫姞鐝伐璧勯厤缃紙鍙栨椂钖渶楂樸�佷笂闄愭渶瀹界殑涓�鏉★級
+        OvertimeWageRule wageRule = resolveOvertimeWageRule(employee);
+        // 骞虫椂鏃惰柂 = 鍩烘湰宸ヨ祫 / 21.75 / 8
+        BigDecimal baseHourlySalary = 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;
+        if (category == HrmEmployeeCategoryEnum.TECHNICIAN || wageRule != null) {
+            // 鎶�宸ワ細1:1锛堜笌骞虫椂鏃惰柂涓�鑷达級锛涙櫘閫氬憳宸ュ懡涓厤缃細鎸夐厤缃浐瀹氭椂钖紝鍧囦笉鍖哄垎鍔犵彮绫诲瀷鍊嶇巼
+            BigDecimal hourlySalary = category == HrmEmployeeCategoryEnum.TECHNICIAN
+                    ? baseHourlySalary : wageRule.hourlyRate();
+            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;
+                }
+                totalHours = totalHours.add(hours);
             }
-            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));
+            // 鎸夊矖浣嶉厤缃皝椤跺姞鐝椂闀�
+            BigDecimal paidHours = wageRule != null && wageRule.maxOvertimeHours() != null
+                    && totalHours.compareTo(wageRule.maxOvertimeHours()) > 0
+                    ? wageRule.maxOvertimeHours() : totalHours;
+            totalPay = hourlySalary.multiply(paidHours);
+        } else {
+            // 鍏煎鏃ч�昏緫锛氭棤宀椾綅閰嶇疆銆佷笖闈炴妧宸�/绠$悊
+            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();
+            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(baseHourlySalary.multiply(hours).multiply(multiplier));
+            }
         }
         return new OvertimeSalary(totalHours.setScale(1, RoundingMode.HALF_UP),
                 totalPay.setScale(2, RoundingMode.HALF_UP));
     }
 
+    /**
+     * 瑙f瀽鍛樺伐宀椾綅瀵瑰簲鐨勫姞鐝伐璧勮鍒欙細鏃惰柂鍙栧懡涓厤缃腑鐨勬渶澶у�硷紱
+     * 涓婇檺鍙栤�滄渶瀹解�濓紙瀛樺湪鏈缃笂闄愮殑閰嶇疆鍒欒涓轰笉闄愶紝鍚﹀垯鍙栨渶澶у�硷級銆�
+     */
+    private OvertimeWageRule resolveOvertimeWageRule(HrmEmployeeDO employee) {
+        if (StrUtil.isEmpty(employee.getPostIds())) {
+            return null;
+        }
+        List<Long> postIds;
+        try {
+            postIds = JSONUtil.toList(employee.getPostIds(), Long.class);
+        } catch (Exception ex) {
+            return null;
+        }
+        if (CollUtil.isEmpty(postIds)) {
+            return null;
+        }
+        List<HrmOvertimeWageConfigDO> configs = overtimeWageConfigMapper.selectEnabledListByPostIds(postIds);
+        if (CollUtil.isEmpty(configs)) {
+            return null;
+        }
+        BigDecimal hourlyRate = null;
+        BigDecimal maxHours = null;
+        boolean unlimited = false;
+        for (HrmOvertimeWageConfigDO config : configs) {
+            if (config.getHourlyRate() != null
+                    && (hourlyRate == null || config.getHourlyRate().compareTo(hourlyRate) > 0)) {
+                hourlyRate = config.getHourlyRate();
+            }
+            if (config.getMaxOvertimeHours() == null) {
+                unlimited = true;
+            } else if (!unlimited && (maxHours == null || config.getMaxOvertimeHours().compareTo(maxHours) > 0)) {
+                maxHours = config.getMaxOvertimeHours();
+            }
+        }
+        if (hourlyRate == null) {
+            return null;
+        }
+        return new OvertimeWageRule(hourlyRate, unlimited ? null : maxHours);
+    }
+
+    private record OvertimeWageRule(BigDecimal hourlyRate, BigDecimal maxOvertimeHours) {}
+
     private record OvertimeSalary(BigDecimal hours, BigDecimal pay) {}
 
+    private record ScheduleStat(int scheduledDays, BigDecimal totalWorkHours) {}
+
+    /**
+     * 鎸夋帓鐝〃绱鍛樺伐鐨勫嚭鍕ゅぉ鏁颁笌鎺掔彮鎬诲伐鏃躲��
+     * 浠呯粺璁℃湁鐝鐨勬帓鐝褰曪紝鐝鏍囧噯宸ユ椂涔嬪拰鍗充负鎺掔彮宸ユ椂銆�
+     */
+    private ScheduleStat calculateScheduleStat(Long employeeId, String period) {
+        YearMonth yearMonth = YearMonth.parse(period);
+        List<HrmEmployeeScheduleDO> schedules = employeeScheduleMapper.selectListByEmployeeIdAndPeriod(
+                employeeId, yearMonth.atDay(1), yearMonth.atEndOfMonth());
+        if (CollUtil.isEmpty(schedules)) {
+            return new ScheduleStat(0, BigDecimal.ZERO);
+        }
+        Set<Long> shiftIds = schedules.stream().map(HrmEmployeeScheduleDO::getShiftId)
+                .filter(Objects::nonNull).collect(Collectors.toSet());
+        Map<Long, HrmShiftDO> shiftMap = CollUtil.isEmpty(shiftIds) ? Collections.emptyMap()
+                : shiftMapper.selectBatchIds(shiftIds).stream()
+                        .collect(Collectors.toMap(HrmShiftDO::getId, shift -> shift, (a, b) -> a));
+        int scheduledDays = 0;
+        BigDecimal totalWorkHours = BigDecimal.ZERO;
+        for (HrmEmployeeScheduleDO schedule : schedules) {
+            HrmShiftDO shift = schedule.getShiftId() == null ? null : shiftMap.get(schedule.getShiftId());
+            if (shift == null) {
+                continue;
+            }
+            scheduledDays++;
+            totalWorkHours = totalWorkHours.add(shift.getWorkHours() != null ? shift.getWorkHours() : BigDecimal.ZERO);
+        }
+        return new ScheduleStat(scheduledDays, totalWorkHours.setScale(2, RoundingMode.HALF_UP));
+    }
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void confirmSalaryCalculation(List<Long> ids) {

--
Gitblit v1.9.3