feat: 补充宏观排产结果展示字段

parent 24eee50e
......@@ -2,6 +2,10 @@ package com.aps.controller;
import com.aps.common.util.ParamValidator;
import com.aps.common.util.R;
import com.aps.entity.ApsDemandOrder;
import com.aps.entity.MaterialInfo;
import com.aps.mapper.ApsDemandOrderMapper;
import com.aps.mapper.MaterialInfoMapper;
import com.aps.macroplanner.MacroPlannerOptimizer;
import com.aps.macroplanner.data.DataValidator;
import com.aps.macroplanner.data.MacroPlannerDataConverter;
......@@ -9,10 +13,12 @@ import com.aps.macroplanner.data.TestDataBuilder;
import com.aps.macroplanner.output.ResultWriter;
import com.aps.macroplanner.output.dto.*;
import com.aps.service.MacroPlannerResultService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.google.ortools.Loader;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
......@@ -51,6 +57,7 @@ import java.util.stream.Collectors;
@RestController
@RequestMapping("/macroResult")
@Tag(name = "MP排产结果", description = "MP宏观排产: 运行优化 & 查询物料供应链、产能、生产网络、KPI 结果")
@Slf4j
public class MacroPlannerResultController {
@Autowired
......@@ -59,6 +66,12 @@ public class MacroPlannerResultController {
@Autowired
private MacroPlannerDataConverter macroPlannerDataConverter;
@Autowired
private ApsDemandOrderMapper apsDemandOrderMapper;
@Autowired
private MaterialInfoMapper materialInfoMapper;
/**
* 加载结果文件, 校验 sceneId 和文件存在性。
* @return OptimizationResult, 或 null (需要调用方返回 R.failed)
......@@ -110,6 +123,8 @@ public class MacroPlannerResultController {
.filter(s -> productId == null || productId.isEmpty() || productId.equals(s.getProductId()))
.filter(s -> periodIndex == null || periodIndex == s.getPeriodIndex())
.collect(Collectors.toList());
enrichSalesDemandDisplayFields(filtered);
normalizeUnmetReasons(filtered);
// 汇总统计
double totalDemand = filtered.stream().mapToDouble(SalesDemandResult::getDemandQty).sum();
......@@ -195,6 +210,7 @@ public class MacroPlannerResultController {
}
List<PispipResult> pispips = result.getPispips();
enrichPispipProductCodes(pispips);
// 按 productId@spId 分组,保留每周期明细,不跨周期聚合
Map<String, List<PispipResult>> grouped = pispips != null
......@@ -210,6 +226,7 @@ public class MacroPlannerResultController {
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("periodCount", records.size());
s.put("records", records); // 每周期明细, 不聚合
......@@ -392,6 +409,145 @@ public class MacroPlannerResultController {
// ==================== 辅助构建方法 ====================
private void enrichSalesDemandDisplayFields(List<SalesDemandResult> demands) {
if (demands == null || demands.isEmpty()) {
return;
}
try {
Set<String> demandOrderIds = demands.stream()
.map(SalesDemandResult::getSalesDemandId)
.filter(this::hasText)
.collect(Collectors.toCollection(LinkedHashSet::new));
Map<String, ApsDemandOrder> orderById = new HashMap<>();
if (!demandOrderIds.isEmpty()) {
List<ApsDemandOrder> orders = apsDemandOrderMapper.selectList(
new LambdaQueryWrapper<ApsDemandOrder>()
.in(ApsDemandOrder::getId, demandOrderIds));
orderById = orders.stream()
.filter(o -> hasText(o.getId()))
.collect(Collectors.toMap(
ApsDemandOrder::getId,
o -> o,
(a, b) -> a));
}
Set<String> materialIds = demands.stream()
.map(SalesDemandResult::getProductId)
.filter(this::hasText)
.collect(Collectors.toCollection(LinkedHashSet::new));
orderById.values().stream()
.map(ApsDemandOrder::getMmid)
.filter(this::hasText)
.forEach(materialIds::add);
Map<String, String> materialCodeById = new HashMap<>();
if (!materialIds.isEmpty()) {
List<MaterialInfo> materials = materialInfoMapper.selectList(
new LambdaQueryWrapper<MaterialInfo>()
.in(MaterialInfo::getId, materialIds));
materialCodeById = materials.stream()
.filter(m -> hasText(m.getId()))
.collect(Collectors.toMap(
MaterialInfo::getId,
m -> firstText(m.getCode(), m.getTempcode(), m.getId()),
(a, b) -> a));
}
for (SalesDemandResult demand : demands) {
ApsDemandOrder order = orderById.get(demand.getSalesDemandId());
if (!hasText(demand.getOrderCode()) && order != null) {
demand.setOrderCode(order.getCode());
}
if (!hasText(demand.getProductCode())) {
String productCode = null;
if (order != null) {
productCode = firstText(order.getMmcode(), materialCodeById.get(order.getMmid()));
}
productCode = firstText(productCode, materialCodeById.get(demand.getProductId()), demand.getProductId());
demand.setProductCode(productCode);
}
}
} catch (Exception e) {
log.warn("补充销售需求订单编号/产品编号失败, 使用结果文件原始字段返回: {}", e.getMessage());
}
}
private void normalizeUnmetReasons(List<SalesDemandResult> demands) {
for (SalesDemandResult demand : demands) {
List<String> reasons = demand.getUnmetReasons();
if (reasons == null || reasons.isEmpty()) {
continue;
}
List<String> normalized = new ArrayList<>();
for (String reason : reasons) {
if (reason != null && reason.startsWith("库存耗尽(") && reason.endsWith(")")) {
normalized.add("库存耗尽");
normalized.add(reason.substring("库存耗尽".length()));
} else {
normalized.add(reason);
}
}
reasons.clear();
reasons.addAll(normalized);
}
}
private void enrichPispipProductCodes(List<PispipResult> pispips) {
if (pispips == null || pispips.isEmpty()) {
return;
}
Set<String> productIds = pispips.stream()
.map(PispipResult::getProductId)
.filter(this::hasText)
.collect(Collectors.toCollection(LinkedHashSet::new));
if (productIds.isEmpty()) {
return;
}
Map<String, String> productCodeByKey = new HashMap<>();
try {
List<MaterialInfo> materials = materialInfoMapper.selectList(
new LambdaQueryWrapper<MaterialInfo>()
.and(w -> w.in(MaterialInfo::getId, productIds)
.or()
.in(MaterialInfo::getCode, productIds)));
for (MaterialInfo material : materials) {
String productCode = firstText(material.getCode(), material.getTempcode(), material.getId());
if (hasText(material.getId())) {
productCodeByKey.put(material.getId(), productCode);
}
if (hasText(material.getCode())) {
productCodeByKey.put(material.getCode(), productCode);
}
}
} catch (Exception e) {
log.warn("补充库存产品编号失败, 使用结果文件原始字段返回: {}", e.getMessage());
}
for (PispipResult pispip : pispips) {
if (!hasText(pispip.getProductCode())) {
pispip.setProductCode(firstText(productCodeByKey.get(pispip.getProductId()), pispip.getProductId()));
}
}
}
private boolean hasText(String value) {
return value != null && !value.trim().isEmpty();
}
private String firstText(String... values) {
for (String value : values) {
if (hasText(value)) {
return value.trim();
}
}
return null;
}
/**
* 构建供应链视图: 每个产品-库位的库存流转 + 销售满足情况。
*
......@@ -406,6 +562,7 @@ public class MacroPlannerResultController {
// 1.1 按产品汇总库存视图
List<PispipResult> pispips = result.getPispips();
enrichPispipProductCodes(pispips);
sc.put("totalPispipRecords", pispips.size());
// 提取产品列表
......@@ -468,6 +625,7 @@ public class MacroPlannerResultController {
Map<String, Object> summary = new LinkedHashMap<>();
summary.put("productId", first.getProductId());
summary.put("productCode", first.getProductCode());
summary.put("spId", first.getSpId());
summary.put("periodCount", records.size());
summary.put("initialInventory", initialInv);
......
......@@ -689,7 +689,7 @@ public class MacroPlannerDataConverter {
// 3.1 为每个 Material 创建 Product
for (Material m : ctx.materialByMaterialId.values()) {
String name = pickName(m.getName(), m.getCode(), m.getId());
Product p = new Product(m.getCode(), name);
Product p = new Product(m.getCode(), name, m.getCode());
products.add(p);
ctx.productByMaterialId.put(m.getId(), p);
}
......@@ -1269,7 +1269,8 @@ public class MacroPlannerDataConverter {
orderEnd = deliveryDate.plusDays(1); // +1 使结束时间包含当天
} else {
// 无时间信息, 放入最后一个周期
salesDemands.add(new SalesDemand(p, sp, lastPeriod, qty, priority, ado.getId()));
salesDemands.add(new SalesDemand(p, sp, lastPeriod, qty, priority,
ado.getId(), ado.getCode(), pickName(ado.getMmcode(), p.getId())));
continue;
}
......@@ -1290,7 +1291,8 @@ public class MacroPlannerDataConverter {
}
if (overlapPeriods.isEmpty()) {
salesDemands.add(new SalesDemand(p, sp, lastPeriod, qty, priority, ado.getId()));
salesDemands.add(new SalesDemand(p, sp, lastPeriod, qty, priority,
ado.getId(), ado.getCode(), pickName(ado.getMmcode(), p.getId())));
} else {
// 按重叠天数比例拆分需求量: 每期四舍五入取整, 最后一期补差保证总和等于总数
int totalQty = (int) Math.round(qty);
......@@ -1306,7 +1308,8 @@ public class MacroPlannerDataConverter {
periodQty = (int) Math.round(qty * overlapDays / (double) totalRangeDays);
allocated += periodQty;
}
salesDemands.add(new SalesDemand(p, sp, per, periodQty, priority, ado.getId()));
salesDemands.add(new SalesDemand(p, sp, per, periodQty, priority,
ado.getId(), ado.getCode(), pickName(ado.getMmcode(), p.getId())));
}
}
}
......
......@@ -6,14 +6,21 @@ package com.aps.macroplanner.data;
public class Product {
private final String id;
private final String name;
private final String code;
public Product(String id, String name) {
this(id, name, id);
}
public Product(String id, String name, String code) {
this.id = id;
this.name = name;
this.code = code;
}
public String getId() { return id; }
public String getName() { return name; }
public String getCode() { return code; }
@Override
public String toString() {
......
......@@ -11,6 +11,8 @@ public class SalesDemand {
private final double quantity; // 需求总量
private final double priority; // 需求优先级
private final String demandOrderId; // 来源订单ID (ApsDemandOrder.id), 可空
private final String orderCode;
private final String productCode;
public SalesDemand(Product product, StockingPoint stockingPoint, Period period,
double quantity, double priority) {
......@@ -19,12 +21,20 @@ public class SalesDemand {
public SalesDemand(Product product, StockingPoint stockingPoint, Period period,
double quantity, double priority, String demandOrderId) {
this(product, stockingPoint, period, quantity, priority, demandOrderId, null, null);
}
public SalesDemand(Product product, StockingPoint stockingPoint, Period period,
double quantity, double priority, String demandOrderId,
String orderCode, String productCode) {
this.product = product;
this.stockingPoint = stockingPoint;
this.period = period;
this.quantity = quantity;
this.priority = priority;
this.demandOrderId = demandOrderId;
this.orderCode = orderCode;
this.productCode = productCode;
}
public Product getProduct() { return product; }
......@@ -33,6 +43,8 @@ public class SalesDemand {
public double getQuantity() { return quantity; }
public double getPriority() { return priority; }
public String getDemandOrderId() { return demandOrderId; }
public String getOrderCode() { return orderCode; }
public String getProductCode() { return productCode; }
public String getKey() {
return product.getId() + "_" + stockingPoint.getId() + "_" + period.getIndex();
......
......@@ -410,7 +410,9 @@ public class ResultWriter {
for (SalesDemand sd : data.getSalesDemands()) {
SalesDemandResult sr = new SalesDemandResult();
sr.setSalesDemandId(sd.getDemandOrderId()==null? sd.getKey():sd.getDemandOrderId());
sr.setOrderCode(sd.getOrderCode());
sr.setProductId(sd.getProduct().getId());
sr.setProductCode(sd.getProductCode());
sr.setSpId(sd.getStockingPoint().getId());
sr.setPeriodIndex(sd.getPeriod().getIndex());
sr.setPeriodStartDate(sd.getPeriod().getStartDate().toString());
......@@ -473,7 +475,8 @@ public class ResultWriter {
}
double available = openingInv + totalArrived;
if (available < demandQty) {
reasons.add("库存耗尽(可用" + fmt(available) + "件, 需求" + fmt(demandQty) + "件)");
addReasonWithDetail(reasons, "库存耗尽",
"(可用" + fmt(available) + "件, 需求" + fmt(demandQty) + "件)");
}
// 2. 产能瓶颈: 所有生产该产品的单元满负荷 (利用率 > 95%)
......@@ -552,6 +555,11 @@ public class ResultWriter {
}
}
private void addReasonWithDetail(java.util.List<String> reasons, String title, String detail) {
reasons.add(title);
reasons.add(detail);
}
/**
* 分析销售需求的风险 (即使本期已满足)。
*
......@@ -702,6 +710,7 @@ public class ResultWriter {
for (Period p : data.getPeriods()) {
PispipResult pr = new PispipResult();
pr.setProductId(prod.getId());
pr.setProductCode(prod.getCode());
pr.setSpId(sp.getId());
pr.setPeriodIndex(p.getIndex());
pr.setPeriodStartDate(p.getStartDate().toString());
......@@ -1221,7 +1230,9 @@ public class ResultWriter {
for (SalesDemandResult sr : result.getSalesDemands()) {
jb.obj();
jb.key("salesDemandId").val(sr.getSalesDemandId());
jb.key("orderCode").val(sr.getOrderCode());
jb.key("productId").val(sr.getProductId());
jb.key("productCode").val(sr.getProductCode());
jb.key("spId").val(sr.getSpId());
jb.key("periodIndex").val(sr.getPeriodIndex());
jb.key("periodStartDate").val(sr.getPeriodStartDate());
......@@ -1251,6 +1262,7 @@ public class ResultWriter {
for (PispipResult pr : result.getPispips()) {
jb.obj();
jb.key("productId").val(pr.getProductId());
jb.key("productCode").val(pr.getProductCode());
jb.key("spId").val(pr.getSpId());
jb.key("periodIndex").val(pr.getPeriodIndex());
jb.key("periodStartDate").val(pr.getPeriodStartDate());
......
......@@ -11,6 +11,7 @@ import java.util.List;
public class PispipResult {
private String productId;
private String productCode;
private String spId;
private int periodIndex;
private String periodStartDate;
......@@ -103,6 +104,9 @@ public class PispipResult {
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; }
......
......@@ -11,7 +11,9 @@ import java.util.List;
public class SalesDemandResult {
private String salesDemandId;
private String orderCode;
private String productId;
private String productCode;
private String spId;
private int periodIndex;
private String periodStartDate;
......@@ -39,9 +41,15 @@ public class SalesDemandResult {
public String getSalesDemandId() { return salesDemandId; }
public void setSalesDemandId(String v) { this.salesDemandId = v; }
public String getOrderCode() { return orderCode; }
public void setOrderCode(String v) { this.orderCode = 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; }
......
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