ソースを参照

库存模块优化(后

Gogs 2 ヶ月 前
コミット
ba8ec29fe8

+ 18 - 7
dtm-storage/src/main/java/com/dtm/storage/controller/StorageUploadController.java

@@ -1,8 +1,10 @@
-package com.dtm.storage.controller;
+package com.dtm.storage.controller;
 
 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 uploadTaskService;
 
-    public StorageUploadController(StorageUploadService uploadService) {
-        this.uploadService = uploadService;
+    public StorageUploadController(InventoryUploadTaskService uploadTaskService) {
+        this.uploadTaskService = uploadTaskService;
     }
 
     @PostMapping("/upload")
@@ -28,18 +30,27 @@ 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 = uploadTaskService.submit(
                     purchaseFile,
                     salesFile,
                     assemblyFile,
                     productFile,
                     semiMappingFile
             );
-            return AjaxResult.success("上传成功", result);
+            return AjaxResult.success("上传任务已创建", result);
         } catch (IllegalArgumentException e) {
             return AjaxResult.error(e.getMessage());
         } catch (Exception e) {
             return AjaxResult.error("上传失败: " + e.getMessage());
         }
     }
+
+    @GetMapping("/upload-task/{taskId}")
+    public AjaxResult getUploadTask(@PathVariable("taskId") String taskId) {
+        Map<String, Object> result = uploadTaskService.getTask(taskId);
+        if (result == null) {
+            return AjaxResult.error("任务不存在");
+        }
+        return AjaxResult.success(result);
+    }
 }

+ 170 - 124
dtm-storage/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,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;
+        }
+    }
+}

+ 111 - 0
dtm-storage/src/main/java/com/dtm/storage/service/InventoryUploadTaskService.java

@@ -0,0 +1,111 @@
+package com.dtm.storage.service;
+
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.annotation.PreDestroy;
+import java.time.Instant;
+import java.util.LinkedHashMap;
+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 ExecutorService executor = Executors.newSingleThreadExecutor(r -> {
+        Thread thread = new Thread(r, "inventory-upload-task");
+        thread.setDaemon(true);
+        return thread;
+    });
+    private final ConcurrentMap<String, TaskRecord> tasks = new ConcurrentHashMap<>();
+
+    public InventoryUploadTaskService(StorageUploadService uploadService) {
+        this.uploadService = uploadService;
+    }
+
+    public Map<String, Object> submit(MultipartFile purchaseFile,
+                                      MultipartFile salesFile,
+                                      MultipartFile assemblyFile,
+                                      MultipartFile productFile,
+                                      MultipartFile semiMappingFile) {
+        StorageUploadService.UploadBatch batch = uploadService.saveUploadFiles(
+                purchaseFile,
+                salesFile,
+                assemblyFile,
+                productFile,
+                semiMappingFile
+        );
+
+        String taskId = UUID.randomUUID().toString().replace("-", "");
+        TaskRecord record = new TaskRecord(taskId, batch);
+        tasks.put(taskId, record);
+
+        executor.submit(() -> runTask(record));
+        return record.toMap();
+    }
+
+    public Map<String, Object> getTask(String taskId) {
+        TaskRecord record = tasks.get(taskId);
+        return record == null ? null : record.toMap();
+    }
+
+    private void runTask(TaskRecord record) {
+        record.status = "running";
+        record.message = "文件已上传,正在处理库存数据";
+        record.startedAt = Instant.now().toString();
+        try {
+            uploadService.activateUploadBatch(record.batch);
+            record.status = "success";
+            record.message = "库存数据处理完成";
+            record.result = record.batch.toResult();
+        } catch (Exception e) {
+            record.status = "failed";
+            record.message = e.getMessage() == null ? "库存数据处理失败" : e.getMessage();
+            uploadService.discardUploadBatch(record.batch);
+        } finally {
+            record.finishedAt = Instant.now().toString();
+        }
+    }
+
+    @PreDestroy
+    public void shutdown() {
+        executor.shutdownNow();
+    }
+
+    private static class TaskRecord {
+        private final String taskId;
+        private final StorageUploadService.UploadBatch batch;
+        private final String createdAt;
+        private volatile String startedAt;
+        private volatile String finishedAt;
+        private volatile String status;
+        private volatile String message;
+        private volatile Map<String, Object> result;
+
+        private TaskRecord(String taskId, StorageUploadService.UploadBatch batch) {
+            this.taskId = taskId;
+            this.batch = batch;
+            this.createdAt = Instant.now().toString();
+            this.status = "pending";
+            this.message = "文件已上传,等待后台处理";
+        }
+
+        private Map<String, Object> toMap() {
+            Map<String, Object> data = new LinkedHashMap<>();
+            data.put("taskId", taskId);
+            data.put("status", status);
+            data.put("message", message);
+            data.put("createdAt", createdAt);
+            data.put("startedAt", startedAt);
+            data.put("finishedAt", finishedAt);
+            data.put("result", result);
+            data.put("count", batch == null ? 0 : batch.getCount());
+            data.put("files", batch == null ? null : batch.getFiles());
+            return data;
+        }
+    }
+}

