导入新产品,20.套餐编辑的时候,不能低于基准价,低于就提示不能保存

This commit is contained in:
wuweihua
2026-09-21 15:11:25 +08:00
parent 6322e20ea4
commit 83f19e1dca
11 changed files with 739 additions and 11 deletions
@@ -110,4 +110,16 @@ public class ProductController extends BaseController
String message = productService.importProductExcel(file, updateSupport, getUsername());
return success(message);
}
/**
* Import a single-country linehaul cost sheet. countryCode is the destination.
*/
@Log(title = "\u4ea7\u54c1\u5217\u8868", businessType = BusinessType.IMPORT)
@PreAuthorize("@ss.hasPermi('price:product:import')")
@PostMapping("/importCountryData")
public AjaxResult importCountryData(MultipartFile file, String countryCode) throws Exception
{
String message = productService.importCountryProductExcel(file, countryCode, getUsername());
return success(message);
}
}
@@ -73,6 +73,8 @@ 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.detail.freight.belowBase=套餐运费不能低于导入基准价 {0}
price.set.detail.registration.belowBase=套餐挂号费不能低于导入基准价 {0}
price.set.customer.notExists=客户绑定不存在
price.set.customer.unbindAudit=审核中的套餐不允许解绑客户
price.set.customer.customerId.empty=客户ID不能为空
@@ -117,6 +119,11 @@ price.product.import.noSheet=未找到产品明细sheet(需与产品名称一
price.product.import.noCountry=产品明细sheet无有效国家/重量段数据:{0}
price.product.import.euEmpty=国家表无欧盟成员(is_eu=1),无法展开「欧盟」
price.product.import.success.unmatched=未匹配国家表(country_code 为空):{0}
price.product.import.countryRequired=请选择目的国家
price.product.import.countryNotFound=国家不存在或已停用:{0}
price.product.import.countrySheetNotFound=未找到「产品目录」sheet,请上传单国专线成本价表
price.product.import.countryNoPrice=未找到该产品的重量段价格:{0}
price.product.import.countrySuccess=按国家导入成功({0}):新增 {1} 个产品,跳过 {2} 个(同代码同生效日不覆盖主数据);写入重量段 {3} 行。
# 报价试算 TmsPriceQuote(对外 TMS API 错误码,保持英文契约)
price.quote.postcodeRequired=该国家/产品有分区,请填写目的邮编
price.quote.postcodeZoneNotFound=邮编未匹配到分区,请核对目的邮编
@@ -43,4 +43,15 @@ public interface IBmsProductService
* @return 导入结果说明(可含 HTML 换行)
*/
public String importProductExcel(MultipartFile file, boolean updateSupport, String operName) throws Exception;
/**
* Import a single-country cost sheet (catalog + product weight rows).
* The destination country is chosen by the user, not read from the sheet.
*
* @param file uploaded xlsx/xls
* @param countryCode ISO country code (bms_region.country_code_2)
* @param operName operator
* @return HTML result message
*/
public String importCountryProductExcel(MultipartFile file, String countryCode, String operName) throws Exception;
}
@@ -321,6 +321,100 @@ public class BmsProductServiceImpl implements IBmsProductService
}
}
@Override
public String importCountryProductExcel(MultipartFile file, String countryCode, String operName) throws Exception
{
if (file == null || file.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.product.import.fileEmpty"));
}
String filename = file.getOriginalFilename();
if (filename == null || !(filename.toLowerCase(Locale.ROOT).endsWith(".xlsx")
|| filename.toLowerCase(Locale.ROOT).endsWith(".xls")))
{
throw new ServiceException(MessageUtils.message("price.product.import.fileType"));
}
if (StringUtils.isEmpty(countryCode))
{
throw new ServiceException(MessageUtils.message("price.product.import.countryRequired"));
}
BmsRegion region = regionMapper.selectRegionByCode2(countryCode.trim().toUpperCase(Locale.ROOT));
if (region == null || "2".equals(region.getDelFlag())
|| (StringUtils.isNotEmpty(region.getStatus()) && !"0".equals(region.getStatus())))
{
throw new ServiceException(MessageUtils.message("price.product.import.countryNotFound", countryCode));
}
try (InputStream is = file.getInputStream(); Workbook workbook = WorkbookFactory.create(is))
{
Sheet catalog = workbook.getSheet(CATALOG_SHEET);
if (catalog == null)
{
throw new ServiceException(MessageUtils.message("price.product.import.countrySheetNotFound"));
}
List<BmsProduct> products = parseCountryCatalog(catalog);
if (products.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.product.import.noData"));
}
products = dedupeCatalogProducts(products);
Map<String, PriceBlock> priceBlocks = parseCountryPriceBlocks(workbook);
int successNum = 0;
int skipNum = 0;
int countryNum = 0;
StringBuilder failureMsg = new StringBuilder();
String countryLabel = StringUtils.isNotEmpty(region.getCountryNameCn())
? region.getCountryNameCn() : region.getCountryCode2();
for (BmsProduct product : products)
{
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
TransactionStatus tx = transactionManager.getTransaction(def);
try
{
PriceBlock block = findPriceBlock(product.getProductName(), priceBlocks);
if (block == null || block.rows.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.product.import.countryNoPrice",
product.getProductName()));
}
product.setDestRegion(countryLabel);
product.setSourceSheet(block.sheetName);
if (StringUtils.isEmpty(product.getTaxMode()))
{
product.setTaxMode(resolveTaxMode(product.getProductName()));
}
List<BmsProductCountry> countries = stampSelectedCountry(block.rows, region, product.getEffectiveDate());
int[] nums = persistImportedProduct(product, countries, operName);
transactionManager.commit(tx);
successNum += nums[0];
skipNum += nums[1];
countryNum += nums[2];
}
catch (Exception e)
{
transactionManager.rollback(tx);
String tip = e.getMessage();
if (StringUtils.isEmpty(tip))
{
tip = e.getClass().getSimpleName();
}
failureMsg.append("<br/>").append(product.getProductCode()).append("\uff1a").append(tip);
}
}
if (failureMsg.length() > 0)
{
return MessageUtils.message("price.product.import.partialFail",
Integer.valueOf(successNum), Integer.valueOf(skipNum), failureMsg.toString());
}
return MessageUtils.message("price.product.import.countrySuccess",
countryLabel, Integer.valueOf(successNum), Integer.valueOf(skipNum), Integer.valueOf(countryNum));
}
}
/** @return int[]{added, skipped, countryRows} */
private int[] importOneProduct(Workbook workbook, BmsProduct product, String operName,
Map<String, BmsRegion> regionByNameCn, List<BmsRegion> euRegions,
@@ -1289,6 +1383,364 @@ public class BmsProductServiceImpl implements IBmsProductService
}
}
/**
* Country cost sheet catalog: product id / name / service / cargo / note / update date.
* Destination country is not in the sheet.
*/
private List<BmsProduct> parseCountryCatalog(Sheet sheet)
{
List<BmsProduct> list = new ArrayList<>();
int[] header = findHeaderCell(sheet, "\u4ea7\u54c1\u540d\u79f0");
if (header == null)
{
header = findHeaderCell(sheet, "\u4ea7\u54c1ID");
}
if (header == null)
{
return list;
}
Row head = sheet.getRow(header[0]);
int colCode = firstCol(head, "\u4ea7\u54c1ID", "\u4ea7\u54c1\u5bf9\u63a5\u4ee3\u7801", "\u4ea7\u54c1\u4ee3\u7801");
int colName = findColByHeader(head, "\u4ea7\u54c1\u540d\u79f0", 0);
int colService = firstCol(head, "\u670d\u52a1", "\u65f6\u6548\u7b49\u7ea7");
int colCargo = findColByHeader(head, "\u8d70\u8d27\u5c5e\u6027", 0);
int colNote = firstCol(head, "\u5907\u6ce8", "\u66f4\u65b0\u5185\u5bb9");
int colDate = firstCol(head, "\u66f4\u65b0\u65f6\u95f4", "\u751f\u6548\u65e5\u671f");
if (colCode < 0 || colName < 0)
{
return list;
}
int lastRow = sheet.getLastRowNum();
for (int i = header[0] + 1; i <= lastRow; i++)
{
Row row = sheet.getRow(i);
if (row == null)
{
continue;
}
String code = cellString(row.getCell(colCode));
if (StringUtils.isEmpty(code) || !code.matches("(?i)^[A-Z0-9_-]+$"))
{
continue;
}
String name = cellString(row.getCell(colName));
if (StringUtils.isEmpty(name))
{
continue;
}
BmsProduct p = new BmsProduct();
p.setProductCode(code.trim());
p.setProductName(name.trim());
if (colService >= 0)
{
p.setServiceLevel(emptyToNull(cellString(row.getCell(colService))));
}
if (colCargo >= 0)
{
p.setCargoAttr(emptyToNull(cellString(row.getCell(colCargo))));
}
if (colNote >= 0)
{
p.setUpdateNote(emptyToNull(cellString(row.getCell(colNote))));
}
if (colDate >= 0)
{
Date eff = cellDate(row.getCell(colDate));
if (eff == null)
{
eff = parseLooseDate(cellString(row.getCell(colDate)));
}
p.setEffectiveDate(toDateOnly(eff));
}
p.setTaxMode(resolveTaxMode(p.getProductName()));
list.add(p);
}
return list;
}
private Map<String, PriceBlock> parseCountryPriceBlocks(Workbook workbook)
{
Map<String, PriceBlock> map = new HashMap<>();
if (workbook == null)
{
return map;
}
for (int i = 0; i < workbook.getNumberOfSheets(); i++)
{
String sheetName = workbook.getSheetName(i);
if (shouldSkipCountryPriceSheet(sheetName))
{
continue;
}
Sheet sheet = workbook.getSheetAt(i);
int[] header = findHeaderCell(sheet, "\u4ea7\u54c1\u540d\u79f0");
if (header == null)
{
continue;
}
Row headerRow = sheet.getRow(header[0]);
int colName = header[1];
int colWeight = findColByHeader(headerRow, "\u91cd\u91cf", 0);
if (colWeight < 0)
{
continue;
}
int colFreight = findColByHeader(headerRow, "\u8fd0\u8d39", 0);
int colRegister = findColByHeader(headerRow, "\u6302\u53f7", 0);
int colSize = findColByHeader(headerRow, "\u5c3a\u5bf8", 0);
int colLastMile = findColByHeader(headerRow, "\u5c3e\u7a0b", 0);
int colLeadTime = findColByHeader(headerRow, "\u65f6\u6548", 0);
String currentName = null;
String currentSize = null;
String currentLastMile = null;
String currentLeadTime = null;
int lastRow = sheet.getLastRowNum();
for (int r = header[0] + 1; r <= lastRow; r++)
{
Row row = sheet.getRow(r);
if (row == null)
{
continue;
}
String nameCell = cellString(row.getCell(colName));
if (StringUtils.isNotEmpty(nameCell))
{
if (isSectionTitle(nameCell))
{
currentName = null;
continue;
}
currentName = nameCell.replace("\n", " ").replace("\r", " ").trim();
currentSize = colSize >= 0 ? emptyToNull(cellString(row.getCell(colSize))) : null;
currentLastMile = colLastMile >= 0 ? emptyToNull(cellString(row.getCell(colLastMile))) : null;
currentLeadTime = colLeadTime >= 0 ? emptyToNull(cellString(row.getCell(colLeadTime))) : null;
}
if (StringUtils.isEmpty(currentName))
{
continue;
}
String weightLabel = cellString(row.getCell(colWeight));
BigDecimal[] range = parseWeightRange(weightLabel);
if (range == null)
{
continue;
}
if (colSize >= 0 && StringUtils.isNotEmpty(cellString(row.getCell(colSize))))
{
currentSize = emptyToNull(cellString(row.getCell(colSize)));
}
if (colLastMile >= 0 && StringUtils.isNotEmpty(cellString(row.getCell(colLastMile))))
{
currentLastMile = emptyToNull(cellString(row.getCell(colLastMile)));
}
if (colLeadTime >= 0 && StringUtils.isNotEmpty(cellString(row.getCell(colLeadTime))))
{
currentLeadTime = emptyToNull(cellString(row.getCell(colLeadTime)));
}
BmsProductCountry c = new BmsProductCountry();
c.setWeightMin(range[0]);
c.setWeightMax(range[1]);
c.setFreightPrice(colFreight >= 0 ? cellDecimal(row.getCell(colFreight)) : null);
c.setRegistrationFee(colRegister >= 0 ? cellDecimal(row.getCell(colRegister)) : null);
c.setSizeLimit(currentSize);
c.setLastMile(currentLastMile);
c.setLeadTime(currentLeadTime);
PriceBlock block = map.get(currentName);
if (block == null)
{
block = new PriceBlock();
block.sheetName = sheetName;
map.put(currentName, block);
}
block.rows.add(c);
}
}
return map;
}
private static boolean shouldSkipCountryPriceSheet(String name)
{
if (StringUtils.isEmpty(name))
{
return true;
}
if (CATALOG_SHEET.equals(name) || SKIP_SHEETS.contains(name))
{
return true;
}
return name.contains("\u90ae\u7f16") || name.contains("\u8d54\u507f");
}
private static boolean isSectionTitle(String name)
{
if (StringUtils.isEmpty(name))
{
return false;
}
String n = name.trim();
return n.startsWith("\u4e00\u3001") || n.startsWith("\u4e8c\u3001") || n.startsWith("\u4e09\u3001")
|| n.startsWith("\u56db\u3001") || n.startsWith("\u4e94\u3001") || n.startsWith("\u516d\u3001")
|| n.startsWith("\u7279\u522b");
}
private PriceBlock findPriceBlock(String productName, Map<String, PriceBlock> blocks)
{
if (StringUtils.isEmpty(productName) || blocks == null || blocks.isEmpty())
{
return null;
}
String name = productName.trim();
PriceBlock exact = blocks.get(name);
if (exact != null)
{
return exact;
}
String stripped = name.replace("\u6807\u51c6", "").trim();
exact = blocks.get(stripped);
if (exact != null)
{
return exact;
}
String want = compactName(name);
PriceBlock hit = null;
int hits = 0;
for (Map.Entry<String, PriceBlock> entry : blocks.entrySet())
{
String key = compactName(entry.getKey());
if (key.equals(want) || (want.contains(key) && key.length() >= 4) || (key.contains(want) && want.length() >= 4))
{
hit = entry.getValue();
hits++;
}
}
return hits == 1 ? hit : null;
}
private static String compactName(String s)
{
if (s == null)
{
return "";
}
return s.trim().replace(" ", "").replace("\u3000", "")
.replace("-", "").replace("\u2013", "").replace("\u2014", "").replace("\uff0d", "");
}
private List<BmsProductCountry> stampSelectedCountry(List<BmsProductCountry> rows, BmsRegion region, Date eff)
{
List<BmsProductCountry> out = new ArrayList<>();
if (rows == null)
{
return out;
}
String countryName = region == null ? null : region.getCountryNameCn();
String countryCode = region == null ? null : region.getCountryCode2();
for (BmsProductCountry src : rows)
{
BmsProductCountry c = new BmsProductCountry();
c.setCountryName(countryName);
c.setCountryCode(countryCode);
c.setWeightMin(src.getWeightMin());
c.setWeightMax(src.getWeightMax());
c.setFreightPrice(src.getFreightPrice());
c.setRegistrationFee(src.getRegistrationFee());
c.setSizeLimit(src.getSizeLimit());
c.setLastMile(src.getLastMile());
c.setLeadTime(src.getLeadTime());
c.setEffectiveDate(eff);
out.add(c);
}
return out;
}
/** @return int[]{added, skipped, countryRows} */
private int[] persistImportedProduct(BmsProduct product, List<BmsProductCountry> countries, String operName)
{
product.setCreateBy(operName);
product.setUpdateBy(operName);
if (StringUtils.isEmpty(product.getSourceSheet()))
{
product.setSourceSheet(product.getProductName());
}
Date newEff = toDateOnly(product.getEffectiveDate());
if (newEff == null)
{
newEff = toDateOnly(resolveImportEffectiveDate(countries));
}
product.setEffectiveDate(newEff);
if (newEff == null)
{
throw new ServiceException(MessageUtils.message("price.product.import.effEmpty"));
}
BmsProduct existing = productMapper.selectBmsProductByCodeAndDate(product.getProductCode(), newEff);
if (existing != null)
{
product.setProductId(existing.getProductId());
int countryRows = saveCountriesVersioned(product, countries);
return new int[] { 0, 1, countryRows };
}
applyProductStatus(product);
try
{
productMapper.insertBmsProduct(product);
}
catch (Exception e)
{
if (isProductCodeUniqueViolation(e))
{
throw new ServiceException(MessageUtils.message("price.product.import.needCodeEffUnique"));
}
throw e;
}
productMapper.expireOpenByProductCode(product.getProductCode(), newEff, product.getProductId());
productMapper.refreshStatusByDates();
int countryRows = saveCountriesVersioned(product, countries);
return new int[] { 1, 0, countryRows };
}
private Date parseLooseDate(String text)
{
if (StringUtils.isEmpty(text))
{
return null;
}
String s = text.trim();
if (s.length() > 20)
{
return null;
}
Matcher ymd = EFFECTIVE_DATE_YMD.matcher(s);
if (ymd.matches())
{
return parseYmd(ymd.group(1), ymd.group(2), ymd.group(3));
}
return null;
}
private static int firstCol(Row row, String... keywords)
{
if (keywords == null)
{
return -1;
}
for (String keyword : keywords)
{
int col = findColByHeader(row, keyword, 0);
if (col >= 0)
{
return col;
}
}
return -1;
}
private static final class PriceBlock
{
private String sheetName;
private final List<BmsProductCountry> rows = new ArrayList<>();
}
private static Date cellDate(Cell cell)
{
if (cell == null)
@@ -1039,6 +1039,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
// (见 TmsPriceQuoteServiceImpl.tryFixed)。
fillProductCountryId(detail);
snapshotWeightAndDates(detail);
assertNotBelowImportedPrice(detail);
}
if (StringUtils.isEmpty(detail.getRoundMode()))
{
@@ -1059,6 +1060,89 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
}
}
/** Package freight / registration must not go below the imported product country price. */
private void assertNotBelowImportedPrice(PrcPriceSetDetail detail)
{
BmsProductCountry base = findImportedPrice(detail);
if (base == null)
{
return;
}
if (detail.getFvValue() != null && base.getFreightPrice() != null
&& detail.getFvValue().compareTo(base.getFreightPrice()) < 0)
{
throw new ServiceException(MessageUtils.message("price.set.detail.freight.belowBase",
stripTrailingZeros(base.getFreightPrice())));
}
if (detail.getFvRegistrationFee() != null && base.getRegistrationFee() != null
&& detail.getFvRegistrationFee().compareTo(base.getRegistrationFee()) < 0)
{
throw new ServiceException(MessageUtils.message("price.set.detail.registration.belowBase",
stripTrailingZeros(base.getRegistrationFee())));
}
}
private BmsProductCountry findImportedPrice(PrcPriceSetDetail detail)
{
if (detail == null || StringUtils.isEmpty(detail.getProductCode()))
{
return null;
}
BmsProductCountry query = new BmsProductCountry();
query.setProductCode(detail.getProductCode());
List<BmsProductCountry> rows = productCountryMapper.selectBmsProductCountryList(query);
if (rows == null || rows.isEmpty())
{
return null;
}
if (detail.getProductCountryId() != null)
{
for (BmsProductCountry row : rows)
{
if (row != null && detail.getProductCountryId().equals(row.getDetailId()))
{
return row;
}
}
}
for (BmsProductCountry row : rows)
{
if (row == null || row.getExpireDate() != null)
{
continue;
}
if (!matchDetailCountry(detail, row))
{
continue;
}
String label = formatWeightLabel(row.getWeightMin(), row.getWeightMax());
if (StringUtils.isNotEmpty(detail.getWeightLabel()) && detail.getWeightLabel().equals(label))
{
return row;
}
if (detail.getWeightMin() != null && row.getWeightMin() != null
&& detail.getWeightMin().compareTo(row.getWeightMin()) == 0
&& sameWeightMax(detail.getWeightMax(), row.getWeightMax()))
{
return row;
}
}
return null;
}
private static boolean sameWeightMax(java.math.BigDecimal a, java.math.BigDecimal b)
{
if (a == null && b == null)
{
return true;
}
if (a == null || b == null)
{
return false;
}
return a.compareTo(b) == 0;
}
/**
* When product country rows for this country have zones, zone is required.
*/