应付配置的附加费配置完善
This commit is contained in:
@@ -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)
|
||||
|
||||
+9
-4
@@ -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);
|
||||
}
|
||||
|
||||
+42
-10
@@ -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)
|
||||
|
||||
+20
-12
@@ -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);
|
||||
|
||||
+294
-36
@@ -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 <> ''
|
||||
and v.product_id <> cur.product_id
|
||||
and (v.channel_id is null or v.channel_id = '')
|
||||
and ifnull(v.version_no, 1) < 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>
|
||||
|
||||
Reference in New Issue
Block a user