gaoluyang
6 天以前 f81b963f9d1b7d4757446970830cb72264c6dc0c
九鸿:工资表按客户口径重构 + 修复考勤汇总生成、新增节假日报错

工资表(薪酬核算):
- 员工档案新增 标准工资/岗位工资/加班工资标准/安全环保奖标准/绩效标准,标准工资未填时按基本工资+岗位工资合成
- hrm_salary_calculation 新增 24 个字段,核算页按客户 Excel 列顺序展示
- 自动计算:计薪出勤、标准出勤工资、加班工资(按元/小时单价,未维护时回退 基本工资/21.75/8)、法定假工资、安全环保奖、浮动绩效工资、应付工资
- 加项/扣款为手工列,保存时后端统一重算 加项合计/扣款合计/税后工资
- 税后工资 = 应付工资 + 加项合计 - 扣款合计 - 社保 - 公积金 - 个税;请假不再单独扣款(已由计薪出勤折减)

缺陷修复:
- 生成考勤统计报系统异常:hrm_attendance_summary.year_month 是 MySQL 保留字,DO 上加反引号
- 新增节假日报系统异常:isLegal 前端传布尔、后端 VO 是 Integer,改为 Boolean 并显式转 0/1
- 批量考勤/薪酬场景 getEmployeeList(null) 应查全员,原先 selectByIds(null) 触发 OGNL 报错

Co-Authored-By: Claude Code <noreply@anthropic.com>
已添加1个文件
已修改12个文件
739 ■■■■■ 文件已修改
docs/sql/hrm_salary_jiuhong_structure.sql 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceHolidaySaveReqVO.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeRespVO.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeSaveReqVO.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalaryCalculationController.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmSalaryCalculationRespVO.java 133 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmSalaryCalculationSaveReqVO.java 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/attendance/HrmAttendanceSummaryDO.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/HrmEmployeeDO.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/salary/HrmSalaryCalculationDO.java 120 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceHolidayServiceImpl.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeServiceImpl.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryCalculationServiceImpl.java 218 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/sql/hrm_salary_jiuhong_structure.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,82 @@
-- ä¹æ³“化工工资表结构扩展
-- 1. å‘˜å·¥æ¡£æ¡ˆè¡¥å……「标准项」:标准工资、岗位工资、加班工资标准(元/小时)、安全环保奖标准、绩效标准
-- 2. è–ªé…¬æ ¸ç®—(工资表)补充图片要求的全部列
-- è¯´æ˜Žï¼šMySQL 8.0 ä¸æ”¯æŒ ADD COLUMN IF NOT EXISTS,这里用存储过程逐列判断后执行
DROP PROCEDURE IF EXISTS add_column_if_missing;
DELIMITER $$
CREATE PROCEDURE add_column_if_missing(IN tbl VARCHAR(64), IN col VARCHAR(64), IN ddl TEXT)
BEGIN
    IF (SELECT COUNT(*) FROM information_schema.columns
        WHERE table_schema = DATABASE() AND table_name = tbl AND column_name = col) = 0 THEN
        SET @sql = ddl;
        PREPARE st FROM @sql;
        EXECUTE st;
        DEALLOCATE PREPARE st;
    END IF;
END$$
DELIMITER ;
-- ---------- å‘˜å·¥æ¡£æ¡ˆï¼šæ ‡å‡†é¡¹ï¼ˆæ ‡å‡†å€¼ç”±å‘˜å·¥æ¡£æ¡ˆç»´æŠ¤ï¼‰ ----------
CALL add_column_if_missing('hrm_employee', 'standard_salary',
    'ALTER TABLE hrm_employee ADD COLUMN standard_salary DECIMAL(12,2) NULL COMMENT ''标准工资'' AFTER base_salary');
CALL add_column_if_missing('hrm_employee', 'post_salary',
    'ALTER TABLE hrm_employee ADD COLUMN post_salary DECIMAL(12,2) NULL COMMENT ''岗位工资'' AFTER standard_salary');
CALL add_column_if_missing('hrm_employee', 'overtime_hourly_rate',
    'ALTER TABLE hrm_employee ADD COLUMN overtime_hourly_rate DECIMAL(12,2) NULL COMMENT ''加班工资标准(元/小时)'' AFTER post_salary');
CALL add_column_if_missing('hrm_employee', 'safety_award_standard',
    'ALTER TABLE hrm_employee ADD COLUMN safety_award_standard DECIMAL(12,2) NULL COMMENT ''安全环保奖标准'' AFTER overtime_hourly_rate');
CALL add_column_if_missing('hrm_employee', 'performance_standard',
    'ALTER TABLE hrm_employee ADD COLUMN performance_standard DECIMAL(12,2) NULL COMMENT ''绩效标准'' AFTER safety_award_standard');
-- ---------- è–ªé…¬æ ¸ç®—(工资表) ----------
CALL add_column_if_missing('hrm_salary_calculation', 'standard_salary',
    'ALTER TABLE hrm_salary_calculation ADD COLUMN standard_salary DECIMAL(12,2) NULL COMMENT ''标准工资'' AFTER dept_id');
