Commit 73c9b8d7 authored by Tong Li's avatar Tong Li

Merge remote-tracking branch 'origin/master' into tl

# Conflicts:
#	src/main/java/com/aps/controller/MacroPlannerResultController.java
parents c2c74064 c6774944
...@@ -532,8 +532,8 @@ public class MacroPlannerResultController { ...@@ -532,8 +532,8 @@ public class MacroPlannerResultController {
description = "执行数据库转换→数据验证→MIP建模→分层求解→JSON结果保存的完整流程。" description = "执行数据库转换→数据验证→MIP建模→分层求解→JSON结果保存的完整流程。"
+ "耗时通常数秒到数分钟, 取决于数据规模。") + "耗时通常数秒到数分钟, 取决于数据规模。")
public R<Map<String, Object>> runOptimization( public R<Map<String, Object>> runOptimization(
@RequestParam("sceneId") @Parameter(description = "场景ID", required = true) String sceneId, @RequestParam("sceneId") @Parameter(description = "场景ID", required = true) String sceneId
@RequestParam("strategyId") @Parameter(description = "kpi设置", required = true) Integer strategyId) { , @RequestParam("strategyId") @Parameter(description = "kpi设置", required = true) Integer strategyId) {
if (sceneId == null || sceneId.trim().isEmpty()) { if (sceneId == null || sceneId.trim().isEmpty()) {
return R.failed("sceneId不能为空"); return R.failed("sceneId不能为空");
} }
...@@ -561,7 +561,7 @@ public class MacroPlannerResultController { ...@@ -561,7 +561,7 @@ public class MacroPlannerResultController {
long solveStart = System.currentTimeMillis(); long solveStart = System.currentTimeMillis();
MacroPlannerOptimizer optimizer = new MacroPlannerOptimizer(data); MacroPlannerOptimizer optimizer = new MacroPlannerOptimizer(data);
optimizer.buildModel(); optimizer.buildModel();
optimizer.solve(sid); optimizer.solve();
long solveEnd = System.currentTimeMillis(); long solveEnd = System.currentTimeMillis();
// 4. 保存结果到 JSON 文件 // 4. 保存结果到 JSON 文件
......
package com.aps.controller; package com.aps.controller;
import org.springframework.web.bind.annotation.RequestMapping; import com.aps.common.util.R;
import org.springframework.web.bind.annotation.RestController; import com.aps.entity.MacroSceneConfig;
import com.aps.entity.PlanPeriod;
import com.aps.macroplanner.scene.MacroSceneContext;
import com.aps.macroplanner.scene.MacroSceneService;
import com.aps.service.PlanPeriodService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
/**
* <p>
* 前端控制器
* </p>
*
* @author MyBatis-Plus
* @since 2026-09-20
*/
@RestController @RestController
@RequestMapping("/planPeriod") @RequestMapping("/planPeriod")
@RequiredArgsConstructor
public class PlanPeriodController { public class PlanPeriodController {
private final PlanPeriodService planPeriodService;
private final MacroSceneService macroSceneService;
@PostMapping("/create")
@Operation(summary = "新增场景计划周期")
public R<PlanPeriod> create(@RequestParam(required = false) String sceneId,
@RequestBody PlanPeriod period) {
String targetScene = resolveScene(sceneId);
return MacroSceneContext.execute(targetScene, () -> {
period.setId(UUID.randomUUID().toString());
period.setMpSceneId(targetScene);
period.setIsdeleted(0L);
period.setDeletiontime(null);
period.setDeleteruserid(null);
period.setCreationtime(LocalDateTime.now());
if (!planPeriodService.save(period)) {
return R.failed("新增计划周期失败");
}
return R.ok(period);
});
}
@GetMapping("/list")
@Operation(summary = "查询场景计划周期")
public R<List<PlanPeriod>> list(@RequestParam(required = false) String sceneId) {
String targetScene = resolveScene(sceneId);
return MacroSceneContext.execute(targetScene, () ->
R.ok(planPeriodService.list(new LambdaQueryWrapper<PlanPeriod>()
.eq(PlanPeriod::getIsdeleted, 0))));
}
private String resolveScene(String sceneId) {
String targetScene = sceneId == null ? MacroSceneContext.getSceneId() : sceneId.trim();
if (targetScene == null || targetScene.isEmpty()) return null;
MacroSceneConfig scene = macroSceneService.getScene(targetScene);
if (scene == null || !targetScene.equals(scene.getSceneId())
|| !"READY".equals(scene.getSceneStatus())) {
throw new IllegalArgumentException("场景不存在或尚未就绪: " + targetScene);
}
return targetScene;
}
} }
package com.aps.entity; package com.aps.entity;
import lombok.Data; import lombok.Data;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
...@@ -8,6 +12,7 @@ import lombok.Data; ...@@ -8,6 +12,7 @@ import lombok.Data;
@Data @Data
public class PlanPeriod { public class PlanPeriod {
@TableId(type = IdType.INPUT)
private String id; private String id;
private LocalDateTime creationtime; private LocalDateTime creationtime;
private Long creatoruserid; private Long creatoruserid;
...@@ -34,5 +39,6 @@ private Long isvisable; ...@@ -34,5 +39,6 @@ private Long isvisable;
private Long top; private Long top;
private LocalDateTime basetime; private LocalDateTime basetime;
private Short enabled; private Short enabled;
@TableField(value = "MP_SCENE_ID", fill = FieldFill.INSERT)
private String mpSceneId; private String mpSceneId;
} }
\ No newline at end of file
...@@ -28,6 +28,7 @@ import com.aps.service.ApsTimeConfigService; ...@@ -28,6 +28,7 @@ import com.aps.service.ApsTimeConfigService;
import com.aps.service.LanuchService; import com.aps.service.LanuchService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import io.swagger.v3.oas.models.security.SecurityScheme; import io.swagger.v3.oas.models.security.SecurityScheme;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
...@@ -43,6 +44,7 @@ import java.time.temporal.WeekFields; ...@@ -43,6 +44,7 @@ import java.time.temporal.WeekFields;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Collection;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
...@@ -130,6 +132,7 @@ public class MacroPlannerDataConverter { ...@@ -130,6 +132,7 @@ public class MacroPlannerDataConverter {
// ==================== 注入的 Service ==================== // ==================== 注入的 Service ====================
@Autowired @Autowired
private LanuchService lanuchService; private LanuchService lanuchService;
@Autowired @Autowired
private PlanPeriodService planPeriodService; private PlanPeriodService planPeriodService;
...@@ -145,7 +148,7 @@ public class MacroPlannerDataConverter { ...@@ -145,7 +148,7 @@ public class MacroPlannerDataConverter {
* @return 填充好的 TestDataBuilder, 可直接传给 MacroPlannerOptimizer * @return 填充好的 TestDataBuilder, 可直接传给 MacroPlannerOptimizer
*/ */
public TestDataBuilder convert(String sceneId,Integer kpiSetting) { public TestDataBuilder convert(String sceneId,Integer kpiSetting) {
return MacroSceneContext.execute(sceneId, () -> convertScoped(sceneId, kpiSetting)); return MacroSceneContext.execute(sceneId, () -> convertScoped(sceneId,kpiSetting));
} }
...@@ -172,6 +175,59 @@ public class MacroPlannerDataConverter { ...@@ -172,6 +175,59 @@ public class MacroPlannerDataConverter {
// ==================== 步骤1: 数据加载 ==================== // ==================== 步骤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,Integer kpiSetting) { private ConvertContext loadRawData(String sceneId,Integer kpiSetting) {
ConvertContext ctx = new ConvertContext(); ConvertContext ctx = new ConvertContext();
...@@ -204,8 +260,10 @@ public class MacroPlannerDataConverter { ...@@ -204,8 +260,10 @@ public class MacroPlannerDataConverter {
ctx.horizonEnd = calculateHorizonEnd(ctx.periodDimension, baseDate, periodCount); ctx.horizonEnd = calculateHorizonEnd(ctx.periodDimension, baseDate, periodCount);
LocalDateTime horizonEndDateTime = ctx.horizonEnd.atStartOfDay(); LocalDateTime horizonEndDateTime = ctx.horizonEnd.atStartOfDay();
log.info("排产周期: dimension={}, periodCount={}, baseTime={}, horizonEnd={}", log.info("排产周期: dimension={}, periodCount={}, baseTime={}, horizonEnd={}",
ctx.periodDimension, periodCount, ctx.baseTime, ctx.horizonEnd);log.info("排产周期: dimension={}, periodCount={}, baseTime={}, horizonEnd={}",
ctx.periodDimension, periodCount, ctx.baseTime, ctx.horizonEnd); ctx.periodDimension, periodCount, ctx.baseTime, ctx.horizonEnd);
// 1. 需求订单 (ApsDemandOrder, 按 deliverytime 时间范围过滤)
// 1. 需求订单 (ApsDemandOrder, 按 deliverytime 时间范围过滤) // 1. 需求订单 (ApsDemandOrder, 按 deliverytime 时间范围过滤)
ctx.apsDemandOrders = apsDemandOrderMapper.selectList( ctx.apsDemandOrders = apsDemandOrderMapper.selectList(
new LambdaQueryWrapper<ApsDemandOrder>().eq(ApsDemandOrder::getIsdeleted, 0) new LambdaQueryWrapper<ApsDemandOrder>().eq(ApsDemandOrder::getIsdeleted, 0)
...@@ -232,8 +290,7 @@ public class MacroPlannerDataConverter { ...@@ -232,8 +290,7 @@ public class MacroPlannerDataConverter {
// 3. 工艺路线头表 // 3. 工艺路线头表
if (!materialIds.isEmpty()) { if (!materialIds.isEmpty()) {
ctx.routingHeaders = routingHeaderMapper.selectList( ctx.routingHeaders = routingHeaderMapper.selectList(
new LambdaQueryWrapper<RoutingHeader>() whereIn(RoutingHeader::getMaterialId, materialIds));
.in(RoutingHeader::getMaterialId, materialIds));
routingIds = ctx.routingHeaders.stream() routingIds = ctx.routingHeaders.stream()
.map(RoutingHeader::getId) .map(RoutingHeader::getId)
...@@ -245,40 +302,7 @@ public class MacroPlannerDataConverter { ...@@ -245,40 +302,7 @@ public class MacroPlannerDataConverter {
// 4. 工序 (通过 LanuchService 批量查询) // 4. 工序 (通过 LanuchService 批量查询)
if (!routingIds.isEmpty()) { if (!routingIds.isEmpty()) {
List<Integer> routingIds1=routingIds; routingIds = expandRoutingHeaders(ctx, materialIds);
// 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;
// }
// }
List<Long> routingIdsLong = routingIds.stream() List<Long> routingIdsLong = routingIds.stream()
.map(Long::valueOf) .map(Long::valueOf)
.collect(Collectors.toList()); .collect(Collectors.toList());
...@@ -292,8 +316,7 @@ public class MacroPlannerDataConverter { ...@@ -292,8 +316,7 @@ public class MacroPlannerDataConverter {
// 5. 工艺物料消耗 (BOM) // 5. 工艺物料消耗 (BOM)
if (!routingIds.isEmpty()) { if (!routingIds.isEmpty()) {
ctx.routingsupportings = routingsupportingMapper.selectList( ctx.routingsupportings = routingsupportingMapper.selectList(
new LambdaQueryWrapper<Routingsupporting>() whereIn(Routingsupporting::getRoutingHeaderId, routingIds)
.in(Routingsupporting::getRoutingHeaderId, routingIds)
.eq(Routingsupporting::getIsdeleted, 0)); .eq(Routingsupporting::getIsdeleted, 0));
} }
log.info("加载工艺物料消耗: {} 条", ctx.routingsupportings.size()); log.info("加载工艺物料消耗: {} 条", ctx.routingsupportings.size());
...@@ -312,32 +335,26 @@ public class MacroPlannerDataConverter { ...@@ -312,32 +335,26 @@ public class MacroPlannerDataConverter {
// 7. 物料主数据 // 7. 物料主数据
if (!materialIds.isEmpty()) { if (!materialIds.isEmpty()) {
ctx.materialInfos = materialInfoMapper.selectList( ctx.materialInfos = materialInfoMapper.selectList(
new LambdaQueryWrapper<MaterialInfo>() whereIn(MaterialInfo::getId, materialIds));
.in(MaterialInfo::getId, materialIds));
} }
log.info("加载物料主数据: {} 条", ctx.materialInfos.size()); log.info("加载物料主数据: {} 条", ctx.materialInfos.size());
// 8. 库存、采购、在途 (按 materialId 过滤, 避免全表扫描) // 8. 库存、采购、在途 (按 materialId 过滤, 避免全表扫描)
if (!materialIds.isEmpty()) { if (!materialIds.isEmpty()) {
ctx.stocks = stockMapper.selectList( ctx.stocks = stockMapper.selectList(
new LambdaQueryWrapper<Stock>() whereIn(Stock::getMaterialId, materialIds)
.in(Stock::getMaterialId, materialIds)
.eq(Stock::getIsdeleted, 0)); .eq(Stock::getIsdeleted, 0));
ctx.materialPurchases = materialPurchaseMapper.selectList( ctx.materialPurchases = materialPurchaseMapper.selectList(
new LambdaQueryWrapper<MaterialPurchase>() whereIn(MaterialPurchase::getMaterialId, materialIds)
.in(MaterialPurchase::getMaterialId, materialIds)
.eq(MaterialPurchase::getIsdeleted, 0)); .eq(MaterialPurchase::getIsdeleted, 0));
ctx.erpPurchaseOrders = erpPurchaseOrderMapper.selectList( ctx.erpPurchaseOrders = erpPurchaseOrderMapper.selectList(
new LambdaQueryWrapper<ErpPurchaseOrder>() whereIn(ErpPurchaseOrder::getMaterialId, materialIds)
.in(ErpPurchaseOrder::getMaterialId, materialIds)
.eq(ErpPurchaseOrder::getIsdeleted, 0)); .eq(ErpPurchaseOrder::getIsdeleted, 0));
ctx.purchaseReceipts = purchaseReceiptMapper.selectList( ctx.purchaseReceipts = purchaseReceiptMapper.selectList(
new LambdaQueryWrapper<PurchaseReceipt>() whereIn(PurchaseReceipt::getMaterialid, materialIds)
.in(PurchaseReceipt::getMaterialid, materialIds)
.eq(PurchaseReceipt::getIsdeleted, 0)); .eq(PurchaseReceipt::getIsdeleted, 0));
ctx.sjzPfWhStocks = sjzPfWhStockMapper.selectList( ctx.sjzPfWhStocks = sjzPfWhStockMapper.selectList(
new LambdaQueryWrapper<SjzPfWhStock>() whereIn(SjzPfWhStock::getMaterialid, materialIds)
.in(SjzPfWhStock::getMaterialid, materialIds)
.eq(SjzPfWhStock::getIsdeleted, 0)); .eq(SjzPfWhStock::getIsdeleted, 0));
} }
log.info("加载库存: {}, 采购: {}, ERP采购订单: {}, 待验: {}, 半成品在途: {}", log.info("加载库存: {}, 采购: {}, ERP采购订单: {}, 待验: {}, 半成品在途: {}",
...@@ -353,8 +370,7 @@ public class MacroPlannerDataConverter { ...@@ -353,8 +370,7 @@ public class MacroPlannerDataConverter {
.collect(Collectors.toList()); .collect(Collectors.toList());
if (!detailIds.isEmpty()) { if (!detailIds.isEmpty()) {
ctx.routingDetailEquips = routingDetailEquipMapper.selectList( ctx.routingDetailEquips = routingDetailEquipMapper.selectList(
new LambdaQueryWrapper<RoutingDetailEquip>() whereIn(RoutingDetailEquip::getRoutingDetailId, detailIds)
.in(RoutingDetailEquip::getRoutingDetailId, detailIds)
.eq(RoutingDetailEquip::getIsdeleted, 0)); .eq(RoutingDetailEquip::getIsdeleted, 0));
} }
} }
...@@ -367,8 +383,7 @@ public class MacroPlannerDataConverter { ...@@ -367,8 +383,7 @@ public class MacroPlannerDataConverter {
.collect(Collectors.toSet()); .collect(Collectors.toSet());
if (!equipIds.isEmpty()) { if (!equipIds.isEmpty()) {
ctx.planResources = planResourceMapper.selectList( ctx.planResources = planResourceMapper.selectList(
new LambdaQueryWrapper<PlanResource>() whereIn(PlanResource::getId, equipIds)
.in(PlanResource::getId, equipIds)
.eq(PlanResource::getIsdeleted, false)); .eq(PlanResource::getIsdeleted, false));
} }
// 通过 PlanResource.referenceId 查询 Equipinfo // 通过 PlanResource.referenceId 查询 Equipinfo
...@@ -378,8 +393,7 @@ public class MacroPlannerDataConverter { ...@@ -378,8 +393,7 @@ public class MacroPlannerDataConverter {
.collect(Collectors.toSet()); .collect(Collectors.toSet());
if (!equipinfoIds.isEmpty()) { if (!equipinfoIds.isEmpty()) {
ctx.equipinfos = equipinfoMapper.selectList( ctx.equipinfos = equipinfoMapper.selectList(
new LambdaQueryWrapper<Equipinfo>() whereIn(Equipinfo::getId, equipinfoIds)
.in(Equipinfo::getId, equipinfoIds)
.eq(Equipinfo::getIsdeleted, false)); .eq(Equipinfo::getIsdeleted, false));
} }
log.info("加载设备资源: {}, 设备信息: {}", ctx.planResources.size(), ctx.equipinfos.size()); log.info("加载设备资源: {}, 设备信息: {}", ctx.planResources.size(), ctx.equipinfos.size());
...@@ -392,22 +406,16 @@ public class MacroPlannerDataConverter { ...@@ -392,22 +406,16 @@ public class MacroPlannerDataConverter {
.collect(Collectors.toSet()); .collect(Collectors.toSet());
if (!planResourceIds.isEmpty()) { if (!planResourceIds.isEmpty()) {
ctx.equipShiftCapacities = equipShiftCapacityMapper.selectList( ctx.equipShiftCapacities = equipShiftCapacityMapper.selectList(
new LambdaQueryWrapper<EquipShiftCapacity>() whereIn(EquipShiftCapacity::getPlanResourceId, planResourceIds)
.in(EquipShiftCapacity::getPlanResourceId, planResourceIds)
.ge(EquipShiftCapacity::getCapacityDate, ctx.baseTime) .ge(EquipShiftCapacity::getCapacityDate, ctx.baseTime)
.lt(EquipShiftCapacity::getCapacityDate, horizonEndDateTime) .lt(EquipShiftCapacity::getCapacityDate, horizonEndDateTime)
.eq(EquipShiftCapacity::getIsDeleted, 0)); .eq(EquipShiftCapacity::getIsDeleted, 0));
} }
log.info("加载设备产能日历(EquipShiftCapacity): {} 条 (时间范围过滤)", ctx.equipShiftCapacities.size()); log.info("加载设备产能日历(EquipShiftCapacity): {} 条 (时间范围过滤)", ctx.equipShiftCapacities.size());
ctx.kpiCategoryItems = kpiCategoryItemService.list( ctx.kpiCategoryItems = kpiCategoryItemService.list(
new LambdaQueryWrapper<KpiCategoryItem>() new LambdaQueryWrapper<KpiCategoryItem>()
.eq(KpiCategoryItem::getIsDeleted, 0) .eq(KpiCategoryItem::getIsDeleted, 0)
.eq(KpiCategoryItem::getCategoryId, kpiSetting)); .eq(KpiCategoryItem::getCategoryId, kpiSetting));
return ctx; return ctx;
} }
...@@ -686,7 +694,6 @@ public class MacroPlannerDataConverter { ...@@ -686,7 +694,6 @@ public class MacroPlannerDataConverter {
return baseDate.plusDays(periodCount); return baseDate.plusDays(periodCount);
} }
} }
public static String toPeriodType(String code) { public static String toPeriodType(String code) {
switch (code) { switch (code) {
case "2": return "WEEK"; case "2": return "WEEK";
...@@ -697,7 +704,6 @@ public class MacroPlannerDataConverter { ...@@ -697,7 +704,6 @@ public class MacroPlannerDataConverter {
default: return "DAY"; default: return "DAY";
} }
} }
/** /**
* 计算给定日期范围内的有效工作日数 (不含周日)。 * 计算给定日期范围内的有效工作日数 (不含周日)。
*/ */
...@@ -1185,8 +1191,6 @@ public class MacroPlannerDataConverter { ...@@ -1185,8 +1191,6 @@ public class MacroPlannerDataConverter {
String dimension = ctx.periodDimension; String dimension = ctx.periodDimension;
LocalDate horizonEnd = ctx.horizonEnd; LocalDate horizonEnd = ctx.horizonEnd;
// 按维度创建 Period // 按维度创建 Period
switch (dimension) { switch (dimension) {
case "WEEK": case "WEEK":
...@@ -1511,7 +1515,6 @@ public class MacroPlannerDataConverter { ...@@ -1511,7 +1515,6 @@ public class MacroPlannerDataConverter {
} }
// ---------- 步骤10: KPIWeights (默认值, 复用 RoutingTestDataBuilder) ---------- // ---------- 步骤10: KPIWeights (默认值, 复用 RoutingTestDataBuilder) ----------
private void fillKpiWeights(ConvertContext ctx) { private void fillKpiWeights(ConvertContext ctx) {
List<KpiSetting> kpis=new ArrayList<>(); List<KpiSetting> kpis=new ArrayList<>();
...@@ -1572,11 +1575,6 @@ public class MacroPlannerDataConverter { ...@@ -1572,11 +1575,6 @@ public class MacroPlannerDataConverter {
List<Equipinfo> equipinfos = new ArrayList<>(); List<Equipinfo> equipinfos = new ArrayList<>();
List<PlanResource> planResources = new ArrayList<>(); List<PlanResource> planResources = new ArrayList<>();
List<EquipShiftCapacity> equipShiftCapacities = new ArrayList<>(); List<EquipShiftCapacity> equipShiftCapacities = new ArrayList<>();
List<KpiCategoryItem> kpiCategoryItems = new ArrayList<>();
LocalDateTime baseTime; LocalDateTime baseTime;
/** 计划截止日期 (不包含), 由 baseTime + periodDimension + periodCount 计算 */ /** 计划截止日期 (不包含), 由 baseTime + periodDimension + periodCount 计算 */
LocalDate horizonEnd; LocalDate horizonEnd;
...@@ -1606,5 +1604,7 @@ public class MacroPlannerDataConverter { ...@@ -1606,5 +1604,7 @@ public class MacroPlannerDataConverter {
Set<String> unlimitedUnitIds = new HashSet<>(); Set<String> unlimitedUnitIds = new HashSet<>();
/** 排产周期数, 从 ApsTimeConfig 计算, 维度为 DAY 时是天数, WEEK 时是周数, MONTH 时是月数 */ /** 排产周期数, 从 ApsTimeConfig 计算, 维度为 DAY 时是天数, WEEK 时是周数, MONTH 时是月数 */
int periodCount = 7; int periodCount = 7;
List<KpiCategoryItem> kpiCategoryItems = new ArrayList<>();
} }
} }
...@@ -724,7 +724,7 @@ public class SolutionPrinter { ...@@ -724,7 +724,7 @@ public class SolutionPrinter {
KpiLib kpi = KpiLib.ofEn(key); KpiLib kpi = KpiLib.ofEn(key);
double val= model.getKpi(key).solutionValue(); double val= model.getKpi(key).solutionValue();
writeLog(" %s: %.2f (权重%.0f × %.2f)", writeLog(" %s: %.2f (权重%.0f × %.2f)",
kpi.getCn(),val * weight, val, val); kpi.getCn(),val * weight, val, weight);
} }
......
...@@ -31,7 +31,8 @@ public class MacroSceneDataPermissionHandler implements MultiDataPermissionHandl ...@@ -31,7 +31,8 @@ public class MacroSceneDataPermissionHandler implements MultiDataPermissionHandl
"PLAN_RESOURCE", "PLAN_RESOURCE",
"EQUIPINFO", "EQUIPINFO",
"EQUIP_SHIFT_CAPACITY", "EQUIP_SHIFT_CAPACITY",
"APS_TIME_CONFIG" "APS_TIME_CONFIG",
"PLAN_PERIOD"
)); ));
@Override @Override
......
...@@ -6,7 +6,7 @@ import org.springframework.stereotype.Component; ...@@ -6,7 +6,7 @@ import org.springframework.stereotype.Component;
@Component @Component
public class MacroSceneMode { public class MacroSceneMode {
// Temporary test mode. Set to false and rebuild to restore scene management. // Temporary test mode. Set to false and rebuild to restore scene management.
private static final boolean DEFAULT_ONLY = true; private static final boolean DEFAULT_ONLY = false;
private final boolean defaultOnly; private final boolean defaultOnly;
public MacroSceneMode() { public MacroSceneMode() {
......
...@@ -71,6 +71,7 @@ public class MacroSceneService { ...@@ -71,6 +71,7 @@ public class MacroSceneService {
private final EquipinfoMapper equipinfoMapper; private final EquipinfoMapper equipinfoMapper;
private final EquipShiftCapacityMapper equipShiftCapacityMapper; private final EquipShiftCapacityMapper equipShiftCapacityMapper;
private final ApsTimeConfigMapper apsTimeConfigMapper; private final ApsTimeConfigMapper apsTimeConfigMapper;
private final PlanPeriodMapper planPeriodMapper;
public List<MacroSceneConfig> listScenes() { public List<MacroSceneConfig> listScenes() {
if (sceneMode.isDefaultOnly()) return java.util.Collections.singletonList(sceneMode.defaultScene()); if (sceneMode.isDefaultOnly()) return java.util.Collections.singletonList(sceneMode.defaultScene());
...@@ -183,6 +184,7 @@ public class MacroSceneService { ...@@ -183,6 +184,7 @@ public class MacroSceneService {
sjzPfWhStockMapper.delete(null); sjzPfWhStockMapper.delete(null);
materialInfoMapper.delete(null); materialInfoMapper.delete(null);
apsTimeConfigMapper.delete(null); apsTimeConfigMapper.delete(null);
planPeriodMapper.delete(null);
}); });
} }
...@@ -214,6 +216,8 @@ public class MacroSceneService { ...@@ -214,6 +216,8 @@ public class MacroSceneService {
copyStocksAndSupplies(sourceSceneId, targetSceneId, materialIds, routingIds); copyStocksAndSupplies(sourceSceneId, targetSceneId, materialIds, routingIds);
copyEquipCapacity(sourceSceneId, targetSceneId, equipIds, resourceIds); copyEquipCapacity(sourceSceneId, targetSceneId, equipIds, resourceIds);
copySimpleRows(sourceSceneId, targetSceneId, apsTimeConfigMapper, ApsTimeConfig.class, null); copySimpleRows(sourceSceneId, targetSceneId, apsTimeConfigMapper, ApsTimeConfig.class, null);
copySimpleRows(sourceSceneId, targetSceneId, planPeriodMapper, PlanPeriod.class,
row -> row.setId(UUID.randomUUID().toString()));
} }
private Map<Integer, Integer> copyEquipinfo(String source, String target) { private Map<Integer, Integer> copyEquipinfo(String source, String target) {
...@@ -1027,7 +1031,8 @@ public class MacroSceneService { ...@@ -1027,7 +1031,8 @@ public class MacroSceneService {
|| mapper == routingDetailEquipMapper || mapper == stockMapper || mapper == routingDetailEquipMapper || mapper == stockMapper
|| mapper == materialPurchaseMapper || mapper == erpPurchaseOrderMapper || mapper == materialPurchaseMapper || mapper == erpPurchaseOrderMapper
|| mapper == purchaseReceiptMapper || mapper == sjzPfWhStockMapper || mapper == purchaseReceiptMapper || mapper == sjzPfWhStockMapper
|| mapper == planResourceMapper || mapper == equipinfoMapper) { || mapper == planResourceMapper || mapper == equipinfoMapper
|| mapper == planPeriodMapper) {
return "ISDELETED"; return "ISDELETED";
} }
if (mapper == routingDetailMapper || mapper == equipShiftCapacityMapper) { if (mapper == routingDetailMapper || mapper == equipShiftCapacityMapper) {
......
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;
}
}
...@@ -45,4 +45,14 @@ class MacroSceneDataPermissionHandlerTest { ...@@ -45,4 +45,14 @@ class MacroSceneDataPermissionHandlerTest {
assertNull(expression); assertNull(expression);
} }
@Test
void planPeriodsAreScopedToBaselineOrSelectedScene() {
Table table = new Table("PLAN_PERIOD");
assertEquals("PLAN_PERIOD.MP_SCENE_ID IS NULL",
handler.getSqlSegment(table, null, "test.select").toString());
MacroSceneContext.setSceneId("scene-001");
assertEquals("PLAN_PERIOD.MP_SCENE_ID = 'scene-001'",
handler.getSqlSegment(table, null, "test.select").toString());
}
} }
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