Gogs 2 months ago
parent
commit
b5376b9cea

+ 17 - 6
dtm-admin/src/main/java/com/dtm/web/controller/storage/StorageUploadController.java

@@ -1,8 +1,10 @@
-package com.dtm.web.controller.storage;
+package com.dtm.web.controller.storage;
 
 import com.dtm.common.annotation.Anonymous;
 import com.dtm.common.core.domain.AjaxResult;
-import com.dtm.storage.service.StorageUploadService;
+import com.dtm.storage.service.InventoryUploadTaskService;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestParam;
@@ -15,10 +17,10 @@ import java.util.Map;
 @RestController
 @RequestMapping("/api/inventory")
 public class StorageUploadController {
-    private final StorageUploadService uploadService;
+    private final InventoryUploadTaskService taskService;
 
-    public StorageUploadController(StorageUploadService uploadService) {
-        this.uploadService = uploadService;
+    public StorageUploadController(InventoryUploadTaskService taskService) {
+        this.taskService = taskService;
     }
 
     @PostMapping("/upload")
@@ -28,7 +30,7 @@ public class StorageUploadController {
                                            @RequestParam(value = "productFile", required = false) MultipartFile productFile,
                                            @RequestParam(value = "semiMappingFile", required = false) MultipartFile semiMappingFile) {
         try {
-            Map<String, Object> result = uploadService.uploadFiles(
+            Map<String, Object> result = taskService.submit(
                     purchaseFile,
                     salesFile,
                     assemblyFile,
@@ -42,4 +44,13 @@ public class StorageUploadController {
             return AjaxResult.error("上传失败: " + e.getMessage());
         }
     }
+
+    @GetMapping("/upload-task/{taskId}")
+    public AjaxResult getUploadTask(@PathVariable("taskId") String taskId) {
+        Map<String, Object> result = taskService.getTask(taskId);
+        if (result == null) {
+            return AjaxResult.error("任务不存在");
+        }
+        return AjaxResult.success(result);
+    }
 }

+ 179 - 151
dtm-system/src/main/java/com/dtm/storage/service/InventoryService.java

@@ -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,79 +25,48 @@ import java.util.stream.Collectors;
 @Service
 public class InventoryService {
     private final StorageDataLoader dataLoader;
+    private final Object snapshotLock = new Object();
+    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;
-
-        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;
+        return ensureSnapshot().overview;
     }
 
     public Map<String, Object> getHealthIndex() {
         Map<String, Object> result = new LinkedHashMap<>();
         result.put("value", 78);
-        result.put("status", "一般");
+        result.put("status", "涓€鑸?");
         result.put("trend", "up");
         return result;
     }
 
     public List<Map<String, Object>> getFsmStates() {
         List<Map<String, Object>> list = new ArrayList<>();
-        list.add(buildFsm("正常库存", 3250, "up", 5.2));
-        list.add(buildFsm("低库存", 820, "down", 2.1));
-        list.add(buildFsm("超储", 450, "up", 3.5));
-        list.add(buildFsm("滞销", 230, "down", 1.2));
-        list.add(buildFsm("在途", 580, "up", 4.8));
-        list.add(buildFsm("待检", 350, "down", 0.5));
+        list.add(buildFsm("姝e父搴撳瓨", 3250, "up", 5.2));
+        list.add(buildFsm("浣庡簱瀛?", 820, "down", 2.1));
+        list.add(buildFsm("瓒呭偍", 450, "up", 3.5));
+        list.add(buildFsm("婊為攢", 230, "down", 1.2));
+        list.add(buildFsm("鍦ㄩ€?", 580, "up", 4.8));
+        list.add(buildFsm("寰呮", 350, "down", 0.5));
         return list;
     }
 
     public Map<String, Object> getLifecycleDistribution() {
         Map<String, Object> result = new LinkedHashMap<>();
         Map<String, Integer> finished = new LinkedHashMap<>();
-        finished.put("引入期", 3200);
-        finished.put("成长期", 8500);
-        finished.put("成熟期", 12800);
-        finished.put("衰退期", 4060);
+        finished.put("寮曞叆鏈?", 3200);
+        finished.put("鎴愰暱鏈?", 8500);
+        finished.put("鎴愮啛鏈?", 12800);
+        finished.put("琛伴€€鏈?", 4060);
         Map<String, Integer> semi = new LinkedHashMap<>();
-        semi.put("引入期", 2100);
-        semi.put("成长期", 5800);
-        semi.put("成熟期", 7200);
-        semi.put("衰退期", 2020);
+        semi.put("寮曞叆鏈?", 2100);
+        semi.put("鎴愰暱鏈?", 5800);
+        semi.put("鎴愮啛鏈?", 7200);
+        semi.put("琛伴€€鏈?", 2020);
         result.put("finished", finished);
         result.put("semi_finished", semi);
         return result;
@@ -111,29 +80,136 @@ public class InventoryService {
     }
 
     public Map<String, Object> getMonthlyComparisonData() {
+        return ensureSnapshot().monthlyComparison;
+    }
+
+    public List<Map<String, Object>> getSkuSummaryTable() {
+        return ensureSnapshot().skuSummary;
+    }
+
+    public List<Map<String, Object>> getSpuSummaryTable() {
+        return ensureSnapshot().spuSummary;
+    }
+
+    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());
+        invalidateCache();
+        return result;
+    }
+
+    public void invalidateCache() {
+        snapshot = null;
+    }
+
+    public void warmCache() {
+        ensureSnapshot();
+    }
+
+    private InventorySnapshot ensureSnapshot() {
+        InventorySnapshot current = snapshot;
+        if (current != null) {
+            return current;
+        }
+        synchronized (snapshotLock) {
+            current = snapshot;
+            if (current == null) {
+                current = buildSnapshot();
+                snapshot = current;
+            }
+        }
+        return current;
+    }
+
+    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);
 
+        Map<String, double[]> purchaseSummary = new HashMap<>();
+        Map<String, Double> salesSummary = new HashMap<>();
         Map<YearMonth, Double> purchaseMonthly = new TreeMap<>();
+        Map<YearMonth, Double> salesMonthly = new TreeMap<>();
+
+        double totalPurchaseQtyRaw = 0.0;
+        double totalSalesQtyRaw = 0.0;
+        double totalPurchaseAmountRaw = 0.0;
+
         for (PurchaseRecord record : purchaseRecords) {
-            LocalDate date = record.getDate();
-            if (date == null) {
+            String code = record.getProductCode();
+            if (!shouldIncludeSku(code, finishedSkus)) {
                 continue;
             }
-            YearMonth month = YearMonth.from(date);
-            purchaseMonthly.merge(month, record.getQuantity(), Double::sum);
+            totalPurchaseQtyRaw += record.getQuantity();
+            totalPurchaseAmountRaw += record.getAmount();
+            double[] agg = purchaseSummary.computeIfAbsent(code, key -> new double[2]);
+            agg[0] += record.getQuantity();
+            agg[1] += record.getAmount();
+
+            LocalDate date = record.getDate();
+            if (date != null) {
+                purchaseMonthly.merge(YearMonth.from(date), record.getQuantity(), Double::sum);
+            }
         }
 
-        Map<YearMonth, Double> salesMonthly = new TreeMap<>();
         for (SalesRecord record : salesRecords) {
-            LocalDate date = record.getDate();
-            if (date == null) {
+            String code = record.getProductCode();
+            if (!shouldIncludeSku(code, finishedSkus)) {
                 continue;
             }
-            YearMonth month = YearMonth.from(date);
-            salesMonthly.merge(month, record.getQuantity(), Double::sum);
+            totalSalesQtyRaw += record.getQuantity();
+            salesSummary.merge(code, record.getQuantity(), Double::sum);
+
+            LocalDate date = record.getDate();
+            if (date != null) {
+                salesMonthly.merge(YearMonth.from(date), record.getQuantity(), Double::sum);
+            }
         }
 
+        int totalPurchaseQty = (int) Math.round(totalPurchaseQtyRaw);
+        int totalSalesQty = (int) Math.round(totalSalesQtyRaw);
+        int assemblyQty = calculateAssemblyQuantity(finishedSkus);
+        int totalInventory = totalPurchaseQty + assemblyQty - totalSalesQty;
+        double totalValue = Math.round((totalPurchaseAmountRaw / 10000.0) * 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> overview = new LinkedHashMap<>();
+        overview.put("totalInventory", totalInventory);
+        overview.put("totalValue", totalValue);
+        overview.put("turnoverRate", turnoverRate);
+        overview.put("inTransitRatio", 12.5);
+        overview.put("purchaseQty", totalPurchaseQty);
+        overview.put("salesQty", totalSalesQty);
+        overview.put("assemblyQty", assemblyQty);
+
+        Map<String, Object> monthlyComparison = buildMonthlyComparison(purchaseMonthly, salesMonthly);
+        List<Map<String, Object>> skuSummary = buildSkuSummary(purchaseSummary, salesSummary, productInfoMap);
+        List<Map<String, Object>> spuSummary = buildSpuSummary(purchaseSummary, salesSummary, productInfoMap);
+
+        dataLoader.clearCache();
+        return new InventorySnapshot(overview, monthlyComparison, skuSummary, spuSummary);
+    }
+
+    private Map<String, Object> buildMonthlyComparison(Map<YearMonth, Double> purchaseMonthly,
+                                                       Map<YearMonth, Double> salesMonthly) {
         Set<YearMonth> allMonths = new HashSet<>();
         allMonths.addAll(purchaseMonthly.keySet());
         allMonths.addAll(salesMonthly.keySet());
@@ -165,37 +241,10 @@ 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>> buildSkuSummary(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<>();
         for (Map.Entry<String, double[]> entry : purchaseSummary.entrySet()) {
             String sku = entry.getKey();
@@ -224,48 +273,22 @@ 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>> buildSpuSummary(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();
             ProductInfo info = productInfoMap.get(sku);
             String spu = info != null && info.getSpuName() != null && !info.getSpuName().trim().isEmpty()
                     ? info.getSpuName()
-                    : "未命名SPU";
-            SpuAggregate agg = aggregates.computeIfAbsent(spu, k -> new SpuAggregate());
+                    : "鏈懡鍚峉PU";
+            SpuAggregate agg = aggregates.computeIfAbsent(spu, key -> new SpuAggregate());
             agg.purchaseQty += entry.getValue()[0];
             agg.purchaseAmount += entry.getValue()[1];
             agg.salesQty += salesSummary.getOrDefault(sku, 0.0);
@@ -303,24 +326,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,12 +342,10 @@ 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()) {
@@ -363,10 +366,10 @@ public class InventoryService {
             return Collections.emptyMap();
         }
 
-        int prodIdx = 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);
+        int prodIdx = findHeaderIndex(headers, new String[]{"鎴愬搧", "鎴愬搧缂栫爜", "浜у搧缂栫爜", "浜у搧浠g爜"}, 0);
+        int innerIdx = findHeaderIndex(headers, new String[]{"鍐呰姱", "鍐呰儐"}, 1);
+        int outerIdx = findHeaderIndex(headers, new String[]{"澶栧3", "澶栧", "澶栧3缂栫爜"}, 2);
+        int accessoryIdx = findHeaderIndex(headers, new String[]{"閰嶄欢", "闄勪欢"}, 3);
 
         Map<String, Set<String>> map = new HashMap<>();
         for (List<Object> row : rows) {
@@ -379,7 +382,7 @@ public class InventoryService {
             collectCodes(semiCodes, toText(getValue(row, outerIdx)));
             collectCodes(semiCodes, toText(getValue(row, accessoryIdx)));
             for (String code : semiCodes) {
-                map.computeIfAbsent(code, k -> new HashSet<>()).add(productCode);
+                map.computeIfAbsent(code, key -> new HashSet<>()).add(productCode);
             }
         }
         return map;
@@ -389,7 +392,7 @@ public class InventoryService {
         if (raw == null || raw.trim().isEmpty()) {
             return;
         }
-        String normalized = raw.replace(',', ',');
+        String normalized = raw.replace(',', ',').replace("锛?", ",");
         for (String part : normalized.split(",")) {
             String code = part.trim();
             if (!code.isEmpty()) {
@@ -399,7 +402,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();
         }
@@ -415,7 +421,7 @@ public class InventoryService {
                 continue;
             }
             String text = category.toLowerCase(Locale.ROOT);
-            if (text.contains("半成") || text.contains("辅料") || text.contains("配件")) {
+            if (text.contains("鍗婃垚") || text.contains("杈呮枡") || text.contains("閰嶄欢")) {
                 continue;
             }
             finished.add(code);
@@ -423,6 +429,13 @@ public class InventoryService {
         return finished;
     }
 
+    private boolean shouldIncludeSku(String code, Set<String> finishedSkus) {
+        if (code == null || code.trim().isEmpty()) {
+            return false;
+        }
+        return finishedSkus.isEmpty() || finishedSkus.contains(code);
+    }
+
     private Map<String, Object> buildFsm(String name, int count, String trend, double percentage) {
         Map<String, Object> row = new LinkedHashMap<>();
         row.put("name", name);
@@ -481,6 +494,21 @@ public class InventoryService {
                     .orElse("");
         }
     }
-}
-
 
+    private static class InventorySnapshot {
+        private final Map<String, Object> overview;
+        private final Map<String, Object> monthlyComparison;
+        private final List<Map<String, Object>> skuSummary;
+        private final List<Map<String, Object>> spuSummary;
+
+        private InventorySnapshot(Map<String, Object> overview,
+                                  Map<String, Object> monthlyComparison,
+                                  List<Map<String, Object>> skuSummary,
+                                  List<Map<String, Object>> spuSummary) {
+            this.overview = overview;
+            this.monthlyComparison = monthlyComparison;
+            this.skuSummary = skuSummary;
+            this.spuSummary = spuSummary;
+        }
+    }
+}

+ 166 - 14
dtm-system/src/main/java/com/dtm/storage/service/InventoryUploadTaskService.java

@@ -1,19 +1,35 @@
-package com.dtm.storage.service;
+package com.dtm.storage.service;
 
 import org.springframework.stereotype.Service;
 import org.springframework.web.multipart.MultipartFile;
 
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
 import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.UUID;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 
 @Service
 public class InventoryUploadTaskService {
     private final StorageUploadService uploadService;
     private final ConcurrentMap<String, TaskRecord> tasks = new ConcurrentHashMap<>();
+    private final ExecutorService executor = Executors.newSingleThreadExecutor(r -> {
+        Thread thread = new Thread(r, "inventory-upload-task");
+        thread.setDaemon(true);
+        return thread;
+    });
 
     public InventoryUploadTaskService(StorageUploadService uploadService) {
         this.uploadService = uploadService;
@@ -28,7 +44,8 @@ public class InventoryUploadTaskService {
         TaskRecord record = new TaskRecord(taskId);
         tasks.put(taskId, record);
 
-        runTask(record, purchaseFile, salesFile, assemblyFile, productFile, semiMappingFile);
+        StagedUploadBatch batch = stageFiles(purchaseFile, salesFile, assemblyFile, productFile, semiMappingFile);
+        executor.execute(() -> runTask(record, batch));
         return record.toMap();
     }
 
@@ -37,22 +54,17 @@ public class InventoryUploadTaskService {
         return record == null ? null : record.toMap();
     }
 
-    private void runTask(TaskRecord record,
-                         MultipartFile purchaseFile,
-                         MultipartFile salesFile,
-                         MultipartFile assemblyFile,
-                         MultipartFile productFile,
-                         MultipartFile semiMappingFile) {
+    private void runTask(TaskRecord record, StagedUploadBatch batch) {
         record.status = "running";
-        record.message = "文件已上传,正在处理库存数据";
+        record.message = "文件已上传,后台正在处理库存数据";
         record.startedAt = Instant.now().toString();
         try {
             Map<String, Object> result = uploadService.uploadFiles(
-                    purchaseFile,
-                    salesFile,
-                    assemblyFile,
-                    productFile,
-                    semiMappingFile
+                    batch.get("purchaseFile"),
+                    batch.get("salesFile"),
+                    batch.get("assemblyFile"),
+                    batch.get("productFile"),
+                    batch.get("semiMappingFile")
             );
             record.status = "success";
             record.message = "库存数据处理完成";
@@ -63,10 +75,55 @@ public class InventoryUploadTaskService {
             record.status = "failed";
             record.message = e.getMessage() == null ? "库存数据处理失败" : e.getMessage();
         } finally {
+            if (batch != null) {
+                batch.cleanup();
+            }
             record.finishedAt = Instant.now().toString();
         }
     }
 
+    private StagedUploadBatch stageFiles(MultipartFile purchaseFile,
+                                         MultipartFile salesFile,
+                                         MultipartFile assemblyFile,
+                                         MultipartFile productFile,
+                                         MultipartFile semiMappingFile) {
+        try {
+            Path dir = Files.createTempDirectory("dtm-upload-stage-");
+            StagedUploadBatch batch = new StagedUploadBatch(dir);
+            batch.put("purchaseFile", stageSingleFile(dir, "purchase", purchaseFile));
+            batch.put("salesFile", stageSingleFile(dir, "sales", salesFile));
+            batch.put("assemblyFile", stageSingleFile(dir, "assembly", assemblyFile));
+            batch.put("productFile", stageSingleFile(dir, "product", productFile));
+            batch.put("semiMappingFile", stageSingleFile(dir, "semi-mapping", semiMappingFile));
+            return batch;
+        } catch (IOException e) {
+            throw new IllegalStateException("创建上传暂存目录失败: " + e.getMessage(), e);
+        }
+    }
+
+    private MultipartFile stageSingleFile(Path dir, String prefix, MultipartFile file) {
+        if (file == null || file.isEmpty()) {
+            return null;
+        }
+        try {
+            String originalName = file.getOriginalFilename();
+            String extension = "";
+            if (originalName != null) {
+                int idx = originalName.lastIndexOf('.');
+                if (idx >= 0) {
+                    extension = originalName.substring(idx);
+                }
+            }
+            Path staged = Files.createTempFile(dir, prefix + "-", extension);
+            try (InputStream input = file.getInputStream()) {
+                Files.copy(input, staged, StandardCopyOption.REPLACE_EXISTING);
+            }
+            return new PathMultipartFile(staged, originalName, file.getContentType(), file.getName());
+        } catch (IOException e) {
+            throw new IllegalStateException("暂存上传文件失败: " + e.getMessage(), e);
+        }
+    }
+
     private Integer parseCount(Map<String, Object> result) {
         if (result == null) {
             return 0;
@@ -116,4 +173,99 @@ public class InventoryUploadTaskService {
             return data;
         }
     }
+
+    private static class StagedUploadBatch {
+        private final Path dir;
+        private final Map<String, MultipartFile> files = new LinkedHashMap<>();
+
+        private StagedUploadBatch(Path dir) {
+            this.dir = dir;
+        }
+
+        private void put(String key, MultipartFile file) {
+            files.put(key, file);
+        }
+
+        private MultipartFile get(String key) {
+            return files.get(key);
+        }
+
+        private void cleanup() {
+            try {
+                if (dir != null && Files.exists(dir)) {
+                    Files.walk(dir)
+                            .sorted((a, b) -> b.compareTo(a))
+                            .forEach(path -> {
+                                try {
+                                    Files.deleteIfExists(path);
+                                } catch (IOException ignored) {
+                                }
+                            });
+                }
+            } catch (IOException ignored) {
+            }
+        }
+    }
+
+    private static class PathMultipartFile implements MultipartFile {
+        private final Path path;
+        private final String originalFilename;
+        private final String contentType;
+        private final String name;
+
+        private PathMultipartFile(Path path, String originalFilename, String contentType, String name) {
+            this.path = path;
+            this.originalFilename = originalFilename == null ? path.getFileName().toString() : originalFilename;
+            this.contentType = contentType;
+            this.name = name == null ? "file" : name;
+        }
+
+        @Override
+        public String getName() {
+            return name;
+        }
+
+        @Override
+        public String getOriginalFilename() {
+            return originalFilename;
+        }
+
+        @Override
+        public String getContentType() {
+            return contentType;
+        }
+
+        @Override
+        public boolean isEmpty() {
+            try {
+                return Files.size(path) == 0;
+            } catch (IOException e) {
+                return true;
+            }
+        }
+
+        @Override
+        public long getSize() {
+            try {
+                return Files.size(path);
+            } catch (IOException e) {
+                return 0L;
+            }
+        }
+
+        @Override
+        public byte[] getBytes() throws IOException {
+            return Files.readAllBytes(path);
+        }
+
+        @Override
+        public InputStream getInputStream() throws IOException {
+            return Files.newInputStream(path);
+        }
+
+        @Override
+        public void transferTo(File dest) throws IOException, IllegalStateException {
+            Files.copy(path, dest.toPath(), StandardCopyOption.REPLACE_EXISTING);
+        }
+    }
 }

+ 11 - 11
dtm-system/src/main/java/com/dtm/storage/service/StorageDataLoader.java

@@ -39,7 +39,7 @@ public class StorageDataLoader {
     private static final long CACHE_EXPIRE_MILLIS = 300_000;
     private static final long TEMP_CACHE_EXPIRE_MILLIS = 120_000;
     private static final long TEMP_DATA_TTL_MILLIS = 30 * 60 * 1000;
-    private static final int TEMP_MAX_ROWS = 200_000;
+    private static final int TEMP_MAX_ROWS = 100_000;
 
     @Value("${storage.data.path:}")
     private String storageDataPath;
@@ -347,28 +347,28 @@ public class StorageDataLoader {
     }
 
     private Path findPurchaseFile() {
-        Path dir = resolveDir("入库数据");
-        return findFile(dir, "入库", "采购");
+        Path dir = resolveDir("purchase");
+        return findFile(dir, "purchase", "入库", "采购");
     }
 
     private Path findSalesFile() {
-        Path dir = resolveDir("销售数据");
-        return findFile(dir, "订单", "销售");
+        Path dir = resolveDir("sales");
+        return findFile(dir, "sales", "订单", "销售");
     }
 
     private Path findAssemblyFile() {
-        Path dir = resolveDir("半成品组装");
-        return findFile(dir, "组装");
+        Path dir = resolveDir("assembly");
+        return findFile(dir, "assembly", "组装");
     }
 
     private Path findSemiMappingFile() {
-        Path dir = resolveDir("半成品组装");
-        return findFile(dir, "匹配");
+        Path dir = resolveDir("semi-mapping");
+        return findFile(dir, "semi-mapping", "匹配");
     }
 
     private Path findProductInfoFile() {
-        Path dir = resolveDir("入库数据");
-        return findFile(dir, "产品", "资料");
+        Path dir = resolveDir("product");
+        return findFile(dir, "product", "产品", "资料");
     }
 
     private Path resolveDir(String dirName) {

+ 14 - 7
dtm-system/src/main/java/com/dtm/storage/service/StorageUploadService.java

@@ -1,4 +1,4 @@
-package com.dtm.storage.service;
+package com.dtm.storage.service;
 
 import org.springframework.stereotype.Service;
 import org.springframework.web.multipart.MultipartFile;
@@ -16,10 +16,14 @@ public class StorageUploadService {
     private static final String[] ALLOWED_EXT = new String[]{".xlsx", ".xls"};
 
     private final StorageDataLoader dataLoader;
+    private final InventoryService inventoryService;
+    private final RiskService riskService;
     private Path activeTempDir;
 
-    public StorageUploadService(StorageDataLoader dataLoader) {
+    public StorageUploadService(StorageDataLoader dataLoader, InventoryService inventoryService, RiskService riskService) {
         this.dataLoader = dataLoader;
+        this.inventoryService = inventoryService;
+        this.riskService = riskService;
     }
 
     public Map<String, Object> uploadFiles(MultipartFile purchaseFile,
@@ -32,23 +36,23 @@ public class StorageUploadService {
 
         int savedCount = 0;
         if (purchaseFile != null && !purchaseFile.isEmpty()) {
-            saved.add(saveFile(basePath, "入库数据", purchaseFile, "入库数据", "00-入库数据-上传"));
+            saved.add(saveFile(basePath, "入库数据", purchaseFile, "purchase", "00-purchase-upload"));
             savedCount++;
         }
         if (salesFile != null && !salesFile.isEmpty()) {
-            saved.add(saveFile(basePath, "销售数据", salesFile, "销售数据", "00-销售数据-上传"));
+            saved.add(saveFile(basePath, "销售数据", salesFile, "sales", "00-sales-upload"));
             savedCount++;
         }
         if (assemblyFile != null && !assemblyFile.isEmpty()) {
-            saved.add(saveFile(basePath, "组装数据", assemblyFile, "半成品组装", "00-半成品组装-上传"));
+            saved.add(saveFile(basePath, "组装数据", assemblyFile, "assembly", "00-assembly-upload"));
             savedCount++;
         }
         if (productFile != null && !productFile.isEmpty()) {
-            saved.add(saveFile(basePath, "产品资料", productFile, "入库数据", "00-产品资料-上传"));
+            saved.add(saveFile(basePath, "产品资料", productFile, "product", "00-product-upload"));
             savedCount++;
         }
         if (semiMappingFile != null && !semiMappingFile.isEmpty()) {
-            saved.add(saveFile(basePath, "半成品映射", semiMappingFile, "半成品组装", "00-半成品组装-匹配-上传"));
+            saved.add(saveFile(basePath, "半成品映射", semiMappingFile, "semi-mapping", "00-semi-mapping-upload"));
             savedCount++;
         }
 
@@ -57,6 +61,9 @@ public class StorageUploadService {
         }
 
         dataLoader.useTemporaryBasePath(basePath);
+        inventoryService.invalidateCache();
+        riskService.invalidateCache();
+        inventoryService.warmCache();
 
         Map<String, Object> result = new LinkedHashMap<>();
         result.put("basePath", basePath.toAbsolutePath().toString());