CALL add_column_if_missing('hrm_salary_calculation', 'post_salary',
    'ALTER TABLE hrm_salary_calculation ADD COLUMN post_salary DECIMAL(12,2) NULL COMMENT ''岗位工资'' AFTER standard_salary');
CALL add_column_if_missing('hrm_salary_calculation', 'overtime_hourly_rate',
    'ALTER TABLE hrm_salary_calculation ADD COLUMN overtime_hourly_rate DECIMAL(12,2) NULL COMMENT ''加班工资标准(元/小时)'' AFTER post_salary');
CALL add_column_if_missing('hrm_salary_calculation', 'safety_award_standard',
    'ALTER TABLE hrm_salary_calculation ADD COLUMN safety_award_standard DECIMAL(12,2) NULL COMMENT ''安全环保奖标准'' AFTER overtime_hourly_rate');
CALL add_column_if_missing('hrm_salary_calculation', 'performance_standard',
    'ALTER TABLE hrm_salary_calculation ADD COLUMN performance_standard DECIMAL(12,2) NULL COMMENT ''绩效标准'' AFTER safety_award_standard');
CALL add_column_if_missing('hrm_salary_calculation', 'paid_attendance_days',
    'ALTER TABLE hrm_salary_calculation ADD COLUMN paid_attendance_days DECIMAL(6,2) NULL COMMENT ''计薪出勤(天)'' AFTER work_days');
CALL add_column_if_missing('hrm_salary_calculation', 'standard_work_hours',
    'ALTER TABLE hrm_salary_calculation ADD COLUMN standard_work_hours DECIMAL(8,2) NULL COMMENT ''标准出勤时长(小时)'' AFTER paid_attendance_days');
CALL add_column_if_missing('hrm_salary_calculation', 'legal_holiday_hours',
    'ALTER TABLE hrm_salary_calculation ADD COLUMN legal_holiday_hours DECIMAL(8,2) NULL COMMENT ''法定假日时长(小时)'' AFTER overtime_hours');
CALL add_column_if_missing('hrm_salary_calculation', 'standard_attendance_pay',
    'ALTER TABLE hrm_salary_calculation ADD COLUMN standard_attendance_pay DECIMAL(12,2) NULL COMMENT ''标准出勤工资'' AFTER legal_holiday_hours');
CALL add_column_if_missing('hrm_salary_calculation', 'legal_holiday_pay',
    'ALTER TABLE hrm_salary_calculation ADD COLUMN legal_holiday_pay DECIMAL(12,2) NULL COMMENT ''法定假工资'' AFTER overtime_pay');
CALL add_column_if_missing('hrm_salary_calculation', 'safety_award',
    'ALTER TABLE hrm_salary_calculation ADD COLUMN safety_award DECIMAL(12,2) NULL COMMENT ''安全环保奖'' AFTER legal_holiday_pay');
CALL add_column_if_missing('hrm_salary_calculation', 'floating_performance_pay',
    'ALTER TABLE hrm_salary_calculation ADD COLUMN floating_performance_pay DECIMAL(12,2) NULL COMMENT ''浮动绩效工资'' AFTER safety_award');
CALL add_column_if_missing('hrm_salary_calculation', 'payable_salary',
    'ALTER TABLE hrm_salary_calculation ADD COLUMN payable_salary DECIMAL(12,2) NULL COMMENT ''应付工资'' AFTER floating_performance_pay');
CALL add_column_if_missing('hrm_salary_calculation', 'seniority_pay',
    'ALTER TABLE hrm_salary_calculation ADD COLUMN seniority_pay DECIMAL(12,2) NULL COMMENT ''工龄工资'' AFTER payable_salary');
CALL add_column_if_missing('hrm_salary_calculation', 'night_shift_allowance',
    'ALTER TABLE hrm_salary_calculation ADD COLUMN night_shift_allowance DECIMAL(12,2) NULL COMMENT ''夜班补助'' AFTER seniority_pay');
CALL add_column_if_missing('hrm_salary_calculation', 'education_allowance',
    'ALTER TABLE hrm_salary_calculation ADD COLUMN education_allowance DECIMAL(12,2) NULL COMMENT ''学历补贴'' AFTER night_shift_allowance');
CALL add_column_if_missing('hrm_salary_calculation', 'health_check_allowance',
    'ALTER TABLE hrm_salary_calculation ADD COLUMN health_check_allowance DECIMAL(12,2) NULL COMMENT ''体检'' AFTER education_allowance');
CALL add_column_if_missing('hrm_salary_calculation', 'excellent_employee_award',
    'ALTER TABLE hrm_salary_calculation ADD COLUMN excellent_employee_award DECIMAL(12,2) NULL COMMENT ''优秀员工奖'' AFTER health_check_allowance');
