Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
H
HYH.APSJ
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
佟礼
HYH.APSJ
Commits
391de2d1
Commit
391de2d1
authored
Aug 11, 2026
by
Tong Li
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
MP
parent
48800682
Show whitespace changes
Inline
Side-by-side
Showing
31 changed files
with
5386 additions
and
77 deletions
+5386
-77
FileHelper.java
src/main/java/com/aps/common/util/FileHelper.java
+13
-4
MacroPlannerResultController.java
...java/com/aps/controller/MacroPlannerResultController.java
+638
-0
CoProductTestRunner.java
src/main/java/com/aps/macroplanner/CoProductTestRunner.java
+42
-0
MacroPlannerDataConverterRunner.java
...com/aps/macroplanner/MacroPlannerDataConverterRunner.java
+85
-0
MacroPlannerOptimizer.java
...main/java/com/aps/macroplanner/MacroPlannerOptimizer.java
+15
-3
MultiLevelBomTestRunner.java
...in/java/com/aps/macroplanner/MultiLevelBomTestRunner.java
+42
-0
RoutingConstraint.java
...va/com/aps/macroplanner/constraint/RoutingConstraint.java
+83
-0
CoProductTestDataBuilder.java
...a/com/aps/macroplanner/data/CoProductTestDataBuilder.java
+190
-0
DataValidator.java
src/main/java/com/aps/macroplanner/data/DataValidator.java
+29
-20
MacroPlannerDataConverter.java
.../com/aps/macroplanner/data/MacroPlannerDataConverter.java
+1226
-0
MultiLevelBomTestDataBuilder.java
...m/aps/macroplanner/data/MultiLevelBomTestDataBuilder.java
+163
-0
Routing.java
src/main/java/com/aps/macroplanner/data/Routing.java
+253
-0
RoutingExpansion.java
...main/java/com/aps/macroplanner/data/RoutingExpansion.java
+65
-0
RoutingStep.java
src/main/java/com/aps/macroplanner/data/RoutingStep.java
+41
-0
RoutingTestDataBuilder.java
...ava/com/aps/macroplanner/data/RoutingTestDataBuilder.java
+74
-49
UnitOperation.java
src/main/java/com/aps/macroplanner/data/UnitOperation.java
+38
-0
JsonBuilder.java
src/main/java/com/aps/macroplanner/output/JsonBuilder.java
+186
-0
ResultWriter.java
src/main/java/com/aps/macroplanner/output/ResultWriter.java
+1138
-0
DemandSummaryResult.java
.../com/aps/macroplanner/output/dto/DemandSummaryResult.java
+61
-0
KpiResult.java
src/main/java/com/aps/macroplanner/output/dto/KpiResult.java
+49
-0
OptimizationResult.java
...a/com/aps/macroplanner/output/dto/OptimizationResult.java
+79
-0
PeriodTaskResult.java
...ava/com/aps/macroplanner/output/dto/PeriodTaskResult.java
+81
-0
PispipResult.java
...in/java/com/aps/macroplanner/output/dto/PispipResult.java
+179
-0
ProductNetworkResult.java
...com/aps/macroplanner/output/dto/ProductNetworkResult.java
+49
-0
SalesDemandResult.java
...va/com/aps/macroplanner/output/dto/SalesDemandResult.java
+63
-0
SolverStatistics.java
...ava/com/aps/macroplanner/output/dto/SolverStatistics.java
+50
-0
SupplyChainNode.java
...java/com/aps/macroplanner/output/dto/SupplyChainNode.java
+131
-0
SupplySummary.java
...n/java/com/aps/macroplanner/output/dto/SupplySummary.java
+91
-0
UnitCapacityResult.java
...a/com/aps/macroplanner/output/dto/UnitCapacityResult.java
+114
-0
MacroPlannerResultService.java
src/main/java/com/aps/service/MacroPlannerResultService.java
+116
-0
PlanResultServiceTest.java
src/test/java/com/aps/demo/PlanResultServiceTest.java
+2
-1
No files found.
src/main/java/com/aps/common/util/FileHelper.java
View file @
391de2d1
...
...
@@ -57,22 +57,31 @@ public class FileHelper {
}
}
public
static
void
writeFile
(
String
message
,
String
fileName
)
{
public
static
void
writeFile
(
String
message
,
String
file
Dir
,
String
file
Name
)
{
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
src/main/java/com/aps/controller/MacroPlannerResultController.java
0 → 100644
View file @
391de2d1
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
;
}
}
src/main/java/com/aps/macroplanner/CoProductTestRunner.java
0 → 100644
View file @
391de2d1
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
src/main/java/com/aps/macroplanner/MacroPlannerDataConverterRunner.java
0 → 100644
View file @
391de2d1
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
);
}
}
}
src/main/java/com/aps/macroplanner/MacroPlannerOptimizer.java
View file @
391de2d1
...
...
@@ -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
);
}
...
...
src/main/java/com/aps/macroplanner/MultiLevelBomTestRunner.java
0 → 100644
View file @
391de2d1
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
src/main/java/com/aps/macroplanner/constraint/RoutingConstraint.java
0 → 100644
View file @
391de2d1
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
src/main/java/com/aps/macroplanner/data/CoProductTestDataBuilder.java
0 → 100644
View file @
391de2d1
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
src/main/java/com/aps/macroplanner/data/DataValidator.java
View file @
391de2d1
...
...
@@ -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 中作为输入的产品
...
...
src/main/java/com/aps/macroplanner/data/MacroPlannerDataConverter.java
0 → 100644
View file @
391de2d1
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
;
}
}
src/main/java/com/aps/macroplanner/data/MultiLevelBomTestDataBuilder.java
0 → 100644
View file @
391de2d1
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
src/main/java/com/aps/macroplanner/data/Routing.java
0 → 100644
View file @
391de2d1
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
src/main/java/com/aps/macroplanner/data/RoutingExpansion.java
0 → 100644
View file @
391de2d1
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
src/main/java/com/aps/macroplanner/data/RoutingStep.java
0 → 100644
View file @
391de2d1
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
src/main/java/com/aps/macroplanner/data/RoutingTestDataBuilder.java
View file @
391de2d1
...
...
@@ -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
(
...
...
src/main/java/com/aps/macroplanner/data/UnitOperation.java
0 → 100644
View file @
391de2d1
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
src/main/java/com/aps/macroplanner/output/JsonBuilder.java
0 → 100644
View file @
391de2d1
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
))
<
1
e
-
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
src/main/java/com/aps/macroplanner/output/ResultWriter.java
0 → 100644
View file @
391de2d1
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
src/main/java/com/aps/macroplanner/output/dto/DemandSummaryResult.java
0 → 100644
View file @
391de2d1
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
;
}
}
src/main/java/com/aps/macroplanner/output/dto/KpiResult.java
0 → 100644
View file @
391de2d1
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
src/main/java/com/aps/macroplanner/output/dto/OptimizationResult.java
0 → 100644
View file @
391de2d1
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
src/main/java/com/aps/macroplanner/output/dto/PeriodTaskResult.java
0 → 100644
View file @
391de2d1
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
src/main/java/com/aps/macroplanner/output/dto/PispipResult.java
0 → 100644
View file @
391de2d1
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
src/main/java/com/aps/macroplanner/output/dto/ProductNetworkResult.java
0 → 100644
View file @
391de2d1
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
src/main/java/com/aps/macroplanner/output/dto/SalesDemandResult.java
0 → 100644
View file @
391de2d1
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
src/main/java/com/aps/macroplanner/output/dto/SolverStatistics.java
0 → 100644
View file @
391de2d1
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
src/main/java/com/aps/macroplanner/output/dto/SupplyChainNode.java
0 → 100644
View file @
391de2d1
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
src/main/java/com/aps/macroplanner/output/dto/SupplySummary.java
0 → 100644
View file @
391de2d1
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
src/main/java/com/aps/macroplanner/output/dto/UnitCapacityResult.java
0 → 100644
View file @
391de2d1
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
;
}
}
src/main/java/com/aps/service/MacroPlannerResultService.java
0 → 100644
View file @
391de2d1
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
;
}
}
src/test/java/com/aps/demo/PlanResultServiceTest.java
View file @
391de2d1
...
...
@@ -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"); //重叠
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment