From 25a40b9c4e6a1b650610b8b13088d486699532ce Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期四, 05 十一月 2026 20:23:21 +0800
Subject: [PATCH] 采购申请批量提交、订单导入重复校验;修复考勤异常申请字段不匹配

---
 yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderServiceImpl.java |  225 +++++++++++++++++++++++++++++++++++++++++++++++++------
 1 files changed, 199 insertions(+), 26 deletions(-)

diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderServiceImpl.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderServiceImpl.java
index 24f455b..c2ab475 100644
--- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderServiceImpl.java
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderServiceImpl.java
@@ -16,12 +16,14 @@
 import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
 import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderPageReqVO;
 import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderSaveReqVO;
+import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderImportDuplicateVO;
 import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderImportExcelVO;
 import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderImportRespVO;
 import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderDO;
 import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderItemDO;
 import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseRequestDO;
 import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseRequestItemDO;
+import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseInvoiceOrderMapper;
 import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseOrderItemMapper;
 import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseOrderMapper;
 import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseRequestItemMapper;
@@ -49,6 +51,7 @@
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
 import java.util.*;
+import java.util.stream.Collectors;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
@@ -78,6 +81,8 @@
     private ErpPurchaseOrderMapper purchaseOrderMapper;
     @Resource
     private ErpPurchaseOrderItemMapper purchaseOrderItemMapper;
+    @Resource
+    private ErpPurchaseInvoiceOrderMapper invoiceOrderMapper;
 
     @Resource
     private ErpNoRedisDAO noRedisDAO;
@@ -180,11 +185,12 @@
     @Override
     @Transactional(rollbackFor = Exception.class)
     public ErpPurchaseOrderImportRespVO importPurchaseOrderList(List<ErpPurchaseOrderImportExcelVO> importList,
-                                                               Boolean directApprove) {
+                                                               Boolean directApprove, Boolean overwrite) {
         ErpPurchaseOrderImportRespVO respVO = ErpPurchaseOrderImportRespVO.builder()
                 .createList(new ArrayList<>())
                 .updateList(new ArrayList<>())
                 .failureList(new LinkedHashMap<>())
+                .duplicateList(new ArrayList<>())
                 .build();
         if (CollUtil.isEmpty(importList)) {
             return respVO;
@@ -204,7 +210,27 @@
             });
         }
 
-        // 2. 鎸夆�滈噰璐崟缂栧彿鈥濆垎缁勶細鍚岀紪鍙峰琛� = 涓�寮犺鍗曠殑澶氭潯鏄庣粏锛涚紪鍙蜂负绌哄垯姣忚鐙珛鎴愬崟
+        // 2. 棰勭儹绯荤粺鍐呮湁鏁堥噰璐鍗曪細鎸夊崟鍙枫�佹寜鈥滀緵搴斿晢 + 鏄庣粏鈥濇寚绾瑰缓绱㈠紩锛岀敤浜庤瘑鍒噸澶嶅鍏�
+        List<ErpPurchaseOrderDO> existingOrders = purchaseOrderMapper.selectList();
+        Map<Long, List<ErpPurchaseOrderItemDO>> existingItemsMap = new HashMap<>();
+        if (CollUtil.isNotEmpty(existingOrders)) {
+            purchaseOrderItemMapper.selectListByOrderIds(
+                            existingOrders.stream().map(ErpPurchaseOrderDO::getId).toList())
+                    .forEach(item -> existingItemsMap.computeIfAbsent(item.getOrderId(), k -> new ArrayList<>()).add(item));
+        }
+        Map<String, ErpPurchaseOrderDO> existingNoMap = new HashMap<>();
+        Map<String, List<ErpPurchaseOrderDO>> existingFingerprintMap = new HashMap<>();
+        for (ErpPurchaseOrderDO order : existingOrders) {
+            if (StrUtil.isNotBlank(order.getNo())) {
+                existingNoMap.put(order.getNo(), order);
+            }
+            String fingerprint = buildOrderFingerprint(order.getSupplierId(), existingItemsMap.get(order.getId()));
+            if (fingerprint != null) {
+                existingFingerprintMap.computeIfAbsent(fingerprint, k -> new ArrayList<>()).add(order);
+            }
+        }
+
+        // 3. 鎸夆�滈噰璐崟缂栧彿鈥濆垎缁勶細鍚岀紪鍙峰琛� = 涓�寮犺鍗曠殑澶氭潯鏄庣粏锛涚紪鍙蜂负绌哄垯姣忚鐙珛鎴愬崟
         Map<String, List<ErpPurchaseOrderImportExcelVO>> orderGroups = new LinkedHashMap<>();
         for (int i = 0; i < importList.size(); i++) {
             ErpPurchaseOrderImportExcelVO row = importList.get(i);
@@ -216,14 +242,50 @@
             orderGroups.computeIfAbsent(key, k -> new ArrayList<>()).add(row);
         }
 