CALL add_column_if_missing('hrm_salary_calculation', 'other_subsidy',
    'ALTER TABLE hrm_salary_calculation ADD COLUMN other_subsidy DECIMAL(12,2) NULL COMMENT ''其他补助'' AFTER excellent_employee_award');
CALL add_column_if_missing('hrm_salary_calculation', 'dept_performance',
    'ALTER TABLE hrm_salary_calculation ADD COLUMN dept_performance DECIMAL(12,2) NULL COMMENT ''部门绩效'' AFTER other_subsidy');
CALL add_column_if_missing('hrm_salary_calculation', 'addition_total',
    'ALTER TABLE hrm_salary_calculation ADD COLUMN addition_total DECIMAL(12,2) NULL COMMENT ''加项合计'' AFTER dept_performance');
CALL add_column_if_missing('hrm_salary_calculation', 'missed_punch_deduction',
    'ALTER TABLE hrm_salary_calculation ADD COLUMN missed_punch_deduction DECIMAL(12,2) NULL COMMENT ''漏打卡'' AFTER addition_total');
CALL add_column_if_missing('hrm_salary_calculation', 'late_early_deduction',
    'ALTER TABLE hrm_salary_calculation ADD COLUMN late_early_deduction DECIMAL(12,2) NULL COMMENT ''迟到早退'' AFTER missed_punch_deduction');
CALL add_column_if_missing('hrm_salary_calculation', 'deduction_total',
    'ALTER TABLE hrm_salary_calculation ADD COLUMN deduction_total DECIMAL(12,2) NULL COMMENT ''扣款合计'' AFTER other_deduction');
DROP PROCEDURE IF EXISTS add_column_if_missing;
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceHolidaySaveReqVO.java
@@ -26,8 +26,8 @@
    @NotNull(message = "类型不能为空")
    private Integer type;
    @Schema(description = "是否法定节假日:0-否,1-是", example = "1")
    private Integer isLegal;
    @Schema(description = "是否法定节假日:false-否,true-是(兼容 0/1 æ•°å­—)", example = "true")
    private Boolean isLegal;
    @Schema(description = "备注", example = "春节假期")
    private String remark;
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeRespVO.java
@@ -116,6 +116,26 @@
    @ExcelProperty("基本工资")
    private BigDecimal baseSalary;
    @Schema(description = "标准工资")
    @ExcelProperty("标准工资")
    private BigDecimal standardSalary;
    @Schema(description = "岗位工资")
    @ExcelProperty("岗位工资")
    private BigDecimal postSalary;
    @Schema(description = "加班工资标准(元/小时)")
    @ExcelProperty("加班工资标准")
    private BigDecimal overtimeHourlyRate;
    @Schema(description = "安全环保奖标准")
    @ExcelProperty("安全环保奖标准")
    private BigDecimal safetyAwardStandard;
    @Schema(description = "绩效标准")
    @ExcelProperty("绩效标准")
    private BigDecimal performanceStandard;
    @Schema(description = "角色ID列表")
    private List<Long> roleIds;
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeSaveReqVO.java
@@ -90,6 +90,21 @@
    @Schema(description = "基本工资", example = "10000.00")
    private BigDecimal baseSalary;
    @Schema(description = "标准工资", example = "8000.00")
    private BigDecimal standardSalary;
    @Schema(description = "岗位工资", example = "2000.00")
    private BigDecimal postSalary;
    @Schema(description = "加班工资标准(元/小时)", example = "45.98")
    private BigDecimal overtimeHourlyRate;
    @Schema(description = "安全环保奖标准", example = "500.00")
    private BigDecimal safetyAwardStandard;
    @Schema(description = "绩效标准", example = "1500.00")
    private BigDecimal performanceStandard;
    @Schema(description = "角色ID列表", example = "[1, 2]")
    private List<Long> roleIds;
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalaryCalculationController.java
@@ -1,6 +1,8 @@
package cn.iocoder.yudao.module.hrm.controller.admin.salary;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
@@ -15,7 +17,9 @@
import cn.iocoder.yudao.module.hrm.service.employee.HrmEmployeeService;
import cn.iocoder.yudao.module.hrm.service.salary.HrmSalaryCalculationService;
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 io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -45,6 +49,8 @@
    private HrmEmployeeService employeeService;
    @Resource
    private DeptApi deptApi;
    @Resource
    private PostApi postApi;
    @GetMapping("/page")
    @Operation(summary = "获得薪酬核算分页")
