From fde53681d1d8a99f930407b6a648828878844d38 Mon Sep 17 00:00:00 2001 From: liuminghui <468701464@qq.com> Date: Thu, 5 Nov 2026 16:05:01 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=BB=BA=E5=BA=94=E4=BB=98=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E7=9A=84=E9=99=84=E5=8A=A0=E8=B4=B9=E5=BC=80=E5=85=B3?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../price/PayProductController.java | 31 ++ .../bms/price/pay/PayWorkbookParserTest.java | 38 ++- .../com/bms/price/domain/PayFeeRuleGroup.java | 18 ++ .../bms/price/domain/PayProductFeeRule.java | 47 +++ .../price/mapper/PayProductFeeRuleMapper.java | 19 ++ .../com/bms/price/pay/PayWorkbookParser.java | 115 ++++++- .../bms/price/service/IPayProductService.java | 7 + .../service/impl/PayProductServiceImpl.java | 181 +++++++++++ .../mapper/price/PayProductFeeRuleMapper.xml | 42 +++ bms-back/sql/data/bms_surcharge_type.sql | 162 ++++++++++ bms-back/sql/price/pay_product.sql | 23 ++ bms-back/sql/price/pay_product_fee_rule.sql | 25 ++ bms-front/src/api/price/pay.js | 18 ++ .../basic/pay/components/PayFeeRuleDialog.vue | 291 ++++++++++++++++++ bms-front/src/views/price/basic/pay/index.vue | 6 + 15 files changed, 1002 insertions(+), 21 deletions(-) create mode 100644 bms-back/bms-system/src/main/java/com/bms/price/domain/PayFeeRuleGroup.java create mode 100644 bms-back/bms-system/src/main/java/com/bms/price/domain/PayProductFeeRule.java create mode 100644 bms-back/bms-system/src/main/java/com/bms/price/mapper/PayProductFeeRuleMapper.java create mode 100644 bms-back/bms-system/src/main/resources/mapper/price/PayProductFeeRuleMapper.xml create mode 100644 bms-back/sql/data/bms_surcharge_type.sql create mode 100644 bms-back/sql/price/pay_product_fee_rule.sql create mode 100644 bms-front/src/views/price/basic/pay/components/PayFeeRuleDialog.vue 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 cd262391..d740f6f1 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 @@ -1,5 +1,6 @@ package com.bms.web.controller.price; +import java.util.ArrayList; import java.util.List; import java.util.Locale; import org.springframework.beans.factory.annotation.Autowired; @@ -24,6 +25,7 @@ import com.bms.common.exception.ServiceException; import com.bms.common.utils.file.FileUtils; import com.bms.common.utils.poi.ExcelUtil; import com.bms.price.domain.ChannelProductMap; +import com.bms.price.domain.PayFeeRuleGroup; import com.bms.price.domain.PayProduct; import com.bms.price.domain.PayProductTerm; import com.bms.price.domain.PaySaveCommand; @@ -70,6 +72,35 @@ public class PayProductController extends BaseController return toAjax(payProductService.updateTerm(term)); } + /** 勾选的应付产品各自带出附加费开关。productIds 用逗号分隔 */ + @PreAuthorize("@ss.hasPermi('price:pay:query')") + @GetMapping("/feeRule/list") + public AjaxResult feeRuleList(@RequestParam String productIds) + { + List ids = new ArrayList<>(); + if (productIds != null) + { + for (String part : productIds.split(",")) + { + if (part != null && !part.isBlank()) + { + ids.add(Long.valueOf(part.trim())); + } + } + } + return success(payProductService.listFeeRules(ids)); + } + + /** 按产品保存附加费开关。多个产品各写一份 */ + @PreAuthorize("@ss.hasPermi('price:pay:edit')") + @Log(title = "应付附加费开关", businessType = BusinessType.UPDATE) + @PutMapping("/feeRule") + public AjaxResult saveFeeRule(@RequestBody List products) + { + payProductService.saveFeeRules(products); + return success(); + } + @PreAuthorize("@ss.hasPermi('price:pay:edit')") @Log(title = "应付有效期", businessType = BusinessType.UPDATE) @PutMapping("/term/status") diff --git a/bms-back/bms-admin/src/test/java/com/bms/price/pay/PayWorkbookParserTest.java b/bms-back/bms-admin/src/test/java/com/bms/price/pay/PayWorkbookParserTest.java index e5ba6004..99e7bfff 100644 --- a/bms-back/bms-admin/src/test/java/com/bms/price/pay/PayWorkbookParserTest.java +++ b/bms-back/bms-admin/src/test/java/com/bms/price/pay/PayWorkbookParserTest.java @@ -91,14 +91,36 @@ class PayWorkbookParserTest { PayPreview preview = parse("32、"); PayProduct ups = product(preview, "德国UPS"); - assertEquals("Zone 1", ups.getWeightBands().get(0).getZoneName()); - assertEquals("DE", ups.getWeightBands().get(0).getCountryCode()); - assertTrue(decimal(ups.getWeightBands().get(0), "3.9")); - assertEquals("Zone 2", ups.getWeightBands().get(1).getZoneName()); - assertTrue(ups.getWeightBands().get(1).getCountryCode().startsWith("BE,LU")); - assertTrue(decimal(ups.getWeightBands().get(1), "7.58")); - assertTrue(ups.getWeightBands().get(4).getZoneName().contains("Zone 5")); - assertTrue(decimal(ups.getWeightBands().get(4), "9.45")); + String[] codes = { + "DE", + "BE", "LU", "NL", "AT", "SI", + "CZ", "PL", "SK", + "DK", "FI", "FR", "SE", "ES", "IT", "PT", "HU", + "GR", "IE", "BG", "HR", "EE", "LT", "LV", "RO" + }; + String[] zones = { + "Zone 1", + "Zone 2", "Zone 2", "Zone 2", "Zone 2", "Zone 2", + "Zone 3", "Zone 3", "Zone 3", + "Zone 4", "Zone 4", "Zone 4", "Zone 4", "Zone 4", "Zone 4", "Zone 4", "Zone 4", + "Zone 5", "Zone 5", "Zone 5", "Zone 5", "Zone 5", "Zone 5", "Zone 5", "Zone 5" + }; + String[] prices = { + "3.9", + "7.58", "7.58", "7.58", "7.58", "7.58", + "7.72", "7.72", "7.72", + "8.41", "8.41", "8.41", "8.41", "8.41", "8.41", "8.41", "8.41", + "9.45", "9.45", "9.45", "9.45", "9.45", "9.45", "9.45", "9.45" + }; + assertEquals(codes.length, ups.getWeightBands().size()); + for (int i = 0; i < codes.length; i++) + { + PayWeightBand band = ups.getWeightBands().get(i); + assertEquals(zones[i], band.getZoneName(), codes[i]); + assertEquals(codes[i], band.getCountryCode()); + assertEquals(codes[i], band.getCountryName()); + assertTrue(decimal(band, prices[i]), codes[i] + " " + prices[i]); + } assertTrue(ups.getSurcharges().stream().anyMatch(fee -> "报价说明".equals(fee.getFeeName()) && fee.getChargeText().contains("不含管理费") && fee.getChargeText().contains("不含燃油"))); PaySurcharge handling = ups.getSurcharges().stream() diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/PayFeeRuleGroup.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/PayFeeRuleGroup.java new file mode 100644 index 00000000..f5a57fcc --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/PayFeeRuleGroup.java @@ -0,0 +1,18 @@ +package com.bms.price.domain; + +import java.util.ArrayList; +import java.util.List; +import lombok.Data; + +/** + * 一个应付产品上的附加费开关。多个产品各自一组,互不影响。 + */ +@Data +public class PayFeeRuleGroup +{ + private Long productId; + + private String productName; + + private List rules = new ArrayList<>(); +} diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/PayProductFeeRule.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/PayProductFeeRule.java new file mode 100644 index 00000000..dd882911 --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/PayProductFeeRule.java @@ -0,0 +1,47 @@ +package com.bms.price.domain; + +import java.math.BigDecimal; +import com.bms.common.core.domain.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 应付产品附加费开关。挂在根产品上,价格仍在价卡版本的附加费行里。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class PayProductFeeRule extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + private Long ruleId; + + /** 根产品编号 */ + private Long productId; + + /** 附加费名称,用来到价卡版本里取价 */ + private String feeName; + + /** 1 参与计算,0 不参与 */ + private String enabled; + + /** SIZE 尺寸,VOLUME 体积,WEIGHT 重量,POSTCODE 邮编,ALWAYS 每票,RETURN 退件 */ + private String triggerType; + + private BigDecimal sideMaxCm; + + private BigDecimal sumMaxCm; + + private BigDecimal volumeMaxCm3; + + private BigDecimal minLengthCm; + + private BigDecimal minWidthCm; + + private BigDecimal minHeightCm; + + private BigDecimal weightMaxKg; + + /** ANY 任一条超出就收,ALL 写了的条件都超出才收 */ + private String joinMode; +} diff --git a/bms-back/bms-system/src/main/java/com/bms/price/mapper/PayProductFeeRuleMapper.java b/bms-back/bms-system/src/main/java/com/bms/price/mapper/PayProductFeeRuleMapper.java new file mode 100644 index 00000000..0f74bac2 --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/mapper/PayProductFeeRuleMapper.java @@ -0,0 +1,19 @@ +package com.bms.price.mapper; + +import java.util.List; +import org.apache.ibatis.annotations.Param; +import com.bms.price.domain.PayProductFeeRule; + +/** + * 应付产品附加费开关 + */ +public interface PayProductFeeRuleMapper +{ + List selectByProductId(Long productId); + + int deleteByProductId(Long productId); + + int deleteByProductIds(@Param("productIds") List productIds); + + int insertBatch(@Param("list") List list); +} 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 def9ba2f..9ccfc0b3 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 @@ -2811,17 +2811,22 @@ public class PayWorkbookParser { continue; } - PayWeightBand band = new PayWeightBand(); - band.setSectionName("分区报价"); String country = countryRow < 0 ? "" : grid.text(countryRow, col); - band.setCountryName(country); - band.setCountryCode(zoneCodes(country)); - band.setZoneName(grid.text(header, col)); - band.setChargeName(charge); - band.setPrice(price); - band.setPriceUnit(unitOf(charge + " EUR")); - band.setSortNo(product.getWeightBands().size() + 1); - product.getWeightBands().add(band); + String zone = grid.text(header, col); + for (String piece : zoneCountryPieces(country)) + { + PayWeightBand band = new PayWeightBand(); + band.setSectionName("分区报价"); + String code = zoneCodes(piece); + band.setCountryName(zoneCountryName(piece, code, country)); + band.setCountryCode(code.contains(",") ? "" : code); + band.setZoneName(zone); + band.setChargeName(charge); + band.setPrice(price); + band.setPriceUnit(unitOf(charge + " EUR")); + band.setSortNo(product.getWeightBands().size() + 1); + product.getWeightBands().add(band); + } } return row + 1; } @@ -3268,19 +3273,103 @@ public class PayWorkbookParser private static String zoneCodes(String text) { - Matcher matcher = Pattern.compile("\\b([A-Z]{2})\\b").matcher(text == null ? "" : text); StringBuilder codes = new StringBuilder(); - while (matcher.find()) + for (String code : zoneCodeList(text)) { if (codes.length() > 0) { codes.append(','); } - codes.append(matcher.group(1)); + codes.append(code); } return codes.toString(); } + private static List zoneCodeList(String text) + { + List codes = new ArrayList<>(); + Matcher matcher = Pattern.compile("\\b([A-Z]{2})\\b").matcher(text == null ? "" : text); + while (matcher.find()) + { + codes.add(matcher.group(1)); + } + return codes; + } + + /** + * 德国 UPS 这类分区国家格把多个国家写在同一格,用顿号或换行接起来。 + * 纯国家码列表按出现顺序拆开;没有国家码、只用顿号连接的国名同样拆开。 + * 只有一个国家时保留原格,避免把说明文字拆碎。 + */ + private static List zoneCountryPieces(String text) + { + if (text == null || text.isBlank()) + { + return List.of(""); + } + String folded = text.replace('\r', '\n'); + List codes = zoneCodeList(folded); + if (codes.size() > 1 && isCountryCodeList(folded)) + { + return codes; + } + if (!folded.contains("、")) + { + return List.of(text.trim()); + } + List pieces = new ArrayList<>(); + for (String part : folded.split("、")) + { + String token = part.replace('\n', ' ').trim(); + if (token.isEmpty()) + { + continue; + } + List tokenCodes = zoneCodeList(token); + if (tokenCodes.size() > 1 && isCountryCodeList(token)) + { + pieces.addAll(tokenCodes); + } + else + { + pieces.add(token); + } + } + return pieces.size() > 1 ? pieces : List.of(text.trim()); + } + + private static boolean isCountryCodeList(String text) + { + String stripped = text.replaceAll("\\b[A-Z]{2}\\b", ""); + return stripped.replaceAll("[、,,;;\\s]+", "").isEmpty(); + } + + /** 拆开后的一格如果就是国家码,名称也用这个码;没拆开的原格保持原文。 */ + private static String zoneCountryName(String piece, String code, String original) + { + if (piece == null) + { + return ""; + } + if (original != null && piece.equals(original.trim())) + { + return original.trim(); + } + String trimmed = piece.trim(); + if (code == null || code.isBlank() || code.contains(",")) + { + return trimmed; + } + if (trimmed.equalsIgnoreCase(code)) + { + return code; + } + String name = trimmed.replaceAll("(?i)\\b" + Pattern.quote(code) + "\\b", "") + .replaceAll("[()()]", " ") + .trim(); + return name.isEmpty() ? code : name; + } + private static int currencyHits(String text) { if (text == null || text.isBlank()) diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/IPayProductService.java b/bms-back/bms-system/src/main/java/com/bms/price/service/IPayProductService.java index fae6c118..1e19c0b2 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/IPayProductService.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/IPayProductService.java @@ -3,6 +3,7 @@ package com.bms.price.service; import java.io.OutputStream; import java.util.List; import java.util.Date; +import com.bms.price.domain.PayFeeRuleGroup; import com.bms.price.domain.PayPreview; import com.bms.price.domain.PayPriceCompare; import com.bms.price.domain.PayProduct; @@ -74,4 +75,10 @@ public interface IPayProductService void writeSource(Long productId, OutputStream output); int deletePayProducts(Long[] productIds); + + /** 按根产品编号带出附加费开关。没有配置过的产品返回空列表 */ + List listFeeRules(List productIds); + + /** 按产品整组替换附加费开关。每个产品一份,互不影响 */ + void saveFeeRules(List products); } 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 deb79eae..459d4652 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 @@ -3,11 +3,13 @@ package com.bms.price.service.impl; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.OutputStream; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.Iterator; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -26,6 +28,8 @@ import com.bms.common.utils.file.OssStorage; import com.bms.common.utils.uuid.IdUtils; import com.bms.price.domain.ChannelProductMap; import com.bms.price.domain.PayExtraLine; +import com.bms.price.domain.PayFeeRuleGroup; +import com.bms.price.domain.PayProductFeeRule; import com.bms.price.domain.PayPreview; import com.bms.price.domain.PayPriceCompare; import com.bms.price.domain.PayPriceDiff; @@ -35,6 +39,7 @@ import com.bms.price.domain.PaySaveCommand; import com.bms.price.domain.PaySurcharge; import com.bms.price.domain.PayWeightBand; import com.bms.price.mapper.ChannelProductMapMapper; +import com.bms.price.mapper.PayProductFeeRuleMapper; import com.bms.price.mapper.PayProductMapper; import com.bms.price.mapper.PayProductTermMapper; import com.bms.price.pay.PayWorkbookParser; @@ -64,6 +69,11 @@ public class PayProductServiceImpl implements IPayProductService @Autowired private PayProductTermMapper payProductTermMapper; + @Autowired + private PayProductFeeRuleMapper payProductFeeRuleMapper; + + private static final Set FEE_TRIGGERS = Set.of("SIZE", "VOLUME", "WEIGHT", "POSTCODE", "ALWAYS", "RETURN"); + @Autowired private ISysDictTypeService dictTypeService; @@ -670,6 +680,7 @@ public class PayProductServiceImpl implements IPayProductService return 0; } payProductTermMapper.deleteByProductIds(ids); + payProductFeeRuleMapper.deleteByProductIds(ids); payProductMapper.deleteWeightBands(ids); payProductMapper.deleteSurcharges(ids); payProductMapper.deleteExtraLines(ids); @@ -1216,6 +1227,176 @@ public class PayProductServiceImpl implements IPayProductService return rows == null ? 0 : rows.size(); } + @Override + public List listFeeRules(List productIds) + { + List groups = new ArrayList<>(); + if (productIds == null) + { + return groups; + } + LinkedHashSet ordered = new LinkedHashSet<>(); + for (Long productId : productIds) + { + if (productId != null) + { + ordered.add(productId); + } + } + for (Long productId : ordered) + { + PayProduct root = requireRoot(productId); + PayFeeRuleGroup group = new PayFeeRuleGroup(); + group.setProductId(root.getProductId()); + group.setProductName(root.getProductName()); + List rules = payProductFeeRuleMapper.selectByProductId(root.getProductId()); + group.setRules(rules == null ? new ArrayList<>() : rules); + groups.add(group); + } + return groups; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void saveFeeRules(List products) + { + if (products == null || products.isEmpty()) + { + throw new ServiceException("请选择应付产品"); + } + String operator = SecurityUtils.getUsername(); + Date now = DateUtils.getNowDate(); + for (PayFeeRuleGroup group : products) + { + if (group == null || group.getProductId() == null) + { + throw new ServiceException("请选择应付产品"); + } + PayProduct root = requireRoot(group.getProductId()); + List rules = normalizeFeeRules(root, group.getRules(), operator, now); + payProductFeeRuleMapper.deleteByProductId(root.getProductId()); + if (!rules.isEmpty()) + { + payProductFeeRuleMapper.insertBatch(rules); + } + } + } + + private PayProduct requireRoot(Long productId) + { + PayProduct product = payProductMapper.selectPayProductById(productId); + if (product == null) + { + throw new ServiceException("应付产品不存在"); + } + if (product.getOriginId() != null) + { + product = payProductMapper.selectPayProductById(product.getOriginId()); + if (product == null) + { + throw new ServiceException("应付产品不存在"); + } + } + return product; + } + + private List normalizeFeeRules(PayProduct root, List source, String operator, Date now) + { + List rules = new ArrayList<>(); + Set names = new HashSet<>(); + String productName = root.getProductName() == null ? String.valueOf(root.getProductId()) : root.getProductName(); + if (source == null) + { + return rules; + } + for (PayProductFeeRule raw : source) + { + if (raw == null || isBlank(raw.getFeeName())) + { + if (raw != null && (feeRuleFilled(raw) || feeRuleChanged(raw))) + { + throw new ServiceException(productName + "有未选择名称的附加费"); + } + continue; + } + String feeName = raw.getFeeName().trim(); + if (feeName.length() > 100) + { + throw new ServiceException(productName + "的附加费名称过长"); + } + if (!names.add(feeName)) + { + throw new ServiceException(productName + "的附加费「" + feeName + "」重复了"); + } + String trigger = isBlank(raw.getTriggerType()) ? "ALWAYS" : raw.getTriggerType().trim().toUpperCase(Locale.ROOT); + if (!FEE_TRIGGERS.contains(trigger)) + { + throw new ServiceException(productName + "的触发类型不正确"); + } + String joinMode = isBlank(raw.getJoinMode()) ? "ANY" : raw.getJoinMode().trim().toUpperCase(Locale.ROOT); + if (!"ANY".equals(joinMode) && !"ALL".equals(joinMode)) + { + throw new ServiceException(productName + "的命中方式不正确"); + } + String enabled = "1".equals(raw.getEnabled()) ? "1" : "0"; + checkFeeNumber(raw.getSideMaxCm(), productName, "最长边"); + checkFeeNumber(raw.getSumMaxCm(), productName, "长宽高之和"); + checkFeeNumber(raw.getVolumeMaxCm3(), productName, "体积"); + checkFeeNumber(raw.getMinLengthCm(), productName, "最小长度"); + checkFeeNumber(raw.getMinWidthCm(), productName, "最小宽度"); + checkFeeNumber(raw.getMinHeightCm(), productName, "最小高度"); + checkFeeNumber(raw.getWeightMaxKg(), productName, "重量上限"); + String remark = raw.getRemark() == null ? null : raw.getRemark().trim(); + if (remark != null && remark.length() > 500) + { + throw new ServiceException(productName + "的说明过长"); + } + PayProductFeeRule rule = new PayProductFeeRule(); + rule.setProductId(root.getProductId()); + rule.setFeeName(feeName); + rule.setEnabled(enabled); + rule.setTriggerType(trigger); + rule.setSideMaxCm(raw.getSideMaxCm()); + rule.setSumMaxCm(raw.getSumMaxCm()); + rule.setVolumeMaxCm3(raw.getVolumeMaxCm3()); + rule.setMinLengthCm(raw.getMinLengthCm()); + rule.setMinWidthCm(raw.getMinWidthCm()); + rule.setMinHeightCm(raw.getMinHeightCm()); + rule.setWeightMaxKg(raw.getWeightMaxKg()); + rule.setJoinMode(joinMode); + rule.setRemark(isBlank(remark) ? null : remark); + rule.setCreateBy(operator); + rule.setCreateTime(now); + rule.setUpdateBy(operator); + rule.setUpdateTime(now); + rules.add(rule); + } + return rules; + } + + private static boolean feeRuleChanged(PayProductFeeRule rule) + { + String trigger = rule.getTriggerType() == null ? "" : rule.getTriggerType().trim(); + String joinMode = rule.getJoinMode() == null ? "" : rule.getJoinMode().trim(); + return (!trigger.isEmpty() && !"ALWAYS".equalsIgnoreCase(trigger)) + || (!joinMode.isEmpty() && !"ANY".equalsIgnoreCase(joinMode)); + } + + private static boolean feeRuleFilled(PayProductFeeRule rule) + { + return rule.getSideMaxCm() != null || rule.getSumMaxCm() != null || rule.getVolumeMaxCm3() != null + || rule.getMinLengthCm() != null || rule.getMinWidthCm() != null || rule.getMinHeightCm() != null + || rule.getWeightMaxKg() != null || !isBlank(rule.getRemark()) || "1".equals(rule.getEnabled()); + } + + private static void checkFeeNumber(BigDecimal value, String productName, String label) + { + if (value != null && value.signum() < 0) + { + throw new ServiceException(productName + "的" + label + "不能小于0"); + } + } + private static boolean isBlank(String value) { return value == null || value.trim().isEmpty(); diff --git a/bms-back/bms-system/src/main/resources/mapper/price/PayProductFeeRuleMapper.xml b/bms-back/bms-system/src/main/resources/mapper/price/PayProductFeeRuleMapper.xml new file mode 100644 index 00000000..41dd74e8 --- /dev/null +++ b/bms-back/bms-system/src/main/resources/mapper/price/PayProductFeeRuleMapper.xml @@ -0,0 +1,42 @@ + + + + + + + + delete from pay_product_fee_rule where product_id = #{productId} + + + + delete from pay_product_fee_rule where product_id in + #{id} + + + + insert into pay_product_fee_rule ( + product_id, fee_name, enabled, trigger_type, side_max_cm, sum_max_cm, volume_max_cm3, + min_length_cm, min_width_cm, min_height_cm, weight_max_kg, join_mode, remark, + created_by, create_time, updated_by, update_time + ) values + + (#{item.productId}, #{item.feeName}, #{item.enabled}, #{item.triggerType}, + #{item.sideMaxCm}, #{item.sumMaxCm}, #{item.volumeMaxCm3}, + #{item.minLengthCm}, #{item.minWidthCm}, #{item.minHeightCm}, #{item.weightMaxKg}, + #{item.joinMode}, #{item.remark}, #{item.createBy}, #{item.createTime}, + #{item.updateBy}, #{item.updateTime}) + + + diff --git a/bms-back/sql/data/bms_surcharge_type.sql b/bms-back/sql/data/bms_surcharge_type.sql new file mode 100644 index 00000000..5b56f83f --- /dev/null +++ b/bms-back/sql/data/bms_surcharge_type.sql @@ -0,0 +1,162 @@ +-- 应付附加费类型。名称来自 D:\应付供应商 全部价卡的附加费收费项,已去重。 +-- 可重复执行。 +SET NAMES utf8mb4; + +INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark) +SELECT '附加费类型', 'bms_surcharge_type', '0', 'admin', sysdate(), '应付价卡附加费名称' +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'bms_surcharge_type'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark) +SELECT v.dict_sort, v.dict_label, v.dict_value, 'bms_surcharge_type', '', 'default', 'N', '0', 'admin', sysdate(), '应付价卡附加费' +FROM ( + SELECT 1 AS dict_sort, '疫情附加费' AS dict_label, '疫情附加费' AS dict_value + UNION ALL SELECT 2, '燃油附加费', '燃油附加费' + UNION ALL SELECT 3, '地址及联系方式修正费', '地址及联系方式修正费' + UNION ALL SELECT 4, '城市邮编不匹配', '城市邮编不匹配' + UNION ALL SELECT 5, '分拣机不可操作费', '分拣机不可操作费' + UNION ALL SELECT 6, '超重超尺寸费', '超重超尺寸费' + UNION ALL SELECT 7, '脱碳附加费', '脱碳附加费' + UNION ALL SELECT 8, '非法国电话联系费', '非法国电话联系费' + UNION ALL SELECT 9, '换面单费', '换面单费' + UNION ALL SELECT 10, '交错仓罚金', '交错仓罚金' + UNION ALL SELECT 11, '乡村邮编单量占比过大罚金', '乡村邮编单量占比过大罚金' + UNION ALL SELECT 12, '仿牌侵权罚款', '仿牌侵权罚款' + UNION ALL SELECT 13, '危险品罚款', '危险品罚款' + UNION ALL SELECT 14, '燃油费', '燃油费' + UNION ALL SELECT 15, '重派费', '重派费' + UNION ALL SELECT 16, '退件仓库操作费', '退件仓库操作费' + UNION ALL SELECT 17, '重派仓库操作费', '重派仓库操作费' + UNION ALL SELECT 18, '旺季附加费', '旺季附加费' + UNION ALL SELECT 19, '高峰旺季附加费', '高峰旺季附加费' + UNION ALL SELECT 20, '超尺寸超重费', '超尺寸超重费' + UNION ALL SELECT 21, '路由码选择附加费', '路由码选择附加费' + UNION ALL SELECT 22, '未预报附加费', '未预报附加费' + UNION ALL SELECT 23, '托收附加费', '托收附加费' + UNION ALL SELECT 24, '提供虚假信息罚款费', '提供虚假信息罚款费' + UNION ALL SELECT 25, '零售网点路线', '零售网点路线' + UNION ALL SELECT 26, '退件费', '退件费' + UNION ALL SELECT 27, '退件重派费', '退件重派费' + UNION ALL SELECT 28, '退件操作费', '退件操作费' + UNION ALL SELECT 29, '重派操作费', '重派操作费' + UNION ALL SELECT 30, '道路通行费', '道路通行费' + UNION ALL SELECT 31, '重量信息修正附加费', '重量信息修正附加费' + UNION ALL SELECT 32, '高风险地区附加费', '高风险地区附加费' + UNION ALL SELECT 33, '取还服务', '取还服务' + UNION ALL SELECT 34, '取货发货服务', '取货发货服务' + UNION ALL SELECT 35, '现金服务', '现金服务' + UNION ALL SELECT 36, '灵活配送服务', '灵活配送服务' + UNION ALL SELECT 37, '安全费', '安全费' + UNION ALL SELECT 38, '危机燃油附加费', '危机燃油附加费' + UNION ALL SELECT 39, '能源附加费', '能源附加费' + UNION ALL SELECT 40, '异形费', '异形费' + UNION ALL SELECT 41, '漏预报或漏发货', '漏预报或漏发货' + UNION ALL SELECT 42, '标签费', '标签费' + UNION ALL SELECT 43, '无法派送费', '无法派送费' + UNION ALL SELECT 44, '变更服务费', '变更服务费' + UNION ALL SELECT 45, '二次派送费', '二次派送费' + UNION ALL SELECT 46, '签名费', '签名费' + UNION ALL SELECT 47, '危险货物费', '危险货物费' + UNION ALL SELECT 48, '特殊产品加收', '特殊产品加收' + UNION ALL SELECT 49, '岛屿附加费', '岛屿附加费' + UNION ALL SELECT 50, '陆运能源附加费', '陆运能源附加费' + UNION ALL SELECT 51, '空运能源附加费', '空运能源附加费' + UNION ALL SELECT 52, '带电附加费', '带电附加费' + UNION ALL SELECT 53, '特货费', '特货费' + UNION ALL SELECT 54, '偏远费', '偏远费' + UNION ALL SELECT 55, '偏远地区附加费', '偏远地区附加费' + UNION ALL SELECT 56, '超大附加费(包裹)', '超大附加费(包裹)' + UNION ALL SELECT 57, '超重附加费(包裹)', '超重附加费(包裹)' + UNION ALL SELECT 58, '重复单号', '重复单号' + UNION ALL SELECT 59, '未预报', '未预报' + UNION ALL SELECT 60, '无法投递或其他原因退件运费', '无法投递或其他原因退件运费' + UNION ALL SELECT 61, '航司可持续性附加费', '航司可持续性附加费' + UNION ALL SELECT 62, '退件处理费', '退件处理费' + UNION ALL SELECT 63, '超尺寸费', '超尺寸费' + UNION ALL SELECT 64, '周末服务费', '周末服务费' + UNION ALL SELECT 65, '空气清洁费', '空气清洁费' + UNION ALL SELECT 66, '拖车附加费', '拖车附加费' + UNION ALL SELECT 67, '派送附加费', '派送附加费' + UNION ALL SELECT 68, '自送海外仓运费', '自送海外仓运费' + UNION ALL SELECT 69, '操作费', '操作费' + UNION ALL SELECT 70, '仓库操作费', '仓库操作费' + UNION ALL SELECT 71, '重新包装费', '重新包装费' + UNION ALL SELECT 72, '换包装费', '换包装费' + UNION ALL SELECT 73, '去碳附加费', '去碳附加费' + UNION ALL SELECT 74, '超重费', '超重费' + UNION ALL SELECT 75, '地址修正费', '地址修正费' + UNION ALL SELECT 76, '邮编城市不对应费', '邮编城市不对应费' + UNION ALL SELECT 77, '邮箱和电话缺失费', '邮箱和电话缺失费' + UNION ALL SELECT 78, '人工处理费', '人工处理费' + UNION ALL SELECT 79, '海外省费', '海外省费' + UNION ALL SELECT 80, '错误送仓罚款', '错误送仓罚款' + UNION ALL SELECT 81, '更换面单费', '更换面单费' + UNION ALL SELECT 82, '乡村地区占比超标准罚款', '乡村地区占比超标准罚款' + UNION ALL SELECT 83, 'COD', 'COD' + UNION ALL SELECT 84, '重新贴标附加费', '重新贴标附加费' + UNION ALL SELECT 85, '重派运费', '重派运费' + UNION ALL SELECT 86, '短期能源附加费', '短期能源附加费' + UNION ALL SELECT 87, '旺季高峰期附加费', '旺季高峰期附加费' + UNION ALL SELECT 88, '面单错误附加费', '面单错误附加费' + UNION ALL SELECT 89, '零售网点自提费', '零售网点自提费' + UNION ALL SELECT 90, '异形件费', '异形件费' + UNION ALL SELECT 91, '重量差异附加费', '重量差异附加费' + UNION ALL SELECT 92, '换单费', '换单费' + UNION ALL SELECT 93, '无法投递处理费', '无法投递处理费' + UNION ALL SELECT 94, '无法投递处理费-陆运', '无法投递处理费-陆运' + UNION ALL SELECT 95, '无法投递处理费-空运', '无法投递处理费-空运' + UNION ALL SELECT 96, '战争附加费', '战争附加费' + UNION ALL SELECT 97, '超尺寸附加费', '超尺寸附加费' + UNION ALL SELECT 98, '偏远附加费', '偏远附加费' + UNION ALL SELECT 99, '空气减排费', '空气减排费' + UNION ALL SELECT 100, '面单无法识别费', '面单无法识别费' + UNION ALL SELECT 101, '无法派送或退件费', '无法派送或退件费' + UNION ALL SELECT 102, '超重附加费', '超重附加费' + UNION ALL SELECT 103, '重复单号费', '重复单号费' + UNION ALL SELECT 104, '使用取消单号发货费', '使用取消单号发货费' + UNION ALL SELECT 105, '面单不符费', '面单不符费' + UNION ALL SELECT 106, '面单粘贴不合规范', '面单粘贴不合规范' + UNION ALL SELECT 107, '未经允许乱交邮费', '未经允许乱交邮费' + UNION ALL SELECT 108, '异形件附加费', '异形件附加费' + UNION ALL SELECT 109, '退件费(陆运)', '退件费(陆运)' + UNION ALL SELECT 110, '退件费(空运)', '退件费(空运)' + UNION ALL SELECT 111, '邮编城市不对应', '邮编城市不对应' + UNION ALL SELECT 112, '收件人信息错误处理费', '收件人信息错误处理费' + UNION ALL SELECT 113, '无法过机操作费', '无法过机操作费' + UNION ALL SELECT 114, '不可达地址费', '不可达地址费' + UNION ALL SELECT 115, '销毁费', '销毁费' + UNION ALL SELECT 116, '仓储费', '仓储费' + UNION ALL SELECT 117, 'CO₂附加费', 'CO₂附加费' + UNION ALL SELECT 118, '退回件处理费', '退回件处理费' + UNION ALL SELECT 119, '异形件/超尺寸处理费', '异形件/超尺寸处理费' + UNION ALL SELECT 120, '重量申报错误操作费', '重量申报错误操作费' + UNION ALL SELECT 121, '垃圾处理费', '垃圾处理费' + UNION ALL SELECT 122, '绿色附加费', '绿色附加费' + UNION ALL SELECT 123, '无法投递费', '无法投递费' + UNION ALL SELECT 124, '额外处理费', '额外处理费' + UNION ALL SELECT 125, '超大件', '超大件' + UNION ALL SELECT 126, '超限制', '超限制' + UNION ALL SELECT 127, '地址更正', '地址更正' + UNION ALL SELECT 128, '扩展区域收货和送货服务', '扩展区域收货和送货服务' + UNION ALL SELECT 129, '偏远地区收货和送货服务', '偏远地区收货和送货服务' + UNION ALL SELECT 130, '私人地址派送费', '私人地址派送费' + UNION ALL SELECT 131, '包裹信息缺失', '包裹信息缺失' + UNION ALL SELECT 132, '高峰附加费', '高峰附加费' + UNION ALL SELECT 133, '更改地址费', '更改地址费' + UNION ALL SELECT 134, '空气减排附加费', '空气减排附加费' + UNION ALL SELECT 135, '异地取货费', '异地取货费' + UNION ALL SELECT 136, '包裹退件运费', '包裹退件运费' + UNION ALL SELECT 137, '退件入仓处理费', '退件入仓处理费' + UNION ALL SELECT 138, '退件重派操作费', '退件重派操作费' + UNION ALL SELECT 139, '退件重派运费', '退件重派运费' + UNION ALL SELECT 140, '退件销毁费', '退件销毁费' + UNION ALL SELECT 141, 'Bulky goods Fee', 'Bulky goods Fee' + UNION ALL SELECT 142, '地址校验费', '地址校验费' + UNION ALL SELECT 143, 'Labeling fee', 'Labeling fee' + UNION ALL SELECT 144, '瑞典和芬兰门到门派送费', '瑞典和芬兰门到门派送费' + UNION ALL SELECT 145, '特别旺季附加费', '特别旺季附加费' + UNION ALL SELECT 146, '脱欧手续费', '脱欧手续费' +) v +WHERE NOT EXISTS ( + SELECT 1 FROM sys_dict_data d + WHERE d.dict_type = 'bms_surcharge_type' AND d.dict_value = v.dict_value +); diff --git a/bms-back/sql/price/pay_product.sql b/bms-back/sql/price/pay_product.sql index fbab0ebd..a3aacdc2 100644 --- a/bms-back/sql/price/pay_product.sql +++ b/bms-back/sql/price/pay_product.sql @@ -88,6 +88,29 @@ CREATE TABLE IF NOT EXISTS `pay_extra_line` ( KEY `idx_pay_extra_product` (`product_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='应付产品上的邮编或其他配置原文'; +CREATE TABLE IF NOT EXISTS `pay_product_fee_rule` ( + `rule_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '规则ID', + `product_id` bigint(20) NOT NULL COMMENT '根产品编号,渠道应付绑定的产品,不是某一版导入的版本号', + `fee_name` varchar(100) NOT NULL COMMENT '附加费名称,对应价卡附加费行,用来取价', + `enabled` char(1) NOT NULL DEFAULT '0' COMMENT '1参与计算,0不参与', + `trigger_type` varchar(20) NOT NULL DEFAULT 'ALWAYS' COMMENT '触发类型:SIZE尺寸 VOLUME体积 WEIGHT重量 POSTCODE邮编 ALWAYS每票 RETURN退件', + `side_max_cm` decimal(10,2) DEFAULT NULL COMMENT '最长边上限,厘米,超出则命中', + `sum_max_cm` decimal(10,2) DEFAULT NULL COMMENT '长+宽+高上限,厘米,超出则命中', + `volume_max_cm3` decimal(14,2) DEFAULT NULL COMMENT '体积上限,立方厘米,长×宽×高超出则命中', + `min_length_cm` decimal(10,2) DEFAULT NULL COMMENT '最小长度,厘米,小于则命中', + `min_width_cm` decimal(10,2) DEFAULT NULL COMMENT '最小宽度,厘米,小于则命中', + `min_height_cm` decimal(10,2) DEFAULT NULL COMMENT '最小高度,厘米,小于则命中', + `weight_max_kg` decimal(10,3) DEFAULT NULL COMMENT '重量上限,千克,计费重超出则命中', + `join_mode` varchar(10) NOT NULL DEFAULT 'ANY' COMMENT 'ANY任一条超出就收,ALL写了的条件都超出才收', + `remark` varchar(500) DEFAULT NULL COMMENT '说明,不参与判断', + `created_by` varchar(64) DEFAULT '' COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `updated_by` varchar(64) DEFAULT '' COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`rule_id`), + UNIQUE KEY `uk_pay_fee_rule_product` (`product_id`, `fee_name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='应付产品附加费开关'; + -- 菜单挂在价格基础下面 SET @parentId := ( SELECT menu_id FROM sys_menu diff --git a/bms-back/sql/price/pay_product_fee_rule.sql b/bms-back/sql/price/pay_product_fee_rule.sql new file mode 100644 index 00000000..19c35360 --- /dev/null +++ b/bms-back/sql/price/pay_product_fee_rule.sql @@ -0,0 +1,25 @@ +-- 应付产品附加费开关。挂在根产品上,同一产品同一费用名称一行。可重复执行。 +SET NAMES utf8mb4; + +CREATE TABLE IF NOT EXISTS `pay_product_fee_rule` ( + `rule_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '规则ID', + `product_id` bigint(20) NOT NULL COMMENT '根产品编号,渠道应付绑定的产品,不是某一版导入的版本号', + `fee_name` varchar(100) NOT NULL COMMENT '附加费名称,对应价卡附加费行,用来取价', + `enabled` char(1) NOT NULL DEFAULT '0' COMMENT '1参与计算,0不参与', + `trigger_type` varchar(20) NOT NULL DEFAULT 'ALWAYS' COMMENT '触发类型:SIZE尺寸 VOLUME体积 WEIGHT重量 POSTCODE邮编 ALWAYS每票 RETURN退件', + `side_max_cm` decimal(10,2) DEFAULT NULL COMMENT '最长边上限,厘米,超出则命中', + `sum_max_cm` decimal(10,2) DEFAULT NULL COMMENT '长+宽+高上限,厘米,超出则命中', + `volume_max_cm3` decimal(14,2) DEFAULT NULL COMMENT '体积上限,立方厘米,长×宽×高超出则命中', + `min_length_cm` decimal(10,2) DEFAULT NULL COMMENT '最小长度,厘米,小于则命中', + `min_width_cm` decimal(10,2) DEFAULT NULL COMMENT '最小宽度,厘米,小于则命中', + `min_height_cm` decimal(10,2) DEFAULT NULL COMMENT '最小高度,厘米,小于则命中', + `weight_max_kg` decimal(10,3) DEFAULT NULL COMMENT '重量上限,千克,计费重超出则命中', + `join_mode` varchar(10) NOT NULL DEFAULT 'ANY' COMMENT 'ANY任一条超出就收,ALL写了的条件都超出才收', + `remark` varchar(500) DEFAULT NULL COMMENT '说明,不参与判断', + `created_by` varchar(64) DEFAULT '' COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `updated_by` varchar(64) DEFAULT '' COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`rule_id`), + UNIQUE KEY `uk_pay_fee_rule_product` (`product_id`, `fee_name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='应付产品附加费开关'; diff --git a/bms-front/src/api/price/pay.js b/bms-front/src/api/price/pay.js index 4a4f290a..373de1b8 100644 --- a/bms-front/src/api/price/pay.js +++ b/bms-front/src/api/price/pay.js @@ -140,6 +140,24 @@ export function generatePayBill(billId) { }) } +/** 勾选产品的附加费开关。productIds 用逗号分隔 */ +export function listFeeRule(productIds) { + return request({ + url: '/price/pay/feeRule/list', + method: 'get', + params: { productIds: productIds.join(',') } + }) +} + +/** 按产品保存附加费开关。每个产品一份 */ +export function saveFeeRule(data) { + return request({ + url: '/price/pay/feeRule', + method: 'put', + data + }) +} + /** 删除产品及其重量段、附加费、其他配置 */ export function delPay(productIds) { return request({ diff --git a/bms-front/src/views/price/basic/pay/components/PayFeeRuleDialog.vue b/bms-front/src/views/price/basic/pay/components/PayFeeRuleDialog.vue new file mode 100644 index 00000000..726a281e --- /dev/null +++ b/bms-front/src/views/price/basic/pay/components/PayFeeRuleDialog.vue @@ -0,0 +1,291 @@ + + + + + diff --git a/bms-front/src/views/price/basic/pay/index.vue b/bms-front/src/views/price/basic/pay/index.vue index a14001b6..76040829 100644 --- a/bms-front/src/views/price/basic/pay/index.vue +++ b/bms-front/src/views/price/basic/pay/index.vue @@ -13,6 +13,9 @@ 导入价卡 + + 附加费配置 + 删除 @@ -60,6 +63,7 @@ +
@@ -150,10 +154,12 @@ import { listPay, getPay, delPay, updatePay, listPayTerm, updatePayTerm, changePayTermStatus, comparePay } from '@/api/price/pay' import PayImportDialog from './components/PayImportDialog.vue' import PayConfigDetail from './components/PayConfigDetail.vue' +import PayFeeRuleDialog from './components/PayFeeRuleDialog.vue' const { proxy } = getCurrentInstance() const tableRef = ref() const importRef = ref() +const feeRuleRef = ref() const detailVisible = ref(false) const detailLoading = ref(false) const editing = ref(false)