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

MP

parent 48800682
......@@ -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"))+"-";
// 确保目录存在
java.io.File logDir = new java.io.File(LOG_FILE_PATH);
java.io.File logDir = new java.io.File(fileDir);
if (!logDir.exists()) {
logDir.mkdirs(); // 创建目录(包括父目录)
}
String filePath = LOG_FILE_PATH + date + fileName;
String filePath = fileDir + date + fileName;
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) {
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.controller;
import com.aps.common.util.ParamValidator;
import com.aps.common.util.R;
import com.aps.macroplanner.MacroPlannerOptimizer;
import com.aps.macroplanner.data.DataValidator;
import com.aps.macroplanner.data.MacroPlannerDataConverter;
import com.aps.macroplanner.data.TestDataBuilder;
import com.aps.macroplanner.output.ResultWriter;
import com.aps.macroplanner.output.dto.*;
import com.aps.service.MacroPlannerResultService;
import com.google.ortools.Loader;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.*;
import java.util.stream.Collectors;
/**
* MP排产结果前端接口 — 提供排产优化运行、结果查询的完整 API。
*
* <h3>接口一览</h3>
* <table>
* <tr><th>方法</th><th>路径</th><th>说明</th></tr>
* <tr><td>POST</td><td>/macroResult/run?sceneId=xxx</td><td>触发排产优化: 转换→验证→建模→分层求解→保存JSON</td></tr>
* <tr><td>GET </td><td>/macroResult/salesDemands?sceneId=xxx</td><td>订单级需求满足明细</td></tr>
* <tr><td>GET </td><td>/macroResult/supplyChain?sceneId=xxx</td><td>产品级库存流转与销售满足汇总</td></tr>
* <tr><td>GET </td><td>/macroResult/productSummary?sceneId=xxx</td><td>产品-库位跨周期库存汇总</td></tr>
* <tr><td>GET </td><td>/macroResult/unitCapacity?sceneId=xxx</td><td>设备产能使用情况(含利用率)</td></tr>
* <tr><td>GET </td><td>/macroResult/productNetwork?sceneId=xxx</td><td>BOM生产网络</td></tr>
* <tr><td>GET </td><td>/macroResult/summary?sceneId=xxx</td><td>KPI与求解统计</td></tr>
* </table>
*
* <h3>典型调用流程</h3>
* <ol>
* <li>POST /run → 触发求解, 返回 status:"SUCCESS"</li>
* <li>GET /salesDemands → 查看订单需求满足</li>
* <li>GET /supplyChain → 查看库存与销售满足</li>
* <li>GET /unitCapacity → 查看设备产能使用</li>
* <li>GET /productNetwork → 查看BOM网络</li>
* <li>GET /summary → 查看KPI与求解统计</li>
* </ol>
*/
@RestController
@RequestMapping("/macroResult")
@Tag(name = "MP排产结果", description = "MP宏观排产: 运行优化 & 查询物料供应链、产能、生产网络、KPI 结果")
public class MacroPlannerResultController {
@Autowired
private MacroPlannerResultService macroPlannerResultService;
@Autowired
private MacroPlannerDataConverter macroPlannerDataConverter;
/**
* 加载结果文件, 校验 sceneId 和文件存在性。
* @return OptimizationResult, 或 null (需要调用方返回 R.failed)
*/
private OptimizationResult loadResult(String sceneId) {
if (sceneId == null || sceneId.trim().isEmpty()) {
return null;
}
return macroPlannerResultService.getResult(sceneId.trim());
}
// ==================== 拆分接口 ====================
/**
* 获取订单级需求满足情况: 每条 SalesDemand 的需求量/满足量/满足率。
*
* <p>每条记录对应一个 SalesDemand (productId + spId + periodIndex 唯一键),
* 即一条订单级需求。可按 productId / periodIndex 可选过滤。</p>
*
* <h3>返回字段说明</h3>
* <table>
* <tr><td>totalCount</td><td>订单级需求总条数</td></tr>
* <tr><td>totalDemand / totalFulfilled / totalUnmet</td><td>全部订单需求量/满足量/缺口合计</td></tr>
* <tr><td>overallFulfillmentRate</td><td>整体满足率 = totalFulfilled / totalDemand</td></tr>
* <tr><td>orders</td><td>SalesDemandResult[] 明细</td></tr>
* <tr><td>orders[].salesDemandId</td><td>键: productId_spId_periodIndex</td></tr>
* <tr><td>orders[].demandQty / fulfilledQty / unmetQty</td><td>需求量/满足量/缺口</td></tr>
* <tr><td>orders[].fulfillmentRate</td><td>该订单满足率 (0.0~1.0)</td></tr>
* <tr><td>orders[].priority</td><td>需求优先级</td></tr>
* </table>
*/
@GetMapping("/salesDemands")
@Operation(summary = "订单需求满足",
description = "订单级需求满足明细: 每条SalesDemand的需求量/满足量/缺口/满足率/优先级。"
+ "支持按 productId / periodIndex 可选过滤。")
public R<Map<String, Object>> getSalesDemands(
@RequestParam("sceneId") @Parameter(description = "场景ID", required = true) String sceneId,
@RequestParam(value = "productId", required = false) @Parameter(description = "按产品ID过滤(可选)") String productId,
@RequestParam(value = "periodIndex", required = false) @Parameter(description = "按周期索引过滤(可选)") Integer periodIndex) {
OptimizationResult result = loadResult(sceneId);
if (result == null) {
return R.failed("未找到场景 " + sceneId + " 的排产结果文件");
}
List<SalesDemandResult> all = result.getSalesDemands();
// 可选过滤
List<SalesDemandResult> filtered = all.stream()
.filter(s -> productId == null || productId.isEmpty() || productId.equals(s.getProductId()))
.filter(s -> periodIndex == null || periodIndex == s.getPeriodIndex())
.collect(Collectors.toList());
// 汇总统计
double totalDemand = filtered.stream().mapToDouble(SalesDemandResult::getDemandQty).sum();
double totalFulfilled = filtered.stream().mapToDouble(SalesDemandResult::getFulfilledQty).sum();
double totalUnmet = filtered.stream().mapToDouble(SalesDemandResult::getUnmetQty).sum();
Map<String, Object> data = new LinkedHashMap<>();
data.put("totalCount", filtered.size());
data.put("totalDemand", totalDemand);
data.put("totalFulfilled", totalFulfilled);
data.put("totalUnmet", totalUnmet);
data.put("overallFulfillmentRate", totalDemand > 0 ? totalFulfilled / totalDemand : 1.0);
data.put("orders", filtered);
return R.ok(data);
}
/**
* 获取产品级物料供应链视图: 各产品-库位-周期的库存流转(期初/期末/流入/流出/偏差)与按产品聚合的销售满足。
*
* <h3>返回字段说明</h3>
* <table>
* <tr><td>totalPispipRecords</td><td>库存点库存总记录数</td></tr>
* <tr><td>productCount / productIds</td><td>产品数量与ID列表</td></tr>
* <tr><td>periodRange</td><td>周期范围 [min, max]</td></tr>
* <tr><td>productSummaries</td><td>按产品+库位聚合的跨周期库存汇总</td></tr>
* <tr><td>productSummaries[].initialInventory</td><td>首周期期初库存</td></tr>
* <tr><td>productSummaries[].finalInventory</td><td>末周期期末库存</td></tr>
* <tr><td>productSummaries[].totalInflow</td><td>总流入(生产到货+在途到货)</td></tr>
* <tr><td>productSummaries[].totalOutflow</td><td>总流出(销售消耗+BOM依赖消耗)</td></tr>
* <tr><td>productSummaries[].belowTarget</td><td>低于目标库存累计值</td></tr>
* <tr><td>productSummaries[].belowMin / aboveMax</td><td>违反最小/最大库存惩罚</td></tr>
* <tr><td>salesDemandCount</td><td>销售需求总记录数</td></tr>
* <tr><td>salesSummaries</td><td>按产品聚合的销售需求满足</td></tr>
* <tr><td>salesSummaries[].totalDemand / totalFulfilled / totalUnmet</td><td>需求量/满足量/缺口</td></tr>
* <tr><td>salesSummaries[].avgFulfillmentRate</td><td>平均满足率 (0.0~1.0)</td></tr>
* <tr><td>pispipDetails</td><td>全量库存点库存明细,前端可按周期过滤</td></tr>
* </table>
*/
@GetMapping("/supplyChain")
@Operation(summary = "物料供应链",
description = "各产品-库位-周期的库存流转(期初/期末/流入/流出)与按产品聚合的销售需求满足情况。"
+ "数据来源: PispipResult(库存点库存) + SalesDemandResult(销售需求)。"
+ "注: 如需订单级需求满足明细, 请使用 /salesDemands 接口。")
public R<Map<String, Object>> getSupplyChain(
@RequestParam("sceneId") @Parameter(description = "场景ID", required = true) String sceneId) {
OptimizationResult result = loadResult(sceneId);
if (result == null) {
return R.failed("未找到场景 " + sceneId + " 的排产结果文件");
}
return R.ok(buildSupplyChain(result));
}
/**
* 获取产品级库存汇总: 按 (productId, spId) 聚合跨周期库存流转。
*
* <h3>返回字段说明</h3>
* <table>
* <tr><td>totalCount</td><td>产品-库位总数</td></tr>
* <tr><td>totalPispipRecords</td><td>库存点库存原始记录数</td></tr>
* <tr><td>summaries</td><td>按产品+库位聚合的库存汇总</td></tr>
* <tr><td>summaries[].productId / spId</td><td>产品ID / 库存点ID</td></tr>
* <tr><td>summaries[].periodCount</td><td>周期数</td></tr>
* <tr><td>summaries[].initialInventory</td><td>首周期期初库存</td></tr>
* <tr><td>summaries[].finalInventory</td><td>末周期期末库存</td></tr>
* <tr><td>summaries[].totalInflow</td><td>总流入(生产到货+在途到货)</td></tr>
* <tr><td>summaries[].totalOutflow</td><td>总流出(销售消耗+BOM依赖消耗)</td></tr>
* <tr><td>summaries[].totalProduction</td><td>生产到货合计</td></tr>
* <tr><td>summaries[].totalInTransit</td><td>在途到货合计</td></tr>
* <tr><td>summaries[].belowTarget</td><td>低于目标库存累计值</td></tr>
* <tr><td>summaries[].belowMin / aboveMax</td><td>违反最小/最大库存惩罚</td></tr>
* </table>
*/
@GetMapping("/productSummary")
@Operation(summary = "产品库存汇总",
description = "按产品+库位聚合跨周期库存汇总: 期初/期末库存、总流入/流出、生产到货、库存偏差。"
+ "数据来源: PispipResult(库存点库存)。"
+ "注: 如需订单级需求满足明细, 请使用 /salesDemands 接口。")
public R<Map<String, Object>> getProductSummary(
@RequestParam("sceneId") @Parameter(description = "场景ID", required = true) String sceneId) {
OptimizationResult result = loadResult(sceneId);
if (result == null) {
return R.failed("未找到场景 " + sceneId + " 的排产结果文件");
}
List<PispipResult> pispips = result.getPispips();
List<Map<String, Object>> summaries = buildProductSummaries(pispips);
Map<String, Object> data = new LinkedHashMap<>();
data.put("totalCount", summaries.size());
data.put("PispipRecords", pispips);
//data.put("summaries", summaries);
return R.ok(data);
}
/**
* 获取unit产能使用情况: 按设备聚合各周期的产能占用和生产量。
*
* <h3>返回字段说明</h3>
* <table>
* <tr><td>unitCount</td><td>设备总数</td></tr>
* <tr><td>totalTaskCount</td><td>生产任务总条数</td></tr>
* <tr><td>units</td><td>设备产能数组</td></tr>
* <tr><td>units[].unitId</td><td>设备ID (EQUIP_xxx)</td></tr>
* <tr><td>units[].operationCount / operationIds</td><td>工序数/工序ID列表</td></tr>
* <tr><td>units[].totalCapacityUsed</td><td>该设备全部周期产能占用合计(小时)</td></tr>
* <tr><td>units[].totalProduction</td><td>该设备全部周期产出合计</td></tr>
* <tr><td>units[].capacityByPeriod</td><td>每周期产能占用 Map{periodIndex: hours}</td></tr>
* <tr><td>units[].periodTasks</td><td>每个工序×周期的生产任务明细(PeriodTaskResult)</td></tr>
* </table>
*/
@GetMapping("/unitCapacity")
@Operation(summary = "Unit产能",
description = "按设备(unitId)聚合各周期的产能使用量(小时)与生产任务明细。"
+ "数据来源: PeriodTaskResult(每工序-设备-周期的生产量和产能占比)。")
public R<Map<String, Object>> getUnitCapacity(
@RequestParam("sceneId") @Parameter(description = "场景ID", required = true) String sceneId) {
OptimizationResult result = loadResult(sceneId);
if (result == null) {
return R.failed("未找到场景 " + sceneId + " 的排产结果文件");
}
return R.ok(buildUnitCapacity(result));
}
/**
* 获取产品生产网络: 成品向下递归展开完整 BOM 供应链树。
*
* <h3>返回字段说明 (ProductNetworkResult)</h3>
* <table>
* <tr><td>finishedGoods</td><td>成品根节点列表 (SupplyChainNode[])</td></tr>
* <tr><td>finishedGoods[].productId / spId</td><td>产品ID / 库存点ID</td></tr>
* <tr><td>finishedGoods[].level</td><td>层级 (0=成品, 1=半成品, N=原材料)</td></tr>
* <tr><td>finishedGoods[].supplySources</td><td>供应来源 (PRODUCTION/INVENTORY/PURCHASE)</td></tr>
* <tr><td>finishedGoods[].consumers</td><td>被哪些上层产品消耗</td></tr>
* <tr><td>finishedGoods[].summary</td><td>{@link SupplySummary} 跨周期供应汇总</td></tr>
* <tr><td>finishedGoods[].children</td><td>BOM子节点 (childProductId, factor, node)</td></tr>
* <tr><td>allNodes</td><td>全部节点扁平索引 Map</td></tr>
* </table>
*/
@GetMapping("/productNetwork")
@Operation(summary = "产品生产网络",
description = "成品→半成品→原材料完整BOM供应链树。"
+ "包含各层级的供应来源、消耗者、跨周期供应汇总。")
public R<ProductNetworkResult> getProductNetwork(
@RequestParam("sceneId") @Parameter(description = "场景ID", required = true) String sceneId) {
OptimizationResult result = loadResult(sceneId);
if (result == null) {
return R.failed("未找到场景 " + sceneId + " 的排产结果文件");
}
return R.ok(result.getProductNetwork());
}
/**
* 获取求解摘要: KPI 目标值与各子项明细 + 求解器运行统计。
*
* <h3>返回字段说明</h3>
* <table>
* <tr><td>timestamp / solver / version</td><td>运行时间戳 / 求解器名称 / 版本</td></tr>
* <tr><td>kpis.objectiveValue</td><td>总目标值(所有KPI加权惩罚之和)</td></tr>
* <tr><td>kpis.entries[].name / rawValue / weight / penalty</td><td>KPI名称/原始值/权重/加权惩罚</td></tr>
* <tr><td>statistics.status</td><td>OPTIMAL / FEASIBLE / INFEASIBLE</td></tr>
* <tr><td>statistics.numVariables / numConstraints</td><td>变量数 / 约束数</td></tr>
* <tr><td>statistics.elapsedSeconds</td><td>求解耗时(秒)</td></tr>
* <tr><td>statistics.objectiveValue / bestBound / gap</td><td>目标值/下界/最优性间隙(0.0=最优)</td></tr>
* <tr><td>statistics.iterations</td><td>迭代次数</td></tr>
* </table>
*/
@GetMapping("/summary")
@Operation(summary = "KPI与求解统计",
description = "KPI目标值与各子项明细(需求缺口/产能超载/批次偏差/库存偏差等) + "
+ "求解器状态/变量规模/约束规模/耗时/gap。")
public R<Map<String, Object>> getSummary(
@RequestParam("sceneId") @Parameter(description = "场景ID", required = true) String sceneId) {
OptimizationResult result = loadResult(sceneId);
if (result == null) {
return R.failed("未找到场景 " + sceneId + " 的排产结果文件");
}
return R.ok(buildSummary(result));
}
/**
* 运行MP排产优化: 数据库转换 → 验证 → 建模 → 分层求解 → 保存结果。
*
* <p>对应 {@code MacroPlannerDataConverterRunner} 的 Web API 版本。
* 求解完成后结果自动保存到 {@code mp/result/optimization_result_{sceneId}.json},
* 前端可通过查询接口读取。</p>
*
* <h3>返回字段说明</h3>
* <table>
* <tr><td>sceneId</td><td>场景ID</td></tr>
* <tr><td>dataSummary</td><td>数据规模: products/operations/routings/periods 等数量</td></tr>
* <tr><td>validation.valid</td><td>数据验证是否通过</td></tr>
* <tr><td>validation.errors / warnings</td><td>验证错误/警告详情</td></tr>
* <tr><td>status</td><td>SUCCESS / VALIDATION_FAILED / SAVE_FAILED / ERROR</td></tr>
* <tr><td>solveElapsedMs</td><td>求解耗时(毫秒)</td></tr>
* <tr><td>totalElapsedMs</td><td>总耗时(含转换+验证, 毫秒)</td></tr>
* <tr><td>resultFile</td><td>结果文件路径</td></tr>
* </table>
*
* <h3>status 枚举</h3>
* <ul>
* <li>SUCCESS — 求解完成, 结果已保存</li>
* <li>VALIDATION_FAILED — 数据验证未通过, 详见 validation.errors</li>
* <li>SAVE_FAILED — 求解完成但 JSON 写入失败</li>
* <li>ERROR — 运行时异常</li>
* </ul>
*/
@PostMapping("/run")
@Operation(summary = "运行MP排产优化",
description = "执行数据库转换→数据验证→MIP建模→分层求解→JSON结果保存的完整流程。"
+ "耗时通常数秒到数分钟, 取决于数据规模。")
public R<Map<String, Object>> runOptimization(
@RequestParam("sceneId") @Parameter(description = "场景ID", required = true) String sceneId) {
if (sceneId == null || sceneId.trim().isEmpty()) {
return R.failed("sceneId不能为空");
}
String sid = sceneId.trim();
Map<String, Object> result = new LinkedHashMap<>();
result.put("sceneId", sid);
long t0 = System.currentTimeMillis();
try {
// 1. 数据转换: 数据库工艺物料类 → macroplanner 实体
TestDataBuilder data = macroPlannerDataConverter.convert(sid);
result.put("dataSummary", buildDataSummary(data));
// 2. 数据验证
DataValidator validator = new DataValidator(data);
boolean valid = validator.validate();
result.put("validation", buildValidationResult(validator, valid));
if (!valid) {
result.put("status", "VALIDATION_FAILED");
result.put("elapsedMs", System.currentTimeMillis() - t0);
return R.failed(result, "数据验证失败, 请检查错误详情");
}
// 3. 加载 native libraries → 构建模型 → 分层求解
Loader.loadNativeLibraries();
long solveStart = System.currentTimeMillis();
MacroPlannerOptimizer optimizer = new MacroPlannerOptimizer(data);
optimizer.buildModel();
optimizer.solve();
long solveEnd = System.currentTimeMillis();
// 4. 保存结果到 JSON 文件
ResultWriter writer = new ResultWriter(optimizer.getModel(), optimizer.getData(), solveStart);
boolean saved = writer.saveResultToFile(sid);
result.put("status", saved ? "SUCCESS" : "SAVE_FAILED");
result.put("solveElapsedMs", solveEnd - solveStart);
result.put("totalElapsedMs", System.currentTimeMillis() - t0);
result.put("resultFile", "mp/result/optimization_result_" + sid + ".json");
return R.ok(result);
} catch (Exception e) {
result.put("status", "ERROR");
result.put("error", e.getMessage());
result.put("elapsedMs", System.currentTimeMillis() - t0);
return R.failed(result, "求解失败: " + e.getMessage());
}
}
// ==================== 辅助构建方法 ====================
/**
* 构建供应链视图: 每个产品-库位的库存流转 + 销售满足情况。
*
* <p>数据来源:</p>
* <ul>
* <li>{@link PispipResult} — 每周期期初/期末库存, 流入(生产到货+在途), 流出(销售+BOM消耗)</li>
* <li>{@link SalesDemandResult} — 销售需求量 vs 满足量, 满足率</li>
* </ul>
*/
private Map<String, Object> buildSupplyChain(OptimizationResult result) {
Map<String, Object> sc = new LinkedHashMap<>();
// 1.1 按产品汇总库存视图
List<PispipResult> pispips = result.getPispips();
sc.put("totalPispipRecords", pispips.size());
// 提取产品列表
Set<String> productIds = pispips.stream()
.map(PispipResult::getProductId)
.filter(Objects::nonNull)
.collect(Collectors.toCollection(LinkedHashSet::new));
sc.put("productCount", productIds.size());
sc.put("productIds", productIds);
// 周期范围
int minPeriod = pispips.stream().mapToInt(PispipResult::getPeriodIndex).min().orElse(0);
int maxPeriod = pispips.stream().mapToInt(PispipResult::getPeriodIndex).max().orElse(0);
sc.put("periodRange", new int[]{minPeriod, maxPeriod});
// 1.2 按产品聚合库存汇总 (跨周期)
List<Map<String, Object>> productSummaries = buildProductSummaries(pispips);
sc.put("productSummaries", productSummaries);
// 1.3 销售需求满足
List<SalesDemandResult> sales = result.getSalesDemands();
List<Map<String, Object>> salesSummaries = buildSalesSummaries(sales);
sc.put("salesDemandCount", sales.size());
sc.put("salesSummaries", salesSummaries);
// 1.4 库存细节 (后端可返回全量, 前端可按需过滤)
sc.put("pispipDetails", pispips);
return sc;
}
/**
* 按产品聚合跨周期库存汇总。
*/
private List<Map<String, Object>> buildProductSummaries(List<PispipResult> pispips) {
// 按 productId@spId 分组
Map<String, List<PispipResult>> grouped = pispips.stream()
.collect(Collectors.groupingBy(p -> p.getProductId() + "@" + p.getSpId()));
List<Map<String, Object>> summaries = new ArrayList<>();
for (Map.Entry<String, List<PispipResult>> entry : grouped.entrySet()) {
List<PispipResult> records = entry.getValue();
PispipResult first = records.get(0);
double totalInflow = records.stream().mapToDouble(PispipResult::getTotalInflow).sum();
double totalOutflow = records.stream().mapToDouble(PispipResult::getTotalOutflow).sum();
double totalProduction = records.stream().mapToDouble(PispipResult::getProductionArrived).sum();
double totalInTransit = records.stream().mapToDouble(PispipResult::getInTransitArrival).sum();
double initialInv = records.stream()
.filter(r -> r.getPeriodIndex() == 0)
.mapToDouble(PispipResult::getOpeningInventory)
.findFirst().orElse(0);
double finalInv = records.stream()
.filter(r -> r.getPeriodIndex() == records.size() - 1)
.mapToDouble(PispipResult::getEndingInventory)
.findFirst().orElse(0);
double belowTarget = records.stream().mapToDouble(PispipResult::getBelowTarget).sum();
double belowMin = records.stream().mapToDouble(PispipResult::getBelowMin).sum();
double aboveMax = records.stream().mapToDouble(PispipResult::getAboveMax).sum();
Map<String, Object> summary = new LinkedHashMap<>();
summary.put("productId", first.getProductId());
summary.put("spId", first.getSpId());
summary.put("periodCount", records.size());
summary.put("initialInventory", initialInv);
summary.put("finalInventory", finalInv);
summary.put("totalInflow", totalInflow);
summary.put("totalOutflow", totalOutflow);
summary.put("totalProduction", totalProduction);
summary.put("totalInTransit", totalInTransit);
summary.put("belowTarget", belowTarget);
summary.put("belowMin", belowMin);
summary.put("aboveMax", aboveMax);
summaries.add(summary);
}
return summaries;
}
/**
* 销售需求汇总: 按产品聚合需求量/满足量/满足率。
*/
private List<Map<String, Object>> buildSalesSummaries(List<SalesDemandResult> sales) {
// 按 productId 分组
Map<String, List<SalesDemandResult>> grouped = sales.stream()
.collect(Collectors.groupingBy(SalesDemandResult::getProductId));
List<Map<String, Object>> summaries = new ArrayList<>();
for (Map.Entry<String, List<SalesDemandResult>> entry : grouped.entrySet()) {
List<SalesDemandResult> items = entry.getValue();
double totalDemand = items.stream().mapToDouble(SalesDemandResult::getDemandQty).sum();
double totalFulfilled = items.stream().mapToDouble(SalesDemandResult::getFulfilledQty).sum();
double totalUnmet = items.stream().mapToDouble(SalesDemandResult::getUnmetQty).sum();
double avgFulfillmentRate = items.stream()
.mapToDouble(SalesDemandResult::getFulfillmentRate)
.average().orElse(0);
Map<String, Object> summary = new LinkedHashMap<>();
summary.put("productId", entry.getKey());
summary.put("demandCount", items.size());
summary.put("totalDemand", totalDemand);
summary.put("totalFulfilled", totalFulfilled);
summary.put("totalUnmet", totalUnmet);
summary.put("avgFulfillmentRate", avgFulfillmentRate);
summaries.add(summary);
}
return summaries;
}
// ==================== 2. unit产能 ====================
/**
* 构建unit产能视图: 从 OptimisationResult.unitCapacities 读取,
* 由 ResultWriter.buildUnitCapacities() 在求解后预计算。
*按产品聚合库存汇总
* <p>数据来源: {@link UnitCapacityResult} — 按设备聚合, 含 maxCapacity / usedCapacity / 利用率。</p>
*/
private Map<String, Object> buildUnitCapacity(OptimizationResult result) {
Map<String, Object> uc = new LinkedHashMap<>();
List<UnitCapacityResult> unitCapacities = result.getUnitCapacities();
if (unitCapacities == null || unitCapacities.isEmpty()) {
uc.put("unitCount", 0);
uc.put("totalTaskCount", 0);
uc.put("units", Collections.emptyList());
return uc;
}
List<Map<String, Object>> unitList = new ArrayList<>();
int totalTasks = 0;
for (UnitCapacityResult ucr : unitCapacities) {
Map<String, Object> ud = new LinkedHashMap<>();
ud.put("unitId", ucr.getUnitId());
ud.put("operationIds", ucr.getOperationIds());
ud.put("totalMaxCapacity", ucr.getTotalMaxCapacity());
ud.put("totalCapacityUsed", ucr.getTotalCapacityUsed());
ud.put("totalUtilizationRate", ucr.getTotalUtilizationRate());
ud.put("totalProduction", ucr.getTotalProduction());
// 周期明细 (含利用率)
List<Map<String, Object>> pdList = new ArrayList<>();
for (UnitCapacityResult.UnitPeriodDetail pd : ucr.getPeriodDetails()) {
Map<String, Object> pdm = new LinkedHashMap<>();
pdm.put("periodIndex", pd.periodIndex);
pdm.put("periodStartDate", pd.periodStartDate);
pdm.put("maxCapacity", pd.maxCapacity);
pdm.put("capacityUsed", pd.capacityUsed);
pdm.put("utilizationRate", pd.utilization);
pdm.put("production", pd.production);
pdm.put("periodTasks", pd.tasks);
totalTasks += pd.tasks.size();
pdList.add(pdm);
}
ud.put("periodDetails", pdList);
unitList.add(ud);
}
uc.put("unitCount", unitCapacities.size());
uc.put("totalTaskCount", totalTasks);
uc.put("units", unitList);
return uc;
}
// ==================== 3. 汇总: KPI + 求解器统计 ====================
/**
* 构建摘要: KPI 汇总 + 求解器运行统计。
*/
private Map<String, Object> buildSummary(OptimizationResult result) {
Map<String, Object> summary = new LinkedHashMap<>();
// 元数据
summary.put("timestamp", result.getTimestamp());
summary.put("solver", result.getSolver());
summary.put("version", result.getVersion());
// KPI
KpiResult kpis = result.getKpis();
if (kpis != null) {
Map<String, Object> kpiData = new LinkedHashMap<>();
kpiData.put("objectiveValue", kpis.getObjectiveValue());
List<Map<String, String>> kpiList = new ArrayList<>();
for (KpiResult.KpiEntry e : kpis.getEntries()) {
Map<String, String> entryMap = new LinkedHashMap<>();
entryMap.put("name", e.name);
entryMap.put("rawValue", String.format("%.2f", e.rawValue));
entryMap.put("weight", String.format("%.1f", e.weight));
entryMap.put("penalty", String.format("%.2f", e.penalty));
kpiList.add(entryMap);
}
kpiData.put("entries", kpiList);
summary.put("kpis", kpiData);
}
// 求解器统计
SolverStatistics stats = result.getStatistics();
if (stats != null) {
Map<String, Object> statData = new LinkedHashMap<>();
statData.put("status", stats.getStatus());
statData.put("numVariables", stats.getNumVariables());
statData.put("numConstraints", stats.getNumConstraints());
statData.put("elapsedSeconds", stats.getElapsedSeconds());
statData.put("objectiveValue", stats.getObjectiveValue());
statData.put("bestBound", stats.getBestBound());
statData.put("gap", stats.getGap());
statData.put("iterations", stats.getIterations());
summary.put("statistics", statData);
}
return summary;
}
// ==================== runOptimization 辅助方法 ====================
/**
* 构建数据转换后的规模摘要。
*/
private Map<String, Object> buildDataSummary(TestDataBuilder data) {
Map<String, Object> summary = new LinkedHashMap<>();
summary.put("products", data.getProducts().size());
summary.put("stockingPoints", data.getStockingPoints().size());
summary.put("operations", data.getOperations().size());
summary.put("routings", data.getRoutings().size());
summary.put("operationInputs", data.getOperationInputs().size());
summary.put("initialInventories", data.getInitialInventories().size());
summary.put("inTransitSupplies", data.getInTransitSupplies().size());
summary.put("salesDemands", data.getSalesDemands().size());
summary.put("periods", data.getPeriods().size());
summary.put("unitPeriods", data.getUnitPeriods().size());
return summary;
}
/**
* 构建数据验证结果。
*/
private Map<String, Object> buildValidationResult(DataValidator validator, boolean valid) {
Map<String, Object> vr = new LinkedHashMap<>();
vr.put("valid", valid);
if (validator.hasErrors()) {
vr.put("errorCount", validator.getErrors().size());
vr.put("errors", validator.getErrors());
}
if (validator.hasWarnings()) {
vr.put("warningCount", validator.getWarnings().size());
vr.put("warnings", validator.getWarnings());
}
return vr;
}
}
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;
import com.aps.macroplanner.model.VariableFactory;
import com.aps.macroplanner.objective.ObjectiveBuilder;
import com.aps.macroplanner.objective.StrategyLevel;
import com.aps.macroplanner.output.ResultWriter;
import com.aps.macroplanner.output.SolutionPrinter;
import java.io.FileOutputStream;
......@@ -82,14 +83,18 @@ import java.util.logging.Logger;
public class MacroPlannerOptimizer {
/** LP 模型文件和日志文件的输出目录 */
private static final String LOG_DIR = "src/main/java/com/aps/log/";
private static final String LOG_DIR = "mp";
/** 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 汇总变量 */
......@@ -313,6 +318,13 @@ public class MacroPlannerOptimizer {
levelObjValues.add(r.optimalValue);
}
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 {
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 {
// 收集哪些产品有生产工序
Set<String> productsWithOperation = new HashSet<>();
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 {
}
// 工序不能消耗自己产出的产品 (自循环) — 仅当输入和输出在同一库存点时才报错
// 多工序路由中同一产品可经不同库存点流转 (如: 下料→SP_WIP1→粗加工→SP_WIP2→精加工→SP_FG)
if (op.getOutputProductId().equals(inputProd.getId())
&& op.getOutputSpId().equals(inputSp.getId())) {
if (op.producesProductAtSp(inputProd.getId(), inputSp.getId())) {
errors.add("工序 " + op.getId() + " 消耗自己产出的产品 " + inputProd.getId()
+ "@" + inputSp.getId() + " (自循环 BOM, 同一库存点)");
}
......@@ -179,15 +180,17 @@ public class DataValidator {
// 循环依赖检测 (含库存点: 仅当产品+库存点都匹配时才构成循环)
// 多工序路由中同一产品经不同库存点流转不构成循环
for (OperationInput input : data.getOperationInputs()) {
String consumerProd = input.getOperation().getOutputProductId();
String consumerSp = input.getOperation().getOutputSpId();
String consumedProd = input.getInputProduct().getId();
String consumedSp = input.getInputSp().getId();
// 遍历消费者工序的所有产出 (支持多产出/联产品)
for (OperationOutput consumerOutput : input.getOperation().getOutputs()) {
String consumerProd = consumerOutput.getProductId();
String consumerSp = consumerOutput.getSpId();
for (OperationInput other : data.getOperationInputs()) {
// 检查 other 是否产出 consumedProd@consumedSp 且消耗 consumerProd@consumerSp
if (other.getOperation().getOutputProductId().equals(consumedProd)
&& other.getOperation().getOutputSpId().equals(consumedSp)
if (other.getOperation().producesProductAtSp(consumedProd, consumedSp)
&& other.getInputProduct().getId().equals(consumerProd)
&& other.getInputSp().getId().equals(consumerSp)) {
errors.add("BOM 循环依赖: " + consumerProd + "@" + consumerSp
......@@ -197,6 +200,7 @@ public class DataValidator {
}
}
}
}
// ==================== 4. 工序→单元映射 ====================
......@@ -207,12 +211,14 @@ public class DataValidator {
}
for (Operation op : data.getOperations()) {
if (!unitIds.contains(op.getUnitId())) {
errors.add("工序 " + op.getId() + " 的单元 " + op.getUnitId()
for (UnitOperation uo : op.getUnitOperations()) {
if (!unitIds.contains(uo.getUnitId())) {
errors.add("工序 " + op.getId() + " 的单元 " + uo.getUnitId()
+ " 在 UnitPeriod 中不存在 (缺少产能定义)");
}
}
}
}
// ==================== 5. 引用完整性 ====================
......@@ -319,9 +325,10 @@ public class DataValidator {
// 有生产工序的产品
Set<String> productsWithOperation = new HashSet<>();
for (Operation op : data.getOperations()) {
productsWithOperation.add(op.getOutputProductId());
for (OperationOutput oo : op.getOutputs()) {
productsWithOperation.add(oo.getProductId());
}
}
// 被 BOM 消耗的产品
Set<String> productsConsumed = new HashSet<>();
for (OperationInput input : data.getOperationInputs()) {
......@@ -379,7 +386,9 @@ public class DataValidator {
// 有生产工序的产品
Set<String> productsWithOperation = new HashSet<>();
for (Operation op : data.getOperations()) {
productsWithOperation.add(op.getOutputProductId());
for (OperationOutput oo : op.getOutputs()) {
productsWithOperation.add(oo.getProductId());
}
}
// 在 BOM 中作为输入的产品
......
package com.aps.macroplanner.data;
import com.aps.common.util.ParamValidator;
import com.aps.entity.*;
import com.aps.entity.basic.Material;
import com.aps.entity.basic.MaterialSupply;
import com.aps.mapper.EquipCapacityDefMapper;
import com.aps.mapper.EquipinfoMapper;
import com.aps.mapper.ErpPurchaseOrderMapper;
import com.aps.mapper.MaterialInfoMapper;
import com.aps.mapper.MaterialPurchaseMapper;
import com.aps.mapper.MesShiftWorkSchedMapper;
import com.aps.mapper.PlanResourceMapper;
import com.aps.mapper.ProdEquipSpecialCalMapper;
import com.aps.mapper.ProdLaunchOrderMapper;
import com.aps.mapper.PurchaseReceiptMapper;
import com.aps.mapper.RoutingDetailEquipMapper;
import com.aps.mapper.RoutingHeaderMapper;
import com.aps.mapper.RoutingsupportingMapper;
import com.aps.mapper.SjzPfWhStockMapper;
import com.aps.mapper.StockMapper;
import com.aps.service.ApsTimeConfigService;
import com.aps.service.LanuchService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 数据库工艺物料类 → macroplanner 实体转换器。
*
* <p>将 {@code PlanResultService.execute2} 处理的数据库"工艺物料类"实体
* ({@link RoutingHeader}/{@link RoutingDetail}/{@link Routingsupporting}/
* {@link Material}/{@link Stock} 等)转换为 {@link RoutingTestDataBuilder}
* 所用的 macroplanner 实体({@link Product}/{@link Operation}/{@link Routing}/
* {@link OperationInput}/{@link StockingPoint}/{@link InitialInventory}/
* {@link InTransitSupply}),返回填充好的 {@link TestDataBuilder},
* 供 {@code MacroPlannerOptimizer} 直接消费。</p>
*
* <h3>设计要点</h3>
* <pre>
* 1. 独立的 Spring @Service,接收 sceneId 自行查询数据库
* 2. 通过同包静态内部类 Sink extends TestDataBuilder 访问 protected 字段
* 3. 利用 Routing.expand() 自动生成工序间 WIP,只手动补 MP 原材料投料
* 4. 消耗因子 factor = Routingsupporting.spentQty / Routingsupporting.mainQty
* 5. Period/UnitPeriod/SalesDemand/InventorySpec/SupplySpec/KPIWeights 用默认值或从订单推导
* </pre>
*
* <h3>核心映射关系</h3>
* <pre>
* Product ← Material (MaterialInfo)
* StockingPoint ← Stock.storeId / Routingsupporting.storeId
* Operation ← RoutingDetail (无产出, 由 Routing.expand() 配置)
* Routing ← RoutingHeader + RoutingDetail 列表 (调用 expand())
* OperationInput ← Routingsupporting (仅 MP 原材料投料)
* InitialInventory ← Material.materialStocks (Stock)
* InTransitSupply ← Material.InTransit (MaterialSupply)
* SalesDemand ← ProdLaunchOrder
* </pre>
*/
@Service
@Slf4j
public class MacroPlannerDataConverter {
/** 默认周期数 (一周) */
private static final int DEFAULT_PERIOD_COUNT = 7;
/** 默认单元最大产能 (小时/天) */
private static final double DEFAULT_UNIT_MAX_CAPACITY = 24.0;
/** 默认宽松库存上限 */
private static final double LOOSE_MAX = 999999.0;
// ==================== 注入的 Mapper ====================
@Autowired
private ProdLaunchOrderMapper prodLaunchOrderMapper;
@Autowired
private RoutingHeaderMapper routingHeaderMapper;
@Autowired
private RoutingsupportingMapper routingsupportingMapper;
@Autowired
private MaterialInfoMapper materialInfoMapper;
@Autowired
private StockMapper stockMapper;
@Autowired
private MaterialPurchaseMapper materialPurchaseMapper;
@Autowired
private ErpPurchaseOrderMapper erpPurchaseOrderMapper;
@Autowired
private PurchaseReceiptMapper purchaseReceiptMapper;
@Autowired
private SjzPfWhStockMapper sjzPfWhStockMapper;
@Autowired
private RoutingDetailEquipMapper routingDetailEquipMapper;
@Autowired
private EquipinfoMapper equipinfoMapper;
@Autowired
private PlanResourceMapper planResourceMapper;
@Autowired
private ProdEquipSpecialCalMapper prodEquipSpecialCalMapper;
@Autowired
private MesShiftWorkSchedMapper mesShiftWorkSchedMapper;
@Autowired
private EquipCapacityDefMapper equipCapacityDefMapper;
// ==================== 注入的 Service ====================
@Autowired
private LanuchService lanuchService;
@Autowired
private ApsTimeConfigService apsTimeConfigService;
// ==================== 公开入口 ====================
/**
* 将指定场景的数据库工艺物料数据转换为 macroplanner 的 TestDataBuilder。
*
* @param sceneId 场景ID
* @return 填充好的 TestDataBuilder, 可直接传给 MacroPlannerOptimizer
*/
public TestDataBuilder convert(String sceneId) {
log.info("开始转换场景数据到 macroplanner: sceneId={}", sceneId);
ConvertContext ctx = loadRawData(sceneId);
buildMaterials(ctx);
buildDailyCapacityByUnitId(ctx);
Sink sink = new Sink();
sink.fill(ctx);
log.info("转换完成: products={}, stockingPoints={}, operations={}, routings={}, operationInputs={}, "
+ "initialInventories={}, inTransitSupplies={}, salesDemands={}",
sink.getProducts().size(), sink.getStockingPoints().size(),
sink.getOperations().size(), sink.getRoutings().size(),
sink.getOperationInputs().size(), sink.getInitialInventories().size(),
sink.getInTransitSupplies().size(), sink.getSalesDemands().size());
return sink;
}
// ==================== 步骤1: 数据加载 ====================
private ConvertContext loadRawData(String sceneId) {
ConvertContext ctx = new ConvertContext();
// 1. 订单 (按 sceneId 过滤)
ctx.prodLaunchOrders = prodLaunchOrderMapper.selectList(
new LambdaQueryWrapper<ProdLaunchOrder>()
.eq(ProdLaunchOrder::getSceneId, sceneId));
log.info("加载订单: {} 条", ctx.prodLaunchOrders.size());
// 2. 收集 routingIds
List<Integer> routingIds = ctx.prodLaunchOrders.stream()
.map(ProdLaunchOrder::getRoutingId)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
// 3. 工艺路线头表
if (!routingIds.isEmpty()) {
ctx.routingHeaders = routingHeaderMapper.selectList(
new LambdaQueryWrapper<RoutingHeader>()
.in(RoutingHeader::getId, routingIds));
}
log.info("加载工艺路线: {} 条", ctx.routingHeaders.size());
// 4. 工序 (通过 LanuchService 批量查询)
if (!routingIds.isEmpty()) {
List<Long> routingIdsLong = routingIds.stream()
.map(Long::valueOf)
.collect(Collectors.toList());
List<RoutingDetail> details = lanuchService.getRoutingDetails(routingIdsLong);
if (details != null) {
ctx.routingDetails = details;
}
}
log.info("加载工序: {} 条", ctx.routingDetails.size());
// 5. 工艺物料消耗 (BOM)
if (!routingIds.isEmpty()) {
ctx.routingsupportings = routingsupportingMapper.selectList(
new LambdaQueryWrapper<Routingsupporting>()
.in(Routingsupporting::getRoutingHeaderId, routingIds)
.eq(Routingsupporting::getIsdeleted, 0));
}
log.info("加载工艺物料消耗: {} 条", ctx.routingsupportings.size());
// 6. 收集所有 materialId (订单 + 工艺物料消耗 + 工艺路线头表)
Set<String> materialIds = new HashSet<>();
ctx.prodLaunchOrders.forEach(o -> {
if (o.getMaterialId() != null) materialIds.add(o.getMaterialId());
});
ctx.routingsupportings.forEach(rs -> {
if (rs.getMaterialId() != null) materialIds.add(rs.getMaterialId());
});
ctx.routingHeaders.forEach(rh -> {
if (rh.getMaterialId() != null) materialIds.add(rh.getMaterialId());
});
// 7. 物料主数据
if (!materialIds.isEmpty()) {
ctx.materialInfos = materialInfoMapper.selectList(
new LambdaQueryWrapper<MaterialInfo>()
.in(MaterialInfo::getId, materialIds));
}
log.info("加载物料主数据: {} 条", ctx.materialInfos.size());
// 8. 库存、采购、在途 (按 materialId 过滤, 避免全表扫描)
if (!materialIds.isEmpty()) {
ctx.stocks = stockMapper.selectList(
new LambdaQueryWrapper<Stock>()
.in(Stock::getMaterialId, materialIds)
.eq(Stock::getIsdeleted, 0));
ctx.materialPurchases = materialPurchaseMapper.selectList(
new LambdaQueryWrapper<MaterialPurchase>()
.in(MaterialPurchase::getMaterialId, materialIds)
.eq(MaterialPurchase::getIsdeleted, 0));
ctx.erpPurchaseOrders = erpPurchaseOrderMapper.selectList(
new LambdaQueryWrapper<ErpPurchaseOrder>()
.in(ErpPurchaseOrder::getMaterialId, materialIds)
.eq(ErpPurchaseOrder::getIsdeleted, 0));
ctx.purchaseReceipts = purchaseReceiptMapper.selectList(
new LambdaQueryWrapper<PurchaseReceipt>()
.in(PurchaseReceipt::getMaterialid, materialIds)
.eq(PurchaseReceipt::getIsdeleted, 0));
ctx.sjzPfWhStocks = sjzPfWhStockMapper.selectList(
new LambdaQueryWrapper<SjzPfWhStock>()
.in(SjzPfWhStock::getMaterialid, materialIds)
.eq(SjzPfWhStock::getIsdeleted, 0));
}
log.info("加载库存: {}, 采购: {}, ERP采购订单: {}, 待验: {}, 半成品在途: {}",
ctx.stocks.size(), ctx.materialPurchases.size(),
ctx.erpPurchaseOrders.size(), ctx.purchaseReceipts.size(), ctx.sjzPfWhStocks.size());
// 9. RoutingDetailEquip (工序-设备关联), 按 routingDetailId 加载
if (!ctx.routingDetails.isEmpty()) {
List<Long> detailIds = ctx.routingDetails.stream()
.map(RoutingDetail::getId)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
if (!detailIds.isEmpty()) {
ctx.routingDetailEquips = routingDetailEquipMapper.selectList(
new LambdaQueryWrapper<RoutingDetailEquip>()
.in(RoutingDetailEquip::getRoutingDetailId, detailIds)
.eq(RoutingDetailEquip::getIsdeleted, 0));
}
}
log.info("加载工序设备关联: {} 条", ctx.routingDetailEquips.size());
// 10. 设备数据: Equipinfo + PlanResource (按 RoutingDetailEquip.equipId 过滤)
Set<Long> equipIds = ctx.routingDetailEquips.stream()
.map(RoutingDetailEquip::getEquipId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
if (!equipIds.isEmpty()) {
ctx.planResources = planResourceMapper.selectList(
new LambdaQueryWrapper<PlanResource>()
.in(PlanResource::getId, equipIds)
.eq(PlanResource::getIsdeleted, false));
}
// 通过 PlanResource.referenceId 查询 Equipinfo
Set<Integer> equipinfoIds = ctx.planResources.stream()
.map(PlanResource::getReferenceId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
if (!equipinfoIds.isEmpty()) {
ctx.equipinfos = equipinfoMapper.selectList(
new LambdaQueryWrapper<Equipinfo>()
.in(Equipinfo::getId, equipinfoIds)
.eq(Equipinfo::getIsdeleted, false));
}
log.info("加载设备资源: {}, 设备信息: {}", ctx.planResources.size(), ctx.equipinfos.size());
// 11. 设备日历: ProdEquipSpecialCal + MesShiftWorkSched + EquipCapacityDef
ctx.prodEquipSpecialCals = prodEquipSpecialCalMapper.selectList(
new LambdaQueryWrapper<ProdEquipSpecialCal>()
.eq(ProdEquipSpecialCal::getSceneId, sceneId));
ctx.mesShiftWorkScheds = mesShiftWorkSchedMapper.selectList(
new LambdaQueryWrapper<MesShiftWorkSched>()
.eq(MesShiftWorkSched::getIsdeleted, 0));
// EquipCapacityDef 按 PlanResource.referenceId 筛选
if (!equipinfoIds.isEmpty()) {
ctx.equipCapacityDefs = equipCapacityDefMapper.selectList(
new LambdaQueryWrapper<EquipCapacityDef>()
.in(EquipCapacityDef::getReferenceId, equipinfoIds)
.eq(EquipCapacityDef::getIsDeleted, 0));
}
log.info("加载设备日历: 特殊日历={}, 班次模板={}, 产能定义={}",
ctx.prodEquipSpecialCals.size(), ctx.mesShiftWorkScheds.size(), ctx.equipCapacityDefs.size());
// 12. baseTime
ApsTimeConfig timeConfig = apsTimeConfigService.getOne(new LambdaQueryWrapper<>());
ctx.baseTime = (timeConfig != null && timeConfig.getBaseTime() != null)
? timeConfig.getBaseTime() : LocalDateTime.now();
return ctx;
}
// ==================== 步骤2: 构建 Material 业务对象 ====================
// 参考 PlanResultService.getMaterials() (PlanResultService.java:2700-2849), 去掉缓存写入
private void buildMaterials(ConvertContext ctx) {
// 构建索引
Map<String, List<Stock>> stocksByMaterialId = ctx.stocks.stream()
.filter(s -> s.getMaterialId() != null)
.collect(Collectors.groupingBy(Stock::getMaterialId));
Map<String, List<MaterialPurchase>> purchasesByMaterialId = ctx.materialPurchases.stream()
.filter(p -> p.getMaterialId() != null)
.collect(Collectors.groupingBy(MaterialPurchase::getMaterialId));
Map<String, List<PurchaseReceipt>> receiptsByMaterialId = ctx.purchaseReceipts.stream()
.filter(r -> r.getMaterialid() != null)
.collect(Collectors.groupingBy(PurchaseReceipt::getMaterialid));
Map<String, List<ErpPurchaseOrder>> erpOrdersByMaterialId = ctx.erpPurchaseOrders.stream()
.filter(e -> e.getMaterialId() != null)
.collect(Collectors.groupingBy(ErpPurchaseOrder::getMaterialId));
Map<String, List<SjzPfWhStock>> sjzByMaterialId = ctx.sjzPfWhStocks.stream()
.filter(s -> s.getMaterialid() != null)
.collect(Collectors.groupingBy(SjzPfWhStock::getMaterialid));
for (MaterialInfo m : ctx.materialInfos) {
if (m.getMaterialTypeName() == null) {
// 无物料类型名, 跳过 (参照 getMaterials 逻辑)
continue;
}
Material material = new Material();
material.setId(m.getId());
material.setMaterialType(m.getMaterialType());
material.setMaterialTypeName(m.getMaterialTypeName());
material.setCode(m.getCode());
material.setName(m.getName());
material.setMaxProduction(m.getMaxProduction());
material.setMinProduction(m.getMinProduction());
// 库存
List<Stock> materialStocks = stocksByMaterialId.getOrDefault(m.getId(), Collections.emptyList());
if (!materialStocks.isEmpty()) {
double stock = materialStocks.stream().mapToDouble(Stock::getTotal).sum();
material.setCurrentStock(stock);
material.setMaterialStocks(new ArrayList<>(materialStocks));
}
// 采购供应商
List<MaterialPurchase> purchases = purchasesByMaterialId.getOrDefault(m.getId(), Collections.emptyList());
if (!purchases.isEmpty()) {
material.setMaterialPurchases(new ArrayList<>(purchases));
}
// 在途物料 (区分 MP 原材料 vs 半成品/成品)
List<MaterialSupply> inTransit = new ArrayList<>();
if ("MP".equals(m.getMaterialTypeName())) {
// 原材料: 待验 (PurchaseReceipt) + 采购订单 (ErpPurchaseOrder, 加检验周期)
buildMpInTransit(inTransit, m.getId(),
receiptsByMaterialId.getOrDefault(m.getId(), Collections.emptyList()),
erpOrdersByMaterialId.getOrDefault(m.getId(), Collections.emptyList()),
purchases);
} else {
// 半成品/成品: SjzPfWhStock
buildNonMpInTransit(inTransit, m.getId(),
sjzByMaterialId.getOrDefault(m.getId(), Collections.emptyList()));
}
material.setInTransit(inTransit);
ctx.materialByMaterialId.put(m.getId(), material);
}
log.info("构建 Material 业务对象: {} 个", ctx.materialByMaterialId.size());
}
/**
* 构建 MP 原材料的在途物料 (待验 + 采购订单)。
* 参考 PlanResultService.getMaterials() 第 2783-2826 行。
*/
private void buildMpInTransit(List<MaterialSupply> inTransit, String materialId,
List<PurchaseReceipt> receipts,
List<ErpPurchaseOrder> erpOrders,
List<MaterialPurchase> purchases) {
// 待验
for (PurchaseReceipt pr : receipts) {
LocalDateTime dt = pr.getExp5();
if (dt == null && pr.getExp1() != null) {
try {
dt = ParamValidator.parseDateTime(pr.getExp1() + " 00:00:00", "");
} catch (Exception e) {
log.warn("解析待验到货时间失败: materialId={}, exp1={}", materialId, pr.getExp1());
continue;
}
}
if (dt == null) {
continue;
}
MaterialSupply ms = new MaterialSupply();
ms.setArrivalTime(dt);
ms.setQuantity(pr.getFjl1Sl());
inTransit.add(ms);
}
// 采购订单 (可用时间 = 到货时间 + 检验周期)
for (ErpPurchaseOrder epo : erpOrders) {
if (epo.getArrivalDate() == null) {
continue;
}
int checkDay = 0;
if (epo.getManufacturerId() != null && purchases != null) {
String manufacturerIdStr = String.valueOf(epo.getManufacturerId());
Optional<MaterialPurchase> matched = purchases.stream()
.filter(p -> p.getSupplyId() != null && p.getSupplyId().equals(manufacturerIdStr))
.findFirst();
if (matched.isPresent() && matched.get().getInspectionCycle() != null) {
checkDay = matched.get().getInspectionCycle();
}
}
MaterialSupply ms = new MaterialSupply();
ms.setArrivalTime(epo.getArrivalDate().plusDays(checkDay));
ms.setQuantity(epo.getPurchaseQty());
inTransit.add(ms);
}
}
/**
* 构建半成品/成品的在途物料 (SjzPfWhStock)。
* 参考 PlanResultService.getMaterials() 第 2827-2840 行。
*/
private void buildNonMpInTransit(List<MaterialSupply> inTransit, String materialId,
List<SjzPfWhStock> sjzStocks) {
for (SjzPfWhStock sjz : sjzStocks) {
LocalDateTime dt = null;
if (sjz.getExp1() != null) {
try {
dt = ParamValidator.parseDateTime(sjz.getExp1() + " 00:00:00", "");
} catch (Exception e) {
log.warn("解析半成品在途时间失败: materialId={}, exp1={}", materialId, sjz.getExp1());
continue;
}
}
if (dt == null) {
continue;
}
MaterialSupply ms = new MaterialSupply();
ms.setArrivalTime(dt);
ms.setQuantity(sjz.getFjl1Sl());
inTransit.add(ms);
}
}
// ==================== 步骤3: 结合设备日历计算每个 PlanResource (设备) 的日产能 ====================
/**
* 按 PlanResource.id 计算每台设备的日产能 (小时), 结合班次+节假日+效率系数。
*
* <p>映射链路:</p>
* <pre>
* RoutingDetailEquip.equipId → PlanResource.id → unitId = "EQUIP_" + id
* PlanResource.referenceId → Equipinfo.id → Equipinfo.xxx
* ProdEquipSpecialCal(referenceType=1) → MesShiftWorkSched → 班次时间
* ProdEquipSpecialCal(referenceType=2) → 节假日日期
* EquipCapacityDef → 效率系数
* </pre>
*
* <p>结果存储到 {@code ctx.dailyCapacityByUnitId} 和节假日映射。</p>
*/
private void buildDailyCapacityByUnitId(ConvertContext ctx) {
// 计算排产周期数 (从 ApsTimeConfig)
ApsTimeConfig timeConfig = apsTimeConfigService.getOne(new LambdaQueryWrapper<>());
if (timeConfig != null && timeConfig.getStartCount() != null && timeConfig.getEndCount() != null) {
long horizonSeconds = timeConfig.getEndCount().longValue()
- timeConfig.getStartCount().longValue();
if (horizonSeconds > 0) {
ctx.periodCount = (int) Math.max(1, horizonSeconds / 86400);
}
}
log.info("排产周期数: {}", ctx.periodCount);
// 构建索引
Map<Integer, PlanResource> planResourceById = ctx.planResources.stream()
.collect(Collectors.toMap(PlanResource::getId, pr -> pr, (a, b) -> a));
Map<Integer, Equipinfo> equipinfoById = ctx.equipinfos.stream()
.collect(Collectors.toMap(Equipinfo::getId, e -> e, (a, b) -> a));
// MesShiftWorkSched: weekWorkSchedId → List
Map<Integer, List<MesShiftWorkSched>> shiftSchedByWeekId = ctx.mesShiftWorkScheds.stream()
.filter(s -> s.getWeekWorkSchedId() != null)
.collect(Collectors.groupingBy(MesShiftWorkSched::getWeekWorkSchedId));
// ProdEquipSpecialCal: planResourceId → List (referenceType=1 班次)
Map<Long, List<ProdEquipSpecialCal>> shiftCalsByPrId = ctx.prodEquipSpecialCals.stream()
.filter(c -> c.getPlanResourceId() != null && c.getReferenceType() != null
&& c.getReferenceType() == 1)
.collect(Collectors.groupingBy(ProdEquipSpecialCal::getPlanResourceId));
// ProdEquipSpecialCal: planResourceId → List (referenceType=2 节假日)
Map<Long, List<ProdEquipSpecialCal>> holidayCalsByPrId = ctx.prodEquipSpecialCals.stream()
.filter(c -> c.getPlanResourceId() != null && c.getReferenceType() != null
&& c.getReferenceType() == 2)
.collect(Collectors.groupingBy(ProdEquipSpecialCal::getPlanResourceId));
// EquipCapacityDef: equipinfo.id → EquipCapacityDef
Map<Long, EquipCapacityDef> capDefByRefId = ctx.equipCapacityDefs.stream()
.filter(d -> d.getReferenceId() != null)
.collect(Collectors.toMap(EquipCapacityDef::getReferenceId, d -> d, (a, b) -> a));
double DEFAULT_DAILY_HOURS = 16.0;
// 收集所有 unitId (从 RoutingDetailEquip)
Set<String> allUnitIds = new HashSet<>();
for (RoutingDetailEquip rde : ctx.routingDetailEquips) {
if (rde.getEquipId() != null) {
allUnitIds.add("EQUIP_" + rde.getEquipId());
}
}
for (String unitId : allUnitIds) {
String idStr = unitId.substring(6); // 去掉 "EQUIP_" 前缀
long planResourceId;
try {
planResourceId = Long.parseLong(idStr);
} catch (NumberFormatException e) {
ctx.dailyCapacityByUnitId.put(unitId, DEFAULT_DAILY_HOURS);
continue;
}
PlanResource pr = planResourceById.get(planResourceId);
if (pr == null) {
ctx.dailyCapacityByUnitId.put(unitId, DEFAULT_DAILY_HOURS);
continue;
}
Equipinfo eq = (pr.getReferenceId() != null) ? equipinfoById.get(pr.getReferenceId()) : null;
// 效率系数: EquipCapacityDef > ProdEquipSpecialCal > 默认 1.0
double efficiency = 1.0;
if (eq != null) {
EquipCapacityDef capDef = capDefByRefId.get(eq.getId().longValue());
if (capDef != null && capDef.getEfficiencyCoeff() != null) {
efficiency = capDef.getEfficiencyCoeff();
}
}
if (efficiency == 1.0) {
List<ProdEquipSpecialCal> shiftCals = shiftCalsByPrId.getOrDefault(pr.getId(), Collections.emptyList());
for (ProdEquipSpecialCal cal : shiftCals) {
if ( cal.getEfficiencyCoeff() > 0) {
efficiency = cal.getEfficiencyCoeff();
break;
}
}
}
// 收集班次时间
List<MesShiftWorkSched> allShifts = new ArrayList<>();
List<ProdEquipSpecialCal> shiftCals = shiftCalsByPrId.getOrDefault(pr.getId(), Collections.emptyList());
for (ProdEquipSpecialCal cal : shiftCals) {
if (cal.getReferenceId() != null) {
List<MesShiftWorkSched> scheds = shiftSchedByWeekId.get(cal.getReferenceId().intValue());
if (scheds != null) {
allShifts.addAll(scheds);
}
}
}
// 备选: 通过 PlanResource.workSchedId
if (allShifts.isEmpty() && pr.getWorkSchedId() != null) {
List<MesShiftWorkSched> scheds = shiftSchedByWeekId.get(pr.getWorkSchedId().intValue());
if (scheds != null) {
allShifts.addAll(scheds);
}
}
double dailyHours;
if (!allShifts.isEmpty()) {
dailyHours = computeDailyHoursFromShifts(allShifts);
} else {
// 无班次: 使用 capabilityValue 回退
if (pr.getCapabilityValue() != null) {
dailyHours = pr.getCapabilityValue().doubleValue();
} else if (eq != null && eq.getCapabilityValue() != null) {
dailyHours = eq.getCapabilityValue().doubleValue();
} else {
dailyHours = DEFAULT_DAILY_HOURS;
}
}
dailyHours *= efficiency;
ctx.dailyCapacityByUnitId.put(unitId, Math.max(0, dailyHours));
}
// 构建节假日映射: unitId → Set<LocalDate>
for (Map.Entry<Long, List<ProdEquipSpecialCal>> entry : holidayCalsByPrId.entrySet()) {
String unitId = "EQUIP_" + entry.getKey();
Set<LocalDate> dates = ctx.holidayDatesByUnitId.computeIfAbsent(unitId, k -> new HashSet<>());
for (ProdEquipSpecialCal cal : entry.getValue()) {
if (cal.getStartDate() == null || cal.getEndDate() == null) {
continue;
}
LocalDate start = cal.getStartDate().toLocalDate();
LocalDate end = cal.getEndDate().toLocalDate();
for (LocalDate d = start; !d.isAfter(end); d = d.plusDays(1)) {
dates.add(d);
}
}
}
log.info("构建日产能(含日历+节假日): {} 台设备, {} 台有节假日, 默认={}h",
ctx.dailyCapacityByUnitId.size(), ctx.holidayDatesByUnitId.size(), DEFAULT_DAILY_HOURS);
}
/**
* 从 MesShiftWorkSched 列表计算平均日工作小时数。
* 遍历每周 7 天, 累加匹配的班次时间, 除以 7 得到日均值。
*/
private double computeDailyHoursFromShifts(List<MesShiftWorkSched> shifts) {
double[] hoursByDay = new double[7]; // 0=Sunday, 1=Monday, ..., 6=Saturday
for (MesShiftWorkSched s : shifts) {
if (s.getShiftStart() == null || s.getShiftEnd() == null) {
continue;
}
LocalTime startTime = s.getShiftStart().toLocalTime();
LocalTime endTime = s.getShiftEnd().toLocalTime();
double shiftHours = calculateShiftHours(startTime, endTime);
int startDay = s.getStartWeekDay() != null ? s.getStartWeekDay() : 1;
int endDay = s.getEndWeekDay() != null ? s.getEndWeekDay() : startDay;
int mappedStart = (startDay % 7); // 数据库: 1=Mon→1, 7=Sun→0
int mappedEnd = (endDay % 7);
if (mappedStart == mappedEnd) {
hoursByDay[mappedStart] += shiftHours;
} else {
int day = mappedStart;
while (day != mappedEnd) {
hoursByDay[day] += shiftHours;
day = (day + 1) % 7;
}
hoursByDay[mappedEnd] += shiftHours;
}
}
double totalWeekHours = 0;
for (double h : hoursByDay) {
totalWeekHours += h;
}
return totalWeekHours / 7.0;
}
/**
* 计算班次时长 (小时), 处理跨天 (如 22:00-06:00)。
*/
private double calculateShiftHours(LocalTime start, LocalTime end) {
long minutes;
if (end.isAfter(start)) {
minutes = Duration.between(start, end).toMinutes();
} else {
minutes = Duration.between(start, LocalTime.MAX).toMinutes()
+ Duration.between(LocalTime.MIN, end).toMinutes();
}
return minutes / 60.0;
}
// ==================== Sink: 继承 TestDataBuilder, 直接操作 protected 字段 ====================
/**
* TestDataBuilder 的同包子类, 通过 protected 字段直接填充数据。
* 与 {@link ComprehensiveTestDataBuilder} / {@link RoutingTestDataBuilder} 模式一致。
*/
private static class Sink extends TestDataBuilder {
Sink() {
super(true); // 跳过父类默认 build
}
void fill(ConvertContext ctx) {
fillProductsAndStockingPoints(ctx);
fillOperationsAndRoutings(ctx);
fillOperationInputs(ctx);
fillInitialInventoryAndInTransit(ctx);
fillPeriodsAndUnitPeriods(ctx);
fillSalesDemands(ctx);
fillInventorySpecsAndSupplySpecs(ctx);
fillKpiWeights();
}
// ---------- 步骤3: Product + StockingPoint ----------
private void fillProductsAndStockingPoints(ConvertContext ctx) {
// 3.1 为每个 Material 创建 Product
for (Material m : ctx.materialByMaterialId.values()) {
String name = pickName(m.getName(), m.getCode(), m.getId());
Product p = new Product(m.getId(), name);
products.add(p);
ctx.productByMaterialId.put(m.getId(), p);
}
// 3.2 收集所有 storeId (库存 + 投料仓库), 建立 storeId → storeName 映射
Map<Long, String> storeNameMap = new HashMap<>();
for (Material m : ctx.materialByMaterialId.values()) {
if (m.getMaterialStocks() != null) {
for (Stock s : m.getMaterialStocks()) {
if (s.getStoreId() != null && s.getStoreName() != null) {
storeNameMap.putIfAbsent(s.getStoreId(), s.getStoreName());
}
}
}
}
for (Routingsupporting rs : ctx.routingsupportings) {
if (rs.getStoreId() != null && rs.getStoreName() != null) {
storeNameMap.putIfAbsent(rs.getStoreId(), rs.getStoreName());
}
}
Set<Long> storeIds = new HashSet<>();
storeIds.addAll(storeNameMap.keySet());
// 补充只有 Stock 有但没 name 的 storeId
for (Material m : ctx.materialByMaterialId.values()) {
if (m.getMaterialStocks() != null) {
for (Stock s : m.getMaterialStocks()) {
if (s.getStoreId() != null) storeIds.add(s.getStoreId());
}
}
}
for (Routingsupporting rs : ctx.routingsupportings) {
if (rs.getStoreId() != null) storeIds.add(rs.getStoreId());
}
for (Long sid : storeIds) {
String spName = storeNameMap.getOrDefault(sid, "仓库_" + sid);
StockingPoint sp = new StockingPoint("SP_" + sid, spName);
stockingPoints.add(sp);
ctx.spByStoreId.put(sid, sp);
}
// 3.3 ProductSpMapping + 记录每个物料的成品库 (首个库存点)
for (Material m : ctx.materialByMaterialId.values()) {
Product p = ctx.productByMaterialId.get(m.getId());
StockingPoint firstSp = null;
if (m.getMaterialStocks() != null) {
for (Stock s : m.getMaterialStocks()) {
if (s.getStoreId() == null) {
continue;
}
StockingPoint sp = ctx.spByStoreId.get(s.getStoreId());
if (sp != null) {
productSpMappings.add(new ProductSpMapping(p, sp));
if (firstSp == null) {
firstSp = sp;
}
}
}
}
if (firstSp == null) {
// 无库存记录, 建默认成品库
firstSp = new StockingPoint("SP_FG_" + m.getId(),
"成品库_" + pickName(m.getName(), m.getId()));
stockingPoints.add(firstSp);
productSpMappings.add(new ProductSpMapping(p, firstSp));
}
ctx.finalSpByMaterialId.put(m.getId(), firstSp);
}
log.info("构建 Product: {}, StockingPoint: {}", products.size(), stockingPoints.size());
}
// ---------- 步骤4: Operation + Routing (调用 expand) ----------
private void fillOperationsAndRoutings(ConvertContext ctx) {
// 4.1 工序按 routingHeaderId 分组, 按 taskSeq 升序排序
Map<Integer, List<RoutingDetail>> detailsByHeader = new HashMap<>();
for (RoutingDetail rd : ctx.routingDetails) {
if (rd.getRoutingHeaderId() == null) {
continue;
}
int hid = rd.getRoutingHeaderId().intValue();
detailsByHeader.computeIfAbsent(hid, k -> new ArrayList<>()).add(rd);
}
detailsByHeader.values().forEach(list ->
list.sort(Comparator.comparing(rd ->
rd.getTaskSeq() == null ? Long.MAX_VALUE : rd.getTaskSeq())));
// 4.2 每个 RoutingHeader 构建 Routing
for (RoutingHeader rh : ctx.routingHeaders) {
if (Boolean.TRUE.equals(rh.getIsDeleted())) {
continue;
}
if (rh.getMaterialId() == null) {
continue;
}
Product product = ctx.productByMaterialId.get(rh.getMaterialId());
if (product == null) {
log.warn("跳过工艺路线 {}: materialId={} 对应的物料不存在", rh.getId(), rh.getMaterialId());
continue;
}
StockingPoint finalSp = ctx.finalSpByMaterialId.get(rh.getMaterialId());
if (finalSp == null) {
log.warn("跳过工艺路线 {}: 找不到成品库", rh.getId());
continue;
}
String routingName = pickName(rh.getName(), rh.getCode(), "工艺路线_" + rh.getId());
Routing routing = new Routing("R_" + rh.getId(), routingName, product, finalSp);
List<RoutingDetail> details = detailsByHeader.getOrDefault(rh.getId(), Collections.emptyList());
// 构建索引: routingDetailId → List<RoutingDetailEquip>
Map<Long, List<RoutingDetailEquip>> equipsByDetailId = ctx.routingDetailEquips.stream()
.filter(e -> e.getRoutingDetailId() != null)
.collect(Collectors.groupingBy(RoutingDetailEquip::getRoutingDetailId));
for (RoutingDetail rd : details) {
if (rd.getIsDeleted() != null && rd.getIsDeleted() != 0) {
continue;
}
String opName = pickName(rd.getName(), "工序" + rd.getTaskSeq());
// 从 RoutingDetailEquip 创建 UnitOperation 列表 (每个设备一个 UnitOperation)
List<RoutingDetailEquip> equips = equipsByDetailId.getOrDefault(rd.getId(), Collections.emptyList());
List<UnitOperation> unitOps;
if (!equips.isEmpty()) {
unitOps = equips.stream()
.filter(e -> e.getEquipId() != null)
.map(e -> {
String unitId = "EQUIP_" + e.getEquipId();
double capCoeff = (e.getDuration() != null) ? e.getDuration().doubleValue() : 1.0;
boolean lotSize = e.getOneBatchQuantity() != null
&& e.getOneBatchQuantity().compareTo(BigDecimal.ZERO) > 0;
double lotSizeVal = lotSize ? e.getOneBatchQuantity().doubleValue() : 0.0;
return new UnitOperation(unitId, capCoeff, lotSize, lotSizeVal, 1.0);
})
.collect(Collectors.toList());
} else {
// 无 RoutingDetailEquip: 回退到 equipTypeId
String unitId = "EQUIP_" + (rd.getEquipTypeId() != null ? rd.getEquipTypeId() : "DEFAULT");
double capCoeff = (rd.getRuntime() != null) ? rd.getRuntime().doubleValue() : 1.0;
unitOps = Arrays.asList(new UnitOperation(unitId, capCoeff, false, 0.0, 1.0));
}
Operation op = new Operation(
"OP_" + rh.getId() + "_" + rd.getId(),
opName, unitOps,
1.0, 0);
routing.addOperation(op);
ctx.operationByRoutingDetailId.put(rd.getId(), op);
}
if (routing.getOperations().isEmpty()) {
log.warn("跳过工艺路线 {}: 无有效工序", rh.getId());
continue;
}
routings.add(routing);
operations.addAll(routing.getOperations());
ctx.routingByHeaderId.put(rh.getId(), routing);
// 调用 expand() 自动生成中间 WIP 库存点 + BOM 输入 + 工序产出配置
try {
RoutingExpansion expansion = routing.expand();
stockingPoints.addAll(expansion.getStockingPoints());
productSpMappings.addAll(expansion.getProductSpMappings());
initialInventories.addAll(expansion.getInitialInventories());
operationInputs.addAll(expansion.getOperationInputs());
} catch (Exception e) {
log.error("工艺路线 {} 展开失败: {}", rh.getId(), e.getMessage(), e);
}
}
log.info("构建 Routing: {}, Operation: {}", routings.size(), operations.size());
}
// ---------- 步骤5: OperationInput (仅 MP 原材料投料) ----------
private void fillOperationInputs(ConvertContext ctx) {
// 仅 MP 原材料作为投料; 半成品由各自 Routing 产出 + RoutingConstraint 管理, 不重复投料
for (Routingsupporting rs : ctx.routingsupportings) {
Operation op = ctx.operationByRoutingDetailId.get(rs.getRoutingDetailId());
if (op == null) {
continue;
}
if (rs.getMaterialId() == null) {
continue;
}
Material inputMaterial = ctx.materialByMaterialId.get(rs.getMaterialId());
if (inputMaterial == null) {
continue;
}
if (!"MP".equals(inputMaterial.getMaterialTypeName())) {
// 半成品/成品不作为投料, 由各自 Routing 产出
continue;
}
if (rs.getMainQty() == null || rs.getMainQty().compareTo(BigDecimal.ZERO) == 0) {
log.warn("跳过主量为0的BOM项: routingDetailId={}, materialId={}",
rs.getRoutingDetailId(), rs.getMaterialId());
continue;
}
if (rs.getSpentQty() == null) {
throw new IllegalStateException(
"BOM消耗量为空: " + rs.getMaterialNumber() + ", routingDetailId=" + rs.getRoutingDetailId());
}
double factor = rs.getSpentQty().doubleValue() / rs.getMainQty().doubleValue();
Product inputProduct = ctx.productByMaterialId.get(rs.getMaterialId());
StockingPoint inputSp = resolveInputSp(ctx, rs, inputProduct);
operationInputs.add(new OperationInput(op, inputProduct, inputSp, factor));
}
// 为无产出工序的 MP 原材料补充"采购 Operation"
for (Material m : ctx.materialByMaterialId.values()) {
if (!"MP".equals(m.getMaterialTypeName())) {
continue;
}
Product p = ctx.productByMaterialId.get(m.getId());
if (p == null) {
continue;
}
boolean hasProducer = operations.stream()
.anyMatch(op -> op.producesProduct(p.getId()));
if (!hasProducer) {
StockingPoint sp = ctx.finalSpByMaterialId.get(m.getId());
if (sp == null) {
sp = new StockingPoint("SP_PROCURE_" + m.getId(),
"采购库_" + pickName(m.getName(), m.getId()));
stockingPoints.add(sp);
productSpMappings.add(new ProductSpMapping(p, sp));
ctx.finalSpByMaterialId.put(m.getId(), sp);
}
int leadTimeDays = m.getPurchaseLeadTime() > 0 ? m.getPurchaseLeadTime() : 1;
Operation procureOp = new Operation(
"OP_PROCURE_" + m.getId(),
"采购-" + pickName(m.getName(), m.getId()),
"UNIT_PROCURE_" + m.getId(),
new OperationOutput(p, sp),
0.5, 1.0, false, 0.0, 1.0, leadTimeDays);
operations.add(procureOp);
log.info("为 MP 原材料 {} 补充采购 Operation (leadTime={}天)", m.getId(), leadTimeDays);
}
}
log.info("构建 OperationInput: {}", operationInputs.size());
}
/**
* 解析投料来源库存点: 优先用 Routingsupporting.storeId, 找不到则用物料默认 SP。
*/
private StockingPoint resolveInputSp(ConvertContext ctx, Routingsupporting rs, Product inputProduct) {
if (rs.getStoreId() != null) {
StockingPoint sp = ctx.spByStoreId.get(rs.getStoreId());
if (sp != null) {
return sp;
}
// 投料仓库未在库存中出现, 新建
String spName = rs.getStoreName() != null ? rs.getStoreName() : "仓库_" + rs.getStoreId();
sp = new StockingPoint("SP_" + rs.getStoreId(), spName);
stockingPoints.add(sp);
ctx.spByStoreId.put(rs.getStoreId(), sp);
productSpMappings.add(new ProductSpMapping(inputProduct, sp));
return sp;
}
// 无仓库信息, 用该物料的默认 SP
StockingPoint sp = ctx.finalSpByMaterialId.get(rs.getMaterialId());
if (sp == null) {
sp = new StockingPoint("SP_DEFAULT_" + rs.getMaterialId(),
"默认库_" + rs.getMaterialId());
stockingPoints.add(sp);
productSpMappings.add(new ProductSpMapping(inputProduct, sp));
ctx.finalSpByMaterialId.put(rs.getMaterialId(), sp);
}
return sp;
}
// ---------- 步骤6: InitialInventory + InTransitSupply ----------
private void fillInitialInventoryAndInTransit(ConvertContext ctx) {
// 已有的 (productId, spId) 集合 (来自 expand 的 0 库存 WIP)
Set<String> existingKeys = new HashSet<>();
for (InitialInventory ii : initialInventories) {
existingKeys.add(ii.getProduct().getId() + "_" + ii.getStockingPoint().getId());
}
for (Material m : ctx.materialByMaterialId.values()) {
Product p = ctx.productByMaterialId.get(m.getId());
if (p == null) {
continue;
}
// 初始库存
if (m.getMaterialStocks() != null) {
for (Stock s : m.getMaterialStocks()) {
if (s.getStoreId() == null) {
continue;
}
StockingPoint sp = ctx.spByStoreId.get(s.getStoreId());
if (sp == null) {
continue;
}
String key = p.getId() + "_" + sp.getId();
if (existingKeys.contains(key)) {
continue; // 已有 (WIP 0库存), 跳过避免重复
}
initialInventories.add(new InitialInventory(p, sp, s.getTotal()));
existingKeys.add(key);
}
}
// 在途供应
StockingPoint defaultSp = ctx.finalSpByMaterialId.get(m.getId());
if (defaultSp == null || m.getInTransit() == null) {
continue;
}
for (MaterialSupply ms : m.getInTransit()) {
if (ms.getArrivalTime() == null) {
continue;
}
inTransitSupplies.add(new InTransitSupply(
p, defaultSp, ms.getArrivalTime().toLocalDate(), ms.getQuantity()));
}
}
log.info("构建 InitialInventory: {}, InTransitSupply: {}",
initialInventories.size(), inTransitSupplies.size());
}
// ---------- 步骤7: Period + UnitPeriod (结合设备日历和周期长度) ----------
private void fillPeriodsAndUnitPeriods(ConvertContext ctx) {
LocalDate baseDate = ctx.baseTime.toLocalDate();
int periodCount = ctx.periodCount;
// 创建周期 (1天/周期)
for (int i = 0; i < periodCount; i++) {
periods.add(new Period(i, "P" + (i + 1), 1.0, baseDate.plusDays(i)));
}
// 收集所有 unitId (遍历每个 Operation 的每个 UnitOperation)
Set<String> unitIds = new LinkedHashSet<>();
for (Operation op : operations) {
for (UnitOperation uo : op.getUnitOperations()) {
unitIds.add(uo.getUnitId());
}
}
double DEFAULT_DAILY_HOURS = 16.0;
for (Period p : periods) {
LocalDate periodStart = p.getStartDate();
LocalDate periodEnd = periodStart.plusDays((long) p.getDurationInDays());
for (String unitId : unitIds) {
double dailyCapacity = ctx.dailyCapacityByUnitId.getOrDefault(unitId, DEFAULT_DAILY_HOURS);
// 节假日扣减
Set<LocalDate> holidays = ctx.holidayDatesByUnitId.getOrDefault(unitId, Collections.emptySet());
long holidaysInPeriod = 0;
for (LocalDate h : holidays) {
if (!h.isBefore(periodStart) && h.isBefore(periodEnd)) {
holidaysInPeriod++;
}
}
long periodDays = (long) p.getDurationInDays();
long effectiveDays = Math.max(0, periodDays - holidaysInPeriod);
double periodMaxCapacity = dailyCapacity * effectiveDays;
if (periodMaxCapacity <= 0) {
periodMaxCapacity = DEFAULT_DAILY_HOURS * effectiveDays;
}
unitPeriods.add(new UnitPeriod(unitId, p, 0.0, periodMaxCapacity, false));
}
}
log.info("构建 Period: {} (共{}天), UnitPeriod: {} (基于设备日历+节假日)",
periods.size(), periodCount, unitPeriods.size());
}
// ---------- 步骤8: SalesDemand (从订单推导) ----------
private void fillSalesDemands(ConvertContext ctx) {
if (periods.isEmpty()) {
return;
}
for (ProdLaunchOrder plo : ctx.prodLaunchOrders) {
if (plo.getMaterialId() == null) {
continue;
}
Product p = ctx.productByMaterialId.get(plo.getMaterialId());
if (p == null) {
continue;
}
StockingPoint sp = ctx.finalSpByMaterialId.get(plo.getMaterialId());
if (sp == null) {
continue;
}
Period targetPeriod = null;
if (plo.getEndDate() != null) {
LocalDate dueDate = plo.getEndDate().toLocalDate();
for (Period per : periods) {
if (per.contains(dueDate)) {
targetPeriod = per;
break;
}
}
}
if (targetPeriod == null) {
// 找不到匹配周期, 用最后一个周期
targetPeriod = periods.get(periods.size() - 1);
}
double priority = plo.getOrderPriority() != null ? plo.getOrderPriority() : 1;
salesDemands.add(new SalesDemand(p, sp, targetPeriod, plo.getQuantity(), priority));
}
log.info("构建 SalesDemand: {}", salesDemands.size());
}
// ---------- 步骤9: InventorySpec + SupplySpec (默认宽松) ----------
private void fillInventorySpecsAndSupplySpecs(ConvertContext ctx) {
// InventorySpec: 每个 (Product, StockingPoint) × Period, 默认宽松约束
Set<String> invSpecKeys = new HashSet<>();
for (ProductSpMapping m : productSpMappings) {
for (Period p : periods) {
String key = m.getProduct().getId() + "_" + m.getStockingPoint().getId() + "_" + p.getIndex();
if (invSpecKeys.add(key)) {
inventorySpecs.add(new InventorySpec(m.getProduct(), m.getStockingPoint(), p,
0.0, 0.0, LOOSE_MAX, true, true, true));
}
}
}
// SupplySpec: 每个 Routing 的最后一道工序 (产出成品) + 采购 Operation
for (Routing r : routings) {
List<Operation> ops = r.getOperations();
if (ops.isEmpty()) {
continue;
}
Operation lastOp = ops.get(ops.size() - 1);
supplySpecs.add(new SupplySpec("Supply-" + r.getId(),
0.0, 0.0, LOOSE_MAX, false, Collections.singletonList(lastOp)));
}
for (Operation op : operations) {
if (op.getId().startsWith("OP_PROCURE_")) {
supplySpecs.add(new SupplySpec("Supply-" + op.getId(),
0.0, 0.0, LOOSE_MAX, false, Collections.singletonList(op)));
}
}
log.info("构建 InventorySpec: {}, SupplySpec: {}", inventorySpecs.size(), supplySpecs.size());
}
// ---------- 步骤10: KPIWeights (默认值, 复用 RoutingTestDataBuilder) ----------
private void fillKpiWeights() {
kpiWeights = new KPIWeights(
100.0, // fulfillmentWeight
10.0, // lotSizeWeight
5.0, // maxInventoryLevelWeight
5.0, // minInventoryLevelWeight
8.0, // targetInventoryLevelWeight
3.0, // unitCapacityWeight
8.0, // supplyTargetWeight
5.0, // minSupplyWeight
5.0, // maxSupplyWeight
1.0, // salesDemandPriorityWeight
20.0, // postponementPenaltyWeight
5.0 // processMaxQuantityWeight
);
}
// ---------- 工具方法 ----------
/**
* 按优先级选取第一个非空字符串。
*/
private static String pickName(String... candidates) {
for (String c : candidates) {
if (c != null && !c.isEmpty()) {
return c;
}
}
return "unknown";
}
}
// ==================== ConvertContext: 承载所有加载的数据库实体 + 转换中间映射 ====================
private static class ConvertContext {
// 数据库加载的原始实体
List<ProdLaunchOrder> prodLaunchOrders = new ArrayList<>();
List<RoutingHeader> routingHeaders = new ArrayList<>();
List<RoutingDetail> routingDetails = new ArrayList<>();
List<Routingsupporting> routingsupportings = new ArrayList<>();
List<MaterialInfo> materialInfos = new ArrayList<>();
List<Stock> stocks = new ArrayList<>();
List<MaterialPurchase> materialPurchases = new ArrayList<>();
List<ErpPurchaseOrder> erpPurchaseOrders = new ArrayList<>();
List<PurchaseReceipt> purchaseReceipts = new ArrayList<>();
List<SjzPfWhStock> sjzPfWhStocks = new ArrayList<>();
List<RoutingDetailEquip> routingDetailEquips = new ArrayList<>();
List<Equipinfo> equipinfos = new ArrayList<>();
List<PlanResource> planResources = new ArrayList<>();
List<ProdEquipSpecialCal> prodEquipSpecialCals = new ArrayList<>();
List<MesShiftWorkSched> mesShiftWorkScheds = new ArrayList<>();
List<EquipCapacityDef> equipCapacityDefs = new ArrayList<>();
LocalDateTime baseTime;
// 转换过程的中间映射
/** materialId → Material 业务对象 */
Map<String, Material> materialByMaterialId = new HashMap<>();
/** materialId → macroplanner Product */
Map<String, Product> productByMaterialId = new HashMap<>();
/** storeId → StockingPoint */
Map<Long, StockingPoint> spByStoreId = new HashMap<>();
/** routingHeaderId → macroplanner Routing */
Map<Integer, Routing> routingByHeaderId = new HashMap<>();
/** routingDetailId → macroplanner Operation */
Map<Long, Operation> operationByRoutingDetailId = new HashMap<>();
/** materialId → 成品/半成品库 StockingPoint (首个库存点) */
Map<String, StockingPoint> finalSpByMaterialId = new HashMap<>();
/** unitId ("EQUIP_" + PlanResource.id) → 日产能 (小时), 含日历+效率系数 */
Map<String, Double> dailyCapacityByUnitId = new HashMap<>();
/** unitId → 节假日日期集合 */
Map<String, Set<LocalDate>> holidayDatesByUnitId = new HashMap<>();
/** 排产周期数 (天), 从 ApsTimeConfig 计算 */
int periodCount = 7;
}
}
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
package com.aps.macroplanner.data;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.logging.Logger;
/**
* 工艺路线 — 对应 Quintiq 中的 Routing。
*
* <p>工艺路线定义了一组串行工序步骤, 用于生产某个产品。
* 用户只需定义工艺路线关联的产品和最终库存点, 以及各道工序,
* 工序间的 WIP 缓冲区和 BOM 依赖关系由 {@link #expand()} 自动生成。</p>
*
* <h3>使用示例</h3>
* <pre>{@code
* // 定义工艺路线: 产品 P 通过 3 道工序生产, 最终入库 SP_FG
* Routing routing = new Routing("R001", "P生产工艺", prodP, spFG);
* routing.addOperation(new Operation("OP_Cut", "下料", "Unit_Cut", 1.0, ...));
* routing.addOperation(new Operation("OP_Rough", "粗加工", "Unit_Rough", 1.5, ...));
* routing.addOperation(new Operation("OP_Finish","精加工", "Unit_Finish",2.0, ...));
*
* // 展开: 自动生成 WIP 库存点 + BOM 输入 + 工序产出配置
* // 只有最后一道工序 (Finish) 产出成品, 中间工序 WIP 自动流转
* RoutingExpansion expansion = routing.expand();
* }</pre>
*
* <h3>与 Quintiq 模型的对应关系</h3>
* <pre>
* Quintiq Routing:
* - RoutingSteps (有序) → steps
* - PISPNodeInRouting → expand() 自动生成的 WIP 库存点
* - OperationLink (BOM) → expand() 自动生成的 OperationInput
* - AllowWIPInventory → 中间 WIP 库存点无容量限制
* - Product/StockingPoint → routing 关联的最终产品和库存点
* </pre>
*
* @see RoutingStep
*/
public class Routing {
private static final Logger LOG = Logger.getLogger(Routing.class.getName());
/** 工艺路线 ID */
private final String id;
/** 工艺路线名称 */
private final String name;
/** 该工艺路线生产的最终产品 */
private final Product product;
/** 最终产出库存点 (成品库) */
private final StockingPoint finalSp;
/** 工序步骤列表 (按 sequenceNumber 排序) */
private final List<RoutingStep> steps = new ArrayList<>();
/**
* 创建工艺路线。
*
* @param id 工艺路线 ID (唯一标识)
* @param name 工艺路线名称 (描述)
* @param product 该工艺路线生产的最终产品
* @param finalSp 最终产出库存点 (成品库)
*/
public Routing(String id, String name, Product product, StockingPoint finalSp) {
this.id = id;
this.name = name;
this.product = product;
this.finalSp = finalSp;
}
/**
* 添加工序 (便捷方法)。自动按添加顺序分配 sequenceNumber。
* 对应 Quintiq 中向 Routing 添加 Unit 创建 Operation。
*
* @param op 工序
* @return this (流式 API)
*/
public Routing addOperation(Operation op) {
int seq = steps.size() + 1;
return addStep(new RoutingStep(seq, op));
}
/**
* 添加工序步骤。步骤按 sequenceNumber 自动排序。
* 对应 Quintiq 中的 RoutingStep。
*
* @param step 工艺路线步骤
* @return this (流式 API)
*/
public Routing addStep(RoutingStep step) {
steps.add(step);
steps.sort((a, b) -> Integer.compare(a.getSequenceNumber(), b.getSequenceNumber()));
return this;
}
/**
* 展开工艺路线 — 自动生成中间 WIP 库存点、BOM 输入和操作输出配置。
*
* <h3>展开逻辑 (对应 Quintiq PISPNodeInRouting 的自动生成)</h3>
* <pre>
* 对于 N 步路由 (S1, S2, ..., SN):
* S1: 产出 → WIP_{routingId}_1 (中间缓冲区)
* S2: 产出 → WIP_{routingId}_2, 消耗 → S1 的 WIP 产出
* ...
* SN-1: 产出 → WIP_{routingId}_{N-1}, 消耗 → SN-2 的 WIP 产出
* SN(最后): 产出 → finalSp (成品库), 消耗 → SN-1 的 WIP 产出
* </pre>
*
* <p>只有最后一步的产出计入最终产品库存, 中间步骤的产出经 BOM 约束
* 被下一道工序消耗, 不会重复计算产出。</p>
*
* @return 展开结果, 包含所有自动生成的库存点、映射和 BOM 输入
*/
public RoutingExpansion expand() {
if (steps.isEmpty()) {
throw new IllegalStateException("工艺路线 '" + id + "' 没有工序步骤");
}
LOG.info(String.format("[路由展开] 开始: %s '%s' → 产品=%s 最终库存点=%s 工序数=%d",
id, name, product.getId(), finalSp.getId(), steps.size()));
// 记录各工序当前已有的产出 (联产品/副产品)
for (int i = 0; i < steps.size(); i++) {
Operation op = steps.get(i).getOperation();
List<OperationOutput> existingOutputs = op.getOutputs();
if (!existingOutputs.isEmpty()) {
List<String> descs = new ArrayList<>();
for (OperationOutput oo : existingOutputs) {
descs.add(oo.getProductId() + "@" + oo.getSpId());
}
LOG.fine(String.format("[路由展开] 工序%d '%s' 已有产出: %s",
i + 1, op.getId(), String.join(", ", descs)));
}
}
RoutingExpansion expansion = new RoutingExpansion(this);
// 单步路由: 直接产出到最终库存点, 无需 WIP
// 使用 addOutput() 保留工序上预配置的联产品/副产品产出
if (steps.size() == 1) {
RoutingStep step = steps.get(0);
Operation op = step.getOperation();
int beforeCount = op.getOutputs().size();
op.addOutput(new OperationOutput(product, finalSp));
int afterCount = op.getOutputs().size();
LOG.info(String.format("[路由展开] 单步路由: '%s' 产出 → %s@%s (产出数: %d→%d)%s",
op.getId(), product.getId(), finalSp.getId(),
beforeCount, afterCount,
afterCount > 1 ? " [多产出: 联产品/副产品]" : ""));
return expansion;
}
// 多步路由: 生成中间 WIP 库存点 (对应 Quintiq PISPNodeInRouting)
StockingPoint previousWipSp = null;
int wipCount = 0;
int bomLinkCount = 0;
for (int i = 0; i < steps.size(); i++) {
RoutingStep step = steps.get(i);
Operation op = step.getOperation();
boolean isLast = (i == steps.size() - 1);
if (isLast) {
// === 最后一步: 产出到最终库存点 ===
// 使用 addOutput() 保留工序上预配置的联产品/副产品产出
int beforeCount = op.getOutputs().size();
op.addOutput(new OperationOutput(product, finalSp));
int afterCount = op.getOutputs().size();
LOG.info(String.format("[路由展开] 最后工序%d '%s' → %s@%s (产出数: %d→%d)%s",
i + 1, op.getId(), product.getId(), finalSp.getId(),
beforeCount, afterCount,
afterCount > 1 ? " [多产出: 联产品/副产品]" : ""));
// 最后一步消耗前一步的 WIP 产出
OperationInput input = new OperationInput(
step.getOperation(), product, previousWipSp, 1.0);
expansion.addOperationInput(input);
bomLinkCount++;
LOG.fine(String.format("[路由展开] BOM链接: '%s' 消耗 %s@%s ×1.0 (来自前序WIP)",
op.getId(), product.getId(), previousWipSp.getId()));
} else {
// === 中间步骤: 产出到自动生成的 WIP 库存点 ===
// 对应 Quintiq 中 PISPNodeInRouting 的自动创建
String wipSpId = "WIP_" + id + "_" + (i + 1);
String wipSpName = name + "-工序" + (i + 1) + "→工序" + (i + 2) + "缓冲区";
StockingPoint wipSp = new StockingPoint(wipSpId, wipSpName);
wipCount++;
LOG.fine(String.format("[路由展开] 创建WIP库存点: %s '%s'", wipSpId, wipSpName));
// 第一步: 只设置产出, 无需消耗
// 后续步骤: 消耗前一步的 WIP 产出
if (previousWipSp != null) {
OperationInput input = new OperationInput(
step.getOperation(), product, previousWipSp, 1.0);
expansion.addOperationInput(input);
bomLinkCount++;
LOG.fine(String.format("[路由展开] BOM链接: '%s' 消耗 %s@%s ×1.0 (前序WIP)",
op.getId(), product.getId(), previousWipSp.getId()));
} else {
LOG.fine(String.format("[路由展开] 首工序%d '%s' 无前序WIP消耗 (仅产出)",
i + 1, op.getId()));
}
// 设置当前步骤的产出到 WIP 缓冲区
// 使用 addOutput() 保留工序上预配置的联产品/副产品产出
int beforeCount = op.getOutputs().size();
op.addOutput(new OperationOutput(product, wipSp));
int afterCount = op.getOutputs().size();
LOG.fine(String.format("[路由展开] 中间工序%d '%s' → %s@%s (产出数: %d→%d)%s",
i + 1, op.getId(), product.getId(), wipSp.getId(),
beforeCount, afterCount,
afterCount > 1 ? " [多产出: 联产品/副产品]" : ""));
// 注册 WIP 库存点、产品映射和初始库存
expansion.addStockingPoint(wipSp);
expansion.addProductSpMapping(new ProductSpMapping(product, wipSp));
expansion.addInitialInventory(new InitialInventory(product, wipSp, 0.0));
previousWipSp = wipSp;
}
}
LOG.info(String.format("[路由展开] 完成: '%s' → 生成 WIP库存点=%d, BOM链接=%d, 展开后库存点=%d, 映射=%d, 初始库存=%d",
id, wipCount, bomLinkCount,
expansion.getStockingPoints().size(),
expansion.getProductSpMappings().size(),
expansion.getInitialInventories().size()));
return expansion;
}
// ==================== Getters ====================
public String getId() { return id; }
public String getName() { return name; }
public Product getProduct() { return product; }
public StockingPoint getFinalSp() { return finalSp; }
public List<RoutingStep> getSteps() { return Collections.unmodifiableList(steps); }
/**
* 获取路由中所有工序 (按顺序)。
*/
public List<Operation> getOperations() {
List<Operation> ops = new ArrayList<>();
for (RoutingStep step : steps) {
ops.add(step.getOperation());
}
return ops;
}
}
\ No newline at end of file
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;
import java.util.Collections;
/**
* 多工序路由测试数据构建器
* 多工序路由测试数据构建器 — 使用 {@link Routing} 自动管理工序间 WIP。
*
* <h3>测试场景: 3 工序串行生产同一产品 P</h3>
* <h3>测试场景: 3 工序串行生产同一产品 P, 第一道工序投料消耗原材料 RM</h3>
* <pre>
* OP_Cut(下料) ──→ P@SP_WIP1 ──→ OP_Rough(粗加工) ──→ P@SP_WIP2 ──→ OP_Finish(精加工) ──→ P@SP_FG ──→ 销售
* Unit_Cut ↑ Unit_Rough ↑ Unit_Finish ↑
* OP_Rough 消耗 OP_Finish 消耗 销售需求 50/周期
* P@SP_WIP1 (BOM) P@SP_WIP2 (BOM)
* 原材料 RM → OP_Cut(下料) → WIP_R001_1 → OP_Rough(粗加工) → WIP_R001_2 → OP_Finish(精加工) → SP_FG(成品)
* ↑消耗RM@SP_RM×1
*
* 关键验证:
* 工艺路线 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 个工序各自消耗各自设备的产能 (独立计算)
* - 只有最后一道工序 OP_Finish 的产出计入成品供应
* - 中间工序的产出被 DependentDemand 抵消, 最终产出 = 销售需求, 不是 3×
* - 只有最后一道工序的产出计入成品供应
* - 中间 WIP 被 BOM 约束完全消耗 + RoutingConstraint 强制产量一致
* - 用户无需手动配置中间 WIP 库存点、BOM 输入
* </pre>
*/
public class RoutingTestDataBuilder extends TestDataBuilder {
......@@ -37,73 +50,85 @@ public class RoutingTestDataBuilder extends TestDataBuilder {
Period p3 = new Period(2, "P3", baseDate.plusDays(2));
periods.addAll(Arrays.asList(p1, p2, p3));
// === 产品: 只有 1 个成品 P ===
// === 产品: 成品 P + 原材料 RM ===
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 spWIP2 = new StockingPoint("SP_WIP2", "粗加工→精加工缓冲区");
// === 库存点: 成品库 + 原材料库 ===
StockingPoint spFG = new StockingPoint("SP_FG", "成品库");
stockingPoints.addAll(Arrays.asList(spWIP1, spWIP2, spFG));
StockingPoint spRM = new StockingPoint("SP_RM", "原材料库");
stockingPoints.addAll(Arrays.asList(spFG, spRM));
// === 产品→库存点映射 (P 在三个库存点都有) ===
productSpMappings.add(new ProductSpMapping(prodP, spWIP1));
productSpMappings.add(new ProductSpMapping(prodP, spWIP2));
// === 产品→库存点映射 ===
productSpMappings.add(new ProductSpMapping(prodP, spFG));
// === 3 道工序, 都产出产品 P, 但分属不同设备 ===
// 下料: 1.0h/件, 设备 Unit_Cut, 产出到 WIP1 缓冲区
Operation opCut = new Operation("OP_Cut", "下料", "Unit_Cut",
new OperationOutput(prodP, spWIP1), 1.0, 1.0, false, 0, 1.0);
// 粗加工: 1.5h/件, 设备 Unit_Rough, 产出到 WIP2 缓冲区
Operation opRough = new Operation("OP_Rough", "粗加工", "Unit_Rough",
new OperationOutput(prodP, spWIP2), 1.5, 1.0, false, 0, 1.0);
// 精加工: 2.0h/件, 设备 Unit_Finish, 产出到成品库
Operation opFinish = new Operation("OP_Finish", "精加工", "Unit_Finish",
new OperationOutput(prodP, spFG), 2.0, 1.0, false, 0, 1.0);
operations.addAll(Arrays.asList(opCut, opRough, opFinish));
// === BOM / OperationInput: 定义工序间流转 ===
// OP_Rough 消耗 P@SP_WIP1 (即 OP_Cut 的产出), factor=1.0 (1:1 无损耗)
operationInputs.add(new OperationInput(opRough, prodP, spWIP1, 1.0));
// OP_Finish 消耗 P@SP_WIP2 (即 OP_Rough 的产出), factor=1.0
operationInputs.add(new OperationInput(opFinish, prodP, spWIP2, 1.0));
// === 设备产能: 三个设备各有独立产能 ===
// Unit_Cut: 最大 200h/周期
productSpMappings.add(new ProductSpMapping(prodRM, spRM));
// === 原材料采购工序 (供应 RM 到 SP_RM) ===
Operation opProcureRM = new Operation("OP_Procure_RM", "采购原材料", "Unit_RM",
new OperationOutput(prodRM, spRM), 0.5, 1.0, false, 0, 1.0);
operations.add(opProcureRM);
// === 工艺路线: 产品 P 经过 3 道工序生产, 最终入库 SP_FG ===
// 对应 Quintiq: Routing R001, 关联 Product P 和 StockingPoint SP_FG
Routing routing = new Routing("R001", "P生产工艺", prodP, spFG);
routing.addOperation(new Operation("OP_Cut", "下料", "Unit_Cut", 1.0, 1.0, false, 0, 1.0));
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));
operations.addAll(routing.getOperations());
routings.add(routing); // 注册到数据构建器, 供 RoutingConstraint 使用
// === 展开工艺路线: 自动生成 WIP 库存点 + BOM 输入 + 工序产出配置 ===
// 只有最后工序 (OP_Finish) 产出成品, 中间工序 WIP 自动流转
RoutingExpansion expansion = routing.expand();
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) {
unitPeriods.add(new UnitPeriod("Unit_Cut", p, 0.0, 200.0, false));
}
// Unit_Rough: 最大 200h/周期
for (Period p : periods) {
unitPeriods.add(new UnitPeriod("Unit_Rough", p, 0.0, 200.0, false));
}
// Unit_Finish: 最大 200h/周期
for (Period p : periods) {
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(prodRM, spRM, 0.0));
// === 销售需求: 只在成品库 SP_FG, 每周期 50 ===
for (Period p : periods) {
salesDemands.add(new SalesDemand(prodP, spFG, p, 50.0, 1.0));
}
// === 库存规格: 只在成品库设目标/最小/最大 ===
// WIP 缓冲区不设库存规格 (不约束中间库存)
// === 库存规格 ===
for (Period p : periods) {
inventorySpecs.add(new InventorySpec(prodP, spFG, 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));
}
// === 供应规格: 只统计精加工(最后一道工序)的产出 ===
// === 供应规格: 精加工产出 + 原材料采购 ===
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 权重 ===
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
package com.aps.macroplanner.output;
import com.aps.entity.Algorithm.Chromosome;
import com.aps.entity.Schedule.SceneChromsome;
import com.aps.entity.Schedule.SceneDetail;
import com.aps.macroplanner.data.*;
import com.aps.macroplanner.model.MacroPlannerModel;
import com.aps.macroplanner.output.dto.*;
import com.aps.service.plan.SceneService;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.google.ortools.linearsolver.MPSolver;
import com.google.ortools.linearsolver.MPVariable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
/**
* 优化结果写入器 — 将求解器变量值回写到业务对象 DTO 并序列化为 JSON。
*
* <h3>对应 Quintiq 原模型</h3>
* <pre>
* PeriodTaskResult ← CapacityPlanningAlgorithmHandleFeasibleOperationPeriodTask
* SalesDemandResult ← CapacityPlanningAlgorithmHandleFeasibleSalesDemand
* PispipResult ← CapacityPlanningAlgorithmHandleFeasibleProductInStockingPointInPeriod
* KpiResult ← DisplayGoalValue + CreateKPISnapshot
* SolverStatistics ← SnapshotMacroPlannerOptimizer
* </pre>
*
* <h3>输出格式</h3>
* <pre>
* optimization_result.json:
* {
* "metadata": { "timestamp": "...", "solver": "SCIP", "version": "1.0.0" },
* "periodTasks": [ { "operationId": "OP1", "periodIndex": 0, "productionQty": 100.0, ... } ],
* "salesDemands": [ { "salesDemandId": "SD1", "fulfilledQty": 80.0, "unmetQty": 20.0, ... } ],
* "pispips": [ { "productId": "PA", "spId": "SP1", "endingInventory": 50.0, ... } ],
* "kpis": { "objectiveValue": 1356.0, "entries": [ ... ] },
* "statistics": { "numVariables": 191, "numConstraints": 144, "elapsedSeconds": 0.05, ... }
* }
* </pre>
*/
public class ResultWriter {
/** 默认输出目录 */
private static final String OUTPUT_DIR = "src/main/java/com/mp/log/";
/** 默认输出文件名 */
private static final String OUTPUT_FILE = "optimization_result.json";
private final MacroPlannerModel model;
private final TestDataBuilder data;
private final long startTimeMs;
/**
* 是否使用 GZIP 压缩,默认 true
*/
private boolean useCompression = false;
ObjectMapper objectMapper = createObjectMapper();
private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(SceneService.class);
private String getFileExtension() {
return useCompression ? ".json.gz" : ".json";
}
public ResultWriter(MacroPlannerModel model, TestDataBuilder data, long startTimeMs) {
this.model = model;
this.data = data;
this.startTimeMs = startTimeMs;
}
private File getResultDirectory() {
File resultDir = new File("mp/result");
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) {
OptimizationResult result = buildResult();
if (result == null) {
logger.warn("对象不能为空");
return false;
}
if (sceneId == null || sceneId.trim().isEmpty()) {
logger.warn("场景ID不能为空");
return false;
}
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);
}
} 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("验证读取失败");
}
}
} 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 (Exception e) {
logger.error("保存文件失败,场景ID: " + sceneId, e);
return false;
}
}
/**
* 从文件中读取 Chromosome 对象,并拆分文件读取与反序列化耗时。
*/
public OptimizationResult getResultToFile(String sceneId) {
if (sceneId == null || sceneId.trim().isEmpty()) {
logger.warn("场景ID不能为空");
return null;
}
try {
long totalStart = System.nanoTime();
File file = getOptimizationFile(sceneId);
if (!file.exists()) {
logger.warn("文件不存在: {}", file.getAbsolutePath());
return null;
}
long fileSize = file.length();
if (fileSize == 0) {
logger.warn("文件为空: {}", file.getAbsolutePath());
return null;
}
logger.info("正在从文件加载: {}, sceneId={}, fileSize={} bytes",
file.getAbsolutePath(), sceneId, fileSize);
long deserializeStart = System.nanoTime();
OptimizationResult optimizationResult;
if (useCompression) {
try (FileInputStream fis = new FileInputStream(file);
GZIPInputStream gzis = new GZIPInputStream(fis)) {
optimizationResult = objectMapper.readValue(gzis, OptimizationResult.class);
}
} else {
try (FileInputStream fis = new FileInputStream(file)) {
optimizationResult = objectMapper.readValue(fis, OptimizationResult.class);
}
}
long deserializeMs = (System.nanoTime() - deserializeStart) / 1_000_000;
long totalMs = (System.nanoTime() - totalStart) / 1_000_000;
logger.info("加载成功,场景ID: {}, fileSize={} bytes, deserialize={} ms, total={} ms",
sceneId, fileSize, deserializeMs, totalMs);
return optimizationResult;
} catch (Exception e) {
logger.error("加载文件失败,场景ID: " + sceneId, e);
throw new RuntimeException("加载文件失败: " + e.getMessage(), e);
}
}
private ObjectMapper createObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.disable(SerializationFeature.INDENT_OUTPUT);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return objectMapper;
}
/**
* 收集所有结果并返回 JSON 字符串。
*/
public String toJsonString() {
return toJson(buildResult());
}
// ==================== 构建 OptimizationResult ====================
private OptimizationResult buildResult() {
OptimizationResult result = new OptimizationResult();
// 元数据
result.setTimestamp(LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
result.setSolver("SCIP (via Google OR-Tools)");
result.setVersion("1.0.0");
// 业务结果
buildPeriodTasks(result);
buildSalesDemands(result);
buildPispips(result);
result.setUnitCapacities(buildUnitCapacities());
result.setProductNetwork(buildProductNetwork());
result.setDemandSummary(buildDemandSummary());
// KPI + 统计
result.setKpis(buildKpis());
result.setStatistics(buildStatistics());
return result;
}
// ==================== PeriodTaskResult ====================
private void buildPeriodTasks(OptimizationResult result) {
for (Operation op : data.getOperations()) {
for (UnitOperation uo : op.getUnitOperations()) {
for (Period p : data.getPeriods()) {
String key = op.ptQtyKey(uo, p.getIndex());
double ptQty = solutionValue(model.getPtQtyVars(), key);
if (ptQty < 0.001) continue;
PeriodTaskResult pt = new PeriodTaskResult();
pt.setOperationId(op.getId());
pt.setOperationName(op.getName());
pt.setUnitId(uo.getUnitId());
pt.setPeriodIndex(p.getIndex());
pt.setPeriodStartDate(p.getStartDate().toString());
pt.setProductionQty(ptQty);
pt.setCapacityCoeff(uo.getCapacityCoeff());
pt.setCapacityUsed(ptQty * uo.getCapacityCoeff());
if (uo.hasLotSize()) {
pt.setLotSize(uo.getLotSize());
}
pt.setLotSizeOver(solutionValue(model.getPtLotSizeOverVars(), key));
pt.setLotSizeUnder(solutionValue(model.getPtLotSizeUnderVars(), key));
// 产出信息
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);
}
}
}
}
// ==================== UnitCapacityResult ====================
/**
* 按设备聚合周期任务, 结合 UnitPeriod.maxCapacity 计算设备利用率。
*
* <p>利用率 = capacityUsed / maxCapacity (每周期和整体)。</p>
*/
/**
* 统计各 Unit 的产能使用情况: 每周期产能占用量、利用率、超载/未满足量。
*/
// ==================== UnitCapacityResult ====================
/**
* 统计各 Unit 的产能使用情况: 每周期产能占用量、利用率、超载/未满足量。
*/
private java.util.List<UnitCapacityResult> buildUnitCapacities() {
java.util.Map<String, UnitCapacityResult> unitMap = new java.util.LinkedHashMap<>();
for (UnitPeriod up : data.getUnitPeriods()) {
String uid = up.getUnitId();
UnitCapacityResult ucr = unitMap.computeIfAbsent(uid, k -> {
UnitCapacityResult r = new UnitCapacityResult();
r.setUnitId(k);
return r;
});
// 计算该 unit 在此周期的实际产能占用量
UnitCapacityResult.UnitPeriodDetail pe = new UnitCapacityResult.UnitPeriodDetail();
pe.periodIndex = up.getPeriod().getIndex();
pe.periodStartDate = up.getPeriod().getStartDate().toString();
double capacityUsed = 0;
for (Operation op : data.getOperations()) {
for (UnitOperation uo : op.getUnitOperations()) {
if (!uo.getUnitId().equals(uid)) continue;
String ptKey = op.ptQtyKey(uo, up.getPeriod().getIndex());
double ptQty = solutionValue(model.getPtQtyVars(), ptKey);
if (ptQty < 0.001) continue;
double capUsed = ptQty * uo.getCapacityCoeff();
capacityUsed += capUsed;
// 工序级明细
UnitCapacityResult.UnitTaskInfo task = new UnitCapacityResult.UnitTaskInfo();
task.operationId = op.getId();
task.operationName = op.getName();
task.productionQty = ptQty;
task.capacityUsed = capUsed;
task.capacityCoeff = uo.getCapacityCoeff();
if (uo.hasLotSize()) {
task.lotSize = uo.getLotSize();
task.lotSizeOver = solutionValue(model.getPtLotSizeOverVars(), ptKey);
task.lotSizeUnder = solutionValue(model.getPtLotSizeUnderVars(), ptKey);
}
for (OperationOutput oo : op.getOutputs()) {
task.outputs.add(oo.getProductId() + "@" + oo.getSpId());
}
pe.tasks.add(task);
}
}
double maxCap = up.getMaxCapacity();
double utilization = maxCap > 0 ? capacityUsed / maxCap : 0;
double overloaded = solutionValue(model.getCapacityOverloadedVars(), up.getKey());
double notMet = solutionValue(model.getCapacityNotMetVars(), up.getKey());
pe.maxCapacity = maxCap;
pe.capacityUsed = capacityUsed;
pe.utilization = utilization;
pe.overloaded = overloaded;
pe.notMet = notMet;
ucr.getPeriodDetails().add(pe);
// 累加汇总
ucr.setTotalMaxCapacity(ucr.getTotalMaxCapacity() + maxCap);
ucr.setTotalCapacityUsed(ucr.getTotalCapacityUsed() + capacityUsed);
ucr.setTotalOverloaded(ucr.getTotalOverloaded() + overloaded);
ucr.setTotalNotMet(ucr.getTotalNotMet() + notMet);
}
// 计算整体利用率
for (UnitCapacityResult ucr : unitMap.values()) {
if (ucr.getTotalMaxCapacity() > 0) {
ucr.setOverallUtilization(
ucr.getTotalCapacityUsed() / ucr.getTotalMaxCapacity());
}
}
return new java.util.ArrayList<>(unitMap.values());
}
// ==================== SalesDemandResult ====================
private void buildSalesDemands(OptimizationResult result) {
for (SalesDemand sd : data.getSalesDemands()) {
SalesDemandResult sr = new SalesDemandResult();
sr.setSalesDemandId(sd.getKey());
sr.setProductId(sd.getProduct().getId());
sr.setSpId(sd.getStockingPoint().getId());
sr.setPeriodIndex(sd.getPeriod().getIndex());
sr.setPeriodStartDate(sd.getPeriod().getStartDate().toString());
sr.setDemandQty(sd.getQuantity());
sr.setPriority(sd.getPriority());
double fulfilled = solutionValue(model.getSalesDemandQtyVars(), sd.getKey());
sr.setFulfilledQty(fulfilled);
sr.setUnmetQty(Math.max(0, sd.getQuantity() - fulfilled));
sr.setFulfillmentRate(sd.getQuantity() > 0 ? fulfilled / sd.getQuantity() : 1.0);
// DemandSlack (PISPIP 级别)
String invKey = sd.getProduct().getId() + "_" + sd.getStockingPoint().getId()
+ "_" + sd.getPeriod().getIndex();
sr.setDemandSlack(solutionValue(model.getDemandSlackVars(), invKey));
result.getSalesDemands().add(sr);
}
}
// ==================== DemandSummaryResult ====================
/**
* 按产品汇总需求量/满足量/满足率。
*/
private java.util.List<DemandSummaryResult> buildDemandSummary() {
java.util.Map<String, DemandSummaryResult> prodMap = new java.util.LinkedHashMap<>();
for (SalesDemand sd : data.getSalesDemands()) {
String prodId = sd.getProduct().getId();
DemandSummaryResult dsr = prodMap.computeIfAbsent(prodId, k -> {
DemandSummaryResult r = new DemandSummaryResult();
r.setProductId(k);
return r;
});
double fulfilled = solutionValue(model.getSalesDemandQtyVars(), sd.getKey());
double demand = sd.getQuantity();
double unmet = Math.max(0, demand - fulfilled);
double rate = demand > 0 ? fulfilled / demand : 1.0;
DemandSummaryResult.PeriodEntry pe = new DemandSummaryResult.PeriodEntry();
pe.periodIndex = sd.getPeriod().getIndex();
pe.periodStartDate = sd.getPeriod().getStartDate().toString();
pe.demandQty = demand;
pe.fulfilledQty = fulfilled;
pe.unmetQty = unmet;
pe.fulfillmentRate = rate;
dsr.getPeriodEntries().add(pe);
dsr.setTotalDemand(dsr.getTotalDemand() + demand);
dsr.setTotalFulfilled(dsr.getTotalFulfilled() + fulfilled);
dsr.setTotalUnmet(dsr.getTotalUnmet() + unmet);
}
for (DemandSummaryResult dsr : prodMap.values()) {
if (dsr.getTotalDemand() > 0) {
dsr.setFulfillmentRate(dsr.getTotalFulfilled() / dsr.getTotalDemand());
} else {
dsr.setFulfillmentRate(1.0);
}
}
return new java.util.ArrayList<>(prodMap.values());
}
// ==================== PispipResult ====================
private void buildPispips(OptimizationResult result) {
for (Product prod : data.getProducts()) {
for (StockingPoint sp : data.getStockingPointsForProduct(prod.getId())) {
for (Period p : data.getPeriods()) {
PispipResult pr = new PispipResult();
pr.setProductId(prod.getId());
pr.setSpId(sp.getId());
pr.setPeriodIndex(p.getIndex());
pr.setPeriodStartDate(p.getStartDate().toString());
String invKey = prod.getId() + "_" + sp.getId() + "_" + p.getIndex();
// 期初库存
double openingInv;
if (p.getIndex() == 0) {
openingInv = data.getInitialInventory(prod.getId(), sp.getId());
} else {
String prevKey = prod.getId() + "_" + sp.getId() + "_" + (p.getIndex() - 1);
openingInv = solutionValue(model.getInvQtyVars(), prevKey);
}
pr.setOpeningInventory(openingInv);
// 期末库存
double endingInv = solutionValue(model.getInvQtyVars(), invKey);
pr.setEndingInventory(endingInv);
// 库存规格
InventorySpec spec = data.getInventorySpecFor(prod, sp, p);
if (spec != null) {
if (spec.hasTarget()) pr.setTargetInventoryLevel(spec.getTargetLevel());
if (spec.hasTargetInDays()) pr.setTargetInventoryDays(spec.getTargetInDays());
if (spec.hasMinLevel()) pr.setMinInventoryLevel(spec.getMinLevel());
if (spec.hasMinLevelInDays()) pr.setMinInventoryDays(spec.getMinLevelInDays());
if (spec.hasMaxLevel()) pr.setMaxInventoryLevel(spec.getMaxLevel());
if (spec.hasMaxLevelInDays()) pr.setMaxInventoryDays(spec.getMaxLevelInDays());
}
pr.setBelowTarget(solutionValue(model.getInvQtyUnderTargetVars(), invKey));
pr.setBelowMin(solutionValue(model.getMinInvQtyUnderVars(), invKey));
pr.setAboveMax(solutionValue(model.getMaxInvQtyOverVars(), invKey));
// 需求满足量 (安全库存天数)
pr.setDemandFulfillment(solutionValue(model.getDemandFulfillmentVars(), invKey));
// 需求松弛
pr.setDemandSlack(solutionValue(model.getDemandSlackVars(), invKey));
// 销售需求
double totalSales = 0;
double totalDemand = 0;
for (SalesDemand sd : data.getSalesDemandsFor(prod, sp, p)) {
double f = solutionValue(model.getSalesDemandQtyVars(), sd.getKey());
totalSales += f;
totalDemand += sd.getQuantity();
}
pr.setSalesDemandQty(totalDemand);
pr.setSalesFulfilledQty(totalSales);
// BOM 依赖需求
double totalDepDemand = 0;
String depKey = prod.getId() + "_" + sp.getId() + "_" + p.getIndex();
totalDepDemand = solutionValue(model.getDependentDemandVars(), depKey);
pr.setDependentDemandQty(totalDepDemand);
// 总流出
pr.setTotalOutflow(totalSales + totalDepDemand);
// 到货量 (考虑 lead time)
double totalArrived = 0;
double totalInProgress = 0;
for (Operation op : data.getOperations()) {
if (!op.producesProductAtSp(prod.getId(), sp.getId())) continue;
int leadTime = op.getLeadTimeDays();
Period arrivalSrcPeriod = data.getPeriodOffsetByDays(p, leadTime);
// 汇总所有产线
for (UnitOperation uo : op.getUnitOperations()) {
// 到货
if (arrivalSrcPeriod != null) {
String ptKey = op.ptQtyKey(uo, arrivalSrcPeriod.getIndex());
totalArrived += solutionValue(model.getPtQtyVars(), ptKey);
}
// 生产中
String ptKeyNow = op.ptQtyKey(uo, p.getIndex());
double ptQtyNow = solutionValue(model.getPtQtyVars(), ptKeyNow);
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.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.factor = input.getFactor();
String opDemandKey = input.getKey() + "_" + p.getIndex();
bc.consumedQty = solutionValue(
model.getOperationDemandQtyVars(), opDemandKey);
pd.bomConsumptions.add(bc);
}
pr.getProductionDetails().add(pd);
}
}
}
pr.setProductionArrived(totalArrived);
pr.setProductionInProgress(totalInProgress);
// 在途到货
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()))) {
inTransit += its.getQuantity();
}
}
pr.setInTransitArrival(inTransit);
// 总流入
pr.setTotalInflow(totalArrived + inTransit);
result.getPispips().add(pr);
}
}
}
}
// ==================== ProductNetworkResult ====================
/**
* 构建产品生产网络 — 从成品向下逐级展开到原材料, 形成完整 BOM 供应链视图。
*
* <h3>展开逻辑</h3>
* <ol>
* <li>确定节点类型: 不被任何工序消耗 → 成品(根节点), 被消耗但也被生产 → 半成品, 只被消耗 → 原材料</li>
* <li>从成品开始, 通过 BOM 关系向下递归展开子物料</li>
* <li>每层记录: 供应源、消费者、库存汇总、BOM子节点</li>
* </ol>
*/
private ProductNetworkResult buildProductNetwork() {
ProductNetworkResult network = new ProductNetworkResult();
// Step 1: 找出被消耗的产品集合 (用于判断成品 vs 半成品 vs 原材料)
java.util.Set<String> consumedProductIds = new java.util.HashSet<>();
for (OperationInput input : data.getOperationInputs()) {
consumedProductIds.add(input.getInputProduct().getId());
}
// 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; }
}
hasProduction.put(prod.getId(), found);
}
// Step 3: 确定成品 (不被任何工序消耗且有生产)
java.util.Set<String> expandedNodes = new java.util.HashSet<>();
for (Product prod : data.getProducts()) {
if (!consumedProductIds.contains(prod.getId())) {
// 成品: 找一种库存点来展开
for (StockingPoint sp : data.getStockingPointsForProduct(prod.getId())) {
SupplyChainNode root = network.getOrCreateNode(prod.getId(), sp.getId());
expandNode(root, network, 0, consumedProductIds, hasProduction,
expandedNodes);
network.getFinishedGoods().add(root);
}
}
}
// Step 4: 对每个节点计算供应/需求/库存汇总
for (SupplyChainNode node : network.getAllNodes().values()) {
fillNodeSummary(node);
}
return network;
}
/**
* 递归展开 BOM 树: 为当前节点填充供应源、消费者, 并递归展开子物料。
*/
private void expandNode(SupplyChainNode node, ProductNetworkResult network,
int level, java.util.Set<String> consumedProductIds,
java.util.Map<String, Boolean> hasProduction,
java.util.Set<String> expandedNodes) {
String nodeKey = node.getProductId() + "@" + node.getSpId();
if (!expandedNodes.add(nodeKey)) return; // 已展开过, 跳过防重复
node.setLevel(level);
String prodId = node.getProductId();
String spId = node.getSpId();
// --- 供应源: 哪些工序生产这个产品@库存点 ---
java.util.Set<String> visitedSupplySources = new java.util.HashSet<>();
for (Operation op : data.getOperations()) {
if (!op.producesProductAtSp(prodId, spId)) continue;
String srcKey = op.getId() + "_" + spId;
if (!visitedSupplySources.add(srcKey)) continue; // 防重复
// 计算总产量 (汇总所有产线)
double totalProd = 0;
double totalCap = 0;
for (Period p : data.getPeriods()) {
for (UnitOperation uo : op.getUnitOperations()) {
String key = op.ptQtyKey(uo, p.getIndex());
double qty = solutionValue(model.getPtQtyVars(), key);
totalProd += qty;
totalCap += qty * uo.getCapacityCoeff();
}
}
SupplyChainNode.SupplySource src = new SupplyChainNode.SupplySource();
src.type = "OPERATION";
src.operationId = op.getId();
src.operationName = op.getName();
src.unitId = op.getUnitId();
src.totalProduction = totalProd;
src.capacityUsed = totalCap;
if (op.hasLotSize()) src.lotSize = op.getLotSize();
node.getSupplySources().add(src);
}
// 在途供应
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);
}
}
// 外部采购 (无生产工序)
if (node.getSupplySources().isEmpty()) {
SupplyChainNode.SupplySource src = new SupplyChainNode.SupplySource();
src.type = "EXTERNAL";
src.totalProduction = 0;
node.getSupplySources().add(src);
}
// --- 消费者: 哪些产品消耗这个节点 ---
java.util.Set<String> visitedConsumers = new java.util.HashSet<>();
for (OperationInput input : data.getOperationInputs()) {
if (!input.getInputProduct().getId().equals(prodId)
|| !input.getInputSp().getId().equals(spId)) continue;
double totalConsumed = 0;
for (Period p : data.getPeriods()) {
String key = input.getKey() + "_" + p.getIndex();
totalConsumed += solutionValue(model.getOperationDemandQtyVars(), key);
}
// 消费者 = 这个工序的产出产品
for (OperationOutput oo : input.getOperation().getOutputs()) {
String ciKey = oo.getProductId() + "@" + oo.getSpId()
+ "_" + input.getOperation().getId();
if (!visitedConsumers.add(ciKey)) continue; // 防重复
SupplyChainNode.ConsumerInfo ci = new SupplyChainNode.ConsumerInfo();
ci.consumerProductId = oo.getProductId();
ci.consumerSpId = oo.getSpId();
ci.operationId = input.getOperation().getId();
ci.operationName = input.getOperation().getName();
ci.factor = input.getFactor();
ci.totalConsumed = totalConsumed;
node.getConsumers().add(ci);
}
}
// --- BOM 子物料: 这个产品的生产消耗什么 ---
java.util.Set<String> expandedChildren = new java.util.HashSet<>();
next_child:
for (Operation op : data.getOperations()) {
if (!op.producesProductAtSp(prodId, spId)) continue;
for (OperationInput input : data.getOperationInputs()) {
if (!input.getOperation().getId().equals(op.getId())) continue;
String childKey = input.getInputProduct().getId()
+ "@" + input.getInputSp().getId();
if (expandedChildren.contains(childKey)) continue;
expandedChildren.add(childKey);
// 汇总该物料的总消耗量
double totalConsumed = 0;
for (Period p : data.getPeriods()) {
String key = input.getKey() + "_" + p.getIndex();
totalConsumed += solutionValue(model.getOperationDemandQtyVars(), key);
}
SupplyChainNode.BomChild child = new SupplyChainNode.BomChild();
child.productId = input.getInputProduct().getId();
child.spId = input.getInputSp().getId();
child.totalFactor = input.getFactor();
child.totalConsumedQty = totalConsumed;
node.getChildren().add(child);
// 递归展开子节点
SupplyChainNode childNode = network.getOrCreateNode(
child.productId, child.spId);
expandNode(childNode, network, level + 1,
consumedProductIds, hasProduction, expandedNodes);
}
break; // 只展开第一个匹配工序的 BOM (多 Routing 场景下各工序 BOM 相同)
}
}
/** 填充节点的跨周期汇总数据 */
private void fillNodeSummary(SupplyChainNode node) {
String prodId = node.getProductId();
String spId = node.getSpId();
SupplySummary s = new SupplySummary();
node.setSummary(s);
int n = data.getPeriods().size();
// 库存汇总
double totalEndingInv = 0;
for (Period p : data.getPeriods()) {
String invKey = prodId + "_" + spId + "_" + p.getIndex();
totalEndingInv += solutionValue(model.getInvQtyVars(), invKey);
}
s.setInitialInventory(data.getInitialInventory(prodId, spId));
s.setFinalInventory(solutionValue(model.getInvQtyVars(),
prodId + "_" + spId + "_" + (n - 1)));
s.setAverageInventory(n > 0 ? totalEndingInv / n : 0);
// 生产汇总
double totalProd = 0;
double totalArrived = 0;
for (Operation op : data.getOperations()) {
if (!op.producesProductAtSp(prodId, spId)) continue;
for (Period p : data.getPeriods()) {
for (UnitOperation uo : op.getUnitOperations()) {
String ptKey = op.ptQtyKey(uo, p.getIndex());
totalProd += solutionValue(model.getPtQtyVars(), ptKey);
// 到货 (考虑 lead time)
Period arrSrc = data.getPeriodOffsetByDays(p, op.getLeadTimeDays());
if (arrSrc != null) {
String arrKey = op.ptQtyKey(uo, arrSrc.getIndex());
totalArrived += solutionValue(model.getPtQtyVars(), arrKey);
}
}
}
}
s.setTotalProduction(totalProd);
s.setTotalArrived(totalArrived);
// 在途汇总
double totalInTransit = 0;
for (InTransitSupply its : data.getInTransitSupplies()) {
if (its.getProduct().getId().equals(prodId)
&& its.getStockingPoint().getId().equals(spId)) {
totalInTransit += its.getQuantity();
}
}
s.setTotalInTransit(totalInTransit);
// 需求汇总
double totalSalesDemand = 0, totalSalesFulfilled = 0;
double totalDepDemand = 0, totalDemandFulf = 0, totalSlack = 0;
for (Period p : data.getPeriods()) {
String invKey = prodId + "_" + spId + "_" + p.getIndex();
totalDepDemand += solutionValue(model.getDependentDemandVars(), invKey);
totalDemandFulf += solutionValue(model.getDemandFulfillmentVars(), invKey);
totalSlack += solutionValue(model.getDemandSlackVars(), invKey);
for (SalesDemand sd : data.getSalesDemands()) {
if (sd.getProduct().getId().equals(prodId)
&& sd.getStockingPoint().getId().equals(spId)
&& sd.getPeriod().getIndex() == p.getIndex()) {
totalSalesDemand += sd.getQuantity();
totalSalesFulfilled += solutionValue(model.getSalesDemandQtyVars(),
sd.getKey());
}
}
}
s.setTotalSalesDemand(totalSalesDemand);
s.setTotalSalesFulfilled(totalSalesFulfilled);
s.setTotalDependentDemand(totalDepDemand);
s.setTotalDemandFulfillment(totalDemandFulf);
s.setTotalDemandSlack(totalSlack);
node.setTotalSalesDemand(totalSalesDemand);
node.setTotalSalesFulfilled(totalSalesFulfilled);
node.setTotalDependentDemand(totalDepDemand);
// 库存规格偏差汇总
double belowTarget = 0, belowMin = 0, aboveMax = 0;
for (Period p : data.getPeriods()) {
String invKey = prodId + "_" + spId + "_" + p.getIndex();
belowTarget += solutionValue(model.getInvQtyUnderTargetVars(), invKey);
belowMin += solutionValue(model.getMinInvQtyUnderVars(), invKey);
aboveMax += solutionValue(model.getMaxInvQtyOverVars(), invKey);
}
s.setTotalBelowTarget(belowTarget);
s.setTotalBelowMin(belowMin);
s.setTotalAboveMax(aboveMax);
}
// ==================== KpiResult ====================
private KpiResult buildKpis() {
KpiResult kr = new KpiResult();
KPIWeights w = data.getKpiWeights();
double objValue = model.getSolver().objective().value();
kr.setObjectiveValue(objValue);
kr.addEntry("需求缺口(Fulfillment)",
model.getTotalFulfillment().solutionValue(),
w.getFulfillmentWeight(), false);
kr.addEntry("批次偏差(LotSize)",
model.getTotalLotSize().solutionValue(),
w.getLotSizeWeight(), false);
kr.addEntry("超库存(MaxInventory)",
model.getTotalMaxInventoryLevel().solutionValue(),
w.getMaxInventoryLevelWeight(), false);
kr.addEntry("欠库存(MinInventory)",
model.getTotalMinInventoryLevel().solutionValue(),
w.getMinInventoryLevelWeight(), false);
kr.addEntry("目标库存偏差(TargetInv)",
model.getTotalTargetInvLevel().solutionValue(),
w.getTargetInventoryLevelWeight(), false);
kr.addEntry("产能超载(UnitCapacity)",
model.getTotalUnitCapacity().solutionValue(),
w.getUnitCapacityWeight(), false);
kr.addEntry("供应目标偏差(SupplyTarget)",
model.getTotalSupplyTarget().solutionValue(),
w.getSupplyTargetWeight(), false);
kr.addEntry("最小供应不足(MinSupply)",
model.getTotalMinSupply().solutionValue(),
w.getMinSupplyWeight(), false);
kr.addEntry("最大供应超出(MaxSupply)",
model.getTotalMaxSupply().solutionValue(),
w.getMaxSupplyWeight(), false);
kr.addEntry("销售优先级(SalesPriority)",
model.getTotalSalesDemandPriority().solutionValue(),
w.getSalesDemandPriorityWeight(), true);
kr.addEntry("推迟惩罚(Postponement)",
model.getTotalPostponementPenalty().solutionValue(),
w.getPostponementPenaltyWeight(), false);
kr.addEntry("过程最大量(ProcessMax)",
model.getTotalProcessMaxQuantity().solutionValue(),
w.getProcessMaxQuantityWeight(), false);
return kr;
}
// ==================== SolverStatistics ====================
private SolverStatistics buildStatistics() {
SolverStatistics ss = new SolverStatistics();
MPSolver solver = model.getSolver();
ss.setStatus(solver.solve().toString());
ss.setNumVariables(solver.numVariables());
ss.setNumConstraints(solver.numConstraints());
ss.setElapsedSeconds((System.currentTimeMillis() - startTimeMs) / 1000.0);
ss.setObjectiveValue(solver.objective().value());
ss.setIterations(solver.iterations());
return ss;
}
// ==================== JSON 序列化 ====================
private String toJson(OptimizationResult result) {
JsonBuilder jb = new JsonBuilder();
jb.obj();
// metadata
jb.key("metadata").obj()
.key("timestamp").val(result.getTimestamp())
.key("solver").val(result.getSolver())
.key("version").val(result.getVersion())
.endObj();
// periodTasks
jb.key("periodTasks").arr();
for (PeriodTaskResult pt : result.getPeriodTasks()) {
jb.obj();
jb.key("operationId").val(pt.getOperationId());
jb.key("operationName").val(pt.getOperationName());
jb.key("unitId").val(pt.getUnitId());
jb.key("periodIndex").val(pt.getPeriodIndex());
jb.key("periodStartDate").val(pt.getPeriodStartDate());
jb.key("productionQty").val(pt.getProductionQty());
jb.key("capacityUsed").val(pt.getCapacityUsed());
jb.key("capacityCoeff").val(pt.getCapacityCoeff());
jb.key("lotSize").valOpt(pt.getLotSize());
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();
}
jb.endArr();
jb.endObj();
}
jb.endArr();
// salesDemands
jb.key("salesDemands").arr();
for (SalesDemandResult sr : result.getSalesDemands()) {
jb.obj();
jb.key("salesDemandId").val(sr.getSalesDemandId());
jb.key("productId").val(sr.getProductId());
jb.key("spId").val(sr.getSpId());
jb.key("periodIndex").val(sr.getPeriodIndex());
jb.key("periodStartDate").val(sr.getPeriodStartDate());
jb.key("demandQty").val(sr.getDemandQty());
jb.key("fulfilledQty").val(sr.getFulfilledQty());
jb.key("unmetQty").val(sr.getUnmetQty());
jb.key("demandSlack").val(sr.getDemandSlack());
jb.key("fulfillmentRate").val(sr.getFulfillmentRate());
jb.key("priority").val(sr.getPriority());
jb.endObj();
}
jb.endArr();
// pispips
jb.key("pispips").arr();
for (PispipResult pr : result.getPispips()) {
jb.obj();
jb.key("productId").val(pr.getProductId());
jb.key("spId").val(pr.getSpId());
jb.key("periodIndex").val(pr.getPeriodIndex());
jb.key("periodStartDate").val(pr.getPeriodStartDate());
jb.key("openingInventory").val(pr.getOpeningInventory());
jb.key("endingInventory").val(pr.getEndingInventory());
// 库存规格
jb.key("targetInventoryLevel").valOpt(pr.getTargetInventoryLevel());
jb.key("targetInventoryDays").valOpt(pr.getTargetInventoryDays());
jb.key("belowTarget").val(pr.getBelowTarget());
jb.key("minInventoryLevel").valOpt(pr.getMinInventoryLevel());
jb.key("minInventoryDays").valOpt(pr.getMinInventoryDays());
jb.key("belowMin").val(pr.getBelowMin());
jb.key("maxInventoryLevel").valOpt(pr.getMaxInventoryLevel());
jb.key("maxInventoryDays").valOpt(pr.getMaxInventoryDays());
jb.key("aboveMax").val(pr.getAboveMax());
// 流入
jb.key("productionArrived").val(pr.getProductionArrived());
jb.key("inTransitArrival").val(pr.getInTransitArrival());
jb.key("totalInflow").val(pr.getTotalInflow());
// 流出
jb.key("salesDemandQty").val(pr.getSalesDemandQty());
jb.key("salesFulfilledQty").val(pr.getSalesFulfilledQty());
jb.key("dependentDemandQty").val(pr.getDependentDemandQty());
jb.key("totalOutflow").val(pr.getTotalOutflow());
// 需求满足
jb.key("demandFulfillment").val(pr.getDemandFulfillment());
jb.key("demandSlack").val(pr.getDemandSlack());
// 生产明细
jb.key("productionInProgress").val(pr.getProductionInProgress());
jb.key("productionDetails").arr();
for (PispipResult.ProductionDetail pd : pr.getProductionDetails()) {
jb.obj();
jb.key("operationId").val(pd.operationId);
jb.key("operationName").val(pd.operationName);
jb.key("unitId").val(pd.unitId);
jb.key("quantity").val(pd.quantity);
jb.key("leadTimeDays").val(pd.leadTimeDays);
jb.key("bomConsumptions").arr();
for (PispipResult.BomConsumption bc : pd.bomConsumptions) {
jb.obj()
.key("inputProductId").val(bc.inputProductId)
.key("inputSpId").val(bc.inputSpId)
.key("factor").val(bc.factor)
.key("consumedQty").val(bc.consumedQty)
.endObj();
}
jb.endArr();
jb.endObj();
}
jb.endArr();
jb.endObj();
}
jb.endArr();
// kpis
KpiResult kr = result.getKpis();
jb.key("kpis").obj();
jb.key("objectiveValue").val(kr.getObjectiveValue());
jb.key("entries").arr();
for (KpiResult.KpiEntry e : kr.getEntries()) {
jb.obj()
.key("name").val(e.name)
.key("rawValue").val(e.rawValue)
.key("weight").val(e.weight)
.key("penalty").val(e.penalty)
.key("isBenefit").val(e.isBenefit)
.endObj();
}
jb.endArr();
jb.endObj();
// statistics
SolverStatistics ss = result.getStatistics();
jb.key("statistics").obj();
jb.key("status").val(ss.getStatus());
jb.key("numVariables").val(ss.getNumVariables());
jb.key("numConstraints").val(ss.getNumConstraints());
jb.key("elapsedSeconds").val(ss.getElapsedSeconds());
jb.key("objectiveValue").val(ss.getObjectiveValue());
jb.key("iterations").val(ss.getIterations());
jb.endObj();
jb.endObj();
return jb.toString();
}
// ==================== 文件写入 ====================
private String writeFile(String json) {
try {
Path dir = Paths.get(OUTPUT_DIR);
if (!Files.exists(dir)) {
Files.createDirectories(dir);
}
Path filePath = dir.resolve(OUTPUT_FILE);
Files.write(filePath, json.getBytes(StandardCharsets.UTF_8));
return filePath.toAbsolutePath().toString();
} catch (IOException e) {
System.err.println("[ERROR] JSON 文件写入失败: " + e.getMessage());
return null;
}
}
// ==================== 辅助方法 ====================
private static double solutionValue(Map<String, MPVariable> varMap, String key) {
MPVariable var = varMap.get(key);
if (var != null) {
return var.solutionValue();
}
return 0.0;
}
}
\ No newline at end of file
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
package com.aps.macroplanner.output.dto;
import java.util.ArrayList;
import java.util.List;
/**
* 产品生产网络结果 — 从成品向下逐级展开的完整 BOM 供应链视图。
*
* <h3>对应关系</h3>
* <pre>
* ProductNetworkResult —— 成品列表, 每个成品是一棵 BOM 树的根节点
* SupplyChainNode —— 单个产品, 包含: 供应(谁生产)、需求(谁消耗)、库存、子树(消耗的物料)
* SupplySummary —— 跨周期汇总的供应/需求/库存数据
* BomChild —— BOM 子物料链接 (消耗因子 + 数量)
* </pre>
*
* <h3>展开逻辑</h3>
* <pre>
* 1. 找到所有成品 (不被任何工序消耗的产品)
* 2. 对每个成品, 构建 SupplyChainNode
* 3. 对每个供应链节点, 查找消耗它的工序(向上 = 成品/半成品)
* 和它消耗的物料(向下 = BOM子节点)
* 4. 递归展开子节点直到原材料 (无 BOM 输入的工序产出, 或纯采购品)
* </pre>
*/
public class ProductNetworkResult {
/** 成品列表 (BOM 树的根节点) */
private final List<SupplyChainNode> finishedGoods = new ArrayList<>();
/** 所有节点的扁平索引 (按 nodeKey 查找, 用于去重) */
private final java.util.Map<String, SupplyChainNode> allNodes = new java.util.LinkedHashMap<>();
// ==================== Getters ====================
public List<SupplyChainNode> getFinishedGoods() { return finishedGoods; }
public java.util.Map<String, SupplyChainNode> getAllNodes() { return allNodes; }
/** 获取或创建节点 (按 productId@spId 去重) */
public SupplyChainNode getOrCreateNode(String productId, String spId) {
String key = productId + "@" + spId;
return allNodes.computeIfAbsent(key, k -> {
SupplyChainNode node = new SupplyChainNode();
node.setProductId(productId);
node.setSpId(spId);
return node;
});
}
}
\ No newline at end of file
package com.aps.macroplanner.output.dto;
/**
* 销售需求满足结果 — 对应 Quintiq 中 LeafSalesDemandInPeriod 的回写数据。
*
* <p>每个销售需求在每个周期的满足量、缺口、优先级等。</p>
*/
public class SalesDemandResult {
private String salesDemandId;
private String productId;
private String spId;
private int periodIndex;
private String periodStartDate;
/** 需求量 */
private double demandQty;
/** 满足量 (SalesDemandQty) */
private double fulfilledQty;
/** 未满足量 = demandQty - fulfilledQty */
private double unmetQty;
/** 需求松弛 (DemandSlack, 防止不可行) */
private double demandSlack;
/** 满足率 = fulfilledQty / demandQty */
private double fulfillmentRate;
/** 优先级 */
private double priority;
// ==================== Getters / Setters ====================
public String getSalesDemandId() { return salesDemandId; }
public void setSalesDemandId(String v) { this.salesDemandId = v; }
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 getDemandQty() { return demandQty; }
public void setDemandQty(double v) { this.demandQty = v; }
public double getFulfilledQty() { return fulfilledQty; }
public void setFulfilledQty(double v) { this.fulfilledQty = v; }
public double getUnmetQty() { return unmetQty; }
public void setUnmetQty(double v) { this.unmetQty = v; }
public double getDemandSlack() { return demandSlack; }
public void setDemandSlack(double v) { this.demandSlack = v; }
public double getFulfillmentRate() { return fulfillmentRate; }
public void setFulfillmentRate(double v) { this.fulfillmentRate = v; }
public double getPriority() { return priority; }
public void setPriority(double v) { this.priority = v; }
}
\ No newline at end of file
package com.aps.macroplanner.output.dto;
/**
* 求解器运行统计 — 对应 Quintiq 中 SnapshotMacroPlannerOptimizer 的运行统计部分。
*/
public class SolverStatistics {
/** 求解状态 */
private String status;
/** 变量数 */
private int numVariables;
/** 约束数 */
private int numConstraints;
/** 求解耗时 (秒) */
private double elapsedSeconds;
/** 目标函数值 */
private double objectiveValue;
/** 最佳界 (Best Bound) */
private Double bestBound;
/** 间隙 (Gap) */
private Double gap;
/** 迭代次数 */
private long iterations;
// ==================== Getters / Setters ====================
public String getStatus() { return status; }
public void setStatus(String v) { this.status = v; }
public int getNumVariables() { return numVariables; }
public void setNumVariables(int v) { this.numVariables = v; }
public int getNumConstraints() { return numConstraints; }
public void setNumConstraints(int v) { this.numConstraints = v; }
public double getElapsedSeconds() { return elapsedSeconds; }
public void setElapsedSeconds(double v) { this.elapsedSeconds = v; }
public double getObjectiveValue() { return objectiveValue; }
public void setObjectiveValue(double v) { this.objectiveValue = v; }
public Double getBestBound() { return bestBound; }
public void setBestBound(Double v) { this.bestBound = v; }
public Double getGap() { return gap; }
public void setGap(Double v) { this.gap = v; }
public long getIterations() { return iterations; }
public void setIterations(long v) { this.iterations = v; }
}
\ No newline at end of file
package com.aps.macroplanner.output.dto;
import java.util.ArrayList;
import java.util.List;
/**
* 供应链节点 — BOM 树中的一个节点, 表示某个产品在某个库存点的完整供需视图。
*
* <h3>节点类型 (level)</h3>
* <pre>
* 0 = 成品 (不被任何工序消耗)
* 1 = 半成品 (被消耗, 同时也被生产)
* 2+ = 原材料 (被消耗, 但不被本模型中的工序生产; 或纯采购品)
* </pre>
*/
public class SupplyChainNode {
// ==================== 基本信息 ====================
/** 产品 ID */
private String productId;
/** 库存点 ID */
private String spId;
/** BOM 层级 (0=成品, N=第N层物料) */
private int level;
// ==================== 供应信息 (谁生产这个产品) ====================
/** 供应来源: 工序生产、在途到货、还是纯外部采购 */
private final List<SupplySource> supplySources = new ArrayList<>();
// ==================== 需求信息 (谁消耗这个产品) ====================
/** 外部销售需求汇总 */
private double totalSalesDemand;
/** 外部销售满足量汇总 */
private double totalSalesFulfilled;
/** BOM 依赖需求汇总 (被哪些产品/工序消耗) */
private double totalDependentDemand;
/** 消费者列表: 哪些产品消耗这个节点 */
private final List<ConsumerInfo> consumers = new ArrayList<>();
// ==================== 库存信息 (跨周期汇总) ====================
/** 跨周期汇总: 供应/需求/库存 */
private SupplySummary summary;
// ==================== BOM 子节点 (这个产品消耗什么物料) ====================
/** 直接子物料列表 (BOM 下一层) */
private final List<BomChild> children = new ArrayList<>();
// ==================== 内嵌类 ====================
/** 供应来源 */
public static class SupplySource {
/** 来源类型: "OPERATION"(工序生产), "IN_TRANSIT"(在途), "EXTERNAL"(外部采购) */
public String type;
/** 工序 ID (type=OPERATION时有值) */
public String operationId;
/** 工序名称 */
public String operationName;
/** 设备 ID */
public String unitId;
/** 跨周期总产量 */
public double totalProduction;
/** 产能占用 */
public double capacityUsed;
/** 批量大小 (如有) */
public Double lotSize;
}
/** 消费者信息 */
public static class ConsumerInfo {
/** 消耗本产品的产品 ID */
public String consumerProductId;
/** 消耗本产品的库存点 ID */
public String consumerSpId;
/** 消耗本产品的工序 ID */
public String operationId;
/** 消耗本产品的工序名称 */
public String operationName;
/** BOM 消耗因子 */
public double factor;
/** 跨周期总消耗量 */
public double totalConsumed;
}
/** BOM 子物料 */
public static class BomChild {
/** 子物料产品 ID */
public String productId;
/** 子物料库存点 ID */
public String spId;
/** BOM 消耗因子 (每单位父产品消耗多少子物料) */
public double totalFactor;
/** 跨周期总消耗量 */
public double totalConsumedQty;
}
// ==================== 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 getLevel() { return level; }
public void setLevel(int v) { this.level = v; }
public List<SupplySource> getSupplySources() { return supplySources; }
public List<ConsumerInfo> getConsumers() { return consumers; }
public double getTotalSalesDemand() { return totalSalesDemand; }
public void setTotalSalesDemand(double v) { this.totalSalesDemand = v; }
public double getTotalSalesFulfilled() { return totalSalesFulfilled; }
public void setTotalSalesFulfilled(double v) { this.totalSalesFulfilled = v; }
public double getTotalDependentDemand() { return totalDependentDemand; }
public void setTotalDependentDemand(double v) { this.totalDependentDemand = v; }
public SupplySummary getSummary() { return summary; }
public void setSummary(SupplySummary v) { this.summary = v; }
public List<BomChild> getChildren() { return children; }
/** BOM 子节点数量 */
public int childCount() { return children.size(); }
}
\ No newline at end of file
package com.aps.macroplanner.output.dto;
/**
* 供应链汇总 — 跨周期汇总的供应/需求/库存数据。
*/
public class SupplySummary {
// ==================== 供应 ====================
/** 总生产量 (所有工序的 PTQty 总和) */
private double totalProduction;
/** 总到货量 (考虑 lead time 偏移的实际到货) */
private double totalArrived;
/** 在途到货量 */
private double totalInTransit;
// ==================== 需求 ====================
/** 外部销售需求量 */
private double totalSalesDemand;
/** 外部销售满足量 */
private double totalSalesFulfilled;
/** BOM 依赖需求量 (被其他工序消耗) */
private double totalDependentDemand;
/** 总需求满足量 (用于安全库存天数) */
private double totalDemandFulfillment;
/** 需求松弛总和 */
private double totalDemandSlack;
// ==================== 库存 ====================
/** 初始库存 (第0周期期初) */
private double initialInventory;
/** 最终库存 (最后一周期期末) */
private double finalInventory;
/** 平均库存 */
private double averageInventory;
// ==================== 库存规格偏差 ====================
/** 低于目标库存总和 */
private double totalBelowTarget;
/** 低于最小库存总和 */
private double totalBelowMin;
/** 高于最大库存总和 */
private double totalAboveMax;
// ==================== Getters / Setters ====================
public double getTotalProduction() { return totalProduction; }
public void setTotalProduction(double v) { this.totalProduction = v; }
public double getTotalArrived() { return totalArrived; }
public void setTotalArrived(double v) { this.totalArrived = v; }
public double getTotalInTransit() { return totalInTransit; }
public void setTotalInTransit(double v) { this.totalInTransit = v; }
public double getTotalSalesDemand() { return totalSalesDemand; }
public void setTotalSalesDemand(double v) { this.totalSalesDemand = v; }
public double getTotalSalesFulfilled() { return totalSalesFulfilled; }
public void setTotalSalesFulfilled(double v) { this.totalSalesFulfilled = v; }
public double getTotalDependentDemand() { return totalDependentDemand; }
public void setTotalDependentDemand(double v) { this.totalDependentDemand = v; }
public double getTotalDemandFulfillment() { return totalDemandFulfillment; }
public void setTotalDemandFulfillment(double v) { this.totalDemandFulfillment = v; }
public double getTotalDemandSlack() { return totalDemandSlack; }
public void setTotalDemandSlack(double v) { this.totalDemandSlack = v; }
public double getInitialInventory() { return initialInventory; }
public void setInitialInventory(double v) { this.initialInventory = v; }
public double getFinalInventory() { return finalInventory; }
public void setFinalInventory(double v) { this.finalInventory = v; }
public double getAverageInventory() { return averageInventory; }
public void setAverageInventory(double v) { this.averageInventory = v; }
public double getTotalBelowTarget() { return totalBelowTarget; }
public void setTotalBelowTarget(double v) { this.totalBelowTarget = v; }
public double getTotalBelowMin() { return totalBelowMin; }
public void setTotalBelowMin(double v) { this.totalBelowMin = v; }
public double getTotalAboveMax() { return totalAboveMax; }
public void setTotalAboveMax(double v) { this.totalAboveMax = v; }
}
\ No newline at end of file
package com.aps.macroplanner.output.dto;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 单个设备的产能使用结果 — 由 {@code ResultWriter.buildUnitCapacities()} 计算。
*
* <h3>字段说明</h3>
* <table>
* <tr><td>unitId</td><td>设备ID</td></tr>
* <tr><td>operationIds</td><td>使用该设备的工序ID列表</td></tr>
* <tr><td>totalMaxCapacity</td><td>该设备在全部周期的最大可用产能合计(小时)</td></tr>
* <tr><td>totalCapacityUsed</td><td>该设备在全部周期的实际产能占用合计(小时)</td></tr>
* <tr><td>totalUtilizationRate</td><td>整体设备利用率 = totalCapacityUsed / totalMaxCapacity</td></tr>
* <tr><td>totalProduction</td><td>该设备全部周期产出合计</td></tr>
* <tr><td>periodDetails</td><td>各周期明细 (UnitPeriodDetail[])</td></tr>
* <tr><td>periodTasks</td><td>该设备的生产任务明细 (PeriodTaskResult[])</td></tr>
* </table>
*/
public class UnitCapacityResult {
/** 设备ID */
private String unitId;
/** 使用该设备的工序ID列表 */
private List<String> operationIds;
/** 全部周期最大可用产能合计 */
private double totalMaxCapacity;
/** 全部周期实际产能占用合计 */
private double totalCapacityUsed;
/** 整体设备利用率 = totalCapacityUsed / totalMaxCapacity */
private double totalUtilizationRate;
/** 全部周期产出合计 */
private double totalProduction;
private double totalOverloaded;
private double totalNotMet;
private double overallUtilization;
/** 各周期明细 */
private final List<UnitPeriodDetail> periodDetails = new ArrayList<>();
// ==================== 内嵌类 ====================
/** 单周期产能明细 */
public static class UnitPeriodDetail {
/** 周期索引 */
public int periodIndex;
/** 周期起始日期 */
public String periodStartDate;
/** 该周期最大可用产能(小时) */
public double maxCapacity;
/** 该周期实际产能占用(小时) */
public double capacityUsed;
/** 该周期设备利用率 = capacityUsed / maxCapacity */
public double utilization;
public double overloaded;
public double notMet;
/** 该周期产出 */
public double production;
/** 该周期内在该设备上执行的工序明细 */
public final List<UnitTaskInfo> tasks = new ArrayList<>();
}
/** 设备上的工序级生产明细 */
public static class UnitTaskInfo {
public String operationId;
public String operationName;
public double productionQty;
public double capacityUsed;
public double capacityCoeff;
public Double lotSize;
public double lotSizeOver;
public double lotSizeUnder;
/** 产出: productId@spId */
public final List<String> outputs = new ArrayList<>();
}
// ==================== Getters / Setters ====================
public String getUnitId() { return unitId; }
public void setUnitId(String v) { this.unitId = v; }
public List<String> getOperationIds() { return operationIds; }
public void setOperationIds(List<String> v) { this.operationIds = v; }
public double getTotalMaxCapacity() { return totalMaxCapacity; }
public void setTotalMaxCapacity(double v) { this.totalMaxCapacity = v; }
public double getTotalCapacityUsed() { return totalCapacityUsed; }
public void setTotalCapacityUsed(double v) { this.totalCapacityUsed = v; }
public double getTotalUtilizationRate() { return totalUtilizationRate; }
public void setTotalUtilizationRate(double v) { this.totalUtilizationRate = v; }
public double getTotalProduction() { return totalProduction; }
public void setTotalProduction(double v) { this.totalProduction = v; }
public List<UnitPeriodDetail> getPeriodDetails() { return periodDetails; }
public double getTotalOverloaded() { return totalOverloaded; }
public void setTotalOverloaded(double v) { this.totalOverloaded = v; }
public double getTotalNotMet() { return totalNotMet; }
public void setTotalNotMet(double v) { this.totalNotMet = v; }
public double getOverallUtilization() { return overallUtilization; }
public void setOverallUtilization(double v) { this.overallUtilization = v; }
}
package com.aps.service;
import com.aps.macroplanner.output.dto.OptimizationResult;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.zip.GZIPInputStream;
/**
* MP排产结果读取服务。
*
* <p>从结果文件反序列化 {@link OptimizationResult},供前端接口使用。
* 逻辑与 {@code ResultWriter.getResultFromFile} 一致,但独立于求解器运行时依赖。</p>
*/
@Service
@Slf4j
public class MacroPlannerResultService {
private static final String OUTPUT_DIR = "mp/result";
private static final String RESULT_FILE_PREFIX = "optimization_result_";
private static final String JSON_EXT = ".json";
private static final String GZ_EXT = ".json.gz";
private final ObjectMapper objectMapper;
public MacroPlannerResultService() {
this.objectMapper = new ObjectMapper();
this.objectMapper.registerModule(new JavaTimeModule());
this.objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
/**
* 根据场景ID读取MP排产结果。
*
* @param sceneId 场景ID
* @return 反序列化后的优化结果, 或 null (文件不存在/读取失败)
*/
public OptimizationResult getResult(String sceneId) {
if (sceneId == null || sceneId.isEmpty()) {
log.warn("sceneId为空, 无法读取结果");
return null;
}
File file = getOptimizationFile(sceneId);
if (file == null || !file.exists() || file.length() == 0) {
log.warn("结果文件不存在或为空: sceneId={}", sceneId);
return null;
}
long start = System.currentTimeMillis();
OptimizationResult result = deserialize(file);
long elapsed = System.currentTimeMillis() - start;
if (result != null) {
log.info("读取MP结果成功: sceneId={}, 耗时={}ms, 文件大小={}KB",
sceneId, elapsed, file.length() / 1024);
}
return result;
}
/**
* 定位结果文件, 优先 .json, 其次 .json.gz。
*/
private File getOptimizationFile(String sceneId) {
String workDir = System.getProperty("user.dir");
Path resultDir = Paths.get(workDir, OUTPUT_DIR);
// 优先未压缩
File jsonFile = resultDir.resolve(RESULT_FILE_PREFIX + sceneId + JSON_EXT).toFile();
if (jsonFile.exists() && jsonFile.length() > 0) {
return jsonFile;
}
// 其次压缩
File gzFile = resultDir.resolve(RESULT_FILE_PREFIX + sceneId + GZ_EXT).toFile();
if (gzFile.exists() && gzFile.length() > 0) {
return gzFile;
}
return jsonFile; // 返回 .json 路径用于日志提示
}
/**
* 反序列化, 自动处理 GZIP 压缩。
*/
private OptimizationResult deserialize(File file) {
try (InputStream is = openInputStream(file)) {
return objectMapper.readValue(is, OptimizationResult.class);
} catch (IOException e) {
log.error("反序列化结果失败: file={}, error={}", file.getAbsolutePath(), e.getMessage(), e);
return null;
}
}
/**
* 根据文件扩展名决定是否包裹 GZIP 输入流。
*/
private InputStream openInputStream(File file) throws IOException {
FileInputStream fis = new FileInputStream(file);
if (file.getName().endsWith(".gz")) {
return new GZIPInputStream(fis);
}
return fis;
}
}
......@@ -43,7 +43,8 @@ public class PlanResultServiceTest {
// planResultService.execute2("64E64F6B68094AF38CEDC418630C3CC2");//2000
// planResultService.execute2("E1448B3C9C8743DEAB39708F2CFE348A");//倒排bomces
planResultService.execute2("85DA28EC5F4643449E65A51253D5F127"); //bom 多半成品
// planResultService.execute2("85DA28EC5F4643449E65A51253D5F127"); //bom 多半成品
planResultService.execute2("B288477F1A594DB584C87EEA77880AA3");
// 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