@@ -84,11 +90,22 @@
                .collect(Collectors.toSet());
        Map<Long, DeptRespDTO> deptMap = CollUtil.isEmpty(deptIds) ? Collections.emptyMap() : deptApi.getDeptMap(deptIds);
        // æ‰¹é‡èŽ·å–å²—ä½ä¿¡æ¯ï¼ˆå‘˜å·¥è¡¨ä»¥ JSON æ•°ç»„字符串保存岗位 ID)
        Set<Long> postIds = new HashSet<>();
        for (HrmEmployeeDO employee : employeeMap.values()) {
            if (StrUtil.isNotBlank(employee.getPostIds())) {
                postIds.addAll(JSONUtil.toList(employee.getPostIds(), Long.class));
            }
        }
        Map<Long, PostRespDTO> postMap = CollUtil.isEmpty(postIds) ? Collections.emptyMap() : postApi.getPostMap(postIds);
        // å¡«å……信息
        for (HrmSalaryCalculationRespVO respVO : respList) {
            HrmEmployeeDO employee = employeeMap.get(respVO.getUserId());
            if (employee != null) {
                respVO.setUserName(employee.getName());
                respVO.setHireDate(employee.getHireDate());
                respVO.setPostName(resolvePostName(employee, postMap));
            }
            DeptRespDTO dept = deptMap.get(respVO.getDeptId());
            if (dept != null) {
@@ -97,6 +114,19 @@
        }
    }
    /**
     * è§£æžå‘˜å·¥å²—位名称(多个岗位用逗号连接)
     */
    private String resolvePostName(HrmEmployeeDO employee, Map<Long, PostRespDTO> postMap) {
        if (StrUtil.isBlank(employee.getPostIds()) || CollUtil.isEmpty(postMap)) {
            return null;
        }
        return JSONUtil.toList(employee.getPostIds(), Long.class).stream()
                .filter(postMap::containsKey)
                .map(id -> postMap.get(id).getName())
                .collect(Collectors.joining(","));
    }
    @GetMapping("/get")
    @Operation(summary = "获得薪酬核算")
    @Parameter(name = "id", description = "核算ID", required = true)
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmSalaryCalculationRespVO.java
@@ -8,6 +8,7 @@
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - è–ªé…¬æ ¸ç®— Response VO")
@@ -41,6 +42,126 @@
    @ExcelProperty("部门名称")
    private String deptName;
    @Schema(description = "入职日期")
    @ExcelProperty("入职日期")
    private LocalDate hireDate;
    @Schema(description = "岗位名称(多个用逗号分隔)")
    @ExcelProperty("岗位")
    private String postName;
    // ========== ä¹æ³“工资表:标准项(取自员工档案) ==========
    @Schema(description = "标准工资")
    @ExcelProperty("标准工资")
    private BigDecimal standardSalary;
    @Schema(description = "岗位工资")
    @ExcelProperty("岗位工资")
    private BigDecimal postSalary;
    @Schema(description = "加班工资标准(元/小时)")
    @ExcelProperty("加班工资(标准)")
    private BigDecimal overtimeHourlyRate;
    @Schema(description = "安全环保奖标准")
    @ExcelProperty("安全环保奖(标准)")
    private BigDecimal safetyAwardStandard;
    @Schema(description = "绩效标准")
    @ExcelProperty("绩效(标准)")
    private BigDecimal performanceStandard;
    // ========== ä¹æ³“工资表:出勤与计薪 ==========
    @Schema(description = "标准出勤天数")
    @ExcelProperty("标准出勤天数")
    private Integer workDays;
    @Schema(description = "计薪出勤(天)")
    @ExcelProperty("计薪出勤")
    private BigDecimal paidAttendanceDays;
    @Schema(description = "标准出勤时长(小时)")
    @ExcelProperty("标准出勤时长")
    private BigDecimal standardWorkHours;
    @Schema(description = "加班时长(小时)")
    @ExcelProperty("加班时长")
    private BigDecimal overtimeHours;
    @Schema(description = "法定假日时长(小时)")
    @ExcelProperty("法定假日时长")
    private BigDecimal legalHolidayHours;
    @Schema(description = "标准出勤工资")
    @ExcelProperty("标准出勤工资")
    private BigDecimal standardAttendancePay;
    @Schema(description = "加班工资")
    @ExcelProperty("加班工资")
    private BigDecimal overtimePay;
    @Schema(description = "法定假工资")
    @ExcelProperty("法定假工资")
    private BigDecimal legalHolidayPay;
    @Schema(description = "安全环保奖")
    @ExcelProperty("安全环保奖")
    private BigDecimal safetyAward;
    @Schema(description = "浮动绩效工资")
    @ExcelProperty("浮动绩效工资")
    private BigDecimal floatingPerformancePay;
    @Schema(description = "应付工资")
    @ExcelProperty("应付工资")
    private BigDecimal payableSalary;
    @Schema(description = "工龄工资")
    @ExcelProperty("工龄工资")
    private BigDecimal seniorityPay;
    @Schema(description = "夜班补助")
    @ExcelProperty("夜班补助")
    private BigDecimal nightShiftAllowance;
    @Schema(description = "学历补贴")
    @ExcelProperty("学历补贴")
    private BigDecimal educationAllowance;
    @Schema(description = "体检")
    @ExcelProperty("体检")
    private BigDecimal healthCheckAllowance;
    @Schema(description = "优秀员工奖")
    @ExcelProperty("优秀员工奖")
    private BigDecimal excellentEmployeeAward;
    @Schema(description = "其他补助")
    @ExcelProperty("其他补助")
    private BigDecimal otherSubsidy;
    @Schema(description = "部门绩效")
    @ExcelProperty("部门绩效")
    private BigDecimal deptPerformance;
    @Schema(description = "加项合计")
    @ExcelProperty("加项合计")
    private BigDecimal additionTotal;
    @Schema(description = "漏打卡")
    @ExcelProperty("漏打卡")
    private BigDecimal missedPunchDeduction;
    @Schema(description = "迟到早退")
    @ExcelProperty("迟到早退")
    private BigDecimal lateEarlyDeduction;
    @Schema(description = "扣款合计")
    @ExcelProperty("扣款合计")
    private BigDecimal deductionTotal;
    @Schema(description = "基本工资")
    @ExcelProperty("基本工资")
    private BigDecimal baseSalary;
