Commit 2ec4b057 authored by Tong Li's avatar Tong Li

MP

parent 7d6de509
......@@ -128,6 +128,71 @@
<artifactId>httpclient</artifactId>
<version>4.5.14</version>
</dependency>
<!-- parquet (用于存储运算结果) -->
<dependency>
<groupId>org.apache.parquet</groupId>
<artifactId>parquet-avro</artifactId>
<version>1.14.4</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-common</artifactId>
<version>3.3.6</version>
<exclusions>
<!-- 排除 Hadoop 自带的 slf4j-reload4j 绑定,避免与 Spring Boot 的 logback 冲突(SLF4J multiple bindings) -->
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-reload4j</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-mapreduce-client-core</artifactId>
<version>3.3.6</version>
<exclusions>
<!-- 排除日志绑定,避免再次出现 SLF4J multiple bindings -->
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-reload4j</artifactId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
<!-- 排除YARN相关,完全用不到 -->
<exclusion>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-yarn-api</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-yarn-common</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-yarn-client</artifactId>
</exclusion>
<!-- 排除其他不需要的模块 -->
<exclusion>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-hdfs-client</artifactId>
</exclusion>
<exclusion>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.xerial.snappy</groupId>
<artifactId>snappy-java</artifactId>
<version>1.1.10.5</version>
</dependency>
</dependencies>
<build>
......
package com.aps.common.util;
/**
* 作者:佟礼
* 时间:2026-09-08
*/
import org.apache.avro.Schema;
import org.apache.avro.reflect.ReflectData;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.parquet.avro.AvroParquetReader;
import org.apache.parquet.avro.AvroParquetWriter;
import org.apache.parquet.hadoop.ParquetReader;
import org.apache.parquet.hadoop.ParquetWriter;
import org.apache.parquet.hadoop.metadata.CompressionCodecName;
import org.apache.parquet.io.LocalInputFile;
import org.apache.parquet.io.LocalOutputFile;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
/**
* 范式化扁平Row POJO读写Parquet
* 要求:POJO不要包含嵌套List集合,子List拆成独立子parquet
*/
public class FlatParquetUtil {
static {
// Windows 下 Hadoop 需要 winutils.exe/hadoop.dll。
// 优先复用 HADOOP_HOME,避免把 hadoop.home.dir 指到当前目录(无 bin/winutils.exe)导致写入失败。
if (System.getProperty("hadoop.home.dir") == null) {
String hadoopHome = System.getenv("HADOOP_HOME");
if (hadoopHome != null && !hadoopHome.trim().isEmpty()) {
System.setProperty("hadoop.home.dir", hadoopHome);
}
}
}
private final Configuration conf;
public FlatParquetUtil() {
conf = new Configuration();
// 使用本地文件系统,不依赖hadoop集群
conf.set("fs.file.impl", org.apache.hadoop.fs.LocalFileSystem.class.getName());
// 关闭hadoop Shell的部分本地命令调用(Windows不需要winutils.exe)
conf.setBoolean("hadoop.native.lib", false);
}
/**
* 写入扁平对象列表到parquet(覆盖)
*/
public <T> void write(List<T> rows, String filePath, Class<T> clazz) throws IOException {
if (rows == null || rows.isEmpty()) {
return;
}
File file = resolvePath(filePath);
File parent = file.getParentFile();
if (parent != null && !parent.exists()) {
parent.mkdirs();
}
if (file.exists()) {
file.delete();
}
Schema schema = ReflectData.AllowNull.get().getSchema(clazz);
LocalOutputFile outputFile = new LocalOutputFile(file.toPath());
try(ParquetWriter<T> writer = AvroParquetWriter.<T>builder(outputFile)
.withSchema(schema)
.withDataModel(ReflectData.AllowNull.get())
.withConf(conf)
.withCompressionCodec(CompressionCodecName.SNAPPY)
.withRowGroupSize(128 * 1024 * 1024)
.build()){
for(T row : rows){
writer.write(row);
}
}
}
private File resolvePath(String filePath) throws IOException {
try {
if (filePath.startsWith("file:")) {
// 是URI格式,用标准URI解析,自动处理Windows盘符、空格、中文
return Paths.get(new URI(filePath)).toFile();
} else {
// 纯本地路径
return new File(filePath);
}
} catch (Exception e) {
throw new IOException("路径解析失败: " + filePath, e);
}
}
/**
* 读取全部行
*/
public <T> List<T> readAll(String filePath, Class<T> clazz) throws IOException {
List<T> res = new ArrayList<>();
File file = resolvePath(filePath);
LocalInputFile inputFile = new LocalInputFile(file.toPath());
try(ParquetReader<T> reader = AvroParquetReader.<T>builder(inputFile)
.withDataModel(ReflectData.AllowNull.get())
.withConf(conf)
.build()){
T t;
while ((t = reader.read()) != null) {
res.add(t);
}
}
return res;
}
/**
* 获取迭代器流式读取,不一次性全加载内存
*/
public <T> ParquetReader<T> getReader(String filePath, Class<T> clazz) throws IOException {
Path path = new Path(filePath);
return AvroParquetReader.<T>builder(path)
.withDataModel(ReflectData.AllowNull.get())
.withConf(conf)
.build();
}
}
......@@ -196,7 +196,7 @@ public class MacroPlannerOptimizer {
writeLog(" [OK] 目标函数创建完成");
// 4. 导出 LP 模型文件
exportLpModel();
//exportLpModel();
writeLog("\n模型统计: 变量=" + model.getSolver().numVariables()
+ ", 约束=" + model.getSolver().numConstraints() + "\n");
......@@ -432,7 +432,7 @@ public class MacroPlannerOptimizer {
l3.addKPI("供应目标偏差", model.getTotalSupplyTarget(), w.getSupplyTargetWeight());
l3.addKPI("销售优先级", model.getTotalSalesDemandPriority(),
w.getSalesDemandPriorityWeight(), true); // 负系数 = 最大化
levels.add(l3);
// levels.add(l3);
// === Level 4: 软约束 (允许 10% 退化, slack=10%) ===
StrategyLevel l4 = new StrategyLevel(4, "软约束", 0.10);
......@@ -440,7 +440,7 @@ public class MacroPlannerOptimizer {
l4.addKPI("欠库存", model.getTotalMinInventoryLevel(), w.getMinInventoryLevelWeight());
l4.addKPI("最小供应不足", model.getTotalMinSupply(), w.getMinSupplyWeight());
l4.addKPI("最大供应超出", model.getTotalMaxSupply(), w.getMaxSupplyWeight());
levels.add(l4);
// levels.add(l4);
return levels;
}
......
......@@ -55,10 +55,11 @@ public class MultiLevelBomTestRunner {
// for (int scale : BenchmarkDataBuilder.SUPPORTED_SCALES) {
//
// 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");
......
......@@ -42,8 +42,7 @@ public class BalanceConstraint {
// === 流入 ===
// PTQty 产出 (考虑提前期: productionDate 生产, productionDate+leadTimeDays 到货)
// 关键: 只统计产出到当前库存点(sp)的操作, 避免多工序路由中产出到错误的库存点
for (Operation op : data.getOperations()) {
if (!op.producesProductAtSp(prod.getId(), sp.getId())) continue;
for (Operation op : data.getOperationsProducing(prod.getId(), sp.getId())) {
int leadTimeDays = op.getLeadTimeDays();
Period productionPeriod = data.getPeriodOffsetByDays(p, leadTimeDays);
if (productionPeriod == null) continue;
......@@ -53,8 +52,8 @@ public class BalanceConstraint {
}
}
// DemandSlack
// MPVariable slackVar = demandSlackVars.get(invKey);
// if (slackVar != null) balance.setCoefficient(slackVar, 1.0);
// MPVariable slackVar = demandSlackVars.get(invKey);
// if (slackVar != null) balance.setCoefficient(slackVar, 1.0);
// 上一周期库存 (t>0)
if (p.getIndex() > 0) {
String prevKey = prod.getId() + "_" + sp.getId() + "_" + (p.getIndex() - 1);
......@@ -84,10 +83,8 @@ public class BalanceConstraint {
}
// 在途供应 (供应商已发货, 固定到货量, 按日期匹配周期)
for (InTransitSupply its : data.getInTransitSupplies()) {
if (its.getProduct().getId().equals(prod.getId())
&& its.getStockingPoint().getId().equals(sp.getId())
&& p.equals(data.getPeriodByDate(its.getArrivalDate()))) {
for (InTransitSupply its : data.getInTransitSuppliesFor(prod.getId(), sp.getId())) {
if (p.equals(data.getPeriodByDate(its.getArrivalDate()))) {
rhs -= its.getQuantity();
}
}
......
......@@ -57,9 +57,7 @@ public class BomConstraint {
"DepDemandDef_" + ddKey);
ddCon.setCoefficient(ddVar, -1.0);
for (OperationInput input : data.getOperationInputs()) {
if (!input.getInputProduct().getId().equals(prod.getId())) continue;
if (!input.getInputSp().getId().equals(sp.getId())) continue;
for (OperationInput input : data.getOperationInputsFor(prod.getId(), sp.getId())) {
MPVariable odVar = operationDemandQtyVars.get(
input.getKey() + "_" + p.getIndex());
if (odVar != null) ddCon.setCoefficient(odVar, 1.0);
......
......@@ -38,7 +38,7 @@ public class CapacityConstraint {
// --- 最大产能 ---
MPConstraint maxCap = model.getSolver().makeConstraint(
-MPSolver.infinity(), up.getMaxCapacity(), "MaxCap_" + capKey);
for (Operation op : data.getOperations()) {
for (Operation op : data.getOperationsByUnitId(up.getUnitId())) {
for (UnitOperation uo : op.getUnitOperations()) {
if (!uo.getUnitId().equals(up.getUnitId())) continue;
MPVariable ptVar = ptQtyVars.get(op.ptQtyKey(uo, up.getPeriod().getIndex()));
......@@ -52,7 +52,7 @@ public class CapacityConstraint {
if (up.hasMinCapacity()) {
MPConstraint minCap = model.getSolver().makeConstraint(
up.getMinCapacity(), MPSolver.infinity(), "MinCap_" + capKey);
for (Operation op : data.getOperations()) {
for (Operation op : data.getOperationsByUnitId(up.getUnitId())) {
for (UnitOperation uo : op.getUnitOperations()) {
if (!uo.getUnitId().equals(up.getUnitId())) continue;
MPVariable ptVar = ptQtyVars.get(op.ptQtyKey(uo, up.getPeriod().getIndex()));
......
......@@ -49,23 +49,16 @@ public class DemandFulfillmentConstraint {
if (dfVar != null) c.setCoefficient(dfVar, -1.0);
// + SalesDemandQty
for (SalesDemand sd : data.getSalesDemands()) {
if (sd.getProduct().getId().equals(prod.getId())
&& sd.getStockingPoint().getId().equals(sp.getId())
&& sd.getPeriod().getIndex() == p.getIndex()) {
MPVariable sdVar = sdVars.get(sd.getKey());
if (sdVar != null) c.setCoefficient(sdVar, 1.0);
}
for (SalesDemand sd : data.getSalesDemandsFor(prod, sp, p)) {
MPVariable sdVar = sdVars.get(sd.getKey());
if (sdVar != null) c.setCoefficient(sdVar, 1.0);
}
// + OperationDemandQty (BOM 消耗)
for (OperationInput input : data.getOperationInputs()) {
if (input.getInputProduct().getId().equals(prod.getId())
&& input.getInputSp().getId().equals(sp.getId())) {
String opKey = input.getKey() + "_" + p.getIndex();
MPVariable odVar = opDemandVars.get(opKey);
if (odVar != null) c.setCoefficient(odVar, 1.0);
}
for (OperationInput input : data.getOperationInputsFor(prod.getId(), sp.getId())) {
String opKey = input.getKey() + "_" + p.getIndex();
MPVariable odVar = opDemandVars.get(opKey);
if (odVar != null) c.setCoefficient(odVar, 1.0);
}
}
}
......
......@@ -6,6 +6,7 @@ import com.google.ortools.linearsolver.MPVariable;
import com.aps.macroplanner.data.*;
import com.aps.macroplanner.model.MacroPlannerModel;
import java.util.List;
import java.util.Map;
/**
......@@ -58,25 +59,22 @@ public class DemandSlackLinkageConstraint {
for (Period p : data.getPeriods()) {
String pispipKey = prod.getId() + "_" + sp.getId() + "_" + p.getIndex();
double totalDemandQty = 0.0;
boolean hasDemand = false;
// 汇总该 PISPIP 的所有销售需求
for (SalesDemand sd : data.getSalesDemandsFor(prod, sp, p)) {
List<SalesDemand> demands = data.getSalesDemandsFor(prod, sp, p);
if (demands.isEmpty()) continue;
double totalDemandQty = 0.0;
for (SalesDemand sd : demands) {
totalDemandQty += sd.getQuantity();
hasDemand = true;
}
// 只对存在销售需求的 PISPIP 创建约束
if (!hasDemand) continue;
// 约束: Σ SalesDemandQty + DemandSlack >= totalDemandQty
MPConstraint c = model.getSolver().makeConstraint(
totalDemandQty, MPSolver.infinity(),
"DSLink_" + pispipKey);
// + Σ SalesDemandQty
for (SalesDemand sd : data.getSalesDemandsFor(prod, sp, p)) {
for (SalesDemand sd : demands) {
MPVariable sdVar = sdVars.get(sd.getKey());
if (sdVar != null) c.setCoefficient(sdVar, 1.0);
}
......
......@@ -19,9 +19,9 @@ import java.util.Random;
*/
public class BenchmarkDataBuilder extends TestDataBuilder {
public static final int[] SUPPORTED_SCALES = {100, 200, 500, 1000, 2000, 5000};
public static final int[] SUPPORTED_SCALES = {100, 200, 500, 1000, 2000, 5000, 10000};
public static final long RANDOM_SEED = 20260826L;
public static final int PERIOD_COUNT = 15;
public static final int PERIOD_COUNT = 30;
private static final int SEMI_DIVISOR = 5;
private static final int RAW_MULTIPLIER = 2;
......
......@@ -179,6 +179,7 @@ public class DataValidator {
// 循环依赖检测 (含库存点: 仅当产品+库存点都匹配时才构成循环)
// 多工序路由中同一产品经不同库存点流转不构成循环
// 优化: 通过 getOperationInputsFor 索引查找反向边, 避免 O(Inputs²) 全表扫描
for (OperationInput input : data.getOperationInputs()) {
String consumedProd = input.getInputProduct().getId();
String consumedSp = input.getInputSp().getId();
......@@ -188,11 +189,10 @@ public class DataValidator {
String consumerProd = consumerOutput.getProductId();
String consumerSp = consumerOutput.getSpId();
for (OperationInput other : data.getOperationInputs()) {
// 检查 other 是否产出 consumedProd@consumedSp 且消耗 consumerProd@consumerSp
if (other.getOperation().producesProductAtSp(consumedProd, consumedSp)
&& other.getInputProduct().getId().equals(consumerProd)
&& other.getInputSp().getId().equals(consumerSp)) {
// 反向边: 消耗 consumerProd@consumerSp 的输入
for (OperationInput other : data.getOperationInputsFor(consumerProd, consumerSp)) {
// 检查 other 是否产出 consumedProd@consumedSp (形成 A→B→A)
if (other.getOperation().producesProductAtSp(consumedProd, consumedSp)) {
errors.add("BOM 循环依赖: " + consumerProd + "@" + consumerSp
+ " → " + consumedProd + "@" + consumedSp
+ " → " + consumerProd + "@" + consumerSp);
......
......@@ -63,6 +63,17 @@ public class TestDataBuilder {
/** 初始库存列表 (子类可访问) */
protected final List<InitialInventory> initialInventories = new ArrayList<>();
// ==================== 查询索引 (懒加载, 避免约束构建中的 O(N²) 线性扫描) ====================
private Map<String, List<StockingPoint>> stockingPointsByProductIndex;
private Map<String, List<SalesDemand>> salesDemandsIndex;
private Map<String, List<OperationInput>> operationInputsIndex;
private Map<String, List<Operation>> operationsByOutputIndex;
private Map<String, List<Operation>> operationsByProductIndex;
private Map<String, List<Operation>> operationsByUnitIndex;
private Map<String, Double> initialInventoryIndex;
private Map<String, InventorySpec> inventorySpecIndex;
private Map<String, List<InTransitSupply>> inTransitSuppliesByProductSpIndex;
/** 公开构造器 — 自动调用 build() 初始化默认测试数据 */
public TestDataBuilder() {
this(false);
......@@ -295,13 +306,83 @@ public class TestDataBuilder {
* @return 库存点列表, 不存在则返回空列表
*/
public List<StockingPoint> getStockingPointsForProduct(String productId) {
List<StockingPoint> result = new ArrayList<>();
for (ProductSpMapping m : productSpMappings) {
if (m.getProduct().getId().equals(productId)) {
result.add(m.getStockingPoint());
if (stockingPointsByProductIndex == null) {
stockingPointsByProductIndex = new HashMap<>();
for (ProductSpMapping m : productSpMappings) {
stockingPointsByProductIndex
.computeIfAbsent(m.getProduct().getId(), k -> new ArrayList<>())
.add(m.getStockingPoint());
}
}
return stockingPointsByProductIndex.getOrDefault(productId, Collections.emptyList());
}
/**
* 获取产出到指定产品/库存点的所有操作 (索引查询, O(1))。
*/
public List<Operation> getOperationsProducing(String productId, String spId) {
if (operationsByOutputIndex == null) {
operationsByOutputIndex = new HashMap<>();
for (Operation op : operations) {
for (OperationOutput oo : op.getOutputs()) {
operationsByOutputIndex
.computeIfAbsent(oo.getProductId() + "_" + oo.getSpId(),
k -> new ArrayList<>())
.add(op);
}
}
}
return operationsByOutputIndex.getOrDefault(productId + "_" + spId, Collections.emptyList());
}
/**
* 获取产出指定产品 (任意库存点) 的所有操作 (索引查询, O(1))。
*/
public List<Operation> getOperationsProducingProduct(String productId) {
if (operationsByProductIndex == null) {
operationsByProductIndex = new HashMap<>();
for (Operation op : operations) {
for (OperationOutput oo : op.getOutputs()) {
operationsByProductIndex
.computeIfAbsent(oo.getProductId(), k -> new ArrayList<>())
.add(op);
}
}
}
return operationsByProductIndex.getOrDefault(productId, Collections.emptyList());
}
/**
* 获取在指定单元上执行的所有操作 (索引查询, O(1))。
*/
public List<Operation> getOperationsByUnitId(String unitId) {
if (operationsByUnitIndex == null) {
operationsByUnitIndex = new HashMap<>();
for (Operation op : operations) {
for (UnitOperation uo : op.getUnitOperations()) {
operationsByUnitIndex
.computeIfAbsent(uo.getUnitId(), k -> new ArrayList<>())
.add(op);
}
}
}
return operationsByUnitIndex.getOrDefault(unitId, Collections.emptyList());
}
/**
* 获取消耗指定产品/库存点的所有 BOM 输入 (索引查询, O(1))。
*/
public List<OperationInput> getOperationInputsFor(String productId, String spId) {
if (operationInputsIndex == null) {
operationInputsIndex = new HashMap<>();
for (OperationInput input : operationInputs) {
operationInputsIndex
.computeIfAbsent(input.getInputProduct().getId() + "_" + input.getInputSp().getId(),
k -> new ArrayList<>())
.add(input);
}
}
return result;
return operationInputsIndex.getOrDefault(productId + "_" + spId, Collections.emptyList());
}
public List<Operation> getOperations() { return operations; }
public List<OperationInput> getOperationInputs() { return operationInputs; }
......@@ -323,42 +404,62 @@ public class TestDataBuilder {
* @return 初始库存量,不存在则返回 0
*/
public double getInitialInventory(String productId, String spId) {
for (InitialInventory inv : initialInventories) {
if (inv.getProduct().getId().equals(productId)
&& inv.getStockingPoint().getId().equals(spId)) {
return inv.getQuantity();
if (initialInventoryIndex == null) {
initialInventoryIndex = new HashMap<>();
for (InitialInventory inv : initialInventories) {
initialInventoryIndex.put(
inv.getProduct().getId() + "_" + inv.getStockingPoint().getId(),
inv.getQuantity());
}
}
return 0.0;
return initialInventoryIndex.getOrDefault(productId + "_" + spId, 0.0);
}
/**
* 获取指定产品/库存点/周期的销售需求
*/
public List<SalesDemand> getSalesDemandsFor(Product product, StockingPoint sp, Period period) {
List<SalesDemand> result = new ArrayList<>();
for (SalesDemand sd : salesDemands) {
if (sd.getProduct().getId().equals(product.getId())
&& sd.getStockingPoint().getId().equals(sp.getId())
&& sd.getPeriod().getIndex() == period.getIndex()) {
result.add(sd);
if (salesDemandsIndex == null) {
salesDemandsIndex = new HashMap<>();
for (SalesDemand sd : salesDemands) {
salesDemandsIndex
.computeIfAbsent(sd.getKey(), k -> new ArrayList<>())
.add(sd);
}
}
return result;
return salesDemandsIndex.getOrDefault(
product.getId() + "_" + sp.getId() + "_" + period.getIndex(),
Collections.emptyList());
}
/**
* 获取指定产品/库存点/周期的库存规格
*/
public InventorySpec getInventorySpecFor(Product product, StockingPoint sp, Period period) {
for (InventorySpec spec : inventorySpecs) {
if (spec.getProduct().getId().equals(product.getId())
&& spec.getStockingPoint().getId().equals(sp.getId())
&& spec.getPeriod().getIndex() == period.getIndex()) {
return spec;
if (inventorySpecIndex == null) {
inventorySpecIndex = new HashMap<>();
for (InventorySpec spec : inventorySpecs) {
inventorySpecIndex.put(spec.getKey(), spec);
}
}
return null;
return inventorySpecIndex.get(
product.getId() + "_" + sp.getId() + "_" + period.getIndex());
}
/**
* 获取指定产品/库存点的所有在途供应 (索引查询, O(1))。
*/
public List<InTransitSupply> getInTransitSuppliesFor(String productId, String spId) {
if (inTransitSuppliesByProductSpIndex == null) {
inTransitSuppliesByProductSpIndex = new HashMap<>();
for (InTransitSupply its : inTransitSupplies) {
inTransitSuppliesByProductSpIndex
.computeIfAbsent(its.getProduct().getId() + "_" + its.getStockingPoint().getId(),
k -> new ArrayList<>())
.add(its);
}
}
return inTransitSuppliesByProductSpIndex.getOrDefault(productId + "_" + spId, Collections.emptyList());
}
/**
......
......@@ -12,7 +12,7 @@ public class UnitOperation {
private final String unitId; // 所属单元ID
private final String unitName; // 所属单元Name
private final double capacityCoeff; // 产能消耗系数 (单件耗时)
private final double capacityCoeff; // 产能消耗系数 (单件工时)
private final boolean hasLotSize; // 是否有批次大小
private final double lotSize; // 批次大小
private final double qtpfactor; // QuantityToProcessFactor
......
......@@ -29,7 +29,7 @@ public class MacroPlannerModel {
private final MPSolver solver;
// ==================== 生产变量 ====================
/** PTQty[operationId_periodIndex] — 生产量, 范围 [0, +∞) */
/** PTQty[operationId_Uintid_periodIndex] — 生产量, 范围 [0, +∞) */
private final Map<String, MPVariable> ptQtyVars = new HashMap<>();
// ==================== 库存变量 ====================
......
package com.aps.macroplanner.output;
import com.aps.common.util.FileHelper;
import com.aps.common.util.FlatParquetUtil;
import com.aps.macroplanner.data.*;
import com.aps.macroplanner.model.MacroPlannerModel;
import com.aps.macroplanner.output.dto.*;
......@@ -91,83 +93,142 @@ public class ResultWriter {
}
return resultDir;
}
private File getResultDirectory(String sceneId) {
File resultDir = new File("mp/result/"+sceneId+"/");
if (!resultDir.exists()) {
boolean created = resultDir.mkdirs();
if (!created) {
logger.warn("无法创建结果目录: {}", resultDir.getAbsolutePath());
throw new RuntimeException("无法创建结果目录: " + resultDir.getAbsolutePath());
}
}
return resultDir;
}
private File getOptimizationFile(String sceneId) {
File resultDir = getResultDirectory();
String fileName = "optimization_result_" + sceneId + getFileExtension();
return new File(resultDir, fileName);
}
// ==================== 主入口 ====================
/**
* 将结果保存到 JSON 文件
*/
public boolean saveResultToFile(String sceneId) {
return saveResultToFile(sceneId, buildResult());
private String getOptimizationPeriodTask(String sceneId) {
File resultDir = getResultDirectory(sceneId);
String fileName =resultDir.getAbsolutePath()+ "\\period_tasks.parquet";
return fileName;
}
private String getOptimizationPispip(String sceneId) {
File resultDir = getResultDirectory(sceneId);
String fileName =resultDir.getAbsolutePath()+ "\\pispips.parquet";
return fileName;
}
private String getOptimizationSalesDemand(String sceneId) {
File resultDir = getResultDirectory(sceneId);
String fileName =resultDir.getAbsolutePath()+ "\\salesdemand.parquet";
return fileName;
}
private String getOptimizationUnitCapacitie(String sceneId) {
File resultDir = getResultDirectory(sceneId);
String fileName =resultDir.getAbsolutePath()+ "\\unitcapacitie.parquet";
return fileName;
}
public boolean saveResultToFile(String sceneId, OptimizationResult result) {
// ==================== 主入口 ====================
public boolean saveResultToFile(String sceneId) {
OptimizationResult result = buildResult();
if (result == null) {
logger.warn("对象不能为空");
return false;
}
if (sceneId == null || sceneId.trim().isEmpty()) {
logger.warn("场景ID不能为空");
return false;
}
return saveResultToFile(sceneId,result);
}
/**
* 将结果保存到 JSON 文件
*/
public boolean saveResultToFile(String sceneId, OptimizationResult result) {
try {
try {
File file = getOptimizationFile(sceneId);
File tempFile = new File(file.getParentFile(), file.getName() + ".tmp");
if (useCompression) {
try (FileOutputStream fos = new FileOutputStream(tempFile);
GZIPOutputStream gzos = new GZIPOutputStream(fos)) {
objectMapper.writeValue(gzos, result);
if (sceneId == null || sceneId.trim().isEmpty()) {
logger.warn("场景ID不能为空");
return false;
}
} else {
objectMapper.writeValue(tempFile, result);
}
if (tempFile.length() == 0) {
logger.error("写入的临时文件为空: {}", tempFile.getAbsolutePath());
Files.deleteIfExists(tempFile.toPath());
return false;
}
if (useCompression) {
try (FileInputStream fis = new FileInputStream(tempFile);
GZIPInputStream gzis = new GZIPInputStream(fis)) {
OptimizationResult verifyChromosome = objectMapper.readValue(gzis, OptimizationResult.class);
if (verifyChromosome == null) {
throw new IOException("验证读取失败");
// 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");
File file = getOptimizationFile(sceneId);
File tempFile = new File(file.getParentFile(), file.getName() + ".tmp");
if (useCompression) {
try (FileOutputStream fos = new FileOutputStream(tempFile);
GZIPOutputStream gzos = new GZIPOutputStream(fos)) {
objectMapper.writeValue(gzos, result);
}
} else {
objectMapper.writeValue(tempFile, result);
}
} else {
try {
OptimizationResult verifyChromosome = objectMapper.readValue(tempFile, OptimizationResult.class);
if (verifyChromosome == null) {
throw new IOException("验证读取失败");
}
} catch (Exception e) {
logger.error("验证染色体文件内容失败,文件: {}", tempFile.getAbsolutePath(), e);
if (tempFile.length() == 0) {
logger.error("写入的临时文件为空: {}", tempFile.getAbsolutePath());
Files.deleteIfExists(tempFile.toPath());
return false;
}
}
if (file.exists()) {
Files.deleteIfExists(file.toPath());
if (useCompression) {
try (FileInputStream fis = new FileInputStream(tempFile);
GZIPInputStream gzis = new GZIPInputStream(fis)) {
OptimizationResult verifyChromosome = objectMapper.readValue(gzis, OptimizationResult.class);
if (verifyChromosome == null) {
throw new IOException("验证读取失败");
}
}
} else {
try {
OptimizationResult verifyChromosome = objectMapper.readValue(tempFile, OptimizationResult.class);
if (verifyChromosome == null) {
throw new IOException("验证读取失败");
}
} catch (Exception e) {
logger.error("验证染色体文件内容失败,文件: {}", tempFile.getAbsolutePath(), e);
Files.deleteIfExists(tempFile.toPath());
return false;
}
}
if (file.exists()) {
Files.deleteIfExists(file.toPath());
}
Files.move(tempFile.toPath(), file.toPath());
logger.info("保存成功,场景ID: {}, 文件: {}", sceneId, file.getAbsolutePath());
return true;
} catch (IOException ex) {
logger.error("保存文件失败,场景ID: " + sceneId, ex);
return false;
}
Files.move(tempFile.toPath(), file.toPath());
logger.info("保存成功,场景ID: {}, 文件: {}", sceneId, file.getAbsolutePath());
return true;
} catch (Exception e) {
logger.error("保存文件失败,场景ID: " + sceneId, e);
return false;
}
}
/**
......@@ -239,7 +300,17 @@ public class ResultWriter {
public String toJsonString() {
return toJson(buildResult());
}
private static final String LOG_DIR = "mp";
/** LP 模型文件路径 */
/** 运行日志文件路径 */
private static final String LOG_FILE_PATH = LOG_DIR + "/log/";
private static void writeLog(String msg)
{
FileHelper.writeFile(msg,LOG_FILE_PATH,"log.txt");
}
// ==================== 构建 OptimizationResult ====================
public OptimizationResult buildResult() {
......@@ -252,17 +323,21 @@ public class ResultWriter {
result.setVersion("1.0.0");
// 业务结果
writeLog("PeriodTasks");
buildPeriodTasks(result);
writeLog("SalesDemands");
buildSalesDemands(result);
writeLog("Pispips");
buildPispips(result);
writeLog("UnitCapacitie");
result.setUnitCapacities(buildUnitCapacities());
result.setProductNetwork(buildProductNetwork());
result.setDemandSummary(buildDemandSummary());
result.setProductNetwork(buildProductNetwork());
result.setDemandSummary(buildDemandSummary());
// KPI + 统计
writeLog("Kpi");
result.setKpis(buildKpis());
result.setStatistics(buildStatistics());
// result.setStatistics(buildStatistics());
return result;
}
......@@ -278,6 +353,8 @@ public class ResultWriter {
if (ptQty< 0.001) continue;
PeriodTaskResult pt = new PeriodTaskResult();
pt.setSnapshotId(key);
pt.setDataKey(key);
pt.setOperationId(op.getId());
pt.setOperationName(op.getName());
pt.setUnitId(uo.getUnitId());
......@@ -293,13 +370,13 @@ public class ResultWriter {
pt.setLotSizeUnder(solutionValue(model.getPtLotSizeUnderVars(), key).setScale(3, RoundingMode.HALF_UP).doubleValue());
// 产出信息
for (OperationOutput oo : op.getOutputs()) {
PeriodTaskResult.OutputInfo oi = new PeriodTaskResult.OutputInfo();
oi.productId = oo.getProductId();
oi.spId = oo.getSpId();
oi.factor = 1.0; // 默认 1:1 产出
pt.getOutputs().add(oi);
}
// for (OperationOutput oo : op.getOutputs()) {
// PeriodTaskResult.OutputInfo oi = new PeriodTaskResult.OutputInfo();
// oi.productId = oo.getProductId();
// oi.spId = oo.getSpId();
// oi.factor = 1.0; // 默认 1:1 产出
// pt.getOutputs().add(oi);
// }
result.getPeriodTasks().add(pt);
}
......@@ -342,7 +419,7 @@ public class ResultWriter {
pe.periodStartDate = up.getPeriod().getStartDate().toString();
double capacityUsed = 0;
for (Operation op : data.getOperations()) {
for (Operation op : data.getOperationsByUnitId(uid)) {
for (UnitOperation uo : op.getUnitOperations()) {
if (!uo.getUnitId().equals(uid)) continue;
String ptKey = op.ptQtyKey(uo, up.getPeriod().getIndex());
......@@ -439,10 +516,10 @@ public class ResultWriter {
sr.setDemandSlack(solutionValue(model.getDemandSlackVars(), invKey).setScale(3, RoundingMode.HALF_UP).doubleValue());
// 未完成原因分析
if (unmet > 0.001) {
analyzeUnmetReasons(sd, sr.getUnmetReasons());
// analyzeUnmetReasons(sd, sr.getUnmetReasons());
}
// 风险分析
analyzeRisks(sd, sr.getRisks());
// analyzeRisks(sd, sr.getRisks());
result.getSalesDemands().add(sr);
}
......@@ -471,8 +548,7 @@ public class ResultWriter {
: solutionValue(model.getInvQtyVars(),
prod.getId() + "_" + sp.getId() + "_" + (p.getIndex() - 1)).setScale(3, RoundingMode.HALF_UP).doubleValue();;
BigDecimal totalArrived = BigDecimal.ZERO;
for (Operation op : data.getOperations()) {
if (!op.producesProductAtSp(prod.getId(), sp.getId())) continue;
for (Operation op : data.getOperationsProducing(prod.getId(), sp.getId())) {
for (UnitOperation uo : op.getUnitOperations()) {
Period src = data.getPeriodOffsetByDays(p, op.getLeadTimeDays());
if (src != null) {
......@@ -489,14 +565,13 @@ public class ResultWriter {
// 2. 产能瓶颈: 所有生产该产品的单元满负荷 (利用率 > 95%)
boolean hasAnyUnit = false;
boolean allUnitsAtMax = true;
for (Operation op : data.getOperations()) {
if (!op.producesProductAtSp(prod.getId(), sp.getId())) continue;
for (Operation op : data.getOperationsProducing(prod.getId(), sp.getId())) {
for (UnitOperation uo : op.getUnitOperations()) {
hasAnyUnit = true;
UnitPeriod up = data.getUnitPeriod(uo.getUnitId(), p);
if (up == null || up.isUnlimited()) continue;
BigDecimal totalUsed = BigDecimal.ZERO;
for (Operation unitOp : data.getOperations()) {
for (Operation unitOp : data.getOperationsByUnitId(uo.getUnitId())) {
for (UnitOperation unitUo : unitOp.getUnitOperations()) {
if (!unitUo.getUnitId().equals(uo.getUnitId())) continue;
totalUsed =totalUsed.add( solutionValue(model.getPtQtyVars(),
......@@ -514,8 +589,7 @@ public class ResultWriter {
// 3. 原材料短缺: BOM 输入物料上期库存见底
if (p.getIndex() > 0) {
for (Operation op : data.getOperations()) {
if (!op.producesProduct(prod.getId())) continue;
for (Operation op : data.getOperationsProducingProduct(prod.getId())) {
for (OperationInput input : data.getOperationInputs()) {
if (!input.getOperation().getId().equals(op.getId())) continue;
Product inputProd = input.getInputProduct();
......@@ -534,8 +608,7 @@ public class ResultWriter {
// 4. 提前期限制: 第1周期生产需 leadTime 天后到货
if (p.getIndex() == 0) {
for (Operation op : data.getOperations()) {
if (!op.producesProduct(prod.getId())) continue;
for (Operation op : data.getOperationsProducingProduct(prod.getId())) {
if (op.getLeadTimeDays() <= 0) continue;
BigDecimal totalProduced = BigDecimal.ZERO;
for (UnitOperation uo : op.getUnitOperations()) {
......@@ -549,10 +622,7 @@ public class ResultWriter {
}
// 5. 无生产工序
boolean hasOperation = false;
for (Operation op : data.getOperations()) {
if (op.producesProduct(prod.getId())) { hasOperation = true; break; }
}
boolean hasOperation = !data.getOperationsProducingProduct(prod.getId()).isEmpty();
if (!hasOperation) {
reasons.add("无生产工序(纯采购品, 依赖库存/在途)");
}
......@@ -593,13 +663,12 @@ public class ResultWriter {
}
// 2. 产线高负荷 (> 90%)
for (Operation op : data.getOperations()) {
if (!op.producesProductAtSp(prod.getId(), sp.getId())) continue;
for (Operation op : data.getOperationsProducing(prod.getId(), sp.getId())) {
for (UnitOperation uo : op.getUnitOperations()) {
UnitPeriod up = data.getUnitPeriod(uo.getUnitId(), p);
if (up == null || up.isUnlimited()) continue;
BigDecimal totalUsed = BigDecimal.ZERO;
for (Operation unitOp : data.getOperations()) {
for (Operation unitOp : data.getOperationsByUnitId(uo.getUnitId())) {
for (UnitOperation unitUo : unitOp.getUnitOperations()) {
if (!unitUo.getUnitId().equals(uo.getUnitId())) continue;
totalUsed =totalUsed.add( solutionValue(model.getPtQtyVars(),
......@@ -619,8 +688,7 @@ public class ResultWriter {
// 3. 单一供应源: 该产品仅一个可生产单元
int unitCount = 0;
for (Operation op : data.getOperations()) {
if (!op.producesProduct(prod.getId())) continue;
for (Operation op : data.getOperationsProducingProduct(prod.getId())) {
unitCount += op.getUnitOperations().size();
}
if (unitCount == 1) {
......@@ -628,13 +696,11 @@ public class ResultWriter {
}
// 4. 原材料提前期长: 关键 BOM 物料 leadTime 长
for (Operation op : data.getOperations()) {
if (!op.producesProduct(prod.getId())) continue;
for (Operation op : data.getOperationsProducingProduct(prod.getId())) {
for (OperationInput input : data.getOperationInputs()) {
if (!input.getOperation().getId().equals(op.getId())) continue;
Product inputProd = input.getInputProduct();
for (Operation upOp : data.getOperations()) {
if (!upOp.producesProduct(inputProd.getId())) continue;
for (Operation upOp : data.getOperationsProducingProduct(inputProd.getId())) {
if (upOp.getLeadTimeDays() >= 2) {
addRisk(risks, "低", "原材料提前期长(" + inputProd.getId() + "采购需"
+ upOp.getLeadTimeDays() + "天)");
......@@ -786,8 +852,7 @@ public class ResultWriter {
BigDecimal totalArrived = BigDecimal.ZERO;
double totalInProgress = 0;
for (Operation op : data.getOperations()) {
if (!op.producesProductAtSp(prod.getId(), sp.getId())) continue;
for (Operation op : data.getOperationsProducing(prod.getId(), sp.getId())) {
int leadTime = op.getLeadTimeDays();
Period arrivalSrcPeriod = data.getPeriodOffsetByDays(p, leadTime);
......@@ -806,34 +871,34 @@ public class ResultWriter {
// 生产中
String ptKeyNow = op.ptQtyKey(uo, p.getIndex());
double ptQtyNow = solutionValue(model.getPtQtyVars(), ptKeyNow).setScale(3, RoundingMode.HALF_UP).doubleValue();
if (ptQtyNow > 0.001) {
totalInProgress += ptQtyNow;
// 生产明细
PispipResult.ProductionDetail pd = new PispipResult.ProductionDetail();
pd.operationId = op.getId();
pd.operationName = op.getName();
pd.unitId = uo.getUnitId();
pd.unitName = uo.getUnitName();
pd.quantity = ptQtyNow;
pd.leadTimeDays = leadTime;
// BOM 消耗
for (OperationInput input : data.getOperationInputs()) {
if (!input.getOperation().getId().equals(op.getId())) continue;
PispipResult.BomConsumption bc = new PispipResult.BomConsumption();
bc.inputProductId = input.getInputProduct().getId();
bc.inputSpId = input.getInputSp().getId();
bc.inputSpName= input.getInputSp().getName();
bc.factor = input.getFactor();
String opDemandKey = input.getKey() + "_" + p.getIndex();
bc.consumedQty = solutionValue(
model.getOperationDemandQtyVars(), opDemandKey).setScale(3, RoundingMode.HALF_UP).doubleValue();
pd.bomConsumptions.add(bc);
}
pr.getProductionDetails().add(pd);
}
//if (ptQtyNow > 0.001) {
totalInProgress += ptQtyNow;
// 生产明细
// PispipResult.ProductionDetail pd = new PispipResult.ProductionDetail();
// pd.operationId = op.getId();
// pd.operationName = op.getName();
// pd.unitId = uo.getUnitId();
// pd.unitName = uo.getUnitName();
// pd.quantity = ptQtyNow;
// pd.leadTimeDays = leadTime;
// BOM 消耗
// for (OperationInput input : data.getOperationInputs()) {
// if (!input.getOperation().getId().equals(op.getId())) continue;
// PispipResult.BomConsumption bc = new PispipResult.BomConsumption();
// bc.inputProductId = input.getInputProduct().getId();
// bc.inputSpId = input.getInputSp().getId();
// bc.inputSpName= input.getInputSp().getName();
// bc.factor = input.getFactor();
// String opDemandKey = input.getKey() + "_" + p.getIndex();
// bc.consumedQty = solutionValue(
// model.getOperationDemandQtyVars(), opDemandKey).setScale(3, RoundingMode.HALF_UP).doubleValue();
// pd.bomConsumptions.add(bc);
// }
// pr.getProductionDetails().add(pd);
//}
}
}
pr.setProductionArrived(totalArrived.doubleValue());
......@@ -841,10 +906,8 @@ public class ResultWriter {
// 在途到货
double inTransit = 0;
for (InTransitSupply its : data.getInTransitSupplies()) {
if (its.getProduct().getId().equals(prod.getId())
&& its.getStockingPoint().getId().equals(sp.getId())
&& p.equals(data.getPeriodByDate(its.getArrivalDate()))) {
for (InTransitSupply its : data.getInTransitSuppliesFor(prod.getId(), sp.getId())) {
if (p.equals(data.getPeriodByDate(its.getArrivalDate()))) {
inTransit += its.getQuantity();
}
}
......@@ -883,10 +946,7 @@ public class ResultWriter {
// Step 2: 判断每个产品是否有生产工序
java.util.Map<String, Boolean> hasProduction = new java.util.LinkedHashMap<>();
for (Product prod : data.getProducts()) {
boolean found = false;
for (Operation op : data.getOperations()) {
if (op.producesProduct(prod.getId())) { found = true; break; }
}
boolean found = !data.getOperationsProducingProduct(prod.getId()).isEmpty();
hasProduction.put(prod.getId(), found);
}
......@@ -929,8 +989,7 @@ public class ResultWriter {
// --- 供应源: 哪些工序生产这个产品@库存点 ---
java.util.Set<String> visitedSupplySources = new java.util.HashSet<>();
for (Operation op : data.getOperations()) {
if (!op.producesProductAtSp(prodId, spId)) continue;
for (Operation op : data.getOperationsProducing(prodId, spId)) {
String srcKey = op.getId() + "_" + spId;
if (!visitedSupplySources.add(srcKey)) continue; // 防重复
......@@ -965,14 +1024,11 @@ public class ResultWriter {
}
// 在途供应
for (InTransitSupply its : data.getInTransitSupplies()) {
if (its.getProduct().getId().equals(prodId)
&& its.getStockingPoint().getId().equals(spId)) {
SupplyChainNode.SupplySource src = new SupplyChainNode.SupplySource();
src.type = "IN_TRANSIT";
src.totalProduction = its.getQuantity();
node.getSupplySources().add(src);
}
for (InTransitSupply its : data.getInTransitSuppliesFor(prodId, spId)) {
SupplyChainNode.SupplySource src = new SupplyChainNode.SupplySource();
src.type = "IN_TRANSIT";
src.totalProduction = its.getQuantity();
node.getSupplySources().add(src);
}
// 外部采购 (无生产工序)
......@@ -1022,8 +1078,7 @@ public class ResultWriter {
// --- BOM 子物料: 这个产品的生产消耗什么 ---
java.util.Set<String> expandedChildren = new java.util.HashSet<>();
next_child:
for (Operation op : data.getOperations()) {
if (!op.producesProductAtSp(prodId, spId)) continue;
for (Operation op : data.getOperationsProducing(prodId, spId)) {
for (OperationInput input : data.getOperationInputs()) {
if (!input.getOperation().getId().equals(op.getId())) continue;
......@@ -1050,7 +1105,7 @@ public class ResultWriter {
child.spName = input.getInputSp().getName();
child.operationId = op.getId();
child.operationName = op.getName();
// child.unitId = op.getUnitId();
// child.unitId = op.getUnitId();
child.totalFactor = input.getFactor();
child.totalConsumedQty = totalConsumed.setScale(3, RoundingMode.HALF_UP).doubleValue();
node.getChildren().add(child);
......@@ -1092,8 +1147,7 @@ public class ResultWriter {
// 生产汇总
BigDecimal totalProd = BigDecimal.ZERO;
BigDecimal totalArrived = BigDecimal.ZERO;
for (Operation op : data.getOperations()) {
if (!op.producesProductAtSp(prodId, spId)) continue;
for (Operation op : data.getOperationsProducing(prodId, spId)) {
for (Period p : data.getPeriods()) {
for (UnitOperation uo : op.getUnitOperations()) {
String ptKey = op.ptQtyKey(uo, p.getIndex());
......@@ -1117,11 +1171,8 @@ public class ResultWriter {
// 在途汇总
double totalInTransit = 0;
for (InTransitSupply its : data.getInTransitSupplies()) {
if (its.getProduct().getId().equals(prodId)
&& its.getStockingPoint().getId().equals(spId)) {
totalInTransit += its.getQuantity();
}
for (InTransitSupply its : data.getInTransitSuppliesFor(prodId, spId)) {
totalInTransit += its.getQuantity();
}
s.setTotalInTransit(totalInTransit);
......@@ -1283,13 +1334,13 @@ public class ResultWriter {
jb.key("lotSizeOver").val(pt.getLotSizeOver());
jb.key("lotSizeUnder").val(pt.getLotSizeUnder());
jb.key("outputs").arr();
for (PeriodTaskResult.OutputInfo oi : pt.getOutputs()) {
jb.obj()
.key("productId").val(oi.productId)
.key("spId").val(oi.spId)
.key("factor").val(oi.factor)
.endObj();
}
// for (PeriodTaskResult.OutputInfo oi : pt.getOutputs()) {
// jb.obj()
// .key("productId").val(oi.productId)
// .key("spId").val(oi.spId)
// .key("factor").val(oi.factor)
// .endObj();
// }
jb.endArr();
jb.endObj();
}
......
......@@ -58,7 +58,7 @@ public class SolutionPrinter {
// printBomStructure();
// printHeader();
// printDailyViews();
// printKpiSummary();
// printKpiSummary();
printStatistics();
}
......@@ -318,10 +318,8 @@ public class SolutionPrinter {
// 在途到货
double inTransitQty = 0;
for (InTransitSupply its : data.getInTransitSupplies()) {
if (its.getProduct().getId().equals(prod.getId())
&& its.getStockingPoint().getId().equals(sp.getId())
&& p.equals(data.getPeriodByDate(its.getArrivalDate()))) {
for (InTransitSupply its : data.getInTransitSuppliesFor(prod.getId(), sp.getId())) {
if (p.equals(data.getPeriodByDate(its.getArrivalDate()))) {
inTransitQty += its.getQuantity();
}
}
......@@ -540,10 +538,8 @@ public class SolutionPrinter {
// 在途到货
double inTransit = 0;
for (InTransitSupply its : data.getInTransitSupplies()) {
if (its.getProduct().getId().equals(inputProd.getId())
&& its.getStockingPoint().getId().equals(inputSp.getId())
&& p.equals(data.getPeriodByDate(its.getArrivalDate()))) {
for (InTransitSupply its : data.getInTransitSuppliesFor(inputProd.getId(), inputSp.getId())) {
if (p.equals(data.getPeriodByDate(its.getArrivalDate()))) {
inTransit += its.getQuantity();
}
}
......
package com.aps.macroplanner.output.dto;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
......@@ -8,8 +10,10 @@ import java.util.List;
*
* <p>每个操作在每个周期的生产量、产能消耗、批次偏差等。</p>
*/
@Data
public class PeriodTaskResult {
private String snapshotId;
private String dataKey;
private String operationId;
private String operationName;
private String unitId;
......@@ -18,9 +22,9 @@ public class PeriodTaskResult {
/** PTQty — 生产量 */
private double productionQty;
/** 产能消耗 = productionQty × coefficient */
/** 产能消耗 总工时= productionQty × coefficient */
private double capacityUsed;
/** 产能消耗系数 */
/** 单件工时 */
private double capacityCoeff;
/** 批次大小 (如有) */
......@@ -31,51 +35,16 @@ public class PeriodTaskResult {
private double lotSizeUnder;
/** 产出: 产品 → 库存点 */
private final List<OutputInfo> outputs = new ArrayList<>();
// private final List<OutputInfo> outputs = new ArrayList<>();
// ==================== 内嵌类 ====================
/** 产出信息 */
public static class OutputInfo {
public String productId;
public String spId;
public double factor;
}
// ==================== Getters / Setters ====================
public String getOperationId() { return operationId; }
public void setOperationId(String v) { this.operationId = v; }
public String getOperationName() { return operationName; }
public void setOperationName(String v) { this.operationName = v; }
public String getUnitId() { return unitId; }
public void setUnitId(String v) { this.unitId = v; }
public int getPeriodIndex() { return periodIndex; }
public void setPeriodIndex(int v) { this.periodIndex = v; }
public String getPeriodStartDate() { return periodStartDate; }
public void setPeriodStartDate(String v) { this.periodStartDate = v; }
public double getProductionQty() { return productionQty; }
public void setProductionQty(double v) { this.productionQty = v; }
public double getCapacityUsed() { return capacityUsed; }
public void setCapacityUsed(double v) { this.capacityUsed = v; }
public double getCapacityCoeff() { return capacityCoeff; }
public void setCapacityCoeff(double v) { this.capacityCoeff = v; }
public Double getLotSize() { return lotSize; }
public void setLotSize(Double v) { this.lotSize = v; }
public double getLotSizeOver() { return lotSizeOver; }
public void setLotSizeOver(double v) { this.lotSizeOver = v; }
// public static class OutputInfo {
// public String productId;
// public String spId;
// public double factor;
// }
public double getLotSizeUnder() { return lotSizeUnder; }
public void setLotSizeUnder(double v) { this.lotSizeUnder = v; }
public List<OutputInfo> getOutputs() { return outputs; }
}
\ No newline at end of file
package com.aps.macroplanner.output.dto;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
......@@ -8,6 +10,7 @@ import java.util.List;
*
* <p>每个产品在每个库存点每个周期的库存状态、流入流出、规格偏差。</p>
*/
@Data
public class PispipResult {
private String productId;
......
......@@ -95,6 +95,13 @@
<logger name="org.mybatis" level="INFO"/>
<logger name="com.zaxxer.hikari" level="INFO"/>
<!-- 关闭 Parquet 内部 DEBUG 刷屏日志 -->
<logger name="org.apache.parquet" level="ERROR" additivity="false">
<appender-ref ref="CONSOLE"/> <!-- 替换为你项目实际的控制台/文件 appender 名称 -->
</logger>
<!-- 屏蔽 Hadoop Shell 的冗余警告 -->
<logger name="org.apache.hadoop.util.Shell" level="ERROR" additivity="false"/>
<!-- 根据不同环境调整 -->
<springProfile name="dev">
<root level="INFO">
......
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