Commit d1745f27 authored by Tong Li's avatar Tong Li

MP

parent ea3cc142
package com.aps.common.util; package com.aps.common.util;
/** /**
* 作者:佟礼 * 范式化扁平 Row POJO 读写 Parquet。
* 时间:2026-09-08 * 要求: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; import org.apache.avro.Schema;
...@@ -19,19 +28,263 @@ import org.apache.parquet.io.LocalOutputFile; ...@@ -19,19 +28,263 @@ import org.apache.parquet.io.LocalOutputFile;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URI; import java.net.URI;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.List; import java.util.List;
/** import java.util.Locale;
* 范式化扁平Row POJO读写Parquet import java.util.function.Predicate;
* 要求:POJO不要包含嵌套List集合,子List拆成独立子parquet import java.util.stream.Stream;
*/ import java.util.stream.StreamSupport;
public class FlatParquetUtil { 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 { static {
// Windows 下 Hadoop 需要 winutils.exe/hadoop.dll。 // 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) { if (System.getProperty("hadoop.home.dir") == null) {
String hadoopHome = System.getenv("HADOOP_HOME"); String hadoopHome = System.getenv("HADOOP_HOME");
if (hadoopHome != null && !hadoopHome.trim().isEmpty()) { if (hadoopHome != null && !hadoopHome.trim().isEmpty()) {
...@@ -44,15 +297,15 @@ public class FlatParquetUtil { ...@@ -44,15 +297,15 @@ public class FlatParquetUtil {
public FlatParquetUtil() { public FlatParquetUtil() {
conf = new Configuration(); conf = new Configuration();
// 使用本地文件系统,不依赖hadoop集群
conf.set("fs.file.impl", org.apache.hadoop.fs.LocalFileSystem.class.getName()); conf.set("fs.file.impl", org.apache.hadoop.fs.LocalFileSystem.class.getName());
// 关闭hadoop Shell的部分本地命令调用(Windows不需要winutils.exe)
conf.setBoolean("hadoop.native.lib", false); conf.setBoolean("hadoop.native.lib", false);
} }
/** // =========================
* 写入扁平对象列表到parquet(覆盖) // 写入
*/ // =========================
/** 写入扁平对象列表到 parquet(覆盖)。 */
public <T> void write(List<T> rows, String filePath, Class<T> clazz) throws IOException { public <T> void write(List<T> rows, String filePath, Class<T> clazz) throws IOException {
if (rows == null || rows.isEmpty()) { if (rows == null || rows.isEmpty()) {
return; return;
...@@ -60,45 +313,63 @@ public class FlatParquetUtil { ...@@ -60,45 +313,63 @@ public class FlatParquetUtil {
File file = resolvePath(filePath); File file = resolvePath(filePath);
File parent = file.getParentFile(); File parent = file.getParentFile();
if (parent != null && !parent.exists()) { if (parent != null && !parent.exists() && !parent.mkdirs() && !parent.exists()) {
parent.mkdirs(); throw new IOException("创建目录失败: " + parent);
} }
if (file.exists()) { if (file.exists() && !file.delete()) {
file.delete(); throw new IOException("删除旧 parquet 文件失败: " + file);
} }
Schema schema = ReflectData.AllowNull.get().getSchema(clazz); Schema schema = ReflectData.AllowNull.get().getSchema(clazz);
LocalOutputFile outputFile = new LocalOutputFile(file.toPath()); LocalOutputFile outputFile = new LocalOutputFile(file.toPath());
try(ParquetWriter<T> writer = AvroParquetWriter.<T>builder(outputFile) try (ParquetWriter<T> writer = AvroParquetWriter.<T>builder(outputFile)
.withSchema(schema) .withSchema(schema)
.withDataModel(ReflectData.AllowNull.get()) .withDataModel(ReflectData.AllowNull.get())
.withConf(conf) .withConf(conf)
.withCompressionCodec(CompressionCodecName.SNAPPY) .withCompressionCodec(CompressionCodecName.SNAPPY)
.withRowGroupSize(128 * 1024 * 1024) .withRowGroupSize(128 * 1024 * 1024)
.build()){ .build()) {
for(T row : rows){ for (T row : rows) {
writer.write(row); writer.write(row);
} }
} }
} }
private File resolvePath(String filePath) throws IOException { private File resolvePath(String filePath) throws IOException {
try { try {
if (filePath.startsWith("file:")) { if (filePath.startsWith("file:")) {
// 是URI格式,用标准URI解析,自动处理Windows盘符、空格、中文
return Paths.get(new URI(filePath)).toFile(); return Paths.get(new URI(filePath)).toFile();
} else {
// 纯本地路径
return new File(filePath);
} }
return new File(filePath);
} catch (Exception e) { } catch (Exception e) {
throw new IOException("路径解析失败: " + filePath, 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<>(); List<T> res = new ArrayList<>();
File file = resolvePath(filePath); File file = resolvePath(filePath);
if (!file.exists()) {
return res;
}
LocalInputFile inputFile = new LocalInputFile(file.toPath()); LocalInputFile inputFile = new LocalInputFile(file.toPath());
try(ParquetReader<T> reader = AvroParquetReader.<T>builder(inputFile) try(ParquetReader<T> reader = AvroParquetReader.<T>builder(inputFile)
...@@ -107,20 +378,576 @@ public class FlatParquetUtil { ...@@ -107,20 +378,576 @@ public class FlatParquetUtil {
.build()){ .build()){
T t; T t;
while ((t = reader.read()) != null) { while ((t = reader.read()) != null) {
res.add(t); if (filter == null || filter.test(t)) {
res.add(t);
}
} }
} }
return res; return res;
} }
/** /** 获取底层 ParquetReader,适合自定义逐行消费。 */
* 获取迭代器流式读取,不一次性全加载内存
*/
public <T> ParquetReader<T> getReader(String filePath, Class<T> clazz) throws IOException { public <T> ParquetReader<T> getReader(String filePath, Class<T> clazz) throws IOException {
Path path = new Path(filePath); File file = resolvePath(filePath);
return AvroParquetReader.<T>builder(path) LocalInputFile inputFile = new LocalInputFile(file.toPath());
return AvroParquetReader.<T>builder(inputFile)
.withDataModel(ReflectData.AllowNull.get()) .withDataModel(ReflectData.AllowNull.get())
.withConf(conf) .withConf(conf)
.build(); .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");
}
}
} }
package com.aps.controller; package com.aps.controller;
import com.aps.common.util.FlatParquetUtil;
import com.aps.common.util.ParamValidator; import com.aps.common.util.ParamValidator;
import com.aps.common.util.R; import com.aps.common.util.R;
import com.aps.entity.ApsDemandOrder; import com.aps.entity.ApsDemandOrder;
...@@ -30,6 +31,7 @@ import org.springframework.web.bind.annotation.RestController; ...@@ -30,6 +31,7 @@ import org.springframework.web.bind.annotation.RestController;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream;
/** /**
* MP排产结果前端接口 — 提供排产优化运行、结果查询的完整 API。 * MP排产结果前端接口 — 提供排产优化运行、结果查询的完整 API。
...@@ -117,42 +119,106 @@ public class MacroPlannerResultController { ...@@ -117,42 +119,106 @@ public class MacroPlannerResultController {
+ "支持按 productId / periodIndex 可选过滤。") + "支持按 productId / periodIndex 可选过滤。")
public R<Map<String, Object>> getSalesDemands( public R<Map<String, Object>> getSalesDemands(
@RequestParam("sceneId") @Parameter(description = "场景ID", required = true) String sceneId, @RequestParam("sceneId") @Parameter(description = "场景ID", required = true) String sceneId,
@RequestParam(value = "productId", required = false) @Parameter(description = "按产品ID过滤(可选)") String productId, @RequestParam(value = "productCode", required = false) @Parameter(description = "按产品过滤(可选)") String productCode,
@RequestParam(value = "periodIndex", required = false) @Parameter(description = "按周期索引过滤(可选)") Integer periodIndex) { @RequestParam(value = "productCateid", required = false) @Parameter(description = "产品类型", required = false) String productCateid
OptimizationResult result = loadResult(sceneId); ,@RequestParam(value = "pageNumber", required = false) @Parameter(description = "当前页数", required = false) Integer pageNumber,
if (result == null) { @RequestParam(value = "pageSize", required = false ) @Parameter(description = "每页数量", required = false) Integer pageSize) {
return R.failed("未找到场景 " + sceneId + " 的排产结果文件"); 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);
// 可选过滤 return R.ok(data);
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());
// 按 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()); if(productCode!=null&& productCode!="") {
data.put("totalDemand", totalDemand); stream = stream.filter(u -> u.getProductCode().contains(productCode));
data.put("totalFulfilled", totalFulfilled);
data.put("totalUnmet", totalUnmet); }
data.put("overallFulfillmentRate", totalDemand > 0 ? totalFulfilled / totalDemand : 1.0); if(categoryIds!=null&& categoryIds!="")
data.put("orders", filtered); {
return R.ok(data); 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 { ...@@ -216,44 +282,124 @@ public class MacroPlannerResultController {
+ "与 supplyChain 中的 productSummaries (跨周期求和) 不同, " + "与 supplyChain 中的 productSummaries (跨周期求和) 不同, "
+ "此接口返回每周期独立数据, 适合前端展开查看各周期详情。") + "此接口返回每周期独立数据, 适合前端展开查看各周期详情。")
public R<Map<String, Object>> getProductSummary( public R<Map<String, Object>> getProductSummary(
@RequestParam("sceneId") @Parameter(description = "场景ID", required = true) String sceneId) { @RequestParam("sceneId") @Parameter(description = "场景ID", required = true) String sceneId,
OptimizationResult result = loadResult(sceneId); @RequestParam(value = "productCode", required = false) @Parameter(description = "产品编号", required = false) String productCode,
if (result == null) { @RequestParam(value = "productCateid", required = false) @Parameter(description = "产品类型", required = false) String productCateid
return R.failed("未找到场景 " + sceneId + " 的排产结果文件"); ,@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(); if (pageSize==null||pageSize == 0) {
enrichPispipProductCodes(pispips); pageSize = 20;
}
if (pageNumber==null||pageNumber == 0) {
pageNumber = 1;
}
// 按 productId@spId 分组,保留每周期明细,不跨周期聚合 Map<String, Object> data= buildPispipSummarie(sceneId,productCode,productCateid,pageNumber,pageSize);
Map<String, List<PispipResult>> grouped = pispips != null
? pispips.stream().collect(Collectors.groupingBy(
p -> p.getProductId() + "@" + p.getSpId(),
LinkedHashMap::new,
Collectors.toList()))
: new LinkedHashMap<>();
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); 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产能使用情况: 按设备聚合各周期的产能占用和生产量。 * 获取unit产能使用情况: 按设备聚合各周期的产能占用和生产量。
* *
...@@ -275,12 +421,19 @@ public class MacroPlannerResultController { ...@@ -275,12 +421,19 @@ public class MacroPlannerResultController {
description = "按设备(unitId)聚合各周期的产能使用量(小时)与生产任务明细。" description = "按设备(unitId)聚合各周期的产能使用量(小时)与生产任务明细。"
+ "数据来源: PeriodTaskResult(每工序-设备-周期的生产量和产能占比)。") + "数据来源: PeriodTaskResult(每工序-设备-周期的生产量和产能占比)。")
public R<Map<String, Object>> getUnitCapacity( public R<Map<String, Object>> getUnitCapacity(
@RequestParam("sceneId") @Parameter(description = "场景ID", required = true) String sceneId) { @RequestParam("sceneId") @Parameter(description = "场景ID", required = true) String sceneId,
OptimizationResult result = loadResult(sceneId); @RequestParam(value = "unitId", required = false) @Parameter(description = "设备ID", required = false) String unitId,
if (result == null) { @RequestParam(value = "shopId", required = false) @Parameter(description = "班组ID", required = false) String shopId
return R.failed("未找到场景 " + sceneId + " 的排产结果文件"); ,@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 { ...@@ -402,7 +555,6 @@ public class MacroPlannerResultController {
result.put("elapsedMs", System.currentTimeMillis() - t0); result.put("elapsedMs", System.currentTimeMillis() - t0);
return R.failed(result, "数据验证失败, 请检查错误详情"); return R.failed(result, "数据验证失败, 请检查错误详情");
} }
// 3. 加载 native libraries → 构建模型 → 分层求解 // 3. 加载 native libraries → 构建模型 → 分层求解
Loader.loadNativeLibraries(); Loader.loadNativeLibraries();
long solveStart = System.currentTimeMillis(); long solveStart = System.currentTimeMillis();
...@@ -413,7 +565,7 @@ public class MacroPlannerResultController { ...@@ -413,7 +565,7 @@ public class MacroPlannerResultController {
// 4. 保存结果到 JSON 文件 // 4. 保存结果到 JSON 文件
ResultWriter writer = new ResultWriter(optimizer.getModel(), optimizer.getData(), solveStart); ResultWriter writer = new ResultWriter(optimizer.getModel(), optimizer.getData(), solveStart);
OptimizationResult optimizationResult = writer.buildResult(); OptimizationResult optimizationResult = writer.buildResult(sid);
boolean saved = writer.saveResultToFile(sid, optimizationResult); boolean saved = writer.saveResultToFile(sid, optimizationResult);
if (saved) { if (saved) {
mpPispipResultPersistenceService.save(sid, optimizationResult); mpPispipResultPersistenceService.save(sid, optimizationResult);
...@@ -730,32 +882,67 @@ public class MacroPlannerResultController { ...@@ -730,32 +882,67 @@ public class MacroPlannerResultController {
*按产品聚合库存汇总 *按产品聚合库存汇总
* <p>数据来源: {@link UnitCapacityResult} — 按设备聚合, 含 maxCapacity / usedCapacity / 利用率。</p> * <p>数据来源: {@link UnitCapacityResult} — 按设备聚合, 含 maxCapacity / usedCapacity / 利用率。</p>
*/ */
private Map<String, Object> buildUnitCapacity(OptimizationResult result) { private Map<String, Object> buildUnitCapacity(String sceneId,String unitId,String shopId,int pageNumber, int pageSize) {
Map<String, Object> uc = new LinkedHashMap<>(); try {
List<UnitCapacityResult> unitCapacities = result.getUnitCapacities(); Map<String, Object> uc = new LinkedHashMap<>();
if (unitCapacities == null || unitCapacities.isEmpty()) { FlatParquetUtil parquetUtil = new FlatParquetUtil();
uc.put("unitCount", 0); ResultWriter rw = new ResultWriter();
uc.put("totalTaskCount", 0); List<FlatParquetUtil.FilterCondition<?>> filters = new ArrayList<>();
uc.put("units", Collections.emptyList());
return uc; 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<>(); List<FlatParquetUtil.SortCondition> sorts = Arrays.asList(
int totalTasks = 0; FlatParquetUtil.SortCondition.asc("unitId")
for (UnitCapacityResult ucr : unitCapacities) {
Map<String, Object> ud = new LinkedHashMap<>(); );
ud.put("unitId", ucr.getUnitId()); FlatParquetUtil.QueryRequest request =
ud.put("unitName", ucr.getUnitName()); new FlatParquetUtil.QueryRequest(
ud.put("operationIds", ucr.getOperationIds()); FlatParquetUtil.PageRequest.of(pageNumber, pageSize),
ud.put("totalMaxCapacity", ucr.getTotalMaxCapacity()); filters,
ud.put("totalCapacityUsed", ucr.getTotalCapacityUsed()); sorts
ud.put("totalUtilizationRate", ucr.getTotalUtilizationRate()); );
ud.put("totalProduction", ucr.getTotalProduction()); FlatParquetUtil.PageResult<UnitCapacityResult> result =
parquetUtil.queryPage(
// 周期明细 (含利用率) rw.getOptimizationUnitCapacitie(sceneId),
List<Map<String, Object>> pdList = new ArrayList<>(); UnitCapacityResult.class,
for (UnitCapacityResult.UnitPeriodDetail pd : ucr.getPeriodDetails()) { 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<>(); Map<String, Object> pdm = new LinkedHashMap<>();
pdm.put("periodIndex", pd.periodIndex); pdm.put("periodIndex", pd.periodIndex);
pdm.put("periodStartDate", pd.periodStartDate); pdm.put("periodStartDate", pd.periodStartDate);
...@@ -763,19 +950,53 @@ public class MacroPlannerResultController { ...@@ -763,19 +950,53 @@ public class MacroPlannerResultController {
pdm.put("capacityUsed", pd.capacityUsed); pdm.put("capacityUsed", pd.capacityUsed);
pdm.put("utilizationRate", pd.utilization); pdm.put("utilizationRate", pd.utilization);
pdm.put("production", pd.production); pdm.put("production", pd.production);
pdm.put("periodTasks", pd.tasks);
totalTasks += pd.tasks.size();
pdList.add(pdm); pdList.add(pdm);
} }
ud.put("periodDetails", pdList); ud.put("periodDetails", pdList);
unitList.add(ud); unitList.add(ud);
} }
uc.put("unitCount", unitCapacities.size()); uc.put("unitCount", result.getTotalMatched());
uc.put("totalTaskCount", totalTasks); // uc.put("totalTaskCount", totalTasks);
uc.put("units", unitList); uc.put("units", unitList);
return uc; 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 + 求解器统计 ==================== // ==================== 3. 汇总: KPI + 求解器统计 ====================
......
...@@ -33,6 +33,8 @@ public class Material { ...@@ -33,6 +33,8 @@ public class Material {
*/ */
private double CurrentStock; private double CurrentStock;
private Long categoryId=1l;
/** /**
* 库存详情 * 库存详情
*/ */
......
...@@ -4,6 +4,8 @@ import com.aps.ApsApplication; ...@@ -4,6 +4,8 @@ import com.aps.ApsApplication;
import com.aps.macroplanner.data.DataValidator; import com.aps.macroplanner.data.DataValidator;
import com.aps.macroplanner.data.MacroPlannerDataConverter; import com.aps.macroplanner.data.MacroPlannerDataConverter;
import com.aps.macroplanner.data.TestDataBuilder; 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 com.google.ortools.Loader;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
...@@ -77,6 +79,10 @@ public class MacroPlannerDataConverterRunner { ...@@ -77,6 +79,10 @@ public class MacroPlannerDataConverterRunner {
optimizer.buildModel(); optimizer.buildModel();
optimizer.solve(); 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 ====="); System.out.println("===== MACROPLANNER DATA CONVERTER RUNNER END =====");
} finally { } finally {
SpringApplication.exit(ctx); SpringApplication.exit(ctx);
......
...@@ -56,38 +56,41 @@ public class MultiLevelBomTestRunner { ...@@ -56,38 +56,41 @@ public class MultiLevelBomTestRunner {
// 通过 Spring 容器获取 service 实例 (static main 中 @Autowired 不会生效) // 通过 Spring 容器获取 service 实例 (static main 中 @Autowired 不会生效)
ApplicationContext ctx = SpringApplication.run(ApsApplication.class, args); ApplicationContext ctx = SpringApplication.run(ApsApplication.class, args);
try { try {
long startTime = System.currentTimeMillis();
// MultiLevelBomTestDataBuilder data=new MultiLevelBomTestDataBuilder(); 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 + "=====");
data.init(); data.init();
writeLog("Data loaded: " + data.getProducts().size() + " products, "
+ data.getOperations().size() + " operations");
long startTime = System.currentTimeMillis();
MacroPlannerOptimizer optimizer = new MacroPlannerOptimizer(data); MacroPlannerOptimizer optimizer = new MacroPlannerOptimizer(data);
optimizer.buildModel(); optimizer.buildModel();
optimizer.solve(String.valueOf(scale)); optimizer.solve("bom");
// 构建结果并调用 save 入库
ResultWriter writer = new ResultWriter(optimizer.getModel(), optimizer.getData(), startTime); ResultWriter writer = new ResultWriter(optimizer.getModel(), optimizer.getData(), startTime);
OptimizationResult result = writer.buildResult(String.valueOf(scale)); OptimizationResult result = writer.buildResult("bom");
boolean jsonPath = writer.saveResultToFile(String.valueOf(scale), result); boolean jsonPath = writer.saveResultToFile("bom", result);
writeLog("PISPIP result saved: "); // MpPispipResultPersistenceService service =
int saved = service.save(String.valueOf(scale), result); // ctx.getBean(MpPispipResultPersistenceService.class);
writeLog("PISPIP result saved: " + saved); //
// 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 ====="); writeLog("===== TEST RUNNER END =====");
} catch (Exception e) { } catch (Exception e) {
......
...@@ -24,6 +24,7 @@ import com.aps.mapper.StockMapper; ...@@ -24,6 +24,7 @@ import com.aps.mapper.StockMapper;
import com.aps.service.ApsTimeConfigService; import com.aps.service.ApsTimeConfigService;
import com.aps.service.LanuchService; import com.aps.service.LanuchService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import io.swagger.v3.oas.models.security.SecurityScheme;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
...@@ -47,6 +48,7 @@ import java.util.Map; ...@@ -47,6 +48,7 @@ import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
...@@ -429,7 +431,7 @@ public class MacroPlannerDataConverter { ...@@ -429,7 +431,7 @@ public class MacroPlannerDataConverter {
material.setName(m.getName()); material.setName(m.getName());
material.setMaxProduction(m.getMaxProduction()); material.setMaxProduction(m.getMaxProduction());
material.setMinProduction(m.getMinProduction()); material.setMinProduction(m.getMinProduction());
material.setCategoryId(m.getCategoryId());
// 库存 // 库存
List<Stock> materialStocks = stocksByMaterialId.getOrDefault(m.getId(), Collections.emptyList()); List<Stock> materialStocks = stocksByMaterialId.getOrDefault(m.getId(), Collections.emptyList());
if (!materialStocks.isEmpty()) { if (!materialStocks.isEmpty()) {
...@@ -700,6 +702,7 @@ public class MacroPlannerDataConverter { ...@@ -700,6 +702,7 @@ public class MacroPlannerDataConverter {
for (Material m : ctx.materialByMaterialId.values()) { for (Material m : ctx.materialByMaterialId.values()) {
String name = pickName(m.getName(), m.getCode(), m.getId()); String name = pickName(m.getName(), m.getCode(), m.getId());
Product p = new Product(m.getCode(), name, m.getCode()); Product p = new Product(m.getCode(), name, m.getCode());
p.setCategoryId(m.getCategoryId());
products.add(p); products.add(p);
ctx.productByMaterialId.put(m.getId(), p); ctx.productByMaterialId.put(m.getId(), p);
} }
...@@ -842,6 +845,10 @@ public class MacroPlannerDataConverter { ...@@ -842,6 +845,10 @@ public class MacroPlannerDataConverter {
int hid = rd.getRoutingHeaderId().intValue(); int hid = rd.getRoutingHeaderId().intValue();
detailsByHeader.computeIfAbsent(hid, k -> new ArrayList<>()).add(rd); 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 -> detailsByHeader.values().forEach(list ->
list.sort(Comparator.comparing(rd -> list.sort(Comparator.comparing(rd ->
rd.getTaskSeq() == null ? Long.MAX_VALUE : rd.getTaskSeq()))); rd.getTaskSeq() == null ? Long.MAX_VALUE : rd.getTaskSeq())));
...@@ -887,13 +894,23 @@ public class MacroPlannerDataConverter { ...@@ -887,13 +894,23 @@ public class MacroPlannerDataConverter {
unitOps = equips.stream() unitOps = equips.stream()
.filter(e -> e.getEquipId() != null) .filter(e -> e.getEquipId() != null)
.map(e -> { .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 unitId = "EQUIP_" + e.getEquipId();
String unitName = e.getName(); String unitName = e.getName();
double capCoeff = (e.getDuration() != null) ? e.getDuration().doubleValue()/3600/e.getOutputQuantity().doubleValue() : 1.0; double capCoeff = (e.getDuration() != null) ? e.getDuration().doubleValue()/3600/e.getOutputQuantity().doubleValue() : 1.0;
boolean lotSize = e.getOneBatchQuantity() != null boolean lotSize = e.getOneBatchQuantity() != null
&& e.getOneBatchQuantity().compareTo(BigDecimal.ZERO) > 0; && e.getOneBatchQuantity().compareTo(BigDecimal.ZERO) > 0;
double lotSizeVal = lotSize ? e.getOneBatchQuantity().doubleValue() : 0.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()); .collect(Collectors.toList());
} else { } else {
...@@ -902,7 +919,7 @@ public class MacroPlannerDataConverter { ...@@ -902,7 +919,7 @@ public class MacroPlannerDataConverter {
String unitName = (rd.getEquipTypeId() != null ? rd.getEquipType() : "DEFAULT"); String unitName = (rd.getEquipTypeId() != null ? rd.getEquipType() : "DEFAULT");
double capCoeff = (rd.getRuntime() != null) ? rd.getRuntime().doubleValue() : 1.0; 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( Operation op = new Operation(
...@@ -1014,7 +1031,7 @@ public class MacroPlannerDataConverter { ...@@ -1014,7 +1031,7 @@ public class MacroPlannerDataConverter {
"采购-" + pickName(m.getName(), m.getId()) "采购-" + pickName(m.getName(), m.getId())
+ (mp.getSupplyName() != null ? "-" + mp.getSupplyName() : ""), + (mp.getSupplyName() != null ? "-" + mp.getSupplyName() : ""),
Collections.singletonList( 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), new OperationOutput(p, sp),
1.0, leadTimeDays); 1.0, leadTimeDays);
operations.add(procureOp); operations.add(procureOp);
...@@ -1029,7 +1046,7 @@ public class MacroPlannerDataConverter { ...@@ -1029,7 +1046,7 @@ public class MacroPlannerDataConverter {
"OP_PROCURE_" + m.getId(), "OP_PROCURE_" + m.getId(),
"采购-" + pickName(m.getName(), m.getId()), "采购-" + pickName(m.getName(), m.getId()),
Collections.singletonList( 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), new OperationOutput(p, sp),
1.0, 0); 1.0, 0);
operations.add(procureOp); operations.add(procureOp);
...@@ -1143,9 +1160,12 @@ public class MacroPlannerDataConverter { ...@@ -1143,9 +1160,12 @@ public class MacroPlannerDataConverter {
// 收集所有 unitId (遍历每个 Operation 的每个 UnitOperation) // 收集所有 unitId (遍历每个 Operation 的每个 UnitOperation)
Map<String,String> unitIds = new HashMap<>() ; Map<String,String> unitIds = new HashMap<>() ;
Map<String,Integer> unitShipIds = new HashMap<>() ;
for (Operation op : operations) { for (Operation op : operations) {
for (UnitOperation uo : op.getUnitOperations()) { for (UnitOperation uo : op.getUnitOperations()) {
unitIds.put(uo.getUnitId(),uo.getUnitName()); unitIds.put(uo.getUnitId(),uo.getUnitName());
unitShipIds.put(uo.getUnitId(),uo.getShopId());
} }
} }
...@@ -1187,7 +1207,9 @@ public class MacroPlannerDataConverter { ...@@ -1187,7 +1207,9 @@ public class MacroPlannerDataConverter {
int workDays = calculateWorkDays(periodStart, periodEnd); int workDays = calculateWorkDays(periodStart, periodEnd);
periodMaxCapacity = DEFAULT_DAILY_HOURS * Math.max(1, workDays); 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: {}", log.info("构建 Period: {} (dimension={}, horizonEnd={}), UnitPeriod: {}",
...@@ -1288,7 +1310,7 @@ public class MacroPlannerDataConverter { ...@@ -1288,7 +1310,7 @@ public class MacroPlannerDataConverter {
} else { } else {
// 无时间信息, 放入最后一个周期 // 无时间信息, 放入最后一个周期
salesDemands.add(new SalesDemand(p, sp, lastPeriod, qty, priority, 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; continue;
} }
...@@ -1310,7 +1332,7 @@ public class MacroPlannerDataConverter { ...@@ -1310,7 +1332,7 @@ public class MacroPlannerDataConverter {
if (overlapPeriods.isEmpty()) { if (overlapPeriods.isEmpty()) {
salesDemands.add(new SalesDemand(p, sp, lastPeriod, qty, priority, 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 { } else {
// 按重叠天数比例拆分需求量: 每期四舍五入取整, 最后一期补差保证总和等于总数 // 按重叠天数比例拆分需求量: 每期四舍五入取整, 最后一期补差保证总和等于总数
int totalQty = (int) Math.round(qty); int totalQty = (int) Math.round(qty);
...@@ -1327,7 +1349,7 @@ public class MacroPlannerDataConverter { ...@@ -1327,7 +1349,7 @@ public class MacroPlannerDataConverter {
allocated += periodQty; allocated += periodQty;
} }
salesDemands.add(new SalesDemand(p, sp, per, periodQty, priority, 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()));
} }
} }
} }
......
...@@ -47,10 +47,14 @@ public class MultiLevelBomTestDataBuilder extends TestDataBuilder { ...@@ -47,10 +47,14 @@ public class MultiLevelBomTestDataBuilder extends TestDataBuilder {
// === 产品: 成品 P1, P2 + 半成品 S1 + 原材料 R1, R2 === // === 产品: 成品 P1, P2 + 半成品 S1 + 原材料 R1, R2 ===
Product prodP1 = new Product("P1", "成品P1"); Product prodP1 = new Product("P1", "成品P1");
prodP1.setCategoryId(1l);
// Product prodP2 = new Product("P2", "成品P2"); // Product prodP2 = new Product("P2", "成品P2");
Product prodS1 = new Product("S1", "半成品S1"); Product prodS1 = new Product("S1", "半成品S1");
prodS1.setCategoryId(12l);
Product prodR1 = new Product("R1", "原材料R1"); Product prodR1 = new Product("R1", "原材料R1");
prodR1.setCategoryId(3l);
Product prodR2 = new Product("R2", "原材料R2"); Product prodR2 = new Product("R2", "原材料R2");
prodR2.setCategoryId(4l);
products.addAll(Arrays.asList(prodP1, prodS1, prodR1, prodR2)); products.addAll(Arrays.asList(prodP1, prodS1, prodR1, prodR2));
// === 库存点: 每种产品一个库存点 === // === 库存点: 每种产品一个库存点 ===
...@@ -117,7 +121,7 @@ public class MultiLevelBomTestDataBuilder extends TestDataBuilder { ...@@ -117,7 +121,7 @@ public class MultiLevelBomTestDataBuilder extends TestDataBuilder {
// === 初始库存: 全部从 0 开始 === // === 初始库存: 全部从 0 开始 ===
initialInventories.add(new InitialInventory(prodP1, spP1, 0.0)); initialInventories.add(new InitialInventory(prodP1, spP1, 0.0));
// initialInventories.add(new InitialInventory(prodP2, spP2, 0.0)); // initialInventories.add(new InitialInventory(prodP2, spP2, 0.0));
initialInventories.add(new InitialInventory(prodS1, spSemi, 0.0)); initialInventories.add(new InitialInventory(prodS1, spSemi, 40.0));
initialInventories.add(new InitialInventory(prodR1, spR1, 0.0)); initialInventories.add(new InitialInventory(prodR1, spR1, 0.0));
initialInventories.add(new InitialInventory(prodR2, spR2, 0.0)); initialInventories.add(new InitialInventory(prodR2, spR2, 0.0));
......
...@@ -69,7 +69,7 @@ public class Operation { ...@@ -69,7 +69,7 @@ public class Operation {
boolean hasLotSize, double lotSize, double qtpfactor, boolean hasLotSize, double lotSize, double qtpfactor,
int leadTimeDays) { int leadTimeDays) {
this(id, name, 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); output, relativeDuration, leadTimeDays);
} }
......
...@@ -18,9 +18,16 @@ public class OperationOutput { ...@@ -18,9 +18,16 @@ public class OperationOutput {
private final Product product; // 产出产品 private final Product product; // 产出产品
private final StockingPoint stockingPoint; // 产出到哪个库存点 private final StockingPoint stockingPoint; // 产出到哪个库存点
public final double factor;
public OperationOutput(Product product, StockingPoint stockingPoint) { public OperationOutput(Product product, StockingPoint stockingPoint) {
this(product,stockingPoint,1);
}
public OperationOutput(Product product, StockingPoint stockingPoint,double factor) {
this.product = product; this.product = product;
this.stockingPoint = stockingPoint; this.stockingPoint = stockingPoint;
this.factor=factor;
} }
public Product getProduct() { return product; } public Product getProduct() { return product; }
...@@ -28,9 +35,14 @@ public class OperationOutput { ...@@ -28,9 +35,14 @@ public class OperationOutput {
/** 便捷方法: 产品 ID */ /** 便捷方法: 产品 ID */
public String getProductId() { return product.getId(); } public String getProductId() { return product.getId(); }
public String getProductCode() { return product.getCode(); }
/** 便捷方法: 库存点 ID */ /** 便捷方法: 库存点 ID */
public String getSpId() { return stockingPoint.getId(); } public String getSpId() { return stockingPoint.getId(); }
public String getSpName() { return stockingPoint.getName(); }
@Override @Override
public String toString() { public String toString() {
return product.getId() + "@" + stockingPoint.getId(); return product.getId() + "@" + stockingPoint.getId();
......
...@@ -8,6 +8,8 @@ public class Product { ...@@ -8,6 +8,8 @@ public class Product {
private final String name; private final String name;
private final String code; private final String code;
private Long categoryId;
public Product(String id, String name) { public Product(String id, String name) {
this(id, name, id); this(id, name, id);
} }
...@@ -22,6 +24,10 @@ public class Product { ...@@ -22,6 +24,10 @@ public class Product {
public String getName() { return name; } public String getName() { return name; }
public String getCode() { return code; } public String getCode() { return code; }
public Long getCategoryId() { return categoryId; }
public void setCategoryId(Long val) { categoryId=val; }
@Override @Override
public String toString() { public String toString() {
return name + "1(" + id + ")"; return name + "1(" + id + ")";
......
...@@ -18,6 +18,8 @@ public class SalesDemand { ...@@ -18,6 +18,8 @@ public class SalesDemand {
private final LocalDate demandOrderDate; private final LocalDate demandOrderDate;
private final Long categoryId;
public SalesDemand(Product product, StockingPoint stockingPoint, Period period, public SalesDemand(Product product, StockingPoint stockingPoint, Period period,
double quantity, double priority) { double quantity, double priority) {
this(product, stockingPoint, period, quantity, priority, null,null); this(product, stockingPoint, period, quantity, priority, null,null);
...@@ -25,12 +27,13 @@ public class SalesDemand { ...@@ -25,12 +27,13 @@ public class SalesDemand {
public SalesDemand(Product product, StockingPoint stockingPoint, Period period, public SalesDemand(Product product, StockingPoint stockingPoint, Period period,
double quantity, double priority, String demandOrderId,LocalDate demandOrderDate) { 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,0l);
} }
public SalesDemand(Product product, StockingPoint stockingPoint, Period period, public SalesDemand(Product product, StockingPoint stockingPoint, Period period,
double quantity, double priority, String demandOrderId, double quantity, double priority, String demandOrderId,
String orderCode, String productCode,LocalDate demandOrderDate) { String orderCode, String productCode,LocalDate demandOrderDate,Long categoryId) {
this.product = product; this.product = product;
this.stockingPoint = stockingPoint; this.stockingPoint = stockingPoint;
this.period = period; this.period = period;
...@@ -40,6 +43,7 @@ public class SalesDemand { ...@@ -40,6 +43,7 @@ public class SalesDemand {
this.orderCode = orderCode; this.orderCode = orderCode;
this.productCode = productCode; this.productCode = productCode;
this.demandOrderDate=demandOrderDate; this.demandOrderDate=demandOrderDate;
this.categoryId= categoryId;
} }
public Product getProduct() { return product; } public Product getProduct() { return product; }
...@@ -50,7 +54,7 @@ public class SalesDemand { ...@@ -50,7 +54,7 @@ public class SalesDemand {
public String getDemandOrderId() { return demandOrderId; } public String getDemandOrderId() { return demandOrderId; }
public String getOrderCode() { return orderCode; } public String getOrderCode() { return orderCode; }
public String getProductCode() { return productCode; } public String getProductCode() { return productCode; }
public Long getCategoryId() { return categoryId; }
public LocalDate getDemandOrderDate() { return demandOrderDate; } public LocalDate getDemandOrderDate() { return demandOrderDate; }
public String getKey() { public String getKey() {
......
...@@ -17,14 +17,21 @@ public class UnitOperation { ...@@ -17,14 +17,21 @@ public class UnitOperation {
private final double lotSize; // 批次大小 private final double lotSize; // 批次大小
private final double qtpfactor; // QuantityToProcessFactor private final double qtpfactor; // QuantityToProcessFactor
private final Integer shopId;
public UnitOperation(String unitId,String unitName, double capacityCoeff, public UnitOperation(String unitId,String unitName, double capacityCoeff,
boolean hasLotSize, double lotSize, double qtpfactor) { 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.unitId = unitId;
this.unitName=unitName; this.unitName=unitName;
this.capacityCoeff = capacityCoeff; this.capacityCoeff = capacityCoeff;
this.hasLotSize = hasLotSize; this.hasLotSize = hasLotSize;
this.lotSize = lotSize; this.lotSize = lotSize;
this.qtpfactor = qtpfactor; this.qtpfactor = qtpfactor;
this.shopId=shopId;
} }
public String getUnitId() { return unitId; } public String getUnitId() { return unitId; }
...@@ -35,6 +42,8 @@ public class UnitOperation { ...@@ -35,6 +42,8 @@ public class UnitOperation {
public double getLotSize() { return lotSize; } public double getLotSize() { return lotSize; }
public double getQtpfactor() { return qtpfactor; } public double getQtpfactor() { return qtpfactor; }
public Integer getShopId() { return shopId; }
@Override @Override
public String toString() { public String toString() {
return unitId + "(产能" + capacityCoeff + "h/件" + return unitId + "(产能" + capacityCoeff + "h/件" +
......
...@@ -15,9 +15,11 @@ public class UnitPeriod { ...@@ -15,9 +15,11 @@ public class UnitPeriod {
private final boolean hasMinCapacity; // 是否有最小产能约束 private final boolean hasMinCapacity; // 是否有最小产能约束
private final boolean unlimited; // 产能是否无上限 (如原材料供应商) private final boolean unlimited; // 产能是否无上限 (如原材料供应商)
private final Integer shopId;
/** 完整构造器 */ /** 完整构造器 */
public UnitPeriod(String unitId,String unitName, Period period, double minCapacity, double maxCapacity, 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.unitId = unitId;
this.unitName=unitName; this.unitName=unitName;
this.period = period; this.period = period;
...@@ -25,16 +27,19 @@ public class UnitPeriod { ...@@ -25,16 +27,19 @@ public class UnitPeriod {
this.maxCapacity = unlimited ? Double.MAX_VALUE : maxCapacity; this.maxCapacity = unlimited ? Double.MAX_VALUE : maxCapacity;
this.hasMinCapacity = hasMinCapacity; this.hasMinCapacity = hasMinCapacity;
this.unlimited = unlimited; this.unlimited = unlimited;
this.shopId=shopId;
} }
/** 有限产能构造器 (向后兼容) */
public UnitPeriod(String unitId,String unitName, Period period, double minCapacity, double maxCapacity, boolean hasMinCapacity) { 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) { 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; } public String getUnitId() { return unitId; }
...@@ -47,6 +52,8 @@ public class UnitPeriod { ...@@ -47,6 +52,8 @@ public class UnitPeriod {
public boolean hasMinCapacity() { return hasMinCapacity; } public boolean hasMinCapacity() { return hasMinCapacity; }
public boolean isUnlimited() { return unlimited; } public boolean isUnlimited() { return unlimited; }
public Integer getShopId() { return shopId; }
public String getKey() { public String getKey() {
return unitId + "_" + period.getIndex(); return unitId + "_" + period.getIndex();
} }
......
...@@ -26,6 +26,7 @@ import java.nio.file.Paths; ...@@ -26,6 +26,7 @@ import java.nio.file.Paths;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.*; import java.util.*;
import java.util.stream.Collectors;
import java.util.zip.GZIPInputStream; import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream; import java.util.zip.GZIPOutputStream;
...@@ -75,7 +76,11 @@ public class ResultWriter { ...@@ -75,7 +76,11 @@ public class ResultWriter {
private String getFileExtension() { private String getFileExtension() {
return useCompression ? ".json.gz" : ".json"; return useCompression ? ".json.gz" : ".json";
} }
public ResultWriter() {
this.model = null;
this.data = null;
this.startTimeMs=0;
}
public ResultWriter(MacroPlannerModel model, TestDataBuilder data, long startTimeMs) { public ResultWriter(MacroPlannerModel model, TestDataBuilder data, long startTimeMs) {
this.model = model; this.model = model;
this.data = data; this.data = data;
...@@ -104,37 +109,59 @@ public class ResultWriter { ...@@ -104,37 +109,59 @@ public class ResultWriter {
} }
return resultDir; return resultDir;
} }
private File getOptimizationFile(String sceneId) { public File getOptimizationFile(String sceneId) {
File resultDir = getResultDirectory(); File resultDir = getResultDirectory();
String fileName = "optimization_result_" + sceneId + getFileExtension(); String fileName = "optimization_result_" + sceneId + getFileExtension();
return new File(resultDir, fileName); return new File(resultDir, fileName);
} }
public String getOptimizationPeriodTask(String sceneId) {
private String getOptimizationPeriodTask(String sceneId) {
File resultDir = getResultDirectory(sceneId); File resultDir = getResultDirectory(sceneId);
String fileName =resultDir.getAbsolutePath()+ "\\period_tasks.parquet"; String fileName =resultDir.getAbsolutePath()+ "\\period_tasks.parquet";
return fileName; 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); File resultDir = getResultDirectory(sceneId);
String fileName =resultDir.getAbsolutePath()+ "\\pispips.parquet"; String fileName =resultDir.getAbsolutePath()+ "\\pispips.parquet";
return fileName; 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); File resultDir = getResultDirectory(sceneId);
String fileName =resultDir.getAbsolutePath()+ "\\salesdemand.parquet"; String fileName =resultDir.getAbsolutePath()+ "\\salesdemand.parquet";
return fileName; 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); File resultDir = getResultDirectory(sceneId);
String fileName =resultDir.getAbsolutePath()+ "\\unitcapacitie.parquet"; String fileName =resultDir.getAbsolutePath()+ "\\unitcapacitie.parquet";
return fileName; 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); File resultDir = getResultDirectory(sceneId);
return resultDir.getAbsolutePath() + "\\bom_structure.json"; return resultDir.getAbsolutePath() + "\\bom_structure.json";
} }
private String getOptimizationOperationDemand(String sceneId) { public String getOptimizationOperationDemand(String sceneId) {
File resultDir = getResultDirectory(sceneId); File resultDir = getResultDirectory(sceneId);
return resultDir.getAbsolutePath() + "\\operation_demands.parquet"; return resultDir.getAbsolutePath() + "\\operation_demands.parquet";
} }
...@@ -167,15 +194,27 @@ public class ResultWriter { ...@@ -167,15 +194,27 @@ public class ResultWriter {
writeLog("writePeriodTasks"+result.getPeriodTasks().size()); writeLog("writePeriodTasks"+result.getPeriodTasks().size());
parquetUtil.write(result.getPeriodTasks(), periodTaskPath, PeriodTaskResult.class); parquetUtil.write(result.getPeriodTasks(), periodTaskPath, PeriodTaskResult.class);
writeLog("writePeriodTasks"); 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()); writeLog("writePispips:"+result.getPispips().size());
String pispiPath = getOptimizationPispip(sceneId); String pispiPath = getOptimizationPispip(sceneId);
parquetUtil.write(result.getPispips(), pispiPath, PispipResult.class); parquetUtil.write(result.getPispips(), pispiPath, PispipResult.class);
String pispiSummariePath = getOptimizationPispipSummarie(sceneId);
parquetUtil.write(result.getPispipSummaries(), pispiSummariePath, PispipResult.PispipSummarieResult.class);
writeLog("writePispips"); writeLog("writePispips");
writeLog("writesalesDemand"); writeLog("writesalesDemand");
String salesDemandPath = getOptimizationSalesDemand(sceneId); String salesDemandPath = getOptimizationSalesDemand(sceneId);
parquetUtil.write(result.getSalesDemands(), salesDemandPath, SalesDemandResult.class); parquetUtil.write(result.getSalesDemands(), salesDemandPath, SalesDemandResult.class);
// List<PispipResult> pispips = parquetUtil.readAll(pispiPath, PispipResult.class); // List<PispipResult> pispips = parquetUtil.readAll(pispiPath, PispipResult.class);
String salesDemandSummariePath = getOptimizationSalesDemandSummarie(sceneId);
parquetUtil.write(result.getSaleSummarieDemands(), salesDemandSummariePath, SalesDemandResult.class);
writeLog("writesalesDemand"); writeLog("writesalesDemand");
writeLog("UnitCapacities"); writeLog("UnitCapacities");
...@@ -183,6 +222,11 @@ public class ResultWriter { ...@@ -183,6 +222,11 @@ public class ResultWriter {
parquetUtil.write(result.getUnitCapacities(), unitPath, UnitCapacityResult.class); parquetUtil.write(result.getUnitCapacities(), unitPath, UnitCapacityResult.class);
writeLog("UnitCapacities"); writeLog("UnitCapacities");
writeLog("UnitPeriod");
String unitPeriodPath = getOptimizationUnitPeriodDetail(sceneId);
parquetUtil.write(result.getUnitPeriodDetails(), unitPeriodPath, UnitCapacityResult.UnitPeriodDetail.class);
writeLog("UnitPeriod");
// BOM 结构持久化 (供 getProductNetwork 按需组装) // BOM 结构持久化 (供 getProductNetwork 按需组装)
writeLog("writeBomStructure"); writeLog("writeBomStructure");
writeBomStructure(sceneId); writeBomStructure(sceneId);
...@@ -480,9 +524,12 @@ public class ResultWriter { ...@@ -480,9 +524,12 @@ public class ResultWriter {
writeLog("Pispips"); writeLog("Pispips");
buildPispips(result); buildPispips(result);
writeLog("UnitCapacitie"); writeLog("UnitCapacitie");
result.setUnitCapacities(buildUnitCapacities()); Map<String,Object> Unit= buildUnitCapacities();
// result.setProductNetwork(buildProductNetwork()); // relst.put("UnitCapacityDetails",unitPeriodDetails);
// result.setDemandSummary(buildDemandSummary());
result.setUnitCapacities((List<UnitCapacityResult>)Unit.get("UnitCapacity"));
result.setUnitPeriodDetails((List<UnitCapacityResult.UnitPeriodDetail>)Unit.get("UnitCapacityDetails"));
result.setDemandSummary(buildDemandSummary());
// KPI + 统计 // KPI + 统计
writeLog("Kpi"); writeLog("Kpi");
...@@ -508,6 +555,7 @@ public class ResultWriter { ...@@ -508,6 +555,7 @@ public class ResultWriter {
pt.setOperationId(op.getId()); pt.setOperationId(op.getId());
pt.setOperationName(op.getName()); pt.setOperationName(op.getName());
pt.setUnitId(uo.getUnitId()); pt.setUnitId(uo.getUnitId());
pt.setUnitName(uo.getUnitName());
pt.setPeriodIndex(p.getIndex()); pt.setPeriodIndex(p.getIndex());
pt.setPeriodStartDate(p.getStartDate().toString()); pt.setPeriodStartDate(p.getStartDate().toString());
pt.setProductionQty(ptQty); pt.setProductionQty(ptQty);
...@@ -520,13 +568,19 @@ public class ResultWriter { ...@@ -520,13 +568,19 @@ public class ResultWriter {
pt.setLotSizeUnder(solutionValue(model.getPtLotSizeUnderVars(), key).setScale(3, RoundingMode.HALF_UP).doubleValue()); pt.setLotSizeUnder(solutionValue(model.getPtLotSizeUnderVars(), key).setScale(3, RoundingMode.HALF_UP).doubleValue());
// 产出信息 // 产出信息
// for (OperationOutput oo : op.getOutputs()) { for (OperationOutput oo : op.getOutputs()) {
// PeriodTaskResult.OutputInfo oi = new PeriodTaskResult.OutputInfo(); PeriodTaskResult.OutputInfo oi = new PeriodTaskResult.OutputInfo();
// oi.productId = oo.getProductId(); oi.setDataKey(key);
// oi.spId = oo.getSpId(); oi.productId = oo.getProductId();
// oi.factor = 1.0; // 默认 1:1 产出 oi.productCode = oo.getProductCode();
// pt.getOutputs().add(oi); 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); result.getPeriodTasks().add(pt);
} }
...@@ -550,21 +604,29 @@ public class ResultWriter { ...@@ -550,21 +604,29 @@ public class ResultWriter {
/** /**
* 统计各 Unit 的产能使用情况: 每周期产能占用量、利用率、超载/未满足量。 * 统计各 Unit 的产能使用情况: 每周期产能占用量、利用率、超载/未满足量。
*/ */
private java.util.List<UnitCapacityResult> buildUnitCapacities() { private Map<String,Object> buildUnitCapacities() {
java.util.Map<String, UnitCapacityResult> unitMap = new java.util.LinkedHashMap<>(); java.util.Map<String, UnitCapacityResult> unitMap = new java.util.LinkedHashMap<>();
List<UnitCapacityResult.UnitPeriodDetail> unitPeriodDetails=new ArrayList<>();
for (UnitPeriod up : data.getUnitPeriods()) { for (UnitPeriod up : data.getUnitPeriods()) {
if (up.isUnlimited())
{
continue;
}
String uid = up.getUnitId(); String uid = up.getUnitId();
String name = up.getUnitName(); String name = up.getUnitName();
Integer shopid= up.getShopId();
UnitCapacityResult ucr = unitMap.computeIfAbsent(uid, k -> { UnitCapacityResult ucr = unitMap.computeIfAbsent(uid, k -> {
UnitCapacityResult r = new UnitCapacityResult(); UnitCapacityResult r = new UnitCapacityResult();
r.setUnitId(k); r.setUnitId(k);
r.setUnitName(name); r.setUnitName(name);
r.setShopId(shopid);
return r; return r;
}); });
// 计算该 unit 在此周期的实际产能占用量 // 计算该 unit 在此周期的实际产能占用量
UnitCapacityResult.UnitPeriodDetail pe = new UnitCapacityResult.UnitPeriodDetail(); UnitCapacityResult.UnitPeriodDetail pe = new UnitCapacityResult.UnitPeriodDetail();
pe.unitId=uid;
pe.unitName=name;
pe.periodIndex = up.getPeriod().getIndex(); pe.periodIndex = up.getPeriod().getIndex();
pe.periodStartDate = up.getPeriod().getStartDate().toString(); pe.periodStartDate = up.getPeriod().getStartDate().toString();
...@@ -579,21 +641,21 @@ public class ResultWriter { ...@@ -579,21 +641,21 @@ public class ResultWriter {
capacityUsed += capUsed; capacityUsed += capUsed;
// 工序级明细 // 工序级明细
UnitCapacityResult.UnitTaskInfo task = new UnitCapacityResult.UnitTaskInfo(); // UnitCapacityResult.UnitTaskInfo task = new UnitCapacityResult.UnitTaskInfo();
task.operationId = op.getId(); // task.operationId = op.getId();
task.operationName = op.getName(); // task.operationName = op.getName();
task.productionQty = ptQty; // task.productionQty = ptQty;
task.capacityUsed = capUsed; // task.capacityUsed = capUsed;
task.capacityCoeff = uo.getCapacityCoeff(); // task.capacityCoeff = uo.getCapacityCoeff();
if (uo.hasLotSize()) { // if (uo.hasLotSize()) {
task.lotSize = uo.getLotSize(); // task.lotSize = uo.getLotSize();
task.lotSizeOver = solutionValue(model.getPtLotSizeOverVars(), ptKey).setScale(3, RoundingMode.HALF_UP).doubleValue();; // task.lotSizeOver = solutionValue(model.getPtLotSizeOverVars(), ptKey).setScale(3, RoundingMode.HALF_UP).doubleValue();;
task.lotSizeUnder = solutionValue(model.getPtLotSizeUnderVars(), ptKey).setScale(3, RoundingMode.HALF_UP).doubleValue();; // task.lotSizeUnder = solutionValue(model.getPtLotSizeUnderVars(), ptKey).setScale(3, RoundingMode.HALF_UP).doubleValue();;
} // }
for (OperationOutput oo : op.getOutputs()) { // for (OperationOutput oo : op.getOutputs()) {
task.outputs.add(oo.getProductId() + "@" + oo.getSpId()); // task.outputs.add(oo.getProductId() + "@" + oo.getSpId());
} // }
pe.tasks.add(task); // pe.tasks.add(task);
} }
} }
...@@ -614,7 +676,7 @@ public class ResultWriter { ...@@ -614,7 +676,7 @@ public class ResultWriter {
pe.notMet = notMet; pe.notMet = notMet;
} }
pe.capacityUsed = capacityUsed; pe.capacityUsed = capacityUsed;
ucr.getPeriodDetails().add(pe); unitPeriodDetails.add(pe);
// 累加汇总 // 累加汇总
if (!up.isUnlimited()) { if (!up.isUnlimited()) {
...@@ -633,8 +695,10 @@ public class ResultWriter { ...@@ -633,8 +695,10 @@ public class ResultWriter {
(ucr.getTotalCapacityUsed() / ucr.getTotalMaxCapacity())*100); (ucr.getTotalCapacityUsed() / ucr.getTotalMaxCapacity())*100);
} }
} }
Map<String,Object> relst=new HashMap<>();
return new java.util.ArrayList<>(unitMap.values()); relst.put("UnitCapacity",new java.util.ArrayList<>(unitMap.values()));
relst.put("UnitCapacityDetails",unitPeriodDetails);
return relst;
} }
...@@ -652,6 +716,7 @@ public class ResultWriter { ...@@ -652,6 +716,7 @@ public class ResultWriter {
sr.setPeriodStartDate(sd.getPeriod().getStartDate().toString()); sr.setPeriodStartDate(sd.getPeriod().getStartDate().toString());
sr.setDemandQty(sd.getQuantity()); sr.setDemandQty(sd.getQuantity());
sr.setPriority(sd.getPriority()); sr.setPriority(sd.getPriority());
sr.setCategoryId(sd.getCategoryId());
sr.setDemandOrderDate(sd.getDemandOrderDate()==null?"":sd.getDemandOrderDate().toString()); sr.setDemandOrderDate(sd.getDemandOrderDate()==null?"":sd.getDemandOrderDate().toString());
double fulfilled = solutionValue(model.getSalesDemandQtyVars(), sd.getKey()).setScale(3, RoundingMode.HALF_UP).doubleValue();; double fulfilled = solutionValue(model.getSalesDemandQtyVars(), sd.getKey()).setScale(3, RoundingMode.HALF_UP).doubleValue();;
double unmet = Math.max(0, sd.getQuantity() - fulfilled); double unmet = Math.max(0, sd.getQuantity() - fulfilled);
...@@ -673,7 +738,29 @@ public class ResultWriter { ...@@ -673,7 +738,29 @@ public class ResultWriter {
result.getSalesDemands().add(sr); 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 { ...@@ -931,15 +1018,17 @@ public class ResultWriter {
// ==================== PispipResult ==================== // ==================== PispipResult ====================
private void buildPispips(OptimizationResult result) { private void buildPispips(OptimizationResult result) {
for (Product prod : data.getProducts()) { for (Product prod : data.getProducts()) {
for (StockingPoint sp : data.getStockingPointsForProduct(prod.getId())) { for (StockingPoint sp : data.getStockingPointsForProduct(prod.getId())) {
for (Period p : data.getPeriods()) { for (Period p : data.getPeriods()) {
PispipResult pr = new PispipResult(); PispipResult pr = new PispipResult();
pr.setProductId(prod.getId()); pr.setProductId(prod.getId());
pr.setProductCode(prod.getCode()); pr.setProductCode(prod.getCode());
pr.setSpId(sp.getId()); pr.setSpId(sp.getId());
pr.setSpName(sp.getName()); pr.setSpName(sp.getName());
pr.setPeriodIndex(p.getIndex()); pr.setCategoryId(prod.getCategoryId()); pr.setPeriodIndex(p.getIndex());
pr.setPeriodStartDate(p.getStartDate().toString()); pr.setPeriodStartDate(p.getStartDate().toString());
String invKey = prod.getId() + "_" + sp.getId() + "_" + p.getIndex(); String invKey = prod.getId() + "_" + sp.getId() + "_" + p.getIndex();
...@@ -1070,6 +1159,25 @@ public class ResultWriter { ...@@ -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 ==================== // ==================== ProductNetworkResult ====================
......
...@@ -27,9 +27,17 @@ public class OptimizationResult { ...@@ -27,9 +27,17 @@ public class OptimizationResult {
// ==================== 业务结果 ==================== // ==================== 业务结果 ====================
private final List<PeriodTaskResult> periodTasks = new ArrayList<>(); private final List<PeriodTaskResult> periodTasks = new ArrayList<>();
private final List<PeriodTaskResult.OutputInfo> periodTaskOutputs = new ArrayList<>();
private final List<SalesDemandResult> salesDemands = 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> pispips = new ArrayList<>();
private final List<PispipResult.PispipSummarieResult> pispipSummaries = new ArrayList<>();
// ==================== 产品生产网络 ==================== // ==================== 产品生产网络 ====================
private ProductNetworkResult productNetwork; private ProductNetworkResult productNetwork;
...@@ -38,6 +46,8 @@ public class OptimizationResult { ...@@ -38,6 +46,8 @@ public class OptimizationResult {
private List<UnitCapacityResult> unitCapacities; private List<UnitCapacityResult> unitCapacities;
private List<UnitCapacityResult.UnitPeriodDetail> unitPeriodDetails;
// ==================== 需求满足汇总 ==================== // ==================== 需求满足汇总 ====================
private List<DemandSummaryResult> demandSummary; private List<DemandSummaryResult> demandSummary;
...@@ -59,15 +69,30 @@ public class OptimizationResult { ...@@ -59,15 +69,30 @@ public class OptimizationResult {
public void setVersion(String v) { this.version = v; } public void setVersion(String v) { this.version = v; }
public List<PeriodTaskResult> getPeriodTasks() { return periodTasks; } public List<PeriodTaskResult> getPeriodTasks() { return periodTasks; }
public List<PeriodTaskResult.OutputInfo> getPeriodTaskOutputs() { return periodTaskOutputs; }
public List<SalesDemandResult> getSalesDemands() { return salesDemands; } 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> getPispips() { return pispips; }
public List<PispipResult.PispipSummarieResult> getPispipSummaries() { return pispipSummaries; }
public ProductNetworkResult getProductNetwork() { return productNetwork; } public ProductNetworkResult getProductNetwork() { return productNetwork; }
public void setProductNetwork(ProductNetworkResult v) { this.productNetwork = v; } public void setProductNetwork(ProductNetworkResult v) { this.productNetwork = v; }
public List<UnitCapacityResult> getUnitCapacities() { return unitCapacities; } public List<UnitCapacityResult> getUnitCapacities() { return unitCapacities; }
public List<UnitCapacityResult.UnitPeriodDetail> getUnitPeriodDetails() { return unitPeriodDetails; }
public void setUnitCapacities(List<UnitCapacityResult> v) { this.unitCapacities = v; } 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 List<DemandSummaryResult> getDemandSummary() { return demandSummary; }
public void setDemandSummary(List<DemandSummaryResult> v) { this.demandSummary = v; } public void setDemandSummary(List<DemandSummaryResult> v) { this.demandSummary = v; }
......
...@@ -17,6 +17,7 @@ public class PeriodTaskResult { ...@@ -17,6 +17,7 @@ public class PeriodTaskResult {
private String operationId; private String operationId;
private String operationName; private String operationName;
private String unitId; private String unitId;
private String unitName;
private int periodIndex; private int periodIndex;
private String periodStartDate; private String periodStartDate;
...@@ -40,11 +41,21 @@ public class PeriodTaskResult { ...@@ -40,11 +41,21 @@ public class PeriodTaskResult {
// ==================== 内嵌类 ==================== // ==================== 内嵌类 ====================
/** 产出信息 */ /** 产出信息 */
// public static class OutputInfo { @Data
// public String productId; public static class OutputInfo {
// public String spId; public String productId;
// public double factor; 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
...@@ -16,7 +16,7 @@ public class PispipResult { ...@@ -16,7 +16,7 @@ public class PispipResult {
private String productId; private String productId;
private String productCode; private String productCode;
private String spId; private String spId;
private Long categoryId;
private String spName; private String spName;
private int periodIndex; private int periodIndex;
private String periodStartDate; private String periodStartDate;
...@@ -83,6 +83,36 @@ public class PispipResult { ...@@ -83,6 +83,36 @@ public class PispipResult {
/** 每个工序的产出明细 */ /** 每个工序的产出明细 */
private final List<ProductionDetail> productionDetails = new ArrayList<>(); 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; }
}
// ==================== 内嵌类 ==================== // ==================== 内嵌类 ====================
/** 工序产出明细 */ /** 工序产出明细 */
......
package com.aps.macroplanner.output.dto; package com.aps.macroplanner.output.dto;
import lombok.Data;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
...@@ -8,6 +10,7 @@ import java.util.List; ...@@ -8,6 +10,7 @@ import java.util.List;
* *
* <p>每个销售需求在每个周期的满足量、缺口、优先级等。</p> * <p>每个销售需求在每个周期的满足量、缺口、优先级等。</p>
*/ */
@Data
public class SalesDemandResult { public class SalesDemandResult {
private String salesDemandId; private String salesDemandId;
...@@ -33,6 +36,8 @@ public class SalesDemandResult { ...@@ -33,6 +36,8 @@ public class SalesDemandResult {
/** 优先级 */ /** 优先级 */
private double priority; private double priority;
private Long categoryId;
/** 未完成原因分析 (仅当 unmetQty > 0 时有内容) */ /** 未完成原因分析 (仅当 unmetQty > 0 时有内容) */
private final List<String> unmetReasons = new ArrayList<>(); private final List<String> unmetReasons = new ArrayList<>();
/** 风险分析 (即使满足也可能存在的供应链风险) */ /** 风险分析 (即使满足也可能存在的供应链风险) */
......
...@@ -19,6 +19,9 @@ public class SupplyChainNode { ...@@ -19,6 +19,9 @@ public class SupplyChainNode {
/** 产品 ID */ /** 产品 ID */
private String productId; private String productId;
public String productCode;
/** 库存点 ID */ /** 库存点 ID */
private String spId; private String spId;
...@@ -148,6 +151,10 @@ public class SupplyChainNode { ...@@ -148,6 +151,10 @@ public class SupplyChainNode {
public String getProductId() { return productId; } public String getProductId() { return productId; }
public void setProductId(String v) { this.productId = v; } 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 String getSpId() { return spId; }
public void setSpId(String v) { this.spId = v; } public void setSpId(String v) { this.spId = v; }
......
package com.aps.macroplanner.output.dto; package com.aps.macroplanner.output.dto;
import lombok.Data;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
...@@ -20,6 +22,7 @@ import java.util.Map; ...@@ -20,6 +22,7 @@ import java.util.Map;
* <tr><td>periodTasks</td><td>该设备的生产任务明细 (PeriodTaskResult[])</td></tr> * <tr><td>periodTasks</td><td>该设备的生产任务明细 (PeriodTaskResult[])</td></tr>
* </table> * </table>
*/ */
@Data
public class UnitCapacityResult { public class UnitCapacityResult {
/** 设备ID */ /** 设备ID */
...@@ -27,6 +30,8 @@ public class UnitCapacityResult { ...@@ -27,6 +30,8 @@ public class UnitCapacityResult {
private String unitName; private String unitName;
private Integer shopId;
/** 使用该设备的工序ID列表 */ /** 使用该设备的工序ID列表 */
private List<String> operationIds; private List<String> operationIds;
/** 全部周期最大可用产能合计 */ /** 全部周期最大可用产能合计 */
...@@ -44,13 +49,19 @@ public class UnitCapacityResult { ...@@ -44,13 +49,19 @@ public class UnitCapacityResult {
private double overallUtilization; private double overallUtilization;
/** 各周期明细 */ /** 各周期明细 */
private final List<UnitPeriodDetail> periodDetails = new ArrayList<>(); // private final List<UnitPeriodDetail> periodDetails = new ArrayList<>();
// ==================== 内嵌类 ==================== // ==================== 内嵌类 ====================
/** 单周期产能明细 */ /** 单周期产能明细 */
public static class UnitPeriodDetail { public static class UnitPeriodDetail {
/** 设备ID */
public String unitId;
public String unitName;
/** 周期索引 */ /** 周期索引 */
public int periodIndex; public int periodIndex;
/** 周期起始日期 */ /** 周期起始日期 */
...@@ -69,7 +80,7 @@ public class UnitCapacityResult { ...@@ -69,7 +80,7 @@ public class UnitCapacityResult {
public double production; public double production;
/** 该周期内在该设备上执行的工序明细 */ /** 该周期内在该设备上执行的工序明细 */
public final List<UnitTaskInfo> tasks = new ArrayList<>(); // public final List<UnitTaskInfo> tasks = new ArrayList<>();
} }
/** 设备上的工序级生产明细 */ /** 设备上的工序级生产明细 */
public static class UnitTaskInfo { public static class UnitTaskInfo {
...@@ -107,7 +118,7 @@ public class UnitCapacityResult { ...@@ -107,7 +118,7 @@ public class UnitCapacityResult {
public double getTotalProduction() { return totalProduction; } public double getTotalProduction() { return totalProduction; }
public void setTotalProduction(double v) { this.totalProduction = v; } 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 double getTotalOverloaded() { return totalOverloaded; }
public void setTotalOverloaded(double v) { this.totalOverloaded = v; } public void setTotalOverloaded(double v) { this.totalOverloaded = v; }
......
...@@ -13,14 +13,16 @@ import java.util.*; ...@@ -13,14 +13,16 @@ import java.util.*;
/** /**
* 产品生产网络按需查询服务。 * 产品生产网络按需查询服务。
* *
* <p>不再在求解阶段预生成完整 BOM 网络 (buildProductNetwork), 而是: * <p>不再在求解阶段预生成完整 BOM 网络, 而是查询时按 productId + spId 定位节点,
* 递归向下展开 BOM 子树。求解值从 parquet 按需过滤读取, 避免全量加载。</p>
*
* <p>数据流:</p>
* <ol> * <ol>
* <li>求解时把静态 BOM 结构持久化为 {@code bom_structure.json};</li> * <li>bom_structure.json —— 静态 BOM 结构 (小, 全量读);</li>
* <li>求解变量值已持久化为 parquet (period_tasks / pispips / salesdemand / operation_demands);</li> * <li>period_task_outputs.parquet —— 工序产出 (谁把什么产入哪个库房);</li>
* <li>查询时按 productId + spId 定位节点, 递归向下展开 BOM 子树, 按需读取变量值。</li> * <li>period_tasks.parquet —— 生产任务 (dataKey → unit/operation/产量);</li>
* <li>pispips / salesdemand / operation_demands —— 库存/销售/工序消耗量。</li>
* </ol> * </ol>
*
* <p>这样避免了构建整棵树的耗时和超大 JSON 的序列化/反序列化开销。</p>
*/ */
@Service @Service
@Slf4j @Slf4j
...@@ -45,8 +47,53 @@ public class MacroPlannerProductNetworkService { ...@@ -45,8 +47,53 @@ public class MacroPlannerProductNetworkService {
} }
try { try {
Integer periodIndex = isBlank(period) ? null : Integer.parseInt(period.trim()); 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) { } catch (Exception e) {
log.warn("按需组装产品网络失败: sceneId={}, productId={}, spId={}, error={}", log.warn("按需组装产品网络失败: sceneId={}, productId={}, spId={}, error={}",
sceneId, productId, spId, e.getMessage()); sceneId, productId, spId, e.getMessage());
...@@ -54,33 +101,37 @@ public class MacroPlannerProductNetworkService { ...@@ -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) { private String path(String sceneId, String fileName) {
return Paths.get(RESULT_DIR, sceneId, fileName).toAbsolutePath().toString(); return Paths.get(RESULT_DIR, sceneId, fileName).toAbsolutePath().toString();
} }
/** /**
* 内存索引: 将持久化结构 + 求解变量值整理成 O(1) 查询结构。 * 纯结构遍历 BOM, 收集目标子树涉及的所有 productId@spId 与 operationId。
*/ */
private static class IndexedData { private void collectSubtree(StructureIndex sidx, String productId, String spId,
final Map<String, List<BomStructureData.StockingPointInfo>> spByProduct = new HashMap<>(); 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, String> spNameById = new HashMap<>();
final Map<String, List<BomStructureData.OperationInfo>> operationsByProductSp = 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, BomStructureData.OperationInfo> operationById = new HashMap<>();
final Map<String, List<BomStructureData.OperationInputInfo>> inputsByOperation = new HashMap<>(); final Map<String, List<BomStructureData.OperationInputInfo>> inputsByOperation = new HashMap<>();
final Map<String, List<BomStructureData.OperationInputInfo>> inputsByProductSp = new HashMap<>(); final Map<String, List<BomStructureData.OperationInputInfo>> inputsByProductSp = new HashMap<>();
...@@ -89,31 +140,14 @@ public class MacroPlannerProductNetworkService { ...@@ -89,31 +140,14 @@ public class MacroPlannerProductNetworkService {
final List<BomStructureData.PeriodInfo> periods = new ArrayList<>(); final List<BomStructureData.PeriodInfo> periods = new ArrayList<>();
final Map<Integer, String> startDateByPeriodIndex = new HashMap<>(); final Map<Integer, String> startDateByPeriodIndex = new HashMap<>();
// 求解变量值 StructureIndex(BomStructureData s) {
final Map<String, Double> ptQty = new HashMap<>(); // operationId_unitId_periodIndex
final Map<String, Double> opDemand = new HashMap<>(); // operationId_inputProductId_inputSpId_periodIndex
final Map<String, PispipResult> pispip = new HashMap<>(); // productId_spId_periodIndex
final Map<String, double[]> sales = new HashMap<>(); // productId_spId_periodIndex -> [demandQty, fulfilledQty]
IndexedData(BomStructureData s,
List<PeriodTaskResult> periodTasks,
List<PispipResult> pispips,
List<SalesDemandResult> salesList,
List<OperationDemandResult> opDemands) {
for (BomStructureData.StockingPointInfo sp : s.stockingPoints) { for (BomStructureData.StockingPointInfo sp : s.stockingPoints) {
spNameById.put(sp.id, sp.name); 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) { for (BomStructureData.OperationInfo op : s.operations) {
operationById.put(op.id, op); operationById.put(op.id, op);
for (BomStructureData.OperationOutputInfo oo : op.outputs) { for (BomStructureData.OperationOutputInfo oo : op.outputs) {
operationsByProductSp.computeIfAbsent(oo.productId + "@" + oo.spId, k -> new ArrayList<>()).add(op); 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) { for (BomStructureData.OperationInputInfo in : s.operationInputs) {
...@@ -132,21 +166,6 @@ public class MacroPlannerProductNetworkService { ...@@ -132,21 +166,6 @@ public class MacroPlannerProductNetworkService {
startDateByPeriodIndex.put(p.index, p.startDate); 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) { List<BomStructureData.OperationInfo> producing(String productId, String spId) {
...@@ -164,25 +183,72 @@ public class MacroPlannerProductNetworkService { ...@@ -164,25 +183,72 @@ public class MacroPlannerProductNetworkService {
List<BomStructureData.InTransitSupplyInfo> inTransit(String productId, String spId) { List<BomStructureData.InTransitSupplyInfo> inTransit(String productId, String spId) {
return inTransitByProductSp.getOrDefault(productId + "@" + spId, Collections.emptyList()); 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 demandQty(String productId, String spId, int periodIndex) {
double[] acc = sales.get(productId + "_" + spId + "_" + periodIndex); double[] acc = salesByKey.get(productId + "_" + spId + "_" + periodIndex);
return acc == null ? 0.0 : acc[0]; return acc == null ? 0.0 : acc[0];
} }
double fulfilledQty(String productId, String spId, int periodIndex) { double fulfilledQty(String productId, String spId, int periodIndex) {
double[] acc = sales.get(productId + "_" + spId + "_" + periodIndex); double[] acc = salesByKey.get(productId + "_" + spId + "_" + periodIndex);
return acc == null ? 0.0 : acc[1]; return acc == null ? 0.0 : acc[1];
} }
PispipResult pispip(String productId, String spId, int periodIndex) { double opDemand(String operationId, String inputProductId, String inputSpId, int periodIndex) {
return pispip.get(productId + "_" + spId + "_" + periodIndex); return opDemandByKey.getOrDefault(
} operationId + "_" + inputProductId + "_" + inputSpId + "_" + periodIndex, 0.0);
String startDate(int periodIndex) {
return startDateByPeriodIndex.get(periodIndex);
} }
} }
// ==================== 节点组装 ==================== // ==================== 节点组装 ====================
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) { Integer periodIndex, int level, Set<String> visited) {
String nodeKey = productId + "@" + spId; String nodeKey = productId + "@" + spId;
if (!visited.add(nodeKey)) { if (!visited.add(nodeKey)) {
...@@ -192,61 +258,37 @@ public class MacroPlannerProductNetworkService { ...@@ -192,61 +258,37 @@ public class MacroPlannerProductNetworkService {
SupplyChainNode node = new SupplyChainNode(); SupplyChainNode node = new SupplyChainNode();
node.setProductId(productId); node.setProductId(productId);
node.setSpId(spId); node.setSpId(spId);
node.setSpName(idx.spNameById.getOrDefault(spId, spId));
node.setLevel(level); node.setLevel(level);
// 周期过滤的目标 startDate String periodStartDate = periodIndex == null ? null : sidx.startDate(periodIndex);
String periodStartDate = periodIndex == null ? null : idx.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); node.getSupplySources().add(src);
} }
// --- 在途供应 --- // --- 在途供应 ---
for (BomStructureData.InTransitSupplyInfo its : idx.inTransit(productId, spId)) { for (BomStructureData.InTransitSupplyInfo its : sidx.inTransit(productId, spId)) {
SupplyChainNode.SupplySource src = new SupplyChainNode.SupplySource(); SupplyChainNode.SupplySource src = new SupplyChainNode.SupplySource();
src.type = "IN_TRANSIT"; src.type = "IN_TRANSIT";
src.totalProduction = its.quantity; src.totalProduction = its.quantity;
...@@ -261,50 +303,12 @@ public class MacroPlannerProductNetworkService { ...@@ -261,50 +303,12 @@ public class MacroPlannerProductNetworkService {
node.getSupplySources().add(src); 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 子物料 (向下展开) --- // --- BOM 子物料 (向下展开) ---
List<BomStructureData.OperationInfo> producingOps = idx.producing(productId, spId); List<BomStructureData.OperationInfo> producingOps = sidx.producing(productId, spId);
if (!producingOps.isEmpty()) { if (!producingOps.isEmpty()) {
BomStructureData.OperationInfo op = producingOps.get(0); // 与原逻辑一致: 只展开第一个工序的 BOM BomStructureData.OperationInfo op = producingOps.get(0);
for (BomStructureData.OperationInputInfo in : idx.inputsOf(op.id)) { for (BomStructureData.OperationInputInfo in : sidx.inputsOf(op.id)) {
SupplyChainNode.BomChild child = new SupplyChainNode.BomChild(); SupplyChainNode.BomChild child = new SupplyChainNode.BomChild();
child.productId = in.inputProductId; child.productId = in.inputProductId;
child.spId = in.inputSpId; child.spId = in.inputSpId;
...@@ -314,9 +318,8 @@ public class MacroPlannerProductNetworkService { ...@@ -314,9 +318,8 @@ public class MacroPlannerProductNetworkService {
child.totalFactor = in.factor; child.totalFactor = in.factor;
double totalConsumed = 0; double totalConsumed = 0;
for (BomStructureData.PeriodInfo p : idx.periods) { for (BomStructureData.PeriodInfo p : sidx.periods) {
double consumed = idx.opDemand.getOrDefault( double consumed = solveIdx.opDemand(in.operationId, in.inputProductId, in.inputSpId, p.index);
in.operationId + "_" + in.inputProductId + "_" + in.inputSpId + "_" + p.index, 0.0);
totalConsumed += consumed; totalConsumed += consumed;
if (consumed > 0) { if (consumed > 0) {
String periodKey = p.startDate == null ? String.valueOf(p.index) : p.startDate; String periodKey = p.startDate == null ? String.valueOf(p.index) : p.startDate;
...@@ -326,14 +329,13 @@ public class MacroPlannerProductNetworkService { ...@@ -326,14 +329,13 @@ public class MacroPlannerProductNetworkService {
child.totalConsumedQty = totalConsumed; child.totalConsumedQty = totalConsumed;
if (periodStartDate != null && !child.consumedByPeriod.containsKey(periodStartDate)) { if (periodStartDate != null && !child.consumedByPeriod.containsKey(periodStartDate)) {
continue; // 该周期无消耗, 剪枝 continue;
} }
SupplyChainNode childNode = buildNode(idx, in.inputProductId, in.inputSpId, periodIndex, level + 1, visited); SupplyChainNode childNode = buildNode(sidx, solveIdx, in.inputProductId, in.inputSpId, periodIndex, level + 1, visited);
if (childNode == null) { if (childNode == null) {
continue; continue;
} }
// 用子节点的供应源回填 child 的 unitId/unitName/totalConsumedQty (与原逻辑对齐)
for (SupplyChainNode.SupplySource ss : childNode.getSupplySources()) { for (SupplyChainNode.SupplySource ss : childNode.getSupplySources()) {
child.totalConsumedQty = ss.production; child.totalConsumedQty = ss.production;
child.unitId = ss.unitId; child.unitId = ss.unitId;
...@@ -345,9 +347,9 @@ public class MacroPlannerProductNetworkService { ...@@ -345,9 +347,9 @@ public class MacroPlannerProductNetworkService {
} }
// --- 汇总 --- // --- 汇总 ---
fillSummary(idx, node, productId, spId); fillSummary(sidx, solveIdx, node, productId, spId);
// period 剪枝: 该周期无活动则剪掉整个节点 (与原 buildProductNetwork 语义一致) // period 剪枝
if (periodStartDate != null && !node.getActivePeriods().contains(periodStartDate)) { if (periodStartDate != null && !node.getActivePeriods().contains(periodStartDate)) {
return null; return null;
} }
...@@ -355,14 +357,15 @@ public class MacroPlannerProductNetworkService { ...@@ -355,14 +357,15 @@ public class MacroPlannerProductNetworkService {
return node; 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(); SupplySummary s = new SupplySummary();
node.setSummary(s); node.setSummary(s);
int n = idx.periods.size(); int n = sidx.periods.size();
double totalEndingInv = 0; double totalEndingInv = 0;
for (BomStructureData.PeriodInfo p : idx.periods) { for (BomStructureData.PeriodInfo p : sidx.periods) {
PispipResult pr = idx.pispip(productId, spId, p.index); PispipResult pr = solveIdx.pispip(productId, spId, p.index);
if (pr != null) { if (pr != null) {
totalEndingInv += pr.getEndingInventory(); totalEndingInv += pr.getEndingInventory();
if (pr.getEndingInventory() > 0) { if (pr.getEndingInventory() > 0) {
...@@ -371,26 +374,21 @@ public class MacroPlannerProductNetworkService { ...@@ -371,26 +374,21 @@ public class MacroPlannerProductNetworkService {
} }
} }
s.setInitialInventory(idx.initialInv(productId, spId)); s.setInitialInventory(sidx.initialInv(productId, spId));
PispipResult last = idx.pispip(productId, spId, n - 1); PispipResult last = solveIdx.pispip(productId, spId, n - 1);
s.setFinalInventory(last == null ? 0 : last.getEndingInventory()); s.setFinalInventory(last == null ? 0 : last.getEndingInventory());
s.setAverageInventory(n > 0 ? totalEndingInv / n : 0); s.setAverageInventory(n > 0 ? totalEndingInv / n : 0);
// 生产汇总 // 生产汇总 (跨周期全量, 不受 period 过滤影响)
double totalProduction = 0; double totalProduction = 0;
for (BomStructureData.OperationInfo op : idx.producing(productId, spId)) { for (PeriodTaskResult.OutputInfo oi : solveIdx.outputsOf(productId, spId)) {
for (BomStructureData.UnitOperationInfo uo : op.unitOperations) { totalProduction += oi.getProductionQty();
for (BomStructureData.PeriodInfo p : idx.periods) {
double qty = idx.ptQty.getOrDefault(op.id + "_" + uo.unitId + "_" + p.index, 0.0);
totalProduction += qty;
}
}
} }
s.setTotalProduction(totalProduction); s.setTotalProduction(totalProduction);
// 在途汇总 // 在途汇总
double totalInTransit = 0; double totalInTransit = 0;
for (BomStructureData.InTransitSupplyInfo its : idx.inTransit(productId, spId)) { for (BomStructureData.InTransitSupplyInfo its : sidx.inTransit(productId, spId)) {
totalInTransit += its.quantity; totalInTransit += its.quantity;
} }
s.setTotalInTransit(totalInTransit); s.setTotalInTransit(totalInTransit);
...@@ -401,8 +399,8 @@ public class MacroPlannerProductNetworkService { ...@@ -401,8 +399,8 @@ public class MacroPlannerProductNetworkService {
double totalDepDemand = 0; double totalDepDemand = 0;
double totalDemandFulf = 0; double totalDemandFulf = 0;
double totalSlack = 0; double totalSlack = 0;
for (BomStructureData.PeriodInfo p : idx.periods) { for (BomStructureData.PeriodInfo p : sidx.periods) {
PispipResult pr = idx.pispip(productId, spId, p.index); PispipResult pr = solveIdx.pispip(productId, spId, p.index);
if (pr != null) { if (pr != null) {
totalDepDemand += pr.getDependentDemandQty(); totalDepDemand += pr.getDependentDemandQty();
totalDemandFulf += pr.getDemandFulfillment(); totalDemandFulf += pr.getDemandFulfillment();
...@@ -411,8 +409,8 @@ public class MacroPlannerProductNetworkService { ...@@ -411,8 +409,8 @@ public class MacroPlannerProductNetworkService {
node.getActivePeriods().add(p.startDate == null ? String.valueOf(p.index) : p.startDate); node.getActivePeriods().add(p.startDate == null ? String.valueOf(p.index) : p.startDate);
} }
} }
totalSalesDemand += idx.demandQty(productId, spId, p.index); totalSalesDemand += solveIdx.demandQty(productId, spId, p.index);
totalSalesFulfilled += idx.fulfilledQty(productId, spId, p.index); totalSalesFulfilled += solveIdx.fulfilledQty(productId, spId, p.index);
} }
s.setTotalSalesDemand(totalSalesDemand); s.setTotalSalesDemand(totalSalesDemand);
s.setTotalSalesFulfilled(totalSalesFulfilled); s.setTotalSalesFulfilled(totalSalesFulfilled);
...@@ -428,8 +426,8 @@ public class MacroPlannerProductNetworkService { ...@@ -428,8 +426,8 @@ public class MacroPlannerProductNetworkService {
double belowTarget = 0; double belowTarget = 0;
double belowMin = 0; double belowMin = 0;
double aboveMax = 0; double aboveMax = 0;
for (BomStructureData.PeriodInfo p : idx.periods) { for (BomStructureData.PeriodInfo p : sidx.periods) {
PispipResult pr = idx.pispip(productId, spId, p.index); PispipResult pr = solveIdx.pispip(productId, spId, p.index);
if (pr != null) { if (pr != null) {
belowTarget += pr.getBelowTarget(); belowTarget += pr.getBelowTarget();
belowMin += pr.getBelowMin(); belowMin += pr.getBelowMin();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment