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
d1745f27
Commit
d1745f27
authored
Sep 20, 2026
by
Tong Li
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
MP
parent
ea3cc142
Hide whitespace changes
Inline
Side-by-side
Showing
21 changed files
with
1729 additions
and
411 deletions
+1729
-411
FlatParquetUtil.java
src/main/java/com/aps/common/util/FlatParquetUtil.java
+858
-31
MacroPlannerResultController.java
...java/com/aps/controller/MacroPlannerResultController.java
+321
-100
Material.java
src/main/java/com/aps/entity/basic/Material.java
+2
-0
MacroPlannerDataConverterRunner.java
...com/aps/macroplanner/MacroPlannerDataConverterRunner.java
+6
-0
MultiLevelBomTestRunner.java
...in/java/com/aps/macroplanner/MultiLevelBomTestRunner.java
+29
-26
MacroPlannerDataConverter.java
.../com/aps/macroplanner/data/MacroPlannerDataConverter.java
+31
-9
MultiLevelBomTestDataBuilder.java
...m/aps/macroplanner/data/MultiLevelBomTestDataBuilder.java
+5
-1
Operation.java
src/main/java/com/aps/macroplanner/data/Operation.java
+1
-1
OperationOutput.java
src/main/java/com/aps/macroplanner/data/OperationOutput.java
+12
-0
Product.java
src/main/java/com/aps/macroplanner/data/Product.java
+6
-0
SalesDemand.java
src/main/java/com/aps/macroplanner/data/SalesDemand.java
+7
-3
UnitOperation.java
src/main/java/com/aps/macroplanner/data/UnitOperation.java
+9
-0
UnitPeriod.java
src/main/java/com/aps/macroplanner/data/UnitPeriod.java
+12
-5
ResultWriter.java
src/main/java/com/aps/macroplanner/output/ResultWriter.java
+148
-40
OptimizationResult.java
...a/com/aps/macroplanner/output/dto/OptimizationResult.java
+25
-0
PeriodTaskResult.java
...ava/com/aps/macroplanner/output/dto/PeriodTaskResult.java
+16
-5
PispipResult.java
...in/java/com/aps/macroplanner/output/dto/PispipResult.java
+31
-1
SalesDemandResult.java
...va/com/aps/macroplanner/output/dto/SalesDemandResult.java
+5
-0
SupplyChainNode.java
...java/com/aps/macroplanner/output/dto/SupplyChainNode.java
+7
-0
UnitCapacityResult.java
...a/com/aps/macroplanner/output/dto/UnitCapacityResult.java
+14
-3
MacroPlannerProductNetworkService.java
...va/com/aps/service/MacroPlannerProductNetworkService.java
+184
-186
No files found.
src/main/java/com/aps/common/util/FlatParquetUtil.java
View file @
d1745f27
package
com
.
aps
.
common
.
util
;
/**
* 作者:佟礼
* 时间:2026-09-08
* 范式化扁平 Row POJO 读写 Parquet。
* 要求:POJO 不要包含嵌套 List 集合,子 List 拆成独立子 parquet。
*
* <p>在基础读写能力上提供:</p>
* <ul>
* <li>PageRequest 分页</li>
* <li>FilterCondition 条件过滤</li>
* <li>SortCondition 多字段排序</li>
* <li>Stream 流式读取</li>
* <li>分页 + 过滤 + 排序组合查询</li>
* </ul>
*/
import
org.apache.avro.Schema
;
...
...
@@ -19,19 +28,263 @@ import org.apache.parquet.io.LocalOutputFile;
import
java.io.File
;
import
java.io.IOException
;
import
java.lang.reflect.Field
;
import
java.lang.reflect.Method
;
import
java.net.URI
;
import
java.nio.file.Paths
;
import
java.util.ArrayList
;
import
java.util.Arrays
;
import
java.util.Collection
;
import
java.util.Comparator
;
import
java.util.List
;
/**
* 范式化扁平Row POJO读写Parquet
* 要求:POJO不要包含嵌套List集合,子List拆成独立子parquet
*/
import
java.util.Locale
;
import
java.util.function.Predicate
;
import
java.util.stream.Stream
;
import
java.util.stream.StreamSupport
;
public
class
FlatParquetUtil
{
// =========================
// 分页
// =========================
/**
* 分页请求,pageNumber 从 1 开始。
*/
public
static
final
class
PageRequest
{
private
final
int
pageNumber
;
private
final
int
pageSize
;
public
PageRequest
(
int
pageNumber
,
int
pageSize
)
{
if
(
pageNumber
<=
0
)
{
throw
new
IllegalArgumentException
(
"pageNumber 必须大于 0"
);
}
if
(
pageSize
<=
0
)
{
throw
new
IllegalArgumentException
(
"pageSize 必须大于 0"
);
}
this
.
pageNumber
=
pageNumber
;
this
.
pageSize
=
pageSize
;
}
public
static
PageRequest
of
(
int
pageNumber
,
int
pageSize
)
{
return
new
PageRequest
(
pageNumber
,
pageSize
);
}
public
int
getPageNumber
()
{
return
pageNumber
;
}
public
int
getPageSize
()
{
return
pageSize
;
}
public
long
getOffset
()
{
return
(
long
)
(
pageNumber
-
1
)
*
pageSize
;
}
}
public
static
final
class
PageResult
<
T
>
{
private
final
List
<
T
>
records
;
private
final
long
offset
;
private
final
int
pageNumber
;
private
final
int
pageSize
;
/**
* 总匹配数。普通分页查询会计算;流式分页为了避免额外扫描时,可以为 -1。
*/
private
final
long
totalMatched
;
private
final
boolean
hasNext
;
public
PageResult
(
List
<
T
>
records
,
long
offset
,
int
pageNumber
,
int
pageSize
,
long
totalMatched
,
boolean
hasNext
)
{
this
.
records
=
records
;
this
.
offset
=
offset
;
this
.
pageNumber
=
pageNumber
;
this
.
pageSize
=
pageSize
;
this
.
totalMatched
=
totalMatched
;
this
.
hasNext
=
hasNext
;
}
public
List
<
T
>
getRecords
()
{
return
records
;
}
public
long
getOffset
()
{
return
offset
;
}
public
int
getPageNumber
()
{
return
pageNumber
;
}
public
int
getPageSize
()
{
return
pageSize
;
}
public
long
getTotalMatched
()
{
return
totalMatched
;
}
public
boolean
isHasNext
()
{
return
hasNext
;
}
public
int
getCurrentCount
()
{
return
records
.
size
();
}
}
// =========================
// 条件过滤
// =========================
public
enum
Operator
{
EQ
,
NE
,
GT
,
GE
,
LT
,
LE
,
LIKE
,
STARTS_WITH
,
ENDS_WITH
,
IN
,
NOT_IN
,
IS_NULL
,
NOT_NULL
,
BETWEEN
}
/**
* 通用字段条件,例如:quantity >= 100、status IN (...)、productCode LIKE "P00"。
* 多个 FilterCondition 默认按 AND 组合。
*/
public
static
final
class
FilterCondition
<
T
>
{
private
final
String
field
;
private
final
Operator
operator
;
private
final
Object
value
;
private
FilterCondition
(
String
field
,
Operator
operator
,
Object
value
)
{
if
(
field
==
null
||
field
.
trim
().
isEmpty
())
{
throw
new
IllegalArgumentException
(
"field 不能为空"
);
}
if
(
operator
==
null
)
{
throw
new
IllegalArgumentException
(
"operator 不能为空"
);
}
this
.
field
=
field
;
this
.
operator
=
operator
;
this
.
value
=
value
;
}
public
static
<
T
>
FilterCondition
<
T
>
of
(
String
field
,
Operator
operator
,
Object
value
)
{
return
new
FilterCondition
<>(
field
,
operator
,
value
);
}
public
static
<
T
>
FilterCondition
<
T
>
eq
(
String
field
,
Object
value
)
{
return
of
(
field
,
Operator
.
EQ
,
value
);
}
public
static
<
T
>
FilterCondition
<
T
>
ne
(
String
field
,
Object
value
)
{
return
of
(
field
,
Operator
.
NE
,
value
);
}
public
static
<
T
>
FilterCondition
<
T
>
gt
(
String
field
,
Object
value
)
{
return
of
(
field
,
Operator
.
GT
,
value
);
}
public
static
<
T
>
FilterCondition
<
T
>
ge
(
String
field
,
Object
value
)
{
return
of
(
field
,
Operator
.
GE
,
value
);
}
public
static
<
T
>
FilterCondition
<
T
>
lt
(
String
field
,
Object
value
)
{
return
of
(
field
,
Operator
.
LT
,
value
);
}
public
static
<
T
>
FilterCondition
<
T
>
le
(
String
field
,
Object
value
)
{
return
of
(
field
,
Operator
.
LE
,
value
);
}
public
static
<
T
>
FilterCondition
<
T
>
like
(
String
field
,
String
value
)
{
return
of
(
field
,
Operator
.
LIKE
,
value
);
}
public
static
<
T
>
FilterCondition
<
T
>
startsWith
(
String
field
,
String
value
)
{
return
of
(
field
,
Operator
.
STARTS_WITH
,
value
);
}
public
static
<
T
>
FilterCondition
<
T
>
endsWith
(
String
field
,
String
value
)
{
return
of
(
field
,
Operator
.
ENDS_WITH
,
value
);
}
public
static
<
T
>
FilterCondition
<
T
>
in
(
String
field
,
Collection
<?>
values
)
{
return
of
(
field
,
Operator
.
IN
,
values
);
}
public
static
<
T
>
FilterCondition
<
T
>
notIn
(
String
field
,
Collection
<?>
values
)
{
return
of
(
field
,
Operator
.
NOT_IN
,
values
);
}
public
static
<
T
>
FilterCondition
<
T
>
isNull
(
String
field
)
{
return
of
(
field
,
Operator
.
IS_NULL
,
null
);
}
public
static
<
T
>
FilterCondition
<
T
>
notNull
(
String
field
)
{
return
of
(
field
,
Operator
.
NOT_NULL
,
null
);
}
/** value 为长度为 2 的 Collection:最小值、最大值。 */
public
static
<
T
>
FilterCondition
<
T
>
between
(
String
field
,
Object
min
,
Object
max
)
{
return
of
(
field
,
Operator
.
BETWEEN
,
Arrays
.
asList
(
min
,
max
));
}
public
String
getField
()
{
return
field
;
}
public
Operator
getOperator
()
{
return
operator
;
}
public
Object
getValue
()
{
return
value
;
}
}
// =========================
// 排序
// =========================
public
enum
Direction
{
ASC
,
DESC
}
public
static
final
class
SortCondition
{
private
final
String
field
;
private
final
Direction
direction
;
private
final
boolean
nullsFirst
;
private
SortCondition
(
String
field
,
Direction
direction
,
boolean
nullsFirst
)
{
if
(
field
==
null
||
field
.
trim
().
isEmpty
())
{
throw
new
IllegalArgumentException
(
"排序字段不能为空"
);
}
this
.
field
=
field
;
this
.
direction
=
direction
==
null
?
Direction
.
ASC
:
direction
;
this
.
nullsFirst
=
nullsFirst
;
}
public
static
SortCondition
asc
(
String
field
)
{
return
new
SortCondition
(
field
,
Direction
.
ASC
,
true
);
}
public
static
SortCondition
desc
(
String
field
)
{
return
new
SortCondition
(
field
,
Direction
.
DESC
,
false
);
}
public
static
SortCondition
of
(
String
field
,
Direction
direction
,
boolean
nullsFirst
)
{
return
new
SortCondition
(
field
,
direction
,
nullsFirst
);
}
public
String
getField
()
{
return
field
;
}
public
Direction
getDirection
()
{
return
direction
;
}
public
boolean
isNullsFirst
()
{
return
nullsFirst
;
}
}
public
static
final
class
QueryRequest
{
private
final
PageRequest
pageRequest
;
private
final
List
<
FilterCondition
<?>>
filters
;
private
final
List
<
SortCondition
>
sorts
;
public
QueryRequest
(
PageRequest
pageRequest
,
List
<
FilterCondition
<?>>
filters
,
List
<
SortCondition
>
sorts
)
{
this
.
pageRequest
=
pageRequest
;
this
.
filters
=
filters
==
null
?
java
.
util
.
Collections
.
emptyList
()
:
java
.
util
.
Collections
.
unmodifiableList
(
new
ArrayList
<>(
filters
));
this
.
sorts
=
sorts
==
null
?
java
.
util
.
Collections
.
emptyList
()
:
java
.
util
.
Collections
.
unmodifiableList
(
new
ArrayList
<>(
sorts
));
}
public
PageRequest
getPageRequest
()
{
return
pageRequest
;
}
public
List
<
FilterCondition
<?>>
getFilters
()
{
return
filters
;
}
public
List
<
SortCondition
>
getSorts
()
{
return
sorts
;
}
}
// =========================
// 配置
// =========================
static
{
// Windows 下 Hadoop 需要 winutils.exe/hadoop.dll。
// 优先复用 HADOOP_HOME,避免把 hadoop.home.dir 指到当前目录
(无 bin/winutils.exe)
导致写入失败。
// 优先复用 HADOOP_HOME,避免把 hadoop.home.dir 指到当前目录导致写入失败。
if
(
System
.
getProperty
(
"hadoop.home.dir"
)
==
null
)
{
String
hadoopHome
=
System
.
getenv
(
"HADOOP_HOME"
);
if
(
hadoopHome
!=
null
&&
!
hadoopHome
.
trim
().
isEmpty
())
{
...
...
@@ -44,15 +297,15 @@ public class FlatParquetUtil {
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(覆盖)
*/
// =========================
// 写入
// =========================
/** 写入扁平对象列表到 parquet(覆盖)。 */
public
<
T
>
void
write
(
List
<
T
>
rows
,
String
filePath
,
Class
<
T
>
clazz
)
throws
IOException
{
if
(
rows
==
null
||
rows
.
isEmpty
())
{
return
;
...
...
@@ -60,45 +313,63 @@ public class FlatParquetUtil {
File
file
=
resolvePath
(
filePath
);
File
parent
=
file
.
getParentFile
();
if
(
parent
!=
null
&&
!
parent
.
exists
())
{
parent
.
mkdirs
(
);
if
(
parent
!=
null
&&
!
parent
.
exists
()
&&
!
parent
.
mkdirs
()
&&
!
parent
.
exists
()
)
{
throw
new
IOException
(
"创建目录失败: "
+
parent
);
}
if
(
file
.
exists
())
{
file
.
delete
(
);
if
(
file
.
exists
()
&&
!
file
.
delete
()
)
{
throw
new
IOException
(
"删除旧 parquet 文件失败: "
+
file
);
}
Schema
schema
=
ReflectData
.
AllowNull
.
get
().
getSchema
(
clazz
);
LocalOutputFile
outputFile
=
new
LocalOutputFile
(
file
.
toPath
());
try
(
ParquetWriter
<
T
>
writer
=
AvroParquetWriter
.<
T
>
builder
(
outputFile
)
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
)
{
.
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
);
}
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
<>();
try
(
ParquetReader
<
T
>
reader
=
newReader
(
filePath
,
clazz
))
{
T
t
;
while
((
t
=
reader
.
read
())
!=
null
)
{
res
.
add
(
t
);
}
}
return
res
;
}
/**
* 读取全部行
* 读取全部行
,可按条件过滤(流式读取,只保留匹配记录,避免一次性全量加载内存)。
*/
public
<
T
>
List
<
T
>
readAll
(
String
filePath
,
Class
<
T
>
clazz
)
throws
IOException
{
public
<
T
>
List
<
T
>
readAll
(
String
filePath
,
Class
<
T
>
clazz
,
java
.
util
.
function
.
Predicate
<
T
>
filter
)
throws
IOException
{
List
<
T
>
res
=
new
ArrayList
<>();
File
file
=
resolvePath
(
filePath
);
if
(!
file
.
exists
())
{
return
res
;
}
LocalInputFile
inputFile
=
new
LocalInputFile
(
file
.
toPath
());
try
(
ParquetReader
<
T
>
reader
=
AvroParquetReader
.<
T
>
builder
(
inputFile
)
...
...
@@ -107,20 +378,576 @@ public class FlatParquetUtil {
.
build
()){
T
t
;
while
((
t
=
reader
.
read
())
!=
null
)
{
res
.
add
(
t
);
if
(
filter
==
null
||
filter
.
test
(
t
))
{
res
.
add
(
t
);
}
}
}
return
res
;
}
/**
* 获取迭代器流式读取,不一次性全加载内存
*/
/** 获取底层 ParquetReader,适合自定义逐行消费。 */
public
<
T
>
ParquetReader
<
T
>
getReader
(
String
filePath
,
Class
<
T
>
clazz
)
throws
IOException
{
Path
path
=
new
Path
(
filePath
);
return
AvroParquetReader
.<
T
>
builder
(
path
)
File
file
=
resolvePath
(
filePath
);
LocalInputFile
inputFile
=
new
LocalInputFile
(
file
.
toPath
());
return
AvroParquetReader
.<
T
>
builder
(
inputFile
)
.
withDataModel
(
ReflectData
.
AllowNull
.
get
())
.
withConf
(
conf
)
.
build
();
}
private
<
T
>
ParquetReader
<
T
>
newReader
(
String
filePath
,
Class
<
T
>
clazz
)
throws
IOException
{
return
getReader
(
filePath
,
clazz
);
}
// =========================
// 兼容旧分页接口
// =========================
public
<
T
>
PageResult
<
T
>
readPage
(
String
filePath
,
Class
<
T
>
clazz
,
long
offset
,
int
limit
)
throws
IOException
{
return
readPage
(
filePath
,
clazz
,
offset
,
limit
,
(
Predicate
<
T
>)
null
);
}
public
<
T
>
PageResult
<
T
>
readPage
(
String
filePath
,
Class
<
T
>
clazz
,
long
offset
,
int
limit
,
Predicate
<
T
>
predicate
)
throws
IOException
{
validateOffsetLimit
(
offset
,
limit
);
List
<
T
>
page
=
new
ArrayList
<>(
Math
.
min
(
limit
,
1024
));
long
matched
=
0
;
boolean
hasNext
=
false
;
try
(
ParquetReader
<
T
>
reader
=
newReader
(
filePath
,
clazz
))
{
T
row
;
while
((
row
=
reader
.
read
())
!=
null
)
{
if
(
predicate
!=
null
&&
!
predicate
.
test
(
row
))
{
continue
;
}
matched
++;
if
(
matched
<=
offset
)
{
continue
;
}
if
(
page
.
size
()
<
limit
)
{
page
.
add
(
row
);
}
else
{
hasNext
=
true
;
break
;
}
}
}
// totalMatched 需要额外扫描剩余文件;为了兼容原方法,这里继续统计。
long
totalMatched
=
matched
;
if
(
hasNext
)
{
totalMatched
=
count
(
filePath
,
clazz
,
predicate
);
}
int
pageNumber
=
(
int
)
(
offset
/
limit
)
+
1
;
return
new
PageResult
<>(
page
,
offset
,
pageNumber
,
limit
,
totalMatched
,
hasNext
);
}
public
<
T
>
PageResult
<
T
>
readPage
(
String
filePath
,
Class
<
T
>
clazz
,
PageRequest
pageRequest
,
List
<
FilterCondition
<?>>
filters
)
throws
IOException
{
return
queryPage
(
filePath
,
clazz
,
pageRequest
,
filters
,
java
.
util
.
Collections
.
emptyList
());
}
// =========================
// 高级 Query API
// =========================
/**
* 分页 + 条件 + 排序。
*
* <p>注意:排序按客户端内存实现,因此有排序条件时会把“过滤后的全部结果”加载到内存。
* 没有排序时,可以走流式分页,不需要一次性加载全部结果。</p>
*/
public
<
T
>
PageResult
<
T
>
queryPage
(
String
filePath
,
Class
<
T
>
clazz
,
QueryRequest
request
)
throws
IOException
{
if
(
request
==
null
||
request
.
getPageRequest
()
==
null
)
{
throw
new
IllegalArgumentException
(
"request/pageRequest 不能为空"
);
}
return
queryPage
(
filePath
,
clazz
,
request
.
getPageRequest
(),
request
.
getFilters
(),
request
.
getSorts
());
}
public
<
T
>
PageResult
<
T
>
queryPage
(
String
filePath
,
Class
<
T
>
clazz
,
PageRequest
pageRequest
,
List
<
FilterCondition
<?>>
filters
,
List
<
SortCondition
>
sorts
)
throws
IOException
{
if
(
pageRequest
==
null
)
{
throw
new
IllegalArgumentException
(
"pageRequest 不能为空"
);
}
List
<
FilterCondition
<?>>
safeFilters
=
filters
==
null
?
java
.
util
.
Collections
.
emptyList
()
:
filters
;
List
<
SortCondition
>
safeSorts
=
sorts
==
null
?
java
.
util
.
Collections
.
emptyList
()
:
sorts
;
if
(!
safeSorts
.
isEmpty
())
{
List
<
T
>
all
=
collect
(
filePath
,
clazz
,
toPredicate
(
safeFilters
));
all
.
sort
(
buildComparator
(
clazz
,
safeSorts
));
return
pageFromList
(
all
,
pageRequest
);
}
return
streamPage
(
filePath
,
clazz
,
pageRequest
,
toPredicate
(
safeFilters
));
}
/** 单纯按条件过滤,兼容旧方法。 */
public
<
T
>
List
<
T
>
filter
(
String
filePath
,
Class
<
T
>
clazz
,
Predicate
<
T
>
predicate
)
throws
IOException
{
if
(
predicate
==
null
)
{
throw
new
IllegalArgumentException
(
"predicate 不能为空"
);
}
return
collect
(
filePath
,
clazz
,
predicate
);
}
/** 条件对象过滤。多个条件默认 AND。 */
public
<
T
>
List
<
T
>
filter
(
String
filePath
,
Class
<
T
>
clazz
,
List
<
FilterCondition
<?>>
filters
)
throws
IOException
{
return
collect
(
filePath
,
clazz
,
toPredicate
(
filters
));
}
/** 过滤 + 分页的简化方法。 */
public
<
T
>
List
<
T
>
filterPage
(
String
filePath
,
Class
<
T
>
clazz
,
Predicate
<
T
>
predicate
,
long
offset
,
int
limit
)
throws
IOException
{
return
readPage
(
filePath
,
clazz
,
offset
,
limit
,
predicate
).
getRecords
();
}
/** 条件对象 + PageRequest。 */
public
<
T
>
PageResult
<
T
>
filterPage
(
String
filePath
,
Class
<
T
>
clazz
,
PageRequest
pageRequest
,
List
<
FilterCondition
<?>>
filters
)
throws
IOException
{
return
queryPage
(
filePath
,
clazz
,
pageRequest
,
filters
,
java
.
util
.
Collections
.
emptyList
());
}
// =========================
// Stream 流式读取
// =========================
/**
* 直接把 Parquet 转为 Java Stream。
* Stream close 时会关闭底层 ParquetReader。
*/
public
<
T
>
Stream
<
T
>
stream
(
String
filePath
,
Class
<
T
>
clazz
)
throws
IOException
{
return
stream
(
filePath
,
clazz
,
null
);
}
/** 流式 + Predicate 过滤。 */
public
<
T
>
Stream
<
T
>
stream
(
String
filePath
,
Class
<
T
>
clazz
,
Predicate
<
T
>
predicate
)
throws
IOException
{
ParquetReader
<
T
>
reader
=
newReader
(
filePath
,
clazz
);
Iterable
<
T
>
iterable
=
()
->
new
java
.
util
.
Iterator
<
T
>()
{
private
T
current
;
private
boolean
prepared
;
private
boolean
finished
;
private
void
prepare
()
{
if
(
prepared
||
finished
)
{
return
;
}
try
{
while
((
current
=
reader
.
read
())
!=
null
)
{
if
(
predicate
==
null
||
predicate
.
test
(
current
))
{
prepared
=
true
;
return
;
}
}
finished
=
true
;
reader
.
close
();
}
catch
(
IOException
e
)
{
finished
=
true
;
try
{
reader
.
close
();
}
catch
(
IOException
ignored
)
{
}
throw
new
RuntimeException
(
"Parquet 流式读取失败"
,
e
);
}
}
@Override
public
boolean
hasNext
()
{
prepare
();
return
prepared
;
}
@Override
public
T
next
()
{
prepare
();
if
(!
prepared
)
{
throw
new
java
.
util
.
NoSuchElementException
();
}
T
result
=
current
;
current
=
null
;
prepared
=
false
;
return
result
;
}
};
return
StreamSupport
.
stream
(
iterable
.
spliterator
(),
false
)
.
onClose
(()
->
{
try
{
reader
.
close
();
}
catch
(
IOException
ignored
)
{
}
});
}
/**
* 流式分页:只扫描到当前页 + 1 条匹配记录即可确定 hasNext。
* totalMatched 返回 -1,避免为了算总数再完整扫描一次文件。
*/
public
<
T
>
PageResult
<
T
>
streamPage
(
String
filePath
,
Class
<
T
>
clazz
,
PageRequest
pageRequest
,
Predicate
<
T
>
predicate
)
throws
IOException
{
if
(
pageRequest
==
null
)
{
throw
new
IllegalArgumentException
(
"pageRequest 不能为空"
);
}
long
offset
=
pageRequest
.
getOffset
();
int
limit
=
pageRequest
.
getPageSize
();
validateOffsetLimit
(
offset
,
limit
);
List
<
T
>
page
=
new
ArrayList
<>(
limit
);
long
matched
=
0
;
boolean
hasNext
=
false
;
try
(
ParquetReader
<
T
>
reader
=
newReader
(
filePath
,
clazz
))
{
T
row
;
while
((
row
=
reader
.
read
())
!=
null
)
{
if
(
predicate
!=
null
&&
!
predicate
.
test
(
row
))
{
continue
;
}
matched
++;
if
(
matched
<=
offset
)
{
continue
;
}
if
(
page
.
size
()
<
limit
)
{
page
.
add
(
row
);
}
else
{
hasNext
=
true
;
break
;
}
}
}
return
new
PageResult
<>(
page
,
offset
,
pageRequest
.
getPageNumber
(),
pageRequest
.
getPageSize
(),
-
1
,
hasNext
);
}
/**
* 分页流:逐页返回结果,不把全部数据装进一个 List。
* 注意:每一页都会重新打开 Parquet 文件,因此适合中小页大小的服务端分页。
*/
public
<
T
>
Stream
<
PageResult
<
T
>>
streamPages
(
String
filePath
,
Class
<
T
>
clazz
,
PageRequest
firstPage
,
List
<
FilterCondition
<?>>
filters
)
{
if
(
firstPage
==
null
)
{
throw
new
IllegalArgumentException
(
"firstPage 不能为空"
);
}
Predicate
<
T
>
predicate
=
toPredicate
(
filters
);
Iterable
<
PageResult
<
T
>>
iterable
=
()
->
new
java
.
util
.
Iterator
<
PageResult
<
T
>>()
{
private
int
pageNumber
=
firstPage
.
getPageNumber
();
private
PageResult
<
T
>
next
;
private
boolean
prepared
;
private
boolean
finished
;
private
void
prepare
()
{
if
(
prepared
||
finished
)
{
return
;
}
try
{
next
=
streamPage
(
filePath
,
clazz
,
new
PageRequest
(
pageNumber
,
firstPage
.
getPageSize
()),
predicate
);
prepared
=
true
;
if
(!
next
.
isHasNext
())
{
finished
=
true
;
}
}
catch
(
IOException
e
)
{
finished
=
true
;
throw
new
RuntimeException
(
"Parquet 分页流读取失败"
,
e
);
}
}
@Override
public
boolean
hasNext
()
{
prepare
();
return
prepared
;
}
@Override
public
PageResult
<
T
>
next
()
{
prepare
();
if
(!
prepared
)
{
throw
new
java
.
util
.
NoSuchElementException
();
}
PageResult
<
T
>
result
=
next
;
next
=
null
;
prepared
=
false
;
pageNumber
++;
return
result
;
}
};
return
StreamSupport
.
stream
(
iterable
.
spliterator
(),
false
);
}
/**
* 流式过滤 + 排序 + 分页。
* 有排序时需要把过滤后的数据收集到内存后排序;排序本身不能保持真正的逐行流式。
*/
public
<
T
>
Stream
<
T
>
streamSorted
(
String
filePath
,
Class
<
T
>
clazz
,
List
<
FilterCondition
<?>>
filters
,
List
<
SortCondition
>
sorts
)
throws
IOException
{
if
(
sorts
==
null
||
sorts
.
isEmpty
())
{
return
stream
(
filePath
,
clazz
,
toPredicate
(
filters
));
}
List
<
T
>
all
=
collect
(
filePath
,
clazz
,
toPredicate
(
filters
));
all
.
sort
(
buildComparator
(
clazz
,
sorts
));
return
all
.
stream
();
}
// =========================
// Predicate / 反射处理
// =========================
private
<
T
>
Predicate
<
T
>
toPredicate
(
List
<
FilterCondition
<?>>
filters
)
{
if
(
filters
==
null
||
filters
.
isEmpty
())
{
return
null
;
}
return
row
->
{
for
(
FilterCondition
<?>
condition
:
filters
)
{
if
(!
matches
(
row
,
condition
))
{
return
false
;
}
}
return
true
;
};
}
private
boolean
matches
(
Object
row
,
FilterCondition
<?>
condition
)
{
Object
actual
=
getPropertyValue
(
row
,
condition
.
getField
());
Operator
op
=
condition
.
getOperator
();
Object
expected
=
condition
.
getValue
();
switch
(
op
)
{
case
IS_NULL:
return
actual
==
null
;
case
NOT_NULL:
return
actual
!=
null
;
case
EQ:
return
valuesEqual
(
actual
,
expected
);
case
NE:
return
!
valuesEqual
(
actual
,
expected
);
case
GT:
return
compare
(
actual
,
expected
)
>
0
;
case
GE:
return
compare
(
actual
,
expected
)
>=
0
;
case
LT:
return
compare
(
actual
,
expected
)
<
0
;
case
LE:
return
compare
(
actual
,
expected
)
<=
0
;
case
LIKE:
return
actual
!=
null
&&
expected
!=
null
&&
normalize
(
actual
).
contains
(
normalize
(
expected
));
case
STARTS_WITH:
return
actual
!=
null
&&
expected
!=
null
&&
normalize
(
actual
).
startsWith
(
normalize
(
expected
));
case
ENDS_WITH:
return
actual
!=
null
&&
expected
!=
null
&&
normalize
(
actual
).
endsWith
(
normalize
(
expected
));
case
IN:
return
collectionContains
(
expected
,
actual
);
case
NOT_IN:
return
!
collectionContains
(
expected
,
actual
);
case
BETWEEN:
if
(!(
expected
instanceof
Collection
<?>))
{
throw
new
IllegalArgumentException
(
"BETWEEN value 必须是 2 元 Collection"
);
}
List
<?>
values
=
new
ArrayList
<>((
Collection
<?>)
expected
);
if
(
values
.
size
()
!=
2
)
{
throw
new
IllegalArgumentException
(
"BETWEEN value 必须包含最小值和最大值"
);
}
return
compare
(
actual
,
values
.
get
(
0
))
>=
0
&&
compare
(
actual
,
values
.
get
(
1
))
<=
0
;
default
:
throw
new
IllegalStateException
(
"未知 Operator: "
+
op
);
}
}
private
boolean
valuesEqual
(
Object
actual
,
Object
expected
)
{
if
(
actual
==
expected
)
{
return
true
;
}
if
(
actual
==
null
||
expected
==
null
)
{
return
false
;
}
if
(
actual
instanceof
Number
&&
expected
instanceof
Number
)
{
return
Double
.
compare
(((
Number
)
actual
).
doubleValue
(),
((
Number
)
expected
).
doubleValue
())
==
0
;
}
return
actual
.
equals
(
expected
);
}
private
boolean
collectionContains
(
Object
expected
,
Object
actual
)
{
if
(!(
expected
instanceof
Collection
<?>))
{
throw
new
IllegalArgumentException
(
"IN/NOT_IN value 必须是 Collection"
);
}
for
(
Object
value
:
(
Collection
<?>)
expected
)
{
if
(
valuesEqual
(
actual
,
value
))
{
return
true
;
}
}
return
false
;
}
@SuppressWarnings
({
"rawtypes"
,
"unchecked"
})
private
int
compare
(
Object
left
,
Object
right
)
{
if
(
left
==
null
&&
right
==
null
)
return
0
;
if
(
left
==
null
)
return
-
1
;
if
(
right
==
null
)
return
1
;
if
(
left
instanceof
Number
&&
right
instanceof
Number
)
{
return
Double
.
compare
(((
Number
)
left
).
doubleValue
(),
((
Number
)
right
).
doubleValue
());
}
if
(
left
instanceof
Comparable
)
{
try
{
return
((
Comparable
)
left
).
compareTo
(
right
);
}
catch
(
ClassCastException
ignored
)
{
// fallback to string comparison
}
}
return
normalize
(
left
).
compareTo
(
normalize
(
right
));
}
private
String
normalize
(
Object
value
)
{
return
String
.
valueOf
(
value
).
toLowerCase
(
Locale
.
ROOT
);
}
private
Object
getPropertyValue
(
Object
bean
,
String
fieldName
)
{
if
(
bean
==
null
)
{
return
null
;
}
String
[]
parts
=
fieldName
.
split
(
"\\."
);
Object
current
=
bean
;
for
(
String
part
:
parts
)
{
current
=
readOneProperty
(
current
,
part
);
if
(
current
==
null
)
{
return
null
;
}
}
return
current
;
}
private
Object
readOneProperty
(
Object
bean
,
String
name
)
{
Class
<?>
type
=
bean
.
getClass
();
String
suffix
=
Character
.
toUpperCase
(
name
.
charAt
(
0
))
+
name
.
substring
(
1
);
try
{
Method
getter
=
type
.
getMethod
(
"get"
+
suffix
);
return
getter
.
invoke
(
bean
);
}
catch
(
NoSuchMethodException
ignored
)
{
try
{
Method
getter
=
type
.
getMethod
(
"is"
+
suffix
);
return
getter
.
invoke
(
bean
);
}
catch
(
NoSuchMethodException
ignored2
)
{
return
readField
(
bean
,
type
,
name
);
}
catch
(
Exception
e
)
{
throw
new
IllegalArgumentException
(
"读取属性失败: "
+
name
,
e
);
}
}
catch
(
Exception
e
)
{
throw
new
IllegalArgumentException
(
"读取属性失败: "
+
name
,
e
);
}
}
private
Object
readField
(
Object
bean
,
Class
<?>
type
,
String
name
)
{
Class
<?>
current
=
type
;
while
(
current
!=
null
)
{
try
{
Field
field
=
current
.
getDeclaredField
(
name
);
field
.
setAccessible
(
true
);
return
field
.
get
(
bean
);
}
catch
(
NoSuchFieldException
ignored
)
{
current
=
current
.
getSuperclass
();
}
catch
(
Exception
e
)
{
throw
new
IllegalArgumentException
(
"读取字段失败: "
+
name
,
e
);
}
}
throw
new
IllegalArgumentException
(
"找不到属性/字段: "
+
name
+
",类型: "
+
type
.
getName
());
}
private
<
T
>
Comparator
<
T
>
buildComparator
(
Class
<
T
>
clazz
,
List
<
SortCondition
>
sorts
)
{
Comparator
<
T
>
result
=
null
;
for
(
SortCondition
sort
:
sorts
)
{
Comparator
<
T
>
one
=
(
a
,
b
)
->
compareForSort
(
getPropertyValue
(
a
,
sort
.
getField
()),
getPropertyValue
(
b
,
sort
.
getField
()),
sort
);
result
=
result
==
null
?
one
:
result
.
thenComparing
(
one
);
}
return
result
==
null
?
(
a
,
b
)
->
0
:
result
;
}
private
int
compareForSort
(
Object
left
,
Object
right
,
SortCondition
sort
)
{
if
(
left
==
right
)
return
0
;
if
(
left
==
null
)
return
sort
.
isNullsFirst
()
?
-
1
:
1
;
if
(
right
==
null
)
return
sort
.
isNullsFirst
()
?
1
:
-
1
;
int
result
=
compare
(
left
,
right
);
return
sort
.
getDirection
()
==
Direction
.
ASC
?
result
:
-
result
;
}
// =========================
// 内部工具
// =========================
private
<
T
>
List
<
T
>
collect
(
String
filePath
,
Class
<
T
>
clazz
,
Predicate
<
T
>
predicate
)
throws
IOException
{
List
<
T
>
result
=
new
ArrayList
<>();
try
(
Stream
<
T
>
stream
=
stream
(
filePath
,
clazz
,
predicate
))
{
stream
.
forEach
(
result:
:
add
);
}
return
result
;
}
private
<
T
>
long
count
(
String
filePath
,
Class
<
T
>
clazz
,
Predicate
<
T
>
predicate
)
throws
IOException
{
long
count
=
0
;
try
(
ParquetReader
<
T
>
reader
=
newReader
(
filePath
,
clazz
))
{
T
row
;
while
((
row
=
reader
.
read
())
!=
null
)
{
if
(
predicate
==
null
||
predicate
.
test
(
row
))
{
count
++;
}
}
}
return
count
;
}
private
<
T
>
PageResult
<
T
>
pageFromList
(
List
<
T
>
all
,
PageRequest
pageRequest
)
{
long
offset
=
pageRequest
.
getOffset
();
int
limit
=
pageRequest
.
getPageSize
();
if
(
offset
>=
all
.
size
())
{
return
new
PageResult
<>(
new
ArrayList
<>(),
offset
,
pageRequest
.
getPageNumber
(),
limit
,
all
.
size
(),
false
);
}
int
from
=
(
int
)
offset
;
int
to
=
Math
.
min
(
from
+
limit
,
all
.
size
());
List
<
T
>
page
=
new
ArrayList
<>(
all
.
subList
(
from
,
to
));
return
new
PageResult
<>(
page
,
offset
,
pageRequest
.
getPageNumber
(),
limit
,
all
.
size
(),
to
<
all
.
size
());
}
private
void
validateOffsetLimit
(
long
offset
,
int
limit
)
{
if
(
offset
<
0
)
{
throw
new
IllegalArgumentException
(
"offset 不能小于 0"
);
}
if
(
limit
<=
0
)
{
throw
new
IllegalArgumentException
(
"limit 必须大于 0"
);
}
}
}
src/main/java/com/aps/controller/MacroPlannerResultController.java
View file @
d1745f27
package
com
.
aps
.
controller
;
import
com.aps.common.util.FlatParquetUtil
;
import
com.aps.common.util.ParamValidator
;
import
com.aps.common.util.R
;
import
com.aps.entity.ApsDemandOrder
;
...
...
@@ -30,6 +31,7 @@ import org.springframework.web.bind.annotation.RestController;
import
java.util.*
;
import
java.util.stream.Collectors
;
import
java.util.stream.Stream
;
/**
* MP排产结果前端接口 — 提供排产优化运行、结果查询的完整 API。
...
...
@@ -117,42 +119,106 @@ public class MacroPlannerResultController {
+
"支持按 productId / periodIndex 可选过滤。"
)
public
R
<
Map
<
String
,
Object
>>
getSalesDemands
(
@RequestParam
(
"sceneId"
)
@Parameter
(
description
=
"场景ID"
,
required
=
true
)
String
sceneId
,
@RequestParam
(
value
=
"productId"
,
required
=
false
)
@Parameter
(
description
=
"按产品ID过滤(可选)"
)
String
productId
,
@RequestParam
(
value
=
"periodIndex"
,
required
=
false
)
@Parameter
(
description
=
"按周期索引过滤(可选)"
)
Integer
periodIndex
)
{
OptimizationResult
result
=
loadResult
(
sceneId
);
if
(
result
==
null
)
{
return
R
.
failed
(
"未找到场景 "
+
sceneId
+
" 的排产结果文件"
);
@RequestParam
(
value
=
"productCode"
,
required
=
false
)
@Parameter
(
description
=
"按产品过滤(可选)"
)
String
productCode
,
@RequestParam
(
value
=
"productCateid"
,
required
=
false
)
@Parameter
(
description
=
"产品类型"
,
required
=
false
)
String
productCateid
,
@RequestParam
(
value
=
"pageNumber"
,
required
=
false
)
@Parameter
(
description
=
"当前页数"
,
required
=
false
)
Integer
pageNumber
,
@RequestParam
(
value
=
"pageSize"
,
required
=
false
)
@Parameter
(
description
=
"每页数量"
,
required
=
false
)
Integer
pageSize
)
{
if
(
pageSize
==
null
||
pageSize
==
0
)
{
pageSize
=
20
;
}
if
(
pageNumber
==
null
||
pageNumber
==
0
)
{
pageNumber
=
1
;
}
List
<
SalesDemandResult
>
all
=
result
.
getSalesDemands
(
);
Map
<
String
,
Object
>
data
=
buildSalesDemandsSummarie
(
sceneId
,
productCode
,
productCateid
,
pageNumber
,
pageSize
);
// 可选过滤
List
<
SalesDemandResult
>
filtered
=
all
.
stream
()
.
filter
(
s
->
productId
==
null
||
productId
.
isEmpty
()
||
productId
.
equals
(
s
.
getProductId
()))
.
filter
(
s
->
periodIndex
==
null
||
periodIndex
==
s
.
getPeriodIndex
())
.
collect
(
Collectors
.
toList
());
return
R
.
ok
(
data
);
// 按 salesDemandId 合并, 对需求量/满足量/缺口/松弛加和
normalizeUnmetReasons
(
filtered
);
filtered
=
mergeBySalesDemandId
(
filtered
);
}
enrichSalesDemandDisplayFields
(
filtered
);
private
Map
<
String
,
Object
>
buildSalesDemandsSummarie
(
String
sceneId
,
String
productCode
,
String
categoryIds
,
int
pageNumber
,
int
pageSize
)
{
try
{
FlatParquetUtil
parquetUtil
=
new
FlatParquetUtil
();
ResultWriter
rw
=
new
ResultWriter
();
List
<
SalesDemandResult
>
lists
=
parquetUtil
.
readAll
(
rw
.
getOptimizationSalesDemandSummarie
(
sceneId
),
SalesDemandResult
.
class
);
Stream
<
SalesDemandResult
>
stream
=
lists
.
stream
();
// 汇总统计
double
totalDemand
=
filtered
.
stream
().
mapToDouble
(
SalesDemandResult:
:
getDemandQty
).
sum
();
double
totalFulfilled
=
filtered
.
stream
().
mapToDouble
(
SalesDemandResult:
:
getFulfilledQty
).
sum
();
double
totalUnmet
=
filtered
.
stream
().
mapToDouble
(
SalesDemandResult:
:
getUnmetQty
).
sum
();
Map
<
String
,
Object
>
data
=
new
LinkedHashMap
<>();
data
.
put
(
"totalCount"
,
filtered
.
size
());
data
.
put
(
"totalDemand"
,
totalDemand
);
data
.
put
(
"totalFulfilled"
,
totalFulfilled
);
data
.
put
(
"totalUnmet"
,
totalUnmet
);
data
.
put
(
"overallFulfillmentRate"
,
totalDemand
>
0
?
totalFulfilled
/
totalDemand
:
1.0
);
data
.
put
(
"orders"
,
filtered
);
return
R
.
ok
(
data
);
if
(
productCode
!=
null
&&
productCode
!=
""
)
{
stream
=
stream
.
filter
(
u
->
u
.
getProductCode
().
contains
(
productCode
));
}
if
(
categoryIds
!=
null
&&
categoryIds
!=
""
)
{
List
<
Integer
>
idList
=
Arrays
.
stream
(
categoryIds
.
split
(
","
))
.
map
(
Integer:
:
parseInt
)
.
collect
(
Collectors
.
toList
());
stream
=
stream
.
filter
(
u
->
idList
.
contains
(
u
.
getCategoryId
()));
}
List
<
SalesDemandResult
>
filtered
=
stream
.
collect
(
Collectors
.
toList
());
List
<
SalesDemandResult
>
SalesDemandSummaries
=
filtered
.
stream
()
.
sorted
(
Comparator
.
comparing
(
SalesDemandResult:
:
getDemandOrderDate
))
.
skip
((
long
)(
pageNumber
-
1
)*
pageSize
)
.
limit
(
pageSize
)
.
collect
(
Collectors
.
toList
());
// List<Map<String, Object>> summaries = new ArrayList<>();
// for (SalesDemandResult entry : SalesDemandSummaries) {
//
// }
double
totalDemand
=
lists
.
stream
().
mapToDouble
(
SalesDemandResult:
:
getDemandQty
).
sum
();
double
totalFulfilled
=
lists
.
stream
().
mapToDouble
(
SalesDemandResult:
:
getFulfilledQty
).
sum
();
double
totalUnmet
=
lists
.
stream
().
mapToDouble
(
SalesDemandResult:
:
getUnmetQty
).
sum
();
Map
<
String
,
Object
>
data
=
new
LinkedHashMap
<>();
data
.
put
(
"totalCount"
,
lists
.
size
());
data
.
put
(
"totalDemand"
,
totalDemand
);
data
.
put
(
"totalFulfilled"
,
totalFulfilled
);
data
.
put
(
"totalUnmet"
,
totalUnmet
);
data
.
put
(
"overallFulfillmentRate"
,
totalDemand
>
0
?
totalFulfilled
/
totalDemand
:
1.0
);
data
.
put
(
"orders"
,
SalesDemandSummaries
);
return
data
;
}
catch
(
Exception
e
)
{
return
null
;
}
}
private
List
<
SalesDemandResult
>
getSalesDemandsResult
(
String
sceneId
,
String
salesDemandId
)
{
try
{
FlatParquetUtil
parquetUtil
=
new
FlatParquetUtil
();
ResultWriter
rw
=
new
ResultWriter
();
List
<
FlatParquetUtil
.
FilterCondition
<?>>
filters
=
new
ArrayList
<>();
filters
.
add
(
FlatParquetUtil
.
FilterCondition
.
eq
(
"salesDemandId"
,
salesDemandId
));
List
<
FlatParquetUtil
.
SortCondition
>
sorts
=
Arrays
.
asList
(
FlatParquetUtil
.
SortCondition
.
asc
(
"periodIndex"
)
);
FlatParquetUtil
.
QueryRequest
request
=
new
FlatParquetUtil
.
QueryRequest
(
FlatParquetUtil
.
PageRequest
.
of
(
1
,
500
),
filters
,
sorts
);
FlatParquetUtil
.
PageResult
<
SalesDemandResult
>
result
=
parquetUtil
.
queryPage
(
rw
.
getOptimizationSalesDemand
(
sceneId
),
SalesDemandResult
.
class
,
request
);
List
<
SalesDemandResult
>
pispips
=
result
.
getRecords
();
return
pispips
;
}
catch
(
Exception
e
)
{
return
null
;
}
}
/**
* 获取产品级物料供应链视图: 各产品-库位-周期的库存流转(期初/期末/流入/流出/偏差)与按产品聚合的销售满足。
...
...
@@ -216,44 +282,124 @@ public class MacroPlannerResultController {
+
"与 supplyChain 中的 productSummaries (跨周期求和) 不同, "
+
"此接口返回每周期独立数据, 适合前端展开查看各周期详情。"
)
public
R
<
Map
<
String
,
Object
>>
getProductSummary
(
@RequestParam
(
"sceneId"
)
@Parameter
(
description
=
"场景ID"
,
required
=
true
)
String
sceneId
)
{
OptimizationResult
result
=
loadResult
(
sceneId
);
if
(
result
==
null
)
{
return
R
.
failed
(
"未找到场景 "
+
sceneId
+
" 的排产结果文件"
);
}
@RequestParam
(
"sceneId"
)
@Parameter
(
description
=
"场景ID"
,
required
=
true
)
String
sceneId
,
@RequestParam
(
value
=
"productCode"
,
required
=
false
)
@Parameter
(
description
=
"产品编号"
,
required
=
false
)
String
productCode
,
@RequestParam
(
value
=
"productCateid"
,
required
=
false
)
@Parameter
(
description
=
"产品类型"
,
required
=
false
)
String
productCateid
,
@RequestParam
(
value
=
"pageNumber"
,
required
=
false
)
@Parameter
(
description
=
"当前页数"
,
required
=
false
)
Integer
pageNumber
,
@RequestParam
(
value
=
"pageSize"
,
required
=
false
)
@Parameter
(
description
=
"每页数量"
,
required
=
false
)
Integer
pageSize
)
{
List
<
PispipResult
>
pispips
=
result
.
getPispips
();
enrichPispipProductCodes
(
pispips
);
if
(
pageSize
==
null
||
pageSize
==
0
)
{
pageSize
=
20
;
}
if
(
pageNumber
==
null
||
pageNumber
==
0
)
{
pageNumber
=
1
;
}
// 按 productId@spId 分组,保留每周期明细,不跨周期聚合
Map
<
String
,
List
<
PispipResult
>>
grouped
=
pispips
!=
null
?
pispips
.
stream
().
collect
(
Collectors
.
groupingBy
(
p
->
p
.
getProductId
()
+
"@"
+
p
.
getSpId
(),
LinkedHashMap:
:
new
,
Collectors
.
toList
()))
:
new
LinkedHashMap
<>();
Map
<
String
,
Object
>
data
=
buildPispipSummarie
(
sceneId
,
productCode
,
productCateid
,
pageNumber
,
pageSize
);
List
<
Map
<
String
,
Object
>>
summaries
=
new
ArrayList
<>();
for
(
Map
.
Entry
<
String
,
List
<
PispipResult
>>
entry
:
grouped
.
entrySet
())
{
List
<
PispipResult
>
records
=
entry
.
getValue
();
Map
<
String
,
Object
>
s
=
new
LinkedHashMap
<>();
s
.
put
(
"key"
,
entry
.
getKey
());
s
.
put
(
"productId"
,
records
.
get
(
0
).
getProductId
());
s
.
put
(
"productCode"
,
records
.
get
(
0
).
getProductCode
());
s
.
put
(
"spId"
,
records
.
get
(
0
).
getSpId
());
s
.
put
(
"spName"
,
records
.
get
(
0
).
getSpName
());
s
.
put
(
"periodCount"
,
records
.
size
());
s
.
put
(
"records"
,
records
);
// 每周期明细, 不聚合
summaries
.
add
(
s
);
}
Map
<
String
,
Object
>
data
=
new
LinkedHashMap
<>();
data
.
put
(
"totalCount"
,
summaries
.
size
());
data
.
put
(
"totalPispipRecords"
,
pispips
!=
null
?
pispips
.
size
()
:
0
);
data
.
put
(
"summaries"
,
summaries
);
return
R
.
ok
(
data
);
}
private
Map
<
String
,
Object
>
buildPispipSummarie
(
String
sceneId
,
String
productCode
,
String
categoryIds
,
int
pageNumber
,
int
pageSize
)
{
try
{
Map
<
String
,
Object
>
uc
=
new
LinkedHashMap
<>();
FlatParquetUtil
parquetUtil
=
new
FlatParquetUtil
();
ResultWriter
rw
=
new
ResultWriter
();
List
<
FlatParquetUtil
.
FilterCondition
<?>>
filters
=
new
ArrayList
<>();
if
(
productCode
!=
null
&&
productCode
!=
""
)
{
filters
.
add
(
FlatParquetUtil
.
FilterCondition
.
eq
(
"productCode"
,
productCode
));
}
if
(
categoryIds
!=
null
&&
categoryIds
!=
""
)
{
List
<
Integer
>
idList
=
Arrays
.
stream
(
categoryIds
.
split
(
","
))
.
map
(
Integer:
:
parseInt
)
.
collect
(
Collectors
.
toList
());
filters
.
add
(
FlatParquetUtil
.
FilterCondition
.
in
(
"categoryId"
,
idList
));
}
List
<
FlatParquetUtil
.
SortCondition
>
sorts
=
Arrays
.
asList
(
FlatParquetUtil
.
SortCondition
.
asc
(
"productCode"
)
);
FlatParquetUtil
.
QueryRequest
request
=
new
FlatParquetUtil
.
QueryRequest
(
FlatParquetUtil
.
PageRequest
.
of
(
pageNumber
,
pageSize
),
filters
,
sorts
);
FlatParquetUtil
.
PageResult
<
PispipResult
.
PispipSummarieResult
>
result
=
parquetUtil
.
queryPage
(
rw
.
getOptimizationPispipSummarie
(
sceneId
),
PispipResult
.
PispipSummarieResult
.
class
,
request
);
List
<
PispipResult
.
PispipSummarieResult
>
pispipSummaries
=
result
.
getRecords
();
List
<
Map
<
String
,
Object
>>
summaries
=
new
ArrayList
<>();
for
(
PispipResult
.
PispipSummarieResult
entry
:
pispipSummaries
)
{
Map
<
String
,
Object
>
s
=
new
LinkedHashMap
<>();
s
.
put
(
"key"
,
entry
.
getKey
());
s
.
put
(
"productId"
,
entry
.
getProductId
());
s
.
put
(
"productCode"
,
entry
.
getProductCode
());
s
.
put
(
"spId"
,
entry
.
getSpId
());
s
.
put
(
"spName"
,
entry
.
getSpName
());
// 周期明细 (含利用率)
List
<
PispipResult
>
records
=
getPispipResult
(
sceneId
,
entry
.
getProductId
(),
entry
.
getSpId
());
s
.
put
(
"periodCount"
,
records
.
size
());
s
.
put
(
"records"
,
records
);
// 每周期明细, 不聚合
summaries
.
add
(
s
);
}
Map
<
String
,
Object
>
data
=
new
LinkedHashMap
<>();
data
.
put
(
"totalCount"
,
result
.
getTotalMatched
());
data
.
put
(
"summaries"
,
summaries
);
return
data
;
}
catch
(
Exception
e
)
{
return
null
;
}
}
private
List
<
PispipResult
>
getPispipResult
(
String
sceneId
,
String
productId
,
String
spId
)
{
try
{
FlatParquetUtil
parquetUtil
=
new
FlatParquetUtil
();
ResultWriter
rw
=
new
ResultWriter
();
List
<
FlatParquetUtil
.
FilterCondition
<?>>
filters
=
new
ArrayList
<>();
filters
.
add
(
FlatParquetUtil
.
FilterCondition
.
eq
(
"productId"
,
productId
));
filters
.
add
(
FlatParquetUtil
.
FilterCondition
.
eq
(
"spId"
,
spId
));
List
<
FlatParquetUtil
.
SortCondition
>
sorts
=
Arrays
.
asList
(
FlatParquetUtil
.
SortCondition
.
asc
(
"periodIndex"
)
);
FlatParquetUtil
.
QueryRequest
request
=
new
FlatParquetUtil
.
QueryRequest
(
FlatParquetUtil
.
PageRequest
.
of
(
1
,
500
),
filters
,
sorts
);
FlatParquetUtil
.
PageResult
<
PispipResult
>
result
=
parquetUtil
.
queryPage
(
rw
.
getOptimizationPispip
(
sceneId
),
PispipResult
.
class
,
request
);
List
<
PispipResult
>
pispips
=
result
.
getRecords
();
return
pispips
;
}
catch
(
Exception
e
)
{
return
null
;
}
}
/**
* 获取unit产能使用情况: 按设备聚合各周期的产能占用和生产量。
*
...
...
@@ -275,12 +421,19 @@ public class MacroPlannerResultController {
description
=
"按设备(unitId)聚合各周期的产能使用量(小时)与生产任务明细。"
+
"数据来源: PeriodTaskResult(每工序-设备-周期的生产量和产能占比)。"
)
public
R
<
Map
<
String
,
Object
>>
getUnitCapacity
(
@RequestParam
(
"sceneId"
)
@Parameter
(
description
=
"场景ID"
,
required
=
true
)
String
sceneId
)
{
OptimizationResult
result
=
loadResult
(
sceneId
);
if
(
result
==
null
)
{
return
R
.
failed
(
"未找到场景 "
+
sceneId
+
" 的排产结果文件"
);
@RequestParam
(
"sceneId"
)
@Parameter
(
description
=
"场景ID"
,
required
=
true
)
String
sceneId
,
@RequestParam
(
value
=
"unitId"
,
required
=
false
)
@Parameter
(
description
=
"设备ID"
,
required
=
false
)
String
unitId
,
@RequestParam
(
value
=
"shopId"
,
required
=
false
)
@Parameter
(
description
=
"班组ID"
,
required
=
false
)
String
shopId
,
@RequestParam
(
value
=
"pageNumber"
,
required
=
false
)
@Parameter
(
description
=
"当前页数"
,
required
=
false
)
Integer
pageNumber
,
@RequestParam
(
value
=
"pageSize"
,
required
=
false
)
@Parameter
(
description
=
"每页数量"
,
required
=
false
)
Integer
pageSize
)
{
if
(
pageSize
==
null
||
pageSize
==
0
)
{
pageSize
=
20
;
}
if
(
pageNumber
==
null
||
pageNumber
==
0
)
{
pageNumber
=
1
;
}
return
R
.
ok
(
buildUnitCapacity
(
result
));
return
R
.
ok
(
buildUnitCapacity
(
sceneId
,
unitId
,
shopId
,
pageNumber
,
pageSize
));
}
/**
...
...
@@ -402,7 +555,6 @@ public class MacroPlannerResultController {
result
.
put
(
"elapsedMs"
,
System
.
currentTimeMillis
()
-
t0
);
return
R
.
failed
(
result
,
"数据验证失败, 请检查错误详情"
);
}
// 3. 加载 native libraries → 构建模型 → 分层求解
Loader
.
loadNativeLibraries
();
long
solveStart
=
System
.
currentTimeMillis
();
...
...
@@ -413,7 +565,7 @@ public class MacroPlannerResultController {
// 4. 保存结果到 JSON 文件
ResultWriter
writer
=
new
ResultWriter
(
optimizer
.
getModel
(),
optimizer
.
getData
(),
solveStart
);
OptimizationResult
optimizationResult
=
writer
.
buildResult
();
OptimizationResult
optimizationResult
=
writer
.
buildResult
(
sid
);
boolean
saved
=
writer
.
saveResultToFile
(
sid
,
optimizationResult
);
if
(
saved
)
{
mpPispipResultPersistenceService
.
save
(
sid
,
optimizationResult
);
...
...
@@ -730,32 +882,67 @@ public class MacroPlannerResultController {
*按产品聚合库存汇总
* <p>数据来源: {@link UnitCapacityResult} — 按设备聚合, 含 maxCapacity / usedCapacity / 利用率。</p>
*/
private
Map
<
String
,
Object
>
buildUnitCapacity
(
OptimizationResult
result
)
{
Map
<
String
,
Object
>
uc
=
new
LinkedHashMap
<>();
List
<
UnitCapacityResult
>
unitCapacities
=
result
.
getUnitCapacities
();
if
(
unitCapacities
==
null
||
unitCapacities
.
isEmpty
())
{
uc
.
put
(
"unitCount"
,
0
);
uc
.
put
(
"totalTaskCount"
,
0
);
uc
.
put
(
"units"
,
Collections
.
emptyList
());
return
uc
;
}
private
Map
<
String
,
Object
>
buildUnitCapacity
(
String
sceneId
,
String
unitId
,
String
shopId
,
int
pageNumber
,
int
pageSize
)
{
try
{
Map
<
String
,
Object
>
uc
=
new
LinkedHashMap
<>();
FlatParquetUtil
parquetUtil
=
new
FlatParquetUtil
();
ResultWriter
rw
=
new
ResultWriter
();
List
<
FlatParquetUtil
.
FilterCondition
<?>>
filters
=
new
ArrayList
<>();
if
(
unitId
!=
null
&&
unitId
!=
""
)
{
filters
.
add
(
FlatParquetUtil
.
FilterCondition
.
eq
(
"unitId"
,
unitId
));
}
if
(
shopId
!=
null
&&
shopId
!=
""
)
{
List
<
Integer
>
idList
=
Arrays
.
stream
(
shopId
.
split
(
","
))
.
map
(
Integer:
:
parseInt
)
.
collect
(
Collectors
.
toList
());
filters
.
add
(
FlatParquetUtil
.
FilterCondition
.
in
(
"shopId"
,
idList
));
}
List
<
Map
<
String
,
Object
>>
unitList
=
new
ArrayList
<>();
int
totalTasks
=
0
;
for
(
UnitCapacityResult
ucr
:
unitCapacities
)
{
Map
<
String
,
Object
>
ud
=
new
LinkedHashMap
<>();
ud
.
put
(
"unitId"
,
ucr
.
getUnitId
());
ud
.
put
(
"unitName"
,
ucr
.
getUnitName
());
ud
.
put
(
"operationIds"
,
ucr
.
getOperationIds
());
ud
.
put
(
"totalMaxCapacity"
,
ucr
.
getTotalMaxCapacity
());
ud
.
put
(
"totalCapacityUsed"
,
ucr
.
getTotalCapacityUsed
());
ud
.
put
(
"totalUtilizationRate"
,
ucr
.
getTotalUtilizationRate
());
ud
.
put
(
"totalProduction"
,
ucr
.
getTotalProduction
());
// 周期明细 (含利用率)
List
<
Map
<
String
,
Object
>>
pdList
=
new
ArrayList
<>();
for
(
UnitCapacityResult
.
UnitPeriodDetail
pd
:
ucr
.
getPeriodDetails
())
{
List
<
FlatParquetUtil
.
SortCondition
>
sorts
=
Arrays
.
asList
(
FlatParquetUtil
.
SortCondition
.
asc
(
"unitId"
)
);
FlatParquetUtil
.
QueryRequest
request
=
new
FlatParquetUtil
.
QueryRequest
(
FlatParquetUtil
.
PageRequest
.
of
(
pageNumber
,
pageSize
),
filters
,
sorts
);
FlatParquetUtil
.
PageResult
<
UnitCapacityResult
>
result
=
parquetUtil
.
queryPage
(
rw
.
getOptimizationUnitCapacitie
(
sceneId
),
UnitCapacityResult
.
class
,
request
);
List
<
UnitCapacityResult
>
unitCapacities
=
result
.
getRecords
();
if
(
unitCapacities
==
null
||
unitCapacities
.
isEmpty
())
{
uc
.
put
(
"unitCount"
,
0
);
uc
.
put
(
"totalTaskCount"
,
0
);
uc
.
put
(
"units"
,
Collections
.
emptyList
());
return
uc
;
}
List
<
Map
<
String
,
Object
>>
unitList
=
new
ArrayList
<>();
int
totalTasks
=
0
;
for
(
UnitCapacityResult
ucr
:
unitCapacities
)
{
Map
<
String
,
Object
>
ud
=
new
LinkedHashMap
<>();
ud
.
put
(
"unitId"
,
ucr
.
getUnitId
());
ud
.
put
(
"unitName"
,
ucr
.
getUnitName
());
ud
.
put
(
"operationIds"
,
ucr
.
getOperationIds
());
ud
.
put
(
"totalMaxCapacity"
,
ucr
.
getTotalMaxCapacity
());
ud
.
put
(
"totalCapacityUsed"
,
ucr
.
getTotalCapacityUsed
());
ud
.
put
(
"totalUtilizationRate"
,
ucr
.
getTotalUtilizationRate
());
ud
.
put
(
"totalProduction"
,
ucr
.
getTotalProduction
());
// 周期明细 (含利用率)
List
<
Map
<
String
,
Object
>>
pdList
=
new
ArrayList
<>();
List
<
UnitCapacityResult
.
UnitPeriodDetail
>
periodDetails
=
getUnitPeriodDetail
(
sceneId
,
ucr
.
getUnitId
());
for
(
UnitCapacityResult
.
UnitPeriodDetail
pd
:
periodDetails
)
{
Map
<
String
,
Object
>
pdm
=
new
LinkedHashMap
<>();
pdm
.
put
(
"periodIndex"
,
pd
.
periodIndex
);
pdm
.
put
(
"periodStartDate"
,
pd
.
periodStartDate
);
...
...
@@ -763,19 +950,53 @@ public class MacroPlannerResultController {
pdm
.
put
(
"capacityUsed"
,
pd
.
capacityUsed
);
pdm
.
put
(
"utilizationRate"
,
pd
.
utilization
);
pdm
.
put
(
"production"
,
pd
.
production
);
pdm
.
put
(
"periodTasks"
,
pd
.
tasks
);
totalTasks
+=
pd
.
tasks
.
size
();
pdList
.
add
(
pdm
);
}
ud
.
put
(
"periodDetails"
,
pdList
);
ud
.
put
(
"periodDetails"
,
pdList
);
unitList
.
add
(
ud
);
}
unitList
.
add
(
ud
);
}
uc
.
put
(
"unitCount"
,
unitCapacities
.
size
());
uc
.
put
(
"totalTaskCount"
,
totalTasks
);
uc
.
put
(
"units"
,
unitList
);
return
uc
;
uc
.
put
(
"unitCount"
,
result
.
getTotalMatched
());
// uc.put("totalTaskCount", totalTasks);
uc
.
put
(
"units"
,
unitList
);
return
uc
;
}
catch
(
Exception
e
)
{
return
null
;
}
}
private
List
<
UnitCapacityResult
.
UnitPeriodDetail
>
getUnitPeriodDetail
(
String
sceneId
,
String
unitId
)
{
try
{
FlatParquetUtil
parquetUtil
=
new
FlatParquetUtil
();
ResultWriter
rw
=
new
ResultWriter
();
List
<
FlatParquetUtil
.
FilterCondition
<?>>
filters
=
new
ArrayList
<>();
filters
.
add
(
FlatParquetUtil
.
FilterCondition
.
eq
(
"unitId"
,
unitId
));
List
<
FlatParquetUtil
.
SortCondition
>
sorts
=
Arrays
.
asList
(
FlatParquetUtil
.
SortCondition
.
asc
(
"periodIndex"
)
);
FlatParquetUtil
.
QueryRequest
request
=
new
FlatParquetUtil
.
QueryRequest
(
FlatParquetUtil
.
PageRequest
.
of
(
1
,
500
),
filters
,
sorts
);
FlatParquetUtil
.
PageResult
<
UnitCapacityResult
.
UnitPeriodDetail
>
result
=
parquetUtil
.
queryPage
(
rw
.
getOptimizationUnitPeriodDetail
(
sceneId
),
UnitCapacityResult
.
UnitPeriodDetail
.
class
,
request
);
List
<
UnitCapacityResult
.
UnitPeriodDetail
>
unitCapacities
=
result
.
getRecords
();
return
unitCapacities
;
}
catch
(
Exception
e
)
{
return
null
;
}
}
// ==================== 3. 汇总: KPI + 求解器统计 ====================
...
...
src/main/java/com/aps/entity/basic/Material.java
View file @
d1745f27
...
...
@@ -33,6 +33,8 @@ public class Material {
*/
private
double
CurrentStock
;
private
Long
categoryId
=
1
l
;
/**
* 库存详情
*/
...
...
src/main/java/com/aps/macroplanner/MacroPlannerDataConverterRunner.java
View file @
d1745f27
...
...
@@ -4,6 +4,8 @@ import com.aps.ApsApplication;
import
com.aps.macroplanner.data.DataValidator
;
import
com.aps.macroplanner.data.MacroPlannerDataConverter
;
import
com.aps.macroplanner.data.TestDataBuilder
;
import
com.aps.macroplanner.output.ResultWriter
;
import
com.aps.macroplanner.output.dto.OptimizationResult
;
import
com.google.ortools.Loader
;
import
org.springframework.boot.SpringApplication
;
import
org.springframework.context.ApplicationContext
;
...
...
@@ -77,6 +79,10 @@ public class MacroPlannerDataConverterRunner {
optimizer
.
buildModel
();
optimizer
.
solve
();
ResultWriter
writer
=
new
ResultWriter
(
optimizer
.
getModel
(),
optimizer
.
getData
(),
1
);
OptimizationResult
result
=
writer
.
buildResult
(
"Default"
);
boolean
jsonPath
=
writer
.
saveResultToFile
(
"Default"
,
result
);
System
.
out
.
println
(
"===== MACROPLANNER DATA CONVERTER RUNNER END ====="
);
}
finally
{
SpringApplication
.
exit
(
ctx
);
...
...
src/main/java/com/aps/macroplanner/MultiLevelBomTestRunner.java
View file @
d1745f27
...
...
@@ -56,38 +56,41 @@ public class MultiLevelBomTestRunner {
// 通过 Spring 容器获取 service 实例 (static main 中 @Autowired 不会生效)
ApplicationContext
ctx
=
SpringApplication
.
run
(
ApsApplication
.
class
,
args
);
try
{
// MultiLevelBomTestDataBuilder data=new MultiLevelBomTestDataBuilder();
// data.init();
//
// MacroPlannerOptimizer optimizer = new MacroPlannerOptimizer(data);
// optimizer.buildModel();
// optimizer.solve("bom");
MpPispipResultPersistenceService
service
=
ctx
.
getBean
(
MpPispipResultPersistenceService
.
class
);
int
scale
=
100
;
BenchmarkDataBuilder
data
=
BenchmarkDataBuilder
.
forScale
(
scale
);
writeLog
(
"===== TEST RUNNER START "
+
scale
+
"====="
);
long
startTime
=
System
.
currentTimeMillis
();
MultiLevelBomTestDataBuilder
data
=
new
MultiLevelBomTestDataBuilder
();
data
.
init
();
writeLog
(
"Data loaded: "
+
data
.
getProducts
().
size
()
+
" products, "
+
data
.
getOperations
().
size
()
+
" operations"
);
long
startTime
=
System
.
currentTimeMillis
();
MacroPlannerOptimizer
optimizer
=
new
MacroPlannerOptimizer
(
data
);
optimizer
.
buildModel
();
optimizer
.
solve
(
String
.
valueOf
(
scale
));
// 构建结果并调用 save 入库
optimizer
.
solve
(
"bom"
);
ResultWriter
writer
=
new
ResultWriter
(
optimizer
.
getModel
(),
optimizer
.
getData
(),
startTime
);
OptimizationResult
result
=
writer
.
buildResult
(
String
.
valueOf
(
scale
)
);
boolean
jsonPath
=
writer
.
saveResultToFile
(
String
.
valueOf
(
scale
)
,
result
);
OptimizationResult
result
=
writer
.
buildResult
(
"bom"
);
boolean
jsonPath
=
writer
.
saveResultToFile
(
"bom"
,
result
);
writeLog
(
"PISPIP result saved: "
);
int
saved
=
service
.
save
(
String
.
valueOf
(
scale
),
result
);
writeLog
(
"PISPIP result saved: "
+
saved
);
// MpPispipResultPersistenceService service =
// ctx.getBean(MpPispipResultPersistenceService.class);
//
// int scale = 2000;
// BenchmarkDataBuilder data = BenchmarkDataBuilder.forScale(scale);
// writeLog("===== TEST RUNNER START " + scale + "=====");
//
// data.init();
// writeLog("Data loaded: " + data.getProducts().size() + " products, "
// + data.getOperations().size() + " operations");
//
// long startTime = System.currentTimeMillis();
// MacroPlannerOptimizer optimizer = new MacroPlannerOptimizer(data);
// optimizer.buildModel();
// optimizer.solve(String.valueOf(scale));
//
// // 构建结果并调用 save 入库
// ResultWriter writer = new ResultWriter(optimizer.getModel(), optimizer.getData(), startTime);
// OptimizationResult result = writer.buildResult(String.valueOf(scale));
// boolean jsonPath = writer.saveResultToFile(String.valueOf(scale), result);
//
// writeLog("PISPIP result saved: ");
// int saved = service.save(String.valueOf(scale), result);
// writeLog("PISPIP result saved: " + saved);
writeLog
(
"===== TEST RUNNER END ====="
);
}
catch
(
Exception
e
)
{
...
...
src/main/java/com/aps/macroplanner/data/MacroPlannerDataConverter.java
View file @
d1745f27
...
...
@@ -24,6 +24,7 @@ import com.aps.mapper.StockMapper;
import
com.aps.service.ApsTimeConfigService
;
import
com.aps.service.LanuchService
;
import
com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper
;
import
io.swagger.v3.oas.models.security.SecurityScheme
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.stereotype.Service
;
...
...
@@ -47,6 +48,7 @@ import java.util.Map;
import
java.util.Objects
;
import
java.util.Optional
;
import
java.util.Set
;
import
java.util.function.Function
;
import
java.util.stream.Collectors
;
/**
...
...
@@ -429,7 +431,7 @@ public class MacroPlannerDataConverter {
material
.
setName
(
m
.
getName
());
material
.
setMaxProduction
(
m
.
getMaxProduction
());
material
.
setMinProduction
(
m
.
getMinProduction
());
material
.
setCategoryId
(
m
.
getCategoryId
());
// 库存
List
<
Stock
>
materialStocks
=
stocksByMaterialId
.
getOrDefault
(
m
.
getId
(),
Collections
.
emptyList
());
if
(!
materialStocks
.
isEmpty
())
{
...
...
@@ -700,6 +702,7 @@ public class MacroPlannerDataConverter {
for
(
Material
m
:
ctx
.
materialByMaterialId
.
values
())
{
String
name
=
pickName
(
m
.
getName
(),
m
.
getCode
(),
m
.
getId
());
Product
p
=
new
Product
(
m
.
getCode
(),
name
,
m
.
getCode
());
p
.
setCategoryId
(
m
.
getCategoryId
());
products
.
add
(
p
);
ctx
.
productByMaterialId
.
put
(
m
.
getId
(),
p
);
}
...
...
@@ -842,6 +845,10 @@ public class MacroPlannerDataConverter {
int
hid
=
rd
.
getRoutingHeaderId
().
intValue
();
detailsByHeader
.
computeIfAbsent
(
hid
,
k
->
new
ArrayList
<>()).
add
(
rd
);
}
Map
<
Integer
,
PlanResource
>
prMap
=
ctx
.
planResources
.
stream
()
.
collect
(
Collectors
.
toMap
(
PlanResource:
:
getId
,
Function
.
identity
()));
Map
<
Integer
,
Equipinfo
>
equipMap
=
ctx
.
equipinfos
.
stream
()
.
collect
(
Collectors
.
toMap
(
Equipinfo:
:
getId
,
Function
.
identity
()));
detailsByHeader
.
values
().
forEach
(
list
->
list
.
sort
(
Comparator
.
comparing
(
rd
->
rd
.
getTaskSeq
()
==
null
?
Long
.
MAX_VALUE
:
rd
.
getTaskSeq
())));
...
...
@@ -887,13 +894,23 @@ public class MacroPlannerDataConverter {
unitOps
=
equips
.
stream
()
.
filter
(
e
->
e
.
getEquipId
()
!=
null
)
.
map
(
e
->
{
PlanResource
res
=
prMap
.
get
(
e
.
getEquipId
().
intValue
());
Integer
shopid
=
0
;
if
(
res
!=
null
)
{
Equipinfo
equip
=
equipMap
.
get
(
res
.
getReferenceId
());
if
(
equip
!=
null
)
{
shopid
=
equip
.
getShopId
();
}
}
String
unitId
=
"EQUIP_"
+
e
.
getEquipId
();
String
unitName
=
e
.
getName
();
double
capCoeff
=
(
e
.
getDuration
()
!=
null
)
?
e
.
getDuration
().
doubleValue
()/
3600
/
e
.
getOutputQuantity
().
doubleValue
()
:
1.0
;
boolean
lotSize
=
e
.
getOneBatchQuantity
()
!=
null
&&
e
.
getOneBatchQuantity
().
compareTo
(
BigDecimal
.
ZERO
)
>
0
;
double
lotSizeVal
=
lotSize
?
e
.
getOneBatchQuantity
().
doubleValue
()
:
0.0
;
return
new
UnitOperation
(
unitId
,
unitName
,
capCoeff
,
lotSize
,
lotSizeVal
,
1.0
);
return
new
UnitOperation
(
unitId
,
unitName
,
capCoeff
,
lotSize
,
lotSizeVal
,
1.0
,
shopid
);
})
.
collect
(
Collectors
.
toList
());
}
else
{
...
...
@@ -902,7 +919,7 @@ public class MacroPlannerDataConverter {
String
unitName
=
(
rd
.
getEquipTypeId
()
!=
null
?
rd
.
getEquipType
()
:
"DEFAULT"
);
double
capCoeff
=
(
rd
.
getRuntime
()
!=
null
)
?
rd
.
getRuntime
().
doubleValue
()
:
1.0
;
unitOps
=
Arrays
.
asList
(
new
UnitOperation
(
unitId
,
unitName
,
capCoeff
,
false
,
0.0
,
1.0
));
unitOps
=
Arrays
.
asList
(
new
UnitOperation
(
unitId
,
unitName
,
capCoeff
,
false
,
0.0
,
1.0
,
0
));
}
Operation
op
=
new
Operation
(
...
...
@@ -1014,7 +1031,7 @@ public class MacroPlannerDataConverter {
"采购-"
+
pickName
(
m
.
getName
(),
m
.
getId
())
+
(
mp
.
getSupplyName
()
!=
null
?
"-"
+
mp
.
getSupplyName
()
:
""
),
Collections
.
singletonList
(
new
UnitOperation
(
unitId
,
unitName
,
0.5
,
false
,
0.0
,
1.0
)),
new
UnitOperation
(
unitId
,
unitName
,
0.5
,
false
,
0.0
,
1.0
,
0
)),
new
OperationOutput
(
p
,
sp
),
1.0
,
leadTimeDays
);
operations
.
add
(
procureOp
);
...
...
@@ -1029,7 +1046,7 @@ public class MacroPlannerDataConverter {
"OP_PROCURE_"
+
m
.
getId
(),
"采购-"
+
pickName
(
m
.
getName
(),
m
.
getId
()),
Collections
.
singletonList
(
new
UnitOperation
(
unitId
,
"通用供应商"
,
0.5
,
false
,
0.0
,
1.0
)),
new
UnitOperation
(
unitId
,
"通用供应商"
,
0.5
,
false
,
0.0
,
1.0
,
0
)),
new
OperationOutput
(
p
,
sp
),
1.0
,
0
);
operations
.
add
(
procureOp
);
...
...
@@ -1143,9 +1160,12 @@ public class MacroPlannerDataConverter {
// 收集所有 unitId (遍历每个 Operation 的每个 UnitOperation)
Map
<
String
,
String
>
unitIds
=
new
HashMap
<>()
;
Map
<
String
,
Integer
>
unitShipIds
=
new
HashMap
<>()
;
for
(
Operation
op
:
operations
)
{
for
(
UnitOperation
uo
:
op
.
getUnitOperations
())
{
unitIds
.
put
(
uo
.
getUnitId
(),
uo
.
getUnitName
());
unitShipIds
.
put
(
uo
.
getUnitId
(),
uo
.
getShopId
());
}
}
...
...
@@ -1187,7 +1207,9 @@ public class MacroPlannerDataConverter {
int
workDays
=
calculateWorkDays
(
periodStart
,
periodEnd
);
periodMaxCapacity
=
DEFAULT_DAILY_HOURS
*
Math
.
max
(
1
,
workDays
);
}
unitPeriods
.
add
(
new
UnitPeriod
(
unitId
,
unitName
,
p
,
0.0
,
periodMaxCapacity
,
false
));
Integer
shopid
=
unitShipIds
.
get
(
unitId
);
unitPeriods
.
add
(
new
UnitPeriod
(
unitId
,
unitName
,
p
,
0.0
,
periodMaxCapacity
,
false
,
shopid
));
}
}
log
.
info
(
"构建 Period: {} (dimension={}, horizonEnd={}), UnitPeriod: {}"
,
...
...
@@ -1288,7 +1310,7 @@ public class MacroPlannerDataConverter {
}
else
{
// 无时间信息, 放入最后一个周期
salesDemands
.
add
(
new
SalesDemand
(
p
,
sp
,
lastPeriod
,
qty
,
priority
,
ado
.
getId
(),
ado
.
getCode
(),
pickName
(
ado
.
getMmcode
(),
p
.
getId
()),
ado
.
getEndtime
()==
null
?
ado
.
getDeliverytime
().
toLocalDate
():
ado
.
getEndtime
().
toLocalDate
()));
ado
.
getId
(),
ado
.
getCode
(),
pickName
(
ado
.
getMmcode
(),
p
.
getId
()),
ado
.
getEndtime
()==
null
?
ado
.
getDeliverytime
().
toLocalDate
():
ado
.
getEndtime
().
toLocalDate
()
,
p
.
getCategoryId
()
));
continue
;
}
...
...
@@ -1310,7 +1332,7 @@ public class MacroPlannerDataConverter {
if
(
overlapPeriods
.
isEmpty
())
{
salesDemands
.
add
(
new
SalesDemand
(
p
,
sp
,
lastPeriod
,
qty
,
priority
,
ado
.
getId
(),
ado
.
getCode
(),
pickName
(
ado
.
getMmcode
(),
p
.
getId
()),
ado
.
getEndtime
()==
null
?
ado
.
getDeliverytime
().
toLocalDate
():
ado
.
getEndtime
().
toLocalDate
()));
ado
.
getId
(),
ado
.
getCode
(),
pickName
(
ado
.
getMmcode
(),
p
.
getId
()),
ado
.
getEndtime
()==
null
?
ado
.
getDeliverytime
().
toLocalDate
():
ado
.
getEndtime
().
toLocalDate
()
,
p
.
getCategoryId
()
));
}
else
{
// 按重叠天数比例拆分需求量: 每期四舍五入取整, 最后一期补差保证总和等于总数
int
totalQty
=
(
int
)
Math
.
round
(
qty
);
...
...
@@ -1327,7 +1349,7 @@ public class MacroPlannerDataConverter {
allocated
+=
periodQty
;
}
salesDemands
.
add
(
new
SalesDemand
(
p
,
sp
,
per
,
periodQty
,
priority
,
ado
.
getId
(),
ado
.
getCode
(),
pickName
(
ado
.
getMmcode
(),
p
.
getId
()),
ado
.
getEndtime
()==
null
?
ado
.
getDeliverytime
().
toLocalDate
():
ado
.
getEndtime
().
toLocalDate
()));
ado
.
getId
(),
ado
.
getCode
(),
pickName
(
ado
.
getMmcode
(),
p
.
getId
()),
ado
.
getEndtime
()==
null
?
ado
.
getDeliverytime
().
toLocalDate
():
ado
.
getEndtime
().
toLocalDate
()
,
p
.
getCategoryId
()
));
}
}
}
...
...
src/main/java/com/aps/macroplanner/data/MultiLevelBomTestDataBuilder.java
View file @
d1745f27
...
...
@@ -47,10 +47,14 @@ public class MultiLevelBomTestDataBuilder extends TestDataBuilder {
// === 产品: 成品 P1, P2 + 半成品 S1 + 原材料 R1, R2 ===
Product
prodP1
=
new
Product
(
"P1"
,
"成品P1"
);
prodP1
.
setCategoryId
(
1
l
);
// Product prodP2 = new Product("P2", "成品P2");
Product
prodS1
=
new
Product
(
"S1"
,
"半成品S1"
);
prodS1
.
setCategoryId
(
12
l
);
Product
prodR1
=
new
Product
(
"R1"
,
"原材料R1"
);
prodR1
.
setCategoryId
(
3
l
);
Product
prodR2
=
new
Product
(
"R2"
,
"原材料R2"
);
prodR2
.
setCategoryId
(
4
l
);
products
.
addAll
(
Arrays
.
asList
(
prodP1
,
prodS1
,
prodR1
,
prodR2
));
// === 库存点: 每种产品一个库存点 ===
...
...
@@ -117,7 +121,7 @@ public class MultiLevelBomTestDataBuilder extends TestDataBuilder {
// === 初始库存: 全部从 0 开始 ===
initialInventories
.
add
(
new
InitialInventory
(
prodP1
,
spP1
,
0.0
));
// initialInventories.add(new InitialInventory(prodP2, spP2, 0.0));
initialInventories
.
add
(
new
InitialInventory
(
prodS1
,
spSemi
,
0.0
));
initialInventories
.
add
(
new
InitialInventory
(
prodS1
,
spSemi
,
4
0.0
));
initialInventories
.
add
(
new
InitialInventory
(
prodR1
,
spR1
,
0.0
));
initialInventories
.
add
(
new
InitialInventory
(
prodR2
,
spR2
,
0.0
));
...
...
src/main/java/com/aps/macroplanner/data/Operation.java
View file @
d1745f27
...
...
@@ -69,7 +69,7 @@ public class Operation {
boolean
hasLotSize
,
double
lotSize
,
double
qtpfactor
,
int
leadTimeDays
)
{
this
(
id
,
name
,
Arrays
.
asList
(
new
UnitOperation
(
unitId
,
""
,
capacityCoeff
,
hasLotSize
,
lotSize
,
qtpfactor
)),
Arrays
.
asList
(
new
UnitOperation
(
unitId
,
""
,
capacityCoeff
,
hasLotSize
,
lotSize
,
qtpfactor
,
0
)),
output
,
relativeDuration
,
leadTimeDays
);
}
...
...
src/main/java/com/aps/macroplanner/data/OperationOutput.java
View file @
d1745f27
...
...
@@ -18,9 +18,16 @@ public class OperationOutput {
private
final
Product
product
;
// 产出产品
private
final
StockingPoint
stockingPoint
;
// 产出到哪个库存点
public
final
double
factor
;
public
OperationOutput
(
Product
product
,
StockingPoint
stockingPoint
)
{
this
(
product
,
stockingPoint
,
1
);
}
public
OperationOutput
(
Product
product
,
StockingPoint
stockingPoint
,
double
factor
)
{
this
.
product
=
product
;
this
.
stockingPoint
=
stockingPoint
;
this
.
factor
=
factor
;
}
public
Product
getProduct
()
{
return
product
;
}
...
...
@@ -28,9 +35,14 @@ public class OperationOutput {
/** 便捷方法: 产品 ID */
public
String
getProductId
()
{
return
product
.
getId
();
}
public
String
getProductCode
()
{
return
product
.
getCode
();
}
/** 便捷方法: 库存点 ID */
public
String
getSpId
()
{
return
stockingPoint
.
getId
();
}
public
String
getSpName
()
{
return
stockingPoint
.
getName
();
}
@Override
public
String
toString
()
{
return
product
.
getId
()
+
"@"
+
stockingPoint
.
getId
();
...
...
src/main/java/com/aps/macroplanner/data/Product.java
View file @
d1745f27
...
...
@@ -8,6 +8,8 @@ public class Product {
private
final
String
name
;
private
final
String
code
;
private
Long
categoryId
;
public
Product
(
String
id
,
String
name
)
{
this
(
id
,
name
,
id
);
}
...
...
@@ -22,6 +24,10 @@ public class Product {
public
String
getName
()
{
return
name
;
}
public
String
getCode
()
{
return
code
;
}
public
Long
getCategoryId
()
{
return
categoryId
;
}
public
void
setCategoryId
(
Long
val
)
{
categoryId
=
val
;
}
@Override
public
String
toString
()
{
return
name
+
"1("
+
id
+
")"
;
...
...
src/main/java/com/aps/macroplanner/data/SalesDemand.java
View file @
d1745f27
...
...
@@ -18,6 +18,8 @@ public class SalesDemand {
private
final
LocalDate
demandOrderDate
;
private
final
Long
categoryId
;
public
SalesDemand
(
Product
product
,
StockingPoint
stockingPoint
,
Period
period
,
double
quantity
,
double
priority
)
{
this
(
product
,
stockingPoint
,
period
,
quantity
,
priority
,
null
,
null
);
...
...
@@ -25,12 +27,13 @@ public class SalesDemand {
public
SalesDemand
(
Product
product
,
StockingPoint
stockingPoint
,
Period
period
,
double
quantity
,
double
priority
,
String
demandOrderId
,
LocalDate
demandOrderDate
)
{
this
(
product
,
stockingPoint
,
period
,
quantity
,
priority
,
demandOrderId
,
null
,
null
,
demandOrderDate
);
this
(
product
,
stockingPoint
,
period
,
quantity
,
priority
,
demandOrderId
,
null
,
null
,
demandOrderDate
,
0
l
);
}
public
SalesDemand
(
Product
product
,
StockingPoint
stockingPoint
,
Period
period
,
double
quantity
,
double
priority
,
String
demandOrderId
,
String
orderCode
,
String
productCode
,
LocalDate
demandOrderDate
)
{
String
orderCode
,
String
productCode
,
LocalDate
demandOrderDate
,
Long
categoryId
)
{
this
.
product
=
product
;
this
.
stockingPoint
=
stockingPoint
;
this
.
period
=
period
;
...
...
@@ -40,6 +43,7 @@ public class SalesDemand {
this
.
orderCode
=
orderCode
;
this
.
productCode
=
productCode
;
this
.
demandOrderDate
=
demandOrderDate
;
this
.
categoryId
=
categoryId
;
}
public
Product
getProduct
()
{
return
product
;
}
...
...
@@ -50,7 +54,7 @@ public class SalesDemand {
public
String
getDemandOrderId
()
{
return
demandOrderId
;
}
public
String
getOrderCode
()
{
return
orderCode
;
}
public
String
getProductCode
()
{
return
productCode
;
}
public
Long
getCategoryId
()
{
return
categoryId
;
}
public
LocalDate
getDemandOrderDate
()
{
return
demandOrderDate
;
}
public
String
getKey
()
{
...
...
src/main/java/com/aps/macroplanner/data/UnitOperation.java
View file @
d1745f27
...
...
@@ -17,14 +17,21 @@ public class UnitOperation {
private
final
double
lotSize
;
// 批次大小
private
final
double
qtpfactor
;
// QuantityToProcessFactor
private
final
Integer
shopId
;
public
UnitOperation
(
String
unitId
,
String
unitName
,
double
capacityCoeff
,
boolean
hasLotSize
,
double
lotSize
,
double
qtpfactor
)
{
this
(
unitId
,
unitName
,
capacityCoeff
,
hasLotSize
,
lotSize
,
qtpfactor
,
0
);
}
public
UnitOperation
(
String
unitId
,
String
unitName
,
double
capacityCoeff
,
boolean
hasLotSize
,
double
lotSize
,
double
qtpfactor
,
Integer
shopId
)
{
this
.
unitId
=
unitId
;
this
.
unitName
=
unitName
;
this
.
capacityCoeff
=
capacityCoeff
;
this
.
hasLotSize
=
hasLotSize
;
this
.
lotSize
=
lotSize
;
this
.
qtpfactor
=
qtpfactor
;
this
.
shopId
=
shopId
;
}
public
String
getUnitId
()
{
return
unitId
;
}
...
...
@@ -35,6 +42,8 @@ public class UnitOperation {
public
double
getLotSize
()
{
return
lotSize
;
}
public
double
getQtpfactor
()
{
return
qtpfactor
;
}
public
Integer
getShopId
()
{
return
shopId
;
}
@Override
public
String
toString
()
{
return
unitId
+
"(产能"
+
capacityCoeff
+
"h/件"
+
...
...
src/main/java/com/aps/macroplanner/data/UnitPeriod.java
View file @
d1745f27
...
...
@@ -15,9 +15,11 @@ public class UnitPeriod {
private
final
boolean
hasMinCapacity
;
// 是否有最小产能约束
private
final
boolean
unlimited
;
// 产能是否无上限 (如原材料供应商)
private
final
Integer
shopId
;
/** 完整构造器 */
public
UnitPeriod
(
String
unitId
,
String
unitName
,
Period
period
,
double
minCapacity
,
double
maxCapacity
,
boolean
hasMinCapacity
,
boolean
unlimited
)
{
boolean
hasMinCapacity
,
boolean
unlimited
,
Integer
shopId
)
{
this
.
unitId
=
unitId
;
this
.
unitName
=
unitName
;
this
.
period
=
period
;
...
...
@@ -25,16 +27,19 @@ public class UnitPeriod {
this
.
maxCapacity
=
unlimited
?
Double
.
MAX_VALUE
:
maxCapacity
;
this
.
hasMinCapacity
=
hasMinCapacity
;
this
.
unlimited
=
unlimited
;
this
.
shopId
=
shopId
;
}
/** 有限产能构造器 (向后兼容) */
public
UnitPeriod
(
String
unitId
,
String
unitName
,
Period
period
,
double
minCapacity
,
double
maxCapacity
,
boolean
hasMinCapacity
)
{
this
(
unitId
,
unitName
,
period
,
minCapacity
,
maxCapacity
,
hasMinCapacity
,
false
);
this
(
unitId
,
unitName
,
period
,
minCapacity
,
maxCapacity
,
hasMinCapacity
,
false
,
0
);
}
/** 有限产能构造器 (向后兼容) */
public
UnitPeriod
(
String
unitId
,
String
unitName
,
Period
period
,
double
minCapacity
,
double
maxCapacity
,
boolean
hasMinCapacity
,
Integer
shopId
)
{
this
(
unitId
,
unitName
,
period
,
minCapacity
,
maxCapacity
,
hasMinCapacity
,
false
,
shopId
);
}
/** 无限产能便捷构造器 */
public
static
UnitPeriod
unlimited
(
String
unitId
,
String
unitName
,
Period
period
)
{
return
new
UnitPeriod
(
unitId
,
unitName
,
period
,
0
,
0
,
false
,
true
);
return
new
UnitPeriod
(
unitId
,
unitName
,
period
,
0
,
0
,
false
,
true
,
0
);
}
public
String
getUnitId
()
{
return
unitId
;
}
...
...
@@ -47,6 +52,8 @@ public class UnitPeriod {
public
boolean
hasMinCapacity
()
{
return
hasMinCapacity
;
}
public
boolean
isUnlimited
()
{
return
unlimited
;
}
public
Integer
getShopId
()
{
return
shopId
;
}
public
String
getKey
()
{
return
unitId
+
"_"
+
period
.
getIndex
();
}
...
...
src/main/java/com/aps/macroplanner/output/ResultWriter.java
View file @
d1745f27
...
...
@@ -26,6 +26,7 @@ import java.nio.file.Paths;
import
java.time.LocalDateTime
;
import
java.time.format.DateTimeFormatter
;
import
java.util.*
;
import
java.util.stream.Collectors
;
import
java.util.zip.GZIPInputStream
;
import
java.util.zip.GZIPOutputStream
;
...
...
@@ -75,7 +76,11 @@ public class ResultWriter {
private
String
getFileExtension
()
{
return
useCompression
?
".json.gz"
:
".json"
;
}
public
ResultWriter
()
{
this
.
model
=
null
;
this
.
data
=
null
;
this
.
startTimeMs
=
0
;
}
public
ResultWriter
(
MacroPlannerModel
model
,
TestDataBuilder
data
,
long
startTimeMs
)
{
this
.
model
=
model
;
this
.
data
=
data
;
...
...
@@ -104,37 +109,59 @@ public class ResultWriter {
}
return
resultDir
;
}
p
rivate
File
getOptimizationFile
(
String
sceneId
)
{
p
ublic
File
getOptimizationFile
(
String
sceneId
)
{
File
resultDir
=
getResultDirectory
();
String
fileName
=
"optimization_result_"
+
sceneId
+
getFileExtension
();
return
new
File
(
resultDir
,
fileName
);
}
private
String
getOptimizationPeriodTask
(
String
sceneId
)
{
public
String
getOptimizationPeriodTask
(
String
sceneId
)
{
File
resultDir
=
getResultDirectory
(
sceneId
);
String
fileName
=
resultDir
.
getAbsolutePath
()+
"\\period_tasks.parquet"
;
return
fileName
;
}
private
String
getOptimizationPispip
(
String
sceneId
)
{
public
String
getOptimizationPeriodTaskOutput
(
String
sceneId
)
{
File
resultDir
=
getResultDirectory
(
sceneId
);
String
fileName
=
resultDir
.
getAbsolutePath
()+
"\\period_task_outputs.parquet"
;
return
fileName
;
}
public
String
getOptimizationPispip
(
String
sceneId
)
{
File
resultDir
=
getResultDirectory
(
sceneId
);
String
fileName
=
resultDir
.
getAbsolutePath
()+
"\\pispips.parquet"
;
return
fileName
;
}
private
String
getOptimizationSalesDemand
(
String
sceneId
)
{
public
String
getOptimizationPispipSummarie
(
String
sceneId
)
{
File
resultDir
=
getResultDirectory
(
sceneId
);
String
fileName
=
resultDir
.
getAbsolutePath
()+
"\\pispipsummaries.parquet"
;
return
fileName
;
}
public
String
getOptimizationSalesDemand
(
String
sceneId
)
{
File
resultDir
=
getResultDirectory
(
sceneId
);
String
fileName
=
resultDir
.
getAbsolutePath
()+
"\\salesdemand.parquet"
;
return
fileName
;
}
private
String
getOptimizationUnitCapacitie
(
String
sceneId
)
{
public
String
getOptimizationSalesDemandSummarie
(
String
sceneId
)
{
File
resultDir
=
getResultDirectory
(
sceneId
);
String
fileName
=
resultDir
.
getAbsolutePath
()+
"\\salesdemandsummarie.parquet"
;
return
fileName
;
}
public
String
getOptimizationUnitCapacitie
(
String
sceneId
)
{
File
resultDir
=
getResultDirectory
(
sceneId
);
String
fileName
=
resultDir
.
getAbsolutePath
()+
"\\unitcapacitie.parquet"
;
return
fileName
;
}
private
String
getOptimizationBomStructure
(
String
sceneId
)
{
public
String
getOptimizationUnitPeriodDetail
(
String
sceneId
)
{
File
resultDir
=
getResultDirectory
(
sceneId
);
String
fileName
=
resultDir
.
getAbsolutePath
()+
"\\unitperioddetail.parquet"
;
return
fileName
;
}
public
String
getOptimizationBomStructure
(
String
sceneId
)
{
File
resultDir
=
getResultDirectory
(
sceneId
);
return
resultDir
.
getAbsolutePath
()
+
"\\bom_structure.json"
;
}
p
rivate
String
getOptimizationOperationDemand
(
String
sceneId
)
{
p
ublic
String
getOptimizationOperationDemand
(
String
sceneId
)
{
File
resultDir
=
getResultDirectory
(
sceneId
);
return
resultDir
.
getAbsolutePath
()
+
"\\operation_demands.parquet"
;
}
...
...
@@ -167,15 +194,27 @@ public class ResultWriter {
writeLog
(
"writePeriodTasks"
+
result
.
getPeriodTasks
().
size
());
parquetUtil
.
write
(
result
.
getPeriodTasks
(),
periodTaskPath
,
PeriodTaskResult
.
class
);
writeLog
(
"writePeriodTasks"
);
writeLog
(
"writePeriodOutputTasks"
+
result
.
getPeriodTaskOutputs
().
size
());
String
periodTaskOutputPath
=
getOptimizationPeriodTaskOutput
(
sceneId
);
parquetUtil
.
write
(
result
.
getPeriodTaskOutputs
(),
periodTaskOutputPath
,
PeriodTaskResult
.
OutputInfo
.
class
);
writeLog
(
"writePeriodOutputTasks"
);
writeLog
(
"writePispips:"
+
result
.
getPispips
().
size
());
String
pispiPath
=
getOptimizationPispip
(
sceneId
);
parquetUtil
.
write
(
result
.
getPispips
(),
pispiPath
,
PispipResult
.
class
);
String
pispiSummariePath
=
getOptimizationPispipSummarie
(
sceneId
);
parquetUtil
.
write
(
result
.
getPispipSummaries
(),
pispiSummariePath
,
PispipResult
.
PispipSummarieResult
.
class
);
writeLog
(
"writePispips"
);
writeLog
(
"writesalesDemand"
);
String
salesDemandPath
=
getOptimizationSalesDemand
(
sceneId
);
parquetUtil
.
write
(
result
.
getSalesDemands
(),
salesDemandPath
,
SalesDemandResult
.
class
);
// List<PispipResult> pispips = parquetUtil.readAll(pispiPath, PispipResult.class);
String
salesDemandSummariePath
=
getOptimizationSalesDemandSummarie
(
sceneId
);
parquetUtil
.
write
(
result
.
getSaleSummarieDemands
(),
salesDemandSummariePath
,
SalesDemandResult
.
class
);
writeLog
(
"writesalesDemand"
);
writeLog
(
"UnitCapacities"
);
...
...
@@ -183,6 +222,11 @@ public class ResultWriter {
parquetUtil
.
write
(
result
.
getUnitCapacities
(),
unitPath
,
UnitCapacityResult
.
class
);
writeLog
(
"UnitCapacities"
);
writeLog
(
"UnitPeriod"
);
String
unitPeriodPath
=
getOptimizationUnitPeriodDetail
(
sceneId
);
parquetUtil
.
write
(
result
.
getUnitPeriodDetails
(),
unitPeriodPath
,
UnitCapacityResult
.
UnitPeriodDetail
.
class
);
writeLog
(
"UnitPeriod"
);
// BOM 结构持久化 (供 getProductNetwork 按需组装)
writeLog
(
"writeBomStructure"
);
writeBomStructure
(
sceneId
);
...
...
@@ -480,9 +524,12 @@ public class ResultWriter {
writeLog
(
"Pispips"
);
buildPispips
(
result
);
writeLog
(
"UnitCapacitie"
);
result
.
setUnitCapacities
(
buildUnitCapacities
());
// result.setProductNetwork(buildProductNetwork());
// result.setDemandSummary(buildDemandSummary());
Map
<
String
,
Object
>
Unit
=
buildUnitCapacities
();
// relst.put("UnitCapacityDetails",unitPeriodDetails);
result
.
setUnitCapacities
((
List
<
UnitCapacityResult
>)
Unit
.
get
(
"UnitCapacity"
));
result
.
setUnitPeriodDetails
((
List
<
UnitCapacityResult
.
UnitPeriodDetail
>)
Unit
.
get
(
"UnitCapacityDetails"
));
result
.
setDemandSummary
(
buildDemandSummary
());
// KPI + 统计
writeLog
(
"Kpi"
);
...
...
@@ -508,6 +555,7 @@ public class ResultWriter {
pt
.
setOperationId
(
op
.
getId
());
pt
.
setOperationName
(
op
.
getName
());
pt
.
setUnitId
(
uo
.
getUnitId
());
pt
.
setUnitName
(
uo
.
getUnitName
());
pt
.
setPeriodIndex
(
p
.
getIndex
());
pt
.
setPeriodStartDate
(
p
.
getStartDate
().
toString
());
pt
.
setProductionQty
(
ptQty
);
...
...
@@ -520,13 +568,19 @@ 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
.
setDataKey
(
key
);
oi
.
productId
=
oo
.
getProductId
();
oi
.
productCode
=
oo
.
getProductCode
();
oi
.
spId
=
oo
.
getSpId
();
oi
.
spName
=
oo
.
getSpName
();
oi
.
setPeriodIndex
(
p
.
getIndex
());
oi
.
setPeriodStartDate
(
p
.
getStartDate
().
toString
());
oi
.
factor
=
oo
.
factor
;
// 默认 1:1 产出
oi
.
setProductionQty
(
ptQty
*
oo
.
factor
);
result
.
getPeriodTaskOutputs
().
add
(
oi
);
}
result
.
getPeriodTasks
().
add
(
pt
);
}
...
...
@@ -550,21 +604,29 @@ public class ResultWriter {
/**
* 统计各 Unit 的产能使用情况: 每周期产能占用量、利用率、超载/未满足量。
*/
private
java
.
util
.
List
<
UnitCapacityResult
>
buildUnitCapacities
()
{
private
Map
<
String
,
Object
>
buildUnitCapacities
()
{
java
.
util
.
Map
<
String
,
UnitCapacityResult
>
unitMap
=
new
java
.
util
.
LinkedHashMap
<>();
List
<
UnitCapacityResult
.
UnitPeriodDetail
>
unitPeriodDetails
=
new
ArrayList
<>();
for
(
UnitPeriod
up
:
data
.
getUnitPeriods
())
{
if
(
up
.
isUnlimited
())
{
continue
;
}
String
uid
=
up
.
getUnitId
();
String
name
=
up
.
getUnitName
();
Integer
shopid
=
up
.
getShopId
();
UnitCapacityResult
ucr
=
unitMap
.
computeIfAbsent
(
uid
,
k
->
{
UnitCapacityResult
r
=
new
UnitCapacityResult
();
r
.
setUnitId
(
k
);
r
.
setUnitName
(
name
);
r
.
setShopId
(
shopid
);
return
r
;
});
// 计算该 unit 在此周期的实际产能占用量
UnitCapacityResult
.
UnitPeriodDetail
pe
=
new
UnitCapacityResult
.
UnitPeriodDetail
();
pe
.
unitId
=
uid
;
pe
.
unitName
=
name
;
pe
.
periodIndex
=
up
.
getPeriod
().
getIndex
();
pe
.
periodStartDate
=
up
.
getPeriod
().
getStartDate
().
toString
();
...
...
@@ -579,21 +641,21 @@ public class ResultWriter {
capacityUsed
+=
capUsed
;
// 工序级明细
UnitCapacityResult
.
UnitTaskInfo
task
=
new
UnitCapacityResult
.
UnitTaskInfo
();
task
.
operationId
=
op
.
getId
();
task
.
operationName
=
op
.
getName
();
task
.
productionQty
=
ptQty
;
task
.
capacityUsed
=
capUsed
;
task
.
capacityCoeff
=
uo
.
getCapacityCoeff
();
if
(
uo
.
hasLotSize
())
{
task
.
lotSize
=
uo
.
getLotSize
();
task
.
lotSizeOver
=
solutionValue
(
model
.
getPtLotSizeOverVars
(),
ptKey
).
setScale
(
3
,
RoundingMode
.
HALF_UP
).
doubleValue
();;
task
.
lotSizeUnder
=
solutionValue
(
model
.
getPtLotSizeUnderVars
(),
ptKey
).
setScale
(
3
,
RoundingMode
.
HALF_UP
).
doubleValue
();;
}
for
(
OperationOutput
oo
:
op
.
getOutputs
())
{
task
.
outputs
.
add
(
oo
.
getProductId
()
+
"@"
+
oo
.
getSpId
());
}
pe
.
tasks
.
add
(
task
);
//
UnitCapacityResult.UnitTaskInfo task = new UnitCapacityResult.UnitTaskInfo();
//
task.operationId = op.getId();
//
task.operationName = op.getName();
//
task.productionQty = ptQty;
//
task.capacityUsed = capUsed;
//
task.capacityCoeff = uo.getCapacityCoeff();
//
if (uo.hasLotSize()) {
//
task.lotSize = uo.getLotSize();
//
task.lotSizeOver = solutionValue(model.getPtLotSizeOverVars(), ptKey).setScale(3, RoundingMode.HALF_UP).doubleValue();;
//
task.lotSizeUnder = solutionValue(model.getPtLotSizeUnderVars(), ptKey).setScale(3, RoundingMode.HALF_UP).doubleValue();;
//
}
//
for (OperationOutput oo : op.getOutputs()) {
//
task.outputs.add(oo.getProductId() + "@" + oo.getSpId());
//
}
//
pe.tasks.add(task);
}
}
...
...
@@ -614,7 +676,7 @@ public class ResultWriter {
pe
.
notMet
=
notMet
;
}
pe
.
capacityUsed
=
capacityUsed
;
u
cr
.
getPeriodDetails
()
.
add
(
pe
);
u
nitPeriodDetails
.
add
(
pe
);
// 累加汇总
if
(!
up
.
isUnlimited
())
{
...
...
@@ -633,8 +695,10 @@ public class ResultWriter {
(
ucr
.
getTotalCapacityUsed
()
/
ucr
.
getTotalMaxCapacity
())*
100
);
}
}
return
new
java
.
util
.
ArrayList
<>(
unitMap
.
values
());
Map
<
String
,
Object
>
relst
=
new
HashMap
<>();
relst
.
put
(
"UnitCapacity"
,
new
java
.
util
.
ArrayList
<>(
unitMap
.
values
()));
relst
.
put
(
"UnitCapacityDetails"
,
unitPeriodDetails
);
return
relst
;
}
...
...
@@ -652,6 +716,7 @@ public class ResultWriter {
sr
.
setPeriodStartDate
(
sd
.
getPeriod
().
getStartDate
().
toString
());
sr
.
setDemandQty
(
sd
.
getQuantity
());
sr
.
setPriority
(
sd
.
getPriority
());
sr
.
setCategoryId
(
sd
.
getCategoryId
());
sr
.
setDemandOrderDate
(
sd
.
getDemandOrderDate
()==
null
?
""
:
sd
.
getDemandOrderDate
().
toString
());
double
fulfilled
=
solutionValue
(
model
.
getSalesDemandQtyVars
(),
sd
.
getKey
()).
setScale
(
3
,
RoundingMode
.
HALF_UP
).
doubleValue
();;
double
unmet
=
Math
.
max
(
0
,
sd
.
getQuantity
()
-
fulfilled
);
...
...
@@ -673,7 +738,29 @@ public class ResultWriter {
result
.
getSalesDemands
().
add
(
sr
);
}
List
<
SalesDemandResult
>
totalDemands
=
mergeBySalesDemandId
(
result
.
getSalesDemands
());
result
.
setSaleSummarieDemands
(
totalDemands
);
}
private
List
<
SalesDemandResult
>
mergeBySalesDemandId
(
List
<
SalesDemandResult
>
demands
)
{
if
(
demands
==
null
||
demands
.
isEmpty
())
{
return
demands
;
}
Map
<
String
,
SalesDemandResult
>
merged
=
new
LinkedHashMap
<>();
for
(
SalesDemandResult
d
:
demands
)
{
SalesDemandResult
acc
=
merged
.
get
(
d
.
getSalesDemandId
());
if
(
acc
==
null
)
{
merged
.
put
(
d
.
getSalesDemandId
(),
d
);
}
else
{
acc
.
setDemandQty
(
acc
.
getDemandQty
()
+
d
.
getDemandQty
());
acc
.
setFulfilledQty
(
acc
.
getFulfilledQty
()
+
d
.
getFulfilledQty
());
acc
.
setUnmetQty
(
acc
.
getUnmetQty
()
+
d
.
getUnmetQty
());
acc
.
setDemandSlack
(
acc
.
getDemandSlack
()
+
d
.
getDemandSlack
());
acc
.
setFulfillmentRate
(
acc
.
getDemandQty
()
>
0
?
acc
.
getFulfilledQty
()
/
acc
.
getDemandQty
()
:
0.0
);
}
}
return
new
ArrayList
<>(
merged
.
values
());
}
/**
* 分析销售需求未完成的原因。
*
...
...
@@ -931,15 +1018,17 @@ public class ResultWriter {
// ==================== PispipResult ====================
private
void
buildPispips
(
OptimizationResult
result
)
{
for
(
Product
prod
:
data
.
getProducts
())
{
for
(
StockingPoint
sp
:
data
.
getStockingPointsForProduct
(
prod
.
getId
()))
{
for
(
Period
p
:
data
.
getPeriods
())
{
PispipResult
pr
=
new
PispipResult
();
pr
.
setProductId
(
prod
.
getId
());
pr
.
setProductCode
(
prod
.
getCode
());
pr
.
setSpId
(
sp
.
getId
());
pr
.
setSpName
(
sp
.
getName
());
pr
.
setPeriodIndex
(
p
.
getIndex
());
pr
.
set
CategoryId
(
prod
.
getCategoryId
());
pr
.
set
PeriodIndex
(
p
.
getIndex
());
pr
.
setPeriodStartDate
(
p
.
getStartDate
().
toString
());
String
invKey
=
prod
.
getId
()
+
"_"
+
sp
.
getId
()
+
"_"
+
p
.
getIndex
();
...
...
@@ -1070,6 +1159,25 @@ public class ResultWriter {
}
}
}
Map
<
String
,
List
<
PispipResult
>>
grouped
=
result
.
getPispips
()
!=
null
?
result
.
getPispips
().
stream
().
collect
(
Collectors
.
groupingBy
(
p
->
p
.
getProductId
()
+
"@"
+
p
.
getSpId
(),
LinkedHashMap:
:
new
,
Collectors
.
toList
()))
:
new
LinkedHashMap
<>();
for
(
Map
.
Entry
<
String
,
List
<
PispipResult
>>
entry
:
grouped
.
entrySet
())
{
List
<
PispipResult
>
records
=
entry
.
getValue
();
PispipResult
.
PispipSummarieResult
s
=
new
PispipResult
.
PispipSummarieResult
();
s
.
setKey
(
entry
.
getKey
());
s
.
setProductId
(
records
.
get
(
0
).
getProductId
());
s
.
setProductCode
(
records
.
get
(
0
).
getProductCode
());
s
.
setSpId
(
records
.
get
(
0
).
getSpId
());
s
.
setSpName
(
records
.
get
(
0
).
getSpName
());
s
.
setCategoryId
(
records
.
get
(
0
).
getCategoryId
());
result
.
getPispipSummaries
().
add
(
s
);
}
}
// ==================== ProductNetworkResult ====================
...
...
src/main/java/com/aps/macroplanner/output/dto/OptimizationResult.java
View file @
d1745f27
...
...
@@ -27,9 +27,17 @@ public class OptimizationResult {
// ==================== 业务结果 ====================
private
final
List
<
PeriodTaskResult
>
periodTasks
=
new
ArrayList
<>();
private
final
List
<
PeriodTaskResult
.
OutputInfo
>
periodTaskOutputs
=
new
ArrayList
<>();
private
final
List
<
SalesDemandResult
>
salesDemands
=
new
ArrayList
<>();
private
List
<
SalesDemandResult
>
saleDemandSummaries
=
new
ArrayList
<>();
private
final
List
<
PispipResult
>
pispips
=
new
ArrayList
<>();
private
final
List
<
PispipResult
.
PispipSummarieResult
>
pispipSummaries
=
new
ArrayList
<>();
// ==================== 产品生产网络 ====================
private
ProductNetworkResult
productNetwork
;
...
...
@@ -38,6 +46,8 @@ public class OptimizationResult {
private
List
<
UnitCapacityResult
>
unitCapacities
;
private
List
<
UnitCapacityResult
.
UnitPeriodDetail
>
unitPeriodDetails
;
// ==================== 需求满足汇总 ====================
private
List
<
DemandSummaryResult
>
demandSummary
;
...
...
@@ -59,15 +69,30 @@ public class OptimizationResult {
public
void
setVersion
(
String
v
)
{
this
.
version
=
v
;
}
public
List
<
PeriodTaskResult
>
getPeriodTasks
()
{
return
periodTasks
;
}
public
List
<
PeriodTaskResult
.
OutputInfo
>
getPeriodTaskOutputs
()
{
return
periodTaskOutputs
;
}
public
List
<
SalesDemandResult
>
getSalesDemands
()
{
return
salesDemands
;
}
public
List
<
SalesDemandResult
>
getSaleSummarieDemands
()
{
return
saleDemandSummaries
;
}
public
void
setSaleSummarieDemands
(
List
<
SalesDemandResult
>
v
)
{
saleDemandSummaries
=
v
;
}
public
List
<
PispipResult
>
getPispips
()
{
return
pispips
;
}
public
List
<
PispipResult
.
PispipSummarieResult
>
getPispipSummaries
()
{
return
pispipSummaries
;
}
public
ProductNetworkResult
getProductNetwork
()
{
return
productNetwork
;
}
public
void
setProductNetwork
(
ProductNetworkResult
v
)
{
this
.
productNetwork
=
v
;
}
public
List
<
UnitCapacityResult
>
getUnitCapacities
()
{
return
unitCapacities
;
}
public
List
<
UnitCapacityResult
.
UnitPeriodDetail
>
getUnitPeriodDetails
()
{
return
unitPeriodDetails
;
}
public
void
setUnitCapacities
(
List
<
UnitCapacityResult
>
v
)
{
this
.
unitCapacities
=
v
;
}
public
void
setUnitPeriodDetails
(
List
<
UnitCapacityResult
.
UnitPeriodDetail
>
v
)
{
this
.
unitPeriodDetails
=
v
;
}
public
List
<
DemandSummaryResult
>
getDemandSummary
()
{
return
demandSummary
;
}
public
void
setDemandSummary
(
List
<
DemandSummaryResult
>
v
)
{
this
.
demandSummary
=
v
;
}
...
...
src/main/java/com/aps/macroplanner/output/dto/PeriodTaskResult.java
View file @
d1745f27
...
...
@@ -17,6 +17,7 @@ public class PeriodTaskResult {
private
String
operationId
;
private
String
operationName
;
private
String
unitId
;
private
String
unitName
;
private
int
periodIndex
;
private
String
periodStartDate
;
...
...
@@ -40,11 +41,21 @@ public class PeriodTaskResult {
// ==================== 内嵌类 ====================
/** 产出信息 */
// public static class OutputInfo {
// public String productId;
// public String spId;
// public double factor;
// }
@Data
public
static
class
OutputInfo
{
public
String
productId
;
public
String
productCode
;
public
String
spId
;
public
String
spName
;
public
double
factor
;
private
String
dataKey
;
private
int
periodIndex
;
private
String
periodStartDate
;
/** PTQty — 生产量 */
private
double
productionQty
;
}
}
\ No newline at end of file
src/main/java/com/aps/macroplanner/output/dto/PispipResult.java
View file @
d1745f27
...
...
@@ -16,7 +16,7 @@ public class PispipResult {
private
String
productId
;
private
String
productCode
;
private
String
spId
;
private
Long
categoryId
;
private
String
spName
;
private
int
periodIndex
;
private
String
periodStartDate
;
...
...
@@ -83,6 +83,36 @@ public class PispipResult {
/** 每个工序的产出明细 */
private
final
List
<
ProductionDetail
>
productionDetails
=
new
ArrayList
<>();
public
static
class
PispipSummarieResult
{
private
String
key
;
private
String
productId
;
private
String
productCode
;
private
String
spId
;
private
String
spName
;
private
Long
categoryId
;
public
String
getKey
()
{
return
key
;
}
public
void
setKey
(
String
v
)
{
this
.
key
=
v
;
}
public
String
getProductId
()
{
return
productId
;
}
public
void
setProductId
(
String
v
)
{
this
.
productId
=
v
;
}
public
String
getProductCode
()
{
return
productCode
;
}
public
void
setProductCode
(
String
v
)
{
this
.
productCode
=
v
;
}
public
String
getSpId
()
{
return
spId
;
}
public
void
setSpId
(
String
v
)
{
this
.
spId
=
v
;
}
public
String
getSpName
()
{
return
spName
;
}
public
void
setSpName
(
String
v
)
{
this
.
spName
=
v
;
}
public
Long
getCategoryId
()
{
return
categoryId
;
}
public
void
setCategoryId
(
Long
v
)
{
this
.
categoryId
=
v
;
}
}
// ==================== 内嵌类 ====================
/** 工序产出明细 */
...
...
src/main/java/com/aps/macroplanner/output/dto/SalesDemandResult.java
View file @
d1745f27
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
SalesDemandResult
{
private
String
salesDemandId
;
...
...
@@ -33,6 +36,8 @@ public class SalesDemandResult {
/** 优先级 */
private
double
priority
;
private
Long
categoryId
;
/** 未完成原因分析 (仅当 unmetQty > 0 时有内容) */
private
final
List
<
String
>
unmetReasons
=
new
ArrayList
<>();
/** 风险分析 (即使满足也可能存在的供应链风险) */
...
...
src/main/java/com/aps/macroplanner/output/dto/SupplyChainNode.java
View file @
d1745f27
...
...
@@ -19,6 +19,9 @@ public class SupplyChainNode {
/** 产品 ID */
private
String
productId
;
public
String
productCode
;
/** 库存点 ID */
private
String
spId
;
...
...
@@ -148,6 +151,10 @@ public class SupplyChainNode {
public
String
getProductId
()
{
return
productId
;
}
public
void
setProductId
(
String
v
)
{
this
.
productId
=
v
;
}
public
String
getProductCode
()
{
return
productCode
;
}
public
void
setProductCode
(
String
v
)
{
this
.
productCode
=
v
;
}
public
String
getSpId
()
{
return
spId
;
}
public
void
setSpId
(
String
v
)
{
this
.
spId
=
v
;
}
...
...
src/main/java/com/aps/macroplanner/output/dto/UnitCapacityResult.java
View file @
d1745f27
package
com
.
aps
.
macroplanner
.
output
.
dto
;
import
lombok.Data
;
import
java.util.ArrayList
;
import
java.util.LinkedHashMap
;
import
java.util.List
;
...
...
@@ -20,6 +22,7 @@ import java.util.Map;
* <tr><td>periodTasks</td><td>该设备的生产任务明细 (PeriodTaskResult[])</td></tr>
* </table>
*/
@Data
public
class
UnitCapacityResult
{
/** 设备ID */
...
...
@@ -27,6 +30,8 @@ public class UnitCapacityResult {
private
String
unitName
;
private
Integer
shopId
;
/** 使用该设备的工序ID列表 */
private
List
<
String
>
operationIds
;
/** 全部周期最大可用产能合计 */
...
...
@@ -44,13 +49,19 @@ public class UnitCapacityResult {
private
double
overallUtilization
;
/** 各周期明细 */
private
final
List
<
UnitPeriodDetail
>
periodDetails
=
new
ArrayList
<>();
//
private final List<UnitPeriodDetail> periodDetails = new ArrayList<>();
// ==================== 内嵌类 ====================
/** 单周期产能明细 */
public
static
class
UnitPeriodDetail
{
/** 设备ID */
public
String
unitId
;
public
String
unitName
;
/** 周期索引 */
public
int
periodIndex
;
/** 周期起始日期 */
...
...
@@ -69,7 +80,7 @@ public class UnitCapacityResult {
public
double
production
;
/** 该周期内在该设备上执行的工序明细 */
public
final
List
<
UnitTaskInfo
>
tasks
=
new
ArrayList
<>();
//
public final List<UnitTaskInfo> tasks = new ArrayList<>();
}
/** 设备上的工序级生产明细 */
public
static
class
UnitTaskInfo
{
...
...
@@ -107,7 +118,7 @@ public class UnitCapacityResult {
public
double
getTotalProduction
()
{
return
totalProduction
;
}
public
void
setTotalProduction
(
double
v
)
{
this
.
totalProduction
=
v
;
}
public
List
<
UnitPeriodDetail
>
getPeriodDetails
()
{
return
periodDetails
;
}
//
public List<UnitPeriodDetail> getPeriodDetails() { return periodDetails; }
public
double
getTotalOverloaded
()
{
return
totalOverloaded
;
}
public
void
setTotalOverloaded
(
double
v
)
{
this
.
totalOverloaded
=
v
;
}
...
...
src/main/java/com/aps/service/MacroPlannerProductNetworkService.java
View file @
d1745f27
...
...
@@ -13,14 +13,16 @@ import java.util.*;
/**
* 产品生产网络按需查询服务。
*
* <p>不再在求解阶段预生成完整 BOM 网络 (buildProductNetwork), 而是:
* <p>不再在求解阶段预生成完整 BOM 网络, 而是查询时按 productId + spId 定位节点,
* 递归向下展开 BOM 子树。求解值从 parquet 按需过滤读取, 避免全量加载。</p>
*
* <p>数据流:</p>
* <ol>
* <li>求解时把静态 BOM 结构持久化为 {@code bom_structure.json};</li>
* <li>求解变量值已持久化为 parquet (period_tasks / pispips / salesdemand / operation_demands);</li>
* <li>查询时按 productId + spId 定位节点, 递归向下展开 BOM 子树, 按需读取变量值。</li>
* <li>bom_structure.json —— 静态 BOM 结构 (小, 全量读);</li>
* <li>period_task_outputs.parquet —— 工序产出 (谁把什么产入哪个库房);</li>
* <li>period_tasks.parquet —— 生产任务 (dataKey → unit/operation/产量);</li>
* <li>pispips / salesdemand / operation_demands —— 库存/销售/工序消耗量。</li>
* </ol>
*
* <p>这样避免了构建整棵树的耗时和超大 JSON 的序列化/反序列化开销。</p>
*/
@Service
@Slf4j
...
...
@@ -45,8 +47,53 @@ public class MacroPlannerProductNetworkService {
}
try
{
Integer
periodIndex
=
isBlank
(
period
)
?
null
:
Integer
.
parseInt
(
period
.
trim
());
IndexedData
idx
=
load
(
sceneId
);
return
buildNode
(
idx
,
productId
,
spId
,
periodIndex
,
0
,
new
HashSet
<>());
// 1. 静态结构 (小, 全量读)
BomStructureData
structure
=
objectMapper
.
readValue
(
new
File
(
path
(
sceneId
,
"bom_structure.json"
)),
BomStructureData
.
class
);
StructureIndex
sidx
=
new
StructureIndex
(
structure
);
// 2. 纯结构展开目标子树, 确定涉及的节点集合与工序集合
Set
<
String
>
nodeKeys
=
new
HashSet
<>();
Set
<
String
>
opIds
=
new
HashSet
<>();
collectSubtree
(
sidx
,
productId
,
spId
,
nodeKeys
,
opIds
);
// 3. 按需过滤读取求解值 (不全量加载)
FlatParquetUtil
parquet
=
new
FlatParquetUtil
();
List
<
PeriodTaskResult
.
OutputInfo
>
outputs
=
parquet
.
readAll
(
path
(
sceneId
,
"period_task_outputs.parquet"
),
PeriodTaskResult
.
OutputInfo
.
class
,
oi
->
nodeKeys
.
contains
(
oi
.
getProductId
()
+
"@"
+
oi
.
getSpId
()));
Set
<
String
>
dataKeys
=
new
HashSet
<>();
for
(
PeriodTaskResult
.
OutputInfo
oi
:
outputs
)
{
dataKeys
.
add
(
oi
.
getDataKey
());
}
List
<
PeriodTaskResult
>
tasks
=
parquet
.
readAll
(
path
(
sceneId
,
"period_tasks.parquet"
),
PeriodTaskResult
.
class
,
pt
->
dataKeys
.
contains
(
pt
.
getDataKey
()));
// List<PispipResult> pispips = parquet.readAll(
// path(sceneId, "pispips.parquet"),
// PispipResult.class,
// p -> nodeKeys.contains(p.getProductId() + "@" + p.getSpId()));
//
// List<SalesDemandResult> sales = parquet.readAll(
// path(sceneId, "salesdemand.parquet"),
// SalesDemandResult.class,
// sd -> nodeKeys.contains(sd.getProductId() + "@" + sd.getSpId()));
//
// List<OperationDemandResult> opDemands = parquet.readAll(
// path(sceneId, "operation_demands.parquet"),
// OperationDemandResult.class,
// od -> opIds.contains(od.getOperationId()));
SolveIndex
solveIdx
=
new
SolveIndex
(
outputs
,
tasks
,
pispips
,
sales
,
opDemands
);
// 4. 递归组装
return
buildNode
(
sidx
,
solveIdx
,
productId
,
spId
,
periodIndex
,
0
,
new
HashSet
<>());
}
catch
(
Exception
e
)
{
log
.
warn
(
"按需组装产品网络失败: sceneId={}, productId={}, spId={}, error={}"
,
sceneId
,
productId
,
spId
,
e
.
getMessage
());
...
...
@@ -54,33 +101,37 @@ public class MacroPlannerProductNetworkService {
}
}
// ==================== 加载 + 索引 ====================
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) 查询结构
。
*
纯结构遍历 BOM, 收集目标子树涉及的所有 productId@spId 与 operationId
。
*/
private
static
class
IndexedData
{
final
Map
<
String
,
List
<
BomStructureData
.
StockingPointInfo
>>
spByProduct
=
new
HashMap
<>();
private
void
collectSubtree
(
StructureIndex
sidx
,
String
productId
,
String
spId
,
Set
<
String
>
nodeKeys
,
Set
<
String
>
opIds
)
{
String
key
=
productId
+
"@"
+
spId
;
if
(!
nodeKeys
.
add
(
key
))
{
return
;
}
for
(
BomStructureData
.
OperationInfo
op
:
sidx
.
producing
(
productId
,
spId
))
{
opIds
.
add
(
op
.
id
);
for
(
BomStructureData
.
OperationInputInfo
in
:
sidx
.
inputsOf
(
op
.
id
))
{
opIds
.
add
(
in
.
operationId
);
collectSubtree
(
sidx
,
in
.
inputProductId
,
in
.
inputSpId
,
nodeKeys
,
opIds
);
}
}
// 消费者工序 (上层) 也需要, 用于查 operation_demands 的消耗量
for
(
BomStructureData
.
OperationInputInfo
in
:
sidx
.
consumersOf
(
productId
,
spId
))
{
opIds
.
add
(
in
.
operationId
);
}
}
// ==================== 结构索引 ====================
private
static
class
StructureIndex
{
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
<>();
...
...
@@ -89,31 +140,14 @@ public class MacroPlannerProductNetworkService {
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
)
{
StructureIndex
(
BomStructureData
s
)
{
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
)
{
...
...
@@ -132,21 +166,6 @@ public class MacroPlannerProductNetworkService {
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
)
{
...
...
@@ -164,25 +183,72 @@ public class MacroPlannerProductNetworkService {
List
<
BomStructureData
.
InTransitSupplyInfo
>
inTransit
(
String
productId
,
String
spId
)
{
return
inTransitByProductSp
.
getOrDefault
(
productId
+
"@"
+
spId
,
Collections
.
emptyList
());
}
String
startDate
(
int
periodIndex
)
{
return
startDateByPeriodIndex
.
get
(
periodIndex
);
}
}
// ==================== 求解值索引 ====================
private
static
class
SolveIndex
{
final
Map
<
String
,
List
<
PeriodTaskResult
.
OutputInfo
>>
outputsByProductSp
=
new
HashMap
<>();
final
Map
<
String
,
PeriodTaskResult
>
taskByDataKey
=
new
HashMap
<>();
final
Map
<
String
,
PispipResult
>
pispipByKey
=
new
HashMap
<>();
final
Map
<
String
,
double
[]>
salesByKey
=
new
HashMap
<>();
final
Map
<
String
,
Double
>
opDemandByKey
=
new
HashMap
<>();
SolveIndex
(
List
<
PeriodTaskResult
.
OutputInfo
>
outputs
,
List
<
PeriodTaskResult
>
tasks
,
List
<
PispipResult
>
pispips
,
List
<
SalesDemandResult
>
sales
,
List
<
OperationDemandResult
>
opDemands
)
{
for
(
PeriodTaskResult
.
OutputInfo
oi
:
outputs
)
{
outputsByProductSp
.
computeIfAbsent
(
oi
.
getProductId
()
+
"@"
+
oi
.
getSpId
(),
k
->
new
ArrayList
<>()).
add
(
oi
);
}
for
(
PeriodTaskResult
pt
:
tasks
)
{
taskByDataKey
.
put
(
pt
.
getDataKey
(),
pt
);
}
for
(
PispipResult
p
:
pispips
)
{
pispipByKey
.
put
(
p
.
getProductId
()
+
"_"
+
p
.
getSpId
()
+
"_"
+
p
.
getPeriodIndex
(),
p
);
}
for
(
SalesDemandResult
sd
:
sales
)
{
String
key
=
sd
.
getProductId
()
+
"_"
+
sd
.
getSpId
()
+
"_"
+
sd
.
getPeriodIndex
();
double
[]
acc
=
salesByKey
.
computeIfAbsent
(
key
,
k
->
new
double
[]{
0.0
,
0.0
});
acc
[
0
]
+=
sd
.
getDemandQty
();
acc
[
1
]
+=
sd
.
getFulfilledQty
();
}
for
(
OperationDemandResult
od
:
opDemands
)
{
opDemandByKey
.
put
(
od
.
getOperationId
()
+
"_"
+
od
.
getInputProductId
()
+
"_"
+
od
.
getInputSpId
()
+
"_"
+
od
.
getPeriodIndex
(),
od
.
getQuantity
());
}
}
List
<
PeriodTaskResult
.
OutputInfo
>
outputsOf
(
String
productId
,
String
spId
)
{
return
outputsByProductSp
.
getOrDefault
(
productId
+
"@"
+
spId
,
Collections
.
emptyList
());
}
PeriodTaskResult
taskOf
(
String
dataKey
)
{
return
taskByDataKey
.
get
(
dataKey
);
}
PispipResult
pispip
(
String
productId
,
String
spId
,
int
periodIndex
)
{
return
pispipByKey
.
get
(
productId
+
"_"
+
spId
+
"_"
+
periodIndex
);
}
double
demandQty
(
String
productId
,
String
spId
,
int
periodIndex
)
{
double
[]
acc
=
sales
.
get
(
productId
+
"_"
+
spId
+
"_"
+
periodIndex
);
double
[]
acc
=
sales
ByKey
.
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
);
double
[]
acc
=
sales
ByKey
.
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
);
double
opDemand
(
String
operationId
,
String
inputProductId
,
String
inputSpId
,
int
periodIndex
)
{
return
opDemandByKey
.
getOrDefault
(
operationId
+
"_"
+
inputProductId
+
"_"
+
inputSpId
+
"_"
+
periodIndex
,
0.0
);
}
}
// ==================== 节点组装 ====================
private
SupplyChainNode
buildNode
(
IndexedData
idx
,
String
productId
,
String
spId
,
private
SupplyChainNode
buildNode
(
StructureIndex
sidx
,
SolveIndex
solveIdx
,
String
productId
,
String
spId
,
Integer
periodIndex
,
int
level
,
Set
<
String
>
visited
)
{
String
nodeKey
=
productId
+
"@"
+
spId
;
if
(!
visited
.
add
(
nodeKey
))
{
...
...
@@ -192,61 +258,37 @@ public class MacroPlannerProductNetworkService {
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
);
String
periodStartDate
=
periodIndex
==
null
?
null
:
sidx
.
startDate
(
periodIndex
);
// --- 供应来源: 以 period_task_outputs 为入口 ---
Map
<
String
,
SupplyChainNode
.
SupplySource
>
srcByOp
=
new
LinkedHashMap
<>();
for
(
PeriodTaskResult
.
OutputInfo
oi
:
solveIdx
.
outputsOf
(
productId
,
spId
))
{
node
.
setSpName
(
oi
.
getSpName
());
node
.
setProductCode
(
oi
.
getProductCode
());
PeriodTaskResult
pt
=
solveIdx
.
taskOf
(
oi
.
getDataKey
());
if
(
pt
==
null
)
continue
;
SupplyChainNode
.
SupplySource
src
=
srcByOp
.
computeIfAbsent
(
pt
.
getOperationId
(),
opId
->
{
SupplyChainNode
.
SupplySource
s
=
new
SupplyChainNode
.
SupplySource
();
s
.
type
=
"OPERATION"
;
s
.
operationId
=
pt
.
getOperationId
();
s
.
operationName
=
pt
.
getOperationName
();
s
.
unitId
=
pt
.
getUnitId
();
s
.
unitName
=
pt
.
getUnitName
();
s
.
totalProduction
=
pt
.
getProductionQty
();
return
s
;
});
// --- 供应来源 ---
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
))
{
for
(
BomStructureData
.
InTransitSupplyInfo
its
:
s
idx
.
inTransit
(
productId
,
spId
))
{
SupplyChainNode
.
SupplySource
src
=
new
SupplyChainNode
.
SupplySource
();
src
.
type
=
"IN_TRANSIT"
;
src
.
totalProduction
=
its
.
quantity
;
...
...
@@ -261,50 +303,12 @@ public class MacroPlannerProductNetworkService {
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
);
List
<
BomStructureData
.
OperationInfo
>
producingOps
=
s
idx
.
producing
(
productId
,
spId
);
if
(!
producingOps
.
isEmpty
())
{
BomStructureData
.
OperationInfo
op
=
producingOps
.
get
(
0
);
// 与原逻辑一致: 只展开第一个工序的 BOM
for
(
BomStructureData
.
OperationInputInfo
in
:
idx
.
inputsOf
(
op
.
id
))
{
BomStructureData
.
OperationInfo
op
=
producingOps
.
get
(
0
);
for
(
BomStructureData
.
OperationInputInfo
in
:
s
idx
.
inputsOf
(
op
.
id
))
{
SupplyChainNode
.
BomChild
child
=
new
SupplyChainNode
.
BomChild
();
child
.
productId
=
in
.
inputProductId
;
child
.
spId
=
in
.
inputSpId
;
...
...
@@ -314,9 +318,8 @@ public class MacroPlannerProductNetworkService {
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
);
for
(
BomStructureData
.
PeriodInfo
p
:
sidx
.
periods
)
{
double
consumed
=
solveIdx
.
opDemand
(
in
.
operationId
,
in
.
inputProductId
,
in
.
inputSpId
,
p
.
index
);
totalConsumed
+=
consumed
;
if
(
consumed
>
0
)
{
String
periodKey
=
p
.
startDate
==
null
?
String
.
valueOf
(
p
.
index
)
:
p
.
startDate
;
...
...
@@ -326,14 +329,13 @@ public class MacroPlannerProductNetworkService {
child
.
totalConsumedQty
=
totalConsumed
;
if
(
periodStartDate
!=
null
&&
!
child
.
consumedByPeriod
.
containsKey
(
periodStartDate
))
{
continue
;
// 该周期无消耗, 剪枝
continue
;
}
SupplyChainNode
childNode
=
buildNode
(
i
dx
,
in
.
inputProductId
,
in
.
inputSpId
,
periodIndex
,
level
+
1
,
visited
);
SupplyChainNode
childNode
=
buildNode
(
sidx
,
solveI
dx
,
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
;
...
...
@@ -345,9 +347,9 @@ public class MacroPlannerProductNetworkService {
}
// --- 汇总 ---
fillSummary
(
i
dx
,
node
,
productId
,
spId
);
fillSummary
(
sidx
,
solveI
dx
,
node
,
productId
,
spId
);
// period 剪枝
: 该周期无活动则剪掉整个节点 (与原 buildProductNetwork 语义一致)
// period 剪枝
if
(
periodStartDate
!=
null
&&
!
node
.
getActivePeriods
().
contains
(
periodStartDate
))
{
return
null
;
}
...
...
@@ -355,14 +357,15 @@ public class MacroPlannerProductNetworkService {
return
node
;
}
private
void
fillSummary
(
IndexedData
idx
,
SupplyChainNode
node
,
String
productId
,
String
spId
)
{
private
void
fillSummary
(
StructureIndex
sidx
,
SolveIndex
solveIdx
,
SupplyChainNode
node
,
String
productId
,
String
spId
)
{
SupplySummary
s
=
new
SupplySummary
();
node
.
setSummary
(
s
);
int
n
=
idx
.
periods
.
size
();
int
n
=
s
idx
.
periods
.
size
();
double
totalEndingInv
=
0
;
for
(
BomStructureData
.
PeriodInfo
p
:
idx
.
periods
)
{
PispipResult
pr
=
i
dx
.
pispip
(
productId
,
spId
,
p
.
index
);
for
(
BomStructureData
.
PeriodInfo
p
:
s
idx
.
periods
)
{
PispipResult
pr
=
solveI
dx
.
pispip
(
productId
,
spId
,
p
.
index
);
if
(
pr
!=
null
)
{
totalEndingInv
+=
pr
.
getEndingInventory
();
if
(
pr
.
getEndingInventory
()
>
0
)
{
...
...
@@ -371,26 +374,21 @@ public class MacroPlannerProductNetworkService {
}
}
s
.
setInitialInventory
(
idx
.
initialInv
(
productId
,
spId
));
PispipResult
last
=
i
dx
.
pispip
(
productId
,
spId
,
n
-
1
);
s
.
setInitialInventory
(
s
idx
.
initialInv
(
productId
,
spId
));
PispipResult
last
=
solveI
dx
.
pispip
(
productId
,
spId
,
n
-
1
);
s
.
setFinalInventory
(
last
==
null
?
0
:
last
.
getEndingInventory
());
s
.
setAverageInventory
(
n
>
0
?
totalEndingInv
/
n
:
0
);
// 生产汇总
// 生产汇总
(跨周期全量, 不受 period 过滤影响)
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
;
}
}
for
(
PeriodTaskResult
.
OutputInfo
oi
:
solveIdx
.
outputsOf
(
productId
,
spId
))
{
totalProduction
+=
oi
.
getProductionQty
();
}
s
.
setTotalProduction
(
totalProduction
);
// 在途汇总
double
totalInTransit
=
0
;
for
(
BomStructureData
.
InTransitSupplyInfo
its
:
idx
.
inTransit
(
productId
,
spId
))
{
for
(
BomStructureData
.
InTransitSupplyInfo
its
:
s
idx
.
inTransit
(
productId
,
spId
))
{
totalInTransit
+=
its
.
quantity
;
}
s
.
setTotalInTransit
(
totalInTransit
);
...
...
@@ -401,8 +399,8 @@ public class MacroPlannerProductNetworkService {
double
totalDepDemand
=
0
;
double
totalDemandFulf
=
0
;
double
totalSlack
=
0
;
for
(
BomStructureData
.
PeriodInfo
p
:
idx
.
periods
)
{
PispipResult
pr
=
i
dx
.
pispip
(
productId
,
spId
,
p
.
index
);
for
(
BomStructureData
.
PeriodInfo
p
:
s
idx
.
periods
)
{
PispipResult
pr
=
solveI
dx
.
pispip
(
productId
,
spId
,
p
.
index
);
if
(
pr
!=
null
)
{
totalDepDemand
+=
pr
.
getDependentDemandQty
();
totalDemandFulf
+=
pr
.
getDemandFulfillment
();
...
...
@@ -411,8 +409,8 @@ public class MacroPlannerProductNetworkService {
node
.
getActivePeriods
().
add
(
p
.
startDate
==
null
?
String
.
valueOf
(
p
.
index
)
:
p
.
startDate
);
}
}
totalSalesDemand
+=
i
dx
.
demandQty
(
productId
,
spId
,
p
.
index
);
totalSalesFulfilled
+=
i
dx
.
fulfilledQty
(
productId
,
spId
,
p
.
index
);
totalSalesDemand
+=
solveI
dx
.
demandQty
(
productId
,
spId
,
p
.
index
);
totalSalesFulfilled
+=
solveI
dx
.
fulfilledQty
(
productId
,
spId
,
p
.
index
);
}
s
.
setTotalSalesDemand
(
totalSalesDemand
);
s
.
setTotalSalesFulfilled
(
totalSalesFulfilled
);
...
...
@@ -428,8 +426,8 @@ public class MacroPlannerProductNetworkService {
double
belowTarget
=
0
;
double
belowMin
=
0
;
double
aboveMax
=
0
;
for
(
BomStructureData
.
PeriodInfo
p
:
idx
.
periods
)
{
PispipResult
pr
=
i
dx
.
pispip
(
productId
,
spId
,
p
.
index
);
for
(
BomStructureData
.
PeriodInfo
p
:
s
idx
.
periods
)
{
PispipResult
pr
=
solveI
dx
.
pispip
(
productId
,
spId
,
p
.
index
);
if
(
pr
!=
null
)
{
belowTarget
+=
pr
.
getBelowTarget
();
belowMin
+=
pr
.
getBelowMin
();
...
...
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