Commit 391de2d1 authored by Tong Li's avatar Tong Li

MP

parent 48800682
...@@ -57,22 +57,31 @@ public class FileHelper { ...@@ -57,22 +57,31 @@ public class FileHelper {
} }
} }
public static void writeFile(String message,String fileName) { public static void writeFile(String message,String fileDir,String fileName) {
String date = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"))+"-"; String date = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"))+"-";
// 确保目录存在 // 确保目录存在
java.io.File logDir = new java.io.File(LOG_FILE_PATH); java.io.File logDir = new java.io.File(fileDir);
if (!logDir.exists()) { if (!logDir.exists()) {
logDir.mkdirs(); // 创建目录(包括父目录) logDir.mkdirs(); // 创建目录(包括父目录)
} }
String filePath = LOG_FILE_PATH + date + fileName; String filePath = fileDir + date + fileName;
try (PrintWriter writer = new PrintWriter(new FileWriter(filePath, true))) { try (PrintWriter writer = new PrintWriter(new FileWriter(filePath, true))) {
writer.print(message);
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"));
writer.println("[" + timestamp + "] " + message);
System.out.println("[" + timestamp + "] " + message);
} catch (IOException e) { } catch (IOException e) {
System.err.println("Failed to write log: " + e.getMessage()); System.err.println("Failed to write log: " + e.getMessage());
} }
} }
public static void writeFile(String message,String fileName) {
writeFile(message,LOG_FILE_PATH,fileName);
}
} }
\ No newline at end of file
package com.aps.macroplanner;
import com.google.ortools.Loader;
import com.aps.macroplanner.data.CoProductTestDataBuilder;
import com.aps.macroplanner.data.TestDataBuilder;
/**
* 联产品/副产品多工序路由测试运行器。
*
* <p>验证场景: 化工反应 2 步工艺路线, 反应工序同时产出主产品 P 和副产品 ByP,
* 验证:
* <ol>
* <li>OP_Reaction 有 2 个产出: P@WIP_R_P_1 + ByP@SP_ByProduct</li>
* <li>RoutingConstraint 确保 PTQty[Reaction] = PTQty[Purify]</li>
* <li>ByP 产量 = PTQty[Reaction], 与主产品 P 同步</li>
* <li>P@SP_FG 物料平衡: 流入 = 流出</li>
* <li>ByP@SP_ByProduct 物料平衡: 联产品流入 = 销售 + 库存</li>
* <li>P@WIP_R_P_1 物料平衡: 流入 = 消耗</li>
* </ol>
*/
public class CoProductTestRunner {
public static void main(String[] args) {
Loader.loadNativeLibraries();
System.out.println("===== 联产品/副产品 多工序路由测试 =====\n");
System.out.println("场景: 化工反应 2 步工艺路线");
System.out.println(" 工序1: OP_Reaction(反应) → 产出 P@WIP + ByP@SP_ByProduct(联产品)");
System.out.println(" 工序2: OP_Purify(提纯) → 消耗 P@WIP, 产出 P@SP_FG");
System.out.println(" 销售: P@SP_FG=50/天, ByP@SP_ByProduct=30/天\n");
TestDataBuilder data = new CoProductTestDataBuilder();
System.out.println("数据加载: " + data.getProducts().size() + " 产品, "
+ data.getOperations().size() + " 工序, "
+ data.getRoutings().size() + " 工艺路线\n");
MacroPlannerOptimizer optimizer = new MacroPlannerOptimizer(data);
optimizer.buildModel();
optimizer.solve();
System.out.println("\n===== 联产品/副产品 多工序路由测试 结束 =====");
}
}
\ No newline at end of file
package com.aps.macroplanner;
import com.aps.ApsApplication;
import com.aps.macroplanner.data.DataValidator;
import com.aps.macroplanner.data.MacroPlannerDataConverter;
import com.aps.macroplanner.data.TestDataBuilder;
import com.google.ortools.Loader;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ApplicationContext;
/**
* 数据库工艺物料类 → macroplanner 转换器测试运行器。
*
* <p>通过 Spring 启动获取 {@link MacroPlannerDataConverter} Bean,
* 从数据库加载指定场景的工艺物料数据并转换为 {@link TestDataBuilder},
* 然后用 {@link DataValidator} 验证 + {@link MacroPlannerOptimizer} 求解。</p>
*
* <h3>验证流程</h3>
* <ol>
* <li>调用 converter.convert(sceneId) 从数据库转换数据</li>
* <li>打印转换后的实体规模 (products/operations/routings/...)</li>
* <li>DataValidator 验证数据完整性 (引用完整性、非负数等)</li>
* <li>MacroPlannerOptimizer 构建模型并求解</li>
* </ol>
*
* <p>用法: {@code java MacroPlannerDataConverterRunner <sceneId>}</p>
*/
public class MacroPlannerDataConverterRunner {
public static void main(String[] args) {
String sceneId = (args.length > 0) ? args[0] : "B288477F1A594DB584C87EEA77880AA3";
System.out.println("===== MACROPLANNER DATA CONVERTER RUNNER START =====");
System.out.println("SceneId: " + sceneId);
ApplicationContext ctx = SpringApplication.run(ApsApplication.class, args);
try {
MacroPlannerDataConverter converter = ctx.getBean(MacroPlannerDataConverter.class);
// 1. 转换数据: 数据库工艺物料类 → macroplanner 实体
TestDataBuilder data = converter.convert(sceneId);
// 2. 打印转换结果规模
System.out.println("Data converted:");
System.out.println(" Products: " + data.getProducts().size());
System.out.println(" StockingPoints: " + data.getStockingPoints().size());
System.out.println(" Operations: " + data.getOperations().size());
System.out.println(" Routings: " + data.getRoutings().size());
System.out.println(" OperationInputs: " + data.getOperationInputs().size());
System.out.println(" InitialInventories: " + data.getInitialInventories().size());
System.out.println(" InTransitSupplies: " + data.getInTransitSupplies().size());
System.out.println(" SalesDemands: " + data.getSalesDemands().size());
System.out.println(" Periods: " + data.getPeriods().size());
System.out.println(" UnitPeriods: " + data.getUnitPeriods().size());
// 3. DataValidator 验证数据完整性
DataValidator validator = new DataValidator(data);
boolean valid = validator.validate();
if (validator.hasErrors()) {
System.err.println("DataValidator ERRORS (" + validator.getErrors().size() + "):");
validator.getErrors().forEach(e -> System.err.println(" [ERROR] " + e));
}
if (validator.hasWarnings()) {
System.out.println("DataValidator WARNINGS (" + validator.getWarnings().size() + "):");
validator.getWarnings().forEach(w -> System.out.println(" [WARN] " + w));
}
System.out.println("DataValidator: valid=" + valid);
if (!valid) {
System.err.println("数据验证失败, 跳过求解。请检查上述 ERROR。");
return;
}
// 4. 喂给优化器求解
Loader.loadNativeLibraries();
MacroPlannerOptimizer optimizer = new MacroPlannerOptimizer(data);
optimizer.buildModel();
optimizer.solve();
System.out.println("===== MACROPLANNER DATA CONVERTER RUNNER END =====");
} finally {
SpringApplication.exit(ctx);
}
}
}
...@@ -8,6 +8,7 @@ import com.aps.macroplanner.model.MacroPlannerModel; ...@@ -8,6 +8,7 @@ import com.aps.macroplanner.model.MacroPlannerModel;
import com.aps.macroplanner.model.VariableFactory; import com.aps.macroplanner.model.VariableFactory;
import com.aps.macroplanner.objective.ObjectiveBuilder; import com.aps.macroplanner.objective.ObjectiveBuilder;
import com.aps.macroplanner.objective.StrategyLevel; import com.aps.macroplanner.objective.StrategyLevel;
import com.aps.macroplanner.output.ResultWriter;
import com.aps.macroplanner.output.SolutionPrinter; import com.aps.macroplanner.output.SolutionPrinter;
import java.io.FileOutputStream; import java.io.FileOutputStream;
...@@ -82,14 +83,18 @@ import java.util.logging.Logger; ...@@ -82,14 +83,18 @@ import java.util.logging.Logger;
public class MacroPlannerOptimizer { public class MacroPlannerOptimizer {
/** LP 模型文件和日志文件的输出目录 */ /** LP 模型文件和日志文件的输出目录 */
private static final String LOG_DIR = "src/main/java/com/aps/log/"; private static final String LOG_DIR = "mp";
/** LP 模型文件路径 */ /** LP 模型文件路径 */
private static final String LP_FILE_PATH = LOG_DIR + "model.lp"; private static final String LP_FILE_PATH = LOG_DIR + "/lp/model.lp";
/** 运行日志文件路径 */ /** 运行日志文件路径 */
private static final String LOG_FILE_PATH = LOG_DIR + "log.txt"; private static final String LOG_FILE_PATH = LOG_DIR + "/log/log.txt";
/** 获取模型容器 (供 ResultWriter 等外部组件使用)。 */
public MacroPlannerModel getModel() { return model; }
/** 获取输入数据 (供 ResultWriter 等外部组件使用)。 */
public TestDataBuilder getData() { return data; }
// ==================== 核心组件 ==================== // ==================== 核心组件 ====================
/** 模型容器 — 持有求解器、所有决策变量和 KPI 汇总变量 */ /** 模型容器 — 持有求解器、所有决策变量和 KPI 汇总变量 */
...@@ -313,6 +318,13 @@ public class MacroPlannerOptimizer { ...@@ -313,6 +318,13 @@ public class MacroPlannerOptimizer {
levelObjValues.add(r.optimalValue); levelObjValues.add(r.optimalValue);
} }
printer.printHierarchicalSummary(levels, levelObjValues); printer.printHierarchicalSummary(levels, levelObjValues);
// 回写业务对象到 JSON 文件
ResultWriter rw = new ResultWriter(model, data, startTimeMs);
boolean jsonPath = rw.saveResultToFile("1");
if (jsonPath) {
System.out.println("\n[OK] 优化结果JSON已导出: " + jsonPath);
}
} else { } else {
System.out.println("求解失败! 状态: " + finalStatus); System.out.println("求解失败! 状态: " + finalStatus);
} }
......
package com.aps.macroplanner;
import com.google.ortools.Loader;
import com.aps.macroplanner.data.MultiLevelBomTestDataBuilder;
import com.aps.macroplanner.data.TestDataBuilder;
/**
* 多级BOM + 多成品 + 共享半成品 测试运行器。
*
* <p>验证场景:
* <pre>
* P1 ──消耗──→ S1×2.0 + R1×3.0
* P2 ──消耗──→ S1×1.0
* S1 ──消耗──→ R2×2.0
* </pre>
*
* <p>关键验证:
* <ol>
* <li>多成品 P1(40/天) + P2(30/天) 需求同时满足</li>
* <li>共享半成品 S1 总产量 = P1×2.0 + P2×1.0</li>
* <li>多级 BOM 展开: R2 消耗 = S1×2.0, R1 消耗 = P1×3.0</li>
* <li>需求缺口 = 0</li>
* </ol>
*/
public class MultiLevelBomTestRunner {
public static void main(String[] args) {
Loader.loadNativeLibraries();
System.out.println("===== MULTI-LEVEL BOM TEST RUNNER START =====");
System.out.println("BOM: P1→S1×2+R1×3, P2→S1×1, S1→R2×2");
System.out.println("Demand: P1=40/day, P2=30/day");
System.out.println();
TestDataBuilder data = new MultiLevelBomTestDataBuilder();
System.out.println("Data loaded: " + data.getProducts().size() + " products, "
+ data.getOperations().size() + " operations");
MacroPlannerOptimizer optimizer = new MacroPlannerOptimizer(data);
optimizer.buildModel();
optimizer.solve();
System.out.println("===== MULTI-LEVEL BOM TEST RUNNER END =====");
}
}
\ No newline at end of file
package com.aps.macroplanner.constraint;
import com.google.ortools.linearsolver.MPConstraint;
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;
/**
* 工序产量一致性约束 (RoutingConstraint)
*
* <p>对于工艺路线中的每对相邻工序,强制每周期产量相等:
* <pre>
* PTQty[op_i][t] = PTQty[op_{i+1}][t] ∀ 相邻工序对, ∀ 周期 t
* </pre>
*
* <h3>设计动机</h3>
* 在多工序工艺路线 (Routing) 中,物料按序流经各道工序。
* 如果仅靠 BOM 约束 (消耗 ≤ 产出),求解器可能让上游工序
* 过量生产,导致中间 WIP 库存异常堆积,产生难以解释的结果。
*
* <p>此约束强制所有工序保持相同节拍,确保:
* <ul>
* <li>工序间 WIP 不异常累积</li>
* <li>排产结果直观可解释</li>
* <li>各工序产量一致,反映真实工艺路线约束</li>
* </ul>
*
* <h3>对应 Quintiq 模型</h3>
* Quintiq 中 RoutingStep 之间通过 PISPNodeInRouting 流转,
* 配合 LeadTime 自然形成工序间产量一致性。
*
* <h3>约束位置</h3>
* 在 BOM 约束之后、产能约束之前构建,确保工序间产量一致
* 后再施加产能限制。
*/
public class RoutingConstraint {
/**
* 构建工序产量一致性约束。
*
* @param model 模型容器 (提供 PTQty 变量)
* @param data 测试数据 (提供 Routing 列表)
*/
public static void build(MacroPlannerModel model, TestDataBuilder data) {
List<Routing> routings = data.getRoutings();
if (routings.isEmpty()) {
return; // 无工艺路线, 无需构建
}
Map<String, MPVariable> ptQtyVars = model.getPtQtyVars();
List<Period> periods = data.getPeriods();
for (Routing routing : routings) {
List<Operation> ops = routing.getOperations();
if (ops.size() < 2) {
continue; // 单步路由无需工序间约束
}
// 对每对相邻工序 (i, i+1), 强制每周期产量相等
// Σ PTQty[opCurrent][allUnits][t] - Σ PTQty[opNext][allUnits][t] = 0
for (int i = 0; i < ops.size() - 1; i++) {
Operation opCurrent = ops.get(i);
Operation opNext = ops.get(i + 1);
for (Period p : periods) {
MPConstraint con = model.getSolver().makeConstraint(0.0, 0.0,
"RoutingThru_" + routing.getId() + "_" + i + "_" + p.getIndex());
for (UnitOperation uoCur : opCurrent.getUnitOperations()) {
MPVariable ptCur = ptQtyVars.get(opCurrent.ptQtyKey(uoCur, p.getIndex()));
if (ptCur != null) con.setCoefficient(ptCur, 1.0);
}
for (UnitOperation uoNext : opNext.getUnitOperations()) {
MPVariable ptNext = ptQtyVars.get(opNext.ptQtyKey(uoNext, p.getIndex()));
if (ptNext != null) con.setCoefficient(ptNext, -1.0);
}
}
}
}
}
}
\ No newline at end of file
package com.aps.macroplanner.data;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.Collections;
/**
* 联产品/副产品多工序路由测试数据构建器。
*
* <h3>测试场景: 化工反应工序同时产出主产品 P 和副产品 ByP</h3>
* <pre>
* 原材料 RM ─→ OP_Reaction(反应) ─→ P@WIP_R_P_1 ─→ OP_Purify(提纯) ─→ P@SP_FG(成品)
* │ 消耗 RM@SP_RM×2.0
* │
* └─→ ByP@SP_ByProduct (副产品, 联产品产出)
*
* 工艺路线 R_P (P主生产工艺) 生产产品 P, 最终入库 SP_FG:
* 工序1: OP_Reaction(反应) → Unit_Reactor → 消耗 RM@SP_RM×2.0
* └ 联产品产出: ByP@SP_ByProduct (预配置, expand() 保留)
* └ WIP 产出: P@WIP_R_P_1 (expand() 自动生成)
* 工序2: OP_Purify(提纯) → Unit_Purify → 消耗 P@WIP_R_P_1, 产出 P@SP_FG
* </pre>
*
* <h3>关键验证点</h3>
* <ol>
* <li>OP_Reaction 有 2 个产出: P@WIP_R_P_1 (路由自动) + ByP@SP_ByProduct (联产品)</li>
* <li>RoutingConstraint 确保 PTQty[Reaction] = PTQty[Purify]</li>
* <li>ByP 的产出量 = PTQty[Reaction], 与主产品 P 同步</li>
* <li>P@SP_FG 物料平衡: 流入 (OP_Purify) = 流出 (Sales)</li>
* <li>ByP@SP_ByProduct 物料平衡: 流入 (OP_Reaction 联产品) = 流出 (Sales) + 期末库存</li>
* <li>P@WIP_R_P_1 物料平衡: 流入 (OP_Reaction) = 流出 (OP_Purify 消耗)</li>
* </ol>
*/
public class CoProductTestDataBuilder extends TestDataBuilder {
// 保存关键对象引用, 用于验证
private Product prodP, prodByP, prodRM;
private StockingPoint spFG, spByProduct, spRM;
private Operation opReaction, opPurify, opProcureRM;
private Routing routingP;
public CoProductTestDataBuilder() {
super(true); // 跳过父类默认 build
build();
}
@Override
protected void build() {
LocalDate baseDate = LocalDate.of(2026, 1, 5);
// === 3 个周期 ===
Period p1 = new Period(0, "P1", baseDate);
Period p2 = new Period(1, "P2", baseDate.plusDays(1));
Period p3 = new Period(2, "P3", baseDate.plusDays(2));
periods.addAll(Arrays.asList(p1, p2, p3));
// === 产品: 主产品 P + 副产品 ByP + 原材料 RM ===
prodP = new Product("P", "主产品");
prodByP = new Product("ByP", "副产品");
prodRM = new Product("RM", "原材料");
products.addAll(Arrays.asList(prodP, prodByP, prodRM));
// === 库存点: 成品库 + 副产品库 + 原材料库 ===
spFG = new StockingPoint("SP_FG", "成品库");
spByProduct = new StockingPoint("SP_ByProduct", "副产品库");
spRM = new StockingPoint("SP_RM", "原材料库");
stockingPoints.addAll(Arrays.asList(spFG, spByProduct, spRM));
// === 产品→库存点映射 ===
productSpMappings.add(new ProductSpMapping(prodP, spFG));
productSpMappings.add(new ProductSpMapping(prodByP, spByProduct));
productSpMappings.add(new ProductSpMapping(prodRM, spRM));
// === 关键: 预配置联产品产出 (在 Routing.expand() 之前) ===
// OP_Reaction 除了产出 P@WIP (由路由自动生成), 还产出 ByP@SP_ByProduct (联产品)
opReaction = new Operation("OP_Reaction", "反应", "Unit_Reactor",
1.0, 1.0, false, 0, 1.0); // 无产出构造器
opReaction.addOutput(new OperationOutput(prodByP, spByProduct));
// 此时 opReaction 已有 1 个产出: ByP@SP_ByProduct
// OP_Purify 消耗前道 WIP, 产出成品 P@SP_FG
opPurify = new Operation("OP_Purify", "提纯", "Unit_Purify",
1.0, 1.0, false, 0, 1.0); // 无产出构造器
// === 工艺路线: P 主产品 2 步生产 ===
routingP = new Routing("R_P", "P主生产工艺", prodP, spFG);
routingP.addOperation(opReaction);
routingP.addOperation(opPurify);
operations.addAll(routingP.getOperations());
routings.add(routingP);
// === 展开工艺路线 ===
// expand() 使用 addOutput() 追加产出, 不会覆盖预配置的 ByP@SP_ByProduct
// 展开后 opReaction 有 2 个产出: ByP@SP_ByProduct(联产品) + P@WIP_R_P_1(路由)
// opPurify 有 1 个产出: P@SP_FG(路由)
RoutingExpansion expansion = routingP.expand();
stockingPoints.addAll(expansion.getStockingPoints());
productSpMappings.addAll(expansion.getProductSpMappings());
initialInventories.addAll(expansion.getInitialInventories());
operationInputs.addAll(expansion.getOperationInputs());
// === 投料: OP_Reaction 消耗 RM@SP_RM (每件消耗 2 件原材料) ===
operationInputs.add(new OperationInput(
routingP.getOperations().get(0), prodRM, spRM, 2.0));
// === 原材料采购工序 ===
opProcureRM = new Operation("OP_Procure_RM", "采购原材料", "Unit_RM",
new OperationOutput(prodRM, spRM), 0.5, 1.0, false, 0, 1.0);
operations.add(opProcureRM);
// === 设备产能 ===
for (Period p : periods) {
unitPeriods.add(new UnitPeriod("Unit_Reactor", p, 0.0, 100.0, false));
}
for (Period p : periods) {
unitPeriods.add(new UnitPeriod("Unit_Purify", p, 0.0, 100.0, false));
}
for (Period p : periods) {
unitPeriods.add(new UnitPeriod("Unit_RM", p, 0.0, 200.0, false));
}
// === 初始库存 (全部为 0) ===
initialInventories.add(new InitialInventory(prodP, spFG, 0.0));
initialInventories.add(new InitialInventory(prodByP, spByProduct, 0.0));
initialInventories.add(new InitialInventory(prodRM, spRM, 50.0)); // 给一些初始库存
// === 销售需求 ===
// 主产品 P: 50/周期
for (Period p : periods) {
salesDemands.add(new SalesDemand(prodP, spFG, p, 50.0, 1.0));
}
// 副产品 ByP: 30/周期 (少于 P 的产量, 会有剩余库存)
for (Period p : periods) {
salesDemands.add(new SalesDemand(prodByP, spByProduct, p, 30.0, 1.0));
}
// === 库存规格 ===
for (Period p : periods) {
inventorySpecs.add(new InventorySpec(prodP, spFG, p,
100.0, 10.0, 200.0, true, true, true));
}
for (Period p : periods) {
inventorySpecs.add(new InventorySpec(prodByP, spByProduct, p,
80.0, 10.0, 200.0, true, true, true));
}
for (Period p : periods) {
inventorySpecs.add(new InventorySpec(prodRM, spRM, p,
80.0, 10.0, 500.0, true, true, true));
}
// === 供应规格 ===
// 主产品 P 由提纯工序产出
supplySpecs.add(new SupplySpec("Supply-P", 150.0, 100.0, 300.0,
true, Collections.singletonList(routingP.getOperations().get(1))));
// 副产品 ByP 由反应工序联产品产出
supplySpecs.add(new SupplySpec("Supply-ByP", 150.0, 100.0, 300.0,
true, Collections.singletonList(routingP.getOperations().get(0))));
// 原材料 RM 采购
supplySpecs.add(new SupplySpec("Supply-RM", 200.0, 100.0, 500.0,
true, Collections.singletonList(opProcureRM)));
// === KPI 权重 ===
kpiWeights = new KPIWeights(
100.0, // fulfillmentWeight
10.0, // lotSizeWeight
5.0, // maxInventoryLevelWeight
5.0, // minInventoryLevelWeight
8.0, // targetInventoryLevelWeight
20.0, // unitCapacityWeight
8.0, // supplyTargetWeight
5.0, // minSupplyWeight
5.0, // maxSupplyWeight
1.0, // salesDemandPriorityWeight
20.0, // postponementPenaltyWeight
5.0 // processMaxQuantityWeight
);
}
// ==================== 便捷访问方法 (用于验证) ====================
public Product getProdP() { return prodP; }
public Product getProdByP() { return prodByP; }
public Product getProdRM() { return prodRM; }
public StockingPoint getSpFG() { return spFG; }
public StockingPoint getSpByProduct() { return spByProduct; }
public StockingPoint getSpRM() { return spRM; }
public Operation getOpReaction() { return opReaction; }
public Operation getOpPurify() { return opPurify; }
public Routing getRoutingP() { return routingP; }
}
\ No newline at end of file
...@@ -95,7 +95,9 @@ public class DataValidator { ...@@ -95,7 +95,9 @@ public class DataValidator {
// 收集哪些产品有生产工序 // 收集哪些产品有生产工序
Set<String> productsWithOperation = new HashSet<>(); Set<String> productsWithOperation = new HashSet<>();
for (Operation op : data.getOperations()) { for (Operation op : data.getOperations()) {
productsWithOperation.add(op.getOutputProductId()); for (OperationOutput oo : op.getOutputs()) {
productsWithOperation.add(oo.getProductId());
}
} }
// 有销售需求但没有生产工序 // 有销售需求但没有生产工序
...@@ -164,8 +166,7 @@ public class DataValidator { ...@@ -164,8 +166,7 @@ public class DataValidator {
} }
// 工序不能消耗自己产出的产品 (自循环) — 仅当输入和输出在同一库存点时才报错 // 工序不能消耗自己产出的产品 (自循环) — 仅当输入和输出在同一库存点时才报错
// 多工序路由中同一产品可经不同库存点流转 (如: 下料→SP_WIP1→粗加工→SP_WIP2→精加工→SP_FG) // 多工序路由中同一产品可经不同库存点流转 (如: 下料→SP_WIP1→粗加工→SP_WIP2→精加工→SP_FG)
if (op.getOutputProductId().equals(inputProd.getId()) if (op.producesProductAtSp(inputProd.getId(), inputSp.getId())) {
&& op.getOutputSpId().equals(inputSp.getId())) {
errors.add("工序 " + op.getId() + " 消耗自己产出的产品 " + inputProd.getId() errors.add("工序 " + op.getId() + " 消耗自己产出的产品 " + inputProd.getId()
+ "@" + inputSp.getId() + " (自循环 BOM, 同一库存点)"); + "@" + inputSp.getId() + " (自循环 BOM, 同一库存点)");
} }
...@@ -179,20 +180,23 @@ public class DataValidator { ...@@ -179,20 +180,23 @@ public class DataValidator {
// 循环依赖检测 (含库存点: 仅当产品+库存点都匹配时才构成循环) // 循环依赖检测 (含库存点: 仅当产品+库存点都匹配时才构成循环)
// 多工序路由中同一产品经不同库存点流转不构成循环 // 多工序路由中同一产品经不同库存点流转不构成循环
for (OperationInput input : data.getOperationInputs()) { for (OperationInput input : data.getOperationInputs()) {
String consumerProd = input.getOperation().getOutputProductId();
String consumerSp = input.getOperation().getOutputSpId();
String consumedProd = input.getInputProduct().getId(); String consumedProd = input.getInputProduct().getId();
String consumedSp = input.getInputSp().getId(); String consumedSp = input.getInputSp().getId();
for (OperationInput other : data.getOperationInputs()) { // 遍历消费者工序的所有产出 (支持多产出/联产品)
// 检查 other 是否产出 consumedProd@consumedSp 且消耗 consumerProd@consumerSp for (OperationOutput consumerOutput : input.getOperation().getOutputs()) {
if (other.getOperation().getOutputProductId().equals(consumedProd) String consumerProd = consumerOutput.getProductId();
&& other.getOperation().getOutputSpId().equals(consumedSp) String consumerSp = consumerOutput.getSpId();
&& other.getInputProduct().getId().equals(consumerProd)
&& other.getInputSp().getId().equals(consumerSp)) { for (OperationInput other : data.getOperationInputs()) {
errors.add("BOM 循环依赖: " + consumerProd + "@" + consumerSp // 检查 other 是否产出 consumedProd@consumedSp 且消耗 consumerProd@consumerSp
+ " → " + consumedProd + "@" + consumedSp if (other.getOperation().producesProductAtSp(consumedProd, consumedSp)
+ " → " + consumerProd + "@" + consumerSp); && other.getInputProduct().getId().equals(consumerProd)
&& other.getInputSp().getId().equals(consumerSp)) {
errors.add("BOM 循环依赖: " + consumerProd + "@" + consumerSp
+ " → " + consumedProd + "@" + consumedSp
+ " → " + consumerProd + "@" + consumerSp);
}
} }
} }
} }
...@@ -207,9 +211,11 @@ public class DataValidator { ...@@ -207,9 +211,11 @@ public class DataValidator {
} }
for (Operation op : data.getOperations()) { for (Operation op : data.getOperations()) {
if (!unitIds.contains(op.getUnitId())) { for (UnitOperation uo : op.getUnitOperations()) {
errors.add("工序 " + op.getId() + " 的单元 " + op.getUnitId() if (!unitIds.contains(uo.getUnitId())) {
+ " 在 UnitPeriod 中不存在 (缺少产能定义)"); errors.add("工序 " + op.getId() + " 的单元 " + uo.getUnitId()
+ " 在 UnitPeriod 中不存在 (缺少产能定义)");
}
} }
} }
} }
...@@ -319,9 +325,10 @@ public class DataValidator { ...@@ -319,9 +325,10 @@ public class DataValidator {
// 有生产工序的产品 // 有生产工序的产品
Set<String> productsWithOperation = new HashSet<>(); Set<String> productsWithOperation = new HashSet<>();
for (Operation op : data.getOperations()) { for (Operation op : data.getOperations()) {
productsWithOperation.add(op.getOutputProductId()); for (OperationOutput oo : op.getOutputs()) {
productsWithOperation.add(oo.getProductId());
}
} }
// 被 BOM 消耗的产品 // 被 BOM 消耗的产品
Set<String> productsConsumed = new HashSet<>(); Set<String> productsConsumed = new HashSet<>();
for (OperationInput input : data.getOperationInputs()) { for (OperationInput input : data.getOperationInputs()) {
...@@ -379,7 +386,9 @@ public class DataValidator { ...@@ -379,7 +386,9 @@ public class DataValidator {
// 有生产工序的产品 // 有生产工序的产品
Set<String> productsWithOperation = new HashSet<>(); Set<String> productsWithOperation = new HashSet<>();
for (Operation op : data.getOperations()) { for (Operation op : data.getOperations()) {
productsWithOperation.add(op.getOutputProductId()); for (OperationOutput oo : op.getOutputs()) {
productsWithOperation.add(oo.getProductId());
}
} }
// 在 BOM 中作为输入的产品 // 在 BOM 中作为输入的产品
......
package com.aps.macroplanner.data;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.Collections;
/**
* 多级 BOM + 多成品 + 共享半成品 测试数据构建器。
*
* <h3>BOM 结构</h3>
* <pre>
* P1 ──消耗──→ S1 × 2.0 + R1 × 3.0
* P2 ──消耗──→ S1 × 1.0
* S1 ──消耗──→ R2 × 2.0
*
* 物料分级:
* 成品 (Finished): P1, P2
* 半成品 (Semi): S1 (被 P1 和 P2 共享)
* 原材料 (Raw): R1 (P1直接消耗), R2 (S1消耗)
* </pre>
*
* <h3>推导需求 (每周期)</h3>
* <pre>
* P1 销售: 40 → S1需求: 40×2=80, R1需求: 40×3=120
* P2 销售: 30 → S1需求: 30×1=30
* S1 总需求: 80+30=110 → R2需求: 110×2=220
* </pre>
*
* <h3>关键验证点</h3>
* <pre>
* - 共享半成品 S1 的总生产量 = P1生产量×2.0 + P2生产量×1.0
* - 原材料 R2 消耗 = S1生产量×2.0
* - 原材料 R1 消耗 = P1生产量×3.0
* - 所有需求满足 (需求缺口=0)
* - 多级 BOM 展开正确
* </pre>
*/
public class MultiLevelBomTestDataBuilder extends TestDataBuilder {
@Override
protected void build() {
// === 周期: 3 天 ===
periods.add(new Period(0, "Day1", LocalDate.of(2026, 8, 7)));
periods.add(new Period(1, "Day2", LocalDate.of(2026, 8, 8)));
periods.add(new Period(2, "Day3", LocalDate.of(2026, 8, 9)));
// === 产品: 成品 P1, P2 + 半成品 S1 + 原材料 R1, R2 ===
Product prodP1 = new Product("P1", "成品P1");
Product prodP2 = new Product("P2", "成品P2");
Product prodS1 = new Product("S1", "半成品S1");
Product prodR1 = new Product("R1", "原材料R1");
Product prodR2 = new Product("R2", "原材料R2");
products.addAll(Arrays.asList(prodP1, prodP2, prodS1, prodR1, prodR2));
// === 库存点: 每种产品一个库存点 ===
StockingPoint spP1 = new StockingPoint("SP_P1", "P1成品库");
StockingPoint spP2 = new StockingPoint("SP_P2", "P2成品库");
StockingPoint spSemi = new StockingPoint("SP_Semi", "半成品库");
StockingPoint spR1 = new StockingPoint("SP_R1", "R1原材料库");
StockingPoint spR2 = new StockingPoint("SP_R2", "R2原材料库");
stockingPoints.addAll(Arrays.asList(spP1, spP2, spSemi, spR1, spR2));
// === 产品→库存点映射 ===
productSpMappings.add(new ProductSpMapping(prodP1, spP1));
productSpMappings.add(new ProductSpMapping(prodP2, spP2));
productSpMappings.add(new ProductSpMapping(prodS1, spSemi));
productSpMappings.add(new ProductSpMapping(prodR1, spR1));
productSpMappings.add(new ProductSpMapping(prodR2, spR2));
// === 生产工序: 成品 + 半成品 ===
// P1: 消耗 S1×2.0 + R1×3.0, 产出 P1@SP_P1
Operation opP1 = new Operation("OP_P1", "生产P1", "Unit_P1",
new OperationOutput(prodP1, spP1), 1.0, 1.0, false, 0, 1.0);
// P2: 消耗 S1×1.0, 产出 P2@SP_P2
Operation opP2 = new Operation("OP_P2", "生产P2", "Unit_P2",
new OperationOutput(prodP2, spP2), 1.0, 1.0, false, 0, 1.0);
// S1: 消耗 R2×2.0, 产出 S1@SP_Semi
Operation opS1 = new Operation("OP_S1", "生产S1", "Unit_S1",
new OperationOutput(prodS1, spSemi), 1.0, 1.0, false, 0, 1.0);
operations.addAll(Arrays.asList(opP1, opP2, opS1));
// === 原材料采购工序 ===
Operation opProcureR1 = new Operation("OP_Procure_R1", "采购R1", "Unit_R1",
new OperationOutput(prodR1, spR1), 0.5, 1.0, false, 0, 1.0);
Operation opProcureR2 = new Operation("OP_Procure_R2", "采购R2", "Unit_R2",
new OperationOutput(prodR2, spR2), 0.5, 1.0, false, 0, 1.0);
operations.addAll(Arrays.asList(opProcureR1, opProcureR2));
// === BOM 投料: P1 消耗 S1×2.0 + R1×3.0 ===
operationInputs.add(new OperationInput(opP1, prodS1, spSemi, 2.0));
operationInputs.add(new OperationInput(opP1, prodR1, spR1, 3.0));
// === BOM 投料: P2 消耗 S1×1.0 ===
operationInputs.add(new OperationInput(opP2, prodS1, spSemi, 1.0));
// === BOM 投料: S1 消耗 R2×2.0 ===
operationInputs.add(new OperationInput(opS1, prodR2, spR2, 2.0));
// === 设备产能: 每个设备 200h/周期 ===
for (Period p : periods) {
unitPeriods.add(new UnitPeriod("Unit_P1", p, 0.0, 200.0, false));
unitPeriods.add(new UnitPeriod("Unit_P2", p, 0.0, 200.0, false));
unitPeriods.add(new UnitPeriod("Unit_S1", p, 0.0, 200.0, false));
unitPeriods.add(new UnitPeriod("Unit_R1", p, 0.0, 200.0, false));
unitPeriods.add(new UnitPeriod("Unit_R2", p, 0.0, 200.0, false));
}
// === 初始库存: 全部从 0 开始 ===
initialInventories.add(new InitialInventory(prodP1, spP1, 0.0));
initialInventories.add(new InitialInventory(prodP2, spP2, 0.0));
initialInventories.add(new InitialInventory(prodS1, spSemi, 0.0));
initialInventories.add(new InitialInventory(prodR1, spR1, 0.0));
initialInventories.add(new InitialInventory(prodR2, spR2, 0.0));
// === 销售需求: P1=40/周期, P2=30/周期 ===
for (Period p : periods) {
salesDemands.add(new SalesDemand(prodP1, spP1, p, 40.0, 1.0));
salesDemands.add(new SalesDemand(prodP2, spP2, p, 30.0, 1.0));
}
// === 库存规格: 目标库存 ===
for (Period p : periods) {
inventorySpecs.add(new InventorySpec(prodP1, spP1, p,
80.0, 10.0, 200.0, true, true, true));
inventorySpecs.add(new InventorySpec(prodP2, spP2, p,
60.0, 10.0, 200.0, true, true, true));
inventorySpecs.add(new InventorySpec(prodS1, spSemi, p,
100.0, 10.0, 300.0, true, true, true));
inventorySpecs.add(new InventorySpec(prodR1, spR1, p,
80.0, 10.0, 500.0, true, true, true));
inventorySpecs.add(new InventorySpec(prodR2, spR2, p,
80.0, 10.0, 500.0, true, true, true));
}
// === 供应规格: 成品+半成品各一个, 原材料各一个 ===
supplySpecs.add(new SupplySpec("Supply-P1", 120.0, 80.0, 300.0,
true, Collections.singletonList(opP1)));
supplySpecs.add(new SupplySpec("Supply-P2", 90.0, 60.0, 300.0,
true, Collections.singletonList(opP2)));
supplySpecs.add(new SupplySpec("Supply-S1", 300.0, 200.0, 500.0,
true, Collections.singletonList(opS1)));
supplySpecs.add(new SupplySpec("Supply-R1", 400.0, 200.0, 800.0,
true, Collections.singletonList(opProcureR1)));
supplySpecs.add(new SupplySpec("Supply-R2", 600.0, 400.0, 1000.0,
true, Collections.singletonList(opProcureR2)));
// === KPI 权重 (必须显式初始化, 因为覆写了 build() 不调用 super) ===
kpiWeights = new KPIWeights(
100.0, // fulfillmentWeight
10.0, // lotSizeWeight
5.0, // maxInventoryLevelWeight
5.0, // minInventoryLevelWeight
8.0, // targetInventoryLevelWeight
20.0, // unitCapacityWeight
8.0, // supplyTargetWeight
5.0, // minSupplyWeight
5.0, // maxSupplyWeight
1.0, // salesDemandPriorityWeight
20.0, // postponementPenaltyWeight
5.0 // processMaxQuantityWeight
);
}
}
\ No newline at end of file
This diff is collapsed.
package com.aps.macroplanner.data;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 工艺路线展开结果 — 包含 {@link Routing#expand} 自动生成的所有数据。
*
* <p>调用方将展开结果中的各项数据合并到 {@link TestDataBuilder} 中。</p>
*
* <h3>展开内容</h3>
* <pre>
* - stockingPoints: 中间 WIP 库存点 (如 WIP_R001_1, WIP_R001_2)
* - productSpMappings: 产品→WIP 库存点映射
* - initialInventories: 中间 WIP 初始库存 (均为 0)
* - operationInputs: 工序间 BOM 消耗关系
* </pre>
*
* @see Routing
*/
public class RoutingExpansion {
private final Routing routing;
/** 自动生成的中间 WIP 库存点 */
private final List<StockingPoint> stockingPoints = new ArrayList<>();
/** 产品→WIP 库存点映射 */
private final List<ProductSpMapping> productSpMappings = new ArrayList<>();
/** 中间 WIP 初始库存 (均为 0) */
private final List<InitialInventory> initialInventories = new ArrayList<>();
/** 工序间 BOM 消耗关系 */
private final List<OperationInput> operationInputs = new ArrayList<>();
RoutingExpansion(Routing routing) {
this.routing = routing;
}
void addStockingPoint(StockingPoint sp) {
stockingPoints.add(sp);
}
void addProductSpMapping(ProductSpMapping mapping) {
productSpMappings.add(mapping);
}
void addInitialInventory(InitialInventory inv) {
initialInventories.add(inv);
}
void addOperationInput(OperationInput input) {
operationInputs.add(input);
}
// ==================== Getters ====================
public Routing getRouting() { return routing; }
public List<StockingPoint> getStockingPoints() { return Collections.unmodifiableList(stockingPoints); }
public List<ProductSpMapping> getProductSpMappings() { return Collections.unmodifiableList(productSpMappings); }
public List<InitialInventory> getInitialInventories() { return Collections.unmodifiableList(initialInventories); }
public List<OperationInput> getOperationInputs() { return Collections.unmodifiableList(operationInputs); }
}
\ No newline at end of file
package com.aps.macroplanner.data;
/**
* 工艺路线中的一个工序步骤 — 对应 Quintiq 中的 RoutingStep。
*
* <p>每个 RoutingStep 包含一个 Operation 和它在工艺路线中的顺序号。
* 工序间的 WIP 由 {@link Routing} 自动管理, 无需手动配置库存点。</p>
*
* <h3>与 Quintiq 模型的对应关系</h3>
* <pre>
* Quintiq RoutingStep:
* - SequenceNumber → sequenceNumber
* - Operation → operation
* - RoutingID → 由父 Routing 管理
* - PISPNodeInRouting → 由 Routing.expand() 自动生成 WIP 库存点
* </pre>
*
* @see Routing
*/
public class RoutingStep {
/** 工序顺序号 (从 1 开始, 越小越靠前) */
private final int sequenceNumber;
/** 该步骤执行的工序 */
private final Operation operation;
/**
* 创建工艺路线步骤。
*
* @param sequenceNumber 顺序号 (1-based, 越小越靠前)
* @param operation 该步骤执行的工序
*/
public RoutingStep(int sequenceNumber, Operation operation) {
this.sequenceNumber = sequenceNumber;
this.operation = operation;
}
public int getSequenceNumber() { return sequenceNumber; }
public Operation getOperation() { return operation; }
}
\ No newline at end of file
...@@ -5,19 +5,32 @@ import java.util.Arrays; ...@@ -5,19 +5,32 @@ import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
/** /**
* 多工序路由测试数据构建器 * 多工序路由测试数据构建器 — 使用 {@link Routing} 自动管理工序间 WIP。
* *
* <h3>测试场景: 3 工序串行生产同一产品 P</h3> * <h3>测试场景: 3 工序串行生产同一产品 P, 第一道工序投料消耗原材料 RM</h3>
* <pre> * <pre>
* OP_Cut(下料) ──→ P@SP_WIP1 ──→ OP_Rough(粗加工) ──→ P@SP_WIP2 ──→ OP_Finish(精加工) ──→ P@SP_FG ──→ 销售 * 原材料 RM → OP_Cut(下料) → WIP_R001_1 → OP_Rough(粗加工) → WIP_R001_2 → OP_Finish(精加工) → SP_FG(成品)
* Unit_Cut ↑ Unit_Rough ↑ Unit_Finish ↑ * ↑消耗RM@SP_RM×1
* OP_Rough 消耗 OP_Finish 消耗 销售需求 50/周期
* P@SP_WIP1 (BOM) P@SP_WIP2 (BOM)
* *
* 关键验证: * 工艺路线 R001 (P生产工艺) 生产产品 P, 最终入库 SP_FG:
* 工序1: OP_Cut(下料) → Unit_Cut → 消耗 RM@SP_RM × 1.0 (投料)
* 工序2: OP_Rough(粗加工) → Unit_Rough → 消耗 WIP_R001_1
* 工序3: OP_Finish(精加工) → Unit_Finish → 产出成品到 SP_FG
*
* 工序间 WIP 由 Routing.expand() 自动生成:
* - 自动创建 WIP_R001_1 (工序1→2 缓冲区)
* - 自动创建 WIP_R001_2 (工序2→3 缓冲区)
* - 自动配置 BOM: OP_Rough 消耗 WIP_R001_1, OP_Finish 消耗 WIP_R001_2
* - 只有 OP_Finish 产出成品, 中间 WIP 不重复计算
* </pre>
*
* <h3>关键验证点</h3>
* <pre>
* - 原材料 RM 通过 BOM 被 OP_Cut 消耗, 每生产 1 件 P 消耗 1 件 RM
* - 3 个工序各自消耗各自设备的产能 (独立计算) * - 3 个工序各自消耗各自设备的产能 (独立计算)
* - 只有最后一道工序 OP_Finish 的产出计入成品供应 * - 只有最后一道工序的产出计入成品供应
* - 中间工序的产出被 DependentDemand 抵消, 最终产出 = 销售需求, 不是 3× * - 中间 WIP 被 BOM 约束完全消耗 + RoutingConstraint 强制产量一致
* - 用户无需手动配置中间 WIP 库存点、BOM 输入
* </pre> * </pre>
*/ */
public class RoutingTestDataBuilder extends TestDataBuilder { public class RoutingTestDataBuilder extends TestDataBuilder {
...@@ -37,73 +50,85 @@ public class RoutingTestDataBuilder extends TestDataBuilder { ...@@ -37,73 +50,85 @@ public class RoutingTestDataBuilder extends TestDataBuilder {
Period p3 = new Period(2, "P3", baseDate.plusDays(2)); Period p3 = new Period(2, "P3", baseDate.plusDays(2));
periods.addAll(Arrays.asList(p1, p2, p3)); periods.addAll(Arrays.asList(p1, p2, p3));
// === 产品: 只有 1 个成品 P === // === 产品: 成品 P + 原材料 RM ===
Product prodP = new Product("P", "Product-P"); Product prodP = new Product("P", "Product-P");
products.add(prodP); Product prodRM = new Product("RM", "RawMaterial");
products.addAll(Arrays.asList(prodP, prodRM));
// === 库存点: 2 个 WIP 缓冲区 + 1 个成品库 === // === 库存点: 成品库 + 原材料库 ===
StockingPoint spWIP1 = new StockingPoint("SP_WIP1", "下料→粗加工缓冲区"); StockingPoint spFG = new StockingPoint("SP_FG", "成品库");
StockingPoint spWIP2 = new StockingPoint("SP_WIP2", "粗加工→精加工缓冲区"); StockingPoint spRM = new StockingPoint("SP_RM", "原材料库");
StockingPoint spFG = new StockingPoint("SP_FG", "成品库"); stockingPoints.addAll(Arrays.asList(spFG, spRM));
stockingPoints.addAll(Arrays.asList(spWIP1, spWIP2, spFG));
// === 产品→库存点映射 (P 在三个库存点都有) === // === 产品→库存点映射 ===
productSpMappings.add(new ProductSpMapping(prodP, spWIP1));
productSpMappings.add(new ProductSpMapping(prodP, spWIP2));
productSpMappings.add(new ProductSpMapping(prodP, spFG)); productSpMappings.add(new ProductSpMapping(prodP, spFG));
productSpMappings.add(new ProductSpMapping(prodRM, spRM));
// === 3 道工序, 都产出产品 P, 但分属不同设备 ===
// 下料: 1.0h/件, 设备 Unit_Cut, 产出到 WIP1 缓冲区 // === 原材料采购工序 (供应 RM 到 SP_RM) ===
Operation opCut = new Operation("OP_Cut", "下料", "Unit_Cut", Operation opProcureRM = new Operation("OP_Procure_RM", "采购原材料", "Unit_RM",
new OperationOutput(prodP, spWIP1), 1.0, 1.0, false, 0, 1.0); new OperationOutput(prodRM, spRM), 0.5, 1.0, false, 0, 1.0);
// 粗加工: 1.5h/件, 设备 Unit_Rough, 产出到 WIP2 缓冲区 operations.add(opProcureRM);
Operation opRough = new Operation("OP_Rough", "粗加工", "Unit_Rough",
new OperationOutput(prodP, spWIP2), 1.5, 1.0, false, 0, 1.0); // === 工艺路线: 产品 P 经过 3 道工序生产, 最终入库 SP_FG ===
// 精加工: 2.0h/件, 设备 Unit_Finish, 产出到成品库 // 对应 Quintiq: Routing R001, 关联 Product P 和 StockingPoint SP_FG
Operation opFinish = new Operation("OP_Finish", "精加工", "Unit_Finish", Routing routing = new Routing("R001", "P生产工艺", prodP, spFG);
new OperationOutput(prodP, spFG), 2.0, 1.0, false, 0, 1.0); routing.addOperation(new Operation("OP_Cut", "下料", "Unit_Cut", 1.0, 1.0, false, 0, 1.0));
operations.addAll(Arrays.asList(opCut, opRough, opFinish)); routing.addOperation(new Operation("OP_Rough", "粗加工", "Unit_Rough", 1.5, 1.0, false, 0, 1.0));
routing.addOperation(new Operation("OP_Finish","精加工", "Unit_Finish",2.0, 1.0, false, 0, 1.0));
// === BOM / OperationInput: 定义工序间流转 ===
// OP_Rough 消耗 P@SP_WIP1 (即 OP_Cut 的产出), factor=1.0 (1:1 无损耗) operations.addAll(routing.getOperations());
operationInputs.add(new OperationInput(opRough, prodP, spWIP1, 1.0)); routings.add(routing); // 注册到数据构建器, 供 RoutingConstraint 使用
// OP_Finish 消耗 P@SP_WIP2 (即 OP_Rough 的产出), factor=1.0
operationInputs.add(new OperationInput(opFinish, prodP, spWIP2, 1.0)); // === 展开工艺路线: 自动生成 WIP 库存点 + BOM 输入 + 工序产出配置 ===
// 只有最后工序 (OP_Finish) 产出成品, 中间工序 WIP 自动流转
// === 设备产能: 三个设备各有独立产能 === RoutingExpansion expansion = routing.expand();
// Unit_Cut: 最大 200h/周期 stockingPoints.addAll(expansion.getStockingPoints());
productSpMappings.addAll(expansion.getProductSpMappings());
initialInventories.addAll(expansion.getInitialInventories());
operationInputs.addAll(expansion.getOperationInputs());
// === 投料: OP_Cut 消耗原材料 RM@SP_RM (每件 P 消耗 1 件 RM) ===
operationInputs.add(new OperationInput(
routing.getOperations().get(0), prodRM, spRM, 1.0));
// === 设备产能 ===
for (Period p : periods) { for (Period p : periods) {
unitPeriods.add(new UnitPeriod("Unit_Cut", p, 0.0, 200.0, false)); unitPeriods.add(new UnitPeriod("Unit_Cut", p, 0.0, 200.0, false));
} }
// Unit_Rough: 最大 200h/周期
for (Period p : periods) { for (Period p : periods) {
unitPeriods.add(new UnitPeriod("Unit_Rough", p, 0.0, 200.0, false)); unitPeriods.add(new UnitPeriod("Unit_Rough", p, 0.0, 200.0, false));
} }
// Unit_Finish: 最大 200h/周期
for (Period p : periods) { for (Period p : periods) {
unitPeriods.add(new UnitPeriod("Unit_Finish", p, 0.0, 200.0, false)); unitPeriods.add(new UnitPeriod("Unit_Finish", p, 0.0, 200.0, false));
} }
for (Period p : periods) {
unitPeriods.add(new UnitPeriod("Unit_RM", p, 0.0, 200.0, false));
}
// === 初始库存: 全部为 0 (没有初始 WIP) === // === 初始库存 ===
initialInventories.add(new InitialInventory(prodP, spWIP1, 0.0));
initialInventories.add(new InitialInventory(prodP, spWIP2, 0.0));
initialInventories.add(new InitialInventory(prodP, spFG, 0.0)); initialInventories.add(new InitialInventory(prodP, spFG, 0.0));
initialInventories.add(new InitialInventory(prodRM, spRM, 0.0));
// === 销售需求: 只在成品库 SP_FG, 每周期 50 === // === 销售需求: 只在成品库 SP_FG, 每周期 50 ===
for (Period p : periods) { for (Period p : periods) {
salesDemands.add(new SalesDemand(prodP, spFG, p, 50.0, 1.0)); salesDemands.add(new SalesDemand(prodP, spFG, p, 50.0, 1.0));
} }
// === 库存规格: 只在成品库设目标/最小/最大 === // === 库存规格 ===
// WIP 缓冲区不设库存规格 (不约束中间库存)
for (Period p : periods) { for (Period p : periods) {
inventorySpecs.add(new InventorySpec(prodP, spFG, p, inventorySpecs.add(new InventorySpec(prodP, spFG, p,
80.0, 10.0, 200.0, true, true, true)); 80.0, 10.0, 200.0, true, true, true));
} }
for (Period p : periods) {
inventorySpecs.add(new InventorySpec(prodRM, spRM, p,
80.0, 10.0, 500.0, true, true, true));
}
// === 供应规格: 只统计精加工(最后一道工序)的产出 === // === 供应规格: 精加工产出 + 原材料采购 ===
supplySpecs.add(new SupplySpec("Supply-P", 150.0, 100.0, 300.0, supplySpecs.add(new SupplySpec("Supply-P", 150.0, 100.0, 300.0,
true, Collections.singletonList(opFinish))); true, Collections.singletonList(routing.getOperations().get(2))));
supplySpecs.add(new SupplySpec("Supply-RM", 200.0, 100.0, 500.0,
true, Collections.singletonList(opProcureRM)));
// === KPI 权重 === // === KPI 权重 ===
kpiWeights = new KPIWeights( kpiWeights = new KPIWeights(
......
package com.aps.macroplanner.data;
/**
* 单元-工序关联 — 定义某个工序在某个单元(Unit)上的产能参数。
*
* <p>一个 Operation 可以有多个 UnitOperation,表示同一工序可在不同单元/产线执行,
* 每个单元的产能消耗、批次大小可能不同。</p>
*
* <p>例如: "采购 R1" 可以由供应商 A (0.5h/件) 和供应商 B (0.8h/件) 执行。</p>
*/
public class UnitOperation {
private final String unitId; // 所属单元ID
private final double capacityCoeff; // 产能消耗系数 (单件耗时)
private final boolean hasLotSize; // 是否有批次大小
private final double lotSize; // 批次大小
private final double qtpfactor; // QuantityToProcessFactor
public UnitOperation(String unitId, double capacityCoeff,
boolean hasLotSize, double lotSize, double qtpfactor) {
this.unitId = unitId;
this.capacityCoeff = capacityCoeff;
this.hasLotSize = hasLotSize;
this.lotSize = lotSize;
this.qtpfactor = qtpfactor;
}
public String getUnitId() { return unitId; }
public double getCapacityCoeff() { return capacityCoeff; }
public boolean hasLotSize() { return hasLotSize; }
public double getLotSize() { return lotSize; }
public double getQtpfactor() { return qtpfactor; }
@Override
public String toString() {
return unitId + "(产能" + capacityCoeff + "h/件" +
(hasLotSize ? ", 批次" + lotSize : "") + ")";
}
}
\ No newline at end of file
package com.aps.macroplanner.output;
import java.util.List;
/**
* 轻量级 JSON 字符串构建器 — 无外部依赖, 兼容 Java 8。
*
* <p>提供以下方法:</p>
* <ul>
* <li>{@link #obj()} / {@link #endObj()} — 开始/结束 JSON 对象</li>
* <li>{@link #arr()} / {@link #endArr()} — 开始/结束 JSON 数组</li>
* <li>{@link #key(String)} — 写入键</li>
* <li>{@link #val(String)} / {@link #val(double)} / {@link #val(int)} / {@link #val(boolean)} / {@link #valNull()} — 写入值</li>
* </ul>
*
* <h3>使用示例</h3>
* <pre>{@code
* JsonBuilder jb = new JsonBuilder();
* jb.obj()
* .key("name").val("Alice")
* .key("age").val(30)
* .key("scores").arr()
* .val(95.5).val(88.0)
* .endArr()
* .endObj();
* String json = jb.toString();
* }</pre>
*/
public class JsonBuilder {
private final StringBuilder sb = new StringBuilder();
private boolean firstInContainer = true;
/** 开始 JSON 对象 { */
public JsonBuilder obj() {
sb.append("{");
firstInContainer = true;
return this;
}
/** 结束 JSON 对象 } */
public JsonBuilder endObj() {
sb.append("}");
return this;
}
/** 开始 JSON 数组 [ */
public JsonBuilder arr() {
sb.append("[");
firstInContainer = true;
return this;
}
/** 结束 JSON 数组 ] */
public JsonBuilder endArr() {
sb.append("]");
return this;
}
/** 写入键 "key": */
public JsonBuilder key(String key) {
if (!firstInContainer) sb.append(",");
sb.append("\"").append(escape(key)).append("\":");
firstInContainer = true;
return this;
}
/** 写入字符串值 */
public JsonBuilder val(String value) {
comma();
if (value == null) {
sb.append("null");
} else {
sb.append("\"").append(escape(value)).append("\"");
}
return this;
}
/** 写入 double 值 (有限小数的 JSON 数字) */
public JsonBuilder val(double value) {
comma();
if (Double.isNaN(value) || Double.isInfinite(value)) {
sb.append("null");
} else if (Math.abs(value - Math.round(value)) < 1e-9) {
sb.append((long) value);
} else {
sb.append(String.format("%.6f", value));
}
return this;
}
/** 写入 int 值 */
public JsonBuilder val(int value) {
comma();
sb.append(value);
return this;
}
/** 写入 long 值 */
public JsonBuilder val(long value) {
comma();
sb.append(value);
return this;
}
/** 写入 boolean 值 */
public JsonBuilder val(boolean value) {
comma();
sb.append(value);
return this;
}
/** 写入 null */
public JsonBuilder valNull() {
comma();
sb.append("null");
return this;
}
/**
* 写入可选的 double 值 (null 时输出 null)。
*/
public JsonBuilder valOpt(Double value) {
if (value == null) {
return valNull();
}
return val(value.doubleValue());
}
/**
* 写入 JSON 对象列表 (每个元素调用 toJson 方法)。
*/
public <T extends JsonSerializable> JsonBuilder valArray(List<T> items) {
arr();
for (int i = 0; i < items.size(); i++) {
if (i > 0) sb.append(",");
items.get(i).toJson(this);
}
endArr();
return this;
}
/** 在值前插入逗号 (如果不是第一个) */
private void comma() {
if (!firstInContainer) sb.append(",");
firstInContainer = false;
}
/** JSON 字符串转义 */
private static String escape(String s) {
StringBuilder out = new StringBuilder(s.length() + 8);
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
switch (c) {
case '"': out.append("\\\""); break;
case '\\': out.append("\\\\"); break;
case '\b': out.append("\\b"); break;
case '\f': out.append("\\f"); break;
case '\n': out.append("\\n"); break;
case '\r': out.append("\\r"); break;
case '\t': out.append("\\t"); break;
default:
if (c < 0x20) {
out.append(String.format("\\u%04x", (int) c));
} else {
out.append(c);
}
}
}
return out.toString();
}
@Override
public String toString() {
return sb.toString();
}
// ==================== 序列化接口 ====================
/**
* 可序列化为 JSON 的对象接口。
*/
public interface JsonSerializable {
void toJson(JsonBuilder jb);
}
}
\ No newline at end of file
This diff is collapsed.
package com.aps.macroplanner.output.dto;
import java.util.ArrayList;
import java.util.List;
/**
* 需求满足汇总 — 按产品/周期聚合需求量、满足量、满足率。
*
* <h3>结构</h3>
* <pre>
* productId: 产品ID
* totalDemand: 总需求量
* totalFulfilled: 总满足量
* totalUnmet: 总未满足量
* fulfillmentRate: 整体满足率
* periodEntries: 每周期明细
* </pre>
*/
public class DemandSummaryResult {
private String productId;
/** 按周期明细 */
private final List<PeriodEntry> periodEntries = new ArrayList<>();
/** 跨周期汇总 */
private double totalDemand;
private double totalFulfilled;
private double totalUnmet;
private double fulfillmentRate;
// ==================== 内嵌类 ====================
public static class PeriodEntry {
public int periodIndex;
public String periodStartDate;
public double demandQty;
public double fulfilledQty;
public double unmetQty;
public double fulfillmentRate;
}
// ==================== Getters / Setters ====================
public String getProductId() { return productId; }
public void setProductId(String v) { this.productId = v; }
public List<PeriodEntry> getPeriodEntries() { return periodEntries; }
public double getTotalDemand() { return totalDemand; }
public void setTotalDemand(double v) { this.totalDemand = v; }
public double getTotalFulfilled() { return totalFulfilled; }
public void setTotalFulfilled(double v) { this.totalFulfilled = v; }
public double getTotalUnmet() { return totalUnmet; }
public void setTotalUnmet(double v) { this.totalUnmet = v; }
public double getFulfillmentRate() { return fulfillmentRate; }
public void setFulfillmentRate(double v) { this.fulfillmentRate = v; }
}
package com.aps.macroplanner.output.dto;
import java.util.ArrayList;
import java.util.List;
/**
* KPI 汇总结果 — 对应 Quintiq 中 OptimizerNonFinancialKPIResult 的快照数据。
*/
public class KpiResult {
/** 目标函数值 (加权总惩罚) */
private double objectiveValue;
/** 各项 KPI 明细 */
private final List<KpiEntry> entries = new ArrayList<>();
// ==================== 内嵌类 ====================
/** 单个 KPI 项 */
public static class KpiEntry {
/** KPI 名称 */
public String name;
/** KPI 原始值 (松弛量总和) */
public double rawValue;
/** 权重 */
public double weight;
/** 加权惩罚 = rawValue × weight */
public double penalty;
/** 是否为收益项 (越大越好, 正系数) */
public boolean isBenefit;
}
// ==================== Getters / Setters ====================
public double getObjectiveValue() { return objectiveValue; }
public void setObjectiveValue(double v) { this.objectiveValue = v; }
public List<KpiEntry> getEntries() { return entries; }
public void addEntry(String name, double rawValue, double weight, boolean isBenefit) {
KpiEntry e = new KpiEntry();
e.name = name;
e.rawValue = rawValue;
e.weight = weight;
e.isBenefit = isBenefit;
e.penalty = isBenefit ? -rawValue * weight : rawValue * weight;
entries.add(e);
}
}
\ No newline at end of file
package com.aps.macroplanner.output.dto;
import java.util.ArrayList;
import java.util.List;
/**
* 优化结果顶层容器 — 对应 Quintiq 中一次完整的优化运行输出。
*
* <p>包含以下子结构:</p>
* <ul>
* <li>metadata — 运行时间戳、求解器、版本</li>
* <li>periodTasks — 生产任务结果 (← PTQty)</li>
* <li>salesDemands — 销售需求满足结果 (← SalesDemandQty)</li>
* <li>pispips — 库存点库存结果 (← InvQty, DemandSlack 等)</li>
* <li>kpis — KPI 汇总结果</li>
* <li>statistics — 求解器运行统计</li>
* </ul>
*/
public class OptimizationResult {
// ==================== 元数据 ====================
private String timestamp;
private String solver;
private String version;
// ==================== 业务结果 ====================
private final List<PeriodTaskResult> periodTasks = new ArrayList<>();
private final List<SalesDemandResult> salesDemands = new ArrayList<>();
private final List<PispipResult> pispips = new ArrayList<>();
// ==================== 产品生产网络 ====================
private ProductNetworkResult productNetwork;
// ==================== Unit产能使用 ====================
private List<UnitCapacityResult> unitCapacities;
// ==================== 需求满足汇总 ====================
private List<DemandSummaryResult> demandSummary;
// ==================== KPI 和统计 ====================
private KpiResult kpis;
private SolverStatistics statistics;
// ==================== Getters / Setters ====================
public String getTimestamp() { return timestamp; }
public void setTimestamp(String v) { this.timestamp = v; }
public String getSolver() { return solver; }
public void setSolver(String v) { this.solver = v; }
public String getVersion() { return version; }
public void setVersion(String v) { this.version = v; }
public List<PeriodTaskResult> getPeriodTasks() { return periodTasks; }
public List<SalesDemandResult> getSalesDemands() { return salesDemands; }
public List<PispipResult> getPispips() { return pispips; }
public ProductNetworkResult getProductNetwork() { return productNetwork; }
public void setProductNetwork(ProductNetworkResult v) { this.productNetwork = v; }
public List<UnitCapacityResult> getUnitCapacities() { return unitCapacities; }
public void setUnitCapacities(List<UnitCapacityResult> v) { this.unitCapacities = v; }
public List<DemandSummaryResult> getDemandSummary() { return demandSummary; }
public void setDemandSummary(List<DemandSummaryResult> v) { this.demandSummary = v; }
public KpiResult getKpis() { return kpis; }
public void setKpis(KpiResult v) { this.kpis = v; }
public SolverStatistics getStatistics() { return statistics; }
public void setStatistics(SolverStatistics v) { this.statistics = v; }
}
\ No newline at end of file
package com.aps.macroplanner.output.dto;
import java.util.ArrayList;
import java.util.List;
/**
* 生产任务结果 — 对应 Quintiq 中 PeriodTaskOperation 的回写数据。
*
* <p>每个操作在每个周期的生产量、产能消耗、批次偏差等。</p>
*/
public class PeriodTaskResult {
private String operationId;
private String operationName;
private String unitId;
private int periodIndex;
private String periodStartDate;
/** PTQty — 生产量 */
private double productionQty;
/** 产能消耗 = productionQty × coefficient */
private double capacityUsed;
/** 产能消耗系数 */
private double capacityCoeff;
/** 批次大小 (如有) */
private Double lotSize;
/** 超过批次上限的量 */
private double lotSizeOver;
/** 低于批次下限的量 */
private double lotSizeUnder;
/** 产出: 产品 → 库存点 */
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 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 java.util.ArrayList;
import java.util.List;
/**
* 库存点库存结果 — 对应 Quintiq 中 ProductInStockingPointInPeriod (PISPIP) 的回写数据。
*
* <p>每个产品在每个库存点每个周期的库存状态、流入流出、规格偏差。</p>
*/
public class PispipResult {
private String productId;
private String spId;
private int periodIndex;
private String periodStartDate;
// ===== 库存状态 =====
/** 期初库存 (第0周期=初始库存, 其后=上周期 InvQty) */
private double openingInventory;
/** 期末库存 (InvQty) */
private double endingInventory;
// ===== 库存规格偏差 =====
/** 目标库存水平 (绝对数量) */
private Double targetInventoryLevel;
/** 目标库存天数 (安全库存天数模式) */
private Double targetInventoryDays;
/** 低于目标库存量 (InvQtyUnderTarget) */
private double belowTarget;
/** 最小库存水平 (绝对数量) */
private Double minInventoryLevel;
/** 最小库存天数 (安全库存天数模式) */
private Double minInventoryDays;
/** 低于最小库存量 (MinInvQtyUnder) */
private double belowMin;
/** 最大库存水平 */
private Double maxInventoryLevel;
/** 最大库存天数 */
private Double maxInventoryDays;
/** 超过最大库存量 (MaxInvQtyOver) */
private double aboveMax;
// ===== 流入 =====
/** 到货量 (生产完成到货, 含 lead time 偏移) */
private double productionArrived;
/** 在途到货量 (InTransitSupply) */
private double inTransitArrival;
/** 总流入量 */
private double totalInflow;
// ===== 流出 =====
/** 销售需求量 (外部需求) */
private double salesDemandQty;
/** 销售满足量 */
private double salesFulfilledQty;
/** BOM 依赖需求量 (被其他工序消耗) */
private double dependentDemandQty;
/** 总流出量 */
private double totalOutflow;
// ===== 需求满足 =====
/** 需求满足量 (DemandFulfillment = SalesDemandQty + BOM消耗, 用于安全库存天数) */
private double demandFulfillment;
/** 需求松弛 (DemandSlack) */
private double demandSlack;
// ===== 库存变化详情 =====
/** 生产中 (当前周期 PTQty, 将来到货) */
private double productionInProgress;
/** 每个工序的产出明细 */
private final List<ProductionDetail> productionDetails = new ArrayList<>();
// ==================== 内嵌类 ====================
/** 工序产出明细 */
public static class ProductionDetail {
public String operationId;
public String operationName;
public String unitId;
public double quantity;
public int leadTimeDays;
/** 该工序的 BOM 输入物料消耗 */
public final List<BomConsumption> bomConsumptions = new ArrayList<>();
}
/** BOM 物料消耗 */
public static class BomConsumption {
public String inputProductId;
public String inputSpId;
public double factor;
public double consumedQty;
}
// ==================== Getters / Setters ====================
public String getProductId() { return productId; }
public void setProductId(String v) { this.productId = v; }
public String getSpId() { return spId; }
public void setSpId(String v) { this.spId = 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 getOpeningInventory() { return openingInventory; }
public void setOpeningInventory(double v) { this.openingInventory = v; }
public double getEndingInventory() { return endingInventory; }
public void setEndingInventory(double v) { this.endingInventory = v; }
public Double getTargetInventoryLevel() { return targetInventoryLevel; }
public void setTargetInventoryLevel(Double v) { this.targetInventoryLevel = v; }
public Double getTargetInventoryDays() { return targetInventoryDays; }
public void setTargetInventoryDays(Double v) { this.targetInventoryDays = v; }
public double getBelowTarget() { return belowTarget; }
public void setBelowTarget(double v) { this.belowTarget = v; }
public Double getMinInventoryLevel() { return minInventoryLevel; }
public void setMinInventoryLevel(Double v) { this.minInventoryLevel = v; }
public Double getMinInventoryDays() { return minInventoryDays; }
public void setMinInventoryDays(Double v) { this.minInventoryDays = v; }
public double getBelowMin() { return belowMin; }
public void setBelowMin(double v) { this.belowMin = v; }
public Double getMaxInventoryLevel() { return maxInventoryLevel; }
public void setMaxInventoryLevel(Double v) { this.maxInventoryLevel = v; }
public Double getMaxInventoryDays() { return maxInventoryDays; }
public void setMaxInventoryDays(Double v) { this.maxInventoryDays = v; }
public double getAboveMax() { return aboveMax; }
public void setAboveMax(double v) { this.aboveMax = v; }
public double getProductionArrived() { return productionArrived; }
public void setProductionArrived(double v) { this.productionArrived = v; }
public double getInTransitArrival() { return inTransitArrival; }
public void setInTransitArrival(double v) { this.inTransitArrival = v; }
public double getTotalInflow() { return totalInflow; }
public void setTotalInflow(double v) { this.totalInflow = v; }
public double getSalesDemandQty() { return salesDemandQty; }
public void setSalesDemandQty(double v) { this.salesDemandQty = v; }
public double getSalesFulfilledQty() { return salesFulfilledQty; }
public void setSalesFulfilledQty(double v) { this.salesFulfilledQty = v; }
public double getDependentDemandQty() { return dependentDemandQty; }
public void setDependentDemandQty(double v) { this.dependentDemandQty = v; }
public double getTotalOutflow() { return totalOutflow; }
public void setTotalOutflow(double v) { this.totalOutflow = v; }
public double getDemandFulfillment() { return demandFulfillment; }
public void setDemandFulfillment(double v) { this.demandFulfillment = v; }
public double getDemandSlack() { return demandSlack; }
public void setDemandSlack(double v) { this.demandSlack = v; }
public double getProductionInProgress() { return productionInProgress; }
public void setProductionInProgress(double v) { this.productionInProgress = v; }
public List<ProductionDetail> getProductionDetails() { return productionDetails; }
}
\ No newline at end of file
This diff is collapsed.
...@@ -43,7 +43,8 @@ public class PlanResultServiceTest { ...@@ -43,7 +43,8 @@ public class PlanResultServiceTest {
// planResultService.execute2("64E64F6B68094AF38CEDC418630C3CC2");//2000 // planResultService.execute2("64E64F6B68094AF38CEDC418630C3CC2");//2000
// planResultService.execute2("E1448B3C9C8743DEAB39708F2CFE348A");//倒排bomces // planResultService.execute2("E1448B3C9C8743DEAB39708F2CFE348A");//倒排bomces
planResultService.execute2("85DA28EC5F4643449E65A51253D5F127"); //bom 多半成品 // planResultService.execute2("85DA28EC5F4643449E65A51253D5F127"); //bom 多半成品
planResultService.execute2("B288477F1A594DB584C87EEA77880AA3");
// planResultService.execute2("FB14AF1623CB461D9BEDAFC35DBF0AFC"); //重叠 // planResultService.execute2("FB14AF1623CB461D9BEDAFC35DBF0AFC"); //重叠
......
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