|
@@ -1,11 +1,15 @@
|
|
|
package com.dtm.storage.service;
|
|
package com.dtm.storage.service;
|
|
|
|
|
|
|
|
|
|
+import com.dtm.storage.model.AssemblyRecord;
|
|
|
|
|
+import com.dtm.storage.model.ProductInfo;
|
|
|
import com.dtm.storage.model.PurchaseRecord;
|
|
import com.dtm.storage.model.PurchaseRecord;
|
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.stereotype.Service;
|
|
|
|
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.ArrayList;
|
|
|
import java.util.Collections;
|
|
import java.util.Collections;
|
|
|
|
|
+import java.util.Comparator;
|
|
|
import java.util.HashMap;
|
|
import java.util.HashMap;
|
|
|
|
|
+import java.util.HashSet;
|
|
|
import java.util.LinkedHashMap;
|
|
import java.util.LinkedHashMap;
|
|
|
import java.util.List;
|
|
import java.util.List;
|
|
|
import java.util.Map;
|
|
import java.util.Map;
|
|
@@ -21,77 +25,73 @@ public class SemiProductService {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public Map<String, Object> getStatistics() {
|
|
public Map<String, Object> getStatistics() {
|
|
|
- List<List<Object>> mappingRows = dataLoader.getSemiMappingRows();
|
|
|
|
|
- List<String> headers = dataLoader.getSemiMappingHeaders();
|
|
|
|
|
- Set<String> semiCodes = collectSemiCodes(mappingRows, headers);
|
|
|
|
|
-
|
|
|
|
|
- Map<String, Double> purchaseByCode = dataLoader.getPurchaseRecords().stream()
|
|
|
|
|
- .collect(Collectors.groupingBy(PurchaseRecord::getProductCode, Collectors.summingDouble(PurchaseRecord::getQuantity)));
|
|
|
|
|
-
|
|
|
|
|
- int totalQty = 0;
|
|
|
|
|
- for (String code : semiCodes) {
|
|
|
|
|
- totalQty += (int) Math.round(purchaseByCode.getOrDefault(code, 0.0));
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
|
|
+ SemiSnapshot snapshot = buildSnapshot();
|
|
|
|
|
+ int totalQty = snapshot.rows.stream()
|
|
|
|
|
+ .mapToInt(row -> ((Number) row.getOrDefault("quantity", 0)).intValue())
|
|
|
|
|
+ .sum();
|
|
|
int estimatedFinished = getAssemblyCapacity().stream()
|
|
int estimatedFinished = getAssemblyCapacity().stream()
|
|
|
.mapToInt(item -> ((Number) item.getOrDefault("capacity", 0)).intValue())
|
|
.mapToInt(item -> ((Number) item.getOrDefault("capacity", 0)).intValue())
|
|
|
.sum();
|
|
.sum();
|
|
|
|
|
|
|
|
Map<String, Object> result = new LinkedHashMap<>();
|
|
Map<String, Object> result = new LinkedHashMap<>();
|
|
|
- result.put("totalSemiProducts", semiCodes.size());
|
|
|
|
|
|
|
+ result.put("totalSemiProducts", snapshot.rows.size());
|
|
|
result.put("totalQuantity", totalQty);
|
|
result.put("totalQuantity", totalQty);
|
|
|
result.put("estimatedFinished", estimatedFinished);
|
|
result.put("estimatedFinished", estimatedFinished);
|
|
|
return result;
|
|
return result;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public Map<String, Object> getList(int page, int pageSize, String keyword, String status, String category) {
|
|
public Map<String, Object> getList(int page, int pageSize, String keyword, String status, String category) {
|
|
|
- List<Map<String, Object>> data = sampleSemiProducts();
|
|
|
|
|
- List<Map<String, Object>> filtered = new ArrayList<>(data);
|
|
|
|
|
|
|
+ SemiSnapshot snapshot = buildSnapshot();
|
|
|
|
|
+ List<Map<String, Object>> filtered = new ArrayList<>(snapshot.rows);
|
|
|
|
|
+
|
|
|
if (keyword != null && !keyword.trim().isEmpty()) {
|
|
if (keyword != null && !keyword.trim().isEmpty()) {
|
|
|
String key = keyword.trim();
|
|
String key = keyword.trim();
|
|
|
- filtered = filtered.stream()
|
|
|
|
|
- .filter(d -> d.get("name").toString().contains(key) || d.get("code").toString().contains(key))
|
|
|
|
|
- .collect(Collectors.toList());
|
|
|
|
|
|
|
+ filtered = filtered.stream().filter(row -> {
|
|
|
|
|
+ String code = String.valueOf(row.getOrDefault("code", ""));
|
|
|
|
|
+ String name = String.valueOf(row.getOrDefault("name", ""));
|
|
|
|
|
+ return code.contains(key) || name.contains(key);
|
|
|
|
|
+ }).collect(Collectors.toList());
|
|
|
}
|
|
}
|
|
|
if (status != null && !status.trim().isEmpty()) {
|
|
if (status != null && !status.trim().isEmpty()) {
|
|
|
filtered = filtered.stream()
|
|
filtered = filtered.stream()
|
|
|
- .filter(d -> status.equals(d.get("status")))
|
|
|
|
|
|
|
+ .filter(row -> status.equals(row.get("status")))
|
|
|
.collect(Collectors.toList());
|
|
.collect(Collectors.toList());
|
|
|
}
|
|
}
|
|
|
if (category != null && !category.trim().isEmpty()) {
|
|
if (category != null && !category.trim().isEmpty()) {
|
|
|
filtered = filtered.stream()
|
|
filtered = filtered.stream()
|
|
|
- .filter(d -> category.equals(d.get("category")))
|
|
|
|
|
|
|
+ .filter(row -> category.equals(row.get("category")))
|
|
|
.collect(Collectors.toList());
|
|
.collect(Collectors.toList());
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ int safePageSize = pageSize <= 0 ? 10 : pageSize;
|
|
|
|
|
+ int safePage = page <= 0 ? 1 : page;
|
|
|
int total = filtered.size();
|
|
int total = filtered.size();
|
|
|
- int start = Math.max(0, (page - 1) * pageSize);
|
|
|
|
|
- int end = Math.min(total, start + pageSize);
|
|
|
|
|
|
|
+ int start = Math.max(0, (safePage - 1) * safePageSize);
|
|
|
|
|
+ int end = Math.min(total, start + safePageSize);
|
|
|
List<Map<String, Object>> list = start >= total ? Collections.emptyList() : filtered.subList(start, end);
|
|
List<Map<String, Object>> list = start >= total ? Collections.emptyList() : filtered.subList(start, end);
|
|
|
|
|
|
|
|
Map<String, Object> result = new LinkedHashMap<>();
|
|
Map<String, Object> result = new LinkedHashMap<>();
|
|
|
result.put("list", list);
|
|
result.put("list", list);
|
|
|
result.put("total", total);
|
|
result.put("total", total);
|
|
|
- result.put("page", page);
|
|
|
|
|
- result.put("pageSize", pageSize);
|
|
|
|
|
|
|
+ result.put("page", safePage);
|
|
|
|
|
+ result.put("pageSize", safePageSize);
|
|
|
return result;
|
|
return result;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public Map<String, Object> getDetail(int id) {
|
|
public Map<String, Object> getDetail(int id) {
|
|
|
- Map<String, Object> result = new LinkedHashMap<>();
|
|
|
|
|
- result.put("id", id);
|
|
|
|
|
- result.put("code", "SEM-101");
|
|
|
|
|
- result.put("name", "电路板 PCB-A");
|
|
|
|
|
- result.put("category", "pcb");
|
|
|
|
|
- result.put("quantity", 3200);
|
|
|
|
|
- result.put("safeStock", 2000);
|
|
|
|
|
- result.put("status", "in-stock");
|
|
|
|
|
- return result;
|
|
|
|
|
|
|
+ SemiSnapshot snapshot = buildSnapshot();
|
|
|
|
|
+ for (Map<String, Object> row : snapshot.rows) {
|
|
|
|
|
+ int rowId = ((Number) row.getOrDefault("id", 0)).intValue();
|
|
|
|
|
+ if (rowId == id) {
|
|
|
|
|
+ return new LinkedHashMap<>(row);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return Collections.emptyMap();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public Map<String, Object> create(Map<String, Object> payload) {
|
|
public Map<String, Object> create(Map<String, Object> payload) {
|
|
|
Map<String, Object> result = new LinkedHashMap<>();
|
|
Map<String, Object> result = new LinkedHashMap<>();
|
|
|
- result.put("id", 999);
|
|
|
|
|
|
|
+ result.put("id", 0);
|
|
|
if (payload != null) {
|
|
if (payload != null) {
|
|
|
result.putAll(payload);
|
|
result.putAll(payload);
|
|
|
}
|
|
}
|
|
@@ -111,78 +111,111 @@ public class SemiProductService {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public List<Map<String, Object>> getTurnoverAnalysis() {
|
|
public List<Map<String, Object>> getTurnoverAnalysis() {
|
|
|
|
|
+ SemiSnapshot snapshot = buildSnapshot();
|
|
|
List<Map<String, Object>> list = new ArrayList<>();
|
|
List<Map<String, Object>> list = new ArrayList<>();
|
|
|
- list.add(buildTurnover("电路板 PCB-A", 18, 85, 850, 1200, "正常", "库存充足,周转良好"));
|
|
|
|
|
- list.add(buildTurnover("显示屏模块", 22, 78, 650, 1000, "正常", "建议保持当前库存水平"));
|
|
|
|
|
|
|
+ for (Map<String, Object> row : snapshot.rows) {
|
|
|
|
|
+ int quantity = ((Number) row.getOrDefault("quantity", 0)).intValue();
|
|
|
|
|
+ int inboundQty = ((Number) row.getOrDefault("inboundQty", 0)).intValue();
|
|
|
|
|
+ int consumedQty = ((Number) row.getOrDefault("consumedQty", 0)).intValue();
|
|
|
|
|
+ int avgTurnover = consumedQty <= 0 ? 999 : (int) Math.round(quantity * 30.0 / consumedQty);
|
|
|
|
|
+ int usageRate = inboundQty <= 0 ? 0 : (int) Math.min(100, Math.round(consumedQty * 100.0 / inboundQty));
|
|
|
|
|
+ int monthlyConsumption = consumedQty;
|
|
|
|
|
+ int reorderPoint = Math.max(100, (int) Math.round(monthlyConsumption * 0.6));
|
|
|
|
|
+ String status = String.valueOf(row.getOrDefault("status", ""));
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> item = new LinkedHashMap<>();
|
|
|
|
|
+ item.put("name", row.getOrDefault("name", ""));
|
|
|
|
|
+ item.put("avgTurnover", avgTurnover);
|
|
|
|
|
+ item.put("usageRate", usageRate);
|
|
|
|
|
+ item.put("monthlyConsumption", monthlyConsumption);
|
|
|
|
|
+ item.put("reorderPoint", reorderPoint);
|
|
|
|
|
+ item.put("stockStatus", convertStockStatus(status));
|
|
|
|
|
+ item.put("suggestion", buildSuggestion(status));
|
|
|
|
|
+ list.add(item);
|
|
|
|
|
+ }
|
|
|
|
|
+ list.sort(Comparator.comparingInt((Map<String, Object> item) ->
|
|
|
|
|
+ ((Number) item.getOrDefault("monthlyConsumption", 0)).intValue()).reversed());
|
|
|
return list;
|
|
return list;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public Map<String, Object> getBomRelation(String productCode) {
|
|
public Map<String, Object> getBomRelation(String productCode) {
|
|
|
|
|
+ SemiSnapshot snapshot = buildSnapshot();
|
|
|
Map<String, Object> result = new LinkedHashMap<>();
|
|
Map<String, Object> result = new LinkedHashMap<>();
|
|
|
- result.put("product", "智能手表 Pro X1");
|
|
|
|
|
|
|
+ ProductInfo finishedInfo = snapshot.productInfoByCode.get(productCode);
|
|
|
|
|
+ result.put("product", finishedInfo != null && !isBlank(finishedInfo.getProductName())
|
|
|
|
|
+ ? finishedInfo.getProductName()
|
|
|
|
|
+ : productCode);
|
|
|
result.put("code", productCode);
|
|
result.put("code", productCode);
|
|
|
- List<Map<String, Object>> bom = new ArrayList<>();
|
|
|
|
|
- Map<String, Object> module1 = new LinkedHashMap<>();
|
|
|
|
|
- module1.put("module", "电子模块");
|
|
|
|
|
- module1.put("items", java.util.Arrays.asList(
|
|
|
|
|
- buildBomItem("电路板 PCB-A", 1, 3200),
|
|
|
|
|
- buildBomItem("显示屏模块", 1, 2800),
|
|
|
|
|
- buildBomItem("锂电池组", 1, 1200)
|
|
|
|
|
- ));
|
|
|
|
|
- Map<String, Object> module2 = new LinkedHashMap<>();
|
|
|
|
|
- module2.put("module", "结构件");
|
|
|
|
|
- module2.put("items", java.util.Arrays.asList(
|
|
|
|
|
- buildBomItem("金属外壳", 1, 4500),
|
|
|
|
|
- buildBomItem("表带", 2, 5200)
|
|
|
|
|
- ));
|
|
|
|
|
- bom.add(module1);
|
|
|
|
|
- bom.add(module2);
|
|
|
|
|
- result.put("bom", bom);
|
|
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Double> bom = snapshot.bomItems.getOrDefault(productCode, Collections.emptyMap());
|
|
|
|
|
+ List<Map<String, Object>> items = new ArrayList<>();
|
|
|
|
|
+ for (Map.Entry<String, Double> entry : bom.entrySet()) {
|
|
|
|
|
+ String semiCode = entry.getKey();
|
|
|
|
|
+ double needQty = entry.getValue() == null ? 0.0 : entry.getValue();
|
|
|
|
|
+ if (needQty <= 0) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ ProductInfo info = snapshot.productInfoByCode.get(semiCode);
|
|
|
|
|
+ int stock = toInt(snapshot.semiInventoryQty.getOrDefault(semiCode, 0.0));
|
|
|
|
|
+ Map<String, Object> item = new LinkedHashMap<>();
|
|
|
|
|
+ item.put("name", info != null && !isBlank(info.getProductName()) ? info.getProductName() : semiCode);
|
|
|
|
|
+ item.put("code", semiCode);
|
|
|
|
|
+ item.put("quantity", toInt(needQty));
|
|
|
|
|
+ item.put("stock", stock);
|
|
|
|
|
+ items.add(item);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> module = new LinkedHashMap<>();
|
|
|
|
|
+ module.put("module", "BOM");
|
|
|
|
|
+ module.put("items", items);
|
|
|
|
|
+ result.put("bom", Collections.singletonList(module));
|
|
|
return result;
|
|
return result;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public List<Map<String, Object>> getAssemblyCapacity() {
|
|
public List<Map<String, Object>> getAssemblyCapacity() {
|
|
|
- List<List<Object>> mappingRows = dataLoader.getSemiMappingRows();
|
|
|
|
|
- List<String> headers = dataLoader.getSemiMappingHeaders();
|
|
|
|
|
- if (mappingRows.isEmpty()) {
|
|
|
|
|
|
|
+ SemiSnapshot snapshot = buildSnapshot();
|
|
|
|
|
+ if (snapshot.bomItems.isEmpty()) {
|
|
|
return Collections.emptyList();
|
|
return Collections.emptyList();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- int productIdx = findHeaderIndex(headers, new String[]{"成品", "成品编码", "产品编码", "产品代码"}, 0);
|
|
|
|
|
- int innerIdx = findHeaderIndex(headers, new String[]{"内芯", "内胆"}, 1);
|
|
|
|
|
- int outerIdx = findHeaderIndex(headers, new String[]{"外壳", "外套"}, 2);
|
|
|
|
|
- int accessoryIdx = findHeaderIndex(headers, new String[]{"配件", "附件"}, 3);
|
|
|
|
|
-
|
|
|
|
|
- Map<String, Double> purchaseByCode = dataLoader.getPurchaseRecords().stream()
|
|
|
|
|
- .collect(Collectors.groupingBy(PurchaseRecord::getProductCode, Collectors.summingDouble(PurchaseRecord::getQuantity)));
|
|
|
|
|
-
|
|
|
|
|
List<Map<String, Object>> results = new ArrayList<>();
|
|
List<Map<String, Object>> results = new ArrayList<>();
|
|
|
- for (List<Object> row : mappingRows) {
|
|
|
|
|
- String productCode = toText(getValue(row, productIdx));
|
|
|
|
|
- if (productCode.isEmpty()) {
|
|
|
|
|
|
|
+ List<String> finishedCodes = new ArrayList<>(snapshot.bomItems.keySet());
|
|
|
|
|
+ Collections.sort(finishedCodes);
|
|
|
|
|
+ for (String finishedCode : finishedCodes) {
|
|
|
|
|
+ Map<String, Double> components = snapshot.bomItems.getOrDefault(finishedCode, Collections.emptyMap());
|
|
|
|
|
+ if (components.isEmpty()) {
|
|
|
continue;
|
|
continue;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- List<String> components = new ArrayList<>();
|
|
|
|
|
- collectCodes(components, toText(getValue(row, innerIdx)));
|
|
|
|
|
- collectCodes(components, toText(getValue(row, outerIdx)));
|
|
|
|
|
- collectCodes(components, toText(getValue(row, accessoryIdx)));
|
|
|
|
|
-
|
|
|
|
|
List<Map<String, Object>> componentList = new ArrayList<>();
|
|
List<Map<String, Object>> componentList = new ArrayList<>();
|
|
|
- List<Integer> capacities = new ArrayList<>();
|
|
|
|
|
- for (String code : components) {
|
|
|
|
|
- int available = (int) Math.round(purchaseByCode.getOrDefault(code, 0.0));
|
|
|
|
|
|
|
+ int capacity = Integer.MAX_VALUE;
|
|
|
|
|
+ for (Map.Entry<String, Double> entry : components.entrySet()) {
|
|
|
|
|
+ String semiCode = entry.getKey();
|
|
|
|
|
+ double needQty = entry.getValue() == null ? 0.0 : entry.getValue();
|
|
|
|
|
+ if (isBlank(semiCode) || needQty <= 0) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ int available = toInt(snapshot.semiInventoryQty.getOrDefault(semiCode, 0.0));
|
|
|
|
|
+ int possible = (int) Math.floor(available / needQty);
|
|
|
|
|
+ capacity = Math.min(capacity, Math.max(0, possible));
|
|
|
|
|
+
|
|
|
|
|
+ ProductInfo semiInfo = snapshot.productInfoByCode.get(semiCode);
|
|
|
Map<String, Object> comp = new LinkedHashMap<>();
|
|
Map<String, Object> comp = new LinkedHashMap<>();
|
|
|
- comp.put("code", code);
|
|
|
|
|
|
|
+ comp.put("code", semiCode);
|
|
|
|
|
+ comp.put("name", semiInfo != null && !isBlank(semiInfo.getProductName()) ? semiInfo.getProductName() : semiCode);
|
|
|
|
|
+ comp.put("required", toInt(needQty));
|
|
|
comp.put("available", available);
|
|
comp.put("available", available);
|
|
|
componentList.add(comp);
|
|
componentList.add(comp);
|
|
|
- capacities.add(available);
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+ if (capacity == Integer.MAX_VALUE) {
|
|
|
|
|
+ capacity = 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- int capacity = capacities.isEmpty() ? 0 : capacities.stream().min(Integer::compareTo).orElse(0);
|
|
|
|
|
|
|
+ ProductInfo finishedInfo = snapshot.productInfoByCode.get(finishedCode);
|
|
|
Map<String, Object> result = new LinkedHashMap<>();
|
|
Map<String, Object> result = new LinkedHashMap<>();
|
|
|
- result.put("product_code", productCode);
|
|
|
|
|
- result.put("product_name", productCode);
|
|
|
|
|
|
|
+ result.put("product_code", finishedCode);
|
|
|
|
|
+ result.put("product_name", finishedInfo != null && !isBlank(finishedInfo.getProductName())
|
|
|
|
|
+ ? finishedInfo.getProductName()
|
|
|
|
|
+ : finishedCode);
|
|
|
result.put("components", componentList);
|
|
result.put("components", componentList);
|
|
|
result.put("capacity", capacity);
|
|
result.put("capacity", capacity);
|
|
|
results.add(result);
|
|
results.add(result);
|
|
@@ -190,128 +223,189 @@ public class SemiProductService {
|
|
|
return results;
|
|
return results;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private Set<String> collectSemiCodes(List<List<Object>> rows, List<String> headers) {
|
|
|
|
|
- if (rows == null || rows.isEmpty()) {
|
|
|
|
|
- return Collections.emptySet();
|
|
|
|
|
- }
|
|
|
|
|
- int innerIdx = findHeaderIndex(headers, new String[]{"内芯", "内胆"}, 2);
|
|
|
|
|
- int outerIdx = findHeaderIndex(headers, new String[]{"外壳", "外套"}, 3);
|
|
|
|
|
- int accessoryIdx = findHeaderIndex(headers, new String[]{"配件", "附件"}, 4);
|
|
|
|
|
-
|
|
|
|
|
- Set<String> codes = new java.util.HashSet<>();
|
|
|
|
|
- for (List<Object> row : rows) {
|
|
|
|
|
- collectCodes(codes, toText(getValue(row, innerIdx)));
|
|
|
|
|
- collectCodes(codes, toText(getValue(row, outerIdx)));
|
|
|
|
|
- collectCodes(codes, toText(getValue(row, accessoryIdx)));
|
|
|
|
|
- }
|
|
|
|
|
- return codes;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ public Map<String, Object> getAssemblyCapacityPage(Integer page, Integer pageSize) {
|
|
|
|
|
+ List<Map<String, Object>> source = getAssemblyCapacity();
|
|
|
|
|
+ int safePageSize = pageSize == null || pageSize <= 0 ? 10 : pageSize;
|
|
|
|
|
+ int safePage = page == null || page <= 0 ? 1 : page;
|
|
|
|
|
+ int total = source.size();
|
|
|
|
|
+ int start = Math.max(0, (safePage - 1) * safePageSize);
|
|
|
|
|
+ int end = Math.min(total, start + safePageSize);
|
|
|
|
|
+ List<Map<String, Object>> list = start >= total ? Collections.emptyList() : source.subList(start, end);
|
|
|
|
|
|
|
|
- private void collectCodes(Set<String> target, String raw) {
|
|
|
|
|
- if (raw == null || raw.trim().isEmpty()) {
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- String normalized = raw.replace(',', ',');
|
|
|
|
|
- for (String part : normalized.split(",")) {
|
|
|
|
|
- String code = part.trim();
|
|
|
|
|
- if (!code.isEmpty()) {
|
|
|
|
|
- target.add(code);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
|
|
+ result.put("list", list);
|
|
|
|
|
+ result.put("total", total);
|
|
|
|
|
+ result.put("page", safePage);
|
|
|
|
|
+ result.put("pageSize", safePageSize);
|
|
|
|
|
+ return result;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private void collectCodes(List<String> target, String raw) {
|
|
|
|
|
- if (raw == null || raw.trim().isEmpty()) {
|
|
|
|
|
- return;
|
|
|
|
|
|
|
+ private SemiSnapshot buildSnapshot() {
|
|
|
|
|
+ List<ProductInfo> productInfos = dataLoader.getProductInfo();
|
|
|
|
|
+ Map<String, ProductInfo> productInfoByCode = productInfos.stream()
|
|
|
|
|
+ .filter(item -> item != null && !isBlank(item.getProductCode()))
|
|
|
|
|
+ .collect(Collectors.toMap(ProductInfo::getProductCode, item -> item, (a, b) -> a));
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Double> purchaseBySku = dataLoader.getPurchaseRecords().stream()
|
|
|
|
|
+ .filter(item -> item != null && !isBlank(item.getProductCode()))
|
|
|
|
|
+ .collect(Collectors.groupingBy(PurchaseRecord::getProductCode, Collectors.summingDouble(PurchaseRecord::getQuantity)));
|
|
|
|
|
+ Map<String, Double> assemblyBySku = dataLoader.getAssemblyRecords().stream()
|
|
|
|
|
+ .filter(item -> item != null && !isBlank(item.getProductCode()))
|
|
|
|
|
+ .collect(Collectors.groupingBy(AssemblyRecord::getProductCode, Collectors.summingDouble(AssemblyRecord::getQuantity)));
|
|
|
|
|
+ Map<String, Map<String, Double>> bomItems = dataLoader.getBomItems();
|
|
|
|
|
+ if (bomItems == null) {
|
|
|
|
|
+ bomItems = Collections.emptyMap();
|
|
|
}
|
|
}
|
|
|
- String normalized = raw.replace(',', ',');
|
|
|
|
|
- for (String part : normalized.split(",")) {
|
|
|
|
|
- String code = part.trim();
|
|
|
|
|
- if (!code.isEmpty()) {
|
|
|
|
|
- target.add(code);
|
|
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Double> semiConsumedQty = new HashMap<>();
|
|
|
|
|
+ Map<String, Set<String>> semiToFinished = new HashMap<>();
|
|
|
|
|
+ for (Map.Entry<String, Map<String, Double>> bomEntry : bomItems.entrySet()) {
|
|
|
|
|
+ String finishedCode = bomEntry.getKey();
|
|
|
|
|
+ Map<String, Double> components = bomEntry.getValue();
|
|
|
|
|
+ if (isBlank(finishedCode) || components == null || components.isEmpty()) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ double assemblyQty = assemblyBySku.getOrDefault(finishedCode, 0.0);
|
|
|
|
|
+ for (Map.Entry<String, Double> component : components.entrySet()) {
|
|
|
|
|
+ String semiCode = component.getKey();
|
|
|
|
|
+ double needQty = component.getValue() == null ? 0.0 : component.getValue();
|
|
|
|
|
+ if (isBlank(semiCode) || needQty <= 0) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ semiConsumedQty.merge(semiCode, assemblyQty * needQty, Double::sum);
|
|
|
|
|
+ semiToFinished.computeIfAbsent(semiCode, key -> new HashSet<>()).add(finishedCode);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- }
|
|
|
|
|
|
|
|
|
|
- private int findHeaderIndex(List<String> headers, String[] keywords, int fallback) {
|
|
|
|
|
- if (headers == null || headers.isEmpty()) {
|
|
|
|
|
- return fallback;
|
|
|
|
|
- }
|
|
|
|
|
- for (int i = 0; i < headers.size(); i++) {
|
|
|
|
|
- String header = headers.get(i) == null ? "" : headers.get(i).trim();
|
|
|
|
|
- if (header.isEmpty()) {
|
|
|
|
|
|
|
+ Set<String> semiCodes = new HashSet<>(semiToFinished.keySet());
|
|
|
|
|
+ for (ProductInfo info : productInfos) {
|
|
|
|
|
+ if (info == null || isBlank(info.getProductCode())) {
|
|
|
continue;
|
|
continue;
|
|
|
}
|
|
}
|
|
|
- for (String keyword : keywords) {
|
|
|
|
|
- if (keyword != null && !keyword.isEmpty() && header.contains(keyword)) {
|
|
|
|
|
- return i;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (isSemiCategory(info.getCategory())) {
|
|
|
|
|
+ semiCodes.add(info.getProductCode());
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- return fallback < headers.size() ? fallback : -1;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ Map<String, Double> semiInventoryQty = new HashMap<>();
|
|
|
|
|
+ List<Map<String, Object>> rows = new ArrayList<>();
|
|
|
|
|
+ List<String> sortedCodes = new ArrayList<>(semiCodes);
|
|
|
|
|
+ Collections.sort(sortedCodes);
|
|
|
|
|
+ int index = 1;
|
|
|
|
|
+ for (String code : sortedCodes) {
|
|
|
|
|
+ ProductInfo info = productInfoByCode.get(code);
|
|
|
|
|
+ String name = info != null && !isBlank(info.getProductName()) ? info.getProductName() : code;
|
|
|
|
|
+ String category = info != null && !isBlank(info.getCategory()) ? info.getCategory() : "\u534a\u6210\u54c1";
|
|
|
|
|
+ int inboundQty = toInt(purchaseBySku.getOrDefault(code, 0.0));
|
|
|
|
|
+ int consumedQty = toInt(semiConsumedQty.getOrDefault(code, 0.0));
|
|
|
|
|
+ int quantity = Math.max(0, inboundQty - consumedQty);
|
|
|
|
|
+ semiInventoryQty.put(code, (double) quantity);
|
|
|
|
|
+ int safeStock = Math.max(50, (int) Math.round(inboundQty * 0.2));
|
|
|
|
|
+ String status = computeStatus(quantity, safeStock);
|
|
|
|
|
+ double unitPrice = info != null && info.getPrice() != null ? info.getPrice() : 0.0;
|
|
|
|
|
+ double totalValue = Math.round(quantity * unitPrice * 100.0) / 100.0;
|
|
|
|
|
+
|
|
|
|
|
+ List<String> usedForProducts = semiToFinished.getOrDefault(code, Collections.emptySet())
|
|
|
|
|
+ .stream()
|
|
|
|
|
+ .map(finishedCode -> {
|
|
|
|
|
+ ProductInfo product = productInfoByCode.get(finishedCode);
|
|
|
|
|
+ if (product == null) {
|
|
|
|
|
+ return finishedCode;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!isBlank(product.getSpuName())) {
|
|
|
|
|
+ return product.getSpuName();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!isBlank(product.getProductName())) {
|
|
|
|
|
+ return product.getProductName();
|
|
|
|
|
+ }
|
|
|
|
|
+ return finishedCode;
|
|
|
|
|
+ })
|
|
|
|
|
+ .distinct()
|
|
|
|
|
+ .sorted()
|
|
|
|
|
+ .collect(Collectors.toList());
|
|
|
|
|
|
|
|
- private Object getValue(List<Object> row, int index) {
|
|
|
|
|
- if (row == null || index < 0 || index >= row.size()) {
|
|
|
|
|
- return null;
|
|
|
|
|
|
|
+ Map<String, Object> row = new LinkedHashMap<>();
|
|
|
|
|
+ row.put("id", index++);
|
|
|
|
|
+ row.put("code", code);
|
|
|
|
|
+ row.put("name", name);
|
|
|
|
|
+ row.put("category", category);
|
|
|
|
|
+ row.put("quantity", quantity);
|
|
|
|
|
+ row.put("safeStock", safeStock);
|
|
|
|
|
+ row.put("status", status);
|
|
|
|
|
+ row.put("usedForProducts", usedForProducts);
|
|
|
|
|
+ row.put("supplier", "");
|
|
|
|
|
+ row.put("leadTime", 0);
|
|
|
|
|
+ row.put("unitPrice", Math.round(unitPrice * 100.0) / 100.0);
|
|
|
|
|
+ row.put("totalValue", totalValue);
|
|
|
|
|
+ row.put("inboundQty", inboundQty);
|
|
|
|
|
+ row.put("consumedQty", consumedQty);
|
|
|
|
|
+ rows.add(row);
|
|
|
}
|
|
}
|
|
|
- return row.get(index);
|
|
|
|
|
|
|
+ rows.sort(Comparator.comparingInt((Map<String, Object> row) ->
|
|
|
|
|
+ ((Number) row.getOrDefault("quantity", 0)).intValue()).reversed());
|
|
|
|
|
+
|
|
|
|
|
+ return new SemiSnapshot(productInfoByCode, bomItems, semiInventoryQty, rows);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private String toText(Object value) {
|
|
|
|
|
- if (value == null) {
|
|
|
|
|
- return "";
|
|
|
|
|
|
|
+ private boolean isSemiCategory(String category) {
|
|
|
|
|
+ if (category == null || category.trim().isEmpty()) {
|
|
|
|
|
+ return false;
|
|
|
}
|
|
}
|
|
|
- return String.valueOf(value).trim();
|
|
|
|
|
|
|
+ String text = category.replace(" ", "").toLowerCase();
|
|
|
|
|
+ return text.contains("\u534a\u6210\u54c1") || text.contains("semi");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private Map<String, Object> buildTurnover(String name, int avgTurnover, int usageRate, int monthlyConsumption,
|
|
|
|
|
- int reorderPoint, String stockStatus, String suggestion) {
|
|
|
|
|
- Map<String, Object> row = new LinkedHashMap<>();
|
|
|
|
|
- row.put("name", name);
|
|
|
|
|
- row.put("avgTurnover", avgTurnover);
|
|
|
|
|
- row.put("usageRate", usageRate);
|
|
|
|
|
- row.put("monthlyConsumption", monthlyConsumption);
|
|
|
|
|
- row.put("reorderPoint", reorderPoint);
|
|
|
|
|
- row.put("stockStatus", stockStatus);
|
|
|
|
|
- row.put("suggestion", suggestion);
|
|
|
|
|
- return row;
|
|
|
|
|
|
|
+ private String computeStatus(int quantity, int safeStock) {
|
|
|
|
|
+ if (quantity <= 0) {
|
|
|
|
|
+ return "used";
|
|
|
|
|
+ }
|
|
|
|
|
+ if (quantity < safeStock) {
|
|
|
|
|
+ return "low-stock";
|
|
|
|
|
+ }
|
|
|
|
|
+ return "in-stock";
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private Map<String, Object> buildBomItem(String name, int quantity, int stock) {
|
|
|
|
|
- Map<String, Object> item = new LinkedHashMap<>();
|
|
|
|
|
- item.put("name", name);
|
|
|
|
|
- item.put("quantity", quantity);
|
|
|
|
|
- item.put("stock", stock);
|
|
|
|
|
- return item;
|
|
|
|
|
|
|
+ private String convertStockStatus(String status) {
|
|
|
|
|
+ if ("in-stock".equals(status)) {
|
|
|
|
|
+ return "\u6b63\u5e38";
|
|
|
|
|
+ }
|
|
|
|
|
+ if ("low-stock".equals(status)) {
|
|
|
|
|
+ return "\u504f\u4f4e";
|
|
|
|
|
+ }
|
|
|
|
|
+ return "\u7d27\u5f20";
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private List<Map<String, Object>> sampleSemiProducts() {
|
|
|
|
|
- List<Map<String, Object>> data = new ArrayList<>();
|
|
|
|
|
- data.add(buildSample("SEM-101", "电路板 PCB-A", "pcb", 3200, 2000, "in-stock",
|
|
|
|
|
- java.util.Arrays.asList("智能手表 Pro X1", "智能手环"), "深圳电子有限公司", 15, 45.8, 146560));
|
|
|
|
|
- data.add(buildSample("SEM-102", "显示屏模块", "electronics", 2800, 1500, "in-stock",
|
|
|
|
|
- java.util.Arrays.asList("智能手表 Pro X1"), "京东方科技", 20, 128.5, 359800));
|
|
|
|
|
- return data;
|
|
|
|
|
|
|
+ private String buildSuggestion(String status) {
|
|
|
|
|
+ if ("in-stock".equals(status)) {
|
|
|
|
|
+ return "\u5e93\u5b58\u5145\u8db3\uff0c\u5efa\u8bae\u4fdd\u6301\u5f53\u524d\u91c7\u8d2d\u8282\u594f";
|
|
|
|
|
+ }
|
|
|
|
|
+ if ("low-stock".equals(status)) {
|
|
|
|
|
+ return "\u5e93\u5b58\u504f\u4f4e\uff0c\u5efa\u8bae\u53ca\u65f6\u8865\u8d27";
|
|
|
|
|
+ }
|
|
|
|
|
+ return "\u5e93\u5b58\u7d27\u5f20\uff0c\u5efa\u8bae\u4f18\u5148\u6392\u4ea7\u5e76\u8865\u5145\u5e93\u5b58";
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private Map<String, Object> buildSample(String code, String name, String category, int quantity, int safeStock,
|
|
|
|
|
- String status, List<String> usedFor, String supplier, int leadTime,
|
|
|
|
|
- double unitPrice, double totalValue) {
|
|
|
|
|
- Map<String, Object> row = new LinkedHashMap<>();
|
|
|
|
|
- row.put("code", code);
|
|
|
|
|
- row.put("name", name);
|
|
|
|
|
- row.put("category", category);
|
|
|
|
|
- row.put("quantity", quantity);
|
|
|
|
|
- row.put("safeStock", safeStock);
|
|
|
|
|
- row.put("status", status);
|
|
|
|
|
- row.put("usedForProducts", usedFor);
|
|
|
|
|
- row.put("supplier", supplier);
|
|
|
|
|
- row.put("leadTime", leadTime);
|
|
|
|
|
- row.put("unitPrice", unitPrice);
|
|
|
|
|
- row.put("totalValue", totalValue);
|
|
|
|
|
- return row;
|
|
|
|
|
|
|
+ private int toInt(double value) {
|
|
|
|
|
+ return (int) Math.round(value);
|
|
|
}
|
|
}
|
|
|
-}
|
|
|
|
|
|
|
|
|
|
|
|
+ private boolean isBlank(String text) {
|
|
|
|
|
+ return text == null || text.trim().isEmpty();
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
|
|
+ private static class SemiSnapshot {
|
|
|
|
|
+ private final Map<String, ProductInfo> productInfoByCode;
|
|
|
|
|
+ private final Map<String, Map<String, Double>> bomItems;
|
|
|
|
|
+ private final Map<String, Double> semiInventoryQty;
|
|
|
|
|
+ private final List<Map<String, Object>> rows;
|
|
|
|
|
+
|
|
|
|
|
+ private SemiSnapshot(Map<String, ProductInfo> productInfoByCode,
|
|
|
|
|
+ Map<String, Map<String, Double>> bomItems,
|
|
|
|
|
+ Map<String, Double> semiInventoryQty,
|
|
|
|
|
+ List<Map<String, Object>> rows) {
|
|
|
|
|
+ this.productInfoByCode = productInfoByCode;
|
|
|
|
|
+ this.bomItems = bomItems;
|
|
|
|
|
+ this.semiInventoryQty = semiInventoryQty;
|
|
|
|
|
+ this.rows = rows;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|