fix(hrm): 薪酬核算按出勤天数联动扣款、个税配置可保存、离职提交报错

一、薪酬核算:应出/实出天数真正参与工资计算(客户反馈"改了天数实发不变")
- 新增「缺勤扣款」算法:日工资 = 基本工资 / 应出天数;
请假天数按「请假扣款配置」比例扣(调休等未配置=不扣,病假按配置比例),
未被请假单覆盖的缺勤按旷工 100% 扣,扣款封顶为基本工资。
- 应出天数优先取考勤汇总的 work_days(客户按实际月份维护,如 30 天),
其次排班累计,最后回退 22;实出天数取考勤汇总 actual_days。
- 未生成考勤汇总时不做缺勤扣款(视为满勤,只按请假单扣),
避免一重算就把没录考勤的员工工资扣光。
- 实发工资统一走 calcActualSalary:新建核算原本是独立算的,导致"新建正确、改单变少";
并且实发不允许为负(缺勤扣满后社保叠加会出现负数,按本月无工资处理置 0)。
- 修改核算单 / 批量保存时同样重算缺勤扣款与实发,列表里改天数后金额立刻跟着变。

二、个税税率配置:税率填 0 保存报「生效日期不能为空」
- 前端表单缺「配置名称」「生效日期」两个字段,后端这两个是必填。
- 前端补上两个字段;后端改为可选并兜底:生效日期默认当月1号,
配置名称自动生成(如「2026年11月个税税率表」),速算扣除数/上限缺省按 0。

三、离职申请:提交报错
- 提交接口的 processDefinitionKey 为必填但前端只传 id,改为可选,
不传时自动取「离职审批(hrm_resignation_approve)」分类下最新活跃流程(与调岗一致)。
- 新增申请报 NPE:adminUserApi.getUser(userId) 返回 null 时只重置了 userId 没重新查,
后面 user.getDeptId() 直接空指针;改为回退后重新查询,部门依次取
请求参数 → 账号部门 → 0。

