From 692c73e703003b9f4517ec54f886be21ba08e2d7 Mon Sep 17 00:00:00 2001 From: fengyuan_yang <1976974459@qq.com> Date: Thu, 12 Mar 2026 11:24:27 +0800 Subject: [PATCH] =?UTF-8?q?2026-03-12=201=E3=80=81=E8=AF=A2=E4=BB=B7?= =?UTF-8?q?=E7=94=B3=E8=AF=B7=E5=A2=9E=E5=8A=A0=E2=80=9C=E5=9B=BE=E7=BA=B8?= =?UTF-8?q?=E5=AD=98=E6=94=BE=E7=9B=AE=E5=BD=95=E2=80=9D=EF=BC=8C=E5=B9=B6?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=8A=82=E7=82=B9=E7=89=B9=E6=AE=8A=E6=9D=83?= =?UTF-8?q?=E9=99=90=202=E3=80=81=E4=BC=98=E5=8C=96=E6=8A=A5=E4=BB=B7?= =?UTF-8?q?=E3=80=90=E5=88=87=E6=8D=A2=E7=89=88=E6=9C=AC=E3=80=91=E8=B6=85?= =?UTF-8?q?=E6=97=B6=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entity/QuotationInformationEntity.java | 2 + .../impl/QuotationInformationServiceImpl.java | 29 +++- .../impl/QuoteDetailBomTreeServiceImpl.java | 126 ++++++++++++++---- .../quotation/QuotationInformationMapper.xml | 4 + 4 files changed, 133 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/spring/modules/quotation/entity/QuotationInformationEntity.java b/src/main/java/com/spring/modules/quotation/entity/QuotationInformationEntity.java index dc63200a..3fc904e7 100644 --- a/src/main/java/com/spring/modules/quotation/entity/QuotationInformationEntity.java +++ b/src/main/java/com/spring/modules/quotation/entity/QuotationInformationEntity.java @@ -185,4 +185,6 @@ public class QuotationInformationEntity extends QueryPage implements Serializabl private String place; + private String drawingStorageDirectory; + } diff --git a/src/main/java/com/spring/modules/quotation/service/impl/QuotationInformationServiceImpl.java b/src/main/java/com/spring/modules/quotation/service/impl/QuotationInformationServiceImpl.java index 5ef709de..f565b3b9 100644 --- a/src/main/java/com/spring/modules/quotation/service/impl/QuotationInformationServiceImpl.java +++ b/src/main/java/com/spring/modules/quotation/service/impl/QuotationInformationServiceImpl.java @@ -663,6 +663,17 @@ public class QuotationInformationServiceImpl extends ServiceImpl().eq("site", data.getSite()).eq("quotation_no", data.getQuotationNo())); + // 流程特殊管控:下达前检查节点权限配置 + PlmProcessControllBaseData issuedCData = new PlmProcessControllBaseData(); + issuedCData.setSite(data.getSite()); + issuedCData.setNodeId(nodeDetails.get(0).getNodeId()); + issuedCData.setWorkflowId(baseData.get("workflowId")); + List issuedControlList = requestManageService.getProcessSelect(issuedCData); + if (!issuedControlList.isEmpty()) { + for (PlmProcessControllBaseData ctrl : issuedControlList) { + checkProcessControl(ctrl.getRoleId(), ctrl.getSite(), data.getQuotationBatchNo(), data.getQuotationAmount(), changeRequest.getDrawingStorageDirectory()); + } + } // 根据字段对应的数据库表+字段查询出数据 for (PlmRequestDetailVo nodeDetail : nodeDetails) { if (nodeDetail.getId() == null) { @@ -815,7 +826,7 @@ public class QuotationInformationServiceImpl extends ServiceImpl controlList = requestManageService.getProcessSelect(cData); if (!controlList.isEmpty()) { for (int i = 0; i < controlList.size(); i++) { - checkProcessControl(controlList.get(i).getRoleId(), controlList.get(i).getSite(), data.getQuotationBatchNo(), data.getQuotationAmount()); + checkProcessControl(controlList.get(i).getRoleId(), controlList.get(i).getSite(), data.getQuotationBatchNo(), data.getQuotationAmount(), changeRequest.getDrawingStorageDirectory()); } } @@ -894,6 +905,22 @@ public class QuotationInformationServiceImpl extends ServiceImpl() + .eq("order_ref1", site) + .eq("order_ref2", quotationBatchNo) + ); + boolean hasDrawingDir = org.springframework.util.StringUtils.hasText(drawingStorageDirectory); + if (ossCount == 0 && !hasDrawingDir) { + throw new RuntimeException("附件信息页签必须有数据或填写图纸存放目录,才能提交!"); + } + } + } + /** * 流程干预提交 * @param data diff --git a/src/main/java/com/spring/modules/quote/service/impl/QuoteDetailBomTreeServiceImpl.java b/src/main/java/com/spring/modules/quote/service/impl/QuoteDetailBomTreeServiceImpl.java index 75ea0391..9dbfd163 100644 --- a/src/main/java/com/spring/modules/quote/service/impl/QuoteDetailBomTreeServiceImpl.java +++ b/src/main/java/com/spring/modules/quote/service/impl/QuoteDetailBomTreeServiceImpl.java @@ -25,6 +25,8 @@ import org.springframework.util.StringUtils; import java.math.BigDecimal; import java.util.*; +import java.util.concurrent.*; +import java.util.function.Supplier; import java.util.stream.Collectors; @Service @@ -50,6 +52,13 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl { Thread t = new Thread(r, "ifs-cost-query"); t.setDaemon(true); return t; } + ); + public QuoteDetailBomTreeServiceImpl(IfsServer ifsServer, @Value("${ifs-control.ifs-username}")String ifsUsername, @Value("${ifs-control.ifs-password}") String ifsPassword) { @@ -90,9 +99,9 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl ifsConFactory = resolveIfsConFactory(); + BomNodeData rootNode = collectBomData(detail, parentId, level, ifsConFactory); if (rootNode == null) { return 0; } @@ -107,8 +116,16 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl执行分两个阶段: + *
    + *
  1. 串行阶段:构建 BOM 层级结构(递归,有顺序依赖)
  2. + *
  3. 并行阶段:并发查询各子物料 IFS 成本(互相独立,{@value IFS_PARALLEL_SIZE} 路并发)
  4. + *
