导入批量修改价格明细
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
+68
@@ -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()
|
||||
{
|
||||
|
||||
+271
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user