Commit be4b7722 authored by Tong Li's avatar Tong Li

MP

parent d1745f27
package com.aps.controller;
import com.aps.common.util.FlatParquetUtil;
import com.aps.common.util.ParamValidator;
import com.aps.common.util.R;
import com.aps.entity.ApsDemandOrder;
import com.aps.entity.MaterialInfo;
......@@ -13,8 +12,9 @@ import com.aps.macroplanner.data.MacroPlannerDataConverter;
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.MacroPlannerResultService;
import com.aps.service.MacroPlannerProductNetworkService1;
import com.aps.service.MpPispipResultPersistenceService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.google.ortools.Loader;
......@@ -456,7 +456,7 @@ public class MacroPlannerResultController {
description = "成品→半成品→原材料完整BOM供应链树。"
+ "包含各层级的供应来源(含每周期产量)、消耗者、BOM子物料(含每周期消耗量)。"
+ "支持按 productId / spId / unitId / periodIndex 可选过滤。")
public R<SupplyChainNode> getProductNetwork(
public R<TraceNode> getProductNetwork(
@RequestParam("sceneId") @Parameter(description = "场景ID", required = true) String sceneId,
@RequestParam(value = "productId", required = false) @Parameter(description = "按产品ID过滤成品根节点(可选)") String productId,
@RequestParam(value = "spId", required = false) @Parameter(description = "按库存点ID过滤(可选)") String spId,
......@@ -464,7 +464,7 @@ public class MacroPlannerResultController {
if (productId == null || productId.trim().isEmpty() || spId == null || spId.trim().isEmpty()) {
return R.failed("productId 和 spId 不能为空");
}
SupplyChainNode node = macroPlannerProductNetworkService.buildProductNetwork(
TraceNode node = macroPlannerProductNetworkService.buildProductNetwork(
sceneId, productId.trim(), spId.trim(), period);
if (node == null) {
return R.failed("未找到场景 " + sceneId + " 中 " + productId + "@" + spId + " 的产品网络");
......
......@@ -36,7 +36,6 @@ import java.util.Map;
* 求解器被激励去增加生产来满足需求,而非简单地降低销售量。</p>
*
* @see BalanceConstraint 物料平衡约束(DemandSlack 在流入侧)
* @see DemandVariableBuilder 需求变量定义(SalesDemandQty, DemandSlack)
*/
public class DemandSlackLinkageConstraint {
......
......@@ -323,81 +323,82 @@ public class ResultWriter {
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);
}
// 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 (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.inputProductCode = input.getInputProduct().getCode();
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);
}
// 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;
}
......
......@@ -64,6 +64,7 @@ public class BomStructureData {
public static class OperationInputInfo {
public String operationId;
public String inputProductId;
public String inputProductCode;
public String inputSpId;
public String inputSpName;
public double factor;
......
package com.aps.macroplanner.output.dto;
/**
* 作者:佟礼
* 时间:2026-09-20
*/
import java.util.ArrayList;
import java.util.List;
/**
* 产品数量追溯网络节点。
*
* <pre>
* Product + Warehouse
* |
* | demand
* v
* Unit
* |
* | productionQty
* v
* BOM
* |
* v
* Product + Warehouse
* |
* v
* 最近历史 Output
* </pre>
*/
public class TraceNode {
/**
* 产品 ID
*/
public String productId;
/**
* 产品编码
*/
public String productCode;
/**
* 库房 / SP
*/
public String warehouseId;
/**
* 库房名称
*/
public String warehouseName;
/**
* 当前节点需求数量。
*
* 根节点:
* 成品需求量
*
* 子节点:
* Unit生产量 × BOM factor
*/
public double quantity;
/**
* BOM factor。
*
* 根节点一般为 1。
*/
public double factor = 1.0;
/**
* 当前节点匹配到的 Output 总数量。
*
* 注意:
* outputQty 可以 > quantity。
*/
public double outputQty;
/**
* 当前节点库存/库房已有供应量。
*
* 这里表示没有通过生产 Output 继续追溯的数量。
*/
public double inventorySupplyQty;
/**
* 当前节点匹配到的生产 Output。
*
* 一个产品可能对应多个 Output。
*/
// public final List<OutputNode> outputs = new ArrayList<>();
/**
* 当前节点对应的 Unit。
*
* 不同 Unit 必须独立。
*/
public final List<UnitNode> units = new ArrayList<>();
/**
* 实际生产 Output 节点。
*/
public static class OutputNode {
/**
* Output 对应的 dataKey。
*/
public String dataKey;
/**
* 产品 ID。
*/
public String productId;
/**
* 产品 Code。
*/
public String productCode;
/**
* 库房。
*/
public String warehouseId;
public String warehouseName;
/**
* Output 日期/期间。
*/
public Integer periodIndex;
/**
* Output 日期。
*
* 如果项目实际使用 LocalDate/LocalDateTime,
* 可以替换这里的类型。
*/
public String outputDate;
/**
* Output 实际数量。
*
* 可以大于当前 demand。
*/
public double outputQty;
/**
* 对应生产 Unit。
*/
public UnitNode unit;
}
/**
* 独立生产 Unit 节点。
*
* 每个 Unit 单独展开 BOM。
*/
public static class UnitNode {
public String unitId;
public String unitName;
/**
* 当前 Unit 的实际生产量。
*/
public double productionQty;
/**
* 产能占用。
*/
public double capacityUsed;
/**
* 工序。
*/
public String operationId;
public String operationName;
/**
* 当前 Unit 的 BOM 上游节点。
*/
public final List<TraceNode> children =
new ArrayList<>();
}
}
package com.aps.macroplanner.output.dto;
/**
* 作者:佟礼
* 时间:2026-09-20
* 数量追溯用的实际生产Output。
*
* OutputInfo + PeriodTaskResult 合并。
*/
public class TraceOutput {
public String productId;
public String warehouseId;
public String warehouseName;
public String dataKey;
public Integer periodIndex;
public double outputQty;
public String outputDate;
public String operationId;
public String operationName;
public String unitId;
public String unitName;
public double capacityUsed;
}
package com.aps.service;
import com.aps.common.util.FlatParquetUtil;
import com.aps.macroplanner.output.ResultWriter;
import com.aps.macroplanner.output.dto.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
......@@ -31,415 +32,930 @@ public class MacroPlannerProductNetworkService {
private static final String RESULT_DIR = "mp/result";
private final ObjectMapper objectMapper = new ObjectMapper();
/**
* 按需组装 productId@spId 节点及其 BOM 子树。
* 构建产品数量追溯网络。
*
* 追溯逻辑:
*
* <pre>
*
* P + 库房P
* |
* | 需求100
* v
* P实际Output
* |
* +---------+
* | |
* UnitA30 UnitB70
* | |
* | |
* v v
* BOM×30 BOM×70
* | |
* v v
* M1+库房M1 M1+库房M1
* | |
* | 找 <= 当前日期最近Output
* v
* M1实际Output
* |
* v
* M1 Unit
* |
* v
* M1 BOM
*
* </pre>
*
* @param sceneId 场景ID
* @param productId 目标产品ID (必填)
* @param spId 目标库存点ID (必填)
* @param period 可选周期过滤 (周期索引的数字字符串, 如 "3")
* @return 目标节点 (含子树), 或 null
* @param productId 根产品ID
* @param warehouseId 根库房ID
* @param period 周期
*/
public SupplyChainNode buildProductNetwork(String sceneId, String productId, String spId, String period) {
if (isBlank(sceneId) || isBlank(productId) || isBlank(spId)) {
return null;
public TraceNode buildProductNetwork(
String sceneId,
String productId,
String warehouseId,
String period
) {
if (isBlank(productId)) {
throw new IllegalArgumentException(
"productId不能为空");
}
if (isBlank(warehouseId)) {
throw new IllegalArgumentException(
"warehouseId不能为空");
}
try {
Integer periodIndex = isBlank(period) ? null : Integer.parseInt(period.trim());
FlatParquetUtil parquet = new FlatParquetUtil();
ResultWriter rw = new ResultWriter();
List<PispipResult> Pispips = parquet.readAll(
rw.getOptimizationPispip(sceneId),
PispipResult.class,
oi -> oi.getProductId().equals(productId) && oi.getSpId().equals(warehouseId)
);
double quantity= Pispips.get(0).getSalesFulfilledQty();
String productCode=Pispips.get(0).getProductCode();
String spName=Pispips.get(0).getSpName();
List<PeriodTaskResult.OutputInfo> allOutputs = parquet.readAll(
rw.getOptimizationPeriodTaskOutput(sceneId),
PeriodTaskResult.OutputInfo.class
);
List<PeriodTaskResult> allTasks = parquet.readAll(
rw.getOptimizationPeriodTask(sceneId),
PeriodTaskResult.class
);
// 1. 静态结构 (小, 全量读)
BomStructureData structure = objectMapper.readValue(
new File(path(sceneId, "bom_structure.json")), BomStructureData.class);
StructureIndex sidx = new StructureIndex(structure);
new File(rw.getOptimizationBomStructure(sceneId)), BomStructureData.class);
List<BomStructureData.OperationInputInfo> operationInputs=structure.operationInputs;
// 2. 纯结构展开目标子树, 确定涉及的节点集合与工序集合
Set<String> nodeKeys = new HashSet<>();
Set<String> opIds = new HashSet<>();
collectSubtree(sidx, productId, spId, nodeKeys, opIds);
/*
* --------------------------------------------------------
* 1. 把 OutputInfo + PeriodTaskResult 合并
* --------------------------------------------------------
*/
List<TraceOutput> traceOutputs =
buildTraceOutputs(
allOutputs,
allTasks
);
/*
* --------------------------------------------------------
* 2. 建立:
*
* productId + warehouseId
* ↓
* List<TraceOutput>
* --------------------------------------------------------
*/
Map<String, List<TraceOutput>> outputIndex =
buildOutputIndex(traceOutputs);
// 3. 按需过滤读取求解值 (不全量加载)
FlatParquetUtil parquet = new FlatParquetUtil();
/*
* --------------------------------------------------------
* 3. 建立:
*
* operationId
* ↓
* OperationInputInfo
* --------------------------------------------------------
*/
Map<String, List<BomStructureData.OperationInputInfo>> bomIndex =
buildBomIndex(operationInputs);
List<PeriodTaskResult.OutputInfo> outputs = parquet.readAll(
path(sceneId, "period_task_outputs.parquet"),
PeriodTaskResult.OutputInfo.class,
oi -> nodeKeys.contains(oi.getProductId() + "@" + oi.getSpId()));
Set<String> dataKeys = new HashSet<>();
for (PeriodTaskResult.OutputInfo oi : outputs) {
dataKeys.add(oi.getDataKey());
}
List<PeriodTaskResult> tasks = parquet.readAll(
path(sceneId, "period_tasks.parquet"),
PeriodTaskResult.class,
pt -> dataKeys.contains(pt.getDataKey()));
// List<PispipResult> pispips = parquet.readAll(
// path(sceneId, "pispips.parquet"),
// PispipResult.class,
// p -> nodeKeys.contains(p.getProductId() + "@" + p.getSpId()));
//
// List<SalesDemandResult> sales = parquet.readAll(
// path(sceneId, "salesdemand.parquet"),
// SalesDemandResult.class,
// sd -> nodeKeys.contains(sd.getProductId() + "@" + sd.getSpId()));
//
// List<OperationDemandResult> opDemands = parquet.readAll(
// path(sceneId, "operation_demands.parquet"),
// OperationDemandResult.class,
// od -> opIds.contains(od.getOperationId()));
SolveIndex solveIdx = new SolveIndex(outputs, tasks, pispips, sales, opDemands);
// 4. 递归组装
return buildNode(sidx, solveIdx, productId, spId, periodIndex, 0, new HashSet<>());
/*
* --------------------------------------------------------
* 4. 开始递归
* --------------------------------------------------------
*/
Set<String> path = new HashSet<>();
return buildTraceNode(
productId,
warehouseId,
quantity,
1.0,
period,
outputIndex,
bomIndex,
path,productCode,spName
);
} catch (Exception e) {
log.warn("按需组装产品网络失败: sceneId={}, productId={}, spId={}, error={}",
sceneId, productId, spId, e.getMessage());
return null;
}
}
/**
* 构建一个产品+库房节点。
*
* 核心规则:
*
* 1. quantity = 当前需求量
*
* 2. 当前产品必须先匹配:
*
* productId + warehouseId
*
* 3. 只能找:
*
* outputDate <= demandDate
*
* 4. 在符合条件的Output中,
* 选择最近日期。
*
* 5. 找到的Output数量不进行min截断。
*
* 例如:
*
* demand = 60
* output = 80
*
* 则:
*
* quantity = 60
* outputQty = 80
*
* 6. Unit按照实际Output数量创建。
*
* 7. BOM按照Unit实际生产数量计算。
*/
private TraceNode buildTraceNode(
String productId,
String warehouseId,
double quantity,
double factor,
String demandDate,
Map<String, List<TraceOutput>> outputIndex,
Map<String, List<BomStructureData.OperationInputInfo>> bomIndex,
Set<String> path,String productCode,
String warehouseName) {
TraceNode node = new TraceNode();
node.productId = productId;
node.productCode = productCode;
node.warehouseId = warehouseId;
node.warehouseName = warehouseName;
node.quantity = quantity;
node.factor = factor;
/*
* 防止:
*
* A -> B -> A
*/
String pathKey =
productId
+ "@"
+ warehouseId
+ "@"
+ demandDate;
if (!path.add(pathKey)) {
/*
* 出现循环BOM。
*
* 不再继续递归。
*/
node.inventorySupplyQty =
quantity;
private String path(String sceneId, String fileName) {
return Paths.get(RESULT_DIR, sceneId, fileName).toAbsolutePath().toString();
return node;
}
/**
* 纯结构遍历 BOM, 收集目标子树涉及的所有 productId@spId 与 operationId。
try {
/*
* ----------------------------------------------------
* 1. 找最近Output
* ----------------------------------------------------
*/
private void collectSubtree(StructureIndex sidx, String productId, String spId,
Set<String> nodeKeys, Set<String> opIds) {
String key = productId + "@" + spId;
if (!nodeKeys.add(key)) {
return;
List<TraceOutput> matchedOutputs =
findHistoricalOutputsForDemand(
productId,
warehouseId,
demandDate,
quantity,
outputIndex
);
/*
* ----------------------------------------------------
* 没有生产Output:
*
* 当前需求由库存/库房提供。
* ----------------------------------------------------
*/
if (matchedOutputs.isEmpty()) {
node.outputQty = 0;
node.inventorySupplyQty =
quantity;
return node;
}
for (BomStructureData.OperationInfo op : sidx.producing(productId, spId)) {
opIds.add(op.id);
for (BomStructureData.OperationInputInfo in : sidx.inputsOf(op.id)) {
opIds.add(in.operationId);
collectSubtree(sidx, in.inputProductId, in.inputSpId, nodeKeys, opIds);
/*
* ----------------------------------------------------
* 2. 创建Output节点
* ----------------------------------------------------
*/
for (TraceOutput output :
matchedOutputs) {
TraceNode.OutputNode outputNode =
new TraceNode.OutputNode();
outputNode.dataKey =
output.dataKey;
outputNode.productId =
output.productId;
outputNode.warehouseId =
output.warehouseId;
outputNode.warehouseName =
output.warehouseName;
outputNode.periodIndex =
output.periodIndex;
outputNode.outputDate =
output.outputDate;
outputNode.outputQty =
output.outputQty;
/*
* Output总数量。
*
* 注意:
* 不做min(quantity, outputQty)。
*/
node.outputQty +=
output.outputQty;
// node.outputs.add(
// outputNode
// );
/*
* ------------------------------------------------
* 3. Output -> Unit
* ------------------------------------------------
*/
TraceNode.UnitNode unit =
new TraceNode.UnitNode();
unit.unitId =
output.unitId;
unit.unitName =
output.unitName;
unit.productionQty =
output.outputQty;
unit.capacityUsed =
output.capacityUsed;
unit.operationId =
output.operationId;
unit.operationName =
output.operationName;
outputNode.unit = unit;
node.units.add(unit);
/*
* ------------------------------------------------
* 4. 当前Unit独立展开BOM
* ------------------------------------------------
*/
buildUnitBomChildren(
unit,
output.outputQty,
output.outputDate,
bomIndex,
outputIndex,
path
);
}
/*
* ----------------------------------------------------
* 5. 库房供应
*
* 例如:
*
* demand = 100
* output = 80
*
* 剩余20由库房供应。
*
* 如果:
*
* demand = 60
* output = 80
*
* 则库存供应 = 0。
* ----------------------------------------------------
*/
node.inventorySupplyQty =
Math.max(
0,
quantity - node.outputQty
);
return node;
} finally {
path.remove(pathKey);
}
}
// 消费者工序 (上层) 也需要, 用于查 operation_demands 的消耗量
for (BomStructureData.OperationInputInfo in : sidx.consumersOf(productId, spId)) {
opIds.add(in.operationId);
/**
* 根据Unit的实际生产量展开BOM。
*
* 关键:
*
* materialDemand
* =
* unitProductionQty × bomFactor
*
* 注意:
*
* 这里创建的是:
*
* M1 + M1库房
*
* 节点。
*
* 然后该节点自己继续寻找:
*
* M1 + M1库房
* <= 当前Output日期
* 的最近Output。
*/
private void buildUnitBomChildren(
TraceNode.UnitNode unit,
double unitProductionQty,
String outputDate,
Map<String, List<BomStructureData.OperationInputInfo>> bomIndex,
Map<String, List<TraceOutput>> outputIndex,
Set<String> path) {
if (unit == null) {
return;
}
if (isBlank(unit.operationId)) {
return;
}
// ==================== 结构索引 ====================
/*
* 找当前Operation的BOM。
*/
List<BomStructureData.OperationInputInfo> inputs =
bomIndex.get(
unit.operationId
);
private static class StructureIndex {
final Map<String, String> spNameById = new HashMap<>();
final Map<String, List<BomStructureData.OperationInfo>> operationsByProductSp = 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<>();
if (inputs == null
|| inputs.isEmpty()) {
StructureIndex(BomStructureData s) {
for (BomStructureData.StockingPointInfo sp : s.stockingPoints) {
spNameById.put(sp.id, sp.name);
}
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);
}
}
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);
return;
}
for (BomStructureData.InitialInventoryInfo inv : s.initialInventories) {
initialInventory.put(inv.productId + "@" + inv.spId, inv.quantity);
/*
* 当前Unit:
*
* 例如:
*
* UnitA = 30
*/
for (BomStructureData.OperationInputInfo input :
inputs) {
if (input == null) {
continue;
}
for (BomStructureData.InTransitSupplyInfo its : s.inTransitSupplies) {
inTransitByProductSp.computeIfAbsent(its.productId + "@" + its.spId, k -> new ArrayList<>()).add(its);
if (isBlank(input.inputProductId)) {
continue;
}
for (BomStructureData.PeriodInfo p : s.periods) {
periods.add(p);
if (p.startDate != null) {
startDateByPeriodIndex.put(p.index, p.startDate);
if (isBlank(input.inputSpId)) {
continue;
}
if (input.factor <= 0) {
continue;
}
/*
* ------------------------------------------------
* BOM需求量
*
* Unit生产量 × BOM factor
* ------------------------------------------------
*/
double materialDemandQty =
unitProductionQty
* input.factor;
if (materialDemandQty <= 0) {
continue;
}
List<BomStructureData.OperationInfo> producing(String productId, String spId) {
return operationsByProductSp.getOrDefault(productId + "@" + spId, Collections.emptyList());
/*
* ------------------------------------------------
* 创建:
*
* M1 + M1库房
* ------------------------------------------------
*/
TraceNode child =
buildTraceNode(
input.inputProductId,
input.inputSpId,
materialDemandQty,
input.factor,
outputDate,
outputIndex,
bomIndex,
path,input.inputProductCode, input.inputSpName
);
/*
* 设置名称。
*/
child.warehouseName =
input.inputSpName;
unit.children.add(
child
);
}
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());
/**
* 将:
*
* PeriodTaskResult.OutputInfo
* +
* PeriodTaskResult
*
* 合并成TraceOutput。
*/
private List<TraceOutput> buildTraceOutputs(
List<PeriodTaskResult.OutputInfo> allOutputs,
List<PeriodTaskResult> allTasks) {
List<TraceOutput> result =
new ArrayList<>();
if (allOutputs == null || allOutputs.isEmpty()) {
return result;
}
double initialInv(String productId, String spId) {
return initialInventory.getOrDefault(productId + "@" + spId, 0.0);
/*
* dataKey -> PeriodTaskResult
*/
Map<String, List<PeriodTaskResult>> taskIndex =
new HashMap<>();
if (allTasks != null) {
for (PeriodTaskResult task : allTasks) {
if (task == null) {
continue;
}
List<BomStructureData.InTransitSupplyInfo> inTransit(String productId, String spId) {
return inTransitByProductSp.getOrDefault(productId + "@" + spId, Collections.emptyList());
if (isBlank(task.getDataKey())) {
continue;
}
String startDate(int periodIndex) {
return startDateByPeriodIndex.get(periodIndex);
taskIndex
.computeIfAbsent(
task.getDataKey(),
k -> new ArrayList<>()
)
.add(task);
}
}
// ==================== 求解值索引 ====================
/*
* OutputInfo
*/
for (PeriodTaskResult.OutputInfo output :
allOutputs) {
private static class SolveIndex {
final Map<String, List<PeriodTaskResult.OutputInfo>> outputsByProductSp = new HashMap<>();
final Map<String, PeriodTaskResult> taskByDataKey = new HashMap<>();
final Map<String, PispipResult> pispipByKey = new HashMap<>();
final Map<String, double[]> salesByKey = new HashMap<>();
final Map<String, Double> opDemandByKey = new HashMap<>();
if (output == null) {
continue;
}
SolveIndex(List<PeriodTaskResult.OutputInfo> outputs,
List<PeriodTaskResult> tasks,
List<PispipResult> pispips,
List<SalesDemandResult> sales,
List<OperationDemandResult> opDemands) {
for (PeriodTaskResult.OutputInfo oi : outputs) {
outputsByProductSp.computeIfAbsent(oi.getProductId() + "@" + oi.getSpId(), k -> new ArrayList<>()).add(oi);
if (isBlank(output.productId)) {
continue;
}
for (PeriodTaskResult pt : tasks) {
taskByDataKey.put(pt.getDataKey(), pt);
if (isBlank(output.spId)) {
continue;
}
for (PispipResult p : pispips) {
pispipByKey.put(p.getProductId() + "_" + p.getSpId() + "_" + p.getPeriodIndex(), p);
if (isBlank(output.getDataKey())) {
continue;
}
for (SalesDemandResult sd : sales) {
String key = sd.getProductId() + "_" + sd.getSpId() + "_" + sd.getPeriodIndex();
double[] acc = salesByKey.computeIfAbsent(key, k -> new double[]{0.0, 0.0});
acc[0] += sd.getDemandQty();
acc[1] += sd.getFulfilledQty();
if (output.getProductionQty() <= 0) {
continue;
}
for (OperationDemandResult od : opDemands) {
opDemandByKey.put(od.getOperationId() + "_" + od.getInputProductId() + "_" + od.getInputSpId() + "_" + od.getPeriodIndex(), od.getQuantity());
List<PeriodTaskResult> tasks =
taskIndex.get(output.getDataKey());
/*
* 正常情况下:
*
* Output dataKey -> 一个Task
*
* 如果存在多个Task,则全部保留。
*/
for (PeriodTaskResult task : tasks) {
TraceOutput traceOutput =
new TraceOutput();
traceOutput.productId =
output.productId;
traceOutput.warehouseId =
output.spId;
traceOutput.dataKey =
output.getDataKey();
traceOutput.periodIndex =
output.getPeriodIndex();
traceOutput.outputQty =
output.getProductionQty();
traceOutput.outputDate =
resolveOutputDate(
output,
task
);
traceOutput.operationId =
task.getOperationId();
traceOutput.operationName =
task.getOperationName();
traceOutput.unitId =
task.getUnitId();
traceOutput.unitName =
task.getUnitName();
traceOutput.capacityUsed =
task.getCapacityUsed();
result.add(traceOutput);
}
}
List<PeriodTaskResult.OutputInfo> outputsOf(String productId, String spId) {
return outputsByProductSp.getOrDefault(productId + "@" + spId, Collections.emptyList());
return result;
}
PeriodTaskResult taskOf(String dataKey) {
return taskByDataKey.get(dataKey);
/**
* 建立:
*
* productId + warehouseId
* ↓
* TraceOutput列表
*/
private Map<String, List<TraceOutput>>
buildOutputIndex(
List<TraceOutput> outputs) {
Map<String, List<TraceOutput>> index =
new HashMap<>();
if (outputs == null) {
return index;
}
PispipResult pispip(String productId, String spId, int periodIndex) {
return pispipByKey.get(productId + "_" + spId + "_" + periodIndex);
for (TraceOutput output : outputs) {
if (output == null) {
continue;
}
double demandQty(String productId, String spId, int periodIndex) {
double[] acc = salesByKey.get(productId + "_" + spId + "_" + periodIndex);
return acc == null ? 0.0 : acc[0];
String key =
buildProductWarehouseKey(
output.productId,
output.warehouseId
);
index
.computeIfAbsent(
key,
k -> new ArrayList<>()
)
.add(output);
}
double fulfilledQty(String productId, String spId, int periodIndex) {
double[] acc = salesByKey.get(productId + "_" + spId + "_" + periodIndex);
return acc == null ? 0.0 : acc[1];
/*
* 每个产品+库房按照日期排序。
*/
for (List<TraceOutput> list :
index.values()) {
list.sort(
Comparator.comparing(
o -> o.outputDate,
Comparator.nullsLast(
Comparator.naturalOrder()
)
)
);
}
double opDemand(String operationId, String inputProductId, String inputSpId, int periodIndex) {
return opDemandByKey.getOrDefault(
operationId + "_" + inputProductId + "_" + inputSpId + "_" + periodIndex, 0.0);
return index;
}
/**
* 建立:
*
* operationId
* ↓
* OperationInputInfo列表
*/
private Map<String, List<BomStructureData.OperationInputInfo>>
buildBomIndex(
List<BomStructureData.OperationInputInfo> operationInputs) {
Map<String, List<BomStructureData.OperationInputInfo>> index =
new HashMap<>();
if (operationInputs == null) {
return index;
}
// ==================== 节点组装 ====================
for (BomStructureData.OperationInputInfo input :
operationInputs) {
private SupplyChainNode buildNode(StructureIndex sidx, SolveIndex solveIdx,
String productId, String spId,
Integer periodIndex, int level, Set<String> visited) {
String nodeKey = productId + "@" + spId;
if (!visited.add(nodeKey)) {
return null; // 防环
if (input == null) {
continue;
}
SupplyChainNode node = new SupplyChainNode();
node.setProductId(productId);
node.setSpId(spId);
node.setLevel(level);
if (isBlank(input.operationId)) {
continue;
}
String periodStartDate = periodIndex == null ? null : sidx.startDate(periodIndex);
if (isBlank(input.inputProductId)) {
continue;
}
// --- 供应来源: 以 period_task_outputs 为入口 ---
Map<String, SupplyChainNode.SupplySource> srcByOp = new LinkedHashMap<>();
for (PeriodTaskResult.OutputInfo oi : solveIdx.outputsOf(productId, spId)) {
node.setSpName(oi.getSpName());
node.setProductCode(oi.getProductCode());
if (isBlank(input.inputSpId)) {
continue;
}
PeriodTaskResult pt = solveIdx.taskOf(oi.getDataKey());
if (pt == null) continue;
if (input.factor <= 0) {
continue;
}
SupplyChainNode.SupplySource src = srcByOp.computeIfAbsent(pt.getOperationId(), opId -> {
SupplyChainNode.SupplySource s = new SupplyChainNode.SupplySource();
s.type = "OPERATION";
s.operationId = pt.getOperationId();
s.operationName = pt.getOperationName();
s.unitId = pt.getUnitId();
s.unitName = pt.getUnitName();
s.totalProduction = pt.getProductionQty();
return s;
});
index
.computeIfAbsent(
input.operationId,
k -> new ArrayList<>()
)
.add(input);
}
node.getSupplySources().add(src);
return index;
}
private List<TraceOutput> findHistoricalOutputsForDemand(
String productId,
String warehouseId,
String demandDate,
double demandQty,
Map<String, List<TraceOutput>> outputIndex) {
// --- 在途供应 ---
for (BomStructureData.InTransitSupplyInfo its : sidx.inTransit(productId, spId)) {
SupplyChainNode.SupplySource src = new SupplyChainNode.SupplySource();
src.type = "IN_TRANSIT";
src.totalProduction = its.quantity;
node.getSupplySources().add(src);
if (isBlank(productId)
|| isBlank(warehouseId)
|| isBlank(demandDate)
|| demandQty <= 0) {
return Collections.emptyList();
}
// --- 外部采购 (无供应源) ---
if (node.getSupplySources().isEmpty()) {
SupplyChainNode.SupplySource src = new SupplyChainNode.SupplySource();
src.type = "EXTERNAL";
src.totalProduction = 0;
node.getSupplySources().add(src);
String key = buildProductWarehouseKey(
productId,
warehouseId
);
List<TraceOutput> candidates = outputIndex.get(key);
if (candidates == null || candidates.isEmpty()) {
return Collections.emptyList();
}
/*
* 日期分组:
*
* 09-19 -> [Output1, Output2]
* 09-18 -> [Output3]
* 09-17 -> [Output4, Output5]
*/
Map<String, List<TraceOutput>> byDate =
new HashMap<>();
// --- BOM 子物料 (向下展开) ---
List<BomStructureData.OperationInfo> producingOps = sidx.producing(productId, spId);
if (!producingOps.isEmpty()) {
BomStructureData.OperationInfo op = producingOps.get(0);
for (BomStructureData.OperationInputInfo in : sidx.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;
for (TraceOutput output : candidates) {
double totalConsumed = 0;
for (BomStructureData.PeriodInfo p : sidx.periods) {
double consumed = solveIdx.opDemand(in.operationId, in.inputProductId, in.inputSpId, p.index);
totalConsumed += consumed;
if (consumed > 0) {
String periodKey = p.startDate == null ? String.valueOf(p.index) : p.startDate;
child.consumedByPeriod.put(periodKey, consumed);
}
if (output == null) {
continue;
}
child.totalConsumedQty = totalConsumed;
if (periodStartDate != null && !child.consumedByPeriod.containsKey(periodStartDate)) {
if (isBlank(output.outputDate)) {
continue;
}
SupplyChainNode childNode = buildNode(sidx, solveIdx, in.inputProductId, in.inputSpId, periodIndex, level + 1, visited);
if (childNode == null) {
// 只允许历史数据
if (compareDate(output.outputDate, demandDate) > 0) {
continue;
}
for (SupplyChainNode.SupplySource ss : childNode.getSupplySources()) {
child.totalConsumedQty = ss.production;
child.unitId = ss.unitId;
child.unitName = ss.unitName;
break;
}
node.getChildren().add(child);
byDate
.computeIfAbsent(
output.outputDate,
k -> new ArrayList<>()
)
.add(output);
}
if (byDate.isEmpty()) {
return Collections.emptyList();
}
// --- 汇总 ---
fillSummary(sidx, solveIdx, node, productId, spId);
/*
* 最近日期 → 最早日期
*/
List<String> dates = new ArrayList<>(
byDate.keySet()
);
// period 剪枝
if (periodStartDate != null && !node.getActivePeriods().contains(periodStartDate)) {
return null;
}
dates.sort(Comparator.reverseOrder());
return node;
List<TraceOutput> result = new ArrayList<>();
double accumulatedQty = 0.0;
for (String date : dates) {
List<TraceOutput> outputs =
byDate.get(date);
if (outputs == null || outputs.isEmpty()) {
continue;
}
private void fillSummary(StructureIndex sidx, SolveIndex solveIdx,
SupplyChainNode node, String productId, String spId) {
SupplySummary s = new SupplySummary();
node.setSummary(s);
/*
* 同一天所有 Unit / Output 都必须保留。
*/
outputs.sort(
Comparator.comparing(
o -> o.dataKey,
Comparator.nullsLast(
Comparator.naturalOrder()
)
)
);
for (TraceOutput output : outputs) {
result.add(output);
int n = sidx.periods.size();
double totalEndingInv = 0;
for (BomStructureData.PeriodInfo p : sidx.periods) {
PispipResult pr = solveIdx.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);
accumulatedQty += output.outputQty;
}
/*
* 一个日期全部处理完以后,
* 再判断是否已经满足需求。
*/
if (accumulatedQty >= demandQty) {
break;
}
}
s.setInitialInventory(sidx.initialInv(productId, spId));
PispipResult last = solveIdx.pispip(productId, spId, n - 1);
s.setFinalInventory(last == null ? 0 : last.getEndingInventory());
s.setAverageInventory(n > 0 ? totalEndingInv / n : 0);
return result;
}
/**
* 日期比较。
*
* 当前默认支持:
*
* yyyy-MM-dd
* yyyy-MM-dd HH:mm:ss
* yyyy-MM-dd'T'HH:mm:ss
*
* 如果你的项目有自己的日期类型,
* 建议这里直接替换。
*/
private int compareDate(
String d1,
String d2) {
// 生产汇总 (跨周期全量, 不受 period 过滤影响)
double totalProduction = 0;
for (PeriodTaskResult.OutputInfo oi : solveIdx.outputsOf(productId, spId)) {
totalProduction += oi.getProductionQty();
if (d1 == null && d2 == null) {
return 0;
}
s.setTotalProduction(totalProduction);
// 在途汇总
double totalInTransit = 0;
for (BomStructureData.InTransitSupplyInfo its : sidx.inTransit(productId, spId)) {
totalInTransit += its.quantity;
if (d1 == null) {
return -1;
}
s.setTotalInTransit(totalInTransit);
// 需求汇总
double totalSalesDemand = 0;
double totalSalesFulfilled = 0;
double totalDepDemand = 0;
double totalDemandFulf = 0;
double totalSlack = 0;
for (BomStructureData.PeriodInfo p : sidx.periods) {
PispipResult pr = solveIdx.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);
if (d2 == null) {
return 1;
}
return d1.compareTo(d2);
}
totalSalesDemand += solveIdx.demandQty(productId, spId, p.index);
totalSalesFulfilled += solveIdx.fulfilledQty(productId, spId, p.index);
/**
* Product + Warehouse唯一Key。
*/
private String buildProductWarehouseKey(
String productId,
String warehouseId) {
return String.valueOf(productId)
+ "@"
+ String.valueOf(warehouseId);
}
s.setTotalSalesDemand(totalSalesDemand);
s.setTotalSalesFulfilled(totalSalesFulfilled);
s.setTotalDependentDemand(totalDepDemand);
s.setTotalDemandFulfillment(totalDemandFulf);
s.setTotalDemandSlack(totalSlack);
node.setTotalSalesDemand(totalSalesDemand);
node.setTotalSalesFulfilled(totalSalesFulfilled);
node.setTotalDependentDemand(totalDepDemand);
/**
* 使用periodIndex作为追溯日期。
*
* 如果项目中periodIndex就是时间顺序,
* 直接使用它最可靠。
*/
private String resolveOutputDate(
PeriodTaskResult.OutputInfo output,
PeriodTaskResult task) {
// 库存规格偏差
double belowTarget = 0;
double belowMin = 0;
double aboveMax = 0;
for (BomStructureData.PeriodInfo p : sidx.periods) {
PispipResult pr = solveIdx.pispip(productId, spId, p.index);
if (pr != null) {
belowTarget += pr.getBelowTarget();
belowMin += pr.getBelowMin();
aboveMax += pr.getAboveMax();
}
if (output == null) {
return null;
}
s.setTotalBelowTarget(belowTarget);
s.setTotalBelowMin(belowMin);
s.setTotalAboveMax(aboveMax);
return String.valueOf(
task.getPeriodStartDate()
);
}
/**
* 判断字符串为空。
*/
private boolean isBlank(String value) {
private boolean isBlank(String v) {
return v == null || v.trim().isEmpty();
return value == null
|| value.trim().isEmpty();
}
}
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 网络, 而是查询时按 productId + spId 定位节点,
* 递归向下展开 BOM 子树。求解值从 parquet 按需过滤读取, 避免全量加载。</p>
*
* <p>数据流:</p>
* <ol>
* <li>bom_structure.json —— 静态 BOM 结构 (小, 全量读);</li>
* <li>period_task_outputs.parquet —— 工序产出 (谁把什么产入哪个库房);</li>
* <li>period_tasks.parquet —— 生产任务 (dataKey → unit/operation/产量);</li>
* <li>pispips / salesdemand / operation_demands —— 库存/销售/工序消耗量。</li>
* </ol>
*/
@Service
@Slf4j
public class MacroPlannerProductNetworkService1 {
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());
// 1. 静态结构 (小, 全量读)
BomStructureData structure = objectMapper.readValue(
new File(path(sceneId, "bom_structure.json")), BomStructureData.class);
StructureIndex sidx = new StructureIndex(structure);
// 2. 纯结构展开目标子树, 确定涉及的节点集合与工序集合
Set<String> nodeKeys = new HashSet<>();
Set<String> opIds = new HashSet<>();
collectSubtree(sidx, productId, spId, nodeKeys, opIds);
// 3. 按需过滤读取求解值 (不全量加载)
FlatParquetUtil parquet = new FlatParquetUtil();
List<PeriodTaskResult.OutputInfo> outputs = parquet.readAll(
path(sceneId, "period_task_outputs.parquet"),
PeriodTaskResult.OutputInfo.class,
oi -> nodeKeys.contains(oi.getProductId() + "@" + oi.getSpId()));
Set<String> dataKeys = new HashSet<>();
for (PeriodTaskResult.OutputInfo oi : outputs) {
dataKeys.add(oi.getDataKey());
}
List<PeriodTaskResult> tasks = parquet.readAll(
path(sceneId, "period_tasks.parquet"),
PeriodTaskResult.class,
pt -> dataKeys.contains(pt.getDataKey()));
List<PispipResult> pispips = parquet.readAll(
path(sceneId, "pispips.parquet"),
PispipResult.class,
p -> nodeKeys.contains(p.getProductId() + "@" + p.getSpId()));
List<SalesDemandResult> sales = parquet.readAll(
path(sceneId, "salesdemand.parquet"),
SalesDemandResult.class,
sd -> nodeKeys.contains(sd.getProductId() + "@" + sd.getSpId()));
List<OperationDemandResult> opDemands = parquet.readAll(
path(sceneId, "operation_demands.parquet"),
OperationDemandResult.class,
od -> opIds.contains(od.getOperationId()));
SolveIndex solveIdx = new SolveIndex(outputs, tasks, pispips, sales, opDemands);
// 4. 递归组装
return buildNode(sidx, solveIdx, productId, spId, periodIndex, 0, new HashSet<>());
} catch (Exception e) {
log.warn("按需组装产品网络失败: sceneId={}, productId={}, spId={}, error={}",
sceneId, productId, spId, e.getMessage());
return null;
}
}
private String path(String sceneId, String fileName) {
return Paths.get(RESULT_DIR, sceneId, fileName).toAbsolutePath().toString();
}
/**
* 纯结构遍历 BOM, 收集目标子树涉及的所有 productId@spId 与 operationId。
*/
private void collectSubtree(StructureIndex sidx, String productId, String spId,
Set<String> nodeKeys, Set<String> opIds) {
String key = productId + "@" + spId;
if (!nodeKeys.add(key)) {
return;
}
for (BomStructureData.OperationInfo op : sidx.producing(productId, spId)) {
opIds.add(op.id);
for (BomStructureData.OperationInputInfo in : sidx.inputsOf(op.id)) {
opIds.add(in.operationId);
collectSubtree(sidx, in.inputProductId, in.inputSpId, nodeKeys, opIds);
}
}
// 消费者工序 (上层) 也需要, 用于查 operation_demands 的消耗量
for (BomStructureData.OperationInputInfo in : sidx.consumersOf(productId, spId)) {
opIds.add(in.operationId);
}
}
// ==================== 结构索引 ====================
private static class StructureIndex {
final Map<String, String> spNameById = new HashMap<>();
final Map<String, List<BomStructureData.OperationInfo>> operationsByProductSp = 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<>();
StructureIndex(BomStructureData s) {
for (BomStructureData.StockingPointInfo sp : s.stockingPoints) {
spNameById.put(sp.id, sp.name);
}
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);
}
}
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);
}
}
}
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());
}
String startDate(int periodIndex) {
return startDateByPeriodIndex.get(periodIndex);
}
}
// ==================== 求解值索引 ====================
private static class SolveIndex {
final Map<String, List<PeriodTaskResult.OutputInfo>> outputsByProductSp = new HashMap<>();
final Map<String, PeriodTaskResult> taskByDataKey = new HashMap<>();
final Map<String, PispipResult> pispipByKey = new HashMap<>();
final Map<String, double[]> salesByKey = new HashMap<>();
final Map<String, Double> opDemandByKey = new HashMap<>();
SolveIndex(List<PeriodTaskResult.OutputInfo> outputs,
List<PeriodTaskResult> tasks,
List<PispipResult> pispips,
List<SalesDemandResult> sales,
List<OperationDemandResult> opDemands) {
for (PeriodTaskResult.OutputInfo oi : outputs) {
outputsByProductSp.computeIfAbsent(oi.getProductId() + "@" + oi.getSpId(), k -> new ArrayList<>()).add(oi);
}
for (PeriodTaskResult pt : tasks) {
taskByDataKey.put(pt.getDataKey(), pt);
}
for (PispipResult p : pispips) {
pispipByKey.put(p.getProductId() + "_" + p.getSpId() + "_" + p.getPeriodIndex(), p);
}
for (SalesDemandResult sd : sales) {
String key = sd.getProductId() + "_" + sd.getSpId() + "_" + sd.getPeriodIndex();
double[] acc = salesByKey.computeIfAbsent(key, k -> new double[]{0.0, 0.0});
acc[0] += sd.getDemandQty();
acc[1] += sd.getFulfilledQty();
}
for (OperationDemandResult od : opDemands) {
opDemandByKey.put(od.getOperationId() + "_" + od.getInputProductId() + "_" + od.getInputSpId() + "_" + od.getPeriodIndex(), od.getQuantity());
}
}
List<PeriodTaskResult.OutputInfo> outputsOf(String productId, String spId) {
return outputsByProductSp.getOrDefault(productId + "@" + spId, Collections.emptyList());
}
PeriodTaskResult taskOf(String dataKey) {
return taskByDataKey.get(dataKey);
}
PispipResult pispip(String productId, String spId, int periodIndex) {
return pispipByKey.get(productId + "_" + spId + "_" + periodIndex);
}
double demandQty(String productId, String spId, int periodIndex) {
double[] acc = salesByKey.get(productId + "_" + spId + "_" + periodIndex);
return acc == null ? 0.0 : acc[0];
}
double fulfilledQty(String productId, String spId, int periodIndex) {
double[] acc = salesByKey.get(productId + "_" + spId + "_" + periodIndex);
return acc == null ? 0.0 : acc[1];
}
double opDemand(String operationId, String inputProductId, String inputSpId, int periodIndex) {
return opDemandByKey.getOrDefault(
operationId + "_" + inputProductId + "_" + inputSpId + "_" + periodIndex, 0.0);
}
}
// ==================== 节点组装 ====================
private SupplyChainNode buildNode(StructureIndex sidx, SolveIndex solveIdx,
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.setLevel(level);
String periodStartDate = periodIndex == null ? null : sidx.startDate(periodIndex);
// --- 供应来源: 以 period_task_outputs 为入口 ---
Map<String, SupplyChainNode.SupplySource> srcByOp = new LinkedHashMap<>();
for (PeriodTaskResult.OutputInfo oi : solveIdx.outputsOf(productId, spId)) {
node.setSpName(oi.getSpName());
node.setProductCode(oi.getProductCode());
PeriodTaskResult pt = solveIdx.taskOf(oi.getDataKey());
if (pt == null) continue;
SupplyChainNode.SupplySource src = srcByOp.computeIfAbsent(pt.getOperationId(), opId -> {
SupplyChainNode.SupplySource s = new SupplyChainNode.SupplySource();
s.type = "OPERATION";
s.operationId = pt.getOperationId();
s.operationName = pt.getOperationName();
s.unitId = pt.getUnitId();
s.unitName = pt.getUnitName();
s.totalProduction = pt.getProductionQty();
return s;
});
node.getSupplySources().add(src);
}
// --- 在途供应 ---
for (BomStructureData.InTransitSupplyInfo its : sidx.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);
}
// --- BOM 子物料 (向下展开) ---
List<BomStructureData.OperationInfo> producingOps = sidx.producing(productId, spId);
if (!producingOps.isEmpty()) {
BomStructureData.OperationInfo op = producingOps.get(0);
for (BomStructureData.OperationInputInfo in : sidx.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 : sidx.periods) {
double consumed = solveIdx.opDemand(in.operationId, in.inputProductId, in.inputSpId, p.index);
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(sidx, solveIdx, in.inputProductId, in.inputSpId, periodIndex, level + 1, visited);
if (childNode == null) {
continue;
}
for (SupplyChainNode.SupplySource ss : childNode.getSupplySources()) {
child.totalConsumedQty = ss.production;
child.unitId = ss.unitId;
child.unitName = ss.unitName;
break;
}
node.getChildren().add(child);
}
}
// --- 汇总 ---
fillSummary(sidx, solveIdx, node, productId, spId);
// period 剪枝
if (periodStartDate != null && !node.getActivePeriods().contains(periodStartDate)) {
return null;
}
return node;
}
private void fillSummary(StructureIndex sidx, SolveIndex solveIdx,
SupplyChainNode node, String productId, String spId) {
SupplySummary s = new SupplySummary();
node.setSummary(s);
int n = sidx.periods.size();
double totalEndingInv = 0;
for (BomStructureData.PeriodInfo p : sidx.periods) {
PispipResult pr = solveIdx.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(sidx.initialInv(productId, spId));
PispipResult last = solveIdx.pispip(productId, spId, n - 1);
s.setFinalInventory(last == null ? 0 : last.getEndingInventory());
s.setAverageInventory(n > 0 ? totalEndingInv / n : 0);
// 生产汇总 (跨周期全量, 不受 period 过滤影响)
double totalProduction = 0;
for (PeriodTaskResult.OutputInfo oi : solveIdx.outputsOf(productId, spId)) {
totalProduction += oi.getProductionQty();
}
s.setTotalProduction(totalProduction);
// 在途汇总
double totalInTransit = 0;
for (BomStructureData.InTransitSupplyInfo its : sidx.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 : sidx.periods) {
PispipResult pr = solveIdx.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 += solveIdx.demandQty(productId, spId, p.index);
totalSalesFulfilled += solveIdx.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 : sidx.periods) {
PispipResult pr = solveIdx.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