From 67d3ab41f4989faf4db5f7f95f56b8595dd8eb3b Mon Sep 17 00:00:00 2001
From: tuqin <tuqin@tuqin.cn>
Date: 星期五, 11 九月 2026 09:39:44 +0800
Subject: [PATCH] feat(crm/erp): 新增采购申请、合同管理 Excel 导入(含菜单权限 SQL)

---
 yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java |  425 +++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 files changed, 409 insertions(+), 16 deletions(-)

diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java
index d25d7c0..448dae2 100644
--- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java
@@ -1,22 +1,33 @@
 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;
 import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
 import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi;
 import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
+import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
+import cn.iocoder.yudao.module.bpm.service.definition.BpmCategoryService;
+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;
@@ -25,12 +36,18 @@
 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.product.CrmProductService;
+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;
+import cn.iocoder.yudao.module.erp.api.sale.dto.ErpSaleOrderRespDTO;
+import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
+import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
 import com.mzt.logapi.context.LogRecordContext;
 import com.mzt.logapi.service.impl.DiffParseFunction;
@@ -44,13 +61,23 @@
 
 import java.math.BigDecimal;
 import java.time.LocalDateTime;
+import java.util.ArrayList;
 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;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 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;
 
 /**
@@ -68,6 +95,11 @@
      */
     public static final String BPM_PROCESS_DEFINITION_KEY = "crm-contract-audit";
 
+    /**
+     * BPM 鍚堝悓瀹℃壒鍒嗙被缂栫爜
+     */
+    private static final String CONTRACT_APPROVE_CATEGORY_CODE = "contract_approve";
+
     @Resource
     private CrmContractMapper contractMapper;
     @Resource
@@ -75,13 +107,15 @@
 
     @Resource
     private CrmNoRedisDAO noRedisDAO;
+    @Resource
+    private CrmCustomerMapper customerMapper;
 
     @Resource
     private CrmPermissionService crmPermissionService;
     @Resource
-    private CrmProductService productService;
-    @Resource
     private CrmCustomerService customerService;
+    @Resource
+    private CrmCustomerApi customerApi;
     @Resource
     private CrmBusinessService businessService;
     @Resource
@@ -95,6 +129,17 @@
     private AdminUserApi adminUserApi;
     @Resource
     private BpmProcessInstanceApi bpmProcessInstanceApi;
+    @Resource
+    private BpmCategoryService bpmCategoryService;
+    @Resource
+    private BpmProcessDefinitionService bpmProcessDefinitionService;
+    @Resource
+    private ErpSaleOrderApi saleOrderApi;
+    @Resource
+    private MdmItemApi mdmItemApi;
+    @Resource
+    @Lazy // 寤惰繜鍔犺浇锛岄伩鍏嶅惊鐜緷璧栵紙quotation 鍐呴儴寮曠敤浜� contractService锛�
+    private CrmSaleQuotationService crmSaleQuotationService;
 
     @Override
     @Transactional(rollbackFor = Exception.class)
@@ -112,7 +157,9 @@
         }
 
         // 2.1 鎻掑叆鍚堝悓
-        CrmContractDO contract = BeanUtils.toBean(createReqVO, CrmContractDO.class).setNo(no);
+        CrmContractDO contract = BeanUtils.toBean(createReqVO, CrmContractDO.class)
+                .setNo(no)
+                .setAuditStatus(CrmAuditStatusEnum.DRAFT.getStatus()); // 榛樿涓烘湭鎻愪氦鐘舵��
         calculateTotalPrice(contract, contractProducts);
         contractMapper.insert(contract);
         // 2.2 鎻掑叆鍚堝悓鍏宠仈鍟嗗搧
