应付配置的附加费配置完善

This commit is contained in:
liuminghui
2026-11-13 11:57:19 +08:00
parent 6675808dfc
commit 3a104d2d8c
20 changed files with 1369 additions and 310 deletions
@@ -0,0 +1,118 @@
package com.bms.price.domain;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
/** 应付附加费配置在某一产品版本上的操作记录。 */
public class PaySurchargeRuleLog
{
private Long logId;
private Long productId;
private Long termId;
private Integer versionNo;
/** 1新增 2修改 3删除 */
private String operationType;
private String ruleKey;
private String changeContent;
private String createBy;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
public Long getLogId()
{
return logId;
}
public void setLogId(Long logId)
{
this.logId = logId;
}
public Long getProductId()
{
return productId;
}
public void setProductId(Long productId)
{
this.productId = productId;
}
public Long getTermId()
{
return termId;
}
public void setTermId(Long termId)
{
this.termId = termId;
}
public Integer getVersionNo()
{
return versionNo;
}
public void setVersionNo(Integer versionNo)
{
this.versionNo = versionNo;
}
public String getOperationType()
{
return operationType;
}
public void setOperationType(String operationType)
{
this.operationType = operationType;
}
public String getRuleKey()
{
return ruleKey;
}
public void setRuleKey(String ruleKey)
{
this.ruleKey = ruleKey;
}
public String getChangeContent()
{
return changeContent;
}
public void setChangeContent(String changeContent)
{
this.changeContent = changeContent;
}
public String getCreateBy()
{
return createBy;
}
public void setCreateBy(String createBy)
{
this.createBy = createBy;
}
public Date getCreateTime()
{
return createTime;
}
public void setCreateTime(Date createTime)
{
this.createTime = createTime;
}
}
@@ -0,0 +1,14 @@
package com.bms.price.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.bms.price.domain.PaySurchargeRuleLog;
public interface PaySurchargeRuleLogMapper
{
List<PaySurchargeRuleLog> selectByTerm(@Param("productId") Long productId, @Param("termId") Long termId);
List<PaySurchargeRuleLog> selectByProductIds(@Param("productIds") List<Long> productIds);
int insertBatch(@Param("list") List<PaySurchargeRuleLog> list);
}
@@ -4,15 +4,16 @@ import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.bms.price.domain.PaySurchargeRule;
/** 应付附加费计费条件。一个产品版本一份。 */
/** 应付附加费计费条件。一份跟产品走。 */
public interface PaySurchargeRuleMapper
{
List<PaySurchargeRule> selectByProduct(@Param("productId") Long productId, @Param("termId") Long termId);
List<PaySurchargeRule> selectByProductId(@Param("productId") Long productId);
int deleteByTerm(@Param("productId") Long productId, @Param("termId") Long termId);
int insertBatch(@Param("list") List<PaySurchargeRule> rules);
int deleteByProductIds(@Param("productIds") List<Long> productIds);
/** 同一产品字典下,比当前版本号小的最近一版 */
PaySurchargeRule selectPreviousVersion(@Param("productId") Long productId);
int insertBatch(@Param("list") List<PaySurchargeRule> rules);
}
@@ -101,6 +101,17 @@ public final class PayCostCalculator
public static PayOrderCostResult calculate(PayProduct product, BigDecimal weight, BigDecimal length, BigDecimal width,
BigDecimal height, String postcode, String zoneName, Date bizDate, Integer orderStatus, List<PayRemoteBook.Hit> remoteHits,
boolean postcodeOn, boolean zoneOn)
{
return calculate(product, weight, length, width, height, postcode, zoneName, bizDate, orderStatus, remoteHits,
postcodeOn, zoneOn, null, null);
}
/**
* 计算一票尾程应付。countryCode、countryName 用来核对配置页上写了国家的命中邮编和命中分区。
*/
public static PayOrderCostResult calculate(PayProduct product, BigDecimal weight, BigDecimal length, BigDecimal width,
BigDecimal height, String postcode, String zoneName, Date bizDate, Integer orderStatus, List<PayRemoteBook.Hit> remoteHits,
boolean postcodeOn, boolean zoneOn, String countryCode, String countryName)
{
PayOrderCostResult result = new PayOrderCostResult();
result.setFees(new ArrayList<>());
@@ -158,7 +169,7 @@ public final class PayCostCalculator
result.getFees().add(freightLine);
result.setCurrency(currency);
addSurcharges(product, weight, length, width, height, postcode, zoneName, bizDate, orderStatus, freight, currency,
band.getSectionName(), remoteHits, postcodeOn, zoneOn, result);
band.getSectionName(), remoteHits, postcodeOn, zoneOn, countryCode, countryName, result);
BigDecimal total = BigDecimal.ZERO;
for (PayCostLine fee : result.getFees())
{
@@ -278,7 +289,8 @@ public final class PayCostCalculator
*/
private static void addSurcharges(PayProduct product, BigDecimal weight, BigDecimal length, BigDecimal width,
BigDecimal height, String postcode, String zoneName, Date bizDate, Integer orderStatus, BigDecimal freight, String currency,
String freightSection, List<PayRemoteBook.Hit> remoteHits, boolean postcodeOn, boolean zoneOn, PayOrderCostResult result)
String freightSection, List<PayRemoteBook.Hit> remoteHits, boolean postcodeOn, boolean zoneOn,
String countryCode, String countryName, PayOrderCostResult result)
{
if (product.getSurcharges() == null)
{
@@ -292,7 +304,9 @@ public final class PayCostCalculator
continue;
}
String rule = fee.getCalcRule() == null || fee.getCalcRule().isBlank() ? PaySurchargeRules.MANUAL : fee.getCalcRule();
if (remoteHits != null && remoteFee(fee))
List<Map<String, Object>> postcodeRules = prefixRules(product, PaySurchargeRules.POSTCODE);
List<Map<String, Object>> zoneRules = prefixRules(product, PaySurchargeRules.ZONE);
if (remoteHits != null && remoteFee(fee) && postcodeRules.isEmpty())
{
String name = blank(fee.getFeeName());
boolean added = false;
@@ -332,8 +346,9 @@ public final class PayCostCalculator
result.getSkipped().add(blank(fee.getFeeName()) + ":未打开命中分区");
continue;
}
boolean configuredZone = PaySurchargeRules.ZONE.equals(rule) && !zoneRules.isEmpty();
boolean postcodeDriven = PaySurchargeRules.POSTCODE.equals(rule) || !blank(fee.getPostcodeParam()).isEmpty();
if (!postcodeDriven && zoneName != null && !zoneName.isBlank() && fee.getZoneName() != null && !fee.getZoneName().isBlank()
if (!configuredZone && !postcodeDriven && zoneName != null && !zoneName.isBlank() && fee.getZoneName() != null && !fee.getZoneName().isBlank()
&& !PayZoneMatcher.sameZone(fee.getZoneName(), zoneName))
{
continue;
@@ -341,22 +356,45 @@ public final class PayCostCalculator
String name = blank(fee.getFeeName());
if (PaySurchargeRules.POSTCODE.equals(rule))
{
String param = blank(fee.getPostcodeParam()).isEmpty() ? fee.getCalcParam() : fee.getPostcodeParam();
if (postcode == null || postcode.isBlank())
if (!postcodeRules.isEmpty())
{
result.getSkipped().add(name + ":订单没有邮编");
addConfiguredZoneFee(result, fee, weight, currency, postcode, countryCode, countryName, postcodeRules, "邮编 ");
}
else if (PaySurchargeRules.matchesPostcode(param, postcode) && weightAtLeast(fee, weight))
else
{
addFlat(result.getFees(), fee, currency, "邮编 " + postcode.trim());
String param = blank(fee.getPostcodeParam()).isEmpty() ? fee.getCalcParam() : fee.getPostcodeParam();
if (postcode == null || postcode.isBlank())
{
result.getSkipped().add(name + ":订单没有邮编");
}
else if (PaySurchargeRules.matchesPostcode(param, postcode) && weightAtLeast(fee, weight))
{
addFlat(result.getFees(), fee, currency, "邮编 " + postcode.trim());
}
}
}
else if (PaySurchargeRules.ZONE.equals(rule))
{
boolean already = result.getFees().stream().anyMatch(line -> name.equals(line.getFeeName()));
if (!already && zoneName != null && !zoneName.isBlank() && PayZoneMatcher.sameZone(fee.getZoneName(), zoneName) && weightIn(fee, weight))
if (!zoneRules.isEmpty())
{
addFlat(result.getFees(), fee, currency, "地区 " + zoneName.replace('\n', ' '));
String hit = PaySurchargeRules.matchedZone(zoneRules, postcode, countryCode, countryName);
boolean already = result.getFees().stream().anyMatch(line -> name.equals(line.getFeeName()));
if (postcode == null || postcode.isBlank())
{
result.getSkipped().add(name + ":订单没有邮编");
}
else if (!already && hit != null && PayZoneMatcher.sameZone(fee.getZoneName(), hit) && weightIn(fee, weight))
{
addFlat(result.getFees(), fee, currency, "地区 " + hit.replace('\n', ' '));
}
}
else
{
boolean already = result.getFees().stream().anyMatch(line -> name.equals(line.getFeeName()));
if (!already && zoneName != null && !zoneName.isBlank() && PayZoneMatcher.sameZone(fee.getZoneName(), zoneName) && weightIn(fee, weight))
{
addFlat(result.getFees(), fee, currency, "地区 " + zoneName.replace('\n', ' '));
}
}
}
else if (PaySurchargeRules.WEIGHT.equals(rule))
@@ -566,6 +604,38 @@ public final class PayCostCalculator
return false;
}
/** 配置页写了邮编前缀的命中邮编或命中分区。命中后只收同一分区的那一档附加费。 */
private static void addConfiguredZoneFee(PayOrderCostResult result, PaySurcharge fee, BigDecimal weight, String currency, String postcode,
String countryCode, String countryName, List<Map<String, Object>> rules, String remark)
{
String name = blank(fee.getFeeName());
if (postcode == null || postcode.isBlank())
{
result.getSkipped().add(name + ":订单没有邮编");
return;
}
String hit = PaySurchargeRules.matchedZone(rules, postcode, countryCode, countryName);
if (hit != null && PayZoneMatcher.sameZone(fee.getZoneName(), hit) && weightAtLeast(fee, weight))
{
addFlat(result.getFees(), fee, currency, remark + postcode.trim());
}
}
/** 只留下写了邮编前缀的配置。没写前缀的旧开关仍走原来的邮编配置。 */
private static List<Map<String, Object>> prefixRules(PayProduct product, String calcRule)
{
List<Map<String, Object>> rules = new ArrayList<>();
for (Map<String, Object> detail : ruleDetails(product, calcRule))
{
Object postcodes = detail.get("postcodes");
if (postcodes != null && !String.valueOf(postcodes).isBlank())
{
rules.add(detail);
}
}
return rules;
}
private static List<Map<String, Object>> ruleDetails(PayProduct product, String calcRule)
{
List<Map<String, Object>> details = new ArrayList<>();
@@ -36,6 +36,8 @@ public final class PaySurchargeRules
private static final Pattern RANGE = Pattern.compile("([A-Z]{1,2})(\\d{1,2})\\s*-\\s*(\\d{1,2})");
private static final Pattern DIGIT_RANGE = Pattern.compile("(\\d{2,10})\\s*-\\s*(\\d{2,10})");
private static final Pattern DISTRICT = Pattern.compile("([A-Z]{1,2})(\\d{1,2})");
private static final Pattern AREA = Pattern.compile("^[A-Z]{1,2}$");
@@ -147,6 +149,132 @@ public final class PaySurchargeRules
return false;
}
/**
* 配置页上的邮编。逗号或换行分隔。短码按前缀,长码按完整邮编,数字区间和英国邮编区间都认。
* 返回命中的最长一段长度,没有命中时为 -1。
*/
public static int configuredMatchLength(String param, String postcode)
{
if (param == null || param.isBlank() || postcode == null || postcode.isBlank())
{
return -1;
}
int best = -1;
for (String token : param.split("[,,、;;\\n\\r]+"))
{
String value = token.trim();
if (value.isEmpty())
{
continue;
}
int length = tokenLength(value, postcode);
if (length > best)
{
best = length;
}
}
return best;
}
/**
* 在命中邮编或命中分区的配置里,选出和订单邮编最贴合的分区。同一长度对上多个分区时返回 null。
*/
public static String matchedZone(java.util.List<java.util.Map<String, Object>> details, String postcode,
String countryCode, String countryName)
{
if (details == null || details.isEmpty() || postcode == null || postcode.isBlank())
{
return null;
}
String bestZone = null;
int best = -1;
boolean tie = false;
for (java.util.Map<String, Object> detail : details)
{
if (detail == null || !countryOk(detail, countryCode, countryName))
{
continue;
}
int length = configuredMatchLength(text(detail.get("postcodes")), postcode);
if (length < 0)
{
continue;
}
String zone = text(detail.get("zoneName"));
if (length > best)
{
best = length;
bestZone = zone;
tie = false;
}
else if (length == best && !PayZoneMatcher.sameZone(zone, bestZone))
{
tie = true;
}
}
return tie ? null : bestZone;
}
private static int tokenLength(String token, String postcode)
{
Matcher range = DIGIT_RANGE.matcher(token);
if (range.matches())
{
String from = range.group(1);
String to = range.group(2);
String digits = postcode.replaceAll("\\D", "");
if (from.length() != to.length() || digits.length() != from.length())
{
return -1;
}
long low = Long.parseLong(from);
long high = Long.parseLong(to);
if (low > high)
{
long swap = low;
low = high;
high = swap;
}
long current = Long.parseLong(digits);
return current >= low && current <= high ? from.length() : -1;
}
if (!matchesPostcode(token, postcode))
{
return -1;
}
String compact = token.toUpperCase(Locale.ROOT).replaceAll("[^A-Z0-9]", "");
return compact.isEmpty() ? -1 : compact.length();
}
/** 配置没写国家时不限制。写了国家时,订单目的国二字码或名称要对上其中一项。 */
private static boolean countryOk(java.util.Map<String, Object> detail, String countryCode, String countryName)
{
String raw = text(detail.get("country"));
if (raw == null)
{
return true;
}
boolean listed = false;
for (String token : raw.split("[,,、;;\\s]+"))
{
String value = token.trim();
if (value.isEmpty())
{
continue;
}
listed = true;
if (countryCode != null && countryCode.equalsIgnoreCase(value))
{
return true;
}
if (countryName != null && countryName.trim().equals(value))
{
return true;
}
}
return !listed;
}
/** 从“07邮编开头”“35、38邮编开头”取出数字前缀。没有邮编字样时不猜。 */
public static String remotePrefixes(String text)
{
@@ -233,7 +361,7 @@ public final class PaySurchargeRules
return checks.contains(Boolean.TRUE);
}
/** 每票都收。写了费用名的规则只约束这一项;带生效月日的规则只在这段日期内收取。 */
/** 每票都收。配置了条件之后,只有写明费用项的规则才收取这一项;带生效月日的只在这段日期内收取。 */
public static boolean allowsAlways(List<java.util.Map<String, Object>> details, String feeName, java.util.Date bizDate)
{
if (details == null || details.isEmpty())
@@ -241,8 +369,11 @@ public final class PaySurchargeRules
return true;
}
String name = feeName == null ? "" : feeName.trim();
List<java.util.Map<String, Object>> named = new ArrayList<>();
List<java.util.Map<String, Object>> shared = new ArrayList<>();
if (name.isEmpty())
{
return false;
}
java.time.LocalDate day = bizDate == null ? null : bizDate.toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate();
for (java.util.Map<String, Object> detail : details)
{
if (detail == null)
@@ -250,23 +381,10 @@ public final class PaySurchargeRules
continue;
}
String ruleName = detail.get("feeName") == null ? "" : String.valueOf(detail.get("feeName")).trim();
if (ruleName.isEmpty())
if (!ruleName.equals(name))
{
shared.add(detail);
continue;
}
else if (ruleName.equals(name))
{
named.add(detail);
}
}
List<java.util.Map<String, Object>> mine = named.isEmpty() ? shared : named;
if (mine.isEmpty())
{
return shared.isEmpty();
}
java.time.LocalDate day = bizDate == null ? null : bizDate.toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate();
for (java.util.Map<String, Object> detail : mine)
{
if (!hasPeriod(detail))
{
return true;
@@ -46,6 +46,10 @@ public final class PayTexts
private static final Pattern MONEY = Pattern.compile(
"(?i)(\\d+(?:\\.\\d+)?)\\s*(EUR|GBP|USD|RMB|CNY|欧元|英镑|人民币)?");
/** GBP 23.00 这种币种写在数字前面 */
private static final Pattern MONEY_BEFORE = Pattern.compile(
"(?i)(EUR|GBP|USD|RMB|CNY|欧元|英镑|人民币)\\s*(\\d+(?:\\.\\d+)?)");
private static final Pattern PERCENT = Pattern.compile("(\\d+(?:\\.\\d+)?)\\s*%");
private PayTexts()
@@ -193,6 +197,11 @@ public final class PayTexts
return found;
}
}
Matcher ahead = MONEY_BEFORE.matcher(source);
if (ahead.find())
{
return new Money(decimal(ahead.group(2)), currency(ahead.group(1)), unit(source));
}
return found;
}
@@ -828,6 +828,20 @@ public class PayWorkbookParser
private static boolean mentionsHighRisk(PayProduct product)
{
if (product.getServiceNote() != null && product.getServiceNote().contains("高风险"))
{
return true;
}
if (product.getExtraLines() != null)
{
for (PayExtraLine line : product.getExtraLines())
{
if (line.getLineText() != null && line.getLineText().contains("高风险"))
{
return true;
}
}
}
if (product.getSurcharges() == null)
{
return false;
@@ -1172,7 +1186,12 @@ public class PayWorkbookParser
continue;
}
String line = joinRow(grid, row);
// 备注、说明段落不是表格,不写入附加费,也不收进服务说明
// 特别注意、体积重计算方式这类说明不是附加费,也不当作下一张表的标题
if (isNarrative(line))
{
row++;
continue;
}
if (countText(grid, row) <= 2 && line.length() <= 40 && !line.contains("特别申明") && !line.contains("。"))
{
pending = line;
@@ -1305,13 +1324,25 @@ public class PayWorkbookParser
String zone = zoneOf(sub[col], parent[col]).replace('\n', ' ').replaceAll("\\s+", " ").trim();
String standard = price.stripTrailingZeros().toPlainString() + " " + feeUnit(parent[col], weight);
String desc = country.isBlank() ? weight : country + ";" + weight;
addSurcharge(product, feeName, feeName, zone, standard.trim(), desc, "");
PaySurcharge fee = product.getSurcharges().get(product.getSurcharges().size() - 1);
applyFeeRateRule(fee, feeName, zone, weight, includedKg);
if (addSurcharge(product, feeName, feeName, zone, standard.trim(), desc, ""))
{
PaySurcharge fee = product.getSurcharges().get(product.getSurcharges().size() - 1);
applyFeeRateRule(fee, feeName, zone, weight, includedKg);
}
}
if (!priced && joinRow(grid, data).length() > 40)
if (!priced)
{
break;
String line = joinRow(grid, data);
if (line.contains("特别注意") || line.contains("体积重") || line.contains("计算方式"))
{
addConfig(product, line);
data++;
break;
}
if (line.length() > 40)
{
break;
}
}
data++;
}
@@ -1492,9 +1523,19 @@ public class PayWorkbookParser
band.setSortNo(product.getWeightBands().size() + 1);
product.getWeightBands().add(band);
}
if (!priced && joinRow(grid, data).length() > 40)
if (!priced)
{
break;
String line = joinRow(grid, data);
if (line.contains("特别注意") || line.contains("体积重") || line.contains("计算方式"))
{
addConfig(product, line);
data++;
break;
}
if (line.length() > 40)
{
break;
}
}
data++;
}
@@ -1867,9 +1908,8 @@ public class PayWorkbookParser
markManual(product);
}
}
else if (!sentence.isBlank())
else if (!sentence.isBlank() && addSurcharge(product, section, name, "", sentence, sentence, ""))
{
addSurcharge(product, section, name, "", sentence, sentence, "");
markManual(product);
keepSingleEuro(product.getSurcharges().get(product.getSurcharges().size() - 1), sentence);
}
@@ -2156,7 +2196,10 @@ public class PayWorkbookParser
}
desc.append(String.join("、", entry.getValue())).append(" ").append(entry.getKey());
}
addSurcharge(product, section, "Cubic Capacity", "", String.join("、", factors.keySet()), desc.toString(), "");
if (!addSurcharge(product, section, "Cubic Capacity", "", String.join("、", factors.keySet()), desc.toString(), ""))
{
return;
}
PaySurcharge fee = product.getSurcharges().get(product.getSurcharges().size() - 1);
fee.setChargeText(String.join("、", factors.keySet()));
fee.setAmount(null);
@@ -3277,11 +3320,21 @@ public class PayWorkbookParser
addSurcharge(product, cleanTitle(pending), blankTo(name, "附加费"), "", standard, desc, "");
}
private void addSurcharge(PayProduct product, String section, String name, String zone, String standard, String desc, String remark)
/** 没有价格、币种、单位的说明不记成附加费。报价里的「不含燃油」仍然保留。 */
private boolean addSurcharge(PayProduct product, String section, String name, String zone, String standard, String desc, String remark)
{
String combined = (standard + " " + desc + " " + remark).trim();
Period period = PayTexts.period(combined, 2026);
Money money = PayTexts.money(blankTo(standard, combined), period);
// 收费标准自己要能认出价格、币种或单位。说明里的 5000、10KG 不能拿来凑成一条附加费
Money money = PayTexts.money(standard, period);
if (!hasCharge(money, standard) && fuelMark(standard).isEmpty() && !"Cubic Capacity".equals(name))
{
if ((name != null && name.contains("高风险")) || (standard != null && standard.contains("高风险")))
{
addConfig(product, (name + " " + standard).trim());
}
return false;
}
PaySurcharge fee = new PaySurcharge();
fee.setSectionName(section);
fee.setFeeName(cut(new StringBuilder(name), 200));
@@ -3309,6 +3362,7 @@ public class PayWorkbookParser
fee.setSortNo(product.getSurcharges().size() + 1);
PaySurchargeRules.apply(fee);
product.getSurcharges().add(fee);
return true;
}
/** 附加费币种为空时取价卡币种,价卡也没有则默认 EUR。 */
@@ -4239,7 +4293,10 @@ public class PayWorkbookParser
{
String feeName = shortCharge(head);
String standard = price.stripTrailingZeros().toPlainString() + " " + feeUnit(head, weight);
addSurcharge(product, section, feeName, zone, standard.trim(), weight, "");
if (!addSurcharge(product, section, feeName, zone, standard.trim(), weight, ""))
{
return;
}
PaySurcharge fee = product.getSurcharges().get(product.getSurcharges().size() - 1);
BigDecimal[] bounds = PayTexts.weightBounds(weight);
if (bounds[0] != null && bounds[1] != null)
@@ -4341,7 +4398,7 @@ public class PayWorkbookParser
private static boolean isLooseSurcharge(ExcelGrid grid, int row)
{
String line = joinRow(grid, row);
if (!line.contains("费") || line.length() > 500)
if (!line.contains("费") || line.length() > 500 || isNarrative(line))
{
return false;
}
@@ -4820,7 +4877,56 @@ public class PayWorkbookParser
return "";
}
String value = title.trim();
return value.length() > 80 ? "" : value;
if (value.length() > 80 || isNarrative(value))
{
return "";
}
return value;
}
/**
* 收费标准要带币种或计价单位。纯说明里的 90cm、5000、10KG 不算价格。
* 单元格里单独一个数字仍然算价格。
*/
private static boolean hasCharge(PayTexts.Money money, String standard)
{
if (money == null)
{
return false;
}
if (money.currency != null && !money.currency.isBlank())
{
return true;
}
if (money.unit != null && !money.unit.isBlank())
{
return true;
}
String text = standard == null ? "" : standard.trim();
if (money.amount != null && text.matches("\\d+(?:\\.\\d+)?"))
{
return true;
}
// 按 31.5kg 收费。体积公式里的 kg/m3 不是计价单位
String upper = text.toUpperCase(Locale.ROOT);
return money.amount != null && text.length() <= 80 && upper.matches("(?s).*\\d(?:\\.\\d+)?\\s*KG(?!/).*")
&& !upper.contains("M3") && !text.contains("立方米");
}
/** 价卡里的说明句。提到费用或除数,也不当成附加费。 */
private static boolean isNarrative(String line)
{
if (line == null || line.isBlank())
{
return false;
}
String text = line.replace('\n', ' ').trim();
if (text.contains("特别注意") || text.contains("特别申明") || text.contains("体积重")
|| text.contains("计算方式") || text.contains("计费说明") || text.contains("计费重量"))
{
return true;
}
return text.length() >= 20 && (text.contains(";") || text.contains("。")) && !isMoneyText(text);
}
private static String blankTo(String value, String fallback)
@@ -1,17 +1,22 @@
package com.bms.price.service;
import java.util.List;
import com.bms.price.domain.PayProduct;
import com.bms.price.domain.PaySurchargeRule;
import com.bms.price.domain.PaySurchargeRuleLog;
import com.bms.price.domain.PaySurchargeRuleSave;
/** 应付附加费计费条件。一个产品版本只保留一份。 */
/** 应付附加费计费条件。一份跟产品走,计算时用当前生效版本上的附加费。 */
public interface IPaySurchargeRuleService
{
List<PaySurchargeRule> list(Long productId, Long termId);
/** 上一版的条件,只读出来供当前页编辑,不直接落库 */
List<PaySurchargeRule> previous(Long productId);
/** 计算时读取的条件。先取产品上的一份,没有再取当前生效版本上的。 */
List<PaySurchargeRule> listForCalc(PayProduct product);
/** 覆盖所选产品当前版本的配置。同一版本不会留下两份。 */
/** 覆盖所选产品的配置。不按价格版本各存一份。 */
void save(PaySurchargeRuleSave command);
/** 这个产品上的附加费配置操作记录。 */
List<PaySurchargeRuleLog> logs(Long productId, Long termId);
}
@@ -110,12 +110,12 @@ public class PayOrderCostServiceImpl implements IPayOrderCostService
Integer orderStatus = bill.getOrderStatus() != null ? bill.getOrderStatus() : (ext == null ? null : ext.getOrderStatus());
List<PayPostcodeGroup> linked = linkedGroups(product);
String zoneName = resolveZone(channel, product, bill.getDestPostcode(), bill.getCountryCode(), bill.getCountryName(), linked);
List<PaySurchargeRule> billingRules = billingRules(product);
product.setBillingRules(billingRules);
applyCurrentSurcharge(channel.getId(), product);
List<PaySurchargeRule> billingRules = product.getBillingRules();
boolean postcodeOn = ruleOn(billingRules, PaySurchargeRules.POSTCODE);
boolean zoneOn = ruleOn(billingRules, PaySurchargeRules.ZONE);
List<PayRemoteBook.Hit> remoteHits = null;
if (postcodeOn)
if (postcodeOn && !hasPrefixRules(billingRules, PaySurchargeRules.POSTCODE))
{
remoteHits = remoteHits(linked, bill.getDestPostcode(), bill.getCountryCode(), bill.getCountryName());
if (remoteHits == null)
@@ -124,7 +124,8 @@ public class PayOrderCostServiceImpl implements IPayOrderCostService
}
}
PayOrderCostResult result = PayCostCalculator.calculate(product, weight, bill.getPkgLength(), bill.getPkgWidth(),
bill.getPkgHeight(), bill.getDestPostcode(), zoneName, bizDate, orderStatus, remoteHits, postcodeOn, zoneOn);
bill.getPkgHeight(), bill.getDestPostcode(), zoneName, bizDate, orderStatus, remoteHits, postcodeOn, zoneOn,
bill.getCountryCode(), bill.getCountryName());
result.setBillId(bill.getBillId());
result.setChannelId(channel.getId());
result.setChannelName(channel.getChannelName());
@@ -138,7 +139,7 @@ public class PayOrderCostServiceImpl implements IPayOrderCostService
saveFees(bill.getBillId(), payBill.getPayBillId(), result.getFees());
String channelName = channel.getChannelName() == null ? channel.getChannelCode() : channel.getChannelName();
String end = product.getTermEnd() == null ? "不限" : day(product.getTermEnd());
result.setMessage("渠道「" + channelName + "」产品编号 " + product.getOriginId() + " 第 " + product.getVersionNo()
result.setMessage("渠道「" + channelName + "」" + product.getProductName() + " 第 " + product.getVersionNo()
+ " 版(" + day(product.getTermStart()) + " 至 " + end + ")," + result.getMessage());
paySettleSupport.stampCalc(bill.getBillId(), product.getSupplierName());
return result;
@@ -176,14 +177,45 @@ public class PayOrderCostServiceImpl implements IPayOrderCostService
* @param product 当前价卡版本
* @return 名称能对上的邮编组,没有时为空列表
*/
private List<PaySurchargeRule> billingRules(PayProduct product)
/** 运费仍按订单日期取版本。附加费条件和附加费明细改用产品当前生效版本。 */
private void applyCurrentSurcharge(String channelId, PayProduct priced)
{
if (product == null || product.getProductId() == null || product.getTermId() == null)
try
{
return new ArrayList<>();
PayProduct current = payProductService.selectPriceForDate(channelId, new Date());
if (current != null && current.getSurcharges() != null)
{
priced.setSurcharges(current.getSurcharges());
}
}
List<PaySurchargeRule> rules = paySurchargeRuleService.list(product.getProductId(), product.getTermId());
return rules == null ? new ArrayList<>() : rules;
catch (ServiceException ignored)
{
// 今天没有生效版本时,附加费明细仍用订单日期命中的那一版
}
List<PaySurchargeRule> rules = paySurchargeRuleService.listForCalc(priced);
priced.setBillingRules(rules == null ? new ArrayList<>() : rules);
}
/** 配置页已经写了邮编前缀时,命中邮编不再去应付邮编配置里查偏远。 */
private static boolean hasPrefixRules(List<PaySurchargeRule> rules, String calcRule)
{
if (rules == null)
{
return false;
}
for (PaySurchargeRule rule : rules)
{
if (rule == null || !calcRule.equals(rule.getCalcRule()) || "0".equals(rule.getEnabled()) || rule.getDetail() == null)
{
continue;
}
Object postcodes = rule.getDetail().get("postcodes");
if (postcodes != null && !String.valueOf(postcodes).isBlank())
{
return true;
}
}
return false;
}
private static boolean ruleOn(List<PaySurchargeRule> rules, String calcRule)
@@ -146,6 +146,14 @@ public class PayProductServiceImpl implements IPayProductService
try
{
PayPreview preview = parser.parseMatched(bytes, filename, dictValue, loadDictValues(), listener);
if (!labelMode)
{
preview.setEffectiveDate(null);
for (PayProduct product : preview.getProducts())
{
product.setEffectiveDate(null);
}
}
writeProgress(progressKey, 98, "正在核对已有产品", PayParseProgress.RUNNING);
String stored = storeSource(bytes, filename);
rememberSource(preview.getFileMd5(), stored);
@@ -271,6 +279,7 @@ public class PayProductServiceImpl implements IPayProductService
continue;
}
product.setProductType(PAY_TYPE_AP);
product.setEffectiveDate(null);
product.setFilePath(resolveSource(product));
prepareCounts(product);
fitColumns(product);
@@ -279,7 +288,7 @@ public class PayProductServiceImpl implements IPayProductService
product.setCreateTime(new Date());
if (existsId != null)
{
appendVersion(existsId, product, operator);
appendVersion(existsId, product, operator, true);
replaced++;
}
else
@@ -628,16 +637,11 @@ public class PayProductServiceImpl implements IPayProductService
return payProductTermMapper.updateStatus(term);
}
/** 给刚保存的这一版记一条生效记录。新版本从生效日开始,旧版本收到生效日前一天。 */
/** 给刚保存的这一版记一条生效记录。应付导入不写生效开始。覆盖旧版时,旧版收到新生效日前一天;没有新生效日则收到昨天。 */
private PayProductTerm openTerm(PayProduct product, boolean replacePrevious, Long priceSetId)
{
String key = termKey(product);
boolean labelMode = priceSetId != null;
// 应付:无生效日则默认今天;打单:创建不写默认生效日,由详情「启动」设置
if (!labelMode && product.getEffectiveDate() == null)
{
product.setEffectiveDate(today());
}
if (replacePrevious)
{
Date endDate;
@@ -1932,10 +1936,10 @@ public class PayProductServiceImpl implements IPayProductService
/** 在同一个产品编号下追加一版价格:复用稳定 product_id,明细挂新 term_id。 */
private PayProduct appendVersion(Long rootId, PayProduct incoming, String operator)
{
return appendVersion(rootId, incoming, operator, null);
return appendVersion(rootId, incoming, operator, false);
}
private PayProduct appendVersion(Long rootId, PayProduct incoming, String operator, Long priceSetId)
private PayProduct appendVersion(Long rootId, PayProduct incoming, String operator, boolean blankEffectiveStart)
{
Long root = resolveRoot(rootId);
PayProduct base = payProductMapper.selectPayProductById(root);
@@ -1944,13 +1948,17 @@ public class PayProductServiceImpl implements IPayProductService
copy.setOriginId(null);
copy.setChannelId(null);
copy.setDictValue(blankTo(copy.getDictValue(), base.getDictValue()));
Integer maxVersion = payProductTermMapper.selectMaxVersionByProduct(root, priceSetId);
Integer maxVersion = payProductTermMapper.selectMaxVersionByProduct(root, null);
if (maxVersion == null || maxVersion < 1)
{
maxVersion = payProductMapper.selectMaxVersion(root);
}
copy.setVersionNo((maxVersion == null ? 1 : maxVersion) + 1);
if (copy.getEffectiveDate() == null)
if (blankEffectiveStart)
{
copy.setEffectiveDate(null);
}
else if (copy.getEffectiveDate() == null)
{
copy.setEffectiveDate(today());
}
@@ -1960,7 +1968,7 @@ public class PayProductServiceImpl implements IPayProductService
prepareCounts(copy);
fitColumns(copy);
payProductMapper.updatePayProduct(copy);
openTerm(copy, true, priceSetId);
openTerm(copy, true, null);
insertBands(copy);
insertSurcharges(copy);
insertExtras(copy);
@@ -2,6 +2,8 @@ package com.bms.price.service.impl;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
@@ -17,9 +19,11 @@ import com.bms.common.utils.SecurityUtils;
import com.bms.price.domain.PayProduct;
import com.bms.price.domain.PayProductTerm;
import com.bms.price.domain.PaySurchargeRule;
import com.bms.price.domain.PaySurchargeRuleLog;
import com.bms.price.domain.PaySurchargeRuleSave;
import com.bms.price.mapper.PayProductMapper;
import com.bms.price.mapper.PayProductTermMapper;
import com.bms.price.mapper.PaySurchargeRuleLogMapper;
import com.bms.price.mapper.PaySurchargeRuleMapper;
import com.bms.price.service.IPaySurchargeRuleService;
@@ -33,9 +37,16 @@ public class PaySurchargeRuleServiceImpl implements IPaySurchargeRuleService
"boxLength", "boxWidth", "boxHeight", "volumeMin", "volumeMax", "cubicMax", "weightMin", "weightMax",
"exceedStepCm", "exceedRatio", "thresholdKg", "stepKg", "minCharge", "ratio");
private static final Map<String, String> RULE_LABELS = Map.of(
"ALWAYS", "每票都收", "POSTCODE", "命中邮编", "ZONE", "命中分区", "EXTRA_KG", "超重续收",
"WEIGHT", "超重", "SIZE", "超尺寸", "RETURN", "退件");
@Autowired
private PaySurchargeRuleMapper ruleMapper;
@Autowired
private PaySurchargeRuleLogMapper logMapper;
@Autowired
private PayProductMapper productMapper;
@@ -45,30 +56,22 @@ public class PaySurchargeRuleServiceImpl implements IPaySurchargeRuleService
@Override
public List<PaySurchargeRule> list(Long productId, Long termId)
{
if (productId == null || termId == null)
if (productId == null)
{
return List.of();
}
List<PaySurchargeRule> rows = ruleMapper.selectByProduct(productId, termId);
rows.forEach(PaySurchargeRuleServiceImpl::hydrate);
return rows;
return hydrateAll(rulesOf(stableId(productId)));
}
@Override
public List<PaySurchargeRule> previous(Long productId)
public List<PaySurchargeRule> listForCalc(PayProduct product)
{
if (productId == null)
{
throw new ServiceException("请选择产品版本");
}
PaySurchargeRule previous = ruleMapper.selectPreviousVersion(productId);
if (previous == null || previous.getProductId() == null || previous.getTermId() == null)
if (product == null || product.getProductId() == null)
{
return List.of();
}
List<PaySurchargeRule> rows = ruleMapper.selectByProduct(previous.getProductId(), previous.getTermId());
rows.forEach(PaySurchargeRuleServiceImpl::hydrate);
return rows;
Long stable = product.getOriginId() != null ? product.getOriginId() : stableId(product.getProductId());
return hydrateAll(rulesOf(stable));
}
@Override
@@ -81,28 +84,29 @@ public class PaySurchargeRuleServiceImpl implements IPaySurchargeRuleService
}
List<PaySurchargeRule> rules = normalize(command.getRules());
String operator = SecurityUtils.getUsername();
Map<String, PaySurchargeRuleSave.PaySurchargeRuleBind> versions = new LinkedHashMap<>();
Map<Long, PaySurchargeRuleSave.PaySurchargeRuleBind> products = new LinkedHashMap<>();
for (PaySurchargeRuleSave.PaySurchargeRuleBind bind : command.getProducts())
{
if (bind == null || bind.getProductId() == null || bind.getTermId() == null)
if (bind == null || bind.getProductId() == null)
{
throw new ServiceException("产品还没有可绑定的生效版本");
throw new ServiceException("请选择要绑定的产品");
}
versions.put(bind.getProductId() + ":" + bind.getTermId(), bind);
products.putIfAbsent(stableId(bind.getProductId()), bind);
}
for (PaySurchargeRuleSave.PaySurchargeRuleBind bind : versions.values())
for (Long stableId : products.keySet())
{
PayProduct product = productMapper.selectPayProductById(bind.getProductId());
if (product == null)
List<Long> family = productMapper.selectFamilyIds(stableId);
if (family == null || family.isEmpty())
{
throw new ServiceException("应付产品不存在");
family = List.of(stableId);
}
PayProductTerm term = termMapper.selectById(bind.getTermId());
if (term == null || term.getProductId() == null || !term.getProductId().equals(bind.getProductId()))
List<PaySurchargeRule> before = hydrateAll(rulesOf(stableId));
List<PaySurchargeRuleLog> logs = diffLogs(before, rules, stableId, 0L, null, operator);
ruleMapper.deleteByProductIds(family);
if (!logs.isEmpty())
{
throw new ServiceException("计费条件必须跟这一版有效期走");
logMapper.insertBatch(logs);
}
ruleMapper.deleteByTerm(bind.getProductId(), bind.getTermId());
if (rules.isEmpty())
{
continue;
@@ -118,9 +122,9 @@ public class PaySurchargeRuleServiceImpl implements IPaySurchargeRuleService
lastRule = rule.getCalcRule();
}
PaySurchargeRule row = copy(rule);
row.setProductId(bind.getProductId());
row.setTermId(bind.getTermId());
row.setVersionNo(term.getVersionNo() == null ? bind.getVersionNo() : term.getVersionNo());
row.setProductId(stableId);
row.setTermId(0L);
row.setVersionNo(0);
row.setSortNo(sort++);
row.setCreateBy(operator);
row.setUpdateBy(operator);
@@ -161,9 +165,79 @@ public class PaySurchargeRuleServiceImpl implements IPaySurchargeRuleService
row.setWeightMax(decimal(detail.get("weightMax"), "重量上限"));
rules.add(row);
}
Set<String> alwaysFees = new HashSet<>();
for (PaySurchargeRule row : rules)
{
if (!"ALWAYS".equals(row.getCalcRule()) || row.getDetail() == null)
{
continue;
}
String feeName = text(row.getDetail().get("feeName"));
if (feeName != null && !alwaysFees.add(feeName))
{
throw new ServiceException("每票都收的费用项不能重复:" + feeName);
}
}
return rules;
}
@Override
public List<PaySurchargeRuleLog> logs(Long productId, Long termId)
{
if (productId == null)
{
return List.of();
}
List<Long> family = productMapper.selectFamilyIds(stableId(productId));
if (family == null || family.isEmpty())
{
return List.of();
}
return logMapper.selectByProductIds(family);
}
/** 产品上的一份。还没迁到产品上时,用当前生效版本里原来那份。 */
private List<PaySurchargeRule> rulesOf(Long stableId)
{
List<PaySurchargeRule> rows = ruleMapper.selectByProductId(stableId);
if (rows != null && !rows.isEmpty())
{
return rows;
}
Long currentId = productMapper.selectEffectiveVersionId(stableId, new Date());
if (currentId == null)
{
return List.of();
}
PayProductTerm term = termMapper.selectByProductId(currentId);
if (term == null || term.getTermId() == null)
{
return List.of();
}
List<PaySurchargeRule> current = ruleMapper.selectByProduct(currentId, term.getTermId());
return current == null ? List.of() : current;
}
private Long stableId(Long productId)
{
PayProduct product = productMapper.selectPayProductById(productId);
if (product == null)
{
throw new ServiceException("应付产品不存在");
}
return product.getOriginId() == null ? product.getProductId() : product.getOriginId();
}
private static List<PaySurchargeRule> hydrateAll(List<PaySurchargeRule> rows)
{
if (rows == null || rows.isEmpty())
{
return List.of();
}
rows.forEach(PaySurchargeRuleServiceImpl::hydrate);
return rows;
}
private static void hydrate(PaySurchargeRule row)
{
Map<String, Object> detail = parse(row.getRuleDetail());
@@ -274,9 +348,14 @@ public class PaySurchargeRuleServiceImpl implements IPaySurchargeRuleService
{
require(detail.get("thresholdKg") != null, "超重续收要填写超过多少公斤后开始续收");
}
else if ("ALWAYS".equals(calcRule))
{
require(text(detail.get("feeName")) != null, "每票都收要选择费用项");
}
else if ("POSTCODE".equals(calcRule) || "ZONE".equals(calcRule))
{
detail.clear();
require(text(detail.get("zoneName")) != null, "命中邮编和命中分区要选择分区");
require(text(detail.get("postcodes")) != null, "命中邮编和命中分区要填写邮编前缀");
}
else if ("RETURN".equals(calcRule))
{
@@ -322,13 +401,13 @@ public class PaySurchargeRuleServiceImpl implements IPaySurchargeRuleService
private static String summary(String calcRule, Map<String, Object> detail)
{
if ("POSTCODE".equals(calcRule))
if ("POSTCODE".equals(calcRule) || "ZONE".equals(calcRule))
{
return "按应付邮编配置判断,邮编前缀和完整邮编都认";
}
if ("ZONE".equals(calcRule))
{
return "按应付邮编配置的分区判断";
List<String> located = new ArrayList<>();
add(located, text(detail.get("zoneName")));
add(located, text(detail.get("postcodes")));
add(located, text(detail.get("country")));
return located.isEmpty() ? null : String.join(";", located);
}
List<String> parts = new ArrayList<>();
add(parts, choice(detail.get("matchMode"), Map.of("ANY", "任一命中", "ALL", "全部命中")));
@@ -384,6 +463,11 @@ public class PaySurchargeRuleServiceImpl implements IPaySurchargeRuleService
{
add(parts, text(detail.get("remark")));
}
String feeName = text(detail.get("feeName"));
if (feeName != null)
{
parts.add(0, feeName);
}
return parts.isEmpty() ? null : String.join(";", parts);
}
@@ -560,4 +644,178 @@ public class PaySurchargeRuleServiceImpl implements IPaySurchargeRuleService
}
return text;
}
private static List<PaySurchargeRuleLog> diffLogs(List<PaySurchargeRule> before, List<PaySurchargeRule> after,
Long productId, Long termId, Integer versionNo, String operator)
{
Map<String, Map<String, String>> oldRows = keyed(before);
Map<String, Map<String, String>> newRows = keyed(after);
List<PaySurchargeRuleLog> logs = new ArrayList<>();
for (Map.Entry<String, Map<String, String>> entry : newRows.entrySet())
{
Map<String, String> previous = oldRows.remove(entry.getKey());
if (previous == null)
{
logs.add(log(productId, termId, versionNo, "1", entry.getKey(), null, entry.getValue(), operator));
continue;
}
Map<String, String> oldChanged = new LinkedHashMap<>();
Map<String, String> newChanged = new LinkedHashMap<>();
Set<String> labels = new java.util.LinkedHashSet<>();
labels.addAll(previous.keySet());
labels.addAll(entry.getValue().keySet());
for (String label : labels)
{
String oldValue = previous.get(label);
String newValue = entry.getValue().get(label);
if (java.util.Objects.equals(oldValue, newValue))
{
continue;
}
if (oldValue != null)
{
oldChanged.put(label, oldValue);
}
if (newValue != null)
{
newChanged.put(label, newValue);
}
}
if (!oldChanged.isEmpty() || !newChanged.isEmpty())
{
logs.add(log(productId, termId, versionNo, "2", entry.getKey(), oldChanged, newChanged, operator));
}
}
for (Map.Entry<String, Map<String, String>> entry : oldRows.entrySet())
{
logs.add(log(productId, termId, versionNo, "3", entry.getKey(), entry.getValue(), null, operator));
}
return logs;
}
private static Map<String, Map<String, String>> keyed(List<PaySurchargeRule> rules)
{
Map<String, Map<String, String>> rows = new LinkedHashMap<>();
Map<String, Integer> index = new LinkedHashMap<>();
if (rules == null)
{
return rows;
}
for (PaySurchargeRule rule : rules)
{
if (rule == null || rule.getCalcRule() == null)
{
continue;
}
Map<String, String> attrs = filled(rule);
if (attrs.isEmpty())
{
continue;
}
String feeName = attrs.get("费用项");
String key;
if ("ALWAYS".equals(rule.getCalcRule()) && feeName != null)
{
key = "ALWAYS|" + feeName;
}
else if ("POSTCODE".equals(rule.getCalcRule()) || "ZONE".equals(rule.getCalcRule()))
{
int next = index.getOrDefault(rule.getCalcRule(), 0);
index.put(rule.getCalcRule(), next + 1);
String zone = attrs.get("分区");
key = rule.getCalcRule() + "|" + (zone == null ? String.valueOf(next) : zone + "#" + next);
}
else
{
int next = index.getOrDefault(rule.getCalcRule(), 0);
index.put(rule.getCalcRule(), next + 1);
key = rule.getCalcRule() + "#" + next;
}
rows.put(key, attrs);
}
return rows;
}
private static Map<String, String> filled(PaySurchargeRule rule)
{
Map<String, Object> detail = rule.getDetail() == null ? Map.of() : rule.getDetail();
Map<String, String> attrs = new LinkedHashMap<>();
putAttr(attrs, "计费条件", RULE_LABELS.getOrDefault(rule.getCalcRule(), rule.getCalcRule()));
putAttr(attrs, "费用项", text(detail.get("feeName")));
putAttr(attrs, "计费方式", choice(detail.get("rateMode"), Map.of("FIXED", "固定金额", "RATIO", "按比例")));
putAttr(attrs, "比例", detail.get("ratio") == null ? null : plain(detail.get("ratio")) + "%");
putAttr(attrs, "计费基数", choice(detail.get("chargeBase"), Map.of("HANDLE", "处理费", "DELIVERY", "派送费", "FREIGHT_FEE", "运费加附加费")));
putAttr(attrs, "生效开始", text(detail.get("periodStart")));
putAttr(attrs, "生效结束", text(detail.get("periodEnd")));
putAttr(attrs, "说明", text(detail.get("remark")));
putAttr(attrs, "命中方式", choice(detail.get("matchMode"), Map.of("ANY", "任一命中", "ALL", "全部命中")));
putAttr(attrs, "邮编匹配", choice(detail.get("postcodeMode"), Map.of("PREFIX", "邮编开头", "EXACT", "完整邮编")));
putAttr(attrs, "最长边下限", unit(detail.get("longestMin"), "cm"));
putAttr(attrs, "最长边上限", unit(detail.get("longestMax"), "cm"));
putAttr(attrs, "次长边下限", unit(detail.get("secondMin"), "cm"));
putAttr(attrs, "次长边上限", unit(detail.get("secondMax"), "cm"));
putAttr(attrs, "三边之和下限", unit(detail.get("sumMin"), "cm"));
putAttr(attrs, "三边之和上限", unit(detail.get("sumMax"), "cm"));
putAttr(attrs, "围长下限", unit(detail.get("girthMin"), "cm"));
putAttr(attrs, "围长上限", unit(detail.get("girthMax"), "cm"));
putAttr(attrs, "箱规比较", choice(detail.get("boxMode"), Map.of("OVER", "超过", "UNDER", "小于")));
if (detail.get("boxLength") != null)
{
putAttr(attrs, "箱规", plain(detail.get("boxLength")) + "×" + plain(detail.get("boxWidth")) + "×" + plain(detail.get("boxHeight")) + "cm");
}
putAttr(attrs, "体积下限", unit(detail.get("volumeMin"), "m³"));
putAttr(attrs, "体积上限", unit(detail.get("volumeMax"), "m³"));
putAttr(attrs, "体积上限立方", unit(detail.get("cubicMax"), "cm³"));
putAttr(attrs, "超出步长", unit(detail.get("exceedStepCm"), "cm"));
putAttr(attrs, "加收比例", detail.get("exceedRatio") == null ? null : plain(detail.get("exceedRatio")) + "%");
putAttr(attrs, "重量下限", unit(detail.get("weightMin"), "kg"));
putAttr(attrs, "重量上限", unit(detail.get("weightMax"), "kg"));
putAttr(attrs, "重量口径", choice(detail.get("weightBasis"), Map.of("ACTUAL", "实重", "CHARGE", "计费重")));
putAttr(attrs, "超过重量", unit(detail.get("thresholdKg"), "kg"));
putAttr(attrs, "每超出", unit(detail.get("stepKg"), "kg"));
putAttr(attrs, "最低消费", plain(detail.get("minCharge")));
putAttr(attrs, "分区", text(detail.get("zoneName")));
putAttr(attrs, "邮编", text(detail.get("postcodes")));
putAttr(attrs, "国家", text(detail.get("country") != null ? detail.get("country") : detail.get("countries")));
putAttr(attrs, "退回方式", choice(detail.get("chargeMode"), Map.of("SAME", "同正向运费", "AIR", "空运退回", "ROAD", "陆运退回", "FIXED", "固定金额")));
return attrs;
}
private static void putAttr(Map<String, String> attrs, String label, String value)
{
if (value == null || value.isBlank())
{
return;
}
attrs.put(label, value);
}
private static String unit(Object value, String unit)
{
String text = plain(value);
return text == null ? null : text + unit;
}
private static PaySurchargeRuleLog log(Long productId, Long termId, Integer versionNo, String operation,
String ruleKey, Map<String, String> before, Map<String, String> after, String operator)
{
Map<String, Object> content = new LinkedHashMap<>();
if (before != null && !before.isEmpty())
{
content.put("before", before);
}
if (after != null && !after.isEmpty())
{
content.put("after", after);
}
PaySurchargeRuleLog row = new PaySurchargeRuleLog();
row.setProductId(productId);
row.setTermId(termId);
row.setVersionNo(versionNo);
row.setOperationType(operation);
row.setRuleKey(ruleKey.length() > 200 ? ruleKey.substring(0, 200) : ruleKey);
row.setChangeContent(JSON.toJSONString(content));
row.setCreateBy(operator);
return row;
}
}
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bms.price.mapper.PaySurchargeRuleLogMapper">
<select id="selectByTerm" resultType="com.bms.price.domain.PaySurchargeRuleLog">
select log_id as logId, product_id as productId, term_id as termId, version_no as versionNo,
operation_type as operationType, rule_key as ruleKey, change_content as changeContent,
create_by as createBy, create_time as createTime
from pay_surcharge_rule_log
where product_id = #{productId}
and term_id = #{termId}
order by log_id desc
</select>
<select id="selectByProductIds" resultType="com.bms.price.domain.PaySurchargeRuleLog">
select log_id as logId, product_id as productId, term_id as termId, version_no as versionNo,
operation_type as operationType, rule_key as ruleKey, change_content as changeContent,
create_by as createBy, create_time as createTime
from pay_surcharge_rule_log
where product_id in
<foreach collection="productIds" item="id" open="(" separator="," close=")">#{id}</foreach>
order by log_id desc
</select>
<insert id="insertBatch">
insert into pay_surcharge_rule_log (
product_id, term_id, version_no, operation_type, rule_key, change_content, create_by, create_time
) values
<foreach collection="list" item="row" separator=",">
(#{row.productId}, #{row.termId}, #{row.versionNo}, #{row.operationType}, #{row.ruleKey},
#{row.changeContent}, #{row.createBy}, sysdate())
</foreach>
</insert>
</mapper>
@@ -12,12 +12,27 @@
order by calc_rule, sort_no, rule_id
</select>
<select id="selectByProductId" resultType="com.bms.price.domain.PaySurchargeRule">
select rule_id as ruleId, product_id as productId, term_id as termId, version_no as versionNo,
calc_rule as calcRule, enabled, calc_param as calcParam, rule_detail as ruleDetail, postcode_param as postcodeParam,
zone_name as zoneName, weight_min as weightMin, weight_max as weightMax, sort_no as sortNo
from pay_surcharge_rule
where product_id = #{productId}
order by calc_rule, sort_no, rule_id
</select>
<delete id="deleteByTerm">
delete from pay_surcharge_rule
where product_id = #{productId}
and term_id = #{termId}
</delete>
<delete id="deleteByProductIds">
delete from pay_surcharge_rule
where product_id in
<foreach collection="productIds" item="id" open="(" separator="," close=")">#{id}</foreach>
</delete>
<insert id="insertBatch">
insert into pay_surcharge_rule (
product_id, term_id, version_no, calc_rule, enabled, calc_param, rule_detail, postcode_param,
@@ -29,18 +44,4 @@
#{row.createBy}, sysdate(), #{row.updateBy}, sysdate())
</foreach>
</insert>
<select id="selectPreviousVersion" resultType="com.bms.price.domain.PaySurchargeRule">
select v.product_id as productId, t.term_id as termId, ifnull(v.version_no, 1) as versionNo
from pay_product cur
join pay_product v on v.dict_value = cur.dict_value
and v.dict_value is not null and v.dict_value &lt;&gt; ''
and v.product_id &lt;&gt; cur.product_id
and (v.channel_id is null or v.channel_id = '')
and ifnull(v.version_no, 1) &lt; ifnull(cur.version_no, 1)
join pay_product_term t on t.product_id = v.product_id and t.price_set_id is null
where cur.product_id = #{productId}
order by ifnull(v.version_no, 1) desc, t.term_id desc
limit 1
</select>
</mapper>