-        // 3. 閫愪釜璁㈠崟瀵煎叆锛堝崟寮犺鍗曞け璐ヤ笉褰卞搷鍏跺畠锛�
+        // 4. 閫愪釜璁㈠崟瀵煎叆锛堝崟寮犺鍗曞け璐ヤ笉褰卞搷鍏跺畠锛�
         for (Map.Entry<String, List<ErpPurchaseOrderImportExcelVO>> entry : orderGroups.entrySet()) {
             List<ErpPurchaseOrderImportExcelVO> rows = entry.getValue();
             String label = StrUtil.isNotBlank(rows.get(0).getNo()) ? rows.get(0).getNo().trim()
                     : "绗� " + entry.getKey().substring("__row_".length()) + " 琛�";
             try {
-                ErpPurchaseOrderDO purchaseOrder = createPurchaseOrderByImport(rows, supplierIdMap, directApprove);
-                respVO.getCreateList().add(purchaseOrder.getNo());
+                ImportOrderData importData = prepareImportOrder(rows, supplierIdMap);
+
+                // 4.1 鍏堟寜鍗曞彿鎵撅紱鍙湁鈥滃崟鍙蜂负绌恒�佺敱绯荤粺鐢熸垚缂栧彿鈥濈殑琛屾墠鎸夆�滀緵搴斿晢 + 瀹屾暣鏄庣粏鈥濇寚绾规壘銆�
+                //     鏂囦欢閲屽啓浜嗘槑纭崟鍙风殑锛岃涓虹敤鎴锋槑纭鏂板璇ュ崟鍙凤紝鎸囩汗鐩稿悓涔熶笉鎷︽埅锛岄伩鍏嶆甯哥殑閲嶅閲囪喘琚鍒ゃ��
+                ErpPurchaseOrderDO existing = importData.order.getNo() == null ? null
+                        : existingNoMap.get(importData.order.getNo());
+                String duplicateReason = existing != null ? "閲囪喘鍗曠紪鍙峰凡瀛樺湪" : null;
+                boolean matchedByFingerprint = false;
+                List<ErpPurchaseOrderDO> fingerprintCandidates = null;
+                if (existing == null && importData.generatedNo && importData.fingerprint != null) {
+                    fingerprintCandidates = existingFingerprintMap.get(importData.fingerprint);
+                    if (CollUtil.isNotEmpty(fingerprintCandidates)) {
+                        existing = fingerprintCandidates.get(0);
+                        matchedByFingerprint = true;
+                        duplicateReason = "渚涘簲鍟嗐�佹槑缁嗐�佹暟閲忋�佸崟浠枫�侀噾棰濅笌宸叉湁璁㈠崟涓�鑷�";
+                    }
+                }
+
+                // 4.2 鍙戠幇閲嶅锛氶粯璁や笉瀵煎叆锛岃繑鍥炵粰鍓嶇寮规鎻愮ず锛涘嬀閫夎鐩栧悗鎵嶆洿鏂板凡鏈夎鍗�
+                if (existing != null) {
+                    if (!Boolean.TRUE.equals(overwrite)) {
+                        respVO.getDuplicateList().add(buildImportDuplicate(label, existing, duplicateReason));
+                        continue;
+                    }
+                    if (matchedByFingerprint && fingerprintCandidates.size() > 1) {
+                        String nos = fingerprintCandidates.stream().map(ErpPurchaseOrderDO::getNo)
+                                .collect(Collectors.joining("銆�"));
+                        throw new IllegalArgumentException("鎵惧埌澶氬紶鐩稿悓璁㈠崟锛�" + nos
+                                + "锛夛紝鏃犳硶鑷姩瑕嗙洊锛岃鍏堜繚鐣欎竴寮犲悗鍐嶅鍏�");
+                    }
+                    updatePurchaseOrderByImport(existing, importData, directApprove);
+                    respVO.getUpdateList().add(existing.getNo());
+                    continue;
+                }
+
+                // 4.3 娌℃湁閲嶅锛屾甯告柊澧�
+                createPurchaseOrderByImport(importData, directApprove);
+                respVO.getCreateList().add(importData.order.getNo());
             } catch (Exception ex) {
                 respVO.getFailureList().put(label, resolveImportErrorMessage(ex));
             }
@@ -232,13 +294,10 @@
     }
 
     /**
-     * 渚濇嵁瀵煎叆鏁版嵁鍒涘缓涓�寮犻噰璐鍗曪紙鍚槑缁嗭級銆�
-     *
-     * 娉ㄦ剰锛氭澶勪笉璋冪敤 submitPurchaseOrder锛屽洜姝や笉浼氬垱寤� BPM 宸ヤ綔娴侊紱璁㈠崟鐘舵�佺敱 directApprove 鐩存帴鍐冲畾锛�
-     * 鍕鹃�夆�滄槸鍚﹀鏍糕�� => 宸插鏍�(20)锛屾湭鍕鹃�� => 鏈鏍�(10)銆�
+     * 鏍¢獙骞剁粍瑁呬竴寮犲鍏ヨ鍗曪紙涓嶈惤搴擄級銆�
      */
