Enhance price set import functionality with preview options and update handling. Added new endpoints for previewing product library pulls and Excel imports, along with support for update-only imports. Updated internationalization messages for import success and error scenarios.
This commit is contained in:
@@ -109,15 +109,24 @@ public interface IPrcPriceSetService
|
||||
*/
|
||||
public String submitImportDetailPricesAsync(Long priceSetId, Long psvId, MultipartFile file, String operName) throws Exception;
|
||||
|
||||
/**
|
||||
* @param updateOnly true = only update matched existing details (no insert / no new product)
|
||||
*/
|
||||
public String submitImportDetailPricesAsync(Long priceSetId, Long psvId, MultipartFile file, String operName,
|
||||
boolean updateOnly) throws Exception;
|
||||
|
||||
/**
|
||||
* Sync worker: parse bytes and upsert FIXED detail prices matched by
|
||||
* product + country + zone(if any) + weight band (insert when missing).
|
||||
* product + country + zone(if any) + weight band (insert when missing unless updateOnly).
|
||||
*/
|
||||
public String importDetailPrices(Long priceSetId, Long psvId, byte[] bytes, String filename, String operName) throws Exception;
|
||||
|
||||
/** Sync worker with optional taskId for progress reporting. */
|
||||
public String importDetailPrices(Long priceSetId, Long psvId, byte[] bytes, String filename, String operName, String taskId) throws Exception;
|
||||
|
||||
public String importDetailPrices(Long priceSetId, Long psvId, byte[] bytes, String filename, String operName,
|
||||
String taskId, boolean updateOnly) throws Exception;
|
||||
|
||||
/**
|
||||
* Submit async pull of product library into version details.
|
||||
* @return taskId for polling getSaveTask
|
||||
@@ -126,4 +135,16 @@ public interface IPrcPriceSetService
|
||||
|
||||
/** Sync worker: replace (or skip) version details from product / product_country. */
|
||||
public String pullFromLibrary(Long priceSetId, Long psvId, boolean force, String operName, String taskId);
|
||||
|
||||
/**
|
||||
* Preview pull: return products + FIXED details from library without writing DB.
|
||||
*/
|
||||
public Map<String, Object> previewPullFromLibrary();
|
||||
|
||||
/**
|
||||
* Preview import: parse Excel into products + details without writing DB.
|
||||
* When updateOnly, merge prices into existingDetails (in-memory match).
|
||||
*/
|
||||
public Map<String, Object> previewImportDetailPrices(byte[] bytes, String filename, boolean updateOnly,
|
||||
List<PrcPriceSetDetail> existingDetails) throws Exception;
|
||||
}
|
||||
|
||||
+601
-73
@@ -11,6 +11,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.zip.CRC32;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
@@ -69,6 +70,14 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
/** Pending: effective_time set but not yet reached */
|
||||
private static final String VER_PENDING = "3";
|
||||
private static final String VER_EXPIRED = "2";
|
||||
/** Bulk detail insert chunk size for create/finish async save. */
|
||||
private static final int DETAIL_INSERT_BATCH = 200;
|
||||
/** Request-scoped cache: productCode -> open product_country rows (bulk upsert). */
|
||||
private final ThreadLocal<Map<String, List<BmsProductCountry>>> countryCacheTl =
|
||||
new ThreadLocal<Map<String, List<BmsProductCountry>>>();
|
||||
/** Request-scoped cache: productCode -> psp_id (bulk upsert). */
|
||||
private final ThreadLocal<Map<String, Long>> setProductCacheTl =
|
||||
new ThreadLocal<Map<String, Long>>();
|
||||
|
||||
@Autowired
|
||||
private PrcPriceSetMapper setMapper;
|
||||
@@ -406,6 +415,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
nv.setStatus(VER_DRAFT);
|
||||
nv.setEffectiveTime(null);
|
||||
nv.setExpireTime(null);
|
||||
nv.setPriceBuildMode(src.getPriceBuildMode());
|
||||
nv.setCreateBy(createBy);
|
||||
versionMapper.insertPrcPriceSetVersion(nv);
|
||||
|
||||
@@ -836,6 +846,13 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
|
||||
@Override
|
||||
public String submitImportDetailPricesAsync(Long priceSetId, Long psvId, MultipartFile file, String operName) throws Exception
|
||||
{
|
||||
return submitImportDetailPricesAsync(priceSetId, psvId, file, operName, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String submitImportDetailPricesAsync(Long priceSetId, Long psvId, MultipartFile file, String operName,
|
||||
boolean updateOnly) throws Exception
|
||||
{
|
||||
PrcPriceSet db = requireSet(priceSetId);
|
||||
assertChildEditable(db);
|
||||
@@ -860,7 +877,7 @@ 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);
|
||||
saveAsyncRunner.runImportPrices(taskId, priceSetId, psvId, bytes, filename, operName, updateOnly);
|
||||
return taskId;
|
||||
}
|
||||
|
||||
@@ -915,16 +932,241 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
return MessageUtils.message("price.set.pull.success", Integer.valueOf(count));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> previewPullFromLibrary()
|
||||
{
|
||||
List<PrcPriceSetProduct> products = setProductMapper.selectFromLibrary();
|
||||
if (products == null || products.isEmpty())
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.set.library.empty"));
|
||||
}
|
||||
List<PrcPriceSetDetail> details = detailMapper.selectFixedDetailsFromLibrary(null, true);
|
||||
if (details == null || details.isEmpty())
|
||||
{
|
||||
details = detailMapper.selectFixedDetailsFromLibrary(new Date(), false);
|
||||
}
|
||||
if (details == null || details.isEmpty())
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.set.library.empty"));
|
||||
}
|
||||
Map<String, Object> result = new HashMap<String, Object>();
|
||||
result.put("products", products);
|
||||
result.put("details", details);
|
||||
result.put("message", MessageUtils.message("price.set.pull.success", Integer.valueOf(details.size())));
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
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);
|
||||
if (byProduct == null || byProduct.isEmpty())
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.set.import.noPriceRows"));
|
||||
}
|
||||
|
||||
List<PrcPriceSetDetail> details = new ArrayList<PrcPriceSetDetail>();
|
||||
if (existingDetails != null)
|
||||
{
|
||||
for (PrcPriceSetDetail d : existingDetails)
|
||||
{
|
||||
if (d != null)
|
||||
{
|
||||
details.add(d);
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
if (updateOnly)
|
||||
{
|
||||
assertUpdateOnlyWeightBandsMatch(byProduct, detailIndex);
|
||||
}
|
||||
|
||||
Map<String, PrcPriceSetProduct> productMap = new LinkedHashMap<String, PrcPriceSetProduct>();
|
||||
int updated = 0;
|
||||
int inserted = 0;
|
||||
int skippedFee = 0;
|
||||
int skippedUnmatched = 0;
|
||||
Set<Integer> touchedLocal = new HashSet<Integer>();
|
||||
|
||||
for (Map.Entry<String, List<BmsProductCountry>> e : byProduct.entrySet())
|
||||
{
|
||||
String productCode = e.getKey();
|
||||
List<PrcPriceSetDetail> candidates = detailIndex.get(productCode);
|
||||
if (candidates == null)
|
||||
{
|
||||
candidates = new ArrayList<PrcPriceSetDetail>();
|
||||
detailIndex.put(productCode, candidates);
|
||||
}
|
||||
if (updateOnly && candidates.isEmpty())
|
||||
{
|
||||
skippedUnmatched += e.getValue() == null ? 0 : e.getValue().size();
|
||||
continue;
|
||||
}
|
||||
String productName = resolveImportProductName(null, productCode, candidates);
|
||||
if (!updateOnly)
|
||||
{
|
||||
ensurePreviewProduct(productMap, productCode, productName);
|
||||
}
|
||||
for (BmsProductCountry row : e.getValue())
|
||||
{
|
||||
if (row == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
BigDecimal freight = row.getFreightPrice();
|
||||
BigDecimal registration = row.getRegistrationFee();
|
||||
List<PrcPriceSetDetail> hits = matchImportDetails(candidates, row);
|
||||
if (!hits.isEmpty())
|
||||
{
|
||||
if (freight == null && registration == null)
|
||||
{
|
||||
skippedFee++;
|
||||
continue;
|
||||
}
|
||||
for (PrcPriceSetDetail hit : hits)
|
||||
{
|
||||
int identity = System.identityHashCode(hit);
|
||||
if (touchedLocal.contains(Integer.valueOf(identity)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (freight != null)
|
||||
{
|
||||
hit.setFvValue(freight);
|
||||
}
|
||||
if (registration != null)
|
||||
{
|
||||
hit.setFvRegistrationFee(registration);
|
||||
}
|
||||
touchedLocal.add(Integer.valueOf(identity));
|
||||
updated++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (updateOnly)
|
||||
{
|
||||
skippedUnmatched++;
|
||||
continue;
|
||||
}
|
||||
if (freight == null && registration == null)
|
||||
{
|
||||
skippedFee++;
|
||||
continue;
|
||||
}
|
||||
PrcPriceSetDetail created = buildDetailFromImportRow(
|
||||
null, null, productCode, productName, row, freight, registration);
|
||||
candidates.add(created);
|
||||
details.add(created);
|
||||
inserted++;
|
||||
}
|
||||
}
|
||||
|
||||
if (updateOnly)
|
||||
{
|
||||
if (updated == 0)
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.set.import.noDetailWritten"));
|
||||
}
|
||||
// Keep product list from existing details codes
|
||||
for (PrcPriceSetDetail d : details)
|
||||
{
|
||||
if (d != null && StringUtils.isNotEmpty(d.getProductCode()))
|
||||
{
|
||||
ensurePreviewProduct(productMap, d.getProductCode(), d.getProductName());
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (inserted == 0 && updated == 0)
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.set.import.noDetailWritten"));
|
||||
}
|
||||
|
||||
Map<String, Object> result = new HashMap<String, Object>();
|
||||
result.put("products", new ArrayList<PrcPriceSetProduct>(productMap.values()));
|
||||
result.put("details", details);
|
||||
if (updateOnly)
|
||||
{
|
||||
result.put("message", MessageUtils.message("price.set.import.updateSuccess",
|
||||
Integer.valueOf(updated), Integer.valueOf(skippedUnmatched), Integer.valueOf(skippedFee)));
|
||||
}
|
||||
else
|
||||
{
|
||||
result.put("message", MessageUtils.message("price.set.import.upsertSuccess",
|
||||
Integer.valueOf(inserted), Integer.valueOf(updated), Integer.valueOf(skippedFee)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void ensurePreviewProduct(Map<String, PrcPriceSetProduct> productMap, String productCode, String productName)
|
||||
{
|
||||
if (productMap == null || StringUtils.isEmpty(productCode))
|
||||
{
|
||||
return;
|
||||
}
|
||||
String code = productCode.trim();
|
||||
String key = code.toUpperCase(Locale.ROOT);
|
||||
if (productMap.containsKey(key))
|
||||
{
|
||||
return;
|
||||
}
|
||||
PrcPriceSetProduct sp = new PrcPriceSetProduct();
|
||||
sp.setProductCode(code);
|
||||
BmsProduct lib = productMapper.selectBmsProductByCode(code);
|
||||
if (lib != null)
|
||||
{
|
||||
sp.setProductName(lib.getProductName());
|
||||
sp.setServiceLevel(lib.getServiceLevel());
|
||||
sp.setCargoAttr(lib.getCargoAttr());
|
||||
sp.setDestRegion(lib.getDestRegion());
|
||||
sp.setUpdateNote(lib.getUpdateNote());
|
||||
sp.setTaxMode(StringUtils.isNotEmpty(lib.getTaxMode()) ? lib.getTaxMode() : "TAX_INCLUDED");
|
||||
sp.setSourceProductId(lib.getProductId());
|
||||
sp.setSourceSheet(lib.getSourceSheet());
|
||||
}
|
||||
else
|
||||
{
|
||||
sp.setProductName(StringUtils.isNotEmpty(productName) ? productName : code);
|
||||
sp.setTaxMode("TAX_INCLUDED");
|
||||
}
|
||||
productMap.put(key, sp);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public String importDetailPrices(Long priceSetId, Long psvId, byte[] bytes, String filename, String operName) throws Exception
|
||||
{
|
||||
return importDetailPrices(priceSetId, psvId, bytes, filename, operName, null);
|
||||
return importDetailPrices(priceSetId, psvId, bytes, filename, operName, null, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public String importDetailPrices(Long priceSetId, Long psvId, byte[] bytes, String filename, String operName, String taskId) throws Exception
|
||||
{
|
||||
return importDetailPrices(priceSetId, psvId, bytes, filename, operName, taskId, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public String importDetailPrices(Long priceSetId, Long psvId, byte[] bytes, String filename, String operName,
|
||||
String taskId, boolean updateOnly) throws Exception
|
||||
{
|
||||
PrcPriceSet db = requireSet(priceSetId);
|
||||
assertChildEditable(db);
|
||||
@@ -972,9 +1214,15 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
list.add(d);
|
||||
}
|
||||
|
||||
if (updateOnly)
|
||||
{
|
||||
assertUpdateOnlyWeightBandsMatch(byProduct, detailIndex);
|
||||
}
|
||||
|
||||
int updated = 0;
|
||||
int inserted = 0;
|
||||
int skippedFee = 0;
|
||||
int skippedUnmatched = 0;
|
||||
Set<Long> touched = new HashSet<Long>();
|
||||
int productTotal = byProduct.size();
|
||||
int productIndex = 0;
|
||||
@@ -991,9 +1239,17 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
candidates = new ArrayList<PrcPriceSetDetail>();
|
||||
detailIndex.put(productCode, candidates);
|
||||
}
|
||||
if (updateOnly && candidates.isEmpty())
|
||||
{
|
||||
skippedUnmatched += e.getValue() == null ? 0 : e.getValue().size();
|
||||
continue;
|
||||
}
|
||||
String productName = resolveImportProductName(psvId, productCode, candidates);
|
||||
// Excel import is one of the only two paths that create set-product rows
|
||||
Long pspId = ensureSetProduct(priceSetId, psvId, productCode, productName);
|
||||
Long pspId = null;
|
||||
if (!updateOnly)
|
||||
{
|
||||
pspId = ensureSetProduct(priceSetId, psvId, productCode, productName);
|
||||
}
|
||||
for (BmsProductCountry row : e.getValue())
|
||||
{
|
||||
if (row == null)
|
||||
@@ -1005,7 +1261,6 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
List<PrcPriceSetDetail> hits = matchImportDetails(candidates, row);
|
||||
if (!hits.isEmpty())
|
||||
{
|
||||
// Existing band: only skip when Excel left both fees blank (keep old prices).
|
||||
if (freight == null && registration == null)
|
||||
{
|
||||
skippedFee++;
|
||||
@@ -1031,7 +1286,11 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// New band: skip when Excel has no usable fee (blank / slash = no service).
|
||||
if (updateOnly)
|
||||
{
|
||||
skippedUnmatched++;
|
||||
continue;
|
||||
}
|
||||
if (freight == null && registration == null)
|
||||
{
|
||||
skippedFee++;
|
||||
@@ -1049,12 +1308,20 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
inserted++;
|
||||
}
|
||||
}
|
||||
setProductMapper.bindDetailPspByPsvId(psvId);
|
||||
if (!updateOnly)
|
||||
{
|
||||
setProductMapper.bindDetailPspByPsvId(psvId);
|
||||
}
|
||||
reportImportProgress(taskId, priceSetId, 95, "finishing");
|
||||
if (inserted == 0 && updated == 0)
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.set.import.noDetailWritten"));
|
||||
}
|
||||
if (updateOnly)
|
||||
{
|
||||
return MessageUtils.message("price.set.import.updateSuccess",
|
||||
Integer.valueOf(updated), Integer.valueOf(skippedUnmatched), Integer.valueOf(skippedFee));
|
||||
}
|
||||
return MessageUtils.message("price.set.import.upsertSuccess",
|
||||
Integer.valueOf(inserted), Integer.valueOf(updated), Integer.valueOf(skippedFee));
|
||||
}
|
||||
@@ -1183,6 +1450,158 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
return pick == null ? null : pick.getPsvId();
|
||||
}
|
||||
|
||||
private void assertUpdateOnlyWeightBandsMatch(Map<String, List<BmsProductCountry>> byProduct,
|
||||
Map<String, List<PrcPriceSetDetail>> detailIndex)
|
||||
{
|
||||
if (byProduct == null || byProduct.isEmpty() || detailIndex == null || detailIndex.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
List<String> mismatches = new ArrayList<String>();
|
||||
for (Map.Entry<String, List<BmsProductCountry>> e : byProduct.entrySet())
|
||||
{
|
||||
String productCode = e.getKey();
|
||||
if (StringUtils.isEmpty(productCode) || e.getValue() == null || e.getValue().isEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String pcKey = productCode.trim().toUpperCase(Locale.ROOT);
|
||||
List<PrcPriceSetDetail> candidates = detailIndex.get(pcKey);
|
||||
if (candidates == null)
|
||||
{
|
||||
candidates = detailIndex.get(productCode);
|
||||
}
|
||||
if (candidates == null || candidates.isEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// Group Excel rows by country (code preferred, else name)
|
||||
Map<String, List<BmsProductCountry>> excelGroups = new LinkedHashMap<String, List<BmsProductCountry>>();
|
||||
for (BmsProductCountry row : e.getValue())
|
||||
{
|
||||
if (row == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String ck = countryKeyOfImport(row);
|
||||
if (StringUtils.isEmpty(ck))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
List<BmsProductCountry> g = excelGroups.get(ck);
|
||||
if (g == null)
|
||||
{
|
||||
g = new ArrayList<BmsProductCountry>();
|
||||
excelGroups.put(ck, g);
|
||||
}
|
||||
g.add(row);
|
||||
}
|
||||
for (Map.Entry<String, List<BmsProductCountry>> ge : excelGroups.entrySet())
|
||||
{
|
||||
List<BmsProductCountry> excelRows = ge.getValue();
|
||||
BmsProductCountry sample = excelRows.get(0);
|
||||
List<PrcPriceSetDetail> matchedDetails = new ArrayList<PrcPriceSetDetail>();
|
||||
for (PrcPriceSetDetail d : candidates)
|
||||
{
|
||||
if (d != null && sameCountry(d, sample))
|
||||
{
|
||||
matchedDetails.add(d);
|
||||
}
|
||||
}
|
||||
if (matchedDetails.isEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Set<String> excelBands = new TreeSet<String>();
|
||||
for (BmsProductCountry row : excelRows)
|
||||
{
|
||||
excelBands.add(weightBandLabel(row.getZoneCode(), row.getWeightMin(), row.getWeightMax()));
|
||||
}
|
||||
Set<String> pkgBands = new TreeSet<String>();
|
||||
for (PrcPriceSetDetail d : matchedDetails)
|
||||
{
|
||||
pkgBands.add(weightBandLabel(d.getZoneCode(), d.getWeightMin(), d.getWeightMax()));
|
||||
}
|
||||
if (pkgBands.equals(excelBands))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
mismatches.add(MessageUtils.message("price.set.import.weightBandMismatch.item",
|
||||
productCode,
|
||||
displayCountryOfImport(sample),
|
||||
joinBands(pkgBands),
|
||||
joinBands(excelBands)));
|
||||
}
|
||||
}
|
||||
if (!mismatches.isEmpty())
|
||||
{
|
||||
String joined = String.join(";", mismatches);
|
||||
if (joined.length() > 800)
|
||||
{
|
||||
joined = joined.substring(0, 800) + "...";
|
||||
}
|
||||
throw new ServiceException(MessageUtils.message("price.set.import.weightBandMismatch", joined));
|
||||
}
|
||||
}
|
||||
|
||||
private static String joinBands(Set<String> bands)
|
||||
{
|
||||
if (bands == null || bands.isEmpty())
|
||||
{
|
||||
return "-";
|
||||
}
|
||||
return String.join("、", bands);
|
||||
}
|
||||
|
||||
private static String countryKeyOfImport(BmsProductCountry row)
|
||||
{
|
||||
if (row == null)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
if (StringUtils.isNotEmpty(row.getCountryCode()))
|
||||
{
|
||||
return "C:" + row.getCountryCode().trim().toUpperCase(Locale.ROOT);
|
||||
}
|
||||
if (StringUtils.isNotEmpty(row.getCountryName()))
|
||||
{
|
||||
return "N:" + row.getCountryName().trim();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private static String displayCountryOfImport(BmsProductCountry row)
|
||||
{
|
||||
if (row == null)
|
||||
{
|
||||
return "-";
|
||||
}
|
||||
if (StringUtils.isNotEmpty(row.getCountryName()))
|
||||
{
|
||||
return row.getCountryName().trim();
|
||||
}
|
||||
if (StringUtils.isNotEmpty(row.getCountryCode()))
|
||||
{
|
||||
return row.getCountryCode().trim().toUpperCase(Locale.ROOT);
|
||||
}
|
||||
return "-";
|
||||
}
|
||||
|
||||
private static String weightBandLabel(String zoneCode, BigDecimal min, BigDecimal max)
|
||||
{
|
||||
String w = formatWeightLabel(min, max);
|
||||
if (StringUtils.isEmpty(w))
|
||||
{
|
||||
w = "-";
|
||||
}
|
||||
String z = zoneCode == null ? "" : zoneCode.trim();
|
||||
if (StringUtils.isNotEmpty(z))
|
||||
{
|
||||
return z + ":" + w;
|
||||
}
|
||||
return w;
|
||||
}
|
||||
|
||||
private List<PrcPriceSetDetail> matchImportDetails(List<PrcPriceSetDetail> candidates, BmsProductCountry row)
|
||||
{
|
||||
List<PrcPriceSetDetail> hits = new ArrayList<PrcPriceSetDetail>();
|
||||
@@ -1698,7 +2117,11 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
{
|
||||
if (detail.getProductCountryId() != null)
|
||||
{
|
||||
syncZoneFromProductCountry(detail);
|
||||
// Preview/pull already stamped weight bounds — skip per-row product_country reload.
|
||||
if (detail.getWeightMin() == null || detail.getWeightMax() == null)
|
||||
{
|
||||
syncZoneFromProductCountry(detail);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (StringUtils.isEmpty(detail.getProductCode()) || StringUtils.isEmpty(detail.getWeightLabel()))
|
||||
@@ -1732,9 +2155,14 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
{
|
||||
return;
|
||||
}
|
||||
BmsProductCountry query = new BmsProductCountry();
|
||||
query.setProductCode(detail.getProductCode());
|
||||
List<BmsProductCountry> rows = productCountryMapper.selectBmsProductCountryList(query);
|
||||
List<BmsProductCountry> rows = listOpenProductCountries(detail.getProductCode());
|
||||
if (rows == null || rows.isEmpty())
|
||||
{
|
||||
// Fallback: include expired rows when open-only cache misses
|
||||
BmsProductCountry query = new BmsProductCountry();
|
||||
query.setProductCode(detail.getProductCode());
|
||||
rows = productCountryMapper.selectBmsProductCountryList(query);
|
||||
}
|
||||
if (rows == null)
|
||||
{
|
||||
return;
|
||||
@@ -1750,6 +2178,28 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
}
|
||||
|
||||
private List<BmsProductCountry> listOpenProductCountries(String productCode)
|
||||
{
|
||||
if (StringUtils.isEmpty(productCode))
|
||||
{
|
||||
return java.util.Collections.emptyList();
|
||||
}
|
||||
Map<String, List<BmsProductCountry>> cache = countryCacheTl.get();
|
||||
if (cache != null)
|
||||
{
|
||||
String key = productCode.trim().toUpperCase(Locale.ROOT);
|
||||
List<BmsProductCountry> hit = cache.get(key);
|
||||
if (hit != null)
|
||||
{
|
||||
return hit;
|
||||
}
|
||||
List<BmsProductCountry> loaded = loadOpenProductCountries(productCode);
|
||||
cache.put(key, loaded);
|
||||
return loaded;
|
||||
}
|
||||
return loadOpenProductCountries(productCode);
|
||||
}
|
||||
|
||||
private List<BmsProductCountry> loadOpenProductCountries(String productCode)
|
||||
{
|
||||
if (StringUtils.isEmpty(productCode))
|
||||
{
|
||||
@@ -2315,6 +2765,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
/**
|
||||
* Upsert details by psdId. Keeps existing rows when id matches; inserts new; deletes orphans
|
||||
* not present in the payload. Avoids full delete+reinsert.
|
||||
* Create/finish path: caches set-product + product_country lookups, batch-inserts new rows.
|
||||
*/
|
||||
private void upsertVersionDetails(PrcPriceSet set, PrcPriceSetVersion version, List<PrcPriceSetDetail> details)
|
||||
{
|
||||
@@ -2332,93 +2783,156 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
}
|
||||
}
|
||||
}
|
||||
Set<Long> keepIds = new HashSet<Long>();
|
||||
int total = details == null ? 0 : details.size();
|
||||
int index = 0;
|
||||
|
||||
Map<String, Long> pspCache = new HashMap<String, Long>();
|
||||
PrcPriceSetProduct pq0 = new PrcPriceSetProduct();
|
||||
pq0.setPsvId(version.getPsvId());
|
||||
List<PrcPriceSetProduct> existingSp = setProductMapper.selectPrcPriceSetProductList(pq0);
|
||||
if (existingSp != null)
|
||||
{
|
||||
for (PrcPriceSetProduct sp : existingSp)
|
||||
{
|
||||
if (sp != null && StringUtils.isNotEmpty(sp.getProductCode()) && sp.getPspId() != null)
|
||||
{
|
||||
pspCache.put(sp.getProductCode().trim().toUpperCase(Locale.ROOT), sp.getPspId());
|
||||
}
|
||||
}
|
||||
}
|
||||
Map<String, List<BmsProductCountry>> countryCache = new HashMap<String, List<BmsProductCountry>>();
|
||||
if (details != null)
|
||||
{
|
||||
Set<String> codes = new HashSet<String>();
|
||||
for (PrcPriceSetDetail d : details)
|
||||
{
|
||||
validateDetail(set, d);
|
||||
d.setPriceSetId(set.getPriceSetId());
|
||||
d.setPsvId(version.getPsvId());
|
||||
Long pspId = lookupSetProductId(version.getPsvId(), d.getProductCode());
|
||||
if (pspId != null)
|
||||
if (d != null && StringUtils.isNotEmpty(d.getProductCode()))
|
||||
{
|
||||
d.setPspId(pspId);
|
||||
codes.add(d.getProductCode().trim());
|
||||
}
|
||||
if (d.getPsdId() != null && byId.containsKey(d.getPsdId()))
|
||||
}
|
||||
for (String code : codes)
|
||||
{
|
||||
countryCache.put(code.toUpperCase(Locale.ROOT), loadOpenProductCountries(code));
|
||||
}
|
||||
}
|
||||
setProductCacheTl.set(pspCache);
|
||||
countryCacheTl.set(countryCache);
|
||||
try
|
||||
{
|
||||
Set<Long> keepIds = new HashSet<Long>();
|
||||
int total = details == null ? 0 : details.size();
|
||||
int done = 0;
|
||||
List<PrcPriceSetDetail> toInsert = new ArrayList<PrcPriceSetDetail>();
|
||||
if (details != null)
|
||||
{
|
||||
for (PrcPriceSetDetail d : details)
|
||||
{
|
||||
detailMapper.updatePrcPriceSetDetail(d);
|
||||
keepIds.add(d.getPsdId());
|
||||
validateDetail(set, d);
|
||||
d.setPriceSetId(set.getPriceSetId());
|
||||
d.setPsvId(version.getPsvId());
|
||||
Long pspId = ensureSetProduct(set.getPriceSetId(), version.getPsvId(),
|
||||
d.getProductCode(), d.getProductName());
|
||||
if (pspId != null)
|
||||
{
|
||||
d.setPspId(pspId);
|
||||
}
|
||||
if (d.getPsdId() != null && byId.containsKey(d.getPsdId()))
|
||||
{
|
||||
detailMapper.updatePrcPriceSetDetail(d);
|
||||
keepIds.add(d.getPsdId());
|
||||
done++;
|
||||
reportDetailProgress(set, done, total);
|
||||
}
|
||||
else
|
||||
{
|
||||
d.setPsdId(null);
|
||||
toInsert.add(d);
|
||||
}
|
||||
}
|
||||
else
|
||||
}
|
||||
for (int i = 0; i < toInsert.size(); i += DETAIL_INSERT_BATCH)
|
||||
{
|
||||
int end = Math.min(i + DETAIL_INSERT_BATCH, toInsert.size());
|
||||
List<PrcPriceSetDetail> chunk = new ArrayList<PrcPriceSetDetail>(toInsert.subList(i, end));
|
||||
detailMapper.insertPrcPriceSetDetailBatch(chunk);
|
||||
for (PrcPriceSetDetail d : chunk)
|
||||
{
|
||||
d.setPsdId(null);
|
||||
detailMapper.insertPrcPriceSetDetail(d);
|
||||
if (d.getPsdId() != null)
|
||||
{
|
||||
keepIds.add(d.getPsdId());
|
||||
}
|
||||
}
|
||||
index++;
|
||||
if (total > 0 && (index == total || index % 50 == 0 || index == 1))
|
||||
done += chunk.size();
|
||||
reportDetailProgress(set, done, total);
|
||||
}
|
||||
|
||||
List<Long> orphanIds = new ArrayList<Long>();
|
||||
for (Long id : byId.keySet())
|
||||
{
|
||||
if (!keepIds.contains(id))
|
||||
{
|
||||
int pct = 25 + (int) Math.floor(index * 55.0 / total);
|
||||
if (pct > 80)
|
||||
orphanIds.add(id);
|
||||
}
|
||||
}
|
||||
if (!orphanIds.isEmpty())
|
||||
{
|
||||
for (Long id : orphanIds)
|
||||
{
|
||||
versionSurchargeMapper.deleteByPsdId(id);
|
||||
}
|
||||
detailMapper.deletePrcPriceSetDetailByIds(orphanIds.toArray(new Long[0]));
|
||||
}
|
||||
Set<String> keepCodes = new HashSet<String>();
|
||||
if (details != null)
|
||||
{
|
||||
for (PrcPriceSetDetail d : details)
|
||||
{
|
||||
if (d != null && StringUtils.isNotEmpty(d.getProductCode()))
|
||||
{
|
||||
pct = 80;
|
||||
keepCodes.add(d.getProductCode().trim());
|
||||
}
|
||||
reportSaveProgress(set, pct, "saving details " + index + "/" + total);
|
||||
}
|
||||
}
|
||||
}
|
||||
List<Long> orphanIds = new ArrayList<Long>();
|
||||
for (Long id : byId.keySet())
|
||||
{
|
||||
if (!keepIds.contains(id))
|
||||
PrcPriceSetProduct pq = new PrcPriceSetProduct();
|
||||
pq.setPsvId(version.getPsvId());
|
||||
List<PrcPriceSetProduct> setProducts = setProductMapper.selectPrcPriceSetProductList(pq);
|
||||
if (setProducts != null)
|
||||
{
|
||||
orphanIds.add(id);
|
||||
}
|
||||
}
|
||||
if (!orphanIds.isEmpty())
|
||||
{
|
||||
for (Long id : orphanIds)
|
||||
{
|
||||
versionSurchargeMapper.deleteByPsdId(id);
|
||||
}
|
||||
detailMapper.deletePrcPriceSetDetailByIds(orphanIds.toArray(new Long[0]));
|
||||
}
|
||||
// drop set-product rows whose codes no longer have any detail
|
||||
Set<String> keepCodes = new HashSet<String>();
|
||||
if (details != null)
|
||||
{
|
||||
for (PrcPriceSetDetail d : details)
|
||||
{
|
||||
if (d != null && StringUtils.isNotEmpty(d.getProductCode()))
|
||||
for (PrcPriceSetProduct sp : setProducts)
|
||||
{
|
||||
keepCodes.add(d.getProductCode().trim());
|
||||
if (sp == null || StringUtils.isEmpty(sp.getProductCode()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!keepCodes.contains(sp.getProductCode()))
|
||||
{
|
||||
setProductMapper.deleteByPsvIdAndCode(version.getPsvId(), sp.getProductCode());
|
||||
}
|
||||
}
|
||||
}
|
||||
version.setDetails(details);
|
||||
}
|
||||
PrcPriceSetProduct pq = new PrcPriceSetProduct();
|
||||
pq.setPsvId(version.getPsvId());
|
||||
List<PrcPriceSetProduct> setProducts = setProductMapper.selectPrcPriceSetProductList(pq);
|
||||
if (setProducts != null)
|
||||
finally
|
||||
{
|
||||
for (PrcPriceSetProduct sp : setProducts)
|
||||
{
|
||||
if (sp == null || StringUtils.isEmpty(sp.getProductCode()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!keepCodes.contains(sp.getProductCode()))
|
||||
{
|
||||
setProductMapper.deleteByPsvIdAndCode(version.getPsvId(), sp.getProductCode());
|
||||
}
|
||||
}
|
||||
setProductCacheTl.remove();
|
||||
countryCacheTl.remove();
|
||||
}
|
||||
}
|
||||
|
||||
private void reportDetailProgress(PrcPriceSet set, int done, int total)
|
||||
{
|
||||
if (total <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (done == total || done == 1 || done % 200 == 0)
|
||||
{
|
||||
int pct = 25 + (int) Math.floor(done * 55.0 / total);
|
||||
if (pct > 80)
|
||||
{
|
||||
pct = 80;
|
||||
}
|
||||
reportSaveProgress(set, pct, "saving details " + done + "/" + total);
|
||||
}
|
||||
version.setDetails(details);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2437,7 +2951,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
|
||||
/**
|
||||
* Ensure package-side product master row exists for this version+code; return psp_id.
|
||||
* Only called from pull-from-library / Excel import.
|
||||
* Uses request-scoped cache during bulk upsert to avoid N selects on same code.
|
||||
*/
|
||||
private Long ensureSetProduct(Long priceSetId, Long psvId, String productCode, String productNameHint)
|
||||
{
|
||||
@@ -2446,6 +2960,12 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
return null;
|
||||
}
|
||||
String code = productCode.trim();
|
||||
String key = code.toUpperCase(Locale.ROOT);
|
||||
Map<String, Long> cache = setProductCacheTl.get();
|
||||
if (cache != null && cache.containsKey(key))
|
||||
{
|
||||
return cache.get(key);
|
||||
}
|
||||
PrcPriceSetProduct existing = setProductMapper.selectByPsvIdAndCode(psvId, code);
|
||||
if (existing != null)
|
||||
{
|
||||
@@ -2455,6 +2975,10 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
existing.setProductName(productNameHint);
|
||||
setProductMapper.updatePrcPriceSetProduct(existing);
|
||||
}
|
||||
if (cache != null)
|
||||
{
|
||||
cache.put(key, existing.getPspId());
|
||||
}
|
||||
return existing.getPspId();
|
||||
}
|
||||
PrcPriceSetProduct row = new PrcPriceSetProduct();
|
||||
@@ -2487,6 +3011,10 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
row.setTaxMode("TAX_INCLUDED");
|
||||
}
|
||||
setProductMapper.insertPrcPriceSetProduct(row);
|
||||
if (cache != null)
|
||||
{
|
||||
cache.put(key, row.getPspId());
|
||||
}
|
||||
return row.getPspId();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user