摘要:反向海淘运费计算,本质上按照首重续重、体积重以及不同国家与渠道的差异化计费规则执行。本文将深入探讨如何利用 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,将运费规则编写为 .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) {
// 1. 汇总订单商品
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);
// 2. 获取运费规则
FreightRule rule = freightRuleMapper.selectByChannelAndCountry(
channel, orders.get(0).getCountry());
FreightCalculator calculator = calculatorFactory.getCalculator(rule.getCalculatorType());
BigDecimal totalFreight = calculator.calculate(new ShippingPackage(finalWeight), rule);
// 3. 按重量分摊
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 并行处理,充分利用多核CPU。
压测结果表明,单机可承受每秒500次运费计算请求,P99延迟低于50ms,满足业务需求。
