Przeglądaj źródła

导出模块更新

liuchengsen 3 tygodni temu
rodzic
commit
57acdaa85d

+ 62 - 35
backend-java/src/main/java/com/pharmacopoeia/service/BrandRecommendService.java

@@ -315,7 +315,7 @@ public class BrandRecommendService {
 
     public Map<String, Object> importBrandsCsv(String csv) {
         List<String[]> rows = parseCsv(csv);
-        Map<String, Integer> colIdx = getIntegerMap(rows);
+        Map<String, Integer> colIdx = buildColIdx(rows, BRAND_CSV_COLS);
 
         int success = 0, skipped = 0, failed = 0;
         for (int i = 1; i < rows.size(); i++) {
@@ -348,27 +348,44 @@ public class BrandRecommendService {
     }
 
     @NotNull
-    private static Map<String, Integer> getIntegerMap(List<String[]> rows) {
+    private static Map<String, Integer> buildColIdx(List<String[]> rows, List<String> expectedHeaders) {
         if (rows.isEmpty()) {
-            throw new IllegalArgumentException("CSV format incorrect: expected header " + BRANDS_CSV_HEADER);
+            throw new IllegalArgumentException("CSV格式不正确,期望表头: " + String.join(",", expectedHeaders));
         }
 
-        // Build header -> index map from the first row
         String[] header = rows.getFirst();
         Map<String, Integer> colIdx = new LinkedHashMap<>();
         for (int i = 0; i < header.length; i++) {
-            colIdx.put(header[i].trim().replace("\"", ""), i);
+            colIdx.put(normalizeHeader(header[i]), i);
         }
 
-        // Verify all expected headers are present
-        for (String h : BRAND_CSV_COLS) {
+        for (String h : expectedHeaders) {
             if (!colIdx.containsKey(h)) {
-                throw new IllegalArgumentException("CSV missing column: " + h);
+                throw new IllegalArgumentException("CSV缺少列: " + h + ",实际表头: " + String.join(", ", header));
             }
         }
         return colIdx;
     }
 
+    /** Normalize a CSV header cell: strip BOM, invisible chars, quotes, and trim. */
+    private static String normalizeHeader(String raw) {
+        if (raw == null) return "";
+        String s = raw;
+        // Strip BOM anywhere (not just at start of file — can appear on first cell)
+        s = s.replace("", "");
+        // Strip zero-width characters that Excel/WPS may inject
+        s = s.replace("​", ""); // zero-width space
+        s = s.replace("‌", ""); // zero-width non-joiner
+        s = s.replace("‍", ""); // zero-width joiner
+        s = s.replace("", ""); // BOM / zero-width no-break space
+        s = s.replace(" ", " "); // non-breaking space → normal space
+        // Strip other common invisible control chars except space
+        s = s.replaceAll("[\\p{Cntrl}&&[^\t]]", "");
+        // Strip surrounding quotes
+        s = s.replace("\"", "").replace("'", "");
+        return s.trim();
+    }
+
     public String exportBrandsCsv(Set<Integer> ids) {
         List<Brand> brands = ids.isEmpty() ? List.of() : brandRepository.findAllById(ids);
         StringBuilder sb = new StringBuilder(BRANDS_CSV_HEADER).append("\n");
@@ -395,7 +412,7 @@ public class BrandRecommendService {
 
     public Map<String, Object> importRulesCsv(String csv) {
         List<String[]> rows = parseCsv(csv);
-        Map<String, Integer> colIdx = getStringIntegerMap(rows);
+        Map<String, Integer> colIdx = buildColIdx(rows, RULES_CSV_COLS);
 
         int success = 0, skipped = 0, failed = 0;
         for (int i = 1; i < rows.size(); i++) {
@@ -426,28 +443,6 @@ public class BrandRecommendService {
         return Map.of("ok", true, "success", success, "skipped", skipped, "failed", failed);
     }
 
-    @NotNull
-    private static Map<String, Integer> getStringIntegerMap(List<String[]> rows) {
-        if (rows.isEmpty()) {
-            throw new IllegalArgumentException("CSV格式不正确,期望表头: " + RULES_CSV_HEADER);
-        }
-
-        // Build header -> index map, stripping quotes/spaces from header names
-        String[] header = rows.getFirst();
-        Map<String, Integer> colIdx = new LinkedHashMap<>();
-        for (int i = 0; i < header.length; i++) {
-            colIdx.put(header[i].trim().replace("\"", ""), i);
-        }
-
-        // Verify all expected headers are present
-        for (String h : RULES_CSV_COLS) {
-            if (!colIdx.containsKey(h)) {
-                throw new IllegalArgumentException("CSV缺少列: " + h);
-            }
-        }
-        return colIdx;
-    }
-
     public String exportRulesCsv(Set<Integer> ids) {
         List<BrandRecommendRule> rules = ids.isEmpty() ? List.of() : ruleRepository.findAllById(ids);
         Set<Integer> brandIds = rules.stream().map(BrandRecommendRule::getBrandId).collect(Collectors.toSet());
@@ -487,7 +482,8 @@ public class BrandRecommendService {
     }
 
     /** Parse CSV content into rows, respecting quoted fields.
-     *  Newlines inside "..." quoted fields do NOT split rows. */
+     *  Newlines inside "..." quoted fields do NOT split rows.
+     *  Auto-detects delimiter from header row (comma, tab, semicolon). */
     private static List<String[]> parseCsv(String csv) {
         List<String[]> rows = new ArrayList<>();
         // Strip UTF-8 BOM if present (Excel adds it when saving UTF-8 CSV)
@@ -526,18 +522,49 @@ public class BrandRecommendService {
             lines.add(lineBuf.toString());
         }
 
+        // Filter empty/comment lines
+        List<String> dataLines = new ArrayList<>();
         for (String line : lines) {
             String trimmed = line.trim();
             if (trimmed.isEmpty() || trimmed.startsWith("#")) {
                 continue;
             }
-            rows.add(parseCsvRow(trimmed));
+            dataLines.add(trimmed);
+        }
+        if (dataLines.isEmpty()) {
+            return rows;
+        }
+
+        // Auto-detect delimiter from header line
+        char delimiter = detectDelimiter(dataLines.getFirst());
+
+        for (String line : dataLines) {
+            rows.add(parseCsvRow(line, delimiter));
         }
         return rows;
     }
 
+    /** Detect the most likely delimiter from a header row. */
+    private static char detectDelimiter(String headerLine) {
+        int[] counts = new int[256];
+        boolean inQ = false;
+        for (int i = 0; i < headerLine.length(); i++) {
+            char ch = headerLine.charAt(i);
+            if (ch == '"') inQ = !inQ;
+            if (!inQ && (ch == ',' || ch == '\t' || ch == ';' || ch == '|')) {
+                counts[ch]++;
+            }
+        }
+        char best = ',';
+        int max = counts[','];
+        if (counts['\t'] > max) { best = '\t'; max = counts['\t']; }
+        if (counts[';'] > max) { best = ';'; max = counts[';']; }
+        if (counts['|'] > max) { best = '|'; max = counts['|']; }
+        return best;
+    }
+
     /** Parse a single CSV row into cells, respecting "..." quoted fields */
-    private static String[] parseCsvRow(String line) {
+    private static String[] parseCsvRow(String line, char delimiter) {
         List<String> cells = new ArrayList<>();
         StringBuilder sb = new StringBuilder();
         boolean inQuotes = false;
@@ -557,7 +584,7 @@ public class BrandRecommendService {
             } else {
                 if (ch == '"') {
                     inQuotes = true;
-                } else if (ch == ',') {
+                } else if (ch == delimiter) {
                     cells.add(sb.toString().trim());
                     sb.setLength(0);
                 } else {