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/ErpPurchaseRequestServiceImpl.java |  217 +++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 files changed, 212 insertions(+), 5 deletions(-)

diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseRequestServiceImpl.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseRequestServiceImpl.java
index 314951b..fef7597 100644
--- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseRequestServiceImpl.java
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseRequestServiceImpl.java
@@ -17,6 +17,7 @@
 import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestSaveReqVO;
 import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestImportExcelVO;
 import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestImportRespVO;
+import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestSubmitBatchRespVO;
 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;
@@ -39,6 +40,8 @@
 import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
 import org.flowable.engine.repository.ProcessDefinition;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
@@ -100,6 +103,13 @@
     private DeptApi deptApi;
     @Resource
     private StorageAttachmentApi storageAttachmentApi;
+
+    /**
+     * 鑷韩浠g悊锛氭壒閲忔彁浜ゆ椂閫愬崟璋冪敤 {@link #submitPurchaseRequest}锛屼繚璇佹瘡寮犵敵璇风嫭绔嬩簨鍔�
+     */
+    @Autowired
+    @Lazy
+    private ErpPurchaseRequestService self;
 
     @Override
     @Transactional(rollbackFor = Exception.class)
@@ -450,13 +460,17 @@
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void submitPurchaseRequest(Long id, String processDefinitionKey, Long userId) {
-        // 1. 鏍¢獙瀛樺湪 + 鑽夌鐘舵��
-        ErpPurchaseRequestDO purchaseRequest = validatePurchaseRequestCanModify(id);
+        // 1. 鏍¢獙瀛樺湪 + 鑽夌/椹冲洖鐘舵��
+        ErpPurchaseRequestDO purchaseRequest = validatePurchaseRequestExists(id);
+        if (!ErpPurchaseRequestStatusEnum.DRAFT.getStatus().equals(purchaseRequest.getStatus())
+                && !ErpPurchaseRequestStatusEnum.REJECT.getStatus().equals(purchaseRequest.getStatus())) {
+            throw exception(PURCHASE_REQUEST_SUBMIT_FAIL_NOT_DRAFT, purchaseRequest.getNo());
+        }
 
         // 2. 鏍¢獙鏄惁鏈夋槑缁�
         List<ErpPurchaseRequestItemDO> items = purchaseRequestItemMapper.selectListByRequestId(id);
         if (CollUtil.isEmpty(items)) {
-            throw exception(PURCHASE_REQUEST_NOT_EXISTS); // TODO: 娣诲姞鏄庣粏涓虹┖鐨勯敊璇爜
+            throw exception(PURCHASE_REQUEST_ITEM_EMPTY, purchaseRequest.getNo());
         }
 
         // 3. 鍒涘缓 BPM 娴佺▼瀹炰緥
@@ -470,6 +484,52 @@
                 .setId(id)
                 .setStatus(ErpPurchaseRequestStatusEnum.PROCESS.getStatus())
                 .setProcessInstanceId(processInstanceId));
+    }
+
+    @Override
+    public ErpPurchaseRequestSubmitBatchRespVO submitPurchaseRequestBatch(List<Long> ids,
+                                                                          String processDefinitionKey,
+                                                                          Long userId) {
+        ErpPurchaseRequestSubmitBatchRespVO respVO = ErpPurchaseRequestSubmitBatchRespVO.builder()
+                .successIds(new ArrayList<>())
+                .failureMap(new LinkedHashMap<>())
+                .build();
+        if (CollUtil.isEmpty(ids)) {
+            return respVO;
+        }
+        List<Long> distinctIds = ids.stream().filter(Objects::nonNull).distinct().toList();
+        if (CollUtil.isEmpty(distinctIds)) {
+            return respVO;
+        }
+
+        // 1. 棰勭儹鐢宠鍗曞彿锛屽け璐ユ椂鍓嶇鍙洿鎺ョ湅鍒版槸鍝紶鐢宠鍗�
+        Map<Long, String> noMap = purchaseRequestMapper.selectRequestListByIds(distinctIds).stream()
+                .filter(request -> request.getId() != null && StrUtil.isNotBlank(request.getNo()))
+                .collect(java.util.stream.Collectors.toMap(ErpPurchaseRequestDO::getId, ErpPurchaseRequestDO::getNo,
+                        (a, b) -> a));
+
+        // 2. 閫愬崟鎻愪氦锛氬崟寮犲け璐ヤ笉褰卞搷鍏跺畠鐢宠锛岄�氳繃 self 浠g悊淇濊瘉姣忓紶鐙珛浜嬪姟
+        for (Long id : distinctIds) {
+            String label = noMap.getOrDefault(id, "缂栧彿 " + id);
+            try {
+                self.submitPurchaseRequest(id, processDefinitionKey, userId);
+                respVO.getSuccessIds().add(id);
+            } catch (Exception ex) {
+                String reason = resolveSubmitErrorMessage(ex);
+                if (respVO.getFailureMap().containsKey(label)) {
+                    label = label + "锛堢紪鍙� " + id + "锛�";
+                }
+                respVO.getFailureMap().put(label, reason);
+            }
+        }
+        return respVO;
+    }
+
+    private String resolveSubmitErrorMessage(Exception ex) {
+        if (ex instanceof cn.iocoder.yudao.framework.common.exception.ServiceException serviceException) {
+            return serviceException.getMessage();
+        }
+        return StrUtil.blankToDefault(ex.getMessage(), "鎻愪氦澶辫触");
     }
 
     @Override