@@ -48,10 +169,6 @@
    @Schema(description = "绩效工资")
    @ExcelProperty("绩效工资")
    private BigDecimal performanceSalary;
    @Schema(description = "加班工资")
    @ExcelProperty("加班工资")
    private BigDecimal overtimePay;
    @Schema(description = "餐补")
    @ExcelProperty("餐补")
@@ -88,14 +205,6 @@
    @Schema(description = "实发工资")
    @ExcelProperty("实发工资")
    private BigDecimal actualSalary;
    @Schema(description = "出勤天数")
    @ExcelProperty("出勤天数")
    private Integer workDays;
    @Schema(description = "加班时长")
    @ExcelProperty("加班时长")
    private BigDecimal overtimeHours;
    @Schema(description = "排班总工时")
    @ExcelProperty("排班总工时")
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmSalaryCalculationSaveReqVO.java
@@ -41,6 +41,78 @@
        @NotNull(message = "基本工资不能为空")
        private BigDecimal baseSalary;
        @Schema(description = "标准工资")
        private BigDecimal standardSalary;
        @Schema(description = "岗位工资")
        private BigDecimal postSalary;
        @Schema(description = "加班工资标准(元/小时)")
        private BigDecimal overtimeHourlyRate;
        @Schema(description = "安全环保奖标准")
        private BigDecimal safetyAwardStandard;
        @Schema(description = "绩效标准")
        private BigDecimal performanceStandard;
        @Schema(description = "计薪出勤(天)")
        private BigDecimal paidAttendanceDays;
        @Schema(description = "标准出勤时长(小时)")
        private BigDecimal standardWorkHours;
        @Schema(description = "法定假日时长(小时)")
        private BigDecimal legalHolidayHours;
        @Schema(description = "标准出勤工资")
        private BigDecimal standardAttendancePay;
        @Schema(description = "法定假工资")
        private BigDecimal legalHolidayPay;
        @Schema(description = "安全环保奖")
        private BigDecimal safetyAward;
        @Schema(description = "浮动绩效工资")
        private BigDecimal floatingPerformancePay;
        @Schema(description = "应付工资")
        private BigDecimal payableSalary;
        @Schema(description = "工龄工资")
        private BigDecimal seniorityPay;
        @Schema(description = "夜班补助")
        private BigDecimal nightShiftAllowance;
        @Schema(description = "学历补贴")
        private BigDecimal educationAllowance;
        @Schema(description = "体检")
        private BigDecimal healthCheckAllowance;
        @Schema(description = "优秀员工奖")
        private BigDecimal excellentEmployeeAward;
        @Schema(description = "其他补助")
        private BigDecimal otherSubsidy;
        @Schema(description = "部门绩效")
        private BigDecimal deptPerformance;
        @Schema(description = "加项合计")
        private BigDecimal additionTotal;
        @Schema(description = "漏打卡")
        private BigDecimal missedPunchDeduction;
        @Schema(description = "迟到早退")
        private BigDecimal lateEarlyDeduction;
        @Schema(description = "扣款合计")
        private BigDecimal deductionTotal;
        @Schema(description = "绩效工资")
        private BigDecimal performanceSalary;
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/attendance/HrmAttendanceSummaryDO.java
@@ -2,6 +2,7 @@
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
@@ -43,7 +44,10 @@
    /**
     * å¹´æœˆï¼Œæ ¼å¼ï¼šyyyy-MM
     *
     * year_month æ˜¯ MySQL ä¿ç•™å­—(INTERVAL å•位),不加反引号会报 SQL è¯­æ³•错误
     */
    @TableField("`year_month`")
    private String yearMonth;
    /**
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/HrmEmployeeDO.java
@@ -160,6 +160,33 @@
    private BigDecimal baseSalary;
    /**
     * æ ‡å‡†å·¥èµ„
     *
     * ç”¨äºŽå·¥èµ„表:标准出勤工资 = æ ‡å‡†å·¥èµ„ Ã· æ ‡å‡†å‡ºå‹¤å¤©æ•° Ã— è®¡è–ªå‡ºå‹¤
     */
    private BigDecimal standardSalary;
    /**
     * å²—位工资
     */
    private BigDecimal postSalary;
    /**
     * åŠ ç­å·¥èµ„æ ‡å‡†ï¼ˆå…ƒ/小时)
     */
    private BigDecimal overtimeHourlyRate;
    /**
     * å®‰å…¨çŽ¯ä¿å¥–æ ‡å‡†
     */
    private BigDecimal safetyAwardStandard;
    /**
     * ç»©æ•ˆæ ‡å‡†
     */
    private BigDecimal performanceStandard;
    /**
     * è§’色ID列表
     *
     * ç”¨äºŽåˆ†é…ç³»ç»Ÿç”¨æˆ·è§’色,JSON æ ¼å¼å­˜å‚¨
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/salary/HrmSalaryCalculationDO.java
@@ -53,6 +53,126 @@
    private Long deptId;
    /**
     * æ ‡å‡†å·¥èµ„
     */
    private BigDecimal standardSalary;
    /**
     * å²—位工资
     */
    private BigDecimal postSalary;
    /**
     * åŠ ç­å·¥èµ„æ ‡å‡†ï¼ˆå…ƒ/小时)
     */
    private BigDecimal overtimeHourlyRate;
    /**
     * å®‰å…¨çŽ¯ä¿å¥–æ ‡å‡†
     */
    private BigDecimal safetyAwardStandard;
    /**
     * ç»©æ•ˆæ ‡å‡†
     */
    private BigDecimal performanceStandard;
    /**
     * è®¡è–ªå‡ºå‹¤ï¼ˆå¤©ï¼‰
     */
    private BigDecimal paidAttendanceDays;
    /**
     * æ ‡å‡†å‡ºå‹¤æ—¶é•¿ï¼ˆå°æ—¶ï¼‰
     */
    private BigDecimal standardWorkHours;
    /**
     * æ³•定假日时长(小时)
     */
    private BigDecimal legalHolidayHours;
    /**
     * æ ‡å‡†å‡ºå‹¤å·¥èµ„
     */
    private BigDecimal standardAttendancePay;
    /**
     * æ³•定假工资
     */
    private BigDecimal legalHolidayPay;
    /**
     * å®‰å…¨çŽ¯ä¿å¥–
     */
    private BigDecimal safetyAward;
    /**
     * æµ®åŠ¨ç»©æ•ˆå·¥èµ„
     */
    private BigDecimal floatingPerformancePay;
    /**
     * åº”付工资
     */
    private BigDecimal payableSalary;
    /**
     * å·¥é¾„工资
     */
    private BigDecimal seniorityPay;
    /**
     * å¤œç­è¡¥åŠ©
     */
    private BigDecimal nightShiftAllowance;
    /**
     * å­¦åŽ†è¡¥è´´
     */
    private BigDecimal educationAllowance;
    /**
     * ä½“检
     */
    private BigDecimal healthCheckAllowance;
    /**
     * ä¼˜ç§€å‘˜å·¥å¥–
     */
    private BigDecimal excellentEmployeeAward;
    /**
     * å…¶ä»–补助
     */
    private BigDecimal otherSubsidy;
    /**
     * éƒ¨é—¨ç»©æ•ˆ
     */
    private BigDecimal deptPerformance;
    /**
     * åŠ é¡¹åˆè®¡
     */
    private BigDecimal additionTotal;
    /**
     * æ¼æ‰“卡
     */
    private BigDecimal missedPunchDeduction;
    /**
     * è¿Ÿåˆ°æ—©é€€
     */
    private BigDecimal lateEarlyDeduction;
    /**
     * æ‰£æ¬¾åˆè®¡
     */
    private BigDecimal deductionTotal;
    /**
     * åŸºæœ¬å·¥èµ„
     */
    private BigDecimal baseSalary;
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceHolidayServiceImpl.java
@@ -35,9 +35,8 @@
        // æ ¡éªŒæ—¥æœŸæ˜¯å¦å·²å­˜åœ¨é…ç½®
        validateAttendanceHolidayDateExists(createReqVO.getDate());
        HrmAttendanceHolidayDO attendanceHoliday = BeanUtils.toBean(createReqVO, HrmAttendanceHolidayDO.class);
        if (attendanceHoliday.getIsLegal() == null) {
            attendanceHoliday.setIsLegal(0);
        }
        // VO ä¾§ç”¨ Boolean å…¼å®¹å‰ç«¯å‹¾é€‰æ¡†çš„ true/false,落库统一为 0/1
        attendanceHoliday.setIsLegal(Boolean.TRUE.equals(createReqVO.getIsLegal()) ? 1 : 0);
        attendanceHolidayMapper.insert(attendanceHoliday);
        return attendanceHoliday.getId();
    }
