提交价格功能绑定
This commit is contained in:
@@ -24,6 +24,10 @@ public class BmsProductCountry implements Serializable
|
|||||||
private BigDecimal weightMin;
|
private BigDecimal weightMin;
|
||||||
/** 重量段最大值 KG */
|
/** 重量段最大值 KG */
|
||||||
private BigDecimal weightMax;
|
private BigDecimal weightMax;
|
||||||
|
/** 运费 RMB/KG */
|
||||||
|
private BigDecimal freightPrice;
|
||||||
|
/** 挂号费 RMB/票 */
|
||||||
|
private BigDecimal registrationFee;
|
||||||
private String sizeLimit;
|
private String sizeLimit;
|
||||||
private String lastMile;
|
private String lastMile;
|
||||||
private String leadTime;
|
private String leadTime;
|
||||||
@@ -46,6 +50,10 @@ public class BmsProductCountry implements Serializable
|
|||||||
public void setWeightMin(BigDecimal weightMin) { this.weightMin = weightMin; }
|
public void setWeightMin(BigDecimal weightMin) { this.weightMin = weightMin; }
|
||||||
public BigDecimal getWeightMax() { return weightMax; }
|
public BigDecimal getWeightMax() { return weightMax; }
|
||||||
public void setWeightMax(BigDecimal weightMax) { this.weightMax = weightMax; }
|
public void setWeightMax(BigDecimal weightMax) { this.weightMax = weightMax; }
|
||||||
|
public BigDecimal getFreightPrice() { return freightPrice; }
|
||||||
|
public void setFreightPrice(BigDecimal freightPrice) { this.freightPrice = freightPrice; }
|
||||||
|
public BigDecimal getRegistrationFee() { return registrationFee; }
|
||||||
|
public void setRegistrationFee(BigDecimal registrationFee) { this.registrationFee = registrationFee; }
|
||||||
public String getSizeLimit() { return sizeLimit; }
|
public String getSizeLimit() { return sizeLimit; }
|
||||||
public void setSizeLimit(String sizeLimit) { this.sizeLimit = sizeLimit; }
|
public void setSizeLimit(String sizeLimit) { this.sizeLimit = sizeLimit; }
|
||||||
public String getLastMile() { return lastMile; }
|
public String getLastMile() { return lastMile; }
|
||||||
@@ -69,6 +77,8 @@ public class BmsProductCountry implements Serializable
|
|||||||
.append("countryCode", getCountryCode())
|
.append("countryCode", getCountryCode())
|
||||||
.append("weightMin", getWeightMin())
|
.append("weightMin", getWeightMin())
|
||||||
.append("weightMax", getWeightMax())
|
.append("weightMax", getWeightMax())
|
||||||
|
.append("freightPrice", getFreightPrice())
|
||||||
|
.append("registrationFee", getRegistrationFee())
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ public interface IBmsProductService
|
|||||||
public List<BmsProductCountry> selectCountryListByProductCode(String productCode);
|
public List<BmsProductCountry> selectCountryListByProductCode(String productCode);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 导入威狮B2C小包价表:产品目录 + 各产品 sheet 国家服务属性(不含运费/挂号费)
|
* 导入威狮B2C小包价表:产品目录 + 各产品 sheet 国家×重量段(含运费/挂号费/尺寸/尾程/时效)
|
||||||
*
|
*
|
||||||
* @param file 上传的 xlsx/xls
|
* @param file 上传的 xlsx/xls
|
||||||
* @param updateSupport 是否更新已存在产品
|
* @param updateSupport 是否更新已存在产品
|
||||||
|
|||||||
+146
-14
@@ -198,6 +198,14 @@ public class BmsProductServiceImpl implements IBmsProductService
|
|||||||
updateNum++;
|
updateNum++;
|
||||||
countryNum += countries.size();
|
countryNum += countries.size();
|
||||||
}
|
}
|
||||||
|
else if (!countries.isEmpty())
|
||||||
|
{
|
||||||
|
// Product already exists and "update" unchecked: still refresh country/fee rows from Excel.
|
||||||
|
countryMapper.deleteByProductCode(product.getProductCode());
|
||||||
|
saveCountries(product.getProductCode(), countries);
|
||||||
|
updateNum++;
|
||||||
|
countryNum += countries.size();
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
skipNum++;
|
skipNum++;
|
||||||
@@ -216,7 +224,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
|||||||
}
|
}
|
||||||
String msg = "\u5bfc\u5165\u6210\u529f\uff1a\u65b0\u589e " + successNum + " \u4e2a\u4ea7\u54c1\uff0c\u66f4\u65b0 " + updateNum + " \u4e2a\uff0c\u8df3\u8fc7 "
|
String msg = "\u5bfc\u5165\u6210\u529f\uff1a\u65b0\u589e " + successNum + " \u4e2a\u4ea7\u54c1\uff0c\u66f4\u65b0 " + updateNum + " \u4e2a\uff0c\u8df3\u8fc7 "
|
||||||
+ skipNum + " \u4e2a\uff1b\u5199\u5165\u56fd\u5bb6/\u91cd\u91cf\u6bb5\u660e\u7ec6 " + countryNum + " \u884c\u3002"
|
+ skipNum + " \u4e2a\uff1b\u5199\u5165\u56fd\u5bb6/\u91cd\u91cf\u6bb5\u660e\u7ec6 " + countryNum + " \u884c\u3002"
|
||||||
+ "<br/>\u8303\u56f4\uff1a\u4ea7\u54c1\u76ee\u5f55 + \u91cd\u91cf\u6bb5min/max + \u5c3a\u5bf8/\u5c3e\u7a0b/\u65f6\u6548\uff1b\u56fd\u5bb6\u7801\u6765\u81ea bms_region.country_name_cn / country_code_2\u3002";
|
+ "<br/>\u8303\u56f4\uff1a\u4ea7\u54c1\u76ee\u5f55 + \u91cd\u91cf\u6bb5 + \u8fd0\u8d39/\u6302\u53f7\u8d39 + \u5c3a\u5bf8/\u5c3e\u7a0b/\u65f6\u6548\uff1b\u56fd\u5bb6\u7801\u6765\u81ea bms_region\u3002";
|
||||||
if (!unmatchedCountries.isEmpty())
|
if (!unmatchedCountries.isEmpty())
|
||||||
{
|
{
|
||||||
msg += "<br/>\u672a\u5339\u914d\u56fd\u5bb6\u8868\uff08country_code \u4e3a\u7a7a\uff09\uff1a" + String.join("\u3001", unmatchedCountries);
|
msg += "<br/>\u672a\u5339\u914d\u56fd\u5bb6\u8868\uff08country_code \u4e3a\u7a7a\uff09\uff1a" + String.join("\u3001", unmatchedCountries);
|
||||||
@@ -230,7 +238,20 @@ public class BmsProductServiceImpl implements IBmsProductService
|
|||||||
for (BmsProductCountry c : countries)
|
for (BmsProductCountry c : countries)
|
||||||
{
|
{
|
||||||
c.setProductCode(productCode);
|
c.setProductCode(productCode);
|
||||||
countryMapper.insertBmsProductCountry(c);
|
try
|
||||||
|
{
|
||||||
|
countryMapper.insertBmsProductCountry(c);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
String tip = e.getMessage() == null ? "" : e.getMessage();
|
||||||
|
if (tip.contains("freight_price") || tip.contains("registration_fee") || tip.contains("Unknown column"))
|
||||||
|
{
|
||||||
|
throw new ServiceException(
|
||||||
|
"\u8bf7\u5148\u6267\u884c SQL\uff1abms-back/sql/price/product_country_fee_alter.sql\uff08\u7f3a\u5c11\u8fd0\u8d39/\u6302\u53f7\u8d39\u5b57\u6bb5\uff09");
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,24 +329,60 @@ public class BmsProductServiceImpl implements IBmsProductService
|
|||||||
|
|
||||||
Date sheetEffective = parseSheetEffectiveDate(sheet);
|
Date sheetEffective = parseSheetEffectiveDate(sheet);
|
||||||
List<BmsProductCountry> list = new ArrayList<>();
|
List<BmsProductCountry> list = new ArrayList<>();
|
||||||
// Product sheets also often start at column B.
|
int[] countryHeader = findHeaderCell(sheet, "\u56fd\u5bb6");
|
||||||
int[] header = findHeaderCell(sheet, "\u56fd\u5bb6");
|
if (countryHeader == null)
|
||||||
int baseCol = header != null ? header[1] : 0;
|
{
|
||||||
int startRow = header != null ? header[0] + 1 : 0;
|
return list;
|
||||||
|
}
|
||||||
|
int headerRowIdx = countryHeader[0];
|
||||||
|
Row headerRow = sheet.getRow(headerRowIdx);
|
||||||
|
int colCountry = countryHeader[1];
|
||||||
|
int colWeight = findColByHeader(headerRow, "\u91cd\u91cf", colCountry + 1);
|
||||||
|
int colFreight = findColByHeader(headerRow, "\u8fd0\u8d39", colCountry + 1);
|
||||||
|
int colRegister = findColByHeader(headerRow, "\u6302\u53f7", colCountry + 1);
|
||||||
|
int colSize = findColByHeader(headerRow, "\u5c3a\u5bf8", colCountry + 1);
|
||||||
|
int colLastMile = findColByHeader(headerRow, "\u5c3e\u7a0b", colCountry + 1);
|
||||||
|
int colLeadTime = findColByHeader(headerRow, "\u65f6\u6548", colCountry + 1);
|
||||||
|
// Fallback offsets when header text missing (Weishi layout: B=country ... H=lead time)
|
||||||
|
if (colWeight < 0)
|
||||||
|
{
|
||||||
|
colWeight = colCountry + 1;
|
||||||
|
}
|
||||||
|
if (colFreight < 0)
|
||||||
|
{
|
||||||
|
colFreight = colCountry + 2;
|
||||||
|
}
|
||||||
|
if (colRegister < 0)
|
||||||
|
{
|
||||||
|
colRegister = colCountry + 3;
|
||||||
|
}
|
||||||
|
if (colSize < 0)
|
||||||
|
{
|
||||||
|
colSize = colCountry + 4;
|
||||||
|
}
|
||||||
|
if (colLastMile < 0)
|
||||||
|
{
|
||||||
|
colLastMile = colCountry + 5;
|
||||||
|
}
|
||||||
|
if (colLeadTime < 0)
|
||||||
|
{
|
||||||
|
colLeadTime = colCountry + 6;
|
||||||
|
}
|
||||||
|
|
||||||
String currentCountry = null;
|
String currentCountry = null;
|
||||||
String currentSize = null;
|
String currentSize = null;
|
||||||
String currentLastMile = null;
|
String currentLastMile = null;
|
||||||
String currentLeadTime = null;
|
String currentLeadTime = null;
|
||||||
int lastRow = sheet.getLastRowNum();
|
int lastRow = sheet.getLastRowNum();
|
||||||
for (int i = startRow; i <= lastRow; i++)
|
for (int i = headerRowIdx + 1; i <= lastRow; i++)
|
||||||
{
|
{
|
||||||
Row row = sheet.getRow(i);
|
Row row = sheet.getRow(i);
|
||||||
if (row == null)
|
if (row == null)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
String countryCell = cellString(row.getCell(baseCol));
|
String countryCell = cellString(row.getCell(colCountry));
|
||||||
String weightLabel = cellString(row.getCell(baseCol + 1));
|
String weightLabel = cellString(row.getCell(colWeight));
|
||||||
if ("\u56fd\u5bb6".equals(countryCell) || weightLabel.contains("\u91cd\u91cf\u6bb5"))
|
if ("\u56fd\u5bb6".equals(countryCell) || weightLabel.contains("\u91cd\u91cf\u6bb5"))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
@@ -333,9 +390,9 @@ public class BmsProductServiceImpl implements IBmsProductService
|
|||||||
if (StringUtils.isNotEmpty(countryCell))
|
if (StringUtils.isNotEmpty(countryCell))
|
||||||
{
|
{
|
||||||
currentCountry = countryCell.trim();
|
currentCountry = countryCell.trim();
|
||||||
currentSize = emptyToNull(cellString(row.getCell(baseCol + 4)));
|
currentSize = emptyToNull(cellString(row.getCell(colSize)));
|
||||||
currentLastMile = emptyToNull(cellString(row.getCell(baseCol + 5)));
|
currentLastMile = emptyToNull(cellString(row.getCell(colLastMile)));
|
||||||
currentLeadTime = emptyToNull(cellString(row.getCell(baseCol + 6)));
|
currentLeadTime = emptyToNull(cellString(row.getCell(colLeadTime)));
|
||||||
}
|
}
|
||||||
if (StringUtils.isEmpty(currentCountry))
|
if (StringUtils.isEmpty(currentCountry))
|
||||||
{
|
{
|
||||||
@@ -361,6 +418,8 @@ public class BmsProductServiceImpl implements IBmsProductService
|
|||||||
}
|
}
|
||||||
c.setWeightMin(range[0]);
|
c.setWeightMin(range[0]);
|
||||||
c.setWeightMax(range[1]);
|
c.setWeightMax(range[1]);
|
||||||
|
c.setFreightPrice(cellDecimal(row.getCell(colFreight)));
|
||||||
|
c.setRegistrationFee(cellDecimal(row.getCell(colRegister)));
|
||||||
c.setSizeLimit(currentSize);
|
c.setSizeLimit(currentSize);
|
||||||
c.setLastMile(currentLastMile);
|
c.setLastMile(currentLastMile);
|
||||||
c.setLeadTime(currentLeadTime);
|
c.setLeadTime(currentLeadTime);
|
||||||
@@ -371,7 +430,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ???? bms_region??country_name_cn -> ????? country_code_2??
|
* Load bms_region map: country_name_cn -> region (for country_code_2).
|
||||||
*/
|
*/
|
||||||
private Map<String, BmsRegion> loadRegionByNameCn()
|
private Map<String, BmsRegion> loadRegionByNameCn()
|
||||||
{
|
{
|
||||||
@@ -395,7 +454,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ????? country_name_cn ?????????????????????????? ??? / ?????????
|
* Exact country_name_cn match first; single contains match as fallback (e.g. Czech / Czech Republic).
|
||||||
*/
|
*/
|
||||||
private BmsRegion resolveRegion(String excelName, Map<String, BmsRegion> regionByNameCn)
|
private BmsRegion resolveRegion(String excelName, Map<String, BmsRegion> regionByNameCn)
|
||||||
{
|
{
|
||||||
@@ -507,6 +566,28 @@ public class BmsProductServiceImpl implements IBmsProductService
|
|||||||
return "TAX_INCLUDED";
|
return "TAX_INCLUDED";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find column index on header row whose text contains keyword; -1 if not found.
|
||||||
|
*/
|
||||||
|
private static int findColByHeader(Row headerRow, String keyword, int fromCol)
|
||||||
|
{
|
||||||
|
if (headerRow == null || StringUtils.isEmpty(keyword))
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
int last = Math.max(headerRow.getLastCellNum(), fromCol + 12);
|
||||||
|
last = Math.min(last, 30);
|
||||||
|
for (int c = Math.max(fromCol, 0); c < last; c++)
|
||||||
|
{
|
||||||
|
String v = cellString(headerRow.getCell(c));
|
||||||
|
if (StringUtils.isNotEmpty(v) && v.contains(keyword))
|
||||||
|
{
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
private static String emptyToNull(String s)
|
private static String emptyToNull(String s)
|
||||||
{
|
{
|
||||||
return StringUtils.isEmpty(s) ? null : s;
|
return StringUtils.isEmpty(s) ? null : s;
|
||||||
@@ -595,6 +676,57 @@ public class BmsProductServiceImpl implements IBmsProductService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static BigDecimal cellDecimal(Cell cell)
|
||||||
|
{
|
||||||
|
if (cell == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
CellType type = cell.getCellType();
|
||||||
|
if (type == CellType.FORMULA)
|
||||||
|
{
|
||||||
|
type = cell.getCachedFormulaResultType();
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (type == CellType.NUMERIC)
|
||||||
|
{
|
||||||
|
return BigDecimal.valueOf(cell.getNumericCellValue());
|
||||||
|
}
|
||||||
|
String s = cellString(cell);
|
||||||
|
if (StringUtils.isEmpty(s))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// strip currency / unit suffixes: 56、56元、56 RMB
|
||||||
|
s = s.replace(",", "").replace("\uffe5", "").replace("\u5143", "")
|
||||||
|
.replace("RMB", "").replace("rmb", "").trim();
|
||||||
|
if (s.isEmpty())
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int cut = -1;
|
||||||
|
for (int i = 0; i < s.length(); i++)
|
||||||
|
{
|
||||||
|
char ch = s.charAt(i);
|
||||||
|
if (!(ch == '.' || ch == '-' || (ch >= '0' && ch <= '9')))
|
||||||
|
{
|
||||||
|
cut = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cut > 0)
|
||||||
|
{
|
||||||
|
s = s.substring(0, cut).trim();
|
||||||
|
}
|
||||||
|
return new BigDecimal(s);
|
||||||
|
}
|
||||||
|
catch (Exception ignored)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static Date cellDate(Cell cell)
|
private static Date cellDate(Cell cell)
|
||||||
{
|
{
|
||||||
if (cell == null)
|
if (cell == null)
|
||||||
|
|||||||
+14
-8
@@ -332,7 +332,8 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
|||||||
int incomplete = 0;
|
int incomplete = 0;
|
||||||
for (PrcPriceSetDetail d : details)
|
for (PrcPriceSetDetail d : details)
|
||||||
{
|
{
|
||||||
if (StringUtils.isEmpty(d.getProductCode()) || StringUtils.isEmpty(d.getCountryCode())
|
boolean noCountry = StringUtils.isEmpty(d.getCountryCode()) && StringUtils.isEmpty(d.getCountryName());
|
||||||
|
if (StringUtils.isEmpty(d.getProductCode()) || noCountry
|
||||||
|| (d.getWsdId() == null && StringUtils.isEmpty(d.getWeightLabel()))
|
|| (d.getWsdId() == null && StringUtils.isEmpty(d.getWeightLabel()))
|
||||||
|| d.getFvValue() == null || d.getFvRegistrationFee() == null)
|
|| d.getFvValue() == null || d.getFvRegistrationFee() == null)
|
||||||
{
|
{
|
||||||
@@ -341,15 +342,16 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
|||||||
}
|
}
|
||||||
if (details.isEmpty())
|
if (details.isEmpty())
|
||||||
{
|
{
|
||||||
items.add(new PrcPriceSetCheckItem("FIXED 明细完整性", "FAIL", "固定价模式至少需要 1 条明细"));
|
items.add(new PrcPriceSetCheckItem("FIXED 明细完整性", "PASS",
|
||||||
|
"\u65e0\u5957\u9910\u660e\u7ec6\uff1a\u8ba1\u8d39\u53d6 bms_product / bms_product_country \u9ed8\u8ba4\u8fd0\u8d39\u4e0e\u6302\u53f7\u8d39"));
|
||||||
}
|
}
|
||||||
else if (incomplete > 0)
|
else if (incomplete > 0)
|
||||||
{
|
{
|
||||||
items.add(new PrcPriceSetCheckItem("FIXED 明细完整性", "FAIL", incomplete + " 条明细缺少产品/国家/重量段/运费/挂号费"));
|
items.add(new PrcPriceSetCheckItem("FIXED 明细完整性", "FAIL", incomplete + " \u6761\u660e\u7ec6\u7f3a\u5c11\u4ea7\u54c1/\u56fd\u5bb6/\u91cd\u91cf\u6bb5/\u8fd0\u8d39/\u6302\u53f7\u8d39"));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
items.add(new PrcPriceSetCheckItem("FIXED 明细完整性", "PASS", details.size() + " 条明细,运费与挂号费均填写"));
|
items.add(new PrcPriceSetCheckItem("FIXED 明细完整性", "PASS", details.size() + " \u6761\u8986\u76d6\u660e\u7ec6\uff08\u672a\u8986\u76d6\u7684\u4ea7\u54c1\u56fd\u5bb6\u91cd\u91cf\u6bb5\u4ecd\u8d70\u9ed8\u8ba4\u4ef7\uff09"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -418,11 +420,15 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
|||||||
|
|
||||||
if (TYPE_FIXED.equals(setType) && !details.isEmpty())
|
if (TYPE_FIXED.equals(setType) && !details.isEmpty())
|
||||||
{
|
{
|
||||||
items.add(new PrcPriceSetCheckItem("运费单价 vs 默认等级价偏差", "INFO", "默认等级价对照将在计费试算上线后自动计算"));
|
items.add(new PrcPriceSetCheckItem("\u8fd0\u8d39\u5355\u4ef7 vs \u9ed8\u8ba4\u7b49\u7ea7\u4ef7\u504f\u5dee", "INFO", "\u6709\u8986\u76d6\u660e\u7ec6\u65f6\u4f18\u5148\u53d6\u5957\u9910\u56fa\u5b9a\u4ef7\uff1b\u672a\u547d\u4e2d\u5219\u56de\u9000 bms_product_country"));
|
||||||
|
}
|
||||||
|
else if (TYPE_FIXED.equals(setType))
|
||||||
|
{
|
||||||
|
items.add(new PrcPriceSetCheckItem("\u8fd0\u8d39\u5355\u4ef7 vs \u9ed8\u8ba4\u7b49\u7ea7\u4ef7\u504f\u5dee", "INFO", "\u65e0\u5957\u9910\u660e\u7ec6\uff0c\u5168\u90e8\u8d70 bms_product_country \u9ed8\u8ba4\u8fd0\u8d39/\u6302\u53f7\u8d39"));
|
||||||
}
|
}
|
||||||
else if (TYPE_DISCOUNT.equals(setType))
|
else if (TYPE_DISCOUNT.equals(setType))
|
||||||
{
|
{
|
||||||
items.add(new PrcPriceSetCheckItem("运费单价 vs 默认等级价偏差", "INFO", "折扣模式按下单时默认等级价 × 折扣计费"));
|
items.add(new PrcPriceSetCheckItem("\u8fd0\u8d39\u5355\u4ef7 vs \u9ed8\u8ba4\u7b49\u7ea7\u4ef7\u504f\u5dee", "INFO", "\u6298\u6263\u6a21\u5f0f\u6309\u4e0b\u5355\u65f6\u9ed8\u8ba4\u7b49\u7ea7\u4ef7 \u00d7 \u6298\u6263\u8ba1\u8d39"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
@@ -599,9 +605,9 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
|||||||
}
|
}
|
||||||
if (TYPE_FIXED.equals(set.getSetType()))
|
if (TYPE_FIXED.equals(set.getSetType()))
|
||||||
{
|
{
|
||||||
if (StringUtils.isEmpty(detail.getCountryCode()))
|
if (StringUtils.isEmpty(detail.getCountryCode()) && StringUtils.isEmpty(detail.getCountryName()))
|
||||||
{
|
{
|
||||||
throw new ServiceException("固定价明细必须填写国家");
|
throw new ServiceException("\u56fa\u5b9a\u4ef7\u660e\u7ec6\u5fc5\u987b\u586b\u5199\u56fd\u5bb6");
|
||||||
}
|
}
|
||||||
if (detail.getWsdId() == null && StringUtils.isEmpty(detail.getWeightLabel()))
|
if (detail.getWsdId() == null && StringUtils.isEmpty(detail.getWeightLabel()))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,23 +5,26 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<mapper namespace="com.ruoyi.price.mapper.BmsProductCountryMapper">
|
<mapper namespace="com.ruoyi.price.mapper.BmsProductCountryMapper">
|
||||||
|
|
||||||
<resultMap type="BmsProductCountry" id="BmsProductCountryResult">
|
<resultMap type="BmsProductCountry" id="BmsProductCountryResult">
|
||||||
<id property="detailId" column="detail_id" />
|
<id property="detailId" column="detail_id" />
|
||||||
<result property="productCode" column="product_code" />
|
<result property="productCode" column="product_code" />
|
||||||
<result property="countryName" column="country_name" />
|
<result property="countryName" column="country_name" />
|
||||||
<result property="countryCode" column="country_code" />
|
<result property="countryCode" column="country_code" />
|
||||||
<result property="weightMin" column="weight_min" />
|
<result property="weightMin" column="weight_min" />
|
||||||
<result property="weightMax" column="weight_max" />
|
<result property="weightMax" column="weight_max" />
|
||||||
<result property="sizeLimit" column="size_limit" />
|
<result property="freightPrice" column="freight_price" />
|
||||||
<result property="lastMile" column="last_mile" />
|
<result property="registrationFee" column="registration_fee" />
|
||||||
<result property="leadTime" column="lead_time" />
|
<result property="sizeLimit" column="size_limit" />
|
||||||
<result property="effectiveDate" column="effective_date" />
|
<result property="lastMile" column="last_mile" />
|
||||||
<result property="createdAt" column="created_at" />
|
<result property="leadTime" column="lead_time" />
|
||||||
<result property="updatedAt" column="updated_at" />
|
<result property="effectiveDate" column="effective_date" />
|
||||||
|
<result property="createdAt" column="created_at" />
|
||||||
|
<result property="updatedAt" column="updated_at" />
|
||||||
</resultMap>
|
</resultMap>
|
||||||
|
|
||||||
<select id="selectBmsProductCountryList" parameterType="BmsProductCountry" resultMap="BmsProductCountryResult">
|
<select id="selectBmsProductCountryList" parameterType="BmsProductCountry" resultMap="BmsProductCountryResult">
|
||||||
select detail_id, product_code, country_name, country_code, weight_min, weight_max,
|
select detail_id, product_code, country_name, country_code, weight_min, weight_max,
|
||||||
size_limit, last_mile, lead_time, effective_date, created_at, updated_at
|
freight_price, registration_fee, size_limit, last_mile, lead_time,
|
||||||
|
effective_date, created_at, updated_at
|
||||||
from bms_product_country
|
from bms_product_country
|
||||||
<where>
|
<where>
|
||||||
<if test="productCode != null and productCode != ''">
|
<if test="productCode != null and productCode != ''">
|
||||||
@@ -46,10 +49,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<insert id="insertBmsProductCountry" parameterType="BmsProductCountry" useGeneratedKeys="true" keyProperty="detailId">
|
<insert id="insertBmsProductCountry" parameterType="BmsProductCountry" useGeneratedKeys="true" keyProperty="detailId">
|
||||||
insert into bms_product_country (
|
insert into bms_product_country (
|
||||||
product_code, country_name, country_code, weight_min, weight_max,
|
product_code, country_name, country_code, weight_min, weight_max,
|
||||||
size_limit, last_mile, lead_time, effective_date, created_at
|
freight_price, registration_fee, size_limit, last_mile, lead_time,
|
||||||
|
effective_date, created_at
|
||||||
) values (
|
) values (
|
||||||
#{productCode}, #{countryName}, #{countryCode}, #{weightMin}, #{weightMax},
|
#{productCode}, #{countryName}, #{countryCode}, #{weightMin}, #{weightMax},
|
||||||
#{sizeLimit}, #{lastMile}, #{leadTime}, #{effectiveDate}, sysdate()
|
#{freightPrice}, #{registrationFee}, #{sizeLimit}, #{lastMile}, #{leadTime},
|
||||||
|
#{effectiveDate}, sysdate()
|
||||||
)
|
)
|
||||||
</insert>
|
</insert>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
-- 产品主数据(来源:威狮B2C小包价表.xlsx)
|
-- 产品主数据(来源:威狮B2C小包价表.xlsx)
|
||||||
-- 导入范围:产品目录 + 各产品 sheet 国家服务明细(尺寸/尾程/时效)
|
-- 导入范围:产品目录 + 各产品 sheet 国家×重量段(运费/挂号费/尺寸/尾程/时效)
|
||||||
-- 不导入:运费/挂号费矩阵、退件重派、申报说明、赔偿标准
|
-- 不导入:退件重派、申报说明、赔偿标准
|
||||||
SET NAMES utf8mb4;
|
SET NAMES utf8mb4;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS bms_product (
|
CREATE TABLE IF NOT EXISTS bms_product (
|
||||||
@@ -33,6 +33,8 @@ CREATE TABLE IF NOT EXISTS bms_product_country (
|
|||||||
country_code char(2) DEFAULT NULL COMMENT 'ISO2,可空待补齐',
|
country_code char(2) DEFAULT NULL COMMENT 'ISO2,可空待补齐',
|
||||||
weight_min decimal(10,3) DEFAULT NULL COMMENT '重量段最小值KG(含)',
|
weight_min decimal(10,3) DEFAULT NULL COMMENT '重量段最小值KG(含)',
|
||||||
weight_max decimal(10,3) DEFAULT NULL COMMENT '重量段最大值KG(含)',
|
weight_max decimal(10,3) DEFAULT NULL COMMENT '重量段最大值KG(含)',
|
||||||
|
freight_price decimal(10,4) DEFAULT NULL COMMENT '运费 RMB/KG',
|
||||||
|
registration_fee decimal(10,2) DEFAULT NULL COMMENT '挂号费 RMB/票',
|
||||||
size_limit varchar(1000) DEFAULT NULL COMMENT '尺寸限制',
|
size_limit varchar(1000) DEFAULT NULL COMMENT '尺寸限制',
|
||||||
last_mile varchar(500) DEFAULT NULL COMMENT '尾程派送说明',
|
last_mile varchar(500) DEFAULT NULL COMMENT '尾程派送说明',
|
||||||
lead_time varchar(100) DEFAULT NULL COMMENT '参考时效',
|
lead_time varchar(100) DEFAULT NULL COMMENT '参考时效',
|
||||||
@@ -44,7 +46,7 @@ CREATE TABLE IF NOT EXISTS bms_product_country (
|
|||||||
KEY idx_country_code (country_code),
|
KEY idx_country_code (country_code),
|
||||||
KEY idx_weight_match (product_code, country_code, weight_min, weight_max),
|
KEY idx_weight_match (product_code, country_code, weight_min, weight_max),
|
||||||
CONSTRAINT fk_product_country_code FOREIGN KEY (product_code) REFERENCES bms_product (product_code) ON DELETE CASCADE ON UPDATE CASCADE
|
CONSTRAINT fk_product_country_code FOREIGN KEY (product_code) REFERENCES bms_product (product_code) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='产品国家服务明细-重量段+尺寸/尾程/时效(不含运费)';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='产品国家明细-重量段+运费挂号费+尺寸/尾程/时效';
|
||||||
|
|
||||||
-- 已建表升级(幂等:列已存在时跳过报错可忽略)
|
-- 已建表升级(幂等:列已存在时跳过报错可忽略)
|
||||||
-- ALTER TABLE bms_product_country ADD COLUMN weight_min decimal(10,3) DEFAULT NULL COMMENT '重量段最小值KG(含)' AFTER country_code;
|
-- ALTER TABLE bms_product_country ADD COLUMN weight_min decimal(10,3) DEFAULT NULL COMMENT '重量段最小值KG(含)' AFTER country_code;
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
-- 产品国家明细增加运费/挂号费(与 Excel 重量段同行)
|
||||||
|
-- 执行后请在「产品列表」重新导入威狮B2C小包价表.xlsx
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
|
||||||
|
ALTER TABLE bms_product_country
|
||||||
|
ADD COLUMN freight_price decimal(10,4) DEFAULT NULL COMMENT '运费 RMB/KG' AFTER weight_max,
|
||||||
|
ADD COLUMN registration_fee decimal(10,2) DEFAULT NULL COMMENT '挂号费 RMB/票' AFTER freight_price;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
-- 若之前 ALTER 将 COMMENT 写成乱码,执行此脚本修复
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
|
||||||
|
ALTER TABLE bms_product_country
|
||||||
|
MODIFY COLUMN freight_price decimal(10,4) DEFAULT NULL COMMENT '运费 RMB/KG',
|
||||||
|
MODIFY COLUMN registration_fee decimal(10,2) DEFAULT NULL COMMENT '挂号费 RMB/票';
|
||||||
@@ -57,6 +57,11 @@ const props = defineProps({
|
|||||||
updateSupportLabel: {
|
updateSupportLabel: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '是否更新已经存在的数据'
|
default: '是否更新已经存在的数据'
|
||||||
|
},
|
||||||
|
// 打开时是否默认勾选“更新已存在”
|
||||||
|
defaultUpdateSupport: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -77,7 +82,7 @@ const templateUrl = computed(() => !!props.templateAction)
|
|||||||
|
|
||||||
// 打开对话框(供父组件通过 ref 调用)
|
// 打开对话框(供父组件通过 ref 调用)
|
||||||
function open() {
|
function open() {
|
||||||
updateSupport.value = false
|
updateSupport.value = props.defaultUpdateSupport
|
||||||
isUploading.value = false
|
isUploading.value = false
|
||||||
visible.value = true
|
visible.value = true
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
:closable="false"
|
:closable="false"
|
||||||
show-icon
|
show-icon
|
||||||
class="mb16"
|
class="mb16"
|
||||||
title="Excel 导入:上传威狮B2C小包价表.xlsx。仅入库产品目录 + 各产品 sheet 的国家尺寸/尾程/时效;不导入运费、挂号费、退件重派、申报说明、赔偿标准。"
|
title="Excel 导入:上传威狮B2C小包价表.xlsx。入库产品目录 + 各产品 sheet 的国家×重量段(运费/挂号费/尺寸/尾程/时效);不导入退件重派、申报说明、赔偿标准。"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<basic-table
|
<basic-table
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #col-action="{ row }">
|
<template #col-action="{ row }">
|
||||||
<el-button link type="primary" icon="View" @click="openCountries(row)" v-hasPermi="['price:product:query']">国家明细</el-button>
|
<el-button link type="primary" icon="View" @click="openCountries(row)" v-hasPermi="['price:product:query']">产品明细</el-button>
|
||||||
<el-button link type="primary" icon="Edit" @click="handleUpdate(row)" v-hasPermi="['price:product:edit']">编辑</el-button>
|
<el-button link type="primary" icon="Edit" @click="handleUpdate(row)" v-hasPermi="['price:product:edit']">编辑</el-button>
|
||||||
<el-button link type="primary" icon="Delete" @click="handleDelete(row.productId)" v-hasPermi="['price:product:remove']">删除</el-button>
|
<el-button link type="primary" icon="Delete" @click="handleDelete(row.productId)" v-hasPermi="['price:product:remove']">删除</el-button>
|
||||||
</template>
|
</template>
|
||||||
@@ -122,7 +122,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<el-drawer v-model="countryOpen" size="720px" :title="countryTitle" append-to-body>
|
<el-drawer v-model="countryOpen" size="920px" :title="countryTitle" append-to-body>
|
||||||
<el-table v-loading="countryLoading" :data="countryList" border empty-text="暂无国家明细">
|
<el-table v-loading="countryLoading" :data="countryList" border empty-text="暂无国家明细">
|
||||||
<el-table-column label="国家" prop="countryName" width="90" show-overflow-tooltip />
|
<el-table-column label="国家" prop="countryName" width="90" show-overflow-tooltip />
|
||||||
<el-table-column label="ISO2" prop="countryCode" width="60" align="center" />
|
<el-table-column label="ISO2" prop="countryCode" width="60" align="center" />
|
||||||
@@ -132,6 +132,8 @@
|
|||||||
<span v-else>-</span>
|
<span v-else>-</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<el-table-column label="运费(RMB/KG)" prop="freightPrice" width="110" align="right" />
|
||||||
|
<el-table-column label="挂号费(RMB/票)" prop="registrationFee" width="120" align="right" />
|
||||||
<el-table-column label="尺寸限制" prop="sizeLimit" min-width="140">
|
<el-table-column label="尺寸限制" prop="sizeLimit" min-width="140">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span class="pre-wrap">{{ row.sizeLimit }}</span>
|
<span class="pre-wrap">{{ row.sizeLimit }}</span>
|
||||||
@@ -150,7 +152,8 @@
|
|||||||
ref="importRef"
|
ref="importRef"
|
||||||
title="产品 Excel 导入"
|
title="产品 Excel 导入"
|
||||||
action="/price/product/importData"
|
action="/price/product/importData"
|
||||||
update-support-label="是否更新已经存在的产品(含国家明细覆盖)"
|
:default-update-support="true"
|
||||||
|
update-support-label="是否更新已经存在的产品主数据(国家/运费明细每次导入都会覆盖)"
|
||||||
@success="() => tableRef.refresh()"
|
@success="() => tableRef.refresh()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
@click="form.setType = 'FIXED'"
|
@click="form.setType = 'FIXED'"
|
||||||
>
|
>
|
||||||
<div class="t-title">FIXED 固定价</div>
|
<div class="t-title">FIXED 固定价</div>
|
||||||
<div class="t-desc">每个产品×国家×重量段独立配置运费单价 + 挂号费,覆盖默认价</div>
|
<div class="t-desc">可不填明细(走产品默认价);有明细则按产品×国家×重量段覆盖运费+挂号费</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="type-card"
|
class="type-card"
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
:closable="false"
|
:closable="false"
|
||||||
show-icon
|
show-icon
|
||||||
class="mb12"
|
class="mb12"
|
||||||
title="FIXED 模式:每个产品 × 国家 × 重量段独立配置运费单价(RMB/KG)+ 挂号费(RMB/票)。命中时直接取该固定价。"
|
title="FIXED 模式:可不填明细(计费取 bms_product_country 默认运费/挂号费);有明细时按产品×国家×重量段覆盖固定价。"
|
||||||
/>
|
/>
|
||||||
<el-alert
|
<el-alert
|
||||||
v-else
|
v-else
|
||||||
@@ -88,24 +88,57 @@
|
|||||||
title="DISCOUNT 模式:可不填明细(全部产品适用默认折扣),或按产品限定折扣范围。"
|
title="DISCOUNT 模式:可不填明细(全部产品适用默认折扣),或按产品限定折扣范围。"
|
||||||
/>
|
/>
|
||||||
<el-table :data="form.details" border empty-text="暂无明细,点击添加明细行">
|
<el-table :data="form.details" border empty-text="暂无明细,点击添加明细行">
|
||||||
<el-table-column label="产品" min-width="150">
|
<el-table-column label="产品" min-width="180">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-select v-model="row.productCode" filterable allow-create placeholder="产品" @change="onProductChange(row)">
|
<el-select
|
||||||
|
v-model="row.productCode"
|
||||||
|
filterable
|
||||||
|
clearable
|
||||||
|
placeholder="选择产品"
|
||||||
|
style="width: 100%"
|
||||||
|
@change="onProductChange(row)"
|
||||||
|
>
|
||||||
<el-option v-for="p in productOptions" :key="p.value" :label="p.label" :value="p.value" />
|
<el-option v-for="p in productOptions" :key="p.value" :label="p.label" :value="p.value" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column v-if="form.setType === 'FIXED'" label="国家" min-width="130">
|
<el-table-column v-if="form.setType === 'FIXED'" label="国家" min-width="140">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-select v-model="row.countryCode" filterable allow-create placeholder="国家" @change="onCountryChange(row)">
|
<el-select
|
||||||
<el-option v-for="c in countryOptions" :key="c.value" :label="c.label" :value="c.value" />
|
v-model="row._countryKey"
|
||||||
|
filterable
|
||||||
|
clearable
|
||||||
|
placeholder="选择国家"
|
||||||
|
style="width: 100%"
|
||||||
|
:disabled="!row.productCode"
|
||||||
|
@change="onCountryChange(row)"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="c in countryOptionsOf(row)"
|
||||||
|
:key="c.key"
|
||||||
|
:label="c.label"
|
||||||
|
:value="c.key"
|
||||||
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column v-if="form.setType === 'FIXED'" label="重量段" min-width="150">
|
<el-table-column v-if="form.setType === 'FIXED'" label="重量段" min-width="150">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-select v-model="row.weightLabel" filterable allow-create placeholder="重量段">
|
<el-select
|
||||||
<el-option v-for="w in weightOptions" :key="w.value" :label="w.label" :value="w.value" />
|
v-model="row.weightLabel"
|
||||||
|
filterable
|
||||||
|
clearable
|
||||||
|
placeholder="选择重量段"
|
||||||
|
style="width: 100%"
|
||||||
|
:disabled="!row._countryKey"
|
||||||
|
@change="onWeightChange(row)"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="w in weightOptionsOf(row)"
|
||||||
|
:key="w.value"
|
||||||
|
:label="w.label"
|
||||||
|
:value="w.value"
|
||||||
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -212,6 +245,7 @@
|
|||||||
|
|
||||||
<script setup name="PriceSetCreate">
|
<script setup name="PriceSetCreate">
|
||||||
import { getPriceSet, savePriceSet, validatePriceSet } from '@/api/price/set'
|
import { getPriceSet, savePriceSet, validatePriceSet } from '@/api/price/set'
|
||||||
|
import { listProduct, listProductCountries } from '@/api/price/product'
|
||||||
|
|
||||||
const { proxy } = getCurrentInstance()
|
const { proxy } = getCurrentInstance()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -219,25 +253,9 @@ const active = ref(0)
|
|||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
const formRef = ref()
|
const formRef = ref()
|
||||||
const checkItems = ref([])
|
const checkItems = ref([])
|
||||||
|
const productOptions = ref([])
|
||||||
const productOptions = [
|
/** productCode -> bms_product_country rows */
|
||||||
{ value: 'WSP037', label: 'WSP037 球衣' },
|
const countryCache = reactive({})
|
||||||
{ value: 'WSP038', label: 'WSP038 普货' },
|
|
||||||
{ value: 'WSP034', label: 'WSP034 带电' },
|
|
||||||
{ value: 'WSP035', label: 'WSP035 敏感' },
|
|
||||||
{ value: 'WSP066', label: 'WSP066 鞋盒' }
|
|
||||||
]
|
|
||||||
const countryOptions = [
|
|
||||||
{ value: 'DE', label: '德国 DE' },
|
|
||||||
{ value: 'FR', label: '法国 FR' },
|
|
||||||
{ value: 'IT', label: '意大利 IT' },
|
|
||||||
{ value: 'ES', label: '西班牙 ES' }
|
|
||||||
]
|
|
||||||
const weightOptions = [
|
|
||||||
{ value: '0.01≤W≤0.3', label: '0.01≤W≤0.3' },
|
|
||||||
{ value: '0.3<W≤1', label: '0.3<W≤1' },
|
|
||||||
{ value: '1<W≤2', label: '1<W≤2' }
|
|
||||||
]
|
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
priceSetId: undefined,
|
priceSetId: undefined,
|
||||||
@@ -257,13 +275,93 @@ const rules = {
|
|||||||
effectiveDate: [{ required: true, message: '生效日期不能为空', trigger: 'change' }]
|
effectiveDate: [{ required: true, message: '生效日期不能为空', trigger: 'change' }]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function trimNum(v) {
|
||||||
|
if (v == null || v === '') return ''
|
||||||
|
const n = Number(v)
|
||||||
|
if (Number.isNaN(n)) return String(v)
|
||||||
|
return String(parseFloat(n.toFixed(6)))
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatWeightLabel(min, max) {
|
||||||
|
return `${trimNum(min)}≤W≤${trimNum(max)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function countryKeyOf(row) {
|
||||||
|
if (!row) return ''
|
||||||
|
if (row.countryCode) return row.countryCode
|
||||||
|
if (row.countryName) return 'N:' + row.countryName
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchCountry(detail, item) {
|
||||||
|
if (detail.countryCode && item.countryCode) {
|
||||||
|
return detail.countryCode === item.countryCode
|
||||||
|
}
|
||||||
|
return detail.countryName && item.countryName === detail.countryName
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadProducts() {
|
||||||
|
return listProduct({ status: '0', pageNum: 1, pageSize: 1000 }).then(res => {
|
||||||
|
const rows = res.rows || []
|
||||||
|
productOptions.value = rows.map(p => ({
|
||||||
|
value: p.productCode,
|
||||||
|
label: `${p.productCode} ${p.productName || ''}`.trim(),
|
||||||
|
name: p.productName
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureCountries(productCode) {
|
||||||
|
if (!productCode) return Promise.resolve([])
|
||||||
|
if (countryCache[productCode]) return Promise.resolve(countryCache[productCode])
|
||||||
|
return listProductCountries(productCode).then(res => {
|
||||||
|
const rows = res.data || []
|
||||||
|
countryCache[productCode] = rows
|
||||||
|
return rows
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function countryOptionsOf(row) {
|
||||||
|
const rows = countryCache[row.productCode] || []
|
||||||
|
const map = new Map()
|
||||||
|
rows.forEach(item => {
|
||||||
|
const key = countryKeyOf(item)
|
||||||
|
if (!key || map.has(key)) return
|
||||||
|
map.set(key, {
|
||||||
|
key,
|
||||||
|
code: item.countryCode || undefined,
|
||||||
|
name: item.countryName,
|
||||||
|
label: item.countryCode ? `${item.countryName} ${item.countryCode}` : item.countryName
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return [...map.values()]
|
||||||
|
}
|
||||||
|
|
||||||
|
function weightOptionsOf(row) {
|
||||||
|
const rows = (countryCache[row.productCode] || []).filter(item => matchCountry(row, item))
|
||||||
|
const map = new Map()
|
||||||
|
rows.forEach(item => {
|
||||||
|
if (item.weightMin == null && item.weightMax == null) return
|
||||||
|
const value = formatWeightLabel(item.weightMin, item.weightMax)
|
||||||
|
if (map.has(value)) return
|
||||||
|
map.set(value, {
|
||||||
|
value,
|
||||||
|
label: value,
|
||||||
|
freightPrice: item.freightPrice,
|
||||||
|
registrationFee: item.registrationFee
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return [...map.values()]
|
||||||
|
}
|
||||||
|
|
||||||
function emptyDetail() {
|
function emptyDetail() {
|
||||||
return {
|
return {
|
||||||
productCode: undefined,
|
productCode: undefined,
|
||||||
productName: undefined,
|
productName: undefined,
|
||||||
countryCode: form.setType === 'FIXED' ? 'DE' : undefined,
|
countryCode: undefined,
|
||||||
countryName: form.setType === 'FIXED' ? '德国' : undefined,
|
countryName: undefined,
|
||||||
weightLabel: form.setType === 'FIXED' ? '0.3<W≤1' : undefined,
|
_countryKey: undefined,
|
||||||
|
weightLabel: undefined,
|
||||||
fvValue: undefined,
|
fvValue: undefined,
|
||||||
fvRegistrationFee: undefined,
|
fvRegistrationFee: undefined,
|
||||||
discountValue: undefined
|
discountValue: undefined
|
||||||
@@ -288,22 +386,74 @@ function addCustomerRow() {
|
|||||||
form.customers.push(emptyCustomer())
|
form.customers.push(emptyCustomer())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearCascadeAfterProduct(row) {
|
||||||
|
row.countryCode = undefined
|
||||||
|
row.countryName = undefined
|
||||||
|
row._countryKey = undefined
|
||||||
|
row.weightLabel = undefined
|
||||||
|
row.fvValue = undefined
|
||||||
|
row.fvRegistrationFee = undefined
|
||||||
|
}
|
||||||
|
|
||||||
function onProductChange(row) {
|
function onProductChange(row) {
|
||||||
const found = productOptions.find(p => p.value === row.productCode)
|
const found = productOptions.value.find(p => p.value === row.productCode)
|
||||||
row.productName = found ? found.label.replace(row.productCode + ' ', '') : row.productCode
|
row.productName = found ? found.name : row.productCode
|
||||||
|
clearCascadeAfterProduct(row)
|
||||||
|
if (!row.productCode || form.setType !== 'FIXED') return
|
||||||
|
ensureCountries(row.productCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onCountryChange(row) {
|
function onCountryChange(row) {
|
||||||
const found = countryOptions.find(c => c.value === row.countryCode)
|
const opt = countryOptionsOf(row).find(c => c.key === row._countryKey)
|
||||||
row.countryName = found ? found.label.replace(' ' + row.countryCode, '') : row.countryCode
|
row.countryCode = opt ? opt.code : undefined
|
||||||
|
row.countryName = opt ? opt.name : undefined
|
||||||
|
row.weightLabel = undefined
|
||||||
|
row.fvValue = undefined
|
||||||
|
row.fvRegistrationFee = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function onWeightChange(row) {
|
||||||
|
const opt = weightOptionsOf(row).find(w => w.value === row.weightLabel)
|
||||||
|
if (!opt) return
|
||||||
|
if (opt.freightPrice != null) row.fvValue = Number(opt.freightPrice)
|
||||||
|
if (opt.registrationFee != null) row.fvRegistrationFee = Number(opt.registrationFee)
|
||||||
|
}
|
||||||
|
|
||||||
|
function hydrateDetailRow(d) {
|
||||||
|
const row = {
|
||||||
|
...d,
|
||||||
|
_countryKey: countryKeyOf(d)
|
||||||
|
}
|
||||||
|
// normalize historical weight labels to min≤W≤max when possible
|
||||||
|
if (row.productCode && form.setType === 'FIXED') {
|
||||||
|
ensureCountries(row.productCode).then(rows => {
|
||||||
|
if (!row.weightLabel) return
|
||||||
|
const hit = rows.find(item => {
|
||||||
|
if (!matchCountry(row, item)) return false
|
||||||
|
return formatWeightLabel(item.weightMin, item.weightMax) === row.weightLabel
|
||||||
|
|| row.weightLabel.includes(String(item.weightMin)) && row.weightLabel.includes(String(item.weightMax))
|
||||||
|
})
|
||||||
|
if (hit) {
|
||||||
|
row.weightLabel = formatWeightLabel(hit.weightMin, hit.weightMax)
|
||||||
|
row._countryKey = countryKeyOf(hit)
|
||||||
|
if (!row.countryCode) row.countryCode = hit.countryCode
|
||||||
|
if (!row.countryName) row.countryName = hit.countryName
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return row
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildPayload(submitFlag) {
|
function buildPayload(submitFlag) {
|
||||||
const details = (form.details || []).filter(d => d.productCode).map(d => ({
|
const details = (form.details || []).filter(d => d.productCode).map(d => ({
|
||||||
...d,
|
productCode: d.productCode,
|
||||||
|
productName: d.productName,
|
||||||
countryCode: d.countryCode || undefined,
|
countryCode: d.countryCode || undefined,
|
||||||
|
countryName: d.countryName || undefined,
|
||||||
|
weightLabel: d.weightLabel || undefined,
|
||||||
fvValue: d.fvValue,
|
fvValue: d.fvValue,
|
||||||
fvRegistrationFee: d.fvRegistrationFee
|
fvRegistrationFee: d.fvRegistrationFee,
|
||||||
|
discountValue: d.discountValue
|
||||||
}))
|
}))
|
||||||
const customers = (form.customers || [])
|
const customers = (form.customers || [])
|
||||||
.filter(c => c.customerId)
|
.filter(c => c.customerId)
|
||||||
@@ -392,7 +542,7 @@ function checkTag(result) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function checkLabel(result) {
|
function checkLabel(result) {
|
||||||
const map = { PASS: '? 通过', WARN: '? 待确认', FAIL: '? 失败', INFO: '? 提示' }
|
const map = { PASS: '通过', WARN: '待确认', FAIL: '失败', INFO: '提示' }
|
||||||
return map[result] || result
|
return map[result] || result
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,18 +556,20 @@ function loadExisting(id) {
|
|||||||
form.effectiveDate = data.effectiveDate
|
form.effectiveDate = data.effectiveDate
|
||||||
form.expireDate = data.expireDate
|
form.expireDate = data.expireDate
|
||||||
form.priceSetNote = data.priceSetNote
|
form.priceSetNote = data.priceSetNote
|
||||||
form.details = (data.details || []).map(d => ({ ...d }))
|
form.details = (data.details || []).map(d => hydrateDetailRow(d))
|
||||||
form.customers = (data.customers || []).map(c => ({ ...c }))
|
form.customers = (data.customers || []).map(c => ({ ...c }))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const id = route.query.priceSetId
|
loadProducts().finally(() => {
|
||||||
if (id) {
|
const id = route.query.priceSetId
|
||||||
loadExisting(id)
|
if (id) {
|
||||||
} else {
|
loadExisting(id)
|
||||||
addDetailRow()
|
} else {
|
||||||
}
|
addDetailRow()
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -126,21 +126,44 @@
|
|||||||
|
|
||||||
<el-dialog :title="detailTitle" v-model="detailOpen" width="560px" append-to-body>
|
<el-dialog :title="detailTitle" v-model="detailOpen" width="560px" append-to-body>
|
||||||
<el-form ref="detailFormRef" :model="detailForm" :rules="detailRules" label-width="120px">
|
<el-form ref="detailFormRef" :model="detailForm" :rules="detailRules" label-width="120px">
|
||||||
<el-form-item label="产品代码" prop="productCode">
|
<el-form-item label="产品" prop="productCode">
|
||||||
<el-input v-model="detailForm.productCode" maxlength="10" />
|
<el-select
|
||||||
</el-form-item>
|
v-model="detailForm.productCode"
|
||||||
<el-form-item label="产品名称" prop="productName">
|
filterable
|
||||||
<el-input v-model="detailForm.productName" maxlength="50" />
|
clearable
|
||||||
|
placeholder="选择产品"
|
||||||
|
style="width: 100%"
|
||||||
|
@change="onDetailProductChange"
|
||||||
|
>
|
||||||
|
<el-option v-for="p in productOptions" :key="p.value" :label="p.label" :value="p.value" />
|
||||||
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<template v-if="set.setType === 'FIXED'">
|
<template v-if="set.setType === 'FIXED'">
|
||||||
<el-form-item label="国家代码" prop="countryCode">
|
<el-form-item label="国家" prop="countryCode">
|
||||||
<el-input v-model="detailForm.countryCode" maxlength="2" />
|
<el-select
|
||||||
</el-form-item>
|
v-model="detailForm._countryKey"
|
||||||
<el-form-item label="国家名称" prop="countryName">
|
filterable
|
||||||
<el-input v-model="detailForm.countryName" maxlength="50" />
|
clearable
|
||||||
|
placeholder="选择国家"
|
||||||
|
style="width: 100%"
|
||||||
|
:disabled="!detailForm.productCode"
|
||||||
|
@change="onDetailCountryChange"
|
||||||
|
>
|
||||||
|
<el-option v-for="c in detailCountryOptions" :key="c.key" :label="c.label" :value="c.key" />
|
||||||
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="重量段" prop="weightLabel">
|
<el-form-item label="重量段" prop="weightLabel">
|
||||||
<el-input v-model="detailForm.weightLabel" maxlength="50" />
|
<el-select
|
||||||
|
v-model="detailForm.weightLabel"
|
||||||
|
filterable
|
||||||
|
clearable
|
||||||
|
placeholder="选择重量段"
|
||||||
|
style="width: 100%"
|
||||||
|
:disabled="!detailForm._countryKey"
|
||||||
|
@change="onDetailWeightChange"
|
||||||
|
>
|
||||||
|
<el-option v-for="w in detailWeightOptions" :key="w.value" :label="w.label" :value="w.value" />
|
||||||
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="运费单价" prop="fvValue">
|
<el-form-item label="运费单价" prop="fvValue">
|
||||||
<el-input-number v-model="detailForm.fvValue" :min="0" :precision="4" controls-position="right" style="width: 100%" />
|
<el-input-number v-model="detailForm.fvValue" :min="0" :precision="4" controls-position="right" style="width: 100%" />
|
||||||
@@ -194,19 +217,22 @@ import {
|
|||||||
addSetDetail, updateSetDetail, delSetDetail,
|
addSetDetail, updateSetDetail, delSetDetail,
|
||||||
addSetCustomer, updateSetCustomer, delSetCustomer
|
addSetCustomer, updateSetCustomer, delSetCustomer
|
||||||
} from '@/api/price/set'
|
} from '@/api/price/set'
|
||||||
|
import { listProduct, listProductCountries } from '@/api/price/product'
|
||||||
import { parseTime } from '@/utils/ruoyi'
|
import { parseTime } from '@/utils/ruoyi'
|
||||||
|
|
||||||
const { proxy } = getCurrentInstance()
|
const { proxy } = getCurrentInstance()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const set = ref({})
|
const set = ref({})
|
||||||
|
const productOptions = ref([])
|
||||||
|
const countryCache = reactive({})
|
||||||
|
|
||||||
const detailOpen = ref(false)
|
const detailOpen = ref(false)
|
||||||
const detailTitle = ref('')
|
const detailTitle = ref('')
|
||||||
const detailFormRef = ref()
|
const detailFormRef = ref()
|
||||||
const detailForm = ref({})
|
const detailForm = ref({})
|
||||||
const detailRules = {
|
const detailRules = {
|
||||||
productCode: [{ required: true, message: '产品代码不能为空', trigger: 'blur' }]
|
productCode: [{ required: true, message: '请选择产品', trigger: 'change' }]
|
||||||
}
|
}
|
||||||
|
|
||||||
const customerOpen = ref(false)
|
const customerOpen = ref(false)
|
||||||
@@ -218,6 +244,113 @@ const customerRules = {
|
|||||||
prBegindate: [{ required: true, message: '生效日期不能为空', trigger: 'change' }]
|
prBegindate: [{ required: true, message: '生效日期不能为空', trigger: 'change' }]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function trimNum(v) {
|
||||||
|
if (v == null || v === '') return ''
|
||||||
|
const n = Number(v)
|
||||||
|
if (Number.isNaN(n)) return String(v)
|
||||||
|
return String(parseFloat(n.toFixed(6)))
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatWeightLabel(min, max) {
|
||||||
|
return `${trimNum(min)}≤W≤${trimNum(max)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function countryKeyOf(row) {
|
||||||
|
if (!row) return ''
|
||||||
|
if (row.countryCode) return row.countryCode
|
||||||
|
if (row.countryName) return 'N:' + row.countryName
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchCountry(detail, item) {
|
||||||
|
if (detail.countryCode && item.countryCode) return detail.countryCode === item.countryCode
|
||||||
|
return detail.countryName && item.countryName === detail.countryName
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureCountries(productCode) {
|
||||||
|
if (!productCode) return Promise.resolve([])
|
||||||
|
if (countryCache[productCode]) return Promise.resolve(countryCache[productCode])
|
||||||
|
return listProductCountries(productCode).then(res => {
|
||||||
|
const rows = res.data || []
|
||||||
|
countryCache[productCode] = rows
|
||||||
|
return rows
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const detailCountryOptions = computed(() => {
|
||||||
|
const rows = countryCache[detailForm.value.productCode] || []
|
||||||
|
const map = new Map()
|
||||||
|
rows.forEach(item => {
|
||||||
|
const key = countryKeyOf(item)
|
||||||
|
if (!key || map.has(key)) return
|
||||||
|
map.set(key, {
|
||||||
|
key,
|
||||||
|
code: item.countryCode || undefined,
|
||||||
|
name: item.countryName,
|
||||||
|
label: item.countryCode ? `${item.countryName} ${item.countryCode}` : item.countryName
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return [...map.values()]
|
||||||
|
})
|
||||||
|
|
||||||
|
const detailWeightOptions = computed(() => {
|
||||||
|
const form = detailForm.value
|
||||||
|
const rows = (countryCache[form.productCode] || []).filter(item => matchCountry(form, item))
|
||||||
|
const map = new Map()
|
||||||
|
rows.forEach(item => {
|
||||||
|
if (item.weightMin == null && item.weightMax == null) return
|
||||||
|
const value = formatWeightLabel(item.weightMin, item.weightMax)
|
||||||
|
if (map.has(value)) return
|
||||||
|
map.set(value, {
|
||||||
|
value,
|
||||||
|
label: value,
|
||||||
|
freightPrice: item.freightPrice,
|
||||||
|
registrationFee: item.registrationFee
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return [...map.values()]
|
||||||
|
})
|
||||||
|
|
||||||
|
function loadProducts() {
|
||||||
|
listProduct({ status: '0', pageNum: 1, pageSize: 1000 }).then(res => {
|
||||||
|
productOptions.value = (res.rows || []).map(p => ({
|
||||||
|
value: p.productCode,
|
||||||
|
label: `${p.productCode} ${p.productName || ''}`.trim(),
|
||||||
|
name: p.productName
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDetailProductChange() {
|
||||||
|
const found = productOptions.value.find(p => p.value === detailForm.value.productCode)
|
||||||
|
detailForm.value.productName = found ? found.name : detailForm.value.productCode
|
||||||
|
detailForm.value.countryCode = undefined
|
||||||
|
detailForm.value.countryName = undefined
|
||||||
|
detailForm.value._countryKey = undefined
|
||||||
|
detailForm.value.weightLabel = undefined
|
||||||
|
detailForm.value.fvValue = undefined
|
||||||
|
detailForm.value.fvRegistrationFee = undefined
|
||||||
|
if (detailForm.value.productCode && set.value.setType === 'FIXED') {
|
||||||
|
ensureCountries(detailForm.value.productCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDetailCountryChange() {
|
||||||
|
const opt = detailCountryOptions.value.find(c => c.key === detailForm.value._countryKey)
|
||||||
|
detailForm.value.countryCode = opt ? opt.code : undefined
|
||||||
|
detailForm.value.countryName = opt ? opt.name : undefined
|
||||||
|
detailForm.value.weightLabel = undefined
|
||||||
|
detailForm.value.fvValue = undefined
|
||||||
|
detailForm.value.fvRegistrationFee = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDetailWeightChange() {
|
||||||
|
const opt = detailWeightOptions.value.find(w => w.value === detailForm.value.weightLabel)
|
||||||
|
if (!opt) return
|
||||||
|
if (opt.freightPrice != null) detailForm.value.fvValue = Number(opt.freightPrice)
|
||||||
|
if (opt.registrationFee != null) detailForm.value.fvRegistrationFee = Number(opt.registrationFee)
|
||||||
|
}
|
||||||
|
|
||||||
const childEditable = computed(() => ['D', 'R', 'E'].includes(set.value.status))
|
const childEditable = computed(() => ['D', 'R', 'E'].includes(set.value.status))
|
||||||
|
|
||||||
const statusLabel = computed(() => {
|
const statusLabel = computed(() => {
|
||||||
@@ -296,12 +429,16 @@ function onCopy() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openDetailDialog(row) {
|
function openDetailDialog(row) {
|
||||||
detailForm.value = row ? { ...row } : {
|
detailForm.value = row ? {
|
||||||
|
...row,
|
||||||
|
_countryKey: countryKeyOf(row)
|
||||||
|
} : {
|
||||||
priceSetId: set.value.priceSetId,
|
priceSetId: set.value.priceSetId,
|
||||||
productCode: undefined,
|
productCode: undefined,
|
||||||
productName: undefined,
|
productName: undefined,
|
||||||
countryCode: undefined,
|
countryCode: undefined,
|
||||||
countryName: undefined,
|
countryName: undefined,
|
||||||
|
_countryKey: undefined,
|
||||||
weightLabel: undefined,
|
weightLabel: undefined,
|
||||||
fvValue: undefined,
|
fvValue: undefined,
|
||||||
fvRegistrationFee: undefined,
|
fvRegistrationFee: undefined,
|
||||||
@@ -309,12 +446,19 @@ function openDetailDialog(row) {
|
|||||||
}
|
}
|
||||||
detailTitle.value = row ? '编辑明细' : '添加明细'
|
detailTitle.value = row ? '编辑明细' : '添加明细'
|
||||||
detailOpen.value = true
|
detailOpen.value = true
|
||||||
|
if (detailForm.value.productCode && set.value.setType === 'FIXED') {
|
||||||
|
ensureCountries(detailForm.value.productCode)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function submitDetail() {
|
function submitDetail() {
|
||||||
proxy.$refs['detailFormRef'].validate(valid => {
|
proxy.$refs['detailFormRef'].validate(valid => {
|
||||||
if (!valid) return
|
if (!valid) return
|
||||||
const payload = { ...detailForm.value, priceSetId: set.value.priceSetId }
|
const payload = {
|
||||||
|
...detailForm.value,
|
||||||
|
priceSetId: set.value.priceSetId
|
||||||
|
}
|
||||||
|
delete payload._countryKey
|
||||||
const req = payload.psdId ? updateSetDetail(payload) : addSetDetail(payload)
|
const req = payload.psdId ? updateSetDetail(payload) : addSetDetail(payload)
|
||||||
req.then(() => {
|
req.then(() => {
|
||||||
proxy.$modal.msgSuccess('保存成功')
|
proxy.$modal.msgSuccess('保存成功')
|
||||||
@@ -369,6 +513,7 @@ function removeCustomer(row) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
loadProducts()
|
||||||
load()
|
load()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user