+ * + * @param ifsConFactory IFS 连接工厂,每个并发线程调用 get() 获取独立连接,避免共享连接线程安全问题 */ - private BomNodeData collectBomData(QuoteDetail detail, Long parentId, Integer level, Server ifsCon) { + private BomNodeData collectBomData(QuoteDetail detail, Long parentId, Integer level, Supplier ifsConFactory) { // 确定 BOM 类型 if (parentId.equals(0L)) { detail.setBomType("Manufacturing"); @@ -150,7 +167,7 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl "Y".equals(c.getStatus())).count(); - log.info("[BOM_PROCESS] 开始处理子物料 - 父物料: {}, 层级: {}, 子物料总数: {}, 需查询成本数: {}", + log.info("[BOM_PROCESS] 开始处理子物料 - 父物料: {}, 层级: {}, 子物料总数: {}, 需并行查询成本数: {}", bom.getPartNo(), level, totalComponents, needCostQueryCount); BomNodeData nodeData = new BomNodeData(); @@ -158,9 +175,7 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl 0) { - costQuerySuccessCount++; - } else { - costQueryFailCount++; - } + nodeData.components.add(component); + } + + // ── 并行阶段:并发查询各子物料 IFS 成本(互相独立,可并行)──────────────────── + // 与原逻辑保持一致:无论是否半成品,只要是正式物料(status="Y")都查询 IFS 参考成本 + // 半成品的 IFS 参考成本用于在材料页签中展示,成本计算仍以 BOM 明细为准 + List needCostComponents = componentParts.stream() + .filter(c -> "Y".equals(c.getStatus())) + .collect(Collectors.toList()); + + if (!needCostComponents.isEmpty()) { + // 预建连接池:最多 IFS_PARALLEL_SIZE 个连接,避免对 IFS 连接数造成冲击 + int poolSize = Math.min(needCostComponents.size(), IFS_PARALLEL_SIZE); + BlockingQueue connectionPool = new LinkedBlockingQueue<>(); + for (int i = 0; i < poolSize; i++) { + connectionPool.add(ifsConFactory.get()); } - nodeData.components.add(component); + List> futures = needCostComponents.stream() + .map(component -> CompletableFuture.runAsync(() -> { + Server conn = null; + try { + conn = connectionPool.take(); + getFinalPartCost(component, conn); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.warn("[BOM_PROCESS] 并行成本查询线程被中断 - PartNo: {}", component.getComponentPart()); + } finally { + if (conn != null) { + connectionPool.offer(conn); + } + } + }, IFS_COST_EXECUTOR)) + .collect(Collectors.toList()); + + try { + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).get(120, TimeUnit.SECONDS); + } catch (TimeoutException e) { + futures.forEach(f -> f.cancel(true)); + log.warn("[BOM_PROCESS] 并行成本查询超时(120s),部分物料成本可能为0 - 父物料: {}", bom.getPartNo()); + } catch (ExecutionException e) { + Throwable cause = e.getCause(); + if (cause instanceof RuntimeException) throw (RuntimeException) cause; + log.warn("[BOM_PROCESS] 并行成本查询异常: {}", e.getMessage()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.warn("[BOM_PROCESS] 并行成本查询主线程被中断 - 父物料: {}", bom.getPartNo()); + } } + // 统计成本查询结果 + int costQuerySuccessCount = 0; + int costQueryFailCount = 0; + for (QuoteDetailBom component : needCostComponents) { + if (component.getUnitPrice() != null && component.getUnitPrice().compareTo(BigDecimal.ZERO) > 0) { + costQuerySuccessCount++; + } else { + costQueryFailCount++; + } + } log.info("[BOM_PROCESS] 子物料处理完成 - 父物料: {}, 层级: {}, 处理总数: {}, 成本查询成功: {}, 成本查询失败: {}", bom.getPartNo(), level, costQuerySuccessCount + costQueryFailCount, costQuerySuccessCount, costQueryFailCount); @@ -281,10 +342,10 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl ifsConFactory = resolveIfsConFactory(); Long newParentId = bomTree.getParentId(); Integer newLevel = Optional.ofNullable(tree.getLevel()).orElse(0); - BomNodeData nodeData = collectBomData(detail, newParentId, newLevel, ifsCon); + BomNodeData nodeData = collectBomData(detail, newParentId, newLevel, ifsConFactory); // 数据已就绪,开始执行 DML(事务持有锁的时间仅限于纯 DB 写入阶段) List ids = getAllChildIds(detail, bomTree.getId()); @@ -315,8 +376,8 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl ifsConFactory = resolveIfsConFactory(); + BomNodeData nodeData = collectBomData(detail, 0L, 0, ifsConFactory); if (nodeData != null) { doSaveBomDataRecursive(nodeData, detail, 0L); } @@ -542,8 +603,12 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl resolveIfsConFactory() { String username = ((SysUserEntity) SecurityUtils.getSubject().getPrincipal()).getUsername(); SysUserEntity ifsUser = sysUserDao.selectOne(new QueryWrapper().eq("username", username)); if (ifsUser == null @@ -551,7 +616,14 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl ifsServer.getIfsServer(ifsUsername, ifsPassword); + } + + /** 获取当前登录用户的 IFS Server 连接(单连接场景,在事务外调用) */ + private Server resolveIfsCon() { + return resolveIfsConFactory().get(); } private void handleTool(QuoteDetail detail, QuoteDetailRouting routing) { diff --git a/src/main/resources/mapper/quotation/QuotationInformationMapper.xml b/src/main/resources/mapper/quotation/QuotationInformationMapper.xml index b6ec4ec6..b5386f6b 100644 --- a/src/main/resources/mapper/quotation/QuotationInformationMapper.xml +++ b/src/main/resources/mapper/quotation/QuotationInformationMapper.xml @@ -62,6 +62,7 @@ +