|
|
|
@ -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<QuoteDetailBomTre |
|
|
|
|
|
|
|
private Server server = null; |
|
|
|
|
|
|
|
/** IFS 成本查询并行线程数,限制 4 路并发,避免对 IFS 造成过大压力 */ |
|
|
|
private static final int IFS_PARALLEL_SIZE = 4; |
|
|
|
private static final ExecutorService IFS_COST_EXECUTOR = Executors.newFixedThreadPool( |
|
|
|
IFS_PARALLEL_SIZE, |
|
|
|
r -> { 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<QuoteDetailBomTre |
|
|
|
*/ |
|
|
|
@Override |
|
|
|
public long initQuoteDetailBomTree(QuoteDetail detail, Long parentId, Integer level) { |
|
|
|
// 阶段一:事务外,获取 IFS 连接并递归收集数据 |
|
|
|
Server ifsCon = resolveIfsCon(); |
|
|
|
BomNodeData rootNode = collectBomData(detail, parentId, level, ifsCon); |
|
|
|
// 阶段一:事务外,获取 IFS 连接工厂并递归收集数据(含并行 IFS 成本查询) |
|
|
|
Supplier<Server> ifsConFactory = resolveIfsConFactory(); |
|
|
|
BomNodeData rootNode = collectBomData(detail, parentId, level, ifsConFactory); |
|
|
|
if (rootNode == null) { |
|
|
|
return 0; |
|
|
|
} |
|
|
|
@ -107,8 +116,16 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl<QuoteDetailBomTre |
|
|
|
/** |
|
|
|
* 递归收集 BOM 树节点数据。所有耗时操作(IFS 接口调用、数据库查询)均在此完成。 |
|
|
|
* 返回 null 表示当前节点无 BOM 信息。 |
|
|
|
* |
|
|
|
* <p>执行分两个阶段: |
|
|
|
* <ol> |
|
|
|
* <li>串行阶段:构建 BOM 层级结构(递归,有顺序依赖)</li> |
|
|
|
* <li>并行阶段:并发查询各子物料 IFS 成本(互相独立,{@value IFS_PARALLEL_SIZE} 路并发)</li> |
|
|
|
* </ol> |
|
|
|
* |
|
|
|
* @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<Server> ifsConFactory) { |
|
|
|
// 确定 BOM 类型 |
|
|
|
if (parentId.equals(0L)) { |
|
|
|
detail.setBomType("Manufacturing"); |
|
|
|
@ -150,7 +167,7 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl<QuoteDetailBomTre |
|
|
|
|
|
|
|
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); |
|
|
|
|
|
|
|
BomNodeData nodeData = new BomNodeData(); |
|
|
|
@ -158,9 +175,7 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl<QuoteDetailBomTre |
|
|
|
nodeData.routing = routing; |
|
|
|
nodeData.isRoot = parentId.equals(0L); |
|
|
|
|
|
|
|
int costQuerySuccessCount = 0; |
|
|
|
int costQueryFailCount = 0; |
|
|
|
|
|
|
|
// ── 串行阶段:构建 BOM 层级结构(递归,有顺序依赖,不可并行)──────────────── |
|
|
|
for (QuoteDetailBom component : componentParts) { |
|
|
|
component.setCreateBy(detail.getCreateBy()); |
|
|
|
component.setCreateDate(detail.getCreateDate()); |
|
|
|
@ -171,11 +186,11 @@ 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, ifsCon); |
|
|
|
BomNodeData childNode = collectBomData(childDetail, -1L, level + 1, ifsConFactory); |
|
|
|
if (childNode != null) { |
|
|
|
nodeData.children.add(childNode); |
|
|
|
} |
|
|
|
// 半成品:bomId 在写入阶段填充,先清零价格(后续 IFS 查询会覆盖) |
|
|
|
// 半成品:bomId 在写入阶段填充,先清零价格(IFS 并行阶段会覆盖) |
|
|
|
component.setBomFlag("Y"); |
|
|
|
component.setUnitPrice(BigDecimal.ZERO); |
|
|
|
component.setActualPrice(BigDecimal.ZERO); |
|
|
|
@ -184,20 +199,66 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl<QuoteDetailBomTre |
|
|
|
component.setBomFlag("N"); |
|
|
|
} |
|
|
|
|
|
|
|
nodeData.components.add(component); |
|
|
|
} |
|
|
|
|
|
|
|
// ── 并行阶段:并发查询各子物料 IFS 成本(互相独立,可并行)──────────────────── |
|
|
|
// 与原逻辑保持一致:无论是否半成品,只要是正式物料(status="Y")都查询 IFS 参考成本 |
|
|
|
// 半成品的 IFS 参考成本用于在材料页签中展示,成本计算仍以 BOM 明细为准 |
|
|
|
if ("Y".equals(component.getStatus())) { |
|
|
|
getFinalPartCost(component, ifsCon); |
|
|
|
List<QuoteDetailBom> 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<Server> connectionPool = new LinkedBlockingQueue<>(); |
|
|
|
for (int i = 0; i < poolSize; i++) { |
|
|
|
connectionPool.add(ifsConFactory.get()); |
|
|
|
} |
|
|
|
|
|
|
|
List<CompletableFuture<Void>> 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++; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
nodeData.components.add(component); |
|
|
|
} |
|
|
|
|
|
|
|
log.info("[BOM_PROCESS] 子物料处理完成 - 父物料: {}, 层级: {}, 处理总数: {}, 成本查询成功: {}, 成本查询失败: {}", |
|
|
|
bom.getPartNo(), level, costQuerySuccessCount + costQueryFailCount, costQuerySuccessCount, costQueryFailCount); |
|
|
|
|
|
|
|
@ -281,10 +342,10 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl<QuoteDetailBomTre |
|
|
|
detail.setAlternativeNo(tree.getAlternativeNo()); |
|
|
|
|
|
|
|
// 先在事务外完成所有 IFS 调用和数据收集,避免 IFS 阻塞期间持有数据库行锁 |
|
|
|
Server ifsCon = resolveIfsCon(); |
|
|
|
Supplier<Server> 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<Long> ids = getAllChildIds(detail, bomTree.getId()); |
|
|
|
@ -315,8 +376,8 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl<QuoteDetailBomTre |
|
|
|
detail.setAlternativeNo(tree.getAlternativeNo()); |
|
|
|
|
|
|
|
// 先收集数据,再写入 |
|
|
|
Server ifsCon = resolveIfsCon(); |
|
|
|
BomNodeData nodeData = collectBomData(detail, 0L, 0, ifsCon); |
|
|
|
Supplier<Server> 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<QuoteDetailBomTre |
|
|
|
// 私有辅助方法 |
|
|
|
// ========================================================================= |
|
|
|
|
|
|
|
/** 获取当前登录用户的 IFS Server 连接(在事务外调用) */ |
|
|
|
private Server resolveIfsCon() { |
|
|
|
/** |
|
|
|
* 解析当前登录用户的 IFS 账号密码,返回一个连接工厂(Supplier)。 |
|
|
|
* 工厂在主线程调用(持有 SecurityContext),可安全传入子线程使用。 |
|
|
|
* 每次调用 get() 会创建一个新的 Server 连接,供各并发线程独立持有。 |
|
|
|
*/ |
|
|
|
private Supplier<Server> resolveIfsConFactory() { |
|
|
|
String username = ((SysUserEntity) SecurityUtils.getSubject().getPrincipal()).getUsername(); |
|
|
|
SysUserEntity ifsUser = sysUserDao.selectOne(new QueryWrapper<SysUserEntity>().eq("username", username)); |
|
|
|
if (ifsUser == null |
|
|
|
@ -551,7 +616,14 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl<QuoteDetailBomTre |
|
|
|
|| !org.apache.commons.lang3.StringUtils.isNotBlank(ifsUser.getIfsPassword())) { |
|
|
|
throw new RuntimeException("请维护IFS账号和密码!"); |
|
|
|
} |
|
|
|
return ifsServer.getIfsServer(ifsUser.getIfsUsername(), ifsUser.getIfsPassword()); |
|
|
|
final String ifsUsername = ifsUser.getIfsUsername(); |
|
|
|
final String ifsPassword = ifsUser.getIfsPassword(); |
|
|
|
return () -> ifsServer.getIfsServer(ifsUsername, ifsPassword); |
|
|
|
} |
|
|
|
|
|
|
|
/** 获取当前登录用户的 IFS Server 连接(单连接场景,在事务外调用) */ |
|
|
|
private Server resolveIfsCon() { |
|
|
|
return resolveIfsConFactory().get(); |
|
|
|
} |
|
|
|
|
|
|
|
private void handleTool(QuoteDetail detail, QuoteDetailRouting routing) { |
|
|
|
|