电商的促销花样越来越多,规则也也越来越复杂,因此,规则的频繁变更可能会带来频繁的版本开发上线,因此,业务希望能够快速上线,这就要求产品能够做到不修改代码快速上线。
平心而论,优惠券目前的几种玩法已经比较固定,通常都是通用券,折扣券,满减券,满赠券,即使不用规则引擎,大部分优惠券的设计都能够支撑业务侧的需求。对于业务侧的一些比较复杂的规则, 例如叠加规则,互斥规则,通常也是在优惠券可配置的一部分,在优惠券的价格计算中,已经实现了互斥,叠加。
其实,优惠券的计算逻辑非常复杂,尤其是可以使用多张优惠券的情况下,还要考虑不同级别的优惠券,在规则引擎中去实现,还是非常麻烦的。此外,由于 drools 的表达能力只能是 when-then
的方式, 并没有实现完整的编程语言的范式,因此,drools 脚本中很难实现复杂的业务逻辑。
我们还对 groovy 脚本进行了调研,在下一篇文章中,我们用 groovy 来实现优惠券的业务逻辑,在优惠券的场景中,能够做到比 drools 更加灵活。
规则引擎
规则引擎的核心包括两部分:
- 规则脚本;
- 规则脚本的编译,解释执行;
通常,规则脚本都是独立的语言实现,大部分规则引擎都是使用 java 的开源库 antlr
。antlr 是开源的语法解析器,规则脚本语法虽然简单,但也是一门独立的语言,因此,语法解析,词法解析是 必须要有的,此外,大部分规则引擎都可以做到和 JVM 相互调用,这部分的处理应该还是比较复杂的,有兴趣的可以研究下源代码。
drools 规则引擎
drools 规则引擎主要是应用于风控、反欺诈、智能营销、网点监控、智能核保、业务流自动化等场景中,核心是将业务的逻辑代码由 java 代码移到 drools 脚本,如果需要修改业务逻辑,只需要修改 drools 脚本,而不需要修改后台代码。
通常在实际中,我们把脚本保存在数据库中,大部分时候,不需要修改 drools 脚本。如果业务逻辑发生变化,可以通过修改 drools 脚本,然后 java 代码重新从数据库中 load 脚本,这样,就实现了
通过将业务逻辑代码与后台代码的分离,做到了可以随时修改
一个 drools 规则引擎的基本流程是:
rule rule001
when条件
then执行结果
end
drools 有几个重要的概念,分别是:
Facts
drools 中的 Facts,可以简单的理解为输入
Working memory
working memory 可以简单的理解为 drools 的运行环境
LHS RHS
LHS:条件部分,即 When
RHS:结果部分,即 then
与 java 的交互
drools 的强大之处在于,可以和 java 深度结合,引用 java 的代码,调用 java 的方法。规则在执行的过程中,经常需要与 java 交互,传递参数,判断条件,更新结果等。
KieServices, kieSession, KieContainer, KieFileSystem, KieModule
- KieServices: kie 整体的入口,可以用来创建 Container,resource,fileSystem 等
- KieContainer:KieContainer 就是一个 KieBase 的容器,通过 KieContainer 来获取具体的 KieSession
- KieFileSystem:Kie 的虚拟文件系统,包括资源和组织结构,drools 脚本可以通过 KieFileSystem 来加载
- KieModule:是一个包含了多个 kiebase 定义的容器。
- KieRepository:是一个 KieModule 的仓库,包含了所有的 KieModule 描述,用一个 ReleaseId 做区分
概念很多,很难理解,我想这也是为什么很多人说 drools 很重的原因吧。
java 使用流程
其实,我们暂时不用关心这么多宏观的概念,先从 hello world 搞起。
通常,java 的使用流程是:
- 获取 kieSession;
- 将变量插入 kieSession 中
- 调用
kieSession.fireAllRules()
假设我们把 drools 脚本放在 resource/rules 目录下,获取 kieSession 的代码如下:
private static Resource[] getRuleFiles() throws IOException {ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();return resourcePatternResolver.getResources("classpath*:rules/" + "**/*.drl");}private static KieSession getSession() throws Exception {KieServices kieServices = KieServices.Factory.get();KieFileSystem kfs = kieServices.newKieFileSystem();for (Resource file : getRuleFiles()) {log.info("rule file: " + file.getFilename());try {kfs.write(ResourceFactory.newClassPathResource("rules/" + file.getFilename(), "UTF-8"));} catch (Exception e) {e.printStackTrace();}}KieBuilder kieBuilder = kieServices.newKieBuilder(kfs).buildAll();Results results = kieBuilder.getResults();if (results.hasMessages(Message.Level.ERROR)) {for (Message msg : results.getMessages()) {log.error("drools script error info : " + msg.getText());}throw new Exception("drools script error");}return kieServices.newKieContainer(KieServices.Factory.get().getRepository().getDefaultReleaseId()).newKieSession();}
执行 rule 的代码:
KieSession kieSession = getSession();kieSession.insert(order);kieSession.insert(coupon);kieSession.insert(result);int hit = kieSession.fireAllRules(); // hit 是所有规则被命中的规则数
代码讲解
代码基于 spring boot,该项目仅为演示项目,因此,并未涉及数据库部分,在实际中可以细化这部分实现。
代码已开源:https://github.com/guotie/drools
核心流程
优惠券的核心在于计算价格的接口,也就是常说的询价接口。
因此,我们写了这么几个 drools 脚本:
- 折扣类优惠计算脚本
- 满减类优惠计算脚本
- 满赠类优惠计算脚本
- 支付类优惠计算脚本
- 其他
例如,折扣类优惠计算脚本的内容如下:
package com.mall.coupon.drools.rules;// 折扣型import com.mall.coupon.drools.model.Coupon
import com.mall.coupon.drools.model.Order
import com.mall.coupon.drools.model.OrderItem
import com.mall.coupon.drools.model.EnquiryResultglobal com.mall.coupon.drools.service.CouponBatchService couponBatchService
global com.mall.coupon.drools.service.UserCouponService userCouponService
//global com.mall.coupon.drools.service.UserCouponService userCouponService
global com.mall.coupon.drools.service.UserService userService// 折扣类优惠券// order 对整个订单打折
rule "rule-discount-order"
when$result: EnquiryResult()$order: Order()$coupon: Coupon(couponType == "5" && subCouponType == "1" &&(minBuyAmount == 0 || $order.totalAmount >= minBuyAmount))
thenSystem.out.println("命中 discount-order");$result.setTotalDiscount($order.getTotalAmount() * $coupon.getNominal() / 100);
end// sku 对特定的sku商品打折
rule "rule-discount-sku"
when$item: OrderItem()$result: EnquiryResult()$coupon: Coupon(couponType == "5" && subCouponType == "2" &&(minBuyAmount == 0 || $item.totalAmount >= minBuyAmount) &&couponBatchService.skuUsable($coupon.getCouponBatchCode(), $item.getProductSkuId()))
thenSystem.out.println("coupon code: " + $coupon.getCouponBatchCode());System.out.println("couponBatchService: " + couponBatchService);System.out.println("命中 discount-sku");$result.setTotalDiscount($item.getTotalAmount() * (100 - $coupon.getNominal()) / 100);
end
总结
如果我们需要新增一种优惠券,那么我们只需要新增该类型优惠券的 drools 脚本,测试无误后,让后台代码重新加载即可,也就实现了业务规则的快速部署。