-    private ErpPurchaseOrderDO createPurchaseOrderByImport(List<ErpPurchaseOrderImportExcelVO> rows,
-                                                           Map<String, Long> supplierIdMap, Boolean directApprove) {
+    private ImportOrderData prepareImportOrder(List<ErpPurchaseOrderImportExcelVO> rows,
+                                               Map<String, Long> supplierIdMap) {
         // 1. 渚涘簲鍟嗭紙鍙栬缁勯琛岋級
         ErpPurchaseOrderImportExcelVO head = rows.get(0);
         if (StrUtil.isBlank(head.getSupplierName())) {
@@ -278,31 +337,145 @@
             items.add(itemVO);
         }
 
-        // 3. 鍗曞彿锛氬~浜嗗氨鐢紙鏍¢獙鍞竴锛夛紝娌″~鑷姩鐢熸垚
-        String no = StrUtil.isNotBlank(head.getNo()) ? head.getNo().trim()
-                : noRedisDAO.generate(ErpNoRedisDAO.PURCHASE_ORDER_NO_PREFIX);
-        if (purchaseOrderMapper.selectByNo(no) != null) {
-            throw new IllegalArgumentException("閲囪喘鍗曠紪鍙峰凡瀛樺湪锛�" + no);
-        }
+        // 3. 鍗曞彿锛氬~浜嗗氨鐢紱鐣欑┖鏃跺厛涓嶇敓鎴愶紝绛夌‘璁よ鏂板鏃跺啀鐢熸垚锛岄伩鍏嶉噸澶嶈琚烦杩囦篃娑堣�楁祦姘村彿
+        boolean generatedNo = StrUtil.isBlank(head.getNo());
 
-        // 4. 缁勮涓昏〃锛氬嬀閫夆�滄槸鍚﹀鏍糕�� => 宸插鏍�(20)锛屽惁鍒欐湭瀹℃牳(10)
+        // 4. 缁勮涓昏〃锛堢姸鎬佺敱璋冪敤鏂瑰喅瀹氾級
         ErpPurchaseOrderDO purchaseOrder = new ErpPurchaseOrderDO();
-        purchaseOrder.setNo(no);
+        purchaseOrder.setNo(generatedNo ? null : head.getNo().trim());
         purchaseOrder.setSupplierId(supplierId);
         purchaseOrder.setOrderTime(parseImportOrderTime(head.getOrderTime()));
         purchaseOrder.setRemark(head.getRemark());
-        purchaseOrder.setStatus(Boolean.TRUE.equals(directApprove)
-                ? ErpAuditStatus.APPROVE.getStatus() : ErpAuditStatus.PROCESS.getStatus());
         purchaseOrder.setDepositPrice(BigDecimal.ZERO); // deposit_price 涓� NOT NULL 涓旀棤榛樿鍊硷紝蹇呴』鏄惧紡璧嬪��
-        purchaseOrder.setInCount(BigDecimal.ZERO);
-        purchaseOrder.setInStatus(ErpPurchaseOrderInStatusEnum.NOT_IN.getStatus());
 
         List<ErpPurchaseOrderItemDO> itemDOs = validatePurchaseOrderItems(items);
         calculateTotalPrice(purchaseOrder, itemDOs);
+        return new ImportOrderData(purchaseOrder, itemDOs,
+                buildOrderFingerprint(supplierId, itemDOs), generatedNo,
+                StrUtil.isNotBlank(head.getOrderTime()));
+    }
+
+    /**
+     * 鏂板瀵煎叆璁㈠崟锛堝惈鏄庣粏锛夈��
+     */
+    private void createPurchaseOrderByImport(ImportOrderData importData, Boolean directApprove) {
+        ErpPurchaseOrderDO purchaseOrder = importData.order;
+        if (StrUtil.isBlank(purchaseOrder.getNo())) {
+            purchaseOrder.setNo(noRedisDAO.generate(ErpNoRedisDAO.PURCHASE_ORDER_NO_PREFIX));
+        }
+        purchaseOrder.setStatus(Boolean.TRUE.equals(directApprove)
+                ? ErpAuditStatus.APPROVE.getStatus() : ErpAuditStatus.PROCESS.getStatus());
+        purchaseOrder.setInCount(BigDecimal.ZERO);
+        purchaseOrder.setInStatus(ErpPurchaseOrderInStatusEnum.NOT_IN.getStatus());
         purchaseOrderMapper.insert(purchaseOrder);
-        itemDOs.forEach(item -> item.setOrderId(purchaseOrder.getId()));
-        purchaseOrderItemMapper.insertBatch(itemDOs);
-        return purchaseOrder;
+        importData.items.forEach(item -> item.setOrderId(purchaseOrder.getId()));
+        purchaseOrderItemMapper.insertBatch(importData.items);
+    }
+
+    /**
+     * 瑕嗙洊宸叉湁瀵煎叆璁㈠崟锛堝惈鏄庣粏锛夈��
+     */
+    private void updatePurchaseOrderByImport(ErpPurchaseOrderDO existing, ImportOrderData importData,
+                                             Boolean directApprove) {
+        // 1. 鏈変笅娓稿崟鎹殑璁㈠崟涓嶅厑璁歌鐩栵紝閬垮厤鎶婂凡鍏ュ簱/宸插紑绁ㄧ殑鏁版嵁鏀规帀
+        if (existing.getInCount() != null && existing.getInCount().compareTo(BigDecimal.ZERO) > 0) {
+            throw new IllegalArgumentException("閲囪喘璁㈠崟[" + existing.getNo() + "]宸插叆搴擄紙宸插叆搴撴暟閲� "
+                    + normalizeNumber(existing.getInCount()) + "锛夛紝涓嶈兘瑕嗙洊锛涜鍏堝垹闄ゅ搴斿叆搴撹褰曞悗鍐嶅鍏�");
+        }
+        if (existing.getReturnCount() != null && existing.getReturnCount().compareTo(BigDecimal.ZERO) > 0) {
+            throw new IllegalArgumentException("閲囪喘璁㈠崟[" + existing.getNo() + "]宸查��璐э紙宸查��璐ф暟閲� "
+                    + normalizeNumber(existing.getReturnCount()) + "锛夛紝涓嶈兘瑕嗙洊锛涜鍏堝垹闄ゅ搴旈��璐ц褰曞悗鍐嶅鍏�");
+        }
+        if (CollUtil.isNotEmpty(invoiceOrderMapper.selectListByPurchaseOrderId(existing.getId()))) {
+            throw new IllegalArgumentException("閲囪喘璁㈠崟[" + existing.getNo()
+                    + "]宸插叧鑱斿彂绁紝涓嶈兘瑕嗙洊锛涜鍏堣В闄ゅ彂绁ㄥ叧鑱斿悗鍐嶅鍏�");
+        }
+        if (StrUtil.isNotBlank(existing.getProcessInstanceId())) {
+            throw new IllegalArgumentException("閲囪喘璁㈠崟[" + existing.getNo()
+                    + "]宸叉彁浜ゅ鎵规祦绋嬶紝涓嶈兘瑕嗙洊锛涜鍏堝湪鍗忓悓鍔炲叕鎾ゅ洖鎴栦綔搴熻瀹℃壒鍚庡啀瀵煎叆");
+        }
+        List<ErpPurchaseOrderItemDO> oldItems = purchaseOrderItemMapper.selectListByOrderId(existing.getId());
+        boolean linkedRequest = oldItems.stream().anyMatch(item -> item.getRequestItemId() != null);
+        if (linkedRequest) {
+            throw new IllegalArgumentException("閲囪喘璁㈠崟[" + existing.getNo()
+                    + "]宸插叧鑱旈噰璐敵璇凤紝涓嶈兘瑕嗙洊锛涜鍏堝湪閲囪喘鐢宠涓挙閿�鎴栧垹闄ゅ搴旂敵璇峰悗鍐嶅鍏�");
+        }
+
+        // 2. 鏇存柊涓昏〃锛堜繚鐣欏師鍗曞彿銆佸師鍏ュ簱鐘舵�侊級
+        ErpPurchaseOrderDO updateObj = importData.order;
+        updateObj.setId(existing.getId());
+        updateObj.setNo(existing.getNo());
+        // 鏂囦欢娌″~閲囪喘鏃堕棿鏃朵繚鐣欏師璁㈠崟鏃堕棿锛岄伩鍏嶈鐩栨椂鎶婃棩鏈熸敼鎴愬綋鍓嶆椂闂�
+        if (!importData.orderTimeProvided) {
+            updateObj.setOrderTime(existing.getOrderTime());
+        }
+        updateObj.setStatus(Boolean.TRUE.equals(directApprove)
+                ? ErpAuditStatus.APPROVE.getStatus() : ErpAuditStatus.PROCESS.getStatus());
+        updateObj.setInCount(existing.getInCount() == null ? BigDecimal.ZERO : existing.getInCount());
+        updateObj.setInStatus(existing.getInStatus());
+        purchaseOrderMapper.updateById(updateObj);
+
+        // 3. 鏄庣粏鏁寸粍鏇挎崲
+        purchaseOrderItemMapper.deleteByOrderId(existing.getId());
+        importData.items.forEach(item -> item.setOrderId(existing.getId()));
+        purchaseOrderItemMapper.insertBatch(importData.items);
+    }
+
+    private ErpPurchaseOrderImportDuplicateVO buildImportDuplicate(String label, ErpPurchaseOrderDO existing,
+                                                                   String reason) {
+        return ErpPurchaseOrderImportDuplicateVO.builder()
+                .key(label)
+                .existingNo(existing.getNo())
+                .existingId(existing.getId())
+                .reason(reason)
+                .build();
+    }
+
+    /**
+     * 鐢熸垚鈥滀緵搴斿晢 + 瀹屾暣鏄庣粏鈥濈殑璁㈠崟鎸囩汗锛岀敤浜庤瘑鍒噸澶嶅鍏ャ��
+     * 鏄庣粏鎸夌墿鏂欍�佹暟閲忋�佸崟浠枫�佺◣鐜囨帓搴忓悗姣旇緝锛屼笉鍙楄椤哄簭褰卞搷銆�
+     */
+    private String buildOrderFingerprint(Long supplierId, List<ErpPurchaseOrderItemDO> items) {
+        if (supplierId == null || CollUtil.isEmpty(items)) {
+            return null;
+        }
+        List<String> parts = items.stream()
+                .map(item -> String.join(":",
+                        String.valueOf(item.getProductId()),
+                        normalizeNumber(item.getCount()),
+                        normalizeNumber(item.getProductPrice()),
+                        normalizeNumber(item.getTaxPercent())))
+                .sorted()
+                .toList();
+        return supplierId + "|" + String.join(",", parts);
+    }
+
+    private String normalizeNumber(BigDecimal value) {
+        return value == null ? "" : value.stripTrailingZeros().toPlainString();
+    }
+
+    private static class ImportOrderData {
+
+        private final ErpPurchaseOrderDO order;
+        private final List<ErpPurchaseOrderItemDO> items;
+        private final String fingerprint;
+        /**
+         * 閲囪喘鍗曠紪鍙锋槸鍚︾敱绯荤粺鑷姩鐢熸垚锛堟枃浠堕噷鐣欑┖锛�
+         */
+        private final boolean generatedNo;
+        /**
+         * 鏂囦欢閲屾槸鍚﹀~鍐欎簡閲囪喘鏃堕棿
+         */
+        private final boolean orderTimeProvided;
+
+        private ImportOrderData(ErpPurchaseOrderDO order, List<ErpPurchaseOrderItemDO> items, String fingerprint,
+                                boolean generatedNo, boolean orderTimeProvided) {
+            this.order = order;
+            this.items = items;
+            this.fingerprint = fingerprint;
+            this.generatedNo = generatedNo;
+            this.orderTimeProvided = orderTimeProvided;
+        }
     }
 
     /**

--
Gitblit v1.9.3