|
|
|
@ -79,7 +79,10 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl<QuoteDetailBomTre |
|
|
|
*/ |
|
|
|
private static class BomNodeData { |
|
|
|
QuoteDetailBomTree tree; |
|
|
|
QuoteDetailRouting routing; |
|
|
|
/** Routing 头信息(用于工具处理、createBy/createDate 透传) */ |
|
|
|
QuoteDetailRouting routingHeader; |
|
|
|
/** 预加载的 Routing 行,事务内直接写入报价 Routing 表 */ |
|
|
|
List<QuoteDetailRouting> routingRows = new ArrayList<>(); |
|
|
|
/** 当前节点下的子物料列表(已填充好成本价格) */ |
|
|
|
List<QuoteDetailBom> components = new ArrayList<>(); |
|
|
|
/** 是否顶级节点(决定是否触发 handleTool) */ |
|
|
|
@ -105,7 +108,8 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl<QuoteDetailBomTre |
|
|
|
// 阶段一:事务外,获取 IFS 连接工厂并递归收集数据(含并行 IFS 成本查询) |
|
|
|
Supplier<Server> ifsConFactory = resolveIfsConFactory(); |
|
|
|
Map<String, BigDecimal> costCache = new ConcurrentHashMap<>(); |
|
|
|
BomNodeData rootNode = collectBomData(detail, parentId, level, ifsConFactory, costCache); |
|
|
|
Map<String, BigDecimal> workCenterCostCache = new ConcurrentHashMap<>(); |
|
|
|
BomNodeData rootNode = collectBomData(detail, parentId, level, ifsConFactory, costCache, workCenterCostCache); |
|
|
|
if (rootNode == null) { |
|
|
|
return 0; |
|
|
|
} |
|
|
|
@ -129,7 +133,7 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl<QuoteDetailBomTre |
|
|
|
* |
|
|
|
* @param ifsConFactory IFS 连接工厂,每个并发线程调用 get() 获取独立连接,避免共享连接线程安全问题 |
|
|
|
*/ |
|
|
|
private BomNodeData collectBomData(QuoteDetail detail, Long parentId, Integer level, Supplier<Server> ifsConFactory, Map<String, BigDecimal> costCache) { |
|
|
|
private BomNodeData collectBomData(QuoteDetail detail, Long parentId, Integer level, Supplier<Server> ifsConFactory, Map<String, BigDecimal> costCache, Map<String, BigDecimal> workCenterCostCache) { |
|
|
|
// 统一先尝试将临时料映射到正式料,保证新建与“切换版本刷新”取数来源一致 |
|
|
|
PartInformationEntity mappedPart = baseMapper.queryPart(detail.getSite(), detail.getPartNo()); |
|
|
|
if (Objects.nonNull(mappedPart) && "Y".equals(mappedPart.getStatus())) { |
|
|
|
@ -178,8 +182,17 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl<QuoteDetailBomTre |
|
|
|
|
|
|
|
BomNodeData nodeData = new BomNodeData(); |
|
|
|
nodeData.tree = bom; |
|
|
|
nodeData.routing = routing; |
|
|
|
nodeData.routingHeader = routing; |
|
|
|
nodeData.isRoot = parentId.equals(0L); |
|
|
|
if (Objects.nonNull(routing)) { |
|
|
|
List<QuoteDetailRouting> routingRows = baseMapper.queryRoutingRowsForSave(routing); |
|
|
|
for (QuoteDetailRouting routingRow : routingRows) { |
|
|
|
BigDecimal workCenterCost = resolveWorkCenterCost(routingRow.getSite(), routingRow.getWorkCenterNo(), workCenterCostCache); |
|
|
|
routingRow.setWorkCenterCost(workCenterCost); |
|
|
|
routingRow.setManuCost(workCenterCost); |
|
|
|
} |
|
|
|
nodeData.routingRows = routingRows; |
|
|
|
} |
|
|
|
|
|
|
|
// ── 串行阶段:构建 BOM 层级结构(递归,有顺序依赖,不可并行)──────────────── |
|
|
|
for (QuoteDetailBom component : componentParts) { |
|
|
|
@ -192,7 +205,7 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl<QuoteDetailBomTre |
|
|
|
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, ifsConFactory, costCache); |
|
|
|
BomNodeData childNode = collectBomData(childDetail, -1L, level + 1, ifsConFactory, costCache, workCenterCostCache); |
|
|
|
if (childNode != null) { |
|
|
|
nodeData.children.add(childNode); |
|
|
|
// 半成品:bomId 在写入阶段填充,先清零价格(IFS 并行阶段会覆盖) |
|
|
|
@ -302,14 +315,13 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl<QuoteDetailBomTre |
|
|
|
long treeId = bom.getId(); |
|
|
|
|
|
|
|
// 写入 Routing |
|
|
|
if (Objects.nonNull(nodeData.routing)) { |
|
|
|
nodeData.routing.setTreeId(treeId); |
|
|
|
quoteDetailRoutingService.saveQuoteDetailRouting(nodeData.routing); |
|
|
|
if (Objects.nonNull(nodeData.routingHeader)) { |
|
|
|
savePreloadedRoutingRows(nodeData, detail, treeId); |
|
|
|
} |
|
|
|
|
|
|
|
// 顶级节点:处理工具信息 |
|
|
|
if (nodeData.isRoot) { |
|
|
|
handleTool(detail, nodeData.routing); |
|
|
|
handleTool(detail, nodeData.routingHeader); |
|
|
|
} |
|
|
|
|
|
|
|
// 递归写入子节点,收集 childNode 对应的真实 treeId,用于回填 bomId |
|
|
|
@ -368,7 +380,8 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl<QuoteDetailBomTre |
|
|
|
newParentId = bomTree.getParentId(); |
|
|
|
Integer newLevel = Optional.ofNullable(tree.getLevel()).orElse(0); |
|
|
|
Map<String, BigDecimal> costCache = new ConcurrentHashMap<>(); |
|
|
|
nodeData = collectBomData(detail, newParentId, newLevel, ifsConFactory, costCache); |
|
|
|
Map<String, BigDecimal> workCenterCostCache = new ConcurrentHashMap<>(); |
|
|
|
nodeData = collectBomData(detail, newParentId, newLevel, ifsConFactory, costCache, workCenterCostCache); |
|
|
|
log.info("[PERF_LOG] Phase 1: collectBomData (including IFS calls) took {} ms", (System.currentTimeMillis() - collectStartTime)); |
|
|
|
} else { |
|
|
|
detail = new QuoteDetail(); |
|
|
|
@ -387,7 +400,8 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl<QuoteDetailBomTre |
|
|
|
long collectStartTime = System.currentTimeMillis(); |
|
|
|
Supplier<Server> ifsConFactory = resolveIfsConFactory(); |
|
|
|
Map<String, BigDecimal> costCache = new ConcurrentHashMap<>(); |
|
|
|
nodeData = collectBomData(detail, 0L, 0, ifsConFactory, costCache); |
|
|
|
Map<String, BigDecimal> workCenterCostCache = new ConcurrentHashMap<>(); |
|
|
|
nodeData = collectBomData(detail, 0L, 0, ifsConFactory, costCache, workCenterCostCache); |
|
|
|
log.info("[PERF_LOG] Phase 1: collectBomData (including IFS calls) took {} ms", (System.currentTimeMillis() - collectStartTime)); |
|
|
|
} |
|
|
|
|
|
|
|
@ -684,6 +698,60 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl<QuoteDetailBomTre |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 事务内写入预加载好的 Routing 行数据,避免每个节点再次执行 insert-select 重查询。 |
|
|
|
*/ |
|
|
|
private void savePreloadedRoutingRows(BomNodeData nodeData, QuoteDetail detail, long treeId) { |
|
|
|
if (nodeData.routingRows == null || nodeData.routingRows.isEmpty()) { |
|
|
|
return; |
|
|
|
} |
|
|
|
Date createDate = Objects.nonNull(nodeData.routingHeader.getCreateDate()) ? nodeData.routingHeader.getCreateDate() : new Date(); |
|
|
|
String createBy = StringUtils.hasText(nodeData.routingHeader.getCreateBy()) ? nodeData.routingHeader.getCreateBy() : detail.getCreateBy(); |
|
|
|
|
|
|
|
for (QuoteDetailRouting routingRow : nodeData.routingRows) { |
|
|
|
routingRow.setId(null); |
|
|
|
routingRow.setQuoteId(detail.getQuoteId()); |
|
|
|
routingRow.setQuoteDetailId(detail.getId()); |
|
|
|
routingRow.setQuoteDetailItemNo(detail.getItemNo()); |
|
|
|
routingRow.setSite(detail.getSite()); |
|
|
|
routingRow.setBuNo(detail.getBuNo()); |
|
|
|
routingRow.setQuoteNo(detail.getQuoteNo()); |
|
|
|
routingRow.setVersionNo(detail.getVersionNo()); |
|
|
|
routingRow.setCreateBy(createBy); |
|
|
|
routingRow.setCreateDate(createDate); |
|
|
|
routingRow.setTreeId(treeId); |
|
|
|
routingRow.setMachCycleTime(BigDecimal.ZERO); |
|
|
|
routingRow.setLaborCycleTime(BigDecimal.ZERO); |
|
|
|
routingRow.setResourceNum(0); |
|
|
|
if (Objects.isNull(routingRow.getWorkCenterCost())) { |
|
|
|
routingRow.setWorkCenterCost(BigDecimal.ZERO); |
|
|
|
} |
|
|
|
if (Objects.isNull(routingRow.getManuCost())) { |
|
|
|
routingRow.setManuCost(routingRow.getWorkCenterCost()); |
|
|
|
} |
|
|
|
} |
|
|
|
// SQL Server + MyBatis-Plus 在 saveBatch 回填自增主键时可能抛出: |
|
|
|
// "The statement must be executed before any results can be obtained." |
|
|
|
// 这里改为逐条插入,规避驱动层 generated keys 兼容性问题。 |
|
|
|
for (QuoteDetailRouting routingRow : nodeData.routingRows) { |
|
|
|
quoteDetailRoutingService.save(routingRow); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 解析加工中心成本并做本次请求级缓存,避免同一加工中心重复调用数据库函数。 |
|
|
|
*/ |
|
|
|
private BigDecimal resolveWorkCenterCost(String site, String workCenterNo, Map<String, BigDecimal> workCenterCostCache) { |
|
|
|
if (!StringUtils.hasText(site) || !StringUtils.hasText(workCenterNo)) { |
|
|
|
return BigDecimal.ZERO; |
|
|
|
} |
|
|
|
String cacheKey = site + "_" + workCenterNo; |
|
|
|
return workCenterCostCache.computeIfAbsent(cacheKey, key -> { |
|
|
|
BigDecimal cost = baseMapper.queryWorkCenterCost(site, workCenterNo); |
|
|
|
return Objects.nonNull(cost) ? cost : BigDecimal.ZERO; |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
private QuoteDetailBom getPurchaseComponentPart(QuoteDetail detail, QuoteDetailBomTree bom, Integer lineSequence) { |
|
|
|
QuoteDetailBom purchase = new QuoteDetailBom(); |
|
|
|
purchase.setQuoteDetailId(detail.getId()); |
|
|
|
|