Merge branch 'test'

This commit is contained in:
caojia
2026-11-02 16:54:26 +08:00
18 changed files with 1399 additions and 40 deletions
@@ -102,6 +102,16 @@ public class PriceSetController extends BaseController
return toAjax(priceSetService.updatePrcPriceSet(set));
}
/** Update package name and note only. */
@PreAuthorize("@ss.hasPermi('price:set:edit')")
@Log(title = "价格套餐基本信息", businessType = BusinessType.UPDATE)
@PutMapping("/basic")
public AjaxResult editBasic(@RequestBody PrcPriceSet set)
{
set.setUpdateBy(getUsername());
return toAjax(priceSetService.updateBasicInfo(set));
}
@PreAuthorize("@ss.hasPermi('price:set:remove')")
@Log(title = "价格套餐", businessType = BusinessType.DELETE)
@DeleteMapping("/{priceSetIds}")
@@ -34,8 +34,12 @@ public class PrcPriceSet extends BaseEntity
private Integer productCount;
private Integer customerCount;
/** List display: bound customer code/name summary (not a DB column). */
private String boundCustomers;
private Boolean expired;
private String keyword;
/** Query only: filter packages by bound customer id. */
private Long customerId;
private String submitFlag;
/** current version on create/edit save */
private PrcPriceSetVersion version;
@@ -21,6 +21,9 @@ public interface PrcPriceSetMapper
public int updatePrcPriceSet(PrcPriceSet set);
/** Update name / note only (does not touch set_type / discount / status). */
public int updateBasicInfo(PrcPriceSet set);
public int updatePrcPriceSetStatus(PrcPriceSet set);
public int deletePrcPriceSetByIds(Long[] priceSetIds);
@@ -26,6 +26,9 @@ public interface IPrcPriceSetService
public int updatePrcPriceSet(PrcPriceSet set);
/** Update package name and note only; allowed for effect / draft / stopped. */
public int updateBasicInfo(PrcPriceSet set);
public int deletePrcPriceSetByIds(Long[] priceSetIds);
public int submit(Long priceSetId, String updateBy);
@@ -383,12 +383,16 @@ public class BmsProductServiceImpl implements IBmsProductService
try (InputStream is = new java.io.ByteArrayInputStream(bytes); Workbook workbook = WorkbookFactory.create(is))
{
assertNotSingleCountryQuoteForMultiImport(workbook);
if (workbookHasInlineProductColumns(workbook))
{
return importProductExcelByInline(workbook, bytes, filename, operName, taskId, forcedEff);
}
Sheet catalog = workbook.getSheet(CATALOG_SHEET);
if (catalog == null)
{
throw new ServiceException(MessageUtils.message("price.product.import.sheetNotFound"));
}
assertNotSingleCountryQuoteForMultiImport(workbook);
List<BmsProduct> products = parseCatalog(catalog);
if (products.isEmpty())
@@ -433,7 +437,7 @@ public class BmsProductServiceImpl implements IBmsProductService
try
{
int[] nums = importOneProductWithDeadlockRetry(workbook, product, operName, regionByNameCn,
euRegions, zoneByCountryAndCode, unmatchedCountries, forcedEff, importPsvId);
euRegions, zoneByCountryAndCode, unmatchedCountries, forcedEff, importPsvId, null);
successNum += nums[0];
skipNum += nums[1];
countryNum += nums[2];
@@ -684,12 +688,18 @@ public class BmsProductServiceImpl implements IBmsProductService
}
try (InputStream is = new java.io.ByteArrayInputStream(bytes); Workbook workbook = WorkbookFactory.create(is))
{
assertNotSingleCountryQuoteForMultiImport(workbook);
// Detail sheets with inline product-code / product-name column: identify by those
// columns, not by catalog sheet-name matching (variants like RM/IP/EV share one sheet).
if (workbookHasInlineProductColumns(workbook))
{
return readQuoteWorkbookByInlineProductCode(workbook);
}
Sheet catalog = workbook.getSheet(CATALOG_SHEET);
if (catalog == null)
{
throw new ServiceException(MessageUtils.message("price.product.import.sheetNotFound"));
}
assertNotSingleCountryQuoteForMultiImport(workbook);
List<BmsProduct> products = dedupeCatalogProducts(parseCatalog(catalog));
if (products.isEmpty())
{
@@ -791,6 +801,543 @@ public class BmsProductServiceImpl implements IBmsProductService
private Map<String, List<BmsProductCountry>> byCode;
}
/**
* Formal product import when price sheets carry inline product-code / product-name columns.
* Identifies products from detail rows (not catalog sheet-name matching).
*/
private String importProductExcelByInline(Workbook workbook, byte[] bytes, String filename,
String operName, String taskId, Date forcedEff) throws Exception
{
QuoteWorkbook parsed = readQuoteWorkbookByInlineProductCode(workbook);
List<BmsProduct> products = enrichInlineProductsFromCatalog(workbook, parsed.products);
if (products.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.product.import.noData"));
}
reportImportProgress(taskId, 20, "parsing excel");
assertImportProductsEffectiveNotBeforePrev(products, forcedEff);
Map<String, BmsRegion> regionByNameCn = loadRegionByNameCn();
if (regionByNameCn.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.product.import.regionEmpty"));
}
List<BmsRegion> euRegions = loadEuRegions(regionByNameCn);
Map<String, PrcZone> zoneByCountryAndCode = loadZoneByCountryAndCode();
Long importPsvId = forcedEff == null
? catalogVersionSupport.ensureDraftVersion(PriceType.PRODUCT, null, false).getPsvId()
: catalogVersionSupport.requirePsvId(PriceType.PRODUCT, null, forcedEff);
priceExcelFileService.saveImportExcel(
PrcPriceExcelFile.TYPE_PRODUCT, forcedEff, bytes, filename, operName, importPsvId);
int successNum = 0;
int skipNum = 0;
int countryNum = 0;
Set<String> unmatchedCountries = new HashSet<String>();
StringBuilder failureMsg = new StringBuilder();
int total = products.size();
int index = 0;
for (BmsProduct product : products)
{
index++;
if (product == null || StringUtils.isEmpty(product.getProductCode()))
{
continue;
}
String code = product.getProductCode().trim().toUpperCase(Locale.ROOT);
reportImportProductProgress(taskId, index, total, product.getProductCode());
List<BmsProductCountry> countries = parsed.byCode.get(code);
try
{
int[] nums = importOneProductWithDeadlockRetry(workbook, product, operName, regionByNameCn,
euRegions, zoneByCountryAndCode, unmatchedCountries, forcedEff, importPsvId, countries);
successNum += nums[0];
skipNum += nums[1];
countryNum += nums[2];
}
catch (Exception e)
{
String tip = e.getMessage();
if (StringUtils.isEmpty(tip))
{
tip = e.getClass().getSimpleName();
}
failureMsg.append("<br/>").append(product.getProductCode()).append(":").append(tip);
}
}
reportImportProgress(taskId, 95, "finishing");
if (failureMsg.length() > 0)
{
return MessageUtils.message("price.product.import.partialFail",
Integer.valueOf(successNum), Integer.valueOf(skipNum), failureMsg.toString());
}
String msg = MessageUtils.message("price.product.import.success", successNum, skipNum, countryNum)
+ "<br/>" + MessageUtils.message("price.product.import.success.detail1")
+ "<br/>" + MessageUtils.message("price.product.import.success.detail2");
if (!unmatchedCountries.isEmpty())
{
msg += "<br/>" + MessageUtils.message("price.product.import.success.unmatched",
String.join("、", unmatchedCountries));
}
return msg;
}
/** Prefer catalog metadata when code matches; keep detail product name for variants. */
private List<BmsProduct> enrichInlineProductsFromCatalog(Workbook workbook, List<BmsProduct> inlineProducts)
{
Map<String, BmsProduct> catalogByCode = new HashMap<String, BmsProduct>();
Sheet catalog = workbook == null ? null : workbook.getSheet(CATALOG_SHEET);
if (catalog != null)
{
try
{
for (BmsProduct p : dedupeCatalogProducts(parseCatalog(catalog)))
{
if (p == null || StringUtils.isEmpty(p.getProductCode()))
{
continue;
}
catalogByCode.put(p.getProductCode().trim().toUpperCase(Locale.ROOT), p);
}
}
catch (Exception ignored)
{
// Catalog optional for inline import.
}
}
List<BmsProduct> out = new ArrayList<BmsProduct>();
if (inlineProducts == null)
{
return out;
}
for (BmsProduct inline : inlineProducts)
{
if (inline == null || StringUtils.isEmpty(inline.getProductCode()))
{
continue;
}
String code = inline.getProductCode().trim().toUpperCase(Locale.ROOT);
BmsProduct cat = catalogByCode.get(code);
if (cat == null)
{
out.add(inline);
continue;
}
BmsProduct merged = new BmsProduct();
merged.setProductCode(inline.getProductCode());
merged.setProductName(StringUtils.isNotEmpty(inline.getProductName())
? inline.getProductName() : cat.getProductName());
merged.setServiceLevel(cat.getServiceLevel());
merged.setCargoAttr(cat.getCargoAttr());
merged.setDestRegion(cat.getDestRegion());
merged.setUpdateNote(cat.getUpdateNote());
merged.setEffectiveDate(cat.getEffectiveDate());
merged.setTaxMode(StringUtils.isNotEmpty(cat.getTaxMode())
? cat.getTaxMode() : resolveTaxMode(merged.getProductName()));
out.add(merged);
}
return out;
}
/**
* True when any multi-country price sheet has an inline product-code or product-name column
* (产品代码 / 产品对接代码 / 产品名称).
*/
private boolean workbookHasInlineProductColumns(Workbook workbook)
{
if (workbook == null)
{
return false;
}
for (int i = 0; i < workbook.getNumberOfSheets(); i++)
{
String sheetName = workbook.getSheetName(i);
if (shouldSkipCountryPriceSheet(sheetName))
{
continue;
}
Sheet sheet = workbook.getSheetAt(i);
if (sheet == null)
{
continue;
}
int[] countryHeader = findHeaderCell(sheet, "国家");
if (countryHeader == null)
{
continue;
}
Row head = sheet.getRow(countryHeader[0]);
if (head == null)
{
continue;
}
if (firstCol(head, "产品代码", "产品对接代码") >= 0
|| findColByHeader(head, "产品名称", 0) >= 0)
{
return true;
}
}
return false;
}
/**
* Multi-country parse keyed by inline product-code column on price sheets.
* Does not use 产品目录 sheet matching.
*/
private QuoteWorkbook readQuoteWorkbookByInlineProductCode(Workbook workbook) throws Exception
{
Map<String, String> catalogCodeByName = buildCatalogCodeByName(workbook);
Map<String, BmsRegion> regionByNameCn = loadRegionByNameCn();
List<BmsRegion> euRegions = loadEuRegions(regionByNameCn);
Map<String, PrcZone> zoneByCountryAndCode = loadZoneByCountryAndCode();
Set<String> unmatchedCountries = new HashSet<String>();
Map<String, List<BmsProductCountry>> out = new LinkedHashMap<String, List<BmsProductCountry>>();
Map<String, BmsProduct> productMeta = new LinkedHashMap<String, BmsProduct>();
List<String> tips = new ArrayList<String>();
for (int i = 0; i < workbook.getNumberOfSheets(); i++)
{
String sheetName = workbook.getSheetName(i);
if (shouldSkipCountryPriceSheet(sheetName))
{
continue;
}
Sheet sheet = workbook.getSheetAt(i);
if (sheet == null || findHeaderCell(sheet, "国家") == null)
{
continue;
}
Map<String, List<BmsProductCountry>> sheetByCode = parseProductSheetByInlineCode(
sheet, catalogCodeByName, regionByNameCn, euRegions, zoneByCountryAndCode, unmatchedCountries);
if (sheetByCode == null || sheetByCode.isEmpty())
{
continue;
}
for (Map.Entry<String, List<BmsProductCountry>> e : sheetByCode.entrySet())
{
if (e == null || StringUtils.isEmpty(e.getKey()) || e.getValue() == null || e.getValue().isEmpty())
{
continue;
}
String code = e.getKey();
List<BmsProductCountry> rows = e.getValue();
List<BmsProductCountry> existing = out.get(code);
if (existing == null)
{
out.put(code, new ArrayList<BmsProductCountry>(rows));
}
else
{
existing.addAll(rows);
}
if (!productMeta.containsKey(code))
{
BmsProduct p = new BmsProduct();
p.setProductCode(code);
String name = null;
for (BmsProductCountry r : rows)
{
if (r != null && StringUtils.isNotEmpty(r.getProductName()))
{
name = r.getProductName();
break;
}
}
p.setProductName(StringUtils.isNotEmpty(name) ? name : code);
p.setTaxMode(resolveTaxMode(p.getProductName()));
productMeta.put(code, p);
}
}
}
for (Map.Entry<String, List<BmsProductCountry>> e : out.entrySet())
{
BmsProduct p = productMeta.get(e.getKey());
String tip = buildDuplicateCountryWeightTip(p, e.getValue());
if (StringUtils.isNotEmpty(tip))
{
tips.add(tip);
}
}
if (!tips.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.product.import.dupCountry",
String.join("<br/>", tips)));
}
if (out.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.set.import.noPriceRows"));
}
QuoteWorkbook parsed = new QuoteWorkbook();
parsed.products = new ArrayList<BmsProduct>(productMeta.values());
parsed.byCode = out;
return parsed;
}
/**
* Parse one multi-country price sheet grouped by inline product code / product name.
* Prefer product-code column; if only product-name exists, resolve code via catalog name map.
* Country / product-code / product-name / size / last-mile / lead-time carry forward
* when subsequent weight-band rows leave those cells blank.
*/
private Map<String, List<BmsProductCountry>> parseProductSheetByInlineCode(Sheet sheet,
Map<String, String> catalogCodeByName, Map<String, BmsRegion> regionByNameCn,
List<BmsRegion> euRegions, Map<String, PrcZone> zoneByCountryAndCode,
Set<String> unmatchedCountries)
{
Map<String, List<BmsProductCountry>> out = new LinkedHashMap<String, List<BmsProductCountry>>();
if (sheet == null)
{
return out;
}
Date sheetEffective = parseSheetEffectiveDate(sheet);
int[] countryHeader = findHeaderCell(sheet, "国家");
if (countryHeader == null)
{
return out;
}
int headerRowIdx = countryHeader[0];
Row headerRow = sheet.getRow(headerRowIdx);
int colCountry = countryHeader[1];
int colProductCode = firstCol(headerRow, "产品代码", "产品对接代码");
int colProductName = findColByHeader(headerRow, "产品名称", 0);
if (colProductCode < 0 && colProductName < 0)
{
return out;
}
int colWeight = findColByHeader(headerRow, "重量", colCountry + 1);
int colFreight = findColByHeader(headerRow, "运费", colCountry + 1);
int colRegister = findColByHeader(headerRow, "挂号", colCountry + 1);
int colSize = findColByHeader(headerRow, "尺寸", colCountry + 1);
int colLastMile = findColByHeader(headerRow, "尾程", colCountry + 1);
int colLeadTime = findColByHeader(headerRow, "时效", colCountry + 1);
if (colWeight < 0 || colFreight < 0)
{
return out;
}
String currentCountryRaw = null;
String currentProductCode = null;
String currentProductName = null;
String currentSize = null;
String currentLastMile = null;
String currentLeadTime = null;
int lastRow = sheet.getLastRowNum();
for (int i = headerRowIdx + 1; i <= lastRow; i++)
{
Row row = sheet.getRow(i);
if (row == null)
{
continue;
}
String countryCell = cellString(row.getCell(colCountry));
String weightLabel = cellString(row.getCell(colWeight));
if ("国家".equals(countryCell)
|| (weightLabel != null && weightLabel.contains("重量段")))
{
continue;
}
if (StringUtils.isNotEmpty(countryCell))
{
currentCountryRaw = countryCell;
}
boolean productChanged = false;
if (colProductCode >= 0)
{
String codeCell = normalizeImportProductCode(cellString(row.getCell(colProductCode)));
if (StringUtils.isNotEmpty(codeCell))
{
String newCode = codeCell.trim().toUpperCase(Locale.ROOT);
if (currentProductCode == null || !newCode.equals(currentProductCode))
{
productChanged = true;
currentProductCode = newCode;
}
}
}
if (colProductName >= 0)
{
String nameCell = cellString(row.getCell(colProductName));
if (StringUtils.isNotEmpty(nameCell) && !isSectionTitle(nameCell))
{
String newName = nameCell.replace("\n", " ").replace("\r", " ").trim();
if (currentProductName == null || !newName.equals(currentProductName))
{
productChanged = true;
currentProductName = newName;
// Name-only sheets: resolve product code from catalog by product name.
if (colProductCode < 0)
{
String mapped = resolveCodeByProductName(currentProductName, catalogCodeByName);
if (StringUtils.isNotEmpty(mapped))
{
currentProductCode = mapped;
}
else
{
currentProductCode = null;
}
}
}
}
}
if (productChanged)
{
currentSize = null;
currentLastMile = null;
currentLeadTime = null;
}
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)));
}
if (StringUtils.isEmpty(currentCountryRaw) || StringUtils.isEmpty(currentProductCode))
{
continue;
}
BigDecimal[] range = parseWeightRange(weightLabel);
if (range == null)
{
continue;
}
BigDecimal freight = cellDecimal(row.getCell(colFreight));
BigDecimal registration = colRegister >= 0 ? cellDecimal(row.getCell(colRegister)) : null;
Date eff = sheetEffective;
ZoneCodeAbbrevs.CountryZoneParts parts = ZoneCodeAbbrevs.splitCountryZone(currentCountryRaw);
if (parts == null || StringUtils.isEmpty(parts.countryName))
{
continue;
}
String baseCountry = parts.countryName;
String zoneLabel = parts.zoneLabel;
List<BmsProductCountry> bucket = out.get(currentProductCode);
if (bucket == null)
{
bucket = new ArrayList<BmsProductCountry>();
out.put(currentProductCode, bucket);
}
BmsRegion region = resolveRegion(baseCountry, regionByNameCn);
if (region != null)
{
BmsProductCountry c = buildCountryRow(region.getCountryNameCn(), region.getCountryCode2(),
zoneLabel, zoneByCountryAndCode,
range, freight, registration, currentSize, currentLastMile, currentLeadTime, eff);
c.setProductCode(currentProductCode);
c.setProductName(currentProductName);
bucket.add(c);
continue;
}
if (isEuZoneName(baseCountry) && StringUtils.isEmpty(zoneLabel))
{
if (euRegions == null || euRegions.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.product.import.euEmpty"));
}
for (BmsRegion eu : euRegions)
{
BmsProductCountry c = buildCountryRow(eu.getCountryNameCn(), eu.getCountryCode2(),
null, zoneByCountryAndCode,
range, freight, registration, currentSize, currentLastMile, currentLeadTime, eff);
c.setProductCode(currentProductCode);
c.setProductName(currentProductName);
bucket.add(c);
}
continue;
}
unmatchedCountries.add(currentCountryRaw.trim().replace("\n", " ").replace("\r", " "));
BmsProductCountry c = buildCountryRow(baseCountry, null, zoneLabel, zoneByCountryAndCode,
range, freight, registration, currentSize, currentLastMile, currentLeadTime, eff);
c.setProductCode(currentProductCode);
c.setProductName(currentProductName);
bucket.add(c);
}
return out;
}
/** Build productName -> productCode from 产品目录 (first row per name wins). */
private Map<String, String> buildCatalogCodeByName(Workbook workbook)
{
Map<String, String> map = new LinkedHashMap<String, String>();
if (workbook == null)
{
return map;
}
Sheet catalog = workbook.getSheet(CATALOG_SHEET);
if (catalog == null)
{
return map;
}
try
{
List<BmsProduct> products = dedupeCatalogProducts(parseCatalog(catalog));
for (BmsProduct p : products)
{
if (p == null || StringUtils.isEmpty(p.getProductCode()) || StringUtils.isEmpty(p.getProductName()))
{
continue;
}
String nameKey = compactName(p.getProductName());
if (StringUtils.isEmpty(nameKey) || map.containsKey(nameKey))
{
continue;
}
map.put(nameKey, p.getProductCode().trim().toUpperCase(Locale.ROOT));
}
}
catch (Exception ignored)
{
// Catalog optional for inline identity; ignore parse errors.
}
return map;
}
/** Resolve product code by product name; exact compact match, then base name before '(' / '('. */
private String resolveCodeByProductName(String productName, Map<String, String> catalogCodeByName)
{
if (StringUtils.isEmpty(productName) || catalogCodeByName == null || catalogCodeByName.isEmpty())
{
return null;
}
String key = compactName(productName);
String hit = catalogCodeByName.get(key);
if (StringUtils.isNotEmpty(hit))
{
return hit;
}
String base = productName.trim();
int idx = base.indexOf('(');
int idx2 = base.indexOf('(');
int cut = -1;
if (idx >= 0)
{
cut = idx;
}
if (idx2 >= 0 && (cut < 0 || idx2 < cut))
{
cut = idx2;
}
if (cut > 0)
{
hit = catalogCodeByName.get(compactName(base.substring(0, cut)));
if (StringUtils.isNotEmpty(hit))
{
return hit;
}
}
return null;
}
/**
* Multi-country import expects price sheets with a "country" column.
* Single-country dedicated-line sheets use product-name + weight blocks instead.
@@ -1293,7 +1840,8 @@ public class BmsProductServiceImpl implements IBmsProductService
*/
private int[] importOneProductWithDeadlockRetry(Workbook workbook, BmsProduct product, String operName,
Map<String, BmsRegion> regionByNameCn, List<BmsRegion> euRegions,
Map<String, PrcZone> zoneByCountryAndCode, Set<String> unmatchedCountries, Date forcedEff, Long importPsvId)
Map<String, PrcZone> zoneByCountryAndCode, Set<String> unmatchedCountries, Date forcedEff, Long importPsvId,
List<BmsProductCountry> preParsedCountries)
throws Exception
{
int max = 8;
@@ -1310,7 +1858,9 @@ public class BmsProductServiceImpl implements IBmsProductService
{
product.setPsvId(importPsvId);
}
int[] nums = importOneProduct(workbook, product, operName, regionByNameCn, euRegions,
int[] nums = preParsedCountries != null
? importOneProductFromRows(product, preParsedCountries, operName, forcedEff)
: importOneProduct(workbook, product, operName, regionByNameCn, euRegions,
zoneByCountryAndCode, unmatchedCountries, forcedEff);
transactionManager.commit(tx);
return nums;
@@ -1461,6 +2011,27 @@ public class BmsProductServiceImpl implements IBmsProductService
throw new ServiceException(MessageUtils.message("price.product.import.noCountry",
productSheet.getSheetName()));
}
return importOneProductFromRows(product, countries, operName, forcedEffectiveDate);
}
/** Import using pre-parsed country rows (inline product-code / product-name path). */
private int[] importOneProductFromRows(BmsProduct product, List<BmsProductCountry> countries,
String operName, Date forcedEffectiveDate)
{
if (product != null)
{
if (StringUtils.isEmpty(product.getSourceSheet()))
{
product.setSourceSheet(product.getProductName());
}
product.setCreateBy(operName);
product.setUpdateBy(operName);
}
if (countries == null || countries.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.product.import.noCountry",
product == null ? "" : product.getProductCode()));
}
assertNoDuplicateCountryWeightRows(product, countries);
// UI-selected effective date wins over Excel; previous open product/country rows expire at this date
Date newEff = forcedEffectiveDate == null && product.getEffectiveDate() == null
@@ -362,6 +362,10 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
try (InputStream is = new java.io.ByteArrayInputStream(bytes); Workbook workbook = WorkbookFactory.create(is))
{
if (workbookHasInlineProductColumns(workbook))
{
return importRedlineExcelByInline(workbook, bytes, filename, operName, taskId, forcedEff);
}
Sheet catalog = workbook.getSheet(CATALOG_SHEET);
if (catalog == null)
{
@@ -665,6 +669,10 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
}
try (InputStream is = new java.io.ByteArrayInputStream(bytes); Workbook workbook = WorkbookFactory.create(is))
{
if (workbookHasInlineProductColumns(workbook))
{
return readQuoteWorkbookByInlineProductCode(workbook).byCode;
}
Sheet catalog = workbook.getSheet(CATALOG_SHEET);
if (catalog == null)
{
@@ -712,9 +720,15 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
@Override
public Map<String, Object> previewImportCatalog(byte[] bytes, String filename) throws Exception
{
Map<String, List<BmsRedlineProductCountry>> byCode = parseQuoteExcelByProduct(bytes, filename);
try (InputStream is = new java.io.ByteArrayInputStream(bytes); Workbook workbook = WorkbookFactory.create(is))
{
if (workbookHasInlineProductColumns(workbook))
{
RedlineQuoteWorkbook parsed = readQuoteWorkbookByInlineProductCode(workbook);
List<BmsRedlineProduct> products = enrichInlineProductsFromCatalog(workbook, parsed.products);
return buildCatalogPreview(products, parsed.byCode);
}
Map<String, List<BmsRedlineProductCountry>> byCode = parseQuoteExcelByProduct(bytes, filename);
Sheet catalog = workbook.getSheet(CATALOG_SHEET);
List<BmsRedlineProduct> catalogProducts = dedupeCatalogProducts(parseCatalog(catalog));
return buildCatalogPreview(catalogProducts, byCode);
@@ -1205,8 +1219,28 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
throw new ServiceException(MessageUtils.message("price.product.import.noCountry",
productSheet.getSheetName()));
}
return importOneProductFromRows(product, countries, operName, forcedEffectiveDate);
}
/** Import using pre-parsed country rows (inline product-code / product-name path). */
private int[] importOneProductFromRows(BmsRedlineProduct product, List<BmsRedlineProductCountry> countries,
String operName, Date forcedEffectiveDate)
{
if (product != null)
{
if (StringUtils.isEmpty(product.getSourceSheet()))
{
product.setSourceSheet(product.getProductName());
}
product.setCreateBy(operName);
product.setUpdateBy(operName);
}
if (countries == null || countries.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.product.import.noCountry",
product == null ? "" : product.getProductCode()));
}
assertNoDuplicateCountryWeightRows(product, countries);
// UI-selected effective date wins over Excel; previous open product/country rows expire at this date
Date newEff = forcedEffectiveDate == null && product.getEffectiveDate() == null
? null
: requireImportEffectiveDate(forcedEffectiveDate != null ? forcedEffectiveDate
@@ -2447,6 +2481,574 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
return map;
}
private static final class RedlineQuoteWorkbook
{
private List<BmsRedlineProduct> products;
private Map<String, List<BmsRedlineProductCountry>> byCode;
}
private int[] importOneProductInline(BmsRedlineProduct product, List<BmsRedlineProductCountry> countries,
String operName, Date forcedEff, Long importPsvId) throws Exception
{
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
TransactionStatus tx = transactionManager.getTransaction(def);
try
{
product.setEffectiveDate(forcedEff);
if (importPsvId != null)
{
product.setPsvId(importPsvId);
}
int[] nums = importOneProductFromRows(product, countries, operName, forcedEff);
transactionManager.commit(tx);
return nums;
}
catch (Exception e)
{
try { transactionManager.rollback(tx); } catch (Exception ignore) { }
throw e;
}
}
/**
* Formal redline import when price sheets carry inline product-code / product-name columns.
* Identifies products from detail rows (not catalog sheet-name matching).
*/
private String importRedlineExcelByInline(Workbook workbook, byte[] bytes, String filename,
String operName, String taskId, Date forcedEff) throws Exception
{
RedlineQuoteWorkbook parsed = readQuoteWorkbookByInlineProductCode(workbook);
List<BmsRedlineProduct> products = enrichInlineProductsFromCatalog(workbook, parsed.products);
if (products.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.product.import.noData"));
}
reportImportProgress(taskId, 20, "parsing excel");
assertImportProductsEffectiveNotBeforePrev(products, forcedEff);
Map<String, BmsRegion> regionByNameCn = loadRegionByNameCn();
if (regionByNameCn.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.product.import.regionEmpty"));
}
List<BmsRegion> euRegions = loadEuRegions(regionByNameCn);
Map<String, PrcZone> zoneByCountryAndCode = loadZoneByCountryAndCode();
Long importPsvId = forcedEff == null
? catalogVersionSupport.ensureDraftVersion(PriceType.REDLINE, null, false).getPsvId()
: catalogVersionSupport.requirePsvId(PriceType.REDLINE, null, forcedEff);
priceExcelFileService.saveImportExcel(
PrcPriceExcelFile.TYPE_REDLINE, forcedEff, bytes, filename, operName, importPsvId);
int successNum = 0;
int skipNum = 0;
int countryNum = 0;
Set<String> unmatchedCountries = new HashSet<String>();
StringBuilder failureMsg = new StringBuilder();
int total = products.size();
int index = 0;
for (BmsRedlineProduct product : products)
{
index++;
if (product == null || StringUtils.isEmpty(product.getProductCode()))
{
continue;
}
String code = product.getProductCode().trim().toUpperCase(Locale.ROOT);
reportImportProductProgress(taskId, index, total, product.getProductCode());
List<BmsRedlineProductCountry> countries = parsed.byCode.get(code);
try
{
int[] nums = importOneProductInline(product, countries, operName, forcedEff, importPsvId);
successNum += nums[0];
skipNum += nums[1];
countryNum += nums[2];
}
catch (Exception e)
{
String tip = e.getMessage();
if (StringUtils.isEmpty(tip))
{
tip = e.getClass().getSimpleName();
}
failureMsg.append("<br/>").append(product.getProductCode()).append(":").append(tip);
}
}
reportImportProgress(taskId, 95, "finishing");
if (failureMsg.length() > 0)
{
return MessageUtils.message("price.product.import.partialFail",
Integer.valueOf(successNum), Integer.valueOf(skipNum), failureMsg.toString());
}
String msg = MessageUtils.message("price.product.import.success", successNum, skipNum, countryNum)
+ "<br/>" + MessageUtils.message("price.product.import.success.detail1")
+ "<br/>" + MessageUtils.message("price.product.import.success.detail2");
if (!unmatchedCountries.isEmpty())
{
msg += "<br/>" + MessageUtils.message("price.product.import.success.unmatched",
String.join("、", unmatchedCountries));
}
return msg;
}
/** Prefer catalog metadata when code matches; keep detail product name for variants. */
private List<BmsRedlineProduct> enrichInlineProductsFromCatalog(Workbook workbook, List<BmsRedlineProduct> inlineProducts)
{
Map<String, BmsRedlineProduct> catalogByCode = new HashMap<String, BmsRedlineProduct>();
Sheet catalog = workbook == null ? null : workbook.getSheet(CATALOG_SHEET);
if (catalog != null)
{
try
{
for (BmsRedlineProduct p : dedupeCatalogProducts(parseCatalog(catalog)))
{
if (p == null || StringUtils.isEmpty(p.getProductCode()))
{
continue;
}
catalogByCode.put(p.getProductCode().trim().toUpperCase(Locale.ROOT), p);
}
}
catch (Exception ignored)
{
// Catalog optional for inline import.
}
}
List<BmsRedlineProduct> out = new ArrayList<BmsRedlineProduct>();
if (inlineProducts == null)
{
return out;
}
for (BmsRedlineProduct inline : inlineProducts)
{
if (inline == null || StringUtils.isEmpty(inline.getProductCode()))
{
continue;
}
String code = inline.getProductCode().trim().toUpperCase(Locale.ROOT);
BmsRedlineProduct cat = catalogByCode.get(code);
if (cat == null)
{
out.add(inline);
continue;
}
BmsRedlineProduct merged = new BmsRedlineProduct();
merged.setProductCode(inline.getProductCode());
merged.setProductName(StringUtils.isNotEmpty(inline.getProductName())
? inline.getProductName() : cat.getProductName());
merged.setServiceLevel(cat.getServiceLevel());
merged.setCargoAttr(cat.getCargoAttr());
merged.setDestRegion(cat.getDestRegion());
merged.setUpdateNote(cat.getUpdateNote());
merged.setEffectiveDate(cat.getEffectiveDate());
merged.setTaxMode(StringUtils.isNotEmpty(cat.getTaxMode())
? cat.getTaxMode() : resolveTaxMode(merged.getProductName()));
out.add(merged);
}
return out;
}
/**
* True when any multi-country price sheet has an inline product-code or product-name column
* (产品代码 / 产品对接代码 / 产品名称).
*/
private boolean workbookHasInlineProductColumns(Workbook workbook)
{
if (workbook == null)
{
return false;
}
for (int i = 0; i < workbook.getNumberOfSheets(); i++)
{
String sheetName = workbook.getSheetName(i);
if (shouldSkipCountryPriceSheet(sheetName))
{
continue;
}
Sheet sheet = workbook.getSheetAt(i);
if (sheet == null)
{
continue;
}
int[] countryHeader = findHeaderCell(sheet, "国家");
if (countryHeader == null)
{
continue;
}
Row head = sheet.getRow(countryHeader[0]);
if (head == null)
{
continue;
}
if (firstCol(head, "产品代码", "产品对接代码") >= 0
|| findColByHeader(head, "产品名称", 0) >= 0)
{
return true;
}
}
return false;
}
/**
* Multi-country parse keyed by inline product-code column on price sheets.
* Does not use 产品目录 sheet matching.
*/
private RedlineQuoteWorkbook readQuoteWorkbookByInlineProductCode(Workbook workbook) throws Exception
{
Map<String, String> catalogCodeByName = buildCatalogCodeByName(workbook);
Map<String, BmsRegion> regionByNameCn = loadRegionByNameCn();
List<BmsRegion> euRegions = loadEuRegions(regionByNameCn);
Map<String, PrcZone> zoneByCountryAndCode = loadZoneByCountryAndCode();
Set<String> unmatchedCountries = new HashSet<String>();
Map<String, List<BmsRedlineProductCountry>> out = new LinkedHashMap<String, List<BmsRedlineProductCountry>>();
Map<String, BmsRedlineProduct> productMeta = new LinkedHashMap<String, BmsRedlineProduct>();
List<String> tips = new ArrayList<String>();
for (int i = 0; i < workbook.getNumberOfSheets(); i++)
{
String sheetName = workbook.getSheetName(i);
if (shouldSkipCountryPriceSheet(sheetName))
{
continue;
}
Sheet sheet = workbook.getSheetAt(i);
if (sheet == null || findHeaderCell(sheet, "国家") == null)
{
continue;
}
Map<String, List<BmsRedlineProductCountry>> sheetByCode = parseProductSheetByInlineCode(
sheet, catalogCodeByName, regionByNameCn, euRegions, zoneByCountryAndCode, unmatchedCountries);
if (sheetByCode == null || sheetByCode.isEmpty())
{
continue;
}
for (Map.Entry<String, List<BmsRedlineProductCountry>> e : sheetByCode.entrySet())
{
if (e == null || StringUtils.isEmpty(e.getKey()) || e.getValue() == null || e.getValue().isEmpty())
{
continue;
}
String code = e.getKey();
List<BmsRedlineProductCountry> rows = e.getValue();
List<BmsRedlineProductCountry> existing = out.get(code);
if (existing == null)
{
out.put(code, new ArrayList<BmsRedlineProductCountry>(rows));
}
else
{
existing.addAll(rows);
}
if (!productMeta.containsKey(code))
{
BmsRedlineProduct p = new BmsRedlineProduct();
p.setProductCode(code);
String name = null;
for (BmsRedlineProductCountry r : rows)
{
if (r != null && StringUtils.isNotEmpty(r.getProductName()))
{
name = r.getProductName();
break;
}
}
p.setProductName(StringUtils.isNotEmpty(name) ? name : code);
p.setTaxMode(resolveTaxMode(p.getProductName()));
productMeta.put(code, p);
}
}
}
for (Map.Entry<String, List<BmsRedlineProductCountry>> e : out.entrySet())
{
BmsRedlineProduct p = productMeta.get(e.getKey());
String tip = buildDuplicateCountryWeightTip(p, e.getValue());
if (StringUtils.isNotEmpty(tip))
{
tips.add(tip);
}
}
if (!tips.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.product.import.dupCountry",
String.join("<br/>", tips)));
}
if (out.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.set.import.noPriceRows"));
}
RedlineQuoteWorkbook parsed = new RedlineQuoteWorkbook();
parsed.products = new ArrayList<BmsRedlineProduct>(productMeta.values());
parsed.byCode = out;
return parsed;
}
/**
* Parse one multi-country price sheet grouped by inline product code / product name.
* Prefer product-code column; if only product-name exists, resolve code via catalog name map.
* Country / product-code / product-name / size / last-mile / lead-time carry forward
* when subsequent weight-band rows leave those cells blank.
*/
private Map<String, List<BmsRedlineProductCountry>> parseProductSheetByInlineCode(Sheet sheet,
Map<String, String> catalogCodeByName, Map<String, BmsRegion> regionByNameCn,
List<BmsRegion> euRegions, Map<String, PrcZone> zoneByCountryAndCode,
Set<String> unmatchedCountries)
{
Map<String, List<BmsRedlineProductCountry>> out = new LinkedHashMap<String, List<BmsRedlineProductCountry>>();
if (sheet == null)
{
return out;
}
Date sheetEffective = parseSheetEffectiveDate(sheet);
int[] countryHeader = findHeaderCell(sheet, "国家");
if (countryHeader == null)
{
return out;
}
int headerRowIdx = countryHeader[0];
Row headerRow = sheet.getRow(headerRowIdx);
int colCountry = countryHeader[1];
int colProductCode = firstCol(headerRow, "产品代码", "产品对接代码");
int colProductName = findColByHeader(headerRow, "产品名称", 0);
if (colProductCode < 0 && colProductName < 0)
{
return out;
}
int colWeight = findColByHeader(headerRow, "重量", colCountry + 1);
int colFreight = findColByHeader(headerRow, "运费", colCountry + 1);
int colRegister = findColByHeader(headerRow, "挂号", colCountry + 1);
int colSize = findColByHeader(headerRow, "尺寸", colCountry + 1);
int colLastMile = findColByHeader(headerRow, "尾程", colCountry + 1);
int colLeadTime = findColByHeader(headerRow, "时效", colCountry + 1);
if (colWeight < 0 || colFreight < 0)
{
return out;
}
String currentCountryRaw = null;
String currentProductCode = null;
String currentProductName = null;
String currentSize = null;
String currentLastMile = null;
String currentLeadTime = null;
int lastRow = sheet.getLastRowNum();
for (int i = headerRowIdx + 1; i <= lastRow; i++)
{
Row row = sheet.getRow(i);
if (row == null)
{
continue;
}
String countryCell = cellString(row.getCell(colCountry));
String weightLabel = cellString(row.getCell(colWeight));
if ("国家".equals(countryCell)
|| (weightLabel != null && weightLabel.contains("重量段")))
{
continue;
}
if (StringUtils.isNotEmpty(countryCell))
{
currentCountryRaw = countryCell;
}
boolean productChanged = false;
if (colProductCode >= 0)
{
String codeCell = normalizeImportProductCode(cellString(row.getCell(colProductCode)));
if (StringUtils.isNotEmpty(codeCell))
{
String newCode = codeCell.trim().toUpperCase(Locale.ROOT);
if (currentProductCode == null || !newCode.equals(currentProductCode))
{
productChanged = true;
currentProductCode = newCode;
}
}
}
if (colProductName >= 0)
{
String nameCell = cellString(row.getCell(colProductName));
if (StringUtils.isNotEmpty(nameCell) && !isSectionTitle(nameCell))
{
String newName = nameCell.replace("\n", " ").replace("\r", " ").trim();
if (currentProductName == null || !newName.equals(currentProductName))
{
productChanged = true;
currentProductName = newName;
// Name-only sheets: resolve product code from catalog by product name.
if (colProductCode < 0)
{
String mapped = resolveCodeByProductName(currentProductName, catalogCodeByName);
if (StringUtils.isNotEmpty(mapped))
{
currentProductCode = mapped;
}
else
{
currentProductCode = null;
}
}
}
}
}
if (productChanged)
{
currentSize = null;
currentLastMile = null;
currentLeadTime = null;
}
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)));
}
if (StringUtils.isEmpty(currentCountryRaw) || StringUtils.isEmpty(currentProductCode))
{
continue;
}
BigDecimal[] range = parseWeightRange(weightLabel);
if (range == null)
{
continue;
}
BigDecimal freight = cellDecimal(row.getCell(colFreight));
BigDecimal registration = colRegister >= 0 ? cellDecimal(row.getCell(colRegister)) : null;
Date eff = sheetEffective;
ZoneCodeAbbrevs.CountryZoneParts parts = ZoneCodeAbbrevs.splitCountryZone(currentCountryRaw);
if (parts == null || StringUtils.isEmpty(parts.countryName))
{
continue;
}
String baseCountry = parts.countryName;
String zoneLabel = parts.zoneLabel;
List<BmsRedlineProductCountry> bucket = out.get(currentProductCode);
if (bucket == null)
{
bucket = new ArrayList<BmsRedlineProductCountry>();
out.put(currentProductCode, bucket);
}
BmsRegion region = resolveRegion(baseCountry, regionByNameCn);
if (region != null)
{
BmsRedlineProductCountry c = buildCountryRow(region.getCountryNameCn(), region.getCountryCode2(),
zoneLabel, zoneByCountryAndCode,
range, freight, registration, currentSize, currentLastMile, currentLeadTime, eff);
c.setProductCode(currentProductCode);
c.setProductName(currentProductName);
bucket.add(c);
continue;
}
if (isEuZoneName(baseCountry) && StringUtils.isEmpty(zoneLabel))
{
if (euRegions == null || euRegions.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.product.import.euEmpty"));
}
for (BmsRegion eu : euRegions)
{
BmsRedlineProductCountry c = buildCountryRow(eu.getCountryNameCn(), eu.getCountryCode2(),
null, zoneByCountryAndCode,
range, freight, registration, currentSize, currentLastMile, currentLeadTime, eff);
c.setProductCode(currentProductCode);
c.setProductName(currentProductName);
bucket.add(c);
}
continue;
}
unmatchedCountries.add(currentCountryRaw.trim().replace("\n", " ").replace("\r", " "));
BmsRedlineProductCountry c = buildCountryRow(baseCountry, null, zoneLabel, zoneByCountryAndCode,
range, freight, registration, currentSize, currentLastMile, currentLeadTime, eff);
c.setProductCode(currentProductCode);
c.setProductName(currentProductName);
bucket.add(c);
}
return out;
}
/** Build productName -> productCode from 产品目录 (first row per name wins). */
private Map<String, String> buildCatalogCodeByName(Workbook workbook)
{
Map<String, String> map = new LinkedHashMap<String, String>();
if (workbook == null)
{
return map;
}
Sheet catalog = workbook.getSheet(CATALOG_SHEET);
if (catalog == null)
{
return map;
}
try
{
List<BmsRedlineProduct> products = dedupeCatalogProducts(parseCatalog(catalog));
for (BmsRedlineProduct p : products)
{
if (p == null || StringUtils.isEmpty(p.getProductCode()) || StringUtils.isEmpty(p.getProductName()))
{
continue;
}
String nameKey = compactName(p.getProductName());
if (StringUtils.isEmpty(nameKey) || map.containsKey(nameKey))
{
continue;
}
map.put(nameKey, p.getProductCode().trim().toUpperCase(Locale.ROOT));
}
}
catch (Exception ignored)
{
// Catalog optional for inline identity; ignore parse errors.
}
return map;
}
/** Resolve product code by product name; exact compact match, then base name before '(' / '('. */
private String resolveCodeByProductName(String productName, Map<String, String> catalogCodeByName)
{
if (StringUtils.isEmpty(productName) || catalogCodeByName == null || catalogCodeByName.isEmpty())
{
return null;
}
String key = compactName(productName);
String hit = catalogCodeByName.get(key);
if (StringUtils.isNotEmpty(hit))
{
return hit;
}
String base = productName.trim();
int idx = base.indexOf('(');
int idx2 = base.indexOf('(');
int cut = -1;
if (idx >= 0)
{
cut = idx;
}
if (idx2 >= 0 && (cut < 0 || idx2 < cut))
{
cut = idx2;
}
if (cut > 0)
{
hit = catalogCodeByName.get(compactName(base.substring(0, cut)));
if (StringUtils.isNotEmpty(hit))
{
return hit;
}
}
return null;
}
private static boolean shouldSkipCountryPriceSheet(String name)
{
if (StringUtils.isEmpty(name))
@@ -258,6 +258,30 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
return setMapper.updatePrcPriceSet(set);
}
@Override
public int updateBasicInfo(PrcPriceSet set)
{
if (set == null || set.getPriceSetId() == null)
{
throw new ServiceException(MessageUtils.message("price.set.notExists"));
}
PrcPriceSet db = requireSet(set.getPriceSetId());
if (STATUS_AUDIT.equals(db.getStatus()))
{
throw new ServiceException(MessageUtils.message("price.set.edit.audit"));
}
if (StringUtils.isEmpty(set.getPriceSetName()))
{
throw new ServiceException(MessageUtils.message("price.set.name.empty"));
}
set.setPriceSetName(set.getPriceSetName().trim());
if (setMapper.countByName(set) > 0)
{
throw new ServiceException(MessageUtils.message("price.set.name.exists"));
}
return setMapper.updateBasicInfo(set);
}
@Override
@Transactional
public int deletePrcPriceSetByIds(Long[] priceSetIds)
@@ -21,6 +21,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="updateTime" column="update_time" />
<result property="productCount" column="product_count" />
<result property="customerCount" column="customer_count" />
<result property="boundCustomers" column="bound_customers" />
</resultMap>
<sql id="selectPrcPriceSetVo">
@@ -33,7 +34,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
inner join prc_price_set_version v on v.psv_id = p.psv_id
where v.price_set_id = s.price_set_id
and ifnull(p.is_del, '0') = '0') as product_count,
(select count(1) from prc_customer_price_set c where c.price_set_id = s.price_set_id) as customer_count
(select count(1) from prc_customer_price_set c where c.price_set_id = s.price_set_id) as customer_count,
(select group_concat(
concat(ifnull(c.customer_code, ''),
case when ifnull(c.customer_name, '') = '' then '' else concat(' ', c.customer_name) end)
order by c.bind_priority asc, c.customer_priceset_id asc
separator '、')
from prc_customer_price_set c
where c.price_set_id = s.price_set_id) as bound_customers
from prc_price_set s
</sql>
@@ -47,7 +55,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
s.set_type, s.default_discount, s.default_type, s.calculate_sign,
s.status, s.tms_id,
s.created_by, s.create_time, s.updated_by, s.update_time,
null as product_count, null as customer_count
null as product_count, null as customer_count, null as bound_customers
from prc_price_set s
where s.price_set_id = #{priceSetId}
</select>
@@ -71,14 +79,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
AND (
s.price_set_code like concat('%', #{keyword}, '%')
OR s.price_set_name like concat('%', #{keyword}, '%')
OR exists (
)
</if>
<if test="customerId != null">
AND exists (
select 1 from prc_customer_price_set c
where c.price_set_id = s.price_set_id
and (
c.customer_name like concat('%', #{keyword}, '%')
or concat(c.customer_id, '') like concat('%', #{keyword}, '%')
)
)
and c.customer_id = #{customerId}
)
</if>
<!-- Data scope: customer sales / creator / explicit viewer bind.
@@ -167,6 +174,17 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
where price_set_id = #{priceSetId}
</update>
<update id="updateBasicInfo" parameterType="PrcPriceSet">
update prc_price_set
<set>
<if test="priceSetName != null and priceSetName != ''">price_set_name = #{priceSetName},</if>
price_set_note = #{priceSetNote},
<if test="updateBy != null and updateBy != ''">updated_by = #{updateBy},</if>
update_time = sysdate()
</set>
where price_set_id = #{priceSetId}
</update>
<update id="updatePrcPriceSetStatus" parameterType="PrcPriceSet">
update prc_price_set
<set>
@@ -0,0 +1,10 @@
-- 供应商管理是一级目录,path 被写成了 "1",列表实际地址变成 /1/supplierinfo。
-- 编辑页取消若写死 /customersuppliermanagement/supplierinfo,路由未注册,会落到 404。
-- 把目录地址改成 supplier,列表即为 /supplier/supplierinfo。可重复执行。
UPDATE sys_menu
SET path = 'supplier',
update_by = 'admin',
update_time = NOW()
WHERE menu_id = 2497
AND menu_type = 'M'
AND path = '1';
+9
View File
@@ -86,6 +86,15 @@ export function updatePriceSet(data) {
})
}
/** Update package name and note only. */
export function updatePriceSetBasic(data) {
return request({
url: '/price/set/basic',
method: 'put',
data: data
})
}
export function delPriceSet(priceSetIds) {
return request({
url: '/price/set/' + priceSetIds,
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

+14 -1
View File
@@ -197,7 +197,11 @@ export const dynamicRoutes = [
path: 'index',
component: () => import('@/views/customersuppliermanagement/supplierinfo/supplieredit/index.vue'),
name: 'SupplierEdit',
meta: { title: '供应商编辑', activeMenu: '/customersuppliermanagement/supplierinfo' }
// 高亮地址跟菜单走。供应商目录 path 曾被写成 "1",列表实际是 /1/supplierinfo,写死客户管理目录会对不上。
beforeEnter(to) {
to.meta.activeMenu = supplierListPath()
},
meta: { title: '供应商编辑', activeMenu: '/supplier/supplierinfo' }
}
]
},
@@ -264,4 +268,13 @@ const router = createRouter({
},
})
/**
* 供应商列表的真实地址由菜单注册,不写死目录。
* 当前库里「供应商管理」目录 path 若仍是 1,这里会得到 /1/supplierinfo。
*/
export function supplierListPath() {
const hit = router.getRoutes().find(r => /\/supplierinfo$/.test(r.path))
return hit ? hit.path : '/supplier/supplierinfo'
}
export default router
@@ -622,6 +622,7 @@ import { getToken } from '@/utils/auth'
import axios from 'axios'
import { saveAs } from 'file-saver'
import { useChannels, parseChannelCodes } from '@/components/ChannelSelect/useChannels'
import { supplierListPath } from '@/router'
const { proxy } = getCurrentInstance()
const route = useRoute()
@@ -1063,7 +1064,8 @@ function submitForm() {
}
function goBack() {
router.push({ path: '/customersuppliermanagement/supplierinfo' })
// 关闭编辑页并回到已注册的供应商列表。菜单目录 path 是 1 时列表为 /1/supplierinfo,不能回到客户管理那条不存在的地址。
proxy.$tab.closeOpenPage({ path: supplierListPath() })
}
/* ============ 初始化 ============ */
@@ -118,7 +118,20 @@ const colSurcharge = '附加费'
const colAction = '操作'
function cellKey(row, field) {
return String(row && row.psdId != null ? row.psdId : row && row.productCountryId) + ':' + field
if (!row) return 'none:' + field
// Prefer DB id; local Excel draft uses _localKey (psdId cleared in applyLocalDraft).
if (row.psdId != null && row.psdId !== '') return String(row.psdId) + ':' + field
if (row._localKey) return String(row._localKey) + ':' + field
const parts = [
row.productCode,
row.countryCode || row.countryName,
row.zoneCode || row.zoneName,
row.weightLabel,
row.weightMin,
row.weightMax,
row.productCountryId
]
return parts.map(v => (v == null ? '' : String(v))).join('|') + ':' + field
}
function formatNum(v, precision) {
@@ -150,7 +163,8 @@ async function commitEdit(row, field) {
editingKey.value = ''
try {
if (!row) return
if (!row.psdId) return
// Always emit: create/IMPORT_FRESH local draft has no psdId until finish;
// parent persist path decides DB write vs local-only.
emit('row-change', row)
} finally {
committing.value = false
@@ -4,11 +4,10 @@
<el-form ref="formRef" :model="basic" :rules="rules" label-width="120px" class="max-form">
<el-form-item label="套餐名称" prop="priceSetName">
<el-input v-model="basic.priceSetName" maxlength="50" placeholder="如:金牌电商专属套餐" />
<div class="form-hint">显示给客户的套餐对外名称</div>
</el-form-item>
<el-form-item label="说明备注" prop="priceSetNote">
<el-input v-model="basic.priceSetNote" type="textarea" :rows="3" maxlength="200" placeholder="套餐业务说明,显示给客户与运营" />
<el-input v-model="basic.priceSetNote" type="textarea" :rows="3" maxlength="200" placeholder="请输入" />
</el-form-item>
</el-form>
</el-card>
@@ -890,7 +890,10 @@ async function onRowPriceChange(row) {
proxy.$modal.msgSuccess('价格已更新(完成时统一落库)')
return
}
if (!row.psdId) return
if (!row.psdId) {
proxy.$modal.msgWarning('明细尚未落库,请先点「完成」保存后再改价')
return
}
const prevFv = row._editPrevFv
const prevReg = row._editPrevReg
try {
@@ -899,6 +902,7 @@ async function onRowPriceChange(row) {
} catch (e) {
if (prevFv !== undefined) row.fvValue = prevFv
if (prevReg !== undefined) row.fvRegistrationFee = prevReg
proxy.$modal.msgError((e && e.message) ? e.message : '价格更新失败')
}
}
+88 -15
View File
@@ -19,6 +19,17 @@
</el-col>
</template>
<template #search-customerId="{ model }">
<customer-select
v-model="model.customerId"
option-select
remote-search
:page-size="20"
placeholder="客户名称 / 代码"
style="width: 240px"
/>
</template>
<template #col-setType="{ row }">
<el-tag :type="row.setType === 'FIXED' ? 'warning' : 'primary'" size="small" effect="plain">
{{ row.setType === 'FIXED' ? 'FIXED 固定价' : 'DISCOUNT 折扣' }}
@@ -29,6 +40,12 @@
<el-tag :type="statusTag(row)" size="small">{{ statusLabel(row) }}</el-tag>
</template>
<template #col-boundCustomers="{ row }">
<span v-if="row.boundCustomers">{{ row.boundCustomers }}</span>
<span v-else class="text-muted">-</span>
<el-tag v-if="row.customerCount > 0" size="small" type="info" effect="plain" class="cust-count">{{ row.customerCount }}</el-tag>
</template>
<template #col-activeDetail="{ row }">
<el-button
link
@@ -41,6 +58,7 @@
<template #col-action="{ row }">
<el-button link type="primary" @click="handleDetail(row)" v-hasPermi="['price:set:query']">详情</el-button>
<el-button link type="primary" @click="openBasicEdit(row)" v-hasPermi="['price:set:edit']">修改</el-button>
<el-button
v-if="row.status === 'E'"
link
@@ -58,6 +76,24 @@
</template>
</basic-table>
<el-dialog v-model="basicDialog.visible" :title="basicDialogTitle" width="520px" append-to-body destroy-on-close>
<el-form ref="basicFormRef" :model="basicForm" :rules="basicRules" label-width="96px">
<el-form-item label="套餐编码">
<el-input :model-value="basicForm.priceSetCode" disabled />
</el-form-item>
<el-form-item label="套餐名称" prop="priceSetName">
<el-input v-model="basicForm.priceSetName" maxlength="50" show-word-limit placeholder="请输入套餐名称" />
</el-form-item>
<el-form-item label="说明备注" prop="priceSetNote">
<el-input v-model="basicForm.priceSetNote" type="textarea" :rows="3" maxlength="200" show-word-limit placeholder="可选" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="basicDialog.visible = false">取消</el-button>
<el-button type="primary" :loading="basicDialog.saving" @click="submitBasicEdit">确定</el-button>
</template>
</el-dialog>
<set-version-detail-drawer
v-model="drawerVisible"
:version="activeVersion"
@@ -69,9 +105,10 @@
<script setup name="PriceSet">
import {
listPriceSet, disablePriceSet, enablePriceSet, copyPriceSet, getPriceSet
listPriceSet, disablePriceSet, enablePriceSet, copyPriceSet, getPriceSet, updatePriceSetBasic
} from '@/api/price/set'
import SetVersionDetailDrawer from './components/detail/SetVersionDetailDrawer.vue'
import CustomerSelect from '@/components/CustomerSelect/index.vue'
import { setStatusLabel, setStatusTagType } from './utils/priceSet'
const { proxy } = getCurrentInstance()
@@ -79,17 +116,6 @@ const tableRef = ref()
const isFirstActivate = ref(true)
const searchColumns = [
{
label: '类型',
prop: 'setType',
component: 'select',
options: [
{ label: 'FIXED 固定价', value: 'FIXED' },
{ label: 'DISCOUNT 折扣', value: 'DISCOUNT' }
],
placeholder: '全部类型',
width: '160px'
},
{
label: '状态',
prop: 'status',
@@ -102,17 +128,19 @@ const searchColumns = [
placeholder: '全部状态',
width: '140px'
},
{ label: '关键字', prop: 'keyword', component: 'input', placeholder: '套餐ID / 名称 / 客户', width: '240px' }
{ label: '关键字', prop: 'keyword', component: 'input', placeholder: '套餐编码 / 名称', width: '200px' },
{ label: '客户', prop: 'customerId', placeholder: '客户名称 / 代码', width: '240px' }
]
const columns = [
{ label: '套餐编码', prop: 'priceSetCode', width: 110 },
{ label: '套餐名称', prop: 'priceSetName', minWidth: 160 },
{ label: '类型', prop: 'setType', width: 140 },
{ label: '说明', prop: 'priceSetNote', minWidth: 160 },
{ label: '绑定客户', prop: 'boundCustomers', minWidth: 220 },
{ label: '说明', prop: 'priceSetNote', minWidth: 140 },
{ label: '状态', prop: 'status', width: 90 },
{ label: '生效明细', prop: 'activeDetail', width: 100 },
{ label: '操作', prop: 'action', width: 160, showInToolbar: false }
{ label: '操作', prop: 'action', width: 200, showInToolbar: false }
]
function queryList(params) {
@@ -135,6 +163,49 @@ function handleDetail(row) {
proxy.$router.push({ path: '/customer/setDetail', query: { priceSetId: row.priceSetId } })
}
const basicDialog = reactive({ visible: false, saving: false })
const basicFormRef = ref()
const basicForm = reactive({
priceSetId: null,
priceSetCode: '',
priceSetName: '',
priceSetNote: ''
})
const basicRules = {
priceSetName: [{ required: true, message: '套餐名称不能为空', trigger: 'blur' }]
}
const basicDialogTitle = computed(() => '修改套餐基本信息')
function openBasicEdit(row) {
if (!row || row.priceSetId == null) return
basicForm.priceSetId = row.priceSetId
basicForm.priceSetCode = row.priceSetCode || ''
basicForm.priceSetName = row.priceSetName || ''
basicForm.priceSetNote = row.priceSetNote || ''
basicDialog.visible = true
nextTick(() => basicFormRef.value && basicFormRef.value.clearValidate())
}
function submitBasicEdit() {
basicFormRef.value.validate(valid => {
if (!valid) return
basicDialog.saving = true
updatePriceSetBasic({
priceSetId: basicForm.priceSetId,
priceSetName: basicForm.priceSetName,
priceSetNote: basicForm.priceSetNote
}).then(() => {
proxy.$modal.msgSuccess('保存成功')
basicDialog.visible = false
refresh()
}).finally(() => {
basicDialog.saving = false
})
})
}
/** 生效明细:拉详情找生效版本(status=1)弹抽屉 */
const drawerVisible = ref(false)
const activeVersion = ref(null)
@@ -200,6 +271,8 @@ function handleCopy(row) {
</script>
<style scoped>
.text-muted { color: #909399; }
.cust-count { margin-left: 6px; vertical-align: middle; }
.hint-card {
margin-top: 16px;
}