From 3a104d2d8c52771743ec6307d715bb41ff874348 Mon Sep 17 00:00:00 2001 From: liuminghui <468701464@qq.com> Date: Fri, 13 Nov 2026 11:57:19 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BA=94=E4=BB=98=E9=85=8D=E7=BD=AE=E7=9A=84?= =?UTF-8?q?=E9=99=84=E5=8A=A0=E8=B4=B9=E9=85=8D=E7=BD=AE=E5=AE=8C=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../price/PayProductController.java | 12 +- .../bms/price/domain/PaySurchargeRuleLog.java | 118 +++++ .../mapper/PaySurchargeRuleLogMapper.java | 14 + .../price/mapper/PaySurchargeRuleMapper.java | 9 +- .../com/bms/price/pay/PayCostCalculator.java | 94 +++- .../com/bms/price/pay/PaySurchargeRules.java | 154 +++++- .../main/java/com/bms/price/pay/PayTexts.java | 9 + .../com/bms/price/pay/PayWorkbookParser.java | 138 ++++- .../service/IPaySurchargeRuleService.java | 13 +- .../service/impl/PayOrderCostServiceImpl.java | 52 +- .../service/impl/PayProductServiceImpl.java | 32 +- .../impl/PaySurchargeRuleServiceImpl.java | 330 ++++++++++-- .../price/PaySurchargeRuleLogMapper.xml | 35 ++ .../mapper/price/PaySurchargeRuleMapper.xml | 29 +- bms-back/sql/price/pay_surcharge_rule_log.sql | 14 + bms-front/src/api/price/pay.js | 14 +- bms-front/src/components/BasicTable/index.vue | 3 + .../basic/pay/components/PayConfigDetail.vue | 52 +- .../pay/components/PaySurchargeRuleDialog.vue | 474 ++++++++++++------ bms-front/src/views/price/basic/pay/index.vue | 83 ++- 20 files changed, 1369 insertions(+), 310 deletions(-) create mode 100644 bms-back/bms-system/src/main/java/com/bms/price/domain/PaySurchargeRuleLog.java create mode 100644 bms-back/bms-system/src/main/java/com/bms/price/mapper/PaySurchargeRuleLogMapper.java create mode 100644 bms-back/bms-system/src/main/resources/mapper/price/PaySurchargeRuleLogMapper.xml create mode 100644 bms-back/sql/price/pay_surcharge_rule_log.sql diff --git a/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/PayProductController.java b/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/PayProductController.java index 1fdc8e99..db133039 100644 --- a/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/PayProductController.java +++ b/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/PayProductController.java @@ -107,20 +107,20 @@ public class PayProductController extends BaseController payProductService.writeSource(productId, response.getOutputStream()); } - /** 当前版本的附加费计费条件。同一版本只有一份。 */ + /** 产品上的附加费计费条件。不按价格版本拆开。 */ @PreAuthorize("@ss.hasAnyPermi('price:pay:query,price:pay:edit')") @GetMapping("/surchargeRule") - public AjaxResult surchargeRule(@RequestParam Long productId, @RequestParam Long termId) + public AjaxResult surchargeRule(@RequestParam Long productId, @RequestParam(required = false) Long termId) { return success(paySurchargeRuleService.list(productId, termId)); } - /** 上一版条件,复制到当前页后再保存。 */ + /** 这个产品上的附加费配置操作记录。 */ @PreAuthorize("@ss.hasAnyPermi('price:pay:query,price:pay:edit')") - @GetMapping("/surchargeRule/previous") - public AjaxResult previousSurchargeRule(@RequestParam Long productId) + @GetMapping("/surchargeRule/log") + public AjaxResult surchargeRuleLog(@RequestParam Long productId, @RequestParam(required = false) Long termId) { - return success(paySurchargeRuleService.previous(productId)); + return success(paySurchargeRuleService.logs(productId, termId)); } @PreAuthorize("@ss.hasPermi('price:pay:edit')") diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/PaySurchargeRuleLog.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/PaySurchargeRuleLog.java new file mode 100644 index 00000000..da3e75be --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/PaySurchargeRuleLog.java @@ -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; + } +} diff --git a/bms-back/bms-system/src/main/java/com/bms/price/mapper/PaySurchargeRuleLogMapper.java b/bms-back/bms-system/src/main/java/com/bms/price/mapper/PaySurchargeRuleLogMapper.java new file mode 100644 index 00000000..d88ca5e5 --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/mapper/PaySurchargeRuleLogMapper.java @@ -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 selectByTerm(@Param("productId") Long productId, @Param("termId") Long termId); + + List selectByProductIds(@Param("productIds") List productIds); + + int insertBatch(@Param("list") List list); +} diff --git a/bms-back/bms-system/src/main/java/com/bms/price/mapper/PaySurchargeRuleMapper.java b/bms-back/bms-system/src/main/java/com/bms/price/mapper/PaySurchargeRuleMapper.java index 97af1682..daf235a2 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/mapper/PaySurchargeRuleMapper.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/mapper/PaySurchargeRuleMapper.java @@ -4,15 +4,16 @@ import java.util.List; import org.apache.ibatis.annotations.Param; import com.bms.price.domain.PaySurchargeRule; -/** 应付附加费计费条件。一个产品版本一份。 */ +/** 应付附加费计费条件。一份跟产品走。 */ public interface PaySurchargeRuleMapper { List selectByProduct(@Param("productId") Long productId, @Param("termId") Long termId); + List selectByProductId(@Param("productId") Long productId); + int deleteByTerm(@Param("productId") Long productId, @Param("termId") Long termId); - int insertBatch(@Param("list") List rules); + int deleteByProductIds(@Param("productIds") List productIds); - /** 同一产品字典下,比当前版本号小的最近一版 */ - PaySurchargeRule selectPreviousVersion(@Param("productId") Long productId); + int insertBatch(@Param("list") List rules); } diff --git a/bms-back/bms-system/src/main/java/com/bms/price/pay/PayCostCalculator.java b/bms-back/bms-system/src/main/java/com/bms/price/pay/PayCostCalculator.java index 5277f439..fa35951b 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/pay/PayCostCalculator.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/pay/PayCostCalculator.java @@ -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 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 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 remoteHits, boolean postcodeOn, boolean zoneOn, PayOrderCostResult result) + String freightSection, List 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> postcodeRules = prefixRules(product, PaySurchargeRules.POSTCODE); + List> 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> 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> prefixRules(PayProduct product, String calcRule) + { + List> rules = new ArrayList<>(); + for (Map detail : ruleDetails(product, calcRule)) + { + Object postcodes = detail.get("postcodes"); + if (postcodes != null && !String.valueOf(postcodes).isBlank()) + { + rules.add(detail); + } + } + return rules; + } + private static List> ruleDetails(PayProduct product, String calcRule) { List> details = new ArrayList<>(); diff --git a/bms-back/bms-system/src/main/java/com/bms/price/pay/PaySurchargeRules.java b/bms-back/bms-system/src/main/java/com/bms/price/pay/PaySurchargeRules.java index 9e83f378..2b54c05d 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/pay/PaySurchargeRules.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/pay/PaySurchargeRules.java @@ -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> 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 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 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> 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> named = new ArrayList<>(); - List> 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 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> 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 detail : mine) - { if (!hasPeriod(detail)) { return true; diff --git a/bms-back/bms-system/src/main/java/com/bms/price/pay/PayTexts.java b/bms-back/bms-system/src/main/java/com/bms/price/pay/PayTexts.java index 8db15bf4..875fab40 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/pay/PayTexts.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/pay/PayTexts.java @@ -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; } diff --git a/bms-back/bms-system/src/main/java/com/bms/price/pay/PayWorkbookParser.java b/bms-back/bms-system/src/main/java/com/bms/price/pay/PayWorkbookParser.java index 717b486a..6b098ba0 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/pay/PayWorkbookParser.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/pay/PayWorkbookParser.java @@ -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) diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/IPaySurchargeRuleService.java b/bms-back/bms-system/src/main/java/com/bms/price/service/IPaySurchargeRuleService.java index 33de2802..f3c82c24 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/IPaySurchargeRuleService.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/IPaySurchargeRuleService.java @@ -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 list(Long productId, Long termId); - /** 上一版的条件,只读出来供当前页编辑,不直接落库 */ - List previous(Long productId); + /** 计算时读取的条件。先取产品上的一份,没有再取当前生效版本上的。 */ + List listForCalc(PayProduct product); - /** 覆盖所选产品当前版本的配置。同一版本不会留下两份。 */ + /** 覆盖所选产品的配置。不按价格版本各存一份。 */ void save(PaySurchargeRuleSave command); + + /** 这个产品上的附加费配置操作记录。 */ + List logs(Long productId, Long termId); } diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayOrderCostServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayOrderCostServiceImpl.java index a22499c2..9598f1c3 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayOrderCostServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayOrderCostServiceImpl.java @@ -110,12 +110,12 @@ public class PayOrderCostServiceImpl implements IPayOrderCostService Integer orderStatus = bill.getOrderStatus() != null ? bill.getOrderStatus() : (ext == null ? null : ext.getOrderStatus()); List linked = linkedGroups(product); String zoneName = resolveZone(channel, product, bill.getDestPostcode(), bill.getCountryCode(), bill.getCountryName(), linked); - List billingRules = billingRules(product); - product.setBillingRules(billingRules); + applyCurrentSurcharge(channel.getId(), product); + List billingRules = product.getBillingRules(); boolean postcodeOn = ruleOn(billingRules, PaySurchargeRules.POSTCODE); boolean zoneOn = ruleOn(billingRules, PaySurchargeRules.ZONE); List 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 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 rules = paySurchargeRuleService.list(product.getProductId(), product.getTermId()); - return rules == null ? new ArrayList<>() : rules; + catch (ServiceException ignored) + { + // 今天没有生效版本时,附加费明细仍用订单日期命中的那一版 + } + List rules = paySurchargeRuleService.listForCalc(priced); + priced.setBillingRules(rules == null ? new ArrayList<>() : rules); + } + + /** 配置页已经写了邮编前缀时,命中邮编不再去应付邮编配置里查偏远。 */ + private static boolean hasPrefixRules(List 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 rules, String calcRule) diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayProductServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayProductServiceImpl.java index b9e60317..92a33d0f 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayProductServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayProductServiceImpl.java @@ -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); diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PaySurchargeRuleServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PaySurchargeRuleServiceImpl.java index 1c5dde23..bbc4b5c3 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PaySurchargeRuleServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PaySurchargeRuleServiceImpl.java @@ -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 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 list(Long productId, Long termId) { - if (productId == null || termId == null) + if (productId == null) { return List.of(); } - List rows = ruleMapper.selectByProduct(productId, termId); - rows.forEach(PaySurchargeRuleServiceImpl::hydrate); - return rows; + return hydrateAll(rulesOf(stableId(productId))); } @Override - public List previous(Long productId) + public List 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 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 rules = normalize(command.getRules()); String operator = SecurityUtils.getUsername(); - Map versions = new LinkedHashMap<>(); + Map 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 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 before = hydrateAll(rulesOf(stableId)); + List 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 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 logs(Long productId, Long termId) + { + if (productId == null) + { + return List.of(); + } + List family = productMapper.selectFamilyIds(stableId(productId)); + if (family == null || family.isEmpty()) + { + return List.of(); + } + return logMapper.selectByProductIds(family); + } + + /** 产品上的一份。还没迁到产品上时,用当前生效版本里原来那份。 */ + private List rulesOf(Long stableId) + { + List 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 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 hydrateAll(List rows) + { + if (rows == null || rows.isEmpty()) + { + return List.of(); + } + rows.forEach(PaySurchargeRuleServiceImpl::hydrate); + return rows; + } + private static void hydrate(PaySurchargeRule row) { Map 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 detail) { - if ("POSTCODE".equals(calcRule)) + if ("POSTCODE".equals(calcRule) || "ZONE".equals(calcRule)) { - return "按应付邮编配置判断,邮编前缀和完整邮编都认"; - } - if ("ZONE".equals(calcRule)) - { - return "按应付邮编配置的分区判断"; + List 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 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 diffLogs(List before, List after, + Long productId, Long termId, Integer versionNo, String operator) + { + Map> oldRows = keyed(before); + Map> newRows = keyed(after); + List logs = new ArrayList<>(); + for (Map.Entry> entry : newRows.entrySet()) + { + Map previous = oldRows.remove(entry.getKey()); + if (previous == null) + { + logs.add(log(productId, termId, versionNo, "1", entry.getKey(), null, entry.getValue(), operator)); + continue; + } + Map oldChanged = new LinkedHashMap<>(); + Map newChanged = new LinkedHashMap<>(); + Set 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> entry : oldRows.entrySet()) + { + logs.add(log(productId, termId, versionNo, "3", entry.getKey(), entry.getValue(), null, operator)); + } + return logs; + } + + private static Map> keyed(List rules) + { + Map> rows = new LinkedHashMap<>(); + Map index = new LinkedHashMap<>(); + if (rules == null) + { + return rows; + } + for (PaySurchargeRule rule : rules) + { + if (rule == null || rule.getCalcRule() == null) + { + continue; + } + Map 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 filled(PaySurchargeRule rule) + { + Map detail = rule.getDetail() == null ? Map.of() : rule.getDetail(); + Map 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 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 before, Map after, String operator) + { + Map 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; + } } diff --git a/bms-back/bms-system/src/main/resources/mapper/price/PaySurchargeRuleLogMapper.xml b/bms-back/bms-system/src/main/resources/mapper/price/PaySurchargeRuleLogMapper.xml new file mode 100644 index 00000000..4b7a387a --- /dev/null +++ b/bms-back/bms-system/src/main/resources/mapper/price/PaySurchargeRuleLogMapper.xml @@ -0,0 +1,35 @@ + + + + + + + + + + insert into pay_surcharge_rule_log ( + product_id, term_id, version_no, operation_type, rule_key, change_content, create_by, create_time + ) values + + (#{row.productId}, #{row.termId}, #{row.versionNo}, #{row.operationType}, #{row.ruleKey}, + #{row.changeContent}, #{row.createBy}, sysdate()) + + + + diff --git a/bms-back/bms-system/src/main/resources/mapper/price/PaySurchargeRuleMapper.xml b/bms-back/bms-system/src/main/resources/mapper/price/PaySurchargeRuleMapper.xml index 21fc3a01..9cabc99c 100644 --- a/bms-back/bms-system/src/main/resources/mapper/price/PaySurchargeRuleMapper.xml +++ b/bms-back/bms-system/src/main/resources/mapper/price/PaySurchargeRuleMapper.xml @@ -12,12 +12,27 @@ order by calc_rule, sort_no, rule_id + + delete from pay_surcharge_rule where product_id = #{productId} and term_id = #{termId} + + delete from pay_surcharge_rule + where product_id in + #{id} + + 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()) - - diff --git a/bms-back/sql/price/pay_surcharge_rule_log.sql b/bms-back/sql/price/pay_surcharge_rule_log.sql new file mode 100644 index 00000000..57abafca --- /dev/null +++ b/bms-back/sql/price/pay_surcharge_rule_log.sql @@ -0,0 +1,14 @@ +-- 新增应付附加费配置操作记录表,按产品版本保存,内容只含有数据的属性 +CREATE TABLE IF NOT EXISTS `bms`.`pay_surcharge_rule_log` ( + `log_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '记录ID', + `product_id` bigint(20) NOT NULL COMMENT '应付产品版本ID', + `term_id` bigint(20) NOT NULL COMMENT '价卡版本周期', + `version_no` int(11) DEFAULT NULL COMMENT '版本号', + `operation_type` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '操作:1新增 2修改 3删除', + `rule_key` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '条件标识,每票都收带费用项', + `change_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '变更内容,只含有数据的属性', + `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '操作人', + `create_time` datetime DEFAULT NULL COMMENT '操作时间', + PRIMARY KEY (`log_id`), + KEY `idx_pay_surcharge_rule_log_ver` (`product_id`, `term_id`, `log_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='应付附加费配置操作记录'; diff --git a/bms-front/src/api/price/pay.js b/bms-front/src/api/price/pay.js index 60f3948e..8b7cf41d 100644 --- a/bms-front/src/api/price/pay.js +++ b/bms-front/src/api/price/pay.js @@ -172,21 +172,21 @@ export function delPay(productIds) { }) } -/** 当前版本的附加费计费条件 */ -export function listPaySurchargeRule(productId, termId) { +/** 产品上的附加费计费条件,不按价格版本拆开 */ +export function listPaySurchargeRule(productId) { return request({ url: '/price/pay/surchargeRule', method: 'get', - params: { productId, termId } + params: { productId } }) } -/** 上一版计费条件,复制到当前页编辑 */ -export function previousPaySurchargeRule(productId) { +/** 这一版产品上的附加费配置操作记录 */ +export function listPaySurchargeRuleLog(productId, termId) { return request({ - url: '/price/pay/surchargeRule/previous', + url: '/price/pay/surchargeRule/log', method: 'get', - params: { productId } + params: { productId, termId } }) } diff --git a/bms-front/src/components/BasicTable/index.vue b/bms-front/src/components/BasicTable/index.vue index 10a2fb95..837b5952 100644 --- a/bms-front/src/components/BasicTable/index.vue +++ b/bms-front/src/components/BasicTable/index.vue @@ -74,6 +74,7 @@ :size="size" :height="height" :max-height="maxHeight" + :scrollbar-always-on="scrollbarAlwaysOn" :default-sort="defaultSort" :span-method="spanMethod" @selection-change="handleSelectionChange" @@ -291,6 +292,8 @@ const props = defineProps({ // 表格高度 height: { type: [String, Number], default: undefined }, maxHeight: { type: [String, Number], default: undefined }, + // 横向、纵向滚动条一直显示 + scrollbarAlwaysOn: { type: Boolean, default: false }, // 默认排序(透传 el-table default-sort,如 { prop: 'createTime', order: 'descending' }) defaultSort: { type: Object, diff --git a/bms-front/src/views/price/basic/pay/components/PayConfigDetail.vue b/bms-front/src/views/price/basic/pay/components/PayConfigDetail.vue index ef7ea6a8..e39db81a 100644 --- a/bms-front/src/views/price/basic/pay/components/PayConfigDetail.vue +++ b/bms-front/src/views/price/basic/pay/components/PayConfigDetail.vue @@ -78,6 +78,14 @@
+ + + 需要计费 + + + 暂不计费 + + 添加附加费 - + -

配置写到每个产品当前生效的版本。同一版本再次保存会覆盖原来的一份。

+

配置跟产品走,不跟价格版本。计算时用这个产品当前生效版本上的附加费。

@@ -36,8 +36,6 @@
-

打开后,用订单目的国去应付邮编配置里对国家和邮编。邮编前缀和完整邮编都会判断,命中偏远或岛屿才加这笔附加费。

-

打开后,用订单目的国和邮编去应付邮编配置里对上分区,再加这一档附加费。

@@ -55,209 +53,310 @@
命中方式 - - - - +
+ + + + +
重量前提 kg - - 至 - +
+ + 至 + +
最长边 cm - - 至 - +
+ + 至 + +
次长边 cm - - 至 - +
+ + 至 + +
三边之和 cm - - 至 - +
+ + 至 + +
围长 cm - - 至 - +
+ + 至 + +
箱规 cm - - - - - - - +
+ + + + + + + +
体积 m³ - - 至 - +
+ + 至 + +
长×宽×高上限 cm³ - +
+ +
每超出 cm - +
+ +
运费加收比例 % - +
+ +
-
+
说明 - +
+ +
重量 kg - - 至 - +
+ + 至 + +
重量口径 - - - - +
+ + + + +
每超出 kg - +
+ +
最低消费 - +
+ +
运费加收比例 % - +
+ +
说明 - +
+ +
超过 kg - +
+ +
每超出 kg - +
+ +
重量口径 - - - - +
+ + + + +
最低消费 - +
+ +
-
+
邮编 - +
+ +
+
+
+ +
+
+ 分区 +
+ + + +
+
+
+ 邮编前缀 +
+ +
+
+
+ 国家 +
+ +
+
+ 费用项 +
+ + + +
+
计费方式 - - - - +
+ + + + +
比例 % - +
+ +
计费基数 - - - - - +
+ + + + + +
生效日期 - - 至 - +
+ + 至 + +
-
+
说明 - +
+ +
退回方式 - - - - - - +
+ + + + + + +
重量 kg - - 至 - +
+ + 至 + +
尺寸命中 - - - - +
+ + + + +
最长边 cm - - 至 - +
+ + 至 + +
围长 cm - - 至 - +
+ + 至 + +
三边之和 cm - - 至 - +
+ + 至 + +
-
+
说明 - +
+ +
@@ -265,7 +364,6 @@
@@ -273,7 +371,7 @@