Jelajahi Sumber

订单监测-上传文件

Gogs 2 bulan lalu
induk
melakukan
30bdb3a2d0

+ 32 - 0
dtm-order/src/main/java/com/dtm/order/DataImportController.java

@@ -1,14 +1,23 @@
 package com.dtm.order;
 
+import com.dtm.common.annotation.Anonymous;
 import com.dtm.order.service.OrderDataStore;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
 
 import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
 
+@Anonymous
 @RestController
 @RequestMapping("/api/import")
 public class DataImportController {
@@ -48,6 +57,29 @@ public class DataImportController {
 
         return response.append("</body></html>").toString();
     }
+
+    @PostMapping("/import-data/upload")
+    public ResponseEntity<Map<String, Object>> importDataByUpload(@RequestParam("files") MultipartFile[] files) {
+        Map<String, Object> response = new HashMap<>();
+        try {
+            int loadedCount = orderDataStore.reloadFromUploads(files);
+            if (loadedCount <= 0) {
+                response.put("success", false);
+                response.put("message", "导入失败,请检查是否上传了有效的 CSV 文件。");
+                response.put("debug", orderDataStore.getLastDebug());
+                return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
+            }
+            response.put("success", true);
+            response.put("message", "上传并导入成功。");
+            response.put("loadedRows", loadedCount);
+            response.put("debug", orderDataStore.getLastDebug());
+            return ResponseEntity.ok(response);
+        } catch (Exception e) {
+            response.put("success", false);
+            response.put("message", "上传导入失败: " + e.getMessage());
+            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
+        }
+    }
 }
 
 

+ 101 - 0
dtm-order/src/main/java/com/dtm/order/service/OrderDataStore.java

@@ -5,10 +5,13 @@ import org.apache.commons.csv.CSVParser;
 import org.apache.commons.csv.CSVRecord;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
 
 import javax.annotation.PostConstruct;
+import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.FileReader;
+import java.io.InputStreamReader;
 import java.io.Reader;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
@@ -88,6 +91,45 @@ public class OrderDataStore {
         return loaded.size();
     }
 
+    public int reloadFromUploads(MultipartFile[] files) {
+        List<OrderRecord> loaded = new ArrayList<>();
+        LocalDate maxDate = null;
+        StringBuilder debug = new StringBuilder();
+
+        if (files == null || files.length == 0) {
+            orders = Collections.emptyList();
+            maxOrderDate = null;
+            lastLoadCount = 0;
+            lastDebug = "No uploaded files received.";
+            return 0;
+        }
+
+        for (MultipartFile file : files) {
+            if (file == null || file.isEmpty()) {
+                continue;
+            }
+            String filename = file.getOriginalFilename();
+            if (filename == null || !filename.toLowerCase().endsWith(".csv")) {
+                continue;
+            }
+            debug.append("FILE=").append(filename).append(" size=").append(file.getSize()).append("\n");
+            int added = loadFromUploadedFile(file, loaded, debug);
+            debug.append("ADDED=").append(added).append("\n");
+            if (added > 0) {
+                LocalDate fileMaxDate = findMaxDate(loaded);
+                if (fileMaxDate != null && (maxDate == null || fileMaxDate.isAfter(maxDate))) {
+                    maxDate = fileMaxDate;
+                }
+            }
+        }
+
+        orders = Collections.unmodifiableList(loaded);
+        maxOrderDate = maxDate;
+        lastLoadCount = loaded.size();
+        lastDebug = debug.toString();
+        return loaded.size();
+    }
+
     public List<OrderRecord> getOrders() {
         return orders;
     }
@@ -188,6 +230,65 @@ public class OrderDataStore {
         return added;
     }
 
