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
be4b7722
Commit
be4b7722
authored
Sep 20, 2026
by
Tong Li
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
MP
parent
d1745f27
Show whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
1593 additions
and
406 deletions
+1593
-406
MacroPlannerResultController.java
...java/com/aps/controller/MacroPlannerResultController.java
+4
-4
DemandSlackLinkageConstraint.java
...macroplanner/constraint/DemandSlackLinkageConstraint.java
+0
-1
ResultWriter.java
src/main/java/com/aps/macroplanner/output/ResultWriter.java
+66
-65
BomStructureData.java
...ava/com/aps/macroplanner/output/dto/BomStructureData.java
+1
-0
TraceNode.java
src/main/java/com/aps/macroplanner/output/dto/TraceNode.java
+190
-0
TraceOutput.java
...ain/java/com/aps/macroplanner/output/dto/TraceOutput.java
+35
-0
MacroPlannerProductNetworkService.java
...va/com/aps/service/MacroPlannerProductNetworkService.java
+852
-336
MacroPlannerProductNetworkService1.java
...a/com/aps/service/MacroPlannerProductNetworkService1.java
+445
-0
No files found.
src/main/java/com/aps/controller/MacroPlannerResultController.java
View file @
be4b7722
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
;
import
com.aps.entity.MaterialInfo
;
...
...
@@ -13,8 +12,9 @@ import com.aps.macroplanner.data.MacroPlannerDataConverter;
import
com.aps.macroplanner.data.TestDataBuilder
;
import
com.aps.macroplanner.output.ResultWriter
;
import
com.aps.macroplanner.output.dto.*
;
import
com.aps.service.MacroPlannerResultService
;
import
com.aps.service.MacroPlannerProductNetworkService
;
import
com.aps.service.MacroPlannerResultService
;
import
com.aps.service.MacroPlannerProductNetworkService1
;
import
com.aps.service.MpPispipResultPersistenceService
;
import
com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper
;
import
com.google.ortools.Loader
;
...
...
@@ -456,7 +456,7 @@ public class MacroPlannerResultController {
description
=
"成品→半成品→原材料完整BOM供应链树。"
+
"包含各层级的供应来源(含每周期产量)、消耗者、BOM子物料(含每周期消耗量)。"
+
"支持按 productId / spId / unitId / periodIndex 可选过滤。"
)
public
R
<
SupplyChain
Node
>
getProductNetwork
(
public
R
<
Trace
Node
>
getProductNetwork
(
@RequestParam
(
"sceneId"
)
@Parameter
(
description
=
"场景ID"
,
required
=
true
)
String
sceneId
,
@RequestParam
(
value
=
"productId"
,
required
=
false
)
@Parameter
(
description
=
"按产品ID过滤成品根节点(可选)"
)
String
productId
,
@RequestParam
(
value
=
"spId"
,
required
=
false
)
@Parameter
(
description
=
"按库存点ID过滤(可选)"
)
String
spId
,
...
...
@@ -464,7 +464,7 @@ public class MacroPlannerResultController {
if
(
productId
==
null
||
productId
.
trim
().
isEmpty
()
||
spId
==
null
||
spId
.
trim
().
isEmpty
())
{
return
R
.
failed
(
"productId 和 spId 不能为空"
);
}
SupplyChain
Node
node
=
macroPlannerProductNetworkService
.
buildProductNetwork
(
Trace
Node
node
=
macroPlannerProductNetworkService
.
buildProductNetwork
(
sceneId
,
productId
.
trim
(),
spId
.
trim
(),
period
);
if
(
node
==
null
)
{
return
R
.
failed
(
"未找到场景 "
+
sceneId
+
" 中 "
+
productId
+
"@"
+
spId
+
" 的产品网络"
);
...
...
src/main/java/com/aps/macroplanner/constraint/DemandSlackLinkageConstraint.java
View file @
be4b7722
...
...
@@ -36,7 +36,6 @@ import java.util.Map;
* 求解器被激励去增加生产来满足需求,而非简单地降低销售量。</p>
*
* @see BalanceConstraint 物料平衡约束(DemandSlack 在流入侧)
* @see DemandVariableBuilder 需求变量定义(SalesDemandQty, DemandSlack)
*/
public
class
DemandSlackLinkageConstraint
{
...
...
src/main/java/com/aps/macroplanner/output/ResultWriter.java
View file @
be4b7722
...
...
@@ -323,81 +323,82 @@ public class ResultWriter {
private
BomStructureData
buildBomStructureData
()
{
BomStructureData
s
=
new
BomStructureData
();
for
(
Product
p
:
data
.
getProducts
())
{
BomStructureData
.
ProductInfo
pi
=
new
BomStructureData
.
ProductInfo
();
pi
.
id
=
p
.
getId
();
pi
.
name
=
p
.
getName
();
pi
.
code
=
p
.
getCode
();
s
.
products
.
add
(
pi
);
}
for
(
StockingPoint
sp
:
data
.
getStockingPoints
())
{
BomStructureData
.
StockingPointInfo
si
=
new
BomStructureData
.
StockingPointInfo
();
si
.
id
=
sp
.
getId
();
si
.
name
=
sp
.
getName
();
s
.
stockingPoints
.
add
(
si
);
}
//
for (Product p : data.getProducts()) {
//
BomStructureData.ProductInfo pi = new BomStructureData.ProductInfo();
//
pi.id = p.getId();
//
pi.name = p.getName();
//
pi.code = p.getCode();
//
s.products.add(pi);
//
}
//
for (StockingPoint sp : data.getStockingPoints()) {
//
BomStructureData.StockingPointInfo si = new BomStructureData.StockingPointInfo();
//
si.id = sp.getId();
//
si.name = sp.getName();
//
s.stockingPoints.add(si);
//
}
// 产品→库存点映射 (通过 getStockingPointsForProduct 反推)
for
(
Product
p
:
data
.
getProducts
())
{
for
(
StockingPoint
sp
:
data
.
getStockingPointsForProduct
(
p
.
getId
()))
{
BomStructureData
.
ProductSpMappingInfo
mi
=
new
BomStructureData
.
ProductSpMappingInfo
();
mi
.
productId
=
p
.
getId
();
mi
.
spId
=
sp
.
getId
();
s
.
productSpMappings
.
add
(
mi
);
}
}
for
(
Operation
op
:
data
.
getOperations
())
{
BomStructureData
.
OperationInfo
oi
=
new
BomStructureData
.
OperationInfo
();
oi
.
id
=
op
.
getId
();
oi
.
name
=
op
.
getName
();
oi
.
leadTimeDays
=
op
.
getLeadTimeDays
();
oi
.
hasLotSize
=
op
.
hasLotSize
();
oi
.
lotSize
=
op
.
hasLotSize
()
?
op
.
getLotSize
()
:
null
;
for
(
OperationOutput
oo
:
op
.
getOutputs
())
{
BomStructureData
.
OperationOutputInfo
ooi
=
new
BomStructureData
.
OperationOutputInfo
();
ooi
.
productId
=
oo
.
getProductId
();
ooi
.
spId
=
oo
.
getSpId
();
oi
.
outputs
.
add
(
ooi
);
}
for
(
UnitOperation
uo
:
op
.
getUnitOperations
())
{
BomStructureData
.
UnitOperationInfo
uoi
=
new
BomStructureData
.
UnitOperationInfo
();
uoi
.
unitId
=
uo
.
getUnitId
();
uoi
.
unitName
=
uo
.
getUnitName
();
uoi
.
capacityCoeff
=
uo
.
getCapacityCoeff
();
oi
.
unitOperations
.
add
(
uoi
);
}
s
.
operations
.
add
(
oi
);
}
//
for (Product p : data.getProducts()) {
//
for (StockingPoint sp : data.getStockingPointsForProduct(p.getId())) {
//
BomStructureData.ProductSpMappingInfo mi = new BomStructureData.ProductSpMappingInfo();
//
mi.productId = p.getId();
//
mi.spId = sp.getId();
//
s.productSpMappings.add(mi);
//
}
//
}
//
for (Operation op : data.getOperations()) {
//
BomStructureData.OperationInfo oi = new BomStructureData.OperationInfo();
//
oi.id = op.getId();
//
oi.name = op.getName();
//
oi.leadTimeDays = op.getLeadTimeDays();
//
oi.hasLotSize = op.hasLotSize();
//
oi.lotSize = op.hasLotSize() ? op.getLotSize() : null;
//
for (OperationOutput oo : op.getOutputs()) {
//
BomStructureData.OperationOutputInfo ooi = new BomStructureData.OperationOutputInfo();
//
ooi.productId = oo.getProductId();
//
ooi.spId = oo.getSpId();
//
oi.outputs.add(ooi);
//
}
//
for (UnitOperation uo : op.getUnitOperations()) {
//
BomStructureData.UnitOperationInfo uoi = new BomStructureData.UnitOperationInfo();
//
uoi.unitId = uo.getUnitId();
//
uoi.unitName = uo.getUnitName();
//
uoi.capacityCoeff = uo.getCapacityCoeff();
//
oi.unitOperations.add(uoi);
//
}
//
s.operations.add(oi);
//
}
for
(
OperationInput
input
:
data
.
getOperationInputs
())
{
BomStructureData
.
OperationInputInfo
ii
=
new
BomStructureData
.
OperationInputInfo
();
ii
.
operationId
=
input
.
getOperation
().
getId
();
ii
.
inputProductId
=
input
.
getInputProduct
().
getId
();
ii
.
inputProductCode
=
input
.
getInputProduct
().
getCode
();
ii
.
inputSpId
=
input
.
getInputSp
().
getId
();
ii
.
inputSpName
=
input
.
getInputSp
().
getName
();
ii
.
factor
=
input
.
getFactor
();
s
.
operationInputs
.
add
(
ii
);
}
for
(
Period
p
:
data
.
getPeriods
())
{
BomStructureData
.
PeriodInfo
pi
=
new
BomStructureData
.
PeriodInfo
();
pi
.
index
=
p
.
getIndex
();
pi
.
name
=
p
.
getName
();
pi
.
startDate
=
p
.
getStartDate
()
==
null
?
null
:
p
.
getStartDate
().
toString
();
s
.
periods
.
add
(
pi
);
}
for
(
InitialInventory
inv
:
data
.
getInitialInventories
())
{
BomStructureData
.
InitialInventoryInfo
ii
=
new
BomStructureData
.
InitialInventoryInfo
();
ii
.
productId
=
inv
.
getProduct
().
getId
();
ii
.
spId
=
inv
.
getStockingPoint
().
getId
();
ii
.
quantity
=
inv
.
getQuantity
();
s
.
initialInventories
.
add
(
ii
);
}
for
(
InTransitSupply
its
:
data
.
getInTransitSupplies
())
{
BomStructureData
.
InTransitSupplyInfo
ti
=
new
BomStructureData
.
InTransitSupplyInfo
();
ti
.
productId
=
its
.
getProduct
().
getId
();
ti
.
spId
=
its
.
getStockingPoint
().
getId
();
ti
.
arrivalDate
=
its
.
getArrivalDate
()
==
null
?
null
:
its
.
getArrivalDate
().
toString
();
ti
.
quantity
=
its
.
getQuantity
();
s
.
inTransitSupplies
.
add
(
ti
);
}
//
for (Period p : data.getPeriods()) {
//
BomStructureData.PeriodInfo pi = new BomStructureData.PeriodInfo();
//
pi.index = p.getIndex();
//
pi.name = p.getName();
//
pi.startDate = p.getStartDate() == null ? null : p.getStartDate().toString();
//
s.periods.add(pi);
//
}
//
for (InitialInventory inv : data.getInitialInventories()) {
//
BomStructureData.InitialInventoryInfo ii = new BomStructureData.InitialInventoryInfo();
//
ii.productId = inv.getProduct().getId();
//
ii.spId = inv.getStockingPoint().getId();
//
ii.quantity = inv.getQuantity();
//
s.initialInventories.add(ii);
//
}
//
for (InTransitSupply its : data.getInTransitSupplies()) {
//
BomStructureData.InTransitSupplyInfo ti = new BomStructureData.InTransitSupplyInfo();
//
ti.productId = its.getProduct().getId();
//
ti.spId = its.getStockingPoint().getId();
//
ti.arrivalDate = its.getArrivalDate() == null ? null : its.getArrivalDate().toString();
//
ti.quantity = its.getQuantity();
//
s.inTransitSupplies.add(ti);
//
}
return
s
;
}
...
...
src/main/java/com/aps/macroplanner/output/dto/BomStructureData.java
View file @
be4b7722
...
...
@@ -64,6 +64,7 @@ public class BomStructureData {
public
static
class
OperationInputInfo
{
public
String
operationId
;
public
String
inputProductId
;
public
String
inputProductCode
;
public
String
inputSpId
;
public
String
inputSpName
;
public
double
factor
;
...
...
src/main/java/com/aps/macroplanner/output/dto/TraceNode.java
0 → 100644
View file @
be4b7722
package
com
.
aps
.
macroplanner
.
output
.
dto
;
/**
* 作者:佟礼
* 时间:2026-09-20
*/
import
java.util.ArrayList
;
import
java.util.List
;
/**
* 产品数量追溯网络节点。
*
* <pre>
* Product + Warehouse
* |
* | demand
* v
* Unit
* |
* | productionQty
* v
* BOM
* |
* v
* Product + Warehouse
* |
* v
* 最近历史 Output
* </pre>
*/
public
class
TraceNode
{
/**
* 产品 ID
*/
public
String
productId
;
/**
* 产品编码
*/
public
String
productCode
;
/**
* 库房 / SP
*/
public
String
warehouseId
;
/**
* 库房名称
*/
public
String
warehouseName
;
/**
* 当前节点需求数量。
*
* 根节点:
* 成品需求量
*
* 子节点:
* Unit生产量 × BOM factor
*/
public
double
quantity
;
/**
* BOM factor。
*
* 根节点一般为 1。
*/
public
double
factor
=
1.0
;
/**
* 当前节点匹配到的 Output 总数量。
*
* 注意:
* outputQty 可以 > quantity。
*/
public
double
outputQty
;
/**
* 当前节点库存/库房已有供应量。
*
* 这里表示没有通过生产 Output 继续追溯的数量。
*/
public
double
inventorySupplyQty
;
/**
* 当前节点匹配到的生产 Output。
*
* 一个产品可能对应多个 Output。
*/
// public final List<OutputNode> outputs = new ArrayList<>();
/**
* 当前节点对应的 Unit。
*
* 不同 Unit 必须独立。
*/
public
final
List
<
UnitNode
>
units
=
new
ArrayList
<>();
/**
* 实际生产 Output 节点。
*/
public
static
class
OutputNode
{
/**
* Output 对应的 dataKey。
*/
public
String
dataKey
;
/**
* 产品 ID。
*/
public
String
productId
;
/**
* 产品 Code。
*/
public
String
productCode
;
/**
* 库房。
*/
public
String
warehouseId
;
public
String
warehouseName
;
/**
* Output 日期/期间。
*/
public
Integer
periodIndex
;
/**
* Output 日期。
*
* 如果项目实际使用 LocalDate/LocalDateTime,
* 可以替换这里的类型。
*/
public
String
outputDate
;
/**
* Output 实际数量。
*
* 可以大于当前 demand。
*/
public
double
outputQty
;
/**
* 对应生产 Unit。
*/
public
UnitNode
unit
;
}
/**
* 独立生产 Unit 节点。
*
* 每个 Unit 单独展开 BOM。
*/
public
static
class
UnitNode
{
public
String
unitId
;
public
String
unitName
;
/**
* 当前 Unit 的实际生产量。
*/
public
double
productionQty
;
/**
* 产能占用。
*/
public
double
capacityUsed
;
/**
* 工序。
*/
public
String
operationId
;
public
String
operationName
;
/**
* 当前 Unit 的 BOM 上游节点。
*/
public
final
List
<
TraceNode
>
children
=
new
ArrayList
<>();
}
}
src/main/java/com/aps/macroplanner/output/dto/TraceOutput.java
0 → 100644
View file @
be4b7722
package
com
.
aps
.
macroplanner
.
output
.
dto
;
/**
* 作者:佟礼
* 时间:2026-09-20
* 数量追溯用的实际生产Output。
*
* OutputInfo + PeriodTaskResult 合并。
*/
public
class
TraceOutput
{
public
String
productId
;
public
String
warehouseId
;
public
String
warehouseName
;
public
String
dataKey
;
public
Integer
periodIndex
;
public
double
outputQty
;
public
String
outputDate
;
public
String
operationId
;
public
String
operationName
;
public
String
unitId
;
public
String
unitName
;
public
double
capacityUsed
;
}
src/main/java/com/aps/service/MacroPlannerProductNetworkService.java
View file @
be4b7722
package
com
.
aps
.
service
;
import
com.aps.common.util.FlatParquetUtil
;
import
com.aps.macroplanner.output.ResultWriter
;
import
com.aps.macroplanner.output.dto.*
;
import
com.fasterxml.jackson.databind.ObjectMapper
;
import
lombok.extern.slf4j.Slf4j
;
...
...
@@ -31,415 +32,930 @@ public class MacroPlannerProductNetworkService {
private
static
final
String
RESULT_DIR
=
"mp/result"
;
private
final
ObjectMapper
objectMapper
=
new
ObjectMapper
();
/**
* 按需组装 productId@spId 节点及其 BOM 子树。
* 构建产品数量追溯网络。
*
* 追溯逻辑:
*
* <pre>
*
* P + 库房P
* |
* | 需求100
* v
* P实际Output
* |
* +---------+
* | |
* UnitA30 UnitB70
* | |
* | |
* v v
* BOM×30 BOM×70
* | |
* v v
* M1+库房M1 M1+库房M1
* | |
* | 找 <= 当前日期最近Output
* v
* M1实际Output
* |
* v
* M1 Unit
* |
* v
* M1 BOM
*
* </pre>
*
* @param sceneId 场景ID
* @param productId 目标产品ID (必填)
* @param spId 目标库存点ID (必填)
* @param period 可选周期过滤 (周期索引的数字字符串, 如 "3")
* @return 目标节点 (含子树), 或 null
* @param productId 根产品ID
* @param warehouseId 根库房ID
* @param period 周期
*/
public
SupplyChainNode
buildProductNetwork
(
String
sceneId
,
String
productId
,
String
spId
,
String
period
)
{
if
(
isBlank
(
sceneId
)
||
isBlank
(
productId
)
||
isBlank
(
spId
))
{
return
null
;
public
TraceNode
buildProductNetwork
(
String
sceneId
,
String
productId
,
String
warehouseId
,
String
period
)
{
if
(
isBlank
(
productId
))
{
throw
new
IllegalArgumentException
(
"productId不能为空"
);
}
if
(
isBlank
(
warehouseId
))
{
throw
new
IllegalArgumentException
(
"warehouseId不能为空"
);
}
try
{
Integer
periodIndex
=
isBlank
(
period
)
?
null
:
Integer
.
parseInt
(
period
.
trim
());
FlatParquetUtil
parquet
=
new
FlatParquetUtil
();
ResultWriter
rw
=
new
ResultWriter
();
List
<
PispipResult
>
Pispips
=
parquet
.
readAll
(
rw
.
getOptimizationPispip
(
sceneId
),
PispipResult
.
class
,
oi
->
oi
.
getProductId
().
equals
(
productId
)
&&
oi
.
getSpId
().
equals
(
warehouseId
)
);
double
quantity
=
Pispips
.
get
(
0
).
getSalesFulfilledQty
();
String
productCode
=
Pispips
.
get
(
0
).
getProductCode
();
String
spName
=
Pispips
.
get
(
0
).
getSpName
();
List
<
PeriodTaskResult
.
OutputInfo
>
allOutputs
=
parquet
.
readAll
(
rw
.
getOptimizationPeriodTaskOutput
(
sceneId
),
PeriodTaskResult
.
OutputInfo
.
class
);
List
<
PeriodTaskResult
>
allTasks
=
parquet
.
readAll
(
rw
.
getOptimizationPeriodTask
(
sceneId
),
PeriodTaskResult
.
class
);
// 1. 静态结构 (小, 全量读)
BomStructureData
structure
=
objectMapper
.
readValue
(
new
File
(
path
(
sceneId
,
"bom_structure.json"
)),
BomStructureData
.
class
);
StructureIndex
sidx
=
new
StructureIndex
(
structure
)
;
new
File
(
rw
.
getOptimizationBomStructure
(
sceneId
)),
BomStructureData
.
class
);
List
<
BomStructureData
.
OperationInputInfo
>
operationInputs
=
structure
.
operationInputs
;
// 2. 纯结构展开目标子树, 确定涉及的节点集合与工序集合
Set
<
String
>
nodeKeys
=
new
HashSet
<>();
Set
<
String
>
opIds
=
new
HashSet
<>();
collectSubtree
(
sidx
,
productId
,
spId
,
nodeKeys
,
opIds
);
/*
* --------------------------------------------------------
* 1. 把 OutputInfo + PeriodTaskResult 合并
* --------------------------------------------------------
*/
List
<
TraceOutput
>
traceOutputs
=
buildTraceOutputs
(
allOutputs
,
allTasks
);
/*
* --------------------------------------------------------
* 2. 建立:
*
* productId + warehouseId
* ↓
* List<TraceOutput>
* --------------------------------------------------------
*/
Map
<
String
,
List
<
TraceOutput
>>
outputIndex
=
buildOutputIndex
(
traceOutputs
);
// 3. 按需过滤读取求解值 (不全量加载)
FlatParquetUtil
parquet
=
new
FlatParquetUtil
();
/*
* --------------------------------------------------------
* 3. 建立:
*
* operationId
* ↓
* OperationInputInfo
* --------------------------------------------------------
*/
Map
<
String
,
List
<
BomStructureData
.
OperationInputInfo
>>
bomIndex
=
buildBomIndex
(
operationInputs
);
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
<>());
/*
* --------------------------------------------------------
* 4. 开始递归
* --------------------------------------------------------
*/
Set
<
String
>
path
=
new
HashSet
<>();
return
buildTraceNode
(
productId
,
warehouseId
,
quantity
,
1.0
,
period
,
outputIndex
,
bomIndex
,
path
,
productCode
,
spName
);
}
catch
(
Exception
e
)
{
log
.
warn
(
"按需组装产品网络失败: sceneId={}, productId={}, spId={}, error={}"
,
sceneId
,
productId
,
spId
,
e
.
getMessage
());
return
null
;
}
}
/**
* 构建一个产品+库房节点。
*
* 核心规则:
*
* 1. quantity = 当前需求量
*
* 2. 当前产品必须先匹配:
*
* productId + warehouseId
*
* 3. 只能找:
*
* outputDate <= demandDate
*
* 4. 在符合条件的Output中,
* 选择最近日期。
*
* 5. 找到的Output数量不进行min截断。
*
* 例如:
*
* demand = 60
* output = 80
*
* 则:
*
* quantity = 60
* outputQty = 80
*
* 6. Unit按照实际Output数量创建。
*
* 7. BOM按照Unit实际生产数量计算。
*/
private
TraceNode
buildTraceNode
(
String
productId
,
String
warehouseId
,
double
quantity
,
double
factor
,
String
demandDate
,
Map
<
String
,
List
<
TraceOutput
>>
outputIndex
,
Map
<
String
,
List
<
BomStructureData
.
OperationInputInfo
>>
bomIndex
,
Set
<
String
>
path
,
String
productCode
,
String
warehouseName
)
{
TraceNode
node
=
new
TraceNode
();
node
.
productId
=
productId
;
node
.
productCode
=
productCode
;
node
.
warehouseId
=
warehouseId
;
node
.
warehouseName
=
warehouseName
;
node
.
quantity
=
quantity
;
node
.
factor
=
factor
;
/*
* 防止:
*
* A -> B -> A
*/
String
pathKey
=
productId
+
"@"
+
warehouseId
+
"@"
+
demandDate
;
if
(!
path
.
add
(
pathKey
))
{
/*
* 出现循环BOM。
*
* 不再继续递归。
*/
node
.
inventorySupplyQty
=
quantity
;
private
String
path
(
String
sceneId
,
String
fileName
)
{
return
Paths
.
get
(
RESULT_DIR
,
sceneId
,
fileName
).
toAbsolutePath
().
toString
();
return
node
;
}
/**
* 纯结构遍历 BOM, 收集目标子树涉及的所有 productId@spId 与 operationId。
try
{
/*
* ----------------------------------------------------
* 1. 找最近Output
* ----------------------------------------------------
*/
private
void
collectSubtree
(
StructureIndex
sidx
,
String
productId
,
String
spId
,
Set
<
String
>
nodeKeys
,
Set
<
String
>
opIds
)
{
String
key
=
productId
+
"@"
+
spId
;
if
(!
nodeKeys
.
add
(
key
))
{
return
;
List
<
TraceOutput
>
matchedOutputs
=
findHistoricalOutputsForDemand
(
productId
,
warehouseId
,
demandDate
,
quantity
,
outputIndex
);
/*
* ----------------------------------------------------
* 没有生产Output:
*
* 当前需求由库存/库房提供。
* ----------------------------------------------------
*/
if
(
matchedOutputs
.
isEmpty
())
{
node
.
outputQty
=
0
;
node
.
inventorySupplyQty
=
quantity
;
return
node
;
}
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
);
/*
* ----------------------------------------------------
* 2. 创建Output节点
* ----------------------------------------------------
*/
for
(
TraceOutput
output
:
matchedOutputs
)
{
TraceNode
.
OutputNode
outputNode
=
new
TraceNode
.
OutputNode
();
outputNode
.
dataKey
=
output
.
dataKey
;
outputNode
.
productId
=
output
.
productId
;
outputNode
.
warehouseId
=
output
.
warehouseId
;
outputNode
.
warehouseName
=
output
.
warehouseName
;
outputNode
.
periodIndex
=
output
.
periodIndex
;
outputNode
.
outputDate
=
output
.
outputDate
;
outputNode
.
outputQty
=
output
.
outputQty
;
/*
* Output总数量。
*
* 注意:
* 不做min(quantity, outputQty)。
*/
node
.
outputQty
+=
output
.
outputQty
;
// node.outputs.add(
// outputNode
// );
/*
* ------------------------------------------------
* 3. Output -> Unit
* ------------------------------------------------
*/
TraceNode
.
UnitNode
unit
=
new
TraceNode
.
UnitNode
();
unit
.
unitId
=
output
.
unitId
;
unit
.
unitName
=
output
.
unitName
;
unit
.
productionQty
=
output
.
outputQty
;
unit
.
capacityUsed
=
output
.
capacityUsed
;
unit
.
operationId
=
output
.
operationId
;
unit
.
operationName
=
output
.
operationName
;
outputNode
.
unit
=
unit
;
node
.
units
.
add
(
unit
);
/*
* ------------------------------------------------
* 4. 当前Unit独立展开BOM
* ------------------------------------------------
*/
buildUnitBomChildren
(
unit
,
output
.
outputQty
,
output
.
outputDate
,
bomIndex
,
outputIndex
,
path
);
}
/*
* ----------------------------------------------------
* 5. 库房供应
*
* 例如:
*
* demand = 100
* output = 80
*
* 剩余20由库房供应。
*
* 如果:
*
* demand = 60
* output = 80
*
* 则库存供应 = 0。
* ----------------------------------------------------
*/
node
.
inventorySupplyQty
=
Math
.
max
(
0
,
quantity
-
node
.
outputQty
);
return
node
;
}
finally
{
path
.
remove
(
pathKey
);
}
}
// 消费者工序 (上层) 也需要, 用于查 operation_demands 的消耗量
for
(
BomStructureData
.
OperationInputInfo
in
:
sidx
.
consumersOf
(
productId
,
spId
))
{
opIds
.
add
(
in
.
operationId
);
/**
* 根据Unit的实际生产量展开BOM。
*
* 关键:
*
* materialDemand
* =
* unitProductionQty × bomFactor
*
* 注意:
*
* 这里创建的是:
*
* M1 + M1库房
*
* 节点。
*
* 然后该节点自己继续寻找:
*
* M1 + M1库房
* <= 当前Output日期
* 的最近Output。
*/
private
void
buildUnitBomChildren
(
TraceNode
.
UnitNode
unit
,
double
unitProductionQty
,
String
outputDate
,
Map
<
String
,
List
<
BomStructureData
.
OperationInputInfo
>>
bomIndex
,
Map
<
String
,
List
<
TraceOutput
>>
outputIndex
,
Set
<
String
>
path
)
{
if
(
unit
==
null
)
{
return
;
}
if
(
isBlank
(
unit
.
operationId
))
{
return
;
}
// ==================== 结构索引 ====================
/*
* 找当前Operation的BOM。
*/
List
<
BomStructureData
.
OperationInputInfo
>
inputs
=
bomIndex
.
get
(
unit
.
operationId
);
private
static
class
StructureIndex
{
final
Map
<
String
,
String
>
spNameById
=
new
HashMap
<>();
final
Map
<
String
,
List
<
BomStructureData
.
OperationInfo
>>
operationsByProductSp
=
new
HashMap
<>();
final
Map
<
String
,
BomStructureData
.
OperationInfo
>
operationById
=
new
HashMap
<>();
final
Map
<
String
,
List
<
BomStructureData
.
OperationInputInfo
>>
inputsByOperation
=
new
HashMap
<>();
final
Map
<
String
,
List
<
BomStructureData
.
OperationInputInfo
>>
inputsByProductSp
=
new
HashMap
<>();
final
Map
<
String
,
Double
>
initialInventory
=
new
HashMap
<>();
final
Map
<
String
,
List
<
BomStructureData
.
InTransitSupplyInfo
>>
inTransitByProductSp
=
new
HashMap
<>();
final
List
<
BomStructureData
.
PeriodInfo
>
periods
=
new
ArrayList
<>();
final
Map
<
Integer
,
String
>
startDateByPeriodIndex
=
new
HashMap
<>();
if
(
inputs
==
null
||
inputs
.
isEmpty
())
{
StructureIndex
(
BomStructureData
s
)
{
for
(
BomStructureData
.
StockingPointInfo
sp
:
s
.
stockingPoints
)
{
spNameById
.
put
(
sp
.
id
,
sp
.
name
);
}
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
);
}
}
for
(
BomStructureData
.
OperationInputInfo
in
:
s
.
operationInputs
)
{
inputsByOperation
.
computeIfAbsent
(
in
.
operationId
,
k
->
new
ArrayList
<>()).
add
(
in
);
inputsByProductSp
.
computeIfAbsent
(
in
.
inputProductId
+
"@"
+
in
.
inputSpId
,
k
->
new
ArrayList
<>()).
add
(
in
);
return
;
}
for
(
BomStructureData
.
InitialInventoryInfo
inv
:
s
.
initialInventories
)
{
initialInventory
.
put
(
inv
.
productId
+
"@"
+
inv
.
spId
,
inv
.
quantity
);
/*
* 当前Unit:
*
* 例如:
*
* UnitA = 30
*/
for
(
BomStructureData
.
OperationInputInfo
input
:
inputs
)
{
if
(
input
==
null
)
{
continue
;
}
for
(
BomStructureData
.
InTransitSupplyInfo
its
:
s
.
inTransitSupplies
)
{
inTransitByProductSp
.
computeIfAbsent
(
its
.
productId
+
"@"
+
its
.
spId
,
k
->
new
ArrayList
<>()).
add
(
its
);
if
(
isBlank
(
input
.
inputProductId
))
{
continue
;
}
for
(
BomStructureData
.
PeriodInfo
p
:
s
.
periods
)
{
periods
.
add
(
p
);
if
(
p
.
startDate
!=
null
)
{
startDateByPeriodIndex
.
put
(
p
.
index
,
p
.
startDate
);
if
(
isBlank
(
input
.
inputSpId
))
{
continue
;
}
if
(
input
.
factor
<=
0
)
{
continue
;
}
/*
* ------------------------------------------------
* BOM需求量
*
* Unit生产量 × BOM factor
* ------------------------------------------------
*/
double
materialDemandQty
=
unitProductionQty
*
input
.
factor
;
if
(
materialDemandQty
<=
0
)
{
continue
;
}
List
<
BomStructureData
.
OperationInfo
>
producing
(
String
productId
,
String
spId
)
{
return
operationsByProductSp
.
getOrDefault
(
productId
+
"@"
+
spId
,
Collections
.
emptyList
());
/*
* ------------------------------------------------
* 创建:
*
* M1 + M1库房
* ------------------------------------------------
*/
TraceNode
child
=
buildTraceNode
(
input
.
inputProductId
,
input
.
inputSpId
,
materialDemandQty
,
input
.
factor
,
outputDate
,
outputIndex
,
bomIndex
,
path
,
input
.
inputProductCode
,
input
.
inputSpName
);
/*
* 设置名称。
*/
child
.
warehouseName
=
input
.
inputSpName
;
unit
.
children
.
add
(
child
);
}
List
<
BomStructureData
.
OperationInputInfo
>
inputsOf
(
String
operationId
)
{
return
inputsByOperation
.
getOrDefault
(
operationId
,
Collections
.
emptyList
());
}
List
<
BomStructureData
.
OperationInputInfo
>
consumersOf
(
String
productId
,
String
spId
)
{
return
inputsByProductSp
.
getOrDefault
(
productId
+
"@"
+
spId
,
Collections
.
emptyList
());
/**
* 将:
*
* PeriodTaskResult.OutputInfo
* +
* PeriodTaskResult
*
* 合并成TraceOutput。
*/
private
List
<
TraceOutput
>
buildTraceOutputs
(
List
<
PeriodTaskResult
.
OutputInfo
>
allOutputs
,
List
<
PeriodTaskResult
>
allTasks
)
{
List
<
TraceOutput
>
result
=
new
ArrayList
<>();
if
(
allOutputs
==
null
||
allOutputs
.
isEmpty
())
{
return
result
;
}
double
initialInv
(
String
productId
,
String
spId
)
{
return
initialInventory
.
getOrDefault
(
productId
+
"@"
+
spId
,
0.0
);
/*
* dataKey -> PeriodTaskResult
*/
Map
<
String
,
List
<
PeriodTaskResult
>>
taskIndex
=
new
HashMap
<>();
if
(
allTasks
!=
null
)
{
for
(
PeriodTaskResult
task
:
allTasks
)
{
if
(
task
==
null
)
{
continue
;
}
List
<
BomStructureData
.
InTransitSupplyInfo
>
inTransit
(
String
productId
,
String
spId
)
{
return
inTransitByProductSp
.
getOrDefault
(
productId
+
"@"
+
spId
,
Collections
.
emptyList
());
if
(
isBlank
(
task
.
getDataKey
()))
{
continue
;
}
String
startDate
(
int
periodIndex
)
{
return
startDateByPeriodIndex
.
get
(
periodIndex
);
taskIndex
.
computeIfAbsent
(
task
.
getDataKey
(),
k
->
new
ArrayList
<>()
)
.
add
(
task
);
}
}
// ==================== 求解值索引 ====================
/*
* OutputInfo
*/
for
(
PeriodTaskResult
.
OutputInfo
output
:
allOutputs
)
{
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
<>();
if
(
output
==
null
)
{
continue
;
}
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
);
if
(
isBlank
(
output
.
productId
))
{
continue
;
}
for
(
PeriodTaskResult
pt
:
tasks
)
{
taskByDataKey
.
put
(
pt
.
getDataKey
(),
pt
);
if
(
isBlank
(
output
.
spId
))
{
continue
;
}
for
(
PispipResult
p
:
pispips
)
{
pispipByKey
.
put
(
p
.
getProductId
()
+
"_"
+
p
.
getSpId
()
+
"_"
+
p
.
getPeriodIndex
(),
p
);
if
(
isBlank
(
output
.
getDataKey
()))
{
continue
;
}
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
();
if
(
output
.
getProductionQty
()
<=
0
)
{
continue
;
}
for
(
OperationDemandResult
od
:
opDemands
)
{
opDemandByKey
.
put
(
od
.
getOperationId
()
+
"_"
+
od
.
getInputProductId
()
+
"_"
+
od
.
getInputSpId
()
+
"_"
+
od
.
getPeriodIndex
(),
od
.
getQuantity
());
List
<
PeriodTaskResult
>
tasks
=
taskIndex
.
get
(
output
.
getDataKey
());
/*
* 正常情况下:
*
* Output dataKey -> 一个Task
*
* 如果存在多个Task,则全部保留。
*/
for
(
PeriodTaskResult
task
:
tasks
)
{
TraceOutput
traceOutput
=
new
TraceOutput
();
traceOutput
.
productId
=
output
.
productId
;
traceOutput
.
warehouseId
=
output
.
spId
;
traceOutput
.
dataKey
=
output
.
getDataKey
();
traceOutput
.
periodIndex
=
output
.
getPeriodIndex
();
traceOutput
.
outputQty
=
output
.
getProductionQty
();
traceOutput
.
outputDate
=
resolveOutputDate
(
output
,
task
);
traceOutput
.
operationId
=
task
.
getOperationId
();
traceOutput
.
operationName
=
task
.
getOperationName
();
traceOutput
.
unitId
=
task
.
getUnitId
();
traceOutput
.
unitName
=
task
.
getUnitName
();
traceOutput
.
capacityUsed
=
task
.
getCapacityUsed
();
result
.
add
(
traceOutput
);
}
}
List
<
PeriodTaskResult
.
OutputInfo
>
outputsOf
(
String
productId
,
String
spId
)
{
return
outputsByProductSp
.
getOrDefault
(
productId
+
"@"
+
spId
,
Collections
.
emptyList
());
return
result
;
}
PeriodTaskResult
taskOf
(
String
dataKey
)
{
return
taskByDataKey
.
get
(
dataKey
);
/**
* 建立:
*
* productId + warehouseId
* ↓
* TraceOutput列表
*/
private
Map
<
String
,
List
<
TraceOutput
>>
buildOutputIndex
(
List
<
TraceOutput
>
outputs
)
{
Map
<
String
,
List
<
TraceOutput
>>
index
=
new
HashMap
<>();
if
(
outputs
==
null
)
{
return
index
;
}
PispipResult
pispip
(
String
productId
,
String
spId
,
int
periodIndex
)
{
return
pispipByKey
.
get
(
productId
+
"_"
+
spId
+
"_"
+
periodIndex
);
for
(
TraceOutput
output
:
outputs
)
{
if
(
output
==
null
)
{
continue
;
}
double
demandQty
(
String
productId
,
String
spId
,
int
periodIndex
)
{
double
[]
acc
=
salesByKey
.
get
(
productId
+
"_"
+
spId
+
"_"
+
periodIndex
);
return
acc
==
null
?
0.0
:
acc
[
0
];
String
key
=
buildProductWarehouseKey
(
output
.
productId
,
output
.
warehouseId
);
index
.
computeIfAbsent
(
key
,
k
->
new
ArrayList
<>()
)
.
add
(
output
);
}
double
fulfilledQty
(
String
productId
,
String
spId
,
int
periodIndex
)
{
double
[]
acc
=
salesByKey
.
get
(
productId
+
"_"
+
spId
+
"_"
+
periodIndex
);
return
acc
==
null
?
0.0
:
acc
[
1
];
/*
* 每个产品+库房按照日期排序。
*/
for
(
List
<
TraceOutput
>
list
:
index
.
values
())
{
list
.
sort
(
Comparator
.
comparing
(
o
->
o
.
outputDate
,
Comparator
.
nullsLast
(
Comparator
.
naturalOrder
()
)
)
);
}
double
opDemand
(
String
operationId
,
String
inputProductId
,
String
inputSpId
,
int
periodIndex
)
{
return
opDemandByKey
.
getOrDefault
(
operationId
+
"_"
+
inputProductId
+
"_"
+
inputSpId
+
"_"
+
periodIndex
,
0.0
);
return
index
;
}
/**
* 建立:
*
* operationId
* ↓
* OperationInputInfo列表
*/
private
Map
<
String
,
List
<
BomStructureData
.
OperationInputInfo
>>
buildBomIndex
(
List
<
BomStructureData
.
OperationInputInfo
>
operationInputs
)
{
Map
<
String
,
List
<
BomStructureData
.
OperationInputInfo
>>
index
=
new
HashMap
<>();
if
(
operationInputs
==
null
)
{
return
index
;
}
// ==================== 节点组装 ====================
for
(
BomStructureData
.
OperationInputInfo
input
:
operationInputs
)
{
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
))
{
return
null
;
// 防环
if
(
input
==
null
)
{
continue
;
}
SupplyChainNode
node
=
new
SupplyChainNode
();
node
.
setProductId
(
productId
);
node
.
setSpId
(
spId
);
node
.
setLevel
(
level
);
if
(
isBlank
(
input
.
operationId
))
{
continue
;
}
String
periodStartDate
=
periodIndex
==
null
?
null
:
sidx
.
startDate
(
periodIndex
);
if
(
isBlank
(
input
.
inputProductId
))
{
continue
;
}
// --- 供应来源: 以 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
());
if
(
isBlank
(
input
.
inputSpId
))
{
continue
;
}
PeriodTaskResult
pt
=
solveIdx
.
taskOf
(
oi
.
getDataKey
());
if
(
pt
==
null
)
continue
;
if
(
input
.
factor
<=
0
)
{
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
;
});
index
.
computeIfAbsent
(
input
.
operationId
,
k
->
new
ArrayList
<>()
)
.
add
(
input
);
}
node
.
getSupplySources
().
add
(
src
)
;
return
index
;
}
private
List
<
TraceOutput
>
findHistoricalOutputsForDemand
(
String
productId
,
String
warehouseId
,
String
demandDate
,
double
demandQty
,
Map
<
String
,
List
<
TraceOutput
>>
outputIndex
)
{
// --- 在途供应 ---
for
(
BomStructureData
.
InTransitSupplyInfo
its
:
sidx
.
inTransit
(
productId
,
spId
))
{
SupplyChainNode
.
SupplySource
src
=
new
SupplyChainNode
.
SupplySource
();
src
.
type
=
"IN_TRANSIT"
;
src
.
totalProduction
=
its
.
quantity
;
node
.
getSupplySources
().
add
(
src
);
if
(
isBlank
(
productId
)
||
isBlank
(
warehouseId
)
||
isBlank
(
demandDate
)
||
demandQty
<=
0
)
{
return
Collections
.
emptyList
();
}
// --- 外部采购 (无供应源) ---
if
(
node
.
getSupplySources
().
isEmpty
())
{
SupplyChainNode
.
SupplySource
src
=
new
SupplyChainNode
.
SupplySource
();
src
.
type
=
"EXTERNAL"
;
src
.
totalProduction
=
0
;
node
.
getSupplySources
().
add
(
src
);
String
key
=
buildProductWarehouseKey
(
productId
,
warehouseId
);
List
<
TraceOutput
>
candidates
=
outputIndex
.
get
(
key
);
if
(
candidates
==
null
||
candidates
.
isEmpty
())
{
return
Collections
.
emptyList
();
}
/*
* 日期分组:
*
* 09-19 -> [Output1, Output2]
* 09-18 -> [Output3]
* 09-17 -> [Output4, Output5]
*/
Map
<
String
,
List
<
TraceOutput
>>
byDate
=
new
HashMap
<>();
// --- BOM 子物料 (向下展开) ---
List
<
BomStructureData
.
OperationInfo
>
producingOps
=
sidx
.
producing
(
productId
,
spId
);
if
(!
producingOps
.
isEmpty
())
{
BomStructureData
.
OperationInfo
op
=
producingOps
.
get
(
0
);
for
(
BomStructureData
.
OperationInputInfo
in
:
sidx
.
inputsOf
(
op
.
id
))
{
SupplyChainNode
.
BomChild
child
=
new
SupplyChainNode
.
BomChild
();
child
.
productId
=
in
.
inputProductId
;
child
.
spId
=
in
.
inputSpId
;
child
.
spName
=
in
.
inputSpName
;
child
.
operationId
=
in
.
operationId
;
child
.
operationName
=
op
.
name
;
child
.
totalFactor
=
in
.
factor
;
for
(
TraceOutput
output
:
candidates
)
{
double
totalConsumed
=
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
;
child
.
consumedByPeriod
.
put
(
periodKey
,
consumed
);
}
if
(
output
==
null
)
{
continue
;
}
child
.
totalConsumedQty
=
totalConsumed
;
if
(
periodStartDate
!=
null
&&
!
child
.
consumedByPeriod
.
containsKey
(
periodStar
tDate
))
{
if
(
isBlank
(
output
.
outpu
tDate
))
{
continue
;
}
SupplyChainNode
childNode
=
buildNode
(
sidx
,
solveIdx
,
in
.
inputProductId
,
in
.
inputSpId
,
periodIndex
,
level
+
1
,
visited
);
if
(
childNode
==
null
)
{
// 只允许历史数据
if
(
compareDate
(
output
.
outputDate
,
demandDate
)
>
0
)
{
continue
;
}
for
(
SupplyChainNode
.
SupplySource
ss
:
childNode
.
getSupplySources
())
{
child
.
totalConsumedQty
=
ss
.
production
;
child
.
unitId
=
ss
.
unitId
;
child
.
unitName
=
ss
.
unitName
;
break
;
}
node
.
getChildren
().
add
(
child
);
byDate
.
computeIfAbsent
(
output
.
outputDate
,
k
->
new
ArrayList
<>()
)
.
add
(
output
);
}
if
(
byDate
.
isEmpty
())
{
return
Collections
.
emptyList
();
}
// --- 汇总 ---
fillSummary
(
sidx
,
solveIdx
,
node
,
productId
,
spId
);
/*
* 最近日期 → 最早日期
*/
List
<
String
>
dates
=
new
ArrayList
<>(
byDate
.
keySet
()
);
// period 剪枝
if
(
periodStartDate
!=
null
&&
!
node
.
getActivePeriods
().
contains
(
periodStartDate
))
{
return
null
;
}
dates
.
sort
(
Comparator
.
reverseOrder
());
return
node
;
List
<
TraceOutput
>
result
=
new
ArrayList
<>();
double
accumulatedQty
=
0.0
;
for
(
String
date
:
dates
)
{
List
<
TraceOutput
>
outputs
=
byDate
.
get
(
date
);
if
(
outputs
==
null
||
outputs
.
isEmpty
())
{
continue
;
}
private
void
fillSummary
(
StructureIndex
sidx
,
SolveIndex
solveIdx
,
SupplyChainNode
node
,
String
productId
,
String
spId
)
{
SupplySummary
s
=
new
SupplySummary
();
node
.
setSummary
(
s
);
/*
* 同一天所有 Unit / Output 都必须保留。
*/
outputs
.
sort
(
Comparator
.
comparing
(
o
->
o
.
dataKey
,
Comparator
.
nullsLast
(
Comparator
.
naturalOrder
()
)
)
);
for
(
TraceOutput
output
:
outputs
)
{
result
.
add
(
output
);
int
n
=
sidx
.
periods
.
size
();
double
totalEndingInv
=
0
;
for
(
BomStructureData
.
PeriodInfo
p
:
sidx
.
periods
)
{
PispipResult
pr
=
solveIdx
.
pispip
(
productId
,
spId
,
p
.
index
);
if
(
pr
!=
null
)
{
totalEndingInv
+=
pr
.
getEndingInventory
();
if
(
pr
.
getEndingInventory
()
>
0
)
{
node
.
getActivePeriods
().
add
(
p
.
startDate
==
null
?
String
.
valueOf
(
p
.
index
)
:
p
.
startDate
);
accumulatedQty
+=
output
.
outputQty
;
}
/*
* 一个日期全部处理完以后,
* 再判断是否已经满足需求。
*/
if
(
accumulatedQty
>=
demandQty
)
{
break
;
}
}
s
.
setInitialInventory
(
sidx
.
initialInv
(
productId
,
spId
));
PispipResult
last
=
solveIdx
.
pispip
(
productId
,
spId
,
n
-
1
);
s
.
setFinalInventory
(
last
==
null
?
0
:
last
.
getEndingInventory
());
s
.
setAverageInventory
(
n
>
0
?
totalEndingInv
/
n
:
0
);
return
result
;
}
/**
* 日期比较。
*
* 当前默认支持:
*
* yyyy-MM-dd
* yyyy-MM-dd HH:mm:ss
* yyyy-MM-dd'T'HH:mm:ss
*
* 如果你的项目有自己的日期类型,
* 建议这里直接替换。
*/
private
int
compareDate
(
String
d1
,
String
d2
)
{
// 生产汇总 (跨周期全量, 不受 period 过滤影响)
double
totalProduction
=
0
;
for
(
PeriodTaskResult
.
OutputInfo
oi
:
solveIdx
.
outputsOf
(
productId
,
spId
))
{
totalProduction
+=
oi
.
getProductionQty
();
if
(
d1
==
null
&&
d2
==
null
)
{
return
0
;
}
s
.
setTotalProduction
(
totalProduction
);
// 在途汇总
double
totalInTransit
=
0
;
for
(
BomStructureData
.
InTransitSupplyInfo
its
:
sidx
.
inTransit
(
productId
,
spId
))
{
totalInTransit
+=
its
.
quantity
;
if
(
d1
==
null
)
{
return
-
1
;
}
s
.
setTotalInTransit
(
totalInTransit
);
// 需求汇总
double
totalSalesDemand
=
0
;
double
totalSalesFulfilled
=
0
;
double
totalDepDemand
=
0
;
double
totalDemandFulf
=
0
;
double
totalSlack
=
0
;
for
(
BomStructureData
.
PeriodInfo
p
:
sidx
.
periods
)
{
PispipResult
pr
=
solveIdx
.
pispip
(
productId
,
spId
,
p
.
index
);
if
(
pr
!=
null
)
{
totalDepDemand
+=
pr
.
getDependentDemandQty
();
totalDemandFulf
+=
pr
.
getDemandFulfillment
();
totalSlack
+=
pr
.
getDemandSlack
();
if
(
pr
.
getDependentDemandQty
()
>
0
)
{
node
.
getActivePeriods
().
add
(
p
.
startDate
==
null
?
String
.
valueOf
(
p
.
index
)
:
p
.
startDate
);
if
(
d2
==
null
)
{
return
1
;
}
return
d1
.
compareTo
(
d2
);
}
totalSalesDemand
+=
solveIdx
.
demandQty
(
productId
,
spId
,
p
.
index
);
totalSalesFulfilled
+=
solveIdx
.
fulfilledQty
(
productId
,
spId
,
p
.
index
);
/**
* Product + Warehouse唯一Key。
*/
private
String
buildProductWarehouseKey
(
String
productId
,
String
warehouseId
)
{
return
String
.
valueOf
(
productId
)
+
"@"
+
String
.
valueOf
(
warehouseId
);
}
s
.
setTotalSalesDemand
(
totalSalesDemand
);
s
.
setTotalSalesFulfilled
(
totalSalesFulfilled
);
s
.
setTotalDependentDemand
(
totalDepDemand
);
s
.
setTotalDemandFulfillment
(
totalDemandFulf
);
s
.
setTotalDemandSlack
(
totalSlack
);
node
.
setTotalSalesDemand
(
totalSalesDemand
);
node
.
setTotalSalesFulfilled
(
totalSalesFulfilled
);
node
.
setTotalDependentDemand
(
totalDepDemand
);
/**
* 使用periodIndex作为追溯日期。
*
* 如果项目中periodIndex就是时间顺序,
* 直接使用它最可靠。
*/
private
String
resolveOutputDate
(
PeriodTaskResult
.
OutputInfo
output
,
PeriodTaskResult
task
)
{
// 库存规格偏差
double
belowTarget
=
0
;
double
belowMin
=
0
;
double
aboveMax
=
0
;
for
(
BomStructureData
.
PeriodInfo
p
:
sidx
.
periods
)
{
PispipResult
pr
=
solveIdx
.
pispip
(
productId
,
spId
,
p
.
index
);
if
(
pr
!=
null
)
{
belowTarget
+=
pr
.
getBelowTarget
();
belowMin
+=
pr
.
getBelowMin
();
aboveMax
+=
pr
.
getAboveMax
();
}
if
(
output
==
null
)
{
return
null
;
}
s
.
setTotalBelowTarget
(
belowTarget
);
s
.
setTotalBelowMin
(
belowMin
);
s
.
setTotalAboveMax
(
aboveMax
);
return
String
.
valueOf
(
task
.
getPeriodStartDate
()
);
}
/**
* 判断字符串为空。
*/
private
boolean
isBlank
(
String
value
)
{
private
boolean
isBlank
(
String
v
)
{
return
v
==
null
||
v
.
trim
().
isEmpty
();
return
value
==
null
||
value
.
trim
().
isEmpty
();
}
}
src/main/java/com/aps/service/MacroPlannerProductNetworkService1.java
0 → 100644
View file @
be4b7722
package
com
.
aps
.
service
;
import
com.aps.common.util.FlatParquetUtil
;
import
com.aps.macroplanner.output.dto.*
;
import
com.fasterxml.jackson.databind.ObjectMapper
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.stereotype.Service
;
import
java.io.File
;
import
java.nio.file.Paths
;
import
java.util.*
;
/**
* 产品生产网络按需查询服务。
*
* <p>不再在求解阶段预生成完整 BOM 网络, 而是查询时按 productId + spId 定位节点,
* 递归向下展开 BOM 子树。求解值从 parquet 按需过滤读取, 避免全量加载。</p>
*
* <p>数据流:</p>
* <ol>
* <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>
*/
@Service
@Slf4j
public
class
MacroPlannerProductNetworkService1
{
private
static
final
String
RESULT_DIR
=
"mp/result"
;
private
final
ObjectMapper
objectMapper
=
new
ObjectMapper
();
/**
* 按需组装 productId@spId 节点及其 BOM 子树。
*
* @param sceneId 场景ID
* @param productId 目标产品ID (必填)
* @param spId 目标库存点ID (必填)
* @param period 可选周期过滤 (周期索引的数字字符串, 如 "3")
* @return 目标节点 (含子树), 或 null
*/
public
SupplyChainNode
buildProductNetwork
(
String
sceneId
,
String
productId
,
String
spId
,
String
period
)
{
if
(
isBlank
(
sceneId
)
||
isBlank
(
productId
)
||
isBlank
(
spId
))
{
return
null
;
}
try
{
Integer
periodIndex
=
isBlank
(
period
)
?
null
:
Integer
.
parseInt
(
period
.
trim
());
// 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
());
return
null
;
}
}
private
String
path
(
String
sceneId
,
String
fileName
)
{
return
Paths
.
get
(
RESULT_DIR
,
sceneId
,
fileName
).
toAbsolutePath
().
toString
();
}
/**
* 纯结构遍历 BOM, 收集目标子树涉及的所有 productId@spId 与 operationId。
*/
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
,
BomStructureData
.
OperationInfo
>
operationById
=
new
HashMap
<>();
final
Map
<
String
,
List
<
BomStructureData
.
OperationInputInfo
>>
inputsByOperation
=
new
HashMap
<>();
final
Map
<
String
,
List
<
BomStructureData
.
OperationInputInfo
>>
inputsByProductSp
=
new
HashMap
<>();
final
Map
<
String
,
Double
>
initialInventory
=
new
HashMap
<>();
final
Map
<
String
,
List
<
BomStructureData
.
InTransitSupplyInfo
>>
inTransitByProductSp
=
new
HashMap
<>();
final
List
<
BomStructureData
.
PeriodInfo
>
periods
=
new
ArrayList
<>();
final
Map
<
Integer
,
String
>
startDateByPeriodIndex
=
new
HashMap
<>();
StructureIndex
(
BomStructureData
s
)
{
for
(
BomStructureData
.
StockingPointInfo
sp
:
s
.
stockingPoints
)
{
spNameById
.
put
(
sp
.
id
,
sp
.
name
);
}
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
);
}
}
for
(
BomStructureData
.
OperationInputInfo
in
:
s
.
operationInputs
)
{
inputsByOperation
.
computeIfAbsent
(
in
.
operationId
,
k
->
new
ArrayList
<>()).
add
(
in
);
inputsByProductSp
.
computeIfAbsent
(
in
.
inputProductId
+
"@"
+
in
.
inputSpId
,
k
->
new
ArrayList
<>()).
add
(
in
);
}
for
(
BomStructureData
.
InitialInventoryInfo
inv
:
s
.
initialInventories
)
{
initialInventory
.
put
(
inv
.
productId
+
"@"
+
inv
.
spId
,
inv
.
quantity
);
}
for
(
BomStructureData
.
InTransitSupplyInfo
its
:
s
.
inTransitSupplies
)
{
inTransitByProductSp
.
computeIfAbsent
(
its
.
productId
+
"@"
+
its
.
spId
,
k
->
new
ArrayList
<>()).
add
(
its
);
}
for
(
BomStructureData
.
PeriodInfo
p
:
s
.
periods
)
{
periods
.
add
(
p
);
if
(
p
.
startDate
!=
null
)
{
startDateByPeriodIndex
.
put
(
p
.
index
,
p
.
startDate
);
}
}
}
List
<
BomStructureData
.
OperationInfo
>
producing
(
String
productId
,
String
spId
)
{
return
operationsByProductSp
.
getOrDefault
(
productId
+
"@"
+
spId
,
Collections
.
emptyList
());
}
List
<
BomStructureData
.
OperationInputInfo
>
inputsOf
(
String
operationId
)
{
return
inputsByOperation
.
getOrDefault
(
operationId
,
Collections
.
emptyList
());
}
List
<
BomStructureData
.
OperationInputInfo
>
consumersOf
(
String
productId
,
String
spId
)
{
return
inputsByProductSp
.
getOrDefault
(
productId
+
"@"
+
spId
,
Collections
.
emptyList
());
}
double
initialInv
(
String
productId
,
String
spId
)
{
return
initialInventory
.
getOrDefault
(
productId
+
"@"
+
spId
,
0.0
);
}
List
<
BomStructureData
.
InTransitSupplyInfo
>
inTransit
(
String
productId
,
String
spId
)
{
return
inTransitByProductSp
.
getOrDefault
(
productId
+
"@"
+
spId
,
Collections
.
emptyList
());
}
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
=
salesByKey
.
get
(
productId
+
"_"
+
spId
+
"_"
+
periodIndex
);
return
acc
==
null
?
0.0
:
acc
[
0
];
}
double
fulfilledQty
(
String
productId
,
String
spId
,
int
periodIndex
)
{
double
[]
acc
=
salesByKey
.
get
(
productId
+
"_"
+
spId
+
"_"
+
periodIndex
);
return
acc
==
null
?
0.0
:
acc
[
1
];
}
double
opDemand
(
String
operationId
,
String
inputProductId
,
String
inputSpId
,
int
periodIndex
)
{
return
opDemandByKey
.
getOrDefault
(
operationId
+
"_"
+
inputProductId
+
"_"
+
inputSpId
+
"_"
+
periodIndex
,
0.0
);
}
}
// ==================== 节点组装 ====================
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
))
{
return
null
;
// 防环
}
SupplyChainNode
node
=
new
SupplyChainNode
();
node
.
setProductId
(
productId
);
node
.
setSpId
(
spId
);
node
.
setLevel
(
level
);
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
;
});
node
.
getSupplySources
().
add
(
src
);
}
// --- 在途供应 ---
for
(
BomStructureData
.
InTransitSupplyInfo
its
:
sidx
.
inTransit
(
productId
,
spId
))
{
SupplyChainNode
.
SupplySource
src
=
new
SupplyChainNode
.
SupplySource
();
src
.
type
=
"IN_TRANSIT"
;
src
.
totalProduction
=
its
.
quantity
;
node
.
getSupplySources
().
add
(
src
);
}
// --- 外部采购 (无供应源) ---
if
(
node
.
getSupplySources
().
isEmpty
())
{
SupplyChainNode
.
SupplySource
src
=
new
SupplyChainNode
.
SupplySource
();
src
.
type
=
"EXTERNAL"
;
src
.
totalProduction
=
0
;
node
.
getSupplySources
().
add
(
src
);
}
// --- BOM 子物料 (向下展开) ---
List
<
BomStructureData
.
OperationInfo
>
producingOps
=
sidx
.
producing
(
productId
,
spId
);
if
(!
producingOps
.
isEmpty
())
{
BomStructureData
.
OperationInfo
op
=
producingOps
.
get
(
0
);
for
(
BomStructureData
.
OperationInputInfo
in
:
sidx
.
inputsOf
(
op
.
id
))
{
SupplyChainNode
.
BomChild
child
=
new
SupplyChainNode
.
BomChild
();
child
.
productId
=
in
.
inputProductId
;
child
.
spId
=
in
.
inputSpId
;
child
.
spName
=
in
.
inputSpName
;
child
.
operationId
=
in
.
operationId
;
child
.
operationName
=
op
.
name
;
child
.
totalFactor
=
in
.
factor
;
double
totalConsumed
=
0
;
for
(
BomStructureData
.
PeriodInfo
p
:
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
;
child
.
consumedByPeriod
.
put
(
periodKey
,
consumed
);
}
}
child
.
totalConsumedQty
=
totalConsumed
;
if
(
periodStartDate
!=
null
&&
!
child
.
consumedByPeriod
.
containsKey
(
periodStartDate
))
{
continue
;
}
SupplyChainNode
childNode
=
buildNode
(
sidx
,
solveIdx
,
in
.
inputProductId
,
in
.
inputSpId
,
periodIndex
,
level
+
1
,
visited
);
if
(
childNode
==
null
)
{
continue
;
}
for
(
SupplyChainNode
.
SupplySource
ss
:
childNode
.
getSupplySources
())
{
child
.
totalConsumedQty
=
ss
.
production
;
child
.
unitId
=
ss
.
unitId
;
child
.
unitName
=
ss
.
unitName
;
break
;
}
node
.
getChildren
().
add
(
child
);
}
}
// --- 汇总 ---
fillSummary
(
sidx
,
solveIdx
,
node
,
productId
,
spId
);
// period 剪枝
if
(
periodStartDate
!=
null
&&
!
node
.
getActivePeriods
().
contains
(
periodStartDate
))
{
return
null
;
}
return
node
;
}
private
void
fillSummary
(
StructureIndex
sidx
,
SolveIndex
solveIdx
,
SupplyChainNode
node
,
String
productId
,
String
spId
)
{
SupplySummary
s
=
new
SupplySummary
();
node
.
setSummary
(
s
);
int
n
=
sidx
.
periods
.
size
();
double
totalEndingInv
=
0
;
for
(
BomStructureData
.
PeriodInfo
p
:
sidx
.
periods
)
{
PispipResult
pr
=
solveIdx
.
pispip
(
productId
,
spId
,
p
.
index
);
if
(
pr
!=
null
)
{
totalEndingInv
+=
pr
.
getEndingInventory
();
if
(
pr
.
getEndingInventory
()
>
0
)
{
node
.
getActivePeriods
().
add
(
p
.
startDate
==
null
?
String
.
valueOf
(
p
.
index
)
:
p
.
startDate
);
}
}
}
s
.
setInitialInventory
(
sidx
.
initialInv
(
productId
,
spId
));
PispipResult
last
=
solveIdx
.
pispip
(
productId
,
spId
,
n
-
1
);
s
.
setFinalInventory
(
last
==
null
?
0
:
last
.
getEndingInventory
());
s
.
setAverageInventory
(
n
>
0
?
totalEndingInv
/
n
:
0
);
// 生产汇总 (跨周期全量, 不受 period 过滤影响)
double
totalProduction
=
0
;
for
(
PeriodTaskResult
.
OutputInfo
oi
:
solveIdx
.
outputsOf
(
productId
,
spId
))
{
totalProduction
+=
oi
.
getProductionQty
();
}
s
.
setTotalProduction
(
totalProduction
);
// 在途汇总
double
totalInTransit
=
0
;
for
(
BomStructureData
.
InTransitSupplyInfo
its
:
sidx
.
inTransit
(
productId
,
spId
))
{
totalInTransit
+=
its
.
quantity
;
}
s
.
setTotalInTransit
(
totalInTransit
);
// 需求汇总
double
totalSalesDemand
=
0
;
double
totalSalesFulfilled
=
0
;
double
totalDepDemand
=
0
;
double
totalDemandFulf
=
0
;
double
totalSlack
=
0
;
for
(
BomStructureData
.
PeriodInfo
p
:
sidx
.
periods
)
{
PispipResult
pr
=
solveIdx
.
pispip
(
productId
,
spId
,
p
.
index
);
if
(
pr
!=
null
)
{
totalDepDemand
+=
pr
.
getDependentDemandQty
();
totalDemandFulf
+=
pr
.
getDemandFulfillment
();
totalSlack
+=
pr
.
getDemandSlack
();
if
(
pr
.
getDependentDemandQty
()
>
0
)
{
node
.
getActivePeriods
().
add
(
p
.
startDate
==
null
?
String
.
valueOf
(
p
.
index
)
:
p
.
startDate
);
}
}
totalSalesDemand
+=
solveIdx
.
demandQty
(
productId
,
spId
,
p
.
index
);
totalSalesFulfilled
+=
solveIdx
.
fulfilledQty
(
productId
,
spId
,
p
.
index
);
}
s
.
setTotalSalesDemand
(
totalSalesDemand
);
s
.
setTotalSalesFulfilled
(
totalSalesFulfilled
);
s
.
setTotalDependentDemand
(
totalDepDemand
);
s
.
setTotalDemandFulfillment
(
totalDemandFulf
);
s
.
setTotalDemandSlack
(
totalSlack
);
node
.
setTotalSalesDemand
(
totalSalesDemand
);
node
.
setTotalSalesFulfilled
(
totalSalesFulfilled
);
node
.
setTotalDependentDemand
(
totalDepDemand
);
// 库存规格偏差
double
belowTarget
=
0
;
double
belowMin
=
0
;
double
aboveMax
=
0
;
for
(
BomStructureData
.
PeriodInfo
p
:
sidx
.
periods
)
{
PispipResult
pr
=
solveIdx
.
pispip
(
productId
,
spId
,
p
.
index
);
if
(
pr
!=
null
)
{
belowTarget
+=
pr
.
getBelowTarget
();
belowMin
+=
pr
.
getBelowMin
();
aboveMax
+=
pr
.
getAboveMax
();
}
}
s
.
setTotalBelowTarget
(
belowTarget
);
s
.
setTotalBelowMin
(
belowMin
);
s
.
setTotalAboveMax
(
aboveMax
);
}
private
boolean
isBlank
(
String
v
)
{
return
v
==
null
||
v
.
trim
().
isEmpty
();
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment