云
5 天以前 a82d847bbb39e2417d4c9773ebc4fc51fdb0b8fc
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/bidbond/CrmBidBondServiceImpl.java
@@ -11,10 +11,13 @@
import cn.iocoder.yudao.module.crm.controller.admin.bidbond.vo.CrmBidBondSaveReqVO;
import cn.iocoder.yudao.module.crm.dal.dataobject.bidbond.CrmBidBondDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.quotation.CrmSaleQuotationDO;
import cn.iocoder.yudao.module.crm.dal.mysql.bidbond.CrmBidBondMapper;
import cn.iocoder.yudao.module.crm.dal.mysql.quotation.CrmSaleQuotationMapper;
import cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants;
import cn.iocoder.yudao.module.crm.enums.bidbond.CrmBidBondRefundStatusEnum;
import cn.iocoder.yudao.module.crm.enums.bidbond.CrmBidResultEnum;
import cn.iocoder.yudao.module.crm.enums.quotation.CrmSaleQuotationStatusEnum;
import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
import cn.iocoder.yudao.module.erp.api.sale.ErpSaleOrderApi;
import cn.iocoder.yudao.module.erp.api.sale.dto.ErpSaleOrderRespDTO;
@@ -48,6 +51,8 @@
    @Resource
    private CrmBidBondMapper bidBondMapper;
    @Resource
    private CrmSaleQuotationMapper saleQuotationMapper;
    @Resource
    private CrmCustomerService customerService;
    @Resource
    private ErpSaleOrderApi erpSaleOrderApi;
@@ -59,23 +64,42 @@
        bond.setId(null);
        bond.setNo(generateBondNo());
        bidBondMapper.insert(bond);
        // 回写报价单绑定关系
        if (bond.getQuotationId() != null) {
            saleQuotationMapper.bindBidBond(bond.getQuotationId(), bond.getId(), bond.getNo());
        }
        return bond.getId();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateBidBond(CrmBidBondSaveReqVO updateReqVO) {
        validateBidBondExists(updateReqVO.getId());
        CrmBidBondDO oldBond = validateBidBondExists(updateReqVO.getId());
        CrmBidBondDO updateObj = validateAndBuild(updateReqVO);
        updateObj.setId(updateReqVO.getId());
        // 单号不允许修改,保持原值
        updateObj.setNo(null);
        bidBondMapper.updateByIdIncludingNulls(updateObj);
        // 报价单变化时:先解绑旧报价单,再绑定新报价单
        if (oldBond.getQuotationId() != null && !oldBond.getQuotationId().equals(updateObj.getQuotationId())) {
            saleQuotationMapper.bindBidBond(oldBond.getQuotationId(), null, null);
        }
        if (updateObj.getQuotationId() != null && !updateObj.getQuotationId().equals(oldBond.getQuotationId())) {
            saleQuotationMapper.bindBidBond(updateObj.getQuotationId(), updateObj.getId(), oldBond.getNo());
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteBidBond(Long id) {
        validateBidBondExists(id);
        CrmBidBondDO bond = validateBidBondExists(id);
        if (CrmBidResultEnum.WON.getResult().equals(bond.getBidResult()) && bond.getQuotationId() != null) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.BID_BOND_DELETE_FAIL_WON);
        }
        // 解除报价单绑定后再删除
        if (bond.getQuotationId() != null) {
            saleQuotationMapper.bindBidBond(bond.getQuotationId(), null, null);
        }
        bidBondMapper.deleteById(id);
    }
@@ -177,7 +201,10 @@
            saleOrderNo = order.getNo();
        }
        // 5. 退回状态与金额
        // 5. 关联报价单
        CrmSaleQuotationDO quotation = validateQuotation(reqVO, bidResult);
        // 6. 退回状态与金额
        Integer refundStatus = resolveRefundStatus(reqVO.getRefundStatus(), bidResult);
        validateRefund(reqVO, bondAmount, refundStatus);
