fix: expand related routings through multi-level BOM

parent ded51bc0
......@@ -25,6 +25,7 @@ import com.aps.service.ApsTimeConfigService;
import com.aps.service.LanuchService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import io.swagger.v3.oas.models.security.SecurityScheme;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
......@@ -38,6 +39,7 @@ import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
......@@ -164,6 +166,59 @@ public class MacroPlannerDataConverter {
// ==================== 步骤1: 数据加载 ====================
static <T> LambdaQueryWrapper<T> whereIn(SFunction<T, ?> column, Collection<?> values) {
List<?> uniqueValues = new ArrayList<>(new LinkedHashSet<>(values));
LambdaQueryWrapper<T> wrapper = new LambdaQueryWrapper<>();
if (uniqueValues.isEmpty()) {
return wrapper.apply("1 = 0");
}
return wrapper.and(group -> {
for (int offset = 0; offset < uniqueValues.size(); offset += 1000) {
if (offset > 0) group.or();
group.in(column, uniqueValues.subList(offset, Math.min(offset + 1000, uniqueValues.size())));
}
});
}
private List<Integer> expandRoutingHeaders(ConvertContext ctx, Set<String> materialIds) {
Map<Integer, RoutingHeader> headers = new LinkedHashMap<>();
for (RoutingHeader header : ctx.routingHeaders) {
if (header.getId() != null) headers.putIfAbsent(header.getId(), header);
}
Set<String> queriedMaterials = new HashSet<>(materialIds);
List<Integer> frontier = new ArrayList<>(headers.keySet());
int depth = 0;
while (!frontier.isEmpty()) {
List<Routingsupporting> inputs = routingsupportingMapper.selectList(
whereIn(Routingsupporting::getRoutingHeaderId, frontier)
.eq(Routingsupporting::getIsdeleted, 0));
Set<String> nextMaterials = inputs.stream()
.map(Routingsupporting::getMaterialId)
.filter(Objects::nonNull)
.collect(Collectors.toCollection(LinkedHashSet::new));
materialIds.addAll(nextMaterials);
nextMaterials.removeAll(queriedMaterials);
queriedMaterials.addAll(nextMaterials);
List<Integer> nextFrontier = new ArrayList<>();
if (!nextMaterials.isEmpty()) {
List<RoutingHeader> children = routingHeaderMapper.selectList(
whereIn(RoutingHeader::getMaterialId, nextMaterials));
for (RoutingHeader child : children) {
Integer childId = child.getId();
if (childId != null && !headers.containsKey(childId)) {
headers.put(childId, child);
nextFrontier.add(childId);
}
}
}
log.info("BOM展开: 层={}, 本层工艺={}, 投料={}, 新工艺={}, 累计工艺={}",
++depth, frontier.size(), inputs.size(), nextFrontier.size(), headers.size());
frontier = nextFrontier;
}
ctx.routingHeaders = new ArrayList<>(headers.values());
return new ArrayList<>(headers.keySet());
}
private ConvertContext loadRawData(String sceneId) {
ConvertContext ctx = new ConvertContext();
......@@ -225,8 +280,7 @@ public class MacroPlannerDataConverter {
// 3. 工艺路线头表
if (!materialIds.isEmpty()) {
ctx.routingHeaders = routingHeaderMapper.selectList(
new LambdaQueryWrapper<RoutingHeader>()
.in(RoutingHeader::getMaterialId, materialIds));
whereIn(RoutingHeader::getMaterialId, materialIds));
routingIds = ctx.routingHeaders.stream()
.map(RoutingHeader::getId)
......@@ -238,40 +292,7 @@ public class MacroPlannerDataConverter {
// 4. 工序 (通过 LanuchService 批量查询)
if (!routingIds.isEmpty()) {
List<Integer> routingIds1=routingIds;
// while (routingIds1!=null&&routingIds1.size()>0) {
// List<Routingsupporting> rss = routingsupportingMapper.selectList(
// new LambdaQueryWrapper<Routingsupporting>()
// .in(Routingsupporting::getRoutingHeaderId, routingIds)
// .eq(Routingsupporting::getIsdeleted, 0));
// if (rss != null && rss.size() > 0) {
// Set<String> materialIdrss = rss.stream()
// .map(Routingsupporting::getMaterialId)
// .filter(Objects::nonNull)
// .distinct()
// .collect(Collectors.toSet());
// materialIds.addAll(materialIdrss);
// ctx.routingsupportings.addAll(rss);
//
// List<RoutingHeader> rhs = routingHeaderMapper.selectList(
// new LambdaQueryWrapper<RoutingHeader>()
// .in(RoutingHeader::getMaterialId, materialIdrss));
//
// if (rhs != null && rhs.size() > 0) {
// routingIds1 = rhs.stream()
// .map(RoutingHeader::getId)
// .filter(Objects::nonNull)
// .distinct()
// .collect(Collectors.toList());
// routingIds.addAll(routingIds1);
// ctx.routingHeaders.addAll(rhs);
// } else {
// routingIds1 = null;
// }
// } else {
// routingIds1 = null;
// }
// }
routingIds = expandRoutingHeaders(ctx, materialIds);
List<Long> routingIdsLong = routingIds.stream()
.map(Long::valueOf)
.collect(Collectors.toList());
......@@ -285,8 +306,7 @@ public class MacroPlannerDataConverter {
// 5. 工艺物料消耗 (BOM)
if (!routingIds.isEmpty()) {
ctx.routingsupportings = routingsupportingMapper.selectList(
new LambdaQueryWrapper<Routingsupporting>()
.in(Routingsupporting::getRoutingHeaderId, routingIds)
whereIn(Routingsupporting::getRoutingHeaderId, routingIds)
.eq(Routingsupporting::getIsdeleted, 0));
}
log.info("加载工艺物料消耗: {} 条", ctx.routingsupportings.size());
......@@ -305,32 +325,26 @@ public class MacroPlannerDataConverter {
// 7. 物料主数据
if (!materialIds.isEmpty()) {
ctx.materialInfos = materialInfoMapper.selectList(
new LambdaQueryWrapper<MaterialInfo>()
.in(MaterialInfo::getId, materialIds));
whereIn(MaterialInfo::getId, materialIds));
}
log.info("加载物料主数据: {} 条", ctx.materialInfos.size());
// 8. 库存、采购、在途 (按 materialId 过滤, 避免全表扫描)
if (!materialIds.isEmpty()) {
ctx.stocks = stockMapper.selectList(
new LambdaQueryWrapper<Stock>()
.in(Stock::getMaterialId, materialIds)
whereIn(Stock::getMaterialId, materialIds)
.eq(Stock::getIsdeleted, 0));
ctx.materialPurchases = materialPurchaseMapper.selectList(
new LambdaQueryWrapper<MaterialPurchase>()
.in(MaterialPurchase::getMaterialId, materialIds)
whereIn(MaterialPurchase::getMaterialId, materialIds)
.eq(MaterialPurchase::getIsdeleted, 0));
ctx.erpPurchaseOrders = erpPurchaseOrderMapper.selectList(
new LambdaQueryWrapper<ErpPurchaseOrder>()
.in(ErpPurchaseOrder::getMaterialId, materialIds)
whereIn(ErpPurchaseOrder::getMaterialId, materialIds)
.eq(ErpPurchaseOrder::getIsdeleted, 0));
ctx.purchaseReceipts = purchaseReceiptMapper.selectList(
new LambdaQueryWrapper<PurchaseReceipt>()
.in(PurchaseReceipt::getMaterialid, materialIds)
whereIn(PurchaseReceipt::getMaterialid, materialIds)
.eq(PurchaseReceipt::getIsdeleted, 0));
ctx.sjzPfWhStocks = sjzPfWhStockMapper.selectList(
new LambdaQueryWrapper<SjzPfWhStock>()
.in(SjzPfWhStock::getMaterialid, materialIds)
whereIn(SjzPfWhStock::getMaterialid, materialIds)
.eq(SjzPfWhStock::getIsdeleted, 0));
}
log.info("加载库存: {}, 采购: {}, ERP采购订单: {}, 待验: {}, 半成品在途: {}",
......@@ -346,8 +360,7 @@ public class MacroPlannerDataConverter {
.collect(Collectors.toList());
if (!detailIds.isEmpty()) {
ctx.routingDetailEquips = routingDetailEquipMapper.selectList(
new LambdaQueryWrapper<RoutingDetailEquip>()
.in(RoutingDetailEquip::getRoutingDetailId, detailIds)
whereIn(RoutingDetailEquip::getRoutingDetailId, detailIds)
.eq(RoutingDetailEquip::getIsdeleted, 0));
}
}
......@@ -360,8 +373,7 @@ public class MacroPlannerDataConverter {
.collect(Collectors.toSet());
if (!equipIds.isEmpty()) {
ctx.planResources = planResourceMapper.selectList(
new LambdaQueryWrapper<PlanResource>()
.in(PlanResource::getId, equipIds)
whereIn(PlanResource::getId, equipIds)
.eq(PlanResource::getIsdeleted, false));
}
// 通过 PlanResource.referenceId 查询 Equipinfo
......@@ -371,8 +383,7 @@ public class MacroPlannerDataConverter {
.collect(Collectors.toSet());
if (!equipinfoIds.isEmpty()) {
ctx.equipinfos = equipinfoMapper.selectList(
new LambdaQueryWrapper<Equipinfo>()
.in(Equipinfo::getId, equipinfoIds)
whereIn(Equipinfo::getId, equipinfoIds)
.eq(Equipinfo::getIsdeleted, false));
}
log.info("加载设备资源: {}, 设备信息: {}", ctx.planResources.size(), ctx.equipinfos.size());
......@@ -385,8 +396,7 @@ public class MacroPlannerDataConverter {
.collect(Collectors.toSet());
if (!planResourceIds.isEmpty()) {
ctx.equipShiftCapacities = equipShiftCapacityMapper.selectList(
new LambdaQueryWrapper<EquipShiftCapacity>()
.in(EquipShiftCapacity::getPlanResourceId, planResourceIds)
whereIn(EquipShiftCapacity::getPlanResourceId, planResourceIds)
.ge(EquipShiftCapacity::getCapacityDate, ctx.baseTime)
.lt(EquipShiftCapacity::getCapacityDate, horizonEndDateTime)
.eq(EquipShiftCapacity::getIsDeleted, 0));
......
package com.aps.macroplanner.data;
import com.aps.entity.RoutingHeader;
import com.aps.entity.Routingsupporting;
import com.aps.mapper.RoutingHeaderMapper;
import com.aps.mapper.RoutingsupportingMapper;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.springframework.test.util.ReflectionTestUtils;
import java.lang.reflect.Constructor;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
class MacroPlannerBomLoadingTest {
private MacroPlannerDataConverter converter;
private RoutingHeaderMapper headers;
private RoutingsupportingMapper inputs;
private Object context;
@BeforeEach
void setUp() throws Exception {
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), "bom-test"), RoutingHeader.class);
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), "bom-test"), Routingsupporting.class);
converter = new MacroPlannerDataConverter();
headers = mock(RoutingHeaderMapper.class);
inputs = mock(RoutingsupportingMapper.class);
ReflectionTestUtils.setField(converter, "routingHeaderMapper", headers);
ReflectionTestUtils.setField(converter, "routingsupportingMapper", inputs);
Class<?> contextType = Class.forName(MacroPlannerDataConverter.class.getName() + "$ConvertContext");
Constructor<?> constructor = contextType.getDeclaredConstructor();
constructor.setAccessible(true);
context = constructor.newInstance();
ReflectionTestUtils.setField(context, "routingHeaders", new ArrayList<>(Collections.singletonList(header(1, "A"))));
}
@Test
@Timeout(5)
void eachLevelLoadsOnlyNewRoutingIds() {
List<Set<Object>> queriedRoutes = new ArrayList<>();
when(inputs.selectList(any())).thenAnswer(call -> {
Set<Object> ids = values(call.getArgument(0));
ids.remove(0);
queriedRoutes.add(ids);
if (ids.equals(Collections.singleton(1))) return Collections.singletonList(input(1, "B"));
if (ids.equals(Collections.singleton(2))) return Collections.singletonList(input(2, "C"));
return Collections.emptyList();
});
when(headers.selectList(any())).thenAnswer(call -> values(call.getArgument(0)).contains("B")
? Collections.singletonList(header(2, "B")) : Collections.emptyList());
Set<String> materials = new HashSet<>(Collections.singleton("A"));
List<Integer> result = ReflectionTestUtils.invokeMethod(converter, "expandRoutingHeaders", context, materials);
assertEquals(Arrays.asList(1, 2), result);
assertEquals(Arrays.asList(Collections.singleton(1), Collections.singleton(2)), queriedRoutes);
assertEquals(new HashSet<>(Arrays.asList("A", "B", "C")), materials);
verify(headers, times(2)).selectList(any());
}
@Test
@Timeout(5)
void cyclicBomTerminatesWithoutDiscardingReachableHeaders() {
when(inputs.selectList(any())).thenAnswer(call -> values(call.getArgument(0)).contains(1)
? Collections.singletonList(input(1, "B")) : Collections.singletonList(input(2, "A")));
when(headers.selectList(any())).thenReturn(Collections.singletonList(header(2, "B")));
List<Integer> result = ReflectionTestUtils.invokeMethod(converter, "expandRoutingHeaders", context,
new HashSet<>(Collections.singleton("A")));
assertEquals(Arrays.asList(1, 2), result);
verify(inputs, times(2)).selectList(any());
verify(headers, times(1)).selectList(any());
}
@Test
void duplicateChildHeadersAreLoadedOnlyOnce() {
when(inputs.selectList(any())).thenReturn(Arrays.asList(input(1, "B"), input(1, "B")), Collections.emptyList());
when(headers.selectList(any())).thenReturn(Arrays.asList(header(2, "B"), header(2, "B"), header(3, "B")));
List<Integer> result = ReflectionTestUtils.invokeMethod(converter, "expandRoutingHeaders", context,
new HashSet<>(Collections.singleton("A")));
assertEquals(Arrays.asList(1, 2, 3), result);
verify(headers, times(1)).selectList(any());
verify(inputs, times(2)).selectList(any());
}
@Test
void nullInputMaterialDoesNotProduceUnboundedHeaderQuery() {
when(inputs.selectList(any())).thenReturn(Collections.singletonList(input(1, null)));
List<Integer> result = ReflectionTestUtils.invokeMethod(converter, "expandRoutingHeaders", context,
new HashSet<>(Collections.singleton("A")));
assertEquals(Collections.singletonList(1), result);
verifyNoInteractions(headers);
}
@Test
void oracleInGroupsAreBoundedDeduplicatedAndParenthesized() {
List<Integer> ids = IntStream.range(1, 2002).boxed().collect(Collectors.toList());
ids.add(1);
LambdaQueryWrapper<RoutingHeader> wrapper = MacroPlannerDataConverter.whereIn(RoutingHeader::getId, ids);
String predicate = wrapper.getSqlSegment();
assertEquals(2001, wrapper.getParamNameValuePairs().size());
assertEquals(3, predicate.split(" IN ", -1).length - 1);
assertEquals(2, predicate.split(" OR ", -1).length - 1);
java.util.regex.Matcher groups = java.util.regex.Pattern.compile(" IN [(]([^)]+)[)]").matcher(predicate);
List<Integer> sizes = new ArrayList<>();
while (groups.find()) sizes.add(groups.group(1).split(",").length);
assertEquals(Arrays.asList(1000, 1000, 1), sizes);
wrapper.eq(RoutingHeader::getIsDeleted, false);
assertTrue(wrapper.getSqlSegment().contains(") AND is_deleted ="));
assertTrue(MacroPlannerDataConverter.whereIn(RoutingHeader::getId, Collections.emptyList())
.getSqlSegment().contains("1 = 0"));
}
private Set<Object> values(LambdaQueryWrapper<?> wrapper) {
wrapper.getSqlSegment();
return new HashSet<>(wrapper.getParamNameValuePairs().values());
}
private RoutingHeader header(int id, String material) {
RoutingHeader header = new RoutingHeader();
header.setId(id);
header.setMaterialId(material);
return header;
}
private Routingsupporting input(int routingId, String material) {
Routingsupporting input = new Routingsupporting();
input.setRoutingHeaderId(routingId);
input.setMaterialId(material);
return input;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment