摘要:反向海淘运费计算涉及首重续重、体积重、多国家差异化、多渠道路由。本文使用Java策略模式和Drools规则引擎实现灵活可配置的计费系统,支持实时计算和批量试算。
一、需求建模
运费规则示例:
美国云途专线:首重0.5kg/85元,续重0.5kg/25元,按实际重量计费
美国EMS:首重0.5kg/110元,续重0.5kg/35元,按体积重(长宽高/5000)取大
加拿大专线:首重1kg/120元,续重0.5kg/30元
二、策略模式实现
java
// 计费策略接口
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"));
}
}
三、规则引擎配置运费模板
java
// Drools规则文件 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
四、合并发货与拼单分摊
java
@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<FreightShare> 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延迟低于50ms。