yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/quotation/CrmSaleQuotationServiceImpl.java
@@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.crm.service.quotation;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.number.MoneyUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
@@ -81,11 +82,11 @@
    public Long createSaleQuotation(CrmSaleQuotationSaveReqVO createReqVO, Long userId) {
        // 1.1 校验物料项的有效性
        List<CrmSaleQuotationProductDO> quotationProducts = validateQuotationItems(createReqVO.getItems());
        // 1.2 生成报价单号,并校验唯一性
        String no = noRedisDAO.generate(CrmNoRedisDAO.SALE_QUOTATION_NO_PREFIX);
        if (saleQuotationMapper.selectByNo(no) != null) {
            throw exception(SALE_QUOTATION_NO_EXISTS);
        }
        // 1.2 确定报价单号(前端填写则用自定义单号,留空则自动生成),并校验唯一性
        String customNo = StrUtil.trim(createReqVO.getNo());
        String no = StrUtil.isBlank(customNo)
                ? noRedisDAO.generate(CrmNoRedisDAO.SALE_QUOTATION_NO_PREFIX) : customNo;
        validateQuotationNoUnique(no, null);
        // 1.3 校验关联字段
        validateRelationDataExists(createReqVO);
@@ -123,8 +124,15 @@
        // 1.4 校验关联字段
        validateRelationDataExists(updateReqVO);
        // 2.1 更新报价单
        // 2.1 更新报价单(报价单号留空表示保持原值,填写则校验唯一性后替换)
        CrmSaleQuotationDO updateObj = BeanUtils.toBean(updateReqVO, CrmSaleQuotationDO.class);
        String no = StrUtil.trim(updateReqVO.getNo());
        if (StrUtil.isBlank(no)) {
            updateObj.setNo(saleQuotation.getNo());
        } else {
            validateQuotationNoUnique(no, saleQuotation.getId());
            updateObj.setNo(no);
        }
        calculateTotalPrice(updateObj, quotationProducts);
        saleQuotationMapper.updateById(updateObj);
        // 2.2 更新报价明细
@@ -167,6 +175,19 @@
        return saleQuotation;
    }
    /**
     * 校验报价单号唯一(含已逻辑删除的报价单,因 uk_no 唯一索引覆盖已删除行)
     *
     * @param no       报价单号
     * @param selfId   自身编号(修改时排除自己,新增传 null)
     */
    private void validateQuotationNoUnique(String no, Long selfId) {
        CrmSaleQuotationDO exists = saleQuotationMapper.selectAnyByNo(no);
        if (exists != null && !exists.getId().equals(selfId)) {
            throw exception(SALE_QUOTATION_NO_EXISTS, no);
        }
    }
    private List<CrmSaleQuotationProductDO> validateQuotationItems(List<CrmSaleQuotationSaveReqVO.Item> list) {
        // 1. 校验物料存在且启用
        Set<Long> itemIds = convertSet(list, CrmSaleQuotationSaveReqVO.Item::getItemId);