规则引擎 Drools
1、问题
对于不经常变化的业务,我们通常是硬编码到程序中。但是经常变化的业务,我们就得把业务流程从代码中剥离出来,我们怎么从程序中剥离出去?这里就需要用到规则引擎了。
规则引擎可以做到把算法剥离出程序,你可以保存到TXT文件或者数据库表里面,用的时候再加载回程序。虽然加载回来的算法是字符串,但是规则引擎有办法运行这些字符串。例如商业中心人流量大的地方,共享充电宝收费就得上调一些。人流量小的地方可以下调一点。既然费用的算法经常要变动,我们肯定不能把算法写死到程序里面。我们要把算法从程序中抽离,保存到MySQL里面。将来我们要改动计费算法,直接添加一个新纪录就行了,原有记录不需要删改,程序默认使用最新的计费方式。
某电商平台的促销活动,活动规则是根据⽤户购买订单的⾦额给⽤户送相应的积分,购买的越多送的积分越多用户购买的金额和对应送多少积分的规则如下:
| 规则编号 | 订单金额 | 奖励积分 |
|---|---|---|
| 1 | 100元以下 | 不加分 |
| 2 | 100元 - 500元 | 加100分 |
| 3 | 500元 - 1000元 | 加500分 |
| 4 | 1000元以上 | 加1000分 |
思考:如何实现上面的业务逻辑呢?
我们最容易想到的就是使用分支判断(if else)来实现,例如通过如下代码来检查用户信息合法性:
/**
* 设置订单积分
*/
public void setOrderPoint(Order order){
if (order.getAmout() <= 100){
order.setScore(0);
}else if(order.getAmout() > 100 && order.getAmout() <= 500){
order.setScore(100);
}else if(order.getAmout() > 500 && order.getAmout() <= 1000){
order.setScore(500);
}else{
order.setScore(1000);
}
}通过上面的伪代码我们可以看到,需要在代码中写很多这样if else的代码。这种实现方式存在如下问题:
1、硬编码实现业务规则难以维护
2、硬编码实现业务规则难以应对变化
3、业务规则发生变化需要修改代码,重启服务后才能生效
那么面对上面的业务场景,还有什么好的实现方式吗?
此时我们需要引入规则引擎来帮助我们将规则从代码中分离出去,让开发人员从规则的代码逻辑中解放出来,把规则的维护和设置交由业务人员去管理。
2、规则引擎概述
2.1、什么是规则引擎
规则引擎,全称为业务规则管理系统,英文名为BRMS(即Business Rule Management System)。规则引擎的主要思想是将应用程序中的业务决策部分分离出来,并使用预定义的语义模块编写业务决策(业务规则),由用户或开发者在需要时进行配置、管理。
需要注意的是规则引擎并不是一个具体的技术框架,而是指的一类系统,即业务规则管理系统。目前市面上具体的规则引擎产品有:drools、VisualRules、iLog等。
规则引擎实现了将业务决策从应用程序代码中分离出来,接收数据输入,解释业务规则,并根据业务规则做出业务决策。规则引擎其实就是一个输入输出平台。
系统中引入规则引擎后,业务规则不再以程序代码的形式驻留在系统中,取而代之的是处理规则的规则引擎,业务规则存储在规则库中,完全独立于程序。业务人员可以像管理数据一样对业务规则进行管理,比如查询、添加、更新、统计、提交业务规则等。业务规则被加载到规则引擎中供应用系统调用。
2.2、使用规则引擎的优势
使用规则引擎的优势如下:
1、业务规则与系统代码分离,实现业务规则的集中管理
2、在不重启服务的情况下可随时对业务规则进行扩展和维护
3、可以动态修改业务规则,从而快速响应需求变更
4、规则引擎是相对独立的,只关心业务规则,使得业务分析人员也可以参与编辑、维护系统的业务规则
5、减少了硬编码业务规则的成本和风险
6、使用规则引擎提供的规则编辑工具,使复杂的业务规则实现变得的简单
2.3、规则引擎应用场景
对于一些存在比较复杂的业务规则并且业务规则会频繁变动的系统比较适合使用规则引擎,如下:
1、风险控制系统----风险贷款、风险评估
2、反欺诈项目----银行贷款、征信验证
3、决策平台系统----财务计算
4、促销平台系统----满减、打折、加价购
2.4、Drools介绍
drools是一款由JBoss组织提供的基于Java语言开发的开源规则引擎,可以将复杂且多变的业务规则从硬编码中解放出来,以规则脚本的形式存放在文件或特定的存储介质中(例如存放在数据库中),使得业务规则的变更不需要修改项目代码、重启服务器就可以在线上环境立即生效。
drools官网地址:https://drools.org/
drools源码下载地址:https://github.com/kiegroup/drools
- Tips: 在idea中安装drools插件
3、Drools入门案例
我们使用上面的问题案例来入门Drools
3.1、创建springboot项目
groupId:com.drools
artifactId:drools_demo3.2、引入依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>17</java.version>
<drools.version>8.41.0.Final</drools.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-core</artifactId>
<version>${drools.version}</version>
</dependency>
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-compiler</artifactId>
<version>${drools.version}</version>
</dependency>
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-decisiontables</artifactId>
<version>${drools.version}</version>
</dependency>
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-mvel</artifactId>
<version>${drools.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>3.3、添加Drools配置类
package com.drools.config;
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";
@Bean
public 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;
}
}说明:
- 定义了一个
KieContainer的Spring Bean,KieContainer用于通过加载应用程序的/resources文件夹下的规则文件来构建规则引擎。 - 创建
KieFileSystem实例并配置规则引擎并从应用程序的资源目录加载规则的DRL文件。 - 使用
KieBuilder实例来构建drools模块。我们可以使用KieSerive单例实例来创建KieBuilder实例。 - 最后,使用
KieService创建一个KieContainer并将其配置为spring bean
3.4、创建实体类Order
package com.drools.model;
public class Order {
private double amout;
private double score;
public double getAmout() {
return amout;
}
public void setAmout(double amout) {
this.amout = amout;
}
public double getScore() {
return score;
}
public void setScore(double score) {
this.score = score;
}
}3.5、order.drl
创建规则文件resources/rules/order.drl
//订单积分规则
package com.order
import com.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分");
end3.6、编写测试类
package com.drools;
import org.junit.jupiter.api.Test;
import com.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 {
@Autowired
private KieContainer kieContainer;
@Test
public 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将数据提供给规则引擎进行规则模式匹配,规则引擎会执行匹配成功的规则并将计算的结果返回给我们。
可能大家会有疑问,就是我们虽然没有在代码中编写规则的判断逻辑,但是我们还是在规则文件中编写了业务规则,这跟在代码中编写规则有什么本质的区别呢?
我们前面其实已经提到,使用规则引擎时业务规则可以做到动态管理。业务人员可以像管理数据一样对业务规则进行管理,比如查询、添加、更新、统计、提交业务规则等。这样就可以做到在不重启服务的情况下调整业务规则。
4、Drools基础语法
4.1、规则文件构成
在使用Drools时非常重要的一个工作就是编写规则文件,通常规则文件的后缀为.drl。
drl是Drools Rule Language的缩写。在规则文件中编写具体的规则内容。
一套完整的规则文件内容构成如下:
| 关键字 | 描述 |
|---|---|
| package | 包名,只限于逻辑上的管理,同一个包名下的查询或者函数可以直接调用 |
| import | 用于导入类或者静态方法 |
| global | 全局变量 |
| function | 自定义函数 |
| query | 查询 |
| rule end | 规则体 |
Drools支持的规则文件,除了drl形式,还有Excel文件类型的。
4.2、规则体语法结构
规则体是规则文件内容中的重要组成部分,是进行业务规则判断、处理业务结果的部分。
规则体语法结构如下:
rule "ruleName"
attributes
when
LHS
then
RHS
endrule:关键字,表示规则开始,参数为规则的唯一名称。
attributes:规则属性,是rule与when之间的参数,为可选项。
when:关键字,后面跟规则的条件部分。
LHS(Left Hand Side):是规则的条件部分的通用名称。它由零个或多个条件元素组成。如果LHS为空,则它将被视为始终为true的条件元素。 (左手边)
then:关键字,后面跟规则的结果部分。
RHS(Right Hand Side):是规则的后果或行动部分的通用名称。 (右手边)
end:关键字,表示一个规则结束。
4.3、注释
在drl形式的规则文件中使用注释和Java类中使用注释一致,分为单行注释和多行注释。
单行注释用"//"进行标记,多行注释以"/"开始,以"/"结束。如下示例:
//规则rule1的注释,这是一个单行注释
rule "rule1"
when
then
System.out.println("rule1触发");
end
/*
规则rule2的注释,
这是一个多行注释
*/
rule "rule2"
when
then
System.out.println("rule2触发");
end4.4、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通过上面的例子我们可以知道,匹配的条件为:
1、工作内存中必须存在Order这种类型的Fact对象-----类型约束
2、Fact对象的amout属性值必须大于等于100------属性约束
3、Fact对象的amout属性值必须小于100------属性约束
以上条件必须同时满足当前规则才有可能被激活。
4.5、比较操作符
Drools提供的比较操作符,如下表:
| 符号 | 说明 |
|---|---|
| < | 小于 |
| >= | 大于等于 |
| > | 大于 |
| <= | 小于等于 |
| == | 等于 |
| != | 不等于 |
| contains | 检查一个Fact对象的某个属性值是否包含一个指定的对象值 |
| not contains | 检查一个Fact对象的某个属性值是否不包含一个指定的对象值 |
| memberOf | 判断一个Fact对象的某个属性是否在一个或多个集合中 |
| not memberOf | 判断一个Fact对象的某个属性是否不在一个或多个集合中 |
| matches | 判断一个Fact对象的属性是否与提供的标准的Java正则表达式进行匹配 |
| not matches | 判断一个Fact对象的属性是否不与提供的标准的Java正则表达式进行匹配 |
前6个比较操作符和Java中的完全相同。
4.6、Drools内置方法
规则文件的RHS部分的主要作用是通过插入,删除或修改工作内存中的Fact数据,来达到控制规则引擎执行的目的。Drools提供了一些方法可以用来操作工作内存中的数据,**操作完成后规则引擎会重新进行相关规则的匹配,**原来没有匹配成功的规则在我们修改数据完成后有可能就会匹配成功了。
4.6.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在更新数据时需要注意防止发生死循环。
4.6.2、insert方法
insert方法的作用是向工作内存中插入数据,并让相关的规则重新匹配。
//规则一:100元以下 不加分
rule "order_rule_1"
when
$order:Order(amout < 100)
then
Order 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分");
end4.6.3、retract方法
retract方法的作用是删除工作内存中的数据,并让相关的规则重新匹配。
//规则一:100元以下 不加分
rule "order_rule_1"
when
$order:Order(amout < 100)
then
retract($order) //retract方法的作用是删除工作内存中的Fact对象,会导致相关规则重新匹配
System.out.println("成功匹配到规则一:100元以下 不加分");
end5、规则属性 attributes
前面我们已经知道了规则体的构成如下:
rule "ruleName"
attributes
when
LHS
then
RHS
end本章节就是针对规则体的attributes属性部分进行讲解。Drools中提供的属性如下表(部分属性):
| 属性名 | 说明 |
|---|---|
| salience | 指定规则执行优先级 |
| dialect | 指定规则使用的语言类型,取值为java和mvel |
| enabled | 指定规则是否启用 |
| date-effective | 指定规则生效时间 |
| date-expires | 指定规则失效时间 |
| activation-group | 激活分组,具有相同分组名称的规则只能有一个规则触发 |
| agenda-group | 议程分组,只有获取焦点的组中的规则才有可能触发 |
| timer | 定时器,指定规则触发的时间 |
| auto-focus | 自动获取焦点,一般结合agenda-group一起使用 |
| no-loop | 防止死循环 |
重点说一下我们项目需要使用的属性
5.1、salience属性
salience属性用于指定规则的执行优先级,取值类型为Integer。数值越大越优先执行。每个规则都有一个默认的执行顺序,如果不设置salience属性,规则体的执行顺序为由上到下。
可以通过创建规则文件salience.drl来测试salience属性,内容如下:
package com.order
rule "rule_1"
when
eval(true)
then
System.out.println("规则rule_1触发");
end
rule "rule_2"
when
eval(true)
then
System.out.println("规则rule_2触发");
end
rule "rule_3"
when
eval(true)
then
System.out.println("规则rule_3触发");
end通过控制台可以看到,由于以上三个规则没有设置salience属性,所以执行的顺序是按照规则文件中规则的顺序由上到下执行的。接下来我们修改一下文件内容:
package com.order
rule "rule_1"
salience 9
when
eval(true)
then
System.out.println("规则rule_1触发");
end
rule "rule_2"
salience 10
when
eval(true)
then
System.out.println("规则rule_2触发");
end
rule "rule_3"
salience 8
when
eval(true)
then
System.out.println("规则rule_3触发");
end通过控制台可以看到,规则文件执行的顺序是按照我们设置的salience值由大到小顺序执行的。
建议在编写规则时使用salience属性明确指定执行优先级。
5.2、no-loop属性
no-loop属性用于防止死循环,当规则通过update之类的函数修改了Fact对象时,可能使当前规则再次被激活从而导致死循环。取值类型为Boolean,默认值为false,测试步骤如下:
编写规则文件/resources/rules/activationgroup.drl
//订单积分规则
package com.order
import com.drools.model.Order
//规则一:100元以下 不加分
rule "order_rule_1"
no-loop true //防止陷入死循环
when
$order:Order(amout < 100)
then
$order.setAmount(0);
update($order)
System.out.println("成功匹配到规则一:100元以下 不加分");
end通过控制台可以看到,由于我们没有设置no-loop属性的值,所以发生了死循环。接下来设置no-loop的值为true再次测试则不会发生死循环。
6、Drools高级语法
前面章节我们已经知道了一套完整的规则文件内容构成如下:
| 关键字 | 描述 |
|---|---|
| package | 包名,只限于逻辑上的管理,同一个包名下的查询或者函数可以直接调用 |
| import | 用于导入类或者静态方法 |
| global | 全局变量 |
| function | 自定义函数 |
| query | 查询 |
| rule end | 规则体 |
重点说一下我们项目需要使用的属性
6.1、global全局变量
global关键字用于在规则文件中定义全局变量,它可以让应用程序的对象在规则文件中能够被访问。可以用来为规则文件提供数据或服务。
语法结构为:global 对象类型 对象名称
在使用global定义的全局变量时有两点需要注意:
1、如果对象类型为包装类型时,在一个规则中改变了global的值,那么只针对当前规则有效,对其他规则中的global不会有影响。可以理解为它是当前规则代码中的global副本,规则内部修改不会影响全局的使用。
2、如果对象类型为集合类型或JavaBean时,在一个规则中改变了global的值,对java代码和所有规则都有效。
订单Order:
package com.drools.model;
public class Order {
private double amout;
public double getAmout() {
return amout;
}
public void setAmout(double amout) {
this.amout = amout;
}
}积分Integral:
package com.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.Order
global com.atguigu.drools.model.Integral integral;
//规则一:100元以下 不加分
rule "order_rule_1"
no-loop true //防止陷入死循环
when
$order:Order(amout < 100)
then
integral.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());
}7.充电桩计费业务(实际应用)
新能源充电桩的计费规则因使用场景(家用、公共、高速等)、地区政策、运营商策略等因素而有所不同,但总体上可归纳为以下几类常见模式:
一、家用充电桩
计费方式:按居民用电电价收费,通常无服务费。
是否分时:多数地区支持
峰谷分时电价:
- 谷段(如22:00–次日8:00):约 0.3–0.44元/度
- 峰段(如8:00–22:00):约 0.6–0.8元/度
示例:60kWh电池在谷电充满约需18–24元;峰电则需36–48元。
建议:申请独立电表 + 定时充电功能,锁定谷电时段,年均成本可控制在500–800元。
二、公共快充桩(市区)
- 计费结构:“电费 + 服务费”复合计价
- 电费部分:约 0.51–1.2元/度(受电网分时影响)
- 服务费:约 0.5–1.0元/度
- 综合单价:通常 1.3–2.8元/度,高峰时段可能达 3.5元/度
- 分时差异:
- 低谷(0:00–8:00):1.3–1.6元/度
- 高峰(17:00–23:00):1.8–3.5元/度
- 附加费用:部分站点收“超时占位费”,如特斯拉最高 6.4元/分钟
- 省钱技巧:
- 使用聚合类APP比价
- 开通会员享服务费折扣(如7折)
- 充至80%即离场,避免占位费
三、高速充电桩(长途出行)
计费结构
:同样采用“电费+服务费”,部分含占位费
- 综合费用:约 1.5–3.0元/度
- 夜间低谷(23:00–次日7:00):可低至 0.9–1.2元/度
占位费:常见 5–10元/小时,充电完成后未及时驶离即收取
建议:优先夜间补能,费用比白天省约40%
四、其他计费方式(较少见)
- 按时间计费:早期或特定场所(如景区、酒店)可能按“元/分钟”收费,但逐渐被按电量取代。
- 按功率计费:极少数高端快充站根据输出功率阶梯定价(如120kW以上加收溢价)。
五、通用省钱建议
- 日常通勤:优先用家用桩 + 谷电充电。
- 应急补电:选择低谷时段(0:00–8:00)使用公共桩。
- 长途出行:规划路线,优先夜间在高速站充电。
- 善用工具:通过“星星充电”“特来电”“加电”等APP比价、领券、查占位费规则。
- 关注提示:仔细阅读充电前的费用说明,避免“价格刺客”。
下面是一个使用 Drools 规则引擎 实现 公共快充桩订单费用结算 的完整示例。我们将基于你之前提到的计费规则:
- 费用 = 电费 + 服务费
- 电费和服务费根据 时间段(峰/平/谷) 不同而变化
- 可能存在 超时占位费
✅ 一、项目依赖(Maven)
确保你的 pom.xml 包含 Drools:
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-core</artifactId>
<version>8.42.0.Final</version>
</dependency>
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-compiler</artifactId>
<version>8.42.0.Final</version>
</dependency>
<dependency>
<groupId>org.kie</groupId>
<artifactId>kie-api</artifactId>
<version>8.42.0.Final</version>
</dependency>注意:Drools 8 使用 Kogito 架构,但传统方式仍可用。以下代码兼容 Drools 7+ 和 8(以 8 为例)。
✅ 二、定义 Java 模型类
1. 充电订单实体(重要,以下只列出了核心的字段)
- 订单的创建时机和结束时间一定要说得出来:
- 什么时候开始创建订单的(充电指令下发后,充电桩收到指令后上报)
- 什么时候结束订单的(充电状态上报100%或者用户手动停止充电)
/**
* @Author:lau
* @Package:com.bw.drools.model
* @Project:drools
* @name:ChargingOrder
* @Date:2025/12/30 14:40
* @Filename:ChargingOrder
*/
@Data
@RequiredArgsConstructor
@AllArgsConstructor
public class ChargingOrder {
private double electricityConsumedKwh; // 充电量(度)
private LocalDateTime startTime; // 开始时间
private LocalDateTime endTime; // 结束时间
private long parkingDurationMinutes; // 停车总时长(分钟),用于判断是否超时
private double electricityPrice; // 电费单价(元/度)
private double serviceFee; // 服务费单价(元/度)
private double overstayFee; // 占位费(元)
private double totalAmount; // 总费用
private Integer status; // 订单状态
private Long order_sn; // 订单号
// 辅助方法:获取开始小时(用于判断时段)
public int getStartHour() {
return startTime.getHour();
}
@Override
public String toString() {
return String.format(
"ChargingOrder{电量=%.2f度, 电费=%.2f元/度, 服务费=%.2f元/度, 占位费=%.2f元, 总计=%.2f元}",
electricityConsumedKwh, electricityPrice, serviceFee, overstayFee, totalAmount
);
}
}✅ 三、Drools 规则文件(charging-rules.drl)
放在 src/main/resources 下:
package charging.rules
import com.bw.drools.model.ChargingOrder
// 规则1:设置谷时段电价(23:00 - 7:59)
rule "Set Off-Peak Pricing"
when
$order : ChargingOrder(
getStartHour() >= 23 || getStartHour() < 8,
electricityPrice == 0.0
)
then
System.out.println("应用谷时段计价");
$order.setElectricityPrice(0.51); // 电费
$order.setServiceFee(0.5); // 服务费
update($order);
end
// 规则2:设置平时段电价(8:00 - 16:59)
rule "Set Mid-Peak Pricing"
when
$order : ChargingOrder(
getStartHour() >= 8 && getStartHour() < 17,
electricityPrice == 0.0
)
then
System.out.println("应用平时段计价");
$order.setElectricityPrice(0.85);
$order.setServiceFee(0.8);
update($order);
end
// 规则3:设置峰时段电价(17:00 - 22:59)
rule "Set Peak Pricing"
when
$order : ChargingOrder(
getStartHour() >= 17 && getStartHour() < 23,
electricityPrice == 0.0
)
then
System.out.println("应用峰时段计价");
$order.setElectricityPrice(1.2);
$order.setServiceFee(1.0);
update($order);
end
// 规则4:计算基础充电费用
rule "Calculate Base Charge"
when
$order : ChargingOrder(
electricityPrice > 0,
serviceFee > 0,
totalAmount == 0.0
)
then
// 计算总金额
double base = $order.getElectricityConsumedKwh() * ($order.getElectricityPrice() + $order.getServiceFee());
$order.setTotalAmount(base);
update($order);
end
// 规则5:超时占位费(假设充电完成后停留超过15分钟即收费)
// 假设:充电时长 = endTime - startTime;停车总时长 = parkingDurationMinutes
// 如果 (停车总时长 - 充电时长) > 15,则收取占位费 5元/分钟
rule "Apply Overstay Fee"
when
$order : ChargingOrder(
parkingDurationMinutes > 0,
overstayFee == 0.0
)
then
// 计算充电的开始时间和结束时间的插值
long chargingMinutes = java.time.Duration.between($order.getStartTime(), $order.getEndTime()).toMinutes();
// 计算充电满后超时的时间
long overstayMinutes = $order.getParkingDurationMinutes() - chargingMinutes;
if (overstayMinutes > 15) {
double fee = overstayMinutes * 5.0; // 5元/分钟
$order.setOverstayFee(fee);
$order.setTotalAmount($order.getTotalAmount() + fee);
System.out.println("收取占位费: " + fee + " 元,超时 " + overstayMinutes + " 分钟");
update($order);
}else{
$order.setOverstayFee(0.0);
update($order);
}
end✅ 四、Java 调用 Drools 引擎
// ChargingCalculator.java
import org.kie.api.KieServices;
import org.kie.api.runtime.KieContainer;
import org.kie.api.runtime.KieSession;
import java.time.LocalDateTime;
public class ChargingCalculator {
public static void main(String[] args) {
// 创建订单
ChargingOrder order = new ChargingOrder();
order.setElectricityConsumedKwh(45.0); // 充了45度电
order.setStartTime(LocalDateTime.of(2025, 12, 30, 18, 30)); // 18:30开始(峰时段)
order.setEndTime(LocalDateTime.of(2025, 12, 30, 19, 45)); // 1小时15分钟 = 75分钟
order.setParkingDurationMinutes(100); // 停了100分钟,超时25分钟
// 初始化费用为0
order.setElectricityPrice(0.0);
order.setServiceFee(0.0);
order.setOverstayFee(0.0);
order.setTotalAmount(0.0);
// 启动Drools
//从Kie容器对象中获取会话对象,用于和规则引擎交互
KieSession session = kieContainer.newKieSession();
// 将ChargingOrder 对象设为Fact对象,实例对象
session.insert(order);
session.fireAllRules();
session.dispose();
System.out.println("最终结算:" + order);
}
}✅ 五、预期输出示例
应用峰时段计价
收取占位费: 125.0 元,超时 25 分钟
最终结算:ChargingOrder{电量=45.00度, 电费=1.20元/度, 服务费=1.00元/度, 占位费=125.00元, 总计=224.00元}计算过程:
- 基础费用:45 × (1.2 + 1.0) = 99 元
- 超时:100 - 75 = 25 分钟 → 25 × 5 = 125 元
- 总计:99 + 125 = 224 元
8、规则接口改造(规则配置放在数据库中)
我们把规则文件写到了项目resources目录下面,显然不利于运营人员调整规则,那么怎么办呢?我们可以把规则保存到数据库表中,需要调整规则时在后台页面更改了即可,同时让他随时生效。只要输入与输出参数不变,怎么调整都没有问题。
7.1、DroolsHelper
定义一个Drools帮助类,接收规则字符串(规则文件的文本内容),返回KieSession即可
package com.bw.drools.helper;
import org.kie.api.KieServices;
import org.kie.api.builder.KieBuilder;
import org.kie.api.builder.KieFileSystem;
import org.kie.api.builder.Message;
import org.kie.api.runtime.KieContainer;
import org.kie.api.runtime.KieSession;
// 定义一个加载文件的工具类
public class DroolsHelper {
// drlStr代表从数据库中查找出来的“规则”这个字段的内容
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加入到KieBuilder
KieBuilder 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();
}
}7.2、规则文件放入数据库表
将规则文件内容访入fee_rule表rule字段
7.2.1、整合充电中计费的核心内容
@Test
// 测试使用数据库
public void tesChargingDb(){
// 创建订单
ChargingOrder order = new ChargingOrder();
order.setElectricityConsumedKwh(45.0); // 充了45度电
order.setStartTime(LocalDateTime.of(2025, 12, 30, 18, 30)); // 18:30开始(峰时段)
order.setEndTime(LocalDateTime.of(2025, 12, 30, 19, 45)); // 1小时15分钟 = 75分钟
order.setParkingDurationMinutes(100); // 停了100分钟,超时25分钟
// 初始化费用为0
order.setElectricityPrice(0.0);
order.setServiceFee(0.0);
order.setOverstayFee(0.0);
order.setTotalAmount(0.0);
// 应该要根据规则id查询出规则字符串
// 从数据表中进行查询
KieSession session = DroolsHelper.loadForRule("package charging.rules\n" +
"\n" +
"import com.bw.drools.model.ChargingOrder\n" +
"\n" +
"// 规则1:设置谷时段电价(23:00 - 7:59)\n" +
"rule \"Set Off-Peak Pricing\"\n" +
"when\n" +
" $order : ChargingOrder(\n" +
" getStartHour() >= 23 || getStartHour() < 8,\n" +
" electricityPrice == 0.0\n" +
" )\n" +
"then\n" +
" System.out.println(\"应用谷时段计价\");\n" +
" $order.setElectricityPrice(0.51); // 电费\n" +
" $order.setServiceFee(0.5); // 服务费\n" +
" update($order);\n" +
"end\n" +
"\n" +
"// 规则2:设置平时段电价(8:00 - 16:59)\n" +
"rule \"Set Mid-Peak Pricing\"\n" +
"when\n" +
" $order : ChargingOrder(\n" +
" getStartHour() >= 8 && getStartHour() < 17,\n" +
" electricityPrice == 0.0\n" +
" )\n" +
"then\n" +
" System.out.println(\"应用平时段计价\");\n" +
" $order.setElectricityPrice(0.85);\n" +
" $order.setServiceFee(0.8);\n" +
" update($order);\n" +
"end\n" +
"\n" +
"// 规则3:设置峰时段电价(17:00 - 22:59)\n" +
"rule \"Set Peak Pricing\"\n" +
"when\n" +
" $order : ChargingOrder(\n" +
" getStartHour() >= 17 && getStartHour() < 23,\n" +
" electricityPrice == 0.0\n" +
" )\n" +
"then\n" +
" System.out.println(\"应用峰时段计价\");\n" +
" $order.setElectricityPrice(1.2);\n" +
" $order.setServiceFee(1.0);\n" +
" update($order);\n" +
"end\n" +
"\n" +
"// 规则4:计算基础充电费用\n" +
"rule \"Calculate Base Charge\"\n" +
"when\n" +
" $order : ChargingOrder(\n" +
" electricityPrice > 0,\n" +
" serviceFee > 0,\n" +
" totalAmount == 0.0\n" +
" )\n" +
"then\n" +
" // 计算总金额\n" +
" double base = $order.getElectricityConsumedKwh() * ($order.getElectricityPrice() + $order.getServiceFee());\n" +
" $order.setTotalAmount(base);\n" +
" update($order);\n" +
"end\n" +
"\n" +
"// 规则5:超时占位费(假设充电完成后停留超过15分钟即收费)\n" +
"// 假设:充电时长 = endTime - startTime;停车总时长 = parkingDurationMinutes\n" +
"// 如果 (停车总时长 - 充电时长) > 15,则收取占位费 5元/分钟\n" +
"rule \"Apply Overstay Fee\"\n" +
"when\n" +
" $order : ChargingOrder(\n" +
" parkingDurationMinutes > 0,\n" +
" overstayFee == 0.0\n" +
" )\n" +
"then\n" +
" // 计算充电的开始时间和结束时间的插值\n" +
" long chargingMinutes = java.time.Duration.between($order.getStartTime(), $order.getEndTime()).toMinutes();\n" +
" // 计算充电满后超时的时间\n" +
" long overstayMinutes = $order.getParkingDurationMinutes() - chargingMinutes;\n" +
"\n" +
" if (overstayMinutes > 15) {\n" +
" double fee = overstayMinutes * 5.0; // 5元/分钟\n" +
" $order.setOverstayFee(fee);\n" +
" $order.setTotalAmount($order.getTotalAmount() + fee);\n" +
" System.out.println(\"收取占位费: \" + fee + \" 元,超时 \" + overstayMinutes + \" 分钟\");\n" +
" update($order);\n" +
" }else{\n" +
" $order.setOverstayFee(0.0);\n" +
" update($order);\n" +
" }\n" +
"end");
// 将ChargingOrder 对象设为Fact对象,实例对象
session.insert(order);
session.fireAllRules();
session.dispose();
System.out.println("最终结算:" + order);
}