|
|
|
@ -56,32 +56,67 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl<QuoteDetailBomTre |
|
|
|
this.server = ifsServer.getIfsServer(ifsUsername,ifsPassword); |
|
|
|
} |
|
|
|
|
|
|
|
// ========================================================================= |
|
|
|
// 用于在事务外承载收集好的待写入数据 |
|
|
|
// ========================================================================= |
|
|
|
|
|
|
|
/** |
|
|
|
* 一个 BOM 节点及其对应的 Routing、子物料数据包。 |
|
|
|
* collectBomData 阶段:所有耗时的 IFS 调用、数据库 SELECT 均在此阶段完成。 |
|
|
|
* doSaveBomData 阶段:事务内只做纯粹的数据库写入,不含任何外部 IO。 |
|
|
|
*/ |
|
|
|
private static class BomNodeData { |
|
|
|
QuoteDetailBomTree tree; |
|
|
|
QuoteDetailRouting routing; |
|
|
|
/** 当前节点下的子物料列表(已填充好成本价格) */ |
|
|
|
List<QuoteDetailBom> components = new ArrayList<>(); |
|
|
|
/** 是否顶级节点(决定是否触发 handleTool) */ |
|
|
|
boolean isRoot; |
|
|
|
/** |
|
|
|
* 子节点数据包,与 components 中 bomFlag="Y" 的条目按顺序一一对应。 |
|
|
|
* 写入时先写子节点拿到真实 treeId,再回填 component.bomId。 |
|
|
|
*/ |
|
|
|
List<BomNodeData> 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<SysUserEntity>().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); |
|
|
|
} |
|
|
|
|
|
|
|
// ========================================================================= |
|
|
|
// 阶段一:递归收集数据(无事务,含 IFS 调用) |
|
|
|
// ========================================================================= |
|
|
|
|
|
|
|
// 1、通过PartNo、Site和BuNo 查询BOM信息 失效日期日期为空 替代为* 和Routing (存在BOM的物料) |
|
|
|
if (parentId.equals(0L)){ |
|
|
|
// 顶级物料强制使用 Manufacturing 类型 |
|
|
|
/** |
|
|
|
* 递归收集 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<QuoteDetailBomTre |
|
|
|
log.info("子物料 {} 使用用户选择的BOM类型 {}", detail.getPartNo(), detail.getBomType()); |
|
|
|
} |
|
|
|
} |
|
|
|
// 获取BOM信息 |
|
|
|
|
|
|
|
QuoteDetailBomTree bom = baseMapper.queryPartBom(detail); |
|
|
|
if (Objects.isNull(bom)){ |
|
|
|
return 0; |
|
|
|
if (Objects.isNull(bom)) { |
|
|
|
return null; |
|
|
|
} |
|
|
|
bom.setParentId(parentId); |
|
|
|
bom.setLevel(level); |
|
|
|
// 2、通过Bom查询 Bom子物料 |
|
|
|
log.info("BOM信息:{}",bom); |
|
|
|
save(bom); |
|
|
|
// 查BOM对应Routing |
|
|
|
log.info("BOM信息:{}", bom); |
|
|
|
|
|
|
|
// 查询 Routing |
|
|
|
QuoteDetailRouting routing = baseMapper.queryPartBomRouting(detail); |
|
|
|
if (Objects.nonNull(routing)) { |
|
|
|
routing.setTreeId(bom.getId()); |
|
|
|
routing.setCreateBy(detail.getCreateBy()); |
|
|
|
routing.setCreateDate(new Date()); |
|
|
|
quoteDetailRoutingService.saveQuoteDetailRouting(routing); |
|
|
|
} |
|
|
|
// 当为顶级BOM 需要插入对应Routing的工具信息 |
|
|
|
if (parentId.equals(0L)){ |
|
|
|
handleTool(detail, routing); |
|
|
|
} |
|
|
|
// 查询BOM的子物料 |
|
|
|
|
|
|
|
// 查询子物料 |
|
|
|
List<QuoteDetailBom> 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] 开始处理子物料 - 父物料: {}, 层级: {}, 子物料总数: {}, 需查询成本数: {}", |
|
|
|
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"); |
|
|
|
} |
|
|
|
// 正式物料:在事务外调用 IFS 查询成本 |
|
|
|
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++; |
|
|
|
} |
|
|
|
} |
|
|
|
//新增子物料信息 |
|
|
|
component.setTreeId(bom.getId()); |
|
|
|
quoteDetailBomService.save(component); |
|
|
|
} |
|
|
|
nodeData.components.add(component); |
|
|
|
} |
|
|
|
|
|
|
|
// 统计日志:记录处理结果 |
|
|
|
log.info("[BOM_PROCESS] 子物料处理完成 - 父物料: {}, 层级: {}, 处理总数: {}, 成本查询成功: {}, 成本查询失败: {}", |
|
|
|
bom.getPartNo(), level, processedCount, costQuerySuccessCount, costQueryFailCount); |
|
|
|
bom.getPartNo(), level, costQuerySuccessCount + costQueryFailCount, costQuerySuccessCount, costQueryFailCount); |
|
|
|
|
|
|
|
return bom.getId(); |
|
|
|
return nodeData; |
|
|
|
} |
|
|
|
|
|
|
|
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<QuoteDetailTool> 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); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
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; |
|
|
|
} |
|
|
|
// ========================================================================= |
|
|
|
// 阶段二:短事务,递归写入(无外部 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<String, String> 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; |
|
|
|
} |
|
|
|
@Transactional |
|
|
|
public long doSaveBomData(BomNodeData nodeData, QuoteDetail detail) { |
|
|
|
return doSaveBomDataRecursive(nodeData, detail, nodeData.tree.getParentId()); |
|
|
|
} |
|
|
|
|
|
|
|
// 如果不是最后一次重试,等待后继续(仅对网络/临时性错误重试) |
|
|
|
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()); |
|
|
|
/** |
|
|
|
* 递归写入 BOM 树节点,全程无外部 IO,事务持有时间极短。 |
|
|
|
*/ |
|
|
|
private long doSaveBomDataRecursive(BomNodeData nodeData, QuoteDetail detail, Long parentId) { |
|
|
|
QuoteDetailBomTree bom = nodeData.tree; |
|
|
|
bom.setParentId(parentId); |
|
|
|
|
|
|
|
// 如果不是最后一次重试,等待后继续 |
|
|
|
if (retryCount < MAX_RETRY_COUNT) { |
|
|
|
try { |
|
|
|
Thread.sleep(RETRY_INTERVAL_MS); |
|
|
|
} catch (InterruptedException ie) { |
|
|
|
Thread.currentThread().interrupt(); |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
// 写入 BOM 树节点,获得自增 ID |
|
|
|
save(bom); |
|
|
|
long treeId = bom.getId(); |
|
|
|
|
|
|
|
// 所有重试都失败,设置成本为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); |
|
|
|
} |
|
|
|
// 写入 Routing |
|
|
|
if (Objects.nonNull(nodeData.routing)) { |
|
|
|
nodeData.routing.setTreeId(treeId); |
|
|
|
quoteDetailRoutingService.saveQuoteDetailRouting(nodeData.routing); |
|
|
|
} |
|
|
|
|
|
|
|
private QuoteDetailBomTree isComponentBom(QuoteDetail component) { |
|
|
|
// 根据子物料信息检查是否有BOM信息 |
|
|
|
return baseMapper.queryPartBom(component); |
|
|
|
// 顶级节点:处理工具信息 |
|
|
|
if (nodeData.isRoot) { |
|
|
|
handleTool(detail, nodeData.routing); |
|
|
|
} |
|
|
|
|
|
|
|
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; |
|
|
|
// 递归写入子节点,收集 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++; |
|
|
|
} |
|
|
|
|
|
|
|
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()); |
|
|
|
component.setTreeId(treeId); |
|
|
|
} |
|
|
|
|
|
|
|
@Override |
|
|
|
public List<QuoteDetailBomTree> queryDetailBomTree(QuoteDetail detail) { |
|
|
|
// 查询 所有 BOM 信息 |
|
|
|
List<QuoteDetailBomTree> list = baseMapper.queryDetailBomTree(detail); |
|
|
|
// 生成BOM树 |
|
|
|
QuoteDetailBomTree quoteDetailBomTree = buildTree(list); |
|
|
|
// 转换成List |
|
|
|
List<QuoteDetailBomTree> treeList = new ArrayList<>(); |
|
|
|
if (Objects.nonNull(quoteDetailBomTree)){ |
|
|
|
treeList.add(quoteDetailBomTree); |
|
|
|
} |
|
|
|
return treeList; |
|
|
|
// 写入子物料(SQL Server 逐条 save 以确保自增主键回填,与原逻辑保持一致) |
|
|
|
for (QuoteDetailBom component : nodeData.components) { |
|
|
|
quoteDetailBomService.save(component); |
|
|
|
} |
|
|
|
|
|
|
|
@Override |
|
|
|
public List<Long> getAllChildIds(QuoteDetail detail,Long id) { |
|
|
|
List<QuoteDetailBomTree> allNodes = getAllNodes(detail); |
|
|
|
List<Long> result = new ArrayList<>(); |
|
|
|
findChildIds(id, allNodes, result); |
|
|
|
return result; |
|
|
|
return treeId; |
|
|
|
} |
|
|
|
|
|
|
|
@Override |
|
|
|
public List<QuoteDetailBomTree> 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<QuoteDetailBomTree> queryDetailBomAlternative(QuoteDetailBom bom) { |
|
|
|
return baseMapper.queryDetailBomAlternative(bom); |
|
|
|
} |
|
|
|
// ========================================================================= |
|
|
|
// 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<Long> 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<Long> 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<QuoteDetailBomTre |
|
|
|
detail.setBuNo(tree.getBuNo()); |
|
|
|
detail.setEngChgLevel(tree.getEngChgLevel()); |
|
|
|
detail.setAlternativeNo(tree.getAlternativeNo()); |
|
|
|
initQuoteDetailBomTree(detail, 0L,0); |
|
|
|
|
|
|
|
// 先收集数据,再写入 |
|
|
|
Server ifsCon = resolveIfsCon(); |
|
|
|
BomNodeData nodeData = collectBomData(detail, 0L, 0, ifsCon); |
|
|
|
if (nodeData != null) { |
|
|
|
doSaveBomDataRecursive(nodeData, detail, 0L); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 如果勾选了重新计算成本,则执行成本计算 |
|
|
|
if (Boolean.TRUE.equals(tree.getRecalculateCost()) && detail != null) { |
|
|
|
log.info("[BOM_SWITCH] Recalculate cost is enabled, executing cost calculation for QuoteDetailId: {}", detail.getId()); |
|
|
|
try { |
|
|
|
@ -455,62 +329,32 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl<QuoteDetailBomTre |
|
|
|
throw new RuntimeException("成本计算失败: " + e.getMessage()); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public List<QuoteDetailBomTree> getAllNodes(QuoteDetail detail) { |
|
|
|
return baseMapper.queryDetailBomTree(detail); |
|
|
|
} |
|
|
|
|
|
|
|
// 递归方法 |
|
|
|
private void findChildIds(Long id, List<QuoteDetailBomTree> allNodes, List<Long> result) { |
|
|
|
// 过滤出当前节点的子节点 |
|
|
|
List<QuoteDetailBomTree> 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<QuoteDetailBomTree> 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<QuoteDetailBomTree> list = queryDetailBomTree(detail); |
|
|
|
|
|
|
|
// 创建老ID到新ID的映射关系 |
|
|
|
Map<Long, Long> oldToNewIdMapping = new HashMap<>(); |
|
|
|
loopTree(list, quoteDetail, 0L, oldToNewIdMapping); |
|
|
|
|
|
|
|
// 复制完成后,更新BOM明细中的bomId引用 |
|
|
|
// 批量更新 bomId 引用(替代原来循环逐条 updateById) |
|
|
|
updateBomIdReferencesWithCorrectLogic(quoteDetail.getId(), oldToNewIdMapping); |
|
|
|
} |
|
|
|
|
|
|
|
private void loopTree (List<QuoteDetailBomTree> list,QuoteDetail quoteDetail,Long parentId){ |
|
|
|
if (!list.isEmpty()){ |
|
|
|
private void loopTree(List<QuoteDetailBomTree> 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<QuoteDetailBomTre |
|
|
|
tree.setId(null); |
|
|
|
save(tree); |
|
|
|
|
|
|
|
// 调用Bom的again方 |
|
|
|
quoteDetailBomService.againQuoteDetailBom(tree, id); |
|
|
|
// 调用Routing的again方法 |
|
|
|
quoteDetailRoutingService.againQuoteDetailRouting(tree, id); |
|
|
|
loopTree(tree.getList(),quoteDetail,tree.getId()); |
|
|
|
loopTree(tree.getList(), quoteDetail, tree.getId()); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
@ -544,12 +386,9 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl<QuoteDetailBomTre |
|
|
|
tree.setId(null); |
|
|
|
save(tree); |
|
|
|
|
|
|
|
// 记录老ID到新ID的映射关系 |
|
|
|
oldToNewIdMapping.put(oldId, tree.getId()); |
|
|
|
|
|
|
|
// 调用Bom的again方 |
|
|
|
quoteDetailBomService.againQuoteDetailBom(tree, oldId); |
|
|
|
// 调用Routing的again方法 |
|
|
|
quoteDetailRoutingService.againQuoteDetailRouting(tree, oldId); |
|
|
|
loopTree(tree.getList(), quoteDetail, tree.getId(), oldToNewIdMapping); |
|
|
|
} |
|
|
|
@ -557,38 +396,82 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl<QuoteDetailBomTre |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 根据正确的业务逻辑更新BOM明细中的bomId引用 |
|
|
|
* bomId应该指向子BOM树节点的ID,而不是根节点ID |
|
|
|
* 批量更新 BOM 明细中的 bomId 引用(替代原来循环逐条 updateById) |
|
|
|
*/ |
|
|
|
private void updateBomIdReferencesWithCorrectLogic(Long quoteDetailId, Map<Long, Long> oldToNewIdMapping) { |
|
|
|
// 查询新复制的所有BOM明细 |
|
|
|
if (oldToNewIdMapping.isEmpty()) { |
|
|
|
return; |
|
|
|
} |
|
|
|
List<QuoteDetailBom> bomList = quoteDetailBomService.lambdaQuery() |
|
|
|
.eq(QuoteDetailBom::getQuoteDetailId, quoteDetailId) |
|
|
|
.isNotNull(QuoteDetailBom::getBomId) // 只处理有bomId的记录 |
|
|
|
.isNotNull(QuoteDetailBom::getBomId) |
|
|
|
.list(); |
|
|
|
|
|
|
|
List<QuoteDetailBom> 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<QuoteDetailBomTree> queryDetailBomTree(QuoteDetail detail) { |
|
|
|
List<QuoteDetailBomTree> list = baseMapper.queryDetailBomTree(detail); |
|
|
|
QuoteDetailBomTree quoteDetailBomTree = buildTree(list); |
|
|
|
List<QuoteDetailBomTree> treeList = new ArrayList<>(); |
|
|
|
if (Objects.nonNull(quoteDetailBomTree)) { |
|
|
|
treeList.add(quoteDetailBomTree); |
|
|
|
} |
|
|
|
return treeList; |
|
|
|
} |
|
|
|
|
|
|
|
@Override |
|
|
|
public List<Long> getAllChildIds(QuoteDetail detail, Long id) { |
|
|
|
List<QuoteDetailBomTree> allNodes = getAllNodes(detail); |
|
|
|
List<Long> result = new ArrayList<>(); |
|
|
|
findChildIds(id, allNodes, result); |
|
|
|
return result; |
|
|
|
} |
|
|
|
|
|
|
|
@Override |
|
|
|
public List<QuoteDetailBomTree> 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<QuoteDetailBomTree> queryDetailBomAlternative(QuoteDetailBom bom) { |
|
|
|
return baseMapper.queryDetailBomAlternative(bom); |
|
|
|
} |
|
|
|
|
|
|
|
public List<QuoteDetailBomTree> 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 ""; |
|
|
|
@ -598,11 +481,9 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl<QuoteDetailBomTre |
|
|
|
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; |
|
|
|
@ -610,9 +491,7 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl<QuoteDetailBomTre |
|
|
|
return result; |
|
|
|
} |
|
|
|
|
|
|
|
// 正式物料(status='Y')调用IFS接口 |
|
|
|
try { |
|
|
|
// 获取当前用户的IFS连接 |
|
|
|
String username = ((SysUserEntity) SecurityUtils.getSubject().getPrincipal()).getUsername(); |
|
|
|
SysUserEntity ifsUser = sysUserDao.selectOne(new QueryWrapper<SysUserEntity>().eq("username", username)); |
|
|
|
if (ifsUser == null || !org.apache.commons.lang3.StringUtils.isNotBlank(ifsUser.getIfsUsername()) |
|
|
|
@ -625,7 +504,6 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl<QuoteDetailBomTre |
|
|
|
} |
|
|
|
Server ifsCon = ifsServer.getIfsServer(ifsUser.getIfsUsername(), ifsUser.getIfsPassword()); |
|
|
|
|
|
|
|
// 调用IFS接口获取预估材料成本 |
|
|
|
PartInformationEntity part = new PartInformationEntity(); |
|
|
|
part.setSite(site); |
|
|
|
part.setPartNo(partNo); |
|
|
|
@ -656,4 +534,197 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl<QuoteDetailBomTre |
|
|
|
public String queryPartType(String site, String partNo) { |
|
|
|
return baseMapper.queryPartType(site, partNo); |
|
|
|
} |
|
|
|
|
|
|
|
// ========================================================================= |
|
|
|
// 私有辅助方法 |
|
|
|
// ========================================================================= |
|
|
|
|
|
|
|
/** 获取当前登录用户的 IFS Server 连接(在事务外调用) */ |
|
|
|
private Server resolveIfsCon() { |
|
|
|
String username = ((SysUserEntity) SecurityUtils.getSubject().getPrincipal()).getUsername(); |
|
|
|
SysUserEntity ifsUser = sysUserDao.selectOne(new QueryWrapper<SysUserEntity>().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<QuoteDetailTool> 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<String, String> 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<QuoteDetailBomTree> allNodes, List<Long> result) { |
|
|
|
List<QuoteDetailBomTree> 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<QuoteDetailBomTree> nodes) { |
|
|
|
QuoteDetailBomTree root = null; |
|
|
|
for (QuoteDetailBomTree node : nodes) { |
|
|
|
if (node.getParentId() == null || node.getParentId() == 0) { |
|
|
|
root = node; |
|
|
|
root.addChildren(nodes); |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
return root; |
|
|
|
} |
|
|
|
} |