2026-08-10 9fceeaad9af96bc58ba714560814c6e94f8af406
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableServiceImpl.java
@@ -14,6 +14,7 @@
import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable.CrmReceivablePageReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable.CrmReceivableSaveReqVO;
import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.invoice.CrmInvoiceDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.CrmReceivableDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.CrmReceivablePlanDO;
import cn.iocoder.yudao.module.crm.dal.mysql.receivable.CrmReceivableMapper;
@@ -23,9 +24,12 @@
import cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionLevelEnum;
import cn.iocoder.yudao.module.crm.framework.permission.core.annotations.CrmPermission;
import cn.iocoder.yudao.module.crm.service.contract.CrmContractService;
import cn.iocoder.yudao.module.crm.service.invoice.CrmInvoiceService;
import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.enums.storage.StorageRecordTypeEnum;
import com.mzt.logapi.context.LogRecordContext;
import com.mzt.logapi.service.impl.DiffParseFunction;
import com.mzt.logapi.starter.annotation.LogRecord;
@@ -72,22 +76,30 @@
    private CrmReceivablePlanService receivablePlanService;
    @Resource
    private CrmPermissionService permissionService;
    @Resource
    private CrmInvoiceService invoiceService;
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private BpmProcessInstanceApi bpmProcessInstanceApi;
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @Override
    @Transactional(rollbackFor = Exception.class)
    @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 校验关联数据存在
        // 1.1 校验关联数据存在
        validateRelationDataExists(createReqVO);
        // 1.3 生成回款编号
        // 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);
@@ -118,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()));
        }
@@ -128,6 +142,56 @@
        // 2. 校验金额是否超过
        if (reqVO.getPrice().compareTo(notReceivablePrice) > 0) {
            throw exception(RECEIVABLE_CREATE_FAIL_PRICE_EXCEEDS_LIMIT, notReceivablePrice);
        }
    }
    /**
     * 校验关联开票合格:存在、审批通过、已上传发票附件、与所选合同一致
     */
    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. 校验本次回款金额不超过该开票剩余可回款金额(开票金额 - 已回款金额)
        BigDecimal availablePrice = invoice.getPrice().subtract(receivablePriceSum);
        if (reqVO.getPrice().compareTo(availablePrice) > 0) {
            throw exception(RECEIVABLE_CREATE_FAIL_INVOICE_PRICE_EXCEEDS, availablePrice);
        }
    }
@@ -160,15 +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.4 校验关联开票合格
        validateInvoiceForReceivable(updateReqVO);
        // 1.5 校验可回款金额不超过该开票剩余可回款金额
        validateReceivablePriceExceedsInvoiceRemaining(updateReqVO);
        // 1.3 只有草稿、审批中,可以编辑;
        // 1.6 只有草稿、审批中,可以编辑;
        if (!ObjectUtils.equalsAny(oldReceivable.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus(),
                CrmAuditStatusEnum.PROCESS.getStatus())) {
            throw exception(RECEIVABLE_UPDATE_FAIL_EDITING_PROHIBITED);
@@ -303,8 +375,30 @@
    }
    @Override
    public Map<Long, BigDecimal> getReceivablePriceMapByInvoiceId(Collection<Long> invoiceIds) {
        return receivableMapper.selectReceivablePriceMapByInvoiceId(invoiceIds);
    }
    @Override
    public Long getReceivableCountByContractId(Long contractId) {
        return receivableMapper.selectCountByContractId(contractId);
    }
    @Override
    public BigDecimal getReceivablePriceByContractId(Long contractId) {
        Map<Long, BigDecimal> priceMap = receivableMapper.selectReceivablePriceMapByContractId(
                java.util.Collections.singleton(contractId));
        return priceMap.getOrDefault(contractId, BigDecimal.ZERO);
    }
    @Override
    public BigDecimal getReceivablePriceSum() {
        return receivableMapper.selectReceivablePriceSum();
    }
    @Override
    public BigDecimal getReceivablePriceByOwnerUserId(Long ownerUserId) {
        return receivableMapper.selectReceivablePriceSumByOwnerUserId(ownerUserId);
    }
}