tuqin
4 天以前 67d3ab41f4989faf4db5f7f95f56b8595dd8eb3b
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java
@@ -1,9 +1,11 @@
package cn.iocoder.yudao.module.crm.service.contract;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
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;
@@ -15,11 +17,17 @@
import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract.CrmContractPageReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract.CrmContractSaveReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract.CrmContractImportExcelVO;
import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract.CrmContractImportRespVO;
import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract.CrmContractTransferReqVO;
import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractConfigDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractProductDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.quotation.CrmSaleQuotationDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.quotation.CrmSaleQuotationProductDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
import cn.iocoder.yudao.module.crm.dal.mysql.contract.CrmContractMapper;
import cn.iocoder.yudao.module.crm.dal.mysql.customer.CrmCustomerMapper;
import cn.iocoder.yudao.module.crm.dal.mysql.contract.CrmContractProductMapper;
import cn.iocoder.yudao.module.crm.dal.redis.no.CrmNoRedisDAO;
import cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum;
@@ -28,10 +36,12 @@
import cn.iocoder.yudao.module.crm.framework.permission.core.annotations.CrmPermission;
import cn.iocoder.yudao.module.crm.service.business.CrmBusinessService;
import cn.iocoder.yudao.module.crm.service.contact.CrmContactService;
import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi;
import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionTransferReqBO;
import cn.iocoder.yudao.module.crm.service.quotation.CrmSaleQuotationService;
import cn.iocoder.yudao.module.crm.service.receivable.CrmReceivableService;
import cn.iocoder.yudao.module.erp.api.sale.ErpSaleOrderApi;
import cn.iocoder.yudao.module.erp.api.sale.dto.ErpSaleOrderCreateReqDTO;
@@ -55,9 +65,11 @@
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.time.format.DateTimeFormatter;
import org.flowable.engine.repository.ProcessDefinition;
@@ -65,6 +77,7 @@
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.crm.enums.LogRecordConstants.*;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
import static cn.iocoder.yudao.module.crm.util.CrmAuditStatusUtils.convertBpmResultToAuditStatus;
/**
@@ -94,11 +107,15 @@
    @Resource
    private CrmNoRedisDAO noRedisDAO;
    @Resource
    private CrmCustomerMapper customerMapper;
    @Resource
    private CrmPermissionService crmPermissionService;
    @Resource
    private CrmCustomerService customerService;
    @Resource
    private CrmCustomerApi customerApi;
    @Resource
    private CrmBusinessService businessService;
    @Resource
@@ -120,6 +137,9 @@
    private ErpSaleOrderApi saleOrderApi;
    @Resource
    private MdmItemApi mdmItemApi;
    @Resource
    @Lazy // 延迟加载,避免循环依赖(quotation 内部引用了 contractService)
    private CrmSaleQuotationService crmSaleQuotationService;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -250,6 +270,182 @@
        }));
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public CrmContractImportRespVO importContractList(List<CrmContractImportExcelVO> importList) {
        CrmContractImportRespVO respVO = CrmContractImportRespVO.builder()
                .createList(new ArrayList<>())
                .updateList(new ArrayList<>())
                .failureList(new LinkedHashMap<>())
                .build();
        if (CollUtil.isEmpty(importList)) {
            return respVO;
        }
        // 1. 预热“客户名称 -> 客户编号”映射
        Map<String, Long> customerIdMap = new HashMap<>();
        List<CrmCustomerDO> customers = customerMapper.selectList();
        if (CollUtil.isNotEmpty(customers)) {
            customers.forEach(customer -> {
                if (StrUtil.isNotBlank(customer.getName())) {
                    customerIdMap.putIfAbsent(customer.getName().trim(), customer.getId());
                }
            });
        }
        // 2. 按“合同编号”分组
        Map<String, List<CrmContractImportExcelVO>> groups = new LinkedHashMap<>();
        for (int i = 0; i < importList.size(); i++) {
            CrmContractImportExcelVO row = importList.get(i);
            if (row == null || (StrUtil.isBlank(row.getName()) && StrUtil.isBlank(row.getCustomerName())
                    && StrUtil.isBlank(row.getNo()))) {
                continue; // 跳过空行
            }
            String key = StrUtil.isNotBlank(row.getNo()) ? row.getNo().trim() : "__row_" + (i + 2);
            groups.computeIfAbsent(key, k -> new ArrayList<>()).add(row);
        }
        // 3. 逐张合同导入
        for (Map.Entry<String, List<CrmContractImportExcelVO>> entry : groups.entrySet()) {
            List<CrmContractImportExcelVO> rows = entry.getValue();
            String label = StrUtil.isNotBlank(rows.get(0).getNo()) ? rows.get(0).getNo().trim()
                    : "第 " + entry.getKey().substring("__row_".length()) + " 行";
            try {
                CrmContractDO contract = createContractByImport(rows, customerIdMap);
                respVO.getCreateList().add(contract.getNo());
            } catch (Exception ex) {
                respVO.getFailureList().put(label, resolveImportErrorMessage(ex));
            }
        }
        return respVO;
    }
    /**
     * 依据导入数据创建一张合同(含明细)。负责人=当前登录用户;状态=未提交;金额按明细与优惠率自动计算。
     */
    private CrmContractDO createContractByImport(List<CrmContractImportExcelVO> rows, Map<String, Long> customerIdMap) {
        CrmContractImportExcelVO head = rows.get(0);
        // 1. 主表必填
        if (StrUtil.isBlank(head.getName())) {
            throw new IllegalArgumentException("合同名称不能为空");
        }
        if (StrUtil.isBlank(head.getCustomerName())) {
            throw new IllegalArgumentException("客户名称不能为空");
        }
        Long customerId = customerIdMap.get(head.getCustomerName().trim());
        if (customerId == null) {
            throw new IllegalArgumentException("客户不存在:" + head.getCustomerName());
        }
        customerService.validateCustomer(customerId);
        // 2. 明细
        List<CrmContractSaveReqVO.Product> products = new ArrayList<>();
        for (CrmContractImportExcelVO row : rows) {
            if (StrUtil.isBlank(row.getProductCode())) {
                throw new IllegalArgumentException("产品编码不能为空");
            }
            CommonResult<MdmItemRespDTO> itemResult = mdmItemApi.getItemByCode(row.getProductCode().trim());
            MdmItemRespDTO item = itemResult == null ? null : itemResult.getData();
            if (item == null) {
                throw new IllegalArgumentException("产品不存在:" + row.getProductCode());
            }
            if (row.getCount() == null || row.getCount() <= 0) {
                throw new IllegalArgumentException("产品[" + row.getProductCode() + "]的数量必须为正整数");
            }
            BigDecimal contractPrice = row.getContractPrice() != null ? row.getContractPrice()
                    : (item.getSalesPrice() != null ? item.getSalesPrice() : BigDecimal.ZERO);
            CrmContractSaveReqVO.Product product = new CrmContractSaveReqVO.Product();
            product.setItemId(item.getId());
            product.setItemPrice(item.getSalesPrice() != null ? item.getSalesPrice() : contractPrice);
            product.setContractPrice(contractPrice);
            product.setCount(row.getCount());
            products.add(product);
        }
        // 3. 合同编号
        String no = StrUtil.isNotBlank(head.getNo()) ? head.getNo().trim()
                : noRedisDAO.generate(CrmNoRedisDAO.CONTRACT_NO_PREFIX);
        if (contractMapper.selectByNo(no) != null) {
            throw new IllegalArgumentException("合同编号已存在:" + no);
        }
        // 4. 组装主表(负责人=登录用户;状态=未提交)
        CrmContractDO contract = new CrmContractDO();
        contract.setNo(no);
        contract.setName(head.getName().trim());
        contract.setCustomerId(customerId);
        contract.setOwnerUserId(getLoginUserId());
        contract.setOrderDate(parseImportDateTime(head.getOrderDate()));
        contract.setStartTime(parseImportDate(head.getStartTime()));
        contract.setEndTime(parseImportDate(head.getEndTime()));
        contract.setDiscountPercent(head.getDiscountPercent() != null ? head.getDiscountPercent() : BigDecimal.ZERO);
        contract.setDepositPrice(head.getDepositPrice() != null ? head.getDepositPrice() : BigDecimal.ZERO);
        contract.setRemark(head.getRemark());
        contract.setAuditStatus(CrmAuditStatusEnum.DRAFT.getStatus());
        // 5. 明细落库 + 自动算价
        List<CrmContractProductDO> productDOs = validateContractProducts(products);
        calculateTotalPrice(contract, productDOs);
        contractMapper.insert(contract);
        productDOs.forEach(product -> product.setContractId(contract.getId()));
        contractProductMapper.insertBatch(productDOs);
        // 6. 创建数据权限(否则当前用户看不到该合同)
        crmPermissionService.createPermission(new CrmPermissionCreateReqBO().setUserId(contract.getOwnerUserId())
                .setBizType(CrmBizTypeEnum.CRM_CONTRACT.getType()).setBizId(contract.getId())
                .setLevel(CrmPermissionLevelEnum.OWNER.getLevel()));
        return contract;
    }
    private LocalDateTime parseImportDateTime(String text) {
        if (StrUtil.isBlank(text)) {
            return LocalDateTime.now();
        }
        String value = text.trim();
        for (String pattern : new String[]{"yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy/MM/dd HH:mm:ss"}) {
            try {
                return LocalDateTime.parse(value, DateTimeFormatter.ofPattern(pattern));
            } catch (Exception ignored) {
                // 继续尝试下一个格式
            }
        }
        for (String pattern : new String[]{"yyyy-MM-dd", "yyyy/M/d"}) {
            try {
                return java.time.LocalDate.parse(value, DateTimeFormatter.ofPattern(pattern)).atStartOfDay();
            } catch (Exception ignored) {
                // 继续尝试下一个格式
            }
        }
        throw new IllegalArgumentException("下单时间格式不正确:" + text + ",示例 2026-01-01 10:00:00");
    }
    private LocalDateTime parseImportDate(String text) {
        if (StrUtil.isBlank(text)) {
            return null;
        }
        String value = text.trim();
        for (String pattern : new String[]{"yyyy-MM-dd", "yyyy/M/d"}) {
            try {
                return java.time.LocalDate.parse(value, DateTimeFormatter.ofPattern(pattern)).atStartOfDay();
            } catch (Exception ignored) {
                // 继续尝试下一个格式
            }
        }
        try {
            return LocalDateTime.parse(value, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        } catch (Exception ignored) {
            // 忽略
        }
        throw new IllegalArgumentException("日期格式不正确:" + text + ",示例 2026-01-01");
    }
    private String resolveImportErrorMessage(Exception ex) {
        if (ex instanceof cn.iocoder.yudao.framework.common.exception.ServiceException serviceException) {
            return serviceException.getMessage();
        }
        return StrUtil.blankToDefault(ex.getMessage(), "导入失败");
    }
    private void calculateTotalPrice(CrmContractDO contract, List<CrmContractProductDO> contractProducts) {
        contract.setTotalProductPrice(getSumValue(contractProducts, CrmContractProductDO::getTotalPrice, BigDecimal::add, BigDecimal.ZERO));
        BigDecimal discountPrice = MoneyUtils.priceMultiplyPercent(contract.getTotalProductPrice(), contract.getDiscountPercent());
@@ -271,6 +467,11 @@
        // 2.1 删除合同
        contractMapper.deleteById(id);
        // 2.2 删除合同时,解除关联的报价单引用
        CrmSaleQuotationDO quotation = crmSaleQuotationService.getByContractId(id);
        if (quotation != null) {
            crmSaleQuotationService.clearContractReference(quotation.getId());
        }
        // 2.2 删除数据权限
        crmPermissionService.deletePermission(CrmBizTypeEnum.CRM_CONTRACT.getType(), id);
@@ -468,8 +669,10 @@
                return PageResult.empty();
            }
        }
        // 2. 查询分页
        return contractMapper.selectPage(pageReqVO, userId, config);
        // 2. 获取用户有权限的客户 ID 列表
        List<Long> permittedCustomerIds = customerApi.getPermittedCustomerIds();
        // 3. 查询分页
        return contractMapper.selectPage(pageReqVO, userId, config, permittedCustomerIds);
    }
    @Override