+ 82 - 22
dtm-storage/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,19 +16,25 @@ 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,
-                                           MultipartFile salesFile,
-                                           MultipartFile assemblyFile,
-                                           MultipartFile productFile,
-                                           MultipartFile semiMappingFile) {
+    public UploadBatch saveUploadFiles(MultipartFile purchaseFile,
+                                       MultipartFile salesFile,
+                                       MultipartFile assemblyFile,
+                                       MultipartFile productFile,
+                                       MultipartFile semiMappingFile) {
         List<Map<String, Object>> saved = new ArrayList<>();
-        Path basePath = prepareTempDir();
+        Path basePath = createTempDir();
 
         int savedCount = 0;
         if (purchaseFile != null && !purchaseFile.isEmpty()) {
@@ -56,32 +62,56 @@ public class StorageUploadService {
             throw new IllegalArgumentException("请至少选择一个要上传的文件");
         }
 
-        dataLoader.useTemporaryBasePath(basePath);
+        return new UploadBatch(basePath, saved, savedCount);
+    }
+
+    public synchronized void activateUploadBatch(UploadBatch batch) {
+        if (batch == null || batch.getBasePath() == null) {
+            throw new IllegalArgumentException("上传批次无效");
+        }
+        Path previous = activeTempDir;
+        dataLoader.useTemporaryBasePath(batch.getBasePath());
+        inventoryService.invalidateCache();
+        riskService.invalidateCache();
+        inventoryService.warmCache();
+        riskService.getRiskStatistics();
+        activeTempDir = batch.getBasePath();
+        if (previous != null && !previous.equals(activeTempDir)) {
+            cleanupDir(previous);
+        }
+    }
 
-        Map<String, Object> result = new LinkedHashMap<>();
-        result.put("basePath", basePath.toAbsolutePath().toString());
-        result.put("files", saved);
-        result.put("count", savedCount);
-        return result;
+    public void discardUploadBatch(UploadBatch batch) {
+        if (batch == null) {
+            return;
+        }
+        Path path = batch.getBasePath();
+        if (path == null) {
+            return;
+        }
+        synchronized (this) {
+            if (path.equals(activeTempDir)) {
+                return;
+            }
+        }
+        cleanupDir(path);
     }
 
-    private synchronized Path prepareTempDir() {
-        cleanupTempDir();
+    private Path createTempDir() {
         try {
             Path temp = Files.createTempDirectory("dtm-storage-");
-            this.activeTempDir = temp;
             return temp;
         } catch (Exception e) {
             throw new IllegalStateException("创建临时目录失败: " + e.getMessage(), e);
         }
     }
 
-    private void cleanupTempDir() {
-        if (activeTempDir == null || !Files.exists(activeTempDir)) {
+    private void cleanupDir(Path targetDir) {
+        if (targetDir == null || !Files.exists(targetDir)) {
             return;
         }
         try {
-            Files.walk(activeTempDir)
+            Files.walk(targetDir)
                     .sorted((a, b) -> b.compareTo(a))
                     .forEach(path -> {
                         try {
@@ -90,8 +120,6 @@ public class StorageUploadService {
                         }
                     });
         } catch (Exception ignored) {
-        } finally {
-            activeTempDir = null;
         }
     }
 
@@ -149,4 +177,36 @@ public class StorageUploadService {
         }
         return false;
     }
+
+    public static class UploadBatch {
+        private final Path basePath;
+        private final List<Map<String, Object>> files;
+        private final int count;
+
+        public UploadBatch(Path basePath, List<Map<String, Object>> files, int count) {
+            this.basePath = basePath;
+            this.files = files;
+            this.count = count;
+        }
+
+        public Path getBasePath() {
+            return basePath;
+        }
+
+        public List<Map<String, Object>> getFiles() {
+            return files;
+        }
+
+        public int getCount() {
+            return count;
+        }
+
+        public Map<String, Object> toResult() {
+            Map<String, Object> result = new LinkedHashMap<>();
+            result.put("basePath", basePath == null ? null : basePath.toAbsolutePath().toString());
+            result.put("files", files);
+            result.put("count", count);
+            return result;
+        }
+    }
 }