场景描述:Java商城项目,使用Drools规则引擎实现优惠券功能。优惠券有满减限制,店铺限制,商品种类限制,商品id限制,会员限制。只有会员券可以和普通券叠加一次,会员券不能和会员券叠加,会员券其实也是一种满减券,只是用户买会员才能领,两个优惠券如果同时使用,金额上的限制也都是按照原始金额计算的。用户提交订单时,查看用户所拥有的所有优惠券,自动计算出最省钱的优惠券并默认使用这个优惠券,如果两个优惠券的优惠力度相同,则优先使用快过期的优惠券,最终金额显示的是使用完优惠券的金额,用户也可以手动更改优惠券。
Drools 规则引擎在商城优惠活动中的优势
优势分析:
-
解耦业务逻辑
- 将易变的优惠规则与核心代码分离,规则存储在外部文件(
.drl)中 - 修改规则无需重新编译部署,支持热更新
- 将易变的优惠规则与核心代码分离,规则存储在外部文件(
-
可维护性高
- 规则使用声明式语法(类似自然语言),业务人员可参与编写
- 集中管理所有促销规则,避免代码分散
-
复杂规则处理能力
- 支持嵌套条件、多规则联动(如:满减 + 会员折扣 + 品类促销)
- 内置 RETE 算法高效匹配海量规则
-
动态扩展性
- 新增促销类型只需添加规则文件,无需修改主流程
- 支持规则版本管理和灰度发布
-
决策透明度
- 规则引擎自动记录触发规则,便于审计和客户解释
实现方案详解(Java 商城项目)
在Java商城项目中,使用Drools规则引擎实现优惠券功能的详细方案如下:
一、核心模型设计
// 优惠券模型
public class Coupon {
private Long id;
private String name;
private int type; // 0-普通券 1-会员券
private BigDecimal minAmount; // 满减门槛
private BigDecimal discountAmount; // 优惠金额
private Set<Long> applicableShopIds; // 店铺限制
private Set<Long> applicableCategoryIds; // 商品类目限制
private Set<Long> applicableItemIds; // 商品ID限制
private boolean forMemberOnly; // 仅会员可用
private Date expireTime; // 过期时间
// getters/setters
}
// 订单上下文(规则引擎输入)
public class OrderContext {
private BigDecimal originalAmount; // 订单原始金额
private boolean isMember; // 是否会员
private List<OrderItem> items; // 订单商品
private List<Coupon> userCoupons; // 用户持有优惠券
private List<Coupon> applicableCoupons = new ArrayList<>(); // 过滤后的可用券
private List<Coupon> bestCombo = new ArrayList<>(); // 最优组合
private BigDecimal finalAmount; // 最终金额
// getters/setters
}
// 订单商品项
public class OrderItem {
private Long itemId;
private Long categoryId;
private Long shopId;
private BigDecimal price;
// getters/setters
}
二、Drools规则实现(coupon-rules.drl)
1. 单券可用性规则
rule "Check coupon applicability"
salience 100
when
$order: OrderContext()
$coupon: Coupon() from $order.getUserCoupons()
// 基础校验
eval($coupon.getExpireTime().after(new Date())) // 未过期
eval($order.getOriginalAmount().compareTo($coupon.getMinAmount()) >= 0) // 满足金额门槛
// 会员限制校验
if ($coupon.isForMemberOnly()) then
eval($order.isMember())
end
// 店铺限制校验
if (!$coupon.getApplicableShopIds().isEmpty()) then
exists OrderItem(shopId in $coupon.getApplicableShopIds()) from $order.getItems()
end
// 类目限制校验
if (!$coupon.getApplicableCategoryIds().isEmpty()) then
exists OrderItem(categoryId in $coupon.getApplicableCategoryIds()) from $order.getItems()
end
// 商品ID限制校验
if (!$coupon.getApplicableItemIds().isEmpty()) then
exists OrderItem(itemId in $coupon.getApplicableItemIds()) from $order.getItems()
end
then
$order.getApplicableCoupons().add($coupon); // 添加可用券
end
2. 组合叠加规则
rule "Find best coupon combo"
salience 50
when
$order: OrderContext(applicableCoupons.size() > 0)
// 分组优惠券
$memberCoupons: List() from collect(Coupon(type == 1) from $order.getApplicableCoupons())
$normalCoupons: List() from collect(Coupon(type == 0) from $order.getApplicableCoupons())
// 计算最优组合
$bestDiscount: BigDecimal() from accumulate(
$c: Coupon() from $order.getApplicableCoupons(),
init(BigDecimal max = BigDecimal.ZERO;),
action(if($c.getDiscountAmount().compareTo(max) > 0) max = $c.getDiscountAmount()),
result(max)
)
then
// 实现组合策略(见下方Java策略)
CouponComboStrategy strategy = new CouponComboStrategy();
strategy.findBestCombo($order, $memberCoupons, $normalCoupons, $bestDiscount);
end
三、组合策略实现(Java)
public class CouponComboStrategy {
public void findBestCombo(OrderContext order,
List<Coupon> memberCoupons,
List<Coupon> normalCoupons,
BigDecimal bestDiscount) {
// 按优惠力度+过期时间排序
Comparator<Coupon> comparator = Comparator
.comparing(Coupon::getDiscountAmount).reversed()
.thenComparing(Coupon::getExpireTime);
memberCoupons.sort(comparator);
normalCoupons.sort(comparator);
// 候选方案
Coupon bestSingle = null;
Coupon bestMember = memberCoupons.isEmpty() ? null : memberCoupons.get(0);
Coupon bestNormal = normalCoupons.isEmpty() ? null : normalCoupons.get(0);
// 方案1:单会员券
if (bestMember != null) {
bestSingle = bestMember;
}
// 方案2:单普通券
if (bestNormal != null &&
(bestSingle == null ||
bestNormal.getDiscountAmount().compareTo(bestSingle.getDiscountAmount()) > 0)) {
bestSingle = bestNormal;
}
// 方案3:会员券+普通券组合
BigDecimal comboDiscount = BigDecimal.ZERO;
List<Coupon> combo = new ArrayList<>();
if (bestMember != null && !normalCoupons.isEmpty()) {
combo.add(bestMember);
combo.add(normalCoupons.get(0));
comboDiscount = bestMember.getDiscountAmount().add(bestNormal.getDiscountAmount());
}
// 结果比较
if (comboDiscount.compareTo(bestSingle.getDiscountAmount()) > 0) {
order.setBestCombo(combo);
order.setFinalAmount(order.getOriginalAmount().subtract(comboDiscount));
} else {
order.setBestCombo(Collections.singletonList(bestSingle));
order.setFinalAmount(order.getOriginalAmount().subtract(bestSingle.getDiscountAmount()));
}
}
}
四、服务层调用逻辑
@Service
public class CouponService {
@Autowired
private KieContainer kieContainer;
public OrderContext calculateBestCoupon(OrderContext context) {
KieSession kieSession = kieContainer.newKieSession();
kieSession.insert(context);
// 插入用户优惠券
context.getUserCoupons().forEach(kieSession::insert);
// 插入订单商品
context.getItems().forEach(kieSession::insert);
kieSession.fireAllRules();
kieSession.dispose();
return context;
}
}
五、订单处理流程
@Controller
public class OrderController {
@Autowired
private CouponService couponService;
public Order submitOrder(OrderRequest request) {
// 1. 构建订单上下文
OrderContext context = new OrderContext();
context.setOriginalAmount(calculateTotal(request.getItems()));
context.setMember(userService.isMember(request.getUserId()));
context.setItems(buildOrderItems(request));
context.setUserCoupons(couponDao.getUserCoupons(request.getUserId()));
// 2. 执行规则引擎计算
couponService.calculateBestCoupon(context);
// 3. 创建订单
Order order = new Order();
order.setCoupons(context.getBestCombo());
order.setFinalAmount(context.getFinalAmount());
orderDao.save(order);
return order;
}
// 用户手动更换优惠券
public BigDecimal recalculate(RecalculateRequest request) {
OrderContext context = buildContext(request);
return couponService.calculateBestCoupon(context).getFinalAmount();
}
}
六、关键优化点
-
规则性能优化:
- 使用
agenda-group分割规则阶段 - 对大型商品列表使用
collect替代exists
rule "Shop restriction optimization" when $coupon: Coupon(!applicableShopIds.isEmpty()) $order: OrderContext() $shopIds: Set() from collect(OrderItem(shopId != null) from $order.getItems()) then // 使用Set交集判断 end - 使用
-
组合策略扩展:
// 支持多策略模式 public interface ComboStrategy { void findBestCombo(OrderContext ctx, List<Coupon> members, List<Coupon> normals); } // 注册策略 @Component public class StrategyFactory { @Resource(name = "simpleCombo") private ComboStrategy defaultStrategy; public ComboStrategy getStrategy(int scenario) { return defaultStrategy; // 可扩展场景策略 } } -
缓存优化:
@Cacheable(value = "couponDecisions", key = "{#userId, #items.hashCode()}") public OrderContext calculateCached(Long userId, List<Item> items) { // 计算逻辑 }
七、前端交互设计
-
默认展示:
- 显示规则引擎计算出的
finalAmount - 默认选中
bestCombo中的优惠券
- 显示规则引擎计算出的
-
手动切换:
// 前端调用示例 function recalculate(couponIds) { POST /order/recalculate { items: [商品数据], couponIds: [用户选择的券ID] } } -
校验规则:
- 前端实时校验:会员券仅限会员使用
- 禁止同时选择多张会员券
- 金额门槛不足时置灰选项
八、完整流程
sequenceDiagram
participant User
participant Frontend
participant Controller
participant Service
participant Drools
participant DB
User->>Frontend: 提交订单
Frontend->>Controller: 请求创建订单
Controller->>Service: 获取用户优惠券
Service->>DB: 查询user_coupons
Controller->>Service: 执行规则计算
Service->>Drools: 传入OrderContext
Drools-->>Service: 返回最佳组合
Service->>DB: 持久化订单
Controller->>Frontend: 返回订单结果
Frontend->>User: 展示优惠后金额
User->>Frontend: 手动更换优惠券
Frontend->>Controller: 请求重算金额
Controller->>Service: 执行重算逻辑
Service->>Drools: 传入新券组合
Drools-->>Service: 返回新金额
Controller->>Frontend: 返回新金额
Frontend->>User: 更新界面显示
九、注意事项
-
金额精度:
- 使用
BigDecimal进行精确计算 - 设置统一的
MathContext舍入规则
- 使用
-
规则热更新:
// 监控规则文件变化 @Scheduled(fixedRate = 5000) public void reloadRules() { if (ruleFileChanged()) { kieContainer = kieServices.newKieContainer(kieContainer.getReleaseId()); } } -
叠加规则扩展:
- 在
CouponComboStrategy中预留扩展点 - 通过
@Rule注解实现自定义策略
- 在
-
防御性编程:
// 在组合计算中添加保护 if (comboDiscount.compareTo(order.getOriginalAmount()) > 0) { comboDiscount = order.getOriginalAmount(); // 避免负金额 }
该方案通过Drools实现核心业务规则解耦,结合策略模式处理复杂优惠逻辑,在保证灵活性的同时提供最佳用户体验。
附:可视化工具Drools Workbench
可以参考这位大佬 @java手拉手 的文章 Drools workbench代码中来整合数据,执行drools workbench中的规则。 级别salience - 掘金