liu
2026-10-23 962f78c8383e7c884ddb08c951c15e5ba6eb4cb8
fix: 完善审批流程与业务状态校验
已修改60个文件
735 ■■■■■ 文件已修改
sql/mysql/mes_em_energy_init.sql 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/compensation/AfterSaleCompensationMapper.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/ErrorCodeConstants.java 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/compensation/AfterSaleCompensationServiceImpl.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtils.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/CrmContractController.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/invoice/CrmInvoiceController.java 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/cancellation/CrmCancellationMapper.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/contract/CrmContractMapper.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/invoice/CrmInvoiceMapper.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/refund/CrmRefundMapper.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/cancellation/CrmCancellationServiceImpl.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/invoice/CrmInvoiceServiceImpl.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/refund/CrmRefundServiceImpl.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/finance/ErpFinancePaymentMapper.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseOrderMapper.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchasePlanMapper.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseRequestMapper.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpFinancePaymentServiceImpl.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderServiceImpl.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchasePlanServiceImpl.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseRequestServiceImpl.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/listener/ErpPurchasePlanStatusListener.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/ErrorCodeConstants.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceExceptionSaveReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/approval/HrmLeaveApplicationMapper.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/approval/HrmRegularizeApplicationMapper.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/approval/HrmResignationApplicationMapper.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/approval/HrmTransferApplicationMapper.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/attendance/HrmAttendanceExceptionMapper.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/performanceappraise/HrmPerformanceAppraiseMapper.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmLeaveApplicationServiceImpl.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmRegularizeApplicationServiceImpl.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmResignationApplicationServiceImpl.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmTransferApplicationServiceImpl.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceExceptionServiceImpl.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRecordServiceImpl.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/performanceappraise/HrmPerformanceAppraiseServiceImpl.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pd/processparamtemplate/vo/MesPdProcessParamTemplateSaveReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pd/techconfirm/vo/ProductTechConfirmSaveReqVO.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pd/processparamtemplate/MesPdProcessParamTemplateServiceImpl.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pd/project/MesPdProjectServiceImpl.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pd/techconfirm/ProductTechConfirmServiceImpl.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemreceipt/MesWmItemReceiptService.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemreceipt/MesWmItemReceiptServiceImpl.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/miscissue/MesWmMiscIssueServiceImpl.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/miscreceipt/MesWmMiscReceiptServiceImpl.java 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnvendor/MesWmReturnVendorDetailServiceImpl.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnvendor/MesWmReturnVendorService.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnvendor/MesWmReturnVendorServiceImpl.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/stocktaking/task/MesWmStockTakingTaskLineServiceImpl.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/stocktaking/task/MesWmStockTakingTaskResultServiceImpl.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/stocktaking/task/MesWmStockTakingTaskService.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/stocktaking/task/MesWmStockTakingTaskServiceImpl.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-server/src/main/resources/application-local.yaml 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/mes_em_energy_init.sql
@@ -195,8 +195,8 @@
--    因此先按主键补插父目录;已存在时仅恢复删除标记,不改动原有名称和路径。
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
VALUES
(1065864, '能耗管理', '', 1, 16, 0, '/energyManagement', 'monitor', NULL, NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0')
ON DUPLICATE KEY UPDATE `deleted` = b'0', `status` = 0, `visible` = b'1', `updater` = '1', `update_time` = NOW();
(1065864, '能耗管理', '', 1, 16, 0, '/energyManagement', 'ep:lightning', NULL, NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0')
ON DUPLICATE KEY UPDATE `icon` = 'ep:lightning', `deleted` = b'0', `status` = 0, `visible` = b'1', `updater` = '1', `update_time` = NOW();
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
VALUES
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/mysql/compensation/AfterSaleCompensationMapper.java
@@ -5,6 +5,7 @@
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.aftersales.controller.admin.compensation.vo.AfterSaleCompensationPageReqVO;
import cn.iocoder.yudao.module.aftersales.dal.dataobject.compensation.AfterSaleCompensationDO;
import cn.iocoder.yudao.module.aftersales.enums.AfterSaleCompensationAuditStatusEnum;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
@@ -33,6 +34,9 @@
    default int submitAndResetAuditInfo(Long id, Integer status) {
        return update(null, new LambdaUpdateWrapper<AfterSaleCompensationDO>()
                .eq(AfterSaleCompensationDO::getId, id)
                .in(AfterSaleCompensationDO::getAuditStatus,
                        AfterSaleCompensationAuditStatusEnum.DRAFT.getStatus(),
                        AfterSaleCompensationAuditStatusEnum.REJECT.getStatus())
                .set(AfterSaleCompensationDO::getAuditStatus, status)
                .set(AfterSaleCompensationDO::getReviewerId, null)
                .set(AfterSaleCompensationDO::getReviewerName, null)
@@ -56,6 +60,8 @@
                                  String reviewerName, String reviewRemark) {
        return update(null, new LambdaUpdateWrapper<AfterSaleCompensationDO>()
                .eq(AfterSaleCompensationDO::getId, id)
                .eq(AfterSaleCompensationDO::getAuditStatus,
                        AfterSaleCompensationAuditStatusEnum.PROCESS.getStatus())
                .set(AfterSaleCompensationDO::getAuditStatus, status)
                .set(AfterSaleCompensationDO::getReviewerId, reviewerId)
                .set(AfterSaleCompensationDO::getReviewerName, reviewerName)
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/enums/ErrorCodeConstants.java
@@ -44,5 +44,6 @@
    ErrorCode COMPENSATION_DELETE_FAIL_NOT_DRAFT = new ErrorCode(1_022_004_004, "赔付单不是草稿状态,无法删除");
    ErrorCode COMPENSATION_TICKET_NOT_EXISTS = new ErrorCode(1_022_004_005, "关联的售后工单不存在");
    ErrorCode COMPENSATION_AUDIT_REJECT_REASON_REQUIRED = new ErrorCode(1_022_004_006, "审核不通过时必须填写不通过原因");
    ErrorCode COMPENSATION_UPDATE_FAIL_NOT_EDITABLE = new ErrorCode(1_022_004_007, "只有未提交或审核不通过状态的赔付单才能修改");
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/compensation/AfterSaleCompensationServiceImpl.java
@@ -69,8 +69,9 @@
        if (exists == null) {
            throw exception(ErrorCodeConstants.COMPENSATION_NOT_EXISTS);
        }
        if (ObjUtil.notEqual(exists.getAuditStatus(), AfterSaleCompensationAuditStatusEnum.DRAFT.getStatus())) {
            throw exception(ErrorCodeConstants.COMPENSATION_DELETE_FAIL_NOT_DRAFT);
        if (ObjUtil.notEqual(exists.getAuditStatus(), AfterSaleCompensationAuditStatusEnum.DRAFT.getStatus())
                && ObjUtil.notEqual(exists.getAuditStatus(), AfterSaleCompensationAuditStatusEnum.REJECT.getStatus())) {
            throw exception(ErrorCodeConstants.COMPENSATION_UPDATE_FAIL_NOT_EDITABLE);
        }
        AfterSaleCompensationDO entity = BeanUtils.toBean(updateReqVO, AfterSaleCompensationDO.class, req -> req
                .setCompensationNo(exists.getCompensationNo())
@@ -115,7 +116,10 @@
        approvalConfigApi.validateApprovalEnabledAndGetApprovers(COMPENSATION_APPROVE_BIZ_TYPE);
        // 3. 状态置为审批中,并清空上一轮审核结果,避免残留「审核不通过」的原因
        compensationMapper.submitAndResetAuditInfo(id, AfterSaleCompensationAuditStatusEnum.PROCESS.getStatus());
        if (compensationMapper.submitAndResetAuditInfo(id,
                AfterSaleCompensationAuditStatusEnum.PROCESS.getStatus()) != 1) {
            throw exception(ErrorCodeConstants.COMPENSATION_SUBMIT_FAIL_NOT_EDITABLE);
        }
    }
    @Override
@@ -140,13 +144,17 @@
            if (StrUtil.isBlank(reviewRemark)) {
                throw exception(ErrorCodeConstants.COMPENSATION_AUDIT_REJECT_REASON_REQUIRED);
            }
            compensationMapper.auditCompensation(id, AfterSaleCompensationAuditStatusEnum.REJECT.getStatus(),
                    userId, getUserNickname(userId), reviewRemark);
            if (compensationMapper.auditCompensation(id, AfterSaleCompensationAuditStatusEnum.REJECT.getStatus(),
                    userId, getUserNickname(userId), reviewRemark) != 1) {
                throw exception(ErrorCodeConstants.COMPENSATION_AUDIT_FAIL_NOT_PROCESS);
            }
            return;
        }
        // 5. 审核通过:记录审核人与审核意见,状态置为审核通过
        compensationMapper.auditCompensation(id, AfterSaleCompensationAuditStatusEnum.APPROVE.getStatus(),
                userId, getUserNickname(userId), reviewRemark);
        if (compensationMapper.auditCompensation(id, AfterSaleCompensationAuditStatusEnum.APPROVE.getStatus(),
                userId, getUserNickname(userId), reviewRemark) != 1) {
            throw exception(ErrorCodeConstants.COMPENSATION_AUDIT_FAIL_NOT_PROCESS);
        }
        // 6. 审核通过后的后置业务逻辑:当前赔付单审批通过后无自动生成下游单据的动作,
        //    支付状态(payStatus)待后续接入财务模块时再联动。
    }
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.java
@@ -215,16 +215,24 @@
        if (historicProcessInstance == null) {
            throw exception(PROCESS_INSTANCE_NOT_EXISTS);
        }
        AdminUserRespDTO startUser = adminUserApi.getUser(Long.valueOf(historicProcessInstance.getStartUserId()));
        DeptRespDTO dept = deptApi.getDept(startUser.getDeptId());
        // 发起人 / 部门可能不存在(历史数据、用户已删除),需做空值保护
        AdminUserRespDTO startUser = adminUserApi.getUser(NumberUtils.parseLong(historicProcessInstance.getStartUserId()));
        DeptRespDTO dept = startUser != null && startUser.getDeptId() != null
                ? deptApi.getDept(startUser.getDeptId()) : null;
        List<HistoricTaskInstance> tasks = taskService.getFinishedTaskListByProcessInstanceIdWithoutCancel(processInstanceId);
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
                convertSet(tasks, item -> Long.valueOf(item.getAssignee())));
                convertSet(tasks, item -> NumberUtils.parseLong(item.getAssignee())));
        UserSimpleBaseVO startUserVO = new UserSimpleBaseVO();
        if (startUser != null) {
            startUserVO.setNickname(startUser.getNickname());
        }
        if (dept != null) {
            startUserVO.setDeptName(dept.getName());
        }
        BpmProcessPrintDataRespVO printData = BpmProcessInstanceConvert.INSTANCE.buildProcessInstancePrintData(
                historicProcessInstance,
                processDefinitionService.getProcessDefinitionInfo(historicProcessInstance.getProcessDefinitionId()),
                tasks, userMap,
                new UserSimpleBaseVO().setNickname(startUser.getNickname()).setDeptName(dept.getName()));
                tasks, userMap, startUserVO);
        // 填充业务附件配置,实现打印自动展示外部业务信息
        businessAttachService.fillBusinessForm(printData.getProcessInstance().getProcessDefinition(),
                processDefinitionService.getProcessDefinition(historicProcessInstance.getProcessDefinitionId()).getKey(),
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtils.java
@@ -246,7 +246,10 @@
            return null;
        }
        // 解析表单配置
        // 解析表单配置。部分历史流程定义没有配置表单字段(form_fields 为空),此时无法生成摘要,直接返回
        if (ObjectUtil.isEmpty(processDefinitionInfo.getFormFields())) {
            return null;
        }
        Map<String, BpmFormFieldVO> formFieldsMap = new LinkedHashMap<>();
        processDefinitionInfo.getFormFields().forEach(formFieldStr -> {
            JsonNode formFieldNode = JsonUtils.parseObject(formFieldStr, JsonNode.class);
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/CrmContractController.java
@@ -151,6 +151,7 @@
    @GetMapping("/page-by-customer")
    @Operation(summary = "获得合同分页,基于指定客户")
    @PreAuthorize("@ss.hasPermission('crm:contract:query')")
    public CommonResult<PageResult<CrmContractRespVO>> getContractPageByCustomer(@Valid CrmContractPageReqVO pageVO) {
        Assert.notNull(pageVO.getCustomerId(), "客户编号不能为空");
        PageResult<CrmContractDO> pageResult = contractService.getContractPageByCustomerId(pageVO);
@@ -159,6 +160,7 @@
    @GetMapping("/page-by-business")
    @Operation(summary = "获得合同分页,基于指定商机")
    @PreAuthorize("@ss.hasPermission('crm:contract:query')")
    public CommonResult<PageResult<CrmContractRespVO>> getContractPageByBusiness(@Valid CrmContractPageReqVO pageVO) {
        Assert.notNull(pageVO.getBusinessId(), "商机编号不能为空");
        PageResult<CrmContractDO> pageResult = contractService.getContractPageByBusinessId(pageVO);
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/invoice/CrmInvoiceController.java
@@ -127,6 +127,7 @@
    @GetMapping("/page-by-customer")
    @Operation(summary = "获得开票分页,基于指定客户")
    @PreAuthorize("@ss.hasPermission('crm:invoice:query')")
    public CommonResult<PageResult<CrmInvoiceRespVO>> getInvoicePageByCustomer(@Valid CrmInvoicePageReqVO pageReqVO) {
        Assert.notNull(pageReqVO.getCustomerId(), "客户编号不能为空");
        PageResult<CrmInvoiceDO> pageResult = invoiceService.getInvoicePageByCustomerId(pageReqVO);
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/cancellation/CrmCancellationMapper.java
@@ -5,6 +5,7 @@
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.crm.controller.admin.cancellation.vo.CrmCancellationPageReqVO;
import cn.iocoder.yudao.module.crm.dal.dataobject.cancellation.CrmCancellationDO;
import cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
@@ -31,6 +32,8 @@
    default int submitAndResetAuditInfo(Long id, Integer status) {
        return update(null, new LambdaUpdateWrapper<CrmCancellationDO>()
                .eq(CrmCancellationDO::getId, id)
                .in(CrmCancellationDO::getAuditStatus, CrmAuditStatusEnum.DRAFT.getStatus(),
                        CrmAuditStatusEnum.REJECT.getStatus())
                .set(CrmCancellationDO::getAuditStatus, status)
                .set(CrmCancellationDO::getReviewerId, null)
                .set(CrmCancellationDO::getReviewerName, null)
@@ -54,6 +57,7 @@
                                  String reviewerName, String reviewRemark) {
        return update(null, new LambdaUpdateWrapper<CrmCancellationDO>()
                .eq(CrmCancellationDO::getId, id)
                .eq(CrmCancellationDO::getAuditStatus, CrmAuditStatusEnum.PROCESS.getStatus())
                .set(CrmCancellationDO::getAuditStatus, status)
                .set(CrmCancellationDO::getReviewerId, reviewerId)
                .set(CrmCancellationDO::getReviewerName, reviewerName)
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/contract/CrmContractMapper.java
@@ -51,6 +51,8 @@
    default int submitAndResetAuditInfo(Long id, Integer status) {
        return update(null, new LambdaUpdateWrapper<CrmContractDO>()
                .eq(CrmContractDO::getId, id)
                .in(CrmContractDO::getAuditStatus, CrmAuditStatusEnum.DRAFT.getStatus(),
                        CrmAuditStatusEnum.REJECT.getStatus())
                .set(CrmContractDO::getAuditStatus, status)
                .set(CrmContractDO::getReviewerId, null)
                .set(CrmContractDO::getReviewerName, null)
@@ -74,6 +76,7 @@
                              String reviewerName, String reviewRemark) {
        return update(null, new LambdaUpdateWrapper<CrmContractDO>()
                .eq(CrmContractDO::getId, id)
                .eq(CrmContractDO::getAuditStatus, CrmAuditStatusEnum.PROCESS.getStatus())
                .set(CrmContractDO::getAuditStatus, status)
                .set(CrmContractDO::getReviewerId, reviewerId)
                .set(CrmContractDO::getReviewerName, reviewerName)
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/invoice/CrmInvoiceMapper.java
@@ -49,6 +49,8 @@
    default int submitAndResetAuditInfo(Long id, Integer status) {
        return update(null, new LambdaUpdateWrapper<CrmInvoiceDO>()
                .eq(CrmInvoiceDO::getId, id)
                .in(CrmInvoiceDO::getAuditStatus, CrmAuditStatusEnum.DRAFT.getStatus(),
                        CrmAuditStatusEnum.REJECT.getStatus())
                .set(CrmInvoiceDO::getAuditStatus, status)
                .set(CrmInvoiceDO::getReviewerId, null)
                .set(CrmInvoiceDO::getReviewerName, null)
@@ -72,6 +74,7 @@
                             String reviewerName, String reviewRemark) {
        return update(null, new LambdaUpdateWrapper<CrmInvoiceDO>()
                .eq(CrmInvoiceDO::getId, id)
                .eq(CrmInvoiceDO::getAuditStatus, CrmAuditStatusEnum.PROCESS.getStatus())
                .set(CrmInvoiceDO::getAuditStatus, status)
                .set(CrmInvoiceDO::getReviewerId, reviewerId)
                .set(CrmInvoiceDO::getReviewerName, reviewerName)
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/refund/CrmRefundMapper.java
@@ -5,6 +5,7 @@
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.crm.controller.admin.refund.vo.CrmRefundPageReqVO;
import cn.iocoder.yudao.module.crm.dal.dataobject.refund.CrmRefundDO;
import cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
@@ -31,6 +32,8 @@
    default int submitAndResetAuditInfo(Long id, Integer status) {
        return update(null, new LambdaUpdateWrapper<CrmRefundDO>()
                .eq(CrmRefundDO::getId, id)
                .in(CrmRefundDO::getAuditStatus, CrmAuditStatusEnum.DRAFT.getStatus(),
                        CrmAuditStatusEnum.REJECT.getStatus())
                .set(CrmRefundDO::getAuditStatus, status)
                .set(CrmRefundDO::getReviewerId, null)
                .set(CrmRefundDO::getReviewerName, null)
@@ -54,6 +57,7 @@
                            String reviewerName, String reviewRemark) {
        return update(null, new LambdaUpdateWrapper<CrmRefundDO>()
                .eq(CrmRefundDO::getId, id)
                .eq(CrmRefundDO::getAuditStatus, CrmAuditStatusEnum.PROCESS.getStatus())
                .set(CrmRefundDO::getAuditStatus, status)
                .set(CrmRefundDO::getReviewerId, reviewerId)
                .set(CrmRefundDO::getReviewerName, reviewerName)
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
@@ -22,6 +22,7 @@
    ErrorCode CONTRACT_SUBMIT_FAIL_NOT_EDITABLE = new ErrorCode(1_020_000_010, "只有未提交或审核不通过状态的合同才能提交审批");
    ErrorCode CONTRACT_AUDIT_FAIL_NOT_PROCESS = new ErrorCode(1_020_000_012, "只有审批中状态的合同才能审核");
    ErrorCode CONTRACT_AUDIT_REJECT_REASON_REQUIRED = new ErrorCode(1_020_000_014, "审核不通过时必须填写不通过原因");
    ErrorCode CONTRACT_DELETE_FAIL_NOT_EDITABLE = new ErrorCode(1_020_000_015, "只有未提交或审核不通过状态的合同才能删除");
    ErrorCode CONTRACT_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_020_000_009, "导入合同数据不能为空!");
    ErrorCode CONTRACT_NAME_EXISTS = new ErrorCode(1_020_000_011, "已存在合同【{}】,且未开启覆盖!");
    ErrorCode CONTRACT_IMPORT_CUSTOMER_NOT_EXISTS = new ErrorCode(1_020_000_013, "客户【{}】不存在!");
@@ -187,4 +188,9 @@
    ErrorCode CRM_CREDIT_RATING_IMPORT_CUSTOMER_NOT_EXISTS = new ErrorCode(1_020_018_020, "客户【{}】不存在!");
    ErrorCode CRM_CREDIT_RATING_NAME_EXISTS = new ErrorCode(1_020_018_021, "已存在客户【{}】的信用评级,且未开启覆盖!");
    ErrorCode CRM_REFUND_UPDATE_FAIL_NOT_EDITABLE = new ErrorCode(1_020_018_022, "只有草稿或审核不通过状态的退费单才能修改");
    ErrorCode CRM_REFUND_DELETE_FAIL_NOT_EDITABLE = new ErrorCode(1_020_018_023, "只有草稿或审核不通过状态的退费单才能删除");
    ErrorCode CRM_CANCELLATION_UPDATE_FAIL_NOT_EDITABLE = new ErrorCode(1_020_018_024, "只有草稿或审核不通过状态的销户申请才能修改");
    ErrorCode CRM_CANCELLATION_DELETE_FAIL_NOT_EDITABLE = new ErrorCode(1_020_018_025, "只有草稿或审核不通过状态的销户申请才能删除");
}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/cancellation/CrmCancellationServiceImpl.java
@@ -17,6 +17,7 @@
import cn.iocoder.yudao.module.system.api.approval.ApprovalConfigApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -78,23 +79,36 @@
    @Override
    public void updateCancellation(CrmCancellationSaveReqVO updateReqVO) {
        // 1. 校验存在 + 审核通过的销户申请不允许修改
        // 1. 校验存在 + 只有草稿、审核不通过可以修改
        CrmCancellationDO cancellation = validateCancellationExists(updateReqVO.getId());
        if (ObjUtil.equal(cancellation.getAuditStatus(), CrmAuditStatusEnum.APPROVE.getStatus())) {
            throw exception(ErrorCodeConstants.CRM_CANCELLATION_UPDATE_FAIL_APPROVE);
        if (ObjUtil.notEqual(cancellation.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus())
                && ObjUtil.notEqual(cancellation.getAuditStatus(), CrmAuditStatusEnum.REJECT.getStatus())) {
            throw exception(ErrorCodeConstants.CRM_CANCELLATION_UPDATE_FAIL_NOT_EDITABLE);
        }
        // 2. 更新销户申请
        // 2. 更新销户申请;审批状态与审核结果只能由提交/审核变更,此处显式清空
        CrmCancellationDO entity = BeanUtils.toBean(updateReqVO, CrmCancellationDO.class);
        entity.setAuditStatus(null); // 审批状态只能由提交/审核变更
        cancellationMapper.updateById(entity);
        // 审核相关字段只能由提交/审核变更
        entity.setAuditStatus(null);
        entity.setReviewerId(null);
        entity.setReviewerName(null);
        entity.setReviewTime(null);
        entity.setReviewRemark(null);
        cancellationMapper.update(entity, new LambdaUpdateWrapper<CrmCancellationDO>()
                .eq(CrmCancellationDO::getId, entity.getId())
                .set(CrmCancellationDO::getAuditStatus, null)
                .set(CrmCancellationDO::getReviewerId, null)
                .set(CrmCancellationDO::getReviewerName, null)
                .set(CrmCancellationDO::getReviewTime, null)
                .set(CrmCancellationDO::getReviewRemark, null));
    }
    @Override
    public void deleteCancellation(Long id) {
        // 校验存在 + 审核通过的销户申请不允许删除
        // 校验存在 + 只有草稿、审核不通过可以删除
        CrmCancellationDO cancellation = validateCancellationExists(id);
        if (ObjUtil.equal(cancellation.getAuditStatus(), CrmAuditStatusEnum.APPROVE.getStatus())) {
            throw exception(ErrorCodeConstants.CRM_CANCELLATION_DELETE_FAIL_APPROVE);
        if (ObjUtil.notEqual(cancellation.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus())
                && ObjUtil.notEqual(cancellation.getAuditStatus(), CrmAuditStatusEnum.REJECT.getStatus())) {
            throw exception(ErrorCodeConstants.CRM_CANCELLATION_DELETE_FAIL_NOT_EDITABLE);
        }
        cancellationMapper.deleteById(id);
    }
@@ -129,7 +143,9 @@
        approvalConfigApi.validateApprovalEnabledAndGetApprovers(CANCELLATION_APPROVE_BIZ_TYPE);
        // 3. 状态置为审批中,并清空上一轮审核结果,避免残留「审核不通过」的原因
        cancellationMapper.submitAndResetAuditInfo(id, CrmAuditStatusEnum.PROCESS.getStatus());
        if (cancellationMapper.submitAndResetAuditInfo(id, CrmAuditStatusEnum.PROCESS.getStatus()) != 1) {
            throw exception(ErrorCodeConstants.CRM_CANCELLATION_SUBMIT_FAIL_NOT_EDITABLE);
        }
    }
    @Override
@@ -154,13 +170,17 @@
            if (StrUtil.isBlank(reviewRemark)) {
                throw exception(ErrorCodeConstants.CRM_CANCELLATION_AUDIT_REJECT_REASON_REQUIRED);
            }
            cancellationMapper.auditCancellation(id, CrmAuditStatusEnum.REJECT.getStatus(),
                    userId, getUserNickname(userId), reviewRemark);
            if (cancellationMapper.auditCancellation(id, CrmAuditStatusEnum.REJECT.getStatus(),
                    userId, getUserNickname(userId), reviewRemark) != 1) {
                throw exception(ErrorCodeConstants.CRM_CANCELLATION_AUDIT_FAIL_NOT_PROCESS);
            }
            return;
        }
        // 5. 审核通过:记录审核人与审核意见,状态置为审核通过
        cancellationMapper.auditCancellation(id, CrmAuditStatusEnum.APPROVE.getStatus(),
                userId, getUserNickname(userId), reviewRemark);
        if (cancellationMapper.auditCancellation(id, CrmAuditStatusEnum.APPROVE.getStatus(),
                userId, getUserNickname(userId), reviewRemark) != 1) {
            throw exception(ErrorCodeConstants.CRM_CANCELLATION_AUDIT_FAIL_NOT_PROCESS);
        }
    }
    private CrmCancellationDO validateCancellationExists(Long id) {
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java
@@ -176,9 +176,9 @@
        updateReqVO.setOwnerUserId(null); // 不允许更新的字段
        // 1.1 校验存在
        CrmContractDO oldContract = validateContractExists(updateReqVO.getId());
        // 1.2 只有草稿、审批中,可以编辑;
        // 1.2 只有草稿、审核不通过,可以编辑;
        if (!ObjectUtils.equalsAny(oldContract.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus(),
                CrmAuditStatusEnum.PROCESS.getStatus())) {
                CrmAuditStatusEnum.REJECT.getStatus())) {
            throw exception(CONTRACT_UPDATE_FAIL_NOT_DRAFT);
        }
        // 1.3 校验产品项的有效性
@@ -272,7 +272,12 @@
    public void deleteContract(Long id) {
        // 1.1 校验存在
        CrmContractDO contract = validateContractExists(id);
        // 1.2 如果被 CrmReceivableDO 所使用,则不允许删除
        // 1.2 只有草稿、审核不通过,可以删除;
        if (!ObjectUtils.equalsAny(contract.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus(),
                CrmAuditStatusEnum.REJECT.getStatus())) {
            throw exception(CONTRACT_DELETE_FAIL_NOT_EDITABLE);
        }
        // 1.3 如果被 CrmReceivableDO 所使用,则不允许删除
        if (receivableService.getReceivableCountByContractId(contract.getId()) > 0) {
            throw exception(CONTRACT_DELETE_FAIL);
        }
@@ -351,6 +356,7 @@
    @Transactional(rollbackFor = Exception.class)
    @LogRecord(type = CRM_CONTRACT_TYPE, subType = CRM_CONTRACT_SUBMIT_SUB_TYPE, bizNo = "{{#id}}",
            success = CRM_CONTRACT_SUBMIT_SUCCESS)
    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTRACT, bizId = "#id", level = CrmPermissionLevelEnum.WRITE)
    public void submitContract(Long id) {
        // 1. 校验存在 + 可提交状态(未提交 / 审核不通过)
        CrmContractDO contract = validateContractExists(id);
@@ -363,7 +369,9 @@
        approvalConfigApi.validateApprovalEnabledAndGetApprovers(CONTRACT_APPROVE_BIZ_TYPE);
        // 3. 状态置为审批中,并清空上一轮审核结果,避免残留「审核不通过」的原因
        contractMapper.submitAndResetAuditInfo(id, CrmAuditStatusEnum.PROCESS.getStatus());
        if (contractMapper.submitAndResetAuditInfo(id, CrmAuditStatusEnum.PROCESS.getStatus()) != 1) {
            throw exception(CONTRACT_SUBMIT_FAIL_NOT_EDITABLE);
        }
        // 4. 记录日志
        LogRecordContext.putVariable("contractName", contract.getName());
@@ -391,13 +399,17 @@
            if (StrUtil.isBlank(reviewRemark)) {
                throw exception(CONTRACT_AUDIT_REJECT_REASON_REQUIRED);
            }
            contractMapper.auditContract(id, CrmAuditStatusEnum.REJECT.getStatus(),
                    userId, getUserNickname(userId), reviewRemark);
            if (contractMapper.auditContract(id, CrmAuditStatusEnum.REJECT.getStatus(),
                    userId, getUserNickname(userId), reviewRemark) != 1) {
                throw exception(CONTRACT_AUDIT_FAIL_NOT_PROCESS);
            }
            return;
        }
        // 5. 审核通过:记录审核人与审核意见,状态置为审核通过
        contractMapper.auditContract(id, CrmAuditStatusEnum.APPROVE.getStatus(),
                userId, getUserNickname(userId), reviewRemark);
        if (contractMapper.auditContract(id, CrmAuditStatusEnum.APPROVE.getStatus(),
                userId, getUserNickname(userId), reviewRemark) != 1) {
            throw exception(CONTRACT_AUDIT_FAIL_NOT_PROCESS);
        }
    }
    // ======================= 查询相关 =======================
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/invoice/CrmInvoiceServiceImpl.java
@@ -166,9 +166,9 @@
        // 1.2 校验可开票金额超过上限
        validateInvoicePriceExceedsLimit(updateReqVO);
        // 1.3 只有草稿、审批中,可以编辑;
        // 1.3 只有草稿、审核不通过,可以编辑;
        if (!ObjectUtils.equalsAny(oldInvoice.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus(),
                CrmAuditStatusEnum.PROCESS.getStatus())) {
                CrmAuditStatusEnum.REJECT.getStatus())) {
            throw exception(INVOICE_UPDATE_FAIL_EDITING_PROHIBITED);
        }
@@ -213,6 +213,7 @@
    @Transactional(rollbackFor = Exception.class)
    @LogRecord(type = CRM_INVOICE_TYPE, subType = CRM_INVOICE_SUBMIT_SUB_TYPE, bizNo = "{{#id}}",
            success = CRM_INVOICE_SUBMIT_SUCCESS)
    @CrmPermission(bizType = CrmBizTypeEnum.CRM_INVOICE, bizId = "#id", level = CrmPermissionLevelEnum.WRITE)
    public void submitInvoice(Long id) {
        // 1. 校验存在 + 可提交状态(未提交 / 审核不通过)
        CrmInvoiceDO invoice = validateInvoiceExists(id);
@@ -229,7 +230,9 @@
        approvalConfigApi.validateApprovalEnabledAndGetApprovers(INVOICE_APPROVE_BIZ_TYPE);
        // 4. 状态置为审批中,并清空上一轮审核结果,避免残留「审核不通过」的原因
        invoiceMapper.submitAndResetAuditInfo(id, CrmAuditStatusEnum.PROCESS.getStatus());
        if (invoiceMapper.submitAndResetAuditInfo(id, CrmAuditStatusEnum.PROCESS.getStatus()) != 1) {
            throw exception(INVOICE_SUBMIT_FAIL_NOT_EDITABLE);
        }
        // 5. 记录日志
        LogRecordContext.putVariable("invoiceNo", invoice.getNo());
@@ -257,14 +260,18 @@
            if (StrUtil.isBlank(reviewRemark)) {
                throw exception(INVOICE_AUDIT_REJECT_REASON_REQUIRED);
            }
            invoiceMapper.auditInvoice(id, CrmAuditStatusEnum.REJECT.getStatus(),
                    userId, getUserNickname(userId), reviewRemark);
            if (invoiceMapper.auditInvoice(id, CrmAuditStatusEnum.REJECT.getStatus(),
                    userId, getUserNickname(userId), reviewRemark) != 1) {
                throw exception(INVOICE_AUDIT_FAIL_NOT_PROCESS);
            }
            return;
        }
        // 5. 审核通过:记录审核人与审核意见,状态置为审核通过
        //    (开票无额外后置业务逻辑,审批通过后即可用于创建回款 / 关联开票选择)
        invoiceMapper.auditInvoice(id, CrmAuditStatusEnum.APPROVE.getStatus(),
                userId, getUserNickname(userId), reviewRemark);
        if (invoiceMapper.auditInvoice(id, CrmAuditStatusEnum.APPROVE.getStatus(),
                userId, getUserNickname(userId), reviewRemark) != 1) {
            throw exception(INVOICE_AUDIT_FAIL_NOT_PROCESS);
        }
    }
    /**
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/refund/CrmRefundServiceImpl.java
@@ -17,6 +17,7 @@
import cn.iocoder.yudao.module.system.api.approval.ApprovalConfigApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -78,23 +79,36 @@
    @Override
    public void updateRefund(CrmRefundSaveReqVO updateReqVO) {
        // 1. 校验存在 + 审核通过的退费单不允许修改
        // 1. 校验存在 + 只有草稿、审核不通过可以修改
        CrmRefundDO refund = validateRefundExists(updateReqVO.getId());
        if (ObjUtil.equal(refund.getAuditStatus(), CrmAuditStatusEnum.APPROVE.getStatus())) {
            throw exception(ErrorCodeConstants.CRM_REFUND_UPDATE_FAIL_APPROVE);
        if (ObjUtil.notEqual(refund.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus())
                && ObjUtil.notEqual(refund.getAuditStatus(), CrmAuditStatusEnum.REJECT.getStatus())) {
            throw exception(ErrorCodeConstants.CRM_REFUND_UPDATE_FAIL_NOT_EDITABLE);
        }
        // 2. 更新退费单
        // 2. 更新退费单;审批状态与审核结果只能由提交/审核变更,此处显式清空
        CrmRefundDO entity = BeanUtils.toBean(updateReqVO, CrmRefundDO.class);
        entity.setAuditStatus(null); // 审批状态只能由提交/审核变更
        refundMapper.updateById(entity);
        // 审核相关字段只能由提交/审核变更
        entity.setAuditStatus(null);
        entity.setReviewerId(null);
        entity.setReviewerName(null);
        entity.setReviewTime(null);
        entity.setReviewRemark(null);
        refundMapper.update(entity, new LambdaUpdateWrapper<CrmRefundDO>()
                .eq(CrmRefundDO::getId, entity.getId())
                .set(CrmRefundDO::getAuditStatus, null)
                .set(CrmRefundDO::getReviewerId, null)
                .set(CrmRefundDO::getReviewerName, null)
                .set(CrmRefundDO::getReviewTime, null)
                .set(CrmRefundDO::getReviewRemark, null));
    }
    @Override
    public void deleteRefund(Long id) {
        // 校验存在 + 审核通过的退费单不允许删除
        // 校验存在 + 只有草稿、审核不通过可以删除
        CrmRefundDO refund = validateRefundExists(id);
        if (ObjUtil.equal(refund.getAuditStatus(), CrmAuditStatusEnum.APPROVE.getStatus())) {
            throw exception(ErrorCodeConstants.CRM_REFUND_DELETE_FAIL_APPROVE);
        if (ObjUtil.notEqual(refund.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus())
                && ObjUtil.notEqual(refund.getAuditStatus(), CrmAuditStatusEnum.REJECT.getStatus())) {
            throw exception(ErrorCodeConstants.CRM_REFUND_DELETE_FAIL_NOT_EDITABLE);
        }
        refundMapper.deleteById(id);
    }
@@ -129,7 +143,9 @@
        approvalConfigApi.validateApprovalEnabledAndGetApprovers(REFUND_APPROVE_BIZ_TYPE);
        // 3. 状态置为审批中,并清空上一轮审核结果,避免残留「审核不通过」的原因
        refundMapper.submitAndResetAuditInfo(id, CrmAuditStatusEnum.PROCESS.getStatus());
        if (refundMapper.submitAndResetAuditInfo(id, CrmAuditStatusEnum.PROCESS.getStatus()) != 1) {
            throw exception(ErrorCodeConstants.CRM_REFUND_SUBMIT_FAIL_NOT_EDITABLE);
        }
    }
    @Override
@@ -154,13 +170,17 @@
            if (StrUtil.isBlank(reviewRemark)) {
                throw exception(ErrorCodeConstants.CRM_REFUND_AUDIT_REJECT_REASON_REQUIRED);
            }
            refundMapper.auditRefund(id, CrmAuditStatusEnum.REJECT.getStatus(),
                    userId, getUserNickname(userId), reviewRemark);
            if (refundMapper.auditRefund(id, CrmAuditStatusEnum.REJECT.getStatus(),
                    userId, getUserNickname(userId), reviewRemark) != 1) {
                throw exception(ErrorCodeConstants.CRM_REFUND_AUDIT_FAIL_NOT_PROCESS);
            }
            return;
        }
        // 5. 审核通过:记录审核人与审核意见,状态置为审核通过
        refundMapper.auditRefund(id, CrmAuditStatusEnum.APPROVE.getStatus(),
                userId, getUserNickname(userId), reviewRemark);
        if (refundMapper.auditRefund(id, CrmAuditStatusEnum.APPROVE.getStatus(),
                userId, getUserNickname(userId), reviewRemark) != 1) {
            throw exception(ErrorCodeConstants.CRM_REFUND_AUDIT_FAIL_NOT_PROCESS);
        }
    }
    private CrmRefundDO validateRefundExists(Long id) {
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/finance/ErpFinancePaymentMapper.java
@@ -94,6 +94,7 @@
    default int submitAndResetAuditInfo(Long id, Integer status) {
        return update(null, new LambdaUpdateWrapper<ErpFinancePaymentDO>()
                .eq(ErpFinancePaymentDO::getId, id)
                .eq(ErpFinancePaymentDO::getStatus, ErpAuditStatus.PROCESS.getStatus())
                .set(ErpFinancePaymentDO::getStatus, status)
                .set(ErpFinancePaymentDO::getReviewerId, null)
                .set(ErpFinancePaymentDO::getReviewerName, null)
@@ -108,6 +109,7 @@
                                    String reviewerName, String reviewRemark) {
        return update(null, new LambdaUpdateWrapper<ErpFinancePaymentDO>()
                .eq(ErpFinancePaymentDO::getId, id)
                .eq(ErpFinancePaymentDO::getStatus, ErpAuditStatus.APPROVING.getStatus())
                .set(ErpFinancePaymentDO::getStatus, status)
                .set(ErpFinancePaymentDO::getReviewerId, reviewerId)
                .set(ErpFinancePaymentDO::getReviewerName, reviewerName)
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseOrderMapper.java
@@ -8,6 +8,7 @@
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderItemDO;
import cn.iocoder.yudao.module.erp.enums.ErpAuditStatus;
import cn.iocoder.yudao.module.erp.enums.ErpPurchaseOrderStatusEnum;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
@@ -80,6 +81,8 @@
    default int submitAndResetAuditInfo(Long id, Integer status) {
        return update(null, new LambdaUpdateWrapper<ErpPurchaseOrderDO>()
                .eq(ErpPurchaseOrderDO::getId, id)
                .in(ErpPurchaseOrderDO::getStatus,
                        ErpPurchaseOrderStatusEnum.DRAFT.getStatus(), ErpPurchaseOrderStatusEnum.REJECT.getStatus())
                .set(ErpPurchaseOrderDO::getStatus, status)
                .set(ErpPurchaseOrderDO::getReviewerId, null)
                .set(ErpPurchaseOrderDO::getReviewerName, null)
@@ -93,6 +96,7 @@
    default int auditPurchaseOrder(Long id, Integer status, Long reviewerId, String reviewerName, String reviewRemark) {
        return update(null, new LambdaUpdateWrapper<ErpPurchaseOrderDO>()
                .eq(ErpPurchaseOrderDO::getId, id)
                .eq(ErpPurchaseOrderDO::getStatus, ErpPurchaseOrderStatusEnum.PROCESS.getStatus())
                .set(ErpPurchaseOrderDO::getStatus, status)
                .set(ErpPurchaseOrderDO::getReviewerId, reviewerId)
                .set(ErpPurchaseOrderDO::getReviewerName, reviewerName)
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchasePlanMapper.java
@@ -5,6 +5,7 @@
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.plan.ErpPurchasePlanPageReqVO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchasePlanDO;
import cn.iocoder.yudao.module.erp.enums.ErpPurchasePlanStatusEnum;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
@@ -53,6 +54,8 @@
    default int submitAndResetAuditInfo(Long id, Integer status) {
        return update(null, new LambdaUpdateWrapper<ErpPurchasePlanDO>()
                .eq(ErpPurchasePlanDO::getId, id)
                .in(ErpPurchasePlanDO::getStatus,
                        ErpPurchasePlanStatusEnum.DRAFT.getStatus(), ErpPurchasePlanStatusEnum.REJECT.getStatus())
                .set(ErpPurchasePlanDO::getStatus, status)
                .set(ErpPurchasePlanDO::getReviewerId, null)
                .set(ErpPurchasePlanDO::getReviewerName, null)
@@ -76,6 +79,7 @@
                                  String reviewerName, String reviewRemark) {
        return update(null, new LambdaUpdateWrapper<ErpPurchasePlanDO>()
                .eq(ErpPurchasePlanDO::getId, id)
                .eq(ErpPurchasePlanDO::getStatus, ErpPurchasePlanStatusEnum.PROCESS.getStatus())
                .set(ErpPurchasePlanDO::getStatus, status)
                .set(ErpPurchasePlanDO::getReviewerId, reviewerId)
                .set(ErpPurchasePlanDO::getReviewerName, reviewerName)
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseRequestMapper.java
@@ -5,6 +5,7 @@
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestPageReqVO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseRequestDO;
import cn.iocoder.yudao.module.erp.enums.ErpPurchaseRequestStatusEnum;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
@@ -53,6 +54,8 @@
    default int submitAndResetAuditInfo(Long id, Integer status) {
        return update(null, new LambdaUpdateWrapper<ErpPurchaseRequestDO>()
                .eq(ErpPurchaseRequestDO::getId, id)
                .in(ErpPurchaseRequestDO::getStatus,
                        ErpPurchaseRequestStatusEnum.DRAFT.getStatus(), ErpPurchaseRequestStatusEnum.REJECT.getStatus())
                .set(ErpPurchaseRequestDO::getStatus, status)
                .set(ErpPurchaseRequestDO::getReviewerId, null)
                .set(ErpPurchaseRequestDO::getReviewerName, null)
@@ -67,6 +70,7 @@
                                     String reviewerName, String reviewRemark) {
        return update(null, new LambdaUpdateWrapper<ErpPurchaseRequestDO>()
                .eq(ErpPurchaseRequestDO::getId, id)
                .eq(ErpPurchaseRequestDO::getStatus, ErpPurchaseRequestStatusEnum.PROCESS.getStatus())
                .set(ErpPurchaseRequestDO::getStatus, status)
                .set(ErpPurchaseRequestDO::getReviewerId, reviewerId)
                .set(ErpPurchaseRequestDO::getReviewerName, reviewerName)
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpFinancePaymentServiceImpl.java
@@ -177,7 +177,9 @@
        // 2. 校验审批配置已启用且配置了有效审批人
        approvalConfigApi.validateApprovalEnabledAndGetApprovers(FINANCE_PAYMENT_APPROVE_BIZ_TYPE);
        // 3. 状态置为审批中,并清空上一轮审核结果
        financePaymentMapper.submitAndResetAuditInfo(id, ErpAuditStatus.APPROVING.getStatus());
        if (financePaymentMapper.submitAndResetAuditInfo(id, ErpAuditStatus.APPROVING.getStatus()) != 1) {
            throw exception(FINANCE_PAYMENT_SUBMIT_FAIL_NOT_ALLOWED);
        }
    }
    @Override
@@ -202,13 +204,17 @@
            if (StrUtil.isBlank(reviewRemark)) {
                throw exception(FINANCE_PAYMENT_AUDIT_REJECT_REASON_REQUIRED);
            }
            financePaymentMapper.auditFinancePayment(id, ErpAuditStatus.PROCESS.getStatus(),
                    userId, getUserNickname(userId), reviewRemark);
            if (financePaymentMapper.auditFinancePayment(id, ErpAuditStatus.PROCESS.getStatus(),
                    userId, getUserNickname(userId), reviewRemark) != 1) {
                throw exception(FINANCE_PAYMENT_AUDIT_FAIL_NOT_PROCESS);
            }
            return;
        }
        // 5. 审核通过:记录审核人与审核意见,状态置为已审核
        financePaymentMapper.auditFinancePayment(id, ErpAuditStatus.APPROVE.getStatus(),
                userId, getUserNickname(userId), reviewRemark);
        if (financePaymentMapper.auditFinancePayment(id, ErpAuditStatus.APPROVE.getStatus(),
                userId, getUserNickname(userId), reviewRemark) != 1) {
            throw exception(FINANCE_PAYMENT_AUDIT_FAIL_NOT_PROCESS);
        }
    }
    /**
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderServiceImpl.java
@@ -242,7 +242,9 @@
        approvalConfigApi.validateApprovalEnabledAndGetApprovers(PURCHASE_ORDER_APPROVE_BIZ_TYPE);
        // 3. 置为审批中,并清空上一轮审核信息,避免驳回原因残留
        purchaseOrderMapper.submitAndResetAuditInfo(id, ErpPurchaseOrderStatusEnum.PROCESS.getStatus());
        if (purchaseOrderMapper.submitAndResetAuditInfo(id, ErpPurchaseOrderStatusEnum.PROCESS.getStatus()) != 1) {
            throw exception(PURCHASE_ORDER_SUBMIT_FAIL_NOT_ALLOWED);
        }
    }
    @Override
@@ -267,14 +269,18 @@
            if (StrUtil.isBlank(reviewRemark)) {
                throw exception(PURCHASE_ORDER_AUDIT_REJECT_REASON_REQUIRED);
            }
            purchaseOrderMapper.auditPurchaseOrder(id, ErpPurchaseOrderStatusEnum.REJECT.getStatus(),
                    userId, getUserNickname(userId), reviewRemark);
            if (purchaseOrderMapper.auditPurchaseOrder(id, ErpPurchaseOrderStatusEnum.REJECT.getStatus(),
                    userId, getUserNickname(userId), reviewRemark) != 1) {
                throw exception(PURCHASE_ORDER_AUDIT_FAIL_NOT_PROCESS);
            }
            return;
        }
        // 3. 审核通过
        purchaseOrderMapper.auditPurchaseOrder(id, ErpPurchaseOrderStatusEnum.APPROVE.getStatus(),
                userId, getUserNickname(userId), reviewRemark);
        if (purchaseOrderMapper.auditPurchaseOrder(id, ErpPurchaseOrderStatusEnum.APPROVE.getStatus(),
                userId, getUserNickname(userId), reviewRemark) != 1) {
            throw exception(PURCHASE_ORDER_AUDIT_FAIL_NOT_PROCESS);
        }
    }
    /**
@@ -404,9 +410,13 @@
            return;
        }
        purchaseOrders.forEach(purchaseOrder -> {
            if (ErpAuditStatus.APPROVE.getStatus().equals(purchaseOrder.getStatus())) {
            if (ErpPurchaseOrderStatusEnum.APPROVE.getStatus().equals(purchaseOrder.getStatus())) {
                throw exception(PURCHASE_ORDER_DELETE_FAIL_APPROVE, purchaseOrder.getNo());
            }
            // 审批中禁止删除,需先由审批人「审核不通过」退回
            if (ErpPurchaseOrderStatusEnum.PROCESS.getStatus().equals(purchaseOrder.getStatus())) {
                throw exception(PURCHASE_ORDER_UPDATE_FAIL_PROCESS, purchaseOrder.getNo());
            }
        });
        // 2. 遍历删除
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchasePlanServiceImpl.java
@@ -216,7 +216,9 @@
        // 3. 校验审批配置已启用且配置了有效审批人(未配置时抛出带明确提示的业务异常)
        approvalConfigApi.validateApprovalEnabledAndGetApprovers(PURCHASE_PLAN_APPROVE_BIZ_TYPE);
        // 4. 状态置为审批中,并清空上一轮审核结果,避免残留「审核不通过」的原因
        purchasePlanMapper.submitAndResetAuditInfo(id, ErpPurchasePlanStatusEnum.PROCESS.getStatus());
        if (purchasePlanMapper.submitAndResetAuditInfo(id, ErpPurchasePlanStatusEnum.PROCESS.getStatus()) != 1) {
            throw exception(PURCHASE_PLAN_SUBMIT_FAIL_NOT_ALLOWED);
        }
    }
    @Override
@@ -241,13 +243,17 @@
            if (StrUtil.isBlank(reviewRemark)) {
                throw exception(PURCHASE_PLAN_AUDIT_REJECT_REASON_REQUIRED);
            }
            purchasePlanMapper.auditPurchasePlan(id, ErpPurchasePlanStatusEnum.REJECT.getStatus(),
                    userId, getUserNickname(userId), reviewRemark);
            if (purchasePlanMapper.auditPurchasePlan(id, ErpPurchasePlanStatusEnum.REJECT.getStatus(),
                    userId, getUserNickname(userId), reviewRemark) != 1) {
                throw exception(PURCHASE_PLAN_AUDIT_FAIL_NOT_PROCESS);
            }
            return;
        }
        // 5. 审核通过:记录审核人与审核意见,状态置为审核通过
        purchasePlanMapper.auditPurchasePlan(id, ErpPurchasePlanStatusEnum.APPROVE.getStatus(),
                userId, getUserNickname(userId), reviewRemark);
        if (purchasePlanMapper.auditPurchasePlan(id, ErpPurchasePlanStatusEnum.APPROVE.getStatus(),
                userId, getUserNickname(userId), reviewRemark) != 1) {
            throw exception(PURCHASE_PLAN_AUDIT_FAIL_NOT_PROCESS);
        }
        // 6. 审核通过且未生成申请,自动生成采购申请
        if (!Boolean.TRUE.equals(plan.getGeneratedFlag())) {
            try {
@@ -566,7 +572,8 @@
    private ErpPurchasePlanDO validatePurchasePlanCanModify(Long id) {
        ErpPurchasePlanDO plan = validatePurchasePlanExists(id);
        if (!ErpPurchasePlanStatusEnum.DRAFT.getStatus().equals(plan.getStatus())) {
        if (!ErpPurchasePlanStatusEnum.DRAFT.getStatus().equals(plan.getStatus())
                && !ErpPurchasePlanStatusEnum.REJECT.getStatus().equals(plan.getStatus())) {
            throw exception(PURCHASE_PLAN_UPDATE_FAIL_NOT_DRAFT, plan.getNo());
        }
        return plan;
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseRequestServiceImpl.java
@@ -22,7 +22,7 @@
import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseRequestMapper;
import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpSupplierMapper;
import cn.iocoder.yudao.module.erp.dal.redis.no.ErpNoRedisDAO;
import cn.iocoder.yudao.module.erp.enums.ErpAuditStatus;
import cn.iocoder.yudao.module.erp.enums.ErpPurchaseOrderStatusEnum;
import cn.iocoder.yudao.module.erp.enums.ErpPurchaseRequestStatusEnum;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
@@ -269,7 +269,9 @@
        // 3. 校验审批配置已启用且配置了有效审批人
        approvalConfigApi.validateApprovalEnabledAndGetApprovers(PURCHASE_REQUEST_APPROVE_BIZ_TYPE);
        // 4. 状态置为审批中,并清空上一轮审核结果
        purchaseRequestMapper.submitAndResetAuditInfo(id, ErpPurchaseRequestStatusEnum.PROCESS.getStatus());
        if (purchaseRequestMapper.submitAndResetAuditInfo(id, ErpPurchaseRequestStatusEnum.PROCESS.getStatus()) != 1) {
            throw exception(PURCHASE_REQUEST_SUBMIT_FAIL_NOT_ALLOWED);
        }
    }
    @Override
@@ -294,13 +296,17 @@
            if (StrUtil.isBlank(reviewRemark)) {
                throw exception(PURCHASE_REQUEST_AUDIT_REJECT_REASON_REQUIRED);
            }
            purchaseRequestMapper.auditPurchaseRequest(id, ErpPurchaseRequestStatusEnum.REJECT.getStatus(),
                    userId, getUserNickname(userId), reviewRemark);
            if (purchaseRequestMapper.auditPurchaseRequest(id, ErpPurchaseRequestStatusEnum.REJECT.getStatus(),
                    userId, getUserNickname(userId), reviewRemark) != 1) {
                throw exception(PURCHASE_REQUEST_AUDIT_FAIL_NOT_PROCESS);
            }
            return;
        }
        // 5. 审核通过:记录审核人与审核意见
        purchaseRequestMapper.auditPurchaseRequest(id, ErpPurchaseRequestStatusEnum.APPROVE.getStatus(),
                userId, getUserNickname(userId), reviewRemark);
        if (purchaseRequestMapper.auditPurchaseRequest(id, ErpPurchaseRequestStatusEnum.APPROVE.getStatus(),
                userId, getUserNickname(userId), reviewRemark) != 1) {
            throw exception(PURCHASE_REQUEST_AUDIT_FAIL_NOT_PROCESS);
        }
        // 6. 审核通过且有供应商、未生成订单时,自动生成采购订单
        if (purchaseRequest.getSupplierId() != null && purchaseRequest.getOrderId() == null) {
            try {
@@ -376,7 +382,8 @@
        // 3. 创建采购订单
        ErpPurchaseOrderDO purchaseOrder = new ErpPurchaseOrderDO();
        purchaseOrder.setNo(orderNo);
        purchaseOrder.setStatus(ErpAuditStatus.APPROVE.getStatus()); // 采购订单直接为已审核状态
        // 自动生成的采购订单仍为草稿,必须走订单自身的提交/审批流程,避免绕过审批
        purchaseOrder.setStatus(ErpPurchaseOrderStatusEnum.DRAFT.getStatus());
        purchaseOrder.setSupplierId(purchaseRequest.getSupplierId());
        purchaseOrder.setOrderTime(purchaseRequest.getRequestTime());
        purchaseOrder.setTotalCount(purchaseRequest.getTotalCount());
@@ -410,9 +417,10 @@
        });
        purchaseOrderItemMapper.insertBatch(orderItems);
        // 5. 更新采购申请的订单信息
        // 5. 更新采购申请的订单信息,并回写最终使用的供应商
        purchaseRequestMapper.updateById(new ErpPurchaseRequestDO()
                .setId(id)
                .setSupplierId(purchaseRequest.getSupplierId())
                .setOrderId(purchaseOrder.getId())
                .setOrderNo(orderNo));
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/listener/ErpPurchasePlanStatusListener.java
@@ -4,16 +4,19 @@
import cn.iocoder.yudao.module.erp.service.purchase.ErpPurchasePlanService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
 * 采购计划审批状态监听器
 * 采购计划审批状态监听器(Legacy,已停用)
 *
 * 监听 BPM 流程实例状态变更事件,更新采购计划的审批状态
 * 采购计划审批已改为「审批配置」轻量级状态机,由
 * {@link ErpPurchasePlanService#submitPurchasePlan(Long)} / {@link ErpPurchasePlanService#auditPurchasePlan(Long, Boolean, String)}
 * 直接驱动状态,不再依赖 BPM。
 *
 * 保留本类仅作历史参考;不再注册为 Spring Bean,避免旧的 BPM 流程实例触发时
 * 与新审批路径重复更新/重复生成采购申请。
 *
 * @author xiaoyi
 */
@Component
@Slf4j
public class ErpPurchasePlanStatusListener extends cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEventListener {
yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/ErrorCodeConstants.java
@@ -41,6 +41,7 @@
    ErrorCode LEAVE_APPLICATION_DURATION_EXCEED = new ErrorCode(1_030_001_005, "请假时长超出年假余额");
    ErrorCode LEAVE_APPLICATION_AUDIT_FAIL_NOT_PROCESS = new ErrorCode(1_030_001_006, "只有审批中状态的请假申请才能审核");
    ErrorCode LEAVE_APPLICATION_AUDIT_REJECT_REASON_REQUIRED = new ErrorCode(1_030_001_007, "审核不通过时必须填写不通过原因");
    ErrorCode LEAVE_APPLICATION_APPLICANT_NOT_EXISTS = new ErrorCode(1_030_001_008, "请假申请人不存在");
    // ========== 调岗申请 1-030-002-000 ==========
    ErrorCode TRANSFER_APPLICATION_NOT_EXISTS = new ErrorCode(1_030_002_000, "调岗申请不存在");
@@ -93,6 +94,7 @@
    ErrorCode ATTENDANCE_RULE_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_030_004_023, "导入考勤规则数据不能为空");
    ErrorCode ATTENDANCE_RULE_EXISTS = new ErrorCode(1_030_004_024, "已存在考勤规则【{}】,且未开启覆盖!");
    ErrorCode ATTENDANCE_RULE_DEPT_NOT_EXISTS = new ErrorCode(1_030_004_025, "部门【{}】不存在");
    ErrorCode ATTENDANCE_EXCEPTION_TIME_REQUIRED = new ErrorCode(1_030_004_026, "补卡申请必须填写补卡时间");
    // ========== 绩效考核 1-030-008-000 ==========
    ErrorCode PERFORMANCE_APPRAISE_NOT_EXISTS = new ErrorCode(1_030_008_000, "绩效考核方案不存在");
@@ -108,6 +110,7 @@
    ErrorCode PERFORMANCE_APPRAISE_SCOPE_TEAM_EMPTY = new ErrorCode(1_030_008_010, "指定班组为空,无法生成被考核人");
    ErrorCode PERFORMANCE_APPRAISE_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_030_008_011, "导入绩效考核方案数据不能为空");
    ErrorCode PERFORMANCE_APPRAISE_EXISTS = new ErrorCode(1_030_008_012, "已存在绩效考核方案【{}】,且未开启覆盖!");
    ErrorCode PERFORMANCE_APPRAISE_ITEM_WEIGHT_INVALID = new ErrorCode(1_030_008_013, "考核指标权重合计必须大于 0");
    // ========== 薪酬管理 1-030-005-000 ==========
    ErrorCode SALARY_STRUCTURE_NOT_EXISTS = new ErrorCode(1_030_005_000, "薪酬结构不存在");
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceExceptionSaveReqVO.java
@@ -24,7 +24,8 @@
    @NotNull(message = "异常日期不能为空")
    private LocalDate exceptionDate;
    @Schema(description = "异常时间点", example = "2024-01-15 09:00:00")
    @Schema(description = "异常时间点", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-01-15 09:00:00")
    @NotNull(message = "异常时间点不能为空")
    private LocalDateTime exceptionTime;
    @Schema(description = "申请原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "忘记打卡")
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/approval/HrmLeaveApplicationMapper.java
@@ -48,6 +48,8 @@
    default int submitAndResetAuditInfo(Long id, Integer status) {
        return update(null, new LambdaUpdateWrapper<HrmLeaveApplicationDO>()
                .eq(HrmLeaveApplicationDO::getId, id)
                .in(HrmLeaveApplicationDO::getStatus,
                        HrmAuditStatusEnum.DRAFT.getStatus(), HrmAuditStatusEnum.REJECT.getStatus())
                .set(HrmLeaveApplicationDO::getStatus, status)
                .set(HrmLeaveApplicationDO::getReviewerId, null)
                .set(HrmLeaveApplicationDO::getReviewerName, null)
@@ -71,6 +73,7 @@
                                      String reviewerName, String reviewRemark) {
        return update(null, new LambdaUpdateWrapper<HrmLeaveApplicationDO>()
                .eq(HrmLeaveApplicationDO::getId, id)
                .eq(HrmLeaveApplicationDO::getStatus, HrmAuditStatusEnum.PROCESS.getStatus())
                .set(HrmLeaveApplicationDO::getStatus, status)
                .set(HrmLeaveApplicationDO::getReviewerId, reviewerId)
                .set(HrmLeaveApplicationDO::getReviewerName, reviewerName)
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/approval/HrmRegularizeApplicationMapper.java
@@ -5,6 +5,7 @@
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmRegularizeApplicationPageReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmRegularizeApplicationDO;
import cn.iocoder.yudao.module.hrm.enums.HrmAuditStatusEnum;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
@@ -46,6 +47,8 @@
    default int submitAndResetAuditInfo(Long id, Integer status) {
        return update(null, new LambdaUpdateWrapper<HrmRegularizeApplicationDO>()
                .eq(HrmRegularizeApplicationDO::getId, id)
                .in(HrmRegularizeApplicationDO::getStatus,
                        HrmAuditStatusEnum.DRAFT.getStatus(), HrmAuditStatusEnum.REJECT.getStatus())
                .set(HrmRegularizeApplicationDO::getStatus, status)
                .set(HrmRegularizeApplicationDO::getReviewerId, null)
                .set(HrmRegularizeApplicationDO::getReviewerName, null)
@@ -69,6 +72,7 @@
                                           String reviewerName, String reviewRemark) {
        return update(null, new LambdaUpdateWrapper<HrmRegularizeApplicationDO>()
                .eq(HrmRegularizeApplicationDO::getId, id)
                .eq(HrmRegularizeApplicationDO::getStatus, HrmAuditStatusEnum.PROCESS.getStatus())
                .set(HrmRegularizeApplicationDO::getStatus, status)
                .set(HrmRegularizeApplicationDO::getReviewerId, reviewerId)
                .set(HrmRegularizeApplicationDO::getReviewerName, reviewerName)
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/approval/HrmResignationApplicationMapper.java
@@ -5,6 +5,7 @@
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmResignationApplicationPageReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmResignationApplicationDO;
import cn.iocoder.yudao.module.hrm.enums.HrmAuditStatusEnum;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
@@ -46,6 +47,8 @@
    default int submitAndResetAuditInfo(Long id, Integer status) {
        return update(null, new LambdaUpdateWrapper<HrmResignationApplicationDO>()
                .eq(HrmResignationApplicationDO::getId, id)
                .in(HrmResignationApplicationDO::getStatus,
                        HrmAuditStatusEnum.DRAFT.getStatus(), HrmAuditStatusEnum.REJECT.getStatus())
                .set(HrmResignationApplicationDO::getStatus, status)
                .set(HrmResignationApplicationDO::getReviewerId, null)
                .set(HrmResignationApplicationDO::getReviewerName, null)
@@ -69,6 +72,7 @@
                                            String reviewerName, String reviewRemark) {
        return update(null, new LambdaUpdateWrapper<HrmResignationApplicationDO>()
                .eq(HrmResignationApplicationDO::getId, id)
                .eq(HrmResignationApplicationDO::getStatus, HrmAuditStatusEnum.PROCESS.getStatus())
                .set(HrmResignationApplicationDO::getStatus, status)
                .set(HrmResignationApplicationDO::getReviewerId, reviewerId)
                .set(HrmResignationApplicationDO::getReviewerName, reviewerName)
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/approval/HrmTransferApplicationMapper.java
@@ -5,6 +5,7 @@
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmTransferApplicationPageReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmTransferApplicationDO;
import cn.iocoder.yudao.module.hrm.enums.HrmAuditStatusEnum;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
@@ -47,6 +48,8 @@
    default int submitAndResetAuditInfo(Long id, Integer status) {
        return update(null, new LambdaUpdateWrapper<HrmTransferApplicationDO>()
                .eq(HrmTransferApplicationDO::getId, id)
                .in(HrmTransferApplicationDO::getStatus,
                        HrmAuditStatusEnum.DRAFT.getStatus(), HrmAuditStatusEnum.REJECT.getStatus())
                .set(HrmTransferApplicationDO::getStatus, status)
                .set(HrmTransferApplicationDO::getReviewerId, null)
                .set(HrmTransferApplicationDO::getReviewerName, null)
@@ -70,12 +73,15 @@
                                         String reviewerName, String reviewRemark) {
        return update(null, new LambdaUpdateWrapper<HrmTransferApplicationDO>()
                .eq(HrmTransferApplicationDO::getId, id)
                .eq(HrmTransferApplicationDO::getStatus, HrmAuditStatusEnum.PROCESS.getStatus())
                .set(HrmTransferApplicationDO::getStatus, status)
                .set(HrmTransferApplicationDO::getReviewerId, reviewerId)
                .set(HrmTransferApplicationDO::getReviewerName, reviewerName)
                .set(HrmTransferApplicationDO::getReviewTime, LocalDateTime.now())
                .set(HrmTransferApplicationDO::getReviewRemark, reviewRemark)
                .set(HrmTransferApplicationDO::getApproveTime, LocalDateTime.now()));
                // 仅审核通过时写入通过时间,驳回不写
                .set(HrmAuditStatusEnum.APPROVE.getStatus().equals(status),
                        HrmTransferApplicationDO::getApproveTime, LocalDateTime.now()));
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/attendance/HrmAttendanceExceptionMapper.java
@@ -5,6 +5,7 @@
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceExceptionPageReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceExceptionDO;
import cn.iocoder.yudao.module.hrm.enums.HrmAuditStatusEnum;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
@@ -45,6 +46,8 @@
    default int submitAndResetAuditInfo(Long id, Integer status) {
        return update(null, new LambdaUpdateWrapper<HrmAttendanceExceptionDO>()
                .eq(HrmAttendanceExceptionDO::getId, id)
                .in(HrmAttendanceExceptionDO::getStatus,
                        HrmAuditStatusEnum.DRAFT.getStatus(), HrmAuditStatusEnum.REJECT.getStatus())
                .set(HrmAttendanceExceptionDO::getStatus, status)
                .set(HrmAttendanceExceptionDO::getReviewerId, null)
                .set(HrmAttendanceExceptionDO::getReviewerName, null)
@@ -68,12 +71,15 @@
                                         String reviewerName, String reviewRemark) {
        return update(null, new LambdaUpdateWrapper<HrmAttendanceExceptionDO>()
                .eq(HrmAttendanceExceptionDO::getId, id)
                .eq(HrmAttendanceExceptionDO::getStatus, HrmAuditStatusEnum.PROCESS.getStatus())
                .set(HrmAttendanceExceptionDO::getStatus, status)
                .set(HrmAttendanceExceptionDO::getReviewerId, reviewerId)
                .set(HrmAttendanceExceptionDO::getReviewerName, reviewerName)
                .set(HrmAttendanceExceptionDO::getReviewTime, LocalDateTime.now())
                .set(HrmAttendanceExceptionDO::getReviewRemark, reviewRemark)
                .set(HrmAttendanceExceptionDO::getApproveTime, LocalDateTime.now()));
                // 仅审核通过时写入通过时间,驳回不写
                .set(HrmAuditStatusEnum.APPROVE.getStatus().equals(status),
                        HrmAttendanceExceptionDO::getApproveTime, LocalDateTime.now()));
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/performanceappraise/HrmPerformanceAppraiseMapper.java
@@ -5,6 +5,7 @@
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.hrm.controller.admin.performanceappraise.vo.HrmPerformanceAppraisePageReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.performanceappraise.HrmPerformanceAppraiseDO;
import cn.iocoder.yudao.module.hrm.enums.HrmAuditStatusEnum;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
@@ -37,6 +38,8 @@
    default int submitAndResetAuditInfo(Long id, Integer auditStatus) {
        return update(null, new LambdaUpdateWrapper<HrmPerformanceAppraiseDO>()
                .eq(HrmPerformanceAppraiseDO::getId, id)
                .in(HrmPerformanceAppraiseDO::getAuditStatus,
                        HrmAuditStatusEnum.DRAFT.getStatus(), HrmAuditStatusEnum.REJECT.getStatus())
                .set(HrmPerformanceAppraiseDO::getAuditStatus, auditStatus)
                .set(HrmPerformanceAppraiseDO::getReviewerId, null)
                .set(HrmPerformanceAppraiseDO::getReviewerName, null)
@@ -61,6 +64,7 @@
                              String reviewerName, String reviewRemark) {
        return update(null, new LambdaUpdateWrapper<HrmPerformanceAppraiseDO>()
                .eq(HrmPerformanceAppraiseDO::getId, id)
                .eq(HrmPerformanceAppraiseDO::getAuditStatus, HrmAuditStatusEnum.PROCESS.getStatus())
                .set(HrmPerformanceAppraiseDO::getAuditStatus, auditStatus)
                .set(HrmPerformanceAppraiseDO::getStatus, status)
                .set(HrmPerformanceAppraiseDO::getReviewerId, reviewerId)
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmLeaveApplicationServiceImpl.java
@@ -77,7 +77,7 @@
        }
        AdminUserRespDTO user = adminUserApi.getUser(userId);
        if (user == null) {
            userId = getCurrentUserId();
            throw exception(LEAVE_APPLICATION_APPLICANT_NOT_EXISTS);
        }
        // 3. 插入
        HrmLeaveApplicationDO application = BeanUtils.toBean(createReqVO, HrmLeaveApplicationDO.class);
@@ -108,9 +108,9 @@
    public void updateLeaveApplication(HrmLeaveApplicationSaveReqVO updateReqVO) {
        // 校验存在
        HrmLeaveApplicationDO application = validateLeaveApplicationExists(updateReqVO.getId());
        // 校验状态为草稿或审批中
        // 校验状态为草稿或审核不通过(审批中禁止修改)
        if (!Objects.equals(application.getStatus(), HrmAuditStatusEnum.DRAFT.getStatus())
                && !Objects.equals(application.getStatus(), HrmAuditStatusEnum.PROCESS.getStatus())) {
                && !Objects.equals(application.getStatus(), HrmAuditStatusEnum.REJECT.getStatus())) {
            throw exception(LEAVE_APPLICATION_UPDATE_FAIL_NOT_DRAFT);
        }
        // 更新
@@ -222,7 +222,9 @@
        approvalConfigApi.validateApprovalEnabledAndGetApprovers(LEAVE_APPROVE_BIZ_TYPE);
        // 3. 状态置为审批中,并清空上一轮审核结果,避免残留「审核不通过」的原因
        leaveApplicationMapper.submitAndResetAuditInfo(id, HrmAuditStatusEnum.PROCESS.getStatus());
        if (leaveApplicationMapper.submitAndResetAuditInfo(id, HrmAuditStatusEnum.PROCESS.getStatus()) != 1) {
            throw exception(LEAVE_APPLICATION_SUBMIT_FAIL_NOT_EDITABLE);
        }
    }
    @Override
@@ -247,13 +249,17 @@
            if (StrUtil.isBlank(reviewRemark)) {
                throw exception(LEAVE_APPLICATION_AUDIT_REJECT_REASON_REQUIRED);
            }
            leaveApplicationMapper.auditLeaveApplication(id, HrmAuditStatusEnum.REJECT.getStatus(),
                    userId, getUserNickname(userId), reviewRemark);
            if (leaveApplicationMapper.auditLeaveApplication(id, HrmAuditStatusEnum.REJECT.getStatus(),
                    userId, getUserNickname(userId), reviewRemark) != 1) {
                throw exception(LEAVE_APPLICATION_AUDIT_FAIL_NOT_PROCESS);
            }
            return;
        }
        // 5. 审核通过:记录审核人与审核意见,状态置为审核通过
        leaveApplicationMapper.auditLeaveApplication(id, HrmAuditStatusEnum.APPROVE.getStatus(),
                userId, getUserNickname(userId), reviewRemark);
        if (leaveApplicationMapper.auditLeaveApplication(id, HrmAuditStatusEnum.APPROVE.getStatus(),
                userId, getUserNickname(userId), reviewRemark) != 1) {
            throw exception(LEAVE_APPLICATION_AUDIT_FAIL_NOT_PROCESS);
        }
    }
    /**
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmRegularizeApplicationServiceImpl.java
@@ -124,7 +124,8 @@
    @Override
    public void updateRegularizeApplication(HrmRegularizeApplicationSaveReqVO updateReqVO) {
        HrmRegularizeApplicationDO application = validateRegularizeApplicationExists(updateReqVO.getId());
        if (!Objects.equals(application.getStatus(), HrmAuditStatusEnum.DRAFT.getStatus())) {
        if (!Objects.equals(application.getStatus(), HrmAuditStatusEnum.DRAFT.getStatus())
                && !Objects.equals(application.getStatus(), HrmAuditStatusEnum.REJECT.getStatus())) {
            throw exception(REGULARIZE_APPLICATION_UPDATE_FAIL_NOT_DRAFT);
        }
        HrmRegularizeApplicationDO updateObj = BeanUtils.toBean(updateReqVO, HrmRegularizeApplicationDO.class);
@@ -134,7 +135,8 @@
    @Override
    public void deleteRegularizeApplication(Long id) {
        HrmRegularizeApplicationDO application = validateRegularizeApplicationExists(id);
        if (!Objects.equals(application.getStatus(), HrmAuditStatusEnum.DRAFT.getStatus())) {
        if (!Objects.equals(application.getStatus(), HrmAuditStatusEnum.DRAFT.getStatus())
                && !Objects.equals(application.getStatus(), HrmAuditStatusEnum.REJECT.getStatus())) {
            throw exception(REGULARIZE_APPLICATION_UPDATE_FAIL_NOT_DRAFT);
        }
        regularizeApplicationMapper.deleteById(id);
@@ -241,7 +243,9 @@
        approvalConfigApi.validateApprovalEnabledAndGetApprovers(REGULARIZE_APPROVE_BIZ_TYPE);
        // 3. 状态置为审批中,并清空上一轮审核结果,避免残留「审核不通过」的原因
        regularizeApplicationMapper.submitAndResetAuditInfo(id, HrmAuditStatusEnum.PROCESS.getStatus());
        if (regularizeApplicationMapper.submitAndResetAuditInfo(id, HrmAuditStatusEnum.PROCESS.getStatus()) != 1) {
            throw exception(REGULARIZE_APPLICATION_SUBMIT_FAIL_NOT_EDITABLE);
        }
    }
    @Override
@@ -266,13 +270,17 @@
            if (StrUtil.isBlank(reviewRemark)) {
                throw exception(REGULARIZE_APPLICATION_AUDIT_REJECT_REASON_REQUIRED);
            }
            regularizeApplicationMapper.auditRegularizeApplication(id, HrmAuditStatusEnum.REJECT.getStatus(),
                    userId, getUserNickname(userId), reviewRemark);
            if (regularizeApplicationMapper.auditRegularizeApplication(id, HrmAuditStatusEnum.REJECT.getStatus(),
                    userId, getUserNickname(userId), reviewRemark) != 1) {
                throw exception(REGULARIZE_APPLICATION_AUDIT_FAIL_NOT_PROCESS);
            }
            return;
        }
        // 5. 审核通过:记录审核人与审核意见,状态置为审核通过
        regularizeApplicationMapper.auditRegularizeApplication(id, HrmAuditStatusEnum.APPROVE.getStatus(),
                userId, getUserNickname(userId), reviewRemark);
        if (regularizeApplicationMapper.auditRegularizeApplication(id, HrmAuditStatusEnum.APPROVE.getStatus(),
                userId, getUserNickname(userId), reviewRemark) != 1) {
            throw exception(REGULARIZE_APPLICATION_AUDIT_FAIL_NOT_PROCESS);
        }
        // 6. 审核通过后置业务逻辑:回写员工主档(状态=在职,用工性质=正式,转正日期 = 拟转正日期或当天)
        writeBackEmployee(application);
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmResignationApplicationServiceImpl.java
@@ -131,7 +131,8 @@
    @Override
    public void updateResignationApplication(HrmResignationApplicationSaveReqVO updateReqVO) {
        HrmResignationApplicationDO application = validateResignationApplicationExists(updateReqVO.getId());
        if (!Objects.equals(application.getStatus(), HrmAuditStatusEnum.DRAFT.getStatus())) {
        if (!Objects.equals(application.getStatus(), HrmAuditStatusEnum.DRAFT.getStatus())
                && !Objects.equals(application.getStatus(), HrmAuditStatusEnum.REJECT.getStatus())) {
            throw exception(RESIGNATION_APPLICATION_UPDATE_FAIL_NOT_DRAFT);
        }
        HrmResignationApplicationDO updateObj = BeanUtils.toBean(updateReqVO, HrmResignationApplicationDO.class);
@@ -142,7 +143,8 @@
    @Override
    public void deleteResignationApplication(Long id) {
        HrmResignationApplicationDO application = validateResignationApplicationExists(id);
        if (!Objects.equals(application.getStatus(), HrmAuditStatusEnum.DRAFT.getStatus())) {
        if (!Objects.equals(application.getStatus(), HrmAuditStatusEnum.DRAFT.getStatus())
                && !Objects.equals(application.getStatus(), HrmAuditStatusEnum.REJECT.getStatus())) {
            throw exception(RESIGNATION_APPLICATION_UPDATE_FAIL_NOT_DRAFT);
        }
        storageAttachmentApi.deleteAttachmentsByRecord("hrm_resignation_application", id);
@@ -260,7 +262,9 @@
        approvalConfigApi.validateApprovalEnabledAndGetApprovers(RESIGNATION_APPROVE_BIZ_TYPE);
        // 3. 状态置为审批中,并清空上一轮审核结果,避免残留「审核不通过」的原因
        resignationApplicationMapper.submitAndResetAuditInfo(id, HrmAuditStatusEnum.PROCESS.getStatus());
        if (resignationApplicationMapper.submitAndResetAuditInfo(id, HrmAuditStatusEnum.PROCESS.getStatus()) != 1) {
            throw exception(RESIGNATION_APPLICATION_SUBMIT_FAIL_NOT_EDITABLE);
        }
    }
    @Override
@@ -285,13 +289,17 @@
            if (StrUtil.isBlank(reviewRemark)) {
                throw exception(RESIGNATION_APPLICATION_AUDIT_REJECT_REASON_REQUIRED);
            }
            resignationApplicationMapper.auditResignationApplication(id, HrmAuditStatusEnum.REJECT.getStatus(),
                    userId, getUserNickname(userId), reviewRemark);
            if (resignationApplicationMapper.auditResignationApplication(id, HrmAuditStatusEnum.REJECT.getStatus(),
                    userId, getUserNickname(userId), reviewRemark) != 1) {
                throw exception(RESIGNATION_APPLICATION_AUDIT_FAIL_NOT_PROCESS);
            }
            return;
        }
        // 5. 审核通过:记录审核人与审核意见,状态置为审核通过
        resignationApplicationMapper.auditResignationApplication(id, HrmAuditStatusEnum.APPROVE.getStatus(),
                userId, getUserNickname(userId), reviewRemark);
        if (resignationApplicationMapper.auditResignationApplication(id, HrmAuditStatusEnum.APPROVE.getStatus(),
                userId, getUserNickname(userId), reviewRemark) != 1) {
            throw exception(RESIGNATION_APPLICATION_AUDIT_FAIL_NOT_PROCESS);
        }
        // 6. 审核通过后置业务逻辑:回写员工主档(状态=离职,离职日期 = 实际离职日期或预计离职日期)
        writeBackEmployee(application);
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmTransferApplicationServiceImpl.java
@@ -124,7 +124,7 @@
    public void updateTransferApplication(HrmTransferApplicationSaveReqVO updateReqVO) {
        HrmTransferApplicationDO application = validateTransferApplicationExists(updateReqVO.getId());
        if (!Objects.equals(application.getStatus(), HrmAuditStatusEnum.DRAFT.getStatus())
                && !Objects.equals(application.getStatus(), HrmAuditStatusEnum.PROCESS.getStatus())) {
                && !Objects.equals(application.getStatus(), HrmAuditStatusEnum.REJECT.getStatus())) {
            throw exception(TRANSFER_APPLICATION_UPDATE_FAIL_NOT_DRAFT);
        }
        if (Objects.equals(updateReqVO.getTargetDeptId(), updateReqVO.getCurrentDeptId())) {
@@ -265,7 +265,9 @@
        approvalConfigApi.validateApprovalEnabledAndGetApprovers(TRANSFER_APPROVE_BIZ_TYPE);
        // 3. 状态置为审批中,并清空上一轮审核结果,避免残留「审核不通过」的原因
        transferApplicationMapper.submitAndResetAuditInfo(id, HrmAuditStatusEnum.PROCESS.getStatus());
        if (transferApplicationMapper.submitAndResetAuditInfo(id, HrmAuditStatusEnum.PROCESS.getStatus()) != 1) {
            throw exception(TRANSFER_APPLICATION_SUBMIT_FAIL_NOT_EDITABLE);
        }
    }
    @Override
@@ -290,13 +292,17 @@
            if (StrUtil.isBlank(reviewRemark)) {
                throw exception(TRANSFER_APPLICATION_AUDIT_REJECT_REASON_REQUIRED);
            }
            transferApplicationMapper.auditTransferApplication(id, HrmAuditStatusEnum.REJECT.getStatus(),
                    userId, getUserNickname(userId), reviewRemark);
            if (transferApplicationMapper.auditTransferApplication(id, HrmAuditStatusEnum.REJECT.getStatus(),
                    userId, getUserNickname(userId), reviewRemark) != 1) {
                throw exception(TRANSFER_APPLICATION_AUDIT_FAIL_NOT_PROCESS);
            }
            return;
        }
        // 5. 审核通过:记录审核人与审核意见,状态置为审核通过
        transferApplicationMapper.auditTransferApplication(id, HrmAuditStatusEnum.APPROVE.getStatus(),
                userId, getUserNickname(userId), reviewRemark);
        if (transferApplicationMapper.auditTransferApplication(id, HrmAuditStatusEnum.APPROVE.getStatus(),
                userId, getUserNickname(userId), reviewRemark) != 1) {
            throw exception(TRANSFER_APPLICATION_AUDIT_FAIL_NOT_PROCESS);
        }
        // 6. 审核通过后置业务逻辑:回写员工主档(部门 = 目标部门,岗位 = 目标岗位)
        writeBackEmployee(application);
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceExceptionServiceImpl.java
@@ -84,7 +84,8 @@
    @Override
    public void updateAttendanceException(HrmAttendanceExceptionSaveReqVO updateReqVO) {
        HrmAttendanceExceptionDO attendanceException = validateAttendanceExceptionExists(updateReqVO.getId());
        if (!HrmAuditStatusEnum.DRAFT.getStatus().equals(attendanceException.getStatus())) {
        if (!HrmAuditStatusEnum.DRAFT.getStatus().equals(attendanceException.getStatus())
                && !HrmAuditStatusEnum.REJECT.getStatus().equals(attendanceException.getStatus())) {
            throw exception(ATTENDANCE_EXCEPTION_UPDATE_FAIL_NOT_DRAFT);
        }
        HrmAttendanceExceptionDO updateObj = BeanUtils.toBean(updateReqVO, HrmAttendanceExceptionDO.class);
@@ -95,7 +96,8 @@
    @Override
    public void deleteAttendanceException(Long id) {
        HrmAttendanceExceptionDO attendanceException = validateAttendanceExceptionExists(id);
        if (!HrmAuditStatusEnum.DRAFT.getStatus().equals(attendanceException.getStatus())) {
        if (!HrmAuditStatusEnum.DRAFT.getStatus().equals(attendanceException.getStatus())
                && !HrmAuditStatusEnum.REJECT.getStatus().equals(attendanceException.getStatus())) {
            throw exception(ATTENDANCE_EXCEPTION_DELETE_FAIL_NOT_DRAFT);
        }
        storageAttachmentApi.deleteAttachmentsByRecord("hrm_attendance_exception", id);
@@ -130,11 +132,19 @@
            throw exception(ATTENDANCE_EXCEPTION_SUBMIT_FAIL_NOT_EDITABLE);
        }
        // 2. 校验审批配置已启用且配置了有效审批人(未配置时抛出带明确提示的业务异常)
        // 2. 补卡类型必须填写补卡时间,避免审核通过后回填考勤记录时出现空指针
        if (HrmAttendanceExceptionTypeEnum.SUPPLEMENT.getType().equals(exception.getType())
                && exception.getExceptionTime() == null) {
            throw exception(ATTENDANCE_EXCEPTION_TIME_REQUIRED);
        }
        // 3. 校验审批配置已启用且配置了有效审批人(未配置时抛出带明确提示的业务异常)
        approvalConfigApi.validateApprovalEnabledAndGetApprovers(ATTENDANCE_APPROVE_BIZ_TYPE);
        // 3. 状态置为审批中,并清空上一轮审核结果,避免残留「审核不通过」的原因
        attendanceExceptionMapper.submitAndResetAuditInfo(id, HrmAuditStatusEnum.PROCESS.getStatus());
        // 4. 状态置为审批中,并清空上一轮审核结果,避免残留「审核不通过」的原因
        if (attendanceExceptionMapper.submitAndResetAuditInfo(id, HrmAuditStatusEnum.PROCESS.getStatus()) != 1) {
            throw exception(ATTENDANCE_EXCEPTION_SUBMIT_FAIL_NOT_EDITABLE);
        }
    }
    @Override
@@ -151,6 +161,11 @@
        if (!HrmAuditStatusEnum.PROCESS.getStatus().equals(exception.getStatus())) {
            throw exception(ATTENDANCE_EXCEPTION_AUDIT_FAIL_NOT_PROCESS);
        }
        // 2.1 补卡类型必须填写补卡时间,避免审核通过后回填考勤记录时出现空指针
        if (HrmAttendanceExceptionTypeEnum.SUPPLEMENT.getType().equals(exception.getType())
                && exception.getExceptionTime() == null) {
            throw exception(ATTENDANCE_EXCEPTION_TIME_REQUIRED);
        }
        // 3. 校验当前登录用户是该业务类型的审批人(或签:任一人均可审核)
        Long userId = getLoginUserId();
        approvalConfigApi.validateApprover(ATTENDANCE_APPROVE_BIZ_TYPE, userId);
@@ -159,13 +174,17 @@
            if (StrUtil.isBlank(reviewRemark)) {
                throw exception(ATTENDANCE_EXCEPTION_AUDIT_REJECT_REASON_REQUIRED);
            }
            attendanceExceptionMapper.auditAttendanceException(id, HrmAuditStatusEnum.REJECT.getStatus(),
                    userId, getUserNickname(userId), reviewRemark);
            if (attendanceExceptionMapper.auditAttendanceException(id, HrmAuditStatusEnum.REJECT.getStatus(),
                    userId, getUserNickname(userId), reviewRemark) != 1) {
                throw exception(ATTENDANCE_EXCEPTION_AUDIT_FAIL_NOT_PROCESS);
            }
            return;
        }
        // 5. 审核通过:记录审核人与审核意见,状态置为审核通过
        attendanceExceptionMapper.auditAttendanceException(id, HrmAuditStatusEnum.APPROVE.getStatus(),
                userId, getUserNickname(userId), reviewRemark);
        if (attendanceExceptionMapper.auditAttendanceException(id, HrmAuditStatusEnum.APPROVE.getStatus(),
                userId, getUserNickname(userId), reviewRemark) != 1) {
            throw exception(ATTENDANCE_EXCEPTION_AUDIT_FAIL_NOT_PROCESS);
        }
        // 6. 审核通过后置业务逻辑:补卡回填上下班时间 / 加班回写加班时长
        if (HrmAttendanceExceptionTypeEnum.SUPPLEMENT.getType().equals(exception.getType())) {
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRecordServiceImpl.java
@@ -20,6 +20,7 @@
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
@@ -43,6 +44,7 @@
 */
@Service
@Validated
@Slf4j
public class HrmAttendanceRecordServiceImpl implements HrmAttendanceRecordService {
    @Resource
@@ -483,6 +485,11 @@
        }
        LocalDate exceptionDate = exception.getExceptionDate();
        LocalDateTime exceptionTime = exception.getExceptionTime();
        // 补卡时间缺失时无法回填考勤记录,记录日志并跳过,避免空指针
        if (exceptionTime == null) {
            log.warn("[supplementClockIn] 补卡申请({}) 补卡时间为空,跳过考勤记录回填", exception.getId());
            return;
        }
        Long userId = exception.getUserId();
        HrmEmployeeDO employee = employeeService.getEmployee(userId);
        HrmAttendanceRuleDO rule = employee == null ? null
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/performanceappraise/HrmPerformanceAppraiseServiceImpl.java
@@ -199,7 +199,9 @@
        approvalConfigApi.validateApprovalEnabledAndGetApprovers(APPRAISE_APPROVE_BIZ_TYPE);
        // 3. 审批状态置为审批中,并清空上一轮审核结果,避免残留「审核不通过」的原因
        appraiseMapper.submitAndResetAuditInfo(id, HrmAuditStatusEnum.PROCESS.getStatus());
        if (appraiseMapper.submitAndResetAuditInfo(id, HrmAuditStatusEnum.PROCESS.getStatus()) != 1) {
            throw exception(ErrorCodeConstants.PERFORMANCE_APPRAISE_SUBMIT_FAIL_NOT_EDITABLE);
        }
    }
    @Override
@@ -225,14 +227,18 @@
                throw exception(ErrorCodeConstants.PERFORMANCE_APPRAISE_AUDIT_REJECT_REASON_REQUIRED);
            }
            // 方案状态保持不变
            appraiseMapper.auditAppraise(id, HrmAuditStatusEnum.REJECT.getStatus(), appraise.getStatus(),
                    userId, getUserNickname(userId), reviewRemark);
            if (appraiseMapper.auditAppraise(id, HrmAuditStatusEnum.REJECT.getStatus(), appraise.getStatus(),
                    userId, getUserNickname(userId), reviewRemark) != 1) {
                throw exception(ErrorCodeConstants.PERFORMANCE_APPRAISE_AUDIT_FAIL_NOT_PROCESS);
            }
            return;
        }
        // 5. 审核通过:记录审核人与审核意见,审批状态置为审核通过
        // 6. 审核通过后置业务逻辑:方案状态流转为「已发布」
        appraiseMapper.auditAppraise(id, HrmAuditStatusEnum.APPROVE.getStatus(), STATUS_PUBLISHED,
                userId, getUserNickname(userId), reviewRemark);
        if (appraiseMapper.auditAppraise(id, HrmAuditStatusEnum.APPROVE.getStatus(), STATUS_PUBLISHED,
                userId, getUserNickname(userId), reviewRemark) != 1) {
            throw exception(ErrorCodeConstants.PERFORMANCE_APPRAISE_AUDIT_FAIL_NOT_PROCESS);
        }
    }
    /**
@@ -323,6 +329,10 @@
                    .setWeight(weight)
                    .setSelfScore(selfScore)
                    .setSuperiorScore(superiorScore));
        }
        // 权重合计为 0 时无法计算加权得分,避免除零
        if (sumWeight.compareTo(BigDecimal.ZERO) <= 0) {
            throw exception(ErrorCodeConstants.PERFORMANCE_APPRAISE_ITEM_WEIGHT_INVALID);
        }
        BigDecimal selfTotal = weightedSelf.divide(sumWeight, 1, RoundingMode.HALF_UP);
        BigDecimal superiorTotal = weightedSuperior.divide(sumWeight, 1, RoundingMode.HALF_UP);
@@ -424,7 +434,9 @@
    private HrmPerformanceAppraiseDO validateAppraiseCanModify(Long id) {
        HrmPerformanceAppraiseDO appraise = validateAppraiseExists(id);
        if (ObjUtil.notEqual(appraise.getStatus(), STATUS_DRAFT)) {
        // 以审批状态为准:仅草稿 / 审核不通过允许修改,审批中禁止修改
        if (ObjUtil.notEqual(appraise.getAuditStatus(), HrmAuditStatusEnum.DRAFT.getStatus())
                && ObjUtil.notEqual(appraise.getAuditStatus(), HrmAuditStatusEnum.REJECT.getStatus())) {
            throw exception(ErrorCodeConstants.PERFORMANCE_APPRAISE_UPDATE_FAIL_NOT_DRAFT);
        }
        return appraise;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pd/processparamtemplate/vo/MesPdProcessParamTemplateSaveReqVO.java
@@ -16,6 +16,9 @@
    @Schema(description = "编号", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "1024")
    private Long id;
    @Schema(description = "模板编码(留空则自动生成)", example = "PPT000001")
    private String templateCode;
    @Schema(description = "模板名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "注塑工艺参数模板")
    @NotBlank(message = "模板名称不能为空")
    private String templateName;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pd/techconfirm/vo/ProductTechConfirmSaveReqVO.java
@@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.mes.controller.admin.pd.techconfirm.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
@@ -26,6 +27,7 @@
    private Long id;
    @Schema(description = "确认函编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "TC20260801001")
    @NotBlank(message = "确认函编号不能为空")
    private String confirmNo;
    @Schema(description = "客户ID(关联 crm_customer.id)", example = "2048")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
@@ -357,6 +357,8 @@
    ErrorCode PRO_FEEDBACK_UNCHECK_QUANTITY_EXISTS = new ErrorCode(1_040_506_012, "当前报工单未完成检验(待检数量:{}),无法执行报工");
    ErrorCode PRO_FEEDBACK_BACKFLUSH_BOM_REQUIRED = new ErrorCode(1_040_506_013, "当前工序为倒冲工序,但未配置BOM消耗,无法报工");
    ErrorCode PRO_FEEDBACK_CANNOT_CREATE_WHEN_PAUSED = new ErrorCode(1_040_506_014, "生产任务处于停工状态,无法报工");
    ErrorCode PRO_FEEDBACK_APPROVER_NOT_CONFIGURED = new ErrorCode(1_040_506_015, "报工单未指定审核人,无法审批");
    ErrorCode PRO_FEEDBACK_NOT_APPROVER = new ErrorCode(1_040_506_016, "您不是该报工单的指定审核人,无权审批");
    // ========== MES 生产管理-生产流转卡(1-040-507-000) ==========
    ErrorCode PRO_CARD_NOT_EXISTS = new ErrorCode(1_040_507_000, "生产流转卡不存在");
@@ -729,6 +731,8 @@
    ErrorCode WM_STOCK_TAKING_TASK_ALREADY_SUBMIT_APPROVAL = new ErrorCode(1_040_714_123, "盘点结果审批已提交,请勿重复提交");
    ErrorCode WM_STOCK_TAKING_TASK_RESULT_NOT_SUBMITTED = new ErrorCode(1_040_714_124, "只有已提交结果审批的盘点任务才能审核");
    ErrorCode WM_STOCK_TAKING_TASK_REJECT_REASON_REQUIRED = new ErrorCode(1_040_714_125, "审核不通过时必须填写不通过原因");
    ErrorCode WM_STOCK_TAKING_TASK_RESULT_APPROVAL_PENDING = new ErrorCode(1_040_714_126, "盘点结果审批已提交,审核完成前不允许修改盘点结果");
    ErrorCode WM_STOCK_TAKING_TASK_LINE_NOT_MATCH = new ErrorCode(1_040_714_127, "盘点任务行不属于指定的盘点任务");
    // ========== MES 仓库管理-销售出库单(1-040-714-000) ==========
    ErrorCode WM_PRODUCT_SALES_NOT_EXISTS = new ErrorCode(1_040_714_000, "销售出库单不存在");
@@ -774,6 +778,7 @@
    ErrorCode WM_MISC_ISSUE_REJECT_REASON_REQUIRED = new ErrorCode(1_040_715_007, "审核不通过时必须填写不通过原因");
    ErrorCode WM_MISC_ISSUE_LINE_NOT_EXISTS = new ErrorCode(1_040_715_100, "杂项出库单行不存在");
    ErrorCode WM_MISC_ISSUE_DETAIL_NOT_EXISTS = new ErrorCode(1_040_715_200, "杂项出库单明细不存在");
    ErrorCode WM_MISC_ISSUE_QUANTITY_INVALID = new ErrorCode(1_040_715_008, "杂项出库数量不能为空");
    // ========== MES 仓库管理-杂项入库单(1-040-716-000) ==========
    ErrorCode WM_MISC_RECEIPT_NOT_EXISTS = new ErrorCode(1_040_716_000, "杂项入库单不存在");
@@ -865,6 +870,7 @@
    ErrorCode PD_PROJECT_NOT_APPROVING = new ErrorCode(1_040_800_009, "只有审批中状态的设计项目才能审核");
    ErrorCode PD_PROJECT_AUDIT_REJECT_REASON_REQUIRED = new ErrorCode(1_040_800_010, "审核不通过时必须填写不通过原因");
    ErrorCode PD_PROJECT_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_040_800_011, "导入设计项目数据不能为空");
    ErrorCode PD_PROJECT_NO_DOCUMENT = new ErrorCode(1_040_800_012, "设计项目至少需要一份资料才允许提交审批");
    // ========== MES 产品设计-资料子表(1-040-801-000) ==========
    ErrorCode PD_DOCUMENT_NOT_EXISTS = new ErrorCode(1_040_801_000, "设计资料不存在");
@@ -902,6 +908,7 @@
    ErrorCode PD_TECH_CONFIRM_NOT_PENDING_AUDIT = new ErrorCode(1_040_808_004, "只有待审核状态的技术确认函才能审核");
    ErrorCode PD_TECH_CONFIRM_REJECT_REASON_REQUIRED = new ErrorCode(1_040_808_007, "审核不通过时必须填写不通过原因");
    ErrorCode PD_TECH_CONFIRM_NOT_AUDITED_FOR_CUSTOMER_CONFIRM = new ErrorCode(1_040_808_008, "只有已审核状态的技术确认函才能进行客户确认");
    ErrorCode PD_TECH_CONFIRM_NO_REQUIRED = new ErrorCode(1_040_808_009, "确认函编号不能为空");
    // ========== MES 产品设计-产品档案与基础数据(1-040-809-000) ==========
    ErrorCode PD_PRODUCT_NOT_EXISTS = new ErrorCode(1_040_809_000, "产品档案不存在");
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pd/processparamtemplate/MesPdProcessParamTemplateServiceImpl.java
@@ -56,9 +56,16 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createProcessParamTemplate(MesPdProcessParamTemplateSaveReqVO createReqVO) {
        // 1. 生成模板编码
        String templateCode = autoCodeRecordService.generateAutoCode(
                MesMdAutoCodeRuleCodeEnum.PD_PROCESS_PARAM_TEMPLATE_CODE.getCode());
        // 1. 使用手填编码,或自动生成编码
        String templateCode = createReqVO.getTemplateCode();
        if (StrUtil.isNotBlank(templateCode)) {
            if (templateMapper.selectByTemplateCode(templateCode) != null) {
                throw exception(PD_PROCESS_PARAM_TEMPLATE_CODE_DUPLICATE);
            }
        } else {
            templateCode = autoCodeRecordService.generateAutoCode(
                    MesMdAutoCodeRuleCodeEnum.PD_PROCESS_PARAM_TEMPLATE_CODE.getCode());
        }
        // 2. 插入模板主表
        MesPdProcessParamTemplateDO template = BeanUtils.toBean(createReqVO, MesPdProcessParamTemplateDO.class);
        template.setTemplateCode(templateCode);
@@ -72,9 +79,19 @@
    @Transactional(rollbackFor = Exception.class)
    public void updateProcessParamTemplate(MesPdProcessParamTemplateSaveReqVO updateReqVO) {
        // 1. 校验存在
        validateProcessParamTemplateExists(updateReqVO.getId());
        // 2. 更新模板主表
        MesPdProcessParamTemplateDO existing = validateProcessParamTemplateExists(updateReqVO.getId());
        // 2. 更新模板主表:编码留空或未变化则不修改;填写了新编码则校验唯一
        MesPdProcessParamTemplateDO updateObj = BeanUtils.toBean(updateReqVO, MesPdProcessParamTemplateDO.class);
        String templateCode = updateReqVO.getTemplateCode();
        if (StrUtil.isNotBlank(templateCode) && !StrUtil.equals(templateCode, existing.getTemplateCode())) {
            if (templateMapper.selectByTemplateCode(templateCode) != null) {
                throw exception(PD_PROCESS_PARAM_TEMPLATE_CODE_DUPLICATE);
            }
            updateObj.setTemplateCode(templateCode);
        } else {
            // 置空,避免 MyBatis-Plus 更新时覆盖原编码
            updateObj.setTemplateCode(null);
        }
        templateMapper.updateById(updateObj);
        // 3. 删除旧明细并重新插入(快照模式:先删后插)
        detailMapper.deleteByTemplateId(updateReqVO.getId());
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pd/project/MesPdProjectServiceImpl.java
@@ -217,6 +217,9 @@
        }
        // 2. 校验该项目下所有资料审核记录均已审核(不存在待审核的记录)
        List<MesPdDocumentDO> documents = documentService.getDocumentListByProjectId(id);
        if (CollUtil.isEmpty(documents)) {
            throw exception(PD_PROJECT_NO_DOCUMENT);
        }
        List<Long> documentIds = documents.stream().map(MesPdDocumentDO::getId).toList();
        if (documentAuditMapper.existsPendingAuditByDocumentIds(documentIds)) {
            throw exception(PD_PROJECT_AUDIT_NOT_FINISHED);
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pd/techconfirm/ProductTechConfirmServiceImpl.java
@@ -260,8 +260,9 @@
    }
    private void validateConfirmNoUnique(String confirmNo, Long excludeId) {
        if (confirmNo == null) {
            return;
        // 确认函编号必填:VO 上的 @NotBlank 只覆盖接口入参,Service 内(含导入)仍需显式校验
        if (StrUtil.isBlank(confirmNo)) {
            throw exception(PD_TECH_CONFIRM_NO_REQUIRED);
        }
        ProductTechConfirmDO techConfirm = techConfirmMapper.selectOne(
                ProductTechConfirmDO::getConfirmNo, confirmNo);
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java
@@ -235,7 +235,9 @@
    @Override
    public void rejectFeedback(Long id) {
        // 1. 校验存在 + 审批中状态
        validateFeedbackStatusApproving(id);
        MesProFeedbackDO feedback = validateFeedbackStatusApproving(id);
        // 1.1 校验当前登录人是该报工单的指定审核人
        validateCurrentUserIsApprover(feedback);
        // 2. 更新状态为草稿
        feedbackMapper.updateById(new MesProFeedbackDO().setId(id)
@@ -247,6 +249,8 @@
    public boolean approveFeedback(MesProFeedbackApproveReqVO reqVO) {
        // 1.1 校验存在 + 审批中状态
        MesProFeedbackDO feedback = validateFeedbackStatusApproving(reqVO.getId());
        // 1.1.1 校验当前登录人是该报工单的指定审核人
        validateCurrentUserIsApprover(feedback);
        // 1.2 校验报工数量 > 0
        if (feedback.getFeedbackQuantity() == null
                || feedback.getFeedbackQuantity().compareTo(BigDecimal.ZERO) <= 0) {
@@ -412,6 +416,23 @@
    }
    /**
     * 校验当前登录用户是否为报工单指定的审核人
     *
     * 报工单创建时必须指定 approveUserId,审批通过/驳回只能由该审核人操作,
     * 避免任意拥有菜单权限的用户越权审批。
     *
     * @param feedback 报工单
     */
    private void validateCurrentUserIsApprover(MesProFeedbackDO feedback) {
        if (feedback.getApproveUserId() == null) {
            throw exception(PRO_FEEDBACK_APPROVER_NOT_CONFIGURED);
        }
        if (!feedback.getApproveUserId().equals(getLoginUserId())) {
            throw exception(PRO_FEEDBACK_NOT_APPROVER);
        }
    }
    /**
     * 校验报工单的业务数据(创建 & 修改共用)
     *
     * <p>以生产任务为核心:前端只需传 taskId,workOrderId、routeId、processId、itemId、workstationId
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemreceipt/MesWmItemReceiptService.java
@@ -68,7 +68,10 @@
    void submitItemReceipt(Long id);
    /**
     * 执行上架(待上架 → 待入库)
     * 执行上架校验(仅校验已审批单据的明细数量一致性,不修改单据状态)
     *
     * 单据状态只能由审核(APPROVING → APPROVED)与入库(APPROVED → FINISHED)推进,
     * 本方法不得把单据直接置为已审批。
     *
     * @param id 编号
     */
@@ -89,7 +92,7 @@
    void cancelItemReceipt(Long id);
    /**
     * 校验采购入库单存在且处于可编辑状态(草稿或待上架)
     * 校验采购入库单存在且处于可编辑状态(仅草稿)
     *
     * @param id 编号
     * @return 采购入库单
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemreceipt/MesWmItemReceiptServiceImpl.java
@@ -265,9 +265,9 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void stockItemReceipt(Long id) {
        // 校验存在
        // 校验存在 + 已审批(状态只能由 audit 推进:APPROVING → APPROVED)
        MesWmItemReceiptDO receipt = validateItemReceiptExists(id);
        if (ObjUtil.notEqual(MesWmItemReceiptStatusEnum.APPROVING.getStatus(), receipt.getStatus())) {
        if (ObjUtil.notEqual(MesWmItemReceiptStatusEnum.APPROVED.getStatus(), receipt.getStatus())) {
            throw exception(WM_ITEM_RECEIPT_STATUS_ERROR);
        }
        // 校验每行明细数量之和是否等于行入库数量
@@ -283,10 +283,8 @@
                throw exception(WM_ITEM_RECEIPT_DETAIL_QUANTITY_MISMATCH);
            }
        }
        // 执行上架(待上架 → 待入库)
        itemReceiptMapper.updateById(new MesWmItemReceiptDO()
                .setId(id).setStatus(MesWmItemReceiptStatusEnum.APPROVED.getStatus()));
        // 注意:本方法仅做数量一致性校验,不得修改单据状态。
        // 状态流转由 audit(APPROVING → APPROVED)与 finish(APPROVED → FINISHED)负责。
    }
    @Override
@@ -408,12 +406,8 @@
    @Override
    public MesWmItemReceiptDO validateItemReceiptEditable(Long id) {
        MesWmItemReceiptDO receipt = validateItemReceiptExists(id);
        if (ObjUtil.notEqual(receipt.getStatus(), MesWmItemReceiptStatusEnum.PREPARE.getStatus())
                && ObjUtil.notEqual(receipt.getStatus(), MesWmItemReceiptStatusEnum.APPROVING.getStatus())) {
            throw exception(WM_ITEM_RECEIPT_STATUS_NOT_PREPARE);
        }
        return receipt;
        // 仅草稿状态允许编辑行/明细;审批中(APPROVING)已锁定,避免绕过审核修改数据
        return validateItemReceiptExistsAndDraft(id);
    }
    private MesWmItemReceiptDO validateItemReceiptExists(Long id) {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/miscissue/MesWmMiscIssueServiceImpl.java
@@ -27,6 +27,7 @@
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
@@ -198,12 +199,22 @@
        List<MesWmMiscIssueLineDO> lines = miscIssueLineService.getMiscIssueLineListByIssueId(issue.getId());
        wmTransactionService.createTransactionList(convertList(lines, line -> new MesWmTransactionSaveReqDTO()
                .setType(MesWmTransactionTypeEnum.OUT.getType()).setItemId(line.getItemId())
                .setQuantity(line.getQuantity().negate()) // 出库数量为负数
                .setQuantity(negateQuantity(line.getQuantity())) // 出库数量为负数
                .setBatchId(line.getBatchId()).setBatchCode(line.getBatchCode())
                .setWarehouseId(line.getWarehouseId()).setLocationId(line.getLocationId()).setAreaId(line.getAreaId())
                .setBizType(MesBizTypeConstants.WM_MISC_ISSUE).setBizId(issue.getId()).setBizCode(issue.getCode()).setBizLineId(line.getId())));
    }
    /**
     * 出库数量取负:数量为空时抛出明确的业务异常,避免 NPE
     */
    private BigDecimal negateQuantity(BigDecimal quantity) {
        if (quantity == null) {
            throw exception(WM_MISC_ISSUE_QUANTITY_INVALID);
        }
        return quantity.negate();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void cancelMiscIssue(Long id) {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/miscreceipt/MesWmMiscReceiptServiceImpl.java
@@ -195,6 +195,7 @@
        wmTransactionService.createTransactionList(convertList(lines, line -> new MesWmTransactionSaveReqDTO()
                .setType(MesWmTransactionTypeEnum.IN.getType()).setItemId(line.getItemId())
                .setQuantity(line.getQuantity()) // 入库数量为正数
                .setBatchCode(line.getBatchCode()) // 批次管理物料需携带批次号
                .setWarehouseId(line.getWarehouseId()).setLocationId(line.getLocationId()).setAreaId(line.getAreaId())
                .setBizType(MesBizTypeConstants.WM_MISC_RECPT).setBizId(receipt.getId()).setBizCode(receipt.getCode()).setBizLineId(line.getId())));
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnvendor/MesWmReturnVendorDetailServiceImpl.java
@@ -32,6 +32,10 @@
    @Resource
    @Lazy
    private MesWmReturnVendorLineService returnVendorLineService;
    // 与 ReturnVendorServiceImpl 相互引用,须用 @Lazy 打破循环
    @Resource
    @Lazy
    private MesWmReturnVendorService returnVendorService;
    @Resource
    private MesMdmItemService mdmItemService;
    @Resource
@@ -66,6 +70,8 @@
     * 校验明细创建/修改的公共数据
     */
    private void validateDetailSaveData(MesWmReturnVendorDetailSaveReqVO reqVO) {
        // 校验父单(退货单)处于准备中状态,审批期间及之后禁止增删改明细
        returnVendorService.validateReturnVendorExistsAndPrepare(reqVO.getReturnId());
        // 校验父数据(行)存在
        MesWmReturnVendorLineDO line = returnVendorLineService.validateReturnVendorLineExists(reqVO.getLineId());
        if (ObjUtil.notEqual(line.getReturnId(), reqVO.getReturnId())) {
@@ -85,7 +91,9 @@
    @Override
    public void deleteReturnVendorDetail(Long id) {
        // 校验存在
        validateReturnVendorDetailExists(id);
        MesWmReturnVendorDetailDO detail = validateReturnVendorDetailExists(id);
        // 校验父单处于准备中状态,审批期间及之后禁止删除明细
        returnVendorService.validateReturnVendorExistsAndPrepare(detail.getReturnId());
        // 删除
        returnVendorDetailMapper.deleteById(id);
    }
@@ -115,10 +123,12 @@
        returnVendorDetailMapper.deleteByLineId(lineId);
    }
    private void validateReturnVendorDetailExists(Long id) {
        if (returnVendorDetailMapper.selectById(id) == null) {
    private MesWmReturnVendorDetailDO validateReturnVendorDetailExists(Long id) {
        MesWmReturnVendorDetailDO detail = returnVendorDetailMapper.selectById(id);
        if (detail == null) {
            throw exception(WM_RETURN_VENDOR_DETAIL_NOT_EXISTS);
        }
        return detail;
    }
    /**
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnvendor/MesWmReturnVendorService.java
@@ -95,7 +95,9 @@
    void auditReturnVendor(Long id, Boolean pass, String reviewRemark);
    /**
     * 执行拣货(待拣货 -> 待执行退货)
     * 执行拣货校验(仅校验已审批单据的明细数量一致性,不修改单据状态)
     *
     * 单据状态只能由审核(APPROVING → APPROVED)与退货(APPROVED → FINISHED)推进。
     *
     * @param id 编号
     */
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnvendor/MesWmReturnVendorServiceImpl.java
@@ -185,14 +185,15 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void stockReturnVendor(Long id) {
        // 校验存在
        // 校验存在 + 已审批(状态只能由 audit 推进:APPROVING → APPROVED)
        MesWmReturnVendorDO returnVendor = validateReturnVendorExists(id);
        if (ObjUtil.notEqual(MesWmReturnVendorStatusEnum.APPROVING.getStatus(), returnVendor.getStatus())) {
        if (ObjUtil.notEqual(MesWmReturnVendorStatusEnum.APPROVED.getStatus(), returnVendor.getStatus())) {
            throw exception(WM_RETURN_VENDOR_STATUS_INVALID);
        }
        // 执行拣货(待拣货 -> 待执行退货)
        returnVendorMapper.updateById(new MesWmReturnVendorDO()
                .setId(id).setStatus(MesWmReturnVendorStatusEnum.APPROVED.getStatus()));
        // 仅校验行数量与明细数量一致,不修改单据状态
        if (!Boolean.TRUE.equals(checkReturnVendorQuantity(id))) {
            throw exception(WM_RETURN_VENDOR_DETAIL_QUANTITY_MISMATCH);
        }
    }
    @Override
@@ -208,6 +209,10 @@
        if (CollUtil.isEmpty(details)) {
            throw exception(WM_RETURN_VENDOR_NO_DETAIL);
        }
        // 1.3 校验每行明细数量之和等于行退货数量,避免出库数量与单据不一致
        if (!Boolean.TRUE.equals(checkReturnVendorQuantity(id))) {
            throw exception(WM_RETURN_VENDOR_DETAIL_QUANTITY_MISMATCH);
        }
        // 2. 遍历所有明细,创建库存事务(扣减库存 + 记录流水)
        createTransactionList(returnVendor, details);
@@ -220,7 +225,7 @@
    private void createTransactionList(MesWmReturnVendorDO returnVendor, List<MesWmReturnVendorDetailDO> details) {
        wmTransactionService.createTransactionList(convertList(details, detail -> new MesWmTransactionSaveReqDTO()
                .setType(MesWmTransactionTypeEnum.OUT.getType()).setItemId(detail.getItemId())
                .setQuantity(detail.getQuantity().negate()) // 出库数量为负数
                .setQuantity(negateQuantity(detail.getQuantity())) // 出库数量为负数
                .setBatchId(detail.getBatchId()).setBatchCode(detail.getBatchCode())
                .setWarehouseId(detail.getWarehouseId()).setLocationId(detail.getLocationId()).setAreaId(detail.getAreaId())
                .setVendorId(returnVendor.getVendorId())
@@ -228,6 +233,16 @@
                .setBizCode(returnVendor.getCode()).setBizLineId(detail.getLineId())));
    }
    /**
     * 出库数量取负:数量为空时抛出明确的业务异常,避免 NPE
     */
    private BigDecimal negateQuantity(BigDecimal quantity) {
        if (quantity == null) {
            throw exception(WM_RETURN_VENDOR_DETAIL_QUANTITY_INVALID);
        }
        return quantity.negate();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void cancelReturnVendor(Long id) {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/stocktaking/task/MesWmStockTakingTaskLineServiceImpl.java
@@ -252,6 +252,8 @@
        }
        // 0. 校验盘点任务处于盘点中状态(与人工录入盘点结果口径一致,仅盘点中可导入)
        stockTakingTaskService.validateStockTakingTaskExistsAndApproving(taskId);
        // 0.1 结果审批已提交且未审核时,盘点结果已锁定,禁止再导入实盘数量
        stockTakingTaskService.validateResultApprovalNotPending(taskId);
        // 1. 获取盘点任务行
        List<MesWmStockTakingTaskLineDO> lines = stockTakingTaskLineMapper.selectListByTaskId(taskId);
        if (CollUtil.isEmpty(lines)) {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/stocktaking/task/MesWmStockTakingTaskResultServiceImpl.java
@@ -19,6 +19,7 @@
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.WM_STOCK_TAKING_TASK_LINE_ALREADY_TAKEN;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.WM_STOCK_TAKING_TASK_LINE_NOT_MATCH;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.WM_STOCK_TAKING_TASK_RESULT_NOT_EXISTS;
/**
@@ -52,8 +53,9 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createStockTakingTaskResult(MesWmStockTakingTaskResultSaveReqVO createReqVO) {
        // 1. 校验任务处于盘点中状态
        // 1. 校验任务处于盘点中状态,且结果审批已提交、未审核时禁止写入
        stockTakingTaskService.validateStockTakingTaskExistsAndApproving(createReqVO.getTaskId());
        stockTakingTaskService.validateResultApprovalNotPending(createReqVO.getTaskId());
        // 2. 确定 lineId 和账面数量
        Long lineId = createReqVO.getLineId();
@@ -71,10 +73,16 @@
        } else {
            // 2.3 如果传递了 lineId,从 line 中获取 quantity
            existingLine = stockTakingTaskLineService.validateStockTakingTaskLineExists(lineId);
            // 2.4 防止串单:盘点行必须属于请求指定的盘点任务
            if (ObjUtil.notEqual(existingLine.getTaskId(), createReqVO.getTaskId())) {
                throw exception(WM_STOCK_TAKING_TASK_LINE_NOT_MATCH);
            }
        }
        if (existingLine != null
                && existingLine.getTakingQuantity() != null
                && existingLine.getTakingQuantity().compareTo(BigDecimal.ZERO) > 0) {
        // 2.5 防止重复:同一盘点任务行只允许存在一条盘点结果(无论实盘数量是否为 0)
        Long finalLineId = lineId;
        boolean resultExists = stockTakingTaskResultMapper.selectListByTaskId(createReqVO.getTaskId()).stream()
                .anyMatch(result -> Objects.equals(result.getLineId(), finalLineId));
        if (resultExists) {
            throw exception(WM_STOCK_TAKING_TASK_LINE_ALREADY_TAKEN);
        }
        BigDecimal quantity = existingLine != null ? existingLine.getQuantity() : BigDecimal.ZERO; // 账面数量
@@ -95,8 +103,9 @@
        updateReqVO.setLineId(null); // lineId 不能修改,避免误操作
        // 1.1 校验记录存在
        MesWmStockTakingTaskResultDO oldResult = validateStockTakingTaskResultExists(updateReqVO.getId());
        // 1.2 校验任务处于盘点中状态
        // 1.2 校验任务处于盘点中状态,且结果审批已提交、未审核时禁止修改
        stockTakingTaskService.validateStockTakingTaskExistsAndApproving(oldResult.getTaskId());
        stockTakingTaskService.validateResultApprovalNotPending(oldResult.getTaskId());
        // 2. 更新 result 记录
        MesWmStockTakingTaskResultDO result = BeanUtils.toBean(updateReqVO, MesWmStockTakingTaskResultDO.class);
@@ -112,15 +121,21 @@
    public void deleteStockTakingTaskResult(Long id) {
        // 1.1 校验 result 是否存在
        MesWmStockTakingTaskResultDO result = validateStockTakingTaskResultExists(id);
        // 1.2 校验 task 是否处于 APPROVING 状态
        // 1.2 校验 task 是否处于 APPROVING 状态,且结果审批已提交、未审核时禁止删除
        stockTakingTaskService.validateStockTakingTaskExistsAndApproving(result.getTaskId());
        stockTakingTaskService.validateResultApprovalNotPending(result.getTaskId());
        // 2. 删除 result 记录
        stockTakingTaskResultMapper.deleteById(id);
        // 3. 更新对应 line 的盘点数量和状态
        // 3. 按剩余盘点结果重算对应 line 的实盘数量(无剩余结果时置 0),避免覆盖成负数
        BigDecimal remainingTakingQuantity = stockTakingTaskResultMapper.selectListByTaskId(result.getTaskId()).stream()
                .filter(item -> Objects.equals(item.getLineId(), result.getLineId()))
                .map(MesWmStockTakingTaskResultDO::getTakingQuantity)
                .filter(Objects::nonNull)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        stockTakingTaskLineService.updateStockTakingTaskLineTakingQuantity(
                result.getLineId(), result.getTakingQuantity().negate());
                result.getLineId(), remainingTakingQuantity);
    }
    @Override
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/stocktaking/task/MesWmStockTakingTaskService.java
@@ -135,6 +135,15 @@
    MesWmStockTakingTaskDO validateStockTakingTaskExistsAndApproving(Long id);
    /**
     * 校验盘点任务的结果审批未处于「已提交、待审核」状态
     *
     * 该阶段盘点结果已被锁定,禁止新增、修改、删除盘点结果或导入实盘数量。
     *
     * @param id 盘点任务编号
     */
    void validateResultApprovalNotPending(Long id);
    /**
     * 分页查询盘点任务
     *
     * @param pageReqVO 分页查询条件
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/stocktaking/task/MesWmStockTakingTaskServiceImpl.java
@@ -276,6 +276,14 @@
        return task.getReviewerId() == null && task.getReviewTime() != null;
    }
    @Override
    public void validateResultApprovalNotPending(Long id) {
        MesWmStockTakingTaskDO task = validateStockTakingTaskExists(id);
        if (isResultApprovalPending(task)) {
            throw exception(WM_STOCK_TAKING_TASK_RESULT_APPROVAL_PENDING);
        }
    }
    /**
     * 获取用户昵称
     *
yudao-server/src/main/resources/application-local.yaml
@@ -65,17 +65,17 @@
      primary: master
      datasource:
        master:
          url: jdbc:mysql://localhost:3306/mom-xgdl?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例
          url: jdbc:mysql://192.168.197.128:3306/mom-xgdl?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例
          username: root
          password: 123456
          password: sugaozhixing.cn
  # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
  data:
    redis:
      host: localhost # 地址
      host: 192.168.197.128 # 地址
      port: 6379 # 端口
      database: 0 # 数据库索引
#      password: 123456
      password: sugaozhixing.cn
--- #################### 定时任务相关配置 ####################
@@ -193,7 +193,7 @@
  storage: # 文件上传配置
    path: D:/uploads # 文件存储根目录(按实际服务器路径配置)
    url-prefix: /admin-api/system/storage-blob # URL路径前缀
    domain: http://192.168.0.10:48080 # 域名前缀(按实际域名配置)
    domain: http://127.0.0.1:48080 # 域名前缀(按实际域名配置)
    expired: 120 # token过期时间(分钟)
    use-limit: 10 # token使用次数上限
    jwt-secret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG # JWT密钥(至少32字符)