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