@@ -186,7 +213,53 @@
        bond.setBidResult(bidResult);
        bond.setRefundStatus(refundStatus);
        bond.setSaleOrderNo(saleOrderNo);
        bond.setQuotationNo(quotation != null ? quotation.getNo() : null);
        bond.setQuotationRound(quotation != null && quotation.getRevisionNo() != null
                ? quotation.getRevisionNo() : 0);
        return bond;
    }
    /**
     * 校验并加载关联报价单
     */
    private CrmSaleQuotationDO validateQuotation(CrmBidBondSaveReqVO reqVO, Integer bidResult) {
        // 中标时报价单必填,保证中标后可以据此转合同
        if (CrmBidResultEnum.WON.getResult().equals(bidResult) && reqVO.getQuotationId() == null) {
            throw new ServiceException(ErrorCodeConstants.BID_BOND_WON_QUOTATION_REQUIRED.getCode(),
                    "中标结果为【已中标】时必须关联报价单,否则无法凭该报价单转合同。"
                            + "请选择本次中标的销售报价单后重新保存。");
        }
        if (reqVO.getQuotationId() == null) {
            return null;
        }
        CrmSaleQuotationDO quotation = saleQuotationMapper.selectById(reqVO.getQuotationId());
        if (quotation == null) {
            throw new ServiceException(ErrorCodeConstants.BID_BOND_QUOTATION_NOT_EXISTS.getCode(),
                    "关联报价单(ID=" + reqVO.getQuotationId() + ")不存在,请刷新报价单列表后重新选择。");
        }
        if (!Objects.equals(reqVO.getCustomerId(), quotation.getCustomerId())) {
            throw new ServiceException(ErrorCodeConstants.BID_BOND_QUOTATION_CUSTOMER_MISMATCH.getCode(),
                    "关联报价单[" + quotation.getNo() + "]的客户(ID=" + quotation.getCustomerId()
                            + ")与保证金客户(ID=" + reqVO.getCustomerId()
                            + ")不一致。请改选该客户名下的报价单,或先调整保证金客户。");
        }
        if (!Set.of(CrmSaleQuotationStatusEnum.DRAFT.getStatus(),
                CrmSaleQuotationStatusEnum.APPROVE.getStatus(),
                CrmSaleQuotationStatusEnum.REJECT.getStatus()).contains(quotation.getStatus())) {
            throw new ServiceException(ErrorCodeConstants.BID_BOND_QUOTATION_STATUS_INVALID.getCode(),
                    "关联报价单[" + quotation.getNo() + "]当前状态为【"
                            + getQuotationStatusName(quotation.getStatus())
                            + "】,只有「草稿、审批通过、审批不通过」的报价单可以绑定投标。"
                            + "请改选其他报价单,或先重新报价后再绑定。");
        }
        if (quotation.getBidBondId() != null && !Objects.equals(quotation.getBidBondId(), reqVO.getId())) {
            CrmBidBondDO boundBond = bidBondMapper.selectById(quotation.getBidBondId());
            throw new ServiceException(ErrorCodeConstants.BID_BOND_QUOTATION_ALREADY_BOUND.getCode(),
                    "关联报价单[" + quotation.getNo() + "]已绑定投标["
                            + (boundBond != null ? boundBond.getNo() : "ID=" + quotation.getBidBondId())
                            + "],同一报价单只能绑定一张投标。请改选其他报价单,或先在该投标中解除关联。");
        }
        return quotation;
    }
    private Integer resolveRefundStatus(Integer refundStatus, Integer bidResult) {
@@ -281,6 +354,7 @@
        Map<Long, CrmCustomerDO> customerMap = customerIds.isEmpty() ? Map.of()
                : customerService.getCustomerMap(customerIds);
        Map<Long, ErpSaleOrderRespDTO> orderMap = loadSaleOrderMap(bonds);
        Map<Long, CrmSaleQuotationDO> quotationMap = loadQuotationMap(bonds);
        return bonds.stream().map(bond -> {
            CrmBidBondRespVO respVO = BeanUtils.toBean(bond, CrmBidBondRespVO.class);
@@ -288,13 +362,30 @@
            if (customer != null) {
                respVO.setCustomerName(customer.getName());
            }
            ErpSaleOrderRespDTO order = orderMap.get(bond.getSaleOrderId());
            ErpSaleOrderRespDTO order = bond.getSaleOrderId() == null ? null : orderMap.get(bond.getSaleOrderId());
            if (order != null) {
                respVO.setSaleOrderNo(order.getNo());
                respVO.setSaleOrderAmount(order.getTotalPrice());
            }
            CrmSaleQuotationDO quotation = bond.getQuotationId() == null ? null
                    : quotationMap.get(bond.getQuotationId());
            if (quotation != null) {
                respVO.setQuotationNo(quotation.getNo());
                respVO.setQuotationStatus(quotation.getStatus());
                respVO.setQuotationTotalPrice(quotation.getTotalPrice());
            }
            return respVO;
        }).collect(Collectors.toList());
    }
    private Map<Long, CrmSaleQuotationDO> loadQuotationMap(List<CrmBidBondDO> bonds) {
        Set<Long> quotationIds = bonds.stream().map(CrmBidBondDO::getQuotationId)
                .filter(Objects::nonNull).collect(Collectors.toSet());
        if (quotationIds.isEmpty()) {
            return Map.of();
        }
        return saleQuotationMapper.selectByIds(quotationIds).stream()
                .collect(Collectors.toMap(CrmSaleQuotationDO::getId, q -> q, (a, b) -> a));
    }
    /**
@@ -324,6 +415,11 @@
        return orderMap;
    }
    private String getQuotationStatusName(Integer status) {
        CrmSaleQuotationStatusEnum statusEnum = CrmSaleQuotationStatusEnum.getByStatus(status);
        return statusEnum != null ? statusEnum.getName() : "未知状态(" + status + ")";
    }
    private String getSaleOrderStatusName(Integer status) {
        if (status == null) {
            return "未知";