解析Java商城项目如何使用Drools规则引擎整合运费计算

117 阅读3分钟

这篇文章是对之前已有功能的增强,之前是使用Drools规则引擎实现优惠券计算的功能,这次增加运费计算。原文见

详解Java商城项目如何使用Drools规则引擎实现优惠活动Drools 规则引擎在商城优惠活动中的优势 优势分析: 解 - 掘金

场景描述:运费的计算也可以结合到Drools规则引擎中,提交订单前需要计算出要支付的运费和原商品费用和优惠的金额,最后得出总费用,优惠券的满减条件、百分比优惠等都不计算运费,运费具体收多少怎么收由商户自己设置,运费的支付方式我们采用的是用户交的钱直接给商户而不进行物流公司分账,商户自行与合作的物流公司处理快递费用发货等事务。

具体实现方案:

一、模型扩展设计

// 新增运费规则模型
public class ShippingRule {
    private Long shopId;          // 关联店铺ID
    private BigDecimal freeThreshold; // 包邮门槛(订单金额)
    private BigDecimal defaultFee;    // 默认运费
    private Set<String> regions;     // 支持配送区域
    // getters/setters
}

// 订单上下文增强
public class OrderContext {
    // 原有字段保持不变...
    private String shippingAddress;  // 收货地址(用于区域运费)
    private BigDecimal shippingFee = BigDecimal.ZERO; // 计算得出的运费
    private BigDecimal itemTotalAmount; // 商品原始总金额(不含运费)
    private BigDecimal discountAmount; // 优惠券减免总额
    // getters/setters
}

二、Drools规则分层实现(新增运费规则组)

// 运费计算规则组(优先级最高)
rule "Calculate shipping fee by region"
salience 200 // 最高优先级
agenda-group "shipping"
when
    $order: OrderContext($address: shippingAddress, $shopId: shopId)
    $rule: ShippingRule(shopId == $shopId, regions contains $address)
then
    // 区域匹配时使用该规则
    if ($order.getItemTotalAmount().compareTo($rule.getFreeThreshold()) >= 0) {
        $order.setShippingFee(BigDecimal.ZERO); // 满足包邮
    } else {
        $order.setShippingFee($rule.getDefaultFee());
    }
end

rule "Apply default shipping rule"
salience 190
agenda-group "shipping"
when
    $order: OrderContext(shopId != null, shippingFee == 0)
    not ShippingRule(shopId == $order.getShopId()) // 无专属规则
    $globalRule: ShippingRule(shopId == 0) // 全局默认规则
then
    if ($order.getItemTotalAmount().compareTo($globalRule.getFreeThreshold()) >= 0) {
        $order.setShippingFee(BigDecimal.ZERO);
    } else {
        $order.setShippingFee($globalRule.getDefaultFee());
    }
end

三、组合计算策略增强

public class CouponComboStrategy {
    public void findBestCombo(OrderContext order, ...) {
        // 原有优惠组合计算逻辑不变...
        
        // 最终金额计算增强
        BigDecimal discountedItems = order.getItemTotalAmount()
                                  .subtract(order.getDiscountAmount());
                                  
        // 关键点:运费独立计算,不参与优惠减免
        BigDecimal finalAmount = discountedItems.add(order.getShippingFee());
        
        // 避免负金额
        if (finalAmount.compareTo(BigDecimal.ZERO) < 0) {
            finalAmount = BigDecimal.ZERO;
        }
        
        order.setFinalAmount(finalAmount);
    }
}

四、服务层调用流程优化

@Service
public class OrderService {
    public OrderContext calculateOrder(OrderContext context) {
        KieSession kieSession = kieContainer.newKieSession();
        
        // 1. 插入运费规则(从DB加载)
        shippingRuleService.getRules().forEach(kieSession::insert);
        
        // 2. 激活运费规则组
        kieSession.getAgenda().getAgendaGroup("shipping").setFocus();
        kieSession.insert(context);
        kieSession.fireAllRules(); // 先计算运费

        // 3. 激活优惠券规则组
        kieSession.getAgenda().getAgendaGroup("coupons").setFocus();
        context.getUserCoupons().forEach(kieSession::insert);
        context.getItems().forEach(kieSession::insert);
        kieSession.fireAllRules(); // 再计算优惠
        
        kieSession.dispose();
        return context;
    }
}

五、运费规则与优惠券的协同要点

  1. 计算顺序

    • 先计算商品总金额 itemTotalAmount
    • 再计算运费(基于原始商品金额判断包邮)
    • 最后计算优惠券减免
  2. 金额关系公式

    最终支付 = (商品总金额 - 优惠券减免) + 运费
    
  3. 规则隔离原则

    • 优惠券规则不读取运费字段
    • 运费规则不读取优惠券字段
    • 包邮判断仅基于商品原始金额

六、多店铺运费处理方案

// 多店铺运费聚合规则
rule "Calculate multi-shop shipping"
salience 180
agenda-group "shipping"
when
    $order: OrderContext(multiShop: true)
    $shops: List() from collect(OrderItem(shopId != null) from $order.getItems())
then
    BigDecimal totalFee = BigDecimal.ZERO;
    for (Long shopId : $shops) {
        ShippingRule rule = shippingService.getRule(shopId);
        BigDecimal shopAmount = calculateShopAmount(shopId); // 计算该店商品金额
        
        if (shopAmount.compareTo(rule.getFreeThreshold()) < 0) {
            totalFee = totalFee.add(rule.getDefaultFee());
        }
    }
    $order.setShippingFee(totalFee);
end

七、前端展示增强

// 订单预览响应示例
{
  "itemTotal": 299.00,    // 商品总额
  "discount": -50.00,     // 优惠券减免
  "shippingFee": 15.00,   // 运费
  "finalAmount": 264.00,  // 实付金额
  "feeDetail": {
    "freeThreshold": 199, // 包邮提示
    "unreachableShops": [ // 未达包邮店铺
      {"shopId": 102, "needAmount": 50.00}
    ]
  }
}

八、运规则热更新机制

// 监听规则变化
@Scheduled(fixedDelay = 30000)
public void refreshShippingRules() {
    if (shippingRuleChanged()) {
        KieServices ks = KieServices.Factory.get();
        KieRepository kr = ks.getRepository();
        KieFileSystem kfs = ks.newKieFileSystem();
        
        // 动态加载新规则
        kfs.write("src/main/resources/shipping.drl", 
                  loadNewShippingRules());
        
        ks.newKieBuilder(kfs).buildAll();
        kieContainer.updateTo(kr.getDefaultReleaseId());
    }
}

九、异常处理增强

// 运费计算兜底规则
rule "Shipping fee fallback"
salience -10 // 最低优先级
when
    $order: OrderContext(shippingFee == null)
then
    $order.setShippingFee(new BigDecimal("15.00")); // 默认运费
    logger.warn("使用兜底运费规则,订单ID:"+$order.getId());
end

十、商户结算流程

graph LR
    A[用户支付] --> B[平台收款]
    B --> C[即时结算商品款]
    B --> D[单独结算运费]
    C --> E[商户账户]
    D --> E
    E --> F[商户自行支付物流]

关键改进总结

  1. 通过agenda-group实现规则执行顺序控制
  2. 运费计算完全独立于优惠体系
  3. 包邮判断基于商品原始金额
  4. 多店铺场景采用运费聚合策略
  5. 前端清晰展示运费构成
  6. 保留商户与物流公司的独立结算通道

特别注意:优惠券的满减条件(如"满200减30")仅针对商品金额,运费部分不参与满减计算。会员券叠加逻辑保持不变,但叠加后的总优惠金额仍然只抵扣商品金额部分。