From 2551c3f59fb1b6ec12049c4e388e6a57e1a0a2d4 Mon Sep 17 00:00:00 2001 From: fengyuan_yang <1976974459@qq.com> Date: Mon, 9 Mar 2026 13:42:05 +0800 Subject: [PATCH] =?UTF-8?q?2026-03-09=20=E9=94=80=E5=94=AE=E6=8A=A5?= =?UTF-8?q?=E4=BB=B7=E5=8D=A1=E9=A1=BF=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/QuoteDetailBomTreeServiceImpl.java | 817 ++++++++++-------- .../service/impl/QuoteDetailServiceImpl.java | 3 +- .../mapper/quote/QuoteDetailMapper.xml | 21 +- .../resources/mapper/quote/QuoteMapper.xml | 49 +- 4 files changed, 492 insertions(+), 398 deletions(-) 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 67d95852..7f56fb21 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 @@ -56,32 +56,67 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl components = new ArrayList<>(); + /** 是否顶级节点(决定是否触发 handleTool) */ + boolean isRoot; + /** + * 子节点数据包,与 components 中 bomFlag="Y" 的条目按顺序一一对应。 + * 写入时先写子节点拿到真实 treeId,再回填 component.bomId。 + */ + List children = new ArrayList<>(); + } + + // ========================================================================= + // 公开方法:两阶段执行 + // ========================================================================= + + /** + * 初始化 BOM 树。 + * 阶段一(无事务):递归收集所有 BOM/Routing/子物料数据,并调用 IFS 查询成本。 + * 阶段二(短事务):批量写入已准备好的数据,事务内无外部 IO,大幅缩短锁持有时间。 + */ @Override - @Transactional public long initQuoteDetailBomTree(QuoteDetail detail, Long parentId, Integer level) { - //获取当前用户的ifs账号和连接 LR 2025-05-30 Start - //获取当前操作的账号 - String username = ((SysUserEntity) SecurityUtils.getSubject().getPrincipal()).getUsername(); - - SysUserEntity ifsUser = sysUserDao.selectOne(new QueryWrapper().eq("username", username)); - if (ifsUser == null || !org.apache.commons.lang3.StringUtils.isNotBlank(ifsUser.getIfsUsername()) || !org.apache.commons.lang3.StringUtils.isNotBlank(ifsUser.getIfsPassword())) { - throw new RuntimeException("请维护IFS账号和密码!"); + // 阶段一:事务外,获取 IFS 连接并递归收集数据 + Server ifsCon = resolveIfsCon(); + BomNodeData rootNode = collectBomData(detail, parentId, level, ifsCon); + if (rootNode == null) { + return 0; } - Server ifsCon = ifsServer.getIfsServer(ifsUser.getIfsUsername(), ifsUser.getIfsPassword()); - //获取当前用户的ifs账号和连接 LR 2025-05-30 End + // 阶段二:短事务,纯粹写入 + return doSaveBomData(rootNode, detail); + } - // 1、通过PartNo、Site和BuNo 查询BOM信息 失效日期日期为空 替代为* 和Routing (存在BOM的物料) - if (parentId.equals(0L)){ - // 顶级物料强制使用 Manufacturing 类型 + // ========================================================================= + // 阶段一:递归收集数据(无事务,含 IFS 调用) + // ========================================================================= + + /** + * 递归收集 BOM 树节点数据。所有耗时操作(IFS 接口调用、数据库查询)均在此完成。 + * 返回 null 表示当前节点无 BOM 信息。 + */ + private BomNodeData collectBomData(QuoteDetail detail, Long parentId, Integer level, Server ifsCon) { + // 确定 BOM 类型 + if (parentId.equals(0L)) { detail.setBomType("Manufacturing"); - }else { - // 子物料:先查询物料信息 - PartInformationEntity part = baseMapper.queryPart(detail.getSite(),detail.getPartNo()); - if (Objects.nonNull(part) && "Y".equals(part.getStatus())){ + } else { + PartInformationEntity part = baseMapper.queryPart(detail.getSite(), detail.getPartNo()); + if (Objects.nonNull(part) && "Y".equals(part.getStatus())) { detail.setPartNo(part.getPartNo()); } - // 只有在bomType为空时(新增报价单场景),才根据物料类型自动设置BOM类型 - // 切换版本时,bomType已经由用户选择,保留用户的选择 if (detail.getBomType() == null || detail.getBomType().isEmpty()) { String partType = baseMapper.queryPartType(detail.getSite(), detail.getPartNo()); if ("Manufactured".equals(partType) || "Manufactured Recipe".equals(partType)) { @@ -94,342 +129,176 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl componentParts = baseMapper.queryBomComponentPart(bom); - // 判断BOM Type是否是Purchase,且没有子物料时,需要将自己当成自己的子物料,用于计算成本 -// if ("Purchase".equals(bom.getBomType()) && componentParts.isEmpty()) { -// QuoteDetailBom purchase = getPurchaseComponentPart(detail, bom, componentParts.size()+1); -// componentParts.add(purchase); -// } - - // 统计日志:记录子物料数量和需要查询成本的数量 + int totalComponents = componentParts.size(); long needCostQueryCount = componentParts.stream().filter(c -> "Y".equals(c.getStatus())).count(); - log.info("[BOM_PROCESS] 开始处理子物料 - 父物料: {}, 层级: {}, 子物料总数: {}, 需查询成本数: {}", + log.info("[BOM_PROCESS] 开始处理子物料 - 父物料: {}, 层级: {}, 子物料总数: {}, 需查询成本数: {}", bom.getPartNo(), level, totalComponents, needCostQueryCount); - - // 3、查询子物料是否存在BOM信息 - int processedCount = 0; + + BomNodeData nodeData = new BomNodeData(); + nodeData.tree = bom; + nodeData.routing = routing; + nodeData.isRoot = parentId.equals(0L); + int costQuerySuccessCount = 0; int costQueryFailCount = 0; - + for (QuoteDetailBom component : componentParts) { - processedCount++; component.setCreateBy(detail.getCreateBy()); component.setCreateDate(detail.getCreateDate()); - QuoteDetail quoteDetail = createQuoteDetail(detail, component); - // 物料是半成品 - QuoteDetailBomTree bomTree = isComponentBom(quoteDetail); - if (Objects.nonNull(bomTree) && "Y".equals(component.getBomFlag())) { - log.debug("[BOM_PROCESS] 递归处理半成品 - PartNo: {}, 当前进度: {}/{}", - component.getComponentPart(), processedCount, totalComponents); - long id = initQuoteDetailBomTree(quoteDetail, bom.getId(), level + 1); - // 如果是BOM 为子料绑定是哪个Bom - component.setBomId(id); - // 半成品 价格为0 + + QuoteDetail childDetail = createQuoteDetail(detail, component); + QuoteDetailBomTree childBomCheck = isComponentBom(childDetail); + + if (Objects.nonNull(childBomCheck) && "Y".equals(component.getBomFlag())) { + log.debug("[BOM_PROCESS] 递归处理半成品 - PartNo: {}", component.getComponentPart()); + // 递归收集子节点,parentId 占位 -1,写入时由 doSaveBomDataRecursive 覆盖 + BomNodeData childNode = collectBomData(childDetail, -1L, level + 1, ifsCon); + if (childNode != null) { + nodeData.children.add(childNode); + } + // 半成品价格为 0,bomId 在写入阶段填充 + component.setBomFlag("Y"); component.setUnitPrice(BigDecimal.ZERO); component.setActualPrice(BigDecimal.ZERO); component.setQuotePrice(BigDecimal.ZERO); } else { component.setBomFlag("N"); - } - if ("Y".equals(component.getStatus())) { - // 记录查询前的状态,用于判断是否成功 - BigDecimal beforePrice = component.getUnitPrice(); - getFinalPartCost(component, ifsCon); - // 判断成本查询是否成功(成功后价格不为0或有变化) - if (component.getUnitPrice() != null && component.getUnitPrice().compareTo(BigDecimal.ZERO) > 0) { - costQuerySuccessCount++; - } else { - costQueryFailCount++; + // 正式物料:在事务外调用 IFS 查询成本 + if ("Y".equals(component.getStatus())) { + getFinalPartCost(component, ifsCon); + if (component.getUnitPrice() != null && component.getUnitPrice().compareTo(BigDecimal.ZERO) > 0) { + costQuerySuccessCount++; + } else { + costQueryFailCount++; + } } } - //新增子物料信息 - component.setTreeId(bom.getId()); - quoteDetailBomService.save(component); + nodeData.components.add(component); } - - // 统计日志:记录处理结果 - log.info("[BOM_PROCESS] 子物料处理完成 - 父物料: {}, 层级: {}, 处理总数: {}, 成本查询成功: {}, 成本查询失败: {}", - bom.getPartNo(), level, processedCount, costQuerySuccessCount, costQueryFailCount); - - return bom.getId(); - } - private void handleTool(QuoteDetail detail, QuoteDetailRouting routing) { - // 1、清空工具信息 -/* quoteDetailToolService.lambdaUpdate() - .eq(QuoteDetailTool::getQuoteDetailId, detail.getId()) - .remove();*/ - QuoteDetailTool tool = new QuoteDetailTool(); - tool.setQuoteDetailId(detail.getId()); - List quoteDetailTools = quoteDetailToolMapper.queryQuoteDetailTool(tool); - if (Objects.nonNull(routing)){ - // 处理工具 - // 2、根据routing 生成工具信息 - if (quoteDetailTools.stream().allMatch(item -> Objects.equals(item.getToolDesc(), "其他"))){ - quoteDetailToolService.saveToolByRouting(routing); - } - // 3、插入一条其他工具信息 -// quoteDetailToolService.saveQuoteDetailOtherTool(routing); - }else { -// quoteDetailToolService.saveQuoteDetailOtherTool(detail); - } - } + log.info("[BOM_PROCESS] 子物料处理完成 - 父物料: {}, 层级: {}, 处理总数: {}, 成本查询成功: {}, 成本查询失败: {}", + bom.getPartNo(), level, costQuerySuccessCount + costQueryFailCount, costQuerySuccessCount, costQueryFailCount); - private QuoteDetailBom getPurchaseComponentPart(QuoteDetail detail, QuoteDetailBomTree bom,Integer lineSequence ) { - QuoteDetailBom purchase = new QuoteDetailBom(); - purchase.setQuoteDetailId(detail.getId()); - purchase.setQuoteId(detail.getQuoteId()); - purchase.setQuoteDetailItemNo(detail.getItemNo()); - purchase.setSite(detail.getSite()); - purchase.setBuNo(detail.getBuNo()); - purchase.setQuoteNo(detail.getQuoteNo()); - purchase.setVersionNo(detail.getVersionNo()); - purchase.setPartNo(bom.getPartNo()); - purchase.setEngChgLevel(bom.getEngChgLevel()); - purchase.setBomType(bom.getBomType()); - purchase.setAlternativeNo(bom.getAlternativeNo()); - purchase.setComponentPart(bom.getPartNo()); - purchase.setPrintUnit(bom.getUmName()); - purchase.setQtyPerAssembly(BigDecimal.ONE); - purchase.setComponentScrap(BigDecimal.ZERO); - purchase.setIssueType(""); - purchase.setShrinkageFactor(BigDecimal.ZERO); - BigDecimal price = baseMapper.getPartCost(purchase.getSite(),purchase.getPartNo()); - purchase.setUnitPrice(price); - purchase.setActualPrice(price); - purchase.setQuotePrice(price); - purchase.setLineSequence(lineSequence); - purchase.setBomFlag("N"); - purchase.setStatus(bom.getStatus()); - return purchase; + return nodeData; } + // ========================================================================= + // 阶段二:短事务,递归写入(无外部 IO) + // ========================================================================= + /** - * 获取物料成本(带重试机制) - * @param component 物料组件 - * @param ifsServer IFS服务器连接 + * 短事务入口:将收集好的 BomNodeData 写入数据库。 + * 调用方在自己的事务上下文中直接调用 doSaveBomDataRecursive 即可(如 changeQuoteDetailBomTree)。 */ - private void getFinalPartCost(QuoteDetailBom component, Server ifsServer) { - // 最大重试次数(设置为2次,平衡成功率和响应时间) - final int MAX_RETRY_COUNT = 2; - // 重试间隔(毫秒,设置为500ms,减少等待时间) - final long RETRY_INTERVAL_MS = 500; - - String partNo = component.getComponentPart(); - String site = component.getSite(); - - log.info("[COST_QUERY] 开始查询物料成本 - PartNo: {}, Site: {}", partNo, site); - - PartInformationEntity part = new PartInformationEntity(); - part.setSite(site); - part.setPartNo(partNo); - - Map map = null; - boolean success = false; - String lastErrorMsg = null; - - // 重试机制 - for (int retryCount = 1; retryCount <= MAX_RETRY_COUNT; retryCount++) { - try { - log.debug("[COST_QUERY] 第 {} 次尝试查询 - PartNo: {}", retryCount, partNo); - - map = baseSearchBean.getInventoryValueByPartNo(ifsServer, part); - - if (Objects.equals(map.get("resultCode"), "200")) { - // 返回成功 - InventoryPartUnitCostSumVo unitCostSumVo = JSONObject.parseObject(map.get("obj"), InventoryPartUnitCostSumVo.class); - BigDecimal unitCost = new BigDecimal(unitCostSumVo.getInventoryValue()); - component.setUnitPrice(unitCost); - component.setActualPrice(unitCost); - component.setQuotePrice(unitCost); - - log.info("[COST_QUERY] 查询成功 - PartNo: {}, Site: {}, UnitCost: {}, 尝试次数: {}", - partNo, site, unitCost, retryCount); - success = true; - break; - } else { - // 接口返回错误 - lastErrorMsg = map.get("resultMsg"); - log.warn("[COST_QUERY] 第 {} 次查询失败 - PartNo: {}, Site: {}, ResultCode: {}, ErrorMsg: {}", - retryCount, partNo, site, map.get("resultCode"), lastErrorMsg); - - // 判断是否需要重试(某些明确的业务错误不需要重试) - if (lastErrorMsg != null) { - // 账号密码错误不重试,直接抛异常 - if (lastErrorMsg.contains("You have entered an invalid username and/or password")) { - log.error("[COST_QUERY] IFS账号密码错误,停止重试 - PartNo: {}", partNo); - throw new RuntimeException(lastErrorMsg); - } - // 业务数据不存在类的错误不重试(重试也不会成功) - if (lastErrorMsg.contains("不存在") || lastErrorMsg.contains("not exist") - || lastErrorMsg.contains("No data found") || lastErrorMsg.contains("not found")) { - log.info("[COST_QUERY] 物料成本数据不存在,无需重试 - PartNo: {}, ErrorMsg: {}", partNo, lastErrorMsg); - break; - } - } - - // 如果不是最后一次重试,等待后继续(仅对网络/临时性错误重试) - if (retryCount < MAX_RETRY_COUNT) { - log.info("[COST_QUERY] 等待 {}ms 后进行第 {} 次重试 - PartNo: {}", - RETRY_INTERVAL_MS, retryCount + 1, partNo); - Thread.sleep(RETRY_INTERVAL_MS); - } - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - log.error("[COST_QUERY] 重试等待被中断 - PartNo: {}, Site: {}", partNo, site); - break; - } catch (RuntimeException e) { - // 重新抛出运行时异常(如账号密码错误) - throw e; - } catch (Exception e) { - lastErrorMsg = e.getMessage(); - log.warn("[COST_QUERY] 第 {} 次查询异常 - PartNo: {}, Site: {}, Exception: {}", - retryCount, partNo, site, e.getMessage()); - - // 如果不是最后一次重试,等待后继续 - if (retryCount < MAX_RETRY_COUNT) { - try { - Thread.sleep(RETRY_INTERVAL_MS); - } catch (InterruptedException ie) { - Thread.currentThread().interrupt(); - break; - } - } - } - } - - // 所有重试都失败,设置成本为0 - if (!success) { - log.error("[COST_QUERY] 查询失败(已重试 {} 次)- PartNo: {}, Site: {}, 最后错误: {}", - MAX_RETRY_COUNT, partNo, site, lastErrorMsg); - component.setUnitPrice(BigDecimal.ZERO); - component.setActualPrice(BigDecimal.ZERO); - component.setQuotePrice(BigDecimal.ZERO); - } - } - - private QuoteDetailBomTree isComponentBom(QuoteDetail component) { - // 根据子物料信息检查是否有BOM信息 - return baseMapper.queryPartBom(component); + @Transactional + public long doSaveBomData(BomNodeData nodeData, QuoteDetail detail) { + return doSaveBomDataRecursive(nodeData, detail, nodeData.tree.getParentId()); } - private QuoteDetail createQuoteDetail(QuoteDetail detail, QuoteDetailBom component) { - QuoteDetail quoteDetail = new QuoteDetail(); - quoteDetail.setPartNo(component.getComponentPart()); - quoteDetail.setSite(component.getSite()); - quoteDetail.setBuNo(component.getBuNo()); - quoteDetail.setCreateBy(detail.getCreateBy()); - quoteDetail.setCreateDate(detail.getCreateDate()); - copyCommonFields(detail, quoteDetail); - return quoteDetail; - } + /** + * 递归写入 BOM 树节点,全程无外部 IO,事务持有时间极短。 + */ + private long doSaveBomDataRecursive(BomNodeData nodeData, QuoteDetail detail, Long parentId) { + QuoteDetailBomTree bom = nodeData.tree; + bom.setParentId(parentId); - private void copyCommonFields(QuoteDetail source, QuoteDetail target) { - target.setId(source.getId()); - target.setQuoteId(source.getQuoteId()); - target.setQuoteNo(source.getQuoteNo()); - target.setItemNo(source.getItemNo()); - target.setVersionNo(source.getVersionNo()); - } + // 写入 BOM 树节点,获得自增 ID + save(bom); + long treeId = bom.getId(); - @Override - public List queryDetailBomTree(QuoteDetail detail) { - // 查询 所有 BOM 信息 - List list = baseMapper.queryDetailBomTree(detail); - // 生成BOM树 - QuoteDetailBomTree quoteDetailBomTree = buildTree(list); - // 转换成List - List treeList = new ArrayList<>(); - if (Objects.nonNull(quoteDetailBomTree)){ - treeList.add(quoteDetailBomTree); + // 写入 Routing + if (Objects.nonNull(nodeData.routing)) { + nodeData.routing.setTreeId(treeId); + quoteDetailRoutingService.saveQuoteDetailRouting(nodeData.routing); } - return treeList; - } - @Override - public List getAllChildIds(QuoteDetail detail,Long id) { - List allNodes = getAllNodes(detail); - List result = new ArrayList<>(); - findChildIds(id, allNodes, result); - return result; - } + // 顶级节点:处理工具信息 + if (nodeData.isRoot) { + handleTool(detail, nodeData.routing); + } - @Override - public List queryDetailBomVersion(QuoteDetailBom bom) { - if (StringUtils.isEmpty(bom.getPlmPartNo()) && "Y".equals(bom.getPartStatus())){ - // 获取正式料号信息 - PartInformationEntity part = baseMapper.queryPart(bom.getSite(), bom.getPartNo()); - if (Objects.isNull(part)){ - throw new RuntimeException("临时物料:"+bom.getPartNo()+"没有找到绑定的正式料号"); + // 递归写入子节点,收集 childNode 对应的真实 treeId,用于回填 bomId + // nodeData.children 与 nodeData.components 中 bomFlag="Y" 的条目按顺序一一对应 + int childIndex = 0; + for (QuoteDetailBom component : nodeData.components) { + if ("Y".equals(component.getBomFlag()) && childIndex < nodeData.children.size()) { + BomNodeData childNode = nodeData.children.get(childIndex); + long childTreeId = doSaveBomDataRecursive(childNode, detail, treeId); + component.setBomId(childTreeId); + childIndex++; } - bom.setPartNo(part.getPartNo()); + component.setTreeId(treeId); } - return baseMapper.queryDetailBomVersion(bom); - } - @Override - public List queryDetailBomAlternative(QuoteDetailBom bom) { - return baseMapper.queryDetailBomAlternative(bom); + // 写入子物料(SQL Server 逐条 save 以确保自增主键回填,与原逻辑保持一致) + for (QuoteDetailBom component : nodeData.components) { + quoteDetailBomService.save(component); + } + + return treeId; } + // ========================================================================= + // changeQuoteDetailBomTree + // ========================================================================= + @Override @Transactional public void changeQuoteDetailBomTree(QuoteDetailBomTree tree) { QuoteDetail detail = null; - - // 获得选中的树 - if (Objects.nonNull(tree.getId())){ + if (Objects.nonNull(tree.getId())) { QuoteDetailBomTree bomTree = getById(tree.getId()); - // 获得对应的QuoteDetail detail = quoteDetailService.getById(bomTree.getQuoteDetailId()); - // 获得节点的所有ids - List ids = getAllChildIds(detail, bomTree.getId()); - ids.add(bomTree.getId()); - // 删除子节点内容 - lambdaUpdate().in(QuoteDetailBomTree::getId,ids).remove(); - quoteDetailBomService.lambdaUpdate().in(QuoteDetailBom::getTreeId,ids).remove(); - quoteDetailRoutingService.lambdaUpdate().in(QuoteDetailRouting::getTreeId,ids).remove(); - - // 替换BOM树 detail.setPartNo(tree.getPartNo()); detail.setSite(tree.getSite()); detail.setBuNo(tree.getBuNo()); detail.setBomType(tree.getBomType()); detail.setEngChgLevel(tree.getEngChgLevel()); detail.setAlternativeNo(tree.getAlternativeNo()); - long bomId = initQuoteDetailBomTree(detail, bomTree.getParentId(), Optional.ofNullable(tree.getLevel()).orElse(0)); + + // 先在事务外完成所有 IFS 调用和数据收集,避免 IFS 阻塞期间持有数据库行锁 + Server ifsCon = resolveIfsCon(); + Long newParentId = bomTree.getParentId(); + Integer newLevel = Optional.ofNullable(tree.getLevel()).orElse(0); + BomNodeData nodeData = collectBomData(detail, newParentId, newLevel, ifsCon); + + // 数据已就绪,开始执行 DML(事务持有锁的时间仅限于纯 DB 写入阶段) + List ids = getAllChildIds(detail, bomTree.getId()); + ids.add(bomTree.getId()); + lambdaUpdate().in(QuoteDetailBomTree::getId, ids).remove(); + quoteDetailBomService.lambdaUpdate().in(QuoteDetailBom::getTreeId, ids).remove(); + quoteDetailRoutingService.lambdaUpdate().in(QuoteDetailRouting::getTreeId, ids).remove(); + + long bomId = 0; + if (nodeData != null) { + bomId = doSaveBomDataRecursive(nodeData, detail, newParentId); + } quoteDetailBomService.lambdaUpdate() - .set(QuoteDetailBom::getBomId,bomId) - .eq(QuoteDetailBom::getBomId,bomTree.getId()) + .set(QuoteDetailBom::getBomId, bomId) + .eq(QuoteDetailBom::getBomId, bomTree.getId()) .update(); - }else { + } else { detail = new QuoteDetail(); detail.setQuoteId(tree.getQuoteId()); detail.setQuoteNo(tree.getQuoteNo()); @@ -441,10 +310,15 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl getAllNodes(QuoteDetail detail) { - return baseMapper.queryDetailBomTree(detail); - } - - // 递归方法 - private void findChildIds(Long id, List allNodes, List result) { - // 过滤出当前节点的子节点 - List children = allNodes.stream() - .filter(node -> id.equals(node.getParentId())) - .collect(Collectors.toList()); - - for (QuoteDetailBomTree child : children) { - result.add(child.getId()); - findChildIds(child.getId(), allNodes, result); // 递归查找子节点 - } - } - private QuoteDetailBomTree buildTree(List nodes){ - QuoteDetailBomTree root = null; - // 找到根节点并构建树 - for (QuoteDetailBomTree node : nodes) { - if (node.getParentId() == null || node.getParentId() == 0) { - root = node; - root.addChildren(nodes); // 从根节点开始递归构建树 - break; - } - } - return root; } + // ========================================================================= + // againQuoteDetailBomTree + // ========================================================================= @Override public void againQuoteDetailBomTree(QuoteDetail quoteDetail, Long detailId) { QuoteDetail detail = new QuoteDetail(); detail.setId(detailId); detail.setItemNo(quoteDetail.getItemNo()); - detail.setCreateBy(quoteDetail.getCreateBy()); detail.setCreateDate(quoteDetail.getCreateDate()); detail.setUpdateBy(quoteDetail.getUpdateBy()); detail.setUpdateDate(quoteDetail.getUpdateDate()); List list = queryDetailBomTree(detail); - - // 创建老ID到新ID的映射关系 + Map oldToNewIdMapping = new HashMap<>(); loopTree(list, quoteDetail, 0L, oldToNewIdMapping); - - // 复制完成后,更新BOM明细中的bomId引用 + + // 批量更新 bomId 引用(替代原来循环逐条 updateById) updateBomIdReferencesWithCorrectLogic(quoteDetail.getId(), oldToNewIdMapping); } - private void loopTree (List list,QuoteDetail quoteDetail,Long parentId){ - if (!list.isEmpty()){ + private void loopTree(List list, QuoteDetail quoteDetail, Long parentId) { + if (!list.isEmpty()) { for (QuoteDetailBomTree tree : list) { tree.setQuoteId(quoteDetail.getQuoteId()); tree.setQuoteDetailId(quoteDetail.getId()); @@ -522,11 +366,9 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl oldToNewIdMapping) { - // 查询新复制的所有BOM明细 + if (oldToNewIdMapping.isEmpty()) { + return; + } List bomList = quoteDetailBomService.lambdaQuery() .eq(QuoteDetailBom::getQuoteDetailId, quoteDetailId) - .isNotNull(QuoteDetailBom::getBomId) // 只处理有bomId的记录 + .isNotNull(QuoteDetailBom::getBomId) .list(); + List toUpdate = new ArrayList<>(); for (QuoteDetailBom bom : bomList) { - // 获取原来的bomId - Long oldBomId = bom.getBomId(); - // 根据老ID找到新ID - Long newBomId = oldToNewIdMapping.get(oldBomId); + Long newBomId = oldToNewIdMapping.get(bom.getBomId()); if (newBomId != null) { bom.setBomId(newBomId); - quoteDetailBomService.updateById(bom); + toUpdate.add(bom); } } + if (!toUpdate.isEmpty()) { + quoteDetailBomService.updateBatchById(toUpdate); + } + } + + // ========================================================================= + // 其他 Service 方法 + // ========================================================================= + + @Override + public List queryDetailBomTree(QuoteDetail detail) { + List list = baseMapper.queryDetailBomTree(detail); + QuoteDetailBomTree quoteDetailBomTree = buildTree(list); + List treeList = new ArrayList<>(); + if (Objects.nonNull(quoteDetailBomTree)) { + treeList.add(quoteDetailBomTree); + } + return treeList; + } + + @Override + public List getAllChildIds(QuoteDetail detail, Long id) { + List allNodes = getAllNodes(detail); + List result = new ArrayList<>(); + findChildIds(id, allNodes, result); + return result; + } + + @Override + public List queryDetailBomVersion(QuoteDetailBom bom) { + if (StringUtils.isEmpty(bom.getPlmPartNo()) && "Y".equals(bom.getPartStatus())) { + PartInformationEntity part = baseMapper.queryPart(bom.getSite(), bom.getPartNo()); + if (Objects.isNull(part)) { + throw new RuntimeException("临时物料:" + bom.getPartNo() + "没有找到绑定的正式料号"); + } + bom.setPartNo(part.getPartNo()); + } + return baseMapper.queryDetailBomVersion(bom); + } + + @Override + public List queryDetailBomAlternative(QuoteDetailBom bom) { + return baseMapper.queryDetailBomAlternative(bom); + } + + public List getAllNodes(QuoteDetail detail) { + return baseMapper.queryDetailBomTree(detail); } @Override public String queryPart(QuoteDetailBom bom) { - // 测试料号使用 PartInformationEntity entity = baseMapper.queryPart(bom.getSite(), bom.getPartNo()); - if (Objects.nonNull(entity)){ + if (Objects.nonNull(entity)) { return entity.getPartNo(); } - // 正式料号使用 PartInformationEntity part = baseMapper.queryPLMPart(bom.getSite(), bom.getPartNo()); - if (Objects.nonNull(part)){ + if (Objects.nonNull(part)) { return part.getPartNo(); } return ""; } - + @Override public BigDecimal queryEstimatedMaterialCost(String site, String partNo) { log.info("queryEstimatedMaterialCost - Request params: site={}, partNo={}", site, partNo); - - // 从part表查询物料状态 + String status = baseMapper.queryPartStatus(site, partNo); log.info("queryEstimatedMaterialCost - Query part status from database: status={}", status); - - // 非正式物料(status='N'或为空)直接从数据库查询 + if (!"Y".equals(status)) { BigDecimal cost = baseMapper.queryEstimatedMaterialCost(site, partNo); BigDecimal result = cost != null ? cost : BigDecimal.ZERO; log.info("queryEstimatedMaterialCost - Unofficial part (status={}), query from database. Result: {}", status, result); return result; } - - // 正式物料(status='Y')调用IFS接口 + try { - // 获取当前用户的IFS连接 String username = ((SysUserEntity) SecurityUtils.getSubject().getPrincipal()).getUsername(); SysUserEntity ifsUser = sysUserDao.selectOne(new QueryWrapper().eq("username", username)); - if (ifsUser == null || !org.apache.commons.lang3.StringUtils.isNotBlank(ifsUser.getIfsUsername()) + if (ifsUser == null || !org.apache.commons.lang3.StringUtils.isNotBlank(ifsUser.getIfsUsername()) || !org.apache.commons.lang3.StringUtils.isNotBlank(ifsUser.getIfsPassword())) { log.warn("queryEstimatedMaterialCost - User {} has no IFS account configured, fallback to database query", username); BigDecimal cost = baseMapper.queryEstimatedMaterialCost(site, partNo); @@ -624,25 +503,24 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl map = baseSearchBean.getInventoryEstimatedMaterialCostByPartNo(ifsCon, part); log.info("queryEstimatedMaterialCost - IFS API response: {}", map); - + if (Objects.equals(map.get("resultCode"), "200")) { InventoryPartUnitCostSumVo unitCostSumVo = JSONObject.parseObject(map.get("obj"), InventoryPartUnitCostSumVo.class); String estimatedMaterialCostStr = unitCostSumVo.getEstimatedMaterialCost(); - BigDecimal estimatedCost = org.apache.commons.lang3.StringUtils.isNotBlank(estimatedMaterialCostStr) + BigDecimal estimatedCost = org.apache.commons.lang3.StringUtils.isNotBlank(estimatedMaterialCostStr) ? new BigDecimal(estimatedMaterialCostStr) : BigDecimal.ZERO; log.info("queryEstimatedMaterialCost - Successfully retrieved estimated material cost from IFS: {}", estimatedCost); return estimatedCost; } else { - log.warn("queryEstimatedMaterialCost - Failed to retrieve from IFS, resultCode={}, resultMsg={}", + log.warn("queryEstimatedMaterialCost - Failed to retrieve from IFS, resultCode={}, resultMsg={}", map.get("resultCode"), map.get("resultMsg")); return BigDecimal.ZERO; } @@ -651,9 +529,202 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl().eq("username", username)); + if (ifsUser == null + || !org.apache.commons.lang3.StringUtils.isNotBlank(ifsUser.getIfsUsername()) + || !org.apache.commons.lang3.StringUtils.isNotBlank(ifsUser.getIfsPassword())) { + throw new RuntimeException("请维护IFS账号和密码!"); + } + return ifsServer.getIfsServer(ifsUser.getIfsUsername(), ifsUser.getIfsPassword()); + } + + private void handleTool(QuoteDetail detail, QuoteDetailRouting routing) { + QuoteDetailTool tool = new QuoteDetailTool(); + tool.setQuoteDetailId(detail.getId()); + List quoteDetailTools = quoteDetailToolMapper.queryQuoteDetailTool(tool); + if (Objects.nonNull(routing)) { + if (quoteDetailTools.stream().allMatch(item -> Objects.equals(item.getToolDesc(), "其他"))) { + quoteDetailToolService.saveToolByRouting(routing); + } + } + } + + private QuoteDetailBom getPurchaseComponentPart(QuoteDetail detail, QuoteDetailBomTree bom, Integer lineSequence) { + QuoteDetailBom purchase = new QuoteDetailBom(); + purchase.setQuoteDetailId(detail.getId()); + purchase.setQuoteId(detail.getQuoteId()); + purchase.setQuoteDetailItemNo(detail.getItemNo()); + purchase.setSite(detail.getSite()); + purchase.setBuNo(detail.getBuNo()); + purchase.setQuoteNo(detail.getQuoteNo()); + purchase.setVersionNo(detail.getVersionNo()); + purchase.setPartNo(bom.getPartNo()); + purchase.setEngChgLevel(bom.getEngChgLevel()); + purchase.setBomType(bom.getBomType()); + purchase.setAlternativeNo(bom.getAlternativeNo()); + purchase.setComponentPart(bom.getPartNo()); + purchase.setPrintUnit(bom.getUmName()); + purchase.setQtyPerAssembly(BigDecimal.ONE); + purchase.setComponentScrap(BigDecimal.ZERO); + purchase.setIssueType(""); + purchase.setShrinkageFactor(BigDecimal.ZERO); + BigDecimal price = baseMapper.getPartCost(purchase.getSite(), purchase.getPartNo()); + purchase.setUnitPrice(price); + purchase.setActualPrice(price); + purchase.setQuotePrice(price); + purchase.setLineSequence(lineSequence); + purchase.setBomFlag("N"); + purchase.setStatus(bom.getStatus()); + return purchase; + } + + /** + * 获取物料成本(带重试机制) + */ + private void getFinalPartCost(QuoteDetailBom component, Server ifsServer) { + final int MAX_RETRY_COUNT = 2; + final long RETRY_INTERVAL_MS = 500; + + String partNo = component.getComponentPart(); + String site = component.getSite(); + + log.info("[COST_QUERY] 开始查询物料成本 - PartNo: {}, Site: {}", partNo, site); + + PartInformationEntity part = new PartInformationEntity(); + part.setSite(site); + part.setPartNo(partNo); + + Map map = null; + boolean success = false; + String lastErrorMsg = null; + + for (int retryCount = 1; retryCount <= MAX_RETRY_COUNT; retryCount++) { + try { + log.debug("[COST_QUERY] 第 {} 次尝试查询 - PartNo: {}", retryCount, partNo); + + map = baseSearchBean.getInventoryValueByPartNo(ifsServer, part); + + if (Objects.equals(map.get("resultCode"), "200")) { + InventoryPartUnitCostSumVo unitCostSumVo = JSONObject.parseObject(map.get("obj"), InventoryPartUnitCostSumVo.class); + BigDecimal unitCost = new BigDecimal(unitCostSumVo.getInventoryValue()); + component.setUnitPrice(unitCost); + component.setActualPrice(unitCost); + component.setQuotePrice(unitCost); + + log.info("[COST_QUERY] 查询成功 - PartNo: {}, Site: {}, UnitCost: {}, 尝试次数: {}", + partNo, site, unitCost, retryCount); + success = true; + break; + } else { + lastErrorMsg = map.get("resultMsg"); + log.warn("[COST_QUERY] 第 {} 次查询失败 - PartNo: {}, Site: {}, ResultCode: {}, ErrorMsg: {}", + retryCount, partNo, site, map.get("resultCode"), lastErrorMsg); + + if (lastErrorMsg != null) { + if (lastErrorMsg.contains("You have entered an invalid username and/or password")) { + log.error("[COST_QUERY] IFS账号密码错误,停止重试 - PartNo: {}", partNo); + throw new RuntimeException(lastErrorMsg); + } + if (lastErrorMsg.contains("不存在") || lastErrorMsg.contains("not exist") + || lastErrorMsg.contains("No data found") || lastErrorMsg.contains("not found")) { + log.info("[COST_QUERY] 物料成本数据不存在,无需重试 - PartNo: {}, ErrorMsg: {}", partNo, lastErrorMsg); + break; + } + } + + if (retryCount < MAX_RETRY_COUNT) { + log.info("[COST_QUERY] 等待 {}ms 后进行第 {} 次重试 - PartNo: {}", + RETRY_INTERVAL_MS, retryCount + 1, partNo); + Thread.sleep(RETRY_INTERVAL_MS); + } + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("[COST_QUERY] 重试等待被中断 - PartNo: {}, Site: {}", partNo, site); + break; + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + lastErrorMsg = e.getMessage(); + log.warn("[COST_QUERY] 第 {} 次查询异常 - PartNo: {}, Site: {}, Exception: {}", + retryCount, partNo, site, e.getMessage()); + + if (retryCount < MAX_RETRY_COUNT) { + try { + Thread.sleep(RETRY_INTERVAL_MS); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + break; + } + } + } + } + + if (!success) { + log.error("[COST_QUERY] 查询失败(已重试 {} 次)- PartNo: {}, Site: {}, 最后错误: {}", + MAX_RETRY_COUNT, partNo, site, lastErrorMsg); + component.setUnitPrice(BigDecimal.ZERO); + component.setActualPrice(BigDecimal.ZERO); + component.setQuotePrice(BigDecimal.ZERO); + } + } + + private QuoteDetailBomTree isComponentBom(QuoteDetail component) { + return baseMapper.queryPartBom(component); + } + + private QuoteDetail createQuoteDetail(QuoteDetail detail, QuoteDetailBom component) { + QuoteDetail quoteDetail = new QuoteDetail(); + quoteDetail.setPartNo(component.getComponentPart()); + quoteDetail.setSite(component.getSite()); + quoteDetail.setBuNo(component.getBuNo()); + quoteDetail.setCreateBy(detail.getCreateBy()); + quoteDetail.setCreateDate(detail.getCreateDate()); + copyCommonFields(detail, quoteDetail); + return quoteDetail; + } + + private void copyCommonFields(QuoteDetail source, QuoteDetail target) { + target.setId(source.getId()); + target.setQuoteId(source.getQuoteId()); + target.setQuoteNo(source.getQuoteNo()); + target.setItemNo(source.getItemNo()); + target.setVersionNo(source.getVersionNo()); + } + + private void findChildIds(Long id, List allNodes, List result) { + List children = allNodes.stream() + .filter(node -> id.equals(node.getParentId())) + .collect(Collectors.toList()); + + for (QuoteDetailBomTree child : children) { + result.add(child.getId()); + findChildIds(child.getId(), allNodes, result); + } + } + + private QuoteDetailBomTree buildTree(List nodes) { + QuoteDetailBomTree root = null; + for (QuoteDetailBomTree node : nodes) { + if (node.getParentId() == null || node.getParentId() == 0) { + root = node; + root.addChildren(nodes); + break; + } + } + return root; + } } diff --git a/src/main/java/com/spring/modules/quote/service/impl/QuoteDetailServiceImpl.java b/src/main/java/com/spring/modules/quote/service/impl/QuoteDetailServiceImpl.java index fce70d96..1f6458db 100644 --- a/src/main/java/com/spring/modules/quote/service/impl/QuoteDetailServiceImpl.java +++ b/src/main/java/com/spring/modules/quote/service/impl/QuoteDetailServiceImpl.java @@ -838,7 +838,8 @@ public class QuoteDetailServiceImpl extends ServiceImpl list = baseMapper.queryQuoteDetail(detail); + return list.isEmpty() ? null : list.get(0); } @Override diff --git a/src/main/resources/mapper/quote/QuoteDetailMapper.xml b/src/main/resources/mapper/quote/QuoteDetailMapper.xml index bded0a9d..c249f903 100644 --- a/src/main/resources/mapper/quote/QuoteDetailMapper.xml +++ b/src/main/resources/mapper/quote/QuoteDetailMapper.xml @@ -65,11 +65,11 @@ qd.quote_tax_total_price, qd.quote_tax_unit_price, qd.currency1, - dbo.plm_get_dictDataLabel('plm_customer_information_customer_customer_currency', qd.currency1, qd.site) as currencyDesc1, + sdd1.dict_label as currencyDesc1, qd.final_transaction_price, qd.exchange_rate1, qd.currency2, - dbo.plm_get_dictDataLabel('plm_customer_information_customer_customer_currency', qd.currency2, qd.site) as currencyDesc2, + sdd2.dict_label as currencyDesc2, qd.exchange_rate2, qd.moq, qd.currency_total_cost1, @@ -88,6 +88,10 @@ from plm_quote_detail qd left join plm_quote q on qd.quote_id = q.id left join part pp on qd.part_no = pp.part_no and qd.site = pp.site + left join sys_dict_data sdd1 on sdd1.dict_type = 'plm_customer_information_customer_customer_currency' + and sdd1.dict_value = qd.currency1 and sdd1.site = qd.site + left join sys_dict_data sdd2 on sdd2.dict_type = 'plm_customer_information_customer_customer_currency' + and sdd2.dict_value = qd.currency2 and sdd2.site = qd.site and qd.id = #{id} @@ -177,12 +181,10 @@ qd.quote_tax_total_price, qd.quote_tax_unit_price, qd.currency1, - dbo.plm_get_dictDataLabel('plm_customer_information_customer_customer_currency', qd.currency1, - qd.site) as currencyDesc1, + sdd1.dict_label as currencyDesc1, qd.exchange_rate1, qd.currency2, - dbo.plm_get_dictDataLabel('plm_customer_information_customer_customer_currency', qd.currency2, - qd.site) as currencyDesc2, + sdd2.dict_label as currencyDesc2, qd.exchange_rate2, qd.moq, qd.currency_total_cost1, @@ -196,8 +198,11 @@ qd.adjust_else_cost from plm_quote_detail qd left join plm_quote q on qd.quote_id = q.id - left join part pp - on qd.part_no = pp.part_no and qd.site = pp.site + left join part pp on qd.part_no = pp.part_no and qd.site = pp.site + left join sys_dict_data sdd1 on sdd1.dict_type = 'plm_customer_information_customer_customer_currency' + and sdd1.dict_value = qd.currency1 and sdd1.site = qd.site + left join sys_dict_data sdd2 on sdd2.dict_type = 'plm_customer_information_customer_customer_currency' + and sdd2.dict_value = qd.currency2 and sdd2.site = qd.site and qd.quote_id = #{params.quoteId} diff --git a/src/main/resources/mapper/quote/QuoteMapper.xml b/src/main/resources/mapper/quote/QuoteMapper.xml index 1a16b7c3..c4b39df5 100644 --- a/src/main/resources/mapper/quote/QuoteMapper.xml +++ b/src/main/resources/mapper/quote/QuoteMapper.xml @@ -137,7 +137,8 @@ - + + and q.id = #{params.id} @@ -170,10 +171,10 @@ and p.project_name like #{params.projectDesc} - and dbo.plm_get_user_display(q.site, q.purchase) like #{params.purchase} + and su_purchase.user_display like #{params.purchase} - and dbo.plm_get_user_display(q.site, q.quoter) like #{params.quoter} + and su_quoter.user_display like #{params.quoter} and q.customer_inquiry_no like #{params.customerInquiryNo} @@ -213,20 +214,27 @@ + + +