修改套餐导入功能

This commit is contained in:
wuweihua
2026-10-10 11:16:03 +08:00
parent 9d7218795a
commit fab2ec7d9c
12 changed files with 578 additions and 127 deletions
@@ -16,6 +16,8 @@ public class BmsProductCountry implements Serializable
/** FK to product.product_id */
private Long productId;
private String productCode;
/** Parse/import helper only (not a DB column). */
private String productName;
private String countryName;
private String countryCode;
/** prc_zone.zone_id; null when product country has no zone */
@@ -54,4 +54,11 @@ public interface IBmsProductService
public java.util.Map<String, List<BmsProductCountry>> parseQuoteExcelByProduct(MultipartFile file) throws Exception;
public java.util.Map<String, List<BmsProductCountry>> parseQuoteExcelByProduct(byte[] bytes, String filename) throws Exception;
/**
* Parse single-country dedicated-line quote Excel (same format as product country import).
* All weight rows are stamped with the selected country. Keys are upper-case product codes.
*/
public java.util.Map<String, List<BmsProductCountry>> parseCountryQuoteExcelByProduct(byte[] bytes, String filename,
String countryCode) throws Exception;
}
@@ -121,6 +121,13 @@ public interface IPrcPriceSetService
public String submitImportDetailPricesAsync(Long priceSetId, Long psvId, MultipartFile file, String operName,
boolean updateOnly) throws Exception;
/**
* @param replaceByProductCountry true = Excel build-price mode: replace only product+country scopes in this file
* @param countryCode optional; when set, parse single-country dedicated-line Excel (product country-import format)
*/
public String submitImportDetailPricesAsync(Long priceSetId, Long psvId, MultipartFile file, String operName,
boolean updateOnly, boolean replaceByProductCountry, String countryCode) throws Exception;
/**
* Sync worker: parse bytes and upsert FIXED detail prices matched by
* product + country + zone(if any) + weight band (insert when missing unless updateOnly).
@@ -133,6 +140,13 @@ public interface IPrcPriceSetService
public String importDetailPrices(Long priceSetId, Long psvId, byte[] bytes, String filename, String operName,
String taskId, boolean updateOnly) throws Exception;
/**
* @param replaceByProductCountry package Excel build-price only; other import modes pass false
* @param countryCode optional single-country dedicated-line import
*/
public String importDetailPrices(Long priceSetId, Long psvId, byte[] bytes, String filename, String operName,
String taskId, boolean updateOnly, boolean replaceByProductCountry, String countryCode) throws Exception;
/**
* Submit async pull of product library into version details.
* @return taskId for polling getSaveTask
@@ -153,4 +167,11 @@ public interface IPrcPriceSetService
*/
public Map<String, Object> previewImportDetailPrices(byte[] bytes, String filename, boolean updateOnly,
List<PrcPriceSetDetail> existingDetails) throws Exception;
/**
* @param replaceByProductCountry package Excel build-price only: keep other product/country rows
* @param countryCode optional single-country dedicated-line import
*/
public Map<String, Object> previewImportDetailPrices(byte[] bytes, String filename, boolean updateOnly,
List<PrcPriceSetDetail> existingDetails, boolean replaceByProductCountry, String countryCode) throws Exception;
}
@@ -628,6 +628,67 @@ public class BmsProductServiceImpl implements IBmsProductService
}
}
@Override
public Map<String, List<BmsProductCountry>> parseCountryQuoteExcelByProduct(byte[] bytes, String filename,
String countryCode) throws Exception
{
validateImportBytes(bytes, filename);
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 = new java.io.ByteArrayInputStream(bytes); 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 = dedupeCatalogProducts(parseCountryCatalog(catalog));
if (products.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.product.import.noData"));
}
Map<String, PriceBlock> priceBlocks = parseCountryPriceBlocks(workbook);
Map<String, List<BmsProductCountry>> out = new LinkedHashMap<String, List<BmsProductCountry>>();
for (BmsProduct product : products)
{
if (product == null || StringUtils.isEmpty(product.getProductCode()))
{
continue;
}
PriceBlock block = findPriceBlock(product.getProductName(), priceBlocks);
if (block == null || block.rows == null || block.rows.isEmpty())
{
continue;
}
List<BmsProductCountry> countries = stampSelectedCountry(block.rows, region, null);
for (BmsProductCountry c : countries)
{
if (c == null)
{
continue;
}
c.setProductCode(product.getProductCode());
c.setProductName(product.getProductName());
}
assertNoDuplicateCountryWeightRows(product, countries);
out.put(product.getProductCode().trim().toUpperCase(Locale.ROOT), countries);
}
if (out.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.set.import.noPriceRows"));
}
return out;
}
}
/** @return int[]{added, skipped, countryRows} */
private int[] importOneProduct(Workbook workbook, BmsProduct product, String operName,
@@ -929,6 +929,13 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
@Override
public String submitImportDetailPricesAsync(Long priceSetId, Long psvId, MultipartFile file, String operName,
boolean updateOnly) throws Exception
{
return submitImportDetailPricesAsync(priceSetId, psvId, file, operName, updateOnly, false, null);
}
@Override
public String submitImportDetailPricesAsync(Long priceSetId, Long psvId, MultipartFile file, String operName,
boolean updateOnly, boolean replaceByProductCountry, String countryCode) throws Exception
{
PrcPriceSet db = requireSet(priceSetId);
assertChildEditable(db);
@@ -953,7 +960,8 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
}
String taskId = IdUtils.fastSimpleUUID();
saveAsyncRunner.put(taskId, PrcPriceSetSaveTask.STATUS_PENDING, 0, "queued", priceSetId, null);
saveAsyncRunner.runImportPrices(taskId, priceSetId, psvId, bytes, filename, operName, updateOnly);
saveAsyncRunner.runImportPrices(taskId, priceSetId, psvId, bytes, filename, operName, updateOnly,
replaceByProductCountry, countryCode);
return taskId;
}
@@ -1036,14 +1044,23 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
public Map<String, Object> previewImportDetailPrices(byte[] bytes, String filename, boolean updateOnly,
List<PrcPriceSetDetail> existingDetails) throws Exception
{
Map<String, List<BmsProductCountry>> byProduct = productService.parseQuoteExcelByProduct(bytes, filename);
return previewImportDetailPrices(bytes, filename, updateOnly, existingDetails, false, null);
}
@Override
public Map<String, Object> previewImportDetailPrices(byte[] bytes, String filename, boolean updateOnly,
List<PrcPriceSetDetail> existingDetails, boolean replaceByProductCountry, String countryCode) throws Exception
{
boolean replaceScope = replaceByProductCountry || StringUtils.isNotEmpty(countryCode);
Map<String, List<BmsProductCountry>> byProduct = parseSetImportExcel(bytes, filename, countryCode);
if (byProduct == null || byProduct.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.set.import.noPriceRows"));
}
List<PrcPriceSetDetail> details = new ArrayList<PrcPriceSetDetail>();
if (existingDetails != null)
// Keep prior draft rows only for updateOnly or Excel build-price merge.
if (existingDetails != null && (updateOnly || replaceScope))
{
for (PrcPriceSetDetail d : existingDetails)
{
@@ -1053,27 +1070,17 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
}
}
}
Map<String, List<PrcPriceSetDetail>> detailIndex = new HashMap<String, List<PrcPriceSetDetail>>();
for (PrcPriceSetDetail d : details)
{
if (d == null || StringUtils.isEmpty(d.getProductCode()))
{
continue;
}
String pc = d.getProductCode().trim().toUpperCase(Locale.ROOT);
List<PrcPriceSetDetail> list = detailIndex.get(pc);
if (list == null)
{
list = new ArrayList<PrcPriceSetDetail>();
detailIndex.put(pc, list);
}
list.add(d);
}
Map<String, List<PrcPriceSetDetail>> detailIndex = buildImportDetailIndex(details);
if (updateOnly)
{
assertUpdateOnlyWeightBandsMatch(byProduct, detailIndex);
}
else if (replaceScope && !details.isEmpty())
{
// Package Excel build-price: replace only product+country scopes in this file.
removeDetailsByReplaceScopes(details, detailIndex, collectImportReplaceScopes(byProduct));
}
String operName = null;
try
{
@@ -1088,6 +1095,16 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
Long pefId = excelFile != null ? excelFile.getPefId() : null;
Map<String, PrcPriceSetProduct> productMap = new LinkedHashMap<String, PrcPriceSetProduct>();
if (replaceScope)
{
for (PrcPriceSetDetail d : details)
{
if (d != null)
{
ensurePreviewProduct(productMap, d.getProductCode(), d.getProductName());
}
}
}
int updated = 0;
int inserted = 0;
int skippedFee = 0;
@@ -1097,21 +1114,26 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
for (Map.Entry<String, List<BmsProductCountry>> e : byProduct.entrySet())
{
String productCode = e.getKey();
List<PrcPriceSetDetail> candidates = detailIndex.get(productCode);
String pcKey = productCode == null ? null : productCode.trim().toUpperCase(Locale.ROOT);
List<PrcPriceSetDetail> candidates = detailIndex.get(pcKey);
if (candidates == null)
{
candidates = new ArrayList<PrcPriceSetDetail>();
detailIndex.put(productCode, candidates);
if (pcKey != null)
{
detailIndex.put(pcKey, candidates);
}
}
if (updateOnly && candidates.isEmpty())
{
skippedUnmatched += e.getValue() == null ? 0 : e.getValue().size();
continue;
}
String productName = resolveImportProductName(null, productCode, candidates);
String productName = resolveImportProductNameFromRows(e.getValue(), null, productCode, candidates);
if (!updateOnly)
{
ensurePreviewProduct(productMap, productCode, productName);
stampPreviewProductDestRegion(productMap, productCode, e.getValue());
}
for (BmsProductCountry row : e.getValue())
{
@@ -1189,14 +1211,29 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
if (pefId != null)
{
for (PrcPriceSetProduct sp : productMap.values())
for (String code : byProduct.keySet())
{
if (StringUtils.isEmpty(code))
{
continue;
}
PrcPriceSetProduct sp = productMap.get(code.trim().toUpperCase(Locale.ROOT));
if (sp != null)
{
sp.setPefId(pefId);
}
}
}
if (replaceScope)
{
for (PrcPriceSetDetail d : details)
{
if (d != null)
{
ensurePreviewProduct(productMap, d.getProductCode(), d.getProductName());
}
}
}
Map<String, Object> result = new HashMap<String, Object>();
result.put("products", new ArrayList<PrcPriceSetProduct>(productMap.values()));
result.put("details", details);
@@ -1214,6 +1251,54 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
return result;
}
private Map<String, List<BmsProductCountry>> parseSetImportExcel(byte[] bytes, String filename, String countryCode)
throws Exception
{
if (StringUtils.isNotEmpty(countryCode))
{
return productService.parseCountryQuoteExcelByProduct(bytes, filename, countryCode);
}
return productService.parseQuoteExcelByProduct(bytes, filename);
}
private String resolveImportProductNameFromRows(List<BmsProductCountry> rows, Long psvId, String productCode,
List<PrcPriceSetDetail> candidates)
{
if (rows != null)
{
for (BmsProductCountry row : rows)
{
if (row != null && StringUtils.isNotEmpty(row.getProductName()))
{
return row.getProductName();
}
}
}
return resolveImportProductName(psvId, productCode, candidates);
}
private void stampPreviewProductDestRegion(Map<String, PrcPriceSetProduct> productMap, String productCode,
List<BmsProductCountry> rows)
{
if (productMap == null || StringUtils.isEmpty(productCode) || rows == null || rows.isEmpty())
{
return;
}
PrcPriceSetProduct sp = productMap.get(productCode.trim().toUpperCase(Locale.ROOT));
if (sp == null || StringUtils.isNotEmpty(sp.getDestRegion()))
{
return;
}
for (BmsProductCountry row : rows)
{
if (row != null && StringUtils.isNotEmpty(row.getCountryName()))
{
sp.setDestRegion(row.getCountryName());
return;
}
}
}
private void ensurePreviewProduct(Map<String, PrcPriceSetProduct> productMap, String productCode, String productName)
{
if (productMap == null || StringUtils.isEmpty(productCode))
@@ -1266,6 +1351,14 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
@Transactional(rollbackFor = Exception.class)
public String importDetailPrices(Long priceSetId, Long psvId, byte[] bytes, String filename, String operName,
String taskId, boolean updateOnly) throws Exception
{
return importDetailPrices(priceSetId, psvId, bytes, filename, operName, taskId, updateOnly, false, null);
}
@Override
@Transactional(rollbackFor = Exception.class)
public String importDetailPrices(Long priceSetId, Long psvId, byte[] bytes, String filename, String operName,
String taskId, boolean updateOnly, boolean replaceByProductCountry, String countryCode) throws Exception
{
PrcPriceSet db = requireSet(priceSetId);
assertChildEditable(db);
@@ -1282,7 +1375,8 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
throw new ServiceException(MessageUtils.message("price.set.version.notExists"));
}
reportImportProgress(taskId, priceSetId, 20, "parsing excel");
Map<String, List<BmsProductCountry>> byProduct = productService.parseQuoteExcelByProduct(bytes, filename);
boolean replaceScope = replaceByProductCountry || StringUtils.isNotEmpty(countryCode);
Map<String, List<BmsProductCountry>> byProduct = parseSetImportExcel(bytes, filename, countryCode);
if (byProduct == null || byProduct.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.set.import.noPriceRows"));
@@ -1296,27 +1390,35 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
{
details = new ArrayList<PrcPriceSetDetail>();
}
Map<String, List<PrcPriceSetDetail>> detailIndex = new HashMap<String, List<PrcPriceSetDetail>>();
for (PrcPriceSetDetail d : details)
{
if (d == null || StringUtils.isEmpty(d.getProductCode()))
{
continue;
}
String pc = d.getProductCode().trim().toUpperCase(Locale.ROOT);
List<PrcPriceSetDetail> list = detailIndex.get(pc);
if (list == null)
{
list = new ArrayList<PrcPriceSetDetail>();
detailIndex.put(pc, list);
}
list.add(d);
}
Map<String, List<PrcPriceSetDetail>> detailIndex = buildImportDetailIndex(details);
if (updateOnly)
{
assertUpdateOnlyWeightBandsMatch(byProduct, detailIndex);
}
else if (replaceScope && !details.isEmpty())
{
// Package Excel build-price only.
Set<String> replaceScopes = collectImportReplaceScopes(byProduct);
List<Long> deleteIds = new ArrayList<Long>();
for (PrcPriceSetDetail d : details)
{
if (d == null || d.getPsdId() == null)
{
continue;
}
String scope = importCountryScopeKey(d.getProductCode(), d.getCountryCode(), d.getCountryName());
if (scope != null && replaceScopes.contains(scope))
{
deleteIds.add(d.getPsdId());
}
}
if (!deleteIds.isEmpty())
{
detailMapper.deletePrcPriceSetDetailByIds(deleteIds.toArray(new Long[0]));
}
removeDetailsByReplaceScopes(details, detailIndex, replaceScopes);
}
PrcPriceSetVersion verForFile = versionMapper.selectPrcPriceSetVersionById(psvId);
Date excelEff = verForFile != null && verForFile.getEffectiveTime() != null
? verForFile.getEffectiveTime() : new Date();
@@ -1338,18 +1440,22 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
reportImportProgress(taskId, priceSetId,
20 + (int) Math.floor(70.0 * productIndex / Math.max(productTotal, 1)),
"importing " + productCode + " (" + productIndex + "/" + productTotal + ")");
List<PrcPriceSetDetail> candidates = detailIndex.get(productCode);
String pcKey = productCode == null ? null : productCode.trim().toUpperCase(Locale.ROOT);
List<PrcPriceSetDetail> candidates = detailIndex.get(pcKey);
if (candidates == null)
{
candidates = new ArrayList<PrcPriceSetDetail>();
detailIndex.put(productCode, candidates);
if (pcKey != null)
{
detailIndex.put(pcKey, candidates);
}
}
if (updateOnly && candidates.isEmpty())
{
skippedUnmatched += e.getValue() == null ? 0 : e.getValue().size();
continue;
}
String productName = resolveImportProductName(psvId, productCode, candidates);
String productName = resolveImportProductNameFromRows(e.getValue(), psvId, productCode, candidates);
Long pspId = null;
if (!updateOnly)
{
@@ -1712,6 +1818,108 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
return w;
}
/**
* Replace scope for upsert import: productCode + country (code preferred, else name).
* Rows in Excel for a scope replace all existing details of that product+country.
*/
private String importCountryScopeKey(String productCode, String countryCode, String countryName)
{
if (StringUtils.isEmpty(productCode))
{
return null;
}
String pc = productCode.trim().toUpperCase(Locale.ROOT);
if (StringUtils.isNotEmpty(countryCode))
{
return pc + "|C:" + countryCode.trim().toUpperCase(Locale.ROOT);
}
if (StringUtils.isNotEmpty(countryName))
{
return pc + "|N:" + countryName.trim();
}
return null;
}
private Set<String> collectImportReplaceScopes(Map<String, List<BmsProductCountry>> byProduct)
{
Set<String> scopes = new HashSet<String>();
if (byProduct == null || byProduct.isEmpty())
{
return scopes;
}
for (Map.Entry<String, List<BmsProductCountry>> e : byProduct.entrySet())
{
if (e.getValue() == null)
{
continue;
}
for (BmsProductCountry row : e.getValue())
{
if (row == null)
{
continue;
}
String key = importCountryScopeKey(e.getKey(), row.getCountryCode(), row.getCountryName());
if (key != null)
{
scopes.add(key);
}
}
}
return scopes;
}
private Map<String, List<PrcPriceSetDetail>> buildImportDetailIndex(List<PrcPriceSetDetail> details)
{
Map<String, List<PrcPriceSetDetail>> detailIndex = new HashMap<String, List<PrcPriceSetDetail>>();
if (details == null)
{
return detailIndex;
}
for (PrcPriceSetDetail d : details)
{
if (d == null || StringUtils.isEmpty(d.getProductCode()))
{
continue;
}
String pc = d.getProductCode().trim().toUpperCase(Locale.ROOT);
List<PrcPriceSetDetail> list = detailIndex.get(pc);
if (list == null)
{
list = new ArrayList<PrcPriceSetDetail>();
detailIndex.put(pc, list);
}
list.add(d);
}
return detailIndex;
}
private void removeDetailsByReplaceScopes(List<PrcPriceSetDetail> details,
Map<String, List<PrcPriceSetDetail>> detailIndex, Set<String> replaceScopes)
{
if (details == null || replaceScopes == null || replaceScopes.isEmpty())
{
return;
}
java.util.Iterator<PrcPriceSetDetail> it = details.iterator();
while (it.hasNext())
{
PrcPriceSetDetail d = it.next();
if (d == null)
{
it.remove();
continue;
}
String scope = importCountryScopeKey(d.getProductCode(), d.getCountryCode(), d.getCountryName());
if (scope != null && replaceScopes.contains(scope))
{
it.remove();
}
}
detailIndex.clear();
detailIndex.putAll(buildImportDetailIndex(details));
}
private List<PrcPriceSetDetail> matchImportDetails(List<PrcPriceSetDetail> candidates, BmsProductCountry row)
{
List<PrcPriceSetDetail> hits = new ArrayList<PrcPriceSetDetail>();
@@ -87,13 +87,22 @@ public class PrcPriceSetSaveAsyncRunner
@Async("threadPoolTaskExecutor")
public void runImportPrices(String taskId, Long priceSetId, Long psvId, byte[] bytes, String filename, String operName,
boolean updateOnly)
{
runImportPrices(taskId, priceSetId, psvId, bytes, filename, operName, updateOnly, false, null);
}
@Async("threadPoolTaskExecutor")
public void runImportPrices(String taskId, Long priceSetId, Long psvId, byte[] bytes, String filename, String operName,
boolean updateOnly, boolean replaceByProductCountry, String countryCode)
{
try
{
put(taskId, PrcPriceSetSaveTask.STATUS_RUNNING, 10, "parsing excel", priceSetId, null);
String message = priceSetService.importDetailPrices(priceSetId, psvId, bytes, filename, operName, taskId, updateOnly);
String message = priceSetService.importDetailPrices(priceSetId, psvId, bytes, filename, operName, taskId,
updateOnly, replaceByProductCountry, countryCode);
put(taskId, PrcPriceSetSaveTask.STATUS_SUCCESS, 100, message, priceSetId, null);
log.info("async import prices done, taskId={}, priceSetId={}, updateOnly={}", taskId, priceSetId, updateOnly);
log.info("async import prices done, taskId={}, priceSetId={}, updateOnly={}, replaceByProductCountry={}, countryCode={}",
taskId, priceSetId, updateOnly, replaceByProductCountry, countryCode);
}
catch (Exception e)
{