From a82d847bbb39e2417d4c9773ebc4fc51fdb0b8fc Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期二, 10 十一月 2026 13:31:31 +0800
Subject: [PATCH] feat(crm): 完善投标保证金与报价单关联及作废功能

---
 yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/bidbond/CrmBidBondServiceImpl.java |  102 +++++++++++++++++++++++++++++++++++++++++++++++++-
 1 files changed, 99 insertions(+), 3 deletions(-)

diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/bidbond/CrmBidBondServiceImpl.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/bidbond/CrmBidBondServiceImpl.java
index 12e86b7..846ec7f 100644
--- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/bidbond/CrmBidBondServiceImpl.java
+++ b/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);
+        // 鎶ヤ环鍗曞彉鍖栨椂锛氬厛瑙g粦鏃ф姤浠峰崟锛屽啀缁戝畾鏂版姤浠峰崟
+        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);
+        }
+        // 瑙i櫎鎶ヤ环鍗曠粦瀹氬悗鍐嶅垹闄�
+        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())
+                            + "]锛屽悓涓�鎶ヤ环鍗曞彧鑳界粦瀹氫竴寮犳姇鏍囥�傝鏀归�夊叾浠栨姤浠峰崟锛屾垨鍏堝湪璇ユ姇鏍囦腑瑙i櫎鍏宠仈銆�");
+        }
+        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);
@@ -293,8 +367,25 @@
                 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 "鏈煡";

--
Gitblit v1.9.3