Commit d1745f27 authored by Tong Li's avatar Tong Li

MP

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