|
|
@@ -1,4 +1,4 @@
|
|
|
-package com.dtm.storage.service;
|
|
|
+package com.dtm.storage.service;
|
|
|
|
|
|
import com.dtm.storage.config.StorageSettings;
|
|
|
import com.dtm.storage.model.AssemblyRecord;
|
|
|
@@ -25,46 +25,22 @@ import java.util.stream.Collectors;
|
|
|
@Service
|
|
|
public class InventoryService {
|
|
|
private final StorageDataLoader dataLoader;
|
|
|
+ private volatile InventorySnapshot snapshot;
|
|
|
|
|
|
public InventoryService(StorageDataLoader dataLoader) {
|
|
|
this.dataLoader = dataLoader;
|
|
|
}
|
|
|
|
|
|
- public Map<String, Object> getOverviewData() {
|
|
|
- List<PurchaseRecord> purchaseRecords = dataLoader.getPurchaseRecords();
|
|
|
- List<SalesRecord> salesRecords = dataLoader.getSalesRecords();
|
|
|
- Set<String> finishedSkus = getFinishedSkus();
|
|
|
-
|
|
|
- if (!finishedSkus.isEmpty()) {
|
|
|
- purchaseRecords = purchaseRecords.stream()
|
|
|
- .filter(r -> finishedSkus.contains(r.getProductCode()))
|
|
|
- .collect(Collectors.toList());
|
|
|
- salesRecords = salesRecords.stream()
|
|
|
- .filter(r -> finishedSkus.contains(r.getProductCode()))
|
|
|
- .collect(Collectors.toList());
|
|
|
- }
|
|
|
-
|
|
|
- int totalPurchaseQty = (int) Math.round(purchaseRecords.stream().mapToDouble(PurchaseRecord::getQuantity).sum());
|
|
|
- int totalSalesQty = (int) Math.round(salesRecords.stream().mapToDouble(SalesRecord::getQuantity).sum());
|
|
|
- int assemblyQty = calculateAssemblyQuantity(finishedSkus);
|
|
|
-
|
|
|
- int totalInventory = totalPurchaseQty + assemblyQty - totalSalesQty;
|
|
|
- double totalValue = purchaseRecords.stream().mapToDouble(PurchaseRecord::getAmount).sum() / 10000.0;
|
|
|
- totalValue = Math.round(totalValue * 100.0) / 100.0;
|
|
|
+ public void invalidateCache() {
|
|
|
+ snapshot = null;
|
|
|
+ }
|
|
|
|
|
|
- double avgInventory = totalInventory > 0 ? totalInventory / 2.0 : 1.0;
|
|
|
- double turnoverRate = avgInventory > 0 ? (totalSalesQty / avgInventory) : 0.0;
|
|
|
- turnoverRate = Math.round(turnoverRate * 100.0) / 100.0;
|
|
|
+ public void warmCache() {
|
|
|
+ ensureSnapshot();
|
|
|
+ }
|
|
|
|
|
|
- Map<String, Object> result = new LinkedHashMap<>();
|
|
|
- result.put("totalInventory", totalInventory);
|
|
|
- result.put("totalValue", totalValue);
|
|
|
- result.put("turnoverRate", turnoverRate);
|
|
|
- result.put("inTransitRatio", 12.5);
|
|
|
- result.put("purchaseQty", totalPurchaseQty);
|
|
|
- result.put("salesQty", totalSalesQty);
|
|
|
- result.put("assemblyQty", assemblyQty);
|
|
|
- return result;
|
|
|
+ public Map<String, Object> getOverviewData() {
|
|
|
+ return new LinkedHashMap<>(ensureSnapshot().overviewData);
|
|
|
}
|
|
|
|
|
|
public Map<String, Object> getHealthIndex() {
|
|
|
@@ -89,12 +65,12 @@ public class InventoryService {
|
|
|
public Map<String, Object> getLifecycleDistribution() {
|
|
|
Map<String, Object> result = new LinkedHashMap<>();
|
|
|
Map<String, Integer> finished = new LinkedHashMap<>();
|
|
|
- finished.put("引入期", 3200);
|
|
|
+ finished.put("导入期", 3200);
|
|
|
finished.put("成长期", 8500);
|
|
|
finished.put("成熟期", 12800);
|
|
|
finished.put("衰退期", 4060);
|
|
|
Map<String, Integer> semi = new LinkedHashMap<>();
|
|
|
- semi.put("引入期", 2100);
|
|
|
+ semi.put("导入期", 2100);
|
|
|
semi.put("成长期", 5800);
|
|
|
semi.put("成熟期", 7200);
|
|
|
semi.put("衰退期", 2020);
|
|
|
@@ -111,9 +87,136 @@ public class InventoryService {
|
|
|
}
|
|
|
|
|
|
public Map<String, Object> getMonthlyComparisonData() {
|
|
|
+ return new LinkedHashMap<>(ensureSnapshot().monthlyComparisonData);
|
|
|
+ }
|
|
|
+
|
|
|
+ public List<Map<String, Object>> getSkuSummaryTable() {
|
|
|
+ return new ArrayList<>(ensureSnapshot().skuSummaryTable);
|
|
|
+ }
|
|
|
+
|
|
|
+ public List<Map<String, Object>> getSpuSummaryTable() {
|
|
|
+ return new ArrayList<>(ensureSnapshot().spuSummaryTable);
|
|
|
+ }
|
|
|
+
|
|
|
+ public List<Map<String, Object>> getMonthlyTurnoverTable() {
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+
|
|
|
+ public Map<String, Object> getSettings() {
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
+ result.put("data", StorageSettings.getAnalysisWeights());
|
|
|
+ result.put("defaults", StorageSettings.DEFAULT_ANALYSIS_WEIGHTS);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ public Map<String, Object> updateSettings(Map<String, Object> payload) {
|
|
|
+ StorageSettings.updateAnalysisWeights(payload);
|
|
|
+ invalidateCache();
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
+ result.put("data", StorageSettings.getAnalysisWeights());
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private InventorySnapshot ensureSnapshot() {
|
|
|
+ InventorySnapshot local = snapshot;
|
|
|
+ if (local != null) {
|
|
|
+ return local;
|
|
|
+ }
|
|
|
+ synchronized (this) {
|
|
|
+ local = snapshot;
|
|
|
+ if (local == null) {
|
|
|
+ local = buildSnapshot();
|
|
|
+ snapshot = local;
|
|
|
+ }
|
|
|
+ return local;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private InventorySnapshot buildSnapshot() {
|
|
|
List<PurchaseRecord> purchaseRecords = dataLoader.getPurchaseRecords();
|
|
|
List<SalesRecord> salesRecords = dataLoader.getSalesRecords();
|
|
|
+ List<ProductInfo> productInfos = dataLoader.getProductInfo();
|
|
|
+ Map<String, ProductInfo> productInfoMap = productInfos.stream()
|
|
|
+ .filter(info -> info.getProductCode() != null && !info.getProductCode().trim().isEmpty())
|
|
|
+ .collect(Collectors.toMap(ProductInfo::getProductCode, info -> info, (a, b) -> a));
|
|
|
+ Set<String> finishedSkus = getFinishedSkus(productInfos);
|
|
|
+
|
|
|
+ List<PurchaseRecord> filteredPurchase = filterPurchaseRecords(purchaseRecords, finishedSkus);
|
|
|
+ List<SalesRecord> filteredSales = filterSalesRecords(salesRecords, finishedSkus);
|
|
|
+ Map<String, double[]> purchaseSummary = summarizePurchases(filteredPurchase);
|
|
|
+ Map<String, Double> salesSummary = summarizeSales(filteredSales);
|
|
|
+
|
|
|
+ return new InventorySnapshot(
|
|
|
+ buildOverviewData(filteredPurchase, filteredSales, finishedSkus),
|
|
|
+ buildMonthlyComparisonData(filteredPurchase, filteredSales),
|
|
|
+ buildSkuSummaryTable(purchaseSummary, salesSummary, productInfoMap),
|
|
|
+ buildSpuSummaryTable(purchaseSummary, salesSummary, productInfoMap)
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<PurchaseRecord> filterPurchaseRecords(List<PurchaseRecord> purchaseRecords, Set<String> finishedSkus) {
|
|
|
+ if (finishedSkus == null || finishedSkus.isEmpty()) {
|
|
|
+ return purchaseRecords;
|
|
|
+ }
|
|
|
+ return purchaseRecords.stream()
|
|
|
+ .filter(r -> finishedSkus.contains(r.getProductCode()))
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<SalesRecord> filterSalesRecords(List<SalesRecord> salesRecords, Set<String> finishedSkus) {
|
|
|
+ if (finishedSkus == null || finishedSkus.isEmpty()) {
|
|
|
+ return salesRecords;
|
|
|
+ }
|
|
|
+ return salesRecords.stream()
|
|
|
+ .filter(r -> finishedSkus.contains(r.getProductCode()))
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ }
|
|
|
|
|
|
+ private Map<String, double[]> summarizePurchases(List<PurchaseRecord> purchaseRecords) {
|
|
|
+ Map<String, double[]> purchaseSummary = new HashMap<>();
|
|
|
+ for (PurchaseRecord record : purchaseRecords) {
|
|
|
+ double[] agg = purchaseSummary.computeIfAbsent(record.getProductCode(), k -> new double[2]);
|
|
|
+ agg[0] += record.getQuantity();
|
|
|
+ agg[1] += record.getAmount();
|
|
|
+ }
|
|
|
+ return purchaseSummary;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, Double> summarizeSales(List<SalesRecord> salesRecords) {
|
|
|
+ Map<String, Double> salesSummary = new HashMap<>();
|
|
|
+ for (SalesRecord record : salesRecords) {
|
|
|
+ salesSummary.merge(record.getProductCode(), record.getQuantity(), Double::sum);
|
|
|
+ }
|
|
|
+ return salesSummary;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, Object> buildOverviewData(List<PurchaseRecord> purchaseRecords,
|
|
|
+ List<SalesRecord> salesRecords,
|
|
|
+ Set<String> finishedSkus) {
|
|
|
+ int totalPurchaseQty = (int) Math.round(purchaseRecords.stream().mapToDouble(PurchaseRecord::getQuantity).sum());
|
|
|
+ int totalSalesQty = (int) Math.round(salesRecords.stream().mapToDouble(SalesRecord::getQuantity).sum());
|
|
|
+ int assemblyQty = calculateAssemblyQuantity(finishedSkus);
|
|
|
+
|
|
|
+ int totalInventory = totalPurchaseQty + assemblyQty - totalSalesQty;
|
|
|
+ double totalValue = purchaseRecords.stream().mapToDouble(PurchaseRecord::getAmount).sum() / 10000.0;
|
|
|
+ totalValue = Math.round(totalValue * 100.0) / 100.0;
|
|
|
+
|
|
|
+ double avgInventory = totalInventory > 0 ? totalInventory / 2.0 : 1.0;
|
|
|
+ double turnoverRate = avgInventory > 0 ? (totalSalesQty / avgInventory) : 0.0;
|
|
|
+ turnoverRate = Math.round(turnoverRate * 100.0) / 100.0;
|
|
|
+
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
+ result.put("totalInventory", totalInventory);
|
|
|
+ result.put("totalValue", totalValue);
|
|
|
+ result.put("turnoverRate", turnoverRate);
|
|
|
+ result.put("inTransitRatio", 12.5);
|
|
|
+ result.put("purchaseQty", totalPurchaseQty);
|
|
|
+ result.put("salesQty", totalSalesQty);
|
|
|
+ result.put("assemblyQty", assemblyQty);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, Object> buildMonthlyComparisonData(List<PurchaseRecord> purchaseRecords, List<SalesRecord> salesRecords) {
|
|
|
Map<YearMonth, Double> purchaseMonthly = new TreeMap<>();
|
|
|
for (PurchaseRecord record : purchaseRecords) {
|
|
|
LocalDate date = record.getDate();
|
|
|
@@ -165,35 +268,9 @@ public class InventoryService {
|
|
|
return result;
|
|
|
}
|
|
|
|
|
|
- public List<Map<String, Object>> getSkuSummaryTable() {
|
|
|
- List<PurchaseRecord> purchaseRecords = dataLoader.getPurchaseRecords();
|
|
|
- List<SalesRecord> salesRecords = dataLoader.getSalesRecords();
|
|
|
- Map<String, ProductInfo> productInfoMap = dataLoader.getProductInfo().stream()
|
|
|
- .filter(info -> info.getProductCode() != null && !info.getProductCode().trim().isEmpty())
|
|
|
- .collect(Collectors.toMap(ProductInfo::getProductCode, info -> info, (a, b) -> a));
|
|
|
- Set<String> finishedSkus = getFinishedSkus();
|
|
|
-
|
|
|
- if (!finishedSkus.isEmpty()) {
|
|
|
- purchaseRecords = purchaseRecords.stream()
|
|
|
- .filter(r -> finishedSkus.contains(r.getProductCode()))
|
|
|
- .collect(Collectors.toList());
|
|
|
- salesRecords = salesRecords.stream()
|
|
|
- .filter(r -> finishedSkus.contains(r.getProductCode()))
|
|
|
- .collect(Collectors.toList());
|
|
|
- }
|
|
|
-
|
|
|
- Map<String, double[]> purchaseSummary = new HashMap<>();
|
|
|
- for (PurchaseRecord record : purchaseRecords) {
|
|
|
- double[] agg = purchaseSummary.computeIfAbsent(record.getProductCode(), k -> new double[2]);
|
|
|
- agg[0] += record.getQuantity();
|
|
|
- agg[1] += record.getAmount();
|
|
|
- }
|
|
|
-
|
|
|
- Map<String, Double> salesSummary = new HashMap<>();
|
|
|
- for (SalesRecord record : salesRecords) {
|
|
|
- salesSummary.merge(record.getProductCode(), record.getQuantity(), Double::sum);
|
|
|
- }
|
|
|
-
|
|
|
+ private List<Map<String, Object>> buildSkuSummaryTable(Map<String, double[]> purchaseSummary,
|
|
|
+ Map<String, Double> salesSummary,
|
|
|
+ Map<String, ProductInfo> productInfoMap) {
|
|
|
double totalAmount = purchaseSummary.values().stream().mapToDouble(v -> v[1]).sum();
|
|
|
|
|
|
List<Map<String, Object>> result = new ArrayList<>();
|
|
|
@@ -224,40 +301,14 @@ public class InventoryService {
|
|
|
|
|
|
result.sort(Comparator.comparing((Map<String, Object> row) -> ((Number) row.getOrDefault("purchaseQty", 0)).doubleValue()).reversed());
|
|
|
if (result.size() > 20) {
|
|
|
- return result.subList(0, 20);
|
|
|
+ return new ArrayList<>(result.subList(0, 20));
|
|
|
}
|
|
|
return result;
|
|
|
}
|
|
|
|
|
|
- public List<Map<String, Object>> getSpuSummaryTable() {
|
|
|
- List<PurchaseRecord> purchaseRecords = dataLoader.getPurchaseRecords();
|
|
|
- List<SalesRecord> salesRecords = dataLoader.getSalesRecords();
|
|
|
- Map<String, ProductInfo> productInfoMap = dataLoader.getProductInfo().stream()
|
|
|
- .filter(info -> info.getProductCode() != null && !info.getProductCode().trim().isEmpty())
|
|
|
- .collect(Collectors.toMap(ProductInfo::getProductCode, info -> info, (a, b) -> a));
|
|
|
- Set<String> finishedSkus = getFinishedSkus();
|
|
|
-
|
|
|
- if (!finishedSkus.isEmpty()) {
|
|
|
- purchaseRecords = purchaseRecords.stream()
|
|
|
- .filter(r -> finishedSkus.contains(r.getProductCode()))
|
|
|
- .collect(Collectors.toList());
|
|
|
- salesRecords = salesRecords.stream()
|
|
|
- .filter(r -> finishedSkus.contains(r.getProductCode()))
|
|
|
- .collect(Collectors.toList());
|
|
|
- }
|
|
|
-
|
|
|
- Map<String, double[]> purchaseSummary = new HashMap<>();
|
|
|
- for (PurchaseRecord record : purchaseRecords) {
|
|
|
- double[] agg = purchaseSummary.computeIfAbsent(record.getProductCode(), k -> new double[2]);
|
|
|
- agg[0] += record.getQuantity();
|
|
|
- agg[1] += record.getAmount();
|
|
|
- }
|
|
|
-
|
|
|
- Map<String, Double> salesSummary = new HashMap<>();
|
|
|
- for (SalesRecord record : salesRecords) {
|
|
|
- salesSummary.merge(record.getProductCode(), record.getQuantity(), Double::sum);
|
|
|
- }
|
|
|
-
|
|
|
+ private List<Map<String, Object>> buildSpuSummaryTable(Map<String, double[]> purchaseSummary,
|
|
|
+ Map<String, Double> salesSummary,
|
|
|
+ Map<String, ProductInfo> productInfoMap) {
|
|
|
Map<String, SpuAggregate> aggregates = new HashMap<>();
|
|
|
for (Map.Entry<String, double[]> entry : purchaseSummary.entrySet()) {
|
|
|
String sku = entry.getKey();
|
|
|
@@ -303,24 +354,6 @@ public class InventoryService {
|
|
|
return result;
|
|
|
}
|
|
|
|
|
|
- public List<Map<String, Object>> getMonthlyTurnoverTable() {
|
|
|
- return Collections.emptyList();
|
|
|
- }
|
|
|
-
|
|
|
- public Map<String, Object> getSettings() {
|
|
|
- Map<String, Object> result = new LinkedHashMap<>();
|
|
|
- result.put("data", StorageSettings.getAnalysisWeights());
|
|
|
- result.put("defaults", StorageSettings.DEFAULT_ANALYSIS_WEIGHTS);
|
|
|
- return result;
|
|
|
- }
|
|
|
-
|
|
|
- public Map<String, Object> updateSettings(Map<String, Object> payload) {
|
|
|
- StorageSettings.updateAnalysisWeights(payload);
|
|
|
- Map<String, Object> result = new LinkedHashMap<>();
|
|
|
- result.put("data", StorageSettings.getAnalysisWeights());
|
|
|
- return result;
|
|
|
- }
|
|
|
-
|
|
|
private int calculateAssemblyQuantity(Set<String> finishedSkus) {
|
|
|
List<AssemblyRecord> assemblyRecords = dataLoader.getAssemblyRecords();
|
|
|
if (assemblyRecords.isEmpty()) {
|
|
|
@@ -337,19 +370,14 @@ public class InventoryService {
|
|
|
continue;
|
|
|
}
|
|
|
boolean counted = false;
|
|
|
- // 组装明细里可能直接记录成品编码
|
|
|
if (finishedSkus.contains(code)) {
|
|
|
total += record.getQuantity();
|
|
|
counted = true;
|
|
|
}
|
|
|
- // 半成品编码需要映射到成品
|
|
|
if (!counted) {
|
|
|
Set<String> finished = semiToFinished.get(code);
|
|
|
- if (finished != null && !finished.isEmpty()) {
|
|
|
- boolean match = finished.stream().anyMatch(finishedSkus::contains);
|
|
|
- if (match) {
|
|
|
- total += record.getQuantity();
|
|
|
- }
|
|
|
+ if (finished != null && !finished.isEmpty() && finished.stream().anyMatch(finishedSkus::contains)) {
|
|
|
+ total += record.getQuantity();
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -389,7 +417,7 @@ public class InventoryService {
|
|
|
if (raw == null || raw.trim().isEmpty()) {
|
|
|
return;
|
|
|
}
|
|
|
- String normalized = raw.replace(',', ',');
|
|
|
+ String normalized = raw.replace(',', ',').replace('、', ',').replace(';', ',').replace(';', ',');
|
|
|
for (String part : normalized.split(",")) {
|
|
|
String code = part.trim();
|
|
|
if (!code.isEmpty()) {
|
|
|
@@ -399,7 +427,10 @@ public class InventoryService {
|
|
|
}
|
|
|
|
|
|
private Set<String> getFinishedSkus() {
|
|
|
- List<ProductInfo> infos = dataLoader.getProductInfo();
|
|
|
+ return getFinishedSkus(dataLoader.getProductInfo());
|
|
|
+ }
|
|
|
+
|
|
|
+ private Set<String> getFinishedSkus(List<ProductInfo> infos) {
|
|
|
if (infos.isEmpty()) {
|
|
|
return Collections.emptySet();
|
|
|
}
|
|
|
@@ -481,6 +512,21 @@ public class InventoryService {
|
|
|
.orElse("");
|
|
|
}
|
|
|
}
|
|
|
-}
|
|
|
-
|
|
|
|
|
|
+ private static class InventorySnapshot {
|
|
|
+ private final Map<String, Object> overviewData;
|
|
|
+ private final Map<String, Object> monthlyComparisonData;
|
|
|
+ private final List<Map<String, Object>> skuSummaryTable;
|
|
|
+ private final List<Map<String, Object>> spuSummaryTable;
|
|
|
+
|
|
|
+ private InventorySnapshot(Map<String, Object> overviewData,
|
|
|
+ Map<String, Object> monthlyComparisonData,
|
|
|
+ List<Map<String, Object>> skuSummaryTable,
|
|
|
+ List<Map<String, Object>> spuSummaryTable) {
|
|
|
+ this.overviewData = overviewData;
|
|
|
+ this.monthlyComparisonData = monthlyComparisonData;
|
|
|
+ this.skuSummaryTable = skuSummaryTable;
|
|
|
+ this.spuSummaryTable = spuSummaryTable;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|