@@ -572,10 +632,20 @@
         // 1. 鑾峰彇鐢宠鏄庣粏
         List<ErpPurchaseRequestItemDO> requestItems = purchaseRequestItemMapper.selectListByRequestId(id);
         if (CollUtil.isEmpty(requestItems)) {
-            throw exception(PURCHASE_REQUEST_NOT_EXISTS);
+            throw exception(PURCHASE_REQUEST_ITEM_EMPTY, purchaseRequest.getNo());
         }
 
-        // 2. 鐢熸垚閲囪喘璁㈠崟鍙�
+        // 2. 鍏堝皾璇曞鐢ㄧ郴缁熷唴宸插瓨鍦ㄤ笖渚涘簲鍟嗐�佹槑缁嗗畬鍏ㄤ竴鑷寸殑宸插鏍搁噰璐鍗曪紙閫氬父鏉ヨ嚜閲囪喘璁㈠崟瀵煎叆锛夛紝
+        //    閬垮厤瀹℃壒閫氳繃鍚庡張鐢熸垚涓�寮犻噸澶嶈鍗�
+        ErpPurchaseOrderDO reusableOrder = findReusablePurchaseOrder(purchaseRequest, requestItems);
+        if (reusableOrder != null) {
+            linkPurchaseRequestToOrder(purchaseRequest, requestItems, reusableOrder);
+            log.info("[doGeneratePurchaseOrder] 閲囪喘鐢宠({}) 澶嶇敤宸叉湁閲囪喘璁㈠崟({})锛屾湭閲嶅鐢熸垚",
+                    id, reusableOrder.getNo());
+            return reusableOrder.getId();
+        }
+
+        // 3. 鐢熸垚閲囪喘璁㈠崟鍙�
         String orderNo = noRedisDAO.generate(ErpNoRedisDAO.PURCHASE_ORDER_NO_PREFIX);
         if (purchaseOrderMapper.selectByNo(orderNo) != null) {
             throw exception(PURCHASE_ORDER_NO_EXISTS);
@@ -628,6 +698,143 @@
     }
 
     /**
+     * 鏌ユ壘鍙鐢ㄧ殑閲囪喘璁㈠崟锛氫緵搴斿晢鐩稿悓锛屼笖鏄庣粏锛堢墿鏂欍�佹暟閲忋�佸崟浠枫�佺◣鐜囷級涓庣敵璇峰畬鍏ㄤ竴鑷淬��
+     *
+     * 鍙尮閰嶅凡瀹℃牳璁㈠崟锛岄伩鍏嶆妸鐢宠鎸傚埌灏氭湭瀹℃牳鐨勮鍗曚笂锛涗紭鍏堥�夋嫨灏氭湭鍏宠仈閲囪喘鐢宠鐨勫鍏ヨ鍗曪紝
+     * 鍏舵閫夋嫨缂栧彿鏈�灏忕殑璁㈠崟銆傚尮閰嶅埌鐨勮鍗曚細鍔犺閿侊紝闃叉骞跺彂瀹℃壒鏃朵袱寮犵敵璇峰悓鏃跺崰鐢ㄥ悓涓�寮犺鍗曘��
+     */
+    private ErpPurchaseOrderDO findReusablePurchaseOrder(ErpPurchaseRequestDO purchaseRequest,
+                                                         List<ErpPurchaseRequestItemDO> requestItems) {
+        if (purchaseRequest.getSupplierId() == null || CollUtil.isEmpty(requestItems)) {
+            return null;
+        }
+        String targetFingerprint = buildRequestFingerprint(purchaseRequest.getSupplierId(), requestItems);
+        if (targetFingerprint == null) {
+            return null;
+        }
+
+        // 1. 鏌ヨ鍚屼緵搴斿晢鐨勫凡瀹℃牳璁㈠崟锛屾寜鎸囩汗绛涢�夊畬鍏ㄤ竴鑷寸殑
+        List<ErpPurchaseOrderDO> orders = purchaseOrderMapper.selectList(
+                purchaseRequest.getSupplierId(), ErpAuditStatus.APPROVE.getStatus());
+        if (CollUtil.isEmpty(orders)) {
+            return null;
+        }
+        Map<Long, List<ErpPurchaseOrderItemDO>> orderItemMap = purchaseOrderItemMapper.selectListByOrderIds(
+                        orders.stream().map(ErpPurchaseOrderDO::getId).toList()).stream()
+                .collect(java.util.stream.Collectors.groupingBy(ErpPurchaseOrderItemDO::getOrderId));
+        List<ErpPurchaseOrderDO> candidates = orders.stream()
+                .filter(order -> targetFingerprint.equals(
+                        buildOrderFingerprint(order.getSupplierId(), orderItemMap.get(order.getId()))))
+                // 宸插叆搴�/宸查��璐х殑璁㈠崟璇存槑杩欑瑪閲囪喘宸茬粡鎵ц杩囷紝涓嶈兘鎷挎潵椤舵柊鐢宠锛屽惁鍒欐柊闇�姹備細琚�滃悆鎺夆��
+                .filter(order -> order.getInCount() == null
+                        || order.getInCount().compareTo(BigDecimal.ZERO) == 0)
+                .filter(order -> order.getReturnCount() == null
+                        || order.getReturnCount().compareTo(BigDecimal.ZERO) == 0)
+                .sorted(Comparator
+                        .comparing((ErpPurchaseOrderDO order) ->
+                                hasLinkedRequestItem(orderItemMap.get(order.getId())) ? 1 : 0)
+                        .thenComparing(ErpPurchaseOrderDO::getId))
+                .toList();
+        if (CollUtil.isEmpty(candidates)) {
+            return null;
+        }
+        if (candidates.size() > 1) {
+            log.warn("[findReusablePurchaseOrder] 閲囪喘鐢宠({}) 鍖归厤鍒� {} 寮犵浉鍚岃鍗曪細{}锛屽皢澶嶇敤鏈�鏃╃殑涓�寮�",
+                    purchaseRequest.getId(), candidates.size(),
+                    candidates.stream().map(ErpPurchaseOrderDO::getNo).toList());
+        }
+
+        // 2. 鍔犺閿佸悗閲嶆柊鍔犺浇锛岄伩鍏嶅苟鍙戝鎵归噸澶嶅崰鐢�
+        ErpPurchaseOrderDO candidate = purchaseOrderMapper.selectByIdForUpdate(candidates.get(0).getId());
+        if (candidate == null || !ErpAuditStatus.APPROVE.getStatus().equals(candidate.getStatus())) {
+            return null;
+        }
+        return candidate;
+    }
+
+    /**
+     * 灏嗛噰璐敵璇峰叧鑱斿埌宸插瓨鍦ㄧ殑閲囪喘璁㈠崟锛屽苟鎶婅鍗曟槑缁嗗洖濉敵璇锋槑缁嗙紪鍙枫��
+     * 宸茶鍏跺畠鐢宠鍏宠仈杩囩殑鏄庣粏涓嶉噸澶嶅洖濉紝鍙缓绔嬬敵璇峰崟涓庤鍗曠殑鍏宠仈銆�
+     */
+    private void linkPurchaseRequestToOrder(ErpPurchaseRequestDO purchaseRequest,
+                                            List<ErpPurchaseRequestItemDO> requestItems,
+                                            ErpPurchaseOrderDO purchaseOrder) {
+        List<ErpPurchaseOrderItemDO> orderItems = purchaseOrderItemMapper.selectListByOrderId(purchaseOrder.getId());
+        List<ErpPurchaseOrderItemDO> remaining = new ArrayList<>(orderItems);
+        List<ErpPurchaseOrderItemDO> updates = new ArrayList<>();
+        for (ErpPurchaseRequestItemDO requestItem : requestItems) {
+            ErpPurchaseOrderItemDO match = remaining.stream()
+                    .filter(orderItem -> orderItem.getRequestItemId() == null)
+                    .filter(orderItem -> Objects.equals(orderItem.getProductId(), requestItem.getProductId())
+                            && numberEquals(orderItem.getCount(), requestItem.getCount())
+                            && numberEquals(orderItem.getProductPrice(), requestItem.getProductPrice())
+                            && numberEquals(orderItem.getTaxPercent(), requestItem.getTaxPercent()))
+                    .findFirst().orElse(null);
+            if (match != null) {
+                match.setRequestItemId(requestItem.getId());
+                updates.add(match);
+                remaining.remove(match);
+            }
+        }
+        if (CollUtil.isNotEmpty(updates)) {
+            purchaseOrderItemMapper.updateBatch(updates);
+        }
+
+        purchaseRequestMapper.updateById(new ErpPurchaseRequestDO()
+                .setId(purchaseRequest.getId())
+                .setOrderId(purchaseOrder.getId())
+                .setOrderNo(purchaseOrder.getNo()));
+        log.info("[linkPurchaseRequestToOrder] 閲囪喘鐢宠({}) 鍏宠仈閲囪喘璁㈠崟({})锛屽洖濉槑缁� {} 鏉�",
+                purchaseRequest.getId(), purchaseOrder.getNo(), updates.size());
+    }
+
+    private String buildRequestFingerprint(Long supplierId, List<ErpPurchaseRequestItemDO> items) {
+        if (supplierId == null || CollUtil.isEmpty(items)) {
+            return null;
+        }
+        return buildFingerprint(supplierId, items.stream()
+                .map(item -> fingerprintPart(item.getProductId(), item.getCount(),
+                        item.getProductPrice(), item.getTaxPercent()))
+                .toList());
+    }
+
+    private String buildOrderFingerprint(Long supplierId, List<ErpPurchaseOrderItemDO> items) {
+        if (supplierId == null || CollUtil.isEmpty(items)) {
+            return null;
+        }
+        return buildFingerprint(supplierId, items.stream()
+                .map(item -> fingerprintPart(item.getProductId(), item.getCount(),
+                        item.getProductPrice(), item.getTaxPercent()))
+                .toList());
+    }
+
+    private String buildFingerprint(Long supplierId, List<String> parts) {
+        return supplierId + "|" + parts.stream().sorted().collect(java.util.stream.Collectors.joining(","));
+    }
+
+    private String fingerprintPart(Long productId, BigDecimal count, BigDecimal productPrice,
+                                   BigDecimal taxPercent) {
+        return String.join(":", String.valueOf(productId), normalizeNumber(count),
+                normalizeNumber(productPrice), normalizeNumber(taxPercent));
+    }
+
+    private String normalizeNumber(BigDecimal value) {
+        return value == null ? "" : value.stripTrailingZeros().toPlainString();
+    }
+
+    private boolean numberEquals(BigDecimal left, BigDecimal right) {
+        if (left == null) {
+            return right == null;
+        }
+        return right != null && left.compareTo(right) == 0;
+    }
+
+    private boolean hasLinkedRequestItem(List<ErpPurchaseOrderItemDO> orderItems) {
+        return CollUtil.isNotEmpty(orderItems)
+                && orderItems.stream().anyMatch(item -> item.getRequestItemId() != null);
+    }
+
+    /**
      * 鏍¢獙閲囪喘鐢宠瀹℃壒鍒嗙被鍜屾祦绋嬪畾涔夋槸鍚﹀瓨鍦�
      */
     private void validatePurchaseRequestApproveCategoryAndProcessDefinition() {

--
Gitblit v1.9.3