Commit f11971ca authored by Tong Li's avatar Tong Li

MP

parent 2ec4b057
......@@ -13,6 +13,7 @@ import com.aps.macroplanner.data.TestDataBuilder;
import com.aps.macroplanner.output.ResultWriter;
import com.aps.macroplanner.output.dto.*;
import com.aps.service.MacroPlannerResultService;
import com.aps.service.MacroPlannerProductNetworkService;
import com.aps.service.MpPispipResultPersistenceService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.google.ortools.Loader;
......@@ -67,6 +68,9 @@ public class MacroPlannerResultController {
@Autowired
private MacroPlannerResultService macroPlannerResultService;
@Autowired
private MacroPlannerProductNetworkService macroPlannerProductNetworkService;
@Autowired
private MacroPlannerDataConverter macroPlannerDataConverter;
......@@ -304,11 +308,15 @@ public class MacroPlannerResultController {
@RequestParam(value = "productId", required = false) @Parameter(description = "按产品ID过滤成品根节点(可选)") String productId,
@RequestParam(value = "spId", required = false) @Parameter(description = "按库存点ID过滤(可选)") String spId,
@RequestParam(value = "period", required = false) @Parameter(description = "按周期过滤, 仅保留该周期内有供应的节点/来源(可选)") String period) {
OptimizationResult result = loadResult(sceneId);
if (result == null) {
return R.failed("未找到场景 " + sceneId + " 的排产结果文件");
if (productId == null || productId.trim().isEmpty() || spId == null || spId.trim().isEmpty()) {
return R.failed("productId 和 spId 不能为空");
}
SupplyChainNode node = macroPlannerProductNetworkService.buildProductNetwork(
sceneId, productId.trim(), spId.trim(), period);
if (node == null) {
return R.failed("未找到场景 " + sceneId + " 中 " + productId + "@" + spId + " 的产品网络");
}
return R.ok(filterProductNetwork(result.getProductNetwork(), productId, spId, period));
return R.ok(node);
}
/**
......@@ -451,132 +459,6 @@ public class MacroPlannerResultController {
return new ArrayList<>(merged.values());
}
/**
* 过滤产品生产网络:
* <ul>
* <li>productId / spId — 仅保留匹配的成品根节点及其子树;</li>
* <li>unitId — 仅保留该设备上的供应来源;</li>
* <li>periodIndex — 仅保留该周期内有供应(生产/消耗/库存/销售)的节点与来源, 其余剪枝。</li>
* </ul>
* 无过滤条件时原样返回。
*/
private SupplyChainNode filterProductNetwork(ProductNetworkResult network,
String productId,
String spId,
String periodIndex) {
if (network == null) {
return null;
}
if (!hasText(productId) && !hasText(spId) && periodIndex == null) {
return null;
}
ProductNetworkResult filtered = new ProductNetworkResult();
SupplyChainNode childSrc = network.getAllNodes().get(productId + "@" + spId);
if (childSrc == null) {
return null;
}
SupplyChainNode childNode= copyNetworkNode(childSrc, network, filtered, periodIndex);
if (childNode == null) {
return null;
}
return childNode;
}
/**
* 递归拷贝网络节点, 同时按设备与周期剪枝供应来源、子节点。
*/
private SupplyChainNode copyNetworkNode(SupplyChainNode src,
ProductNetworkResult source,
ProductNetworkResult target,
String periodIndex) {
if (periodIndex != null && !src.getActivePeriods().contains(periodIndex)) {
return null;
}
String key = src.getProductId() + "@" + src.getSpId();
SupplyChainNode existing = target.getAllNodes().get(key);
if (existing != null) {
return existing;
}
SupplyChainNode copy = target.getOrCreateNode(src.getProductId(), src.getSpId(),src.getSpName());
copy.setLevel(src.getLevel());
copy.setTotalSalesDemand(src.getTotalSalesDemand());
copy.setTotalSalesFulfilled(src.getTotalSalesFulfilled());
copy.setTotalDependentDemand(src.getTotalDependentDemand());
// copy.setSummary(src.getSummary());
// 供应来源: 按设备 + 周期过滤
for (SupplyChainNode.SupplySource ss : src.getSupplySources()) {
for (Map.Entry<String, SupplyChainNode.SupplySourceDetail> entry : ss.productionByPeriod.entrySet()) {
String period = entry.getKey(); // 周期key,对应截图里的 0、3
if(period.equals(periodIndex))
{
SupplyChainNode.SupplySourceDetail detail = entry.getValue(); // 该周期的产能详情
ss.production=detail.production;
ss.unitId=detail.unitId;
ss.unitName=detail.unitName;
ss.productionByPeriod.clear();
copy.getSupplySources().add(ss);
break;
}
}
}
// 消费者: 按周期过滤
for (SupplyChainNode.ConsumerInfo ci : src.getConsumers()) {
if (periodIndex != null && !ci.consumedByPeriod.containsKey(periodIndex)) {
continue;
}
copy.getConsumers().add(ci);
}
// BOM 子物料: 按周期剪枝
for (SupplyChainNode.BomChild child : src.getChildren()) {
if (periodIndex != null && !child.consumedByPeriod.containsKey(periodIndex)) {
continue;
}
SupplyChainNode childSrc = source.getAllNodes().get(child.productId + "@" + child.spId);
if (childSrc == null) {
continue;
}
SupplyChainNode childNode= copyNetworkNode(childSrc, source, target, periodIndex);
if (childNode == null) {
continue; // 该子节点在该周期无活动, 剪枝
}
for (SupplyChainNode.SupplySource ss : childNode.getSupplySources()) {
child.totalConsumedQty = ss.production;
child.unitId = ss.unitId;
child.unitName = ss.unitName;
child.consumedByPeriod.clear();
break;
}
copy.getChildren().add(child);
}
return copy;
}
/**
* 供应来源在指定周期是否有产量 (在途/外部采购视为全周期可用)。
*/
private boolean isSourceActiveInPeriod(SupplyChainNode.SupplySource ss, int periodIndex) {
if ("IN_TRANSIT".equals(ss.type) || "EXTERNAL".equals(ss.type)) {
return true;
}
return ss.productionByPeriod.containsKey(periodIndex);
}
private void enrichSalesDemandDisplayFields(List<SalesDemandResult> demands) {
if (demands == null || demands.isEmpty()) {
return;
......
package com.aps.macroplanner;
import com.aps.common.util.FileHelper;
import com.aps.macroplanner.output.dto.OptimizationResult;
import com.aps.service.MpPispipResultPersistenceService;
import com.google.ortools.Loader;
import com.google.ortools.linearsolver.MPSolver;
import com.aps.macroplanner.constraint.ConstraintFactory;
......@@ -11,6 +13,7 @@ import com.aps.macroplanner.objective.ObjectiveBuilder;
import com.aps.macroplanner.objective.StrategyLevel;
import com.aps.macroplanner.output.ResultWriter;
import com.aps.macroplanner.output.SolutionPrinter;
import org.springframework.beans.factory.annotation.Autowired;
import java.io.FileOutputStream;
import java.io.PrintStream;
......@@ -83,6 +86,8 @@ import java.util.logging.Logger;
*/
public class MacroPlannerOptimizer {
/** LP 模型文件和日志文件的输出目录 */
private static final String LOG_DIR = "mp";
......@@ -382,7 +387,12 @@ public class MacroPlannerOptimizer {
// 回写业务对象到JSON文件(预留)
ResultWriter rw = new ResultWriter(model, data, startTimeMs);
boolean jsonPath = rw.saveResultToFile(sceneId);
OptimizationResult optimizationResult = rw.buildResult();
boolean jsonPath = rw.saveResultToFile(sceneId, optimizationResult);
if (jsonPath) {
writeLog("\n[OK] 优化结果JSON已导出: " + jsonPath);
}
......
package com.aps.macroplanner;
import com.aps.ApsApplication;
import com.aps.common.util.FileHelper;
import com.aps.macroplanner.data.BenchmarkDataBuilder;
import com.aps.macroplanner.data.LargeScaleBomTestDataBuilder;
import com.aps.macroplanner.output.ResultWriter;
import com.aps.macroplanner.output.dto.OptimizationResult;
import com.aps.service.MpPispipResultPersistenceService;
import com.google.ortools.Loader;
import com.aps.macroplanner.data.MultiLevelBomTestDataBuilder;
import com.aps.macroplanner.data.TestDataBuilder;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ApplicationContext;
/**
* 多级BOM + 多成品 + 共享半成品 测试运行器。
......@@ -26,6 +32,7 @@ import com.aps.macroplanner.data.TestDataBuilder;
* </ol>
*/
public class MultiLevelBomTestRunner {
private static final String LOG_DIR = "mp";
/** LP 模型文件路径 */
......@@ -46,29 +53,44 @@ public class MultiLevelBomTestRunner {
System.out.println("Demand: P1=40/day, P2=30/day");
System.out.println();
// 通过 Spring 容器获取 service 实例 (static main 中 @Autowired 不会生效)
ApplicationContext ctx = SpringApplication.run(ApsApplication.class, args);
try {
MultiLevelBomTestDataBuilder data=new MultiLevelBomTestDataBuilder();
data.init();
MultiLevelBomTestDataBuilder builder=new MultiLevelBomTestDataBuilder();
builder.init();
MacroPlannerOptimizer optimizer = new MacroPlannerOptimizer(builder);
optimizer.buildModel();
optimizer.solve("bom");
MacroPlannerOptimizer optimizer = new MacroPlannerOptimizer(data);
optimizer.buildModel();
optimizer.solve("bom");
// for (int scale : BenchmarkDataBuilder.SUPPORTED_SCALES) {
// MpPispipResultPersistenceService service =
// ctx.getBean(MpPispipResultPersistenceService.class);
//
// int scale = 10000;
// BenchmarkDataBuilder data = BenchmarkDataBuilder.forScale(scale);
// writeLog("===== TEST RUNNER START " + scale + "=====");
//
// int scale=10000;
// BenchmarkDataBuilder data =
// BenchmarkDataBuilder.forScale(scale);
// writeLog("===== TEST RUNNER START "+scale+"=====");
////
// data.init();
// writeLog("Data loaded: " + data.getProducts().size() + " products, "
// + data.getOperations().size() + " operations");
//
// long startTime = System.currentTimeMillis();
// MacroPlannerOptimizer optimizer = new MacroPlannerOptimizer(data);
// optimizer.buildModel();
// optimizer.solve(String.valueOf(scale));
//
// // 构建结果并调用 save 入库
// ResultWriter writer = new ResultWriter(optimizer.getModel(), optimizer.getData(), startTime);
// OptimizationResult result = writer.buildResult();
// int saved = service.save(String.valueOf(scale), result);
// writeLog("PISPIP result saved: " + saved);
//
// writeLog("===== TEST RUNNER END =====");
// }
} catch (Exception e) {
} finally {
SpringApplication.exit(ctx);
}
}
}
\ No newline at end of file
......@@ -130,47 +130,63 @@ public class ResultWriter {
String fileName =resultDir.getAbsolutePath()+ "\\unitcapacitie.parquet";
return fileName;
}
private String getOptimizationBomStructure(String sceneId) {
File resultDir = getResultDirectory(sceneId);
return resultDir.getAbsolutePath() + "\\bom_structure.json";
}
private String getOptimizationOperationDemand(String sceneId) {
File resultDir = getResultDirectory(sceneId);
return resultDir.getAbsolutePath() + "\\operation_demands.parquet";
}
// ==================== 主入口 ====================
public boolean saveResultToFile(String sceneId) {
OptimizationResult result = buildResult();
if (result == null) {
logger.warn("对象不能为空");
return false;
}
return saveResultToFile(sceneId,result);
}
// ==================== 主入口 ====================
/**
* 将结果保存到 JSON 文件
*/
public boolean saveResultToFile(String sceneId, OptimizationResult result) {
public boolean saveResultToFile(String sceneId,OptimizationResult result) {
try {
try {
if (result == null) {
logger.warn("对象不能为空");
return false;
}
if (sceneId == null || sceneId.trim().isEmpty()) {
logger.warn("场景ID不能为空");
return false;
}
// FlatParquetUtil parquetUtil = new FlatParquetUtil();
// String periodTaskPath = getOptimizationPeriodTask(sceneId);
// writeLog("writePeriodTasks");
// parquetUtil.write(result.getPeriodTasks(), periodTaskPath, PeriodTaskResult.class);
// writeLog("writePeriodTasks");
// writeLog("writePispips");
// String pispiPath = getOptimizationPispip(sceneId);
// parquetUtil.write(result.getPispips(), pispiPath, PispipResult.class);
// writeLog("writesalesDemand");
// String salesDemandPath = getOptimizationSalesDemand(sceneId);
// parquetUtil.write(result.getSalesDemands(), salesDemandPath, SalesDemandResult.class);
// writeLog("writesalesDemand");
// writeLog("writesalesDemand");
// String unitPath = getOptimizationUnitCapacitie(sceneId);
// parquetUtil.write(result.getUnitCapacities(), unitPath, UnitCapacityResult.class);
// writeLog("writesalesDemand");
FlatParquetUtil parquetUtil = new FlatParquetUtil();
String periodTaskPath = getOptimizationPeriodTask(sceneId);
writeLog("writePeriodTasks");
parquetUtil.write(result.getPeriodTasks(), periodTaskPath, PeriodTaskResult.class);
writeLog("writePeriodTasks");
writeLog("writePispips");
String pispiPath = getOptimizationPispip(sceneId);
parquetUtil.write(result.getPispips(), pispiPath, PispipResult.class);
writeLog("writesalesDemand");
String salesDemandPath = getOptimizationSalesDemand(sceneId);
parquetUtil.write(result.getSalesDemands(), salesDemandPath, SalesDemandResult.class);
writeLog("writesalesDemand");
writeLog("writesalesDemand");
String unitPath = getOptimizationUnitCapacitie(sceneId);
parquetUtil.write(result.getUnitCapacities(), unitPath, UnitCapacityResult.class);
writeLog("writesalesDemand");
// BOM 结构持久化 (供 getProductNetwork 按需组装)
writeLog("writeBomStructure");
writeBomStructure(sceneId);
// 工序×物料×周期消耗量持久化
writeLog("writeOperationDemands");
List<OperationDemandResult> opDemands = buildOperationDemandResults();
parquetUtil.write(opDemands, getOptimizationOperationDemand(sceneId), OperationDemandResult.class);
try {
File file = getOptimizationFile(sceneId);
File tempFile = new File(file.getParentFile(), file.getName() + ".tmp");
......@@ -218,7 +234,10 @@ public class ResultWriter {
logger.info("保存成功,场景ID: {}, 文件: {}", sceneId, file.getAbsolutePath());
return true;
} catch (Exception e) {
logger.error("保存文件失败,场景ID: " + sceneId, e);
return false;
}
} catch (IOException ex) {
logger.error("保存文件失败,场景ID: " + sceneId, ex);
return false;
......@@ -231,6 +250,130 @@ public class ResultWriter {
}
/**
* 将 BOM 结构 (静态工艺关系) 持久化为 JSON, 供查询接口按需组装产品网络。
*/
private void writeBomStructure(String sceneId) {
try {
BomStructureData structure = buildBomStructureData();
String path = getOptimizationBomStructure(sceneId);
File file = new File(path);
File parent = file.getParentFile();
if (parent != null && !parent.exists()) {
parent.mkdirs();
}
objectMapper.writeValue(file, structure);
} catch (Exception e) {
logger.error("持久化 BOM 结构失败, sceneId=" + sceneId, e);
}
}
/**
* 从 TestDataBuilder 提取静态 BOM 结构 (产品/库存点/工序/BOM输入/周期/初始库存/在途)。
*/
private BomStructureData buildBomStructureData() {
BomStructureData s = new BomStructureData();
for (Product p : data.getProducts()) {
BomStructureData.ProductInfo pi = new BomStructureData.ProductInfo();
pi.id = p.getId();
pi.name = p.getName();
pi.code = p.getCode();
s.products.add(pi);
}
for (StockingPoint sp : data.getStockingPoints()) {
BomStructureData.StockingPointInfo si = new BomStructureData.StockingPointInfo();
si.id = sp.getId();
si.name = sp.getName();
s.stockingPoints.add(si);
}
// 产品→库存点映射 (通过 getStockingPointsForProduct 反推)
for (Product p : data.getProducts()) {
for (StockingPoint sp : data.getStockingPointsForProduct(p.getId())) {
BomStructureData.ProductSpMappingInfo mi = new BomStructureData.ProductSpMappingInfo();
mi.productId = p.getId();
mi.spId = sp.getId();
s.productSpMappings.add(mi);
}
}
for (Operation op : data.getOperations()) {
BomStructureData.OperationInfo oi = new BomStructureData.OperationInfo();
oi.id = op.getId();
oi.name = op.getName();
oi.leadTimeDays = op.getLeadTimeDays();
oi.hasLotSize = op.hasLotSize();
oi.lotSize = op.hasLotSize() ? op.getLotSize() : null;
for (OperationOutput oo : op.getOutputs()) {
BomStructureData.OperationOutputInfo ooi = new BomStructureData.OperationOutputInfo();
ooi.productId = oo.getProductId();
ooi.spId = oo.getSpId();
oi.outputs.add(ooi);
}
for (UnitOperation uo : op.getUnitOperations()) {
BomStructureData.UnitOperationInfo uoi = new BomStructureData.UnitOperationInfo();
uoi.unitId = uo.getUnitId();
uoi.unitName = uo.getUnitName();
uoi.capacityCoeff = uo.getCapacityCoeff();
oi.unitOperations.add(uoi);
}
s.operations.add(oi);
}
for (OperationInput input : data.getOperationInputs()) {
BomStructureData.OperationInputInfo ii = new BomStructureData.OperationInputInfo();
ii.operationId = input.getOperation().getId();
ii.inputProductId = input.getInputProduct().getId();
ii.inputSpId = input.getInputSp().getId();
ii.inputSpName = input.getInputSp().getName();
ii.factor = input.getFactor();
s.operationInputs.add(ii);
}
for (Period p : data.getPeriods()) {
BomStructureData.PeriodInfo pi = new BomStructureData.PeriodInfo();
pi.index = p.getIndex();
pi.name = p.getName();
pi.startDate = p.getStartDate() == null ? null : p.getStartDate().toString();
s.periods.add(pi);
}
for (InitialInventory inv : data.getInitialInventories()) {
BomStructureData.InitialInventoryInfo ii = new BomStructureData.InitialInventoryInfo();
ii.productId = inv.getProduct().getId();
ii.spId = inv.getStockingPoint().getId();
ii.quantity = inv.getQuantity();
s.initialInventories.add(ii);
}
for (InTransitSupply its : data.getInTransitSupplies()) {
BomStructureData.InTransitSupplyInfo ti = new BomStructureData.InTransitSupplyInfo();
ti.productId = its.getProduct().getId();
ti.spId = its.getStockingPoint().getId();
ti.arrivalDate = its.getArrivalDate() == null ? null : its.getArrivalDate().toString();
ti.quantity = its.getQuantity();
s.inTransitSupplies.add(ti);
}
return s;
}
/**
* 从求解结果提取每个工序×物料×周期的消耗量 (OperationDemandQty)。
*/
private List<OperationDemandResult> buildOperationDemandResults() {
List<OperationDemandResult> list = new ArrayList<>();
Map<String, MPVariable> opDemandVars = model.getOperationDemandQtyVars();
for (OperationInput input : data.getOperationInputs()) {
String opId = input.getOperation().getId();
String inputProductId = input.getInputProduct().getId();
String inputSpId = input.getInputSp().getId();
for (Period p : data.getPeriods()) {
String key = input.getKey() + "_" + p.getIndex();
MPVariable var = opDemandVars.get(key);
if (var == null) continue;
double qty = var.solutionValue();
if (Math.abs(qty) < 1e-9) continue;
list.add(new OperationDemandResult(opId, inputProductId, inputSpId, p.getIndex(), qty));
}
}
return list;
}
/**
* 从文件中读取 Chromosome 对象,并拆分文件读取与反序列化耗时。
*/
......@@ -331,8 +474,8 @@ public class ResultWriter {
buildPispips(result);
writeLog("UnitCapacitie");
result.setUnitCapacities(buildUnitCapacities());
result.setProductNetwork(buildProductNetwork());
result.setDemandSummary(buildDemandSummary());
// result.setProductNetwork(buildProductNetwork());
// result.setDemandSummary(buildDemandSummary());
// KPI + 统计
writeLog("Kpi");
......
package com.aps.macroplanner.output.dto;
import java.util.ArrayList;
import java.util.List;
/**
* BOM 结构持久化数据 — 求解后将静态工艺/BOM 关系固化,供查询接口按需组装产品生产网络。
*
* <p>与求解结果 (period_tasks / pispips / salesdemand / operation_demands) 分离:
* 求解结果存 "数值", 本结构存 "关系" (谁产出什么、谁消耗什么、消耗因子)。</p>
*
* <p>字段全部使用扁平化嵌套 POJO + public 字段, 便于 Jackson 序列化/反序列化。</p>
*/
public class BomStructureData {
public List<ProductInfo> products = new ArrayList<>();
public List<StockingPointInfo> stockingPoints = new ArrayList<>();
public List<ProductSpMappingInfo> productSpMappings = new ArrayList<>();
public List<OperationInfo> operations = new ArrayList<>();
public List<OperationInputInfo> operationInputs = new ArrayList<>();
public List<PeriodInfo> periods = new ArrayList<>();
public List<InitialInventoryInfo> initialInventories = new ArrayList<>();
public List<InTransitSupplyInfo> inTransitSupplies = new ArrayList<>();
// ==================== 扁平 POJO ====================
public static class ProductInfo {
public String id;
public String name;
public String code;
}
public static class StockingPointInfo {
public String id;
public String name;
}
public static class ProductSpMappingInfo {
public String productId;
public String spId;
}
public static class OperationInfo {
public String id;
public String name;
public int leadTimeDays;
public boolean hasLotSize;
public Double lotSize;
public List<OperationOutputInfo> outputs = new ArrayList<>();
public List<UnitOperationInfo> unitOperations = new ArrayList<>();
}
public static class OperationOutputInfo {
public String productId;
public String spId;
}
public static class UnitOperationInfo {
public String unitId;
public String unitName;
public double capacityCoeff;
}
public static class OperationInputInfo {
public String operationId;
public String inputProductId;
public String inputSpId;
public String inputSpName;
public double factor;
}
public static class PeriodInfo {
public int index;
public String name;
public String startDate;
}
public static class InitialInventoryInfo {
public String productId;
public String spId;
public double quantity;
}
public static class InTransitSupplyInfo {
public String productId;
public String spId;
public String arrivalDate;
public double quantity;
}
}
package com.aps.macroplanner.output.dto;
/**
* 工序消耗物料结果 — 每个工序在每周期消耗每种 BOM 输入物料的量 (OperationDemandQty)。
*
* <p>用于按需组装产品生产网络时还原 "消费者 consumedQty" 与 "BOM 子物料 consumedQty"。
* 扁平 POJO, 直接映射到 operation_demands.parquet。</p>
*/
public class OperationDemandResult {
private String operationId;
private String inputProductId;
private String inputSpId;
private int periodIndex;
private double quantity;
public OperationDemandResult() {
}
public OperationDemandResult(String operationId, String inputProductId, String inputSpId,
int periodIndex, double quantity) {
this.operationId = operationId;
this.inputProductId = inputProductId;
this.inputSpId = inputSpId;
this.periodIndex = periodIndex;
this.quantity = quantity;
}
public String getOperationId() { return operationId; }
public void setOperationId(String v) { this.operationId = v; }
public String getInputProductId() { return inputProductId; }
public void setInputProductId(String v) { this.inputProductId = v; }
public String getInputSpId() { return inputSpId; }
public void setInputSpId(String v) { this.inputSpId = v; }
public int getPeriodIndex() { return periodIndex; }
public void setPeriodIndex(int v) { this.periodIndex = v; }
public double getQuantity() { return quantity; }
public void setQuantity(double v) { this.quantity = v; }
}
package com.aps.service;
import com.aps.common.util.FlatParquetUtil;
import com.aps.macroplanner.output.dto.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.io.File;
import java.nio.file.Paths;
import java.util.*;
/**
* 产品生产网络按需查询服务。
*
* <p>不再在求解阶段预生成完整 BOM 网络 (buildProductNetwork), 而是:
* <ol>
* <li>求解时把静态 BOM 结构持久化为 {@code bom_structure.json};</li>
* <li>求解变量值已持久化为 parquet (period_tasks / pispips / salesdemand / operation_demands);</li>
* <li>查询时按 productId + spId 定位节点, 递归向下展开 BOM 子树, 按需读取变量值。</li>
* </ol>
*
* <p>这样避免了构建整棵树的耗时和超大 JSON 的序列化/反序列化开销。</p>
*/
@Service
@Slf4j
public class MacroPlannerProductNetworkService {
private static final String RESULT_DIR = "mp/result";
private final ObjectMapper objectMapper = new ObjectMapper();
/**
* 按需组装 productId@spId 节点及其 BOM 子树。
*
* @param sceneId 场景ID
* @param productId 目标产品ID (必填)
* @param spId 目标库存点ID (必填)
* @param period 可选周期过滤 (周期索引的数字字符串, 如 "3")
* @return 目标节点 (含子树), 或 null
*/
public SupplyChainNode buildProductNetwork(String sceneId, String productId, String spId, String period) {
if (isBlank(sceneId) || isBlank(productId) || isBlank(spId)) {
return null;
}
try {
Integer periodIndex = isBlank(period) ? null : Integer.parseInt(period.trim());
IndexedData idx = load(sceneId);
return buildNode(idx, productId, spId, periodIndex, 0, new HashSet<>());
} catch (Exception e) {
log.warn("按需组装产品网络失败: sceneId={}, productId={}, spId={}, error={}",
sceneId, productId, spId, e.getMessage());
return null;
}
}
// ==================== 加载 + 索引 ====================
private IndexedData load(String sceneId) throws Exception {
BomStructureData structure = objectMapper.readValue(
new File(path(sceneId, "bom_structure.json")), BomStructureData.class);
FlatParquetUtil parquet = new FlatParquetUtil();
List<PeriodTaskResult> periodTasks = parquet.readAll(path(sceneId, "period_tasks.parquet"), PeriodTaskResult.class);
List<PispipResult> pispips = parquet.readAll(path(sceneId, "pispips.parquet"), PispipResult.class);
List<SalesDemandResult> sales = parquet.readAll(path(sceneId, "salesdemand.parquet"), SalesDemandResult.class);
List<OperationDemandResult> opDemands = parquet.readAll(path(sceneId, "operation_demands.parquet"), OperationDemandResult.class);
return new IndexedData(structure, periodTasks, pispips, sales, opDemands);
}
private String path(String sceneId, String fileName) {
return Paths.get(RESULT_DIR, sceneId, fileName).toAbsolutePath().toString();
}
/**
* 内存索引: 将持久化结构 + 求解变量值整理成 O(1) 查询结构。
*/
private static class IndexedData {
final Map<String, List<BomStructureData.StockingPointInfo>> spByProduct = new HashMap<>();
final Map<String, String> spNameById = new HashMap<>();
final Map<String, List<BomStructureData.OperationInfo>> operationsByProductSp = new HashMap<>();
final Map<String, List<BomStructureData.OperationInfo>> operationsByProduct = new HashMap<>();
final Map<String, BomStructureData.OperationInfo> operationById = new HashMap<>();
final Map<String, List<BomStructureData.OperationInputInfo>> inputsByOperation = new HashMap<>();
final Map<String, List<BomStructureData.OperationInputInfo>> inputsByProductSp = new HashMap<>();
final Map<String, Double> initialInventory = new HashMap<>();
final Map<String, List<BomStructureData.InTransitSupplyInfo>> inTransitByProductSp = new HashMap<>();
final List<BomStructureData.PeriodInfo> periods = new ArrayList<>();
final Map<Integer, String> startDateByPeriodIndex = new HashMap<>();
// 求解变量值
final Map<String, Double> ptQty = new HashMap<>(); // operationId_unitId_periodIndex
final Map<String, Double> opDemand = new HashMap<>(); // operationId_inputProductId_inputSpId_periodIndex
final Map<String, PispipResult> pispip = new HashMap<>(); // productId_spId_periodIndex
final Map<String, double[]> sales = new HashMap<>(); // productId_spId_periodIndex -> [demandQty, fulfilledQty]
IndexedData(BomStructureData s,
List<PeriodTaskResult> periodTasks,
List<PispipResult> pispips,
List<SalesDemandResult> salesList,
List<OperationDemandResult> opDemands) {
for (BomStructureData.StockingPointInfo sp : s.stockingPoints) {
spNameById.put(sp.id, sp.name);
}
for (BomStructureData.ProductSpMappingInfo m : s.productSpMappings) {
BomStructureData.StockingPointInfo info = new BomStructureData.StockingPointInfo();
info.id = m.spId;
info.name = spNameById.get(m.spId);
spByProduct.computeIfAbsent(m.productId, k -> new ArrayList<>()).add(info);
}
for (BomStructureData.OperationInfo op : s.operations) {
operationById.put(op.id, op);
for (BomStructureData.OperationOutputInfo oo : op.outputs) {
operationsByProductSp.computeIfAbsent(oo.productId + "@" + oo.spId, k -> new ArrayList<>()).add(op);
operationsByProduct.computeIfAbsent(oo.productId, k -> new ArrayList<>()).add(op);
}
}
for (BomStructureData.OperationInputInfo in : s.operationInputs) {
inputsByOperation.computeIfAbsent(in.operationId, k -> new ArrayList<>()).add(in);
inputsByProductSp.computeIfAbsent(in.inputProductId + "@" + in.inputSpId, k -> new ArrayList<>()).add(in);
}
for (BomStructureData.InitialInventoryInfo inv : s.initialInventories) {
initialInventory.put(inv.productId + "@" + inv.spId, inv.quantity);
}
for (BomStructureData.InTransitSupplyInfo its : s.inTransitSupplies) {
inTransitByProductSp.computeIfAbsent(its.productId + "@" + its.spId, k -> new ArrayList<>()).add(its);
}
for (BomStructureData.PeriodInfo p : s.periods) {
periods.add(p);
if (p.startDate != null) {
startDateByPeriodIndex.put(p.index, p.startDate);
}
}
for (PeriodTaskResult pt : periodTasks) {
ptQty.put(pt.getOperationId() + "_" + pt.getUnitId() + "_" + pt.getPeriodIndex(), pt.getProductionQty());
}
for (OperationDemandResult od : opDemands) {
opDemand.put(od.getOperationId() + "_" + od.getInputProductId() + "_" + od.getInputSpId() + "_" + od.getPeriodIndex(), od.getQuantity());
}
for (PispipResult p : pispips) {
pispip.put(p.getProductId() + "_" + p.getSpId() + "_" + p.getPeriodIndex(), p);
}
for (SalesDemandResult sd : salesList) {
String key = sd.getProductId() + "_" + sd.getSpId() + "_" + sd.getPeriodIndex();
double[] acc = sales.computeIfAbsent(key, k -> new double[]{0.0, 0.0});
acc[0] += sd.getDemandQty();
acc[1] += sd.getFulfilledQty();
}
}
List<BomStructureData.OperationInfo> producing(String productId, String spId) {
return operationsByProductSp.getOrDefault(productId + "@" + spId, Collections.emptyList());
}
List<BomStructureData.OperationInputInfo> inputsOf(String operationId) {
return inputsByOperation.getOrDefault(operationId, Collections.emptyList());
}
List<BomStructureData.OperationInputInfo> consumersOf(String productId, String spId) {
return inputsByProductSp.getOrDefault(productId + "@" + spId, Collections.emptyList());
}
double initialInv(String productId, String spId) {
return initialInventory.getOrDefault(productId + "@" + spId, 0.0);
}
List<BomStructureData.InTransitSupplyInfo> inTransit(String productId, String spId) {
return inTransitByProductSp.getOrDefault(productId + "@" + spId, Collections.emptyList());
}
double demandQty(String productId, String spId, int periodIndex) {
double[] acc = sales.get(productId + "_" + spId + "_" + periodIndex);
return acc == null ? 0.0 : acc[0];
}
double fulfilledQty(String productId, String spId, int periodIndex) {
double[] acc = sales.get(productId + "_" + spId + "_" + periodIndex);
return acc == null ? 0.0 : acc[1];
}
PispipResult pispip(String productId, String spId, int periodIndex) {
return pispip.get(productId + "_" + spId + "_" + periodIndex);
}
String startDate(int periodIndex) {
return startDateByPeriodIndex.get(periodIndex);
}
}
// ==================== 节点组装 ====================
private SupplyChainNode buildNode(IndexedData idx, String productId, String spId,
Integer periodIndex, int level, Set<String> visited) {
String nodeKey = productId + "@" + spId;
if (!visited.add(nodeKey)) {
return null; // 防环
}
SupplyChainNode node = new SupplyChainNode();
node.setProductId(productId);
node.setSpId(spId);
node.setSpName(idx.spNameById.getOrDefault(spId, spId));
node.setLevel(level);
// 周期过滤的目标 startDate
String periodStartDate = periodIndex == null ? null : idx.startDate(periodIndex);
// --- 供应来源 ---
for (BomStructureData.OperationInfo op : idx.producing(productId, spId)) {
SupplyChainNode.SupplySource src = new SupplyChainNode.SupplySource();
src.type = "OPERATION";
src.operationId = op.id;
src.operationName = op.name;
if (op.hasLotSize) {
src.lotSize = op.lotSize;
}
double totalProduction = 0;
double totalCapacity = 0;
for (BomStructureData.UnitOperationInfo uo : op.unitOperations) {
for (BomStructureData.PeriodInfo p : idx.periods) {
double qty = idx.ptQty.getOrDefault(op.id + "_" + uo.unitId + "_" + p.index, 0.0);
if (qty <= 0) continue;
SupplyChainNode.SupplySourceDetail detail = new SupplyChainNode.SupplySourceDetail();
detail.unitId = uo.unitId;
detail.unitName = uo.unitName;
detail.production = qty;
detail.capacityUsed = qty * uo.capacityCoeff;
totalProduction += qty;
totalCapacity += detail.capacityUsed;
String periodKey = p.startDate == null ? String.valueOf(p.index) : p.startDate;
src.productionByPeriod.put(periodKey, detail);
node.getActivePeriods().add(periodKey);
}
}
src.totalProduction = totalProduction;
src.capacityUsed = totalCapacity;
src.unitId = op.unitOperations.isEmpty() ? null : op.unitOperations.get(0).unitId;
src.unitName = op.unitOperations.isEmpty() ? null : op.unitOperations.get(0).unitName;
// period 过滤: 仅保留该周期有产量的来源, 并把 production 收敛到该周期
if (periodStartDate != null) {
SupplyChainNode.SupplySourceDetail d = src.productionByPeriod.get(periodStartDate);
if (d == null) {
continue;
}
src.production = d.production;
src.unitId = d.unitId;
src.unitName = d.unitName;
src.productionByPeriod.clear();
}
node.getSupplySources().add(src);
}
// --- 在途供应 ---
for (BomStructureData.InTransitSupplyInfo its : idx.inTransit(productId, spId)) {
SupplyChainNode.SupplySource src = new SupplyChainNode.SupplySource();
src.type = "IN_TRANSIT";
src.totalProduction = its.quantity;
node.getSupplySources().add(src);
}
// --- 外部采购 (无供应源) ---
if (node.getSupplySources().isEmpty()) {
SupplyChainNode.SupplySource src = new SupplyChainNode.SupplySource();
src.type = "EXTERNAL";
src.totalProduction = 0;
node.getSupplySources().add(src);
}
// --- 消费者 ---
Set<String> visitedConsumers = new HashSet<>();
for (BomStructureData.OperationInputInfo in : idx.consumersOf(productId, spId)) {
BomStructureData.OperationInfo consumerOp = idx.operationById.get(in.operationId);
double totalConsumed = 0;
Map<Integer, Double> consumedByPeriod = new LinkedHashMap<>();
for (BomStructureData.PeriodInfo p : idx.periods) {
double consumed = idx.opDemand.getOrDefault(
in.operationId + "_" + in.inputProductId + "_" + in.inputSpId + "_" + p.index, 0.0);
totalConsumed += consumed;
if (consumed > 0) {
consumedByPeriod.put(p.index, consumed);
}
}
if (consumerOp != null) {
for (BomStructureData.OperationOutputInfo oo : consumerOp.outputs) {
String ciKey = oo.productId + "@" + oo.spId + "_" + in.operationId;
if (!visitedConsumers.add(ciKey)) {
continue;
}
if (periodIndex != null && !consumedByPeriod.containsKey(periodIndex)) {
continue;
}
SupplyChainNode.ConsumerInfo ci = new SupplyChainNode.ConsumerInfo();
ci.consumerProductId = oo.productId;
ci.consumerSpId = oo.spId;
ci.operationId = in.operationId;
ci.operationName = consumerOp.name;
ci.unitId = consumerOp.unitOperations.isEmpty() ? null : consumerOp.unitOperations.get(0).unitId;
ci.factor = in.factor;
ci.totalConsumed = totalConsumed;
ci.consumedByPeriod.putAll(consumedByPeriod);
node.getConsumers().add(ci);
}
}
}
// --- BOM 子物料 (向下展开) ---
List<BomStructureData.OperationInfo> producingOps = idx.producing(productId, spId);
if (!producingOps.isEmpty()) {
BomStructureData.OperationInfo op = producingOps.get(0); // 与原逻辑一致: 只展开第一个工序的 BOM
for (BomStructureData.OperationInputInfo in : idx.inputsOf(op.id)) {
SupplyChainNode.BomChild child = new SupplyChainNode.BomChild();
child.productId = in.inputProductId;
child.spId = in.inputSpId;
child.spName = in.inputSpName;
child.operationId = in.operationId;
child.operationName = op.name;
child.totalFactor = in.factor;
double totalConsumed = 0;
for (BomStructureData.PeriodInfo p : idx.periods) {
double consumed = idx.opDemand.getOrDefault(
in.operationId + "_" + in.inputProductId + "_" + in.inputSpId + "_" + p.index, 0.0);
totalConsumed += consumed;
if (consumed > 0) {
String periodKey = p.startDate == null ? String.valueOf(p.index) : p.startDate;
child.consumedByPeriod.put(periodKey, consumed);
}
}
child.totalConsumedQty = totalConsumed;
if (periodStartDate != null && !child.consumedByPeriod.containsKey(periodStartDate)) {
continue; // 该周期无消耗, 剪枝
}
SupplyChainNode childNode = buildNode(idx, in.inputProductId, in.inputSpId, periodIndex, level + 1, visited);
if (childNode == null) {
continue;
}
// 用子节点的供应源回填 child 的 unitId/unitName/totalConsumedQty (与原逻辑对齐)
for (SupplyChainNode.SupplySource ss : childNode.getSupplySources()) {
child.totalConsumedQty = ss.production;
child.unitId = ss.unitId;
child.unitName = ss.unitName;
break;
}
node.getChildren().add(child);
}
}
// --- 汇总 ---
fillSummary(idx, node, productId, spId);
// period 剪枝: 该周期无活动则剪掉整个节点 (与原 buildProductNetwork 语义一致)
if (periodStartDate != null && !node.getActivePeriods().contains(periodStartDate)) {
return null;
}
return node;
}
private void fillSummary(IndexedData idx, SupplyChainNode node, String productId, String spId) {
SupplySummary s = new SupplySummary();
node.setSummary(s);
int n = idx.periods.size();
double totalEndingInv = 0;
for (BomStructureData.PeriodInfo p : idx.periods) {
PispipResult pr = idx.pispip(productId, spId, p.index);
if (pr != null) {
totalEndingInv += pr.getEndingInventory();
if (pr.getEndingInventory() > 0) {
node.getActivePeriods().add(p.startDate == null ? String.valueOf(p.index) : p.startDate);
}
}
}
s.setInitialInventory(idx.initialInv(productId, spId));
PispipResult last = idx.pispip(productId, spId, n - 1);
s.setFinalInventory(last == null ? 0 : last.getEndingInventory());
s.setAverageInventory(n > 0 ? totalEndingInv / n : 0);
// 生产汇总
double totalProduction = 0;
for (BomStructureData.OperationInfo op : idx.producing(productId, spId)) {
for (BomStructureData.UnitOperationInfo uo : op.unitOperations) {
for (BomStructureData.PeriodInfo p : idx.periods) {
double qty = idx.ptQty.getOrDefault(op.id + "_" + uo.unitId + "_" + p.index, 0.0);
totalProduction += qty;
}
}
}
s.setTotalProduction(totalProduction);
// 在途汇总
double totalInTransit = 0;
for (BomStructureData.InTransitSupplyInfo its : idx.inTransit(productId, spId)) {
totalInTransit += its.quantity;
}
s.setTotalInTransit(totalInTransit);
// 需求汇总
double totalSalesDemand = 0;
double totalSalesFulfilled = 0;
double totalDepDemand = 0;
double totalDemandFulf = 0;
double totalSlack = 0;
for (BomStructureData.PeriodInfo p : idx.periods) {
PispipResult pr = idx.pispip(productId, spId, p.index);
if (pr != null) {
totalDepDemand += pr.getDependentDemandQty();
totalDemandFulf += pr.getDemandFulfillment();
totalSlack += pr.getDemandSlack();
if (pr.getDependentDemandQty() > 0) {
node.getActivePeriods().add(p.startDate == null ? String.valueOf(p.index) : p.startDate);
}
}
totalSalesDemand += idx.demandQty(productId, spId, p.index);
totalSalesFulfilled += idx.fulfilledQty(productId, spId, p.index);
}
s.setTotalSalesDemand(totalSalesDemand);
s.setTotalSalesFulfilled(totalSalesFulfilled);
s.setTotalDependentDemand(totalDepDemand);
s.setTotalDemandFulfillment(totalDemandFulf);
s.setTotalDemandSlack(totalSlack);
node.setTotalSalesDemand(totalSalesDemand);
node.setTotalSalesFulfilled(totalSalesFulfilled);
node.setTotalDependentDemand(totalDepDemand);
// 库存规格偏差
double belowTarget = 0;
double belowMin = 0;
double aboveMax = 0;
for (BomStructureData.PeriodInfo p : idx.periods) {
PispipResult pr = idx.pispip(productId, spId, p.index);
if (pr != null) {
belowTarget += pr.getBelowTarget();
belowMin += pr.getBelowMin();
aboveMax += pr.getAboveMax();
}
}
s.setTotalBelowTarget(belowTarget);
s.setTotalBelowMin(belowMin);
s.setTotalAboveMax(aboveMax);
}
private boolean isBlank(String v) {
return v == null || v.trim().isEmpty();
}
}
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