@@ -51,6 +50,7 @@
            validateAttendanceHolidayDateExists(updateReqVO.getDate());
        }
        HrmAttendanceHolidayDO updateObj = BeanUtils.toBean(updateReqVO, HrmAttendanceHolidayDO.class);
        updateObj.setIsLegal(Boolean.TRUE.equals(updateReqVO.getIsLegal()) ? 1 : 0);
        attendanceHolidayMapper.updateById(updateObj);
    }
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeServiceImpl.java
@@ -340,6 +340,14 @@
    @Override
    public List<HrmEmployeeDO> getEmployeeList(Collection<Long> ids) {
        // ids ä¸º null æ—¶è¡¨ç¤ºæŸ¥è¯¢å…¨éƒ¨å‘˜å·¥ï¼ˆè€ƒå‹¤æ±‡æ€»ã€è–ªé…¬æ ¸ç®—等批量场景依赖此语义);
        // æ˜¾å¼ä¼ å…¥ç©ºé›†åˆæ—¶è¿”回空,避免误查全员
        if (ids == null) {
            return employeeMapper.selectList(null);
        }
        if (ids.isEmpty()) {
            return Collections.emptyList();
        }
        return employeeMapper.selectByIds(ids);
    }
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryCalculationServiceImpl.java
@@ -6,6 +6,7 @@
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;
@@ -21,6 +22,7 @@
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;
@@ -32,6 +34,7 @@
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;
@@ -110,6 +113,8 @@
    private HrmEmployeeScheduleMapper employeeScheduleMapper;
    @Resource
    private HrmShiftMapper shiftMapper;
    @Resource
    private HrmAttendanceRecordMapper attendanceRecordMapper;
    @Override
    public PageResult<HrmSalaryCalculationDO> getSalaryCalculationPage(HrmSalaryCalculationPageReqVO pageReqVO) {
@@ -235,17 +240,65 @@
        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;
        OvertimeSalary overtimeSalary = calculateOvertimeSalary(employee, employeeSalary, period, baseSalary);
        BigDecimal overtimePay = overtimeSalary.pay();
        // æŒ‰æŽ’班累计出勤天数与排班总工时(无排班时出勤天数回退默认 22)
        // ========== ä¹æ³“工资表:标准项取自员工档案 ==========
        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));
        // åº”发工资总额
        BigDecimal totalIncome = baseSalary.add(performanceSalary).add(mealAllowance)
                .add(transportAllowance).add(otherAllowance).add(overtimePay);
        // è®¡è–ªå‡ºå‹¤ = æ ‡å‡†å‡ºå‹¤å¤©æ•° - ç¼ºå‹¤å¤©æ•°ï¼ˆè€ƒå‹¤è®°å½•中缺卡的天数,按天去重)
        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);
        // åŠ é¡¹ï¼ˆå·¥é¾„å·¥èµ„/夜班补助/学历补贴/体检/优秀员工奖/其他补助/部门绩效)与扣项
        // ï¼ˆæ¼æ‰“卡/迟到早退/其他扣款)生成时默认为 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;
        // è®¡ç¨Žå£å¾„:应付工资 + åŠ é¡¹åˆè®¡ - æ‰£æ¬¾åˆè®¡
        BigDecimal totalIncome = payableSalary.add(additionTotal).subtract(deductionTotal);
        // èŽ·å–ç¤¾ä¿åŸºæ•°ï¼šå‘˜å·¥ç¤¾ä¿æ¡£æ¡ˆ > æ–¹æ¡ˆé»˜è®¤ç¼´è´¹åŸºæ•° > å‘˜å·¥åŸºæœ¬å·¥èµ„
        BigDecimal socialSecurityBase = (socialSecurity != null && socialSecurity.getSocialSecurityBase() != null)