实测(mom-zyjc):
- 应出30/实出0 → 缺勤扣款 1950(封顶)→ 实发 0.00 ✔
- 应出30/实出20 → 缺勤扣款 650(10天×65)→ 实发 749.17 ✔
- 个税税率 0 + 不传生效日期 → 保存成功,名称/日期自动生成 ✔
已修改5个文件
238 ■■■■■ 文件已修改
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/HrmResignationApplicationController.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmTaxRateConfigSaveReqVO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmResignationApplicationServiceImpl.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryCalculationServiceImpl.java 167 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmTaxRateConfigServiceImpl.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/HrmResignationApplicationController.java
@@ -83,10 +83,10 @@
    @PostMapping("/submit")
    @Operation(summary = "提交离职申请审批")
    @Parameter(name = "id", description = "申请ID", required = true)
    @Parameter(name = "processDefinitionKey", description = "流程定义Key", required = true)
    @Parameter(name = "processDefinitionKey", description = "流程定义Key(不传时自动使用「离职审批」分类下最新的活跃流程)")
    @PreAuthorize("@ss.hasPermission('hrm:resignation:submit')")
    public CommonResult<Boolean> submitResignationApplication(@RequestParam("id") Long id,
                                                              @RequestParam("processDefinitionKey") String processDefinitionKey) {
                                                              @RequestParam(value = "processDefinitionKey", required = false) String processDefinitionKey) {
        Long userId = SecurityFrameworkUtils.getLoginUserId();
        resignationApplicationService.submitResignationApplication(id, processDefinitionKey, userId);
        return success(true);
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmTaxRateConfigSaveReqVO.java
@@ -15,12 +15,10 @@
    @Schema(description = "配置ID", example = "1")
    private Long id;
    @Schema(description = "配置名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024年个税税率表")
    @NotBlank(message = "配置名称不能为空")
    @Schema(description = "配置名称,不传时自动生成(如 2026年11月个税税率表)", example = "2024年个税税率表")
    private String name;
    @Schema(description = "生效日期", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "生效日期不能为空")
    @Schema(description = "生效日期,不传时默认当月1号")
    private LocalDate effectiveDate;
    @Schema(description = "税率级数:1-7", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmResignationApplicationServiceImpl.java
@@ -2,6 +2,7 @@
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi;
@@ -33,6 +34,7 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -93,12 +95,17 @@
        String no = generateUniqueNo();
        AdminUserRespDTO user = adminUserApi.getUser(userId);
        if (user == null) {
            // 传入的可能不是系统用户ID(例如员工ID),回退到当前登录用户并重新查询,避免后续 NPE
            userId = getCurrentUserId();
            user = adminUserApi.getUser(userId);
        }
        HrmResignationApplicationDO application = BeanUtils.toBean(createReqVO, HrmResignationApplicationDO.class);
        application.setNo(no);
        application.setUserId(userId);
        application.setDeptId(createReqVO.getDeptId() != null ? createReqVO.getDeptId() : user.getDeptId());
        // 部门:优先取请求参数,其次取账号所属部门,最后兜底 0(该列 NOT NULL)
        Long deptId = createReqVO.getDeptId() != null ? createReqVO.getDeptId()
                : (user != null ? user.getDeptId() : null);
        application.setDeptId(deptId != null ? deptId : 0L);
        application.setStatus(HrmAuditStatusEnum.DRAFT.getStatus());
        application.setHandoverStatus(HANDOVER_STATUS_NOT_STARTED);
        resignationApplicationMapper.insert(application);
@@ -245,12 +252,14 @@
            throw exception(RESIGNATION_APPLICATION_SUBMIT_FAIL_NOT_DRAFT);
        }
        validateApproveCategoryAndProcessDefinition();
        ProcessDefinition processDefinition = bpmProcessDefinitionService.getActiveProcessDefinition(processDefinitionKey);
        // 未指定流程定义 Key 时,自动取「离职审批」分类下最新的活跃流程,避免前端未传 key 直接报系统异常
        String key = resolveProcessDefinitionKey(processDefinitionKey);
        ProcessDefinition processDefinition = bpmProcessDefinitionService.getActiveProcessDefinition(key);
        if (processDefinition == null) {
            throw exception(RESIGNATION_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS);
        }
        String processInstanceId = bpmProcessInstanceApi.createProcessInstance(userId, new BpmProcessInstanceCreateReqDTO()
                .setProcessDefinitionKey(processDefinitionKey).setBusinessKey(String.valueOf(id)));
                .setProcessDefinitionKey(key).setBusinessKey(String.valueOf(id)));
        resignationApplicationMapper.updateById(HrmResignationApplicationDO.builder()
                .id(id)
                .processInstanceId(processInstanceId)
@@ -258,6 +267,27 @@
                .build());
    }
    /**
     * 解析流程定义 Key:优先使用调用方传入值,为空时取「离职审批」分类下最新的活跃流程
     */
    private String resolveProcessDefinitionKey(String processDefinitionKey) {
        if (StrUtil.isNotBlank(processDefinitionKey)) {
            return processDefinitionKey.trim();
        }
        List<BpmProcessDefinitionInfoDO> definitionInfoList = bpmProcessDefinitionService
                .getProcessDefinitionInfoListByCategory(APPROVE_CATEGORY_CODE);
        if (CollUtil.isEmpty(definitionInfoList)) {
            throw exception(RESIGNATION_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS);
        }
        Set<String> processDefinitionIds = convertSet(definitionInfoList, BpmProcessDefinitionInfoDO::getProcessDefinitionId);
        List<ProcessDefinition> processDefinitions = bpmProcessDefinitionService.getProcessDefinitionList(processDefinitionIds);
        return processDefinitions.stream()
                .filter(pd -> !pd.isSuspended())
                .max(Comparator.comparingInt(ProcessDefinition::getVersion))
                .map(ProcessDefinition::getKey)
                .orElseThrow(() -> exception(RESIGNATION_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS));
    }
    private void validateApproveCategoryAndProcessDefinition() {
        List<String> codes = Collections.singletonList(APPROVE_CATEGORY_CODE);
        Map<String, ?> categoryMap = bpmCategoryService.getCategoryMap(codes);
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryCalculationServiceImpl.java
@@ -252,17 +252,25 @@
        OvertimeSalary overtimeSalary = calculateOvertimeSalary(employee, employeeSalary, period, baseSalary);
        BigDecimal overtimePay = overtimeSalary.pay();
        // 按排班累计应出天数与排班总工时(无排班时应出天数回退默认 22)
        // 应出天数:优先取考勤汇总里的「应出勤天数」(客户按实际月份维护,如 30 天),
        // 没有汇总时退回排班累计天数,再退回默认 22
        ScheduleStat scheduleStat = calculateScheduleStat(employeeId, period);
        Integer workDays = scheduleStat.scheduledDays() > 0 ? scheduleStat.scheduledDays() : 22;
        BigDecimal totalWorkHours = scheduleStat.totalWorkHours();
        // 实出天数:取自考勤汇总(无汇总记录时按 0 处理)
        Integer actualDays = 0;
        // 实出天数:取自考勤汇总。
        // 注意:null 表示该月尚未生成考勤汇总,此时不做「缺勤扣款」(视为满勤,只按请假单扣),
        //       否则一重算就会把所有没录考勤的员工工资扣光。
        Integer actualDays = null;
        HrmAttendanceSummaryDO attendanceSummary =
                attendanceSummaryMapper.selectByUserIdAndYearMonth(employeeId, period);
        if (attendanceSummary != null && attendanceSummary.getActualDays() != null) {
            actualDays = attendanceSummary.getActualDays();
        if (attendanceSummary != null) {
            if (attendanceSummary.getActualDays() != null) {
                actualDays = attendanceSummary.getActualDays();
            }
            if (attendanceSummary.getWorkDays() != null && attendanceSummary.getWorkDays() > 0) {
                workDays = attendanceSummary.getWorkDays();
            }
        }
        // 全勤奖:与其他津贴一致,由人工在核算单中维护,新建时默认 0
@@ -307,7 +315,8 @@
        }
        // 计算请假扣款
        BigDecimal leaveDeduction = calculateLeaveDeduction(employee, period, baseSalary);
        // 计算缺勤扣款(请假 + 旷工)
        BigDecimal leaveDeduction = calculateAttendanceDeduction(employee, period, baseSalary, workDays, actualDays);
        // 计算个税扣除
        // 应纳税所得额 = 应发工资 - 社保扣除 - 公积金扣除 - 起征点(5000)
@@ -323,12 +332,10 @@
        BigDecimal otherDeduction = BigDecimal.ZERO;
        // 计算实发工资 = 应发 - 扣除
        BigDecimal totalDeduction = socialSecurityDeduction.add(housingFundDeduction)
                .add(taxDeduction).add(otherDeduction).add(leaveDeduction);
        BigDecimal actualSalary = totalIncome.subtract(totalDeduction);
        // 说明:实发工资统一走 calcActualSalary 计算(收入 - 扣除,且不允许为负),
        // 与「修改核算单 / 批量保存」保持同一口径,避免出现负数工资
        return HrmSalaryCalculationDO.builder()
        HrmSalaryCalculationDO calculation = HrmSalaryCalculationDO.builder()
                .period(period)
                .userId(employeeId) // 使用员工ID
                .deptId(employee.getDeptId())
@@ -346,65 +353,83 @@
                .taxDeduction(taxDeduction)
                .otherDeduction(otherDeduction)
                .leaveDeduction(leaveDeduction)
                .actualSalary(actualSalary)
                .workDays(workDays) // 应出天数:按排班累计,无排班时回退 22
                .workDays(workDays) // 应出天数:优先考勤汇总,其次排班累计,最后回退 22
                .actualDays(actualDays) // 实出天数:取自考勤汇总
                .overtimeHours(overtimeSalary.hours())
                .totalWorkHours(totalWorkHours) // 保留历史字段,业务已改用 actualDays
                .status(0) // 待确认
                .build();
        calculation.setActualSalary(calcActualSalary(calculation));
        return calculation;
    }
    /**
     * 计算请假扣款
     * 计算「缺勤扣款」= 请假扣款 + 旷工扣款
     *
     * <p>规则:
     * <ol>
     *     <li>日工资 = 基本工资 / 应出天数(与应出天数口径一致,避免缺勤天数超过计薪天数时扣超);</li>
     *     <li>请假天数按「请假扣款配置」的比例扣:调休等未配置的类型比例视为 0(不扣),病假按配置比例扣;</li>
     *     <li>若该月已生成考勤汇总(actualDays 不为 null):
     *         旷工天数 = 应出天数 − 实出天数 − 请假天数(不为负),按 100% 扣;</li>
     *     <li>若未生成考勤汇总(actualDays 为 null),视为满勤,不做旷工扣款,只按请假单扣;</li>
     *     <li>扣款总额封顶为基本工资,避免出现负工资。</li>
     * </ol>
     *
     * @param employee   员工信息
     * @param period     核算周期(如2026-02)
     * @param period     核算周期(如 2026-02)
     * @param baseSalary 基本工资
     * @return 请假扣款金额
     * @param workDays   应出天数(排班累计,无排班时传 22)
     * @param actualDays 实出天数(取自考勤汇总;null 表示该月未生成汇总)
     * @return 缺勤扣款金额
     */
    private BigDecimal calculateLeaveDeduction(HrmEmployeeDO employee, String period, BigDecimal baseSalary) {
        // 解析周期获取月份开始和结束时间
        YearMonth yearMonth = YearMonth.parse(period);
        LocalDateTime monthStart = yearMonth.atDay(1).atStartOfDay();
        LocalDateTime monthEnd = yearMonth.atEndOfMonth().atTime(23, 59, 59);
    private BigDecimal calculateAttendanceDeduction(HrmEmployeeDO employee, String period,
                                                    BigDecimal baseSalary, Integer workDays, Integer actualDays) {
        int shouldDays = (workDays != null && workDays > 0) ? workDays : 22;
        // 日工资 = 基本工资 / 应出天数(保留 4 位,最后统一保留 2 位)
        BigDecimal dailySalary = baseSalary.divide(new BigDecimal(shouldDays), 4, RoundingMode.HALF_UP);
        // 查询员工在该月已审批通过的请假记录
        // 注意:请假申请的 userId 是系统用户ID,需要通过员工表获取
        BigDecimal totalDeduction = BigDecimal.ZERO;
        BigDecimal leaveDays = BigDecimal.ZERO;
        // 1. 请假扣款:按请假类型的配置比例
        Long systemUserId = employee.getUserId();
        if (systemUserId == null) {
            return BigDecimal.ZERO;
        }
        List<HrmLeaveApplicationDO> leaveApplications = leaveApplicationMapper
                .selectApprovedListByUserIdAndTimeRange(systemUserId, monthStart, monthEnd);
        if (leaveApplications.isEmpty()) {
            return BigDecimal.ZERO;
        if (systemUserId != null) {
            YearMonth yearMonth = YearMonth.parse(period);
            LocalDateTime monthStart = yearMonth.atDay(1).atStartOfDay();
            LocalDateTime monthEnd = yearMonth.atEndOfMonth().atTime(23, 59, 59);
            List<HrmLeaveApplicationDO> leaveApplications = leaveApplicationMapper
                    .selectApprovedListByUserIdAndTimeRange(systemUserId, monthStart, monthEnd);
            if (CollUtil.isNotEmpty(leaveApplications)) {
                List<HrmLeaveTypeConfigDO> configList = leaveTypeConfigMapper.selectList();
                Map<Integer, BigDecimal> configMap = configList.stream()
                        .filter(c -> c.getStatus() != null && c.getStatus() == 0) // 只取启用的配置
                        .collect(Collectors.toMap(HrmLeaveTypeConfigDO::getLeaveType,
                                HrmLeaveTypeConfigDO::getDeductionRatio));
                for (HrmLeaveApplicationDO application : leaveApplications) {
                    BigDecimal duration = application.getDuration() != null
                            ? application.getDuration() : BigDecimal.ZERO;
                    // 未配置的类型(如调休)比例按 0 处理 = 不扣款
                    BigDecimal deductionRatio = configMap.getOrDefault(application.getLeaveType(), BigDecimal.ZERO);
                    totalDeduction = totalDeduction.add(
                            dailySalary.multiply(duration).multiply(deductionRatio));
                    leaveDays = leaveDays.add(duration);
                }
            }
        }
        // 获取所有请假类型配置
        List<HrmLeaveTypeConfigDO> configList = leaveTypeConfigMapper.selectList();
        Map<Integer, BigDecimal> configMap = configList.stream()
                .filter(c -> c.getStatus() == 0) // 只取启用的配置
                .collect(Collectors.toMap(HrmLeaveTypeConfigDO::getLeaveType, HrmLeaveTypeConfigDO::getDeductionRatio));
        // 计算日工资(基本工资 / 21.75,按国家规定的工作日计算)
        BigDecimal dailySalary = baseSalary.divide(new BigDecimal("21.75"), 2, RoundingMode.HALF_UP);
        // 累计请假扣款
        BigDecimal totalLeaveDeduction = BigDecimal.ZERO;
        for (HrmLeaveApplicationDO application : leaveApplications) {
            Integer leaveType = application.getLeaveType();
            BigDecimal duration = application.getDuration() != null ? application.getDuration() : BigDecimal.ZERO;
            BigDecimal deductionRatio = configMap.getOrDefault(leaveType, BigDecimal.ZERO);
            // 请假扣款 = 日工资 × 请假天数 × 扣款比例
            BigDecimal deduction = dailySalary.multiply(duration).multiply(deductionRatio)
                    .setScale(2, RoundingMode.HALF_UP);
            totalLeaveDeduction = totalLeaveDeduction.add(deduction);
        // 2. 旷工扣款:缺勤天数中未被请假单覆盖的部分,按 100% 扣(仅当有考勤汇总时)
        if (actualDays != null) {
            BigDecimal absentDays = new BigDecimal(shouldDays - actualDays).subtract(leaveDays);
            if (absentDays.signum() > 0) {
                totalDeduction = totalDeduction.add(dailySalary.multiply(absentDays));
            }
        }
        return totalLeaveDeduction;
        // 3. 封顶:扣款不超过基本工资
        BigDecimal result = totalDeduction.setScale(2, RoundingMode.HALF_UP);
        BigDecimal cap = baseSalary.setScale(2, RoundingMode.HALF_UP);
        return result.compareTo(cap) > 0 ? cap : result;
    }
    /**
@@ -677,6 +702,9 @@
    @Transactional(rollbackFor = Exception.class)
    public void saveSalaryCalculationList(List<HrmSalaryCalculationDO> calculationList) {
        for (HrmSalaryCalculationDO calculation : calculationList) {
            // 缺勤扣款(请假 + 旷工)与实发工资统一由后端重算,
            // 保证列表里改「应出天数 / 实出天数」后金额跟着变
            recalcAttendanceDeduction(calculation);
            if (calculation.getId() == null) {
                // 新增记录,生成核算单号
                String no = noRedisDAO.generateSalaryCalculationNo();
@@ -728,10 +756,33 @@
        calculation.setHousingFundDeduction(nvl(calculation.getHousingFundDeduction()));
        calculation.setTaxDeduction(nvl(calculation.getTaxDeduction()));
        calculation.setOtherDeduction(nvl(calculation.getOtherDeduction()));
        calculation.setLeaveDeduction(nvl(calculation.getLeaveDeduction()));
        // 实发工资由后端重算,避免前端传入值与明细不一致
        calculation.setActualSalary(calcActualSalary(calculation));
        // 缺勤扣款(请假 + 旷工)与实发工资统一由后端重算,
        // 保证列表里改「应出天数 / 实出天数」后金额跟着变,且不受前端传入的 leaveDeduction 影响
        recalcAttendanceDeduction(calculation);
        salaryCalculationMapper.updateById(calculation);
    }
    /**
     * 重算「缺勤扣款」与「实发工资」
     *
     * <p>核算单 userId 存的是员工ID(hrm_employee.id)。按「应出天数 / 实出天数 + 请假单」重算
     * 缺勤扣款(请假按配置比例扣、旷工按 100% 扣、封顶基本工资),再重算实发工资(不可为负)。
     * 查不到员工或核算周期时,保留原缺勤扣款,只重算实发。
     */
    private void recalcAttendanceDeduction(HrmSalaryCalculationDO calculation) {
        HrmEmployeeDO employee = calculation.getUserId() != null
                ? employeeMapper.selectById(calculation.getUserId()) : null;
        if (employee != null && calculation.getPeriod() != null) {
            BigDecimal deduction = calculateAttendanceDeduction(
                    employee, calculation.getPeriod(),
                    nvl(calculation.getBaseSalary()),
                    calculation.getWorkDays(),
                    calculation.getActualDays());
            calculation.setLeaveDeduction(deduction);
        } else {
            calculation.setLeaveDeduction(nvl(calculation.getLeaveDeduction()));
        }
        calculation.setActualSalary(calcActualSalary(calculation));
    }
    /**
@@ -739,6 +790,9 @@
     *
     * 收入项必须与 buildSalaryCalculation 中 totalIncome 的口径完全一致,
     * 否则会出现「新建核算算得对、修改一次核算单实发就变少」的不一致。
     *
     * 实发工资不允许为负:缺勤扣款等已封顶为基本工资,但社保/公积金/个税叠加后仍可能为负,
     * 业务上按「本月无工资」处理(置 0),避免出现员工倒欠公司的情况。
     */
    private static BigDecimal calcActualSalary(HrmSalaryCalculationDO c) {
        BigDecimal income = nvl(c.getBaseSalary())
@@ -755,7 +809,8 @@
                .add(nvl(c.getTaxDeduction()))
                .add(nvl(c.getOtherDeduction()))
                .add(nvl(c.getLeaveDeduction()));
        return income.subtract(deduction).setScale(2, RoundingMode.HALF_UP);
        BigDecimal actual = income.subtract(deduction).setScale(2, RoundingMode.HALF_UP);
        return actual.signum() < 0 ? BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP) : actual;
    }
    private static BigDecimal nvl(BigDecimal v) {
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmTaxRateConfigServiceImpl.java
@@ -7,6 +7,7 @@
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
@@ -29,6 +30,7 @@
    @Transactional(rollbackFor = Exception.class)
    public void createTaxRateConfigList(List<HrmTaxRateConfigDO> configList) {
        for (HrmTaxRateConfigDO config : configList) {
            applyDefaults(config);
            taxRateConfigMapper.insert(config);
        }
    }
@@ -36,9 +38,32 @@
    @Override
    public void updateTaxRateConfig(HrmTaxRateConfigDO config) {
        validateTaxRateConfigExists(config.getId());
        applyDefaults(config);
        taxRateConfigMapper.updateById(config);
    }
    /**
     * 补全默认值:生效日期默认为当月 1 号,配置名称自动生成,速算扣除数/上限缺省按 0
     */
    private void applyDefaults(HrmTaxRateConfigDO config) {
        if (config.getEffectiveDate() == null) {
            config.setEffectiveDate(LocalDate.now().withDayOfMonth(1));
        }
        if (config.getName() == null || config.getName().isBlank()) {
            config.setName(config.getEffectiveDate().getYear() + "年"
                    + config.getEffectiveDate().getMonthValue() + "月个税税率表");
        }
        if (config.getStatus() == null) {
            config.setStatus(0);
        }
        if (config.getQuickDeduction() == null) {
            config.setQuickDeduction(BigDecimal.ZERO);
        }
        if (config.getMaxAmount() == null) {
            config.setMaxAmount(BigDecimal.ZERO);
        }
    }
    @Override
    public void deleteTaxRateConfig(Long id) {
        validateTaxRateConfigExists(id);