2026-08-10 9fceeaad9af96bc58ba714560814c6e94f8af406
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableServiceImpl.java
@@ -91,13 +91,15 @@
    @LogRecord(type = CRM_RECEIVABLE_TYPE, subType = CRM_RECEIVABLE_CREATE_SUB_TYPE, bizNo = "{{#receivable.id}}",
            success = CRM_RECEIVABLE_CREATE_SUCCESS)
    public Long createReceivable(CrmReceivableSaveReqVO createReqVO) {
        // 1.1 校验可回款金额超过上限
        validateReceivablePriceExceedsLimit(createReqVO);
        // 1.2 校验可回款金额不超过累计已开票金额(先开票后回款)
        validateReceivablePriceExceedsInvoiceLimit(createReqVO);
        // 1.3 校验关联数据存在
        // 1.1 校验关联数据存在
        validateRelationDataExists(createReqVO);
        // 1.4 生成回款编号
        // 1.2 校验关联开票合格(审批通过 + 已上传发票附件 + 与合同匹配)
        validateInvoiceForReceivable(createReqVO);
        // 1.3 校验可回款金额不超过合同剩余可回款金额
        validateReceivablePriceExceedsLimit(createReqVO);
        // 1.4 校验可回款金额不超过该开票剩余可回款金额(先开票后回款)
        validateReceivablePriceExceedsInvoiceRemaining(createReqVO);
        // 1.5 生成回款编号
        String no = noRedisDAO.generate(CrmNoRedisDAO.RECEIVABLE_PREFIX);
        if (receivableMapper.selectByNo(no) != null) {
            throw exception(RECEIVABLE_NO_EXISTS);
@@ -128,8 +130,10 @@
    private void validateReceivablePriceExceedsLimit(CrmReceivableSaveReqVO reqVO) {
        // 1. 计算剩余可退款金额,不包括 reqVO 自身
        CrmContractDO contract = contractService.validateContract(reqVO.getContractId());
        // 统计口径:草稿 + 审批中 + 审批通过(与开票级校验、统计方法 selectReceivablePriceMapByContractId 保持一致)
        List<CrmReceivableDO> receivables = receivableMapper.selectListByContractIdAndStatus(reqVO.getContractId(),
                Arrays.asList(CrmAuditStatusEnum.APPROVE.getStatus(), CrmAuditStatusEnum.PROCESS.getStatus()));
                Arrays.asList(CrmAuditStatusEnum.DRAFT.getStatus(), CrmAuditStatusEnum.PROCESS.getStatus(),
                        CrmAuditStatusEnum.APPROVE.getStatus()));
        if (reqVO.getId() != null) {
            receivables.removeIf(receivable -> ObjectUtil.equal(receivable.getId(), reqVO.getId()));
        }
@@ -142,33 +146,52 @@
    }
    /**
     * 校验累计回款金额不超过累计已开票且已上传发票附件的金额(先开票后回款)
     * 校验关联开票合格:存在、审批通过、已上传发票附件、与所选合同一致
     */
    private void validateReceivablePriceExceedsInvoiceLimit(CrmReceivableSaveReqVO reqVO) {
        // 1. 计算合同累计回款金额(不含本次,统计口径同合同金额校验:审批通过 + 审批中)
        List<CrmReceivableDO> receivables = receivableMapper.selectListByContractIdAndStatus(reqVO.getContractId(),
                Arrays.asList(CrmAuditStatusEnum.APPROVE.getStatus(), CrmAuditStatusEnum.PROCESS.getStatus()));
    private void validateInvoiceForReceivable(CrmReceivableSaveReqVO reqVO) {
        // 1. 校验开票存在
        CrmInvoiceDO invoice = invoiceService.getInvoice(reqVO.getInvoiceId());
        if (invoice == null) {
            throw exception(RECEIVABLE_CREATE_FAIL_INVOICE_NOT_EXISTS);
        }
        // 2. 校验开票审批通过
        if (ObjectUtil.notEqual(invoice.getAuditStatus(), CrmAuditStatusEnum.APPROVE.getStatus())) {
            throw exception(RECEIVABLE_CREATE_FAIL_INVOICE_NOT_APPROVE);
        }
        // 3. 校验开票已上传发票附件
        if (CollUtil.isEmpty(storageAttachmentApi.listAttachments(
                StorageRecordTypeEnum.CRM_INVOICE.getType(), invoice.getId()))) {
            throw exception(RECEIVABLE_CREATE_FAIL_INVOICE_NOT_ATTACHMENT);
        }
        // 4. 校验开票与所选合同一致
        if (ObjectUtil.notEqual(invoice.getContractId(), reqVO.getContractId())) {
            throw exception(RECEIVABLE_CREATE_FAIL_INVOICE_CONTRACT_MISMATCH);
        }
        // 5. 校验开票与所选合同客户一致(防御脏数据:合同-开票-回款一对多对多,开票必须属于该合同客户)
        if (reqVO.getCustomerId() != null && ObjectUtil.notEqual(invoice.getCustomerId(), reqVO.getCustomerId())) {
            throw exception(RECEIVABLE_CREATE_FAIL_INVOICE_CONTRACT_MISMATCH);
        }
    }
    /**
     * 校验累计回款金额不超过该开票剩余可回款金额(先开票后回款)
     */
    private void validateReceivablePriceExceedsInvoiceRemaining(CrmReceivableSaveReqVO reqVO) {
        // 1. 计算该开票累计回款金额(不含本次,统计口径同合同金额校验:草稿 + 审批中 + 审批通过)
        CrmInvoiceDO invoice = invoiceService.getInvoice(reqVO.getInvoiceId());
        List<CrmReceivableDO> receivables = receivableMapper.selectListByInvoiceIdAndStatus(reqVO.getInvoiceId(),
                Arrays.asList(CrmAuditStatusEnum.DRAFT.getStatus(), CrmAuditStatusEnum.PROCESS.getStatus(),
                        CrmAuditStatusEnum.APPROVE.getStatus()));
        if (reqVO.getId() != null) {
            receivables.removeIf(receivable -> ObjectUtil.equal(receivable.getId(), reqVO.getId()));
        }
        BigDecimal receivablePriceSum = CollectionUtils.getSumValue(receivables, CrmReceivableDO::getPrice,
                BigDecimal::add, BigDecimal.ZERO);
        // 2. 计算合同下「审批通过且已上传发票附件」的累计开票金额
        List<CrmInvoiceDO> invoices = invoiceService.getInvoiceListByContractIdAndStatus(reqVO.getContractId(),
                List.of(CrmAuditStatusEnum.APPROVE.getStatus()));
        BigDecimal invoicePriceSum = BigDecimal.ZERO;
        for (CrmInvoiceDO invoice : invoices) {
            if (CollUtil.isNotEmpty(storageAttachmentApi.listAttachments(
                    StorageRecordTypeEnum.CRM_INVOICE.getType(), invoice.getId()))) {
                invoicePriceSum = invoicePriceSum.add(invoice.getPrice());
            }
        }
        // 3. 校验本次回款金额不超过剩余可回款金额(已开票金额 - 已回款金额)
        BigDecimal availablePrice = invoicePriceSum.subtract(receivablePriceSum);
        // 2. 校验本次回款金额不超过该开票剩余可回款金额(开票金额 - 已回款金额)
        BigDecimal availablePrice = invoice.getPrice().subtract(receivablePriceSum);
        if (reqVO.getPrice().compareTo(availablePrice) > 0) {
            throw exception(RECEIVABLE_CREATE_FAIL_INVOICE_NOT_ENOUGH, availablePrice);
            throw exception(RECEIVABLE_CREATE_FAIL_INVOICE_PRICE_EXCEEDS, availablePrice);
        }
    }
@@ -201,17 +224,23 @@
    @CrmPermission(bizType = CrmBizTypeEnum.CRM_RECEIVABLE, bizId = "#updateReqVO.id", level = CrmPermissionLevelEnum.WRITE)
    public void updateReceivable(CrmReceivableSaveReqVO updateReqVO) {
        Assert.notNull(updateReqVO.getId(), "回款编号不能为空");
        updateReqVO.setOwnerUserId(null).setCustomerId(null).setContractId(null).setPlanId(null); // 不允许修改的字段
        updateReqVO.setOwnerUserId(null).setCustomerId(null).setContractId(null).setPlanId(null); // 不允许修改的字段(invoiceId 允许修改)
        // 1.1 校验存在
        CrmReceivableDO oldReceivable = validateReceivableExists(updateReqVO.getId());
        updateReqVO.setOwnerUserId(oldReceivable.getOwnerUserId()).setCustomerId(oldReceivable.getCustomerId())
                .setContractId(oldReceivable.getContractId()).setPlanId(oldReceivable.getPlanId()); // 设置已存在的值
        // 1.2 校验可回款金额超过上限
        // 1.2 若未传递关联开票,沿用旧值
        if (updateReqVO.getInvoiceId() == null) {
            updateReqVO.setInvoiceId(oldReceivable.getInvoiceId());
        }
        // 1.3 校验可回款金额不超过合同剩余可回款金额
        validateReceivablePriceExceedsLimit(updateReqVO);
        // 1.3 校验可回款金额不超过累计已开票金额(先开票后回款)
        validateReceivablePriceExceedsInvoiceLimit(updateReqVO);
        // 1.4 校验关联开票合格
        validateInvoiceForReceivable(updateReqVO);
        // 1.5 校验可回款金额不超过该开票剩余可回款金额
        validateReceivablePriceExceedsInvoiceRemaining(updateReqVO);
        // 1.4 只有草稿、审批中,可以编辑;
        // 1.6 只有草稿、审批中,可以编辑;
        if (!ObjectUtils.equalsAny(oldReceivable.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus(),
                CrmAuditStatusEnum.PROCESS.getStatus())) {
            throw exception(RECEIVABLE_UPDATE_FAIL_EDITING_PROHIBITED);
@@ -346,6 +375,11 @@
    }
    @Override
    public Map<Long, BigDecimal> getReceivablePriceMapByInvoiceId(Collection<Long> invoiceIds) {
        return receivableMapper.selectReceivablePriceMapByInvoiceId(invoiceIds);
    }
    @Override
    public Long getReceivableCountByContractId(Long contractId) {
        return receivableMapper.selectCountByContractId(contractId);
    }