优惠券使用规则引擎来计算优惠 (drools)

发布时间 2023-05-24 14:47:42作者: IT随笔

电商的促销花样越来越多,规则也也越来越复杂,因此,规则的频繁变更可能会带来频繁的版本开发上线,因此,业务希望能够快速上线,这就要求产品能够做到不修改代码快速上线。

平心而论,优惠券目前的几种玩法已经比较固定,通常都是通用券,折扣券,满减券,满赠券,即使不用规则引擎,大部分优惠券的设计都能够支撑业务侧的需求。对于业务侧的一些比较复杂的规则, 例如叠加规则,互斥规则,通常也是在优惠券可配置的一部分,在优惠券的价格计算中,已经实现了互斥,叠加。

其实,优惠券的计算逻辑非常复杂,尤其是可以使用多张优惠券的情况下,还要考虑不同级别的优惠券,在规则引擎中去实现,还是非常麻烦的。此外,由于 drools 的表达能力只能是 when-then 的方式, 并没有实现完整的编程语言的范式,因此,drools 脚本中很难实现复杂的业务逻辑。

我们还对 groovy 脚本进行了调研,在下一篇文章中,我们用 groovy 来实现优惠券的业务逻辑,在优惠券的场景中,能够做到比 drools 更加灵活。

规则引擎

规则引擎的核心包括两部分:

  1. 规则脚本;
  2. 规则脚本的编译,解释执行;

通常,规则脚本都是独立的语言实现,大部分规则引擎都是使用 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 的使用流程是:

  1. 获取 kieSession;
  2. 将变量插入 kieSession 中
  3. 调用 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.EnquiryResult

global 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))
then
    System.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()))
then
    System.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 脚本,测试无误后,让后台代码重新加载即可,也就实现了业务规则的快速部署。