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