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
f11971ca
Commit
f11971ca
authored
Sep 15, 2026
by
Tong Li
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
MP
parent
2ec4b057
Show whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
809 additions
and
172 deletions
+809
-172
MacroPlannerResultController.java
...java/com/aps/controller/MacroPlannerResultController.java
+12
-130
MacroPlannerOptimizer.java
...main/java/com/aps/macroplanner/MacroPlannerOptimizer.java
+11
-1
MultiLevelBomTestRunner.java
...in/java/com/aps/macroplanner/MultiLevelBomTestRunner.java
+35
-13
ResultWriter.java
src/main/java/com/aps/macroplanner/output/ResultWriter.java
+171
-28
BomStructureData.java
...ava/com/aps/macroplanner/output/dto/BomStructureData.java
+90
-0
OperationDemandResult.java
...om/aps/macroplanner/output/dto/OperationDemandResult.java
+43
-0
MacroPlannerProductNetworkService.java
...va/com/aps/service/MacroPlannerProductNetworkService.java
+447
-0
No files found.
src/main/java/com/aps/controller/MacroPlannerResultController.java
View file @
f11971ca
...
@@ -13,6 +13,7 @@ import com.aps.macroplanner.data.TestDataBuilder;
...
@@ -13,6 +13,7 @@ import com.aps.macroplanner.data.TestDataBuilder;
import
com.aps.macroplanner.output.ResultWriter
;
import
com.aps.macroplanner.output.ResultWriter
;
import
com.aps.macroplanner.output.dto.*
;
import
com.aps.macroplanner.output.dto.*
;
import
com.aps.service.MacroPlannerResultService
;
import
com.aps.service.MacroPlannerResultService
;
import
com.aps.service.MacroPlannerProductNetworkService
;
import
com.aps.service.MpPispipResultPersistenceService
;
import
com.aps.service.MpPispipResultPersistenceService
;
import
com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper
;
import
com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper
;
import
com.google.ortools.Loader
;
import
com.google.ortools.Loader
;
...
@@ -67,6 +68,9 @@ public class MacroPlannerResultController {
...
@@ -67,6 +68,9 @@ public class MacroPlannerResultController {
@Autowired
@Autowired
private
MacroPlannerResultService
macroPlannerResultService
;
private
MacroPlannerResultService
macroPlannerResultService
;
@Autowired
private
MacroPlannerProductNetworkService
macroPlannerProductNetworkService
;
@Autowired
@Autowired
private
MacroPlannerDataConverter
macroPlannerDataConverter
;
private
MacroPlannerDataConverter
macroPlannerDataConverter
;
...
@@ -304,11 +308,15 @@ public class MacroPlannerResultController {
...
@@ -304,11 +308,15 @@ public class MacroPlannerResultController {
@RequestParam
(
value
=
"productId"
,
required
=
false
)
@Parameter
(
description
=
"按产品ID过滤成品根节点(可选)"
)
String
productId
,
@RequestParam
(
value
=
"productId"
,
required
=
false
)
@Parameter
(
description
=
"按产品ID过滤成品根节点(可选)"
)
String
productId
,
@RequestParam
(
value
=
"spId"
,
required
=
false
)
@Parameter
(
description
=
"按库存点ID过滤(可选)"
)
String
spId
,
@RequestParam
(
value
=
"spId"
,
required
=
false
)
@Parameter
(
description
=
"按库存点ID过滤(可选)"
)
String
spId
,
@RequestParam
(
value
=
"period"
,
required
=
false
)
@Parameter
(
description
=
"按周期过滤, 仅保留该周期内有供应的节点/来源(可选)"
)
String
period
)
{
@RequestParam
(
value
=
"period"
,
required
=
false
)
@Parameter
(
description
=
"按周期过滤, 仅保留该周期内有供应的节点/来源(可选)"
)
String
period
)
{
OptimizationResult
result
=
loadResult
(
sceneId
);
if
(
productId
==
null
||
productId
.
trim
().
isEmpty
()
||
spId
==
null
||
spId
.
trim
().
isEmpty
())
{
if
(
result
==
null
)
{
return
R
.
failed
(
"productId 和 spId 不能为空"
);
return
R
.
failed
(
"未找到场景 "
+
sceneId
+
" 的排产结果文件"
);
}
SupplyChainNode
node
=
macroPlannerProductNetworkService
.
buildProductNetwork
(
sceneId
,
productId
.
trim
(),
spId
.
trim
(),
period
);
if
(
node
==
null
)
{
return
R
.
failed
(
"未找到场景 "
+
sceneId
+
" 中 "
+
productId
+
"@"
+
spId
+
" 的产品网络"
);
}
}
return
R
.
ok
(
filterProductNetwork
(
result
.
getProductNetwork
(),
productId
,
spId
,
period
)
);
return
R
.
ok
(
node
);
}
}
/**
/**
...
@@ -451,132 +459,6 @@ public class MacroPlannerResultController {
...
@@ -451,132 +459,6 @@ public class MacroPlannerResultController {
return
new
ArrayList
<>(
merged
.
values
());
return
new
ArrayList
<>(
merged
.
values
());
}
}
/**
* 过滤产品生产网络:
* <ul>
* <li>productId / spId — 仅保留匹配的成品根节点及其子树;</li>
* <li>unitId — 仅保留该设备上的供应来源;</li>
* <li>periodIndex — 仅保留该周期内有供应(生产/消耗/库存/销售)的节点与来源, 其余剪枝。</li>
* </ul>
* 无过滤条件时原样返回。
*/
private
SupplyChainNode
filterProductNetwork
(
ProductNetworkResult
network
,
String
productId
,
String
spId
,
String
periodIndex
)
{
if
(
network
==
null
)
{
return
null
;
}
if
(!
hasText
(
productId
)
&&
!
hasText
(
spId
)
&&
periodIndex
==
null
)
{
return
null
;
}
ProductNetworkResult
filtered
=
new
ProductNetworkResult
();
SupplyChainNode
childSrc
=
network
.
getAllNodes
().
get
(
productId
+
"@"
+
spId
);
if
(
childSrc
==
null
)
{
return
null
;
}
SupplyChainNode
childNode
=
copyNetworkNode
(
childSrc
,
network
,
filtered
,
periodIndex
);
if
(
childNode
==
null
)
{
return
null
;
}
return
childNode
;
}
/**
* 递归拷贝网络节点, 同时按设备与周期剪枝供应来源、子节点。
*/
private
SupplyChainNode
copyNetworkNode
(
SupplyChainNode
src
,
ProductNetworkResult
source
,
ProductNetworkResult
target
,
String
periodIndex
)
{
if
(
periodIndex
!=
null
&&
!
src
.
getActivePeriods
().
contains
(
periodIndex
))
{
return
null
;
}
String
key
=
src
.
getProductId
()
+
"@"
+
src
.
getSpId
();
SupplyChainNode
existing
=
target
.
getAllNodes
().
get
(
key
);
if
(
existing
!=
null
)
{
return
existing
;
}
SupplyChainNode
copy
=
target
.
getOrCreateNode
(
src
.
getProductId
(),
src
.
getSpId
(),
src
.
getSpName
());
copy
.
setLevel
(
src
.
getLevel
());
copy
.
setTotalSalesDemand
(
src
.
getTotalSalesDemand
());
copy
.
setTotalSalesFulfilled
(
src
.
getTotalSalesFulfilled
());
copy
.
setTotalDependentDemand
(
src
.
getTotalDependentDemand
());
// copy.setSummary(src.getSummary());
// 供应来源: 按设备 + 周期过滤
for
(
SupplyChainNode
.
SupplySource
ss
:
src
.
getSupplySources
())
{
for
(
Map
.
Entry
<
String
,
SupplyChainNode
.
SupplySourceDetail
>
entry
:
ss
.
productionByPeriod
.
entrySet
())
{
String
period
=
entry
.
getKey
();
// 周期key,对应截图里的 0、3
if
(
period
.
equals
(
periodIndex
))
{
SupplyChainNode
.
SupplySourceDetail
detail
=
entry
.
getValue
();
// 该周期的产能详情
ss
.
production
=
detail
.
production
;
ss
.
unitId
=
detail
.
unitId
;
ss
.
unitName
=
detail
.
unitName
;
ss
.
productionByPeriod
.
clear
();
copy
.
getSupplySources
().
add
(
ss
);
break
;
}
}
}
// 消费者: 按周期过滤
for
(
SupplyChainNode
.
ConsumerInfo
ci
:
src
.
getConsumers
())
{
if
(
periodIndex
!=
null
&&
!
ci
.
consumedByPeriod
.
containsKey
(
periodIndex
))
{
continue
;
}
copy
.
getConsumers
().
add
(
ci
);
}
// BOM 子物料: 按周期剪枝
for
(
SupplyChainNode
.
BomChild
child
:
src
.
getChildren
())
{
if
(
periodIndex
!=
null
&&
!
child
.
consumedByPeriod
.
containsKey
(
periodIndex
))
{
continue
;
}
SupplyChainNode
childSrc
=
source
.
getAllNodes
().
get
(
child
.
productId
+
"@"
+
child
.
spId
);
if
(
childSrc
==
null
)
{
continue
;
}
SupplyChainNode
childNode
=
copyNetworkNode
(
childSrc
,
source
,
target
,
periodIndex
);
if
(
childNode
==
null
)
{
continue
;
// 该子节点在该周期无活动, 剪枝
}
for
(
SupplyChainNode
.
SupplySource
ss
:
childNode
.
getSupplySources
())
{
child
.
totalConsumedQty
=
ss
.
production
;
child
.
unitId
=
ss
.
unitId
;
child
.
unitName
=
ss
.
unitName
;
child
.
consumedByPeriod
.
clear
();
break
;
}
copy
.
getChildren
().
add
(
child
);
}
return
copy
;
}
/**
* 供应来源在指定周期是否有产量 (在途/外部采购视为全周期可用)。
*/
private
boolean
isSourceActiveInPeriod
(
SupplyChainNode
.
SupplySource
ss
,
int
periodIndex
)
{
if
(
"IN_TRANSIT"
.
equals
(
ss
.
type
)
||
"EXTERNAL"
.
equals
(
ss
.
type
))
{
return
true
;
}
return
ss
.
productionByPeriod
.
containsKey
(
periodIndex
);
}
private
void
enrichSalesDemandDisplayFields
(
List
<
SalesDemandResult
>
demands
)
{
private
void
enrichSalesDemandDisplayFields
(
List
<
SalesDemandResult
>
demands
)
{
if
(
demands
==
null
||
demands
.
isEmpty
())
{
if
(
demands
==
null
||
demands
.
isEmpty
())
{
return
;
return
;
...
...
src/main/java/com/aps/macroplanner/MacroPlannerOptimizer.java
View file @
f11971ca
package
com
.
aps
.
macroplanner
;
package
com
.
aps
.
macroplanner
;
import
com.aps.common.util.FileHelper
;
import
com.aps.common.util.FileHelper
;
import
com.aps.macroplanner.output.dto.OptimizationResult
;
import
com.aps.service.MpPispipResultPersistenceService
;
import
com.google.ortools.Loader
;
import
com.google.ortools.Loader
;
import
com.google.ortools.linearsolver.MPSolver
;
import
com.google.ortools.linearsolver.MPSolver
;
import
com.aps.macroplanner.constraint.ConstraintFactory
;
import
com.aps.macroplanner.constraint.ConstraintFactory
;
...
@@ -11,6 +13,7 @@ import com.aps.macroplanner.objective.ObjectiveBuilder;
...
@@ -11,6 +13,7 @@ import com.aps.macroplanner.objective.ObjectiveBuilder;
import
com.aps.macroplanner.objective.StrategyLevel
;
import
com.aps.macroplanner.objective.StrategyLevel
;
import
com.aps.macroplanner.output.ResultWriter
;
import
com.aps.macroplanner.output.ResultWriter
;
import
com.aps.macroplanner.output.SolutionPrinter
;
import
com.aps.macroplanner.output.SolutionPrinter
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
java.io.FileOutputStream
;
import
java.io.FileOutputStream
;
import
java.io.PrintStream
;
import
java.io.PrintStream
;
...
@@ -83,6 +86,8 @@ import java.util.logging.Logger;
...
@@ -83,6 +86,8 @@ import java.util.logging.Logger;
*/
*/
public
class
MacroPlannerOptimizer
{
public
class
MacroPlannerOptimizer
{
/** LP 模型文件和日志文件的输出目录 */
/** LP 模型文件和日志文件的输出目录 */
private
static
final
String
LOG_DIR
=
"mp"
;
private
static
final
String
LOG_DIR
=
"mp"
;
...
@@ -382,7 +387,12 @@ public class MacroPlannerOptimizer {
...
@@ -382,7 +387,12 @@ public class MacroPlannerOptimizer {
// 回写业务对象到JSON文件(预留)
// 回写业务对象到JSON文件(预留)
ResultWriter
rw
=
new
ResultWriter
(
model
,
data
,
startTimeMs
);
ResultWriter
rw
=
new
ResultWriter
(
model
,
data
,
startTimeMs
);
boolean
jsonPath
=
rw
.
saveResultToFile
(
sceneId
);
OptimizationResult
optimizationResult
=
rw
.
buildResult
();
boolean
jsonPath
=
rw
.
saveResultToFile
(
sceneId
,
optimizationResult
);
if
(
jsonPath
)
{
if
(
jsonPath
)
{
writeLog
(
"\n[OK] 优化结果JSON已导出: "
+
jsonPath
);
writeLog
(
"\n[OK] 优化结果JSON已导出: "
+
jsonPath
);
}
}
...
...
src/main/java/com/aps/macroplanner/MultiLevelBomTestRunner.java
View file @
f11971ca
package
com
.
aps
.
macroplanner
;
package
com
.
aps
.
macroplanner
;
import
com.aps.ApsApplication
;
import
com.aps.common.util.FileHelper
;
import
com.aps.common.util.FileHelper
;
import
com.aps.macroplanner.data.BenchmarkDataBuilder
;
import
com.aps.macroplanner.data.BenchmarkDataBuilder
;
import
com.aps.macroplanner.data.LargeScaleBomTestDataBuilder
;
import
com.aps.macroplanner.data.LargeScaleBomTestDataBuilder
;
import
com.aps.macroplanner.output.ResultWriter
;
import
com.aps.macroplanner.output.dto.OptimizationResult
;
import
com.aps.service.MpPispipResultPersistenceService
;
import
com.google.ortools.Loader
;
import
com.google.ortools.Loader
;
import
com.aps.macroplanner.data.MultiLevelBomTestDataBuilder
;
import
com.aps.macroplanner.data.MultiLevelBomTestDataBuilder
;
import
com.aps.macroplanner.data.TestDataBuilder
;
import
com.aps.macroplanner.data.TestDataBuilder
;
import
org.springframework.boot.SpringApplication
;
import
org.springframework.context.ApplicationContext
;
/**
/**
* 多级BOM + 多成品 + 共享半成品 测试运行器。
* 多级BOM + 多成品 + 共享半成品 测试运行器。
...
@@ -26,6 +32,7 @@ import com.aps.macroplanner.data.TestDataBuilder;
...
@@ -26,6 +32,7 @@ import com.aps.macroplanner.data.TestDataBuilder;
* </ol>
* </ol>
*/
*/
public
class
MultiLevelBomTestRunner
{
public
class
MultiLevelBomTestRunner
{
private
static
final
String
LOG_DIR
=
"mp"
;
private
static
final
String
LOG_DIR
=
"mp"
;
/** LP 模型文件路径 */
/** LP 模型文件路径 */
...
@@ -46,29 +53,44 @@ public class MultiLevelBomTestRunner {
...
@@ -46,29 +53,44 @@ public class MultiLevelBomTestRunner {
System
.
out
.
println
(
"Demand: P1=40/day, P2=30/day"
);
System
.
out
.
println
(
"Demand: P1=40/day, P2=30/day"
);
System
.
out
.
println
();
System
.
out
.
println
();
// 通过 Spring 容器获取 service 实例 (static main 中 @Autowired 不会生效)
ApplicationContext
ctx
=
SpringApplication
.
run
(
ApsApplication
.
class
,
args
);
try
{
MultiLevelBomTestDataBuilder
data
=
new
MultiLevelBomTestDataBuilder
();
data
.
init
();
MultiLevelBomTestDataBuilder
builder
=
new
MultiLevelBomTestDataBuilder
();
MacroPlannerOptimizer
optimizer
=
new
MacroPlannerOptimizer
(
data
);
builder
.
init
();
MacroPlannerOptimizer
optimizer
=
new
MacroPlannerOptimizer
(
builder
);
optimizer
.
buildModel
();
optimizer
.
buildModel
();
optimizer
.
solve
(
"bom"
);
optimizer
.
solve
(
"bom"
);
// for (int scale : BenchmarkDataBuilder.SUPPORTED_SCALES) {
// MpPispipResultPersistenceService service =
// ctx.getBean(MpPispipResultPersistenceService.class);
//
// int scale = 10000;
// BenchmarkDataBuilder data = BenchmarkDataBuilder.forScale(scale);
// writeLog("===== TEST RUNNER START " + scale + "=====");
//
//
// int scale=10000;
// BenchmarkDataBuilder data =
// BenchmarkDataBuilder.forScale(scale);
// writeLog("===== TEST RUNNER START "+scale+"=====");
////
// data.init();
// data.init();
// writeLog("Data loaded: " + data.getProducts().size() + " products, "
// writeLog("Data loaded: " + data.getProducts().size() + " products, "
// + data.getOperations().size() + " operations");
// + data.getOperations().size() + " operations");
//
// long startTime = System.currentTimeMillis();
// MacroPlannerOptimizer optimizer = new MacroPlannerOptimizer(data);
// MacroPlannerOptimizer optimizer = new MacroPlannerOptimizer(data);
// optimizer.buildModel();
// optimizer.buildModel();
// optimizer.solve(String.valueOf(scale));
// optimizer.solve(String.valueOf(scale));
//
// // 构建结果并调用 save 入库
// ResultWriter writer = new ResultWriter(optimizer.getModel(), optimizer.getData(), startTime);
// OptimizationResult result = writer.buildResult();
// int saved = service.save(String.valueOf(scale), result);
// writeLog("PISPIP result saved: " + saved);
//
// writeLog("===== TEST RUNNER END =====");
// writeLog("===== TEST RUNNER END =====");
// }
}
catch
(
Exception
e
)
{
}
finally
{
SpringApplication
.
exit
(
ctx
);
}
}
}
}
}
\ No newline at end of file
src/main/java/com/aps/macroplanner/output/ResultWriter.java
View file @
f11971ca
...
@@ -130,47 +130,63 @@ public class ResultWriter {
...
@@ -130,47 +130,63 @@ public class ResultWriter {
String
fileName
=
resultDir
.
getAbsolutePath
()+
"\\unitcapacitie.parquet"
;
String
fileName
=
resultDir
.
getAbsolutePath
()+
"\\unitcapacitie.parquet"
;
return
fileName
;
return
fileName
;
}
}
private
String
getOptimizationBomStructure
(
String
sceneId
)
{
File
resultDir
=
getResultDirectory
(
sceneId
);
return
resultDir
.
getAbsolutePath
()
+
"\\bom_structure.json"
;
}
private
String
getOptimizationOperationDemand
(
String
sceneId
)
{
File
resultDir
=
getResultDirectory
(
sceneId
);
return
resultDir
.
getAbsolutePath
()
+
"\\operation_demands.parquet"
;
}
// ==================== 主入口 ====================
public
boolean
saveResultToFile
(
String
sceneId
)
{
public
boolean
saveResultToFile
(
String
sceneId
)
{
OptimizationResult
result
=
buildResult
();
OptimizationResult
result
=
buildResult
();
if
(
result
==
null
)
{
logger
.
warn
(
"对象不能为空"
);
return
false
;
}
return
saveResultToFile
(
sceneId
,
result
);
return
saveResultToFile
(
sceneId
,
result
);
}
}
// ==================== 主入口 ====================
/**
/**
* 将结果保存到 JSON 文件
* 将结果保存到 JSON 文件
*/
*/
public
boolean
saveResultToFile
(
String
sceneId
,
OptimizationResult
result
)
{
public
boolean
saveResultToFile
(
String
sceneId
,
OptimizationResult
result
)
{
try
{
try
{
try
{
try
{
if
(
result
==
null
)
{
logger
.
warn
(
"对象不能为空"
);
return
false
;
}
if
(
sceneId
==
null
||
sceneId
.
trim
().
isEmpty
())
{
if
(
sceneId
==
null
||
sceneId
.
trim
().
isEmpty
())
{
logger
.
warn
(
"场景ID不能为空"
);
logger
.
warn
(
"场景ID不能为空"
);
return
false
;
return
false
;
}
}
// FlatParquetUtil parquetUtil = new FlatParquetUtil();
FlatParquetUtil
parquetUtil
=
new
FlatParquetUtil
();
// String periodTaskPath = getOptimizationPeriodTask(sceneId);
String
periodTaskPath
=
getOptimizationPeriodTask
(
sceneId
);
// writeLog("writePeriodTasks");
writeLog
(
"writePeriodTasks"
);
// parquetUtil.write(result.getPeriodTasks(), periodTaskPath, PeriodTaskResult.class);
parquetUtil
.
write
(
result
.
getPeriodTasks
(),
periodTaskPath
,
PeriodTaskResult
.
class
);
// writeLog("writePeriodTasks");
writeLog
(
"writePeriodTasks"
);
// writeLog("writePispips");
writeLog
(
"writePispips"
);
// String pispiPath = getOptimizationPispip(sceneId);
String
pispiPath
=
getOptimizationPispip
(
sceneId
);
// parquetUtil.write(result.getPispips(), pispiPath, PispipResult.class);
parquetUtil
.
write
(
result
.
getPispips
(),
pispiPath
,
PispipResult
.
class
);
// writeLog("writesalesDemand");
writeLog
(
"writesalesDemand"
);
// String salesDemandPath = getOptimizationSalesDemand(sceneId);
String
salesDemandPath
=
getOptimizationSalesDemand
(
sceneId
);
// parquetUtil.write(result.getSalesDemands(), salesDemandPath, SalesDemandResult.class);
parquetUtil
.
write
(
result
.
getSalesDemands
(),
salesDemandPath
,
SalesDemandResult
.
class
);
// writeLog("writesalesDemand");
writeLog
(
"writesalesDemand"
);
// writeLog("writesalesDemand");
writeLog
(
"writesalesDemand"
);
// String unitPath = getOptimizationUnitCapacitie(sceneId);
String
unitPath
=
getOptimizationUnitCapacitie
(
sceneId
);
// parquetUtil.write(result.getUnitCapacities(), unitPath, UnitCapacityResult.class);
parquetUtil
.
write
(
result
.
getUnitCapacities
(),
unitPath
,
UnitCapacityResult
.
class
);
// writeLog("writesalesDemand");
writeLog
(
"writesalesDemand"
);
// BOM 结构持久化 (供 getProductNetwork 按需组装)
writeLog
(
"writeBomStructure"
);
writeBomStructure
(
sceneId
);
// 工序×物料×周期消耗量持久化
writeLog
(
"writeOperationDemands"
);
List
<
OperationDemandResult
>
opDemands
=
buildOperationDemandResults
();
parquetUtil
.
write
(
opDemands
,
getOptimizationOperationDemand
(
sceneId
),
OperationDemandResult
.
class
);
try
{
File
file
=
getOptimizationFile
(
sceneId
);
File
file
=
getOptimizationFile
(
sceneId
);
File
tempFile
=
new
File
(
file
.
getParentFile
(),
file
.
getName
()
+
".tmp"
);
File
tempFile
=
new
File
(
file
.
getParentFile
(),
file
.
getName
()
+
".tmp"
);
...
@@ -218,7 +234,10 @@ public class ResultWriter {
...
@@ -218,7 +234,10 @@ public class ResultWriter {
logger
.
info
(
"保存成功,场景ID: {}, 文件: {}"
,
sceneId
,
file
.
getAbsolutePath
());
logger
.
info
(
"保存成功,场景ID: {}, 文件: {}"
,
sceneId
,
file
.
getAbsolutePath
());
return
true
;
return
true
;
}
catch
(
Exception
e
)
{
logger
.
error
(
"保存文件失败,场景ID: "
+
sceneId
,
e
);
return
false
;
}
}
catch
(
IOException
ex
)
{
}
catch
(
IOException
ex
)
{
logger
.
error
(
"保存文件失败,场景ID: "
+
sceneId
,
ex
);
logger
.
error
(
"保存文件失败,场景ID: "
+
sceneId
,
ex
);
return
false
;
return
false
;
...
@@ -231,6 +250,130 @@ public class ResultWriter {
...
@@ -231,6 +250,130 @@ public class ResultWriter {
}
}
/**
* 将 BOM 结构 (静态工艺关系) 持久化为 JSON, 供查询接口按需组装产品网络。
*/
private
void
writeBomStructure
(
String
sceneId
)
{
try
{
BomStructureData
structure
=
buildBomStructureData
();
String
path
=
getOptimizationBomStructure
(
sceneId
);
File
file
=
new
File
(
path
);
File
parent
=
file
.
getParentFile
();
if
(
parent
!=
null
&&
!
parent
.
exists
())
{
parent
.
mkdirs
();
}
objectMapper
.
writeValue
(
file
,
structure
);
}
catch
(
Exception
e
)
{
logger
.
error
(
"持久化 BOM 结构失败, sceneId="
+
sceneId
,
e
);
}
}
/**
* 从 TestDataBuilder 提取静态 BOM 结构 (产品/库存点/工序/BOM输入/周期/初始库存/在途)。
*/
private
BomStructureData
buildBomStructureData
()
{
BomStructureData
s
=
new
BomStructureData
();
for
(
Product
p
:
data
.
getProducts
())
{
BomStructureData
.
ProductInfo
pi
=
new
BomStructureData
.
ProductInfo
();
pi
.
id
=
p
.
getId
();
pi
.
name
=
p
.
getName
();
pi
.
code
=
p
.
getCode
();
s
.
products
.
add
(
pi
);
}
for
(
StockingPoint
sp
:
data
.
getStockingPoints
())
{
BomStructureData
.
StockingPointInfo
si
=
new
BomStructureData
.
StockingPointInfo
();
si
.
id
=
sp
.
getId
();
si
.
name
=
sp
.
getName
();
s
.
stockingPoints
.
add
(
si
);
}
// 产品→库存点映射 (通过 getStockingPointsForProduct 反推)
for
(
Product
p
:
data
.
getProducts
())
{
for
(
StockingPoint
sp
:
data
.
getStockingPointsForProduct
(
p
.
getId
()))
{
BomStructureData
.
ProductSpMappingInfo
mi
=
new
BomStructureData
.
ProductSpMappingInfo
();
mi
.
productId
=
p
.
getId
();
mi
.
spId
=
sp
.
getId
();
s
.
productSpMappings
.
add
(
mi
);
}
}
for
(
Operation
op
:
data
.
getOperations
())
{
BomStructureData
.
OperationInfo
oi
=
new
BomStructureData
.
OperationInfo
();
oi
.
id
=
op
.
getId
();
oi
.
name
=
op
.
getName
();
oi
.
leadTimeDays
=
op
.
getLeadTimeDays
();
oi
.
hasLotSize
=
op
.
hasLotSize
();
oi
.
lotSize
=
op
.
hasLotSize
()
?
op
.
getLotSize
()
:
null
;
for
(
OperationOutput
oo
:
op
.
getOutputs
())
{
BomStructureData
.
OperationOutputInfo
ooi
=
new
BomStructureData
.
OperationOutputInfo
();
ooi
.
productId
=
oo
.
getProductId
();
ooi
.
spId
=
oo
.
getSpId
();
oi
.
outputs
.
add
(
ooi
);
}
for
(
UnitOperation
uo
:
op
.
getUnitOperations
())
{
BomStructureData
.
UnitOperationInfo
uoi
=
new
BomStructureData
.
UnitOperationInfo
();
uoi
.
unitId
=
uo
.
getUnitId
();
uoi
.
unitName
=
uo
.
getUnitName
();
uoi
.
capacityCoeff
=
uo
.
getCapacityCoeff
();
oi
.
unitOperations
.
add
(
uoi
);
}
s
.
operations
.
add
(
oi
);
}
for
(
OperationInput
input
:
data
.
getOperationInputs
())
{
BomStructureData
.
OperationInputInfo
ii
=
new
BomStructureData
.
OperationInputInfo
();
ii
.
operationId
=
input
.
getOperation
().
getId
();
ii
.
inputProductId
=
input
.
getInputProduct
().
getId
();
ii
.
inputSpId
=
input
.
getInputSp
().
getId
();
ii
.
inputSpName
=
input
.
getInputSp
().
getName
();
ii
.
factor
=
input
.
getFactor
();
s
.
operationInputs
.
add
(
ii
);
}
for
(
Period
p
:
data
.
getPeriods
())
{
BomStructureData
.
PeriodInfo
pi
=
new
BomStructureData
.
PeriodInfo
();
pi
.
index
=
p
.
getIndex
();
pi
.
name
=
p
.
getName
();
pi
.
startDate
=
p
.
getStartDate
()
==
null
?
null
:
p
.
getStartDate
().
toString
();
s
.
periods
.
add
(
pi
);
}
for
(
InitialInventory
inv
:
data
.
getInitialInventories
())
{
BomStructureData
.
InitialInventoryInfo
ii
=
new
BomStructureData
.
InitialInventoryInfo
();
ii
.
productId
=
inv
.
getProduct
().
getId
();
ii
.
spId
=
inv
.
getStockingPoint
().
getId
();
ii
.
quantity
=
inv
.
getQuantity
();
s
.
initialInventories
.
add
(
ii
);
}
for
(
InTransitSupply
its
:
data
.
getInTransitSupplies
())
{
BomStructureData
.
InTransitSupplyInfo
ti
=
new
BomStructureData
.
InTransitSupplyInfo
();
ti
.
productId
=
its
.
getProduct
().
getId
();
ti
.
spId
=
its
.
getStockingPoint
().
getId
();
ti
.
arrivalDate
=
its
.
getArrivalDate
()
==
null
?
null
:
its
.
getArrivalDate
().
toString
();
ti
.
quantity
=
its
.
getQuantity
();
s
.
inTransitSupplies
.
add
(
ti
);
}
return
s
;
}
/**
* 从求解结果提取每个工序×物料×周期的消耗量 (OperationDemandQty)。
*/
private
List
<
OperationDemandResult
>
buildOperationDemandResults
()
{
List
<
OperationDemandResult
>
list
=
new
ArrayList
<>();
Map
<
String
,
MPVariable
>
opDemandVars
=
model
.
getOperationDemandQtyVars
();
for
(
OperationInput
input
:
data
.
getOperationInputs
())
{
String
opId
=
input
.
getOperation
().
getId
();
String
inputProductId
=
input
.
getInputProduct
().
getId
();
String
inputSpId
=
input
.
getInputSp
().
getId
();
for
(
Period
p
:
data
.
getPeriods
())
{
String
key
=
input
.
getKey
()
+
"_"
+
p
.
getIndex
();
MPVariable
var
=
opDemandVars
.
get
(
key
);
if
(
var
==
null
)
continue
;
double
qty
=
var
.
solutionValue
();
if
(
Math
.
abs
(
qty
)
<
1
e
-
9
)
continue
;
list
.
add
(
new
OperationDemandResult
(
opId
,
inputProductId
,
inputSpId
,
p
.
getIndex
(),
qty
));
}
}
return
list
;
}
/**
/**
* 从文件中读取 Chromosome 对象,并拆分文件读取与反序列化耗时。
* 从文件中读取 Chromosome 对象,并拆分文件读取与反序列化耗时。
*/
*/
...
@@ -331,8 +474,8 @@ public class ResultWriter {
...
@@ -331,8 +474,8 @@ public class ResultWriter {
buildPispips
(
result
);
buildPispips
(
result
);
writeLog
(
"UnitCapacitie"
);
writeLog
(
"UnitCapacitie"
);
result
.
setUnitCapacities
(
buildUnitCapacities
());
result
.
setUnitCapacities
(
buildUnitCapacities
());
result
.
setProductNetwork
(
buildProductNetwork
());
//
result.setProductNetwork(buildProductNetwork());
result
.
setDemandSummary
(
buildDemandSummary
());
//
result.setDemandSummary(buildDemandSummary());
// KPI + 统计
// KPI + 统计
writeLog
(
"Kpi"
);
writeLog
(
"Kpi"
);
...
...
src/main/java/com/aps/macroplanner/output/dto/BomStructureData.java
0 → 100644
View file @
f11971ca
package
com
.
aps
.
macroplanner
.
output
.
dto
;
import
java.util.ArrayList
;
import
java.util.List
;
/**
* BOM 结构持久化数据 — 求解后将静态工艺/BOM 关系固化,供查询接口按需组装产品生产网络。
*
* <p>与求解结果 (period_tasks / pispips / salesdemand / operation_demands) 分离:
* 求解结果存 "数值", 本结构存 "关系" (谁产出什么、谁消耗什么、消耗因子)。</p>
*
* <p>字段全部使用扁平化嵌套 POJO + public 字段, 便于 Jackson 序列化/反序列化。</p>
*/
public
class
BomStructureData
{
public
List
<
ProductInfo
>
products
=
new
ArrayList
<>();
public
List
<
StockingPointInfo
>
stockingPoints
=
new
ArrayList
<>();
public
List
<
ProductSpMappingInfo
>
productSpMappings
=
new
ArrayList
<>();
public
List
<
OperationInfo
>
operations
=
new
ArrayList
<>();
public
List
<
OperationInputInfo
>
operationInputs
=
new
ArrayList
<>();
public
List
<
PeriodInfo
>
periods
=
new
ArrayList
<>();
public
List
<
InitialInventoryInfo
>
initialInventories
=
new
ArrayList
<>();
public
List
<
InTransitSupplyInfo
>
inTransitSupplies
=
new
ArrayList
<>();
// ==================== 扁平 POJO ====================
public
static
class
ProductInfo
{
public
String
id
;
public
String
name
;
public
String
code
;
}
public
static
class
StockingPointInfo
{
public
String
id
;
public
String
name
;
}
public
static
class
ProductSpMappingInfo
{
public
String
productId
;
public
String
spId
;
}
public
static
class
OperationInfo
{
public
String
id
;
public
String
name
;
public
int
leadTimeDays
;
public
boolean
hasLotSize
;
public
Double
lotSize
;
public
List
<
OperationOutputInfo
>
outputs
=
new
ArrayList
<>();
public
List
<
UnitOperationInfo
>
unitOperations
=
new
ArrayList
<>();
}
public
static
class
OperationOutputInfo
{
public
String
productId
;
public
String
spId
;
}
public
static
class
UnitOperationInfo
{
public
String
unitId
;
public
String
unitName
;
public
double
capacityCoeff
;
}
public
static
class
OperationInputInfo
{
public
String
operationId
;
public
String
inputProductId
;
public
String
inputSpId
;
public
String
inputSpName
;
public
double
factor
;
}
public
static
class
PeriodInfo
{
public
int
index
;
public
String
name
;
public
String
startDate
;
}
public
static
class
InitialInventoryInfo
{
public
String
productId
;
public
String
spId
;
public
double
quantity
;
}
public
static
class
InTransitSupplyInfo
{
public
String
productId
;
public
String
spId
;
public
String
arrivalDate
;
public
double
quantity
;
}
}
src/main/java/com/aps/macroplanner/output/dto/OperationDemandResult.java
0 → 100644
View file @
f11971ca
package
com
.
aps
.
macroplanner
.
output
.
dto
;
/**
* 工序消耗物料结果 — 每个工序在每周期消耗每种 BOM 输入物料的量 (OperationDemandQty)。
*
* <p>用于按需组装产品生产网络时还原 "消费者 consumedQty" 与 "BOM 子物料 consumedQty"。
* 扁平 POJO, 直接映射到 operation_demands.parquet。</p>
*/
public
class
OperationDemandResult
{
private
String
operationId
;
private
String
inputProductId
;
private
String
inputSpId
;
private
int
periodIndex
;
private
double
quantity
;
public
OperationDemandResult
()
{
}
public
OperationDemandResult
(
String
operationId
,
String
inputProductId
,
String
inputSpId
,
int
periodIndex
,
double
quantity
)
{
this
.
operationId
=
operationId
;
this
.
inputProductId
=
inputProductId
;
this
.
inputSpId
=
inputSpId
;
this
.
periodIndex
=
periodIndex
;
this
.
quantity
=
quantity
;
}
public
String
getOperationId
()
{
return
operationId
;
}
public
void
setOperationId
(
String
v
)
{
this
.
operationId
=
v
;
}
public
String
getInputProductId
()
{
return
inputProductId
;
}
public
void
setInputProductId
(
String
v
)
{
this
.
inputProductId
=
v
;
}
public
String
getInputSpId
()
{
return
inputSpId
;
}
public
void
setInputSpId
(
String
v
)
{
this
.
inputSpId
=
v
;
}
public
int
getPeriodIndex
()
{
return
periodIndex
;
}
public
void
setPeriodIndex
(
int
v
)
{
this
.
periodIndex
=
v
;
}
public
double
getQuantity
()
{
return
quantity
;
}
public
void
setQuantity
(
double
v
)
{
this
.
quantity
=
v
;
}
}
src/main/java/com/aps/service/MacroPlannerProductNetworkService.java
0 → 100644
View file @
f11971ca
package
com
.
aps
.
service
;
import
com.aps.common.util.FlatParquetUtil
;
import
com.aps.macroplanner.output.dto.*
;
import
com.fasterxml.jackson.databind.ObjectMapper
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.stereotype.Service
;
import
java.io.File
;
import
java.nio.file.Paths
;
import
java.util.*
;
/**
* 产品生产网络按需查询服务。
*
* <p>不再在求解阶段预生成完整 BOM 网络 (buildProductNetwork), 而是:
* <ol>
* <li>求解时把静态 BOM 结构持久化为 {@code bom_structure.json};</li>
* <li>求解变量值已持久化为 parquet (period_tasks / pispips / salesdemand / operation_demands);</li>
* <li>查询时按 productId + spId 定位节点, 递归向下展开 BOM 子树, 按需读取变量值。</li>
* </ol>
*
* <p>这样避免了构建整棵树的耗时和超大 JSON 的序列化/反序列化开销。</p>
*/
@Service
@Slf4j
public
class
MacroPlannerProductNetworkService
{
private
static
final
String
RESULT_DIR
=
"mp/result"
;
private
final
ObjectMapper
objectMapper
=
new
ObjectMapper
();
/**
* 按需组装 productId@spId 节点及其 BOM 子树。
*
* @param sceneId 场景ID
* @param productId 目标产品ID (必填)
* @param spId 目标库存点ID (必填)
* @param period 可选周期过滤 (周期索引的数字字符串, 如 "3")
* @return 目标节点 (含子树), 或 null
*/
public
SupplyChainNode
buildProductNetwork
(
String
sceneId
,
String
productId
,
String
spId
,
String
period
)
{
if
(
isBlank
(
sceneId
)
||
isBlank
(
productId
)
||
isBlank
(
spId
))
{
return
null
;
}
try
{
Integer
periodIndex
=
isBlank
(
period
)
?
null
:
Integer
.
parseInt
(
period
.
trim
());
IndexedData
idx
=
load
(
sceneId
);
return
buildNode
(
idx
,
productId
,
spId
,
periodIndex
,
0
,
new
HashSet
<>());
}
catch
(
Exception
e
)
{
log
.
warn
(
"按需组装产品网络失败: sceneId={}, productId={}, spId={}, error={}"
,
sceneId
,
productId
,
spId
,
e
.
getMessage
());
return
null
;
}
}
// ==================== 加载 + 索引 ====================
private
IndexedData
load
(
String
sceneId
)
throws
Exception
{
BomStructureData
structure
=
objectMapper
.
readValue
(
new
File
(
path
(
sceneId
,
"bom_structure.json"
)),
BomStructureData
.
class
);
FlatParquetUtil
parquet
=
new
FlatParquetUtil
();
List
<
PeriodTaskResult
>
periodTasks
=
parquet
.
readAll
(
path
(
sceneId
,
"period_tasks.parquet"
),
PeriodTaskResult
.
class
);
List
<
PispipResult
>
pispips
=
parquet
.
readAll
(
path
(
sceneId
,
"pispips.parquet"
),
PispipResult
.
class
);
List
<
SalesDemandResult
>
sales
=
parquet
.
readAll
(
path
(
sceneId
,
"salesdemand.parquet"
),
SalesDemandResult
.
class
);
List
<
OperationDemandResult
>
opDemands
=
parquet
.
readAll
(
path
(
sceneId
,
"operation_demands.parquet"
),
OperationDemandResult
.
class
);
return
new
IndexedData
(
structure
,
periodTasks
,
pispips
,
sales
,
opDemands
);
}
private
String
path
(
String
sceneId
,
String
fileName
)
{
return
Paths
.
get
(
RESULT_DIR
,
sceneId
,
fileName
).
toAbsolutePath
().
toString
();
}
/**
* 内存索引: 将持久化结构 + 求解变量值整理成 O(1) 查询结构。
*/
private
static
class
IndexedData
{
final
Map
<
String
,
List
<
BomStructureData
.
StockingPointInfo
>>
spByProduct
=
new
HashMap
<>();
final
Map
<
String
,
String
>
spNameById
=
new
HashMap
<>();
final
Map
<
String
,
List
<
BomStructureData
.
OperationInfo
>>
operationsByProductSp
=
new
HashMap
<>();
final
Map
<
String
,
List
<
BomStructureData
.
OperationInfo
>>
operationsByProduct
=
new
HashMap
<>();
final
Map
<
String
,
BomStructureData
.
OperationInfo
>
operationById
=
new
HashMap
<>();
final
Map
<
String
,
List
<
BomStructureData
.
OperationInputInfo
>>
inputsByOperation
=
new
HashMap
<>();
final
Map
<
String
,
List
<
BomStructureData
.
OperationInputInfo
>>
inputsByProductSp
=
new
HashMap
<>();
final
Map
<
String
,
Double
>
initialInventory
=
new
HashMap
<>();
final
Map
<
String
,
List
<
BomStructureData
.
InTransitSupplyInfo
>>
inTransitByProductSp
=
new
HashMap
<>();
final
List
<
BomStructureData
.
PeriodInfo
>
periods
=
new
ArrayList
<>();
final
Map
<
Integer
,
String
>
startDateByPeriodIndex
=
new
HashMap
<>();
// 求解变量值
final
Map
<
String
,
Double
>
ptQty
=
new
HashMap
<>();
// operationId_unitId_periodIndex
final
Map
<
String
,
Double
>
opDemand
=
new
HashMap
<>();
// operationId_inputProductId_inputSpId_periodIndex
final
Map
<
String
,
PispipResult
>
pispip
=
new
HashMap
<>();
// productId_spId_periodIndex
final
Map
<
String
,
double
[]>
sales
=
new
HashMap
<>();
// productId_spId_periodIndex -> [demandQty, fulfilledQty]
IndexedData
(
BomStructureData
s
,
List
<
PeriodTaskResult
>
periodTasks
,
List
<
PispipResult
>
pispips
,
List
<
SalesDemandResult
>
salesList
,
List
<
OperationDemandResult
>
opDemands
)
{
for
(
BomStructureData
.
StockingPointInfo
sp
:
s
.
stockingPoints
)
{
spNameById
.
put
(
sp
.
id
,
sp
.
name
);
}
for
(
BomStructureData
.
ProductSpMappingInfo
m
:
s
.
productSpMappings
)
{
BomStructureData
.
StockingPointInfo
info
=
new
BomStructureData
.
StockingPointInfo
();
info
.
id
=
m
.
spId
;
info
.
name
=
spNameById
.
get
(
m
.
spId
);
spByProduct
.
computeIfAbsent
(
m
.
productId
,
k
->
new
ArrayList
<>()).
add
(
info
);
}
for
(
BomStructureData
.
OperationInfo
op
:
s
.
operations
)
{
operationById
.
put
(
op
.
id
,
op
);
for
(
BomStructureData
.
OperationOutputInfo
oo
:
op
.
outputs
)
{
operationsByProductSp
.
computeIfAbsent
(
oo
.
productId
+
"@"
+
oo
.
spId
,
k
->
new
ArrayList
<>()).
add
(
op
);
operationsByProduct
.
computeIfAbsent
(
oo
.
productId
,
k
->
new
ArrayList
<>()).
add
(
op
);
}
}
for
(
BomStructureData
.
OperationInputInfo
in
:
s
.
operationInputs
)
{
inputsByOperation
.
computeIfAbsent
(
in
.
operationId
,
k
->
new
ArrayList
<>()).
add
(
in
);
inputsByProductSp
.
computeIfAbsent
(
in
.
inputProductId
+
"@"
+
in
.
inputSpId
,
k
->
new
ArrayList
<>()).
add
(
in
);
}
for
(
BomStructureData
.
InitialInventoryInfo
inv
:
s
.
initialInventories
)
{
initialInventory
.
put
(
inv
.
productId
+
"@"
+
inv
.
spId
,
inv
.
quantity
);
}
for
(
BomStructureData
.
InTransitSupplyInfo
its
:
s
.
inTransitSupplies
)
{
inTransitByProductSp
.
computeIfAbsent
(
its
.
productId
+
"@"
+
its
.
spId
,
k
->
new
ArrayList
<>()).
add
(
its
);
}
for
(
BomStructureData
.
PeriodInfo
p
:
s
.
periods
)
{
periods
.
add
(
p
);
if
(
p
.
startDate
!=
null
)
{
startDateByPeriodIndex
.
put
(
p
.
index
,
p
.
startDate
);
}
}
for
(
PeriodTaskResult
pt
:
periodTasks
)
{
ptQty
.
put
(
pt
.
getOperationId
()
+
"_"
+
pt
.
getUnitId
()
+
"_"
+
pt
.
getPeriodIndex
(),
pt
.
getProductionQty
());
}
for
(
OperationDemandResult
od
:
opDemands
)
{
opDemand
.
put
(
od
.
getOperationId
()
+
"_"
+
od
.
getInputProductId
()
+
"_"
+
od
.
getInputSpId
()
+
"_"
+
od
.
getPeriodIndex
(),
od
.
getQuantity
());
}
for
(
PispipResult
p
:
pispips
)
{
pispip
.
put
(
p
.
getProductId
()
+
"_"
+
p
.
getSpId
()
+
"_"
+
p
.
getPeriodIndex
(),
p
);
}
for
(
SalesDemandResult
sd
:
salesList
)
{
String
key
=
sd
.
getProductId
()
+
"_"
+
sd
.
getSpId
()
+
"_"
+
sd
.
getPeriodIndex
();
double
[]
acc
=
sales
.
computeIfAbsent
(
key
,
k
->
new
double
[]{
0.0
,
0.0
});
acc
[
0
]
+=
sd
.
getDemandQty
();
acc
[
1
]
+=
sd
.
getFulfilledQty
();
}
}
List
<
BomStructureData
.
OperationInfo
>
producing
(
String
productId
,
String
spId
)
{
return
operationsByProductSp
.
getOrDefault
(
productId
+
"@"
+
spId
,
Collections
.
emptyList
());
}
List
<
BomStructureData
.
OperationInputInfo
>
inputsOf
(
String
operationId
)
{
return
inputsByOperation
.
getOrDefault
(
operationId
,
Collections
.
emptyList
());
}
List
<
BomStructureData
.
OperationInputInfo
>
consumersOf
(
String
productId
,
String
spId
)
{
return
inputsByProductSp
.
getOrDefault
(
productId
+
"@"
+
spId
,
Collections
.
emptyList
());
}
double
initialInv
(
String
productId
,
String
spId
)
{
return
initialInventory
.
getOrDefault
(
productId
+
"@"
+
spId
,
0.0
);
}
List
<
BomStructureData
.
InTransitSupplyInfo
>
inTransit
(
String
productId
,
String
spId
)
{
return
inTransitByProductSp
.
getOrDefault
(
productId
+
"@"
+
spId
,
Collections
.
emptyList
());
}
double
demandQty
(
String
productId
,
String
spId
,
int
periodIndex
)
{
double
[]
acc
=
sales
.
get
(
productId
+
"_"
+
spId
+
"_"
+
periodIndex
);
return
acc
==
null
?
0.0
:
acc
[
0
];
}
double
fulfilledQty
(
String
productId
,
String
spId
,
int
periodIndex
)
{
double
[]
acc
=
sales
.
get
(
productId
+
"_"
+
spId
+
"_"
+
periodIndex
);
return
acc
==
null
?
0.0
:
acc
[
1
];
}
PispipResult
pispip
(
String
productId
,
String
spId
,
int
periodIndex
)
{
return
pispip
.
get
(
productId
+
"_"
+
spId
+
"_"
+
periodIndex
);
}
String
startDate
(
int
periodIndex
)
{
return
startDateByPeriodIndex
.
get
(
periodIndex
);
}
}
// ==================== 节点组装 ====================
private
SupplyChainNode
buildNode
(
IndexedData
idx
,
String
productId
,
String
spId
,
Integer
periodIndex
,
int
level
,
Set
<
String
>
visited
)
{
String
nodeKey
=
productId
+
"@"
+
spId
;
if
(!
visited
.
add
(
nodeKey
))
{
return
null
;
// 防环
}
SupplyChainNode
node
=
new
SupplyChainNode
();
node
.
setProductId
(
productId
);
node
.
setSpId
(
spId
);
node
.
setSpName
(
idx
.
spNameById
.
getOrDefault
(
spId
,
spId
));
node
.
setLevel
(
level
);
// 周期过滤的目标 startDate
String
periodStartDate
=
periodIndex
==
null
?
null
:
idx
.
startDate
(
periodIndex
);
// --- 供应来源 ---
for
(
BomStructureData
.
OperationInfo
op
:
idx
.
producing
(
productId
,
spId
))
{
SupplyChainNode
.
SupplySource
src
=
new
SupplyChainNode
.
SupplySource
();
src
.
type
=
"OPERATION"
;
src
.
operationId
=
op
.
id
;
src
.
operationName
=
op
.
name
;
if
(
op
.
hasLotSize
)
{
src
.
lotSize
=
op
.
lotSize
;
}
double
totalProduction
=
0
;
double
totalCapacity
=
0
;
for
(
BomStructureData
.
UnitOperationInfo
uo
:
op
.
unitOperations
)
{
for
(
BomStructureData
.
PeriodInfo
p
:
idx
.
periods
)
{
double
qty
=
idx
.
ptQty
.
getOrDefault
(
op
.
id
+
"_"
+
uo
.
unitId
+
"_"
+
p
.
index
,
0.0
);
if
(
qty
<=
0
)
continue
;
SupplyChainNode
.
SupplySourceDetail
detail
=
new
SupplyChainNode
.
SupplySourceDetail
();
detail
.
unitId
=
uo
.
unitId
;
detail
.
unitName
=
uo
.
unitName
;
detail
.
production
=
qty
;
detail
.
capacityUsed
=
qty
*
uo
.
capacityCoeff
;
totalProduction
+=
qty
;
totalCapacity
+=
detail
.
capacityUsed
;
String
periodKey
=
p
.
startDate
==
null
?
String
.
valueOf
(
p
.
index
)
:
p
.
startDate
;
src
.
productionByPeriod
.
put
(
periodKey
,
detail
);
node
.
getActivePeriods
().
add
(
periodKey
);
}
}
src
.
totalProduction
=
totalProduction
;
src
.
capacityUsed
=
totalCapacity
;
src
.
unitId
=
op
.
unitOperations
.
isEmpty
()
?
null
:
op
.
unitOperations
.
get
(
0
).
unitId
;
src
.
unitName
=
op
.
unitOperations
.
isEmpty
()
?
null
:
op
.
unitOperations
.
get
(
0
).
unitName
;
// period 过滤: 仅保留该周期有产量的来源, 并把 production 收敛到该周期
if
(
periodStartDate
!=
null
)
{
SupplyChainNode
.
SupplySourceDetail
d
=
src
.
productionByPeriod
.
get
(
periodStartDate
);
if
(
d
==
null
)
{
continue
;
}
src
.
production
=
d
.
production
;
src
.
unitId
=
d
.
unitId
;
src
.
unitName
=
d
.
unitName
;
src
.
productionByPeriod
.
clear
();
}
node
.
getSupplySources
().
add
(
src
);
}
// --- 在途供应 ---
for
(
BomStructureData
.
InTransitSupplyInfo
its
:
idx
.
inTransit
(
productId
,
spId
))
{
SupplyChainNode
.
SupplySource
src
=
new
SupplyChainNode
.
SupplySource
();
src
.
type
=
"IN_TRANSIT"
;
src
.
totalProduction
=
its
.
quantity
;
node
.
getSupplySources
().
add
(
src
);
}
// --- 外部采购 (无供应源) ---
if
(
node
.
getSupplySources
().
isEmpty
())
{
SupplyChainNode
.
SupplySource
src
=
new
SupplyChainNode
.
SupplySource
();
src
.
type
=
"EXTERNAL"
;
src
.
totalProduction
=
0
;
node
.
getSupplySources
().
add
(
src
);
}
// --- 消费者 ---
Set
<
String
>
visitedConsumers
=
new
HashSet
<>();
for
(
BomStructureData
.
OperationInputInfo
in
:
idx
.
consumersOf
(
productId
,
spId
))
{
BomStructureData
.
OperationInfo
consumerOp
=
idx
.
operationById
.
get
(
in
.
operationId
);
double
totalConsumed
=
0
;
Map
<
Integer
,
Double
>
consumedByPeriod
=
new
LinkedHashMap
<>();
for
(
BomStructureData
.
PeriodInfo
p
:
idx
.
periods
)
{
double
consumed
=
idx
.
opDemand
.
getOrDefault
(
in
.
operationId
+
"_"
+
in
.
inputProductId
+
"_"
+
in
.
inputSpId
+
"_"
+
p
.
index
,
0.0
);
totalConsumed
+=
consumed
;
if
(
consumed
>
0
)
{
consumedByPeriod
.
put
(
p
.
index
,
consumed
);
}
}
if
(
consumerOp
!=
null
)
{
for
(
BomStructureData
.
OperationOutputInfo
oo
:
consumerOp
.
outputs
)
{
String
ciKey
=
oo
.
productId
+
"@"
+
oo
.
spId
+
"_"
+
in
.
operationId
;
if
(!
visitedConsumers
.
add
(
ciKey
))
{
continue
;
}
if
(
periodIndex
!=
null
&&
!
consumedByPeriod
.
containsKey
(
periodIndex
))
{
continue
;
}
SupplyChainNode
.
ConsumerInfo
ci
=
new
SupplyChainNode
.
ConsumerInfo
();
ci
.
consumerProductId
=
oo
.
productId
;
ci
.
consumerSpId
=
oo
.
spId
;
ci
.
operationId
=
in
.
operationId
;
ci
.
operationName
=
consumerOp
.
name
;
ci
.
unitId
=
consumerOp
.
unitOperations
.
isEmpty
()
?
null
:
consumerOp
.
unitOperations
.
get
(
0
).
unitId
;
ci
.
factor
=
in
.
factor
;
ci
.
totalConsumed
=
totalConsumed
;
ci
.
consumedByPeriod
.
putAll
(
consumedByPeriod
);
node
.
getConsumers
().
add
(
ci
);
}
}
}
// --- BOM 子物料 (向下展开) ---
List
<
BomStructureData
.
OperationInfo
>
producingOps
=
idx
.
producing
(
productId
,
spId
);
if
(!
producingOps
.
isEmpty
())
{
BomStructureData
.
OperationInfo
op
=
producingOps
.
get
(
0
);
// 与原逻辑一致: 只展开第一个工序的 BOM
for
(
BomStructureData
.
OperationInputInfo
in
:
idx
.
inputsOf
(
op
.
id
))
{
SupplyChainNode
.
BomChild
child
=
new
SupplyChainNode
.
BomChild
();
child
.
productId
=
in
.
inputProductId
;
child
.
spId
=
in
.
inputSpId
;
child
.
spName
=
in
.
inputSpName
;
child
.
operationId
=
in
.
operationId
;
child
.
operationName
=
op
.
name
;
child
.
totalFactor
=
in
.
factor
;
double
totalConsumed
=
0
;
for
(
BomStructureData
.
PeriodInfo
p
:
idx
.
periods
)
{
double
consumed
=
idx
.
opDemand
.
getOrDefault
(
in
.
operationId
+
"_"
+
in
.
inputProductId
+
"_"
+
in
.
inputSpId
+
"_"
+
p
.
index
,
0.0
);
totalConsumed
+=
consumed
;
if
(
consumed
>
0
)
{
String
periodKey
=
p
.
startDate
==
null
?
String
.
valueOf
(
p
.
index
)
:
p
.
startDate
;
child
.
consumedByPeriod
.
put
(
periodKey
,
consumed
);
}
}
child
.
totalConsumedQty
=
totalConsumed
;
if
(
periodStartDate
!=
null
&&
!
child
.
consumedByPeriod
.
containsKey
(
periodStartDate
))
{
continue
;
// 该周期无消耗, 剪枝
}
SupplyChainNode
childNode
=
buildNode
(
idx
,
in
.
inputProductId
,
in
.
inputSpId
,
periodIndex
,
level
+
1
,
visited
);
if
(
childNode
==
null
)
{
continue
;
}
// 用子节点的供应源回填 child 的 unitId/unitName/totalConsumedQty (与原逻辑对齐)
for
(
SupplyChainNode
.
SupplySource
ss
:
childNode
.
getSupplySources
())
{
child
.
totalConsumedQty
=
ss
.
production
;
child
.
unitId
=
ss
.
unitId
;
child
.
unitName
=
ss
.
unitName
;
break
;
}
node
.
getChildren
().
add
(
child
);
}
}
// --- 汇总 ---
fillSummary
(
idx
,
node
,
productId
,
spId
);
// period 剪枝: 该周期无活动则剪掉整个节点 (与原 buildProductNetwork 语义一致)
if
(
periodStartDate
!=
null
&&
!
node
.
getActivePeriods
().
contains
(
periodStartDate
))
{
return
null
;
}
return
node
;
}
private
void
fillSummary
(
IndexedData
idx
,
SupplyChainNode
node
,
String
productId
,
String
spId
)
{
SupplySummary
s
=
new
SupplySummary
();
node
.
setSummary
(
s
);
int
n
=
idx
.
periods
.
size
();
double
totalEndingInv
=
0
;
for
(
BomStructureData
.
PeriodInfo
p
:
idx
.
periods
)
{
PispipResult
pr
=
idx
.
pispip
(
productId
,
spId
,
p
.
index
);
if
(
pr
!=
null
)
{
totalEndingInv
+=
pr
.
getEndingInventory
();
if
(
pr
.
getEndingInventory
()
>
0
)
{
node
.
getActivePeriods
().
add
(
p
.
startDate
==
null
?
String
.
valueOf
(
p
.
index
)
:
p
.
startDate
);
}
}
}
s
.
setInitialInventory
(
idx
.
initialInv
(
productId
,
spId
));
PispipResult
last
=
idx
.
pispip
(
productId
,
spId
,
n
-
1
);
s
.
setFinalInventory
(
last
==
null
?
0
:
last
.
getEndingInventory
());
s
.
setAverageInventory
(
n
>
0
?
totalEndingInv
/
n
:
0
);
// 生产汇总
double
totalProduction
=
0
;
for
(
BomStructureData
.
OperationInfo
op
:
idx
.
producing
(
productId
,
spId
))
{
for
(
BomStructureData
.
UnitOperationInfo
uo
:
op
.
unitOperations
)
{
for
(
BomStructureData
.
PeriodInfo
p
:
idx
.
periods
)
{
double
qty
=
idx
.
ptQty
.
getOrDefault
(
op
.
id
+
"_"
+
uo
.
unitId
+
"_"
+
p
.
index
,
0.0
);
totalProduction
+=
qty
;
}
}
}
s
.
setTotalProduction
(
totalProduction
);
// 在途汇总
double
totalInTransit
=
0
;
for
(
BomStructureData
.
InTransitSupplyInfo
its
:
idx
.
inTransit
(
productId
,
spId
))
{
totalInTransit
+=
its
.
quantity
;
}
s
.
setTotalInTransit
(
totalInTransit
);
// 需求汇总
double
totalSalesDemand
=
0
;
double
totalSalesFulfilled
=
0
;
double
totalDepDemand
=
0
;
double
totalDemandFulf
=
0
;
double
totalSlack
=
0
;
for
(
BomStructureData
.
PeriodInfo
p
:
idx
.
periods
)
{
PispipResult
pr
=
idx
.
pispip
(
productId
,
spId
,
p
.
index
);
if
(
pr
!=
null
)
{
totalDepDemand
+=
pr
.
getDependentDemandQty
();
totalDemandFulf
+=
pr
.
getDemandFulfillment
();
totalSlack
+=
pr
.
getDemandSlack
();
if
(
pr
.
getDependentDemandQty
()
>
0
)
{
node
.
getActivePeriods
().
add
(
p
.
startDate
==
null
?
String
.
valueOf
(
p
.
index
)
:
p
.
startDate
);
}
}
totalSalesDemand
+=
idx
.
demandQty
(
productId
,
spId
,
p
.
index
);
totalSalesFulfilled
+=
idx
.
fulfilledQty
(
productId
,
spId
,
p
.
index
);
}
s
.
setTotalSalesDemand
(
totalSalesDemand
);
s
.
setTotalSalesFulfilled
(
totalSalesFulfilled
);
s
.
setTotalDependentDemand
(
totalDepDemand
);
s
.
setTotalDemandFulfillment
(
totalDemandFulf
);
s
.
setTotalDemandSlack
(
totalSlack
);
node
.
setTotalSalesDemand
(
totalSalesDemand
);
node
.
setTotalSalesFulfilled
(
totalSalesFulfilled
);
node
.
setTotalDependentDemand
(
totalDepDemand
);
// 库存规格偏差
double
belowTarget
=
0
;
double
belowMin
=
0
;
double
aboveMax
=
0
;
for
(
BomStructureData
.
PeriodInfo
p
:
idx
.
periods
)
{
PispipResult
pr
=
idx
.
pispip
(
productId
,
spId
,
p
.
index
);
if
(
pr
!=
null
)
{
belowTarget
+=
pr
.
getBelowTarget
();
belowMin
+=
pr
.
getBelowMin
();
aboveMax
+=
pr
.
getAboveMax
();
}
}
s
.
setTotalBelowTarget
(
belowTarget
);
s
.
setTotalBelowMin
(
belowMin
);
s
.
setTotalAboveMax
(
aboveMax
);
}
private
boolean
isBlank
(
String
v
)
{
return
v
==
null
||
v
.
trim
().
isEmpty
();
}
}
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