新建应付配置的附加费开关配置

This commit is contained in:
liuminghui
2026-11-05 16:05:01 +08:00
parent 116b3b74e4
commit fde53681d1
15 changed files with 1002 additions and 21 deletions
@@ -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<PayProductFeeRule> rules = new ArrayList<>();
}
@@ -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;
}
@@ -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<PayProductFeeRule> selectByProductId(Long productId);
int deleteByProductId(Long productId);
int deleteByProductIds(@Param("productIds") List<Long> productIds);
int insertBatch(@Param("list") List<PayProductFeeRule> list);
}
@@ -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<String> zoneCodeList(String text)
{
List<String> 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<String> zoneCountryPieces(String text)
{
if (text == null || text.isBlank())
{
return List.of("");
}
String folded = text.replace('\r', '\n');
List<String> codes = zoneCodeList(folded);
if (codes.size() > 1 && isCountryCodeList(folded))
{
return codes;
}
if (!folded.contains("、"))
{
return List.of(text.trim());
}
List<String> pieces = new ArrayList<>();
for (String part : folded.split("、"))
{
String token = part.replace('\n', ' ').trim();
if (token.isEmpty())
{
continue;
}
List<String> 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())
@@ -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<PayFeeRuleGroup> listFeeRules(List<Long> productIds);
/** 按产品整组替换附加费开关。每个产品一份,互不影响 */
void saveFeeRules(List<PayFeeRuleGroup> products);
}
@@ -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<String> 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<PayFeeRuleGroup> listFeeRules(List<Long> productIds)
{
List<PayFeeRuleGroup> groups = new ArrayList<>();
if (productIds == null)
{
return groups;
}
LinkedHashSet<Long> 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<PayProductFeeRule> 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<PayFeeRuleGroup> 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<PayProductFeeRule> 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<PayProductFeeRule> normalizeFeeRules(PayProduct root, List<PayProductFeeRule> source, String operator, Date now)
{
List<PayProductFeeRule> rules = new ArrayList<>();
Set<String> 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();
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bms.price.mapper.PayProductFeeRuleMapper">
<select id="selectByProductId" resultType="com.bms.price.domain.PayProductFeeRule">
select rule_id as ruleId, product_id as productId, fee_name as feeName, enabled,
trigger_type as triggerType, side_max_cm as sideMaxCm, sum_max_cm as sumMaxCm,
volume_max_cm3 as volumeMaxCm3, min_length_cm as minLengthCm, min_width_cm as minWidthCm,
min_height_cm as minHeightCm, weight_max_kg as weightMaxKg, join_mode as joinMode,
remark, created_by as createBy, create_time as createTime, updated_by as updateBy,
update_time as updateTime
from pay_product_fee_rule
where product_id = #{productId}
order by rule_id
</select>
<delete id="deleteByProductId">
delete from pay_product_fee_rule where product_id = #{productId}
</delete>
<delete id="deleteByProductIds">
delete from pay_product_fee_rule where product_id in
<foreach collection="productIds" item="id" open="(" separator="," close=")">#{id}</foreach>
</delete>
<insert id="insertBatch">
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
<foreach collection="list" item="item" separator=",">
(#{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})
</foreach>
</insert>
</mapper>