@@ -284,8 +337,8 @@
        // å…¬ç§¯é‡‘扣除 = åŸºæ•° Ã— å…¬ç§¯é‡‘个人比例
        BigDecimal housingFundDeduction = housingFundBase.multiply(housingFundPersonalRatio).setScale(2, RoundingMode.HALF_UP);
        // è®¡ç®—请假扣款
        BigDecimal leaveDeduction = calculateLeaveDeduction(employee, period, baseSalary);
        // è¯·å‡æ‰£æ¬¾ï¼šæ–°ç‰ˆå·¥èµ„表按「计薪出勤」折减标准出勤工资,不再单独重复扣一次,保留字段兼容旧数据
        BigDecimal leaveDeduction = BigDecimal.ZERO;
        // è®¡ç®—个税扣除
        // åº”纳税所得额 = åº”发工资 - ç¤¾ä¿æ‰£é™¤ - å…¬ç§¯é‡‘扣除 - èµ·å¾ç‚¹(5000)
@@ -301,15 +354,43 @@
        BigDecimal otherDeduction = BigDecimal.ZERO;
        // è®¡ç®—实发工资 = åº”发 - æ‰£é™¤
        BigDecimal totalDeduction = socialSecurityDeduction.add(housingFundDeduction)
                .add(taxDeduction).add(otherDeduction).add(leaveDeduction);
        BigDecimal actualSalary = totalIncome.subtract(totalDeduction);
        // ç¨ŽåŽå·¥èµ„ = åº”付工资 + åŠ é¡¹åˆè®¡ - æ‰£æ¬¾åˆè®¡ - ç¤¾ä¿ - å…¬ç§¯é‡‘ - ä¸ªç¨Ž
        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)
                // ä¹æ³“工资表:加项 / æ‰£é¡¹
                .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)
