导入批量修改价格明细

This commit is contained in:
wuweihua
2026-09-28 18:01:26 +08:00
parent c278ed76f7
commit b247bc2fd2
19 changed files with 777 additions and 54 deletions
@@ -37,6 +37,10 @@ public class PrcPriceSetDetail implements Serializable
private Long productCountryId;
private BigDecimal fvValue;
private BigDecimal fvRegistrationFee;
/** product base freight from bms_product_country (display only) */
private BigDecimal baseFreightPrice;
/** product base registration from bms_product_country (display only) */
private BigDecimal baseRegistrationFee;
private BigDecimal discountValue;
/** UP / DOWN / HALF_UP */
private String roundMode;
@@ -1,5 +1,6 @@
package com.bms.price.mapper;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import org.apache.ibatis.annotations.Param;
@@ -75,4 +76,10 @@ public interface PrcPriceSetDetailMapper
@Param("priceSetId") Long priceSetId,
@Param("psvId") Long psvId,
@Param("productCode") String productCode);
/** Update package freight / registration only */
public int updateDetailPrices(
@Param("psdId") Long psdId,
@Param("fvValue") BigDecimal fvValue,
@Param("fvRegistrationFee") BigDecimal fvRegistrationFee);
}
@@ -58,4 +58,13 @@ public interface IBmsProductService
/** Distinct base-price version options (by effective_date desc) for package create */
public List<BmsProductPriceVersion> selectPriceVersionOptions();
/**
* Parse Weishi B2C quote Excel into productCode -> country/zone/weight price rows (no persist).
* Used by price-set detail price import.
*/
public java.util.Map<String, List<BmsProductCountry>> parseQuoteExcelByProduct(MultipartFile file) throws Exception;
/** Parse quote Excel from in-memory bytes (async import). */
public java.util.Map<String, List<BmsProductCountry>> parseQuoteExcelByProduct(byte[] bytes, String filename) throws Exception;
}
@@ -1,6 +1,7 @@
package com.bms.price.service;
import java.util.Date;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
import java.util.Map;
import com.bms.price.domain.PrcCustomerPriceSet;
@@ -96,4 +97,19 @@ public interface IPrcPriceSetService
/** poll async save progress */
public PrcPriceSetSaveTask getSaveTask(String taskId);
/**
* Submit async import of Weishi B2C quote Excel into FIXED package detail prices.
* @return taskId for polling getSaveTask
*/
public String submitImportDetailPricesAsync(Long priceSetId, Long psvId, MultipartFile file, String operName) throws Exception;
/**
* Sync worker: parse bytes and update FIXED detail prices matched by
* product + country + zone(if any) + weight band.
*/
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;
}
@@ -8,6 +8,7 @@ import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -421,6 +422,73 @@ public class BmsProductServiceImpl implements IBmsProductService
}
}
@Override
public Map<String, List<BmsProductCountry>> parseQuoteExcelByProduct(MultipartFile file) throws Exception
{
if (file == null || file.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.product.import.fileEmpty"));
}
return parseQuoteExcelByProduct(file.getBytes(), file.getOriginalFilename());
}
@Override
public Map<String, List<BmsProductCountry>> parseQuoteExcelByProduct(byte[] bytes, String filename) throws Exception
{
if (bytes == null || bytes.length == 0)
{
throw new ServiceException(MessageUtils.message("price.product.import.fileEmpty"));
}
if (filename == null || !(filename.toLowerCase(Locale.ROOT).endsWith(".xlsx")
|| filename.toLowerCase(Locale.ROOT).endsWith(".xls")))
{
throw new ServiceException(MessageUtils.message("price.product.import.fileType"));
}
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.sheetNotFound"));
}
List<BmsProduct> products = dedupeCatalogProducts(parseCatalog(catalog));
if (products.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.product.import.noData"));
}
Map<String, BmsRegion> regionByNameCn = loadRegionByNameCn();
List<BmsRegion> euRegions = loadEuRegions(regionByNameCn);
Map<String, PrcZone> zoneByCountryAndCode = loadZoneByCountryAndCode();
Set<String> unmatchedCountries = new HashSet<>();
Map<String, List<BmsProductCountry>> out = new LinkedHashMap<>();
for (BmsProduct product : products)
{
if (product == null || StringUtils.isEmpty(product.getProductCode()))
{
continue;
}
Sheet productSheet = findProductSheet(workbook, product);
if (productSheet == null)
{
continue;
}
List<BmsProductCountry> rows = parseProductSheet(productSheet, product,
regionByNameCn, euRegions, zoneByCountryAndCode, unmatchedCountries);
if (rows == null || rows.isEmpty())
{
continue;
}
String code = product.getProductCode().trim().toUpperCase(Locale.ROOT);
out.put(code, rows);
}
if (out.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.set.import.noPriceRows"));
}
return out;
}
}
@Override
public List<BmsProductPriceVersion> selectPriceVersionOptions()
{
@@ -9,12 +9,14 @@ import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Locale;
import java.util.Set;
import java.util.zip.CRC32;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import com.bms.common.exception.ServiceException;
import com.bms.common.utils.DateUtils;
import com.bms.common.utils.MessageUtils;
@@ -38,6 +40,7 @@ import com.bms.price.mapper.PrcPriceSetVersionMapper;
import com.bms.price.mapper.PrcPriceSetVersionSurchargeMapper;
import com.bms.price.mapper.PrcSurchargeMapper;
import com.bms.price.mapper.PrcSurchargeRelMapper;
import com.bms.price.service.IBmsProductService;
import com.bms.price.service.IPrcPriceSetService;
import com.bms.price.task.PrcPriceSetSaveAsyncRunner;
@@ -87,6 +90,10 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
@Autowired
private BmsProductCountryMapper productCountryMapper;
@Lazy
@Autowired
private IBmsProductService productService;
@Lazy
@Autowired
private PrcPriceSetSaveAsyncRunner saveAsyncRunner;
@@ -797,6 +804,270 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
return rows;
}
@Override
public String submitImportDetailPricesAsync(Long priceSetId, Long psvId, MultipartFile file, String operName) throws Exception
{
PrcPriceSet db = requireSet(priceSetId);
assertChildEditable(db);
if (!TYPE_FIXED.equals(db.getSetType()))
{
throw new ServiceException(MessageUtils.message("price.set.import.fixedOnly"));
}
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"));
}
byte[] bytes = file.getBytes();
if (psvId == null)
{
psvId = resolveCurrentPsvId(db);
}
String taskId = IdUtils.fastSimpleUUID();
saveAsyncRunner.put(taskId, PrcPriceSetSaveTask.STATUS_PENDING, 0, "queued", priceSetId, null);
saveAsyncRunner.runImportPrices(taskId, priceSetId, psvId, bytes, filename, operName);
return taskId;
}
@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);
}
@Override
@Transactional(rollbackFor = Exception.class)
public String importDetailPrices(Long priceSetId, Long psvId, byte[] bytes, String filename, String operName, String taskId) throws Exception
{
PrcPriceSet db = requireSet(priceSetId);
assertChildEditable(db);
if (!TYPE_FIXED.equals(db.getSetType()))
{
throw new ServiceException(MessageUtils.message("price.set.import.fixedOnly"));
}
if (psvId == null)
{
psvId = resolveCurrentPsvId(db);
}
reportImportProgress(taskId, priceSetId, 20, "parsing excel");
Map<String, List<BmsProductCountry>> byProduct = productService.parseQuoteExcelByProduct(bytes, filename);
reportImportProgress(taskId, priceSetId, 50, "matching details");
PrcPriceSetDetail q = new PrcPriceSetDetail();
q.setPriceSetId(priceSetId);
if (psvId != null)
{
q.setPsvId(psvId);
}
List<PrcPriceSetDetail> details = detailMapper.selectPrcPriceSetDetailList(q);
if (details == null || details.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.set.import.noDetails"));
}
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);
}
int updated = 0;
int unmatched = 0;
int skippedProduct = 0;
Set<Long> touched = new HashSet<Long>();
for (Map.Entry<String, List<BmsProductCountry>> e : byProduct.entrySet())
{
String productCode = e.getKey();
List<PrcPriceSetDetail> candidates = detailIndex.get(productCode);
if (candidates == null || candidates.isEmpty())
{
skippedProduct++;
continue;
}
for (BmsProductCountry row : e.getValue())
{
if (row == null)
{
continue;
}
List<PrcPriceSetDetail> hits = matchImportDetails(candidates, row);
if (hits.isEmpty())
{
unmatched++;
continue;
}
for (PrcPriceSetDetail hit : hits)
{
if (hit.getPsdId() == null || touched.contains(hit.getPsdId()))
{
continue;
}
detailMapper.updateDetailPrices(hit.getPsdId(), row.getFreightPrice(), row.getRegistrationFee());
hit.setFvValue(row.getFreightPrice());
hit.setFvRegistrationFee(row.getRegistrationFee());
touched.add(hit.getPsdId());
updated++;
}
}
}
reportImportProgress(taskId, priceSetId, 95, "finishing");
return MessageUtils.message("price.set.import.success",
Integer.valueOf(updated), Integer.valueOf(unmatched), Integer.valueOf(skippedProduct));
}
private void reportImportProgress(String taskId, Long priceSetId, int percent, String message)
{
if (StringUtils.isEmpty(taskId) || saveAsyncRunner == null)
{
return;
}
saveAsyncRunner.put(taskId, PrcPriceSetSaveTask.STATUS_RUNNING, percent, message, priceSetId, null);
}
/** Resolve editable version id: draft > pending > effect > first version. */
private Long resolveCurrentPsvId(PrcPriceSet set)
{
if (set == null)
{
return null;
}
if (set.getVersion() != null && set.getVersion().getPsvId() != null)
{
return set.getVersion().getPsvId();
}
List<PrcPriceSetVersion> versions = set.getVersions();
if (versions == null || versions.isEmpty())
{
PrcPriceSetVersion q = new PrcPriceSetVersion();
q.setPriceSetId(set.getPriceSetId());
versions = versionMapper.selectPrcPriceSetVersionList(q);
}
if (versions == null || versions.isEmpty())
{
return null;
}
PrcPriceSetVersion draft = null;
PrcPriceSetVersion pending = null;
PrcPriceSetVersion effect = null;
for (PrcPriceSetVersion v : versions)
{
if (v == null)
{
continue;
}
if (VER_DRAFT.equals(v.getStatus()) && draft == null)
{
draft = v;
}
else if (VER_PENDING.equals(v.getStatus()) && pending == null)
{
pending = v;
}
else if (VER_EFFECT.equals(v.getStatus()) && effect == null)
{
effect = v;
}
}
PrcPriceSetVersion pick = draft != null ? draft : (pending != null ? pending : (effect != null ? effect : versions.get(0)));
return pick == null ? null : pick.getPsvId();
}
private List<PrcPriceSetDetail> matchImportDetails(List<PrcPriceSetDetail> candidates, BmsProductCountry row)
{
List<PrcPriceSetDetail> hits = new ArrayList<PrcPriceSetDetail>();
if (candidates == null || row == null)
{
return hits;
}
String importZone = normZone(row.getZoneCode());
for (PrcPriceSetDetail d : candidates)
{
if (!sameCountry(d, row))
{
continue;
}
if (!sameWeight(d.getWeightMin(), d.getWeightMax(), row.getWeightMin(), row.getWeightMax()))
{
continue;
}
String detailZone = normZone(d.getZoneCode());
if (StringUtils.isNotEmpty(importZone))
{
if (!importZone.equals(detailZone))
{
continue;
}
}
else if (StringUtils.isNotEmpty(detailZone))
{
// Excel has no zone: only match non-zone details
continue;
}
hits.add(d);
}
return hits;
}
private boolean sameCountry(PrcPriceSetDetail d, BmsProductCountry row)
{
if (d == null || row == null)
{
return false;
}
if (StringUtils.isNotEmpty(d.getCountryCode()) && StringUtils.isNotEmpty(row.getCountryCode()))
{
return d.getCountryCode().trim().equalsIgnoreCase(row.getCountryCode().trim());
}
if (StringUtils.isNotEmpty(d.getCountryName()) && StringUtils.isNotEmpty(row.getCountryName()))
{
return d.getCountryName().trim().equals(row.getCountryName().trim());
}
return false;
}
private boolean sameWeight(BigDecimal aMin, BigDecimal aMax, BigDecimal bMin, BigDecimal bMax)
{
return decimalEq(aMin, bMin) && decimalEq(aMax, bMax);
}
private boolean decimalEq(BigDecimal a, BigDecimal b)
{
if (a == null && b == null)
{
return true;
}
if (a == null || b == null)
{
return false;
}
return a.compareTo(b) == 0;
}
private String normZone(String zone)
{
if (StringUtils.isEmpty(zone))
{
return "";
}
return zone.trim().toUpperCase(Locale.ROOT);
}
@Override
public int updateDetail(PrcPriceSetDetail detail)
{
@@ -76,6 +76,30 @@ public class PrcPriceSetSaveAsyncRunner
}
}
/** Background import of quote Excel into package detail prices. */
@Async("threadPoolTaskExecutor")
public void runImportPrices(String taskId, Long priceSetId, Long psvId, byte[] bytes, String filename, String operName)
{
try
{
put(taskId, PrcPriceSetSaveTask.STATUS_RUNNING, 10, "parsing excel", priceSetId, null);
String message = priceSetService.importDetailPrices(priceSetId, psvId, bytes, filename, operName, taskId);
put(taskId, PrcPriceSetSaveTask.STATUS_SUCCESS, 100, message, priceSetId, null);
log.info("async import prices done, taskId={}, priceSetId={}", taskId, priceSetId);
}
catch (Exception e)
{
log.error("async import prices failed, taskId={}, priceSetId={}", taskId, priceSetId, e);
String msg = e.getMessage();
if (StringUtils.isEmpty(msg))
{
msg = e.getClass().getSimpleName();
}
put(taskId, PrcPriceSetSaveTask.STATUS_FAIL, 100, "failed", priceSetId, msg);
}
}
public void put(String taskId, String status, int percent, String message, Long priceSetId, String error)
{
PrcPriceSetSaveTask task = new PrcPriceSetSaveTask();