国际集运计费方案:策略模式+规则引擎深度解析
摘要:反向海淘运费计算核心依赖首重续重、体积重及不同国家与渠道的差异化计费规则。本文阐述如何基于Java策略模式与Drools规则引擎,构建一套灵活可配置的计费引擎,支持实时计算与批量试算。
运费规则需求建模
以下列举几类典型的国际运费规则:
- 美国云途专线:首重0.5kg,85元;续重每0.5kg,25元,采用实际重量计费。
- 美国EMS:首重0.5kg,110元;续重每0.5kg,35元,计费取实际重量与体积重量(长*宽*高/5000)中的较大值。
- 加拿大专线:首重1kg,120元;续重每0.5kg,30元。
由此可见,核心变量仅两个:计费方式(实际重量与体积重)和费率结构(首重/续重)。面对频繁变动的业务需求,代码设计需高度灵活可配置。
基于策略模式的运费计算实现
首先定义运费计算策略接口:
public interface FreightCalculator {
BigDecimal calculate(ShippingPackage pkg, FreightRule rule);
}
实际重量策略:
@Component
public class ActualWeightCalculator implements FreightCalculator {
@Override
public BigDecimal calculate(ShippingPackage pkg, FreightRule rule) {
double weight = pkg.getActualWeightKg();
if (weight <= rule.getFirstWeight()) {
return rule.getFirstPrice();
}
double additional = weight - rule.getFirstWeight();
int units = (int) Math.ceil(additional / rule.getAdditionalUnit());
return rule.getFirstPrice().add(
rule.getAdditionalPrice().multiply(BigDecimal.valueOf(units))
);
}
}
体积重策略:
@Component
public class VolumetricWeightCalculator implements FreightCalculator {
@Override
public BigDecimal calculate(ShippingPackage pkg, FreightRule rule) {
double volumetric = pkg.getLengthCm() * pkg.getWidthCm() * pkg.getHeightCm() / 5000.0;
double weight = Math.max(pkg.getActualWeightKg(), volumetric);
// 复用实际重量计算逻辑,传入修正后的重量
return actualWeightCalculator.calculate(new ShippingPackage(weight), rule);
}
}
策略工厂,按类型定位对应计算器:
@Component
public class FreightCalculatorFactory {
private final Map calculatorMap = new HashMap<>();
public FreightCalculatorFactory() {
calculatorMap.put("actual", new ActualWeightCalculator());
calculatorMap.put("volumetric", new VolumetricWeightCalculator());
// 可扩展更多策略
}
public FreightCalculator getCalculator(String type) {
return calculatorMap.getOrDefault(type, calculatorMap.get("actual"));
}
}
此时可能出现疑问:运费模板如何配置?难道每次规则调整都需要修改代码?
答案是利用规则引擎实现灵活配置。
基于Drools规则引擎配置运费模板
用Drools,我们把运费规则写成 .drl 文件:
// 运费规则定义文件 freight.drl
package com.taocarts.freight;
import com.taocarts.domain.ShippingRequest;
import com.taocarts.domain.FreightResult;
rule "US_YunExpress_Rule"
when
$req: ShippingRequest(destinationCountry == "US", channel == "YunExpress")
then
FreightRule rule = new FreightRule();
rule.setFirstWeight(0.5);
rule.setFirstPrice(new BigDecimal("85"));
rule.setAdditionalUnit(0.5);
rule.setAdditionalPrice(new BigDecimal("25"));
rule.setCalculatorType("actual");
$req.setMatchedRule(rule);
end
rule "US_EMS_Rule"
when
$req: ShippingRequest(destinationCountry == "US", channel == "EMS")
then
FreightRule rule = new FreightRule();
rule.setFirstWeight(0.5);
rule.setFirstPrice(new BigDecimal("110"));
rule.setAdditionalUnit(0.5);
rule.setAdditionalPrice(new BigDecimal("35"));
rule.setCalculatorType("volumetric");
$req.setMatchedRule(rule);
end
通过以上方式,新增物流渠道或调整费率仅需修改DRL文件,应用无需重启(结合规则热加载机制效果更佳)。
合并发货与拼单运费分摊
实际业务中,用户常将多笔订单合并发货,再按重量比例分摊总运费。
@Service
public class CombinedFreightService {
public CombinedFreightResult combineAndCalculate(List orderIds, String channel) {
// 第一步:汇总订单商品重量
List orders = orderService.listByIds(orderIds);
double totalActualWeight = orders.stream()
.mapToDouble(Order::getTotalWeight).sum();
double totalVolumetricWeight = orders.stream()
.mapToDouble(o -> o.getLengthCm() * o.getWidthCm() * o.getHeightCm() / 5000.0)
.max().orElse(0);
double finalWeight = Math.max(totalActualWeight, totalVolumetricWeight);
// 第二步:获取对应运费规则
FreightRule rule = freightRuleMapper.selectByChannelAndCountry(
channel, orders.get(0).getCountry());
FreightCalculator calculator = calculatorFactory.getCalculator(rule.getCalculatorType());
BigDecimal totalFreight = calculator.calculate(new ShippingPackage(finalWeight), rule);
// 第三步:按实际重量比例分摊
List shares = new ArrayList<>();
for (Order order : orders) {
double shareRatio = order.getTotalWeight() / totalActualWeight;
BigDecimal shareFreight = totalFreight.multiply(BigDecimal.valueOf(shareRatio));
shares.add(new FreightShare(order.getId(), shareFreight));
}
return new CombinedFreightResult(totalFreight, shares);
}
}
关键点:体积重取各订单中的最大值(按整箱体积计算),实际重量累加,最终计费重量取二者较大值。分摊策略采用实际重量比例,相对简洁;若存在大件商品等特殊场景,需针对性细化分摊逻辑。
性能优化策略
高频计算场景下,常用优化手段:
- 运费模板缓存至Redis,每小时刷新一次,降低数据库查询频率。
- 体积重计算采用本地缓存,同一包裹重复计算时直接命中结果。
- 批量计费场景利用CompletableFuture并行处理,充分提升多核利用率。
经压力测试,单节点可支撑500次/秒的运费计算请求,P99延迟低于50毫秒,满足生产环境性能需求。
