diff --git a/bms-back/ruoyi-admin/src/main/resources/i18n/messages.properties b/bms-back/ruoyi-admin/src/main/resources/i18n/messages.properties index c5eb5bf2..19d48034 100644 --- a/bms-back/ruoyi-admin/src/main/resources/i18n/messages.properties +++ b/bms-back/ruoyi-admin/src/main/resources/i18n/messages.properties @@ -85,7 +85,7 @@ price.weight.range.invalid=起始重量必须小于结束重量 # Zone PrcZone (country -> zone -> postcode) price.zone.import.fileEmpty=上传文件不能为空 price.zone.import.fileType=仅支持 xls / xlsx 文件 -price.zone.import.noSheet=未找到分区 sheet(sheet 名需以「分区」结尾,如「澳大利亚分区」) +price.zone.import.noSheet=未找到可导入的分区 sheet(澳大利亚分区/加拿大偏远附加费/法国MR邮编/日本附加费/英国附加费) price.zone.import.noData=分区 sheet 无有效邮编/分区数据 price.zone.import.success=导入成功:{0} 个国家,{1} 个分区,{2} 条邮编明细({3}) price.zone.import.unmatched=;未匹配国家已跳过:{0} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcZoneServiceImpl.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcZoneServiceImpl.java index cbcab490..72a3476f 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcZoneServiceImpl.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcZoneServiceImpl.java @@ -128,6 +128,10 @@ public class PrcZoneServiceImpl implements IPrcZoneService { zone.setZoneSort(extractZoneSort(zone.getZoneCode())); } + if (StringUtils.isNotEmpty(zone.getZoneCode())) + { + zone.setZoneCode(toZoneCodeAbbrev(zone.getZoneCode())); + } if (StringUtils.isEmpty(zone.getZoneName())) { zone.setZoneName(buildZoneName(region.getCountryNameCn(), zone.getZoneCode())); @@ -150,6 +154,10 @@ public class PrcZoneServiceImpl implements IPrcZoneService { zone.setCountryCode(zone.getCountryCode().trim().toUpperCase(Locale.ROOT)); } + if (StringUtils.isNotEmpty(zone.getZoneCode())) + { + zone.setZoneCode(toZoneCodeAbbrev(zone.getZoneCode())); + } if (zone.getZoneSort() == null && StringUtils.isNotEmpty(zone.getZoneCode())) { zone.setZoneSort(extractZoneSort(zone.getZoneCode())); @@ -223,27 +231,22 @@ public class PrcZoneServiceImpl implements IPrcZoneService try (InputStream is = file.getInputStream(); Workbook workbook = WorkbookFactory.create(is)) { - List zoneSheets = findZoneSheets(workbook); - if (zoneSheets.isEmpty()) + List imports = findZoneImports(workbook); + if (imports.isEmpty()) { throw new ServiceException(MessageUtils.message("price.zone.import.noSheet")); } - for (Sheet sheet : zoneSheets) + for (SheetImport item : imports) { - String countryName = parseCountryNameFromSheet(sheet.getSheetName()); - if (StringUtils.isEmpty(countryName)) + Map> zonePostcodes = item.zonePostcodes; + if (zonePostcodes == null || zonePostcodes.isEmpty()) { continue; } - Map> zonePostcodes = parseZonePostcodes(sheet); - if (zonePostcodes.isEmpty()) - { - continue; - } - BmsRegion region = resolveRegion(countryName, regionByNameCn); + BmsRegion region = resolveRegion(item.countryHint, regionByNameCn); if (region == null) { - unmatched.add(countryName); + unmatched.add(item.countryHint + "(" + item.sheetName + ")"); continue; } int[] counts = replaceRegionZones(region, zonePostcodes); @@ -290,13 +293,13 @@ public class PrcZoneServiceImpl implements IPrcZoneService String countryName = region.getCountryNameCn(); for (Map.Entry> entry : entries) { - String zoneCode = entry.getKey(); + String zoneCode = toZoneCodeAbbrev(entry.getKey()); PrcZone zone = new PrcZone(); zone.setRegionId(regionId); zone.setCountryCode(region.getCountryCode2()); zone.setZoneCode(zoneCode); zone.setZoneName(buildZoneName(countryName, zoneCode)); - zone.setZoneSort(extractZoneSort(zoneCode)); + zone.setZoneSort(extractZoneSort(entry.getKey())); zoneMapper.insertPrcZone(zone); zoneInserted++; @@ -324,9 +327,23 @@ public class PrcZoneServiceImpl implements IPrcZoneService return new int[] { zoneInserted, detailInserted }; } - private List findZoneSheets(Workbook workbook) + private static final class SheetImport { - List list = new ArrayList<>(); + private final String sheetName; + private final String countryHint; + private final Map> zonePostcodes; + + private SheetImport(String sheetName, String countryHint, Map> zonePostcodes) + { + this.sheetName = sheetName; + this.countryHint = countryHint; + this.zonePostcodes = zonePostcodes; + } + } + + private List findZoneImports(Workbook workbook) + { + List list = new ArrayList<>(); for (int i = 0; i < workbook.getNumberOfSheets(); i++) { Sheet sheet = workbook.getSheetAt(i); @@ -334,26 +351,374 @@ public class PrcZoneServiceImpl implements IPrcZoneService { continue; } - String name = StringUtils.trim(sheet.getSheetName()); - if (name != null && name.endsWith(SHEET_SUFFIX) && name.length() > SHEET_SUFFIX.length()) + String sheetName = StringUtils.trim(sheet.getSheetName()); + if (StringUtils.isEmpty(sheetName)) { - list.add(sheet); + continue; } + String countryHint = guessCountryFromSheetName(sheetName); + Map> parsed = parseSheetByName(sheet, sheetName); + if (parsed.isEmpty()) + { + parsed = parseZonePostcodes(sheet); + } + if (parsed.isEmpty()) + { + parsed = parseRemoteZonePostcodes(sheet); + } + if (parsed.isEmpty()) + { + parsed = parseProvincePostcodes(sheet); + } + if (parsed.isEmpty()) + { + parsed = parseCoveragePostcodes(sheet); + } + if (parsed.isEmpty()) + { + parsed = parseUkSurchargePostcodes(sheet); + } + if (parsed.isEmpty()) + { + continue; + } + if (StringUtils.isEmpty(countryHint)) + { + countryHint = sheetName; + } + list.add(new SheetImport(sheetName, countryHint, parsed)); } return list; } - private String parseCountryNameFromSheet(String sheetName) + private String guessCountryFromSheetName(String sheetName) { String name = StringUtils.trim(sheetName); - if (name == null || !name.endsWith(SHEET_SUFFIX)) + if (name == null) { return null; } - return StringUtils.trim(name.substring(0, name.length() - SHEET_SUFFIX.length())); + if (name.endsWith(SHEET_SUFFIX) && name.length() > SHEET_SUFFIX.length()) + { + return StringUtils.trim(name.substring(0, name.length() - SHEET_SUFFIX.length())); + } + // strip common suffixes: remote surcharge / surcharge / postcode + String[] suffixes = { + "\u504f\u8fdc\u9644\u52a0\u8d39", + "\u9644\u52a0\u8d39", + "MR\u90ae\u7f16", + "\u90ae\u7f16" + }; + for (String suffix : suffixes) + { + if (name.endsWith(suffix) && name.length() > suffix.length()) + { + return StringUtils.trim(name.substring(0, name.length() - suffix.length())); + } + } + if (name.contains("\u52a0\u62ff\u5927")) + { + return "\u52a0\u62ff\u5927"; + } + if (name.contains("\u6fb3\u5927\u5229\u4e9a")) + { + return "\u6fb3\u5927\u5229\u4e9a"; + } + if (name.contains("\u82f1\u56fd")) + { + return "\u82f1\u56fd"; + } + if (name.contains("\u65e5\u672c")) + { + return "\u65e5\u672c"; + } + if (name.contains("\u6cd5\u56fd")) + { + return "\u6cd5\u56fd"; + } + return null; } + private Map> parseSheetByName(Sheet sheet, String sheetName) + { + if (sheetName.endsWith(SHEET_SUFFIX)) + { + return parseZonePostcodes(sheet); + } + if (sheetName.contains("\u52a0\u62ff\u5927")) + { + return parseRemoteZonePostcodes(sheet); + } + if (sheetName.contains("\u6cd5\u56fd") && sheetName.contains("\u90ae\u7f16")) + { + return parseCoveragePostcodes(sheet); + } + if (sheetName.contains("\u65e5\u672c")) + { + return parseProvincePostcodes(sheet); + } + if (sheetName.contains("\u82f1\u56fd")) + { + return parseUkSurchargePostcodes(sheet); + } + return new LinkedHashMap<>(); + } + + private List findZoneSheets(Workbook workbook) + { + return new ArrayList<>(); + } + + private String parseCountryNameFromSheet(String sheetName) + { + return guessCountryFromSheetName(sheetName); + } + + /** Australia style: postcode + zone */ private Map> parseZonePostcodes(Sheet sheet) + { + return parseTwoColZoneMap(sheet, true); + } + + /** Canada style: remote postcode + remote zone */ + private Map> parseRemoteZonePostcodes(Sheet sheet) + { + Map> map = new LinkedHashMap<>(); + int postcodeCol = -1; + int zoneCol = -1; + int headerRow = -1; + int lastRow = sheet.getLastRowNum(); + for (int r = 0; r <= Math.min(lastRow, 20); r++) + { + Row row = sheet.getRow(r); + if (row == null) + { + continue; + } + for (int c = 0; c < row.getLastCellNum(); c++) + { + String val = cellText(row.getCell(c)); + if (val.contains("\u90ae\u7f16") && postcodeCol < 0) + { + postcodeCol = c; + headerRow = r; + } + if ((val.equals("\u504f\u8fdc\u5206\u533a") || val.equals(COL_ZONE) || val.contains("\u5206\u533a")) + && !val.contains("\u9644\u52a0") && zoneCol < 0) + { + zoneCol = c; + headerRow = r; + } + } + if (postcodeCol >= 0 && zoneCol >= 0) + { + break; + } + } + if (postcodeCol < 0 || zoneCol < 0 || headerRow < 0) + { + return map; + } + fillTwoCol(map, sheet, headerRow, postcodeCol, zoneCol); + return map; + } + + /** Japan style: province + postcode */ + private Map> parseProvincePostcodes(Sheet sheet) + { + Map> map = new LinkedHashMap<>(); + int provinceCol = -1; + int postcodeCol = -1; + int headerRow = -1; + int lastRow = sheet.getLastRowNum(); + for (int r = 0; r <= Math.min(lastRow, 10); r++) + { + Row row = sheet.getRow(r); + if (row == null) + { + continue; + } + for (int c = 0; c < row.getLastCellNum(); c++) + { + String val = cellText(row.getCell(c)); + if ((val.contains("\u5dde") || val.contains("\u7701") || "\u5dde\u7701".equals(val)) && provinceCol < 0) + { + provinceCol = c; + headerRow = r; + } + if (COL_POSTCODE.equals(val) || val.equals("\u90ae\u7f16")) + { + postcodeCol = c; + headerRow = r; + } + } + if (provinceCol >= 0 && postcodeCol >= 0) + { + break; + } + } + if (provinceCol < 0 || postcodeCol < 0 || headerRow < 0) + { + return map; + } + // zone = province, detail = postcode (swap vs AU) + for (int r = headerRow + 1; r <= lastRow; r++) + { + Row row = sheet.getRow(r); + if (row == null) + { + continue; + } + String zoneCode = normalizeCell(cellText(row.getCell(provinceCol))); + String postcode = normalizePostcode(cellText(row.getCell(postcodeCol))); + if (StringUtils.isEmpty(zoneCode) || StringUtils.isEmpty(postcode)) + { + continue; + } + map.computeIfAbsent(zoneCode, k -> new ArrayList<>()).add(postcode); + } + return map; + } + + /** France MR style: carrier/zone + coverage postcode */ + private Map> parseCoveragePostcodes(Sheet sheet) + { + Map> map = new LinkedHashMap<>(); + int zoneCol = -1; + int postcodeCol = -1; + int headerRow = -1; + int lastRow = sheet.getLastRowNum(); + for (int r = 0; r <= Math.min(lastRow, 10); r++) + { + Row row = sheet.getRow(r); + if (row == null) + { + continue; + } + for (int c = 0; c < row.getLastCellNum(); c++) + { + String val = cellText(row.getCell(c)); + if (val.contains("\u8986\u76d6\u90ae\u7f16") || val.equals(COL_POSTCODE)) + { + postcodeCol = c; + headerRow = r; + } + if (val.contains("\u5c3e\u7a0b") || val.contains("\u81ea\u63d0")) + { + zoneCol = c; + headerRow = r; + } + } + if (zoneCol >= 0 && postcodeCol >= 0) + { + break; + } + } + if (postcodeCol < 0) + { + return map; + } + if (zoneCol < 0) + { + zoneCol = 0; + } + if (headerRow < 0) + { + headerRow = 0; + } + for (int r = headerRow + 1; r <= lastRow; r++) + { + Row row = sheet.getRow(r); + if (row == null) + { + continue; + } + String zoneCode = normalizeCell(cellText(row.getCell(zoneCol))); + if (StringUtils.isEmpty(zoneCode)) + { + zoneCode = "Mondial Relay"; + } + String postcode = normalizePostcode(cellText(row.getCell(postcodeCol))); + if (StringUtils.isEmpty(postcode)) + { + continue; + } + map.computeIfAbsent(zoneCode, k -> new ArrayList<>()).add(postcode); + } + return map; + } + + /** UK surcharge sheet: multi blocks of region + postcode prefix */ + private Map> parseUkSurchargePostcodes(Sheet sheet) + { + Map> map = new LinkedHashMap<>(); + int lastRow = sheet.getLastRowNum(); + // detect header row containing ???? + int headerRow = -1; + for (int r = 0; r <= Math.min(lastRow, 10); r++) + { + Row row = sheet.getRow(r); + if (row == null) + { + continue; + } + for (int c = 0; c < row.getLastCellNum(); c++) + { + if (cellText(row.getCell(c)).contains("\u90ae\u7f16\u5f00\u5934") + || cellText(row.getCell(c)).contains("\u7a7a\u6c14\u51cf\u6392")) + { + headerRow = r; + break; + } + } + if (headerRow >= 0) + { + break; + } + } + if (headerRow < 0) + { + return map; + } + String lastRm = ""; + for (int r = headerRow + 1; r <= lastRow; r++) + { + Row row = sheet.getRow(r); + if (row == null) + { + continue; + } + String rmName = normalizeCell(cellText(row.getCell(0))); + if (StringUtils.isNotEmpty(rmName)) + { + lastRm = rmName; + } + String rmPc = normalizePostcode(cellText(row.getCell(1))); + if (StringUtils.isNotEmpty(lastRm) && StringUtils.isNotEmpty(rmPc)) + { + map.computeIfAbsent(lastRm, k -> new ArrayList<>()).add(rmPc); + } + String evName = normalizeCell(cellText(row.getCell(5))); + if (StringUtils.isEmpty(evName)) + { + evName = normalizeCell(cellText(row.getCell(4))); + } + String evPc = normalizePostcode(cellText(row.getCell(6))); + if (StringUtils.isNotEmpty(evName) && StringUtils.isNotEmpty(evPc)) + { + map.computeIfAbsent(evName, k -> new ArrayList<>()).add(evPc); + } + String airName = normalizeCell(cellText(row.getCell(8))); + String airPc = normalizePostcode(cellText(row.getCell(9))); + if (StringUtils.isNotEmpty(airName) && StringUtils.isNotEmpty(airPc)) + { + map.computeIfAbsent(airName, k -> new ArrayList<>()).add(airPc); + } + } + return map; + } + + private Map> parseTwoColZoneMap(Sheet sheet, boolean requireExactPostcode) { Map> map = new LinkedHashMap<>(); int postcodeCol = -1; @@ -371,7 +736,7 @@ public class PrcZoneServiceImpl implements IPrcZoneService for (int c = 0; c < row.getLastCellNum(); c++) { String val = cellText(row.getCell(c)); - if (COL_POSTCODE.equals(val)) + if (COL_POSTCODE.equals(val) || (!requireExactPostcode && val.contains(COL_POSTCODE))) { postcodeCol = c; headerRow = r; @@ -391,7 +756,13 @@ public class PrcZoneServiceImpl implements IPrcZoneService { return map; } + fillTwoCol(map, sheet, headerRow, postcodeCol, zoneCol); + return map; + } + private void fillTwoCol(Map> map, Sheet sheet, int headerRow, int postcodeCol, int zoneCol) + { + int lastRow = sheet.getLastRowNum(); for (int r = headerRow + 1; r <= lastRow; r++) { Row row = sheet.getRow(r); @@ -399,19 +770,36 @@ public class PrcZoneServiceImpl implements IPrcZoneService { continue; } - String postcode = cellText(row.getCell(postcodeCol)); - String zoneCode = cellText(row.getCell(zoneCol)); + String postcode = normalizePostcode(cellText(row.getCell(postcodeCol))); + String zoneCode = normalizeCell(cellText(row.getCell(zoneCol))); if (StringUtils.isEmpty(postcode) || StringUtils.isEmpty(zoneCode)) { continue; } - if (postcode.endsWith(".0") && postcode.matches("\\d+\\.0")) - { - postcode = postcode.substring(0, postcode.length() - 2); - } map.computeIfAbsent(zoneCode, k -> new ArrayList<>()).add(postcode); } - return map; + } + + private static String normalizePostcode(String postcode) + { + if (StringUtils.isEmpty(postcode)) + { + return postcode; + } + if (postcode.endsWith(".0") && postcode.matches("\\d+\\.0")) + { + return postcode.substring(0, postcode.length() - 2); + } + return postcode; + } + + private static String normalizeCell(String text) + { + if (text == null) + { + return ""; + } + return text.replace('\r', ' ').replace('\n', ' ').replaceAll("\\s+", " ").trim(); } private String cellText(Cell cell) @@ -480,6 +868,78 @@ public class PrcZoneServiceImpl implements IPrcZoneService return (countryName == null ? "" : countryName) + "(" + zoneCode + ")"; } + /** + * Map Excel/local zone labels to English abbreviations for zone_code. + */ + private static String toZoneCodeAbbrev(String raw) + { + if (StringUtils.isEmpty(raw)) + { + return raw; + } + String key = raw.replace('\r', ' ').replace('\n', ' ').replaceAll("\\s+", " ").trim(); + switch (key) + { + case "1\u533a": + return "Z1"; + case "2\u533a": + return "Z2"; + case "3\u533a": + return "Z3"; + case "4\u533a": + return "Z4"; + case "5\u533a": + return "Z5"; + case "Mondial Relay": + return "MR"; + case "\u6c96\u7e04\u770c": + return "OKN"; + case "\u5317\u7231\u5c14\u5170": + return "NI"; + case "\u9ad8\u5730\u548c\u5c9b\u5c7f": + return "HI"; + case "\u6d77\u5ce1\u7fa4\u5c9b(\u542b\u9521\u5229\u5c9b)": + return "CI"; + case "\u9a6c\u6069\u5c9b": + return "IM"; + case "Beckton": + return "BEC"; + case "Bristol": + return "BRS"; + case "Cambridge": + return "CAM"; + case "Collier Wood": + return "CW"; + case "Guernsey": + return "GG"; + case "Isle of Man Bailiwick of Guernsey States of Jersey": + return "IM_GG_JE"; + case "Lakeside": + return "LKS"; + case "London Congestion Zone": + return "LCZ"; + case "London Congestion Zone - Gemini": + return "LCZ_GEM"; + case "London ULEZ - Wimbledon": + return "ULEZ_WIM"; + case "Northern Ireland Isles of Scilly": + return "NI_SCILLY"; + case "Old Kent Road Cargo Bikes": + return "OKR_CB"; + case "Oxford": + return "OXF"; + case "The Highlands Islands of Scotland": + return "HIGHLANDS"; + default: + // already abbrev or unknown: keep trimmed original (spaces -> underscore) + if (key.matches(".*[\\u4e00-\\u9fff].*")) + { + return key; + } + return key.replace(' ', '_').toUpperCase(Locale.ROOT).replaceAll("_+", "_"); + } + } + private static int extractZoneSort(String zoneCode) { if (StringUtils.isEmpty(zoneCode)) diff --git a/bms-front/src/views/price/basic/zone/index.vue b/bms-front/src/views/price/basic/zone/index.vue index c1e519bf..6983f725 100644 --- a/bms-front/src/views/price/basic/zone/index.vue +++ b/bms-front/src/views/price/basic/zone/index.vue @@ -5,7 +5,7 @@ :closable="false" show-icon class="mb16" - title="国家来自 bms_region;点击国家查看分区,点击「分区点击」查看邮编明细。导入 Excel 时需 sheet 名以「分区」结尾且能匹配国家表。" + title="国家来自 bms_region。支持导入:澳大利亚分区、加拿大偏远附加费、法国MR邮编、日本附加费、英国附加费。" /> {{ selectedCountry.countryNameCn }} ({{ selectedCountry.countryCode2 }}) - + - + - + @@ -163,7 +163,6 @@ -