From 46855633ce1809df905efa8e0b03ca94a49a227b Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期六, 31 十月 2026 13:48:21 +0800
Subject: [PATCH] feat(mes): 销售出库单支持 Excel 导入

---
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesServiceImpl.java |  259 +++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 259 insertions(+), 0 deletions(-)

diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesServiceImpl.java
index 9338217..79e4404 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesServiceImpl.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesServiceImpl.java
@@ -2,13 +2,26 @@
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.exception.ServiceException;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
+import cn.iocoder.yudao.module.crm.api.customer.dto.CrmCustomerRespDTO;
+import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.MesWmProductSalesImportExcelVO;
+import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.MesWmProductSalesImportRespVO;
 import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.MesWmProductSalesPageReqVO;
 import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.MesWmProductSalesSaveReqVO;
 import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.MesWmProductSalesShippingReqVO;
+import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.line.MesWmProductSalesLineSaveReqVO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO;
+import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodeRuleCodeEnum;
+import cn.iocoder.yudao.module.mes.service.md.autocode.MesMdAutoCodeRecordService;
+import cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService;
+import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
+import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
 import cn.iocoder.yudao.module.mes.dal.dataobject.wm.salesnotice.MesWmSalesNoticeDO;
 import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productsales.MesWmProductSalesDO;
 import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productsales.MesWmProductSalesDetailDO;
@@ -34,11 +47,20 @@
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.PlatformTransactionManager;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.transaction.support.TransactionTemplate;
 import org.springframework.validation.annotation.Validated;
 
 import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
@@ -79,6 +101,15 @@
 
     @Resource
     private ErpSaleOrderApi saleOrderApi;
+
+    @Resource
+    private MdmItemApi mdmItemApi;
+    @Resource
+    private MesWmBatchService batchService;
+    @Resource
+    private MesMdAutoCodeRecordService autoCodeRecordService;
+    @Resource
+    private PlatformTransactionManager transactionManager;
 
     @Override
     public Long createProductSales(MesWmProductSalesSaveReqVO createReqVO) {
@@ -441,4 +472,232 @@
         productSalesMapper.updateById(new MesWmProductSalesDO().setId(id).setStatus(status));
     }
 
