Browse Source

2026-03-12

1、询价申请增加“图纸存放目录”,并增加节点特殊权限
2、优化报价【切换版本】超时问题
master
fengyuan_yang 1 month ago
parent
commit
692c73e703
  1. 2
      src/main/java/com/spring/modules/quotation/entity/QuotationInformationEntity.java
  2. 29
      src/main/java/com/spring/modules/quotation/service/impl/QuotationInformationServiceImpl.java
  3. 118
      src/main/java/com/spring/modules/quote/service/impl/QuoteDetailBomTreeServiceImpl.java
  4. 4
      src/main/resources/mapper/quotation/QuotationInformationMapper.xml

2
src/main/java/com/spring/modules/quotation/entity/QuotationInformationEntity.java

@ -185,4 +185,6 @@ public class QuotationInformationEntity extends QueryPage implements Serializabl
private String place; private String place;
private String drawingStorageDirectory;
} }

29
src/main/java/com/spring/modules/quotation/service/impl/QuotationInformationServiceImpl.java

@ -663,6 +663,17 @@ public class QuotationInformationServiceImpl extends ServiceImpl<QuotationInform
// 查出询价单相关表的数据 // 查出询价单相关表的数据
// 主信息 // 主信息
QuotationInformationEntity changeRequest = quotationInformationMapper.selectOne(new QueryWrapper<QuotationInformationEntity>().eq("site", data.getSite()).eq("quotation_no", data.getQuotationNo())); QuotationInformationEntity changeRequest = quotationInformationMapper.selectOne(new QueryWrapper<QuotationInformationEntity>().eq("site", data.getSite()).eq("quotation_no", data.getQuotationNo()));
// 流程特殊管控下达前检查节点权限配置
PlmProcessControllBaseData issuedCData = new PlmProcessControllBaseData();
issuedCData.setSite(data.getSite());
issuedCData.setNodeId(nodeDetails.get(0).getNodeId());
issuedCData.setWorkflowId(baseData.get("workflowId"));
List<PlmProcessControllBaseData> issuedControlList = requestManageService.getProcessSelect(issuedCData);
if (!issuedControlList.isEmpty()) {
for (PlmProcessControllBaseData ctrl : issuedControlList) {
checkProcessControl(ctrl.getRoleId(), ctrl.getSite(), data.getQuotationBatchNo(), data.getQuotationAmount(), changeRequest.getDrawingStorageDirectory());
}
}
// 根据字段对应的数据库表+字段查询出数据 // 根据字段对应的数据库表+字段查询出数据
for (PlmRequestDetailVo nodeDetail : nodeDetails) { for (PlmRequestDetailVo nodeDetail : nodeDetails) {
if (nodeDetail.getId() == null) { if (nodeDetail.getId() == null) {
@ -815,7 +826,7 @@ public class QuotationInformationServiceImpl extends ServiceImpl<QuotationInform
List<PlmProcessControllBaseData> controlList = requestManageService.getProcessSelect(cData); List<PlmProcessControllBaseData> controlList = requestManageService.getProcessSelect(cData);
if (!controlList.isEmpty()) { if (!controlList.isEmpty()) {
for (int i = 0; i < controlList.size(); i++) { for (int i = 0; i < controlList.size(); i++) {
checkProcessControl(controlList.get(i).getRoleId(), controlList.get(i).getSite(), data.getQuotationBatchNo(), data.getQuotationAmount());
checkProcessControl(controlList.get(i).getRoleId(), controlList.get(i).getSite(), data.getQuotationBatchNo(), data.getQuotationAmount(), changeRequest.getDrawingStorageDirectory());
} }
} }
@ -894,6 +905,22 @@ public class QuotationInformationServiceImpl extends ServiceImpl<QuotationInform
} }
} }
private void checkProcessControl(Integer roleId, String site, String quotationBatchNo, BigDecimal quotationAmount, String drawingStorageDirectory) {
checkProcessControl(roleId, site, quotationBatchNo, quotationAmount);
if (roleId.equals(14)) {
// 校验附件信息页签必须有数据 或者 图纸存放目录有值
long ossCount = sysOssDao.selectCount(
new QueryWrapper<SysOssEntity>()
.eq("order_ref1", site)
.eq("order_ref2", quotationBatchNo)
);
boolean hasDrawingDir = org.springframework.util.StringUtils.hasText(drawingStorageDirectory);
if (ossCount == 0 && !hasDrawingDir) {
throw new RuntimeException("附件信息页签必须有数据或填写图纸存放目录,才能提交!");
}
}
}
/** /**
* 流程干预提交 * 流程干预提交
* @param data * @param data

118
src/main/java/com/spring/modules/quote/service/impl/QuoteDetailBomTreeServiceImpl.java

@ -25,6 +25,8 @@ import org.springframework.util.StringUtils;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.*; import java.util.*;
import java.util.concurrent.*;
import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Service @Service
@ -50,6 +52,13 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl<QuoteDetailBomTre
private Server server = null; 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, public QuoteDetailBomTreeServiceImpl(IfsServer ifsServer,
@Value("${ifs-control.ifs-username}")String ifsUsername, @Value("${ifs-control.ifs-username}")String ifsUsername,
@Value("${ifs-control.ifs-password}") String ifsPassword) { @Value("${ifs-control.ifs-password}") String ifsPassword) {
@ -90,9 +99,9 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl<QuoteDetailBomTre
*/ */
@Override @Override
public long initQuoteDetailBomTree(QuoteDetail detail, Long parentId, Integer level) { 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) { if (rootNode == null) {
return 0; return 0;
} }
@ -107,8 +116,16 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl<QuoteDetailBomTre
/** /**
* 递归收集 BOM 树节点数据所有耗时操作IFS 接口调用数据库查询均在此完成 * 递归收集 BOM 树节点数据所有耗时操作IFS 接口调用数据库查询均在此完成
* 返回 null 表示当前节点无 BOM 信息 * 返回 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 类型 // 确定 BOM 类型
if (parentId.equals(0L)) { if (parentId.equals(0L)) {
detail.setBomType("Manufacturing"); detail.setBomType("Manufacturing");
@ -150,7 +167,7 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl<QuoteDetailBomTre
int totalComponents = componentParts.size(); int totalComponents = componentParts.size();
long needCostQueryCount = componentParts.stream().filter(c -> "Y".equals(c.getStatus())).count(); long needCostQueryCount = componentParts.stream().filter(c -> "Y".equals(c.getStatus())).count();
log.info("[BOM_PROCESS] 开始处理子物料 - 父物料: {}, 层级: {}, 子物料总数: {}, 需查询成本数: {}",
log.info("[BOM_PROCESS] 开始处理子物料 - 父物料: {}, 层级: {}, 子物料总数: {}, 需并行查询成本数: {}",
bom.getPartNo(), level, totalComponents, needCostQueryCount); bom.getPartNo(), level, totalComponents, needCostQueryCount);
BomNodeData nodeData = new BomNodeData(); BomNodeData nodeData = new BomNodeData();
@ -158,9 +175,7 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl<QuoteDetailBomTre
nodeData.routing = routing; nodeData.routing = routing;
nodeData.isRoot = parentId.equals(0L); nodeData.isRoot = parentId.equals(0L);
int costQuerySuccessCount = 0;
int costQueryFailCount = 0;
// 串行阶段构建 BOM 层级结构递归有顺序依赖不可并行
for (QuoteDetailBom component : componentParts) { for (QuoteDetailBom component : componentParts) {
component.setCreateBy(detail.getCreateBy()); component.setCreateBy(detail.getCreateBy());
component.setCreateDate(detail.getCreateDate()); component.setCreateDate(detail.getCreateDate());
@ -171,11 +186,11 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl<QuoteDetailBomTre
if (Objects.nonNull(childBomCheck) && "Y".equals(component.getBomFlag())) { if (Objects.nonNull(childBomCheck) && "Y".equals(component.getBomFlag())) {
log.debug("[BOM_PROCESS] 递归处理半成品 - PartNo: {}", component.getComponentPart()); log.debug("[BOM_PROCESS] 递归处理半成品 - PartNo: {}", component.getComponentPart());
// 递归收集子节点parentId 占位 -1写入时由 doSaveBomDataRecursive 覆盖 // 递归收集子节点parentId 占位 -1写入时由 doSaveBomDataRecursive 覆盖
BomNodeData childNode = collectBomData(childDetail, -1L, level + 1, ifsCon);
BomNodeData childNode = collectBomData(childDetail, -1L, level + 1, ifsConFactory);
if (childNode != null) { if (childNode != null) {
nodeData.children.add(childNode); nodeData.children.add(childNode);
} }
// 半成品bomId 在写入阶段填充先清零价格后续 IFS 查询会覆盖
// 半成品bomId 在写入阶段填充先清零价格IFS 并行阶段会覆盖
component.setBomFlag("Y"); component.setBomFlag("Y");
component.setUnitPrice(BigDecimal.ZERO); component.setUnitPrice(BigDecimal.ZERO);
component.setActualPrice(BigDecimal.ZERO); component.setActualPrice(BigDecimal.ZERO);
@ -184,20 +199,66 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl<QuoteDetailBomTre
component.setBomFlag("N"); component.setBomFlag("N");
} }
nodeData.components.add(component);
}
// 并行阶段并发查询各子物料 IFS 成本互相独立可并行
// 与原逻辑保持一致无论是否半成品只要是正式物料status="Y"都查询 IFS 参考成本 // 与原逻辑保持一致无论是否半成品只要是正式物料status="Y"都查询 IFS 参考成本
// 半成品的 IFS 参考成本用于在材料页签中展示成本计算仍以 BOM 明细为准 // 半成品的 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) { if (component.getUnitPrice() != null && component.getUnitPrice().compareTo(BigDecimal.ZERO) > 0) {
costQuerySuccessCount++; costQuerySuccessCount++;
} else { } else {
costQueryFailCount++; costQueryFailCount++;
} }
} }
nodeData.components.add(component);
}
log.info("[BOM_PROCESS] 子物料处理完成 - 父物料: {}, 层级: {}, 处理总数: {}, 成本查询成功: {}, 成本查询失败: {}", log.info("[BOM_PROCESS] 子物料处理完成 - 父物料: {}, 层级: {}, 处理总数: {}, 成本查询成功: {}, 成本查询失败: {}",
bom.getPartNo(), level, costQuerySuccessCount + costQueryFailCount, costQuerySuccessCount, costQueryFailCount); bom.getPartNo(), level, costQuerySuccessCount + costQueryFailCount, costQuerySuccessCount, costQueryFailCount);
@ -281,10 +342,10 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl<QuoteDetailBomTre
detail.setAlternativeNo(tree.getAlternativeNo()); detail.setAlternativeNo(tree.getAlternativeNo());
// 先在事务外完成所有 IFS 调用和数据收集避免 IFS 阻塞期间持有数据库行锁 // 先在事务外完成所有 IFS 调用和数据收集避免 IFS 阻塞期间持有数据库行锁
Server ifsCon = resolveIfsCon();
Supplier<Server> ifsConFactory = resolveIfsConFactory();
Long newParentId = bomTree.getParentId(); Long newParentId = bomTree.getParentId();
Integer newLevel = Optional.ofNullable(tree.getLevel()).orElse(0); Integer newLevel = Optional.ofNullable(tree.getLevel()).orElse(0);
BomNodeData nodeData = collectBomData(detail, newParentId, newLevel, ifsCon);
BomNodeData nodeData = collectBomData(detail, newParentId, newLevel, ifsConFactory);
// 数据已就绪开始执行 DML事务持有锁的时间仅限于纯 DB 写入阶段 // 数据已就绪开始执行 DML事务持有锁的时间仅限于纯 DB 写入阶段
List<Long> ids = getAllChildIds(detail, bomTree.getId()); List<Long> ids = getAllChildIds(detail, bomTree.getId());
@ -315,8 +376,8 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl<QuoteDetailBomTre
detail.setAlternativeNo(tree.getAlternativeNo()); 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) { if (nodeData != null) {
doSaveBomDataRecursive(nodeData, detail, 0L); 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(); String username = ((SysUserEntity) SecurityUtils.getSubject().getPrincipal()).getUsername();
SysUserEntity ifsUser = sysUserDao.selectOne(new QueryWrapper<SysUserEntity>().eq("username", username)); SysUserEntity ifsUser = sysUserDao.selectOne(new QueryWrapper<SysUserEntity>().eq("username", username));
if (ifsUser == null if (ifsUser == null
@ -551,7 +616,14 @@ public class QuoteDetailBomTreeServiceImpl extends ServiceImpl<QuoteDetailBomTre
|| !org.apache.commons.lang3.StringUtils.isNotBlank(ifsUser.getIfsPassword())) { || !org.apache.commons.lang3.StringUtils.isNotBlank(ifsUser.getIfsPassword())) {
throw new RuntimeException("请维护IFS账号和密码!"); 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) { private void handleTool(QuoteDetail detail, QuoteDetailRouting routing) {

4
src/main/resources/mapper/quotation/QuotationInformationMapper.xml

@ -62,6 +62,7 @@
<result column="reject_flag" property="rejectFlag"/> <result column="reject_flag" property="rejectFlag"/>
<result column="plm_part_no" property="plmPartNo"/> <result column="plm_part_no" property="plmPartNo"/>
<result column="place" property="place"/> <result column="place" property="place"/>
<result column="drawing_storage_directory" property="drawingStorageDirectory"/>
</resultMap> </resultMap>
<!-- 报价信息列表 --> <!-- 报价信息列表 -->
<select id="quotationInformationSearch" resultMap="getQuotation" parameterType="com.spring.modules.quotation.vo.QuotationInformationVo"> <select id="quotationInformationSearch" resultMap="getQuotation" parameterType="com.spring.modules.quotation.vo.QuotationInformationVo">
@ -116,6 +117,7 @@
a.reject_flag, a.reject_flag,
a.reject_step_id, a.reject_step_id,
a.place, a.place,
a.drawing_storage_directory,
vpp.plm_part_no, vpp.plm_part_no,
<if test="query.menuId != null and query.menuId != ''"> <if test="query.menuId != null and query.menuId != ''">
d.is_reject, d.is_reject,
@ -264,6 +266,7 @@
a.reject_flag, a.reject_flag,
a.reject_step_id, a.reject_step_id,
a.place, a.place,
a.drawing_storage_directory,
vpp.plm_part_no, vpp.plm_part_no,
d.is_reject, d.is_reject,
d.node_id, d.node_id,
@ -849,6 +852,7 @@
a.reject_flag, a.reject_flag,
a.reject_step_id, a.reject_step_id,
a.place, a.place,
a.drawing_storage_directory,
vpp.plm_part_no, vpp.plm_part_no,
<if test="query.menuId != null and query.menuId != ''"> <if test="query.menuId != null and query.menuId != ''">
d.is_reject, d.is_reject,

Loading…
Cancel
Save