增加分区修改

This commit is contained in:
wuweihua
2026-09-15 11:23:39 +08:00
parent c28b5a2b6e
commit 76208604bb
3 changed files with 495 additions and 36 deletions
@@ -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}
@@ -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<Sheet> zoneSheets = findZoneSheets(workbook);
if (zoneSheets.isEmpty())
List<SheetImport> 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<String, List<String>> zonePostcodes = item.zonePostcodes;
if (zonePostcodes == null || zonePostcodes.isEmpty())
{
continue;
}
Map<String, List<String>> 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<String, List<String>> 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<Sheet> findZoneSheets(Workbook workbook)
private static final class SheetImport
{
List<Sheet> list = new ArrayList<>();
private final String sheetName;
private final String countryHint;
private final Map<String, List<String>> zonePostcodes;
private SheetImport(String sheetName, String countryHint, Map<String, List<String>> zonePostcodes)
{
this.sheetName = sheetName;
this.countryHint = countryHint;
this.zonePostcodes = zonePostcodes;
}
}
private List<SheetImport> findZoneImports(Workbook workbook)
{
List<SheetImport> 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<String, List<String>> 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<String, List<String>> 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<Sheet> findZoneSheets(Workbook workbook)
{
return new ArrayList<>();
}
private String parseCountryNameFromSheet(String sheetName)
{
return guessCountryFromSheetName(sheetName);
}
/** Australia style: postcode + zone */
private Map<String, List<String>> parseZonePostcodes(Sheet sheet)
{
return parseTwoColZoneMap(sheet, true);
}
/** Canada style: remote postcode + remote zone */
private Map<String, List<String>> parseRemoteZonePostcodes(Sheet sheet)
{
Map<String, List<String>> 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<String, List<String>> parseProvincePostcodes(Sheet sheet)
{
Map<String, List<String>> 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<String, List<String>> parseCoveragePostcodes(Sheet sheet)
{
Map<String, List<String>> 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<String, List<String>> parseUkSurchargePostcodes(Sheet sheet)
{
Map<String, List<String>> 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<String, List<String>> parseTwoColZoneMap(Sheet sheet, boolean requireExactPostcode)
{
Map<String, List<String>> 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<String, List<String>> 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))
@@ -5,7 +5,7 @@
:closable="false"
show-icon
class="mb16"
title="国家来自 bms_region;点击国家查看分区,点击「分区点击」查看邮编明细。导入 Excel 时需 sheet 名以「分区」结尾且能匹配国家表。"
title="国家来自 bms_region。支持导入:澳大利亚分区、加拿大偏远附加费、法国MR邮编、日本附加费、英国附加费。"
/>
<basic-table
@@ -116,13 +116,13 @@
<span>{{ selectedCountry.countryNameCn }} ({{ selectedCountry.countryCode2 }})</span>
</el-form-item>
<el-form-item label="国家编码" prop="countryCode">
<el-input v-model="zoneForm.countryCode" placeholder="ISO2,如 AU" maxlength="10" style="text-transform: uppercase" />
<el-input v-model="zoneForm.countryCode" disabled placeholder="ISO2,如 AU" maxlength="10" style="text-transform: uppercase" />
</el-form-item>
<el-form-item label="分区代码" prop="zoneCode">
<el-input v-model="zoneForm.zoneCode" placeholder="如 1区" maxlength="50" />
<el-input v-model="zoneForm.zoneCode" placeholder="如 Z1" maxlength="50" />
</el-form-item>
<el-form-item label="显示名" prop="zoneName">
<el-input v-model="zoneForm.zoneName" placeholder="如 澳大利亚(1区)" maxlength="100" />
<el-input v-model="zoneForm.zoneName" placeholder="如 澳大利亚(Z1)" maxlength="100" />
</el-form-item>
<el-form-item label="排序号" prop="zoneSort">
<el-input-number v-model="zoneForm.zoneSort" :min="0" :max="9999" controls-position="right" />
@@ -163,7 +163,6 @@
</div>
<el-table v-loading="detailLoading" :data="detailList" border row-key="detailId" empty-text="暂无邮编">
<el-table-column label="ID" prop="detailId" align="center" width="90" />
<el-table-column label="邮编" prop="postcode" align="center" min-width="140" />
<el-table-column label="操作" align="center" width="160">
<template #default="{ row }">