Browse Source

订单监测_筛选功能

Gogs 1 month ago
parent
commit
6ab577eeca

+ 3 - 2
dtm-admin/src/main/java/com/dtm/web/controller/order/AnalysisController.java

@@ -89,8 +89,9 @@ public class AnalysisController {
     }
 
     @RequestMapping(value = "/co-purchase", method = RequestMethod.GET)
-    public List<CoPurchaseDTO> getCoPurchaseRules() {
-        return analysisService.findCoPurchaseRules();
+    public List<CoPurchaseDTO> getCoPurchaseRules(
+            @RequestParam(required = false) String skuKeyword) {
+        return analysisService.findCoPurchaseRules(skuKeyword);
     }
 
     @GetMapping("/average-payment-time")

+ 82 - 109
dtm-admin/src/main/java/com/dtm/web/controller/order/shop/ShopDataImportController.java

@@ -13,10 +13,10 @@ import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 import org.springframework.web.multipart.MultipartFile;
 
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Collections;
 
 @Anonymous
 @RestController
@@ -38,6 +38,11 @@ public class ShopDataImportController {
         return "Shop data store ready. Path: " + shopDataFolderPath;
     }
 
+    @GetMapping("/max-date")
+    public String getMaxSalesDate() {
+        return shopAnalysisService.getMaxSalesDate();
+    }
+
     @GetMapping("/import-sales-data")
     public ResponseEntity<Map<String, Object>> importSalesData() {
         Map<String, Object> response = new HashMap<>();
@@ -87,176 +92,137 @@ public class ShopDataImportController {
     }
 
     @GetMapping("/channel-contribution")
-    public ResponseEntity<Map<String, Object>> getChannelContribution() {
-        Map<String, Object> response = new HashMap<>();
+    public ResponseEntity<Map<String, Object>> getChannelContribution(
+            @RequestParam(required = false) String startDate,
+            @RequestParam(required = false) String endDate) {
         try {
-            Map<String, Double> contributionData = shopAnalysisService.getChannelSalesContribution();
-
+            Map<String, Double> contributionData = shopAnalysisService.getChannelSalesContribution(startDate, endDate);
             if (contributionData == null || contributionData.isEmpty()) {
-                return ResponseEntity.ok(buildEmptyResponse("请先上传店铺价值文件", new HashMap<>()));
+                return ResponseEntity.ok(buildEmptyResponse("请先上传店铺价值 CSV 文件", new HashMap<>()));
             }
-
-            response.put("success", true);
-            response.put("message", "Channel contribution ready.");
-            response.put("data", contributionData);
-            return ResponseEntity.ok(response);
+            return ResponseEntity.ok(buildSuccessResponse("Channel contribution ready.", contributionData));
         } catch (Exception e) {
-            response.put("success", false);
-            response.put("message", "Analysis failed: " + e.getMessage());
-            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
+            return buildFailureResponse(e);
         }
     }
 
     @GetMapping("/channel-roi-value")
-    public ResponseEntity<Map<String, Object>> getChannelRoiValue() {
-        Map<String, Object> response = new HashMap<>();
+    public ResponseEntity<Map<String, Object>> getChannelRoiValue(
+            @RequestParam(required = false) String startDate,
+            @RequestParam(required = false) String endDate) {
         try {
-            Map<String, Double> roiData = shopAnalysisService.getChannelRoiValue();
-
+            Map<String, Double> roiData = shopAnalysisService.getChannelRoiValue(startDate, endDate);
             if (roiData == null || roiData.isEmpty()) {
-                return ResponseEntity.ok(buildEmptyResponse("请先上传店铺价值文件", new HashMap<>()));
+                return ResponseEntity.ok(buildEmptyResponse("请先上传店铺价值 CSV 文件", new HashMap<>()));
             }
-
-            response.put("success", true);
-            response.put("message", "Channel ROI ready.");
-            response.put("data", roiData);
-            return ResponseEntity.ok(response);
+            return ResponseEntity.ok(buildSuccessResponse("Channel ROI ready.", roiData));
         } catch (Exception e) {
-            response.put("success", false);
-            response.put("message", "Analysis failed: " + e.getMessage());
-            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
+            return buildFailureResponse(e);
         }
     }
 
     @GetMapping("/unit-contribution")
-    public ResponseEntity<Map<String, Object>> getUnitContribution() {
-        Map<String, Object> response = new HashMap<>();
-        List<Map<String, Object>> data = shopAnalysisService.getUnitContribution();
-
+    public ResponseEntity<Map<String, Object>> getUnitContribution(
+            @RequestParam(required = false) String startDate,
+            @RequestParam(required = false) String endDate) {
+        List<Map<String, Object>> data = shopAnalysisService.getUnitContribution(startDate, endDate);
         if (data == null || data.isEmpty()) {
-            return ResponseEntity.ok(buildEmptyResponse("请先上传店铺价值文件", Collections.emptyList()));
+            return ResponseEntity.ok(buildEmptyResponse("请先上传店铺价值 CSV 文件", Collections.emptyList()));
         }
-
-        response.put("success", true);
-        response.put("message", "Unit contribution ready.");
-        response.put("data", data);
-        return ResponseEntity.ok(response);
+        return ResponseEntity.ok(buildSuccessResponse("Unit contribution ready.", data));
     }
 
     @GetMapping("/channel-total-contribution")
-    public ResponseEntity<Map<String, Object>> getChannelTotalContribution() {
-        Map<String, Object> response = new HashMap<>();
-        List<Map<String, Object>> data = shopAnalysisService.getChannelTotalContribution();
-
+    public ResponseEntity<Map<String, Object>> getChannelTotalContribution(
+            @RequestParam(required = false) String startDate,
+            @RequestParam(required = false) String endDate) {
+        List<Map<String, Object>> data = shopAnalysisService.getChannelTotalContribution(startDate, endDate);
         if (data == null || data.isEmpty()) {
-            return ResponseEntity.ok(buildEmptyResponse("请先上传店铺价值文件", Collections.emptyList()));
+            return ResponseEntity.ok(buildEmptyResponse("请先上传店铺价值 CSV 文件", Collections.emptyList()));
         }
-
-        response.put("success", true);
-        response.put("message", "Channel total contribution ready.");
-        response.put("data", data);
-        return ResponseEntity.ok(response);
+        return ResponseEntity.ok(buildSuccessResponse("Channel total contribution ready.", data));
     }
 
     @GetMapping("/platform-total-contribution")
-    public ResponseEntity<Map<String, Object>> getPlatformTotalContribution() {
-        Map<String, Object> response = new HashMap<>();
-        List<Map<String, Object>> data = shopAnalysisService.getPlatformTotalContribution();
-
+    public ResponseEntity<Map<String, Object>> getPlatformTotalContribution(
+            @RequestParam(required = false) String startDate,
+            @RequestParam(required = false) String endDate) {
+        List<Map<String, Object>> data = shopAnalysisService.getPlatformTotalContribution(startDate, endDate);
         if (data == null || data.isEmpty()) {
-            return ResponseEntity.ok(buildEmptyResponse("请先上传店铺价值文件", Collections.emptyList()));
+            return ResponseEntity.ok(buildEmptyResponse("请先上传店铺价值 CSV 文件", Collections.emptyList()));
         }
-
-        response.put("success", true);
-        response.put("message", "Platform total contribution ready.");
-        response.put("data", data);
-        return ResponseEntity.ok(response);
+        return ResponseEntity.ok(buildSuccessResponse("Platform total contribution ready.", data));
     }
 
     @GetMapping("/top-product-contribution")
-    public ResponseEntity<Map<String, Object>> getTopProductContribution() {
-        Map<String, Object> response = new HashMap<>();
+    public ResponseEntity<Map<String, Object>> getTopProductContribution(
+            @RequestParam(required = false) String startDate,
+            @RequestParam(required = false) String endDate) {
         try {
-            Map<String, Object> result = shopAnalysisService.getTopProductContribution();
-
+            Map<String, Object> result = shopAnalysisService.getTopProductContribution(startDate, endDate);
             if (result == null || result.isEmpty()) {
-                return ResponseEntity.ok(buildEmptyResponse("请先上传店铺价值文件", new HashMap<>()));
+                return ResponseEntity.ok(buildEmptyResponse("请先上传店铺价值 CSV 文件", new HashMap<>()));
             }
-
-            response.put("success", true);
-            response.put("message", "Top product contribution ready.");
-            response.put("data", result);
-            return ResponseEntity.ok(response);
+            return ResponseEntity.ok(buildSuccessResponse("Top product contribution ready.", result));
         } catch (Exception e) {
-            response.put("success", false);
-            response.put("message", "Analysis failed: " + e.getMessage());
-            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
+            return buildFailureResponse(e);
         }
     }
 
     @GetMapping("/cross-selling-products")
-    public ResponseEntity<Map<String, Object>> getCrossSellingProducts() {
-        Map<String, Object> response = new HashMap<>();
+    public ResponseEntity<Map<String, Object>> getCrossSellingProducts(
+            @RequestParam(required = false) String startDate,
+            @RequestParam(required = false) String endDate,
+            @RequestParam(required = false) String skuKeyword) {
         try {
-            List<Map<String, Object>> data = shopAnalysisService.getCrossSellingProducts();
-
+            List<Map<String, Object>> data = shopAnalysisService.getCrossSellingProducts(startDate, endDate, skuKeyword);
             if (data == null || data.isEmpty()) {
-                return ResponseEntity.ok(buildEmptyResponse("请先上传店铺价值文件", Collections.emptyList()));
+                return ResponseEntity.ok(buildEmptyResponse("请先上传店铺价值 CSV 件或调整 SKU 条件", Collections.emptyList()));
             }
-
-            response.put("success", true);
-            response.put("message", "Cross-selling products ready.");
-            response.put("data", data);
-            return ResponseEntity.ok(response);
+            return ResponseEntity.ok(buildSuccessResponse("Cross-selling products ready.", data));
         } catch (Exception e) {
-            response.put("success", false);
-            response.put("message", "Analysis failed: " + e.getMessage());
-            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
+            return buildFailureResponse(e);
         }
     }
 
     @GetMapping("/department-efficiency")
-    public ResponseEntity<Map<String, Object>> getDepartmentOperationalEfficiency() {
-        Map<String, Object> response = new HashMap<>();
+    public ResponseEntity<Map<String, Object>> getDepartmentOperationalEfficiency(
+            @RequestParam(required = false) String startDate,
+            @RequestParam(required = false) String endDate) {
         try {
-            Map<String, Double> data = shopAnalysisService.getDepartmentOperationalEfficiency();
-
+            Map<String, Double> data = shopAnalysisService.getDepartmentOperationalEfficiency(startDate, endDate);
             if (data == null || data.isEmpty()) {
-                return ResponseEntity.ok(buildEmptyResponse("请先上传店铺价值文件", new HashMap<>()));
+                return ResponseEntity.ok(buildEmptyResponse("请先上传店铺价值 CSV 文件", new HashMap<>()));
             }
-
-            response.put("success", true);
-            response.put("message", "Department efficiency ready.");
-            response.put("data", data);
-            return ResponseEntity.ok(response);
+            return ResponseEntity.ok(buildSuccessResponse("Department efficiency ready.", data));
         } catch (Exception e) {
-            response.put("success", false);
-            response.put("message", "Analysis failed: " + e.getMessage());
-            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
+            return buildFailureResponse(e);
         }
     }
 
     @GetMapping("/channel-diversity")
-    public ResponseEntity<Map<String, Object>> getChannelProductDiversity() {
-        Map<String, Object> response = new HashMap<>();
+    public ResponseEntity<Map<String, Object>> getChannelProductDiversity(
+            @RequestParam(required = false) String startDate,
+            @RequestParam(required = false) String endDate) {
         try {
-            Map<String, Long> data = shopAnalysisService.getChannelProductDiversity();
-
+            Map<String, Long> data = shopAnalysisService.getChannelProductDiversity(startDate, endDate);
             if (data == null || data.isEmpty()) {
-                return ResponseEntity.ok(buildEmptyResponse("请先上传店铺价值文件", new HashMap<>()));
+                return ResponseEntity.ok(buildEmptyResponse("请先上传店铺价值 CSV 文件", new HashMap<>()));
             }
-
-            response.put("success", true);
-            response.put("message", "Channel diversity ready.");
-            response.put("data", data);
-            return ResponseEntity.ok(response);
+            return ResponseEntity.ok(buildSuccessResponse("Channel diversity ready.", data));
         } catch (Exception e) {
-            response.put("success", false);
-            response.put("message", "Analysis failed: " + e.getMessage());
-            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
+            return buildFailureResponse(e);
         }
     }
 
+    private Map<String, Object> buildSuccessResponse(String message, Object data) {
+        Map<String, Object> response = new HashMap<>();
+        response.put("success", true);
+        response.put("message", message);
+        response.put("data", data);
+        return response;
+    }
+
     private Map<String, Object> buildEmptyResponse(String message, Object data) {
         Map<String, Object> response = new HashMap<>();
         response.put("success", true);
@@ -265,4 +231,11 @@ public class ShopDataImportController {
         response.put("data", data);
         return response;
     }
+
+    private ResponseEntity<Map<String, Object>> buildFailureResponse(Exception e) {
+        Map<String, Object> response = new HashMap<>();
+        response.put("success", false);
+        response.put("message", "Analysis failed: " + e.getMessage());
+        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
+    }
 }

+ 7 - 1
dtm-system/src/main/java/com/dtm/order/service/AnalysisService.java

@@ -197,7 +197,8 @@ public class AnalysisService {
         return result;
     }
 
-    public List<CoPurchaseDTO> findCoPurchaseRules() {
+    public List<CoPurchaseDTO> findCoPurchaseRules(String skuKeyword) {
+        String normalizedKeyword = normalize(skuKeyword).toLowerCase();
         Map<String, Set<String>> purchaseProducts = new HashMap<>();
         Map<String, String> productTitle = new HashMap<>();
 
@@ -255,6 +256,11 @@ public class AnalysisService {
             String[] ids = sortedPairs.get(i).getKey().split("\\|\\|", 2);
             String aId = ids[0];
             String bId = ids.length > 1 ? ids[1] : "";
+            if (!normalizedKeyword.isEmpty()
+                    && !aId.toLowerCase().contains(normalizedKeyword)
+                    && !bId.toLowerCase().contains(normalizedKeyword)) {
+                continue;
+            }
             result.add(new CoPurchaseDTO(
                     productTitle.getOrDefault(aId, aId),
                     aId,

+ 107 - 24
dtm-system/src/main/java/com/dtm/order/shop/service/ShopAnalysisService.java

@@ -4,6 +4,9 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.web.multipart.MultipartFile;
 
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -33,56 +36,61 @@ public class ShopAnalysisService {
         return dataStore.getLastUploadDebug();
     }
 
-    public Map<String, Double> getChannelSalesContribution() {
+    public String getMaxSalesDate() {
+        LocalDate maxDate = dataStore.getMaxSalesDate();
+        return maxDate == null ? LocalDate.now().toString() : maxDate.toString();
+    }
+
+    public Map<String, Double> getChannelSalesContribution(String startDate, String endDate) {
         Map<String, Double> contributionData = new HashMap<>();
-        for (ShopSalesRecord record : dataStore.getSalesRecords()) {
+        for (ShopSalesRecord record : filterRecords(startDate, endDate, null)) {
             contributionData.merge(record.getPlatformName(), 1.0, Double::sum);
         }
         return contributionData.isEmpty() ? null : contributionData;
     }
 
-    public Map<String, Double> getChannelRoiValue() {
+    public Map<String, Double> getChannelRoiValue(String startDate, String endDate) {
         Map<String, Double> resultData = new HashMap<>();
-        for (ShopSalesRecord record : dataStore.getSalesRecords()) {
+        for (ShopSalesRecord record : filterRecords(startDate, endDate, null)) {
             resultData.merge(record.getPlatformName(), record.getSalesAmount(), Double::sum);
         }
         return resultData.isEmpty() ? null : resultData;
     }
 
-    public List<Map<String, Object>> getUnitContribution() {
+    public List<Map<String, Object>> getUnitContribution(String startDate, String endDate) {
         Map<String, Double> volumeByUnit = new HashMap<>();
         Map<String, Double> amountByUnit = new HashMap<>();
-        for (ShopSalesRecord record : dataStore.getSalesRecords()) {
+        for (ShopSalesRecord record : filterRecords(startDate, endDate, null)) {
             volumeByUnit.merge(record.getBusinessUnitName(), record.getQuantity(), Double::sum);
             amountByUnit.merge(record.getBusinessUnitName(), record.getSalesAmount(), Double::sum);
         }
         return toContributionList(volumeByUnit, amountByUnit);
     }
 
-    public List<Map<String, Object>> getChannelTotalContribution() {
+    public List<Map<String, Object>> getChannelTotalContribution(String startDate, String endDate) {
         Map<String, Double> volumeByChannel = new HashMap<>();
         Map<String, Double> amountByChannel = new HashMap<>();
-        for (ShopSalesRecord record : dataStore.getSalesRecords()) {
+        for (ShopSalesRecord record : filterRecords(startDate, endDate, null)) {
             volumeByChannel.merge(record.getChannelName(), record.getQuantity(), Double::sum);
             amountByChannel.merge(record.getChannelName(), record.getSalesAmount(), Double::sum);
         }
         return toContributionList(volumeByChannel, amountByChannel);
     }
 
-    public List<Map<String, Object>> getPlatformTotalContribution() {
+    public List<Map<String, Object>> getPlatformTotalContribution(String startDate, String endDate) {
         Map<String, Double> volumeByPlatform = new HashMap<>();
         Map<String, Double> amountByPlatform = new HashMap<>();
-        for (ShopSalesRecord record : dataStore.getSalesRecords()) {
+        for (ShopSalesRecord record : filterRecords(startDate, endDate, null)) {
             volumeByPlatform.merge(record.getPlatformName(), record.getQuantity(), Double::sum);
             amountByPlatform.merge(record.getPlatformName(), record.getSalesAmount(), Double::sum);
         }
         return toContributionList(volumeByPlatform, amountByPlatform);
     }
 
-    public Map<String, Object> getTopProductContribution() {
+    public Map<String, Object> getTopProductContribution(String startDate, String endDate) {
         double totalSales = 0.0;
         Map<String, Double> productSales = new HashMap<>();
-        for (ShopSalesRecord record : dataStore.getSalesRecords()) {
+        for (ShopSalesRecord record : filterRecords(startDate, endDate, null)) {
             totalSales += record.getSalesAmount();
             productSales.merge(record.getProductCode(), record.getSalesAmount(), Double::sum);
         }
@@ -102,7 +110,7 @@ public class ShopAnalysisService {
             topProducts.add(productMap);
         }
 
-        double contributionRatio = (totalSales > 0) ? (top5SalesSum / totalSales) : 0.0;
+        double contributionRatio = totalSales > 0 ? (top5SalesSum / totalSales) : 0.0;
         Map<String, Object> finalResult = new HashMap<>();
         finalResult.put("top5Products", topProducts);
         finalResult.put("top5TotalSales", top5SalesSum);
@@ -111,9 +119,9 @@ public class ShopAnalysisService {
         return finalResult;
     }
 
-    public List<Map<String, Object>> getCrossSellingProducts() {
+    public List<Map<String, Object>> getCrossSellingProducts(String startDate, String endDate, String skuKeyword) {
         Map<String, Set<String>> platformByProduct = new HashMap<>();
-        for (ShopSalesRecord record : dataStore.getSalesRecords()) {
+        for (ShopSalesRecord record : filterRecords(startDate, endDate, skuKeyword)) {
             platformByProduct
                     .computeIfAbsent(record.getProductCode(), k -> new HashSet<>())
                     .add(record.getPlatformName());
@@ -129,13 +137,14 @@ public class ShopAnalysisService {
                 crossSellingData.add(productMap);
             }
         }
+        crossSellingData.sort((a, b) -> Long.compare((Long) b.get("platformCount"), (Long) a.get("platformCount")));
         return crossSellingData.isEmpty() ? null : crossSellingData;
     }
 
-    public Map<String, Double> getDepartmentOperationalEfficiency() {
+    public Map<String, Double> getDepartmentOperationalEfficiency(String startDate, String endDate) {
         Map<String, Double> sumByUnit = new HashMap<>();
         Map<String, Long> countByUnit = new HashMap<>();
-        for (ShopSalesRecord record : dataStore.getSalesRecords()) {
+        for (ShopSalesRecord record : filterRecords(startDate, endDate, null)) {
             sumByUnit.merge(record.getBusinessUnitName(), record.getSalesAmount(), Double::sum);
             countByUnit.merge(record.getBusinessUnitName(), 1L, Long::sum);
         }
@@ -149,9 +158,9 @@ public class ShopAnalysisService {
         return efficiencyData.isEmpty() ? null : efficiencyData;
     }
 
-    public Map<String, Long> getChannelProductDiversity() {
+    public Map<String, Long> getChannelProductDiversity(String startDate, String endDate) {
         Map<String, Set<String>> productsByChannel = new HashMap<>();
-        for (ShopSalesRecord record : dataStore.getSalesRecords()) {
+        for (ShopSalesRecord record : filterRecords(startDate, endDate, null)) {
             productsByChannel
                     .computeIfAbsent(record.getChannelName(), k -> new HashSet<>())
                     .add(record.getProductCode());
@@ -163,10 +172,25 @@ public class ShopAnalysisService {
         return diversityData.isEmpty() ? null : diversityData;
     }
 
-    private List<Map<String, Object>> toContributionList(
-            Map<String, Double> volume,
-            Map<String, Double> amount
-    ) {
+    private List<ShopSalesRecord> filterRecords(String startDate, String endDate, String skuKeyword) {
+        LocalDateRange range = parseRange(startDate, endDate);
+        String normalizedKeyword = normalize(skuKeyword).toLowerCase();
+        boolean hasKeyword = !normalizedKeyword.isEmpty();
+
+        List<ShopSalesRecord> filtered = new ArrayList<>();
+        for (ShopSalesRecord record : dataStore.getSalesRecords()) {
+            if (!range.contains(record.getSalesDate())) {
+                continue;
+            }
+            if (hasKeyword && !normalize(record.getProductCode()).toLowerCase().contains(normalizedKeyword)) {
+                continue;
+            }
+            filtered.add(record);
+        }
+        return filtered;
+    }
+
+    private List<Map<String, Object>> toContributionList(Map<String, Double> volume, Map<String, Double> amount) {
         List<Map<String, Object>> result = new ArrayList<>();
         for (String name : volume.keySet()) {
             Map<String, Object> map = new HashMap<>();
@@ -175,9 +199,68 @@ public class ShopAnalysisService {
             map.put("totalAmount", amount.getOrDefault(name, 0.0));
             result.add(map);
         }
+        result.sort((a, b) -> Double.compare((Double) b.get("totalAmount"), (Double) a.get("totalAmount")));
         return result.isEmpty() ? null : result;
     }
-}
 
+    private LocalDateRange parseRange(String startDate, String endDate) {
+        LocalDate start = parseDate(startDate);
+        LocalDate end = parseDate(endDate);
+        if (start == null && end == null) {
+            return new LocalDateRange(null, null);
+        }
+        if (start == null) {
+            start = end;
+        }
+        if (end == null) {
+            end = start;
+        }
+        return new LocalDateRange(start, end);
+    }
+
+    private LocalDate parseDate(String raw) {
+        if (raw == null) {
+            return null;
+        }
+        String value = raw.trim();
+        if (value.isEmpty()) {
+            return null;
+        }
+        if (value.length() > 10) {
+            value = value.substring(0, 10);
+        }
+        value = value.replace('/', '-');
+        try {
+            return LocalDate.parse(value, DateTimeFormatter.ofPattern("yyyy-M-d"));
+        } catch (DateTimeParseException e) {
+            return null;
+        }
+    }
 
+    private String normalize(String value) {
+        return value == null ? "" : value.trim();
+    }
 
+    private static class LocalDateRange {
+        private final LocalDate start;
+        private final LocalDate end;
+
+        private LocalDateRange(LocalDate start, LocalDate end) {
+            this.start = start;
+            this.end = end;
+        }
+
+        public boolean contains(LocalDate date) {
+            if (date == null) {
+                return false;
+            }
+            if (start != null && date.isBefore(start)) {
+                return false;
+            }
+            if (end != null && date.isAfter(end)) {
+                return false;
+            }
+            return true;
+        }
+    }
+}

+ 71 - 70
dtm-system/src/main/java/com/dtm/order/shop/service/ShopSalesDataStore.java

@@ -15,6 +15,7 @@ import java.io.Reader;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
+import java.time.LocalDate;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Iterator;
@@ -28,6 +29,7 @@ public class ShopSalesDataStore {
     private volatile List<ShopSalesRecord> salesRecords = Collections.emptyList();
     private volatile long lastLoadCount = 0;
     private volatile String lastUploadDebug = "";
+    private volatile LocalDate maxSalesDate = null;
 
     @PostConstruct
     public void loadOnStartup() {
@@ -36,11 +38,13 @@ public class ShopSalesDataStore {
 
     public int reload() {
         List<ShopSalesRecord> loaded = new ArrayList<>();
+        LocalDate maxDate = null;
         File folder = new File(shopDataFolderPath);
         File[] files = folder.listFiles((dir, name) -> name.toLowerCase().endsWith(".csv"));
         if (files == null || files.length == 0) {
             salesRecords = Collections.emptyList();
             lastLoadCount = 0;
+            maxSalesDate = null;
             return 0;
         }
 
@@ -54,36 +58,14 @@ public class ShopSalesDataStore {
                 }
 
                 while (csvIterator.hasNext()) {
-                    CSVRecord csvRecord = csvIterator.next();
-                    if (csvRecord.size() < 10) {
+                    ShopSalesRecord record = parseRecord(csvIterator.next());
+                    if (record == null) {
                         continue;
                     }
-                    try {
-                        int year = Integer.parseInt(csvRecord.get(0));
-                        int month = Integer.parseInt(csvRecord.get(1));
-                        int day = Integer.parseInt(csvRecord.get(2));
-                        String businessUnitName = csvRecord.get(3);
-                        String channelName = csvRecord.get(4);
-                        String platformName = csvRecord.get(5);
-                        String productCode = csvRecord.get(6);
-                        double quantity = Double.parseDouble(csvRecord.get(7));
-                        double unitPrice = Double.parseDouble(csvRecord.get(8));
-                        double salesAmount = Double.parseDouble(csvRecord.get(9));
-
-                        loaded.add(new ShopSalesRecord(
-                                year,
-                                month,
-                                day,
-                                businessUnitName,
-                                channelName,
-                                platformName,
-                                productCode,
-                                quantity,
-                                unitPrice,
-                                salesAmount
-                        ));
-                    } catch (Exception ignored) {
-                        // Skip bad rows.
+                    loaded.add(record);
+                    LocalDate recordDate = record.getSalesDate();
+                    if (maxDate == null || recordDate.isAfter(maxDate)) {
+                        maxDate = recordDate;
                     }
                 }
             } catch (Exception ignored) {
@@ -93,16 +75,19 @@ public class ShopSalesDataStore {
 
         salesRecords = Collections.unmodifiableList(loaded);
         lastLoadCount = loaded.size();
+        maxSalesDate = maxDate;
         return loaded.size();
     }
 
     public int reloadFromUploads(MultipartFile[] files) {
         List<ShopSalesRecord> loaded = new ArrayList<>();
+        LocalDate maxDate = null;
         StringBuilder debug = new StringBuilder();
         if (files == null || files.length == 0) {
             salesRecords = Collections.emptyList();
             lastLoadCount = 0;
-            lastUploadDebug = "未收到上传文件。";
+            lastUploadDebug = "No uploaded files received.";
+            maxSalesDate = null;
             return 0;
         }
 
@@ -115,27 +100,35 @@ public class ShopSalesDataStore {
             received++;
             String filename = file.getOriginalFilename();
             if (filename == null || !filename.toLowerCase().endsWith(".csv")) {
-                debug.append("跳过非CSV文件: ").append(filename).append("\n");
+                debug.append("Skip non-CSV file: ").append(filename).append("\n");
                 continue;
             }
             csvCount++;
-            debug.append("文件: ").append(filename).append(", 大小=").append(file.getSize()).append("\n");
+            debug.append("File: ").append(filename).append(", size=").append(file.getSize()).append("\n");
             try {
                 byte[] bytes = file.getBytes();
                 int before = loaded.size();
                 int added = parseCsvBytes(bytes, loaded, debug);
-                debug.append("导入行数: ").append(added).append("\n");
+                debug.append("Imported rows: ").append(added).append("\n");
                 if (loaded.size() == before) {
-                    debug.append("提示: 该文件未解析到有效数据,可能是编码/分隔符/列格式不匹配。\n");
+                    debug.append("Hint: no valid rows parsed. Check encoding, delimiter, or columns.\n");
+                    continue;
+                }
+                for (int i = before; i < loaded.size(); i++) {
+                    LocalDate recordDate = loaded.get(i).getSalesDate();
+                    if (maxDate == null || recordDate.isAfter(maxDate)) {
+                        maxDate = recordDate;
+                    }
                 }
             } catch (Exception e) {
-                debug.append("读取文件异常: ").append(e.getMessage()).append("\n");
+                debug.append("Read file error: ").append(e.getMessage()).append("\n");
             }
         }
 
         salesRecords = Collections.unmodifiableList(loaded);
         lastLoadCount = loaded.size();
-        lastUploadDebug = "收到文件数=" + received + ", CSV文件数=" + csvCount + ", 总导入行数=" + loaded.size() + "\n" + debug;
+        maxSalesDate = maxDate;
+        lastUploadDebug = "Received files=" + received + ", CSV files=" + csvCount + ", imported rows=" + loaded.size() + "\n" + debug;
         return loaded.size();
     }
 
@@ -151,6 +144,10 @@ public class ShopSalesDataStore {
         return lastUploadDebug;
     }
 
+    public LocalDate getMaxSalesDate() {
+        return maxSalesDate;
+    }
+
     private int parseCsvBytes(byte[] bytes, List<ShopSalesRecord> loaded, StringBuilder debug) {
         int added = parseCsvBytesWithCharset(bytes, loaded, StandardCharsets.UTF_8, debug);
         if (added > 0) {
@@ -163,10 +160,10 @@ public class ShopSalesDataStore {
         try (Reader reader = new InputStreamReader(new ByteArrayInputStream(bytes), charset);
              CSVParser csvParser = new CSVParser(reader, CSVFormat.EXCEL.builder().setTrim(true).build())) {
             int added = appendCsvRecords(csvParser, loaded);
-            debug.append("编码 ").append(charset.displayName()).append(" 解析行数: ").append(added).append("\n");
+            debug.append("Charset ").append(charset.displayName()).append(" parsed rows: ").append(added).append("\n");
             return added;
         } catch (Exception e) {
-            debug.append("编码 ").append(charset.displayName()).append(" 解析异常: ").append(e.getMessage()).append("\n");
+            debug.append("Charset ").append(charset.displayName()).append(" parse error: ").append(e.getMessage()).append("\n");
             return 0;
         }
     }
@@ -178,42 +175,46 @@ public class ShopSalesDataStore {
         }
         int added = 0;
         while (csvIterator.hasNext()) {
-            CSVRecord csvRecord = csvIterator.next();
-            if (csvRecord.size() < 10) {
+            ShopSalesRecord record = parseRecord(csvIterator.next());
+            if (record == null) {
                 continue;
             }
-            try {
-                int year = Integer.parseInt(csvRecord.get(0));
-                int month = Integer.parseInt(csvRecord.get(1));
-                int day = Integer.parseInt(csvRecord.get(2));
-                String businessUnitName = csvRecord.get(3);
-                String channelName = csvRecord.get(4);
-                String platformName = csvRecord.get(5);
-                String productCode = csvRecord.get(6);
-                double quantity = Double.parseDouble(csvRecord.get(7));
-                double unitPrice = Double.parseDouble(csvRecord.get(8));
-                double salesAmount = Double.parseDouble(csvRecord.get(9));
-
-                loaded.add(new ShopSalesRecord(
-                        year,
-                        month,
-                        day,
-                        businessUnitName,
-                        channelName,
-                        platformName,
-                        productCode,
-                        quantity,
-                        unitPrice,
-                        salesAmount
-                ));
-                added++;
-            } catch (Exception ignored) {
-                // Skip bad rows.
-            }
+            loaded.add(record);
+            added++;
         }
         return added;
     }
-}
-
-
 
+    private ShopSalesRecord parseRecord(CSVRecord csvRecord) {
+        if (csvRecord == null || csvRecord.size() < 10) {
+            return null;
+        }
+        try {
+            int year = Integer.parseInt(csvRecord.get(0));
+            int month = Integer.parseInt(csvRecord.get(1));
+            int day = Integer.parseInt(csvRecord.get(2));
+            String businessUnitName = csvRecord.get(3);
+            String channelName = csvRecord.get(4);
+            String platformName = csvRecord.get(5);
+            String productCode = csvRecord.get(6);
+            double quantity = Double.parseDouble(csvRecord.get(7));
+            double unitPrice = Double.parseDouble(csvRecord.get(8));
+            double salesAmount = Double.parseDouble(csvRecord.get(9));
+
+            return new ShopSalesRecord(
+                    year,
+                    month,
+                    day,
+                    businessUnitName,
+                    channelName,
+                    platformName,
+                    productCode,
+                    quantity,
+                    unitPrice,
+                    salesAmount
+            );
+        } catch (Exception ignored) {
+            return null;
+        }
+    }
+}

+ 3 - 0
dtm-system/src/main/java/com/dtm/order/shop/service/ShopSalesRecord.java

@@ -1,5 +1,7 @@
 package com.dtm.order.shop.service;
 
+import java.time.LocalDate;
+
 public class ShopSalesRecord {
     private final int year;
     private final int month;
@@ -46,6 +48,7 @@ public class ShopSalesRecord {
     public double getQuantity() { return quantity; }
     public double getUnitPrice() { return unitPrice; }
     public double getSalesAmount() { return salesAmount; }
+    public LocalDate getSalesDate() { return LocalDate.of(year, month, day); }
 }