腾讯位置服务 —— 预估订单路线金额(使用Drools规则引擎处理)
目录
引言:
费用规则:
1.里程费
2.等候费
3.远途费
一.规则引擎Drools:
规则引擎构成:
规则引擎执行过程:
KIE介绍:
Drools语法:
(1)规则体语法结构:
(2)注解:
(3)Pattern模式匹配:
(4)比较操作符:
(5)Drools内置方法:
【1】修改方法(update):
【2】新增方法(insert):
【3】删除方法(retract):
(6)规则属性 attributes:
【1】salience属性
【2】no-loop属性
(7)全局变量 global:
二.预估订单路线金额:
封装微服务接口
三.规则接口优化:
本博客参考乐尚代驾,视频链接:【Java项目【乐尚代驾】,微信小程序+最新技术栈,java微服务项目】
https://www.bilibili.com/video/BV1nW421R7qJ?p=40&vd_source=6d3b71d8e280ffe05e3d66d60a042d65
引言:
乘客在选完代驾起始点后,页面显示预估的订单金额,我们需要根据代驾的费用计算规则,计算代驾费用。
费用规则:
代驾费用 = 里程费 + 等候费 + 远途费, 等候费当前为0,后续根据时间情况获取
说明:费用规则可能根据实际业务情况,随时会更新调整
1.里程费
里程费 = 基础里程费 + 超出起步里程费
时间段 | 基础里程 | 收费 | 超出起步里程费 |
---|---|---|---|
00:00 - 06:00 | 3公里 | 19元 | 4元/1公里 |
06:00 - 22:00 | 5公里 | 19元 | 3元/1公里 |
2.等候费
司机达代驾起始点后,可免费等候10分钟,超出后每1分钟收取1
规则 | 收费 |
---|---|
等候10分钟后 | 1元/1分钟 |
3.远途费
订单行程超出12公里后每公里1元收取远途费
规则 | 收费 |
---|---|
订单行程超出12公里后 | 1元/1公里 |
一.规则引擎Drools:
对于不经常变化的业务,我们通常是硬编码到程序中。但是经常变化的业务,我们就得把业务流程从代码中剥离出来,我们怎么从程序中剥离出去?这里就需要用到规则引擎了。
规则引擎可以做到把算法剥离出程序,你可以保存到TXT文件或者数据库表里面,用的时候再加载回程序。虽然加载回来的算法是字符串,但是规则引擎有办法运行这些字符串。例如遇到雨雪天气,代驾费用就得上调一些。如果是业务淡季,代驾费用可以下调一点。既然代驾费的算法经常要变动,我们肯定不能把算法写死到程序里面。我们要把算法从程序中抽离,保存到MySQL里面。将来我们要改动计费算法,直接添加一个新纪录就行了,原有记录不需要删改,程序默认使用最新的计费方式。
规则引擎,全称为业务规则管理系统,英文名为BRMS(即Business Rule Management System)。规则引擎的主要思想是将应用程序中的业务决策部分分离出来,并使用预定义的语义模块编写业务决策(业务规则),由用户或开发者在需要时进行配置、管理。
需要注意的是规则引擎并不是一个具体的技术框架,而是指的一类系统,即业务规则管理系统。目前市面上具体的规则引擎产品有:drools、VisualRules、iLog等。
规则引擎实现了将业务决策从应用程序代码中分离出来,接收数据输入,解释业务规则,并根据业务规则做出业务决策。规则引擎其实就是一个输入输出平台。
系统中引入规则引擎后,业务规则不再以程序代码的形式驻留在系统中,取而代之的是处理规则的规则引擎,业务规则存储在规则库中,完全独立于程序。业务人员可以像管理数据一样对业务规则进行管理,比如查询、添加、更新、统计、提交业务规则等。业务规则被加载到规则引擎中供应用系统调用。
Drools 是一个开源的 业务规则管理系统(BRMS, Business Rules Management System),基于 Java 编写,主要用于实现 规则引擎(Rule Engine)。它的核心功能是 将业务规则与应用程序代码解耦,以规则脚本的形式存放在文件或特定的存储介质中(例如存放在数据库中),使得业务规则的变更不需要修改项目代码、重启服务器就可以在线上环境立即生效。
drools官网地址:Drools - Drools - Business Rules Management System (Java™, Open Source)
drools源码下载地址:https://github.com/kiegroup/drools
首先引入依赖:
<dependency><groupId>org.drools</groupId><artifactId>drools-core</artifactId><version>8.41.0.Final</version>
</dependency>
<dependency><groupId>org.drools</groupId><artifactId>drools-compiler</artifactId><version>8.41.0.Final</version>
</dependency>
<dependency><groupId>org.drools</groupId><artifactId>drools-decisiontables</artifactId><version>8.41.0.Final</version>
</dependency>
<dependency><groupId>org.drools</groupId><artifactId>drools-mvel</artifactId><version>8.41.0.Final</version>
</dependency>
随后编写Drools配置类:
说明:
定义了一个
KieContainer
的Spring Bean
,KieContainer
用于通过加载应用程序的/resources
文件夹下的规则文件来构建规则引擎。创建
KieFileSystem
实例并配置规则引擎并从应用程序的资源目录加载规则的DRL
文件。使用
KieBuilder
实例来构建drools
模块。我们可以使用KieSerive单例实例来创建KieBuilder
实例。最后,使用
KieService
创建一个KieContainer
并将其配置为spring bean
import org.kie.api.KieServices;
import org.kie.api.builder.*;
import org.kie.api.runtime.KieContainer;
import org.kie.api.runtime.KieSession;
import org.kie.internal.io.ResourceFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** 规则引擎配置类*/
@Configuration
public class DroolsConfig {private static final KieServices kieServices = KieServices.Factory.get();//制定规则文件的路径private static final String RULES_CUSTOMER_RULES_DRL = "rules/order.drl";@Beanpublic KieContainer kieContainer() {//获得Kie容器对象KieFileSystem kieFileSystem = kieServices.newKieFileSystem();kieFileSystem.write(ResourceFactory.newClassPathResource(RULES_CUSTOMER_RULES_DRL));KieBuilder kieBuilder = kieServices.newKieBuilder(kieFileSystem);kieBuilder.buildAll();KieModule kieModule = kieBuilder.getKieModule();KieContainer kieContainer = kieServices.newKieContainer(kieModule.getReleaseId());return kieContainer;}}
随后我们就要编写规则定义文件:
DRL(Drools Rule Language)文件 是 Drools 规则引擎专用的规则定义文件,使用 声明式语法 编写业务规则,扩展名为 .drl
。它允许将业务逻辑(如 if-then
规则)从 Java 代码中分离出来,实现动态可配置的规则管理。
假设我们现在有这样一规则:
某电商平台的促销活动,活动规则是根据⽤户购买订单的⾦额给⽤户送相应的积分,购买的越多送的积分越多用户购买的金额和对应送多少积分的规则如下:
规则编号 | 订单金额 | 奖励积分 |
---|---|---|
1 | 100元以下 | 不加分 |
2 | 100元 - 500元 | 加100分 |
3 | 500元 - 1000元 | 加500分 |
4 | 1000元以上 | 加1000分 |
创建规则文件resources/rules/order.drl :
//订单积分规则
package com.eleven.drools.order
import com.eleven.drools.model.Order//规则一:100元以下 不加分
rule "order_rule_1"when$order:Order(amout < 100)then$order.setScore(0);System.out.println("成功匹配到规则一:100元以下 不加分");
end//规则二:100元 - 500元 加100分
rule "order_rule_2"when$order:Order(amout >= 100 && amout < 500)then$order.setScore(100);System.out.println("成功匹配到规则二:100元 - 500元 加100分");
end//规则三:500元 - 1000元 加500分
rule "order_rule_3"when$order:Order(amout >= 500 && amout < 1000)then$order.setScore(500);System.out.println("成功匹配到规则三:500元 - 1000元 加500分");
end//规则四:1000元以上 加1000分
rule "order_rule_4"when$order:Order(amout >= 1000)then$order.setScore(1000);System.out.println("成功匹配到规则四:1000元以上 加1000分");
end
随后编写测试类:
步骤详情:
- 首先从上面自定义的Kie容器对象获取会话对象。
- 将对象放入工作内存中,激活规则后,Drools会自动进行规则匹配,匹配结束后关闭会话。
- 输出结果判断是否成功匹配规则。
import org.junit.jupiter.api.Test;
import com.eleven.drools.model.Order;
import org.kie.api.runtime.KieContainer;
import org.kie.api.runtime.KieSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;@SpringBootTest
class DroolsDemosApplicationTests {@Autowiredprivate KieContainer kieContainer;@Testpublic void test(){//从Kie容器对象中获取会话对象KieSession session = kieContainer.newKieSession();//Fact对象,事实对象Order order = new Order();order.setAmout(1300);//将Order对象插入到工作内存中session.insert(order);//激活规则,由Drools框架自动进行规则匹配,如果规则匹配成功,则执行当前规则session.fireAllRules();//关闭会话session.dispose();System.out.println("订单金额:" + order.getAmout() +",添加积分:" + order.getScore());}}
通过上面的入门案例我们可以发现,使用drools规则引擎主要工作就是编写规则文件,在规则文件中定义跟业务相关的业务规则。规则定义好后就需要调用drools提供的API将数据提供给规则引擎进行规则模式匹配,规则引擎会执行匹配成功的规则并将计算的结果返回给我们。
规则引擎构成:
drools规则引擎由以下三部分构成:
-
Working Memory(工作内存)
-
Rule Base(规则库)
-
Inference Engine(推理引擎)
其中Inference Engine(推理引擎)又包括:
-
Pattern Matcher(匹配器) 具体匹配哪一个规则,由这个完成
-
Agenda(议程)
-
Execution Engine(执行引擎)
如下图所示:
- Working Memory:工作内存,drools规则引擎会从Working Memory中获取数据并和规则文件中定义的规则进行模式匹配,所以我们开发的应用程序只需要将我们的数据插入到Working Memory中即可,例如本案例中我们调用kieSession.insert(order)就是将order对象插入到了工作内存中。
- Fact:事实,是指在drools 规则应用当中,将一个普通的JavaBean插入到Working Memory后的对象就是Fact对象,例如本案例中的Order对象就属于Fact对象。Fact对象是我们的应用和规则引擎进行数据交互的桥梁或通道。
- Rule Base:规则库,我们在规则文件中定义的规则都会被加载到规则库中。
- Pattern Matcher:匹配器,将Rule Base中的所有规则与Working Memory中的Fact对象进行模式匹配,匹配成功的规则将被激活并放入Agenda(议程)中。
- Agenda:议程,用于存放通过匹配器进行模式匹配后被激活的规则。
- Execution Engine:执行引擎,执行Agenda中被激活的规则。
规则引擎执行过程:
KIE介绍:
Drools语法:
在使用 Drools 时非常重要的一个工作就是编写规则文件,通常规则文件的后缀为 .drl 。
drl 是 Drools Rule Language 的缩写。在规则文件中编写具体的规则内容。
一套完整的规则文件内容构成如下:
关键字 | 描述 |
---|---|
package | 包名,只限于逻辑上的管理,同一个包名下的查询或者函数可以直接调用 |
import | 用于导入类或者静态方法 |
global | 全局变量 |
function | 自定义函数 |
query | 查询 |
rule end | 规则体 |
Drools支持的规则文件,除了drl形式,还有Excel文件类型的。
(1)规则体语法结构:
规则体是规则文件内容中的重要组成部分,是进行业务规则判断、处理业务结果的部分。
规则体语法结构如下:
rule "ruleName"attributeswhenLHS thenRHS end
- rule:关键字,表示规则开始,参数为规则的唯一名称。
- attributes:规则属性,是rule与when之间的参数,为可选项。
- when:关键字,后面跟规则的条件部分。
- LHS(Left Hand Side):是规则的条件部分的通用名称。它由零个或多个条件元素组成。如果LHS为空,则它将被视为始终为true的条件元素。 (左手边)
- then:关键字,后面跟规则的结果部分。
- RHS(Right Hand Side):是规则的后果或行动部分的通用名称。 (右手边)
- end:关键字,表示一个规则结束。
(2)注解:
在drl形式的规则文件中使用注释和Java类中使用注释一致,分为单行注释和多行注释。
单行注释用"//"进行标记,多行注释以"/"开始,以"/"结束。如下示例:
//规则rule1的注释,这是一个单行注释
rule "rule1"whenthenSystem.out.println("rule1触发");
end/*
规则rule2的注释,
这是一个多行注释
*/
rule "rule2"whenthenSystem.out.println("rule2触发");
end
(3)Pattern模式匹配:
前面我们已经知道了Drools中的匹配器可以将Rule Base中的所有规则与Working Memory中的Fact对象进行模式匹配,那么我们就需要在规则体的LHS部分定义规则并进行模式匹配。LHS部分由一个或者多个条件组成,条件又称为pattern。
pattern的语法结构为:绑定变量名:Object(Field约束)
其中绑定变量名可以省略,通常绑定变量名的命名一般建议以 $ 开始。如果定义了绑定变量名,就可以在规则体的RHS部分使用此绑定变量名来操作相应的Fact对象。Field约束部分是需要返回true或者false的0个或多个表达式。
例如我们的入门案例中:【$变量名:对象名(条件)】
//规则二:100元 - 500元 加100分 rule "order_rule_2"when$order:Order(amout >= 100 && amout < 500)then$order.setScore(100);System.out.println("成功匹配到规则二:100元 - 500元 加100分"); end
通过上面的例子我们可以知道,匹配的条件为:
- 工作内存中必须存在Order这种类型的Fact对象-----类型约束
- Fact对象的amout属性值必须大于等于100------属性约束
- Fact对象的amout属性值必须小于500------属性约束
以上条件必须同时满足当前规则才有可能被激活。
(4)比较操作符:
Drools提供的比较操作符,如下表:
符号 | 说明 |
---|---|
< | 小于 |
> | 大于 |
>= | 大于等于 |
<= | 小于等于 |
== | 等于 |
!= | 不等于 |
contains | 检查一个Fact对象的某个属性值是否包含一个指定的对象值 |
not contains | 检查一个Fact对象的某个属性值是否不包含一个指定的对象值 |
memberOf | 判断一个Fact对象的某个属性是否在一个或多个集合中 |
not memberOf | 判断一个Fact对象的某个属性是否不在一个或多个集合中 |
matches | 判断一个Fact对象的属性是否与提供的标准的Java正则表达式进行匹配 |
not matches | 判断一个Fact对象的属性是否不与提供的标准的Java正则表达式进行匹配 |
前6个比较操作符和Java中的完全相同。
(5)Drools内置方法:
规则文件的RHS
部分的主要作用是通过插入,删除或修改工作内存中的Fact数据,来达到控制规则引擎执行的目的。Drools提供了一些方法可以用来操作工作内存中的数据,操作完成后规则引擎会重新进行相关规则的匹配,原来没有匹配成功的规则在我们修改数据完成后有可能就会匹配成功了。
【1】修改方法(update):
update方法的作用是更新工作内存中的数据,并让相关的规则重新匹配。 (要避免死循环)
参数:
//Fact对象,事实对象
Order order = new Order();
order.setAmout(30);
规则:
//规则一:100元以下 不加分
rule "order_rule_1"when$order:Order(amout < 100)then$order.setAmout(150);update($order) //update方法用于更新Fact对象,会导致相关规则重新匹配System.out.println("成功匹配到规则一:100元以下 不加分");
end//规则二:100元 - 500元 加100分
rule "order_rule_2"when$order:Order(amout >= 100 && amout < 500)then$order.setScore(100);System.out.println("成功匹配到规则二:100元 - 500元 加100分");
end
在更新数据时需要注意防止发生死循环。
【2】新增方法(insert):
insert方法的作用是向工作内存中插入数据,并让相关的规则重新匹配。
//规则一:100元以下 不加分
rule "order_rule_1"when$order:Order(amout < 100)thenOrder order = new Order();order.setAmout(130);insert(order); //insert方法的作用是向工作内存中插入Fact对象,会导致相关规则重新匹配System.out.println("成功匹配到规则一:100元以下 不加分");
end//规则二:100元 - 500元 加100分
rule "order_rule_2"when$order:Order(amout >= 100 && amout < 500)then$order.setScore(100);System.out.println("成功匹配到规则二:100元 - 500元 加100分");
end
【3】删除方法(retract):
retract方法的作用是删除工作内存中的数据,并让相关的规则重新匹配。
//规则一:100元以下 不加分
rule "order_rule_1"when$order:Order(amout < 100)thenretract($order) //retract方法的作用是删除工作内存中的Fact对象,会导致相关规则重新匹配System.out.println("成功匹配到规则一:100元以下 不加分");
end
(6)规则属性 attributes:
前面我们已经知道了规则体的构成如下:
rule "ruleName"attributeswhenLHSthenRHS
end
本章节就是针对规则体的attributes属性部分进行讲解。Drools中提供的属性如下表(部分属性):
属性名 | 说明 |
---|---|
salience | 指定规则执行优先级 |
dialect | 指定规则使用的语言类型,取值为java和mvel |
enabled | 指定规则是否启用 |
date-effective | 指定规则生效时间 |
date-expires | 指定规则失效时间 |
activation-group | 激活分组,具有相同分组名称的规则只能有一个规则触发 |
agenda-group | 议程分组,只有获取焦点的组中的规则才有可能触发 |
timer | 定时器,指定规则触发的时间 |
auto-focus | 自动获取焦点,一般结合agenda-group一起使用 |
no-loop | 防止死循环 |
重点说一下我们项目需要使用的属性
【1】salience属性
salience属性用于指定规则的执行优先级,取值类型为Integer。数值越大越优先执行。每个规则都有一个默认的执行顺序,如果不设置salience属性,规则体的执行顺序为由上到下。
可以通过创建规则文件salience.drl来测试salience属性,内容如下:
package com.orderrule "rule_1"salience 9wheneval(true)thenSystem.out.println("规则rule_1触发");
endrule "rule_2"salience 10wheneval(true)thenSystem.out.println("规则rule_2触发");
endrule "rule_3"salience 8wheneval(true)thenSystem.out.println("规则rule_3触发");
end
通过控制台可以看到,规则文件执行的顺序是按照我们设置的salience值由大到小顺序执行的。
建议在编写规则时使用salience属性明确指定执行优先级。
【2】no-loop属性
no-loop属性用于防止死循环,当规则通过update之类的函数修改了Fact对象时,可能使当前规则再次被激活从而导致死循环。取值类型为Boolean,默认值为false,测试步骤如下:
编写规则文件/resources/rules/activationgroup.drl:
//订单积分规则
package com.order
import com.atguigu.drools.model.Order//规则一:100元以下 不加分
rule "order_rule_1"no-loop true //防止陷入死循环when$order:Order(amout < 100)then$order.setScore(0);update($order)System.out.println("成功匹配到规则一:100元以下 不加分");
end
通过控制台可以看到,由于我们没有设置no-loop属性的值,所以发生了死循环。接下来设置no-loop的值为true再次测试则不会发生死循环。
(7)全局变量 global:
global关键字用于在规则文件中定义全局变量,它可以让应用程序的对象在规则文件中能够被访问。可以用来为规则文件提供数据或服务。
语法结构为:global 对象类型 对象名称
在使用global定义的全局变量时有两点需要注意:
- 如果对象类型为包装类型时,在一个规则中改变了global的值,那么只针对当前规则有效,对其他规则中的global不会有影响。可以理解为它是当前规则代码中的global副本,规则内部修改不会影响全局的使用。
- 如果对象类型为集合类型或JavaBean时,在一个规则中改变了global的值,对java代码和所有规则都有效。
订单Order:
package com.atguigu.drools.model;public class Order {private double amout;public double getAmout() {return amout;}public void setAmout(double amout) {this.amout = amout;}}
积分Integral:
package com.atguigu.drools.model;public class Integral {private double score;public double getScore() {return score;}public void setScore(double score) {this.score = score;}
}
规则文件:
//订单积分规则
package com.order
import com.atguigu.drools.model.Orderglobal com.atguigu.drools.model.Integral integral;//规则一:100元以下 不加分
rule "order_rule_1"no-loop true //防止陷入死循环when$order:Order(amout < 100)thenintegral.setScore(10);update($order)System.out.println("成功匹配到规则一:100元以下 不加分");
end
测试:
@Test
public void test1(){//从Kie容器对象中获取会话对象KieSession session = kieContainer.newKieSession();//Fact对象,事实对象Order order = new Order();order.setAmout(30);//全局变量Integral integral = new Integral();session.setGlobal("integral", integral);//将Order对象插入到工作内存中session.insert(order);//激活规则,由Drools框架自动进行规则匹配,如果规则匹配成功,则执行当前规则session.fireAllRules();//关闭会话session.dispose();System.out.println("订单金额:" + order.getAmout());System.out.println("添加积分:" + integral.getScore());
}
二.预估订单路线金额:
首先引入依赖并编写配置类:(依赖在上面引入完成)
注意!!!规则文件的路径可能会根据备份文件路径自动改变,如有报错可检查这里。
import org.kie.api.KieServices;
import org.kie.api.builder.*;
import lombok.extern.slf4j.Slf4j;
import org.kie.api.runtime.KieContainer;
import org.kie.api.runtime.KieSession;
import org.kie.internal.io.ResourceFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** 规则引擎配置类*/
@Slf4j
@Configuration
public class DroolsConfig {// 制定规则文件的路径private static final String RULES_CUSTOMER_RULES_DRL = "rules/FeeRule.drl";@Beanpublic KieContainer kieContainer() {KieServices kieServices = KieServices.Factory.get();KieFileSystem kieFileSystem = kieServices.newKieFileSystem();kieFileSystem.write(ResourceFactory.newClassPathResource(RULES_CUSTOMER_RULES_DRL));KieBuilder kb = kieServices.newKieBuilder(kieFileSystem);kb.buildAll();KieModule kieModule = kb.getKieModule();KieContainer kieContainer = kieServices.newKieContainer(kieModule.getReleaseId());return kieContainer;}
}
之后创建封装输入、输出对象:
封装输入对象:
@Data
public class FeeRuleRequest {@Schema(description = "代驾里程")private BigDecimal distance;@Schema(description = "代驾时间")private String startTime;@Schema(description = "等候分钟")private Integer waitMinute;}
封装输出对象:
@Data
public class FeeRuleResponse {@Schema(description = "总金额")private BigDecimal totalAmount;@Schema(description = "里程费")private BigDecimal distanceFee;@Schema(description = "等时费用")private BigDecimal waitFee;@Schema(description = "远程费")private BigDecimal longDistanceFee;@Schema(description = "基础里程(公里)")private BigDecimal baseDistance;@Schema(description = "基础里程费(元)")private BigDecimal baseDistanceFee;@Schema(description = "超出基础里程的里程(公里)")private BigDecimal exceedDistance;@Schema(description = "超出基础里程的价格(元/公里)")private BigDecimal exceedDistancePrice;@Schema(description = "基础等时分钟(分钟)")private Integer baseWaitMinute;@Schema(description = "超出基础等时的分钟(分钟)")private Integer exceedWaitMinute;@Schema(description = "超出基础分钟的价格(元/分钟)")private BigDecimal exceedWaitMinutePrice;@Schema(description = "基础远途里程(公里)")private BigDecimal baseLongDistance;@Schema(description = "超出基础远程里程的里程(公里)")private BigDecimal exceedLongDistance;@Schema(description = "超出基础远程里程的价格(元/公里)")private BigDecimal exceedLongDistancePrice;
}
随后创建规则文件FeeRule.drl:(在/resource/rules/FeeRule.drl下)
规则按计费维度分为 5 类:
规则类型 条件 费用计算逻辑 起步价 根据时间段(00:00-07:00 或 07:00-24:00) 固定 19 元(含 3 或 5 公里) 里程费 超出起步里程后 00:00-07:00 按 4 元/公里,07:00-24:00 按 3 元/公里 等候费 等候超过 10 分钟 超时部分按 1 元/分钟 远途费 总里程超过 12 公里 超出部分按 1 元/公里 总金额 汇总所有费用 总金额 = 起步价 + 里程费 + 等候费 + 远途费
对于里程费计算中有个BigDecimal exceedDistance = $rule.getDistance().subtract(new BigDecimal("3.0"));代码,其作用是从规则匹配的
FeeRuleRequest
对象中获取 订单总里程后,执行-3运算得到结果。
要点:不难发现,我们通过传入FeeRuleRequest输入对象,将FeeRuleResponse输出对象作为全局变量,通过不断匹配规则对输出对象赋值来得到结果。
//package对应的不一定是真正的目录,可以任意写com.abc,同一个包下的drl文件可以相互访问
package com.atguigu.daijiaimport com.atguigu.daijia.model.form.rules.FeeRuleRequest;
import java.math.BigDecimal;
import java.math.RoundingMode;global com.atguigu.daijia.model.vo.rules.FeeRuleResponse feeRuleResponse;/**
1.起步价00:00:00-07:00:00 19元(含3公里)07:00:00-24:00:00 19元(含5公里)
*/
rule "起步价 00:00:00-07:00:00 19元(含3公里)"salience 10 //指定优先级,数值越大优先级越高,不指定的情况下由上到下执行no-loop true //防止陷入死循环when/*规则条件,到工作内存中查找FeeRuleRequest对象里面出来的结果只能是ture或者false$rule是绑定变量名,可以任意命名,官方推荐$符号,定义了绑定变量名,可以在then部分操作fact对象*/$rule:FeeRuleRequest(startTime >= "00:00:00" && startTime < "07:00:00")thenfeeRuleResponse.setBaseDistance(new BigDecimal("3.0")); // 基础里程feeRuleResponse.setBaseDistanceFee(new BigDecimal("19.0")); // 基础里程费//3公里内里程费为0feeRuleResponse.setExceedDistance(new BigDecimal("0.0")); // 超出基础里程的里程feeRuleResponse.setExceedDistancePrice(new BigDecimal("4.0")); // 超出基础里程的费用System.out.println("00:00:00-07:00:00 " + feeRuleResponse.getBaseDistance() + "公里,起步价:" + feeRuleResponse.getBaseDistanceFee() + "元");
end
rule "起步价 07:00:00-24:00:00 19元(含5公里)"salience 10 //指定优先级,数值越大优先级越高,不指定的情况下由上到下执行no-loop true //防止陷入死循环when/*规则条件,到工作内存中查找FeeRuleRequest对象里面出来的结果只能是ture或者false$rule是绑定变量名,可以任意命名,官方推荐$符号,定义了绑定变量名,可以在then部分操作fact对象*/$rule:FeeRuleRequest(startTime >= "07:00:00" && startTime < "24:00:00")thenfeeRuleResponse.setBaseDistance(new BigDecimal("5.0"));feeRuleResponse.setBaseDistanceFee(new BigDecimal("19.0"));//5公里内里程费为0feeRuleResponse.setExceedDistance(new BigDecimal("0.0"));feeRuleResponse.setExceedDistancePrice(new BigDecimal("3.0"));System.out.println("07:00:00-24:00:00 " + feeRuleResponse.getBaseDistance() + "公里,起步价:" + feeRuleResponse.getBaseDistanceFee() + "元");
end/**
2.里程费超出起步里程后开始计算00:00:00-07:00:00 4元/1公里07:00:00-24:00:00 3元/1公里
*/
rule "里程费 00:00:00-07:00:00 4元/1公里"salience 10 //指定优先级,数值越大优先级越高,不指定的情况下由上到下执行no-loop true //防止陷入死循环when/*规则条件,到工作内存中查找FeeRuleRequest对象里面出来的结果只能是ture或者false$rule是绑定变量名,可以任意命名,官方推荐$符号,定义了绑定变量名,可以在then部分操作fact对象*/$rule:FeeRuleRequest(startTime >= "00:00:00"&& startTime < "07:00:00"&& distance.doubleValue() > 3.0)thenBigDecimal exceedDistance = $rule.getDistance().subtract(new BigDecimal("3.0"));feeRuleResponse.setExceedDistance(exceedDistance);feeRuleResponse.setExceedDistancePrice(new BigDecimal("4.0"));System.out.println("里程费,超出里程:" + feeRuleResponse.getExceedDistance() + "公里,单价:" + feeRuleResponse.getExceedDistancePrice());
end
rule "里程费 07:00:00-24:00:00 3元/1公里"salience 10 //指定优先级,数值越大优先级越高,不指定的情况下由上到下执行no-loop true //防止陷入死循环when/*规则条件,到工作内存中查找FeeRuleRequest对象里面出来的结果只能是ture或者false$rule是绑定变量名,可以任意命名,官方推荐$符号,定义了绑定变量名,可以在then部分操作fact对象*/$rule:FeeRuleRequest(startTime >= "07:00:00"&& startTime < "24:00:00"&& distance.doubleValue() > 5.0)thenBigDecimal exceedDistance = $rule.getDistance().subtract(new BigDecimal("5.0"));feeRuleResponse.setExceedDistance(exceedDistance);feeRuleResponse.setExceedDistancePrice(new BigDecimal("3.0"));System.out.println("里程费,超出里程:" + feeRuleResponse.getExceedDistance() + "公里,单价:" + feeRuleResponse.getExceedDistancePrice());
end/**
3.等候费等候10分钟后 1元/1分钟
*/
rule "等候费 等候10分钟后 1元/1分钟"salience 10 //指定优先级,数值越大优先级越高,不指定的情况下由上到下执行no-loop true //防止陷入死循环when/*规则条件,到工作内存中查找FeeRuleRequest对象里面出来的结果只能是ture或者false$rule是绑定变量名,可以任意命名,官方推荐$符号,定义了绑定变量名,可以在then部分操作fact对象*/$rule:FeeRuleRequest(waitMinute > 10)thenInteger exceedWaitMinute = $rule.getWaitMinute() - 10;feeRuleResponse.setBaseWaitMinute(10);feeRuleResponse.setExceedWaitMinute(exceedWaitMinute);feeRuleResponse.setExceedWaitMinutePrice(new BigDecimal("1.0"));System.out.println("等候费,超出分钟:" + feeRuleResponse.getExceedWaitMinute() + "分钟,单价:" + feeRuleResponse.getExceedWaitMinutePrice());
end
rule "无等候费"salience 10 //指定优先级,数值越大优先级越高,不指定的情况下由上到下执行no-loop true //防止陷入死循环when/*规则条件,到工作内存中查找FeeRuleRequest对象里面出来的结果只能是ture或者false$rule是绑定变量名,可以任意命名,官方推荐$符号,定义了绑定变量名,可以在then部分操作fact对象*/$rule:FeeRuleRequest(waitMinute <= 10)thenfeeRuleResponse.setBaseWaitMinute(10);feeRuleResponse.setExceedWaitMinute(0);feeRuleResponse.setExceedWaitMinutePrice(new BigDecimal("1.0"));System.out.println("等候费:无");
end/**
4.远途费订单行程超出12公里后每公里1元
*/
rule "远途费 订单行程超出12公里后每公里1元"salience 10 //指定优先级,数值越大优先级越高,不指定的情况下由上到下执行no-loop true //防止陷入死循环when/*规则条件,到工作内存中查找FeeRuleRequest对象里面出来的结果只能是ture或者false$rule是绑定变量名,可以任意命名,官方推荐$符号,定义了绑定变量名,可以在then部分操作fact对象*/$rule:FeeRuleRequest(distance.doubleValue() > 12.0)thenBigDecimal exceedLongDistance = $rule.getDistance().subtract(new BigDecimal("12.0"));feeRuleResponse.setBaseLongDistance(new BigDecimal("12.0"));feeRuleResponse.setExceedLongDistance(exceedLongDistance);feeRuleResponse.setExceedLongDistancePrice(new BigDecimal("1.0"));System.out.println("远途费,超出公里:" + feeRuleResponse.getExceedLongDistance() + "公里,单价:" + feeRuleResponse.getExceedLongDistancePrice());
end
rule "无远途费"salience 10 //指定优先级,数值越大优先级越高,不指定的情况下由上到下执行no-loop true //防止陷入死循环when/*规则条件,到工作内存中查找FeeRuleRequest对象里面出来的结果只能是ture或者false$rule是绑定变量名,可以任意命名,官方推荐$符号,定义了绑定变量名,可以在then部分操作fact对象*/$rule:FeeRuleRequest(distance.doubleValue() <= 12.0)thenfeeRuleResponse.setBaseLongDistance(new BigDecimal("12.0"));feeRuleResponse.setExceedLongDistance(new BigDecimal("0"));feeRuleResponse.setExceedLongDistancePrice(new BigDecimal("0"));System.out.println("远途费:无");
end/**
5.计算总金额订单总金额 = 基础里程费 + 超出基础里程的费 + 等候费 + 远程费
*/
rule "计算总金额"salience 10 //指定优先级,数值越大优先级越高,不指定的情况下由上到下执行no-loop true //防止陷入死循环when/*规则条件,到工作内存中查找FeeRuleRequest对象里面出来的结果只能是ture或者false$rule是绑定变量名,可以任意命名,官方推荐$符号,定义了绑定变量名,可以在then部分操作fact对象*/$rule:FeeRuleRequest(distance.doubleValue() > 0.0)then//订单总金额 = 基础里程费 + 超出基础里程的费 + 等候费 + 远程费BigDecimal distanceFee = feeRuleResponse.getBaseDistanceFee().add(feeRuleResponse.getExceedDistance().multiply(feeRuleResponse.getExceedDistancePrice()));BigDecimal waitFee = new BigDecimal(feeRuleResponse.getExceedWaitMinute()).multiply(feeRuleResponse.getExceedWaitMinutePrice());BigDecimal longDistanceFee = feeRuleResponse.getExceedLongDistance().multiply(feeRuleResponse.getExceedLongDistancePrice());BigDecimal totalAmount = distanceFee.add(waitFee).add(longDistanceFee);feeRuleResponse.setDistanceFee(distanceFee);feeRuleResponse.setWaitFee(waitFee);feeRuleResponse.setLongDistanceFee(longDistanceFee);feeRuleResponse.setTotalAmount(totalAmount);System.out.println("计算总金额:" + feeRuleResponse.getTotalAmount() + "元");
end
封装微服务接口
在上面我们已经成功的编写了drl文件,接下来我们就要创建接口:
@Autowired
private FeeRuleService feeRuleService;@Operation(summary = "计算订单费用")
@PostMapping("/calculateOrderFee")
public Result<FeeRuleResponseVo> calculateOrderFee(@RequestBody FeeRuleRequestForm calculateOrderFeeForm) {return Result.ok(feeRuleService.calculateOrderFee(calculateOrderFeeForm));
}
其中FeeRuleRequestForm:
@Data
public class FeeRuleRequest {@Schema(description = "代驾里程")private BigDecimal distance;@Schema(description = "代驾时间")private String startTime;@Schema(description = "等候分钟")private Integer waitMinute;}
之后编写Service:
步骤解析:
- 将形参数据对象封装成FeeRuleRequest输入对象。
- 开启会话,封装输出对象,将输出对象设置为全局变量,将输入对象插入到工作内存中后触发规则,随后终止会话,拿到输出对象。
- 将输出对象使用BeanUtils.copyProperties()方法封装成FeeRuleResponseVo对象返回。
@Autowired
private KieContainer kieContainer;@Override
public FeeRuleResponseVo calculateOrderFee(FeeRuleRequestForm feeRuleRequestForm) {//封装传入对象FeeRuleRequest feeRuleRequest = new FeeRuleRequest();feeRuleRequest.setDistance(feeRuleRequestForm.getDistance());feeRuleRequest.setStartTime(new DateTime(feeRuleRequestForm.getStartTime()).toString("HH:mm:ss"));feeRuleRequest.setWaitMinute(feeRuleRequestForm.getWaitMinute());log.info("传入参数:{}", JSON.toJSONString(feeRuleRequest));// 开启会话KieSession kieSession = kieContainer.newKieSession();//封装返回对象FeeRuleResponse feeRuleResponse = new FeeRuleResponse();kieSession.setGlobal("feeRuleResponse", feeRuleResponse);// 设置订单对象kieSession.insert(feeRuleRequest);// 触发规则kieSession.fireAllRules();// 中止会话kieSession.dispose();log.info("计算结果:{}", JSON.toJSONString(feeRuleResponse));//封装返回对象FeeRuleResponseVo feeRuleResponseVo = new FeeRuleResponseVo();BeanUtils.copyProperties(feeRuleResponse, feeRuleResponseVo);return feeRuleResponseVo;
}
随后使用feign远程暴露接口:
/*** 计算订单费用* @param calculateOrderFeeForm* @return*/
@PostMapping("/rules/fee/calculateOrderFee")
Result<FeeRuleResponseVo> calculateOrderFee(@RequestBody FeeRuleRequestForm calculateOrderFeeForm);
三.规则接口优化:
我们把规则文件固定不变写到了项目resources目录下面,显然不利于运营人员调整规则,那么怎么办呢?我们可以把规则保存到数据库表中,需要调整规则时在后台页面更改了即可,同时让他随时生效。只要输入与输出参数不变,怎么调整都没有问题。
下面是我创建的FeeRule数据表:
CREATE TABLE `fee_rule` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',`rule_name` varchar(100) NOT NULL COMMENT '规则名称',`rule` text NOT NULL COMMENT 'DRL规则内容(对应代码中的rule字段)',`rule_content` text COMMENT '规则描述性内容(可选)',`is_active` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否生效(1:生效 0:失效)',`version` varchar(20) NOT NULL COMMENT '规则版本号',`creator` varchar(50) DEFAULT NULL COMMENT '创建人',`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`updater` varchar(50) DEFAULT NULL COMMENT '更新人',`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',PRIMARY KEY (`id`),KEY `idx_active_version` (`is_active`, `version`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='费用规则表';
首先定义一个Drools帮助类,接收规则字符串(规则文件的文本内容),返回KieSession即可:
public class DroolsHelper {public static KieSession loadForRule(String drlStr) {KieServices kieServices = KieServices.Factory.get();KieFileSystem kieFileSystem = kieServices.newKieFileSystem();kieFileSystem.write("src/main/resources/rules/" + drlStr.hashCode() + ".drl", drlStr);// 将KieFileSystem加入到KieBuilderKieBuilder kieBuilder = kieServices.newKieBuilder(kieFileSystem);// 编译此时的builder中所有的规则kieBuilder.buildAll();if (kieBuilder.getResults().hasMessages(Message.Level.ERROR)) {throw new RuntimeException("Build Errors:\n" + kieBuilder.getResults().toString());}KieContainer kieContainer = kieServices.newKieContainer(kieServices.getRepository().getDefaultReleaseId());return kieContainer.newKieSession();}
}
随后将规则文件内容访问fee_rule表rule字段:
@Autowired
private FeeRuleMapper feeRuleMapper;@Override
public FeeRuleResponseVo calculateOrderFee(FeeRuleRequestForm feeRuleRequestForm) {//封装传入对象FeeRuleRequest feeRuleRequest = new FeeRuleRequest();feeRuleRequest.setDistance(feeRuleRequestForm.getDistance());feeRuleRequest.setStartTime(new DateTime(feeRuleRequestForm.getStartTime()).toString("HH:mm:ss"));feeRuleRequest.setWaitMinute(feeRuleRequestForm.getWaitMinute());log.info("传入参数:{}", JSON.toJSONString(feeRuleRequest));//获取最新订单费用规则FeeRule feeRule = feeRuleMapper.selectOne(new LambdaQueryWrapper<FeeRule>().orderByDesc(FeeRule::getId).last("limit 1"));KieSession kieSession = DroolsHelper.loadForRule(feeRule.getRule());//封装返回对象FeeRuleResponse feeRuleResponse = new FeeRuleResponse();kieSession.setGlobal("feeRuleResponse", feeRuleResponse);// 设置订单对象kieSession.insert(feeRuleRequest);// 触发规则kieSession.fireAllRules();// 中止会话kieSession.dispose();log.info("计算结果:{}", JSON.toJSONString(feeRuleResponse));//封装返回对象FeeRuleResponseVo feeRuleResponseVo = new FeeRuleResponseVo();feeRuleResponseVo.setFeeRuleId(feeRule.getId());BeanUtils.copyProperties(feeRuleResponse, feeRuleResponseVo);return feeRuleResponseVo;
}
说明:规则调整可以产生一条新记录,每次去最新的一条作为当前费用规则。