+    // ==================== 瀵煎叆 ====================
+
+    @Override
+    public MesWmProductSalesImportRespVO importProductSalesList(List<MesWmProductSalesImportExcelVO> importList) {
+        MesWmProductSalesImportRespVO respVO = MesWmProductSalesImportRespVO.builder()
+                .createList(new ArrayList<>())
+                .failureList(new LinkedHashMap<>())
+                .build();
+        if (CollUtil.isEmpty(importList)) {
+            return respVO;
+        }
+
+        // 1. 棰勭儹鈥滃鎴峰悕绉� -> 瀹㈡埛缂栧彿鈥濇槧灏勶紙瀹㈡埛涓绘暟鎹湪 CRM 妯″潡锛�
+        Map<String, Long> customerIdMap = new HashMap<>();
+        List<CrmCustomerRespDTO> customers = customerApi.getCustomerSimpleList();
+        if (CollUtil.isNotEmpty(customers)) {
+            customers.forEach(customer -> {
+                if (customer != null && StrUtil.isNotBlank(customer.getName())) {
+                    customerIdMap.putIfAbsent(customer.getName().trim(), customer.getId());
+                }
+            });
+        }
+
+        // 2. 鎸夆�滃嚭搴撳崟鍙封�濆垎缁勶細鍚屽崟鍙峰琛� = 涓�寮犲嚭搴撳崟鐨勫鏉℃槑缁嗭紱鍗曞彿涓虹┖鍒欐瘡琛岀嫭绔嬫垚鍗�
+        Map<String, List<ImportRow>> orderGroups = new LinkedHashMap<>();
+        for (int i = 0; i < importList.size(); i++) {
+            MesWmProductSalesImportExcelVO row = importList.get(i);
+            if (row == null || isEmptyImportRow(row)) {
+                continue; // 璺宠繃绌鸿
+            }
+            String key = StrUtil.isNotBlank(row.getCode()) ? row.getCode().trim() : "__row_" + (i + 2);
+            orderGroups.computeIfAbsent(key, k -> new ArrayList<>()).add(new ImportRow(row, i + 2));
+        }
+
+        // 3. 閫愬崟瀵煎叆锛氭瘡寮犲崟鐙珛浜嬪姟锛屽け璐ョ殑鍗曟暣鍗曞洖婊氾紝涓嶅奖鍝嶅叾瀹冨崟
+        TransactionTemplate importTxTemplate = new TransactionTemplate(transactionManager);
+        for (Map.Entry<String, List<ImportRow>> entry : orderGroups.entrySet()) {
+            List<ImportRow> rows = entry.getValue();
+            String label = StrUtil.isNotBlank(rows.get(0).data().getCode())
+                    ? rows.get(0).data().getCode().trim() : "绗� " + rows.get(0).rowNum() + " 琛�";
+            try {
+                String code = importTxTemplate.execute(status -> createProductSalesByImport(rows, customerIdMap));
+                respVO.getCreateList().add(code);
+            } catch (Exception ex) {
+                respVO.getFailureList().put(label, resolveImportErrorMessage(ex));
+            }
+        }
+        return respVO;
+    }
+
+    /**
+     * 渚濇嵁鍚屼竴鍑哄簱鍗曞彿鐨勫琛屽鍏ユ暟鎹紝鍒涘缓涓�寮犻攢鍞嚭搴撳崟锛堝崟澶� + 鏄庣粏锛�
+     *
+     * @return 鍑哄簱鍗曞彿
+     */
+    private String createProductSalesByImport(List<ImportRow> rows, Map<String, Long> customerIdMap) {
+        ImportRow headRow = rows.get(0);
+        MesWmProductSalesImportExcelVO head = headRow.data();
+        String rowPrefix = "绗� " + headRow.rowNum() + " 琛岋細";
+
+        // 1. 瀹㈡埛锛堝彇璇ョ粍棣栬锛�
+        if (StrUtil.isBlank(head.getClientName())) {
+            throw new IllegalArgumentException(rowPrefix + "瀹㈡埛鍚嶇О涓嶈兘涓虹┖");
+        }
+        String clientName = head.getClientName().trim();
+        Long clientId = customerIdMap.get(clientName);
+        if (clientId == null) {
+            throw new IllegalArgumentException(rowPrefix + "瀹㈡埛鍚嶇О[" + clientName
+                    + "]鍦� CRM 涓笉瀛樺湪锛岃鏍稿鍚嶇О鏄惁涓庣郴缁熷鎴峰畬鍏ㄤ竴鑷达紙鍚叏鍗婅銆佺┖鏍硷級");
+        }
+
+        // 2. 鍑哄簱鏃ユ湡锛堢暀绌哄彇褰撳ぉ锛�
+        LocalDateTime salesDate = parseImportSalesDate(head.getSalesDate(), rowPrefix);
+
+        // 3. 鍑哄簱鍗曞彿锛氬~浜嗗氨鐢紙鏍¢獙鍞竴锛夛紝娌″~鑷姩鐢熸垚
+        String code = StrUtil.isNotBlank(head.getCode()) ? head.getCode().trim()
+                : autoCodeRecordService.generateAutoCode(MesMdAutoCodeRuleCodeEnum.WM_PRODUCT_SALES_CODE.getCode());
+        if (StrUtil.isBlank(code)) {
+            throw new IllegalArgumentException(rowPrefix
+                    + "鍑哄簱鍗曞彿鑷姩鐢熸垚澶辫触锛岃绋嶅悗閲嶈瘯锛屾垨鍦ㄨ鍒楁墜宸ュ~鍐欏嚭搴撳崟鍙�");
+        }
+        if (productSalesMapper.selectByCode(code) != null) {
+            throw new IllegalArgumentException(rowPrefix + "鍑哄簱鍗曞彿[" + code
+                    + "]宸插瓨鍦紝璇蜂慨鏀硅鍒楁垨娓呯┖璇ュ垪鐢辩郴缁熻嚜鍔ㄧ敓鎴�");
+        }
+
+        // 4. 鍒涘缓鍗曞ご锛堣崏绋跨姸鎬侊紝涓嶆彁浜ゃ�佷笉鍗犵敤搴撳瓨锛�
+        MesWmProductSalesSaveReqVO saveReqVO = new MesWmProductSalesSaveReqVO();
+        saveReqVO.setCode(code);
+        saveReqVO.setName(StrUtil.isNotBlank(head.getName()) ? head.getName().trim()
+                : clientName + "-" + salesDate.toLocalDate() + " 鍑哄簱");
+        saveReqVO.setClientId(clientId);
+        saveReqVO.setSalesDate(salesDate);
+        saveReqVO.setSalesOrderCode(StrUtil.trimToNull(head.getSalesOrderCode()));
+        saveReqVO.setContactName(StrUtil.trimToNull(head.getContactName()));
+        saveReqVO.setContactTelephone(StrUtil.trimToNull(head.getContactTelephone()));
+        saveReqVO.setContactAddress(StrUtil.trimToNull(head.getContactAddress()));
+        saveReqVO.setRemark(StrUtil.trimToNull(head.getRemark()));
+        Long salesId = createProductSales(saveReqVO);
+
+        // 5. 閫愯鍒涘缓鏄庣粏
+        for (ImportRow row : rows) {
+            createProductSalesLineByImport(salesId, row);
+        }
+        return code;
+    }
+
+    /**
+     * 渚濇嵁瀵煎叆琛屽垱寤轰竴鏉¢攢鍞嚭搴撳崟琛�
+     */
+    private void createProductSalesLineByImport(Long salesId, ImportRow row) {
+        MesWmProductSalesImportExcelVO data = row.data();
+        String rowPrefix = "绗� " + row.rowNum() + " 琛岋細";
+
+        // 1. 鐗╂枡缂栫爜锛堝繀濉紝涓斿繀椤诲瓨鍦ㄣ�佸惎鐢級
+        if (StrUtil.isBlank(data.getItemCode())) {
+            throw new IllegalArgumentException(rowPrefix + "鐗╂枡缂栫爜涓嶈兘涓虹┖");
+        }
+        String itemCode = data.getItemCode().trim();
+        MdmItemRespDTO item = mdmItemApi.getItemByCode(itemCode).getCheckedData();
+        if (item == null) {
+            throw new IllegalArgumentException(rowPrefix + "鐗╂枡缂栫爜[" + itemCode
+                    + "]涓嶅瓨鍦紝璇峰厛鍒般�愬熀纭�鏁版嵁-鐗╂枡涓绘暟鎹�戜腑缁存姢璇ョ墿鏂�");
+        }
+        if (!CommonStatusEnum.ENABLE.getStatus().equals(item.getStatus())) {
+            throw new IllegalArgumentException(rowPrefix + "鐗╂枡缂栫爜[" + itemCode
+                    + "-" + item.getName() + "]宸插仠鐢紝璇峰厛鍦ㄧ墿鏂欎富鏁版嵁涓惎鐢ㄥ悗鍐嶅鍏�");
+        }
+
+        // 2. 鍑哄簱鏁伴噺锛堝繀濉紝涓诲崟浣嶅彛寰勶級
+        if (data.getQuantity() == null) {
+            throw new IllegalArgumentException(rowPrefix + "鍑哄簱鏁伴噺涓嶈兘涓虹┖锛堢墿鏂橻" + itemCode + "]锛�");
+        }
+        if (data.getQuantity().compareTo(BigDecimal.ZERO) <= 0) {
+            throw new IllegalArgumentException(rowPrefix + "鍑哄簱鏁伴噺蹇呴』澶т簬 0锛屽綋鍓嶅~鍐欎负["
+                    + data.getQuantity().toPlainString() + "]锛堢墿鏂橻" + itemCode + "]锛�");
+        }
+
+        // 3. 鎵规鍙凤紙鍙┖锛涘~浜嗗繀椤绘槸宸插瓨鍦ㄤ笖褰掑睘璇ョ墿鏂欑殑鎵规锛�
+        String batchCode = StrUtil.trimToNull(data.getBatchCode());
+        if (batchCode != null) {
+            MesWmBatchDO batch = batchService.getBatchByCode(batchCode);
+            if (batch == null) {
+                throw new IllegalArgumentException(rowPrefix + "鎵规鍙穂" + batchCode
+                        + "]涓嶅瓨鍦紝璇峰厛鍦ㄣ�愭壒娆$鐞嗐�戜腑鍒涘缓锛屾垨娓呯┖璇ヨ鐨勬壒娆″彿");
+            }
+            if (batch.getItemId() != null && !ObjUtil.equal(batch.getItemId(), item.getId())) {
+                String batchItemText = resolveItemText(batch.getItemId());
+                throw new IllegalArgumentException(rowPrefix + "鎵规鍙穂" + batchCode + "]灞炰簬鐗╂枡["
+                        + batchItemText + "]锛屼笌褰撳墠琛岀墿鏂橻" + itemCode + "-" + item.getName()
+                        + "]涓嶄竴鑷达紝璇锋牳瀵规壒娆″彿鎴栫墿鏂欑紪鐮�");
+            }
+        }
+
+        // 4. 鍒涘缓琛岋紙鑽夌鐘舵�侊紱鐗╂枡瀛樺湪鎬�/鍚敤銆佹壒娆¤В鏋愮瓑鏍¢獙鐢辫鏈嶅姟缁熶竴瀹屾垚锛�
+        MesWmProductSalesLineSaveReqVO lineVO = new MesWmProductSalesLineSaveReqVO();
+        lineVO.setSalesId(salesId);
+        lineVO.setItemId(item.getId());
+        lineVO.setQuantity(data.getQuantity());
+        lineVO.setBatchCode(batchCode);
+        lineVO.setOqcCheckFlag(parseImportYesNo(data.getOqcCheckFlag()));
+        lineVO.setRemark(StrUtil.trimToNull(data.getRemark()));
+        productSalesLineService.createProductSalesLine(lineVO);
+    }
+
+    private boolean isEmptyImportRow(MesWmProductSalesImportExcelVO row) {
+        return StrUtil.isBlank(row.getCode()) && StrUtil.isBlank(row.getClientName())
+                && StrUtil.isBlank(row.getItemCode()) && row.getQuantity() == null
+                && StrUtil.isBlank(row.getName()) && StrUtil.isBlank(row.getSalesDate());
+    }
+
+    private LocalDateTime parseImportSalesDate(String text, String rowPrefix) {
+        if (StrUtil.isBlank(text)) {
+            return LocalDate.now().atStartOfDay();
+        }
+        String value = text.trim();
+        for (String pattern : new String[]{"yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm"}) {
+            try {
+                return LocalDateTime.parse(value, DateTimeFormatter.ofPattern(pattern));
+            } catch (Exception ignored) {
+                // 缁х画灏濊瘯涓嬩竴涓牸寮�
+            }
+        }
+        for (String pattern : new String[]{"yyyy-MM-dd", "yyyy/M/d"}) {
+            try {
+                return LocalDate.parse(value, DateTimeFormatter.ofPattern(pattern)).atStartOfDay();
+            } catch (Exception ignored) {
+                // 缁х画灏濊瘯涓嬩竴涓牸寮�
+            }
+        }
+        throw new IllegalArgumentException(rowPrefix + "鍑哄簱鏃ユ湡[" + text
+                + "]鏍煎紡涓嶆纭紝璇蜂娇鐢� yyyy-MM-dd锛屽 2026-10-31");
+    }
+
+    private Boolean parseImportYesNo(String text) {
+        if (StrUtil.isBlank(text)) {
+            return false;
+        }
+        String value = text.trim();
+        return "鏄�".equals(value) || "1".equals(value) || "true".equalsIgnoreCase(value)
+                || "yes".equalsIgnoreCase(value) || "闇�瑕�".equals(value);
+    }
+
+    private String resolveItemText(Long itemId) {
+        try {
+            MdmItemRespDTO item = mdmItemApi.getItem(itemId).getCheckedData();
+            if (item != null) {
+                return item.getCode() + "-" + item.getName();
+            }
+        } catch (Exception ex) {
+            log.warn("[resolveItemText][鐗╂枡({}) 鍚嶇О瑙f瀽澶辫触]", itemId, ex);
+        }
+        return "ID=" + itemId;
+    }
+
+    private String resolveImportErrorMessage(Exception ex) {
+        if (ex instanceof ServiceException serviceException) {
+            return serviceException.getMessage();
+        }
+        return StrUtil.blankToDefault(ex.getMessage(), "瀵煎叆澶辫触");
+    }
+
+    /**
+     * 瀵煎叆琛岋紙淇濈暀 Excel 琛屽彿锛屼究浜庢彁绀衡�滅 N 琛屸�濓級
+     */
+    private record ImportRow(MesWmProductSalesImportExcelVO data, int rowNum) {
+    }
+
 }

--
Gitblit v1.9.3