@@ -208,11 +255,195 @@
     }
 
     private List<CrmContractProductDO> validateContractProducts(List<CrmContractSaveReqVO.Product> list) {
-        // 1. 鏍¢獙浜у搧瀛樺湪
-        productService.validProductList(convertSet(list, CrmContractSaveReqVO.Product::getProductId));
+        // 1. 鏍¢獙鐗╂枡瀛樺湪涓斿惎鐢�
+        Set<Long> itemIds = convertSet(list, CrmContractSaveReqVO.Product::getItemId);
+        Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(itemIds);
         // 2. 杞寲涓� CrmContractProductDO 鍒楄〃
-        return convertList(list, o -> BeanUtils.toBean(o, CrmContractProductDO.class,
-                item -> item.setTotalPrice(MoneyUtils.priceMultiply(item.getContractPrice(), item.getCount()))));
+        return convertList(list, o -> BeanUtils.toBean(o, CrmContractProductDO.class, item -> {
+            MdmItemRespDTO mdmItem = itemMap.get(item.getItemId());
+            if (mdmItem == null) {
+                throw exception(PRODUCT_NOT_EXISTS);
+            }
+            item.setItemUnitId(mdmItem.getUnitMeasureId());
+            item.setItemPrice(mdmItem.getSalesPrice());
+            item.setTotalPrice(MoneyUtils.priceMultiply(item.getContractPrice(), item.getCount()));
+        }));
+    }
+
+    @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;
+    }
+
+    /**
+     * 渚濇嵁瀵煎叆鏁版嵁鍒涘缓涓�寮犲悎鍚岋紙鍚槑缁嗭級銆傝礋璐d汉=褰撳墠鐧诲綍鐢ㄦ埛锛涚姸鎬�=鏈彁浜わ紱閲戦鎸夋槑缁嗕笌浼樻儬鐜囪嚜鍔ㄨ绠椼��
+     */
+    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() + "]鐨勬暟閲忓繀椤讳负姝f暣鏁�");
+            }
+            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. 缁勮涓昏〃锛堣礋璐d汉=鐧诲綍鐢ㄦ埛锛涚姸鎬�=鏈彁浜わ級
+        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) {
@@ -236,6 +467,11 @@
 
         // 2.1 鍒犻櫎鍚堝悓
         contractMapper.deleteById(id);
+        // 2.2 鍒犻櫎鍚堝悓鏃讹紝瑙i櫎鍏宠仈鐨勬姤浠峰崟寮曠敤
+        CrmSaleQuotationDO quotation = crmSaleQuotationService.getByContractId(id);
+        if (quotation != null) {
+            crmSaleQuotationService.clearContractReference(quotation.getId());
+        }
         // 2.2 鍒犻櫎鏁版嵁鏉冮檺
         crmPermissionService.deletePermission(CrmBizTypeEnum.CRM_CONTRACT.getType(), id);
 
@@ -289,23 +525,98 @@
     @Transactional(rollbackFor = Exception.class)
     @LogRecord(type = CRM_CONTRACT_TYPE, subType = CRM_CONTRACT_SUBMIT_SUB_TYPE, bizNo = "{{#id}}",
             success = CRM_CONTRACT_SUBMIT_SUCCESS)
-    public void submitContract(Long id, Long userId) {
+    public void submitContract(Long id, String processDefinitionKey, Long userId) {
         // 1. 鏍¢獙鍚堝悓鏄惁鍦ㄥ鎵�
         CrmContractDO contract = validateContractExists(id);
         if (ObjUtil.notEqual(contract.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus())) {
             throw exception(CONTRACT_SUBMIT_FAIL_NOT_DRAFT);
         }
 
-        // 2. 鍒涘缓鍚堝悓瀹℃壒娴佺▼瀹炰緥
-        String processInstanceId = bpmProcessInstanceApi.createProcessInstance(userId, new BpmProcessInstanceCreateReqDTO()
-                .setProcessDefinitionKey(BPM_PROCESS_DEFINITION_KEY).setBusinessKey(String.valueOf(id)));
+        // 2. 鏍¢獙鍒嗙被鍜屾祦绋嬪畾涔夋槸鍚﹀瓨鍦�
+        validateContractApproveCategoryAndProcessDefinition();
 
-        // 3. 鏇存柊鍚堝悓宸ヤ綔娴佺紪鍙�
+        // 3. 鏍¢獙浼犲叆鐨勬祦绋嬪畾涔� Key 鏄惁灞炰簬璇ュ垎绫讳笖澶勪簬婵�娲荤姸鎬�
+        ProcessDefinition processDefinition = bpmProcessDefinitionService.getActiveProcessDefinition(processDefinitionKey);
+        if (processDefinition == null) {
+            throw exception(CONTRACT_BPM_PROCESS_DEFINITION_NOT_EXISTS);
+        }
+
+        // 4. 鍒涘缓鍚堝悓瀹℃壒娴佺▼瀹炰緥
+        String processInstanceId = bpmProcessInstanceApi.createProcessInstance(userId, new BpmProcessInstanceCreateReqDTO()
+                .setProcessDefinitionKey(processDefinitionKey).setBusinessKey(String.valueOf(id)));
+
+        // 5. 鏇存柊鍚堝悓宸ヤ綔娴佺紪鍙�
         contractMapper.updateById(new CrmContractDO().setId(id).setProcessInstanceId(processInstanceId)
                 .setAuditStatus(CrmAuditStatusEnum.PROCESS.getStatus()));
 
-        // 3. 璁板綍鏃ュ織
+        // 6. 璁板綍鏃ュ織
         LogRecordContext.putVariable("contractName", contract.getName());
+    }
+
+    /**
+     * 鏍¢獙 BPM 鍚堝悓瀹℃壒鍒嗙被鍜屾祦绋嬪畾涔夋槸鍚﹀瓨鍦�
+     */
+    private void validateContractApproveCategoryAndProcessDefinition() {
+        // 1. 鏍¢獙鍒嗙被鏄惁瀛樺湪
+        List<String> codes = Collections.singletonList(CONTRACT_APPROVE_CATEGORY_CODE);
+        Map<String, ?> categoryMap = bpmCategoryService.getCategoryMap(codes);
+        if (!categoryMap.containsKey(CONTRACT_APPROVE_CATEGORY_CODE)) {
+            throw exception(CONTRACT_BPM_PROCESS_DEFINITION_NOT_EXISTS);
+        }
+
+        // 2. 鏍¢獙鍒嗙被涓嬫槸鍚︽湁鍙敤鐨勬祦绋嬪畾涔�
+        List<BpmProcessDefinitionInfoDO> definitionInfoList = bpmProcessDefinitionService
+                .getProcessDefinitionInfoListByCategory(CONTRACT_APPROVE_CATEGORY_CODE);
+        if (CollUtil.isEmpty(definitionInfoList)) {
+            throw exception(CONTRACT_BPM_PROCESS_DEFINITION_NOT_EXISTS);
+        }
+
+        // 3. 鏍¢獙鏄惁鏈夋縺娲荤姸鎬佺殑娴佺▼瀹氫箟
+        Set<String> processDefinitionIds = convertSet(definitionInfoList, BpmProcessDefinitionInfoDO::getProcessDefinitionId);
+        List<ProcessDefinition> processDefinitions = bpmProcessDefinitionService.getProcessDefinitionList(processDefinitionIds);
+        boolean hasActiveDefinition = processDefinitions.stream()
+                .anyMatch(pd -> !pd.isSuspended());
+        if (!hasActiveDefinition) {
+            throw exception(CONTRACT_BPM_PROCESS_DEFINITION_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public List<Map<String, Object>> getContractApproveProcessDefinitionList() {
+        // 1. 鏍¢獙鍒嗙被鍜屾祦绋嬪畾涔夋槸鍚﹀瓨鍦�
+        validateContractApproveCategoryAndProcessDefinition();
+
+        // 2. 鑾峰彇鍒嗙被涓嬬殑娴佺▼瀹氫箟淇℃伅
+        List<BpmProcessDefinitionInfoDO> definitionInfoList = bpmProcessDefinitionService
+                .getProcessDefinitionInfoListByCategory(CONTRACT_APPROVE_CATEGORY_CODE);
+
+        // 3. 鑾峰彇娴佺▼瀹氫箟璇︽儏
+        Set<String> processDefinitionIds = convertSet(definitionInfoList, BpmProcessDefinitionInfoDO::getProcessDefinitionId);
+        List<ProcessDefinition> processDefinitions = bpmProcessDefinitionService.getProcessDefinitionList(processDefinitionIds);
+
+        // 4. 杩囨护锛氬彧淇濈暀婵�娲荤姸鎬侊紝涓旂浉鍚� key 鍙繚鐣欐渶鏂扮増鏈�
+        Map<String, ProcessDefinition> latestVersionMap = new HashMap<>();
+        for (ProcessDefinition pd : processDefinitions) {
+            if (pd.isSuspended()) {
+                continue;
+            }
+            ProcessDefinition existing = latestVersionMap.get(pd.getKey());
+            if (existing == null || pd.getVersion() > existing.getVersion()) {
+                latestVersionMap.put(pd.getKey(), pd);
+            }
+        }
+
+        // 5. 杩斿洖娴佺▼瀹氫箟鍒楄〃
+        List<Map<String, Object>> result = new ArrayList<>();
+        for (ProcessDefinition pd : latestVersionMap.values()) {
+            Map<String, Object> item = new HashMap<>();
+            item.put("id", pd.getId());
+            item.put("key", pd.getKey());
+            item.put("name", pd.getName());
+            item.put("version", pd.getVersion());
+            result.add(item);
+        }
+        return result;
     }
 
     @Override
@@ -358,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
@@ -413,4 +726,84 @@
         return contractMapper.selectListByCustomerIdOwnerUserId(customerId, ownerUserId);
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    @LogRecord(type = CRM_CONTRACT_TYPE, subType = "鐢熸垚閿�鍞鍗�", bizNo = "{{#id}}",
+            success = "鍚堝悓銆恵{#contractName}}銆戠敓鎴愰攢鍞鍗曘�恵{#orderNo}}銆戞垚鍔�")
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTRACT, bizId = "#id", level = CrmPermissionLevelEnum.WRITE)
+    public Long generateSaleOrder(Long id, Long userId) {
+        // 1. 鏍¢獙鍚堝悓瀛樺湪涓斿凡瀹℃壒閫氳繃
+        CrmContractDO contract = validateContractExists(id);
+        if (!CrmAuditStatusEnum.APPROVE.getStatus().equals(contract.getAuditStatus())) {
+            throw exception(CONTRACT_GENERATE_ORDER_FAIL_NOT_APPROVE);
+        }
+        // 2. 鏍¢獙鏄惁宸茬敓鎴愰攢鍞鍗�
+        ErpSaleOrderRespDTO existOrder = saleOrderApi.getSaleOrderByContractId(id).getCheckedData();
+        if (existOrder != null) {
+            throw exception(CONTRACT_GENERATE_ORDER_FAIL_EXISTS_ORDER, existOrder.getNo());
+        }
+
+        // 3. 鑾峰彇鍚堝悓浜у搧鍒楄〃
+        List<CrmContractProductDO> contractProducts = contractProductMapper.selectListByContractId(id);
+        if (CollUtil.isEmpty(contractProducts)) {
+            throw exception(CONTRACT_GENERATE_ORDER_FAIL_NO_PRODUCT);
+        }
+
+        // 4. 鏋勫缓閿�鍞鍗曞垱寤鸿姹�
+        ErpSaleOrderCreateReqDTO createReqDTO = new ErpSaleOrderCreateReqDTO();
+        createReqDTO.setCustomerId(contract.getCustomerId());
+        createReqDTO.setContractId(contract.getId());
+        createReqDTO.setContractNo(contract.getNo());
+        createReqDTO.setSaleUserId(contract.getOwnerUserId());
+        createReqDTO.setOrderTime(LocalDateTime.now().toString());
+        createReqDTO.setDiscountPercent(contract.getDiscountPercent() != null ? contract.getDiscountPercent() : BigDecimal.ZERO);
+        createReqDTO.setDepositPrice(contract.getDepositPrice() != null ? contract.getDepositPrice() : BigDecimal.ZERO);
+        createReqDTO.setRemark("鐢卞悎鍚屻��" + contract.getNo() + "銆戣嚜鍔ㄧ敓鎴�");
+
+        // 5. 鏋勫缓璁㈠崟椤�
+        List<ErpSaleOrderCreateReqDTO.Item> items = contractProducts.stream()
+                .map(cp -> {
+                    ErpSaleOrderCreateReqDTO.Item item = new ErpSaleOrderCreateReqDTO.Item();
+                    item.setProductId(cp.getItemId());
+                    item.setProductPrice(cp.getContractPrice());
+                    item.setCount(cp.getCount());
+                    return item;
+                })
+                .toList();
+        createReqDTO.setItems(items);
+
+        // 6. 鍒涘缓閿�鍞鍗�
+        Long orderId = saleOrderApi.createSaleOrder(createReqDTO).getCheckedData();
+        ErpSaleOrderRespDTO order = saleOrderApi.getSaleOrder(orderId).getCheckedData();
+
+        // 7. 鏇存柊鍚堝悓鍏宠仈璁㈠崟淇℃伅
+        contractMapper.updateById(new CrmContractDO()
+                .setId(id)
+                .setOrderId(orderId)
+                .setOrderNo(order.getNo()));
+
+        // 8. 璁板綍鎿嶄綔鏃ュ織
+        LogRecordContext.putVariable("contractName", contract.getName());
+        LogRecordContext.putVariable("orderNo", order.getNo());
+
+        return orderId;
+    }
+
+    @Override
+    public void updateContractStatus(Long id, Integer status) {
+        contractMapper.updateById(new CrmContractDO()
+                .setId(id)
+                .setAuditStatus(status));
+    }
+
+    @Override
+    public void updateContractStatusByOrderId(Long orderId, Integer status) {
+        CrmContractDO contract = contractMapper.selectByOrderId(orderId);
+        if (contract != null) {
+            contractMapper.updateById(new CrmContractDO()
+                    .setId(contract.getId())
+                    .setAuditStatus(status));
+        }
+    }
+
 }

--
Gitblit v1.9.3