@@ -323,7 +404,7 @@
                .leaveDeduction(leaveDeduction)
                .actualSalary(actualSalary)
                .workDays(workDays) // æŒ‰æŽ’班累计应出勤天数,无排班时回退 22
                .overtimeHours(overtimeSalary.hours())
                .overtimeHours(overtimeHours)
                .totalWorkHours(totalWorkHours)
                .status(0) // å¾…确认
                .build();
@@ -406,19 +487,24 @@
    }
    /**
     * æ±‡æ€»å½“月审批通过的加班申请。小时工资按基本工资 / 21.75 / 8 è®¡ç®—ï¼›
     * å·¥ä½œæ—¥é»˜è®¤ 1.5 å€ã€ä¼‘息日默认 2 å€ã€æ³•定节假日默认 3 å€ï¼Œå‡å¯ç”±è–ªé…¬ç»“构覆盖。
     * æ±‡æ€»å½“月审批通过的加班申请。
     *
     * å‘˜å·¥æ¡£æ¡ˆç»´æŠ¤äº†ã€ŒåŠ ç­å·¥èµ„æ ‡å‡†ï¼ˆå…ƒ/小时)」时:加班工资 = åŠ ç­æ—¶é•¿ Ã— å•价,
     * æ³•定假工资 = æ³•定假日时长 Ã— å•ä»· Ã— æ³•定节假日倍率(默认 3)。
     * æœªç»´æŠ¤å•价时回退旧口径:基本工资 / 21.75 / 8 Ã— å„类型倍率。
     *
     * æ³•定节假日加班单独统计,不计入「加班时长」列,避免与「法定假工资」重复计薪。
     */
    private OvertimeSalary calculateOvertimeSalary(HrmEmployeeDO employee, HrmEmployeeSalaryDO employeeSalary,
                                                    String period, BigDecimal baseSalary) {
                                                    String period, BigDecimal baseSalary, BigDecimal overtimeHourlyRate) {
        if (employee.getUserId() == null) {
            return new OvertimeSalary(BigDecimal.ZERO, BigDecimal.ZERO);
            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 new OvertimeSalary(BigDecimal.ZERO, BigDecimal.ZERO);
            return OvertimeSalary.EMPTY;
        }
        Long structureId = employeeSalary.getSalaryStructureId() != null
                ? employeeSalary.getSalaryStructureId() : employee.getSalaryStructureId();
@@ -429,29 +515,62 @@
                ? 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 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;
            }
            BigDecimal multiplier = switch (type) {
                case WORKDAY -> workdayMultiplier;
                case REST_DAY -> restDayMultiplier;
                case LEGAL_HOLIDAY -> legalHolidayMultiplier;
            };
            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));
                totalPay.setScale(2, RoundingMode.HALF_UP),
                legalHolidayHours.setScale(1, RoundingMode.HALF_UP),
                legalHolidayPay.setScale(2, RoundingMode.HALF_UP));
    }
    private record OvertimeSalary(BigDecimal hours, BigDecimal pay) {}
    /**
     * ç»Ÿè®¡å½“月考勤记录中缺卡的天数(同一日期多次缺卡只算一天)
     */
    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) {}
@@ -594,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();
@@ -611,6 +731,50 @@
    }
    /**
     * ä¿å­˜å‰æŒ‰ä¹æ³“工资表口径重算合计,避免前端漏算或旧客户端传入不一致的合计值。
     *
     * åŠ é¡¹åˆè®¡ = å·¥é¾„工资 + å¤œç­è¡¥åŠ© + å­¦åŽ†è¡¥è´´ + ä½“检 + ä¼˜ç§€å‘˜å·¥å¥– + å…¶ä»–补助 + éƒ¨é—¨ç»©æ•ˆ
     * æ‰£æ¬¾åˆè®¡ = æ¼æ‰“卡 + è¿Ÿåˆ°æ—©é€€ + å…¶ä»–扣款
     * ç¨ŽåŽå·¥èµ„ = åº”付工资 + åŠ é¡¹åˆè®¡ - æ‰£æ¬¾åˆè®¡ - ç¤¾ä¿ - å…¬ç§¯é‡‘ - ä¸ªç¨Ž
     *
     * ä»…当本次记录带有「应付工资」时重算;只有旧字段的老数据保持原样,避免把未维护的新字段清成 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));
    }
    /**
     * å†…部生成发放台账方法(供自动生成调用)
     */
    private void generatePaymentInternal(String period, List<HrmSalaryCalculationDO> calculationList) {