套餐加分区功能

This commit is contained in:
wuweihua
2026-09-15 14:10:44 +08:00
parent 76208604bb
commit 7679a7d091
26 changed files with 1279 additions and 597 deletions
@@ -70,6 +70,7 @@ price.set.detail.country.empty=固定价明细必须填写国家
price.set.detail.weight.empty=固定价明细必须填写重量段
price.set.detail.freight.empty=固定价明细必须填写运费单价
price.set.detail.registration.empty=固定价明细必须填写挂号费
price.set.detail.zone.empty=固定价明细必须选择分区
price.set.detail.roundMode.invalid=取值方式仅支持 UP / DOWN / HALF_UP
price.set.customer.notExists=客户绑定不存在
price.set.customer.unbindAudit=审核中的套餐不允许解绑客户
@@ -18,6 +18,12 @@ public class BmsProductCountry implements Serializable
private String productCode;
private String countryName;
private String countryCode;
/** prc_zone.zone_id; null when product country has no zone */
private Long zoneId;
/** e.g. Z1; null when no zone */
private String zoneCode;
/** e.g. Australia(Z1); null when no zone */
private String zoneName;
private BigDecimal weightMin;
private BigDecimal weightMax;
private BigDecimal freightPrice;
@@ -18,6 +18,12 @@ public class PrcPriceSetDetail implements Serializable
private String productName;
private String countryCode;
private String countryName;
/** prc_zone.zone_id; null when country has no zone */
private Long zoneId;
/** e.g. Z1; null when no zone */
private String zoneCode;
/** zone display name; null when no zone */
private String zoneName;
private Long zdId;
private Long wsdId;
private String weightLabel;
@@ -27,12 +27,13 @@ public interface PrcPriceSetDetailMapper
public int countByPriceSetId(Long priceSetId);
/** FIXED:按产品+国家+关联产品国家明细/重量段命中 */
/** FIXED: product + country + zone + productCountryId / weight */
public PrcPriceSetDetail selectFixedMatch(
@Param("priceSetId") Long priceSetId,
@Param("productCode") String productCode,
@Param("countryCode") String countryCode,
@Param("countryName") String countryName,
@Param("zoneCode") String zoneCode,
@Param("productCountryId") Long productCountryId,
@Param("weightLabel") String weightLabel);
@@ -34,10 +34,13 @@ import com.ruoyi.common.utils.MessageUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.price.domain.BmsProduct;
import com.ruoyi.price.domain.BmsProductCountry;
import com.ruoyi.price.domain.PrcZone;
import com.ruoyi.price.mapper.BmsProductCountryMapper;
import com.ruoyi.price.mapper.BmsProductMapper;
import com.ruoyi.price.mapper.PrcZoneMapper;
import com.ruoyi.price.service.IBmsProductService;
import com.ruoyi.price.service.IPrcSurchargeRelService;
import com.ruoyi.price.util.ZoneCodeAbbrevs;
import com.ruoyi.system.domain.BmsRegion;
import com.ruoyi.system.mapper.BmsRegionMapper;
@@ -74,6 +77,9 @@ public class BmsProductServiceImpl implements IBmsProductService
@Autowired
private BmsRegionMapper regionMapper;
@Autowired
private PrcZoneMapper zoneMapper;
@Autowired
private IPrcSurchargeRelService surchargeRelService;
@@ -248,6 +254,7 @@ public class BmsProductServiceImpl implements IBmsProductService
throw new ServiceException(MessageUtils.message("price.product.import.regionEmpty"));
}
List<BmsRegion> euRegions = loadEuRegions(regionByNameCn);
Map<String, PrcZone> zoneByCountryAndCode = loadZoneByCountryAndCode();
int successNum = 0;
int skipNum = 0;
@@ -263,7 +270,7 @@ public class BmsProductServiceImpl implements IBmsProductService
try
{
int[] nums = importOneProduct(workbook, product, operName, regionByNameCn, euRegions,
unmatchedCountries);
zoneByCountryAndCode, unmatchedCountries);
transactionManager.commit(tx);
successNum += nums[0];
skipNum += nums[1];
@@ -300,7 +307,8 @@ public class BmsProductServiceImpl implements IBmsProductService
/** @return int[]{added, skipped, countryRows} */
private int[] importOneProduct(Workbook workbook, BmsProduct product, String operName,
Map<String, BmsRegion> regionByNameCn, List<BmsRegion> euRegions, Set<String> unmatchedCountries)
Map<String, BmsRegion> regionByNameCn, List<BmsRegion> euRegions,
Map<String, PrcZone> zoneByCountryAndCode, Set<String> unmatchedCountries)
{
product.setSourceSheet(product.getProductName());
product.setCreateBy(operName);
@@ -314,7 +322,7 @@ public class BmsProductServiceImpl implements IBmsProductService
}
product.setSourceSheet(productSheet.getSheetName());
List<BmsProductCountry> countries = parseProductSheet(productSheet, product,
regionByNameCn, euRegions, unmatchedCountries);
regionByNameCn, euRegions, zoneByCountryAndCode, unmatchedCountries);
if (countries == null || countries.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.product.import.noCountry",
@@ -412,7 +420,8 @@ public class BmsProductServiceImpl implements IBmsProductService
{
throw new ServiceException(MessageUtils.message("price.product.import.needFeeAlter"));
}
if (tip.contains("expire_date") || tip.contains("uk_product_country_weight"))
if (tip.contains("expire_date") || tip.contains("uk_product_country_weight")
|| tip.contains("uk_product_country_zone_weight"))
{
throw new ServiceException(MessageUtils.message("price.product.import.needExpireAlter"));
}
@@ -716,7 +725,8 @@ public class BmsProductServiceImpl implements IBmsProductService
}
private List<BmsProductCountry> parseProductSheet(Sheet sheet, BmsProduct product,
Map<String, BmsRegion> regionByNameCn, List<BmsRegion> euRegions, Set<String> unmatchedCountries)
Map<String, BmsRegion> regionByNameCn, List<BmsRegion> euRegions,
Map<String, PrcZone> zoneByCountryAndCode, Set<String> unmatchedCountries)
{
if (sheet == null)
{
@@ -739,7 +749,6 @@ public class BmsProductServiceImpl implements IBmsProductService
int colSize = findColByHeader(headerRow, "\u5c3a\u5bf8", colCountry + 1);
int colLastMile = findColByHeader(headerRow, "\u5c3e\u7a0b", colCountry + 1);
int colLeadTime = findColByHeader(headerRow, "\u65f6\u6548", colCountry + 1);
// Fallback offsets when header text missing (Weishi layout: B=country ... H=lead time)
if (colWeight < 0)
{
colWeight = colCountry + 1;
@@ -765,7 +774,7 @@ public class BmsProductServiceImpl implements IBmsProductService
colLeadTime = colCountry + 6;
}
String currentCountry = null;
String currentCountryRaw = null;
String currentSize = null;
String currentLastMile = null;
String currentLeadTime = null;
@@ -785,12 +794,12 @@ public class BmsProductServiceImpl implements IBmsProductService
}
if (StringUtils.isNotEmpty(countryCell))
{
currentCountry = normalizeCountryName(countryCell);
currentCountryRaw = countryCell;
currentSize = emptyToNull(cellString(row.getCell(colSize)));
currentLastMile = emptyToNull(cellString(row.getCell(colLastMile)));
currentLeadTime = emptyToNull(cellString(row.getCell(colLeadTime)));
}
if (StringUtils.isEmpty(currentCountry))
if (StringUtils.isEmpty(currentCountryRaw))
{
continue;
}
@@ -803,14 +812,23 @@ public class BmsProductServiceImpl implements IBmsProductService
BigDecimal registration = cellDecimal(row.getCell(colRegister));
Date eff = sheetEffective != null ? sheetEffective : product.getEffectiveDate();
BmsRegion region = resolveRegion(currentCountry, regionByNameCn);
ZoneCodeAbbrevs.CountryZoneParts parts = ZoneCodeAbbrevs.splitCountryZone(currentCountryRaw);
if (parts == null || StringUtils.isEmpty(parts.countryName))
{
continue;
}
String baseCountry = parts.countryName;
String zoneLabel = parts.zoneLabel;
BmsRegion region = resolveRegion(baseCountry, regionByNameCn);
if (region != null)
{
list.add(buildCountryRow(region.getCountryNameCn(), region.getCountryCode2(),
zoneLabel, zoneByCountryAndCode,
range, freight, registration, currentSize, currentLastMile, currentLeadTime, eff));
continue;
}
if (isEuZoneName(currentCountry))
if (isEuZoneName(baseCountry) && StringUtils.isEmpty(zoneLabel))
{
if (euRegions == null || euRegions.isEmpty())
{
@@ -819,23 +837,26 @@ public class BmsProductServiceImpl implements IBmsProductService
for (BmsRegion eu : euRegions)
{
list.add(buildCountryRow(eu.getCountryNameCn(), eu.getCountryCode2(),
null, zoneByCountryAndCode,
range, freight, registration, currentSize, currentLastMile, currentLeadTime, eff));
}
continue;
}
unmatchedCountries.add(currentCountry);
list.add(buildCountryRow(currentCountry, null,
unmatchedCountries.add(currentCountryRaw.trim().replace("\n", " ").replace("\r", " "));
list.add(buildCountryRow(baseCountry, null, zoneLabel, zoneByCountryAndCode,
range, freight, registration, currentSize, currentLastMile, currentLeadTime, eff));
}
return list;
}
private BmsProductCountry buildCountryRow(String countryName, String countryCode, BigDecimal[] range,
private BmsProductCountry buildCountryRow(String countryName, String countryCode, String zoneLabel,
Map<String, PrcZone> zoneByCountryAndCode, BigDecimal[] range,
BigDecimal freight, BigDecimal registration, String sizeLimit, String lastMile, String leadTime, Date eff)
{
BmsProductCountry c = new BmsProductCountry();
c.setCountryName(countryName);
c.setCountryCode(countryCode);
applyZoneFields(c, countryCode, zoneLabel, zoneByCountryAndCode);
c.setWeightMin(range[0]);
c.setWeightMax(range[1]);
c.setFreightPrice(freight);
@@ -847,6 +868,68 @@ public class BmsProductServiceImpl implements IBmsProductService
return c;
}
private void applyZoneFields(BmsProductCountry c, String countryCode, String zoneLabel,
Map<String, PrcZone> zoneByCountryAndCode)
{
if (StringUtils.isEmpty(zoneLabel))
{
c.setZoneId(null);
c.setZoneCode(null);
c.setZoneName(null);
return;
}
String zoneCode = ZoneCodeAbbrevs.toAbbrev(zoneLabel);
c.setZoneCode(zoneCode);
PrcZone zone = null;
if (StringUtils.isNotEmpty(countryCode) && zoneByCountryAndCode != null)
{
zone = zoneByCountryAndCode.get(zoneKey(countryCode, zoneCode));
}
if (zone != null)
{
c.setZoneId(zone.getZoneId());
c.setZoneCode(zone.getZoneCode());
c.setZoneName(zone.getZoneName());
}
else
{
c.setZoneId(null);
c.setZoneName(StringUtils.isNotEmpty(c.getCountryName())
? c.getCountryName() + "(" + zoneCode + ")"
: zoneCode);
}
}
private Map<String, PrcZone> loadZoneByCountryAndCode()
{
Map<String, PrcZone> map = new HashMap<>();
List<PrcZone> list = zoneMapper.selectPrcZoneList(new PrcZone());
if (list == null)
{
return map;
}
for (PrcZone zone : list)
{
if (zone == null || StringUtils.isEmpty(zone.getZoneCode()))
{
continue;
}
String code2 = zone.getCountryCode();
if (StringUtils.isEmpty(code2))
{
continue;
}
map.put(zoneKey(code2, zone.getZoneCode()), zone);
}
return map;
}
private static String zoneKey(String countryCode, String zoneCode)
{
return String.valueOf(countryCode).trim().toUpperCase(Locale.ROOT)
+ "|" + String.valueOf(zoneCode).trim().toUpperCase(Locale.ROOT);
}
/**
* Load bms_region map: country_name_cn -> region (for country_code_2).
*/
@@ -907,7 +990,6 @@ public class BmsProductServiceImpl implements IBmsProductService
{
return candidates.get(0);
}
// Prefer longest Chinese name match (e.g. 捷克共和国 over 捷克)
BmsRegion best = candidates.get(0);
for (int i = 1; i < candidates.size(); i++)
{
@@ -920,29 +1002,6 @@ public class BmsProductServiceImpl implements IBmsProductService
return best;
}
/** Strip Excel footnotes like "澳大利亚(1区)\\n分区点击" */
private static String normalizeCountryName(String name)
{
if (name == null)
{
return null;
}
String n = name.trim();
int cut = n.length();
int nl = n.indexOf('\n');
if (nl >= 0 && nl < cut)
{
cut = nl;
}
int cr = n.indexOf('\r');
if (cr >= 0 && cr < cut)
{
cut = cr;
}
n = n.substring(0, cut).trim();
return n.isEmpty() ? null : n;
}
private BigDecimal[] parseWeightRange(String label)
{
if (StringUtils.isEmpty(label))
@@ -248,6 +248,9 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
nd.setProductName(d.getProductName());
nd.setCountryCode(d.getCountryCode());
nd.setCountryName(d.getCountryName());
nd.setZoneId(d.getZoneId());
nd.setZoneCode(d.getZoneCode());
nd.setZoneName(d.getZoneName());
nd.setZdId(d.getZdId());
nd.setWsdId(d.getWsdId());
nd.setWeightLabel(d.getWeightLabel());
@@ -659,6 +662,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
{
throw new ServiceException(MessageUtils.message("price.set.detail.country.empty"));
}
requireZoneIfNeeded(detail);
if (detail.getWsdId() == null && StringUtils.isEmpty(detail.getWeightLabel()))
{
throw new ServiceException(MessageUtils.message("price.set.detail.weight.empty"));
@@ -692,6 +696,29 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
}
}
/**
* When product country rows for this country have zones, zone is required.
*/
private void requireZoneIfNeeded(PrcPriceSetDetail detail)
{
if (detail.getZoneId() != null || StringUtils.isNotEmpty(detail.getZoneCode()))
{
return;
}
List<BmsProductCountry> rows = listOpenProductCountries(detail.getProductCode());
for (BmsProductCountry row : rows)
{
if (!matchDetailCountryOnly(detail, row))
{
continue;
}
if (row.getZoneId() != null || StringUtils.isNotEmpty(row.getZoneCode()))
{
throw new ServiceException(MessageUtils.message("price.set.detail.zone.empty"));
}
}
}
/**
* Resolve / verify product_country_id from bms_product_country when missing.
*/
@@ -699,25 +726,16 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
{
if (detail.getProductCountryId() != null)
{
syncZoneFromProductCountry(detail);
return;
}
if (StringUtils.isEmpty(detail.getProductCode()) || StringUtils.isEmpty(detail.getWeightLabel()))
{
return;
}
BmsProductCountry query = new BmsProductCountry();
query.setProductCode(detail.getProductCode());
List<BmsProductCountry> rows = productCountryMapper.selectBmsProductCountryList(query);
if (rows == null || rows.isEmpty())
{
return;
}
List<BmsProductCountry> rows = listOpenProductCountries(detail.getProductCode());
for (BmsProductCountry row : rows)
{
if (row.getExpireDate() != null)
{
continue;
}
if (!matchDetailCountry(detail, row))
{
continue;
@@ -730,12 +748,80 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
&& detail.getWeightLabel().contains(stripTrailingZeros(row.getWeightMax()))))
{
detail.setProductCountryId(row.getDetailId());
applyZoneFromRow(detail, row);
return;
}
}
}
private static boolean matchDetailCountry(PrcPriceSetDetail detail, BmsProductCountry row)
private void syncZoneFromProductCountry(PrcPriceSetDetail detail)
{
if (detail.getProductCountryId() == null)
{
return;
}
if (detail.getZoneId() != null || StringUtils.isNotEmpty(detail.getZoneCode()))
{
return;
}
BmsProductCountry query = new BmsProductCountry();
query.setProductCode(detail.getProductCode());
List<BmsProductCountry> rows = productCountryMapper.selectBmsProductCountryList(query);
if (rows == null)
{
return;
}
for (BmsProductCountry row : rows)
{
if (detail.getProductCountryId().equals(row.getDetailId()))
{
applyZoneFromRow(detail, row);
return;
}
}
}
private List<BmsProductCountry> listOpenProductCountries(String productCode)
{
if (StringUtils.isEmpty(productCode))
{
return java.util.Collections.emptyList();
}
BmsProductCountry query = new BmsProductCountry();
query.setProductCode(productCode);
List<BmsProductCountry> rows = productCountryMapper.selectBmsProductCountryList(query);
if (rows == null || rows.isEmpty())
{
return java.util.Collections.emptyList();
}
List<BmsProductCountry> open = new ArrayList<BmsProductCountry>();
for (BmsProductCountry row : rows)
{
if (row.getExpireDate() == null)
{
open.add(row);
}
}
return open;
}
private static void applyZoneFromRow(PrcPriceSetDetail detail, BmsProductCountry row)
{
if (detail.getZoneId() == null)
{
detail.setZoneId(row.getZoneId());
}
if (StringUtils.isEmpty(detail.getZoneCode()))
{
detail.setZoneCode(row.getZoneCode());
}
if (StringUtils.isEmpty(detail.getZoneName()))
{
detail.setZoneName(row.getZoneName());
}
}
private static boolean matchDetailCountryOnly(PrcPriceSetDetail detail, BmsProductCountry row)
{
if (StringUtils.isNotEmpty(detail.getCountryCode()) && StringUtils.isNotEmpty(row.getCountryCode()))
{
@@ -745,6 +831,38 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
&& detail.getCountryName().equals(row.getCountryName());
}
private static boolean matchDetailCountry(PrcPriceSetDetail detail, BmsProductCountry row)
{
if (!matchDetailCountryOnly(detail, row))
{
return false;
}
return matchDetailZone(detail, row);
}
private static boolean matchDetailZone(PrcPriceSetDetail detail, BmsProductCountry row)
{
boolean detailHas = detail.getZoneId() != null || StringUtils.isNotEmpty(detail.getZoneCode());
boolean rowHas = row.getZoneId() != null || StringUtils.isNotEmpty(row.getZoneCode());
if (!detailHas && !rowHas)
{
return true;
}
if (detailHas != rowHas)
{
return false;
}
if (detail.getZoneId() != null && row.getZoneId() != null)
{
return detail.getZoneId().equals(row.getZoneId());
}
if (StringUtils.isNotEmpty(detail.getZoneCode()) && StringUtils.isNotEmpty(row.getZoneCode()))
{
return detail.getZoneCode().equalsIgnoreCase(row.getZoneCode());
}
return false;
}
private static String formatWeightLabel(BigDecimal min, BigDecimal max)
{
return stripTrailingZeros(min) + "<W\u2264" + stripTrailingZeros(max);
@@ -31,6 +31,7 @@ import com.ruoyi.price.domain.PrcZoneRegion;
import com.ruoyi.price.mapper.PrcZoneDetailMapper;
import com.ruoyi.price.mapper.PrcZoneMapper;
import com.ruoyi.price.service.IPrcZoneService;
import com.ruoyi.price.util.ZoneCodeAbbrevs;
import com.ruoyi.system.domain.BmsRegion;
import com.ruoyi.system.mapper.BmsRegionMapper;
@@ -130,7 +131,7 @@ public class PrcZoneServiceImpl implements IPrcZoneService
}
if (StringUtils.isNotEmpty(zone.getZoneCode()))
{
zone.setZoneCode(toZoneCodeAbbrev(zone.getZoneCode()));
zone.setZoneCode(ZoneCodeAbbrevs.toAbbrev(zone.getZoneCode()));
}
if (StringUtils.isEmpty(zone.getZoneName()))
{
@@ -156,7 +157,7 @@ public class PrcZoneServiceImpl implements IPrcZoneService
}
if (StringUtils.isNotEmpty(zone.getZoneCode()))
{
zone.setZoneCode(toZoneCodeAbbrev(zone.getZoneCode()));
zone.setZoneCode(ZoneCodeAbbrevs.toAbbrev(zone.getZoneCode()));
}
if (zone.getZoneSort() == null && StringUtils.isNotEmpty(zone.getZoneCode()))
{
@@ -293,7 +294,7 @@ public class PrcZoneServiceImpl implements IPrcZoneService
String countryName = region.getCountryNameCn();
for (Map.Entry<String, List<String>> entry : entries)
{
String zoneCode = toZoneCodeAbbrev(entry.getKey());
String zoneCode = ZoneCodeAbbrevs.toAbbrev(entry.getKey());
PrcZone zone = new PrcZone();
zone.setRegionId(regionId);
zone.setCountryCode(region.getCountryCode2());
@@ -868,78 +869,6 @@ 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))
@@ -248,7 +248,8 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
PrcPriceSetDetail detail = detailMapper.selectFixedMatch(
set.getPriceSetId(), productCode,
base.getCountryCode(), base.getCountryName(), base.getDetailId(),
base.getCountryCode(), base.getCountryName(), base.getZoneCode(),
base.getDetailId(),
formatWeightLabel(base.getWeightMin(), base.getWeightMax()));
if (detail == null)
{
@@ -0,0 +1,144 @@
package com.ruoyi.price.util;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.ruoyi.common.utils.StringUtils;
/**
* Parse Excel country cell with optional zone suffix, map labels to English abbreviations.
*/
public final class ZoneCodeAbbrevs
{
// fullwidth () and ASCII ()
private static final Pattern ZONE_SUFFIX = Pattern.compile(
"^(.+?)[\\(\\uFF08]([^\\)\\uFF09]+)[\\)\\uFF09]\\s*$");
private ZoneCodeAbbrevs()
{
}
public static final class CountryZoneParts
{
public final String countryName;
public final String zoneLabel;
public CountryZoneParts(String countryName, String zoneLabel)
{
this.countryName = countryName;
this.zoneLabel = zoneLabel;
}
}
/**
* Split country cell like Country(zoneLabel); no parenthesis -> zoneLabel null.
*/
public static CountryZoneParts splitCountryZone(String raw)
{
if (StringUtils.isEmpty(raw))
{
return null;
}
String n = raw.trim();
int cut = n.length();
int nl = n.indexOf('\n');
if (nl >= 0 && nl < cut)
{
cut = nl;
}
int cr = n.indexOf('\r');
if (cr >= 0 && cr < cut)
{
cut = cr;
}
n = n.substring(0, cut).trim();
if (n.isEmpty())
{
return null;
}
Matcher m = ZONE_SUFFIX.matcher(n);
if (m.matches())
{
String country = m.group(1).trim();
String zone = m.group(2).trim();
if (StringUtils.isEmpty(country))
{
return new CountryZoneParts(n, null);
}
return new CountryZoneParts(country, StringUtils.isEmpty(zone) ? null : zone);
}
return new CountryZoneParts(n, null);
}
public static String toAbbrev(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:
if (key.matches("(?i)Z[1-9]\\d*"))
{
return key.toUpperCase(Locale.ROOT);
}
if (key.matches(".*[\\u4e00-\\u9fff].*"))
{
return key;
}
return key.replace(' ', '_').toUpperCase(Locale.ROOT).replaceAll("_+", "_");
}
}
}
@@ -10,6 +10,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="productCode" column="product_code" />
<result property="countryName" column="country_name" />
<result property="countryCode" column="country_code" />
<result property="zoneId" column="zone_id" />
<result property="zoneCode" column="zone_code" />
<result property="zoneName" column="zone_name" />
<result property="weightMin" column="weight_min" />
<result property="weightMax" column="weight_max" />
<result property="freightPrice" column="freight_price" />
@@ -26,7 +29,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</resultMap>
<sql id="selectVo">
select detail_id, product_id, product_code, country_name, country_code, weight_min, weight_max,
select detail_id, product_id, product_code, country_name, country_code, zone_id, zone_code, zone_name,
weight_min, weight_max,
freight_price, registration_fee, size_limit, last_mile, lead_time,
effective_date, expire_date, status, create_time, update_time
from bms_product_country
@@ -47,6 +51,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="countryCode != null and countryCode != ''">
AND country_code = #{countryCode}
</if>
<if test="zoneId != null">
AND zone_id = #{zoneId}
</if>
<if test="zoneCode != null and zoneCode != ''">
AND zone_code = #{zoneCode}
</if>
<if test="weightMin != null">
AND weight_min = #{weightMin}
</if>
@@ -66,16 +76,18 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
AND status = #{effectStatus}
</if>
</where>
order by effective_date desc, country_name asc, weight_min asc, detail_id asc
order by effective_date desc, country_name asc, zone_code asc, weight_min asc, detail_id asc
</select>
<insert id="insertBmsProductCountry" parameterType="BmsProductCountry" useGeneratedKeys="true" keyProperty="detailId">
insert into bms_product_country (
product_id, product_code, country_name, country_code, weight_min, weight_max,
product_id, product_code, country_name, country_code, zone_id, zone_code, zone_name,
weight_min, weight_max,
freight_price, registration_fee, size_limit, last_mile, lead_time,
effective_date, expire_date, status, create_time
) values (
#{productId}, #{productCode}, #{countryName}, #{countryCode}, #{weightMin}, #{weightMax},
#{productId}, #{productCode}, #{countryName}, #{countryCode}, #{zoneId}, #{zoneCode}, #{zoneName},
#{weightMin}, #{weightMax},
#{freightPrice}, #{registrationFee}, #{sizeLimit}, #{lastMile}, #{leadTime},
#{effectiveDate}, #{expireDate}, IFNULL(#{status}, '0'), sysdate()
)
@@ -162,7 +174,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="bizDate == null">
and expire_date is null
</if>
order by effective_date desc, weight_min asc, detail_id asc
order by
case when zone_code is null or zone_code = '' then 0 else 1 end,
effective_date desc, weight_min asc, detail_id asc
limit 1
</select>
@@ -11,6 +11,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="productName" column="product_name" />
<result property="countryCode" column="country_code" />
<result property="countryName" column="country_name" />
<result property="zoneId" column="zone_id" />
<result property="zoneCode" column="zone_code" />
<result property="zoneName" column="zone_name" />
<result property="zdId" column="zd_id" />
<result property="wsdId" column="wsd_id" />
<result property="weightLabel" column="weight_label" />
@@ -26,6 +29,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<sql id="selectPrcPriceSetDetailVo">
select psd_id, price_set_id, product_code, product_name, country_code, country_name,
zone_id, zone_code, zone_name,
zd_id, wsd_id, weight_label, product_country_id, fv_value, fv_registration_fee,
discount_value, round_mode, pr_id, create_time, update_time
from prc_price_set_detail
@@ -48,6 +52,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="countryCode != null and countryCode != ''">
AND country_code = #{countryCode}
</if>
<if test="zoneCode != null and zoneCode != ''">
AND zone_code = #{zoneCode}
</if>
</where>
order by psd_id asc
</select>
@@ -67,6 +74,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="productName != null">product_name,</if>
<if test="countryCode != null">country_code,</if>
<if test="countryName != null">country_name,</if>
<if test="zoneId != null">zone_id,</if>
<if test="zoneCode != null">zone_code,</if>
<if test="zoneName != null">zone_name,</if>
<if test="zdId != null">zd_id,</if>
<if test="wsdId != null">wsd_id,</if>
<if test="weightLabel != null">weight_label,</if>
@@ -83,6 +93,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="productName != null">#{productName},</if>
<if test="countryCode != null">#{countryCode},</if>
<if test="countryName != null">#{countryName},</if>
<if test="zoneId != null">#{zoneId},</if>
<if test="zoneCode != null">#{zoneCode},</if>
<if test="zoneName != null">#{zoneName},</if>
<if test="zdId != null">#{zdId},</if>
<if test="wsdId != null">#{wsdId},</if>
<if test="weightLabel != null">#{weightLabel},</if>
@@ -103,6 +116,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="productName != null">product_name = #{productName},</if>
country_code = #{countryCode},
country_name = #{countryName},
zone_id = #{zoneId},
zone_code = #{zoneCode},
zone_name = #{zoneName},
zd_id = #{zdId},
wsd_id = #{wsdId},
weight_label = #{weightLabel},
@@ -149,6 +165,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
1 = 0
</otherwise>
</choose>
<if test="zoneCode != null and zoneCode != ''">
and (zone_code is null or upper(zone_code) = upper(#{zoneCode}))
</if>
<if test="weightLabel != null and weightLabel != ''">
and (weight_label is null or weight_label = #{weightLabel} or weight_label like concat('%', #{weightLabel}, '%'))
</if>
@@ -156,6 +175,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
)
order by
case when #{productCountryId} is not null and product_country_id = #{productCountryId} then 0 else 1 end,
case when #{zoneCode} is not null and upper(ifnull(zone_code,'')) = upper(#{zoneCode}) then 0 else 1 end,
case when zone_code is null then 1 else 0 end,
psd_id asc
limit 1
</select>
+6 -3
View File
@@ -38,9 +38,12 @@ CREATE TABLE IF NOT EXISTS prc_price_set_detail (
price_set_id BIGINT(20) NOT NULL COMMENT '价格套餐id',
product_code VARCHAR(10) NOT NULL COMMENT '产品代码',
product_name VARCHAR(50) DEFAULT NULL COMMENT '产品名称',
country_code CHAR(2) DEFAULT NULL COMMENT '国家二字码',
country_name VARCHAR(50) DEFAULT NULL COMMENT '国家名称',
zd_id BIGINT(20) DEFAULT NULL COMMENT '收费区划id',
country_code CHAR(2) DEFAULT NULL COMMENT 'country ISO2',
country_name VARCHAR(50) DEFAULT NULL COMMENT 'country name',
zone_id BIGINT(20) DEFAULT NULL COMMENT 'prc_zone.zone_id',
zone_code VARCHAR(50) DEFAULT NULL COMMENT 'zone code e.g. Z1',
zone_name VARCHAR(100) DEFAULT NULL COMMENT 'zone display name',
zd_id BIGINT(20) DEFAULT NULL COMMENT 'legacy zone division id',
wsd_id BIGINT(20) DEFAULT NULL COMMENT '重量段明细id',
weight_label VARCHAR(50) DEFAULT NULL COMMENT '重量段展示',
product_country_id BIGINT(20) DEFAULT NULL COMMENT '关联 bms_product_country.detail_id',
@@ -0,0 +1,55 @@
-- Add zone fields to prc_price_set_detail (nullable when country has no zone)
-- Idempotent: safe to re-run
-- charset: utf8mb4
SET NAMES utf8mb4;
SET @db := DATABASE();
-- zone_id
SET @has := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'prc_price_set_detail' AND COLUMN_NAME = 'zone_id'
);
SET @sql := IF(@has = 0,
'ALTER TABLE prc_price_set_detail ADD COLUMN zone_id BIGINT(20) DEFAULT NULL COMMENT ''prc_zone.zone_id'' AFTER country_name',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- zone_code
SET @has := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'prc_price_set_detail' AND COLUMN_NAME = 'zone_code'
);
SET @sql := IF(@has = 0,
'ALTER TABLE prc_price_set_detail ADD COLUMN zone_code VARCHAR(50) DEFAULT NULL COMMENT ''zone code e.g. Z1'' AFTER zone_id',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- zone_name
SET @has := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'prc_price_set_detail' AND COLUMN_NAME = 'zone_name'
);
SET @sql := IF(@has = 0,
'ALTER TABLE prc_price_set_detail ADD COLUMN zone_name VARCHAR(100) DEFAULT NULL COMMENT ''zone display name'' AFTER zone_code',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- index
SET @has := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'prc_price_set_detail' AND INDEX_NAME = 'idx_psd_zone'
);
SET @sql := IF(@has = 0,
'ALTER TABLE prc_price_set_detail ADD INDEX idx_psd_zone (zone_id)',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @has := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'prc_price_set_detail' AND INDEX_NAME = 'idx_psd_country_zone'
);
SET @sql := IF(@has = 0,
'ALTER TABLE prc_price_set_detail ADD INDEX idx_psd_country_zone (price_set_id, product_code, country_code, zone_code)',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+6 -1
View File
@@ -34,6 +34,9 @@ CREATE TABLE IF NOT EXISTS bms_product_country (
product_code varchar(20) NOT NULL COMMENT '产品代码',
country_name varchar(50) NOT NULL COMMENT '国家中文名',
country_code char(2) DEFAULT NULL COMMENT 'ISO2,可空待补齐',
zone_id bigint DEFAULT NULL COMMENT 'prc_zone.zone_id',
zone_code varchar(50) DEFAULT NULL COMMENT 'zone code e.g. Z1',
zone_name varchar(100) DEFAULT NULL COMMENT 'zone display name',
weight_min decimal(10,3) DEFAULT NULL COMMENT '重量段最小值KG(含)',
weight_max decimal(10,3) DEFAULT NULL COMMENT '重量段最大值KG(含)',
freight_price decimal(10,4) DEFAULT NULL COMMENT '运费 RMB/KG',
@@ -47,9 +50,11 @@ CREATE TABLE IF NOT EXISTS bms_product_country (
create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (detail_id),
UNIQUE KEY uk_product_country_weight_eff (product_code, country_name, weight_min, weight_max, effective_date),
UNIQUE KEY uk_product_country_zone_weight_eff (product_code, country_name, zone_code, weight_min, weight_max, effective_date),
KEY idx_product_country_pid (product_id),
KEY idx_country_code (country_code),
KEY idx_product_country_zone (zone_id),
KEY idx_product_country_zone_code (country_code, zone_code),
KEY idx_weight_match (product_code, country_code, weight_min, weight_max),
KEY idx_product_eff (product_code, effective_date, expire_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='产品国家明细-重量段+运费挂号费+尺寸/尾程/时效';
@@ -0,0 +1,74 @@
-- Add zone fields to bms_product_country (nullable when no zone)
-- Idempotent: safe to re-run (skip if column/index already exists)
-- charset: utf8mb4
SET NAMES utf8mb4;
SET @db := DATABASE();
-- zone_id
SET @has := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'bms_product_country' AND COLUMN_NAME = 'zone_id'
);
SET @sql := IF(@has = 0,
'ALTER TABLE bms_product_country ADD COLUMN zone_id BIGINT(20) DEFAULT NULL COMMENT ''prc_zone.zone_id'' AFTER country_code',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- zone_code
SET @has := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'bms_product_country' AND COLUMN_NAME = 'zone_code'
);
SET @sql := IF(@has = 0,
'ALTER TABLE bms_product_country ADD COLUMN zone_code VARCHAR(50) DEFAULT NULL COMMENT ''zone code e.g. Z1'' AFTER zone_id',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- zone_name
SET @has := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'bms_product_country' AND COLUMN_NAME = 'zone_name'
);
SET @sql := IF(@has = 0,
'ALTER TABLE bms_product_country ADD COLUMN zone_name VARCHAR(100) DEFAULT NULL COMMENT ''zone display name'' AFTER zone_code',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- indexes
SET @has := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'bms_product_country' AND INDEX_NAME = 'idx_product_country_zone'
);
SET @sql := IF(@has = 0,
'ALTER TABLE bms_product_country ADD INDEX idx_product_country_zone (zone_id)',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @has := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'bms_product_country' AND INDEX_NAME = 'idx_product_country_zone_code'
);
SET @sql := IF(@has = 0,
'ALTER TABLE bms_product_country ADD INDEX idx_product_country_zone_code (country_code, zone_code)',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- unique key: drop old, add new
SET @has := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'bms_product_country' AND INDEX_NAME = 'uk_product_country_weight_eff'
);
SET @sql := IF(@has > 0,
'ALTER TABLE bms_product_country DROP INDEX uk_product_country_weight_eff',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @has := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'bms_product_country' AND INDEX_NAME = 'uk_product_country_zone_weight_eff'
);
SET @sql := IF(@has = 0,
'ALTER TABLE bms_product_country ADD UNIQUE KEY uk_product_country_zone_weight_eff (product_code, country_name, zone_code, weight_min, weight_max, effective_date)',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;