+    private int loadFromUploadedFile(MultipartFile file, List<OrderRecord> target, StringBuilder debug) {
+        byte[] bytes;
+        try {
+            bytes = file.getBytes();
+        } catch (Exception e) {
+            debug.append("ERROR=").append(e.getMessage()).append("\n");
+            return 0;
+        }
+        int added = loadFromBytes(bytes, CSVFormat.DEFAULT.withHeader(), Charset.forName("GBK"), target, debug);
+        if (added == 0) {
+            added = loadFromBytes(bytes, CSVFormat.DEFAULT.withDelimiter(';').withHeader(), Charset.forName("GBK"), target, debug);
+        }
+        if (added == 0) {
+            added = loadFromBytes(bytes, CSVFormat.TDF.withHeader(), Charset.forName("GBK"), target, debug);
+        }
+        if (added == 0) {
+            added = loadFromBytes(bytes, CSVFormat.DEFAULT.withHeader(), StandardCharsets.UTF_8, target, debug);
+        }
+        if (added == 0) {
+            added = loadFromBytes(bytes, CSVFormat.DEFAULT.withDelimiter(';').withHeader(), StandardCharsets.UTF_8, target, debug);
+        }
+        if (added == 0) {
+            added = loadFromBytes(bytes, CSVFormat.TDF.withHeader(), StandardCharsets.UTF_8, target, debug);
+        }
+        return added;
+    }
+
+    private int loadFromBytes(byte[] bytes, CSVFormat format, Charset charset, List<OrderRecord> target, StringBuilder debug) {
+        List<OrderRecord> buffer = new ArrayList<>();
+        int added = 0;
+        int garbled = 0;
+        try (Reader reader = new InputStreamReader(new ByteArrayInputStream(bytes), charset);
+             CSVParser csvParser = new CSVParser(reader, format)) {
+            for (CSVRecord record : csvParser) {
+                OrderRecord order = toOrderRecord(record);
+                if (order != null) {
+                    buffer.add(order);
+                    added++;
+                    if (isGarbled(order.getProductTitle())) {
+                        garbled++;
+                    }
+                }
+            }
+        } catch (Exception e) {
+            debug.append("ERROR=").append(e.getMessage()).append("\n");
+            return 0;
+        }
+
+        if (added > 0) {
+            double ratio = (double) garbled / (double) added;
+            if (ratio > 0.2) {
+                debug.append("GARBLED_RATIO=").append(ratio).append("\n");
+                return 0;
+            }
+            target.addAll(buffer);
+        }
+        return added;
+    }
+
     private LocalDate findMaxDate(List<OrderRecord> records) {
         LocalDate max = null;
         for (OrderRecord record : records) {

+ 32 - 1
dtm-order/src/main/java/com/dtm/order/shop/Controller/ShopDataImportController.java

@@ -1,18 +1,23 @@
-package com.dtm.order.shop.Controller;
+package com.dtm.order.shop.Controller;
 
+import com.dtm.common.annotation.Anonymous;
 import com.dtm.order.shop.service.ShopAnalysisService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
 
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+@Anonymous
 @RestController
 @RequestMapping("/api/shop/import")
 public class ShopDataImportController {
@@ -55,6 +60,31 @@ public class ShopDataImportController {
         }
     }
 
+    @PostMapping("/import-sales-data/upload")
+    public ResponseEntity<Map<String, Object>> importSalesDataByUpload(@RequestParam("files") MultipartFile[] files) {
+        Map<String, Object> response = new HashMap<>();
+        try {
+            int importedCount = shopAnalysisService.importShopSalesDataFromUploads(files);
+
+            if (importedCount <= 0) {
+                response.put("success", false);
+                response.put("message", "上传导入失败,请检查文件编码或列格式(需为CSV且前10列可解析)。");
+                response.put("debug", shopAnalysisService.getLastUploadDebug());
+                return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
+            }
+
+            response.put("success", true);
+            response.put("message", "上传并导入成功。");
+            response.put("importedRows", importedCount);
+            response.put("debug", shopAnalysisService.getLastUploadDebug());
+            return ResponseEntity.ok(response);
+        } catch (Exception e) {
+            response.put("success", false);
+            response.put("message", "上传导入失败: " + e.getMessage());
+            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
+        }
+    }
+
     @GetMapping("/channel-contribution")
     public ResponseEntity<Map<String, Object>> getChannelContribution() {
         Map<String, Object> response = new HashMap<>();
@@ -247,3 +277,4 @@ public class ShopDataImportController {
 
 
 
+

+ 9 - 0
dtm-order/src/main/java/com/dtm/order/shop/service/ShopAnalysisService.java

@@ -2,6 +2,7 @@ package com.dtm.order.shop.service;
 
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -24,6 +25,14 @@ public class ShopAnalysisService {
         return dataStore.reload();
     }
 
+    public int importShopSalesDataFromUploads(MultipartFile[] files) {
+        return dataStore.reloadFromUploads(files);
+    }
+
+    public String getLastUploadDebug() {
+        return dataStore.getLastUploadDebug();
+    }
+
     public Map<String, Double> getChannelSalesContribution() {
         Map<String, Double> contributionData = new HashMap<>();
         for (ShopSalesRecord record : dataStore.getSalesRecords()) {

+ 115 - 0
dtm-order/src/main/java/com/dtm/order/shop/service/ShopSalesDataStore.java

@@ -5,11 +5,15 @@ import org.apache.commons.csv.CSVParser;
 import org.apache.commons.csv.CSVRecord;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
 
 import javax.annotation.PostConstruct;
+import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.FileReader;
+import java.io.InputStreamReader;
 import java.io.Reader;
+import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -23,6 +27,7 @@ public class ShopSalesDataStore {
 
     private volatile List<ShopSalesRecord> salesRecords = Collections.emptyList();
     private volatile long lastLoadCount = 0;
+    private volatile String lastUploadDebug = "";
 
     @PostConstruct
     public void loadOnStartup() {
@@ -91,6 +96,49 @@ public class ShopSalesDataStore {
         return loaded.size();
     }
 
+    public int reloadFromUploads(MultipartFile[] files) {
+        List<ShopSalesRecord> loaded = new ArrayList<>();
+        StringBuilder debug = new StringBuilder();
+        if (files == null || files.length == 0) {
+            salesRecords = Collections.emptyList();
+            lastLoadCount = 0;
+            lastUploadDebug = "未收到上传文件。";
+            return 0;
+        }
+
+        int received = 0;
+        int csvCount = 0;
+        for (MultipartFile file : files) {
+            if (file == null || file.isEmpty()) {
+                continue;
+            }
+            received++;
+            String filename = file.getOriginalFilename();
+            if (filename == null || !filename.toLowerCase().endsWith(".csv")) {
+                debug.append("跳过非CSV文件: ").append(filename).append("\n");
+                continue;
+            }
+            csvCount++;
+            debug.append("文件: ").append(filename).append(", 大小=").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");
+                if (loaded.size() == before) {
+                    debug.append("提示: 该文件未解析到有效数据,可能是编码/分隔符/列格式不匹配。\n");
+                }
+            } catch (Exception e) {
+                debug.append("读取文件异常: ").append(e.getMessage()).append("\n");
+            }
+        }
+
+        salesRecords = Collections.unmodifiableList(loaded);
+        lastLoadCount = loaded.size();
+        lastUploadDebug = "收到文件数=" + received + ", CSV文件数=" + csvCount + ", 总导入行数=" + loaded.size() + "\n" + debug;
+        return loaded.size();
+    }
+
     public List<ShopSalesRecord> getSalesRecords() {
         return salesRecords;
     }
@@ -98,6 +146,73 @@ public class ShopSalesDataStore {
     public long getLastLoadCount() {
         return lastLoadCount;
     }
+
+    public String getLastUploadDebug() {
+        return lastUploadDebug;
+    }
+
+    private int parseCsvBytes(byte[] bytes, List<ShopSalesRecord> loaded, StringBuilder debug) {
+        int added = parseCsvBytesWithCharset(bytes, loaded, StandardCharsets.UTF_8, debug);
+        if (added > 0) {
+            return added;
+        }
+        return parseCsvBytesWithCharset(bytes, loaded, Charset.forName("GBK"), debug);
+    }
+
+    private int parseCsvBytesWithCharset(byte[] bytes, List<ShopSalesRecord> loaded, Charset charset, StringBuilder debug) {
+        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");
+            return added;
+        } catch (Exception e) {
+            debug.append("编码 ").append(charset.displayName()).append(" 解析异常: ").append(e.getMessage()).append("\n");
+            return 0;
+        }
+    }
+
+    private int appendCsvRecords(CSVParser csvParser, List<ShopSalesRecord> loaded) {
+        Iterator<CSVRecord> csvIterator = csvParser.iterator();
+        if (csvIterator.hasNext()) {
+            csvIterator.next();
+        }
+        int added = 0;
+        while (csvIterator.hasNext()) {
+            CSVRecord csvRecord = csvIterator.next();
+            if (csvRecord.size() < 10) {
+                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.
+            }
+        }
+        return added;
+    }
 }